statewire 0.0.0-0 → 0.0.1
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/LICENSE +21 -0
- package/README.md +83 -1
- package/dist/client/changes.d.ts +7 -0
- package/dist/client/changes.d.ts.map +1 -0
- package/dist/client/changes.js +32 -0
- package/dist/client/changes.js.map +1 -0
- package/dist/client/connection.d.ts +84 -0
- package/dist/client/connection.d.ts.map +1 -0
- package/dist/client/connection.js +204 -0
- package/dist/client/connection.js.map +1 -0
- package/dist/client/core.d.ts +18 -0
- package/dist/client/core.d.ts.map +1 -0
- package/dist/client/core.js +269 -0
- package/dist/client/core.js.map +1 -0
- package/dist/client/optimistic.d.ts +10 -0
- package/dist/client/optimistic.d.ts.map +1 -0
- package/dist/client/optimistic.js +128 -0
- package/dist/client/optimistic.js.map +1 -0
- package/dist/client/session.d.ts +88 -0
- package/dist/client/session.d.ts.map +1 -0
- package/dist/client/session.js +509 -0
- package/dist/client/session.js.map +1 -0
- package/dist/client/sessionStore.d.ts +16 -0
- package/dist/client/sessionStore.d.ts.map +1 -0
- package/dist/client/sessionStore.js +14 -0
- package/dist/client/sessionStore.js.map +1 -0
- package/dist/client/timing.d.ts +10 -0
- package/dist/client/timing.d.ts.map +1 -0
- package/dist/client/timing.js +11 -0
- package/dist/client/timing.js.map +1 -0
- package/dist/host/draft.d.ts +14 -0
- package/dist/host/draft.d.ts.map +1 -0
- package/dist/host/draft.js +124 -0
- package/dist/host/draft.js.map +1 -0
- package/dist/host/engine.d.ts +14 -0
- package/dist/host/engine.d.ts.map +1 -0
- package/dist/host/engine.js +17 -0
- package/dist/host/engine.js.map +1 -0
- package/dist/host/host.d.ts +79 -0
- package/dist/host/host.d.ts.map +1 -0
- package/dist/host/host.js +303 -0
- package/dist/host/host.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +3 -0
- package/dist/statewire.d.ts +20 -0
- package/dist/statewire.d.ts.map +1 -0
- package/dist/statewire.js +65 -0
- package/dist/statewire.js.map +1 -0
- package/dist/transports/remote.d.ts +19 -0
- package/dist/transports/remote.d.ts.map +1 -0
- package/dist/transports/remote.js +40 -0
- package/dist/transports/remote.js.map +1 -0
- package/dist/transports/sse-parser.d.ts +5 -0
- package/dist/transports/sse-parser.d.ts.map +1 -0
- package/dist/transports/sse-parser.js +26 -0
- package/dist/transports/sse-parser.js.map +1 -0
- package/dist/transports/sse.d.ts +74 -0
- package/dist/transports/sse.d.ts.map +1 -0
- package/dist/transports/sse.js +136 -0
- package/dist/transports/sse.js.map +1 -0
- package/dist/transports/testing.d.ts +31 -0
- package/dist/transports/testing.d.ts.map +1 -0
- package/dist/transports/testing.js +121 -0
- package/dist/transports/testing.js.map +1 -0
- package/dist/transports/ws.d.ts +18 -0
- package/dist/transports/ws.d.ts.map +1 -0
- package/dist/transports/ws.js +172 -0
- package/dist/transports/ws.js.map +1 -0
- package/dist/types.d.ts +107 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +24 -0
- package/dist/types.js.map +1 -0
- package/dist/wire/meta.d.ts +22 -0
- package/dist/wire/meta.d.ts.map +1 -0
- package/dist/wire/meta.js +73 -0
- package/dist/wire/meta.js.map +1 -0
- package/dist/wire/ops.d.ts +40 -0
- package/dist/wire/ops.d.ts.map +1 -0
- package/dist/wire/ops.js +125 -0
- package/dist/wire/ops.js.map +1 -0
- package/package.json +49 -6
- package/src/client/changes.ts +48 -0
- package/src/client/connection.ts +327 -0
- package/src/client/core.ts +346 -0
- package/src/client/optimistic.ts +0 -0
- package/src/client/session.ts +679 -0
- package/src/client/sessionStore.ts +23 -0
- package/src/client/timing.ts +8 -0
- package/src/host/draft.ts +155 -0
- package/src/host/engine.ts +13 -0
- package/src/host/host.ts +481 -0
- package/src/index.ts +65 -0
- package/src/server.ts +32 -0
- package/src/statewire.ts +107 -0
- package/src/transports/remote.ts +72 -0
- package/src/transports/sse-parser.ts +24 -0
- package/src/transports/sse.ts +280 -0
- package/src/transports/testing.ts +155 -0
- package/src/transports/ws.ts +272 -0
- package/src/types.ts +169 -0
- package/src/wire/meta.ts +106 -0
- package/src/wire/ops.ts +201 -0
- package/index.js +0 -1
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/wire/ops.d.ts
|
|
2
|
+
type StatewirePath = readonly (string | number)[];
|
|
3
|
+
type StatewireOp = {
|
|
4
|
+
op: "replace";
|
|
5
|
+
path: StatewirePath;
|
|
6
|
+
value: unknown;
|
|
7
|
+
} | {
|
|
8
|
+
op: "add";
|
|
9
|
+
path: StatewirePath;
|
|
10
|
+
value: unknown;
|
|
11
|
+
} | {
|
|
12
|
+
op: "append";
|
|
13
|
+
path: StatewirePath;
|
|
14
|
+
value: unknown;
|
|
15
|
+
} | {
|
|
16
|
+
op: "remove";
|
|
17
|
+
path: StatewirePath;
|
|
18
|
+
} | {
|
|
19
|
+
op: "event";
|
|
20
|
+
value: object;
|
|
21
|
+
};
|
|
22
|
+
declare const sanitizeValue: (value: unknown, parent?: string) => unknown;
|
|
23
|
+
type MutableOp = Extract<StatewireOp, {
|
|
24
|
+
op: "replace" | "add" | "append" | "remove";
|
|
25
|
+
}>;
|
|
26
|
+
declare const applyOp: (root: unknown, op: MutableOp) => {
|
|
27
|
+
state: unknown;
|
|
28
|
+
changedPaths: StatewirePath[];
|
|
29
|
+
};
|
|
30
|
+
declare const applyOps: (state: unknown, ops: readonly unknown[]) => {
|
|
31
|
+
state: unknown;
|
|
32
|
+
events: {
|
|
33
|
+
event: unknown;
|
|
34
|
+
state: unknown;
|
|
35
|
+
}[];
|
|
36
|
+
changedPaths: StatewirePath[];
|
|
37
|
+
};
|
|
38
|
+
//#endregion
|
|
39
|
+
export { StatewireOp, StatewirePath, applyOp, applyOps, sanitizeValue };
|
|
40
|
+
//# sourceMappingURL=ops.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ops.d.ts","names":[],"sources":["../../src/wire/ops.ts"],"mappings":";KAEY;KAEA;EACN;EAAe,MAAM;EAAe;;EACpC;EAAW,MAAM;EAAe;;EAChC;EAAc,MAAM;EAAe;;EACnC;EAAc,MAAM;;EACpB;EAAa;;cAiBN,gBAAiB,gBAAgB;KAYzC,YAAY,QACf;EACE;;cA6GS,UACX,eACA,IAAI;EACD;EAAgB,cAAc;;cAUtB,WACX,gBACA;EAEA;EACA;IAAU;IAAgB;;EAC1B,cAAc"}
|
package/dist/wire/ops.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { recordMutations } from "../client/optimistic.js";
|
|
2
|
+
//#region src/wire/ops.ts
|
|
3
|
+
const isUnsafe = (key, parent) => key === "__proto__" || key === "prototype" && parent === "constructor";
|
|
4
|
+
const warnStripped = (key, parent) => {
|
|
5
|
+
if (typeof process === "undefined" || process.env["NODE_ENV"] !== "production") console.warn(`statewire: stripped unsafe key "${key}"` + (parent === void 0 ? "" : ` under "${parent}"`));
|
|
6
|
+
};
|
|
7
|
+
const sanitizeValue = (value, parent) => {
|
|
8
|
+
if (typeof value !== "object" || value === null) return value;
|
|
9
|
+
if (value instanceof Date) return new Date(value.getTime());
|
|
10
|
+
if (Array.isArray(value)) return value.map((item) => sanitizeValue(item));
|
|
11
|
+
const out = {};
|
|
12
|
+
for (const key of Object.keys(value)) if (isUnsafe(key, parent)) warnStripped(key, parent);
|
|
13
|
+
else out[key] = sanitizeValue(value[key], key);
|
|
14
|
+
return out;
|
|
15
|
+
};
|
|
16
|
+
const invalid = (op) => /* @__PURE__ */ new Error(`Statewire cannot apply ${op.op} at path: ${op.path.join(".")}`);
|
|
17
|
+
const appended = (current, op) => {
|
|
18
|
+
if (typeof current !== "string" || typeof op.value !== "string") throw invalid(op);
|
|
19
|
+
return current + op.value;
|
|
20
|
+
};
|
|
21
|
+
const applyAtRoot = (box, op) => {
|
|
22
|
+
if (op.op === "replace") box.state = sanitizeValue(op.value);
|
|
23
|
+
else if (op.op === "append") box.state = appended(box.state, op);
|
|
24
|
+
else throw invalid(op);
|
|
25
|
+
};
|
|
26
|
+
const applyInList = (list, index, op) => {
|
|
27
|
+
if (op.op === "replace") {
|
|
28
|
+
if (index >= list.length) throw invalid(op);
|
|
29
|
+
list[index] = sanitizeValue(op.value);
|
|
30
|
+
} else if (op.op === "add") {
|
|
31
|
+
if (index > list.length) throw invalid(op);
|
|
32
|
+
list.splice(index, 0, sanitizeValue(op.value));
|
|
33
|
+
} else if (op.op === "append") {
|
|
34
|
+
if (index >= list.length) throw invalid(op);
|
|
35
|
+
list[index] = appended(list[index], op);
|
|
36
|
+
} else {
|
|
37
|
+
if (index >= list.length) throw invalid(op);
|
|
38
|
+
list.splice(index, 1);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const applyInObject = (obj, key, op) => {
|
|
42
|
+
const exists = Object.hasOwn(obj, key);
|
|
43
|
+
if (op.op === "replace") obj[key] = sanitizeValue(op.value, key);
|
|
44
|
+
else if (op.op === "append") obj[key] = appended(exists ? obj[key] : void 0, op);
|
|
45
|
+
else if (op.op === "remove") {
|
|
46
|
+
if (!exists) throw invalid(op);
|
|
47
|
+
delete obj[key];
|
|
48
|
+
} else throw invalid(op);
|
|
49
|
+
};
|
|
50
|
+
const applyOpToDraft = (box, op) => {
|
|
51
|
+
if (op.path.length === 0) {
|
|
52
|
+
applyAtRoot(box, op);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let node = box.state;
|
|
56
|
+
for (let depth = 0; depth < op.path.length; depth++) {
|
|
57
|
+
const key = op.path[depth];
|
|
58
|
+
const last = depth === op.path.length - 1;
|
|
59
|
+
if (typeof key === "number") {
|
|
60
|
+
if (!Array.isArray(node) || !Number.isInteger(key) || key < 0) throw invalid(op);
|
|
61
|
+
if (last) {
|
|
62
|
+
applyInList(node, key, op);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (key >= node.length) throw invalid(op);
|
|
66
|
+
node = node[key];
|
|
67
|
+
} else {
|
|
68
|
+
if (isUnsafe(key, op.path[depth - 1]) || typeof node !== "object" || node === null || Array.isArray(node)) throw invalid(op);
|
|
69
|
+
const obj = node;
|
|
70
|
+
if (last) {
|
|
71
|
+
applyInObject(obj, key, op);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!Object.hasOwn(obj, key)) throw invalid(op);
|
|
75
|
+
node = obj[key];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const warnedOps = /* @__PURE__ */ new Set();
|
|
80
|
+
const warnUnknownOp = (type) => {
|
|
81
|
+
if (warnedOps.has(type)) return;
|
|
82
|
+
warnedOps.add(type);
|
|
83
|
+
console.warn(`statewire: ignoring unknown op type ${JSON.stringify(type)}`);
|
|
84
|
+
};
|
|
85
|
+
const unboxPaths = (paths) => paths.map((path) => path.slice(1));
|
|
86
|
+
const applyOp = (root, op) => {
|
|
87
|
+
const recorded = recordMutations({ state: root }, (draft) => applyOpToDraft(draft, op));
|
|
88
|
+
return {
|
|
89
|
+
state: recorded.state.state,
|
|
90
|
+
changedPaths: unboxPaths(recorded.changedPaths)
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
const applyOps = (state, ops) => {
|
|
94
|
+
const events = [];
|
|
95
|
+
const changedPaths = [];
|
|
96
|
+
let segment = [];
|
|
97
|
+
const flush = () => {
|
|
98
|
+
if (segment.length === 0) return;
|
|
99
|
+
const ops = segment;
|
|
100
|
+
segment = [];
|
|
101
|
+
const recorded = recordMutations({ state }, (draft) => {
|
|
102
|
+
for (const op of ops) applyOpToDraft(draft, op);
|
|
103
|
+
});
|
|
104
|
+
state = recorded.state.state;
|
|
105
|
+
changedPaths.push(...unboxPaths(recorded.changedPaths));
|
|
106
|
+
};
|
|
107
|
+
for (const op of ops) if (op.op === "event") {
|
|
108
|
+
flush();
|
|
109
|
+
events.push({
|
|
110
|
+
event: sanitizeValue(op.value),
|
|
111
|
+
state
|
|
112
|
+
});
|
|
113
|
+
} else if (op.op === "replace" || op.op === "add" || op.op === "append" || op.op === "remove") segment.push(op);
|
|
114
|
+
else warnUnknownOp(op.op);
|
|
115
|
+
flush();
|
|
116
|
+
return {
|
|
117
|
+
state,
|
|
118
|
+
events,
|
|
119
|
+
changedPaths
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
//#endregion
|
|
123
|
+
export { applyOp, applyOps, sanitizeValue };
|
|
124
|
+
|
|
125
|
+
//# sourceMappingURL=ops.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ops.js","names":["recordMutations","StatewirePath","StatewireOp","op","path","value","isUnsafe","key","parent","warnStripped","process","env","console","warn","undefined","sanitizeValue","Date","getTime","Array","isArray","map","item","out","Record","Object","keys","MutableOp","Extract","invalid","Error","join","appended","current","applyAtRoot","box","state","applyInList","list","index","length","splice","applyInObject","obj","exists","hasOwn","applyOpToDraft","node","depth","last","Number","isInteger","warnedOps","Set","warnUnknownOp","type","has","add","JSON","stringify","unboxPaths","paths","slice","applyOp","root","changedPaths","recorded","draft","applyOps","ops","events","event","segment","flush","push"],"sources":["../../src/wire/ops.ts"],"sourcesContent":["import { recordMutations } from \"../client/optimistic\";\n\nexport type StatewirePath = readonly (string | number)[];\n\nexport type StatewireOp =\n | { op: \"replace\"; path: StatewirePath; value: unknown }\n | { op: \"add\"; path: StatewirePath; value: unknown }\n | { op: \"append\"; path: StatewirePath; value: unknown }\n | { op: \"remove\"; path: StatewirePath }\n | { op: \"event\"; value: object };\n\nconst isUnsafe = (key: string, parent: string | number | undefined) =>\n key === \"__proto__\" || (key === \"prototype\" && parent === \"constructor\");\n\nconst warnStripped = (key: string, parent: string | undefined) => {\n if (\n typeof process === \"undefined\" ||\n process.env[\"NODE_ENV\"] !== \"production\"\n ) {\n console.warn(\n `statewire: stripped unsafe key \"${key}\"` +\n (parent === undefined ? \"\" : ` under \"${parent}\"`),\n );\n }\n};\n\nexport const sanitizeValue = (value: unknown, parent?: string): unknown => {\n if (typeof value !== \"object\" || value === null) return value;\n if (value instanceof Date) return new Date(value.getTime());\n if (Array.isArray(value)) return value.map((item) => sanitizeValue(item));\n const out: Record<string, unknown> = {};\n for (const key of Object.keys(value)) {\n if (isUnsafe(key, parent)) warnStripped(key, parent);\n else out[key] = sanitizeValue((value as Record<string, unknown>)[key], key);\n }\n return out;\n};\n\ntype MutableOp = Extract<\n StatewireOp,\n { op: \"replace\" | \"add\" | \"append\" | \"remove\" }\n>;\n\nconst invalid = (op: MutableOp) =>\n new Error(`Statewire cannot apply ${op.op} at path: ${op.path.join(\".\")}`);\n\nconst appended = (\n current: unknown,\n op: Extract<MutableOp, { op: \"append\" }>,\n): string => {\n if (typeof current !== \"string\" || typeof op.value !== \"string\") {\n throw invalid(op);\n }\n return current + op.value;\n};\n\nconst applyAtRoot = (box: { state: unknown }, op: MutableOp) => {\n if (op.op === \"replace\") {\n box.state = sanitizeValue(op.value);\n } else if (op.op === \"append\") {\n box.state = appended(box.state, op);\n } else {\n throw invalid(op);\n }\n};\n\nconst applyInList = (list: unknown[], index: number, op: MutableOp) => {\n if (op.op === \"replace\") {\n if (index >= list.length) throw invalid(op);\n list[index] = sanitizeValue(op.value);\n } else if (op.op === \"add\") {\n if (index > list.length) throw invalid(op);\n list.splice(index, 0, sanitizeValue(op.value));\n } else if (op.op === \"append\") {\n if (index >= list.length) throw invalid(op);\n list[index] = appended(list[index], op);\n } else {\n if (index >= list.length) throw invalid(op);\n list.splice(index, 1);\n }\n};\n\nconst applyInObject = (\n obj: Record<string, unknown>,\n key: string,\n op: MutableOp,\n) => {\n const exists = Object.hasOwn(obj, key);\n if (op.op === \"replace\") {\n obj[key] = sanitizeValue(op.value, key);\n } else if (op.op === \"append\") {\n obj[key] = appended(exists ? obj[key] : undefined, op);\n } else if (op.op === \"remove\") {\n if (!exists) throw invalid(op);\n delete obj[key];\n } else {\n throw invalid(op);\n }\n};\n\nconst applyOpToDraft = (box: { state: unknown }, op: MutableOp): void => {\n if (op.path.length === 0) {\n applyAtRoot(box, op);\n return;\n }\n let node: unknown = box.state;\n for (let depth = 0; depth < op.path.length; depth++) {\n const key = op.path[depth]!;\n const last = depth === op.path.length - 1;\n if (typeof key === \"number\") {\n if (!Array.isArray(node) || !Number.isInteger(key) || key < 0) {\n throw invalid(op);\n }\n if (last) {\n applyInList(node, key, op);\n return;\n }\n if (key >= node.length) throw invalid(op);\n node = node[key];\n } else {\n if (\n isUnsafe(key, op.path[depth - 1]) ||\n typeof node !== \"object\" ||\n node === null ||\n Array.isArray(node)\n ) {\n throw invalid(op);\n }\n const obj = node as Record<string, unknown>;\n if (last) {\n applyInObject(obj, key, op);\n return;\n }\n if (!Object.hasOwn(obj, key)) throw invalid(op);\n node = obj[key];\n }\n }\n};\n\nconst warnedOps = new Set<unknown>();\nconst warnUnknownOp = (type: unknown) => {\n if (warnedOps.has(type)) return;\n warnedOps.add(type);\n console.warn(`statewire: ignoring unknown op type ${JSON.stringify(type)}`);\n};\n\nconst unboxPaths = (paths: readonly StatewirePath[]): StatewirePath[] =>\n paths.map((path) => path.slice(1));\n\nexport const applyOp = (\n root: unknown,\n op: MutableOp,\n): { state: unknown; changedPaths: StatewirePath[] } => {\n const recorded = recordMutations({ state: root }, (draft) =>\n applyOpToDraft(draft, op),\n );\n return {\n state: recorded.state.state,\n changedPaths: unboxPaths(recorded.changedPaths),\n };\n};\n\nexport const applyOps = (\n state: unknown,\n ops: readonly unknown[],\n): {\n state: unknown;\n events: { event: unknown; state: unknown }[];\n changedPaths: StatewirePath[];\n} => {\n const events: { event: unknown; state: unknown }[] = [];\n const changedPaths: StatewirePath[] = [];\n let segment: MutableOp[] = [];\n const flush = () => {\n if (segment.length === 0) return;\n const ops = segment;\n segment = [];\n const recorded = recordMutations({ state }, (draft) => {\n for (const op of ops) applyOpToDraft(draft, op);\n });\n state = recorded.state.state;\n changedPaths.push(...unboxPaths(recorded.changedPaths));\n };\n for (const op of ops as StatewireOp[]) {\n if (op.op === \"event\") {\n flush();\n events.push({ event: sanitizeValue(op.value), state });\n } else if (\n op.op === \"replace\" ||\n op.op === \"add\" ||\n op.op === \"append\" ||\n op.op === \"remove\"\n ) {\n segment.push(op);\n } else {\n warnUnknownOp((op as { op: unknown }).op);\n }\n }\n flush();\n return { state, events, changedPaths };\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,11 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "statewire",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "State over the wire: snapshot-first delta streaming and command submission for any state and any command",
|
|
5
|
+
"license": "MIT",
|
|
5
6
|
"repository": {
|
|
6
7
|
"type": "git",
|
|
7
|
-
"url": "https://github.com/assistant-ui/harness-sdk"
|
|
8
|
+
"url": "git+https://github.com/assistant-ui/harness-sdk.git",
|
|
9
|
+
"directory": "packages/statewire"
|
|
8
10
|
},
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
}
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./server": {
|
|
21
|
+
"types": "./dist/server.d.ts",
|
|
22
|
+
"default": "./dist/server.js"
|
|
23
|
+
},
|
|
24
|
+
"./testing": {
|
|
25
|
+
"types": "./dist/transports/testing.d.ts",
|
|
26
|
+
"default": "./dist/transports/testing.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"main": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"src",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@assistant-ui/tap": "^0.9.6"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"react": "^18 || ^19"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@assistant-ui/x-buildutils": "^0.0.20",
|
|
44
|
+
"@types/react": "^19.2.17",
|
|
45
|
+
"babel-plugin-react-compiler": "^1.0.0",
|
|
46
|
+
"react": "^19.2.8",
|
|
47
|
+
"vitest": "^4.1.10"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "aui-build",
|
|
51
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
52
|
+
"test": "vitest run"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { StatewirePath } from "../wire/ops";
|
|
2
|
+
import type { StatewireChanges } from "../types";
|
|
3
|
+
|
|
4
|
+
const lookupValue = (state: unknown, path: StatewirePath): unknown => {
|
|
5
|
+
let node = state;
|
|
6
|
+
for (const key of path) {
|
|
7
|
+
if (typeof node !== "object" || node === null) return undefined;
|
|
8
|
+
node = Object.hasOwn(node, key)
|
|
9
|
+
? (node as Record<string, unknown>)[key]
|
|
10
|
+
: undefined;
|
|
11
|
+
}
|
|
12
|
+
return node;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const keysAt = (node: unknown): string[] =>
|
|
16
|
+
typeof node === "object" && node !== null ? Object.keys(node) : [];
|
|
17
|
+
|
|
18
|
+
const isPrefix = (prefix: StatewirePath, path: StatewirePath): boolean =>
|
|
19
|
+
prefix.length <= path.length &&
|
|
20
|
+
prefix.every((key, i) => String(key) === String(path[i]));
|
|
21
|
+
|
|
22
|
+
export const createChanges = (
|
|
23
|
+
prev: unknown,
|
|
24
|
+
next: unknown,
|
|
25
|
+
changedPaths: readonly StatewirePath[],
|
|
26
|
+
): StatewireChanges => ({
|
|
27
|
+
isChangedAt: (path) =>
|
|
28
|
+
changedPaths.some((p) => isPrefix(p, path) || isPrefix(path, p)),
|
|
29
|
+
getChangedKeys: (path) => {
|
|
30
|
+
const wiped = changedPaths.some(
|
|
31
|
+
(p) => p.length <= path.length && isPrefix(p, path),
|
|
32
|
+
);
|
|
33
|
+
if (wiped) {
|
|
34
|
+
const keys = keysAt(lookupValue(next, path));
|
|
35
|
+
for (const key of keysAt(lookupValue(prev, path))) {
|
|
36
|
+
if (!keys.includes(key)) keys.push(key);
|
|
37
|
+
}
|
|
38
|
+
return keys;
|
|
39
|
+
}
|
|
40
|
+
const keys: string[] = [];
|
|
41
|
+
for (const p of changedPaths) {
|
|
42
|
+
if (p.length <= path.length || !isPrefix(path, p)) continue;
|
|
43
|
+
const key = String(p[path.length]);
|
|
44
|
+
if (!keys.includes(key)) keys.push(key);
|
|
45
|
+
}
|
|
46
|
+
return keys;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { flushTapSync } from "@assistant-ui/tap";
|
|
3
|
+
import { reviveOps, reviveResponse, reviveValue } from "../wire/meta";
|
|
4
|
+
import { backoff } from "./timing";
|
|
5
|
+
import type {
|
|
6
|
+
ConnectionState,
|
|
7
|
+
RemoteLink,
|
|
8
|
+
StatewireConnection,
|
|
9
|
+
StatewireTransportProps,
|
|
10
|
+
} from "../transports/sse";
|
|
11
|
+
|
|
12
|
+
export type Episode = {
|
|
13
|
+
cause: "dropped" | "ended" | "error";
|
|
14
|
+
message?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type StatewireFinish = {
|
|
18
|
+
reason: "ended" | "error" | "gone" | "superseded" | (string & {});
|
|
19
|
+
message?: string;
|
|
20
|
+
payload?: unknown;
|
|
21
|
+
meta?: unknown;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type StatewireResponse = {
|
|
25
|
+
seq: number;
|
|
26
|
+
type: "accepted" | "rejected" | "pending" | "crashed" | "result-unavailable";
|
|
27
|
+
code?: string;
|
|
28
|
+
message?: string;
|
|
29
|
+
payload?: unknown;
|
|
30
|
+
meta?: unknown;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type StatewireSyn = {
|
|
34
|
+
lastSeq: number;
|
|
35
|
+
lease?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type StatewireEnvelope = {
|
|
39
|
+
ops?: unknown[];
|
|
40
|
+
res?: StatewireResponse[];
|
|
41
|
+
ack?: number;
|
|
42
|
+
syn?: StatewireSyn;
|
|
43
|
+
fin?: StatewireFinish;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const createGate = () => {
|
|
47
|
+
let latched = false;
|
|
48
|
+
let wake: (() => void) | undefined;
|
|
49
|
+
return {
|
|
50
|
+
take: () => {
|
|
51
|
+
const was = latched;
|
|
52
|
+
latched = false;
|
|
53
|
+
return was;
|
|
54
|
+
},
|
|
55
|
+
wait: (timeoutMs?: number) => {
|
|
56
|
+
if (latched) {
|
|
57
|
+
latched = false;
|
|
58
|
+
return Promise.resolve();
|
|
59
|
+
}
|
|
60
|
+
return new Promise<void>((resolve) => {
|
|
61
|
+
const timer =
|
|
62
|
+
timeoutMs === undefined
|
|
63
|
+
? undefined
|
|
64
|
+
: setTimeout(() => {
|
|
65
|
+
wake = undefined;
|
|
66
|
+
resolve();
|
|
67
|
+
}, timeoutMs);
|
|
68
|
+
wake = () => {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
wake = undefined;
|
|
71
|
+
resolve();
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
open: () => {
|
|
76
|
+
if (wake) wake();
|
|
77
|
+
else latched = true;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type Gate = ReturnType<typeof createGate>;
|
|
83
|
+
|
|
84
|
+
export type ConnBase = {
|
|
85
|
+
connection: StatewireConnection;
|
|
86
|
+
hasSnapshot: boolean;
|
|
87
|
+
lease: string | null;
|
|
88
|
+
attempt: number;
|
|
89
|
+
episode: Episode | null;
|
|
90
|
+
gate: Gate;
|
|
91
|
+
abort: AbortController | null;
|
|
92
|
+
lifetime: AbortController | null;
|
|
93
|
+
latest: {
|
|
94
|
+
readonly current: {
|
|
95
|
+
options: { onChange?: (connection: StatewireConnection) => void };
|
|
96
|
+
ctx: StatewireTransportProps;
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
notify: (connection: StatewireConnection) => void;
|
|
100
|
+
reconnect: () => void;
|
|
101
|
+
attemptOnce: (
|
|
102
|
+
abort: AbortController,
|
|
103
|
+
) => Promise<"stop" | "reattach" | "ended">;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export const setStatus = (c: ConnBase, next: ConnectionState) => {
|
|
107
|
+
const changed = next.status !== c.connection.status;
|
|
108
|
+
c.connection = { ...next, reconnect: c.reconnect };
|
|
109
|
+
c.notify(c.connection);
|
|
110
|
+
if (changed) c.latest.current.options.onChange?.(c.connection);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const deliverEnvelope = (
|
|
114
|
+
c: ConnBase,
|
|
115
|
+
env: Pick<StatewireEnvelope, "ops" | "res" | "ack" | "syn">,
|
|
116
|
+
attached: boolean,
|
|
117
|
+
) => {
|
|
118
|
+
if (env.syn?.lease !== undefined) c.lease = env.syn.lease;
|
|
119
|
+
const ctx = c.latest.current.ctx;
|
|
120
|
+
const ops = env.ops === undefined ? [] : reviveOps(env.ops);
|
|
121
|
+
const res = env.res?.map(reviveResponse);
|
|
122
|
+
if (!attached) {
|
|
123
|
+
ctx.onSnapshot({ ops, lastSeq: env.syn?.lastSeq ?? -1 });
|
|
124
|
+
} else if (
|
|
125
|
+
env.ops !== undefined ||
|
|
126
|
+
env.res !== undefined ||
|
|
127
|
+
env.ack !== undefined
|
|
128
|
+
) {
|
|
129
|
+
ctx.onOps({
|
|
130
|
+
ops,
|
|
131
|
+
...(res !== undefined && { res }),
|
|
132
|
+
...(env.ack !== undefined && { ack: env.ack }),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
c.hasSnapshot = true;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const isTransientFinish = (
|
|
139
|
+
reason: StatewireFinish["reason"],
|
|
140
|
+
): reason is "ended" | "error" => reason === "ended" || reason === "error";
|
|
141
|
+
|
|
142
|
+
export const settleEnvelope = (
|
|
143
|
+
c: ConnBase,
|
|
144
|
+
env: StatewireEnvelope,
|
|
145
|
+
): "stop" | "reattach" | null => {
|
|
146
|
+
let finish = env.fin;
|
|
147
|
+
if (finish) {
|
|
148
|
+
if (finish.meta !== undefined) {
|
|
149
|
+
const { meta, ...rest } = finish;
|
|
150
|
+
finish = { ...rest, payload: reviveValue(rest.payload, meta) };
|
|
151
|
+
}
|
|
152
|
+
c.latest.current.ctx.onFinish(finish);
|
|
153
|
+
if (isTransientFinish(finish.reason)) {
|
|
154
|
+
c.episode = {
|
|
155
|
+
cause: finish.reason,
|
|
156
|
+
...(finish.message !== undefined && { message: finish.message }),
|
|
157
|
+
};
|
|
158
|
+
return "reattach";
|
|
159
|
+
}
|
|
160
|
+
setStatus(c, {
|
|
161
|
+
status: "gone",
|
|
162
|
+
...(finish.message !== undefined && { message: finish.message }),
|
|
163
|
+
...(finish.payload !== undefined && { payload: finish.payload }),
|
|
164
|
+
});
|
|
165
|
+
return "stop";
|
|
166
|
+
}
|
|
167
|
+
if (c.connection.status !== "live") {
|
|
168
|
+
c.attempt = 0;
|
|
169
|
+
c.episode = null;
|
|
170
|
+
setStatus(c, { status: "live" });
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const handleEnvelope = (
|
|
176
|
+
c: ConnBase,
|
|
177
|
+
env: StatewireEnvelope,
|
|
178
|
+
attached: boolean,
|
|
179
|
+
): "stop" | "reattach" | null => {
|
|
180
|
+
deliverEnvelope(c, env, attached);
|
|
181
|
+
return settleEnvelope(c, env);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export const scheduleRetry = (
|
|
185
|
+
c: ConnBase,
|
|
186
|
+
failure: string | undefined,
|
|
187
|
+
immediate: boolean,
|
|
188
|
+
retryAfterMs?: number,
|
|
189
|
+
) => {
|
|
190
|
+
c.attempt += 1;
|
|
191
|
+
c.episode ??= { cause: "dropped" };
|
|
192
|
+
const delay = immediate ? 0 : (retryAfterMs ?? backoff(c.attempt));
|
|
193
|
+
if (c.hasSnapshot) {
|
|
194
|
+
setStatus(c, {
|
|
195
|
+
status: "reconnecting",
|
|
196
|
+
...c.episode,
|
|
197
|
+
attempt: c.attempt,
|
|
198
|
+
nextRetryAt: Date.now() + delay,
|
|
199
|
+
...(failure !== undefined && { lastError: failure }),
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
return delay;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export const reconnectConn = (c: ConnBase) => {
|
|
206
|
+
if (!c.lifetime) return;
|
|
207
|
+
if (c.connection.status === "gone") {
|
|
208
|
+
c.attempt = 0;
|
|
209
|
+
c.episode = { cause: "dropped" };
|
|
210
|
+
flushTapSync(() => {
|
|
211
|
+
if (c.hasSnapshot) scheduleRetry(c, undefined, true);
|
|
212
|
+
else setStatus(c, { status: "connecting" });
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
c.gate.open();
|
|
216
|
+
c.abort?.abort();
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const run = async (c: ConnBase, lifetime: AbortController, gate: Gate) => {
|
|
220
|
+
const alive = () => c.lifetime === lifetime;
|
|
221
|
+
while (alive()) {
|
|
222
|
+
const abort = new AbortController();
|
|
223
|
+
c.abort = abort;
|
|
224
|
+
let failure: string | undefined;
|
|
225
|
+
let retryAfterMs: number | undefined;
|
|
226
|
+
let gone = false;
|
|
227
|
+
try {
|
|
228
|
+
gone = (await c.attemptOnce(abort)) === "stop";
|
|
229
|
+
} catch (error) {
|
|
230
|
+
if (!abort.signal.aborted) {
|
|
231
|
+
failure =
|
|
232
|
+
error instanceof Error && error.message ? error.message : "network";
|
|
233
|
+
if (
|
|
234
|
+
error instanceof Error &&
|
|
235
|
+
"retryAfterMs" in error &&
|
|
236
|
+
typeof error.retryAfterMs === "number"
|
|
237
|
+
) {
|
|
238
|
+
retryAfterMs = error.retryAfterMs;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
abort.abort();
|
|
243
|
+
if (!alive()) return;
|
|
244
|
+
if (gone) {
|
|
245
|
+
await gate.wait();
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const immediate = gate.take();
|
|
249
|
+
const delay = flushTapSync(() =>
|
|
250
|
+
scheduleRetry(c, failure, immediate, retryAfterMs),
|
|
251
|
+
);
|
|
252
|
+
if (delay === 0) continue;
|
|
253
|
+
await gate.wait(delay);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const startConn = (c: ConnBase) => {
|
|
258
|
+
c.hasSnapshot = false;
|
|
259
|
+
c.lease = null;
|
|
260
|
+
c.attempt = 0;
|
|
261
|
+
c.episode = null;
|
|
262
|
+
if (c.connection.status !== "connecting") {
|
|
263
|
+
setStatus(c, { status: "connecting" });
|
|
264
|
+
}
|
|
265
|
+
c.lifetime = new AbortController();
|
|
266
|
+
c.gate = createGate();
|
|
267
|
+
void run(c, c.lifetime, c.gate);
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const stopConn = (c: ConnBase) => {
|
|
271
|
+
c.abort?.abort();
|
|
272
|
+
c.lifetime?.abort();
|
|
273
|
+
c.lifetime = null;
|
|
274
|
+
c.gate.open();
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export const useStatewireConn = <
|
|
278
|
+
TOptions extends {
|
|
279
|
+
url: string;
|
|
280
|
+
onChange?: (connection: StatewireConnection) => void;
|
|
281
|
+
},
|
|
282
|
+
>(
|
|
283
|
+
options: TOptions,
|
|
284
|
+
ctx: StatewireTransportProps,
|
|
285
|
+
create: (
|
|
286
|
+
latest: {
|
|
287
|
+
readonly current: {
|
|
288
|
+
options: TOptions;
|
|
289
|
+
ctx: StatewireTransportProps;
|
|
290
|
+
};
|
|
291
|
+
},
|
|
292
|
+
notify: (connection: StatewireConnection) => void,
|
|
293
|
+
) => ConnBase & { send: RemoteLink["send"] },
|
|
294
|
+
): RemoteLink => {
|
|
295
|
+
const latest = useRef({ options, ctx });
|
|
296
|
+
const [connection, setConnection] = useState<StatewireConnection | null>(
|
|
297
|
+
null,
|
|
298
|
+
);
|
|
299
|
+
const [c] = useState(() => create(latest, setConnection));
|
|
300
|
+
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
latest.current = { options, ctx };
|
|
303
|
+
});
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
startConn(c);
|
|
306
|
+
return () => stopConn(c);
|
|
307
|
+
}, [c, options.url]);
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
connection: connection ?? c.connection,
|
|
311
|
+
send: c.send,
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
export const classify = (timeout: AbortSignal) =>
|
|
316
|
+
new Error(timeout.aborted ? "timeout" : "network");
|
|
317
|
+
|
|
318
|
+
export const parseRetryAfter = (header: string | null): number | undefined => {
|
|
319
|
+
if (!header) return undefined;
|
|
320
|
+
const value = header.trim();
|
|
321
|
+
if (/^\d+$/.test(value)) return Number(value) * 1000;
|
|
322
|
+
const date = Date.parse(value);
|
|
323
|
+
return Number.isNaN(date) ? undefined : Math.max(0, date - Date.now());
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
export const overloadedError = (retryAfterMs?: number) =>
|
|
327
|
+
Object.assign(new Error("overloaded"), { retryAfterMs });
|