{"id":29651279,"url":"https://github.com/calmdownval/signal","last_synced_at":"2025-07-22T05:06:31.529Z","repository":{"id":57100792,"uuid":"265601317","full_name":"CalmDownVal/signal","owner":"CalmDownVal","description":"A lightweight event dispatcher with async handler support.","archived":false,"fork":false,"pushed_at":"2024-05-27T17:14:33.000Z","size":1314,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-07-22T05:06:30.493Z","etag":null,"topics":["async","asynchronous","dispatcher","events","lightweight"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/CalmDownVal.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"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}},"created_at":"2020-05-20T15:04:10.000Z","updated_at":"2024-05-23T01:04:03.000Z","dependencies_parsed_at":"2024-05-23T02:24:19.472Z","dependency_job_id":"caefd476-eee5-4990-8a52-5691d92b24a4","html_url":"https://github.com/CalmDownVal/signal","commit_stats":{"total_commits":45,"total_committers":2,"mean_commits":22.5,"dds":"0.022222222222222254","last_synced_commit":"ef5067b215c96892ee50756c680503e60df30daa"},"previous_names":[],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/CalmDownVal/signal","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CalmDownVal%2Fsignal","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CalmDownVal%2Fsignal/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CalmDownVal%2Fsignal/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CalmDownVal%2Fsignal/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/CalmDownVal","download_url":"https://codeload.github.com/CalmDownVal/signal/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/CalmDownVal%2Fsignal/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":266430668,"owners_count":23927169,"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-07-22T02:00:09.085Z","response_time":66,"last_error":null,"robots_txt_status":null,"robots_txt_updated_at":null,"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":["async","asynchronous","dispatcher","events","lightweight"],"created_at":"2025-07-22T05:06:30.826Z","updated_at":"2025-07-22T05:06:31.516Z","avatar_url":"https://github.com/CalmDownVal.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Signal\n\nA lightweight event dispatcher.\n\n- [Installation](#installation)\n- [Key Benefits](#key-benefits)\n- [Usage Guide](#usage-guide)\n  - [Creating a Signal](#creating-a-signal)\n  - [Signal Backend](#signal-backend)\n  - [Adding Handlers](#adding-handlers)\n  - [Removing Handlers](#removing-handlers)\n  - [Triggering a Signal](#triggering-a-signal)\n  - [Checking for Handlers](#checking-for-handlers)\n  - [Async Signals](#async-signals)\n    - [Serial Execution](#serial-execution)\n    - [Parallel Execution](#parallel-execution)\n  - [Forwarding this](#forwarding-this)\n  - [Wrapping an EventEmitter](#wrapping-an-eventemitter)\n- [Changelog](#changelog)\n\n## Installation\n\nThe package is distributed via NPM and can be installed by any compatible\npackage manager. It already contains its own typings and needs no additional\ndependencies to work with TypeScript.\n\n```sh\n# NPM\nnpm install @cdv/signal\n\n# Yarn\nyarn add @cdv/signal\n```\n\n## Key Benefits\n\nSignal is a somewhat niche alternative to the usual EventEmitter (Node) /\nEventTarget (DOM) APIs. It looks and feels quite different and may involve a\nslight learning curve, but here's why it might be worth it:\n\n- ✅ does not rely on class inheritance or mixins\n- ✅ recognizes and awaits Promises (async handlers)\n- ✅ avoids event name strings, making TypeScript integration effortless\n- ✅ smoothly integrates with standard event emitter APIs\n- ✅ comes bundled with TypeScript declarations\n- ✅ tiny (\u003c2 kB) and without any dependencies\n\n## Usage Guide\n\nThe library provides everything as named exports. Usually the best approach with\nregards to code readability is to import the entire namespace as `Signal`.\n\n```ts\nimport * as Signal from '@cdv/signal';\n```\n\n### Creating a Signal\n\nTo create a signal call the `Signal.create` function. You can pass an options\nobject with the following properties:\n\n- `async: boolean`  \n  controls whether the signal should await promises to be returned by event\n  handlers, defaults to `false`\n- `parallel: boolean`  \n  controls whether asynchronous handlers will run in parallel or in series, only\n  has effect if `async` is set to `true`, defaults to `false` (i.e. serial\n  execution)\n- `backend: 'array' | 'set'`  \n  controls which data structure is used to hold the handler collection, see the\n  [Signal Backend](#signal-backend) section for more information, defaults to\n  `'array'`\n\n```ts\n// will invoke handlers synchronously in series\nconst syncSignal = Signal.create();\n\n// will invoke handlers asynchronously in series\nconst serialAsyncSignal = Signal.create({ async: true });\n\n// will invoke handlers asynchronously in parallel\nconst parallelAsyncSignal = Signal.create({\n  async: true,\n  parallel: true\n});\n\n// will use a set to hold its handlers\nconst uniqueHandlerSignal = Signal.create({ backend: 'set' });\n```\n\n### Signal Backend\n\nSignals offer the choice between arrays and sets as the backing data structure\nholding the collection of registered handlers. The key difference is that sets\nonly store unique handlers whereas arrays allow the same handler to be added\nmultiple times.\n\nArray is the default backend as it is supported in every environment and offers\nthe best overall performance for almost all use cases.\n\nSets have a larger memory footprint and decrease the speed of creating new\nSignal instances. Generally sets should be preferred when you need to enforce\nunique handlers or when optimizing for *a lot* of `on` and `off` calls.\n\nFor a more in-depth performance analysis see\n[latest benchmark results](./packages/signal-benchmark/benchmark-results.md).\n\n### Adding Handlers\n\nTo add a handler use the `Signal.on` function. The first argument is a signal\ninstance, the second is the handler to add. An optional third argument with an\noptions object can be provided.\n\n```ts\nSignal.on(mySignal, () =\u003e console.log('foo'));\n```\n\nHandlers will be invoked every time the signal is triggered. You can enable the\n`once` option to only invoke a handler once and then have it automatically\nremoved from the handler collection.\n\n```ts\nSignal.on(mySignal, myHandler, { once: true });\n\n// shorter version using the .once util\nSignal.once(mySignal, myHandler);\n```\n\nBy default handlers are added at the end of the handler collection and are\ninvoked in the same order when the signal is triggered. The `prepend` option can\nbe enabled to instead insert at the start of the handler collection. Note that\nthis option is only supported by the array backend.\n\n```ts\nSignal.on(mySignal, myHandler1);\n\n// when mySignal is triggered, myHandler2 will be invoked before myHandler1\nSignal.on(mySignal, myHandler2, { prepend: true });\n```\n\nAn alternative way to add handlers to a signal is the `Signal.subscribe`\nfunction. It has the same usage as `Signal.on` but in addition will return an\n'unsubscriber' function. Often useful when working with libraries like React.\n\n### Removing Handlers\n\nTo remove a handler (regardless of the once option), use the `Signal.off`\nfunction.\n\nIf no specific handler is provided as the second argument, the `.off` function\nwill remove *all* handlers registered for the signal.\n\n```ts\n// will remove the first found occurrence of myHandler\nSignal.off(s1, myHandler);\n\n// will remove all registered handlers\nSignal.off(s1);\n```\n\nThe `.off` function will return a boolean indicating whether the operation\nremoved any handlers.\n\n### Triggering a Signal\n\nEach signal instance is simultaneously a function. Triggering it is as simple as\nadding a pair of brackets! You can pass any data as the first argument to a\nsignal, it will be forwarded to each handler. Typically this will be an event\nobject with additional information.\n\n```ts\nmySignal(123);\n```\n\nSignals return a boolean value (or for async signals, a Promise resolving to\none) indicating whether any handlers were present and invoked. This is often\nuseful for fallback behavior, e.g. logging when no handlers are attached to an\nerror signal:\n\n```ts\ntry {\n  // ...\n}\ncatch (ex) {\n  if (errorSignal(ex)) {\n    console.error(ex);\n  }\n}\n```\n\nSynchronous signals will always invoke handlers in series. The execution stops\nimmediately if any one of them throws. It is the caller's responsibility to\nhandle thrown exceptions:\n\n```ts\ntry {\n  mySignal();\n}\ncatch (ex) {\n  console.error('one of the handlers threw an exception', ex);\n}\n```\n\nYou can add async handlers to synchronous signals, but they will be executed in\na fire-and-forget fashion. This may be desirable in some cases, but keep in mind\nthat *it will become impossible to handle any potential promise rejections!*\n\n### Checking for Handlers\n\nWhen computationally expensive operations are needed for event data creation,\nit may be worth checking whether there are any handlers beforehand to avoid such\noperations when they're not necessary.\n\nFor this task, Signal provides the `lazy` utility function. It accepts a signal\ninstance and a factory callback to create event data. This callback will only be\ninvoked if the signal has any handlers.\n\n```ts\nSignal.lazy(mySignal, () =\u003e ({\n  value: heavyFn()\n}));\n```\n\nA boolean value (or for async signals, a Promise resolving to one) indicating\nwhether any handlers were present and invoked is returned.\n\n### Async Signals\n\nAsynchronous signal interface is almost identical to its synchronous\ncounterpart. The key difference is that an async signal will check the return\ntype of every handler and handle any promises it receives.\n\nWhen using async signals all promise rejections are guaranteed to be handled\nregardless of execution strategy used. In some cases errors may be suppressed,\nsee below for details.\n\nThe execution strategy of async handlers is configurable via the `parallel`\noption (see [Creating a Signal](#creating-a-signal)).\n\n#### Serial Execution\n\nThe default strategy is serial execution. Execution will await each handler\nbefore moving onto the next one.\n\nThis is the default strategy as it's analogous to synchronous signals. A promise\nrejection will immediately propagate upwards and terminate the execution.\nHandlers further down the execution order will not run in such case.\n\n```ts\nconst mySignal = Signal.create({ async: true });\n\nSignal.on(mySignal, () =\u003e sleep(100));\nSignal.on(mySignal, () =\u003e sleep(100));\n\n// will take ~200ms\nawait mySignal();\n```\n\n#### Parallel Execution\n\nWhen enabled, the signal will invoke all handlers simultaneously and resolve\nonce *all* have resolved. If a handler rejects, the wrapping promise returned by\nthe signal will immediately reject as well. This is similar to the behavior of\n`Promise.all`.\n\n```ts\nconst mySignal = Signal.create({\n  async: true,\n  parallel: true\n});\n\nSignal.on(mySignal, () =\u003e sleep(100));\nSignal.on(mySignal, () =\u003e sleep(100));\n\n// will take ~100ms\nawait mySignal();\n```\n\nNote that after the first rejection, other handlers continue their execution and\nthere is no way to await them anymore. Should any additional rejections occur,\nthey are suppressed as there is no longer a way to propagate upwards.\n\nWith parallel execution, it is a good practice to either make sure none of the\nhandlers ever reject, or pass an abort signal through the event object so that\nyou retain control over the still-pending actions in case of a rejection,\ne.g.:\n\n```ts\nconst abort = Signal.create();\ntry {\n  await mySignal({ abort });\n}\ncatch (ex) {\n  console.error(ex);\n  abort();\n}\n```\n\nNote that the above example has nothing to do with the `AbortController` and\n`AbortSignal` browser APIs. However, you could use those for this purpose too!\n\n### Forwarding this\n\nSignals forward `this` to all its handlers, however there are a few caveats to\nusing this feature. These stem from how JavaScript functions and the binding of\n`this` work.\n\nAny handler that relies on forwarded `this` has to be a regular function, not an\narrow function. When contained in an object and called as `obj.signal(...)`,\nthat object will be passed as `this` to the signal's handlers.\n\n```ts\nconst obj = {\n  value: 'foo',\n  mySignal: Signal.create()\n};\n\nSignal.on(obj.mySignal, function () {\n  console.log(this.value);\n});\n\n// will print 'foo'\nobj.mySignal();\n```\n\nWhen signals are not contained within an object, or you wish to forward a\ndifferent one, it is necessary to instead trigger using the `.call` method and\nexplicitly pass the desired reference:\n\n```ts\nconst obj = { value: 'bar' };\nconst mySignal = Signal.create();\n\nSignal.on(mySignal, function () {\n  console.log(this.value);\n});\n\n// will print 'bar'\nmySignal.call(obj);\n```\n\n### Wrapping an EventEmitter\n\nIf you have an EventEmitter (Node) or an EventTarget (browser) that you wish to\n'signalify' you can do so by passing a signal instance to the `addEventListener`\nmethod:\n\n```ts\nconst confirmed = Signal.create\u003cMouseEvent\u003e();\nconst button = document.getElementById('ok-button');\n\nbutton.addEventListener('click', confirmed);\n```\n\nNow every time the button is clicked the `confirmed` signal will trigger\nforwarding the `MouseEvent` object and `this` (in this example, the `\u003cbutton\u003e`\nreference) to all its handlers.\n\n## Changelog\n\n- 4.5.0\n  - Signals now return booleans indicating whether any handlers were invoked.\n- 4.4.0\n  - Added the `prepend` option to `on`, `once` and `subscribe`.\n- 4.3.0\n  - The package is now distributed under `@cdv/signal`.\n  - Improved backend implementation.\n- 4.2.0\n  - Added the `subscribe` method.\n- 4.1.0\n  - The `lazy` util now returns booleans indicating whether any handlers were\n    invoked.\n- 4.0.0\n  - Changed `es6map` backend to `set`.\n  - Removed `hasHandlers` getter, use `lazy` instead.\n  - Removed `createSync` util, use `create` instead.\n  - Removed `createAsync` util, use `create` instead.\n  - Improved performance and unit test coverage.\n- 3.1.0\n  - Added the `lazy` utility function.\n  - Added the `isAsync` property to signals.\n  - Added the `hasHandlers` property to signals.\n  - Added JSDoc comments.\n- 3.0.0\n  - Added the option to choose between backends.\n  - Renamed type `Handler` to `SignalHandler`.\n  - Renamed type `HandlerOptions` to `SignalHandlerOptions`.\n- 2.0.0\n  - Signals now only pass the first argument to handlers.\n- 1.0.0\n  - Initial implementation.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcalmdownval%2Fsignal","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcalmdownval%2Fsignal","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcalmdownval%2Fsignal/lists"}