assistant-stream 0.2.6 → 0.2.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/dist/core/AssistantStreamChunk.d.ts +5 -1
- package/dist/core/AssistantStreamChunk.d.ts.map +1 -1
- package/dist/core/accumulators/AssistantMessageStream.d.ts.map +1 -1
- package/dist/core/accumulators/AssistantMessageStream.js +1 -0
- package/dist/core/accumulators/AssistantMessageStream.js.map +1 -1
- package/dist/core/accumulators/assistant-message-accumulator.d.ts.map +1 -1
- package/dist/core/accumulators/assistant-message-accumulator.js +19 -1
- package/dist/core/accumulators/assistant-message-accumulator.js.map +1 -1
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +9 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/modules/tool-call.d.ts.map +1 -1
- package/dist/core/modules/tool-call.js +1 -1
- package/dist/core/modules/tool-call.js.map +1 -1
- package/dist/core/object/ObjectStream.test.d.ts +2 -0
- package/dist/core/object/ObjectStream.test.d.ts.map +1 -0
- package/dist/core/object/ObjectStreamAccumulator.d.ts +11 -0
- package/dist/core/object/ObjectStreamAccumulator.d.ts.map +1 -0
- package/dist/core/object/ObjectStreamAccumulator.js +64 -0
- package/dist/core/object/ObjectStreamAccumulator.js.map +1 -0
- package/dist/core/object/ObjectStreamResponse.d.ts +13 -0
- package/dist/core/object/ObjectStreamResponse.d.ts.map +1 -0
- package/dist/core/object/ObjectStreamResponse.js +66 -0
- package/dist/core/object/ObjectStreamResponse.js.map +1 -0
- package/dist/core/object/createObjectStream.d.ts +13 -0
- package/dist/core/object/createObjectStream.d.ts.map +1 -0
- package/dist/core/object/createObjectStream.js +63 -0
- package/dist/core/object/createObjectStream.js.map +1 -0
- package/dist/core/object/types.d.ts +15 -0
- package/dist/core/object/types.d.ts.map +1 -0
- package/dist/core/object/types.js +1 -0
- package/dist/core/object/types.js.map +1 -0
- package/dist/core/serialization/PlainText.d.ts.map +1 -1
- package/dist/core/serialization/PlainText.js.map +1 -1
- package/dist/core/serialization/data-stream/DataStream.d.ts.map +1 -1
- package/dist/core/serialization/data-stream/DataStream.js +14 -0
- package/dist/core/serialization/data-stream/DataStream.js.map +1 -1
- package/dist/core/serialization/data-stream/chunk-types.d.ts +4 -1
- package/dist/core/serialization/data-stream/chunk-types.d.ts.map +1 -1
- package/dist/core/serialization/data-stream/chunk-types.js +1 -0
- package/dist/core/serialization/data-stream/chunk-types.js.map +1 -1
- package/dist/core/tool/ToolCallReader.d.ts.map +1 -1
- package/dist/core/tool/ToolCallReader.js.map +1 -1
- package/dist/core/tool/ToolResponse.d.ts +1 -1
- package/dist/core/tool/ToolResponse.d.ts.map +1 -1
- package/dist/core/tool/ToolResponse.js +3 -1
- package/dist/core/tool/ToolResponse.js.map +1 -1
- package/dist/core/tool/toolResultStream.d.ts.map +1 -1
- package/dist/core/tool/toolResultStream.js +1 -1
- package/dist/core/tool/toolResultStream.js.map +1 -1
- package/dist/core/tool/type-path-utils.d.ts.map +1 -1
- package/dist/core/utils/stream/SSE.d.ts +10 -0
- package/dist/core/utils/stream/SSE.d.ts.map +1 -0
- package/dist/core/utils/stream/SSE.js +102 -0
- package/dist/core/utils/stream/SSE.js.map +1 -0
- package/dist/core/utils/types.d.ts +14 -3
- package/dist/core/utils/types.d.ts.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/AssistantStreamChunk.ts +6 -1
- package/src/core/accumulators/AssistantMessageStream.ts +1 -0
- package/src/core/accumulators/assistant-message-accumulator.ts +25 -1
- package/src/core/index.ts +7 -0
- package/src/core/modules/tool-call.ts +3 -1
- package/src/core/object/ObjectStream.test.ts +376 -0
- package/src/core/object/ObjectStreamAccumulator.ts +80 -0
- package/src/core/object/ObjectStreamResponse.ts +81 -0
- package/src/core/object/createObjectStream.ts +87 -0
- package/src/core/object/types.ts +18 -0
- package/src/core/serialization/PlainText.ts +2 -1
- package/src/core/serialization/data-stream/DataStream.ts +16 -0
- package/src/core/serialization/data-stream/chunk-types.ts +6 -0
- package/src/core/tool/ToolCallReader.ts +0 -3
- package/src/core/tool/ToolResponse.ts +4 -2
- package/src/core/tool/toolResultStream.ts +3 -2
- package/src/core/tool/type-path-utils.ts +0 -2
- package/src/core/utils/stream/SSE.ts +116 -0
- package/src/core/utils/types.ts +18 -3
- package/src/utils/index.ts +4 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/utils/types.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,iBAAiB,EAClB,MAAM,6BAA6B,CAAC;AAErC,KAAK,UAAU,GACX;IACE,IAAI,EAAE,SAAS,CAAC;CACjB,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B,GACD;IACE,IAAI,EAAE,YAAY,CAAC;IACnB,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,gBAAgB,GAAG,OAAO,CAAC;CAC7D,CAAC;AAMN,MAAM,MAAM,QAAQ,GAAG;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;CACpB,CAAC;AAEF,KAAK,cAAc,GACf;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,cAAc,EAAE,OAAO,CAAC;CACzB,GACD;IACE,IAAI,EAAE,iBAAiB,CAAC;IACxB,MAAM,EAAE,kBAAkB,CAAC;CAC5B,GACD;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B,GACD;IACE,IAAI,EAAE,YAAY,CAAC;IACnB,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,gBAAgB,GAAG,OAAO,CAAC;CAC7D,CAAC;AAEN,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/core/utils/types.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { asAsyncIterableStream, type AsyncIterableStream } from "./AsyncIterableStream";
|
|
1
|
+
export { asAsyncIterableStream, type AsyncIterableStream, } from "./AsyncIterableStream";
|
|
2
2
|
export { type ReadonlyJSONObject, type ReadonlyJSONValue } from "./json";
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,qBAAqB,EACrB,KAAK,mBAAmB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,KAAK,kBAAkB,EAAE,KAAK,iBAAiB,EAAE,MAAM,QAAQ,CAAC"}
|
package/dist/utils/index.js
CHANGED
package/dist/utils/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/index.ts"],"sourcesContent":["export {
|
|
1
|
+
{"version":3,"sources":["../../src/utils/index.ts"],"sourcesContent":["export {\n asAsyncIterableStream,\n type AsyncIterableStream,\n} from \"./AsyncIterableStream\";\nexport { type ReadonlyJSONObject, type ReadonlyJSONValue } from \"./json\";\n"],"mappings":";AAAA;AAAA,EACE;AAAA,OAEK;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ReadonlyJSONValue } from "../utils/json/json-value";
|
|
2
|
+
import { ObjectStreamOperation } from "./object/types";
|
|
2
3
|
|
|
3
4
|
export type PartInit =
|
|
4
5
|
| {
|
|
@@ -82,7 +83,7 @@ export type AssistantStreamChunk = { readonly path: readonly number[] } & (
|
|
|
82
83
|
}
|
|
83
84
|
| {
|
|
84
85
|
readonly type: "result";
|
|
85
|
-
readonly artifact?: ReadonlyJSONValue
|
|
86
|
+
readonly artifact?: ReadonlyJSONValue;
|
|
86
87
|
readonly result: ReadonlyJSONValue;
|
|
87
88
|
readonly isError: boolean;
|
|
88
89
|
}
|
|
@@ -90,4 +91,8 @@ export type AssistantStreamChunk = { readonly path: readonly number[] } & (
|
|
|
90
91
|
readonly type: "error";
|
|
91
92
|
readonly error: string;
|
|
92
93
|
}
|
|
94
|
+
| {
|
|
95
|
+
readonly type: "update-state";
|
|
96
|
+
readonly operations: ObjectStreamOperation[];
|
|
97
|
+
}
|
|
93
98
|
);
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ReasoningPart,
|
|
12
12
|
FilePart,
|
|
13
13
|
} from "../utils/types";
|
|
14
|
+
import { ObjectStreamAccumulator } from "../object/ObjectStreamAccumulator";
|
|
14
15
|
|
|
15
16
|
const createInitialMessage = (): AssistantMessage => ({
|
|
16
17
|
role: "assistant",
|
|
@@ -20,6 +21,7 @@ const createInitialMessage = (): AssistantMessage => ({
|
|
|
20
21
|
return this.parts;
|
|
21
22
|
},
|
|
22
23
|
metadata: {
|
|
24
|
+
unstable_state: null,
|
|
23
25
|
unstable_data: [],
|
|
24
26
|
unstable_annotations: [],
|
|
25
27
|
steps: [],
|
|
@@ -132,6 +134,9 @@ const handleToolCallArgsTextFinish = (
|
|
|
132
134
|
if (part.type !== "tool-call") {
|
|
133
135
|
throw new Error("Last is not a tool call");
|
|
134
136
|
}
|
|
137
|
+
if (part.state !== "partial-call")
|
|
138
|
+
throw new Error("Last is not a partial call");
|
|
139
|
+
|
|
135
140
|
return {
|
|
136
141
|
...part,
|
|
137
142
|
state: "call",
|
|
@@ -180,7 +185,7 @@ const handleResult = (
|
|
|
180
185
|
return {
|
|
181
186
|
...part,
|
|
182
187
|
state: "result",
|
|
183
|
-
artifact: chunk.artifact,
|
|
188
|
+
...(chunk.artifact !== undefined ? { artifact: chunk.artifact } : {}),
|
|
184
189
|
result: chunk.result,
|
|
185
190
|
isError: chunk.isError ?? false,
|
|
186
191
|
status: { type: "complete", reason: "stop" },
|
|
@@ -316,6 +321,22 @@ const handleErrorChunk = (
|
|
|
316
321
|
};
|
|
317
322
|
};
|
|
318
323
|
|
|
324
|
+
const handleUpdateState = (
|
|
325
|
+
message: AssistantMessage,
|
|
326
|
+
chunk: AssistantStreamChunk & { type: "update-state" },
|
|
327
|
+
): AssistantMessage => {
|
|
328
|
+
const acc = new ObjectStreamAccumulator(message.metadata.unstable_state);
|
|
329
|
+
acc.append(chunk.operations);
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
...message,
|
|
333
|
+
metadata: {
|
|
334
|
+
...message.metadata,
|
|
335
|
+
unstable_state: acc.state,
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
};
|
|
339
|
+
|
|
319
340
|
export class AssistantMessageAccumulator extends TransformStream<
|
|
320
341
|
AssistantStreamChunk,
|
|
321
342
|
AssistantMessage
|
|
@@ -366,6 +387,9 @@ export class AssistantMessageAccumulator extends TransformStream<
|
|
|
366
387
|
case "error":
|
|
367
388
|
message = handleErrorChunk(message, chunk);
|
|
368
389
|
break;
|
|
390
|
+
case "update-state":
|
|
391
|
+
message = handleUpdateState(message, chunk);
|
|
392
|
+
break;
|
|
369
393
|
default: {
|
|
370
394
|
const unhandledType: never = type;
|
|
371
395
|
throw new Error(`Unsupported chunk type: ${unhandledType}`);
|
package/src/core/index.ts
CHANGED
|
@@ -15,3 +15,10 @@ export { AssistantMessageStream } from "./accumulators/AssistantMessageStream";
|
|
|
15
15
|
export type { AssistantMessage } from "./utils/types";
|
|
16
16
|
|
|
17
17
|
export * from "./tool";
|
|
18
|
+
|
|
19
|
+
export { createObjectStream } from "./object/createObjectStream";
|
|
20
|
+
export {
|
|
21
|
+
ObjectStreamResponse,
|
|
22
|
+
fromObjectStreamResponse,
|
|
23
|
+
} from "./object/ObjectStreamResponse";
|
|
24
|
+
export type { ObjectStreamChunk } from "./object/types";
|
|
@@ -57,7 +57,9 @@ class ToolCallStreamControllerImpl implements ToolCallStreamController {
|
|
|
57
57
|
this._controller.enqueue({
|
|
58
58
|
type: "result",
|
|
59
59
|
path: [],
|
|
60
|
-
|
|
60
|
+
...(response.artifact !== undefined
|
|
61
|
+
? { artifact: response.artifact }
|
|
62
|
+
: {}),
|
|
61
63
|
result: response.result,
|
|
62
64
|
isError: response.isError ?? false,
|
|
63
65
|
});
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createObjectStream } from "./createObjectStream";
|
|
3
|
+
import {
|
|
4
|
+
ObjectStreamEncoder,
|
|
5
|
+
ObjectStreamDecoder,
|
|
6
|
+
} from "./ObjectStreamResponse";
|
|
7
|
+
import { ReadonlyJSONValue } from "../../utils";
|
|
8
|
+
import { ObjectStreamChunk } from "./types";
|
|
9
|
+
|
|
10
|
+
// Helper function to collect all chunks from a stream
|
|
11
|
+
async function collectChunks<T>(stream: ReadableStream<T>): Promise<T[]> {
|
|
12
|
+
const reader = stream.getReader();
|
|
13
|
+
const chunks: T[] = [];
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
while (true) {
|
|
17
|
+
const { done, value } = await reader.read();
|
|
18
|
+
if (done) break;
|
|
19
|
+
chunks.push(value);
|
|
20
|
+
}
|
|
21
|
+
} finally {
|
|
22
|
+
reader.releaseLock();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return chunks;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Helper function to encode and decode a stream
|
|
29
|
+
async function encodeAndDecode(
|
|
30
|
+
stream: ReadableStream<ObjectStreamChunk>,
|
|
31
|
+
): Promise<ReadableStream<ObjectStreamChunk>> {
|
|
32
|
+
// Encode the stream to Uint8Array (simulating network transmission)
|
|
33
|
+
const encodedStream = stream.pipeThrough(new ObjectStreamEncoder());
|
|
34
|
+
|
|
35
|
+
// Collect all encoded chunks
|
|
36
|
+
const encodedChunks = await collectChunks(encodedStream);
|
|
37
|
+
|
|
38
|
+
// Create a new stream from the encoded chunks
|
|
39
|
+
const reconstructedStream = new ReadableStream<Uint8Array>({
|
|
40
|
+
start(controller) {
|
|
41
|
+
for (const chunk of encodedChunks) {
|
|
42
|
+
controller.enqueue(chunk);
|
|
43
|
+
}
|
|
44
|
+
controller.close();
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Decode the stream back to ObjectStreamChunk
|
|
49
|
+
return reconstructedStream.pipeThrough(new ObjectStreamDecoder());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("ObjectStream serialization and deserialization", () => {
|
|
53
|
+
it("should correctly serialize and deserialize simple objects", async () => {
|
|
54
|
+
// Create an object stream with simple operations
|
|
55
|
+
const stream = createObjectStream({
|
|
56
|
+
execute: (controller) => {
|
|
57
|
+
controller.enqueue([
|
|
58
|
+
{ type: "set", path: ["name"], value: "John" },
|
|
59
|
+
{ type: "set", path: ["age"], value: 30 },
|
|
60
|
+
]);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Encode and decode the stream
|
|
65
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
66
|
+
|
|
67
|
+
// Collect all chunks from the decoded stream
|
|
68
|
+
const chunks = await collectChunks(decodedStream);
|
|
69
|
+
|
|
70
|
+
// Verify the final state
|
|
71
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
72
|
+
expect(finalChunk.snapshot).toEqual({
|
|
73
|
+
name: "John",
|
|
74
|
+
age: 30,
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should correctly handle nested objects", async () => {
|
|
79
|
+
const stream = createObjectStream({
|
|
80
|
+
execute: (controller) => {
|
|
81
|
+
controller.enqueue([
|
|
82
|
+
{ type: "set", path: ["user", "profile", "name"], value: "Jane" },
|
|
83
|
+
{
|
|
84
|
+
type: "set",
|
|
85
|
+
path: ["user", "profile", "email"],
|
|
86
|
+
value: "jane@example.com",
|
|
87
|
+
},
|
|
88
|
+
{ type: "set", path: ["user", "settings", "theme"], value: "dark" },
|
|
89
|
+
]);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
94
|
+
const chunks = await collectChunks(decodedStream);
|
|
95
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
96
|
+
|
|
97
|
+
expect(finalChunk.snapshot).toEqual({
|
|
98
|
+
user: {
|
|
99
|
+
profile: {
|
|
100
|
+
name: "Jane",
|
|
101
|
+
email: "jane@example.com",
|
|
102
|
+
},
|
|
103
|
+
settings: {
|
|
104
|
+
theme: "dark",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should correctly handle arrays", async () => {
|
|
111
|
+
const stream = createObjectStream({
|
|
112
|
+
execute: (controller) => {
|
|
113
|
+
controller.enqueue([
|
|
114
|
+
{ type: "set", path: ["items"], value: [] },
|
|
115
|
+
{ type: "set", path: ["items", "0"], value: "apple" },
|
|
116
|
+
{ type: "set", path: ["items", "1"], value: "banana" },
|
|
117
|
+
{ type: "set", path: ["items", "2"], value: "cherry" },
|
|
118
|
+
]);
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
123
|
+
const chunks = await collectChunks(decodedStream);
|
|
124
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
125
|
+
|
|
126
|
+
expect(finalChunk.snapshot).toEqual({
|
|
127
|
+
items: ["apple", "banana", "cherry"],
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("should correctly handle mixed arrays and objects", async () => {
|
|
132
|
+
const stream = createObjectStream({
|
|
133
|
+
execute: (controller) => {
|
|
134
|
+
controller.enqueue([
|
|
135
|
+
{ type: "set", path: ["users"], value: [] },
|
|
136
|
+
{ type: "set", path: ["users", "0"], value: {} },
|
|
137
|
+
{ type: "set", path: ["users", "0", "id"], value: 1 },
|
|
138
|
+
{ type: "set", path: ["users", "0", "name"], value: "Alice" },
|
|
139
|
+
{ type: "set", path: ["users", "1"], value: {} },
|
|
140
|
+
{ type: "set", path: ["users", "1", "id"], value: 2 },
|
|
141
|
+
{ type: "set", path: ["users", "1", "name"], value: "Bob" },
|
|
142
|
+
]);
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
147
|
+
const chunks = await collectChunks(decodedStream);
|
|
148
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
149
|
+
|
|
150
|
+
expect(finalChunk.snapshot).toEqual({
|
|
151
|
+
users: [
|
|
152
|
+
{ id: 1, name: "Alice" },
|
|
153
|
+
{ id: 2, name: "Bob" },
|
|
154
|
+
],
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("should correctly handle append-text operations", async () => {
|
|
159
|
+
const stream = createObjectStream({
|
|
160
|
+
execute: (controller) => {
|
|
161
|
+
controller.enqueue([
|
|
162
|
+
{ type: "set", path: ["message"], value: "Hello" },
|
|
163
|
+
{ type: "append-text", path: ["message"], value: " " },
|
|
164
|
+
{ type: "append-text", path: ["message"], value: "World" },
|
|
165
|
+
{ type: "append-text", path: ["message"], value: "!" },
|
|
166
|
+
]);
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
171
|
+
const chunks = await collectChunks(decodedStream);
|
|
172
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
173
|
+
|
|
174
|
+
expect(finalChunk.snapshot).toEqual({
|
|
175
|
+
message: "Hello World!",
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("should correctly handle special characters and Unicode", async () => {
|
|
180
|
+
const stream = createObjectStream({
|
|
181
|
+
execute: (controller) => {
|
|
182
|
+
controller.enqueue([
|
|
183
|
+
{
|
|
184
|
+
type: "set",
|
|
185
|
+
path: ["special"],
|
|
186
|
+
value: "Special chars: !@#$%^&*()",
|
|
187
|
+
},
|
|
188
|
+
{ type: "set", path: ["unicode"], value: "Unicode: 😀🌍🚀" },
|
|
189
|
+
{ type: "set", path: ["quotes"], value: "Quotes: \"'`" },
|
|
190
|
+
{
|
|
191
|
+
type: "set",
|
|
192
|
+
path: ["newlines"],
|
|
193
|
+
value: "Line 1\nLine 2\r\nLine 3",
|
|
194
|
+
},
|
|
195
|
+
]);
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
200
|
+
const chunks = await collectChunks(decodedStream);
|
|
201
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
202
|
+
|
|
203
|
+
expect(finalChunk.snapshot).toEqual({
|
|
204
|
+
special: "Special chars: !@#$%^&*()",
|
|
205
|
+
unicode: "Unicode: 😀🌍🚀",
|
|
206
|
+
quotes: "Quotes: \"'`",
|
|
207
|
+
newlines: "Line 1\nLine 2\r\nLine 3",
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("should correctly handle null and undefined values", async () => {
|
|
212
|
+
const stream = createObjectStream({
|
|
213
|
+
execute: (controller) => {
|
|
214
|
+
controller.enqueue([
|
|
215
|
+
{ type: "set", path: ["nullValue"], value: null },
|
|
216
|
+
{ type: "set", path: ["emptyObject"], value: {} },
|
|
217
|
+
{ type: "set", path: ["emptyArray"], value: [] },
|
|
218
|
+
]);
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
223
|
+
const chunks = await collectChunks(decodedStream);
|
|
224
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
225
|
+
|
|
226
|
+
expect(finalChunk.snapshot).toEqual({
|
|
227
|
+
nullValue: null,
|
|
228
|
+
emptyObject: {},
|
|
229
|
+
emptyArray: [],
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should correctly handle large nested structures", async () => {
|
|
234
|
+
// Create a deep nested structure
|
|
235
|
+
const stream = createObjectStream({
|
|
236
|
+
execute: (controller) => {
|
|
237
|
+
controller.enqueue([
|
|
238
|
+
{ type: "set", path: ["level1"], value: {} },
|
|
239
|
+
{ type: "set", path: ["level1", "level2"], value: {} },
|
|
240
|
+
{ type: "set", path: ["level1", "level2", "level3"], value: {} },
|
|
241
|
+
{
|
|
242
|
+
type: "set",
|
|
243
|
+
path: ["level1", "level2", "level3", "level4"],
|
|
244
|
+
value: {},
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
type: "set",
|
|
248
|
+
path: ["level1", "level2", "level3", "level4", "level5"],
|
|
249
|
+
value: "deep value",
|
|
250
|
+
},
|
|
251
|
+
]);
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
256
|
+
const chunks = await collectChunks(decodedStream);
|
|
257
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
258
|
+
|
|
259
|
+
expect(finalChunk.snapshot).toEqual({
|
|
260
|
+
level1: {
|
|
261
|
+
level2: {
|
|
262
|
+
level3: {
|
|
263
|
+
level4: {
|
|
264
|
+
level5: "deep value",
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("should correctly handle operations in multiple enqueue calls", async () => {
|
|
273
|
+
const stream = createObjectStream({
|
|
274
|
+
execute: (controller) => {
|
|
275
|
+
// First batch of operations
|
|
276
|
+
controller.enqueue([
|
|
277
|
+
{ type: "set", path: ["user"], value: { name: "Initial" } },
|
|
278
|
+
]);
|
|
279
|
+
|
|
280
|
+
// Second batch of operations
|
|
281
|
+
controller.enqueue([
|
|
282
|
+
{ type: "set", path: ["user", "name"], value: "Updated" },
|
|
283
|
+
{ type: "set", path: ["user", "email"], value: "user@example.com" },
|
|
284
|
+
]);
|
|
285
|
+
|
|
286
|
+
// Third batch of operations
|
|
287
|
+
controller.enqueue([
|
|
288
|
+
{ type: "set", path: ["status"], value: "complete" },
|
|
289
|
+
]);
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
294
|
+
const chunks = await collectChunks(decodedStream);
|
|
295
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
296
|
+
|
|
297
|
+
expect(finalChunk.snapshot).toEqual({
|
|
298
|
+
user: {
|
|
299
|
+
name: "Updated",
|
|
300
|
+
email: "user@example.com",
|
|
301
|
+
},
|
|
302
|
+
status: "complete",
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Verify that we got the correct number of chunks
|
|
306
|
+
expect(chunks.length).toBe(3);
|
|
307
|
+
|
|
308
|
+
// Verify intermediate states
|
|
309
|
+
expect(chunks[0]!.snapshot).toEqual({
|
|
310
|
+
user: { name: "Initial" },
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
expect(chunks[1]!.snapshot).toEqual({
|
|
314
|
+
user: {
|
|
315
|
+
name: "Updated",
|
|
316
|
+
email: "user@example.com",
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("should correctly handle overwriting existing values", async () => {
|
|
322
|
+
const stream = createObjectStream({
|
|
323
|
+
execute: (controller) => {
|
|
324
|
+
controller.enqueue([
|
|
325
|
+
{ type: "set", path: ["value"], value: "initial" },
|
|
326
|
+
{ type: "set", path: ["nested"], value: { prop: "initial" } },
|
|
327
|
+
]);
|
|
328
|
+
|
|
329
|
+
controller.enqueue([
|
|
330
|
+
{ type: "set", path: ["value"], value: "updated" },
|
|
331
|
+
{ type: "set", path: ["nested"], value: "completely replaced" },
|
|
332
|
+
]);
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
337
|
+
const chunks = await collectChunks(decodedStream);
|
|
338
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
339
|
+
|
|
340
|
+
expect(finalChunk.snapshot).toEqual({
|
|
341
|
+
value: "updated",
|
|
342
|
+
nested: "completely replaced",
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("should correctly handle custom initial values", async () => {
|
|
347
|
+
const initialValue: ReadonlyJSONValue = {
|
|
348
|
+
existing: "value",
|
|
349
|
+
nested: {
|
|
350
|
+
prop: 123,
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const stream = createObjectStream({
|
|
355
|
+
defaultValue: initialValue,
|
|
356
|
+
execute: (controller) => {
|
|
357
|
+
controller.enqueue([
|
|
358
|
+
{ type: "set", path: ["new"], value: "added" },
|
|
359
|
+
{ type: "set", path: ["nested", "prop"], value: 456 },
|
|
360
|
+
]);
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
365
|
+
const chunks = await collectChunks(decodedStream);
|
|
366
|
+
const finalChunk = chunks[chunks.length - 1]!;
|
|
367
|
+
|
|
368
|
+
expect(finalChunk.snapshot).toEqual({
|
|
369
|
+
existing: "value",
|
|
370
|
+
nested: {
|
|
371
|
+
prop: 456,
|
|
372
|
+
},
|
|
373
|
+
new: "added",
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ReadonlyJSONValue, ReadonlyJSONObject } from "../../utils";
|
|
2
|
+
import { ObjectStreamOperation } from "./types";
|
|
3
|
+
|
|
4
|
+
export class ObjectStreamAccumulator {
|
|
5
|
+
private _state: ReadonlyJSONValue;
|
|
6
|
+
|
|
7
|
+
constructor(initialValue: ReadonlyJSONValue = null) {
|
|
8
|
+
this._state = initialValue;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get state() {
|
|
12
|
+
return this._state;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
append(ops: readonly ObjectStreamOperation[]) {
|
|
16
|
+
this._state = ops.reduce(
|
|
17
|
+
(state, op) => ObjectStreamAccumulator.apply(state, op),
|
|
18
|
+
this._state,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private static apply(state: ReadonlyJSONValue, op: ObjectStreamOperation) {
|
|
23
|
+
const type = op.type;
|
|
24
|
+
switch (type) {
|
|
25
|
+
case "set":
|
|
26
|
+
return ObjectStreamAccumulator.updatePath(
|
|
27
|
+
state,
|
|
28
|
+
op.path,
|
|
29
|
+
() => op.value,
|
|
30
|
+
);
|
|
31
|
+
case "append-text":
|
|
32
|
+
return ObjectStreamAccumulator.updatePath(state, op.path, (current) => {
|
|
33
|
+
if (typeof current !== "string")
|
|
34
|
+
throw new Error(`Expected string at path [${op.path.join(", ")}]`);
|
|
35
|
+
return current + op.value;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
default: {
|
|
39
|
+
const _exhaustiveCheck: never = type;
|
|
40
|
+
throw new Error(`Invalid operation type: ${_exhaustiveCheck}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private static updatePath(
|
|
46
|
+
state: ReadonlyJSONValue | undefined,
|
|
47
|
+
path: readonly string[],
|
|
48
|
+
updater: (current: ReadonlyJSONValue | undefined) => ReadonlyJSONValue,
|
|
49
|
+
): ReadonlyJSONValue {
|
|
50
|
+
if (path.length === 0) return updater(state);
|
|
51
|
+
|
|
52
|
+
// Initialize state as empty object if it's null and we're trying to set a property
|
|
53
|
+
if (state === null) {
|
|
54
|
+
state = {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof state !== "object") {
|
|
58
|
+
throw new Error(`Invalid path: [${path.join(", ")}]`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const [key, ...rest] = path as [string, ...(readonly string[])];
|
|
62
|
+
if (Array.isArray(state)) {
|
|
63
|
+
const idx = Number(key);
|
|
64
|
+
if (isNaN(idx))
|
|
65
|
+
throw new Error(`Expected array index at [${path.join(", ")}]`);
|
|
66
|
+
if (idx > state.length || idx < 0)
|
|
67
|
+
throw new Error(`Insert array index out of bounds`);
|
|
68
|
+
|
|
69
|
+
const nextState = [...state];
|
|
70
|
+
nextState[idx] = this.updatePath(nextState[idx], rest, updater);
|
|
71
|
+
|
|
72
|
+
return nextState;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const nextState = { ...(state as ReadonlyJSONObject) };
|
|
76
|
+
nextState[key] = this.updatePath(nextState[key], rest, updater);
|
|
77
|
+
|
|
78
|
+
return nextState;
|
|
79
|
+
}
|
|
80
|
+
}
|