{"_id":"@alwaysblank/denden","_rev":"2-5bc54501e6ff84617ab698a2c890e634","name":"@alwaysblank/denden","dist-tags":{"latest":"2.0.0"},"versions":{"1.1.0":{"name":"@alwaysblank/denden","version":"1.1.0","_id":"@alwaysblank/denden@1.1.0","maintainers":[{"name":"alwaysblank","email":"ben@alwaysblank.org"}],"homepage":"https://github.com/alwaysblank/denden#readme","bugs":{"url":"https://github.com/alwaysblank/denden/issues"},"dist":{"shasum":"b9b1cdf0c7586c81d250a81a9379a984762cca92","tarball":"https://registry.npmjs.org/@alwaysblank/denden/-/denden-1.1.0.tgz","fileCount":8,"integrity":"sha512-DAiTfURD3nohejWMpOXwmcvUcewB8QKODBiYQjzLsty2bH6tYrLj/sqha/QXx1p+htLU7pXsSi1QUeiMXUCk6g==","signatures":[{"sig":"MEQCIEmCkrKnvh4SGmU09vH6oWot7ddeWmWU1BNzJfDijaaaAiBSeSRbKXbFSJJlPxMaISQzauqC6XPTxNEwcChl5gNNgg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":17048},"gitHead":"80fd475c4b761b2adc455f2c2fefcb151f951fc7","scripts":{"test":"jest","build":"rollup --config"},"_npmUser":{"name":"alwaysblank","email":"ben@alwaysblank.org"},"repository":{"url":"git+https://github.com/alwaysblank/denden.git","type":"git"},"_npmVersion":"10.9.2","description":"Extremely simple publish/subscribe system built on events and nothing else.","directories":{},"_nodeVersion":"22.14.0","_hasShrinkwrap":false,"devDependencies":{"jest":"^29.7.0","tslib":"^2.8.1","rollup":"^4.36.0","babel-jest":"^29.7.0","typescript":"^5.8.2","@babel/core":"^7.26.10","@types/jest":"^29.5.14","@babel/preset-env":"^7.26.9","@rollup/plugin-terser":"^0.4.4","@babel/preset-typescript":"^7.26.0","@rollup/plugin-typescript":"^12.1.2"},"_npmOperationalInternal":{"tmp":"tmp/denden_1.1.0_1768155307794_0.8851497496335852","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"name":"@alwaysblank/denden","repository":{"type":"git","url":"git+https://github.com/alwaysblank/denden.git"},"version":"2.0.0","description":"Extremely simple publish/subscribe system built on events.","scripts":{"build":"rollup -c","test":"jest","docs":"npx typedoc"},"browser":"dist/browser.js","module":"dist/denden.js","devDependencies":{"@babel/core":"^7.26.10","@babel/preset-env":"^7.26.9","@babel/preset-typescript":"^7.26.0","@rollup/plugin-terser":"^0.4.4","@rollup/plugin-typescript":"^12.1.2","@types/jest":"^29.5.14","babel-jest":"^29.7.0","jest":"^29.7.0","rollup":"^4.36.0","tslib":"^2.8.1","typedoc":"^0.28.16","typescript":"^5.8.2"},"_id":"@alwaysblank/denden@2.0.0","gitHead":"ec0bac82d929cf6e2a2047143e029c639f911449","bugs":{"url":"https://github.com/alwaysblank/denden/issues"},"homepage":"https://github.com/alwaysblank/denden#readme","_nodeVersion":"22.21.1","_npmVersion":"10.9.4","dist":{"integrity":"sha512-RJIu98wzaUP79lnoiEPtgMs2gbvVH2hkmlNjf7VqntDaN2bPGxlV2stxiOB+nCCR4wdec+NCQLkaXpW+QHgTOA==","shasum":"22a9dce170d25d537fc93aa75d83f0ad20a03984","tarball":"https://registry.npmjs.org/@alwaysblank/denden/-/denden-2.0.0.tgz","fileCount":14,"unpackedSize":89916,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCW09ozSulwrLED9mlvmsTQJwMIHEX706fKgHjXbnamqgIgExJ4APbhFKy6djYK+OPJUwFJWN9gZrw218neWBZ313Q="}]},"_npmUser":{"name":"alwaysblank","email":"ben@alwaysblank.org"},"directories":{},"maintainers":[{"name":"alwaysblank","email":"ben@alwaysblank.org"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/denden_2.0.0_1771434426286_0.595469634225946"},"_hasShrinkwrap":false}},"time":{"created":"2026-01-11T18:15:07.709Z","modified":"2026-02-18T17:07:06.563Z","1.1.0":"2026-01-11T18:15:07.931Z","2.0.0":"2026-02-18T17:07:06.466Z"},"bugs":{"url":"https://github.com/alwaysblank/denden/issues"},"homepage":"https://github.com/alwaysblank/denden#readme","repository":{"type":"git","url":"git+https://github.com/alwaysblank/denden.git"},"description":"Extremely simple publish/subscribe system built on events.","maintainers":[{"name":"alwaysblank","email":"ben@alwaysblank.org"}],"readme":"# Den-Den 🐌\nExtremely simple publish/subscribe system built on events.\n\nDesigned for the browser, but should work in an JS context that supports events.\n\n## Why?\nDen-Den is meant to solve a couple of specific problems:\n<details>\n<summary>Retain a log of dispatched events.</summary>\n\n```ts\nconst hub = new Hub();\nhub.pub('sandwich', 'reuben');\nhub.pub('sandwich', 'club');\n\nhub.getMessages({cid: 'sandwich'})\n  .map(m => m.payload);\n// ['reuben', 'club']\n\nhub.getMessages({cid: 'sandwich', order: 'ASC'})\nhub.getMessages({cid: 'sandwich'})\n  .map(m => m.payload);\n// ['club', 'reuben']\n\n// The '2' says to include the last 2 messages on the 'sandwich' channel.\nhub.sub('sandwich', (p) => console.log(`payload: ${p}`), 2);\n// \"payload: club\"\n// \"payload: reuben\"\n\nhub.pub('sandwich', 'cheese');\n// \"payload: cheese\"\n```\n\n</details>\n\n<details>\n<summary>Attach subscribers to complex routes.</summary>\n\n```ts\nconst hub = new Hub();\n\nhub.sub('*', (payload) => console.log(`*: ${payload}`));\nhub.sub('sand*', (payload) => console.log(`sand*: ${payload}`));\nhub.sub(/.*er$/, (payload) => console.log(`regex: ${payload}`));\n\nhub.pub('test', 'test value');\n// \"*: test value\"\n\nhub.pub('sandwich', 'reuben');\n// \"*: reuben\"\n// \"sand*: reuben\"\n\nhub.pub('hammer', 'claw');\n// \"*: claw\"\n// \"regex: claw\"\n\nhub.pub('sandpiper', 'bird');\n// \"*: bird\"\n// \"sand*: bird\"\n// \"regex: bird\"\n```\n\n</details>\n\n<details>\n<summary>Know when all subscribers have received a given event.</summary>\n\n```ts\nconst hub = new Hub();\n\nhub.sub('sandwich', (p) => {\n  doSomething(p);\n});\n\nhub.sub('sandwich', (p) => {\n  return doSomethingAsynchronously(p);\n});\n\nhub.pub('sandwhich', 'reuben')\n  .then(r => {\n    // This executed when both callbacks have finished.\n    // `r` is an array of the return values of both callbacks.\n  });\n```\n\n> **NOTE:**\n> A callback which contains a Promise but is not `async` must return a Promise or it cannot be tracked.\n\n</details>\n\n<details>\n<summary>Create a system for dynamic dependency resolution at runtime without direct communication, race conditions, or timeouts.</summary>\n\n**You have some logic that depends on dependencies whose load order is non-deterministic.**\n```ts\nconst hub = new Hub();\n\nfirst(hub, (results) => {\n  const [dep1result, dep2result] = results;\n  // This will execute after both dependencies have resolved, or after 1 second.\n}, ['dependency-1', 'dependency-2'], 1000);\n\nsomeExternalDependency().then(dependency => {\n  hub.pub('dependency-1', dependency);\n});\n\nhub.pub('dependency-2', someOtherExternalDependency());\n```\n\n**You have some logic that other elements of your application should be able to modify.**\n```ts\nconst hub = new Hub();\n\nasync function doSomething() {\n  const settings = Map([\n    ['active', true]\n  ]);\n\n  await hub.pub('doSomething/settings', settings)\n    .then(results => {\n      results.forEach(r => {\n        if (r instanceof Error) {\n          return; // Don't process errors.\n        }\n        const {add = [], remove = []} = r;\n        add.forEach(([key, value]) => settings.set(key, value));\n        remove.forEach(key => settings.delete(key));\n      });\n    });\n\n  activate(settings);\n}\n\nhub.sub('doSomething/settings', (settings) => {\n  if (settings.get('active')) {\n    return ['port', 1234];\n  }\n});\n\ndoSomething();\n// Settings map would look like:\n// [ ['active', true], ['port', 1234] ]\n```\n\n</details>\n\n## Installation\n### Browser\nLoad `dist/browser.js` via a script tag to create a `window.denden` global hub.\n\n`window.denden.queue` is an array to which you can push two types of records before Den-Den loads:\n- **Message:** A tuple where the first element is the name of the channel, and the second is the payload to be sent.\n- **Command:** A callback that will receive the hub instance as its only argument.\n\n```html\n<script>\n  window.denden = window.denden || { queue: [] };\n  window.denden.queue.push((hub) => hub.sub('sandwich', payload => console.log(`msg: ${payload}`), 1));\n  window.denden.queue.push(['sandwich', 'reuben']);\n  window.denden.queue.push(() => console.log('initialized'));\n</script>\n\n<script src=\"dist/browser.js\"></script>\n```\n\n> You can still push to the queue *after* Den-Den loads, and the messages/commands will be processed immediately.\n> However, in most cases it will be simpler to just call the hub instance directly once it exists.\n\nThis global instance also provides access to all tools and extensions via `window.denden.tools` and `window.denden.extensions`.\nAll extensions are bound to the global hub instance, so you don't need to provide a hub instance when calling them:\n\n```js\n// Yes.\nwindow.denden.extensions.once('sandwich', p => console.log(p));\n\n// No.\nwindow.denden.extensions.once(window.denden, 'sandwich', p => console.log(p));\n```\n\nThe `withHub` tool is similarly also bound to the global hub instance, so you need only pass the method you wish to bind:\n\n```js\n// Yes.\nwindow.denden.tools.withHub((hub) => doSomethingWithHub(hub));\n\n// No.\nwindow.denden.tools.withHub(window.denden, (hub) => doSomethingWithHub(hub));\n```\n\n### Module\nThe `@alwaysblank/denden` module can be imported and will provide access to all internal classes and functions.\nIn most cases, you will only need `Hub` and any extensions you might want to use:\n\n```ts\nimport { Hub, once } from '@alwaysblank/denden';\n\nconst hub = new Hub();\n\n// Run a callback only once.\nonce(hub, 'sandwich', p => console.log(p));\n\nhub.pub('sandwich', 'reuben');\nhub.pub('sandwich', 'club');\n// \"reuben\"\n```\n\nFor more usage details, see the [documentation](https://alwaysblank.github.io/denden/).\n\n## Channels\n\nA \"channel\" is where messages are dispatched to, and the mechanism by which subscribers indicate which messages they wish to receive.\nEach channel has a single name, in the form of a string key.\nThe key can be any string, but cannot contain a wildcard symbol (`*`).\n\n### Routes\n\nA channel \"route\" can describe a literal channel name (`sandwich`), a wildcard (`sand*` or `*wich`), or as regular expression (`/^sa.*ch$/`).\n\nRoutes can be used in both `Hub.sub()` and `Hub.pub()`, but the behavior is slightly different:\n\n#### `sub()`\n\nWhen `sub()` is invoked, it takes a route, or array of routes, as its first argument.\nAny messages sent to a channel which matches any of the specified routes will be dispatched to the callback.\n\nRoutes can be either definitively named channels (i.e. `sandwich`), wildcard routes (e.g. `sand*` or `*wich`), regular expression routes (e.g. `/^sa.*ch$/`), or a mix of all three.\n\n#### `pub()`\n\nWhen `pub()` is invoked, it takes a route, or array of routes, as its first argument and a payload as its second argument.\nThe payload will be dispatched to all channels that match the specified routes.\n\nRoutes can be either definitively named channels (i.e. `sandwich`), wildcard routes (e.g. `sand*` or `*wich`), regular expression routes (e.g. `/^sa.*ch$/`), or a mix of all three.\n\nDefinitively named channels will be created if they do not already exist.\nNon-definitive routes (i.e., wildcard or regex routes) will be resolved against any existing channels but _will not be created if they don't exist_ (since there is not enough information to infer definitive names from them).\nThis means that it is _not_ possible to \"prepopulate\" a channel search with messages for future subscribers.\n\nExample:\n```ts\nhub.createChannel('sandwich');\nhub.pub('sand*', 'reuben');\n\nhub.sub('sandwich', (p) => console.log(`sandwich: ${p}`), 1);\n// \"sandwich: reuben\"\nhub.sub('sandpiper', (p) => console.log(`sandpiper: ${p}`), 1);\n// No messages received.\n\nhub.pub('sand*', 'club');\n// \"sandwich: club\"\n// \"sandpiper: club\"\n```\n\nChannels can also be manually created with `Hub.createChannel(name)`.\n\n## Modules\n\nThe core module (`src/core.ts`) provides the most basic functionaltiy: \nA hub that can dispatch messages to subscribers and subscribe to channels.\nThis may provide all you need!\nBut the package also includes some other functionality in \"extensions\" to handle specific tasks that may save you some time.\n\n> All extensions use the public API for the `Hub` and `Message` classes.\n> In other words, you could build them yourself if you wanted to—they're only here to save you some time.\n\n### Conditionals\n\nA set of small tools to help with `Hub.sub()` for conditional subscriptions.\n\n- [`once`](https://alwaysblank.github.io/denden/functions/once) - Remove the subscription after the first message is received.\n- [`only`](https://alwaysblank.github.io/denden/functions/only) - Only messages in the channel which match a test will be sent to the callback.\n- [`until`](https://alwaysblank.github.io/denden/functions/until) - Remove the subscription after a given condition is met. This is dependent on the order in which messages are received.\n\n### Waiter\n\nTools for handling cases where you wish to collect responses from a set of different channels, potentially over time.\n\n> These functions accept channel routes as arguments, so be sure to read the section on [Routes](#routes) above.\n\n- [`first`](https://alwaysblank.github.io/denden/functions/first) - Returns an array containing the first message sent to each channel (route). Includes a timeout value, which will cause it to return early if no messages are received.\n  - [`firstAsync`](https://alwaysblank.github.io/denden/functions/firstAsync) - The same behavior as `first`, except it returns a Promise instead of taking a callback.\n- [`latest`](https://alwaysblank.github.io/denden/functions/latest) - Returns an array containing the most recent message sent to each channel (route). Includes a timeout value, which will cause it to return early if no messages are received.\n  - [`latestAsync`](https://alwaysblank.github.io/denden/functions/latestAsync) - The same behavior as `latest`, except it returns a Promise instead of taking a callback.\n\n### Watch\n\n- [`watch`](https://alwaysblank.github.io/denden/functions/watch) - Watches an `EventEmitter` for events of the specified type, then dispatches them to the hub on the specified channel(s). See the rules above for using routes on `pub()`, which also apply here","readmeFilename":"README.md"}