@pretable/stream-adapter 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Brian Love
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # @pretable/stream-adapter
2
+
3
+ RAF-batched streaming integration for [pretable](https://pretable.dev/). Bridges async streams (HTTP SSE, WebSocket, partial-JSON from LLMs) into the grid with predictable per-frame batching.
4
+
5
+ ## When to reach for this
6
+
7
+ Use `@pretable/stream-adapter` when you need to drive a grid from a live data stream and want one DOM mutation per animation frame regardless of incoming stream rate. The package ships:
8
+
9
+ - A **batcher** ([`createBatcher`](#createbatcher)) that coalesces `add` / `update` / `remove` into one `applyTransaction` per RAF tick.
10
+ - Two **connect** functions that wire an `AsyncIterable` source through the batcher into a grid.
11
+ - Two **parse** functions that turn raw UTF-8 string streams (e.g., `fetch().body`) into typed row iterables.
12
+
13
+ If you only need one-shot row mounting, use `<Pretable>` from `@pretable/react` instead — this package is for live streams.
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ npm install @pretable/stream-adapter
19
+ # or pnpm add @pretable/stream-adapter, yarn add @pretable/stream-adapter
20
+ ```
21
+
22
+ ## Minimal example — element stream
23
+
24
+ ```ts
25
+ import { connectElementStream, parseElementStream } from "@pretable/stream-adapter";
26
+ import { createGrid } from "@pretable/core";
27
+
28
+ const grid = createGrid({ columns: [...], rows: [] });
29
+
30
+ const response = await fetch("/api/rows");
31
+ const stringStream = response.body!.pipeThrough(new TextDecoderStream());
32
+ const rowStream = parseElementStream<MyRow>(stringStream);
33
+
34
+ const connection = connectElementStream(grid, rowStream);
35
+ await connection.done; // resolves when the server closes the response
36
+ ```
37
+
38
+ ## Minimal example — partial-update stream (e.g., LLM)
39
+
40
+ When an LLM is streaming partial JSON, every chunk is an incomplete row. `connectPartialStream` upserts by row id so each chunk visibly fills out the corresponding row.
41
+
42
+ ```ts
43
+ import {
44
+ connectPartialStream,
45
+ parsePartialStream,
46
+ } from "@pretable/stream-adapter";
47
+
48
+ const partialStream = parsePartialStream<MyRow>(stringStream);
49
+ const connection = connectPartialStream(grid, partialStream, { rowId: "id" });
50
+ ```
51
+
52
+ ## API
53
+
54
+ See **[`stream-adapter.api.md`](./stream-adapter.api.md)** for the full generated public-API report.
55
+
56
+ ### `createBatcher`
57
+
58
+ Returns a `TransactionBatcher` that buffers add / update / remove calls and applies them once per `requestAnimationFrame`. Use directly when you have a custom stream source that doesn't fit the `connect*Stream` shape.
59
+
60
+ ### `connectElementStream` / `parseElementStream`
61
+
62
+ `connectElementStream(grid, stream)` drives a grid from an `AsyncIterable<TRow>`. `parseElementStream(stream)` turns a string stream into an `AsyncIterable<TRow>` by parsing top-level array elements.
63
+
64
+ ### `connectPartialStream` / `parsePartialStream`
65
+
66
+ Same shape, but for incremental field updates: `parsePartialStream` emits `Partial<TRow>` values as the streaming parser fills each element; `connectPartialStream` upserts by `options.rowId`.
67
+
68
+ ### Types
69
+
70
+ `GridLike<TRow>` is the structural grid contract — any object with `applyTransaction({ add?, update?, remove? })` works, including a custom adapter. `TransactionBatcher<TRow>` and `StreamConnection` are the handle types returned by the constructors above. `PartialStreamOptions` is the options bag for `connectPartialStream`.
71
+
72
+ ## License
73
+
74
+ MIT — see [LICENSE](../../LICENSE).
package/dist/index.cjs ADDED
@@ -0,0 +1,230 @@
1
+ 'use strict';
2
+
3
+ var jsonStream = require('@cacheplane/json-stream');
4
+
5
+ // src/create-batcher.ts
6
+ function createBatcher(grid) {
7
+ let addBuffer = [];
8
+ let updateBuffer = [];
9
+ let removeBuffer = [];
10
+ let rafId = null;
11
+ let disposed = false;
12
+ function scheduleFlush() {
13
+ if (rafId !== null || disposed) return;
14
+ rafId = requestAnimationFrame(() => {
15
+ rafId = null;
16
+ applyBuffered();
17
+ });
18
+ }
19
+ function applyBuffered() {
20
+ if (addBuffer.length === 0 && updateBuffer.length === 0 && removeBuffer.length === 0) {
21
+ return;
22
+ }
23
+ const tx = {};
24
+ if (addBuffer.length > 0) {
25
+ tx.add = addBuffer;
26
+ addBuffer = [];
27
+ }
28
+ if (updateBuffer.length > 0) {
29
+ tx.update = updateBuffer;
30
+ updateBuffer = [];
31
+ }
32
+ if (removeBuffer.length > 0) {
33
+ tx.remove = removeBuffer;
34
+ removeBuffer = [];
35
+ }
36
+ grid.applyTransaction(tx);
37
+ }
38
+ return {
39
+ add(rows) {
40
+ if (disposed) return;
41
+ addBuffer.push(...rows);
42
+ scheduleFlush();
43
+ },
44
+ update(patches) {
45
+ if (disposed) return;
46
+ updateBuffer.push(...patches);
47
+ scheduleFlush();
48
+ },
49
+ remove(ids) {
50
+ if (disposed) return;
51
+ removeBuffer.push(...ids);
52
+ scheduleFlush();
53
+ },
54
+ flush() {
55
+ if (disposed) return;
56
+ if (rafId !== null) {
57
+ cancelAnimationFrame(rafId);
58
+ rafId = null;
59
+ }
60
+ applyBuffered();
61
+ },
62
+ dispose() {
63
+ if (disposed) return;
64
+ disposed = true;
65
+ if (rafId !== null) {
66
+ cancelAnimationFrame(rafId);
67
+ rafId = null;
68
+ }
69
+ addBuffer = [];
70
+ updateBuffer = [];
71
+ removeBuffer = [];
72
+ }
73
+ };
74
+ }
75
+
76
+ // src/connect-element-stream.ts
77
+ function connectElementStream(grid, stream) {
78
+ const batcher = createBatcher(grid);
79
+ let disposed = false;
80
+ let resolveDone;
81
+ let rejectDone;
82
+ const done = new Promise((resolve, reject) => {
83
+ resolveDone = resolve;
84
+ rejectDone = reject;
85
+ });
86
+ done.catch(() => void 0);
87
+ (async () => {
88
+ try {
89
+ for await (const element of stream) {
90
+ if (disposed) break;
91
+ batcher.add([element]);
92
+ }
93
+ batcher.flush();
94
+ batcher.dispose();
95
+ resolveDone();
96
+ } catch (err) {
97
+ batcher.flush();
98
+ batcher.dispose();
99
+ rejectDone(err);
100
+ }
101
+ })();
102
+ return {
103
+ done,
104
+ dispose() {
105
+ if (disposed) return;
106
+ disposed = true;
107
+ batcher.flush();
108
+ batcher.dispose();
109
+ resolveDone();
110
+ }
111
+ };
112
+ }
113
+
114
+ // src/connect-partial-stream.ts
115
+ function connectPartialStream(grid, stream, options) {
116
+ const batcher = createBatcher(grid);
117
+ let disposed = false;
118
+ let resolveDone;
119
+ let rejectDone;
120
+ const done = new Promise((resolve, reject) => {
121
+ resolveDone = resolve;
122
+ rejectDone = reject;
123
+ });
124
+ done.catch(() => void 0);
125
+ (async () => {
126
+ try {
127
+ for await (const partial of stream) {
128
+ if (disposed) break;
129
+ batcher.update([{ ...partial, id: options.rowId }]);
130
+ }
131
+ batcher.flush();
132
+ batcher.dispose();
133
+ resolveDone();
134
+ } catch (err) {
135
+ batcher.flush();
136
+ batcher.dispose();
137
+ rejectDone(err);
138
+ }
139
+ })();
140
+ return {
141
+ done,
142
+ dispose() {
143
+ if (disposed) return;
144
+ disposed = true;
145
+ batcher.flush();
146
+ batcher.dispose();
147
+ resolveDone();
148
+ }
149
+ };
150
+ }
151
+ async function* parseElementStream(stream) {
152
+ let state = jsonStream.create();
153
+ let yieldedCount = 0;
154
+ for await (const chunk of stream) {
155
+ state = jsonStream.push(state, chunk);
156
+ if (state.error) {
157
+ throw new Error(state.error.message);
158
+ }
159
+ if (state.rootId !== null) {
160
+ const root = state.nodes[state.rootId];
161
+ if (!jsonStream.isArrayNode(root)) {
162
+ throw new Error(
163
+ `parseElementStream expects root to be an array, got "${root.kind}"`
164
+ );
165
+ }
166
+ while (yieldedCount < root.children.length) {
167
+ const childNode = state.nodes[root.children[yieldedCount]];
168
+ if (!jsonStream.isComplete(childNode)) break;
169
+ if (childNode.value !== void 0) {
170
+ yield childNode.value;
171
+ }
172
+ yieldedCount++;
173
+ }
174
+ }
175
+ }
176
+ state = jsonStream.finish(state);
177
+ if (state.error) {
178
+ throw new Error(state.error.message);
179
+ }
180
+ if (state.rootId !== null) {
181
+ const root = state.nodes[state.rootId];
182
+ if (jsonStream.isArrayNode(root)) {
183
+ while (yieldedCount < root.children.length) {
184
+ const childNode = state.nodes[root.children[yieldedCount]];
185
+ if (jsonStream.isComplete(childNode) && childNode.value !== void 0) {
186
+ yield childNode.value;
187
+ }
188
+ yieldedCount++;
189
+ }
190
+ }
191
+ }
192
+ }
193
+ async function* parsePartialStream(stream) {
194
+ let state = jsonStream.create();
195
+ let lastValue;
196
+ for await (const chunk of stream) {
197
+ state = jsonStream.push(state, chunk);
198
+ if (state.error) {
199
+ throw new Error(state.error.message);
200
+ }
201
+ if (state.rootId !== null) {
202
+ const root = state.nodes[state.rootId];
203
+ if (!jsonStream.isObjectNode(root)) {
204
+ throw new Error(
205
+ `parsePartialStream expects root to be an object, got "${root.kind}"`
206
+ );
207
+ }
208
+ if (root.value !== void 0 && root.value !== lastValue && Object.keys(root.value).length > 0) {
209
+ lastValue = root.value;
210
+ yield root.value;
211
+ }
212
+ }
213
+ }
214
+ state = jsonStream.finish(state);
215
+ if (state.error) {
216
+ throw new Error(state.error.message);
217
+ }
218
+ if (state.rootId !== null) {
219
+ const root = state.nodes[state.rootId];
220
+ if (jsonStream.isObjectNode(root) && root.value !== void 0 && root.value !== lastValue && Object.keys(root.value).length > 0) {
221
+ yield root.value;
222
+ }
223
+ }
224
+ }
225
+
226
+ exports.connectElementStream = connectElementStream;
227
+ exports.connectPartialStream = connectPartialStream;
228
+ exports.createBatcher = createBatcher;
229
+ exports.parseElementStream = parseElementStream;
230
+ exports.parsePartialStream = parsePartialStream;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Structural type for any grid that supports `applyTransaction`. Avoids
3
+ * hard coupling to `@pretable-internal/grid-core` so consumers can wire
4
+ * up streaming against a custom grid implementation that conforms to the
5
+ * same shape.
6
+ *
7
+ * @public
8
+ */
9
+ interface GridLike<TRow extends Record<string, unknown>> {
10
+ applyTransaction(transaction: {
11
+ add?: TRow[];
12
+ update?: Partial<TRow>[];
13
+ remove?: string[];
14
+ }): void;
15
+ }
16
+ /**
17
+ * RAF-batched mutator returned by {@link createBatcher}. Buffer
18
+ * `add` / `update` / `remove` calls; the batcher coalesces them into a
19
+ * single `applyTransaction` per animation frame. `flush()` forces an
20
+ * immediate apply; `dispose()` cancels any pending RAF and stops
21
+ * accepting new calls.
22
+ *
23
+ * @public
24
+ */
25
+ interface TransactionBatcher<TRow extends Record<string, unknown>> {
26
+ add(rows: TRow[]): void;
27
+ update(patches: Partial<TRow>[]): void;
28
+ remove(ids: string[]): void;
29
+ flush(): void;
30
+ dispose(): void;
31
+ }
32
+ /**
33
+ * Handle returned by the `connect*Stream` functions. `done` resolves
34
+ * when the source stream ends (or rejects on stream error); `dispose()`
35
+ * cancels the active read loop and resolves `done` immediately.
36
+ *
37
+ * @public
38
+ */
39
+ interface StreamConnection {
40
+ done: Promise<void>;
41
+ dispose(): void;
42
+ }
43
+
44
+ /**
45
+ * Create a `requestAnimationFrame`-batched mutator that coalesces
46
+ * `add` / `update` / `remove` calls into a single `applyTransaction` per
47
+ * frame. Use this when driving a grid from a stream that emits faster
48
+ * than the browser can render — batching keeps DOM mutations to one per
49
+ * frame regardless of stream rate.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * const batcher = createBatcher(grid);
54
+ * batcher.add([{ id: "1", name: "Ada" }]);
55
+ * batcher.update([{ id: "1", age: 36 }]);
56
+ * batcher.flush(); // optional — RAF will flush automatically
57
+ * ```
58
+ *
59
+ * @public
60
+ */
61
+ declare function createBatcher<TRow extends Record<string, unknown>>(grid: GridLike<TRow>): TransactionBatcher<TRow>;
62
+
63
+ /**
64
+ * Drive a grid from an `AsyncIterable<TRow>`. Each yielded row is added
65
+ * via a {@link createBatcher | RAF batcher}; the returned
66
+ * {@link StreamConnection} resolves `done` when the stream ends and
67
+ * supports `dispose()` for early cancellation.
68
+ *
69
+ * Pair with {@link parseElementStream} to turn a raw UTF-8 string stream
70
+ * (e.g., from `fetch().body`) into a row stream end-to-end.
71
+ *
72
+ * @public
73
+ */
74
+ declare function connectElementStream<TRow extends Record<string, unknown>>(grid: GridLike<TRow>, stream: AsyncIterable<TRow>): StreamConnection;
75
+
76
+ /**
77
+ * Options for {@link connectPartialStream}. `rowId` names the field on
78
+ * each partial row used to identify it for upsert (the field must be
79
+ * present in every emitted partial — partials missing the rowId are
80
+ * ignored).
81
+ *
82
+ * @public
83
+ */
84
+ interface PartialStreamOptions {
85
+ rowId: string;
86
+ }
87
+ /**
88
+ * Drive a grid from an `AsyncIterable<Partial<TRow>>`. Each yielded
89
+ * partial is upserted by `options.rowId` — new rowIds are added,
90
+ * existing rowIds are merged via `applyTransaction.update`. Useful when
91
+ * a stream emits incremental field updates (e.g., partial JSON parses)
92
+ * rather than complete rows.
93
+ *
94
+ * Pair with {@link parsePartialStream} for end-to-end partial-update
95
+ * streaming over UTF-8 strings.
96
+ *
97
+ * @public
98
+ */
99
+ declare function connectPartialStream<TRow extends Record<string, unknown> & {
100
+ id: string;
101
+ }>(grid: GridLike<TRow>, stream: AsyncIterable<Partial<TRow>>, options: PartialStreamOptions): StreamConnection;
102
+
103
+ /**
104
+ * Parse a UTF-8 string stream into an `AsyncIterable<TRow>`. Built on
105
+ * `@cacheplane/json-stream`'s incremental JSON parser; emits each
106
+ * complete top-level array element as a typed row.
107
+ *
108
+ * Pair with {@link connectElementStream} for end-to-end element-stream
109
+ * → grid wiring.
110
+ *
111
+ * @public
112
+ */
113
+ declare function parseElementStream<TRow>(stream: AsyncIterable<string>): AsyncIterable<TRow>;
114
+
115
+ /**
116
+ * Parse a UTF-8 string stream into an `AsyncIterable<Partial<TRow>>`.
117
+ * Emits incremental partial rows as a streaming JSON parse fills out
118
+ * each top-level array element — useful when an LLM is streaming
119
+ * partial JSON and you want field-by-field updates instead of waiting
120
+ * for each row to complete.
121
+ *
122
+ * Pair with {@link connectPartialStream} for end-to-end partial-stream
123
+ * → grid wiring.
124
+ *
125
+ * @public
126
+ */
127
+ declare function parsePartialStream<TRow>(stream: AsyncIterable<string>): AsyncIterable<Partial<TRow>>;
128
+
129
+ export { type GridLike, type PartialStreamOptions, type StreamConnection, type TransactionBatcher, connectElementStream, connectPartialStream, createBatcher, parseElementStream, parsePartialStream };
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Structural type for any grid that supports `applyTransaction`. Avoids
3
+ * hard coupling to `@pretable-internal/grid-core` so consumers can wire
4
+ * up streaming against a custom grid implementation that conforms to the
5
+ * same shape.
6
+ *
7
+ * @public
8
+ */
9
+ interface GridLike<TRow extends Record<string, unknown>> {
10
+ applyTransaction(transaction: {
11
+ add?: TRow[];
12
+ update?: Partial<TRow>[];
13
+ remove?: string[];
14
+ }): void;
15
+ }
16
+ /**
17
+ * RAF-batched mutator returned by {@link createBatcher}. Buffer
18
+ * `add` / `update` / `remove` calls; the batcher coalesces them into a
19
+ * single `applyTransaction` per animation frame. `flush()` forces an
20
+ * immediate apply; `dispose()` cancels any pending RAF and stops
21
+ * accepting new calls.
22
+ *
23
+ * @public
24
+ */
25
+ interface TransactionBatcher<TRow extends Record<string, unknown>> {
26
+ add(rows: TRow[]): void;
27
+ update(patches: Partial<TRow>[]): void;
28
+ remove(ids: string[]): void;
29
+ flush(): void;
30
+ dispose(): void;
31
+ }
32
+ /**
33
+ * Handle returned by the `connect*Stream` functions. `done` resolves
34
+ * when the source stream ends (or rejects on stream error); `dispose()`
35
+ * cancels the active read loop and resolves `done` immediately.
36
+ *
37
+ * @public
38
+ */
39
+ interface StreamConnection {
40
+ done: Promise<void>;
41
+ dispose(): void;
42
+ }
43
+
44
+ /**
45
+ * Create a `requestAnimationFrame`-batched mutator that coalesces
46
+ * `add` / `update` / `remove` calls into a single `applyTransaction` per
47
+ * frame. Use this when driving a grid from a stream that emits faster
48
+ * than the browser can render — batching keeps DOM mutations to one per
49
+ * frame regardless of stream rate.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * const batcher = createBatcher(grid);
54
+ * batcher.add([{ id: "1", name: "Ada" }]);
55
+ * batcher.update([{ id: "1", age: 36 }]);
56
+ * batcher.flush(); // optional — RAF will flush automatically
57
+ * ```
58
+ *
59
+ * @public
60
+ */
61
+ declare function createBatcher<TRow extends Record<string, unknown>>(grid: GridLike<TRow>): TransactionBatcher<TRow>;
62
+
63
+ /**
64
+ * Drive a grid from an `AsyncIterable<TRow>`. Each yielded row is added
65
+ * via a {@link createBatcher | RAF batcher}; the returned
66
+ * {@link StreamConnection} resolves `done` when the stream ends and
67
+ * supports `dispose()` for early cancellation.
68
+ *
69
+ * Pair with {@link parseElementStream} to turn a raw UTF-8 string stream
70
+ * (e.g., from `fetch().body`) into a row stream end-to-end.
71
+ *
72
+ * @public
73
+ */
74
+ declare function connectElementStream<TRow extends Record<string, unknown>>(grid: GridLike<TRow>, stream: AsyncIterable<TRow>): StreamConnection;
75
+
76
+ /**
77
+ * Options for {@link connectPartialStream}. `rowId` names the field on
78
+ * each partial row used to identify it for upsert (the field must be
79
+ * present in every emitted partial — partials missing the rowId are
80
+ * ignored).
81
+ *
82
+ * @public
83
+ */
84
+ interface PartialStreamOptions {
85
+ rowId: string;
86
+ }
87
+ /**
88
+ * Drive a grid from an `AsyncIterable<Partial<TRow>>`. Each yielded
89
+ * partial is upserted by `options.rowId` — new rowIds are added,
90
+ * existing rowIds are merged via `applyTransaction.update`. Useful when
91
+ * a stream emits incremental field updates (e.g., partial JSON parses)
92
+ * rather than complete rows.
93
+ *
94
+ * Pair with {@link parsePartialStream} for end-to-end partial-update
95
+ * streaming over UTF-8 strings.
96
+ *
97
+ * @public
98
+ */
99
+ declare function connectPartialStream<TRow extends Record<string, unknown> & {
100
+ id: string;
101
+ }>(grid: GridLike<TRow>, stream: AsyncIterable<Partial<TRow>>, options: PartialStreamOptions): StreamConnection;
102
+
103
+ /**
104
+ * Parse a UTF-8 string stream into an `AsyncIterable<TRow>`. Built on
105
+ * `@cacheplane/json-stream`'s incremental JSON parser; emits each
106
+ * complete top-level array element as a typed row.
107
+ *
108
+ * Pair with {@link connectElementStream} for end-to-end element-stream
109
+ * → grid wiring.
110
+ *
111
+ * @public
112
+ */
113
+ declare function parseElementStream<TRow>(stream: AsyncIterable<string>): AsyncIterable<TRow>;
114
+
115
+ /**
116
+ * Parse a UTF-8 string stream into an `AsyncIterable<Partial<TRow>>`.
117
+ * Emits incremental partial rows as a streaming JSON parse fills out
118
+ * each top-level array element — useful when an LLM is streaming
119
+ * partial JSON and you want field-by-field updates instead of waiting
120
+ * for each row to complete.
121
+ *
122
+ * Pair with {@link connectPartialStream} for end-to-end partial-stream
123
+ * → grid wiring.
124
+ *
125
+ * @public
126
+ */
127
+ declare function parsePartialStream<TRow>(stream: AsyncIterable<string>): AsyncIterable<Partial<TRow>>;
128
+
129
+ export { type GridLike, type PartialStreamOptions, type StreamConnection, type TransactionBatcher, connectElementStream, connectPartialStream, createBatcher, parseElementStream, parsePartialStream };
package/dist/index.mjs ADDED
@@ -0,0 +1,224 @@
1
+ import { create, push, isArrayNode, isComplete, finish, isObjectNode } from '@cacheplane/json-stream';
2
+
3
+ // src/create-batcher.ts
4
+ function createBatcher(grid) {
5
+ let addBuffer = [];
6
+ let updateBuffer = [];
7
+ let removeBuffer = [];
8
+ let rafId = null;
9
+ let disposed = false;
10
+ function scheduleFlush() {
11
+ if (rafId !== null || disposed) return;
12
+ rafId = requestAnimationFrame(() => {
13
+ rafId = null;
14
+ applyBuffered();
15
+ });
16
+ }
17
+ function applyBuffered() {
18
+ if (addBuffer.length === 0 && updateBuffer.length === 0 && removeBuffer.length === 0) {
19
+ return;
20
+ }
21
+ const tx = {};
22
+ if (addBuffer.length > 0) {
23
+ tx.add = addBuffer;
24
+ addBuffer = [];
25
+ }
26
+ if (updateBuffer.length > 0) {
27
+ tx.update = updateBuffer;
28
+ updateBuffer = [];
29
+ }
30
+ if (removeBuffer.length > 0) {
31
+ tx.remove = removeBuffer;
32
+ removeBuffer = [];
33
+ }
34
+ grid.applyTransaction(tx);
35
+ }
36
+ return {
37
+ add(rows) {
38
+ if (disposed) return;
39
+ addBuffer.push(...rows);
40
+ scheduleFlush();
41
+ },
42
+ update(patches) {
43
+ if (disposed) return;
44
+ updateBuffer.push(...patches);
45
+ scheduleFlush();
46
+ },
47
+ remove(ids) {
48
+ if (disposed) return;
49
+ removeBuffer.push(...ids);
50
+ scheduleFlush();
51
+ },
52
+ flush() {
53
+ if (disposed) return;
54
+ if (rafId !== null) {
55
+ cancelAnimationFrame(rafId);
56
+ rafId = null;
57
+ }
58
+ applyBuffered();
59
+ },
60
+ dispose() {
61
+ if (disposed) return;
62
+ disposed = true;
63
+ if (rafId !== null) {
64
+ cancelAnimationFrame(rafId);
65
+ rafId = null;
66
+ }
67
+ addBuffer = [];
68
+ updateBuffer = [];
69
+ removeBuffer = [];
70
+ }
71
+ };
72
+ }
73
+
74
+ // src/connect-element-stream.ts
75
+ function connectElementStream(grid, stream) {
76
+ const batcher = createBatcher(grid);
77
+ let disposed = false;
78
+ let resolveDone;
79
+ let rejectDone;
80
+ const done = new Promise((resolve, reject) => {
81
+ resolveDone = resolve;
82
+ rejectDone = reject;
83
+ });
84
+ done.catch(() => void 0);
85
+ (async () => {
86
+ try {
87
+ for await (const element of stream) {
88
+ if (disposed) break;
89
+ batcher.add([element]);
90
+ }
91
+ batcher.flush();
92
+ batcher.dispose();
93
+ resolveDone();
94
+ } catch (err) {
95
+ batcher.flush();
96
+ batcher.dispose();
97
+ rejectDone(err);
98
+ }
99
+ })();
100
+ return {
101
+ done,
102
+ dispose() {
103
+ if (disposed) return;
104
+ disposed = true;
105
+ batcher.flush();
106
+ batcher.dispose();
107
+ resolveDone();
108
+ }
109
+ };
110
+ }
111
+
112
+ // src/connect-partial-stream.ts
113
+ function connectPartialStream(grid, stream, options) {
114
+ const batcher = createBatcher(grid);
115
+ let disposed = false;
116
+ let resolveDone;
117
+ let rejectDone;
118
+ const done = new Promise((resolve, reject) => {
119
+ resolveDone = resolve;
120
+ rejectDone = reject;
121
+ });
122
+ done.catch(() => void 0);
123
+ (async () => {
124
+ try {
125
+ for await (const partial of stream) {
126
+ if (disposed) break;
127
+ batcher.update([{ ...partial, id: options.rowId }]);
128
+ }
129
+ batcher.flush();
130
+ batcher.dispose();
131
+ resolveDone();
132
+ } catch (err) {
133
+ batcher.flush();
134
+ batcher.dispose();
135
+ rejectDone(err);
136
+ }
137
+ })();
138
+ return {
139
+ done,
140
+ dispose() {
141
+ if (disposed) return;
142
+ disposed = true;
143
+ batcher.flush();
144
+ batcher.dispose();
145
+ resolveDone();
146
+ }
147
+ };
148
+ }
149
+ async function* parseElementStream(stream) {
150
+ let state = create();
151
+ let yieldedCount = 0;
152
+ for await (const chunk of stream) {
153
+ state = push(state, chunk);
154
+ if (state.error) {
155
+ throw new Error(state.error.message);
156
+ }
157
+ if (state.rootId !== null) {
158
+ const root = state.nodes[state.rootId];
159
+ if (!isArrayNode(root)) {
160
+ throw new Error(
161
+ `parseElementStream expects root to be an array, got "${root.kind}"`
162
+ );
163
+ }
164
+ while (yieldedCount < root.children.length) {
165
+ const childNode = state.nodes[root.children[yieldedCount]];
166
+ if (!isComplete(childNode)) break;
167
+ if (childNode.value !== void 0) {
168
+ yield childNode.value;
169
+ }
170
+ yieldedCount++;
171
+ }
172
+ }
173
+ }
174
+ state = finish(state);
175
+ if (state.error) {
176
+ throw new Error(state.error.message);
177
+ }
178
+ if (state.rootId !== null) {
179
+ const root = state.nodes[state.rootId];
180
+ if (isArrayNode(root)) {
181
+ while (yieldedCount < root.children.length) {
182
+ const childNode = state.nodes[root.children[yieldedCount]];
183
+ if (isComplete(childNode) && childNode.value !== void 0) {
184
+ yield childNode.value;
185
+ }
186
+ yieldedCount++;
187
+ }
188
+ }
189
+ }
190
+ }
191
+ async function* parsePartialStream(stream) {
192
+ let state = create();
193
+ let lastValue;
194
+ for await (const chunk of stream) {
195
+ state = push(state, chunk);
196
+ if (state.error) {
197
+ throw new Error(state.error.message);
198
+ }
199
+ if (state.rootId !== null) {
200
+ const root = state.nodes[state.rootId];
201
+ if (!isObjectNode(root)) {
202
+ throw new Error(
203
+ `parsePartialStream expects root to be an object, got "${root.kind}"`
204
+ );
205
+ }
206
+ if (root.value !== void 0 && root.value !== lastValue && Object.keys(root.value).length > 0) {
207
+ lastValue = root.value;
208
+ yield root.value;
209
+ }
210
+ }
211
+ }
212
+ state = finish(state);
213
+ if (state.error) {
214
+ throw new Error(state.error.message);
215
+ }
216
+ if (state.rootId !== null) {
217
+ const root = state.nodes[state.rootId];
218
+ if (isObjectNode(root) && root.value !== void 0 && root.value !== lastValue && Object.keys(root.value).length > 0) {
219
+ yield root.value;
220
+ }
221
+ }
222
+ }
223
+
224
+ export { connectElementStream, connectPartialStream, createBatcher, parseElementStream, parsePartialStream };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@pretable/stream-adapter",
3
+ "version": "0.0.3",
4
+ "description": "Streaming row adapters for Pretable.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/cacheplane/pretable.git",
9
+ "directory": "packages/stream-adapter"
10
+ },
11
+ "homepage": "https://pretable.ai/docs/streaming",
12
+ "bugs": {
13
+ "url": "https://github.com/cacheplane/pretable/issues"
14
+ },
15
+ "type": "module",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "main": "./dist/index.cjs",
20
+ "module": "./dist/index.mjs",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "import": {
25
+ "types": "./dist/index.d.ts",
26
+ "default": "./dist/index.mjs"
27
+ },
28
+ "require": {
29
+ "types": "./dist/index.d.cts",
30
+ "default": "./dist/index.cjs"
31
+ }
32
+ }
33
+ },
34
+ "dependencies": {
35
+ "@cacheplane/json-stream": "0.0.3"
36
+ },
37
+ "scripts": {
38
+ "api": "api-extractor run --local --config api-extractor.json",
39
+ "api:check": "api-extractor run --config api-extractor.json",
40
+ "build": "pnpm --filter @cacheplane/json-stream build && tsup",
41
+ "lint": "eslint src --ext .ts",
42
+ "lint:packaging": "publint --strict && attw --pack",
43
+ "test": "pnpm --filter @cacheplane/json-stream build && vitest run --passWithNoTests",
44
+ "typecheck": "pnpm --filter @cacheplane/json-stream build && tsc -b --pretty false"
45
+ }
46
+ }