@pretable/stream-adapter 0.4.0 → 0.5.0
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/README.md +47 -29
- package/dist/index.cjs +197 -47
- package/dist/index.d.cts +54 -40
- package/dist/index.d.ts +54 -40
- package/dist/index.mjs +197 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# @pretable/stream-adapter
|
|
2
2
|
|
|
3
|
-
RAF-batched streaming integration for [
|
|
3
|
+
RAF-batched streaming integration for [Pretable](https://pretable.dev/). It bridges async sources such as SSE, WebSockets, and partial JSON into an ID-generic row model with one atomic transaction per animation frame.
|
|
4
4
|
|
|
5
5
|
## When to reach for this
|
|
6
6
|
|
|
7
|
-
Use `@pretable/stream-adapter` when
|
|
7
|
+
Use `@pretable/stream-adapter` when a live source emits faster than the browser should publish row-model revisions. The package ships:
|
|
8
8
|
|
|
9
|
-
- A **batcher**
|
|
10
|
-
- Two **
|
|
11
|
-
- Two **
|
|
9
|
+
- A **batcher** that coalesces `add`, `{ id, changes }` updates, and ID removals into one transaction per RAF tick.
|
|
10
|
+
- Two **connectors** that consume `AsyncIterable` sources and target a structural row model.
|
|
11
|
+
- Two **parsers** that turn raw UTF-8 string streams into typed row iterables.
|
|
12
12
|
|
|
13
|
-
If
|
|
13
|
+
If your data changes only through ordinary React props, pass `rows` to `@pretable/react` instead. This package is for explicit streaming ownership.
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
@@ -19,25 +19,27 @@ npm install @pretable/stream-adapter
|
|
|
19
19
|
# or pnpm add @pretable/stream-adapter, yarn add @pretable/stream-adapter
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## Element streams
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
import { connectElementStream, parseElementStream } from "@pretable/stream-adapter";
|
|
26
|
-
import { createGrid } from "@pretable/core";
|
|
24
|
+
`connectElementStream` treats every complete element as a row to add. Pass a Pretable row model or any object satisfying `RowModelLike<TRow, TRowId>`.
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
```ts
|
|
27
|
+
import {
|
|
28
|
+
connectElementStream,
|
|
29
|
+
parseElementStream,
|
|
30
|
+
} from "@pretable/stream-adapter";
|
|
29
31
|
|
|
30
32
|
const response = await fetch("/api/rows");
|
|
31
33
|
const stringStream = response.body!.pipeThrough(new TextDecoderStream());
|
|
32
|
-
const
|
|
34
|
+
const rows = parseElementStream<MyRow>(stringStream);
|
|
33
35
|
|
|
34
|
-
const connection = connectElementStream(
|
|
35
|
-
await connection.done;
|
|
36
|
+
const connection = connectElementStream(rowModel, rows);
|
|
37
|
+
await connection.done;
|
|
36
38
|
```
|
|
37
39
|
|
|
38
|
-
##
|
|
40
|
+
## Partial streams
|
|
39
41
|
|
|
40
|
-
|
|
42
|
+
`connectPartialStream` sends every partial to the fixed `options.rowId` as `{ id, changes }`. The target row must already exist unless `createRow(partial, id)` is provided. The connector never asserts that a partial is a complete row.
|
|
41
43
|
|
|
42
44
|
```ts
|
|
43
45
|
import {
|
|
@@ -45,29 +47,45 @@ import {
|
|
|
45
47
|
parsePartialStream,
|
|
46
48
|
} from "@pretable/stream-adapter";
|
|
47
49
|
|
|
48
|
-
const
|
|
49
|
-
const connection = connectPartialStream(
|
|
50
|
+
const partials = parsePartialStream<ChatRow>(stringStream);
|
|
51
|
+
const connection = connectPartialStream(rowModel, partials, {
|
|
52
|
+
rowId: "assistant-1",
|
|
53
|
+
onIssue(issue) {
|
|
54
|
+
console.warn(issue.code, issue.rowId);
|
|
55
|
+
},
|
|
56
|
+
createRow(partial, id) {
|
|
57
|
+
return { id, role: "assistant", content: partial.content ?? "" };
|
|
58
|
+
},
|
|
59
|
+
});
|
|
50
60
|
```
|
|
51
61
|
|
|
52
|
-
|
|
62
|
+
When the model returns an `unknown-update-id` issue, `onIssue` receives it. If `createRow` is present, its complete result is added in a separate atomic transaction; otherwise nothing is fabricated.
|
|
53
63
|
|
|
54
|
-
|
|
64
|
+
## Direct batching
|
|
55
65
|
|
|
56
|
-
|
|
66
|
+
Use `createBatcher` when your source does not fit either connector:
|
|
57
67
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
### `connectElementStream` / `parseElementStream`
|
|
68
|
+
```ts
|
|
69
|
+
const batcher = createBatcher(rowModel);
|
|
61
70
|
|
|
62
|
-
|
|
71
|
+
batcher.update([{ id: 42, changes: { status: "ready" } }]);
|
|
72
|
+
batcher.remove([7]);
|
|
73
|
+
// The scheduled RAF flushes automatically; flush() is also available.
|
|
74
|
+
```
|
|
63
75
|
|
|
64
|
-
|
|
76
|
+
The batcher preserves string and number IDs. It detaches the transaction payload before publication, clears the published batch before calling the model, and remains usable if `applyTransaction` throws.
|
|
65
77
|
|
|
66
|
-
|
|
78
|
+
## API
|
|
67
79
|
|
|
68
|
-
|
|
80
|
+
See **[`stream-adapter.api.md`](./stream-adapter.api.md)** for the generated public-API report.
|
|
69
81
|
|
|
70
|
-
|
|
82
|
+
- `createBatcher(rowModel)` returns a `TransactionBatcher<TRow, TRowId>`.
|
|
83
|
+
- `connectElementStream(rowModel, stream)` adds complete streamed rows.
|
|
84
|
+
- `connectPartialStream(rowModel, stream, options)` updates one fixed row ID.
|
|
85
|
+
- `parseElementStream(stream)` parses complete top-level array elements.
|
|
86
|
+
- `parsePartialStream(stream)` parses incremental partial objects.
|
|
87
|
+
- `RowModelLike<TRow, TRowId>` is the dependency-free structural model contract.
|
|
88
|
+
- `PartialStreamOptions<TRow, TRowId>`, `TransactionBatcher<TRow, TRowId>`, and `StreamConnection` describe the connector and lifecycle handles.
|
|
71
89
|
|
|
72
90
|
## License
|
|
73
91
|
|
package/dist/index.cjs
CHANGED
|
@@ -3,39 +3,93 @@
|
|
|
3
3
|
var jsonStream = require('@cacheplane/json-stream');
|
|
4
4
|
|
|
5
5
|
// src/create-batcher.ts
|
|
6
|
-
function createBatcher(
|
|
6
|
+
function createBatcher(rowModel) {
|
|
7
7
|
let addBuffer = [];
|
|
8
8
|
let updateBuffer = [];
|
|
9
9
|
let removeBuffer = [];
|
|
10
10
|
let rafId = null;
|
|
11
11
|
let disposed = false;
|
|
12
|
+
let rejectError;
|
|
13
|
+
const errorListeners = /* @__PURE__ */ new Set();
|
|
14
|
+
let failed = false;
|
|
15
|
+
let failure;
|
|
16
|
+
const error = new Promise((_resolve, reject) => {
|
|
17
|
+
rejectError = reject;
|
|
18
|
+
});
|
|
19
|
+
error.catch(() => void 0);
|
|
20
|
+
function fail(error2) {
|
|
21
|
+
if (disposed) return;
|
|
22
|
+
disposed = true;
|
|
23
|
+
failed = true;
|
|
24
|
+
failure = error2;
|
|
25
|
+
if (rafId !== null) {
|
|
26
|
+
cancelAnimationFrame(rafId);
|
|
27
|
+
rafId = null;
|
|
28
|
+
}
|
|
29
|
+
addBuffer = [];
|
|
30
|
+
updateBuffer = [];
|
|
31
|
+
removeBuffer = [];
|
|
32
|
+
const listeners = [...errorListeners];
|
|
33
|
+
errorListeners.clear();
|
|
34
|
+
for (const listener of listeners) {
|
|
35
|
+
try {
|
|
36
|
+
listener(error2);
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
rejectError(error2);
|
|
41
|
+
}
|
|
12
42
|
function scheduleFlush() {
|
|
13
43
|
if (rafId !== null || disposed) return;
|
|
14
44
|
rafId = requestAnimationFrame(() => {
|
|
15
45
|
rafId = null;
|
|
16
|
-
|
|
46
|
+
if (disposed) return;
|
|
47
|
+
try {
|
|
48
|
+
applyBuffered();
|
|
49
|
+
} catch (error2) {
|
|
50
|
+
fail(error2);
|
|
51
|
+
}
|
|
17
52
|
});
|
|
18
53
|
}
|
|
19
54
|
function applyBuffered() {
|
|
20
55
|
if (addBuffer.length === 0 && updateBuffer.length === 0 && removeBuffer.length === 0) {
|
|
21
56
|
return;
|
|
22
57
|
}
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
58
|
+
const bufferedAdds = addBuffer;
|
|
59
|
+
const bufferedUpdates = updateBuffer;
|
|
60
|
+
const bufferedRemovals = removeBuffer;
|
|
61
|
+
addBuffer = [];
|
|
62
|
+
updateBuffer = [];
|
|
63
|
+
removeBuffer = [];
|
|
64
|
+
const transaction = {};
|
|
65
|
+
if (bufferedAdds.length > 0) {
|
|
66
|
+
transaction.add = bufferedAdds.map((row) => ({ ...row }));
|
|
27
67
|
}
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
68
|
+
if (bufferedUpdates.length > 0) {
|
|
69
|
+
transaction.update = bufferedUpdates.map(({ id, changes }) => ({
|
|
70
|
+
id,
|
|
71
|
+
changes: { ...changes }
|
|
72
|
+
}));
|
|
31
73
|
}
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
removeBuffer = [];
|
|
74
|
+
if (bufferedRemovals.length > 0) {
|
|
75
|
+
transaction.remove = [...bufferedRemovals];
|
|
35
76
|
}
|
|
36
|
-
|
|
77
|
+
rowModel.applyTransaction(transaction);
|
|
37
78
|
}
|
|
38
79
|
return {
|
|
80
|
+
error,
|
|
81
|
+
subscribeError(listener) {
|
|
82
|
+
if (failed) {
|
|
83
|
+
try {
|
|
84
|
+
listener(failure);
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
return () => void 0;
|
|
88
|
+
}
|
|
89
|
+
if (disposed) return () => void 0;
|
|
90
|
+
errorListeners.add(listener);
|
|
91
|
+
return () => errorListeners.delete(listener);
|
|
92
|
+
},
|
|
39
93
|
add(rows) {
|
|
40
94
|
if (disposed) return;
|
|
41
95
|
addBuffer.push(...rows);
|
|
@@ -69,14 +123,17 @@ function createBatcher(grid) {
|
|
|
69
123
|
addBuffer = [];
|
|
70
124
|
updateBuffer = [];
|
|
71
125
|
removeBuffer = [];
|
|
126
|
+
errorListeners.clear();
|
|
72
127
|
}
|
|
73
128
|
};
|
|
74
129
|
}
|
|
75
130
|
|
|
76
131
|
// src/connect-element-stream.ts
|
|
77
|
-
function connectElementStream(
|
|
78
|
-
const
|
|
132
|
+
function connectElementStream(rowModel, stream) {
|
|
133
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
134
|
+
const batcher = createBatcher(rowModel);
|
|
79
135
|
let disposed = false;
|
|
136
|
+
let sourceClosed = false;
|
|
80
137
|
let resolveDone;
|
|
81
138
|
let rejectDone;
|
|
82
139
|
const done = new Promise((resolve, reject) => {
|
|
@@ -84,37 +141,102 @@ function connectElementStream(grid, stream) {
|
|
|
84
141
|
rejectDone = reject;
|
|
85
142
|
});
|
|
86
143
|
done.catch(() => void 0);
|
|
87
|
-
|
|
144
|
+
const closeSource = () => {
|
|
145
|
+
if (sourceClosed) return;
|
|
146
|
+
sourceClosed = true;
|
|
88
147
|
try {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
148
|
+
const closing = iterator.return?.();
|
|
149
|
+
if (closing !== void 0)
|
|
150
|
+
void Promise.resolve(closing).catch(() => void 0);
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
let settled = false;
|
|
155
|
+
const settle = (failure, flush = true, close = failure !== void 0) => {
|
|
156
|
+
if (settled) return;
|
|
157
|
+
settled = true;
|
|
158
|
+
disposed = true;
|
|
159
|
+
if (close) closeSource();
|
|
160
|
+
let finalFailure = failure;
|
|
161
|
+
if (flush) {
|
|
162
|
+
try {
|
|
163
|
+
batcher.flush();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
finalFailure ??= { error };
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
batcher.dispose();
|
|
169
|
+
if (finalFailure === void 0) resolveDone();
|
|
170
|
+
else rejectDone(finalFailure.error);
|
|
171
|
+
};
|
|
172
|
+
batcher.subscribeError((error) => {
|
|
173
|
+
settle({ error }, false, true);
|
|
174
|
+
});
|
|
175
|
+
void (async () => {
|
|
176
|
+
try {
|
|
177
|
+
while (!disposed) {
|
|
178
|
+
const result = await iterator.next();
|
|
179
|
+
if (disposed) return;
|
|
180
|
+
if (result.done) {
|
|
181
|
+
settle();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
batcher.add([result.value]);
|
|
92
185
|
}
|
|
93
|
-
batcher.flush();
|
|
94
|
-
batcher.dispose();
|
|
95
|
-
resolveDone();
|
|
96
186
|
} catch (err) {
|
|
97
|
-
|
|
98
|
-
batcher.dispose();
|
|
99
|
-
rejectDone(err);
|
|
187
|
+
settle({ error: err }, true, true);
|
|
100
188
|
}
|
|
101
|
-
})();
|
|
189
|
+
})().catch((error) => settle({ error }));
|
|
102
190
|
return {
|
|
103
191
|
done,
|
|
104
192
|
dispose() {
|
|
105
193
|
if (disposed) return;
|
|
106
|
-
|
|
107
|
-
batcher.flush();
|
|
108
|
-
batcher.dispose();
|
|
109
|
-
resolveDone();
|
|
194
|
+
settle(void 0, true, true);
|
|
110
195
|
}
|
|
111
196
|
};
|
|
112
197
|
}
|
|
113
198
|
|
|
114
199
|
// src/connect-partial-stream.ts
|
|
115
|
-
function
|
|
116
|
-
|
|
200
|
+
function sameValueZero(left, right) {
|
|
201
|
+
return left === right || typeof left === "number" && typeof right === "number" && Number.isNaN(left) && Number.isNaN(right);
|
|
202
|
+
}
|
|
203
|
+
function connectPartialStream(rowModel, stream, options) {
|
|
204
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
205
|
+
const issueAwareRowModel = {
|
|
206
|
+
applyTransaction(transaction) {
|
|
207
|
+
const result = rowModel.applyTransaction(transaction);
|
|
208
|
+
if (result === void 0 || result.issues === void 0) return result;
|
|
209
|
+
let targetMissing = false;
|
|
210
|
+
for (const issue of result.issues) {
|
|
211
|
+
if (issue.code !== "unknown-update-id" || issue.rowId === void 0 || !sameValueZero(issue.rowId, options.rowId)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
targetMissing = true;
|
|
215
|
+
options.onIssue?.({
|
|
216
|
+
code: "unknown-update-id",
|
|
217
|
+
rowId: options.rowId
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (targetMissing && options.createRow !== void 0) {
|
|
221
|
+
const updates = transaction.update ?? [];
|
|
222
|
+
const combinedChanges = {};
|
|
223
|
+
let hasTargetUpdate = false;
|
|
224
|
+
for (const update of updates) {
|
|
225
|
+
if (!sameValueZero(update.id, options.rowId)) continue;
|
|
226
|
+
Object.assign(combinedChanges, update.changes);
|
|
227
|
+
hasTargetUpdate = true;
|
|
228
|
+
}
|
|
229
|
+
if (hasTargetUpdate) {
|
|
230
|
+
const row = options.createRow(combinedChanges, options.rowId);
|
|
231
|
+
rowModel.applyTransaction({ add: [row] });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const batcher = createBatcher(issueAwareRowModel);
|
|
117
238
|
let disposed = false;
|
|
239
|
+
let sourceClosed = false;
|
|
118
240
|
let resolveDone;
|
|
119
241
|
let rejectDone;
|
|
120
242
|
const done = new Promise((resolve, reject) => {
|
|
@@ -122,29 +244,57 @@ function connectPartialStream(grid, stream, options) {
|
|
|
122
244
|
rejectDone = reject;
|
|
123
245
|
});
|
|
124
246
|
done.catch(() => void 0);
|
|
125
|
-
|
|
247
|
+
const closeSource = () => {
|
|
248
|
+
if (sourceClosed) return;
|
|
249
|
+
sourceClosed = true;
|
|
250
|
+
try {
|
|
251
|
+
const closing = iterator.return?.();
|
|
252
|
+
if (closing !== void 0)
|
|
253
|
+
void Promise.resolve(closing).catch(() => void 0);
|
|
254
|
+
} catch {
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
let settled = false;
|
|
258
|
+
const settle = (failure, flush = true, close = failure !== void 0) => {
|
|
259
|
+
if (settled) return;
|
|
260
|
+
settled = true;
|
|
261
|
+
disposed = true;
|
|
262
|
+
if (close) closeSource();
|
|
263
|
+
let finalFailure = failure;
|
|
264
|
+
if (flush) {
|
|
265
|
+
try {
|
|
266
|
+
batcher.flush();
|
|
267
|
+
} catch (error) {
|
|
268
|
+
finalFailure ??= { error };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
batcher.dispose();
|
|
272
|
+
if (finalFailure === void 0) resolveDone();
|
|
273
|
+
else rejectDone(finalFailure.error);
|
|
274
|
+
};
|
|
275
|
+
batcher.subscribeError((error) => {
|
|
276
|
+
settle({ error }, false, true);
|
|
277
|
+
});
|
|
278
|
+
void (async () => {
|
|
126
279
|
try {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
280
|
+
while (!disposed) {
|
|
281
|
+
const result = await iterator.next();
|
|
282
|
+
if (disposed) return;
|
|
283
|
+
if (result.done) {
|
|
284
|
+
settle();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
batcher.update([{ id: options.rowId, changes: result.value }]);
|
|
130
288
|
}
|
|
131
|
-
batcher.flush();
|
|
132
|
-
batcher.dispose();
|
|
133
|
-
resolveDone();
|
|
134
289
|
} catch (err) {
|
|
135
|
-
|
|
136
|
-
batcher.dispose();
|
|
137
|
-
rejectDone(err);
|
|
290
|
+
settle({ error: err }, true, true);
|
|
138
291
|
}
|
|
139
|
-
})();
|
|
292
|
+
})().catch((error) => settle({ error }));
|
|
140
293
|
return {
|
|
141
294
|
done,
|
|
142
295
|
dispose() {
|
|
143
296
|
if (disposed) return;
|
|
144
|
-
|
|
145
|
-
batcher.flush();
|
|
146
|
-
batcher.dispose();
|
|
147
|
-
resolveDone();
|
|
297
|
+
settle(void 0, true, true);
|
|
148
298
|
}
|
|
149
299
|
};
|
|
150
300
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Structural
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* same shape.
|
|
2
|
+
* Structural contract for a row model that accepts atomic row transactions.
|
|
3
|
+
* The adapter depends only on this ID-generic shape, so callers may pass a
|
|
4
|
+
* Pretable row model or a compatible custom implementation.
|
|
6
5
|
*
|
|
7
6
|
* @public
|
|
8
7
|
*/
|
|
9
|
-
interface
|
|
10
|
-
applyTransaction(transaction: {
|
|
8
|
+
interface RowModelLike<TRow extends object, TRowId extends string | number> {
|
|
9
|
+
readonly applyTransaction: (transaction: {
|
|
11
10
|
add?: TRow[];
|
|
12
|
-
update?:
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
update?: {
|
|
12
|
+
id: TRowId;
|
|
13
|
+
changes: Partial<TRow>;
|
|
14
|
+
}[];
|
|
15
|
+
remove?: TRowId[];
|
|
16
|
+
}) => void | {
|
|
17
|
+
readonly issues?: readonly {
|
|
18
|
+
readonly code: string;
|
|
19
|
+
readonly rowId?: TRowId;
|
|
20
|
+
}[];
|
|
21
|
+
};
|
|
15
22
|
}
|
|
16
23
|
/**
|
|
17
24
|
* RAF-batched mutator returned by {@link createBatcher}. Buffer
|
|
@@ -22,12 +29,19 @@ interface GridLike<TRow extends Record<string, unknown>> {
|
|
|
22
29
|
*
|
|
23
30
|
* @public
|
|
24
31
|
*/
|
|
25
|
-
interface TransactionBatcher<TRow extends
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
interface TransactionBatcher<TRow extends object, TRowId extends string | number> {
|
|
33
|
+
/** Rejects with the exact model error from a scheduled RAF transaction. */
|
|
34
|
+
readonly error: Promise<never>;
|
|
35
|
+
/** Observes a scheduled model failure synchronously before later races. */
|
|
36
|
+
readonly subscribeError: (listener: (error: unknown) => void) => () => void;
|
|
37
|
+
readonly add: (rows: readonly TRow[]) => void;
|
|
38
|
+
readonly update: (patches: readonly {
|
|
39
|
+
readonly id: TRowId;
|
|
40
|
+
readonly changes: Partial<TRow>;
|
|
41
|
+
}[]) => void;
|
|
42
|
+
readonly remove: (ids: readonly TRowId[]) => void;
|
|
43
|
+
readonly flush: () => void;
|
|
44
|
+
readonly dispose: () => void;
|
|
31
45
|
}
|
|
32
46
|
/**
|
|
33
47
|
* Handle returned by the `connect*Stream` functions. `done` resolves
|
|
@@ -37,31 +51,30 @@ interface TransactionBatcher<TRow extends Record<string, unknown>> {
|
|
|
37
51
|
* @public
|
|
38
52
|
*/
|
|
39
53
|
interface StreamConnection {
|
|
40
|
-
done: Promise<void>;
|
|
41
|
-
dispose()
|
|
54
|
+
readonly done: Promise<void>;
|
|
55
|
+
readonly dispose: () => void;
|
|
42
56
|
}
|
|
43
57
|
|
|
44
58
|
/**
|
|
45
59
|
* Create a `requestAnimationFrame`-batched mutator that coalesces
|
|
46
60
|
* `add` / `update` / `remove` calls into a single `applyTransaction` per
|
|
47
|
-
* frame. Use this when driving a
|
|
48
|
-
* than the browser can render
|
|
49
|
-
* frame regardless of stream rate.
|
|
61
|
+
* frame. Use this when driving a row model from a stream that emits faster
|
|
62
|
+
* than the browser can render.
|
|
50
63
|
*
|
|
51
64
|
* @example
|
|
52
65
|
* ```ts
|
|
53
|
-
* const batcher = createBatcher(
|
|
66
|
+
* const batcher = createBatcher(rowModel);
|
|
54
67
|
* batcher.add([{ id: "1", name: "Ada" }]);
|
|
55
|
-
* batcher.update([{ id: "1", age: 36 }]);
|
|
68
|
+
* batcher.update([{ id: "1", changes: { age: 36 } }]);
|
|
56
69
|
* batcher.flush(); // optional — RAF will flush automatically
|
|
57
70
|
* ```
|
|
58
71
|
*
|
|
59
72
|
* @public
|
|
60
73
|
*/
|
|
61
|
-
declare function createBatcher<TRow extends
|
|
74
|
+
declare function createBatcher<TRow extends object, TRowId extends string | number>(rowModel: RowModelLike<TRow, TRowId>): TransactionBatcher<TRow, TRowId>;
|
|
62
75
|
|
|
63
76
|
/**
|
|
64
|
-
* Drive a
|
|
77
|
+
* Drive a row model from an `AsyncIterable<TRow>`. Each yielded row is added
|
|
65
78
|
* via a {@link createBatcher | RAF batcher}; the returned
|
|
66
79
|
* {@link StreamConnection} resolves `done` when the stream ends and
|
|
67
80
|
* supports `dispose()` for early cancellation.
|
|
@@ -71,34 +84,35 @@ declare function createBatcher<TRow extends Record<string, unknown>>(grid: GridL
|
|
|
71
84
|
*
|
|
72
85
|
* @public
|
|
73
86
|
*/
|
|
74
|
-
declare function connectElementStream<TRow extends
|
|
87
|
+
declare function connectElementStream<TRow extends object, TRowId extends string | number>(rowModel: RowModelLike<TRow, TRowId>, stream: AsyncIterable<TRow>): StreamConnection;
|
|
75
88
|
|
|
76
89
|
/**
|
|
77
|
-
* Options for {@link connectPartialStream}. `rowId`
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* ignored).
|
|
90
|
+
* Options for {@link connectPartialStream}. `rowId` is the fixed target for
|
|
91
|
+
* every partial update. Unknown targets are reported through `onIssue`; an
|
|
92
|
+
* optional `createRow` factory may turn the partial into a complete row to add.
|
|
81
93
|
*
|
|
82
94
|
* @public
|
|
83
95
|
*/
|
|
84
|
-
interface PartialStreamOptions {
|
|
85
|
-
rowId:
|
|
96
|
+
interface PartialStreamOptions<TRow extends object, TRowId extends string | number> {
|
|
97
|
+
readonly rowId: TRowId;
|
|
98
|
+
readonly onIssue?: (issue: {
|
|
99
|
+
readonly code: "unknown-update-id";
|
|
100
|
+
readonly rowId: TRowId;
|
|
101
|
+
}) => void;
|
|
102
|
+
readonly createRow?: (partial: Partial<TRow>, id: TRowId) => TRow;
|
|
86
103
|
}
|
|
87
104
|
/**
|
|
88
|
-
* Drive a
|
|
89
|
-
* partial
|
|
90
|
-
*
|
|
91
|
-
* a
|
|
92
|
-
* rather than complete rows.
|
|
105
|
+
* Drive a row model from an `AsyncIterable<Partial<TRow>>`. Every yielded
|
|
106
|
+
* partial updates the fixed `options.rowId` via a RAF-batched
|
|
107
|
+
* `{ id, changes }` transaction. A missing target is reported instead of
|
|
108
|
+
* fabricating a row; provide `createRow` when the stream is allowed to add one.
|
|
93
109
|
*
|
|
94
110
|
* Pair with {@link parsePartialStream} for end-to-end partial-update
|
|
95
111
|
* streaming over UTF-8 strings.
|
|
96
112
|
*
|
|
97
113
|
* @public
|
|
98
114
|
*/
|
|
99
|
-
declare function connectPartialStream<TRow extends
|
|
100
|
-
id: string;
|
|
101
|
-
}>(grid: GridLike<TRow>, stream: AsyncIterable<Partial<TRow>>, options: PartialStreamOptions): StreamConnection;
|
|
115
|
+
declare function connectPartialStream<TRow extends object, TRowId extends string | number>(rowModel: RowModelLike<TRow, TRowId>, stream: AsyncIterable<Partial<TRow>>, options: PartialStreamOptions<TRow, TRowId>): StreamConnection;
|
|
102
116
|
|
|
103
117
|
/**
|
|
104
118
|
* Parse a UTF-8 string stream into an `AsyncIterable<TRow>`. Built on
|
|
@@ -126,4 +140,4 @@ declare function parseElementStream<TRow>(stream: AsyncIterable<string>): AsyncI
|
|
|
126
140
|
*/
|
|
127
141
|
declare function parsePartialStream<TRow>(stream: AsyncIterable<string>): AsyncIterable<Partial<TRow>>;
|
|
128
142
|
|
|
129
|
-
export { type
|
|
143
|
+
export { type PartialStreamOptions, type RowModelLike, type StreamConnection, type TransactionBatcher, connectElementStream, connectPartialStream, createBatcher, parseElementStream, parsePartialStream };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Structural
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* same shape.
|
|
2
|
+
* Structural contract for a row model that accepts atomic row transactions.
|
|
3
|
+
* The adapter depends only on this ID-generic shape, so callers may pass a
|
|
4
|
+
* Pretable row model or a compatible custom implementation.
|
|
6
5
|
*
|
|
7
6
|
* @public
|
|
8
7
|
*/
|
|
9
|
-
interface
|
|
10
|
-
applyTransaction(transaction: {
|
|
8
|
+
interface RowModelLike<TRow extends object, TRowId extends string | number> {
|
|
9
|
+
readonly applyTransaction: (transaction: {
|
|
11
10
|
add?: TRow[];
|
|
12
|
-
update?:
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
update?: {
|
|
12
|
+
id: TRowId;
|
|
13
|
+
changes: Partial<TRow>;
|
|
14
|
+
}[];
|
|
15
|
+
remove?: TRowId[];
|
|
16
|
+
}) => void | {
|
|
17
|
+
readonly issues?: readonly {
|
|
18
|
+
readonly code: string;
|
|
19
|
+
readonly rowId?: TRowId;
|
|
20
|
+
}[];
|
|
21
|
+
};
|
|
15
22
|
}
|
|
16
23
|
/**
|
|
17
24
|
* RAF-batched mutator returned by {@link createBatcher}. Buffer
|
|
@@ -22,12 +29,19 @@ interface GridLike<TRow extends Record<string, unknown>> {
|
|
|
22
29
|
*
|
|
23
30
|
* @public
|
|
24
31
|
*/
|
|
25
|
-
interface TransactionBatcher<TRow extends
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
interface TransactionBatcher<TRow extends object, TRowId extends string | number> {
|
|
33
|
+
/** Rejects with the exact model error from a scheduled RAF transaction. */
|
|
34
|
+
readonly error: Promise<never>;
|
|
35
|
+
/** Observes a scheduled model failure synchronously before later races. */
|
|
36
|
+
readonly subscribeError: (listener: (error: unknown) => void) => () => void;
|
|
37
|
+
readonly add: (rows: readonly TRow[]) => void;
|
|
38
|
+
readonly update: (patches: readonly {
|
|
39
|
+
readonly id: TRowId;
|
|
40
|
+
readonly changes: Partial<TRow>;
|
|
41
|
+
}[]) => void;
|
|
42
|
+
readonly remove: (ids: readonly TRowId[]) => void;
|
|
43
|
+
readonly flush: () => void;
|
|
44
|
+
readonly dispose: () => void;
|
|
31
45
|
}
|
|
32
46
|
/**
|
|
33
47
|
* Handle returned by the `connect*Stream` functions. `done` resolves
|
|
@@ -37,31 +51,30 @@ interface TransactionBatcher<TRow extends Record<string, unknown>> {
|
|
|
37
51
|
* @public
|
|
38
52
|
*/
|
|
39
53
|
interface StreamConnection {
|
|
40
|
-
done: Promise<void>;
|
|
41
|
-
dispose()
|
|
54
|
+
readonly done: Promise<void>;
|
|
55
|
+
readonly dispose: () => void;
|
|
42
56
|
}
|
|
43
57
|
|
|
44
58
|
/**
|
|
45
59
|
* Create a `requestAnimationFrame`-batched mutator that coalesces
|
|
46
60
|
* `add` / `update` / `remove` calls into a single `applyTransaction` per
|
|
47
|
-
* frame. Use this when driving a
|
|
48
|
-
* than the browser can render
|
|
49
|
-
* frame regardless of stream rate.
|
|
61
|
+
* frame. Use this when driving a row model from a stream that emits faster
|
|
62
|
+
* than the browser can render.
|
|
50
63
|
*
|
|
51
64
|
* @example
|
|
52
65
|
* ```ts
|
|
53
|
-
* const batcher = createBatcher(
|
|
66
|
+
* const batcher = createBatcher(rowModel);
|
|
54
67
|
* batcher.add([{ id: "1", name: "Ada" }]);
|
|
55
|
-
* batcher.update([{ id: "1", age: 36 }]);
|
|
68
|
+
* batcher.update([{ id: "1", changes: { age: 36 } }]);
|
|
56
69
|
* batcher.flush(); // optional — RAF will flush automatically
|
|
57
70
|
* ```
|
|
58
71
|
*
|
|
59
72
|
* @public
|
|
60
73
|
*/
|
|
61
|
-
declare function createBatcher<TRow extends
|
|
74
|
+
declare function createBatcher<TRow extends object, TRowId extends string | number>(rowModel: RowModelLike<TRow, TRowId>): TransactionBatcher<TRow, TRowId>;
|
|
62
75
|
|
|
63
76
|
/**
|
|
64
|
-
* Drive a
|
|
77
|
+
* Drive a row model from an `AsyncIterable<TRow>`. Each yielded row is added
|
|
65
78
|
* via a {@link createBatcher | RAF batcher}; the returned
|
|
66
79
|
* {@link StreamConnection} resolves `done` when the stream ends and
|
|
67
80
|
* supports `dispose()` for early cancellation.
|
|
@@ -71,34 +84,35 @@ declare function createBatcher<TRow extends Record<string, unknown>>(grid: GridL
|
|
|
71
84
|
*
|
|
72
85
|
* @public
|
|
73
86
|
*/
|
|
74
|
-
declare function connectElementStream<TRow extends
|
|
87
|
+
declare function connectElementStream<TRow extends object, TRowId extends string | number>(rowModel: RowModelLike<TRow, TRowId>, stream: AsyncIterable<TRow>): StreamConnection;
|
|
75
88
|
|
|
76
89
|
/**
|
|
77
|
-
* Options for {@link connectPartialStream}. `rowId`
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* ignored).
|
|
90
|
+
* Options for {@link connectPartialStream}. `rowId` is the fixed target for
|
|
91
|
+
* every partial update. Unknown targets are reported through `onIssue`; an
|
|
92
|
+
* optional `createRow` factory may turn the partial into a complete row to add.
|
|
81
93
|
*
|
|
82
94
|
* @public
|
|
83
95
|
*/
|
|
84
|
-
interface PartialStreamOptions {
|
|
85
|
-
rowId:
|
|
96
|
+
interface PartialStreamOptions<TRow extends object, TRowId extends string | number> {
|
|
97
|
+
readonly rowId: TRowId;
|
|
98
|
+
readonly onIssue?: (issue: {
|
|
99
|
+
readonly code: "unknown-update-id";
|
|
100
|
+
readonly rowId: TRowId;
|
|
101
|
+
}) => void;
|
|
102
|
+
readonly createRow?: (partial: Partial<TRow>, id: TRowId) => TRow;
|
|
86
103
|
}
|
|
87
104
|
/**
|
|
88
|
-
* Drive a
|
|
89
|
-
* partial
|
|
90
|
-
*
|
|
91
|
-
* a
|
|
92
|
-
* rather than complete rows.
|
|
105
|
+
* Drive a row model from an `AsyncIterable<Partial<TRow>>`. Every yielded
|
|
106
|
+
* partial updates the fixed `options.rowId` via a RAF-batched
|
|
107
|
+
* `{ id, changes }` transaction. A missing target is reported instead of
|
|
108
|
+
* fabricating a row; provide `createRow` when the stream is allowed to add one.
|
|
93
109
|
*
|
|
94
110
|
* Pair with {@link parsePartialStream} for end-to-end partial-update
|
|
95
111
|
* streaming over UTF-8 strings.
|
|
96
112
|
*
|
|
97
113
|
* @public
|
|
98
114
|
*/
|
|
99
|
-
declare function connectPartialStream<TRow extends
|
|
100
|
-
id: string;
|
|
101
|
-
}>(grid: GridLike<TRow>, stream: AsyncIterable<Partial<TRow>>, options: PartialStreamOptions): StreamConnection;
|
|
115
|
+
declare function connectPartialStream<TRow extends object, TRowId extends string | number>(rowModel: RowModelLike<TRow, TRowId>, stream: AsyncIterable<Partial<TRow>>, options: PartialStreamOptions<TRow, TRowId>): StreamConnection;
|
|
102
116
|
|
|
103
117
|
/**
|
|
104
118
|
* Parse a UTF-8 string stream into an `AsyncIterable<TRow>`. Built on
|
|
@@ -126,4 +140,4 @@ declare function parseElementStream<TRow>(stream: AsyncIterable<string>): AsyncI
|
|
|
126
140
|
*/
|
|
127
141
|
declare function parsePartialStream<TRow>(stream: AsyncIterable<string>): AsyncIterable<Partial<TRow>>;
|
|
128
142
|
|
|
129
|
-
export { type
|
|
143
|
+
export { type PartialStreamOptions, type RowModelLike, type StreamConnection, type TransactionBatcher, connectElementStream, connectPartialStream, createBatcher, parseElementStream, parsePartialStream };
|
package/dist/index.mjs
CHANGED
|
@@ -1,39 +1,93 @@
|
|
|
1
1
|
import { create, push, isArrayNode, isComplete, finish, isObjectNode } from '@cacheplane/json-stream';
|
|
2
2
|
|
|
3
3
|
// src/create-batcher.ts
|
|
4
|
-
function createBatcher(
|
|
4
|
+
function createBatcher(rowModel) {
|
|
5
5
|
let addBuffer = [];
|
|
6
6
|
let updateBuffer = [];
|
|
7
7
|
let removeBuffer = [];
|
|
8
8
|
let rafId = null;
|
|
9
9
|
let disposed = false;
|
|
10
|
+
let rejectError;
|
|
11
|
+
const errorListeners = /* @__PURE__ */ new Set();
|
|
12
|
+
let failed = false;
|
|
13
|
+
let failure;
|
|
14
|
+
const error = new Promise((_resolve, reject) => {
|
|
15
|
+
rejectError = reject;
|
|
16
|
+
});
|
|
17
|
+
error.catch(() => void 0);
|
|
18
|
+
function fail(error2) {
|
|
19
|
+
if (disposed) return;
|
|
20
|
+
disposed = true;
|
|
21
|
+
failed = true;
|
|
22
|
+
failure = error2;
|
|
23
|
+
if (rafId !== null) {
|
|
24
|
+
cancelAnimationFrame(rafId);
|
|
25
|
+
rafId = null;
|
|
26
|
+
}
|
|
27
|
+
addBuffer = [];
|
|
28
|
+
updateBuffer = [];
|
|
29
|
+
removeBuffer = [];
|
|
30
|
+
const listeners = [...errorListeners];
|
|
31
|
+
errorListeners.clear();
|
|
32
|
+
for (const listener of listeners) {
|
|
33
|
+
try {
|
|
34
|
+
listener(error2);
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
rejectError(error2);
|
|
39
|
+
}
|
|
10
40
|
function scheduleFlush() {
|
|
11
41
|
if (rafId !== null || disposed) return;
|
|
12
42
|
rafId = requestAnimationFrame(() => {
|
|
13
43
|
rafId = null;
|
|
14
|
-
|
|
44
|
+
if (disposed) return;
|
|
45
|
+
try {
|
|
46
|
+
applyBuffered();
|
|
47
|
+
} catch (error2) {
|
|
48
|
+
fail(error2);
|
|
49
|
+
}
|
|
15
50
|
});
|
|
16
51
|
}
|
|
17
52
|
function applyBuffered() {
|
|
18
53
|
if (addBuffer.length === 0 && updateBuffer.length === 0 && removeBuffer.length === 0) {
|
|
19
54
|
return;
|
|
20
55
|
}
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
56
|
+
const bufferedAdds = addBuffer;
|
|
57
|
+
const bufferedUpdates = updateBuffer;
|
|
58
|
+
const bufferedRemovals = removeBuffer;
|
|
59
|
+
addBuffer = [];
|
|
60
|
+
updateBuffer = [];
|
|
61
|
+
removeBuffer = [];
|
|
62
|
+
const transaction = {};
|
|
63
|
+
if (bufferedAdds.length > 0) {
|
|
64
|
+
transaction.add = bufferedAdds.map((row) => ({ ...row }));
|
|
25
65
|
}
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
|
|
66
|
+
if (bufferedUpdates.length > 0) {
|
|
67
|
+
transaction.update = bufferedUpdates.map(({ id, changes }) => ({
|
|
68
|
+
id,
|
|
69
|
+
changes: { ...changes }
|
|
70
|
+
}));
|
|
29
71
|
}
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
removeBuffer = [];
|
|
72
|
+
if (bufferedRemovals.length > 0) {
|
|
73
|
+
transaction.remove = [...bufferedRemovals];
|
|
33
74
|
}
|
|
34
|
-
|
|
75
|
+
rowModel.applyTransaction(transaction);
|
|
35
76
|
}
|
|
36
77
|
return {
|
|
78
|
+
error,
|
|
79
|
+
subscribeError(listener) {
|
|
80
|
+
if (failed) {
|
|
81
|
+
try {
|
|
82
|
+
listener(failure);
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
return () => void 0;
|
|
86
|
+
}
|
|
87
|
+
if (disposed) return () => void 0;
|
|
88
|
+
errorListeners.add(listener);
|
|
89
|
+
return () => errorListeners.delete(listener);
|
|
90
|
+
},
|
|
37
91
|
add(rows) {
|
|
38
92
|
if (disposed) return;
|
|
39
93
|
addBuffer.push(...rows);
|
|
@@ -67,14 +121,17 @@ function createBatcher(grid) {
|
|
|
67
121
|
addBuffer = [];
|
|
68
122
|
updateBuffer = [];
|
|
69
123
|
removeBuffer = [];
|
|
124
|
+
errorListeners.clear();
|
|
70
125
|
}
|
|
71
126
|
};
|
|
72
127
|
}
|
|
73
128
|
|
|
74
129
|
// src/connect-element-stream.ts
|
|
75
|
-
function connectElementStream(
|
|
76
|
-
const
|
|
130
|
+
function connectElementStream(rowModel, stream) {
|
|
131
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
132
|
+
const batcher = createBatcher(rowModel);
|
|
77
133
|
let disposed = false;
|
|
134
|
+
let sourceClosed = false;
|
|
78
135
|
let resolveDone;
|
|
79
136
|
let rejectDone;
|
|
80
137
|
const done = new Promise((resolve, reject) => {
|
|
@@ -82,37 +139,102 @@ function connectElementStream(grid, stream) {
|
|
|
82
139
|
rejectDone = reject;
|
|
83
140
|
});
|
|
84
141
|
done.catch(() => void 0);
|
|
85
|
-
|
|
142
|
+
const closeSource = () => {
|
|
143
|
+
if (sourceClosed) return;
|
|
144
|
+
sourceClosed = true;
|
|
86
145
|
try {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
146
|
+
const closing = iterator.return?.();
|
|
147
|
+
if (closing !== void 0)
|
|
148
|
+
void Promise.resolve(closing).catch(() => void 0);
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
let settled = false;
|
|
153
|
+
const settle = (failure, flush = true, close = failure !== void 0) => {
|
|
154
|
+
if (settled) return;
|
|
155
|
+
settled = true;
|
|
156
|
+
disposed = true;
|
|
157
|
+
if (close) closeSource();
|
|
158
|
+
let finalFailure = failure;
|
|
159
|
+
if (flush) {
|
|
160
|
+
try {
|
|
161
|
+
batcher.flush();
|
|
162
|
+
} catch (error) {
|
|
163
|
+
finalFailure ??= { error };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
batcher.dispose();
|
|
167
|
+
if (finalFailure === void 0) resolveDone();
|
|
168
|
+
else rejectDone(finalFailure.error);
|
|
169
|
+
};
|
|
170
|
+
batcher.subscribeError((error) => {
|
|
171
|
+
settle({ error }, false, true);
|
|
172
|
+
});
|
|
173
|
+
void (async () => {
|
|
174
|
+
try {
|
|
175
|
+
while (!disposed) {
|
|
176
|
+
const result = await iterator.next();
|
|
177
|
+
if (disposed) return;
|
|
178
|
+
if (result.done) {
|
|
179
|
+
settle();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
batcher.add([result.value]);
|
|
90
183
|
}
|
|
91
|
-
batcher.flush();
|
|
92
|
-
batcher.dispose();
|
|
93
|
-
resolveDone();
|
|
94
184
|
} catch (err) {
|
|
95
|
-
|
|
96
|
-
batcher.dispose();
|
|
97
|
-
rejectDone(err);
|
|
185
|
+
settle({ error: err }, true, true);
|
|
98
186
|
}
|
|
99
|
-
})();
|
|
187
|
+
})().catch((error) => settle({ error }));
|
|
100
188
|
return {
|
|
101
189
|
done,
|
|
102
190
|
dispose() {
|
|
103
191
|
if (disposed) return;
|
|
104
|
-
|
|
105
|
-
batcher.flush();
|
|
106
|
-
batcher.dispose();
|
|
107
|
-
resolveDone();
|
|
192
|
+
settle(void 0, true, true);
|
|
108
193
|
}
|
|
109
194
|
};
|
|
110
195
|
}
|
|
111
196
|
|
|
112
197
|
// src/connect-partial-stream.ts
|
|
113
|
-
function
|
|
114
|
-
|
|
198
|
+
function sameValueZero(left, right) {
|
|
199
|
+
return left === right || typeof left === "number" && typeof right === "number" && Number.isNaN(left) && Number.isNaN(right);
|
|
200
|
+
}
|
|
201
|
+
function connectPartialStream(rowModel, stream, options) {
|
|
202
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
203
|
+
const issueAwareRowModel = {
|
|
204
|
+
applyTransaction(transaction) {
|
|
205
|
+
const result = rowModel.applyTransaction(transaction);
|
|
206
|
+
if (result === void 0 || result.issues === void 0) return result;
|
|
207
|
+
let targetMissing = false;
|
|
208
|
+
for (const issue of result.issues) {
|
|
209
|
+
if (issue.code !== "unknown-update-id" || issue.rowId === void 0 || !sameValueZero(issue.rowId, options.rowId)) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
targetMissing = true;
|
|
213
|
+
options.onIssue?.({
|
|
214
|
+
code: "unknown-update-id",
|
|
215
|
+
rowId: options.rowId
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
if (targetMissing && options.createRow !== void 0) {
|
|
219
|
+
const updates = transaction.update ?? [];
|
|
220
|
+
const combinedChanges = {};
|
|
221
|
+
let hasTargetUpdate = false;
|
|
222
|
+
for (const update of updates) {
|
|
223
|
+
if (!sameValueZero(update.id, options.rowId)) continue;
|
|
224
|
+
Object.assign(combinedChanges, update.changes);
|
|
225
|
+
hasTargetUpdate = true;
|
|
226
|
+
}
|
|
227
|
+
if (hasTargetUpdate) {
|
|
228
|
+
const row = options.createRow(combinedChanges, options.rowId);
|
|
229
|
+
rowModel.applyTransaction({ add: [row] });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const batcher = createBatcher(issueAwareRowModel);
|
|
115
236
|
let disposed = false;
|
|
237
|
+
let sourceClosed = false;
|
|
116
238
|
let resolveDone;
|
|
117
239
|
let rejectDone;
|
|
118
240
|
const done = new Promise((resolve, reject) => {
|
|
@@ -120,29 +242,57 @@ function connectPartialStream(grid, stream, options) {
|
|
|
120
242
|
rejectDone = reject;
|
|
121
243
|
});
|
|
122
244
|
done.catch(() => void 0);
|
|
123
|
-
|
|
245
|
+
const closeSource = () => {
|
|
246
|
+
if (sourceClosed) return;
|
|
247
|
+
sourceClosed = true;
|
|
248
|
+
try {
|
|
249
|
+
const closing = iterator.return?.();
|
|
250
|
+
if (closing !== void 0)
|
|
251
|
+
void Promise.resolve(closing).catch(() => void 0);
|
|
252
|
+
} catch {
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
let settled = false;
|
|
256
|
+
const settle = (failure, flush = true, close = failure !== void 0) => {
|
|
257
|
+
if (settled) return;
|
|
258
|
+
settled = true;
|
|
259
|
+
disposed = true;
|
|
260
|
+
if (close) closeSource();
|
|
261
|
+
let finalFailure = failure;
|
|
262
|
+
if (flush) {
|
|
263
|
+
try {
|
|
264
|
+
batcher.flush();
|
|
265
|
+
} catch (error) {
|
|
266
|
+
finalFailure ??= { error };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
batcher.dispose();
|
|
270
|
+
if (finalFailure === void 0) resolveDone();
|
|
271
|
+
else rejectDone(finalFailure.error);
|
|
272
|
+
};
|
|
273
|
+
batcher.subscribeError((error) => {
|
|
274
|
+
settle({ error }, false, true);
|
|
275
|
+
});
|
|
276
|
+
void (async () => {
|
|
124
277
|
try {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
278
|
+
while (!disposed) {
|
|
279
|
+
const result = await iterator.next();
|
|
280
|
+
if (disposed) return;
|
|
281
|
+
if (result.done) {
|
|
282
|
+
settle();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
batcher.update([{ id: options.rowId, changes: result.value }]);
|
|
128
286
|
}
|
|
129
|
-
batcher.flush();
|
|
130
|
-
batcher.dispose();
|
|
131
|
-
resolveDone();
|
|
132
287
|
} catch (err) {
|
|
133
|
-
|
|
134
|
-
batcher.dispose();
|
|
135
|
-
rejectDone(err);
|
|
288
|
+
settle({ error: err }, true, true);
|
|
136
289
|
}
|
|
137
|
-
})();
|
|
290
|
+
})().catch((error) => settle({ error }));
|
|
138
291
|
return {
|
|
139
292
|
done,
|
|
140
293
|
dispose() {
|
|
141
294
|
if (disposed) return;
|
|
142
|
-
|
|
143
|
-
batcher.flush();
|
|
144
|
-
batcher.dispose();
|
|
145
|
-
resolveDone();
|
|
295
|
+
settle(void 0, true, true);
|
|
146
296
|
}
|
|
147
297
|
};
|
|
148
298
|
}
|