{"_id":"@3sln/ngin","_rev":"5-f81dfdfa4f6476e3cd9c637a8a27ad3f","name":"@3sln/ngin","dist-tags":{"latest":"0.0.5"},"versions":{"0.0.1":{"name":"@3sln/ngin","version":"0.0.1","keywords":["state management","dependency injection","flux","cqrs"],"author":"Ray Stubbs","license":"MIT","_id":"@3sln/ngin@0.0.1","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"dist":{"shasum":"8782d03acfde028c0ad419adac2f430266978d74","tarball":"https://registry.npmjs.org/@3sln/ngin/-/ngin-0.0.1.tgz","fileCount":3,"integrity":"sha512-gwLlFaw0YdFz+QfPlAFZLDFNfCneAXmjpg84c46eeXaVkJCJNZLIMumrYghz+kF2aqIbR1YDLx83Q2qKF0/vlg==","signatures":[{"sig":"MEUCIQD6tBdSKtELTdLWNHYNsGIq7/jFa6juPkc9IWMyiRHLPAIgaRrB5sKOgh+2GMBNi30O6u81GTwAlJxaWQnxVZOp1SM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":23549},"main":"index.js","type":"module","shasum":"8782d03acfde028c0ad419adac2f430266978d74","_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"_integrity":"sha512-gwLlFaw0YdFz+QfPlAFZLDFNfCneAXmjpg84c46eeXaVkJCJNZLIMumrYghz+kF2aqIbR1YDLx83Q2qKF0/vlg==","repository":{"url":"https://github.com/3sln/ngin.git","type":"git"},"_npmVersion":"10.8.3","description":"A lightweight, dependency-injection based state management library.","directories":{},"_nodeVersion":"24.3.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"@types/bun":"latest"},"_npmOperationalInternal":{"tmp":"tmp/ngin_0.0.1_1759796686551_0.17774044574522585","host":"s3://npm-registry-packages-npm-production"}},"0.0.2":{"name":"@3sln/ngin","version":"0.0.2","keywords":["state management","dependency injection","flux","cqrs"],"author":"Ray Stubbs","license":"MIT","_id":"@3sln/ngin@0.0.2","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"dist":{"shasum":"6b2f30178b0acc7de7bea9b5488a0bc568b2facc","tarball":"https://registry.npmjs.org/@3sln/ngin/-/ngin-0.0.2.tgz","fileCount":3,"integrity":"sha512-B5VbVDYznL8NqtzxZZdP91DCYElq5Y3P+WsgVGf/+cE2ORzqngZYnZwDLYo6UWdQkxtrdOwShR0sgUGZ6Ncksg==","signatures":[{"sig":"MEUCIQD2zIXWfWNPO7ncXCPHJIpUikGv/z36zP9IUagLfseJYQIgUtSIm+Xo7iz+0/r2G5SFvy3LzPkPEC7u9F4D5s8MTlo=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":24660},"main":"index.js","type":"module","shasum":"6b2f30178b0acc7de7bea9b5488a0bc568b2facc","_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"_integrity":"sha512-B5VbVDYznL8NqtzxZZdP91DCYElq5Y3P+WsgVGf/+cE2ORzqngZYnZwDLYo6UWdQkxtrdOwShR0sgUGZ6Ncksg==","repository":{"url":"https://github.com/3sln/ngin.git","type":"git"},"_npmVersion":"10.8.3","description":"A lightweight, dependency-injection based state management library.","directories":{},"_nodeVersion":"24.3.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"@types/bun":"latest"},"_npmOperationalInternal":{"tmp":"tmp/ngin_0.0.2_1767315528398_0.5506687099309093","host":"s3://npm-registry-packages-npm-production"}},"0.0.3":{"name":"@3sln/ngin","version":"0.0.3","keywords":["state management","dependency injection","flux","cqrs"],"author":"Ray Stubbs","license":"MIT","_id":"@3sln/ngin@0.0.3","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"dist":{"shasum":"b9209cb7f5bdabe8511adf87e76489d30e5f0a9c","tarball":"https://registry.npmjs.org/@3sln/ngin/-/ngin-0.0.3.tgz","fileCount":3,"integrity":"sha512-8v+tYVWJiheSy6sXlNYl2hsx//iXLmnIjmTZsQDiOez5Wh9MpoWm6psP9jydN/lh6tUfbMgVSRTgo815xQC45A==","signatures":[{"sig":"MEYCIQDQYvYnDD4Y1x7G4eMZ/oXSanRJJh7RtUYeVXSHmXvoiwIhAJTrFJ7a/fxGeJuA1m9utUAfU3xZ1E4/hCW8V3JRYMg+","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":24751},"main":"index.js","type":"module","shasum":"b9209cb7f5bdabe8511adf87e76489d30e5f0a9c","_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"_integrity":"sha512-8v+tYVWJiheSy6sXlNYl2hsx//iXLmnIjmTZsQDiOez5Wh9MpoWm6psP9jydN/lh6tUfbMgVSRTgo815xQC45A==","repository":{"url":"https://github.com/3sln/ngin.git","type":"git"},"_npmVersion":"10.8.3","description":"A lightweight, dependency-injection based state management library.","directories":{},"_nodeVersion":"24.3.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"@types/bun":"latest"},"_npmOperationalInternal":{"tmp":"tmp/ngin_0.0.3_1767767188783_0.627506445936985","host":"s3://npm-registry-packages-npm-production"}},"0.0.4":{"name":"@3sln/ngin","version":"0.0.4","keywords":["state management","dependency injection","flux","cqrs"],"author":{"name":"Ray Stubbs"},"license":"MIT","_id":"@3sln/ngin@0.0.4","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"homepage":"https://github.com/3sln/ngin#readme","bugs":{"url":"https://github.com/3sln/ngin/issues"},"dist":{"shasum":"d2ec4968939e599bd431a6681a62667fe93e404f","tarball":"https://registry.npmjs.org/@3sln/ngin/-/ngin-0.0.4.tgz","fileCount":9,"integrity":"sha512-lTx+5gcIhmlk6av1JSWPIpY7qeyTY2l6IoCh8OgySzSxO1tWZv2D7pOKZHKX5dlO8kQZgeViAYJgs2wxGfiINw==","signatures":[{"sig":"MEQCIHBZpkLePzKOmvAtuI5J4w+BfXGmKTkq75P1PukXrC6QAiBQYrSnHQPRyJ8h0ux2MRIYudTRwHMLLJ3fHRZ9tZpa+A==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2fngin@0.0.4","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":66308},"main":"index.js","type":"module","exports":{".":"./index.js","./engine":"./src/engine.js","./actions":"./src/actions.js","./queries":"./src/queries.js","./index.js":"./index.js","./providers":"./src/providers.js","./package.json":"./package.json"},"gitHead":"785afba07562ab02a9b11ec3c53a5c78a3c72e98","scripts":{"test":"bun test"},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"repository":{"url":"git+https://github.com/3sln/ngin.git","type":"git"},"_npmVersion":"10.9.8","description":"A lightweight, dependency-injection based state management library.","directories":{},"_nodeVersion":"22.23.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"@types/bun":"latest"},"_npmOperationalInternal":{"tmp":"tmp/ngin_0.0.4_1785189022599_0.7111563199265531","host":"s3://npm-registry-packages-npm-production"}},"0.0.5":{"name":"@3sln/ngin","version":"0.0.5","description":"A lightweight, dependency-injection based state management library.","main":"index.js","type":"module","exports":{".":"./index.js","./index.js":"./index.js","./providers":"./src/providers.js","./actions":"./src/actions.js","./queries":"./src/queries.js","./engine":"./src/engine.js","./package.json":"./package.json"},"author":{"name":"Ray Stubbs"},"repository":{"type":"git","url":"git+https://github.com/3sln/ngin.git"},"license":"MIT","keywords":["state management","dependency injection","flux","cqrs"],"scripts":{"test":"bun test"},"devDependencies":{"@types/bun":"latest"},"publishConfig":{"access":"public"},"_id":"@3sln/ngin@0.0.5","gitHead":"71682360d7c56190138775ec2874dfd3875eb9a5","bugs":{"url":"https://github.com/3sln/ngin/issues"},"homepage":"https://github.com/3sln/ngin#readme","_nodeVersion":"22.23.1","_npmVersion":"10.9.8","dist":{"integrity":"sha512-+DztuCyhZpkEqHdEZOg5xXCVLYV2L3xgKlpngJDPpGGHe90si6ZzubEDad2+0ggVB+d2FTTa2Rb+GOqSA6QSKQ==","shasum":"cb6251d2a0e88232475c4fe74ddc1914e59e01a5","tarball":"https://registry.npmjs.org/@3sln/ngin/-/ngin-0.0.5.tgz","fileCount":10,"unpackedSize":77793,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@3sln%2fngin@0.0.5","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIBQVkuPsxUXZGqabIZSPBg5FerNpTQR8YQBx0mHtvnD3AiAMc9iZUJQxHDew1Sx9blyaTmXn91oIQ4GdM9rC36cYGg=="}]},"_npmUser":{"name":"ray.3sln","email":"contact+npm@3sln.com"},"directories":{},"maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ngin_0.0.5_1785445796016_0.9689469160755559"},"_hasShrinkwrap":false}},"time":{"created":"2025-10-07T00:24:46.445Z","modified":"2026-07-30T21:09:56.522Z","0.0.1":"2025-10-07T00:24:46.742Z","0.0.2":"2026-01-02T00:58:48.564Z","0.0.3":"2026-01-07T06:26:28.917Z","0.0.4":"2026-07-27T21:50:22.779Z","0.0.5":"2026-07-30T21:09:56.170Z"},"bugs":{"url":"https://github.com/3sln/ngin/issues"},"author":{"name":"Ray Stubbs"},"license":"MIT","homepage":"https://github.com/3sln/ngin#readme","keywords":["state management","dependency injection","flux","cqrs"],"repository":{"type":"git","url":"git+https://github.com/3sln/ngin.git"},"description":"A lightweight, dependency-injection based state management library.","maintainers":[{"name":"ray.3sln","email":"contact+npm@3sln.com"}],"readme":"# ngin\n\n> [!WARNING]\n> This is a work-in-progress project and is not yet ready for production use.\n\n`ngin` is a lightweight and flexible state management library designed to help\nyou organize your frontend application logic. It uses a dependency injection\nmodel to coordinate resources, actions, and queries, keeping your code clean\nand testable.\n\nIt is built as three independent layers, so you can take only what you need:\n\n| Layer | Import | Exports | Depends on |\n| --- | --- | --- | --- |\n| Dependency injection | `@3sln/ngin/providers` | `Provider`, `Container` | — |\n| Actions | `@3sln/ngin/actions` | `Action`, `Dispatcher`, `DispatchFeed` | providers |\n| Queries | `@3sln/ngin/queries` | `Query`, `QueryStore` | providers |\n| Everything | `@3sln/ngin` | all of the above, plus `Engine` | — |\n\nActions and queries are siblings: neither imports the other. See\n[Layered Usage](#layered-usage).\n\n## Quick Start\n\n```javascript\nimport { Engine, Action, Query, Provider } from 'ngin';\n\n// Set up a LoggerProvider. It's a dependency for other stuff.\nclass LoggerProvider extends Provider {\n  obtain() {\n    // This is the actual resource.\n    return {\n      log: (message) => console.log(`[LOG]: ${message}`)\n    };\n  }\n  release() {}\n}\n\n// Next, a DataProvider that needs the logger. Providers get other providers\n// injected, so they can handle their dependencies' lifecycles.\nclass DataProvider extends Provider {\n  static deps = ['logger'];\n  \n  constructor({ logger }) {\n    super();\n    this.loggerProvider = logger;\n  }\n  \n  async obtain() {\n    const logger = await this.loggerProvider.obtain();\n    logger.log('Data connection created.');\n    this.loggerProvider.release(logger);\n    // The resource is an object with a fetchData method.\n    return {\n      fetchData: () => 'some data'\n    };\n  }\n  \n  async release() {\n    const logger = await this.loggerProvider.obtain();\n    logger.log('Data connection destroyed.');\n    this.loggerProvider.release(logger);\n  }\n}\n\n// An interceptor that uses the logger. Interceptors, actions, and queries\n// all get the actual resource injected.\n//\n// One interceptor wraps both: the context carries `action` for a dispatch and\n// `query` for a query, so a hook can tell them apart when it cares.\nconst loggerInterceptor = {\n  deps: ['logger'],\n  enter: ({ logger }, { action, query }) => {\n    logger.log(`Entering: ${(action ?? query).constructor.name}`);\n  },\n  leave: ({ logger }, { action, query }) => {\n    logger.log(`Leaving: ${(action ?? query).constructor.name}`);\n  },\n};\n\n// An Action that depends on the Data Provider.\nclass MyAction extends Action {\n  static deps = ['data'];\n  async execute({ data }) {\n    console.log(`Executing action with: ${data.fetchData()}`);\n  }\n}\n\n// A Query that also depends on the Data Provider.\nclass MyQuery extends Query {\n  static deps = ['data'];\n  // Queries are booted on the first subscriber.\n  async boot({ data }, { notify }) {\n    notify(data.fetchData());\n  }\n  // Queries are killed when the last subscriber unsubscribes.\n  async kill({ data }) {\n    console.log(`Query killed with data: ${data.fetchData()}`);\n  }\n}\n\n// Instantiate the engine and kick off the examples.\nconst engine = new Engine({\n  providers: {\n    logger: LoggerProvider,\n    data: DataProvider,\n  },\n  interceptors: [loggerInterceptor],\n});\n\nconsole.log('--- Dispatching Action ---');\nconst actionFeed = engine.dispatch(new MyAction());\n// The dispatch feed is an EventTarget, and `next` awaits one of its events.\nawait actionFeed.next('complete');\n\nconsole.log('--- Query Lifecycle ---');\n// The result of Engine#query is a minimal RxJS-style observable.\n// It also has a 'peek' method to get the current value. An active query\n// answers it from its own last value -- waiting for the first one if it has\n// not emitted yet -- and an inactive query is answered by calling its 'fetch'.\nconst queryHandle = engine.query(new MyQuery());\nconst subscription = queryHandle.subscribe({\n  next: (value) => console.log(`Query received value: ${value}`),\n});\n\nsetTimeout(() => {\n  console.log('Unsubscribing from query...');\n  subscription.unsubscribe();\n}, 100);\n\nawait engine.dispose();\n```\n\n-----\n\n## Layered Usage\n\n`Engine` is a facade. It builds a `Container`, a `Dispatcher` and a\n`QueryStore` and wires them together — nothing more. When you only need part of\nthat, build the part you need.\n\n### Just dependency injection\n\n```javascript\nimport { Container, Provider } from '@3sln/ngin/providers';\n\nconst container = new Container({\n  providers: {\n    config: Provider.fromSingleton({ apiUrl: 'https://api.example.com' }),\n    api: Provider.fromRefCounted(\n      async ({ config }) => {\n        const cfg = await config.obtain();\n        try {\n          return new ApiClient(cfg.apiUrl);\n        } finally {\n          config.release(cfg);\n        }\n      },\n      (api) => api.close(),\n      { deps: ['config'] }\n    ),\n  },\n});\n\n// Scoped: obtain, run, release — even if the callback throws.\nconst users = await container.use(['api'], ({ api }) => api.getUsers());\n\n// Or hold a lease for work that outlives a single call.\nconst lease = await container.lease(['api']);\nlease.resources.api.subscribe(/* ... */);\nawait lease.release();\n\nawait container.dispose();\n```\n\n### Dependency injection plus actions\n\n```javascript\nimport { Container } from '@3sln/ngin/providers';\nimport { Action, Dispatcher } from '@3sln/ngin/actions';\n\nconst dispatcher = new Dispatcher({\n  container: new Container({ providers }),\n  interceptors: [loggerInterceptor],\n});\n\ndispatcher.dispatch(new MyAction());\n```\n\n### Dependency injection plus queries\n\n```javascript\nimport { Container } from '@3sln/ngin/providers';\nimport { QueryStore } from '@3sln/ngin/queries';\n\nconst queries = new QueryStore({\n  container: new Container({ providers }),\n  interceptors: [loggerInterceptor],\n});\n\nqueries.query(new MyQuery()).subscribe(console.log);\n```\n\nA `QueryStore` built without a `dispatcher` works fine; it only needs one if a\nquery you realize declares a `bootAction` or `killAction`.\n\n#### One-shot queries\n\nA query that implements `fetch` and not `boot` is a **read**: it has one answer\nand no way of learning of a second. Subscribing to one fetches once, emits the\nvalue, and completes — an ordinary single-value observable, so a consumer can\nsubscribe to any query without knowing in advance whether it is live.\n\n```javascript\nclass ReadItems extends Query {\n  static deps = ['db'];\n  constructor(collection) { super(); this.collection = collection; }\n  async fetch({ db }) { return db.list(this.collection); }\n}\n\nengine.query(new ReadItems('photos')).subscribe({\n  next: (items) => render(items),\n  complete: () => {},          // immediately after next\n});\n\nawait engine.query(new ReadItems('photos')).peek();   // or just this\n```\n\nThree details:\n\n- **Completing evicts it**, so the next subscribe fetches again. Keeping it\n  would serve the first answer forever with nothing able to invalidate it.\n- **Subscribers arriving together share one fetch**, as with any query.\n- **A `fetch` that throws reaches `observer.error`**, so a failed read is not\n  indistinguishable from a successful one that found nothing. Observers with no\n  `error` handler still get `complete`, as before.\n\n### Composing them yourself\n\n`Engine` accepts pre-built layers, which is how you share a container between\nengines or substitute a stub in tests. It also hands them back:\n\n```javascript\nconst container = new Container({ providers });\nconst engine = new Engine({ container, interceptors });\n\nengine.container;   // the Container above\nengine.dispatcher;  // Dispatcher\nengine.queries;     // QueryStore\n```\n\n### The seam\n\nEverything above the provider layer talks to it through four members and\nnothing else — `feed`, `resolve(depsConfig)`, `lease(...depsConfigs)` and\n`use(depsConfig, fn)` — so anything implementing those can stand in for a\n`Container`.\n\nA **lease** is what lets the two upper layers share one resource model despite\nvery different lifetimes: an action holds a lease for a single dispatch, a\nquery holds one from `boot` until `kill`. `lease()` obtains every declared\nresource or none of them, and its `release()` is idempotent.\n\n-----\n\n## The Dispatch Feed\n\n`dispatch()` returns a `DispatchFeed`. It is an `EventTarget`, and it is the\nonly way results leave an action — deliberately, because an action that emits\n`progress` five times before it emits `result` is saying more than a return\nvalue could.\n\n```javascript\nclass ScanCollection extends Action {\n  static deps = ['storage'];\n  async execute({ storage }, { dispatchFeed, signal }) {\n    for await (const page of storage.pages()) {\n      if (signal.aborted) return;         // cooperative — see abort() below\n      dispatchFeed.dispatchEvent(\n        Object.assign(new Event('progress'), { scanned: page.seen })\n      );\n    }\n    dispatchFeed.dispatchEvent(\n      Object.assign(new Event('result'), { ok: true })\n    );\n  }\n}\n```\n\nA dispatch ends on exactly one of **`complete`**, **`error`** or **`abort`**,\nonce.\n\n### `feed.next(names, { signal })`\n\nAwaits the first of `names` to fire and resolves with that event — the shape a\nrequest handler wants: dispatch, await the event carrying the answer, reply.\n\n```javascript\nconst feed = engine.dispatch(new ScanCollection());\nfeed.addEventListener('progress', (e) => console.log(e.scanned));\n\nconst result = await feed.next('result');     // or feed.next(['hit', 'miss'])\n```\n\nAll three terminal events end the wait unless you name one, because after one of\nthem nothing else will ever fire:\n\n- the action **throws** → rejects with the error the action threw, unwrapped, so\n  a caller mapping error types onto something else (an HTTP status, say) still\n  can;\n- the dispatch is **aborted** → rejects with the abort reason;\n- it **ends without emitting** what you asked for → rejects saying so. A promise\n  that never settles is the worst thing this could do to a caller — no timeout,\n  no cancellation, nothing to log — so it never does;\n- naming `'complete'`, `'error'` or `'abort'` opts back in to receiving it as a\n  value.\n\n`next` also answers correctly after the fact. Terminal events fire exactly once,\nso a caller that awaits something else first would otherwise be waiting on an\nevent already gone past; the feed remembers how it ended, and `feed.settled`\nreports it (`'complete'`, `'error'`, `'abort'`, or `null` while running).\n\nThe optional `signal` is the **caller's own** cancellation, distinct from\n`feed.abort()`. An HTTP handler passes the request's signal so a disconnect\nunblocks the handler while work already underway carries on.\n\n### `feed.abort(reason)` and `context.signal`\n\n`abort()` says the caller is no longer interested. The signal is handed to the\naction and to every interceptor as `context.signal`:\n\n```javascript\nconst feed = engine.dispatch(new ScanCollection());\nfeed.abort(new Error('client disconnected'));\nconst ended = await feed.next('abort');       // ended.reason === that error\n```\n\nCooperative, necessarily: nothing can interrupt a running function, so an action\nthat wants to be stoppable checks `signal.aborted`, or hands the signal to\nsomething that honours it (`fetch`, a scan loop, a nested dispatch).\n\n`feed.reason` is what `abort()` was given — an `AbortError` if it was given\nnothing, never `undefined`, since that value is what `next()` rejects with and a\ncaller writing `catch (e) { e.message }` should not have to guard it. The feed\nkeeps its own copy rather than reading `signal.reason` back at the point of use;\nthat one belongs to the runtime, and Bun 1.3 drops it under memory pressure.\n\n**An aborted dispatch ends on `abort` and never on `complete`.** A scan stopped\nat thirty percent did not complete, and saying it did is how \"it worked\" gets\nreported about work that did not happen. That holds however the action ended: one\nthat throws because it honoured the signal threw *because* of the abort, so\nreporting the throw would name the symptom rather than the cause. The error rides\nalong on the event as `event.error`, so nothing is lost.\n\nThree details worth knowing:\n\n- **Anything waiting on `next()` rejects at once**, except a wait for `abort`\n  itself. The action may take a while to notice; the caller does not have to\n  wait for it. Awaiting `abort` after aborting is the ordinary wind-down — you\n  have stopped caring about the answer but still need to know the work stopped.\n- **Aborting something already finished changes nothing.** What happened,\n  happened; a late `abort()` does not rewrite a `complete` into an `abort`.\n- **Aborting before the action starts is a real ordering**, since `dispatch()`\n  returns synchronously and the action runs on a later turn. The action still\n  runs, and sees `signal.aborted === true` immediately — skipping it would skip\n  the interceptor unwinding with it, and an interceptor that opened something has\n  to be given the chance to close it.\n\n### The `abort` interceptor hook\n\nInterceptors unwind through `leave` on success and `error` on failure. An\naborted dispatch gets a third: `abort`. It matters because of what `leave` means\nto the obvious interceptor —\n\n```javascript\nconst transaction = {\n  deps: ['db'],\n  enter: ({ db }) => db.begin(),\n  leave: ({ db }) => db.commit(),\n  error: ({ db }) => db.rollback(),\n  abort: ({ db }) => db.rollback(),   // without this, `leave` commits\n};\n```\n\n— which without an `abort` hook **commits the work of a dispatch that was\ncancelled half way through**, since `error` is null and `leave` is what runs.\n\nThe hook receives `reason` (what `abort()` was given) and `error` (what the\naction threw on its way out, usually because it honoured the signal; `null`\notherwise). There is no `handled()`: a cancellation cannot be handled into a\nsuccess, because the work did not happen.\n\nEvery interceptor that entered gets exactly **one** unwind call — that is what\nmakes it safe to acquire something in `enter` — so `abort` falls back to the\nhook that would have run without it, never to nothing:\n\n```\naborted   → abort ?? (error ? error : leave)\nerror     → error ?? nothing\notherwise → leave ?? nothing\n```\n\nAn interceptor that has not heard of aborting therefore behaves exactly as it\ndoes today; defining `abort` is how you opt into the distinction.\n\n-----\n\n## Interceptors Wrap Queries Too\n\nAn interceptor wraps *work*, and a query is work. The same list covers both\nlayers, so a concern that belongs to your application rather than to one kind\nof call — an access check, a metric, a usage record, a log — is registered once:\n\n```javascript\nconst engine = new Engine({ providers, interceptors: [usage] });\n// or, by hand:\nconst dispatcher = new Dispatcher({ container, interceptors: [usage] });\nconst queries = new QueryStore({ container, dispatcher, interceptors: [usage] });\n```\n\n### Which one am I wrapping?\n\nThe context says so by name. `action` is there for a dispatch, `query` for a\nquery, and never both:\n\n| Key | Present for | Alongside |\n| --- | --- | --- |\n| `action` | a dispatch | `dispatchFeed`, `signal` |\n| `query` | a query | `bootFeed`, `killFeed` |\n\n`engineFeed` and `state` are on both, and `state` reaches the work itself: an\naction's `execute`, a query's `boot`, `fetch` and `kill` all receive what the\n`enter` hooks threaded through.\n\n```javascript\nconst usage = {\n  deps: ['metrics'],\n  enter: ({ metrics }, { action, query }) => {\n    metrics.increment(action ? 'action' : 'query', (action ?? query).constructor.name);\n  },\n};\n```\n\n### Enter on boot, leave on tear down\n\nA live query is entered **before it boots** — before its `bootAction` is\ndispatched and before its resources are leased, so an access check can refuse it\nbefore anything happens on its behalf — and left once the last observer has\nunsubscribed, the query has been killed and its lease released. `enter` and\n`leave` can therefore be minutes apart, which is exactly what makes them useful\nfor a permit, a span, or a subscription count.\n\nA query answered by `fetch` is the shape a dispatch is:\n\n```\nsubscribe to a live query   enter, boot ... kill, leave\nsubscribe to a one-shot     enter, fetch, leave\npeek() an inactive query    enter, fetch, leave\npeek() an active query      nothing — it was entered when it booted\n```\n\nTwo consequences, which matter most to anything counting:\n\n- **A query is entered once per realization, not once per subscriber.** The\n  second observer of a query that is already running joins the live one; there\n  is no second `enter`, and `leave` runs when the *last* of them unsubscribes.\n- **A `bootAction` or `killAction` is a dispatch in its own right**, so it runs\n  the stack again with `action` set, nested inside the query's.\n\nThe `abort` hook is a dispatch's alone: a query has no caller to give up on it,\nso it ends through `leave` or `error` and never through `abort`.\n\n### They have no say in the result\n\nValues reaching your subscribers, and the value `peek()` resolves with, are the\nquery's own whatever the hooks return. A hook's return value goes to `state`\nand nowhere else.\n\nFailure works as it does for a dispatch — `error` hooks in reverse order, one\nunwind call per interceptor that entered — with one difference worth stating:\n`handled()` cannot bring a query back. By the time the stack unwinds, a query\nthat failed has already told its observers and released its lease, so handling\nits error only means the interceptors outside see a query that ended rather than\none that broke. A query that fails to boot is unwound there and then, so being\nkilled afterwards does not unwind it twice.\n\n-----\n\n## Provider Options\nYou can configure dependencies for actions, interceptors, or queries with an\noptions object. This object is passed as the first argument to the provider's\n`obtain` method.\n\nFor these components, you can specify dependencies using an object instead of\nan array. The values of this object are the options passed to the corresponding\nprovider.\n\n```javascript\nclass MyProviderWithOptions extends Provider {\n  obtain(options) {\n    // options will be { timeout: 5000 }\n    return { timeout: options.timeout };\n  }\n}\n\n// In an action, you can declare a dependency with options.\nclass MyActionWithOptions extends Action {\n  static deps = {\n    myProvider: { timeout: 5000 }\n  };\n  async execute({ myProvider }) {\n    console.log(`Provider configured with timeout: ${myProvider.timeout}`);\n  }\n}\n\nconst engine = new Engine({\n  providers: {\n    myProvider: MyProviderWithOptions,\n  }\n});\n\nengine.dispatch(new MyActionWithOptions());\n```\n\n-----\n\n## Provider Lifecycle: `obtain`, `release`, `flush`, and `dispose`\nProviders manage the lifecycle of the resources they provide.\n\n- **`obtain(options)`**: This method is called whenever a consumer (like an Action or Query) needs a resource. It's responsible for creating or acquiring the resource. It can optionally receive an `options` object from the consumer.\n\n- **`release(resource, options)`**: This is the counterpart to `obtain`. It's called after the consumer has finished its work. For providers that manage a pool of resources, this is where you would return the `resource` to the pool. For singleton-like providers, this method is often a no-op.\n\n- **`flush()`**: This optional method is called on all providers when the `engine.dispose()` method is invoked, just before the `dispose` methods are called. This is the ideal place to perform any finalization that needs to happen before resources are permanently cleaned up, especially if that finalization requires using other providers.\n\n- **`dispose()`**: This optional method is called on all providers when the `engine.dispose()` method is invoked. It is the correct place to perform permanent cleanup of a provider's underlying resources, such as closing database connections, terminating web sockets, or completing observable streams.\n\n-----\n\n## Built-in Provider Implementations\n`ngin` offers three built-in provider types for resource management.\n\n### Singleton Provider\nPerfect for resources that are globally shared.\n\n```javascript\nconst mySingletonProvider = Provider.fromSingleton({ database: 'my-db-instance' });\n```\n\n### Pool Provider\nIdeal for managing a fixed number of resources, like a pool of database\nconnections.\n\n```javascript\nconst createConnection = async () => new DatabaseConnection();\nconst destroyConnection = (conn) => conn.close();\nconst myPoolProvider = Provider.fromPool(createConnection, destroyConnection, {size: 10});\n```\n\n### Reference-Counted Provider\nUse this for resources that are lazily created and destroyed only when no\nlonger in use.\n\n```javascript\nconst createResource = async () => new ExpensiveResource();\nconst destroyResource = (res) => res.cleanup();\nconst myRefCountedProvider = Provider.fromRefCounted(createResource, destroyResource);\n```\n\n### Lazy Singleton Provider\nFor a long-lived backbone — a database handle, a storage client, a search index:\nbuilt once, lazily, shared by every consumer at once, and destroyed only when\nthe container is.\n\n```javascript\nconst myBackboneProvider = Provider.fromLazySingleton(\n  async ({ config }) => {\n    const cfg = await config.obtain();\n    const db = new Database(cfg.url);\n    await db.connect();          // building is async, and often ordered\n    return db;\n  },\n  (db) => db.close(),\n  { deps: ['config'] }\n);\n```\n\n`release` is a synchronous no-op: consumers that lease the backbone on every\nrequest should not pay teardown on the way out. The resource goes down in\n`dispose`, and since the container disposes in reverse construction order,\nteardown order falls out of the dependency graph instead of being maintained by\nhand.\n\nThe other three do not cover this case, and the ways they miss are worth knowing:\n\n| | lazy build | concurrent consumers | destroyed on `release` |\n| --- | --- | --- | --- |\n| `fromSingleton` | no — needs it already built | yes | no |\n| `fromPool({size: 1})` | yes | **no — it is a mutex** | returned to the pool |\n| `fromRefCounted` | yes | yes | **yes, at zero** |\n| `fromLazySingleton` | yes | yes | no |\n\nA pool of one blocks the second `obtain` until the first releases, so one slow\nconsumer stops every other. `fromRefCounted` shares correctly, but an ordinary\nlease/release cycle drops the count to zero and tears the resource down — the\nnext consumer then silently gets a *different* one while whatever held the old\nis still pointing at it.\n\n### Dependencies in the built-ins\n\nAll three factories take a `deps` option. Those dependencies arrive at\n`create`, `destroy` and `dispose` as **providers** — the same thing a provider\nwritten by hand receives in its constructor — so the resource you build can\nhold a dependency for its own lifetime:\n\n```javascript\nconst ConnectionProvider = Provider.fromPool(\n  async ({ credentials }) => {\n    // Obtained in create, held by the connection, released in destroy.\n    const creds = await credentials.obtain();\n    return { conn: await connect(creds), credentials, creds };\n  },\n  async ({ conn, credentials, creds }) => {\n    await conn.close();\n    credentials.release(creds);\n  },\n  { size: 10, deps: ['credentials'] }\n);\n```\n\nIf a dependency is only read at creation time, obtain and release it around\nthat read:\n\n```javascript\nconst WorkerProvider = Provider.fromPool(\n  async ({ config }) => {\n    const cfg = await config.obtain();\n    try {\n      return new Worker(cfg.workerPath);   // only the path is retained\n    } finally {\n      config.release(cfg);\n    }\n  },\n  (worker) => worker.terminate(),\n  { size: 4, deps: ['config'] }\n);\n```\n\nActions, queries and interceptors are the other way around: they are consumers\nrather than resource managers, so they receive already-obtained **resources**,\nreleased for them when their work finishes.\n","readmeFilename":"README.md"}