streamfold 0.1.6 → 0.1.8
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 +4 -1
- package/src/index.js +1 -0
- package/src/internal/abortable-source.js +8 -8
- package/src/internal/wasm-runtime.js +45 -3
- 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>,
|
|
@@ -240,6 +240,9 @@ export function createStructuredStreamPool<Id = string>(
|
|
|
240
240
|
options?: StructuredStreamPoolOptions,
|
|
241
241
|
): StructuredStreamPool<Id>;
|
|
242
242
|
|
|
243
|
+
/** Compile and cache the embedded WebAssembly module before creating scanners. */
|
|
244
|
+
export function prepareStreamfold(): Promise<void>;
|
|
245
|
+
|
|
243
246
|
export const STREAMFOLD_ENGINE: "rust-wasm";
|
|
244
247
|
export const DEFAULT_STREAM_LIMITS: Readonly<
|
|
245
248
|
Required<
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { applyImmutableChanges, freezeJson } from "./internal/snapshots.js";
|
|
|
9
9
|
import { annotateError, streamError } from "./internal/errors.js";
|
|
10
10
|
|
|
11
11
|
export { isStructuredStreamError } from "./internal/errors.js";
|
|
12
|
+
export { prepareWasm as prepareStreamfold } from "./internal/wasm-runtime.js";
|
|
12
13
|
export { defineAdapter } from "./adapter.js";
|
|
13
14
|
export { readStructured } from "./read-structured.js";
|
|
14
15
|
|
|
@@ -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
|
}
|
|
@@ -17,6 +17,8 @@ const PATCH_COMPLETE = 8;
|
|
|
17
17
|
|
|
18
18
|
const encoder = new TextEncoder();
|
|
19
19
|
let exports;
|
|
20
|
+
let compiledModule;
|
|
21
|
+
let compiling;
|
|
20
22
|
|
|
21
23
|
const decodeBase64 = (base64) => {
|
|
22
24
|
const binary = globalThis.atob(base64);
|
|
@@ -32,11 +34,39 @@ const getExports = () => {
|
|
|
32
34
|
if (typeof WebAssembly !== "object") {
|
|
33
35
|
throw new Error("Streamfold requires WebAssembly support");
|
|
34
36
|
}
|
|
35
|
-
const module =
|
|
37
|
+
const module =
|
|
38
|
+
compiledModule ?? new WebAssembly.Module(decodeBase64(wasmBinaryBase64));
|
|
36
39
|
exports = new WebAssembly.Instance(module, {}).exports;
|
|
37
40
|
return exports;
|
|
38
41
|
};
|
|
39
42
|
|
|
43
|
+
export const prepareWasm = () => {
|
|
44
|
+
if (exports !== undefined || compiledModule !== undefined)
|
|
45
|
+
return Promise.resolve();
|
|
46
|
+
if (compiling !== undefined) return compiling;
|
|
47
|
+
if (
|
|
48
|
+
typeof WebAssembly !== "object" ||
|
|
49
|
+
typeof WebAssembly.compile !== "function"
|
|
50
|
+
) {
|
|
51
|
+
return Promise.reject(new Error("Streamfold requires WebAssembly support"));
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
compiling = WebAssembly.compile(decodeBase64(wasmBinaryBase64)).then(
|
|
55
|
+
(module) => {
|
|
56
|
+
compiledModule = module;
|
|
57
|
+
compiling = undefined;
|
|
58
|
+
},
|
|
59
|
+
(error) => {
|
|
60
|
+
compiling = undefined;
|
|
61
|
+
throw error;
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return Promise.reject(error);
|
|
66
|
+
}
|
|
67
|
+
return compiling;
|
|
68
|
+
};
|
|
69
|
+
|
|
40
70
|
const describeParserError = ({ wasm, handle, maxBytes, maxDepth }, code) => {
|
|
41
71
|
const offset = wasm.streamfold_parser_error_offset(handle) >>> 0;
|
|
42
72
|
const byte = wasm.streamfold_parser_error_byte(handle);
|
|
@@ -196,15 +226,27 @@ export const createWasmParser = ({ maxBytes, maxDepth }) => {
|
|
|
196
226
|
};
|
|
197
227
|
|
|
198
228
|
const pushChunk = (parser, chunk) => {
|
|
229
|
+
// In Unicode mode this range matches lone surrogate code units, not valid
|
|
230
|
+
// pairs. TextEncoder would silently replace them with U+FFFD, while the
|
|
231
|
+
// pool's final JSON.parse would preserve them, producing different values.
|
|
232
|
+
// A trailing high surrogate is buffered by pushWasmParser before this check.
|
|
233
|
+
if (/[\uD800-\uDFFF]/u.test(chunk)) {
|
|
234
|
+
throw streamError(
|
|
235
|
+
new TypeError("Unpaired UTF-16 surrogate; use a JSON Unicode escape"),
|
|
236
|
+
"INVALID_CHUNK",
|
|
237
|
+
);
|
|
238
|
+
}
|
|
199
239
|
const { wasm, handle } = parser;
|
|
200
|
-
const
|
|
240
|
+
const remaining = parser.maxBytes - (wasm.streamfold_parser_bytes_seen(handle) >>> 0);
|
|
241
|
+
if (chunk.length > remaining) throw parserError(parser, 7);
|
|
242
|
+
const capacity = Math.min(chunk.length * 3, remaining);
|
|
201
243
|
const pointer = wasm.streamfold_parser_input(handle, capacity);
|
|
202
244
|
let length = 0;
|
|
203
245
|
if (capacity > 0) {
|
|
204
246
|
const input = new Uint8Array(wasm.memory.buffer, pointer, capacity);
|
|
205
247
|
const result = encoder.encodeInto(chunk, input);
|
|
206
248
|
if (result.read !== chunk.length) {
|
|
207
|
-
throw
|
|
249
|
+
throw parserError(parser, 7);
|
|
208
250
|
}
|
|
209
251
|
length = result.written;
|
|
210
252
|
}
|
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();
|