{"_id":"@accelint/stream","_rev":"2-0925905965725b6d61f0092720054206","name":"@accelint/stream","dist-tags":{"latest":"0.3.0"},"versions":{"0.2.0":{"name":"@accelint/stream","version":"0.2.0","keywords":["dev-tk","react","sse","streams","tooling","websocket"],"author":{"name":"https://hypergiant.com"},"license":"Apache-2.0","_id":"@accelint/stream@0.2.0","maintainers":[{"name":"hg-devsecops","email":"hg-devsecops@hypergiant.com"},{"name":"brandonjpierce","email":"brandonpierce91@gmail.com"},{"name":"bryan_hg","email":"bryan.kizer@hypergiant.com"},{"name":"colin.leahy","email":"colin.leahy@hypergiant.com"},{"name":"jacob.foster.hg","email":"jacob.foster@hypergiant.com"}],"homepage":"https://github.com/gohypergiant/standard-toolkit#readme","bugs":{"url":"https://github.com/gohypergiant/standard-toolkit/issues"},"dist":{"shasum":"46b646f392c3c8d995c70fe0439ce21cd938affb","tarball":"https://registry.npmjs.org/@accelint/stream/-/stream-0.2.0.tgz","fileCount":56,"integrity":"sha512-F9mtkq+5h8OFhCzkU2PDq8hzKgehXPUNbKC7Rlwd3UE1d0+A+7xAdrdB8Bb/x6ejwtbJfCdLqVj+ny8m1dqfDg==","signatures":[{"sig":"MEUCIQC3Fiy2pAMrAmEy4XiIb7yKvrU7wNCNx02od/Visr5wsgIgXWcsr6kijJcTLZRdeyG8mlYsfLw/HaVxMgM2rb+hgmI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":173463},"main":"./dist/index.js","type":"module","_from":"file:accelint-stream-0.2.0.tgz","owner":"default/pathfinder","title":"Accelint Stream","types":"./dist/index.d.ts","module":"./dist/index.js","$schema":"https://json.schemastore.org/package","engines":{"node":">=22","pnpm":">=10"},"exports":{".":"./dist/index.js","./react":"./dist/react/index.js","./types":"./dist/types.js","./utils":"./dist/utils.js","./stream":"./dist/stream.js","./constants":"./dist/constants.js","./removable":"./dist/removable.js","./transport":"./dist/transport.js","./package.json":"./package.json","./stream-cache":"./dist/stream-cache.js","./subscribable":"./dist/subscribable.js","./stream-client":"./dist/stream-client.js","./stream-observer":"./dist/stream-observer.js","./react/use-stream":"./dist/react/use-stream.js","./react/use-sse-stream":"./dist/react/use-sse-stream.js","./react/use-stream-count":"./dist/react/use-stream-count.js","./react/use-stream-state":"./dist/react/use-stream-state.js","./react/use-websocket-stream":"./dist/react/use-websocket-stream.js","./react/stream-client-provider":"./dist/react/stream-client-provider.js"},"private":false,"scripts":{"dev":"pnpm tsc --watch","lint":"pnpm biome lint . --verbose","test":"pnpm vitest --dir=src","build":"pnpm tsdown","index":"pnpm zx ../../scripts/indexer.mjs packages/stream","format":"pnpm biome format . --write --verbose","license":"pnpm zx ../../scripts/license.mjs","test:watch":"pnpm vitest --dir=src --watch","format:check":"pnpm biome format . --verbose","lint:package":"pnpm publint"},"subPath":"packages/stream","_npmUser":{"name":"brandonjpierce","email":"brandonpierce91@gmail.com"},"_resolved":"/tmp/83db8497eb6a8c0602df3e9e347bb93b/accelint-stream-0.2.0.tgz","_integrity":"sha512-F9mtkq+5h8OFhCzkU2PDq8hzKgehXPUNbKC7Rlwd3UE1d0+A+7xAdrdB8Bb/x6ejwtbJfCdLqVj+ny8m1dqfDg==","repository":{"url":"git+https://github.com/gohypergiant/standard-toolkit.git","type":"git"},"_npmVersion":"11.17.0","description":"TanStack-Query-style caching for held-open stream connections (SSE and WebSocket): framework-agnostic core, React hooks at @accelint/stream/react.","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","dependencies":{"@tanstack/query-core":"^5.101.2"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"react":"^19","tsdown":"^0.18.0","vitest":"^4.0.15","react-dom":"^19","typescript":"^5.9.3","@types/react":"^19","@testing-library/dom":"^10.4.1","@testing-library/react":"^16.3.0","@accelint/vitest-config":"0.1.6","@testing-library/jest-dom":"^6.9.1","@accelint/typescript-config":"0.1.4"},"peerDependencies":{"react":"^19"},"peerDependenciesMeta":{"react":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/stream_0.2.0_1787681429825_0.6952261134890483","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@accelint/stream","description":"TanStack-Query-style caching for held-open stream connections (SSE and WebSocket): framework-agnostic core, React hooks at @accelint/stream/react.","version":"0.3.0","author":{"name":"https://hypergiant.com"},"$schema":"https://json.schemastore.org/package","dependencies":{"@tanstack/query-core":"^5.101.2"},"devDependencies":{"@testing-library/dom":"^10.4.1","@testing-library/jest-dom":"^6.9.1","@testing-library/react":"^16.3.0","@types/react":"^19","react":"^19","react-dom":"^19","tsdown":"^0.18.0","typescript":"^5.9.3","vitest":"^4.0.15","@accelint/typescript-config":"0.1.4","@accelint/vitest-config":"0.1.6"},"engines":{"node":">=22","pnpm":">=10"},"exports":{".":"./dist/index.js","./constants":"./dist/constants.js","./react":"./dist/react/index.js","./react/stream-client-provider":"./dist/react/stream-client-provider.js","./react/use-sse-stream":"./dist/react/use-sse-stream.js","./react/use-sse-streams":"./dist/react/use-sse-streams.js","./react/use-stream":"./dist/react/use-stream.js","./react/use-stream-count":"./dist/react/use-stream-count.js","./react/use-stream-state":"./dist/react/use-stream-state.js","./react/use-streams":"./dist/react/use-streams.js","./react/use-websocket-stream":"./dist/react/use-websocket-stream.js","./react/use-websocket-streams":"./dist/react/use-websocket-streams.js","./removable":"./dist/removable.js","./stream":"./dist/stream.js","./stream-cache":"./dist/stream-cache.js","./stream-client":"./dist/stream-client.js","./stream-observer":"./dist/stream-observer.js","./streams-observer":"./dist/streams-observer.js","./subscribable":"./dist/subscribable.js","./transport":"./dist/transport.js","./types":"./dist/types.js","./utils":"./dist/utils.js","./package.json":"./package.json"},"keywords":["dev-tk","react","sse","streams","tooling","websocket"],"license":"Apache-2.0","main":"./dist/index.js","module":"./dist/index.js","owner":"default/pathfinder","peerDependencies":{"react":"^19"},"peerDependenciesMeta":{"react":{"optional":true}},"private":false,"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/gohypergiant/standard-toolkit.git"},"sideEffects":false,"subPath":"packages/stream","title":"Accelint Stream","type":"module","types":"./dist/index.d.ts","scripts":{"build":"pnpm tsdown","dev":"pnpm tsc --watch","format":"pnpm biome format . --write --verbose","format:check":"pnpm biome format . --verbose","index":"pnpm zx ../../scripts/indexer.mjs packages/stream","license":"pnpm zx ../../scripts/license.mjs","lint":"pnpm biome lint . --verbose","lint:package":"pnpm publint","test":"pnpm vitest --dir=src","test:watch":"pnpm vitest --dir=src --watch"},"_id":"@accelint/stream@0.3.0","bugs":{"url":"https://github.com/gohypergiant/standard-toolkit/issues"},"homepage":"https://github.com/gohypergiant/standard-toolkit#readme","_integrity":"sha512-VGi5iBm4AtuBoddR000sZ/KRaitImSmMkdMedODKnVS+GSxvQ3xNNcMmf3EFyiPguavl9BAg2TXP8CivjoeeKA==","_resolved":"/tmp/213641e8c86644de197ca3537ab6fcbd/accelint-stream-0.3.0.tgz","_from":"file:accelint-stream-0.3.0.tgz","_nodeVersion":"24.19.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-VGi5iBm4AtuBoddR000sZ/KRaitImSmMkdMedODKnVS+GSxvQ3xNNcMmf3EFyiPguavl9BAg2TXP8CivjoeeKA==","shasum":"9b965cf82d9838bd4449650fa994d44e6ad30478","tarball":"https://registry.npmjs.org/@accelint/stream/-/stream-0.3.0.tgz","fileCount":68,"unpackedSize":227550,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCpuzGgGAdn1a+py2jxd1EwYKvPItsKpKg8WAU72L7ieAIhAMgTGSBiS3EhSb9FaWiEK1viutl65Ij9OB5tnwMXMOpx"}]},"_npmUser":{"name":"brandonjpierce","email":"brandonpierce91@gmail.com"},"directories":{},"maintainers":[{"name":"hg-devsecops","email":"hg-devsecops@hypergiant.com"},{"name":"brandonjpierce","email":"brandonpierce91@gmail.com"},{"name":"bryan_hg","email":"bryan.kizer@hypergiant.com"},{"name":"colin.leahy","email":"colin.leahy@hypergiant.com"},{"name":"jacob.foster.hg","email":"jacob.foster@hypergiant.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/stream_0.3.0_1788360750486_0.9146688230995674"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-25T18:10:29.613Z","modified":"2026-09-02T14:52:30.773Z","0.2.0":"2026-08-25T18:10:29.972Z","0.3.0":"2026-09-02T14:52:30.613Z"},"bugs":{"url":"https://github.com/gohypergiant/standard-toolkit/issues"},"author":{"name":"https://hypergiant.com"},"license":"Apache-2.0","homepage":"https://github.com/gohypergiant/standard-toolkit#readme","keywords":["dev-tk","react","sse","streams","tooling","websocket"],"repository":{"type":"git","url":"git+https://github.com/gohypergiant/standard-toolkit.git"},"description":"TanStack-Query-style caching for held-open stream connections (SSE and WebSocket): framework-agnostic core, React hooks at @accelint/stream/react.","maintainers":[{"name":"hg-devsecops","email":"hg-devsecops@hypergiant.com"},{"name":"brandonjpierce","email":"brandonpierce91@gmail.com"},{"name":"bryan_hg","email":"bryan.kizer@hypergiant.com"},{"name":"colin.leahy","email":"colin.leahy@hypergiant.com"},{"name":"jacob.foster.hg","email":"jacob.foster@hypergiant.com"}],"readme":"<!-- Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\nThis file is licensed to you under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License. You may obtain a copy\nof the License at https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software distributed under\nthe License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\nOF ANY KIND, either express or implied. See the License for the specific language\ngoverning permissions and limitations under the License. -->\n\n# @accelint/stream\n\nCache and share SSE and WebSocket connections by key.\n\n`@accelint/stream` manages held-open browser stream connections. Consumers use\n`streamKey` as the stream identity. Consumers with the same key share one\nconnection.\n\nThe package has two entry points:\n\n- `@accelint/stream` — framework-agnostic core APIs\n- `@accelint/stream/react` — React hooks and provider\n\nReact is an optional peer dependency. Core-only consumers do not need to load\nReact.\n\n## Installation\n\n```bash\npnpm add @accelint/stream\n```\n\n### Peer dependencies\n\nInstall React only if you use `@accelint/stream/react`.\n\n```bash\npnpm add react\n```\n\nTypeScript types are included.\n\n## Quick Start\n\n```tsx\nimport { StreamClient } from '@accelint/stream';\nimport { StreamClientProvider, useSSEStream } from '@accelint/stream/react';\n\nconst client = new StreamClient();\n\nfunction HealthStatus({ apiUri }: { apiUri: string }) {\n  const { data, status } = useSSEStream<{ status: string }>({\n    streamKey: ['health', apiUri],\n    uri: `${apiUri}/stream/health`,\n  });\n\n  return <pre>{JSON.stringify({ status, data }, null, 2)}</pre>;\n}\n\nexport function App({ apiUri }: { apiUri: string }) {\n  return (\n    <StreamClientProvider client={client}>\n      <HealthStatus apiUri={apiUri} />\n    </StreamClientProvider>\n  );\n}\n```\n\n## What is @accelint/stream?\n\n`@accelint/stream` is a cache and observer layer for live browser streams. It\nsupports Server-Sent Events and WebSocket transports. It provides shared-key\nstream reuse, lazy connection setup, observer results, and cache-wide state\ninspection. This library is heavily inspired by tanstack query.\n\nThe core package works without React. The React subpath adds hooks and a\nprovider.\n\n## Why use @accelint/stream?\n\nThis package helps when multiple consumers need to share stream lifecycle and\nstate.\n\nIt provides:\n\n- shared connections by `streamKey`\n- lazy connection setup on first subscription\n- short unobserved linger through `gcTime`\n- SSE and WebSocket support through the same cache model\n- cache-wide inspection for counts and filtered state\n\n## TanStack Query mapping\n\n| `@accelint/stream` | TanStack Query | Description |\n| --- | --- | --- |\n| `useSSEStream()` / `useWebSocketStream()` | `useQuery()` | Hook-level stream access |\n| `useSSEStreams()` / `useWebSocketStreams()` | `useQueries()` | One hook call over a dynamic set of streams |\n| `streamKey` / `streamHash` | `queryKey` / `queryHash` | Stream identity |\n| `decodeFn` | `queryFn` | Raw frame decoder |\n| `StreamObserver` | `QueryObserver` | Per-consumer observer |\n| `StreamsObserver` | `QueriesObserver` | Dynamic set of per-stream observers |\n| `Stream` | `Query` | One shared stream per key |\n| `StreamCache` | `QueryCache` | Cache of streams |\n| `StreamClient` | `QueryClient` | Cache owner and imperative API |\n| `StreamClientProvider` / `useStreamClient` | `QueryClientProvider` / `useQueryClient` | React context wiring |\n| `useStreamState(filters, select)` | `useMutationState` | Cache-wide observation |\n| `useStreamCount(filters)` | `useIsFetching` | Count of matching streams |\n| `gcTime` linger | `gcTime` linger | Unobserved retention window |\n\n## Relationships\n\n```mermaid\nflowchart LR\n    subgraph React\n        A[Component A] --> HA[\"useSSEStream({ streamKey: K })\"]\n        B[Component B] --> HB[\"useSSEStream({ streamKey: K })\"]\n    end\n    HA --> OA[StreamObserver A]\n    HB --> OB[StreamObserver B]\n    subgraph StreamClient\n        C[\"StreamCache (Map by streamHash)\"]\n    end\n    OA -- \"cache.build(K)\" --> C\n    OB -- \"cache.build(K)\" --> C\n    C --> S[\"Stream for K<br/>(state: data, status)\"]\n    S --> T[\"Shared transport<br/>(EventSourceTransport | WebSocketTransport)\"]\n    T -- \"open connection\" --> SV[(Server)]\n    SV -- \"raw frames\" --> T\n    T -- \"decodeFn(raw) → data | error | ignore\" --> S\n```\n\nConsumers with the same `streamKey` share one `Stream` instance and one\ntransport connection.\n\n## Lifecycle\n\n### Mount, connect, and message flow\n\n```mermaid\nsequenceDiagram\n    participant Comp as Component\n    participant Hook as useSSEStream\n    participant Obs as StreamObserver\n    participant Cache as StreamCache\n    participant Str as Stream\n    participant T as Transport (SSE here)\n\n    Comp->>Hook: render\n    Hook->>Obs: new StreamObserver(options)\n    Obs->>Cache: build(streamKey, uri, { decodeFn, gcTime, transport })\n    Note over Str: get existing or create Stream for this streamHash\n    Comp->>Hook: commit (useSyncExternalStore subscribes)\n    Hook->>Obs: subscribe\n    Obs->>Str: addObserver(observer)\n    Note over Str: first observer triggers connection setup\n    Str->>T: createTransport('sse', uri)\n    T-->>Str: onOpen\n    Str-->>Obs: status connected\n    Obs-->>Comp: re-render\n    T-->>Str: onMessage(raw text frame)\n    Note over Str: decodeFn(raw) → data | error | ignore\n    Str-->>Obs: state update + message notification\n    Obs-->>Comp: re-render\n```\n\nFacts:\n\n- Render does not open a connection.\n- The connection starts when the first observer subscribes.\n- SSR renders do not connect.\n- `onMessage` runs for every message, including duplicate payloads.\n- `state.data` uses structural sharing, so equal payloads can keep the same\n  reference.\n\n### Unmount, linger, and removal\n\n```mermaid\nflowchart TB\n    Start([Start]) --> Observed\n    Observed -->|last observer leaves| Lingering\n    Lingering -->|observer returns| Observed\n    Lingering -->|gcTime expires| Removed\n    Observed -->|explicit remove| Removed\n    Removed -->|hook remounts| Observed\n    Removed --> End([End])\n```\n\nWhen the last observer unsubscribes, the stream remains in the cache until\n`gcTime` expires. If another observer subscribes before that, the same stream\nis reused.\n\n## Differences from TanStack Query\n\n- A stream holds an open network connection.\n- Default `gcTime` is 30 seconds.\n- There is no `staleTime` concept.\n- `retry()` closes and reopens the connection.\n\n## Transports\n\n- `useSSEStream` uses `EventSource`. Browser reconnect behavior follows the\n  server's `retry:` configuration.\n- `useWebSocketStream` uses WebSocket transport with client-side reconnect\n  backoff.\n- WebSocket URIs can be `http(s)://` or `ws(s)://`. `http(s)` is converted to\n  `ws(s)`.\n- A `streamKey` identifies one stream on one transport. If the same key is\n  reused with a different transport or URI, the existing stream remains in\n  use and the package logs an error.\n\n## API\n\n### `StreamClient`\n\nOwns a `StreamCache` and provides imperative reads.\n\n```ts\nconst client = new StreamClient();\n```\n\nKey methods:\n\n- `getStreamCache()`\n- `getStreamState(streamKey)`\n- `getStreams(filters?)`\n- `getStreamCount(filters?)`\n- `getStreamKeys()`\n- `clear()`\n\n### `StreamCache`\n\nStores `Stream` instances by hashed `streamKey`.\n\nImportant behavior:\n\n- `streamKey` is the identity, not `uri`\n- later observers can increase `gcTime` and `messageHistory`, but do not lower\n  them\n- reusing the same key with a different `uri` or transport logs an error and\n  keeps the existing stream\n\n### `Stream`\n\nRepresents one shared live connection and its current state.\n\nUseful surface:\n\n- `state` — `{ data, dataUpdateCount, dataUpdatedAt, status }`\n- `getMessages()` — retained raw messages when `messageHistory > 0`\n- `getTransport()` — current live transport instance, if connected\n- `getEventSource()` — underlying `EventSource` for SSE streams\n- `retry()` — close and reopen the connection\n- `close()` — tear down the current connection\n\n### `StreamObserver`\n\nPer-consumer observer used by the React hooks. It applies `select`, tracks\nenabled state, and exposes the observer result.\n\n### `StreamsObserver`\n\n`QueriesObserver` analog used by the plural hooks: owns a dynamic set of\nchild `StreamObserver`s behind one subscribe/`getCurrentResult()` pair.\n`setOptions(configs)` reconciles the children by `streamKey` hash — new\nconfigs create observers, removed configs release their stream\nsubscriptions (normal gc linger), survivors keep observer state and result\nidentity.\n\n### `useSSEStream(options)`\n\nReact hook for SSE streams.\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `streamKey` | `readonly unknown[]` | Stream identity. Include all values the `uri` depends on. |\n| `uri` | `string` | SSE endpoint. |\n| `decodeFn` | `DecodeFn<T>` | Converts a raw frame into `data`, `error`, or `ignore`. |\n| `enabled` | `boolean` | Skip connecting when `false`. |\n| `gcTime` | `number` | Unobserved linger before removal. |\n| `select` | `(data: T) => TData` | Observer-specific derived slice. |\n| `messageHistory` | `number` | Retain the last N raw messages. |\n| `onOpen` | `(status: StreamStatus) => void` | Called when the connection opens. |\n| `onMessage` | `(data: T) => void` | Called for every message. |\n| `onError` | `(status: StreamStatus) => void` | Called when the stream errors. |\n| `client` | `StreamClient` | Optional client override instead of context. |\n\n**Returns:** `StreamObserverResult<TData, T>` with `data`, `messages`,\n`status`, derived booleans, and `retry()`, `pause()`, `resume()`.\n\n### `useSSEStreams(configs, options?)`\n\n`useQueries` analog\n\n| Argument | Type | Description |\n| --- | --- | --- |\n| `configs` | `UseSSEStreamsConfig<T, TData>[]` | One entry per stream, same shape as `useSSEStream`'s options (`streamKey`, `uri`, `enabled`, `select`, `messageHistory`, callbacks) minus `client`. |\n| `options.combine` | `(results) => TCombined` | Derive one value from the per-stream results. |\n| `options.client` | `StreamClient` | Optional client override instead of context. |\n\n**Returns:** without `combine`, `StreamObserverResult<TData, T>[]`\n\nWhen the UI naturally has a component per unique stream, prefer one\n`useSSEStream` per component. Reach for `useSSEStreams` when one component\nmust own display an aggregation across multiple streams.\n\n### `useWebSocketStream(options)`\n\nSame result shape as `useSSEStream`, but uses WebSocket transport.\n\nNotable differences:\n\n- accepts `http(s)://` or `ws(s)://` URIs\n- converts `http(s)` to `ws(s)` automatically\n- retries closed sockets with doubling backoff\n\n### `useWebSocketStreams(configs, options?)`\n\n`useSSEStreams` over WebSockets: same arguments, reconciliation, and\n`combine` semantics, with each stream keeping the singular WS hook's\nbehavior above.\n\n### `useStream(options)`\n\nTransport-agnostic React hook used by both transport-specific hooks.\n\n### `useStreams(configs, options?)`\n\nTransport-agnostic plural hook behind `useSSEStreams`. Each config may set\nits own `transport`, so one call can mix SSE and WebSocket streams.\n\n### `useStreamState(options?, client?)`\n\nObserves cache-wide stream state.\n\nFilters support:\n\n- `streamKey` prefix matching\n- `exact: true` for whole-key matching\n- `status`\n- `transport`\n- `predicate(stream)` for custom filtering\n\nUse `select(stream)` to project each matching stream into a smaller result.\n\n### `useStreamCount(filters?, client?)`\n\nCounts streams matching a filter set. It re-renders when membership changes.\n\n### `defaultDecodeFn(raw)`\n\nParses each raw message as JSON and treats the result as stream data.\n\n### `createTransport(kind, uri, handlers)`\n\nCreates an `EventSourceTransport` or `WebSocketTransport` instance.\n\n### `toWebSocketUri(uri)`\n\nConverts `http://` to `ws://` and `https://` to `wss://`. Existing `ws://`\nand `wss://` URIs are returned unchanged.\n\n### `STREAM_STATUS`\n\nStatus constants exported by the package:\n\n```ts\nconst STREAM_STATUS = {\n  CONNECTING: 'connecting',\n  CONNECTED: 'connected',\n  ERROR: 'error',\n  DISCONNECTED: 'disconnected',\n} as const;\n```\n\n## Examples\n\n### Setup (React)\n\n```tsx\nimport { StreamClient } from '@accelint/stream';\nimport { StreamClientProvider } from '@accelint/stream/react';\n\nconst streamClient = new StreamClient();\n\nfunction App({ children }) {\n  return (\n    <StreamClientProvider client={streamClient}>\n      {children}\n    </StreamClientProvider>\n  );\n}\n```\n\n### Shared SSE connection in multiple components\n\n```tsx\nimport { useSSEStream } from '@accelint/stream/react';\n\nfunction ComponentA() {\n  const { data, status } = useSSEStream({\n    streamKey: ['health', apiUri],\n    uri: `${apiUri}/stream/health`,\n  });\n\n  return <div>Status: {status}</div>;\n}\n\nfunction ComponentB() {\n  const { data } = useSSEStream({\n    streamKey: ['health', apiUri],\n    uri: `${apiUri}/stream/health`,\n  });\n\n  return <div>Data: {JSON.stringify(data)}</div>;\n}\n```\n\n### Common options\n\n- `streamKey` — stream identity. Include all values the `uri` depends on.\n- `uri` — stream endpoint.\n- `decodeFn` — converts raw frames into `data`, `error`, or `ignore`.\n- `enabled` — set `false` to skip connecting.\n- `gcTime` — unobserved linger before removal.\n- `select` — per-observer derived slice of `data`.\n- `messageHistory` — retained message count for `messages`.\n- `onOpen` / `onError` — status callbacks.\n- `onMessage` — called for every message.\n- `client` — optional `StreamClient` override.\n\n### Result shape\n\nThe observer result includes:\n\n- `data`\n- `dataUpdatedAt`\n- `status`\n- `isConnecting`\n- `isConnected`\n- `isError`\n- `isDisconnected`\n- `isEnabled`\n- `messages`\n- `retry()`\n- `pause()`\n- `resume()`\n\n`messages` is empty until `messageHistory` is set.\n\n### Observe many streams at once\n\n```tsx\nconst activationStreams = useStreamState({\n  filters: { streamKey: ['activations'] },\n  select: (stream) => ({\n    id: (stream.streamKey[1] as { id: string }).id,\n    status: stream.state.status,\n  }),\n});\n```\n\n`useStreamCount(filters?)` returns the number of matching streams.\n\n```tsx\nconst erroredCount = useStreamCount({ status: 'error' });\n```\n\n### One component over N streams (dynamic N)\n\n`useStreamState` observes cache-wide state, but does not subscribe to the\nstreams (nothing connects) and offers no per-stream `select`/`messages`.\nWhen one component must own N live streams and N changes at runtime — a\nmerged feed across datasets — use `useSSEStreams` (the `useQueries`\nanalog, backed by `StreamsObserver`, the `QueriesObserver` analog):\n\n```tsx\nconst feed = useSSEStreams(\n  datasets.map((dataset) => ({\n    streamKey: ['activations', dataset.id],\n    uri: `${baseUri}/datasets/${dataset.id}/stream`,\n    messageHistory: 50,\n  })),\n  {\n    // runs inside the snapshot; reference-stable while inputs are unchanged\n    combine: (results) => mergeNewestFirst(results),\n  },\n);\n```\n\n### Imperative access without React\n\n```ts\nimport { StreamClient } from '@accelint/stream';\n\nconst client = new StreamClient();\nclient.getStreamState(['health', uri]);\nclient.getStreams({ status: 'error' });\nclient.getStreamCount({ transport: 'websocket' });\nclient.clear();\n```\n\n`StreamCache.subscribe()` emits cache lifecycle events.\n\n### Message history\n\nSet `messageHistory` to retain the last N raw messages. Read retained entries\nthrough `messages` on the observer result or `stream.getMessages()` on the\nstream instance.\n\n### WebSocket transport and URI conversion\n\n```tsx\nimport { useWebSocketStream } from '@accelint/stream/react';\n\nfunction StatsSocket({ cortexUri }: { cortexUri: string }) {\n  const { data, isConnected } = useWebSocketStream<{ tick: number }>({\n    streamKey: ['cortex-stats-ws', cortexUri],\n    uri: `${cortexUri}/ws/health`,\n  });\n\n  return <div>{isConnected ? data?.tick : 'connecting'}</div>;\n}\n```\n\n### Select a stable slice per observer\n\n```tsx\ntype Frame = {\n  cpu: { load: number };\n  memory: { used: number };\n};\n\nconst { data: cpu } = useSSEStream<Frame, Frame['cpu']>({\n  streamKey: ['stats', apiUri],\n  uri: `${apiUri}/stream/stats`,\n  select: (frame) => frame.cpu,\n});\n```\n\nIf the full frame changes but the selected slice stays deep-equal, the hook\ncan keep the same `data` reference.\n\n### Custom frame decoding\n\n```tsx\nimport type { DecodeFn, StreamFrame } from '@accelint/stream';\n\nconst decodeFn: DecodeFn<{ value: number }> = (raw): StreamFrame<{ value: number }> => {\n  const frame = JSON.parse(raw) as\n    | { type: 'data'; value: number }\n    | { type: 'heartbeat' }\n    | { type: 'error'; message: string };\n\n  if (frame.type === 'heartbeat') {\n    return { kind: 'ignore' };\n  }\n\n  if (frame.type === 'error') {\n    return { kind: 'error', error: frame.message };\n  }\n\n  return { kind: 'data', data: { value: frame.value } };\n};\n```\n\n### Filter cache state by named key segments\n\n```tsx\nconst activationStreams = useStreamState({\n  filters: {\n    streamKey: ['cortex', 'activations', { datasetId: 'dataset-b' }],\n  },\n  select: (stream) => ({\n    datasetId: (stream.streamKey[2] as { datasetId: string }).datasetId,\n    status: stream.state.status,\n  }),\n});\n```\n\nObject segments inside `streamKey` use deep partial matching.\n\n## Further Reading\n\n- [`src/stream-cache.ts`](./src/stream-cache.ts) - cache creation and key conflict behavior\n- [`src/stream-observer.ts`](./src/stream-observer.ts) - observer result semantics and callbacks\n- [`src/transport.ts`](./src/transport.ts) - SSE and WebSocket transport behavior\n- [`src/react/use-stream-state.ts`](./src/react/use-stream-state.ts) - cache-wide React observation\n\n## DevTools\n\n`@accelint/stream-devtools` adds a Streams tab to TanStack Devtools showing\nevery stream's status, observer count, message log, and lifecycle timeline,\nwith Reconnect / Close / Clear All / Simulate-Error / Inject-Message\nactions.\n\n## License\n\nApache-2.0 - see [LICENSE](../../LICENSE) for details.\n\n## Contributing\n\nContributions are welcome. Read [../../CONTRIBUTING.md](../../CONTRIBUTING.md)\nbefore opening a pull request.\n\n```bash\npnpm test --dir=src\npnpm build\n```\n","readmeFilename":"README.md"}