assistant-stream 0.3.25 → 0.3.27
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 +4 -1
- package/dist/core/AssistantStream.d.ts +0 -1
- package/dist/core/AssistantStream.d.ts.map +1 -1
- package/dist/core/AssistantStreamChunk.d.ts +26 -14
- package/dist/core/AssistantStreamChunk.d.ts.map +1 -1
- package/dist/core/accumulators/AssistantMessageStream.d.ts +0 -1
- package/dist/core/accumulators/AssistantMessageStream.d.ts.map +1 -1
- package/dist/core/accumulators/TimingTracker.d.ts +0 -1
- package/dist/core/accumulators/TimingTracker.d.ts.map +1 -1
- package/dist/core/accumulators/assistant-message-accumulator.d.ts +3 -9
- package/dist/core/accumulators/assistant-message-accumulator.d.ts.map +1 -1
- package/dist/core/accumulators/assistant-message-accumulator.js +70 -28
- package/dist/core/accumulators/assistant-message-accumulator.js.map +1 -1
- package/dist/core/converters/toGenericMessages.d.ts.map +1 -1
- package/dist/core/converters/toGenericMessages.js +2 -1
- package/dist/core/converters/toGenericMessages.js.map +1 -1
- package/dist/core/gorp/GorpStreamAccumulator.d.ts +15 -0
- package/dist/core/gorp/GorpStreamAccumulator.d.ts.map +1 -0
- package/dist/core/{object/ObjectStreamAccumulator.js → gorp/GorpStreamAccumulator.js} +11 -9
- package/dist/core/gorp/GorpStreamAccumulator.js.map +1 -0
- package/dist/core/gorp/GorpStreamDeltaTracker.d.ts +17 -0
- package/dist/core/gorp/GorpStreamDeltaTracker.d.ts.map +1 -0
- package/dist/core/gorp/GorpStreamDeltaTracker.js +38 -0
- package/dist/core/gorp/GorpStreamDeltaTracker.js.map +1 -0
- package/dist/core/gorp/GorpStreamResponse.d.ts +16 -0
- package/dist/core/gorp/GorpStreamResponse.d.ts.map +1 -0
- package/dist/core/gorp/GorpStreamResponse.js +51 -0
- package/dist/core/gorp/GorpStreamResponse.js.map +1 -0
- package/dist/core/gorp/changeTree.d.ts +16 -0
- package/dist/core/gorp/changeTree.d.ts.map +1 -0
- package/dist/core/gorp/changeTree.js +77 -0
- package/dist/core/gorp/changeTree.js.map +1 -0
- package/dist/core/gorp/createGorpStream.d.ts +16 -0
- package/dist/core/gorp/createGorpStream.d.ts.map +1 -0
- package/dist/core/{object/createObjectStream.js → gorp/createGorpStream.js} +13 -8
- package/dist/core/gorp/createGorpStream.js.map +1 -0
- package/dist/core/gorp/types.d.ts +20 -0
- package/dist/core/gorp/types.d.ts.map +1 -0
- package/dist/core/modules/assistant-stream.d.ts +18 -11
- package/dist/core/modules/assistant-stream.d.ts.map +1 -1
- package/dist/core/modules/assistant-stream.js +10 -6
- package/dist/core/modules/assistant-stream.js.map +1 -1
- package/dist/core/modules/text.d.ts +0 -1
- package/dist/core/modules/text.d.ts.map +1 -1
- package/dist/core/modules/tool-call.d.ts +0 -1
- package/dist/core/modules/tool-call.d.ts.map +1 -1
- package/dist/core/modules/tool-call.js +2 -2
- package/dist/core/modules/tool-call.js.map +1 -1
- package/dist/core/serialization/PlainText.d.ts +0 -1
- package/dist/core/serialization/PlainText.d.ts.map +1 -1
- package/dist/core/serialization/assistant-transport/AssistantTransport.d.ts +0 -1
- package/dist/core/serialization/assistant-transport/AssistantTransport.d.ts.map +1 -1
- package/dist/core/serialization/assistant-transport/AssistantTransport.js +93 -53
- package/dist/core/serialization/assistant-transport/AssistantTransport.js.map +1 -1
- package/dist/core/serialization/data-stream/DataStream.d.ts +0 -1
- package/dist/core/serialization/data-stream/DataStream.d.ts.map +1 -1
- package/dist/core/serialization/data-stream/chunk-types.d.ts +3 -4
- package/dist/core/serialization/data-stream/chunk-types.d.ts.map +1 -1
- package/dist/core/serialization/data-stream/chunk-types.js.map +1 -1
- package/dist/core/serialization/data-stream/serialization.d.ts +0 -1
- package/dist/core/serialization/data-stream/serialization.d.ts.map +1 -1
- package/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts +0 -1
- package/dist/core/serialization/ui-message-stream/UIMessageStream.d.ts.map +1 -1
- package/dist/core/serialization/ui-message-stream/UIMessageStream.js +76 -53
- package/dist/core/serialization/ui-message-stream/UIMessageStream.js.map +1 -1
- package/dist/core/serialization/ui-message-stream/chunk-types.d.ts +0 -1
- package/dist/core/serialization/ui-message-stream/chunk-types.d.ts.map +1 -1
- package/dist/core/tool/ToolCallReader.d.ts +1 -0
- package/dist/core/tool/ToolCallReader.d.ts.map +1 -1
- package/dist/core/tool/ToolCallReader.js.map +1 -1
- package/dist/core/tool/ToolExecutionStream.d.ts +0 -1
- package/dist/core/tool/ToolExecutionStream.d.ts.map +1 -1
- package/dist/core/tool/ToolExecutionStream.js +9 -1
- package/dist/core/tool/ToolExecutionStream.js.map +1 -1
- package/dist/core/tool/ToolResponse.d.ts +6 -4
- package/dist/core/tool/ToolResponse.d.ts.map +1 -1
- package/dist/core/tool/schema-utils.d.ts +0 -1
- package/dist/core/tool/schema-utils.d.ts.map +1 -1
- package/dist/core/tool/tool-types.d.ts +71 -33
- package/dist/core/tool/tool-types.d.ts.map +1 -1
- package/dist/core/tool/toolResultStream.d.ts +3 -2
- package/dist/core/tool/toolResultStream.d.ts.map +1 -1
- package/dist/core/tool/toolResultStream.js +7 -2
- package/dist/core/tool/toolResultStream.js.map +1 -1
- package/dist/core/tool/type-path-utils.d.ts +2 -2
- package/dist/core/tool/type-path-utils.d.ts.map +1 -1
- package/dist/core/utils/Counter.d.ts.map +1 -1
- package/dist/core/utils/generateId.d.ts.map +1 -1
- package/dist/core/utils/stream/AssistantMetaTransformStream.d.ts +0 -1
- package/dist/core/utils/stream/AssistantMetaTransformStream.d.ts.map +1 -1
- package/dist/core/utils/stream/AssistantTransformStream.d.ts +0 -1
- package/dist/core/utils/stream/AssistantTransformStream.d.ts.map +1 -1
- package/dist/core/utils/stream/LineDecoderStream.d.ts +1 -0
- package/dist/core/utils/stream/LineDecoderStream.d.ts.map +1 -1
- package/dist/core/utils/stream/LineDecoderStream.js +19 -6
- package/dist/core/utils/stream/LineDecoderStream.js.map +1 -1
- package/dist/core/utils/stream/PipeableTransformStream.d.ts.map +1 -1
- package/dist/core/utils/stream/SSE.d.ts +0 -1
- package/dist/core/utils/stream/SSE.d.ts.map +1 -1
- package/dist/core/utils/stream/SSE.js +2 -52
- package/dist/core/utils/stream/SSE.js.map +1 -1
- package/dist/core/utils/stream/SSEEventDecoder.d.ts +27 -0
- package/dist/core/utils/stream/SSEEventDecoder.d.ts.map +1 -0
- package/dist/core/utils/stream/SSEEventDecoder.js +84 -0
- package/dist/core/utils/stream/SSEEventDecoder.js.map +1 -0
- package/dist/core/utils/stream/SSEEventDecoderStream.d.ts +13 -0
- package/dist/core/utils/stream/SSEEventDecoderStream.d.ts.map +1 -0
- package/dist/core/utils/stream/SSEEventDecoderStream.js +24 -0
- package/dist/core/utils/stream/SSEEventDecoderStream.js.map +1 -0
- package/dist/core/utils/stream/UnderlyingReadable.d.ts.map +1 -1
- package/dist/core/utils/stream/merge.d.ts +2 -1
- package/dist/core/utils/stream/merge.d.ts.map +1 -1
- package/dist/core/utils/stream/merge.js +27 -8
- package/dist/core/utils/stream/merge.js.map +1 -1
- package/dist/core/utils/stream/path-utils.d.ts +0 -1
- package/dist/core/utils/stream/path-utils.d.ts.map +1 -1
- package/dist/core/utils/types.d.ts +16 -8
- package/dist/core/utils/types.d.ts.map +1 -1
- package/dist/core/utils/withPromiseOrValue.d.ts.map +1 -1
- package/dist/index.d.ts +25 -4
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -3
- package/dist/index.js.map +1 -0
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts +3 -3
- package/dist/node_modules/.pnpm/@types_json-schema@7.0.15/node_modules/@types/json-schema/index.d.ts.map +1 -1
- package/dist/resumable/ResumableStreamContext.d.ts +0 -1
- package/dist/resumable/ResumableStreamContext.d.ts.map +1 -1
- package/dist/resumable/ResumableStreamContext.js +12 -5
- package/dist/resumable/ResumableStreamContext.js.map +1 -1
- package/dist/resumable/constants.d.ts.map +1 -1
- package/dist/resumable/createResumableAssistantStreamResponse.d.ts +6 -4
- package/dist/resumable/createResumableAssistantStreamResponse.d.ts.map +1 -1
- package/dist/resumable/errors.d.ts.map +1 -1
- package/dist/resumable/stores/InMemoryResumableStreamStore.d.ts +0 -1
- package/dist/resumable/stores/InMemoryResumableStreamStore.d.ts.map +1 -1
- package/dist/resumable/stores/InMemoryResumableStreamStore.js +2 -1
- package/dist/resumable/stores/InMemoryResumableStreamStore.js.map +1 -1
- package/dist/resumable/stores/ioredis.d.ts +0 -1
- package/dist/resumable/stores/ioredis.d.ts.map +1 -1
- package/dist/resumable/stores/redis-impl.d.ts +2 -2
- package/dist/resumable/stores/redis-impl.d.ts.map +1 -1
- package/dist/resumable/stores/redis.d.ts +0 -1
- package/dist/resumable/stores/redis.d.ts.map +1 -1
- package/dist/resumable/types.d.ts.map +1 -1
- package/dist/utils/AsyncIterableStream.d.ts.map +1 -1
- package/dist/utils/AsyncIterableStream.js +18 -3
- package/dist/utils/AsyncIterableStream.js.map +1 -1
- package/dist/utils/json/fix-json.d.ts.map +1 -1
- package/dist/utils/json/json-value.d.ts.map +1 -1
- package/dist/utils/json/parse-partial-json-object.d.ts +0 -1
- package/dist/utils/json/parse-partial-json-object.d.ts.map +1 -1
- package/dist/utils/promiseWithResolvers.d.ts.map +1 -1
- package/dist/utils.d.ts +2 -1
- package/dist/utils.js +2 -1
- package/package.json +5 -5
- package/src/core/AssistantStreamChunk.ts +5 -3
- package/src/core/accumulators/assistant-message-accumulator.test.ts +437 -1
- package/src/core/accumulators/assistant-message-accumulator.ts +89 -26
- package/src/core/{object/ObjectStream.test.ts → gorp/GorpStream.test.ts} +104 -23
- package/src/core/gorp/GorpStreamAccumulator.test.ts +28 -0
- package/src/core/{object/ObjectStreamAccumulator.ts → gorp/GorpStreamAccumulator.ts} +13 -15
- package/src/core/gorp/GorpStreamDeltaTracker.test.ts +182 -0
- package/src/core/gorp/GorpStreamDeltaTracker.ts +55 -0
- package/src/core/gorp/GorpStreamResponse.test.ts +32 -0
- package/src/core/gorp/GorpStreamResponse.ts +88 -0
- package/src/core/gorp/changeTree.test.ts +168 -0
- package/src/core/gorp/changeTree.ts +96 -0
- package/src/core/{object/createObjectStream.ts → gorp/createGorpStream.ts} +23 -17
- package/src/core/{object → gorp}/types.ts +5 -3
- package/src/core/modules/assistant-stream.test.ts +154 -2
- package/src/core/modules/assistant-stream.ts +16 -6
- package/src/core/modules/tool-call.test.ts +67 -0
- package/src/core/modules/tool-call.ts +3 -3
- package/src/core/serialization/assistant-transport/AssistantTransport.test.ts +215 -1
- package/src/core/serialization/assistant-transport/AssistantTransport.ts +83 -71
- package/src/core/serialization/data-stream/chunk-types.ts +2 -2
- package/src/core/serialization/ui-message-stream/UIMessageStream.test.ts +323 -4
- package/src/core/serialization/ui-message-stream/UIMessageStream.ts +81 -71
- package/src/core/tool/ToolExecutionStream.ts +10 -0
- package/src/core/tool/toolResultStream.test.ts +50 -0
- package/src/core/tool/toolResultStream.ts +7 -2
- package/src/core/utils/stream/LineDecoderStream.test.ts +26 -2
- package/src/core/utils/stream/LineDecoderStream.ts +21 -11
- package/src/core/utils/stream/SSE.ts +6 -70
- package/src/core/utils/stream/SSEEventDecoder.test.ts +146 -0
- package/src/core/utils/stream/SSEEventDecoder.ts +109 -0
- package/src/core/utils/stream/SSEEventDecoderStream.test.ts +42 -0
- package/src/core/utils/stream/SSEEventDecoderStream.ts +34 -0
- package/src/core/utils/stream/merge.ts +31 -9
- package/src/gorp.test.ts +42 -0
- package/src/index.ts +21 -6
- package/src/resumable/ResumableStreamContext.test.ts +116 -1
- package/src/resumable/ResumableStreamContext.ts +32 -5
- package/src/resumable/createResumableAssistantStreamResponse.test.ts +1 -25
- package/src/utils/AsyncIterableStream.test.ts +61 -0
- package/src/utils/AsyncIterableStream.ts +18 -3
- package/src/utils.ts +4 -0
- package/dist/core/object/ObjectStreamAccumulator.d.ts +0 -15
- package/dist/core/object/ObjectStreamAccumulator.d.ts.map +0 -1
- package/dist/core/object/ObjectStreamAccumulator.js.map +0 -1
- package/dist/core/object/ObjectStreamResponse.d.ts +0 -17
- package/dist/core/object/ObjectStreamResponse.d.ts.map +0 -1
- package/dist/core/object/ObjectStreamResponse.js +0 -57
- package/dist/core/object/ObjectStreamResponse.js.map +0 -1
- package/dist/core/object/createObjectStream.d.ts +0 -19
- package/dist/core/object/createObjectStream.d.ts.map +0 -1
- package/dist/core/object/createObjectStream.js.map +0 -1
- package/dist/core/object/types.d.ts +0 -18
- package/dist/core/object/types.d.ts.map +0 -1
- package/dist/utils/json/is-json.d.ts +0 -9
- package/dist/utils/json/is-json.d.ts.map +0 -1
- package/dist/utils/json/is-json.js +0 -17
- package/dist/utils/json/is-json.js.map +0 -1
- package/src/core/object/ObjectStreamResponse.ts +0 -109
- package/src/utils/json/is-json.ts +0 -43
- /package/dist/core/{object → gorp}/types.js +0 -0
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
ObjectStreamEncoder,
|
|
5
|
-
ObjectStreamDecoder,
|
|
6
|
-
} from "./ObjectStreamResponse";
|
|
2
|
+
import { createGorpStream } from "./createGorpStream";
|
|
3
|
+
import { GorpStreamEncoder, GorpStreamDecoder } from "./GorpStreamResponse";
|
|
7
4
|
import type { ReadonlyJSONValue } from "../../utils";
|
|
8
|
-
import type {
|
|
5
|
+
import type { GorpStreamChunk } from "./types";
|
|
9
6
|
|
|
10
7
|
// Helper function to collect all chunks from a stream
|
|
11
8
|
async function collectChunks<T>(stream: ReadableStream<T>): Promise<T[]> {
|
|
@@ -27,10 +24,10 @@ async function collectChunks<T>(stream: ReadableStream<T>): Promise<T[]> {
|
|
|
27
24
|
|
|
28
25
|
// Helper function to encode and decode a stream
|
|
29
26
|
async function encodeAndDecode(
|
|
30
|
-
stream: ReadableStream<
|
|
31
|
-
): Promise<ReadableStream<
|
|
27
|
+
stream: ReadableStream<GorpStreamChunk>,
|
|
28
|
+
): Promise<ReadableStream<GorpStreamChunk>> {
|
|
32
29
|
// Encode the stream to Uint8Array (simulating network transmission)
|
|
33
|
-
const encodedStream = stream.pipeThrough(new
|
|
30
|
+
const encodedStream = stream.pipeThrough(new GorpStreamEncoder());
|
|
34
31
|
|
|
35
32
|
// Collect all encoded chunks
|
|
36
33
|
const encodedChunks = await collectChunks(encodedStream);
|
|
@@ -45,14 +42,58 @@ async function encodeAndDecode(
|
|
|
45
42
|
},
|
|
46
43
|
});
|
|
47
44
|
|
|
48
|
-
// Decode the stream back to
|
|
49
|
-
return reconstructedStream.pipeThrough(new
|
|
45
|
+
// Decode the stream back to GorpStreamChunk
|
|
46
|
+
return reconstructedStream.pipeThrough(new GorpStreamDecoder());
|
|
50
47
|
}
|
|
51
48
|
|
|
52
|
-
describe("
|
|
49
|
+
describe("GorpStream serialization and deserialization", () => {
|
|
50
|
+
it("does not settle the stream after cancellation", async () => {
|
|
51
|
+
let finishExecution!: () => void;
|
|
52
|
+
let abortSignal!: AbortSignal;
|
|
53
|
+
const execution = new Promise<void>((resolve) => {
|
|
54
|
+
finishExecution = resolve;
|
|
55
|
+
});
|
|
56
|
+
const stream = createGorpStream({
|
|
57
|
+
execute: (controller) => {
|
|
58
|
+
abortSignal = controller.abortSignal;
|
|
59
|
+
return execution;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const reader = stream.getReader();
|
|
63
|
+
|
|
64
|
+
await reader.cancel("consumer stopped");
|
|
65
|
+
|
|
66
|
+
expect(abortSignal.aborted).toBe(true);
|
|
67
|
+
expect(abortSignal.reason).toBe("consumer stopped");
|
|
68
|
+
|
|
69
|
+
finishExecution();
|
|
70
|
+
await execution;
|
|
71
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should discard an unterminated SSE event", async () => {
|
|
75
|
+
const operations: GorpStreamChunk["operations"] = [
|
|
76
|
+
{ type: "set", path: ["status"], value: "complete" },
|
|
77
|
+
];
|
|
78
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
79
|
+
start(controller) {
|
|
80
|
+
controller.enqueue(
|
|
81
|
+
new TextEncoder().encode(`data: ${JSON.stringify(operations)}\n`),
|
|
82
|
+
);
|
|
83
|
+
controller.close();
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const chunks = await collectChunks(
|
|
88
|
+
stream.pipeThrough(new GorpStreamDecoder()),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(chunks).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
|
|
53
94
|
it("should correctly serialize and deserialize simple objects", async () => {
|
|
54
95
|
// Create an object stream with simple operations
|
|
55
|
-
const stream =
|
|
96
|
+
const stream = createGorpStream({
|
|
56
97
|
execute: (controller) => {
|
|
57
98
|
controller.enqueue([
|
|
58
99
|
{ type: "set", path: ["name"], value: "John" },
|
|
@@ -76,7 +117,7 @@ describe("ObjectStream serialization and deserialization", () => {
|
|
|
76
117
|
});
|
|
77
118
|
|
|
78
119
|
it("should correctly handle nested objects", async () => {
|
|
79
|
-
const stream =
|
|
120
|
+
const stream = createGorpStream({
|
|
80
121
|
execute: (controller) => {
|
|
81
122
|
controller.enqueue([
|
|
82
123
|
{ type: "set", path: ["user", "profile", "name"], value: "Jane" },
|
|
@@ -108,7 +149,7 @@ describe("ObjectStream serialization and deserialization", () => {
|
|
|
108
149
|
});
|
|
109
150
|
|
|
110
151
|
it("should correctly handle arrays", async () => {
|
|
111
|
-
const stream =
|
|
152
|
+
const stream = createGorpStream({
|
|
112
153
|
execute: (controller) => {
|
|
113
154
|
controller.enqueue([
|
|
114
155
|
{ type: "set", path: ["items"], value: [] },
|
|
@@ -129,7 +170,7 @@ describe("ObjectStream serialization and deserialization", () => {
|
|
|
129
170
|
});
|
|
130
171
|
|
|
131
172
|
it("should correctly handle mixed arrays and objects", async () => {
|
|
132
|
-
const stream =
|
|
173
|
+
const stream = createGorpStream({
|
|
133
174
|
execute: (controller) => {
|
|
134
175
|
controller.enqueue([
|
|
135
176
|
{ type: "set", path: ["users"], value: [] },
|
|
@@ -156,7 +197,7 @@ describe("ObjectStream serialization and deserialization", () => {
|
|
|
156
197
|
});
|
|
157
198
|
|
|
158
199
|
it("should correctly handle append-text operations", async () => {
|
|
159
|
-
const stream =
|
|
200
|
+
const stream = createGorpStream({
|
|
160
201
|
execute: (controller) => {
|
|
161
202
|
controller.enqueue([
|
|
162
203
|
{ type: "set", path: ["message"], value: "Hello" },
|
|
@@ -176,8 +217,48 @@ describe("ObjectStream serialization and deserialization", () => {
|
|
|
176
217
|
});
|
|
177
218
|
});
|
|
178
219
|
|
|
220
|
+
it("does not apply first-chunk operations twice", async () => {
|
|
221
|
+
const stream = createGorpStream({
|
|
222
|
+
defaultValue: { message: "Hello" },
|
|
223
|
+
execute: (controller) => {
|
|
224
|
+
controller.enqueue([
|
|
225
|
+
{ type: "append-text", path: ["message"], value: " world" },
|
|
226
|
+
]);
|
|
227
|
+
controller.enqueue([
|
|
228
|
+
{ type: "append-text", path: ["message"], value: "!" },
|
|
229
|
+
]);
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
const decodedStream = await encodeAndDecode(stream);
|
|
234
|
+
const chunks = await collectChunks(decodedStream);
|
|
235
|
+
|
|
236
|
+
expect(chunks).toEqual([
|
|
237
|
+
{
|
|
238
|
+
snapshot: { message: "Hello world" },
|
|
239
|
+
operations: [
|
|
240
|
+
{
|
|
241
|
+
type: "set",
|
|
242
|
+
path: [],
|
|
243
|
+
value: { message: "Hello world" },
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
snapshot: { message: "Hello world!" },
|
|
249
|
+
operations: [
|
|
250
|
+
{
|
|
251
|
+
type: "append-text",
|
|
252
|
+
path: ["message"],
|
|
253
|
+
value: "!",
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
},
|
|
257
|
+
]);
|
|
258
|
+
});
|
|
259
|
+
|
|
179
260
|
it("should correctly handle special characters and Unicode", async () => {
|
|
180
|
-
const stream =
|
|
261
|
+
const stream = createGorpStream({
|
|
181
262
|
execute: (controller) => {
|
|
182
263
|
controller.enqueue([
|
|
183
264
|
{
|
|
@@ -209,7 +290,7 @@ describe("ObjectStream serialization and deserialization", () => {
|
|
|
209
290
|
});
|
|
210
291
|
|
|
211
292
|
it("should correctly handle null and undefined values", async () => {
|
|
212
|
-
const stream =
|
|
293
|
+
const stream = createGorpStream({
|
|
213
294
|
execute: (controller) => {
|
|
214
295
|
controller.enqueue([
|
|
215
296
|
{ type: "set", path: ["nullValue"], value: null },
|
|
@@ -232,7 +313,7 @@ describe("ObjectStream serialization and deserialization", () => {
|
|
|
232
313
|
|
|
233
314
|
it("should correctly handle large nested structures", async () => {
|
|
234
315
|
// Create a deep nested structure
|
|
235
|
-
const stream =
|
|
316
|
+
const stream = createGorpStream({
|
|
236
317
|
execute: (controller) => {
|
|
237
318
|
controller.enqueue([
|
|
238
319
|
{ type: "set", path: ["level1"], value: {} },
|
|
@@ -270,7 +351,7 @@ describe("ObjectStream serialization and deserialization", () => {
|
|
|
270
351
|
});
|
|
271
352
|
|
|
272
353
|
it("should correctly handle operations in multiple enqueue calls", async () => {
|
|
273
|
-
const stream =
|
|
354
|
+
const stream = createGorpStream({
|
|
274
355
|
execute: (controller) => {
|
|
275
356
|
// First batch of operations
|
|
276
357
|
controller.enqueue([
|
|
@@ -319,7 +400,7 @@ describe("ObjectStream serialization and deserialization", () => {
|
|
|
319
400
|
});
|
|
320
401
|
|
|
321
402
|
it("should correctly handle overwriting existing values", async () => {
|
|
322
|
-
const stream =
|
|
403
|
+
const stream = createGorpStream({
|
|
323
404
|
execute: (controller) => {
|
|
324
405
|
controller.enqueue([
|
|
325
406
|
{ type: "set", path: ["value"], value: "initial" },
|
|
@@ -351,7 +432,7 @@ describe("ObjectStream serialization and deserialization", () => {
|
|
|
351
432
|
},
|
|
352
433
|
};
|
|
353
434
|
|
|
354
|
-
const stream =
|
|
435
|
+
const stream = createGorpStream({
|
|
355
436
|
defaultValue: initialValue,
|
|
356
437
|
execute: (controller) => {
|
|
357
438
|
controller.enqueue([
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { GorpStreamAccumulator } from "./GorpStreamAccumulator";
|
|
3
|
+
|
|
4
|
+
describe("GorpStreamAccumulator", () => {
|
|
5
|
+
it("rejects unsafe path segments", () => {
|
|
6
|
+
for (const path of [
|
|
7
|
+
["__proto__", "polluted"],
|
|
8
|
+
["constructor", "prototype", "polluted"],
|
|
9
|
+
["a", "__proto__"],
|
|
10
|
+
]) {
|
|
11
|
+
const acc = new GorpStreamAccumulator({});
|
|
12
|
+
expect(() => acc.append([{ type: "set", path, value: true }])).toThrow(
|
|
13
|
+
/Unsafe gorp path segment/,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
expect(({} as { polluted?: unknown }).polluted).toBeUndefined();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("treats inherited keys as absent when navigating", () => {
|
|
20
|
+
const acc = new GorpStreamAccumulator({});
|
|
21
|
+
acc.append([{ type: "set", path: ["toString", "x"], value: 1 }]);
|
|
22
|
+
expect(acc.state).toEqual({ toString: { x: 1 } });
|
|
23
|
+
|
|
24
|
+
const append = new GorpStreamAccumulator({ toString: "hi" });
|
|
25
|
+
append.append([{ type: "append-text", path: ["toString"], value: "!" }]);
|
|
26
|
+
expect(append.state).toEqual({ toString: "hi!" });
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ReadonlyJSONValue, ReadonlyJSONObject } from "../../utils";
|
|
2
|
-
import
|
|
2
|
+
import { assertSafePathSegment } from "./changeTree";
|
|
3
|
+
import type { GorpStreamOperation } from "./types";
|
|
3
4
|
|
|
4
|
-
export class
|
|
5
|
+
export class GorpStreamAccumulator {
|
|
5
6
|
private _state: ReadonlyJSONValue;
|
|
6
7
|
|
|
7
8
|
constructor(initialValue: ReadonlyJSONValue = null) {
|
|
@@ -12,24 +13,20 @@ export class ObjectStreamAccumulator {
|
|
|
12
13
|
return this._state;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
append(ops: readonly
|
|
16
|
+
append(ops: readonly GorpStreamOperation[]) {
|
|
16
17
|
this._state = ops.reduce(
|
|
17
|
-
(state, op) =>
|
|
18
|
+
(state, op) => GorpStreamAccumulator.apply(state, op),
|
|
18
19
|
this._state,
|
|
19
20
|
);
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
private static apply(state: ReadonlyJSONValue, op:
|
|
23
|
+
private static apply(state: ReadonlyJSONValue, op: GorpStreamOperation) {
|
|
23
24
|
const type = op.type;
|
|
24
25
|
switch (type) {
|
|
25
26
|
case "set":
|
|
26
|
-
return
|
|
27
|
-
state,
|
|
28
|
-
op.path,
|
|
29
|
-
() => op.value,
|
|
30
|
-
);
|
|
27
|
+
return GorpStreamAccumulator.updatePath(state, op.path, () => op.value);
|
|
31
28
|
case "append-text":
|
|
32
|
-
return
|
|
29
|
+
return GorpStreamAccumulator.updatePath(state, op.path, (current) => {
|
|
33
30
|
if (typeof current !== "string")
|
|
34
31
|
throw new Error(`Expected string at path [${op.path.join(", ")}]`);
|
|
35
32
|
return current + op.value;
|
|
@@ -57,6 +54,7 @@ export class ObjectStreamAccumulator {
|
|
|
57
54
|
}
|
|
58
55
|
|
|
59
56
|
const [key, ...rest] = path as [string, ...(readonly string[])];
|
|
57
|
+
assertSafePathSegment(key);
|
|
60
58
|
if (Array.isArray(state)) {
|
|
61
59
|
const idx = Number(key);
|
|
62
60
|
if (Number.isNaN(idx))
|
|
@@ -65,8 +63,8 @@ export class ObjectStreamAccumulator {
|
|
|
65
63
|
throw new Error(`Insert array index out of bounds`);
|
|
66
64
|
|
|
67
65
|
const nextState = [...state];
|
|
68
|
-
nextState[idx] =
|
|
69
|
-
nextState[idx],
|
|
66
|
+
nextState[idx] = GorpStreamAccumulator.updatePath(
|
|
67
|
+
Object.hasOwn(nextState, idx) ? nextState[idx] : undefined,
|
|
70
68
|
rest,
|
|
71
69
|
updater,
|
|
72
70
|
);
|
|
@@ -75,8 +73,8 @@ export class ObjectStreamAccumulator {
|
|
|
75
73
|
}
|
|
76
74
|
|
|
77
75
|
const nextState = { ...(state as ReadonlyJSONObject) };
|
|
78
|
-
nextState[key] =
|
|
79
|
-
nextState[key],
|
|
76
|
+
nextState[key] = GorpStreamAccumulator.updatePath(
|
|
77
|
+
Object.hasOwn(nextState, key) ? nextState[key] : undefined,
|
|
80
78
|
rest,
|
|
81
79
|
updater,
|
|
82
80
|
);
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { GorpStreamDeltaTracker } from "./GorpStreamDeltaTracker";
|
|
3
|
+
import { createGorpStream } from "./createGorpStream";
|
|
4
|
+
import type { ReadonlyJSONObject } from "../../utils";
|
|
5
|
+
|
|
6
|
+
describe("GorpStreamDeltaTracker frames", () => {
|
|
7
|
+
it("reports ops of the current frame in getChangedKeys", () => {
|
|
8
|
+
const tracker = new GorpStreamDeltaTracker({ items: {} });
|
|
9
|
+
tracker.append([
|
|
10
|
+
{ type: "set", path: ["items", "a"], value: { name: "alpha" } },
|
|
11
|
+
]);
|
|
12
|
+
expect(tracker.getChangedKeys(["items"])).toEqual(["a"]);
|
|
13
|
+
expect((tracker.state as ReadonlyJSONObject).items).toEqual({
|
|
14
|
+
a: { name: "alpha" },
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("opens a separate frame per append", () => {
|
|
19
|
+
const tracker = new GorpStreamDeltaTracker({ items: {} });
|
|
20
|
+
tracker.append([
|
|
21
|
+
{ type: "set", path: ["items", "a"], value: { name: "alpha" } },
|
|
22
|
+
]);
|
|
23
|
+
tracker.append([
|
|
24
|
+
{ type: "set", path: ["items", "b"], value: { name: "beta" } },
|
|
25
|
+
]);
|
|
26
|
+
expect(tracker.getChangedKeys(["items"])).toEqual(["b"]);
|
|
27
|
+
expect(tracker.isChangedAt(["items", "a"])).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("reports an empty change set before the first append and after an empty append", () => {
|
|
31
|
+
const tracker = new GorpStreamDeltaTracker({ count: 0 });
|
|
32
|
+
expect(tracker.isChangedAt([])).toBe(false);
|
|
33
|
+
expect(tracker.isChangedAt(["count"])).toBe(false);
|
|
34
|
+
expect(tracker.getChangedKeys([])).toEqual([]);
|
|
35
|
+
tracker.append([{ type: "set", path: ["count"], value: 1 }]);
|
|
36
|
+
expect(tracker.isChangedAt([])).toBe(true);
|
|
37
|
+
tracker.append([]);
|
|
38
|
+
expect(tracker.isChangedAt([])).toBe(false);
|
|
39
|
+
expect(tracker.isChangedAt(["count"])).toBe(false);
|
|
40
|
+
expect(tracker.getChangedKeys([])).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("GorpStreamDeltaTracker isChangedAt", () => {
|
|
45
|
+
it("returns truthy for any path inside a marked subtree", () => {
|
|
46
|
+
const tracker = new GorpStreamDeltaTracker();
|
|
47
|
+
tracker.append([{ type: "set", path: [], value: { items: {} } }]);
|
|
48
|
+
expect(tracker.isChangedAt([])).toBe(true);
|
|
49
|
+
expect(tracker.isChangedAt(["items"])).toBe(true);
|
|
50
|
+
expect(tracker.isChangedAt(["items", "anything", "deep"])).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns true for ancestors of a changed path and false for siblings", () => {
|
|
54
|
+
const tracker = new GorpStreamDeltaTracker({ a: { b: "x" }, c: "y" });
|
|
55
|
+
tracker.append([{ type: "set", path: ["a", "b"], value: "z" }]);
|
|
56
|
+
expect(tracker.isChangedAt([])).toBe(true);
|
|
57
|
+
expect(tracker.isChangedAt(["a"])).toBe(true);
|
|
58
|
+
expect(tracker.isChangedAt(["a", "b"])).toBe(true);
|
|
59
|
+
expect(tracker.isChangedAt(["c"])).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("marks append-text targets as changed", () => {
|
|
63
|
+
const tracker = new GorpStreamDeltaTracker({ message: "Hello" });
|
|
64
|
+
tracker.append([{ type: "append-text", path: ["message"], value: "!" }]);
|
|
65
|
+
expect(tracker.isChangedAt(["message"])).toBe(true);
|
|
66
|
+
expect(tracker.getChangedKeys([])).toEqual(["message"]);
|
|
67
|
+
expect((tracker.state as ReadonlyJSONObject).message).toBe("Hello!");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("GorpStreamDeltaTracker getChangedKeys", () => {
|
|
72
|
+
it("diffs keys against the previous frame when a subtree is fully replaced", () => {
|
|
73
|
+
const tracker = new GorpStreamDeltaTracker({ count: 0, items: {} });
|
|
74
|
+
tracker.append([
|
|
75
|
+
{ type: "set", path: ["items", "a"], value: { name: "alpha" } },
|
|
76
|
+
]);
|
|
77
|
+
tracker.append([
|
|
78
|
+
{ type: "set", path: ["items", "b"], value: { name: "beta" } },
|
|
79
|
+
]);
|
|
80
|
+
tracker.append([
|
|
81
|
+
{
|
|
82
|
+
type: "set",
|
|
83
|
+
path: [],
|
|
84
|
+
value: {
|
|
85
|
+
count: 99,
|
|
86
|
+
items: { b: { name: "beta-new" }, c: { name: "gamma" } },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
]);
|
|
90
|
+
expect(tracker.getChangedKeys(["items"])).toEqual(["b", "c", "a"]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("returns interior node keys for a partially changed subtree", () => {
|
|
94
|
+
const tracker = new GorpStreamDeltaTracker({ items: { a: 1, b: 2 } });
|
|
95
|
+
tracker.append([
|
|
96
|
+
{ type: "set", path: ["items", "a"], value: 10 },
|
|
97
|
+
{ type: "set", path: ["items", "c"], value: 3 },
|
|
98
|
+
]);
|
|
99
|
+
expect(tracker.getChangedKeys(["items"])).toEqual(["a", "c"]);
|
|
100
|
+
expect(tracker.getChangedKeys([])).toEqual(["items"]);
|
|
101
|
+
expect(tracker.getChangedKeys(["other"])).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("diffs keys when the previous value at a replaced path was not an object", () => {
|
|
105
|
+
const tracker = new GorpStreamDeltaTracker({ value: "initial" });
|
|
106
|
+
tracker.append([{ type: "set", path: ["value"], value: { nested: true } }]);
|
|
107
|
+
expect(tracker.getChangedKeys(["value"])).toEqual(["nested"]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("GorpStreamDeltaTracker inherited object members", () => {
|
|
112
|
+
it("does not treat inherited members as changes on a fresh tracker", () => {
|
|
113
|
+
const tracker = new GorpStreamDeltaTracker({ items: {} });
|
|
114
|
+
expect(tracker.isChangedAt(["toString"])).toBe(false);
|
|
115
|
+
expect(tracker.isChangedAt(["hasOwnProperty"])).toBe(false);
|
|
116
|
+
expect(tracker.getChangedKeys(["toString"])).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("does not treat inherited members as changes on a populated tree", () => {
|
|
120
|
+
const tracker = new GorpStreamDeltaTracker({ items: {} });
|
|
121
|
+
tracker.append([{ type: "set", path: ["items", "a"], value: 1 }]);
|
|
122
|
+
expect(tracker.isChangedAt(["toString"])).toBe(false);
|
|
123
|
+
expect(tracker.isChangedAt(["items", "toString"])).toBe(false);
|
|
124
|
+
expect(tracker.isChangedAt(["items", "valueOf", "deep"])).toBe(false);
|
|
125
|
+
expect(tracker.getChangedKeys([])).toEqual(["items"]);
|
|
126
|
+
expect(tracker.getChangedKeys(["items"])).toEqual(["a"]);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("GorpStreamDeltaTracker failed append", () => {
|
|
131
|
+
it("leaves state and the change frame untouched when an operation throws", () => {
|
|
132
|
+
const tracker = new GorpStreamDeltaTracker({ count: 5, message: "hi" });
|
|
133
|
+
tracker.append([{ type: "set", path: ["message"], value: "hello" }]);
|
|
134
|
+
const stateBefore = tracker.state;
|
|
135
|
+
|
|
136
|
+
expect(() =>
|
|
137
|
+
tracker.append([
|
|
138
|
+
{ type: "set", path: ["other"], value: 1 },
|
|
139
|
+
{ type: "append-text", path: ["count"], value: "!" },
|
|
140
|
+
]),
|
|
141
|
+
).toThrow(/Expected string/);
|
|
142
|
+
|
|
143
|
+
expect(tracker.state).toBe(stateBefore);
|
|
144
|
+
expect(tracker.isChangedAt(["message"])).toBe(true);
|
|
145
|
+
expect(tracker.isChangedAt(["other"])).toBe(false);
|
|
146
|
+
expect(tracker.isChangedAt(["count"])).toBe(false);
|
|
147
|
+
expect(tracker.getChangedKeys([])).toEqual(["message"]);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("GorpStreamDeltaTracker streamed accumulation", () => {
|
|
152
|
+
it("tracks one frame per streamed chunk", async () => {
|
|
153
|
+
const stream = createGorpStream({
|
|
154
|
+
execute: (controller) => {
|
|
155
|
+
controller.enqueue([
|
|
156
|
+
{ type: "set", path: ["user"], value: { name: "Initial" } },
|
|
157
|
+
]);
|
|
158
|
+
controller.enqueue([
|
|
159
|
+
{ type: "set", path: ["user", "name"], value: "Updated" },
|
|
160
|
+
{ type: "set", path: ["user", "email"], value: "user@example.com" },
|
|
161
|
+
]);
|
|
162
|
+
controller.enqueue([
|
|
163
|
+
{ type: "set", path: ["status"], value: "complete" },
|
|
164
|
+
]);
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const tracker = new GorpStreamDeltaTracker();
|
|
169
|
+
const frames: string[][] = [];
|
|
170
|
+
for await (const chunk of stream) {
|
|
171
|
+
tracker.append(chunk.operations);
|
|
172
|
+
frames.push(tracker.getChangedKeys(["user"]));
|
|
173
|
+
expect(tracker.state).toEqual(chunk.snapshot);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
expect(frames).toEqual([["name"], ["name", "email"], []]);
|
|
177
|
+
expect(tracker.state).toEqual({
|
|
178
|
+
user: { name: "Updated", email: "user@example.com" },
|
|
179
|
+
status: "complete",
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ReadonlyJSONValue } from "../../utils";
|
|
2
|
+
import { GorpStreamAccumulator } from "./GorpStreamAccumulator";
|
|
3
|
+
import type { GorpStreamOperation } from "./types";
|
|
4
|
+
import {
|
|
5
|
+
type ChangeNode,
|
|
6
|
+
createChangeNode,
|
|
7
|
+
diffKeys,
|
|
8
|
+
lookupChange,
|
|
9
|
+
lookupValue,
|
|
10
|
+
markChanged,
|
|
11
|
+
} from "./changeTree";
|
|
12
|
+
|
|
13
|
+
export class GorpStreamDeltaTracker {
|
|
14
|
+
private readonly accumulator: GorpStreamAccumulator;
|
|
15
|
+
private previousState: ReadonlyJSONValue;
|
|
16
|
+
private changes: ChangeNode = createChangeNode();
|
|
17
|
+
|
|
18
|
+
constructor(initialValue: ReadonlyJSONValue = null) {
|
|
19
|
+
this.accumulator = new GorpStreamAccumulator(initialValue);
|
|
20
|
+
this.previousState = this.accumulator.state;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get state(): ReadonlyJSONValue {
|
|
24
|
+
return this.accumulator.state;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
append(operations: readonly GorpStreamOperation[]): void {
|
|
28
|
+
const previousState = this.accumulator.state;
|
|
29
|
+
let changes: ChangeNode = createChangeNode();
|
|
30
|
+
for (const op of operations) {
|
|
31
|
+
changes = markChanged(changes, op.path);
|
|
32
|
+
}
|
|
33
|
+
this.accumulator.append(operations);
|
|
34
|
+
this.previousState = previousState;
|
|
35
|
+
this.changes = changes;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
isChangedAt(path: readonly string[]): boolean {
|
|
39
|
+
const node = lookupChange(this.changes, path);
|
|
40
|
+
if (node === true) return true;
|
|
41
|
+
return node !== false && Object.keys(node).length > 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getChangedKeys(path: readonly string[]): string[] {
|
|
45
|
+
const node = lookupChange(this.changes, path);
|
|
46
|
+
if (node === false) return [];
|
|
47
|
+
if (node === true) {
|
|
48
|
+
return diffKeys(
|
|
49
|
+
lookupValue(this.accumulator.state, path),
|
|
50
|
+
lookupValue(this.previousState, path),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return Object.keys(node);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { fromGorpStreamResponse } from "./GorpStreamResponse";
|
|
3
|
+
|
|
4
|
+
const createResponse = (contentType?: string) => {
|
|
5
|
+
const headers = new Headers({
|
|
6
|
+
"Assistant-Stream-Format": "object-stream/v0",
|
|
7
|
+
});
|
|
8
|
+
if (contentType) headers.set("Content-Type", contentType);
|
|
9
|
+
|
|
10
|
+
return new Response(new Uint8Array(), { headers });
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe("fromGorpStreamResponse", () => {
|
|
14
|
+
it.each([
|
|
15
|
+
"text/event-stream",
|
|
16
|
+
"text/event-stream; charset=utf-8",
|
|
17
|
+
"Text/Event-Stream; Charset=UTF-8",
|
|
18
|
+
])("accepts the event-stream content type %s", (contentType) => {
|
|
19
|
+
expect(() =>
|
|
20
|
+
fromGorpStreamResponse(createResponse(contentType)),
|
|
21
|
+
).not.toThrow();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it.each([undefined, "application/json", "text/event-streaming"])(
|
|
25
|
+
"rejects the content type %s",
|
|
26
|
+
(contentType) => {
|
|
27
|
+
expect(() => fromGorpStreamResponse(createResponse(contentType))).toThrow(
|
|
28
|
+
"Response is not an event stream",
|
|
29
|
+
);
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { PipeableTransformStream } from "../utils/stream/PipeableTransformStream";
|
|
2
|
+
import { GorpStreamAccumulator } from "./GorpStreamAccumulator";
|
|
3
|
+
import { SSEDecoder, SSEEncoder } from "../utils/stream/SSE";
|
|
4
|
+
import type { GorpStreamChunk, GorpStreamOperation } from "./types";
|
|
5
|
+
|
|
6
|
+
export class GorpStreamEncoder extends PipeableTransformStream<
|
|
7
|
+
GorpStreamChunk,
|
|
8
|
+
Uint8Array
|
|
9
|
+
> {
|
|
10
|
+
constructor() {
|
|
11
|
+
let isFirstChunk = true;
|
|
12
|
+
super((readable) =>
|
|
13
|
+
readable
|
|
14
|
+
.pipeThrough(
|
|
15
|
+
new TransformStream<GorpStreamChunk, readonly GorpStreamOperation[]>({
|
|
16
|
+
transform(chunk, controller) {
|
|
17
|
+
if (isFirstChunk) {
|
|
18
|
+
controller.enqueue([
|
|
19
|
+
{ type: "set", path: [], value: chunk.snapshot },
|
|
20
|
+
]);
|
|
21
|
+
} else {
|
|
22
|
+
controller.enqueue(chunk.operations);
|
|
23
|
+
}
|
|
24
|
+
isFirstChunk = false;
|
|
25
|
+
},
|
|
26
|
+
}),
|
|
27
|
+
)
|
|
28
|
+
.pipeThrough(new SSEEncoder()),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class GorpStreamDecoder extends PipeableTransformStream<
|
|
34
|
+
Uint8Array<ArrayBuffer>,
|
|
35
|
+
GorpStreamChunk
|
|
36
|
+
> {
|
|
37
|
+
constructor() {
|
|
38
|
+
const accumulator = new GorpStreamAccumulator();
|
|
39
|
+
super((readable) =>
|
|
40
|
+
readable
|
|
41
|
+
.pipeThrough(new SSEDecoder<readonly GorpStreamOperation[]>())
|
|
42
|
+
.pipeThrough(
|
|
43
|
+
new TransformStream<readonly GorpStreamOperation[], GorpStreamChunk>({
|
|
44
|
+
transform(operations, controller) {
|
|
45
|
+
accumulator.append(operations);
|
|
46
|
+
controller.enqueue({
|
|
47
|
+
snapshot: accumulator.state,
|
|
48
|
+
operations,
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export class GorpStreamResponse extends Response {
|
|
58
|
+
constructor(body: ReadableStream<GorpStreamChunk>) {
|
|
59
|
+
super(body.pipeThrough(new GorpStreamEncoder()), {
|
|
60
|
+
headers: new Headers({
|
|
61
|
+
"Content-Type": "text/event-stream",
|
|
62
|
+
"Cache-Control": "no-cache",
|
|
63
|
+
Connection: "keep-alive",
|
|
64
|
+
"Assistant-Stream-Format": "object-stream/v0",
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const fromGorpStreamResponse = (
|
|
71
|
+
response: Response,
|
|
72
|
+
): ReadableStream<GorpStreamChunk> => {
|
|
73
|
+
if (!response.ok)
|
|
74
|
+
throw new Error(`Response failed, status ${response.status}`);
|
|
75
|
+
if (!response.body) throw new Error("Response body is null");
|
|
76
|
+
const mediaType = response.headers
|
|
77
|
+
.get("Content-Type")
|
|
78
|
+
?.split(";", 1)[0]
|
|
79
|
+
?.trim()
|
|
80
|
+
.toLowerCase();
|
|
81
|
+
if (mediaType !== "text/event-stream") {
|
|
82
|
+
throw new Error("Response is not an event stream");
|
|
83
|
+
}
|
|
84
|
+
if (response.headers.get("Assistant-Stream-Format") !== "object-stream/v0") {
|
|
85
|
+
throw new Error("Unsupported Assistant-Stream-Format header");
|
|
86
|
+
}
|
|
87
|
+
return response.body.pipeThrough(new GorpStreamDecoder());
|
|
88
|
+
};
|