streamfold 0.1.6 → 0.1.7
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 +9 -0
- package/package.json +1 -1
- package/src/index.d.ts +1 -1
- package/src/internal/abortable-source.js +8 -8
- package/src/internal/wasm-runtime.js +10 -0
- package/src/read-structured.js +4 -2
package/README.md
CHANGED
|
@@ -34,6 +34,11 @@ for frozen snapshots that preserve earlier values and share unchanged branches.
|
|
|
34
34
|
Pools and custom adapter factories accept the same option; managed readers
|
|
35
35
|
forward it through `limits`.
|
|
36
36
|
|
|
37
|
+
Chunks must contain well-formed Unicode. Valid surrogate pairs may span chunks;
|
|
38
|
+
raw unpaired UTF-16 code units fail with `INVALID_CHUNK` instead of being silently
|
|
39
|
+
replaced during UTF-8 encoding. Use JSON Unicode escapes for unpaired code units
|
|
40
|
+
(as produced by `JSON.stringify`).
|
|
41
|
+
|
|
37
42
|
Use `streamfold/assistant-ui`, `streamfold/vercel-ai`, `streamfold/openai`,
|
|
38
43
|
`streamfold/anthropic`, `streamfold/gemini`, `streamfold/langchain`, or
|
|
39
44
|
`streamfold/ag-ui` for decoded SDK events. Integrations use structural event
|
|
@@ -50,6 +55,10 @@ same batch adapter, finalizes remaining calls at the end of the source, and
|
|
|
50
55
|
disposes on completion, failure, or early exit. `limits` is optional.
|
|
51
56
|
For a built-in SDK factory, use `{ integration: assistantUI, limits }` instead.
|
|
52
57
|
Both paths yield the same lifecycle updates without replaying completions.
|
|
58
|
+
Readable streams are consumed through `getReader()` even without an abort
|
|
59
|
+
signal or native async-iterator support.
|
|
60
|
+
Upstream failures close the iterator or reader while preserving the original
|
|
61
|
+
error, even when no abort signal is supplied.
|
|
53
62
|
|
|
54
63
|
Every built-in adapter supports `pushAll(event)`, returning all ordered
|
|
55
64
|
`start`, `update`, and `complete` updates, including multiple calls in one event.
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -193,7 +193,7 @@ export interface ReadStructuredIntegrationOptions<Event, Id = string>
|
|
|
193
193
|
|
|
194
194
|
/** Consume decoded events, finalizing at EOF and disposing on every exit. */
|
|
195
195
|
export function readStructured<Event, Id = string>(
|
|
196
|
-
events: AsyncIterable<Event> | Iterable<Event>,
|
|
196
|
+
events: AsyncIterable<Event> | Iterable<Event> | ReadableStream<Event>,
|
|
197
197
|
options:
|
|
198
198
|
| ReadStructuredOptions<Event, Id>
|
|
199
199
|
| ReadStructuredIntegrationOptions<Event, Id>,
|
|
@@ -8,7 +8,7 @@ export function abortableSource(events, signal, onAbort) {
|
|
|
8
8
|
let closed = false;
|
|
9
9
|
// Each read removes its listener; one shared pending promise would retain
|
|
10
10
|
// a Promise.race reaction for every event until the session ends.
|
|
11
|
-
const waitFor = (promise) => new Promise((resolve, reject) => {
|
|
11
|
+
const waitFor = (promise) => signal === undefined ? Promise.resolve(promise) : new Promise((resolve, reject) => {
|
|
12
12
|
let listening = true;
|
|
13
13
|
const detach = () => {
|
|
14
14
|
if (!listening) return;
|
|
@@ -44,16 +44,16 @@ export function abortableSource(events, signal, onAbort) {
|
|
|
44
44
|
try { onAbort(); } catch { /* Cancellation preserves the signal's reason. */ }
|
|
45
45
|
close(signal.reason).catch(() => {});
|
|
46
46
|
};
|
|
47
|
-
signal
|
|
48
|
-
if (signal
|
|
47
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
48
|
+
if (signal?.aborted) abort();
|
|
49
49
|
|
|
50
50
|
return {
|
|
51
51
|
[Symbol.asyncIterator]() { return this; },
|
|
52
52
|
async next() {
|
|
53
|
-
signal
|
|
53
|
+
signal?.throwIfAborted();
|
|
54
54
|
const result = await waitFor(
|
|
55
55
|
Promise.resolve().then(async () => {
|
|
56
|
-
signal
|
|
56
|
+
signal?.throwIfAborted();
|
|
57
57
|
const next = reader !== undefined ? await reader.read() : await iterator.next();
|
|
58
58
|
if (next === null || typeof next !== "object") {
|
|
59
59
|
throw new TypeError("Iterator result must be an object");
|
|
@@ -61,7 +61,7 @@ export function abortableSource(events, signal, onAbort) {
|
|
|
61
61
|
return synchronous ? { done: next.done, value: await next.value } : next;
|
|
62
62
|
}),
|
|
63
63
|
);
|
|
64
|
-
signal
|
|
64
|
+
signal?.throwIfAborted();
|
|
65
65
|
if (result.done) {
|
|
66
66
|
closed = true;
|
|
67
67
|
reader?.releaseLock();
|
|
@@ -71,12 +71,12 @@ export function abortableSource(events, signal, onAbort) {
|
|
|
71
71
|
async return() {
|
|
72
72
|
const cleanup = close();
|
|
73
73
|
cleanup.catch(() => {});
|
|
74
|
-
if (!signal
|
|
74
|
+
if (!signal?.aborted) await waitFor(cleanup);
|
|
75
75
|
return { done: true, value: undefined };
|
|
76
76
|
},
|
|
77
77
|
async dispose() {
|
|
78
78
|
try { await this.return(); }
|
|
79
|
-
finally { signal
|
|
79
|
+
finally { signal?.removeEventListener("abort", abort); }
|
|
80
80
|
},
|
|
81
81
|
};
|
|
82
82
|
}
|
|
@@ -196,6 +196,16 @@ export const createWasmParser = ({ maxBytes, maxDepth }) => {
|
|
|
196
196
|
};
|
|
197
197
|
|
|
198
198
|
const pushChunk = (parser, chunk) => {
|
|
199
|
+
// In Unicode mode this range matches lone surrogate code units, not valid
|
|
200
|
+
// pairs. TextEncoder would silently replace them with U+FFFD, while the
|
|
201
|
+
// pool's final JSON.parse would preserve them, producing different values.
|
|
202
|
+
// A trailing high surrogate is buffered by pushWasmParser before this check.
|
|
203
|
+
if (/[\uD800-\uDFFF]/u.test(chunk)) {
|
|
204
|
+
throw streamError(
|
|
205
|
+
new TypeError("Unpaired UTF-16 surrogate; use a JSON Unicode escape"),
|
|
206
|
+
"INVALID_CHUNK",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
199
209
|
const { wasm, handle } = parser;
|
|
200
210
|
const capacity = chunk.length * 3;
|
|
201
211
|
const pointer = wasm.streamfold_parser_input(handle, capacity);
|
package/src/read-structured.js
CHANGED
|
@@ -35,8 +35,10 @@ export async function* readStructured(
|
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
signal?.throwIfAborted();
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// Own the source even without cancellation so a rejected next()
|
|
39
|
+
// still triggers upstream cleanup.
|
|
40
|
+
source = abortableSource(events, signal, dispose);
|
|
41
|
+
for await (const event of source) {
|
|
40
42
|
signal?.throwIfAborted();
|
|
41
43
|
for (const update of stream.pushAll(event)) {
|
|
42
44
|
signal?.throwIfAborted();
|