{"id":29090808,"url":"https://github.com/lppedd/message-bus","last_synced_at":"2025-11-17T13:02:30.056Z","repository":{"id":301600335,"uuid":"1009771331","full_name":"lppedd/message-bus","owner":"lppedd","description":"A minimal, type-safe, hierarchical pub-sub message bus for TypeScript and JavaScript","archived":false,"fork":false,"pushed_at":"2025-11-17T11:22:35.000Z","size":218,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-11-17T12:11:12.957Z","etag":null,"topics":["event-bus","javascript","message-bus","messaging","typescript"],"latest_commit_sha":null,"homepage":"https://lppedd.github.io/message-bus/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lppedd.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-06-27T17:24:08.000Z","updated_at":"2025-11-17T11:21:45.000Z","dependencies_parsed_at":"2025-06-27T21:29:07.899Z","dependency_job_id":null,"html_url":"https://github.com/lppedd/message-bus","commit_stats":null,"previous_names":["lppedd/event-bus","lppedd/message-bus"],"tags_count":22,"template":false,"template_full_name":null,"purl":"pkg:github/lppedd/message-bus","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lppedd%2Fmessage-bus","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lppedd%2Fmessage-bus/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lppedd%2Fmessage-bus/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lppedd%2Fmessage-bus/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lppedd","download_url":"https://codeload.github.com/lppedd/message-bus/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lppedd%2Fmessage-bus/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":284885669,"owners_count":27079125,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-11-17T02:00:06.431Z","response_time":55,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["event-bus","javascript","message-bus","messaging","typescript"],"created_at":"2025-06-28T05:37:50.567Z","updated_at":"2025-11-17T13:02:30.051Z","avatar_url":"https://github.com/lppedd.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003c!--suppress HtmlDeprecatedAttribute --\u003e\n\u003ch1 align=\"center\"\u003emessage-bus\u003c/h1\u003e\n\u003cp align=\"center\"\u003eA minimal, type-safe, hierarchical pub-sub message bus for TypeScript and JavaScript\u003c/p\u003e\n\u003cdiv align=\"center\"\u003e\n\n[![npm](https://img.shields.io/npm/v/@lppedd/message-bus?color=%23de1f1f\u0026logo=npm)](https://www.npmjs.com/package/@lppedd/message-bus)\n[![ecmascript](https://img.shields.io/badge/ES-2022-blue?logo=javascript)](https://en.wikipedia.org/wiki/ECMAScript_version_history#13th_edition_%E2%80%93_ECMAScript_2022)\n[![status](https://img.shields.io/badge/status-alpha-DB3683)](https://github.com/lppedd/message-bus)\n[![build](https://img.shields.io/github/actions/workflow/status/lppedd/message-bus/test.yml.svg?branch=main)](https://github.com/lppedd/message-bus/actions/workflows/test.yml)\n[![minified size](https://img.shields.io/bundlejs/size/@lppedd/message-bus)](https://bundlejs.com/?q=@lppedd/message-bus)\n[![license](https://img.shields.io/badge/license-MIT-F7F7F7)](https://github.com/lppedd/message-bus/blob/main/LICENSE)\n\n\u003c/div\u003e\n\u003cimg align=\"center\" src=\"./.github/images/hierarchical-bus.jpg\"  alt=\"Hierarchical Bus\" /\u003e\n\n### Table of Contents\n\n- [Installation](#installation)\n- [API reference](#api-reference)\n- [Quickstart](#quickstart)\n- [Creating a message bus](#creating-a-message-bus)\n  - [Child buses](#child-buses)\n- [Publishing messages](#publishing-messages)\n  - [Message ordering](#message-ordering-)\n- [Subscribing to topics](#subscribing-to-topics)\n  - [Single message subscription](#single-message-subscription)\n  - [Multiple topic subscription](#multiple-topic-subscription)\n- [Async subscription](#async-subscription-)\n  - [Async single message subscription](#async-single-message-subscription)\n- [Decorator-based subscription](#decorator-based-subscription)\n  - [Unsubscribing programmatically](#unsubscribing-programmatically)\n- [Subscription options](#subscription-options)\n  - [Limit](#limit)\n  - [Priority](#priority)\n- [Listening to all messages](#listening-to-all-messages)\n\n### Installation\n\n```sh\nnpm i @lppedd/message-bus\n```\n\n```sh\npnpm add @lppedd/message-bus\n```\n\n```sh\nyarn add @lppedd/message-bus\n```\n\n### API reference\n\nYou can find the complete API reference at [lppedd.github.io/message-bus](https://lppedd.github.io/message-bus).\n\n### Requirements\n\nThe JavaScript environment must support or polyfill `Map`, `Set`, `WeakMap` and `WeakRef`.\n\n## Quickstart\n\n```ts\n// Create a message bus\nconst bus = createMessageBus();\n\n// Create a topic to publish messages to\nconst CommandTopic = createTopic\u003cstring\u003e(\"Command\");\n\n// Subscribe to the topic using a message handler/callback\nbus.subscribe(CommandTopic, (command) =\u003e {\n  if (command === \"shutdown\") {\n    /* ... */\n  }\n});\n\n// Publish a new message to the topic\nbus.publish(CommandTopic, \"shutdown\");\n```\n\n## Creating a message bus\n\nUse the `createMessageBus` factory function to create a new message bus:\n\n```ts\nconst bus = createMessageBus();\n```\n\nYou can customize the message bus behavior by also passing options:\n\n```ts\nconst bus = createMessageBus({\n  // Handles errors thrown from message handlers.\n  // By default, caught unhandled errors are printed to console.error.\n  errorHandler: (e) =\u003e { /* ... */ }\n});\n```\n\n### Child buses\n\nA `MessageBus` can create child buses.  \nBy default, subscriptions registered on a child bus will also receive messages published\nby its parent bus.\n\nTo create a child bus, call:\n\n```ts\nconst childBus = bus.createChildBus();\n```\n\n## Publishing messages\n\nTo publish a message via the message bus, the first step is to define a _topic_.  \nA _topic_ is a typed object that uniquely identifies a message channel.\n\n```ts\n// Messages sent to the CommandTopic must be strings\nconst CommandTopic = createTopic\u003cstring\u003e(\"Command\");\n```\n\nOnce you have a topic, you can publish a message:\n\n```ts\nbus.publish(CommandTopic, \"shutdown\");\n```\n\nNote that if the topic uses a `void` type, the value parameter can be omitted:\n\n```ts\nconst PingTopic = createTopic\u003cvoid\u003e(\"Ping\"); // Or just createTopic(\"Ping\")\n\n// No value needed\nbus.publish(PingTopic);\n```\n\n### Message ordering 🚥\n\nThe message bus guarantees that messages are always dispatched to handlers in the order\nthey are published.\n\nIf a message is published from within a message handler, it is **not** dispatched immediately.\nInstead, it is enqueued and will only be processed after all previously published (but not yet\ndispatched) messages. This ensures consistent, FIFO-style message delivery, even across nested\n`publish` calls.\n\n## Subscribing to topics\n\nThere are multiple ways to subscribe to a topic, but the most straightforward\nis to provide a message handler (a callback):\n\n```ts\nconst subscription = bus.subscribe(CommandTopic, (command) =\u003e {\n  /* ... */\n});\n```\n\nThe handler is invoked each time a message is published to the topic, whether it is\npublished on the current bus or any of its parent buses.\n\nYou can unsubscribe from the topic at any time by calling `subscription.dispose()`.\n\n### Single message subscription\n\nIf you're only interested in the single next message, use:\n\n```ts\nbus.subscribeOnce(CommandTopic, (command) =\u003e {\n  /* ... */\n});\n```\n\nThis subscribes to the topic and automatically disposes the created `Subscription`\nafter receiving a single message.\n\n### Multiple topic subscription\n\nYou can subscribe to multiple topics with a single handler by passing an array of topics:\n\n```ts\nconst StringTopic = createTopic\u003cstring\u003e(\"...\");\nconst NumberTopic = createTopic\u003cnumber\u003e(\"...\");\n\nbus.subscribe([StringTopic, NumberTopic], (data /* string | number */) =\u003e {\n  /* ... */\n});\n```\n\nThe `data` parameter is automatically inferred as `string | number`, based on the union\nof all topic types. This pattern is useful when the same logic should apply to multiple\nrelated message types.\n\n## Async subscription ⚡\n\nAn alternative way to subscribe to a topic is using async iterations:\n\n```ts\nconst subscription = bus.subscribe(CommandTopic); // AsyncIterableIterator\u003cstring\u003e\n```\n\nThis creates a **lazy** subscription: no actual subscription is made until you\nstart consuming messages.  \nYou can do that using a `for await ... of` loop:\n\n```ts\nfor await (const command of subscription) {\n  /* ... */\n}\n```\n\nOr by awaiting the next message directly with `subscription.single()`:\n\n```ts\nconst command = await subscription.single(); // Promise\u003cstring\u003e\n```\n\nNote that calling `single()` does not automatically dispose the subscription.\nIn contrast, a `for await ... of` loop disposes it automatically when the iteration\nends, whether normally or due to a `break`, a `return`, or an error.\n\nIf you use `single()` and no longer need the subscription afterward, remember to\ndispose it manually with `subscription.dispose()`.\n\n### Async single message subscription\n\nThe asynchronous alternative to `bus.subscribeOnce(topic, handler)` is:\n\n```ts\nconst command = await bus.subscribeOnce(CommandTopic); // Promise\u003cstring\u003e\n```\n\n\u003e [!TIP]  \n\u003e If you are only interested in a single message, prefer using `subscribeOnce(Topic)`\n\u003e over `subscribe(Topic)` + `subscription.single()`. This avoids the need to manually\n\u003e dispose the subscription.\n\n## Decorator-based subscription\n\nThe library also supports a declarative way to subscribe to topics, by using\nTypeScript's experimental decorators.\n\nWhen applied to a method parameter, a topic created with `createTopic` can act\nas a parameter decorator. This allows wiring up subscriptions directly inside\nclass methods.\n\nTo enable this behavior, decorate the class with `@AutoSubscribe` and pass the target\nmessage bus, where subscriptions will be created:\n\n```ts\n@AutoSubscribe(messageBus) // or () =\u003e messageBus, if it needs to be lazily resolved\nexport class CommandProcessor {\n  onCommand(@CommandTopic() command: string): void {\n    if (command === \"shutdown\") {\n      /* ... */\n    }\n  }\n}\n```\n\nThis automatically subscribes the `onCommand` method to `CommandTopic`,\nand unsubscribes it when the instance is garbage-collected.\n\n\u003e [!NOTE]  \n\u003e The class must be instantiated, either manually or via a third-party mechanism,\n\u003e for the subscription to be activated. Decorating the class alone does not trigger\n\u003e any subscriptions.\n\n### Unsubscribing programmatically\n\nIf you do not want to rely on garbage collection to clean up the subscriptions,\nyou can unsubscribe manually. To do that, declare a `Subscription` parameter\nimmediately after the decorated topic parameter. The runtime will automatically\ninject the corresponding subscription object:\n\n```ts\n@AutoSubscribe(messageBus)\nexport class CommandProcessor {\n  onCommand(@CommandTopic() command: string, subscription: Subscription): void {\n    if (command === \"shutdown\") {\n      /* ... */\n      subscription.dispose();\n    }\n  }\n}\n```\n\n\u003e [!NOTE]  \n\u003e Only one `Subscription` parameter is allowed per method, and it must follow the topic parameter.\n\n## Subscription options\n\n### Limit\n\nLimits how many messages a subscription can receive before it is automatically disposed.\n\nThis option is useful when you are only interested in the **first n** messages of a topic\nand want to avoid manually disposing the subscription.\n\n```ts\n// The handler will be called at most 3 times\nbus.withLimit(3).subscribe(CommandTopic, (command) =\u003e {\n  /* ... */\n});\n```\n\nThe same applies to asynchronous subscriptions:\n\n```ts\n// The loop will iterate up to 3 times\nfor await (const command of bus.withLimit(3).subscribe(CommandTopic)) {\n  /* ... */\n}\n```\n\nIf fewer than `limit` messages are published, the subscription simply remains idle\nunless manually disposed.\n\n\u003e [!NOTE]  \n\u003e `withLimit` returns a subscription builder, not the message bus itself.  \n\u003e This builder allows fluently applying options before finalizing the subscription.\n\n### Priority\n\nSets the delivery priority of a subscription.\n\nLower values mean higher priority: for example, a subscription with priority `0`\nwill receive messages before other subscriptions with priority `1`.\n\nBy default, all subscriptions use a priority of `1`.\n\n```ts\nbus.withPriority(0).subscribe(CommandTopic, (command) =\u003e {\n  /* ... */\n});\n```\n\nYou can also combine `withPriority` and `withLimit`:\n\n```ts\n// Subscribe with both a custom priority and message limit\nbus.withLimit(2).withPriority(0).subscribe(CommandTopic, (command) =\u003e {\n  /* ... */\n});\n```\n\n## Listening to all messages\n\nIn addition to subscribing to specific topics, you can also listen to all messages\npublished on the bus, regardless of topic. Listeners are invoked before any topic-specific\nsubscribers, and they are notified for every message, even if no topic subscriptions\n(whether active or inactive) exist.\n\nThis might be useful for logging, analytics, or debugging.\n\n```ts\nconst listener: MessageListener = (topic, data, activeSubscriptions) =\u003e {\n  console.log(`Message published to ${topic} with ${activeSubscriptions} subscribers: ${data}`);\n};\n\n// Add the listener\nbus.addListener(listener);\n\n// Remove the listener later, if needed\nbus.removeListener(listener);\n```\n\n\u003e [!IMPORTANT]  \n\u003e Listeners only run on the bus where the message is initially published.\n\u003e If the message propagates to child buses (the default behavior), or to the parent bus,\n\u003e listeners added to those buses will not be called.\n\n## License\n\n[MIT license](https://github.com/lppedd/message-bus/blob/main/LICENSE)\n\n2025-present [Edoardo Luppi](https://github.com/lppedd)  \n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flppedd%2Fmessage-bus","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flppedd%2Fmessage-bus","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flppedd%2Fmessage-bus/lists"}