@diffcp/core 0.1.0-canary.2

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 ADDED
@@ -0,0 +1,47 @@
1
+ # ☄️ Diffcp Core
2
+
3
+ Diffcp (Differential Context Protocol) is the new standard to stream AI Agent state to the user interface. Is a
4
+ lightweight alternative to any bespoke AI protocol currently in the industry. Purpose built to be versatile
5
+ unopinionated and highly efficient (95%+ compression).
6
+
7
+ - [Documentation](https://github.com/axelered/diffcp)
8
+ - [GitHub](https://github.com/axelered/diffcp)
9
+ - [Issues](https://github.com/axelered/diffcp/issues)
10
+
11
+ ## Install
12
+
13
+ ```shell
14
+ npm i @diffcp/core
15
+ ```
16
+
17
+ ## Use
18
+
19
+ Convert your existing APIs to a streaming endpoint with a **one line** and a simple function which
20
+ **yield updated state objects**
21
+
22
+ ```ts
23
+ export interface MessageType {
24
+ text: string
25
+ }
26
+ ```
27
+
28
+ ```ts
29
+ export async function* streamMessage(): AsyncIterable<MessageType> {
30
+ yield { text: 'This' }
31
+ yield { text: 'This is' }
32
+ // ...
33
+ yield { text: 'This is a stream message completed' }
34
+ }
35
+
36
+ export function GET() {
37
+ return new ObjectStreamResponse(streamMessage())
38
+ }
39
+ ```
40
+
41
+ On the client just **consume an updating state stream**
42
+
43
+ ```ts
44
+ for await (const data of fetchObjectStream<MessageType>('/api')) {
45
+ // Consume data
46
+ }
47
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,411 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ NdJSONStreamResponse: () => NdJSONStreamResponse,
24
+ ObjectStreamResponse: () => ObjectStreamResponse,
25
+ PlainJsonError: () => PlainJsonError,
26
+ StreamEvent: () => StreamEvent,
27
+ StreamReinit: () => StreamReinit,
28
+ deflate: () => deflate,
29
+ diffApply: () => diffApply,
30
+ diffCreate: () => diffCreate,
31
+ fetchNdJSON: () => fetchNdJSON,
32
+ fetchObjectStream: () => fetchObjectStream,
33
+ inflate: () => inflate
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/diff.ts
38
+ function diffEncodePath(path) {
39
+ if (path.length === 0) return "";
40
+ return "/" + path.map((x) => (x + "").replace(/~/g, "~0").replace(/\//g, "~1")).join("/");
41
+ }
42
+ function diffDecodePath(path) {
43
+ if (path == "") {
44
+ return [];
45
+ } else if (!path.startsWith("/")) {
46
+ throw Error(`Invalid path string "${path}"`);
47
+ }
48
+ return path.substring(1).split("/").map((x) => x.replace(/~1/g, "/").replace(/~0/g, "~"));
49
+ }
50
+ function jsonTypeof(value) {
51
+ const t = typeof value;
52
+ if (t === "bigint") {
53
+ return "number";
54
+ } else if (t === "function" || t === "symbol") {
55
+ throw Error(`Unsupported value type "${t}"`);
56
+ } else if (t === "object" && Array.isArray(value)) {
57
+ return "array";
58
+ } else {
59
+ return t;
60
+ }
61
+ }
62
+ function diffCreateOp(diff, currentPath, from, to) {
63
+ if (from === to) {
64
+ return;
65
+ }
66
+ const tFrom = jsonTypeof(from);
67
+ const tTo = jsonTypeof(to);
68
+ if (tFrom !== tTo) {
69
+ diff.push(["s", diffEncodePath(currentPath), to]);
70
+ } else if (tFrom === "object") {
71
+ const fromKeys = new Set(Object.keys(from));
72
+ const toKeys = new Set(Object.keys(to));
73
+ const keys = /* @__PURE__ */ new Set([...fromKeys, ...toKeys]);
74
+ for (const key of keys) {
75
+ if (!toKeys.has(key)) {
76
+ diff.push(["d", diffEncodePath([...currentPath, key])]);
77
+ } else if (!fromKeys.has(key)) {
78
+ diff.push(["s", diffEncodePath([...currentPath, key]), to[key]]);
79
+ } else {
80
+ diffCreateOp(diff, [...currentPath, key], from[key], to[key]);
81
+ }
82
+ }
83
+ } else if (tFrom === "array") {
84
+ const lengthMax = Math.max(from.length, to.length);
85
+ const lengthMin = Math.min(from.length, to.length);
86
+ for (let i = 0; i < lengthMax; i++) {
87
+ if (i < lengthMin) {
88
+ diffCreateOp(diff, [...currentPath, i], from[i], to[i]);
89
+ } else if (from.length > to.length) {
90
+ diff.push(["d", diffEncodePath([...currentPath, i])]);
91
+ } else {
92
+ diff.push(["a", diffEncodePath([...currentPath, "-"]), to[i]]);
93
+ }
94
+ }
95
+ } else if (tFrom === "string") {
96
+ if (to.startsWith(from)) {
97
+ diff.push(["a", diffEncodePath([...currentPath, "-"]), to.substring(from.length)]);
98
+ } else {
99
+ diff.push(["s", diffEncodePath(currentPath), to]);
100
+ }
101
+ } else {
102
+ diff.push(["s", diffEncodePath(currentPath), to]);
103
+ }
104
+ }
105
+ function diffCreate(from, to) {
106
+ const diff = [];
107
+ diffCreateOp(diff, [], from, to);
108
+ return diff;
109
+ }
110
+ function diffApplyOp(currentPath, restPath, value, op) {
111
+ if (restPath.length === 0) {
112
+ if (op[0] !== "s") {
113
+ throw Error(`Cannot perform operation ${op[0]} at "${op[1]}"`);
114
+ } else {
115
+ return op[2];
116
+ }
117
+ }
118
+ const key = restPath[0];
119
+ if (key === "-" && (op[0] !== "a" || restPath.length > 1)) {
120
+ throw Error(`Cannot perform operation ${op[0]} for "-" indexes at "${op[1]}"`);
121
+ }
122
+ const nextCurrentPath = [...currentPath, key];
123
+ const nextRestPath = restPath.slice(1);
124
+ const isDelete = nextRestPath.length === 0 && op[0] === "d";
125
+ if (typeof value === "object" && Array.isArray(value)) {
126
+ if (key === "-") {
127
+ return [...value, op[2]];
128
+ } else {
129
+ const ix = Number.parseInt(key, 10);
130
+ if (Number.isNaN(ix) || ix < 0 || ix >= value.length) {
131
+ throw Error(`Invalid access index ${ix} for operation ${op[0]} at "${op[1]}"`);
132
+ }
133
+ if (isDelete) {
134
+ return value.filter((_, jx) => ix !== jx);
135
+ } else {
136
+ return value.map(
137
+ (v, jx) => ix !== jx ? v : diffApplyOp(nextCurrentPath, nextRestPath, v, op)
138
+ );
139
+ }
140
+ }
141
+ } else if (typeof value === "object") {
142
+ if (isDelete) {
143
+ return { ...value, [key]: void 0 };
144
+ } else {
145
+ return {
146
+ ...value,
147
+ [key]: diffApplyOp(nextCurrentPath, nextRestPath, value[key], op)
148
+ };
149
+ }
150
+ } else if (typeof value === "string") {
151
+ if (key === "-") {
152
+ return value + op[2];
153
+ }
154
+ }
155
+ }
156
+ function diffApply(value, diff) {
157
+ if (value === void 0 && diff.length === 0) {
158
+ throw Error(`Cannot apply empty diff without an object`);
159
+ }
160
+ for (const op of diff) {
161
+ const path = diffDecodePath(op[1]);
162
+ value = diffApplyOp([], path, value, op);
163
+ }
164
+ return value;
165
+ }
166
+
167
+ // src/ndjson.ts
168
+ var NdJSONStreamResponse = class extends Response {
169
+ constructor(messages, init) {
170
+ const encoder = new TextEncoder();
171
+ let heartbeat = null;
172
+ const stream = new ReadableStream({
173
+ async start(controller) {
174
+ heartbeat = setInterval(() => {
175
+ controller.enqueue("\n");
176
+ }, init?.heartbeatMs ?? 15e3);
177
+ for await (const msg of messages) {
178
+ controller.enqueue(encoder.encode(JSON.stringify(msg) + "\n"));
179
+ }
180
+ clearInterval(heartbeat);
181
+ controller.close();
182
+ },
183
+ cancel() {
184
+ clearInterval(heartbeat);
185
+ }
186
+ });
187
+ super(stream, {
188
+ ...init,
189
+ headers: {
190
+ "Content-Type": "application/x-ndjson; charset=utf-8",
191
+ "Transfer-Encoding": "chunked",
192
+ "Cache-Control": "no-cache, no-transform",
193
+ Connection: "keep-alive",
194
+ "X-Accel-Buffering": "no",
195
+ // Disable nginx buffering
196
+ ...init?.headers
197
+ }
198
+ });
199
+ }
200
+ };
201
+ function parseContentType(value) {
202
+ const [typePart, ...paramParts] = value.split(";");
203
+ const type = typePart.trim().toLowerCase();
204
+ const parameters = {};
205
+ for (const part of paramParts) {
206
+ const [key, ...rest] = part.split("=");
207
+ if (!key || rest.length === 0) continue;
208
+ let val = rest.join("=").trim();
209
+ if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
210
+ parameters[key.trim().toLowerCase()] = val;
211
+ }
212
+ return [type, parameters["charset"] || "utf8"];
213
+ }
214
+ var PlainJsonError = class extends Error {
215
+ constructor(data) {
216
+ super("fetch failed: received plain application/json response");
217
+ this.data = data;
218
+ }
219
+ };
220
+ async function* fetchNdJSON(input, init) {
221
+ const res = await fetch(input, init);
222
+ if (!res.ok) {
223
+ throw new Error(`fetch failed: ${res.status} ${res.statusText}`);
224
+ } else if (!res.body) {
225
+ throw new Error(`fetch failed: empty request body`);
226
+ }
227
+ const ctHeader = res.headers.get("content-type");
228
+ const [type, encoding] = parseContentType(ctHeader ?? "");
229
+ if (type === "application/x-ndjson") {
230
+ const reader = res.body.getReader();
231
+ const decoder = new TextDecoder(encoding);
232
+ let buffer = "";
233
+ while (true) {
234
+ const { value, done } = await reader.read();
235
+ if (done) break;
236
+ buffer += decoder.decode(value, { stream: true });
237
+ const lines = buffer.split("\n");
238
+ buffer = lines.pop() ?? "";
239
+ for (const line of lines) {
240
+ const trimmed = line.trim();
241
+ if (!trimmed) continue;
242
+ const v = JSON.parse(trimmed);
243
+ init?.onLine?.(v);
244
+ yield v;
245
+ }
246
+ }
247
+ const tail = buffer.trim();
248
+ if (tail) {
249
+ const v = JSON.parse(tail);
250
+ init?.onLine?.(v);
251
+ yield v;
252
+ }
253
+ } else if (type === "application/json") {
254
+ throw new PlainJsonError(await res.json());
255
+ } else {
256
+ throw new Error(`fetch failed: invalid response content type ${ctHeader}`);
257
+ }
258
+ }
259
+
260
+ // src/zip.ts
261
+ async function* deflate(messages) {
262
+ const objType = { init: 1, delta: 2, done: 3, event: 9 };
263
+ const patchType = { a: 1, d: 2, s: 3 };
264
+ const deltaDict = {};
265
+ let deltaOffset = 0;
266
+ for await (const msg of messages) {
267
+ if (msg.t === "delta") {
268
+ const delta = [];
269
+ for (const [typ, path, ...rest] of msg.d) {
270
+ delta.push([
271
+ patchType[typ],
272
+ // Previous Occurrence Compression
273
+ path in deltaDict ? deltaOffset - deltaDict[path] : path,
274
+ ...rest
275
+ ]);
276
+ deltaDict[path] = deltaOffset;
277
+ deltaOffset += 1;
278
+ }
279
+ yield [objType.delta, delta];
280
+ } else {
281
+ yield msg.d ? [objType[msg.t], msg.d] : [objType[msg.t]];
282
+ }
283
+ }
284
+ }
285
+ async function* inflate(compressed) {
286
+ const objType = { 1: "init", 2: "delta", 3: "done", 9: "event" };
287
+ const patchType = { 1: "a", 2: "d", 3: "s" };
288
+ const deltaDict = {};
289
+ const deltaDictInv = {};
290
+ let deltaOffset = 0;
291
+ for await (const rec of compressed) {
292
+ if (!Array.isArray(rec)) {
293
+ yield rec;
294
+ continue;
295
+ }
296
+ const [typ, data] = rec;
297
+ if (objType[typ] === "delta") {
298
+ const delta = [];
299
+ for (const [typ2, path, ...rest] of data) {
300
+ let decPath = path;
301
+ if (typeof path === "number") {
302
+ const oldOffset = deltaOffset - path;
303
+ decPath = deltaDict[oldOffset];
304
+ delete deltaDict[oldOffset];
305
+ }
306
+ if (decPath in deltaDictInv) delete deltaDict[deltaDictInv[decPath]];
307
+ deltaDictInv[decPath] = deltaOffset;
308
+ deltaDict[deltaOffset] = decPath;
309
+ deltaOffset += 1;
310
+ delta.push([patchType[typ2], decPath, ...rest]);
311
+ }
312
+ yield { t: "delta", d: delta };
313
+ } else {
314
+ yield { t: objType[typ], d: data };
315
+ }
316
+ }
317
+ }
318
+
319
+ // src/stream.ts
320
+ var StreamEvent = class {
321
+ constructor(data) {
322
+ this.data = data;
323
+ }
324
+ };
325
+ var StreamReinit = class {
326
+ constructor(data) {
327
+ this.data = data;
328
+ }
329
+ };
330
+ var ObjectStreamResponse = class extends NdJSONStreamResponse {
331
+ constructor(stream, init) {
332
+ async function* wrapped() {
333
+ let last = void 0;
334
+ for await (const chunk of stream) {
335
+ if (chunk instanceof StreamEvent) {
336
+ yield { t: "event", d: chunk.data };
337
+ continue;
338
+ }
339
+ if (chunk instanceof StreamReinit) last = void 0;
340
+ const data = chunk instanceof StreamReinit ? chunk.data : chunk;
341
+ if (last === void 0) {
342
+ yield { t: "init", d: data };
343
+ } else {
344
+ yield { t: "delta", d: diffCreate(last, data) };
345
+ }
346
+ last = JSON.parse(JSON.stringify(data));
347
+ }
348
+ yield init?.sendDataOnDone ? { t: "done", d: last } : { t: "done" };
349
+ }
350
+ const compress = init?.compressed !== false;
351
+ const res = compress ? deflate(wrapped()) : wrapped();
352
+ super(res, {
353
+ ...init,
354
+ headers: {
355
+ ...init?.headers,
356
+ "X-Diff-Version": compress ? "1; compressed" : "1"
357
+ }
358
+ });
359
+ }
360
+ };
361
+ async function* fetchObjectStream(input, init) {
362
+ const { onEvent, ...ndInit } = init ?? {};
363
+ let done = false;
364
+ let dataValue = void 0;
365
+ try {
366
+ const res = fetchNdJSON(input, ndInit);
367
+ for await (const data of inflate(res)) {
368
+ init?.onFrame?.(data);
369
+ if (data.t === "init") {
370
+ dataValue = data.d;
371
+ init?.onData?.(dataValue);
372
+ yield dataValue;
373
+ } else if (data.t === "delta") {
374
+ dataValue = diffApply(dataValue, data.d);
375
+ init?.onData?.(dataValue);
376
+ yield dataValue;
377
+ } else if (data.t === "event") {
378
+ onEvent?.(data.d);
379
+ } else if (data.t === "done") {
380
+ if (data.d !== void 0) dataValue = data.d;
381
+ done = true;
382
+ }
383
+ }
384
+ } catch (error) {
385
+ if (error instanceof PlainJsonError && init?.fallbackPlainJson !== false) {
386
+ return error.data;
387
+ } else {
388
+ throw error;
389
+ }
390
+ }
391
+ if (!dataValue) {
392
+ throw Error(`Object stream ended without any data`);
393
+ } else if (!done) {
394
+ throw Error(`Object stream ended without done signal`);
395
+ }
396
+ return dataValue;
397
+ }
398
+ // Annotate the CommonJS export names for ESM import in node:
399
+ 0 && (module.exports = {
400
+ NdJSONStreamResponse,
401
+ ObjectStreamResponse,
402
+ PlainJsonError,
403
+ StreamEvent,
404
+ StreamReinit,
405
+ deflate,
406
+ diffApply,
407
+ diffCreate,
408
+ fetchNdJSON,
409
+ fetchObjectStream,
410
+ inflate
411
+ });
@@ -0,0 +1,81 @@
1
+ type ObjectPatchAdd = ['a', string, any];
2
+ type ObjectPatchSet = ['s', string, any];
3
+ type ObjectPatchRemove = ['d', string];
4
+ type ObjectPatchOp = ObjectPatchAdd | ObjectPatchSet | ObjectPatchRemove;
5
+ type ObjectPatchDiff = ObjectPatchOp[];
6
+ /**
7
+ * Creates a set of diff operations to transition from Object to Object
8
+ */
9
+ declare function diffCreate<T extends object>(from: T | undefined, to: T): ObjectPatchDiff;
10
+ /**
11
+ * Apply a complete set of diff operations to an Object
12
+ */
13
+ declare function diffApply<T extends object>(value: T | undefined, diff: ObjectPatchDiff): T;
14
+
15
+ interface NDJSONStreamResponseInit extends ResponseInit {
16
+ heartbeatMs?: number;
17
+ }
18
+ /**
19
+ * New line delimited JSON response stream
20
+ */
21
+ declare class NdJSONStreamResponse<M> extends Response {
22
+ constructor(messages: AsyncIterable<M>, init?: NDJSONStreamResponseInit);
23
+ }
24
+ declare class PlainJsonError extends Error {
25
+ readonly data: any;
26
+ constructor(data: any);
27
+ }
28
+ interface NdJSONFetchRequestInit<T> extends RequestInit {
29
+ onLine?: (value: T) => void;
30
+ }
31
+ /**
32
+ * New line delimited JSON fetch implementation
33
+ */
34
+ declare function fetchNdJSON<T>(input: string | URL | Request, init?: NdJSONFetchRequestInit<T>): AsyncIterable<T>;
35
+
36
+ type CompressedObjectPatchDiff = [1 | 3, number | string, any] | [2, number | string];
37
+ type CompressedObjectStream<T extends object, E> = [1, T] | [2, CompressedObjectPatchDiff] | [3, T] | [9, E];
38
+ declare function deflate<T extends object, E>(messages: AsyncIterable<ObjectStream<T, E>>): AsyncIterable<CompressedObjectStream<T, E>>;
39
+ declare function inflate<T extends object, E>(compressed: AsyncIterable<CompressedObjectStream<T, E>>): AsyncIterable<ObjectStream<T, E>>;
40
+
41
+ type ObjectStream<T extends object, E> = {
42
+ t: 'init';
43
+ d: T;
44
+ } | {
45
+ t: 'delta';
46
+ d: ObjectPatchDiff;
47
+ } | {
48
+ t: 'done';
49
+ d?: T;
50
+ } | {
51
+ t: 'event';
52
+ d: E;
53
+ };
54
+ declare class StreamEvent<E> {
55
+ readonly data: E;
56
+ constructor(data: E);
57
+ }
58
+ declare class StreamReinit<T extends object> {
59
+ readonly data: T;
60
+ constructor(data: T);
61
+ }
62
+ type ObjectStreamIterable<T extends object, E> = AsyncIterable<T | StreamReinit<T> | StreamEvent<E>>;
63
+ interface ObjectStreamResponseInit extends NDJSONStreamResponseInit {
64
+ sendDataOnDone?: boolean;
65
+ compressed?: boolean;
66
+ }
67
+ /**
68
+ * Differential Context Protocol Stream
69
+ */
70
+ declare class ObjectStreamResponse<T extends object, E = any> extends NdJSONStreamResponse<ObjectStream<T, E> | CompressedObjectStream<T, E>> {
71
+ constructor(stream: ObjectStreamIterable<T, E>, init?: ObjectStreamResponseInit);
72
+ }
73
+ interface ObjectStreamRequestInit<T extends object, E> extends NdJSONFetchRequestInit<ObjectStream<T, E> | CompressedObjectStream<T, E>> {
74
+ onData?: (data: T) => void;
75
+ onEvent?: (event: E) => void;
76
+ onFrame?: (frame: ObjectStream<T, E>) => void;
77
+ fallbackPlainJson?: boolean;
78
+ }
79
+ declare function fetchObjectStream<T extends object, E = any>(input: string | URL | Request, init?: ObjectStreamRequestInit<T, E>): AsyncIterable<T>;
80
+
81
+ export { type CompressedObjectPatchDiff, type CompressedObjectStream, type NDJSONStreamResponseInit, type NdJSONFetchRequestInit, NdJSONStreamResponse, type ObjectPatchAdd, type ObjectPatchDiff, type ObjectPatchOp, type ObjectPatchRemove, type ObjectPatchSet, type ObjectStream, type ObjectStreamIterable, type ObjectStreamRequestInit, ObjectStreamResponse, type ObjectStreamResponseInit, PlainJsonError, StreamEvent, StreamReinit, deflate, diffApply, diffCreate, fetchNdJSON, fetchObjectStream, inflate };
@@ -0,0 +1,81 @@
1
+ type ObjectPatchAdd = ['a', string, any];
2
+ type ObjectPatchSet = ['s', string, any];
3
+ type ObjectPatchRemove = ['d', string];
4
+ type ObjectPatchOp = ObjectPatchAdd | ObjectPatchSet | ObjectPatchRemove;
5
+ type ObjectPatchDiff = ObjectPatchOp[];
6
+ /**
7
+ * Creates a set of diff operations to transition from Object to Object
8
+ */
9
+ declare function diffCreate<T extends object>(from: T | undefined, to: T): ObjectPatchDiff;
10
+ /**
11
+ * Apply a complete set of diff operations to an Object
12
+ */
13
+ declare function diffApply<T extends object>(value: T | undefined, diff: ObjectPatchDiff): T;
14
+
15
+ interface NDJSONStreamResponseInit extends ResponseInit {
16
+ heartbeatMs?: number;
17
+ }
18
+ /**
19
+ * New line delimited JSON response stream
20
+ */
21
+ declare class NdJSONStreamResponse<M> extends Response {
22
+ constructor(messages: AsyncIterable<M>, init?: NDJSONStreamResponseInit);
23
+ }
24
+ declare class PlainJsonError extends Error {
25
+ readonly data: any;
26
+ constructor(data: any);
27
+ }
28
+ interface NdJSONFetchRequestInit<T> extends RequestInit {
29
+ onLine?: (value: T) => void;
30
+ }
31
+ /**
32
+ * New line delimited JSON fetch implementation
33
+ */
34
+ declare function fetchNdJSON<T>(input: string | URL | Request, init?: NdJSONFetchRequestInit<T>): AsyncIterable<T>;
35
+
36
+ type CompressedObjectPatchDiff = [1 | 3, number | string, any] | [2, number | string];
37
+ type CompressedObjectStream<T extends object, E> = [1, T] | [2, CompressedObjectPatchDiff] | [3, T] | [9, E];
38
+ declare function deflate<T extends object, E>(messages: AsyncIterable<ObjectStream<T, E>>): AsyncIterable<CompressedObjectStream<T, E>>;
39
+ declare function inflate<T extends object, E>(compressed: AsyncIterable<CompressedObjectStream<T, E>>): AsyncIterable<ObjectStream<T, E>>;
40
+
41
+ type ObjectStream<T extends object, E> = {
42
+ t: 'init';
43
+ d: T;
44
+ } | {
45
+ t: 'delta';
46
+ d: ObjectPatchDiff;
47
+ } | {
48
+ t: 'done';
49
+ d?: T;
50
+ } | {
51
+ t: 'event';
52
+ d: E;
53
+ };
54
+ declare class StreamEvent<E> {
55
+ readonly data: E;
56
+ constructor(data: E);
57
+ }
58
+ declare class StreamReinit<T extends object> {
59
+ readonly data: T;
60
+ constructor(data: T);
61
+ }
62
+ type ObjectStreamIterable<T extends object, E> = AsyncIterable<T | StreamReinit<T> | StreamEvent<E>>;
63
+ interface ObjectStreamResponseInit extends NDJSONStreamResponseInit {
64
+ sendDataOnDone?: boolean;
65
+ compressed?: boolean;
66
+ }
67
+ /**
68
+ * Differential Context Protocol Stream
69
+ */
70
+ declare class ObjectStreamResponse<T extends object, E = any> extends NdJSONStreamResponse<ObjectStream<T, E> | CompressedObjectStream<T, E>> {
71
+ constructor(stream: ObjectStreamIterable<T, E>, init?: ObjectStreamResponseInit);
72
+ }
73
+ interface ObjectStreamRequestInit<T extends object, E> extends NdJSONFetchRequestInit<ObjectStream<T, E> | CompressedObjectStream<T, E>> {
74
+ onData?: (data: T) => void;
75
+ onEvent?: (event: E) => void;
76
+ onFrame?: (frame: ObjectStream<T, E>) => void;
77
+ fallbackPlainJson?: boolean;
78
+ }
79
+ declare function fetchObjectStream<T extends object, E = any>(input: string | URL | Request, init?: ObjectStreamRequestInit<T, E>): AsyncIterable<T>;
80
+
81
+ export { type CompressedObjectPatchDiff, type CompressedObjectStream, type NDJSONStreamResponseInit, type NdJSONFetchRequestInit, NdJSONStreamResponse, type ObjectPatchAdd, type ObjectPatchDiff, type ObjectPatchOp, type ObjectPatchRemove, type ObjectPatchSet, type ObjectStream, type ObjectStreamIterable, type ObjectStreamRequestInit, ObjectStreamResponse, type ObjectStreamResponseInit, PlainJsonError, StreamEvent, StreamReinit, deflate, diffApply, diffCreate, fetchNdJSON, fetchObjectStream, inflate };
package/dist/index.js ADDED
@@ -0,0 +1,374 @@
1
+ // src/diff.ts
2
+ function diffEncodePath(path) {
3
+ if (path.length === 0) return "";
4
+ return "/" + path.map((x) => (x + "").replace(/~/g, "~0").replace(/\//g, "~1")).join("/");
5
+ }
6
+ function diffDecodePath(path) {
7
+ if (path == "") {
8
+ return [];
9
+ } else if (!path.startsWith("/")) {
10
+ throw Error(`Invalid path string "${path}"`);
11
+ }
12
+ return path.substring(1).split("/").map((x) => x.replace(/~1/g, "/").replace(/~0/g, "~"));
13
+ }
14
+ function jsonTypeof(value) {
15
+ const t = typeof value;
16
+ if (t === "bigint") {
17
+ return "number";
18
+ } else if (t === "function" || t === "symbol") {
19
+ throw Error(`Unsupported value type "${t}"`);
20
+ } else if (t === "object" && Array.isArray(value)) {
21
+ return "array";
22
+ } else {
23
+ return t;
24
+ }
25
+ }
26
+ function diffCreateOp(diff, currentPath, from, to) {
27
+ if (from === to) {
28
+ return;
29
+ }
30
+ const tFrom = jsonTypeof(from);
31
+ const tTo = jsonTypeof(to);
32
+ if (tFrom !== tTo) {
33
+ diff.push(["s", diffEncodePath(currentPath), to]);
34
+ } else if (tFrom === "object") {
35
+ const fromKeys = new Set(Object.keys(from));
36
+ const toKeys = new Set(Object.keys(to));
37
+ const keys = /* @__PURE__ */ new Set([...fromKeys, ...toKeys]);
38
+ for (const key of keys) {
39
+ if (!toKeys.has(key)) {
40
+ diff.push(["d", diffEncodePath([...currentPath, key])]);
41
+ } else if (!fromKeys.has(key)) {
42
+ diff.push(["s", diffEncodePath([...currentPath, key]), to[key]]);
43
+ } else {
44
+ diffCreateOp(diff, [...currentPath, key], from[key], to[key]);
45
+ }
46
+ }
47
+ } else if (tFrom === "array") {
48
+ const lengthMax = Math.max(from.length, to.length);
49
+ const lengthMin = Math.min(from.length, to.length);
50
+ for (let i = 0; i < lengthMax; i++) {
51
+ if (i < lengthMin) {
52
+ diffCreateOp(diff, [...currentPath, i], from[i], to[i]);
53
+ } else if (from.length > to.length) {
54
+ diff.push(["d", diffEncodePath([...currentPath, i])]);
55
+ } else {
56
+ diff.push(["a", diffEncodePath([...currentPath, "-"]), to[i]]);
57
+ }
58
+ }
59
+ } else if (tFrom === "string") {
60
+ if (to.startsWith(from)) {
61
+ diff.push(["a", diffEncodePath([...currentPath, "-"]), to.substring(from.length)]);
62
+ } else {
63
+ diff.push(["s", diffEncodePath(currentPath), to]);
64
+ }
65
+ } else {
66
+ diff.push(["s", diffEncodePath(currentPath), to]);
67
+ }
68
+ }
69
+ function diffCreate(from, to) {
70
+ const diff = [];
71
+ diffCreateOp(diff, [], from, to);
72
+ return diff;
73
+ }
74
+ function diffApplyOp(currentPath, restPath, value, op) {
75
+ if (restPath.length === 0) {
76
+ if (op[0] !== "s") {
77
+ throw Error(`Cannot perform operation ${op[0]} at "${op[1]}"`);
78
+ } else {
79
+ return op[2];
80
+ }
81
+ }
82
+ const key = restPath[0];
83
+ if (key === "-" && (op[0] !== "a" || restPath.length > 1)) {
84
+ throw Error(`Cannot perform operation ${op[0]} for "-" indexes at "${op[1]}"`);
85
+ }
86
+ const nextCurrentPath = [...currentPath, key];
87
+ const nextRestPath = restPath.slice(1);
88
+ const isDelete = nextRestPath.length === 0 && op[0] === "d";
89
+ if (typeof value === "object" && Array.isArray(value)) {
90
+ if (key === "-") {
91
+ return [...value, op[2]];
92
+ } else {
93
+ const ix = Number.parseInt(key, 10);
94
+ if (Number.isNaN(ix) || ix < 0 || ix >= value.length) {
95
+ throw Error(`Invalid access index ${ix} for operation ${op[0]} at "${op[1]}"`);
96
+ }
97
+ if (isDelete) {
98
+ return value.filter((_, jx) => ix !== jx);
99
+ } else {
100
+ return value.map(
101
+ (v, jx) => ix !== jx ? v : diffApplyOp(nextCurrentPath, nextRestPath, v, op)
102
+ );
103
+ }
104
+ }
105
+ } else if (typeof value === "object") {
106
+ if (isDelete) {
107
+ return { ...value, [key]: void 0 };
108
+ } else {
109
+ return {
110
+ ...value,
111
+ [key]: diffApplyOp(nextCurrentPath, nextRestPath, value[key], op)
112
+ };
113
+ }
114
+ } else if (typeof value === "string") {
115
+ if (key === "-") {
116
+ return value + op[2];
117
+ }
118
+ }
119
+ }
120
+ function diffApply(value, diff) {
121
+ if (value === void 0 && diff.length === 0) {
122
+ throw Error(`Cannot apply empty diff without an object`);
123
+ }
124
+ for (const op of diff) {
125
+ const path = diffDecodePath(op[1]);
126
+ value = diffApplyOp([], path, value, op);
127
+ }
128
+ return value;
129
+ }
130
+
131
+ // src/ndjson.ts
132
+ var NdJSONStreamResponse = class extends Response {
133
+ constructor(messages, init) {
134
+ const encoder = new TextEncoder();
135
+ let heartbeat = null;
136
+ const stream = new ReadableStream({
137
+ async start(controller) {
138
+ heartbeat = setInterval(() => {
139
+ controller.enqueue("\n");
140
+ }, init?.heartbeatMs ?? 15e3);
141
+ for await (const msg of messages) {
142
+ controller.enqueue(encoder.encode(JSON.stringify(msg) + "\n"));
143
+ }
144
+ clearInterval(heartbeat);
145
+ controller.close();
146
+ },
147
+ cancel() {
148
+ clearInterval(heartbeat);
149
+ }
150
+ });
151
+ super(stream, {
152
+ ...init,
153
+ headers: {
154
+ "Content-Type": "application/x-ndjson; charset=utf-8",
155
+ "Transfer-Encoding": "chunked",
156
+ "Cache-Control": "no-cache, no-transform",
157
+ Connection: "keep-alive",
158
+ "X-Accel-Buffering": "no",
159
+ // Disable nginx buffering
160
+ ...init?.headers
161
+ }
162
+ });
163
+ }
164
+ };
165
+ function parseContentType(value) {
166
+ const [typePart, ...paramParts] = value.split(";");
167
+ const type = typePart.trim().toLowerCase();
168
+ const parameters = {};
169
+ for (const part of paramParts) {
170
+ const [key, ...rest] = part.split("=");
171
+ if (!key || rest.length === 0) continue;
172
+ let val = rest.join("=").trim();
173
+ if (val.startsWith('"') && val.endsWith('"')) val = val.slice(1, -1);
174
+ parameters[key.trim().toLowerCase()] = val;
175
+ }
176
+ return [type, parameters["charset"] || "utf8"];
177
+ }
178
+ var PlainJsonError = class extends Error {
179
+ constructor(data) {
180
+ super("fetch failed: received plain application/json response");
181
+ this.data = data;
182
+ }
183
+ };
184
+ async function* fetchNdJSON(input, init) {
185
+ const res = await fetch(input, init);
186
+ if (!res.ok) {
187
+ throw new Error(`fetch failed: ${res.status} ${res.statusText}`);
188
+ } else if (!res.body) {
189
+ throw new Error(`fetch failed: empty request body`);
190
+ }
191
+ const ctHeader = res.headers.get("content-type");
192
+ const [type, encoding] = parseContentType(ctHeader ?? "");
193
+ if (type === "application/x-ndjson") {
194
+ const reader = res.body.getReader();
195
+ const decoder = new TextDecoder(encoding);
196
+ let buffer = "";
197
+ while (true) {
198
+ const { value, done } = await reader.read();
199
+ if (done) break;
200
+ buffer += decoder.decode(value, { stream: true });
201
+ const lines = buffer.split("\n");
202
+ buffer = lines.pop() ?? "";
203
+ for (const line of lines) {
204
+ const trimmed = line.trim();
205
+ if (!trimmed) continue;
206
+ const v = JSON.parse(trimmed);
207
+ init?.onLine?.(v);
208
+ yield v;
209
+ }
210
+ }
211
+ const tail = buffer.trim();
212
+ if (tail) {
213
+ const v = JSON.parse(tail);
214
+ init?.onLine?.(v);
215
+ yield v;
216
+ }
217
+ } else if (type === "application/json") {
218
+ throw new PlainJsonError(await res.json());
219
+ } else {
220
+ throw new Error(`fetch failed: invalid response content type ${ctHeader}`);
221
+ }
222
+ }
223
+
224
+ // src/zip.ts
225
+ async function* deflate(messages) {
226
+ const objType = { init: 1, delta: 2, done: 3, event: 9 };
227
+ const patchType = { a: 1, d: 2, s: 3 };
228
+ const deltaDict = {};
229
+ let deltaOffset = 0;
230
+ for await (const msg of messages) {
231
+ if (msg.t === "delta") {
232
+ const delta = [];
233
+ for (const [typ, path, ...rest] of msg.d) {
234
+ delta.push([
235
+ patchType[typ],
236
+ // Previous Occurrence Compression
237
+ path in deltaDict ? deltaOffset - deltaDict[path] : path,
238
+ ...rest
239
+ ]);
240
+ deltaDict[path] = deltaOffset;
241
+ deltaOffset += 1;
242
+ }
243
+ yield [objType.delta, delta];
244
+ } else {
245
+ yield msg.d ? [objType[msg.t], msg.d] : [objType[msg.t]];
246
+ }
247
+ }
248
+ }
249
+ async function* inflate(compressed) {
250
+ const objType = { 1: "init", 2: "delta", 3: "done", 9: "event" };
251
+ const patchType = { 1: "a", 2: "d", 3: "s" };
252
+ const deltaDict = {};
253
+ const deltaDictInv = {};
254
+ let deltaOffset = 0;
255
+ for await (const rec of compressed) {
256
+ if (!Array.isArray(rec)) {
257
+ yield rec;
258
+ continue;
259
+ }
260
+ const [typ, data] = rec;
261
+ if (objType[typ] === "delta") {
262
+ const delta = [];
263
+ for (const [typ2, path, ...rest] of data) {
264
+ let decPath = path;
265
+ if (typeof path === "number") {
266
+ const oldOffset = deltaOffset - path;
267
+ decPath = deltaDict[oldOffset];
268
+ delete deltaDict[oldOffset];
269
+ }
270
+ if (decPath in deltaDictInv) delete deltaDict[deltaDictInv[decPath]];
271
+ deltaDictInv[decPath] = deltaOffset;
272
+ deltaDict[deltaOffset] = decPath;
273
+ deltaOffset += 1;
274
+ delta.push([patchType[typ2], decPath, ...rest]);
275
+ }
276
+ yield { t: "delta", d: delta };
277
+ } else {
278
+ yield { t: objType[typ], d: data };
279
+ }
280
+ }
281
+ }
282
+
283
+ // src/stream.ts
284
+ var StreamEvent = class {
285
+ constructor(data) {
286
+ this.data = data;
287
+ }
288
+ };
289
+ var StreamReinit = class {
290
+ constructor(data) {
291
+ this.data = data;
292
+ }
293
+ };
294
+ var ObjectStreamResponse = class extends NdJSONStreamResponse {
295
+ constructor(stream, init) {
296
+ async function* wrapped() {
297
+ let last = void 0;
298
+ for await (const chunk of stream) {
299
+ if (chunk instanceof StreamEvent) {
300
+ yield { t: "event", d: chunk.data };
301
+ continue;
302
+ }
303
+ if (chunk instanceof StreamReinit) last = void 0;
304
+ const data = chunk instanceof StreamReinit ? chunk.data : chunk;
305
+ if (last === void 0) {
306
+ yield { t: "init", d: data };
307
+ } else {
308
+ yield { t: "delta", d: diffCreate(last, data) };
309
+ }
310
+ last = JSON.parse(JSON.stringify(data));
311
+ }
312
+ yield init?.sendDataOnDone ? { t: "done", d: last } : { t: "done" };
313
+ }
314
+ const compress = init?.compressed !== false;
315
+ const res = compress ? deflate(wrapped()) : wrapped();
316
+ super(res, {
317
+ ...init,
318
+ headers: {
319
+ ...init?.headers,
320
+ "X-Diff-Version": compress ? "1; compressed" : "1"
321
+ }
322
+ });
323
+ }
324
+ };
325
+ async function* fetchObjectStream(input, init) {
326
+ const { onEvent, ...ndInit } = init ?? {};
327
+ let done = false;
328
+ let dataValue = void 0;
329
+ try {
330
+ const res = fetchNdJSON(input, ndInit);
331
+ for await (const data of inflate(res)) {
332
+ init?.onFrame?.(data);
333
+ if (data.t === "init") {
334
+ dataValue = data.d;
335
+ init?.onData?.(dataValue);
336
+ yield dataValue;
337
+ } else if (data.t === "delta") {
338
+ dataValue = diffApply(dataValue, data.d);
339
+ init?.onData?.(dataValue);
340
+ yield dataValue;
341
+ } else if (data.t === "event") {
342
+ onEvent?.(data.d);
343
+ } else if (data.t === "done") {
344
+ if (data.d !== void 0) dataValue = data.d;
345
+ done = true;
346
+ }
347
+ }
348
+ } catch (error) {
349
+ if (error instanceof PlainJsonError && init?.fallbackPlainJson !== false) {
350
+ return error.data;
351
+ } else {
352
+ throw error;
353
+ }
354
+ }
355
+ if (!dataValue) {
356
+ throw Error(`Object stream ended without any data`);
357
+ } else if (!done) {
358
+ throw Error(`Object stream ended without done signal`);
359
+ }
360
+ return dataValue;
361
+ }
362
+ export {
363
+ NdJSONStreamResponse,
364
+ ObjectStreamResponse,
365
+ PlainJsonError,
366
+ StreamEvent,
367
+ StreamReinit,
368
+ deflate,
369
+ diffApply,
370
+ diffCreate,
371
+ fetchNdJSON,
372
+ fetchObjectStream,
373
+ inflate
374
+ };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@diffcp/core",
3
+ "version": "0.1.0-canary.2",
4
+ "description": "Open-source standard for connecting AI user interfaces to backend systems",
5
+ "author": {
6
+ "name": "Axelered AI",
7
+ "email": "dev@axelered.com",
8
+ "url": "https://axelered.com/"
9
+ },
10
+ "contributors": [
11
+ {
12
+ "name": "Borut Svara",
13
+ "email": "borut@axelered.com",
14
+ "url": "https://svara.io/"
15
+ }
16
+ ],
17
+ "keywords": [
18
+ "ai",
19
+ "protocol",
20
+ "axelered"
21
+ ],
22
+ "homepage": "https://github.com/axelered/diffcp",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/axelered/diffcp.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/axelered/diffcp/issues",
29
+ "email": "dev@axelered.com"
30
+ },
31
+ "license": "MIT",
32
+ "scripts": {
33
+ "build": "tsup",
34
+ "dev": "tsup --watch",
35
+ "test": "vitest run"
36
+ },
37
+ "type": "module",
38
+ "sideEffects": false,
39
+ "types": "dist/index.d.ts",
40
+ "main": "dist/index.cjs",
41
+ "module": "dist/index.js",
42
+ "exports": {
43
+ ".": {
44
+ "types": "./dist/index.d.ts",
45
+ "require": "./dist/index.cjs",
46
+ "import": "./dist/index.js"
47
+ }
48
+ },
49
+ "files": [
50
+ "dist",
51
+ "README.md"
52
+ ],
53
+ "publishConfig": {
54
+ "access": "public"
55
+ }
56
+ }