{"_id":"@akshilmy/eventloom-core","_rev":"2-b6c32e830a35dd3d11934c6efc8fdd70","name":"@akshilmy/eventloom-core","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@akshilmy/eventloom-core","version":"0.1.0","keywords":["sse","server-sent-events","streaming","events","real-time"],"author":{"name":"Akshil"},"license":"MIT","_id":"@akshilmy/eventloom-core@0.1.0","maintainers":[{"name":"akshilmy","email":"akshilmy.19@gmail.com"}],"homepage":"https://github.com/AKSHILMY/eventloom/tree/main/typescript/packages/core","bugs":{"url":"https://github.com/AKSHILMY/eventloom/issues"},"dist":{"shasum":"ef0a4d83bd6cc2dda1eb82813e0f83063d644a6c","tarball":"https://registry.npmjs.org/@akshilmy/eventloom-core/-/eventloom-core-0.1.0.tgz","fileCount":9,"integrity":"sha512-tpqh+XblcBukEL4ZnkozMuEQN6R2MdK7t4YeH5L2as1zQg7+Qr84Yp2+WeeRF3Hm8I+AC1gdwnGsbu/u1VsDjw==","signatures":[{"sig":"MEUCICAvRYaF8vWGCgWQz2E/v6U3fSOYC4gRyriaoo9TKuxCAiEA80mEgc9RClelSJ0wNE8BIEGYXbfQtw5O4C02PstiZBI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":72906},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"gitHead":"4a40739d0742ff8ec8ce22e63b69eab205db9186","scripts":{"test":"vitest run","build":"tsup src/index.ts --format esm,cjs --dts --sourcemap --clean","typecheck":"tsc --noEmit"},"_npmUser":{"name":"akshilmy","email":"akshilmy.19@gmail.com"},"repository":{"url":"git+https://github.com/AKSHILMY/eventloom.git","type":"git","directory":"typescript/packages/core"},"_npmVersion":"11.12.1","description":"Framework-agnostic engine for eventloom: wire-protocol types, component registry, SSE connection handling, and event store merge logic.","directories":{},"sideEffects":false,"_nodeVersion":"25.9.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vitest":"^2.1.8","typescript":"^5.6.3"},"_npmOperationalInternal":{"tmp":"tmp/eventloom-core_0.1.0_1787071022763_0.08371142063192805","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@akshilmy/eventloom-core","version":"0.2.0","description":"Framework-agnostic engine for eventloom: wire-protocol types, component registry, SSE connection handling, and event store merge logic.","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"sideEffects":false,"scripts":{"build":"tsup src/index.ts --format esm,cjs --dts --sourcemap --clean","test":"vitest run","typecheck":"tsc --noEmit"},"keywords":["sse","server-sent-events","streaming","events","real-time"],"author":{"name":"Akshil"},"license":"MIT","homepage":"https://github.com/AKSHILMY/eventloom/tree/main/typescript/packages/core","repository":{"type":"git","url":"git+https://github.com/AKSHILMY/eventloom.git","directory":"typescript/packages/core"},"publishConfig":{"access":"public"},"devDependencies":{"typescript":"^5.6.3","tsup":"^8.3.5","vitest":"^2.1.8"},"_id":"@akshilmy/eventloom-core@0.2.0","gitHead":"dc4286a10430808e5a0ba7ec1857ca46cf937d17","bugs":{"url":"https://github.com/AKSHILMY/eventloom/issues"},"_nodeVersion":"20.20.1","_npmVersion":"10.8.2","dist":{"integrity":"sha512-DJVjgyU1Hngc8kM+5RNHhl1tLfPBnHMBRU4iuu0fAZHmPTjHAd7bp7rRtXdJs6Br2B2tE2NM66YgZ3jxZZz04Q==","shasum":"873c5efe45e8f40f2df38d861c9aadd725489235","tarball":"https://registry.npmjs.org/@akshilmy/eventloom-core/-/eventloom-core-0.2.0.tgz","fileCount":9,"unpackedSize":88297,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIFXlOzfhcqpevXlCqOYicqtSurGVpYQXNCA8vcp7QIDOAiEAgLu0iBDcdwTmREAky4FQEqsICJ+kDrN8ck1SThgBT9s="}]},"_npmUser":{"name":"akshilmy","email":"akshilmy.19@gmail.com"},"directories":{},"maintainers":[{"name":"akshilmy","email":"akshilmy.19@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/eventloom-core_0.2.0_1788421878382_0.33986630254088035"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-18T16:37:02.541Z","modified":"2026-09-03T07:51:18.675Z","0.1.0":"2026-08-18T16:37:02.908Z","0.2.0":"2026-09-03T07:51:18.518Z"},"bugs":{"url":"https://github.com/AKSHILMY/eventloom/issues"},"author":{"name":"Akshil"},"license":"MIT","homepage":"https://github.com/AKSHILMY/eventloom/tree/main/typescript/packages/core","keywords":["sse","server-sent-events","streaming","events","real-time"],"repository":{"type":"git","url":"git+https://github.com/AKSHILMY/eventloom.git","directory":"typescript/packages/core"},"description":"Framework-agnostic engine for eventloom: wire-protocol types, component registry, SSE connection handling, and event store merge logic.","maintainers":[{"name":"akshilmy","email":"akshilmy.19@gmail.com"}],"readme":"# @akshilmy/eventloom-core\n\nFramework-agnostic engine for [eventloom](https://github.com/akshilmy/eventloom): wire-protocol\ntypes, an SSE connection with reconnect/backoff, per-`id` merge/replace/append logic, and a\ntype-accumulating component registry. Zero React/Vue/framework dependency — pairs with\n[`@akshilmy/eventloom-react`](https://www.npmjs.com/package/@akshilmy/eventloom-react) (first-class)\nor your own adapter (Vue, vanilla JS). Pairs on the backend with the Python\n[`eventloom`](https://pypi.org/project/eventloom/) package, but the wire protocol is\nlanguage-agnostic — any backend that emits matching JSON over SSE works.\n\n## Install\n\n```bash\nnpm install @akshilmy/eventloom-core\n```\n\nMost apps want [`@akshilmy/eventloom-react`](https://www.npmjs.com/package/@akshilmy/eventloom-react)\ninstead, which depends on this package and adds React bindings. Install `core` directly only if\nyou're writing your own framework adapter or consuming the stream from vanilla JS.\n\n## Quickstart (framework-agnostic)\n\n```ts\nimport { StreamConnection, EventStore } from \"@akshilmy/eventloom-core\";\n\nconst store = new EventStore();\n\nconst connection = new StreamConnection(\n  \"/stream/dashboard\",\n  (envelope) => {\n    store.apply(envelope); // combines with any prior state for envelope.id per its strategy\n    render(store.snapshot()); // your own render function\n  },\n  {\n    onError: (err) => console.error(\"stream error:\", err),\n  }\n);\n\nconnection.connect();\n// later: connection.disconnect();\n```\n\n## Wire protocol\n\n```ts\ninterface StreamEnvelope<T = unknown> {\n  type: string; // event type, e.g. \"chart.data\" — routes to a component\n  id: string; // groups events belonging to the same logical \"thing\"\n  seq: number; // monotonic sequence number within this id\n  data: T; // event-specific payload\n  strategy: \"replace\" | \"merge\" | \"append\";\n  done: boolean; // true if this is the final event for this id\n  ts: string; // ISO timestamp\n}\n```\n\nThis is a TypeScript `interface`, not a class — envelopes arrive as plain parsed JSON. Use\n`isStreamEnvelope(value)` to runtime-check an unknown value (this is what `StreamConnection` uses\ninternally to reject malformed frames instead of crashing).\n\n## `EventStore` — merge/replace/append logic\n\n```ts\nconst store = new EventStore();\nstore.apply(envelope); // combine one envelope into the store\nstore.get(id); // -> EventSnapshot | undefined, current state for one id\nstore.snapshot(); // -> EventSnapshot[], every id's current state\nstore.clear();\n```\n\nPer `envelope.strategy`:\n\n| Strategy | Behavior | `data` shape you get back from `get`/`snapshot` |\n|---|---|---|\n| `replace` | New data fully replaces old. | Whatever the latest envelope's `data` was. |\n| `merge` | Shallow-merges new fields into the existing object (`{...existing, ...incoming}`). | The accumulated object. |\n| `append` | Pushes into an array. | **An array of every item emitted for that `id` so far** — not a single item. A renderer for an `append`-strategy type should expect `data: T[]`, e.g. a log viewer rendering every line, not just the latest one. |\n\n`EventStore` also defensively ignores a stale/duplicate envelope (`seq <= last applied seq for\nthat id`) — cheap insurance against a reconnect replaying an already-applied event; SSE already\nguarantees in-order delivery per connection, so this rarely triggers in practice.\n\n## `StreamConnection` — transport\n\n```ts\nconst connection = new StreamConnection(url, onEnvelope, {\n  fetchOptions: { headers: { Authorization: \"Bearer ...\" }, credentials: \"include\" },\n  reconnect: true, // default true — reconnect after an error\n  reconnectOnComplete: false, // default false — do NOT reconnect after a clean, intentional close\n  initialReconnectDelayMs: 1000, // default\n  maxReconnectDelayMs: 30000, // default; backoff doubles each failed attempt\n  onOpen: () => {},\n  onError: (err) => {},\n  onComplete: () => {}, // fires once, when the stream ends cleanly and won't reconnect\n});\nconnection.connect();\nconnection.disconnect();\n```\n\n**Clean completion vs. error — these reconnect differently, on purpose.** If the backend closes\nthe HTTP response normally (e.g. `to_sse_response`'s producer finishes and the adapter closes the\nemitter), that's treated as *done*, not a failure: `onComplete` fires once and the connection does\nnot retry, even though `reconnect` defaults to `true`. Only an actual error (non-2xx response,\nnetwork failure) triggers the reconnect-with-backoff loop. This matters for the common \"emit some\nevents then close\" shape (mirrors how most streamed responses work) — without this distinction,\nevery finite stream would get re-fetched and replayed forever the moment it finished, which is\nexactly the failure mode this default avoids. If you're building a backend that intentionally\nexpects the client to keep re-polling after every close, set `reconnectOnComplete: true`.\n\nBuilt on `fetch` + manual SSE-frame parsing rather than the native `EventSource` API — deliberately,\nfor two reasons:\n\n1. **`EventSource` can't set custom headers**, so there's no way to attach an `Authorization`\n   header to it. `fetchOptions` solves auth/credentials cleanly.\n2. **Routing is driven purely by the JSON `type` field**, not any SSE `event:` line. A backend may\n   still send `event: <type>` (the Python adapter does, for tooling like `curl`/devtools), but\n   `StreamConnection` ignores it — per the wire protocol's design (see the project's wire-protocol\n   section), the JSON body is the single source of truth, so switching between languages/adapters\n   that include or omit `event:` never changes client behavior.\n\n## `ComponentRegistry` — the framework-agnostic base\n\n```ts\nimport { ComponentRegistry } from \"@akshilmy/eventloom-core\";\n\nconst registry = new ComponentRegistry()\n  .register(\"chart.data\", { renderer: myChartRenderFn })\n  .register(\"log.line\", { renderer: myLogRenderFn, strategy: \"append\" });\n\nregistry.get(\"chart.data\"); // -> { renderer: myChartRenderFn }\nregistry.has(\"chart.data\"); // -> true\nregistry.types(); // -> [\"chart.data\", \"log.line\"]\n```\n\n`renderer` is deliberately typed `unknown` here — core doesn't know what a \"renderer\" means.\n`@akshilmy/eventloom-react`'s `createRegistry()` wraps this with a React-aware `register()` that\nrequires `renderer` to be a `ComponentType<{ data: T }>`, so passing a component with the wrong\nprop type for the event type you're registering is a **compile error**. Writing a Vue or vanilla\nadapter means doing the same narrowing for whatever \"renderer\" means in that context — `core`\nitself only needs to store the config and accumulate the type-level `{ type -> payload }` map.\n\nA registration's `strategy` field overrides the backend's declared strategy for that type, if set\n— useful when a frontend wants different combine semantics than the backend author chose.\n\n## Extensibility\n\n| What to override | How |\n|---|---|\n| Reconnect/backoff behavior | `StreamConnectionOptions` — `reconnect`, `reconnectOnComplete`, `initialReconnectDelayMs`, `maxReconnectDelayMs` |\n| Auth headers, credentials | `StreamConnectionOptions.fetchOptions` |\n| Transport (SSE vs WebSocket later) | Implement your own class with the same `connect()`/`disconnect()`/envelope-callback shape and swap it in |\n| Merge strategy (frontend override of backend default) | `registry.register(type, { renderer, strategy: \"...\" })`, then apply the override before calling `store.apply()` (see `@akshilmy/eventloom-react`'s `useEventStream` for the reference implementation) |\n| Layout/ordering of rendered events | Don't use a higher-level component like `<StreamView>` — drive `StreamConnection` + `EventStore` directly and render `store.snapshot()` however you like |\n\n## Development\n\n```bash\ncd typescript\nnpm install\nnpm run build --workspace=@akshilmy/eventloom-core\nnpm run test --workspace=@akshilmy/eventloom-core\nnpm run typecheck --workspace=@akshilmy/eventloom-core\n```\n\n## License\n\nMIT\n","readmeFilename":"README.md"}