iterate 0.2.6 → 0.3.0
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 +86 -76
- package/THIRD_PARTY_NOTICES.md +55 -0
- package/bin/iterate.js +18 -3
- package/dist/api-url-B6404M82.mjs +17 -0
- package/dist/api-url-B6404M82.mjs.map +1 -0
- package/dist/app-ref-BipL0feU.mjs +35 -0
- package/dist/app-ref-BipL0feU.mjs.map +1 -0
- package/dist/app-ref-C1CrgXqX.mjs +7 -0
- package/dist/app-ref-C1CrgXqX.mjs.map +1 -0
- package/dist/app-ref-DYai_om1.mjs +7 -0
- package/dist/app-ref-DYai_om1.mjs.map +1 -0
- package/dist/cli-D0c-pDL_.mjs +1010 -0
- package/dist/cli-D0c-pDL_.mjs.map +1 -0
- package/dist/client.d.ts +3 -0
- package/dist/client.mjs +4 -0
- package/dist/cloudflare-BTm90gQ4.mjs +951 -0
- package/dist/cloudflare-BTm90gQ4.mjs.map +1 -0
- package/dist/contract-s4FW4eES.mjs +309 -0
- package/dist/contract-s4FW4eES.mjs.map +1 -0
- package/dist/document-review/index.d.ts +5 -0
- package/dist/document-review/types.d.ts +107 -0
- package/dist/document-review.mjs +7015 -0
- package/dist/document-review.mjs.map +1 -0
- package/dist/durable-object-processor-durability-CNsTjAJS.mjs +205 -0
- package/dist/durable-object-processor-durability-CNsTjAJS.mjs.map +1 -0
- package/dist/idempotency-DleloJNt.mjs +28 -0
- package/dist/idempotency-DleloJNt.mjs.map +1 -0
- package/dist/index.mjs +1 -1
- package/dist/itx/api-url.d.ts +6 -0
- package/dist/itx/itx-node-client.d.ts +65 -0
- package/dist/itx/itx-session.d.ts +215 -0
- package/dist/itx/owned-rpc-session.d.ts +14 -0
- package/dist/itx/query-client.d.ts +10 -0
- package/dist/itx-api.generated.d.ts +6195 -0
- package/dist/itx-session-sjud8GiT.mjs +534 -0
- package/dist/itx-session-sjud8GiT.mjs.map +1 -0
- package/dist/live-state-BJNqOwFw.mjs +299 -0
- package/dist/live-state-BJNqOwFw.mjs.map +1 -0
- package/dist/next/api.d.ts +479 -0
- package/dist/next/api.mjs +0 -0
- package/dist/next/app-server.d.ts +44 -0
- package/dist/next/app-server.mjs +479 -0
- package/dist/next/app-server.mjs.map +1 -0
- package/dist/next/app-session.d.ts +49 -0
- package/dist/next/app-session.mjs +238 -0
- package/dist/next/app-session.mjs.map +1 -0
- package/dist/next/app.d.ts +29 -0
- package/dist/next/app.mjs +141 -0
- package/dist/next/app.mjs.map +1 -0
- package/dist/next/client/live-state.d.ts +63 -0
- package/dist/next/client/oauth.d.ts +12 -0
- package/dist/next/client/react.d.ts +109 -0
- package/dist/next/client/socket.d.ts +6 -0
- package/dist/next/client.mjs +156 -0
- package/dist/next/client.mjs.map +1 -0
- package/dist/next/expression.d.ts +146 -0
- package/dist/next/expression.mjs +399 -0
- package/dist/next/expression.mjs.map +1 -0
- package/dist/next/lib.d.ts +56 -0
- package/dist/next/lib.mjs +199 -0
- package/dist/next/lib.mjs.map +1 -0
- package/dist/next/oauth-scopes.d.ts +32 -0
- package/dist/next/oauth-scopes.mjs +40 -0
- package/dist/next/oauth-scopes.mjs.map +1 -0
- package/dist/next/oauth.mjs +29 -0
- package/dist/next/oauth.mjs.map +1 -0
- package/dist/next/principal.d.ts +64 -0
- package/dist/next/principal.mjs +98 -0
- package/dist/next/principal.mjs.map +1 -0
- package/dist/next/project-ingress.d.ts +37 -0
- package/dist/next/project-ingress.mjs +75 -0
- package/dist/next/project-ingress.mjs.map +1 -0
- package/dist/next/react.mjs +285 -0
- package/dist/next/react.mjs.map +1 -0
- package/dist/next/sdk/auth.d.ts +5 -0
- package/dist/next/sdk/index.d.ts +112 -0
- package/dist/next/sdk.mjs +139 -0
- package/dist/next/sdk.mjs.map +1 -0
- package/dist/next/stream/processor.d.ts +378 -0
- package/dist/next/stream/processor.mjs +582 -0
- package/dist/next/stream/processor.mjs.map +1 -0
- package/dist/next/stream/run.d.ts +58 -0
- package/dist/next/stream/run.mjs +40 -0
- package/dist/next/stream/run.mjs.map +1 -0
- package/dist/next-node.d.ts +15 -0
- package/dist/next-node.mjs +51 -0
- package/dist/next-node.mjs.map +1 -0
- package/dist/node.d.ts +3 -0
- package/dist/node.mjs +185 -0
- package/dist/node.mjs.map +1 -0
- package/dist/processor-host-capabilities-BMFH3KTM.mjs +56 -0
- package/dist/processor-host-capabilities-BMFH3KTM.mjs.map +1 -0
- package/dist/processors/cloudflare.d.ts +3 -0
- package/dist/processors/durable-object-processor-durability.d.ts +79 -0
- package/dist/processors/event-consumption-metrics.d.ts +82 -0
- package/dist/processors/idempotency.d.ts +13 -0
- package/dist/processors/index.d.ts +12 -0
- package/dist/processors/processor-contracts.d.ts +342 -0
- package/dist/processors/processor-facet.d.ts +186 -0
- package/dist/processors/processor-host-capabilities.d.ts +60 -0
- package/dist/processors/prompt-sections.d.ts +17 -0
- package/dist/processors/rpc-types.d.ts +515 -0
- package/dist/processors/schemas.d.ts +102 -0
- package/dist/processors/stream-handle.d.ts +45 -0
- package/dist/processors/stream-processor-keepalive.d.ts +95 -0
- package/dist/processors/stream-processor-registry.d.ts +233 -0
- package/dist/processors/stream-processor-runner.d.ts +289 -0
- package/dist/processors/stream-processor.d.ts +339 -0
- package/dist/processors/stream-runtime-metrics.d.ts +107 -0
- package/dist/processors/testing.d.ts +302 -0
- package/dist/processors-BoNyeBfQ.mjs +10 -0
- package/dist/processors-BoNyeBfQ.mjs.map +1 -0
- package/dist/processors-cloudflare.mjs +3 -0
- package/dist/processors-testing.mjs +435 -0
- package/dist/processors-testing.mjs.map +1 -0
- package/dist/processors.mjs +52 -0
- package/dist/processors.mjs.map +1 -0
- package/dist/protocol-DnK_f2m6.mjs +251 -0
- package/dist/protocol-DnK_f2m6.mjs.map +1 -0
- package/dist/sdk/capnweb/index.d.ts +2 -0
- package/dist/sdk/capnweb/live-state/compact.d.ts +5 -0
- package/dist/sdk/capnweb/live-state/diff.d.ts +41 -0
- package/dist/sdk/capnweb/live-state/engine.d.ts +44 -0
- package/dist/sdk/capnweb/live-state/index.d.ts +41 -0
- package/dist/sdk/capnweb/live-state/protocol.d.ts +87 -0
- package/dist/sdk/capnweb/live-state/retain.d.ts +23 -0
- package/dist/sdk/capnweb/live-state/store.d.ts +20 -0
- package/dist/sdk/capnweb/live-state/types.d.ts +11 -0
- package/dist/sdk/capnweb/react.d.ts +45 -0
- package/dist/sdk/capnweb/react.mjs +316 -0
- package/dist/sdk/capnweb/react.mjs.map +1 -0
- package/dist/sdk/capnweb.mjs +4 -0
- package/dist/sdk/itx/react.d.ts +191 -0
- package/dist/sdk/itx/react.mjs +383 -0
- package/dist/sdk/itx/react.mjs.map +1 -0
- package/dist/sdk-DMB-IM11.mjs +933 -0
- package/dist/sdk-DMB-IM11.mjs.map +1 -0
- package/dist/sdk.d.ts +339 -0
- package/dist/sdk.mjs +2 -0
- package/dist/serve-itx.d.ts +46 -0
- package/dist/starter-apps/flake-dashboard/app-ref.d.ts +31 -0
- package/dist/starter-apps/flake-dashboard/configured-worker.mjs +1055 -0
- package/dist/starter-apps/flake-dashboard/configured-worker.mjs.map +1 -0
- package/dist/starter-apps/flake-dashboard/contract.d.ts +4839 -0
- package/dist/starter-apps/flake-dashboard/contract.mjs +2 -0
- package/dist/starter-apps/flake-dashboard/index.d.ts +17 -0
- package/dist/starter-apps/flake-dashboard/index.mjs +56 -0
- package/dist/starter-apps/flake-dashboard/index.mjs.map +1 -0
- package/dist/starter-apps/flake-dashboard/worker.d.ts +4607 -0
- package/dist/starter-apps/github-ai-linter/ai-linter.d.ts +8914 -0
- package/dist/starter-apps/github-ai-linter/configured-worker.mjs +17987 -0
- package/dist/starter-apps/github-ai-linter/configured-worker.mjs.map +1 -0
- package/dist/starter-apps/github-ai-linter/contract.d.ts +9193 -0
- package/dist/starter-apps/github-ai-linter/index.d.ts +10 -0
- package/dist/starter-apps/github-ai-linter/index.mjs +36 -0
- package/dist/starter-apps/github-ai-linter/index.mjs.map +1 -0
- package/dist/starter-apps/github-ai-linter/prompt.d.ts +13 -0
- package/dist/starter-apps/github-ai-linter/review-bot.d.ts +808 -0
- package/dist/starter-apps/github-ai-linter/rules.d.ts +34 -0
- package/dist/starter-apps/github-ai-linter/worker-ref.d.ts +19 -0
- package/dist/starter-apps/github-ai-linter/worker.d.ts +19 -0
- package/dist/starter-apps/github-ai-linter/worker.mjs +947 -0
- package/dist/starter-apps/github-ai-linter/worker.mjs.map +1 -0
- package/dist/starter-apps/guestbook/app-ref.d.ts +27 -0
- package/dist/starter-apps/guestbook/client.d.ts +7 -0
- package/dist/starter-apps/guestbook/client.mjs +59 -0
- package/dist/starter-apps/guestbook/configured-worker.mjs +205 -0
- package/dist/starter-apps/guestbook/configured-worker.mjs.map +1 -0
- package/dist/starter-apps/guestbook/index.d.ts +9 -0
- package/dist/starter-apps/guestbook/index.mjs +31 -0
- package/dist/starter-apps/guestbook/index.mjs.map +1 -0
- package/dist/starter-apps/guestbook/processor.d.ts +2267 -0
- package/dist/starter-apps/guestbook/worker.d.ts +26 -0
- package/dist/starter-apps/guestbook/worker.mjs +191 -0
- package/dist/starter-apps/guestbook/worker.mjs.map +1 -0
- package/dist/starter-apps/media/configured-worker.mjs +577 -0
- package/dist/starter-apps/media/configured-worker.mjs.map +1 -0
- package/dist/starter-apps/media/index.mjs +36 -0
- package/dist/starter-apps/media/index.mjs.map +1 -0
- package/dist/starter-apps/media/ref.mjs +20 -0
- package/dist/starter-apps/media/ref.mjs.map +1 -0
- package/dist/starter-apps/media/worker.mjs +579 -0
- package/dist/starter-apps/media/worker.mjs.map +1 -0
- package/dist/starter-apps/notes/configured-worker.mjs +6134 -0
- package/dist/starter-apps/notes/configured-worker.mjs.map +1 -0
- package/dist/starter-apps/notes/index.mjs +23 -0
- package/dist/starter-apps/notes/index.mjs.map +1 -0
- package/dist/starter-apps/notes/ref.mjs +21 -0
- package/dist/starter-apps/notes/ref.mjs.map +1 -0
- package/dist/starter-apps/notes/worker.mjs +427 -0
- package/dist/starter-apps/notes/worker.mjs.map +1 -0
- package/dist/starter-apps/todo/client.mjs +59 -0
- package/dist/starter-apps/todo/configured-worker.mjs +2864 -0
- package/dist/starter-apps/todo/configured-worker.mjs.map +1 -0
- package/dist/starter-apps/todo/index.d.ts +8 -0
- package/dist/starter-apps/todo/index.mjs +29 -0
- package/dist/starter-apps/todo/index.mjs.map +1 -0
- package/dist/stream-processor-keepalive-DAQTP6m3.mjs +2082 -0
- package/dist/stream-processor-keepalive-DAQTP6m3.mjs.map +1 -0
- package/dist/usingCtx-inzbY1Qz.mjs +57 -0
- package/dist/usingCtx-mZx5nsAW.mjs +11800 -0
- package/dist/usingCtx-mZx5nsAW.mjs.map +1 -0
- package/dist/worker-ref-DZxPDmb_.mjs +390 -0
- package/dist/worker-ref-DZxPDmb_.mjs.map +1 -0
- package/menubar/Iterate.entitlements +12 -0
- package/menubar/Iterate.swift +914 -0
- package/menubar/IterateIcon.swift +145 -0
- package/menubar/README.md +28 -0
- package/menubar/build-menubar-app.sh +59 -0
- package/package.json +235 -18
- package/dist/cli-DMS4kJph.mjs +0 -868
- package/dist/cli-DMS4kJph.mjs.map +0 -1
- package/dist/config-DtnR7Lv7.mjs +0 -170
- package/dist/config-DtnR7Lv7.mjs.map +0 -1
- package/dist/index.d.mts.map +0 -1
- package/dist/stream-tui/agent-chat-terminal.d.mts +0 -1
- package/dist/stream-tui/agent-chat-terminal.mjs +0 -933
- package/dist/stream-tui/agent-chat-terminal.mjs.map +0 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
//#region src/sdk/capnweb/live-state/diff.ts
|
|
2
|
+
/**
|
|
3
|
+
* Structural diff between two JSON values, producing the minimal `LiveStatePatch`
|
|
4
|
+
* — or `undefined` when nothing changed.
|
|
5
|
+
*
|
|
6
|
+
* The diff is REFERENCE-FIRST: identical references short-circuit immediately
|
|
7
|
+
* (`Object.is`), so the cost is O(changed), not O(size) — *as long as callers
|
|
8
|
+
* update state immutably* (unchanged sub-objects keep their identity). That one
|
|
9
|
+
* discipline is the whole performance story: a single touched row in a
|
|
10
|
+
* thousand-entry index yields one tiny patch instead of a full rescan.
|
|
11
|
+
*
|
|
12
|
+
* PLAIN objects (prototype `Object.prototype` or `null`) are treated as keyed
|
|
13
|
+
* maps and diffed per key. Dense arrays are diffed by position. Everything
|
|
14
|
+
* else — primitives, `null`, sparse arrays, and
|
|
15
|
+
* non-plain instances like `Date`/`Map`/`Set` — is a leaf, replaced wholesale.
|
|
16
|
+
* Descending into an instance would diff its own enumerable keys, which for a
|
|
17
|
+
* `Date` is NONE — two different Dates would read as "unchanged" and the
|
|
18
|
+
* subscriber would stay stale forever. Keyed objects remain preferable for
|
|
19
|
+
* collections whose entries frequently move; positional patches do not infer moves.
|
|
20
|
+
*/
|
|
21
|
+
function diff(prev, next, options = {}) {
|
|
22
|
+
if (Object.is(prev, next)) return void 0;
|
|
23
|
+
if (Array.isArray(prev) && Array.isArray(next) && options.arrays !== false) {
|
|
24
|
+
const items = [];
|
|
25
|
+
for (let index = 0; index < next.length; index++) {
|
|
26
|
+
if (!Object.hasOwn(next, index) || index < prev.length && !Object.hasOwn(prev, index)) return { set: next };
|
|
27
|
+
const patch = index < prev.length ? diff(prev[index], next[index], options) : { set: next[index] };
|
|
28
|
+
if (patch) items.push([index, patch]);
|
|
29
|
+
}
|
|
30
|
+
return items.length > 0 || prev.length !== next.length ? { array: {
|
|
31
|
+
length: next.length,
|
|
32
|
+
items
|
|
33
|
+
} } : void 0;
|
|
34
|
+
}
|
|
35
|
+
if (!isPlainObject(prev) || !isPlainObject(next)) return { set: next };
|
|
36
|
+
const fields = [];
|
|
37
|
+
const drop = [];
|
|
38
|
+
for (const key of Object.keys(next)) {
|
|
39
|
+
if (next[key] === void 0) {
|
|
40
|
+
if (Object.hasOwn(prev, key)) drop.push(key);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
const childPatch = diff(Object.hasOwn(prev, key) ? prev[key] : void 0, next[key], options);
|
|
44
|
+
if (childPatch !== void 0) fields.push([key, childPatch]);
|
|
45
|
+
}
|
|
46
|
+
for (const key of Object.keys(prev)) if (!Object.hasOwn(next, key)) drop.push(key);
|
|
47
|
+
if (fields.length === 0 && drop.length === 0) return void 0;
|
|
48
|
+
const patch = {};
|
|
49
|
+
if (fields.length > 0) patch.fields = Object.fromEntries(fields);
|
|
50
|
+
if (drop.length > 0) patch.drop = drop;
|
|
51
|
+
return patch;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Apply a `LiveStatePatch` to a previous value, returning the next value.
|
|
55
|
+
*
|
|
56
|
+
* Like `diff`, this preserves structural sharing: a fresh object is built only
|
|
57
|
+
* along changed paths, so untouched branches keep their previous reference.
|
|
58
|
+
* That's what lets a client selector (`s => s.streamsIndex`) skip re-rendering
|
|
59
|
+
* when an unrelated slice changed — the slice it reads stays `Object.is`-equal
|
|
60
|
+
* across applies.
|
|
61
|
+
*/
|
|
62
|
+
function applyPatch(prev, patch) {
|
|
63
|
+
if ("set" in patch) return patch.set;
|
|
64
|
+
if ("array" in patch) {
|
|
65
|
+
if (!Array.isArray(prev)) throw new Error("Live-state array patch requires an array baseline");
|
|
66
|
+
const next = prev.slice(0, patch.array.length);
|
|
67
|
+
next.length = patch.array.length;
|
|
68
|
+
for (const [index, child] of patch.array.items) {
|
|
69
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= next.length) throw new Error("Live-state array patch index is outside its resulting length");
|
|
70
|
+
next[index] = applyPatch(prev[index], child);
|
|
71
|
+
}
|
|
72
|
+
return next;
|
|
73
|
+
}
|
|
74
|
+
const base = isPlainObject(prev) ? prev : {};
|
|
75
|
+
const next = { ...base };
|
|
76
|
+
if (patch.fields) for (const [key, childPatch] of Object.entries(patch.fields)) Object.defineProperty(next, key, {
|
|
77
|
+
value: applyPatch(Object.hasOwn(base, key) ? base[key] : void 0, childPatch),
|
|
78
|
+
enumerable: true,
|
|
79
|
+
writable: true,
|
|
80
|
+
configurable: true
|
|
81
|
+
});
|
|
82
|
+
if (patch.drop) for (const key of patch.drop) delete next[key];
|
|
83
|
+
return next;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* A PLAIN object — prototype `Object.prototype` or `null` — the only thing
|
|
87
|
+
* `diff` descends into and `applyPatch` merges over. Arrays and class instances
|
|
88
|
+
* (`Date`, `Map`, `Set`, …) fail this on purpose: they carry state outside
|
|
89
|
+
* their own enumerable keys, so per-key diffing would misread them (see the
|
|
90
|
+
* `diff` docstring) — they are leaves, replaced wholesale.
|
|
91
|
+
*/
|
|
92
|
+
function isPlainObject(value) {
|
|
93
|
+
if (typeof value !== "object" || value === null) return false;
|
|
94
|
+
const proto = Object.getPrototypeOf(value);
|
|
95
|
+
return proto === Object.prototype || proto === null;
|
|
96
|
+
}
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/sdk/capnweb/live-state/compact.ts
|
|
99
|
+
const fieldAddresses = /* @__PURE__ */ new WeakMap();
|
|
100
|
+
function addresses(value) {
|
|
101
|
+
let cached = fieldAddresses.get(value);
|
|
102
|
+
if (!cached) {
|
|
103
|
+
const keys = Object.keys(value).filter((key) => value[key] !== void 0).sort();
|
|
104
|
+
cached = {
|
|
105
|
+
keys,
|
|
106
|
+
indices: new Map(keys.map((key, index) => [key, index]))
|
|
107
|
+
};
|
|
108
|
+
fieldAddresses.set(value, cached);
|
|
109
|
+
}
|
|
110
|
+
return cached;
|
|
111
|
+
}
|
|
112
|
+
/** Encode only the changed paths of an existing structural diff. */
|
|
113
|
+
function compactPatch(previous, patch) {
|
|
114
|
+
if ("set" in patch) {
|
|
115
|
+
const next = patch.set;
|
|
116
|
+
if (typeof previous === "string" && typeof next === "string" && previous.length >= 16 && previous.length <= 4096 && next.length > previous.length && next.startsWith(previous)) return [previous.length, next.slice(previous.length)];
|
|
117
|
+
if (next === null || typeof next === "string" || typeof next === "number" || typeof next === "boolean") return next;
|
|
118
|
+
return [next];
|
|
119
|
+
}
|
|
120
|
+
if ("array" in patch) {
|
|
121
|
+
if (!Array.isArray(previous)) throw new Error("Live-state array patch requires an array baseline");
|
|
122
|
+
const result = {};
|
|
123
|
+
if (patch.array.length !== previous.length) result["#"] = patch.array.length;
|
|
124
|
+
for (const [index, child] of patch.array.items) result[index] = compactPatch(previous[index], child);
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
if (!isPlainObject(previous)) throw new Error("Live-state object patch requires an object baseline");
|
|
128
|
+
const { indices } = addresses(previous);
|
|
129
|
+
const fields = [];
|
|
130
|
+
for (const [key, child] of Object.entries(patch.fields || {})) {
|
|
131
|
+
const index = indices.get(key);
|
|
132
|
+
fields.push([index === void 0 ? `+${key}` : String(index), compactPatch(index === void 0 ? void 0 : previous[key], child)]);
|
|
133
|
+
}
|
|
134
|
+
for (const key of patch.drop || []) {
|
|
135
|
+
const index = indices.get(key);
|
|
136
|
+
if (index !== void 0) fields.push([String(index), []]);
|
|
137
|
+
}
|
|
138
|
+
return Object.fromEntries(fields);
|
|
139
|
+
}
|
|
140
|
+
/** Apply a version 3 patch, retaining every untouched object's identity. */
|
|
141
|
+
function applyCompactPatch(previous, patch) {
|
|
142
|
+
return applyCompactValue(previous, patch);
|
|
143
|
+
}
|
|
144
|
+
function applyCompactValue(previous, patch) {
|
|
145
|
+
if (patch === null || typeof patch !== "object") return patch;
|
|
146
|
+
if (Array.isArray(patch)) {
|
|
147
|
+
if (patch.length === 1) return patch[0];
|
|
148
|
+
if (patch.length !== 2 || typeof previous !== "string" || previous.length !== patch[0]) throw new Error("Live-state append does not match its string baseline");
|
|
149
|
+
return previous + patch[1];
|
|
150
|
+
}
|
|
151
|
+
if (Array.isArray(previous)) {
|
|
152
|
+
const length = Object.hasOwn(patch, "#") ? patch["#"] : previous.length;
|
|
153
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > 4294967295) throw new Error("Live-state array patch has an invalid length");
|
|
154
|
+
const next = previous.slice(0, length);
|
|
155
|
+
next.length = length;
|
|
156
|
+
for (const [address, child] of Object.entries(patch)) {
|
|
157
|
+
if (address === "#") continue;
|
|
158
|
+
const index = position(address, length);
|
|
159
|
+
next[index] = applyCompactValue(previous[index], child);
|
|
160
|
+
}
|
|
161
|
+
return next;
|
|
162
|
+
}
|
|
163
|
+
if (!isPlainObject(previous)) throw new Error("Live-state object patch requires an object baseline");
|
|
164
|
+
const { keys } = addresses(previous);
|
|
165
|
+
const next = { ...previous };
|
|
166
|
+
for (const [address, child] of Object.entries(patch)) {
|
|
167
|
+
const added = address.startsWith("+");
|
|
168
|
+
const key = added ? address.slice(1) : keys[position(address, keys.length)];
|
|
169
|
+
if (added && Object.hasOwn(previous, key) && previous[key] !== void 0) throw new Error("Live-state new field already exists in its baseline");
|
|
170
|
+
if (Array.isArray(child) && child.length === 0) {
|
|
171
|
+
if (added) throw new Error("Live-state cannot delete a new field");
|
|
172
|
+
delete next[key];
|
|
173
|
+
} else Object.defineProperty(next, key, {
|
|
174
|
+
value: applyCompactValue(added ? void 0 : previous[key], child),
|
|
175
|
+
enumerable: true,
|
|
176
|
+
writable: true,
|
|
177
|
+
configurable: true
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return next;
|
|
181
|
+
}
|
|
182
|
+
function position(address, length) {
|
|
183
|
+
const index = Number(address);
|
|
184
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= length || String(index) !== address) throw new Error("Live-state field address is outside its baseline");
|
|
185
|
+
return index;
|
|
186
|
+
}
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/sdk/capnweb/live-state/store.ts
|
|
189
|
+
function createLiveStateStore() {
|
|
190
|
+
let held = {
|
|
191
|
+
revision: -1,
|
|
192
|
+
state: void 0
|
|
193
|
+
};
|
|
194
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
195
|
+
const notify = () => listeners.forEach((listener) => listener());
|
|
196
|
+
return {
|
|
197
|
+
getState: () => held.state,
|
|
198
|
+
subscribe: (listener) => {
|
|
199
|
+
listeners.add(listener);
|
|
200
|
+
return () => void listeners.delete(listener);
|
|
201
|
+
},
|
|
202
|
+
reset: () => {
|
|
203
|
+
held = {
|
|
204
|
+
revision: -1,
|
|
205
|
+
state: void 0
|
|
206
|
+
};
|
|
207
|
+
notify();
|
|
208
|
+
},
|
|
209
|
+
apply: (update, resync) => {
|
|
210
|
+
if ("s" in update) held = {
|
|
211
|
+
revision: update.s[0],
|
|
212
|
+
state: update.s[1]
|
|
213
|
+
};
|
|
214
|
+
else if ("p" in update) {
|
|
215
|
+
if (update.p[0] !== held.revision) {
|
|
216
|
+
resync();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
held = {
|
|
220
|
+
revision: update.p[1],
|
|
221
|
+
state: applyCompactPatch(held.state, update.p[2])
|
|
222
|
+
};
|
|
223
|
+
} else if (update.type === "snapshot") held = {
|
|
224
|
+
revision: update.revision,
|
|
225
|
+
state: update.state
|
|
226
|
+
};
|
|
227
|
+
else if (update.from !== held.revision) {
|
|
228
|
+
resync();
|
|
229
|
+
return;
|
|
230
|
+
} else held = {
|
|
231
|
+
revision: update.to,
|
|
232
|
+
state: applyPatch(held.state, update.patch)
|
|
233
|
+
};
|
|
234
|
+
notify();
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/sdk/capnweb/live-state/protocol.ts
|
|
240
|
+
function isLiveStateSnapshot(update) {
|
|
241
|
+
return "s" in update || "type" in update && update.type === "snapshot";
|
|
242
|
+
}
|
|
243
|
+
function liveStateRevision(update) {
|
|
244
|
+
if ("s" in update) return update.s[0];
|
|
245
|
+
if ("p" in update) return update.p[1];
|
|
246
|
+
return update.type === "snapshot" ? update.revision : update.to;
|
|
247
|
+
}
|
|
248
|
+
//#endregion
|
|
249
|
+
export { compactPatch as a, applyCompactPatch as i, liveStateRevision as n, applyPatch as o, createLiveStateStore as r, diff as s, isLiveStateSnapshot as t };
|
|
250
|
+
|
|
251
|
+
//# sourceMappingURL=protocol-DnK_f2m6.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol-DnK_f2m6.mjs","names":[],"sources":["../src/sdk/capnweb/live-state/diff.ts","../src/sdk/capnweb/live-state/compact.ts","../src/sdk/capnweb/live-state/store.ts","../src/sdk/capnweb/live-state/protocol.ts"],"sourcesContent":["import type { LiveStatePatch } from \"./protocol.ts\";\n\n/**\n * Structural diff between two JSON values, producing the minimal `LiveStatePatch`\n * — or `undefined` when nothing changed.\n *\n * The diff is REFERENCE-FIRST: identical references short-circuit immediately\n * (`Object.is`), so the cost is O(changed), not O(size) — *as long as callers\n * update state immutably* (unchanged sub-objects keep their identity). That one\n * discipline is the whole performance story: a single touched row in a\n * thousand-entry index yields one tiny patch instead of a full rescan.\n *\n * PLAIN objects (prototype `Object.prototype` or `null`) are treated as keyed\n * maps and diffed per key. Dense arrays are diffed by position. Everything\n * else — primitives, `null`, sparse arrays, and\n * non-plain instances like `Date`/`Map`/`Set` — is a leaf, replaced wholesale.\n * Descending into an instance would diff its own enumerable keys, which for a\n * `Date` is NONE — two different Dates would read as \"unchanged\" and the\n * subscriber would stay stale forever. Keyed objects remain preferable for\n * collections whose entries frequently move; positional patches do not infer moves.\n */\nexport function diff(\n prev: unknown,\n next: unknown,\n options: { arrays?: boolean } = {},\n): LiveStatePatch | undefined {\n if (Object.is(prev, next)) return undefined;\n if (Array.isArray(prev) && Array.isArray(next) && options.arrays !== false) {\n const items: [number, LiveStatePatch][] = [];\n for (let index = 0; index < next.length; index++) {\n // Sparse arrays retain the replacement semantics of their wire value.\n if (!Object.hasOwn(next, index) || (index < prev.length && !Object.hasOwn(prev, index))) {\n return { set: next };\n }\n const patch =\n index < prev.length ? diff(prev[index], next[index], options) : { set: next[index] };\n if (patch) items.push([index, patch]);\n }\n return items.length > 0 || prev.length !== next.length\n ? { array: { length: next.length, items } }\n : undefined;\n }\n if (!isPlainObject(prev) || !isPlainObject(next)) return { set: next };\n\n // Entries + fromEntries, not `bag[key] = …`: assignment with key \"__proto__\"\n // would SET THE BAG'S PROTOTYPE instead of recording the field — the change\n // would silently vanish from the patch. `Object.fromEntries` DEFINES own\n // properties (safe for any key) and yields an ordinary Object.prototype\n // object — which matters, because patches cross capnweb, whose serializer\n // accepts exactly Object.prototype (a null-proto bag reads as unsupported\n // and kills the push). (applyPatch has the mirror-image write guard.)\n // Own-property checks throughout (`Object.hasOwn`, not `in` / bare reads):\n // `\"__proto__\" in x` is true for EVERY object via inheritance, so `in` would\n // misread that key's presence in both directions.\n const fields: [string, LiveStatePatch][] = [];\n const drop: string[] = [];\n for (const key of Object.keys(next)) {\n if (next[key] === undefined) {\n if (Object.hasOwn(prev, key)) drop.push(key); // a key set to `undefined` reads as removed\n continue;\n }\n const childPatch = diff(Object.hasOwn(prev, key) ? prev[key] : undefined, next[key], options);\n if (childPatch !== undefined) fields.push([key, childPatch]);\n }\n for (const key of Object.keys(prev)) {\n if (!Object.hasOwn(next, key)) drop.push(key);\n }\n\n if (fields.length === 0 && drop.length === 0) return undefined;\n const patch: { fields?: Record<string, LiveStatePatch>; drop?: string[] } = {};\n if (fields.length > 0) patch.fields = Object.fromEntries(fields);\n if (drop.length > 0) patch.drop = drop;\n return patch;\n}\n\n/**\n * Apply a `LiveStatePatch` to a previous value, returning the next value.\n *\n * Like `diff`, this preserves structural sharing: a fresh object is built only\n * along changed paths, so untouched branches keep their previous reference.\n * That's what lets a client selector (`s => s.streamsIndex`) skip re-rendering\n * when an unrelated slice changed — the slice it reads stays `Object.is`-equal\n * across applies.\n */\nexport function applyPatch<State>(prev: State, patch: LiveStatePatch): State {\n if (\"set\" in patch) return patch.set as State;\n if (\"array\" in patch) {\n if (!Array.isArray(prev)) throw new Error(\"Live-state array patch requires an array baseline\");\n const next = prev.slice(0, patch.array.length);\n next.length = patch.array.length;\n for (const [index, child] of patch.array.items) {\n if (!Number.isSafeInteger(index) || index < 0 || index >= next.length) {\n throw new Error(\"Live-state array patch index is outside its resulting length\");\n }\n next[index] = applyPatch(prev[index], child);\n }\n // The array operation preserves State's shape; only the generic parameter\n // prevents TypeScript from expressing the Array.isArray narrowing on return.\n return next as State;\n }\n const base: Record<string, unknown> = isPlainObject(prev) ? prev : {};\n const next: Record<string, unknown> = { ...base };\n if (patch.fields) {\n for (const [key, childPatch] of Object.entries(patch.fields)) {\n // Define, don't assign: `next[key] =` with key \"__proto__\" would SET THE\n // PROTOTYPE instead of creating an own property — dropping the field and\n // letting a hostile patch inject one. (The spread above is already safe:\n // spread uses define semantics.) `Object.hasOwn` guards the read the same\n // way — a bare `base[\"__proto__\"]` reads the prototype, not a field.\n Object.defineProperty(next, key, {\n value: applyPatch(Object.hasOwn(base, key) ? base[key] : undefined, childPatch),\n enumerable: true,\n writable: true,\n configurable: true,\n });\n }\n }\n if (patch.drop) {\n for (const key of patch.drop) delete next[key];\n }\n return next as State;\n}\n\n/**\n * A PLAIN object — prototype `Object.prototype` or `null` — the only thing\n * `diff` descends into and `applyPatch` merges over. Arrays and class instances\n * (`Date`, `Map`, `Set`, …) fail this on purpose: they carry state outside\n * their own enumerable keys, so per-key diffing would misread them (see the\n * `diff` docstring) — they are leaves, replaced wholesale.\n */\nexport function isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== \"object\" || value === null) return false;\n const proto: unknown = Object.getPrototypeOf(value);\n return proto === Object.prototype || proto === null;\n}\n","import { isPlainObject } from \"./diff.ts\";\nimport type { CompactLiveStatePatch, LiveStatePatch } from \"./protocol.ts\";\n\nconst fieldAddresses = new WeakMap<object, { keys: string[]; indices: Map<string, number> }>();\n\nfunction addresses(value: Record<string, unknown>) {\n let cached = fieldAddresses.get(value);\n if (!cached) {\n // Undefined object fields are absent after a JSON transport. They must not\n // shift addresses on either side of the next update.\n const keys = Object.keys(value)\n .filter((key) => value[key] !== undefined)\n .sort();\n cached = { keys, indices: new Map(keys.map((key, index) => [key, index])) };\n fieldAddresses.set(value, cached);\n }\n return cached;\n}\n\n/** Encode only the changed paths of an existing structural diff. */\nexport function compactPatch(previous: unknown, patch: LiveStatePatch): CompactLiveStatePatch {\n if (\"set\" in patch) {\n const next = patch.set;\n if (\n typeof previous === \"string\" &&\n typeof next === \"string\" &&\n previous.length >= 16 &&\n // Bound prefix checks even for a multi-megabyte string. Immutable text\n // blocks are ~1 KiB; larger ordinary strings retain replacement semantics.\n previous.length <= 4096 &&\n next.length > previous.length &&\n next.startsWith(previous)\n )\n return [previous.length, next.slice(previous.length)];\n if (\n next === null ||\n typeof next === \"string\" ||\n typeof next === \"number\" ||\n typeof next === \"boolean\"\n )\n return next;\n return [next];\n }\n if (\"array\" in patch) {\n if (!Array.isArray(previous))\n throw new Error(\"Live-state array patch requires an array baseline\");\n const result: Record<string, CompactLiveStatePatch> = {};\n if (patch.array.length !== previous.length) result[\"#\"] = patch.array.length;\n for (const [index, child] of patch.array.items)\n result[index] = compactPatch(previous[index], child);\n return result;\n }\n if (!isPlainObject(previous))\n throw new Error(\"Live-state object patch requires an object baseline\");\n const { indices } = addresses(previous);\n const fields: [string, CompactLiveStatePatch][] = [];\n for (const [key, child] of Object.entries(patch.fields || {})) {\n const index = indices.get(key);\n fields.push([\n index === undefined ? `+${key}` : String(index),\n compactPatch(index === undefined ? undefined : previous[key], child),\n ]);\n }\n for (const key of patch.drop || []) {\n const index = indices.get(key);\n // An undefined field was already absent on the wire.\n if (index !== undefined) fields.push([String(index), []]);\n }\n return Object.fromEntries(fields);\n}\n\n/** Apply a version 3 patch, retaining every untouched object's identity. */\nexport function applyCompactPatch<State>(previous: State, patch: CompactLiveStatePatch): State {\n // Like applyPatch, the wire operation preserves the caller's State contract;\n // TypeScript cannot infer that contract from a recursive, generic JSON codec.\n return applyCompactValue(previous, patch) as State;\n}\n\nfunction applyCompactValue(previous: unknown, patch: CompactLiveStatePatch): unknown {\n if (patch === null || typeof patch !== \"object\") return patch;\n if (Array.isArray(patch)) {\n if (patch.length === 1) return patch[0];\n if (patch.length !== 2 || typeof previous !== \"string\" || previous.length !== patch[0]) {\n throw new Error(\"Live-state append does not match its string baseline\");\n }\n return previous + patch[1];\n }\n if (Array.isArray(previous)) {\n const length = Object.hasOwn(patch, \"#\") ? patch[\"#\"] : previous.length;\n if (\n typeof length !== \"number\" ||\n !Number.isSafeInteger(length) ||\n length < 0 ||\n length > 0xffffffff\n ) {\n throw new Error(\"Live-state array patch has an invalid length\");\n }\n const next = previous.slice(0, length);\n next.length = length;\n for (const [address, child] of Object.entries(patch)) {\n if (address === \"#\") continue;\n const index = position(address, length);\n next[index] = applyCompactValue(previous[index], child);\n }\n return next;\n }\n if (!isPlainObject(previous))\n throw new Error(\"Live-state object patch requires an object baseline\");\n const { keys } = addresses(previous);\n const next = { ...previous };\n for (const [address, child] of Object.entries(patch)) {\n const added = address.startsWith(\"+\");\n const key = added ? address.slice(1) : keys[position(address, keys.length)]!;\n if (added && Object.hasOwn(previous, key) && previous[key] !== undefined) {\n throw new Error(\"Live-state new field already exists in its baseline\");\n }\n if (Array.isArray(child) && child.length === 0) {\n if (added) throw new Error(\"Live-state cannot delete a new field\");\n delete next[key];\n } else {\n // Define own properties, including __proto__, without mutating prototypes.\n Object.defineProperty(next, key, {\n value: applyCompactValue(added ? undefined : previous[key], child),\n enumerable: true,\n writable: true,\n configurable: true,\n });\n }\n }\n return next;\n}\n\nfunction position(address: string, length: number): number {\n const index = Number(address);\n if (!Number.isSafeInteger(index) || index < 0 || index >= length || String(index) !== address) {\n throw new Error(\"Live-state field address is outside its baseline\");\n }\n return index;\n}\n","import { applyPatch } from \"./diff.ts\";\nimport { applyCompactPatch } from \"./compact.ts\";\nimport type { LiveUpdate } from \"./protocol.ts\";\n\n/**\n * The client half of the live-state channel: a tiny external store that folds\n * wire updates (one snapshot, then patches) back into the live value. The\n * server's `LiveState` engine is the producing half; `useLiveState` renders\n * this store via `useSyncExternalStore`.\n *\n * Revision discipline: a patch only applies when its `from` matches the held\n * revision. A mismatch means a message was missed (or arrived from a stale\n * subscription's revision line) — the store calls `resync` and holds its value\n * until the fresh snapshot lands, so a gap can never silently corrupt state.\n */\nexport type LiveStateStore<State> = {\n getState: () => State | undefined;\n subscribe: (listener: () => void) => () => void;\n reset: () => void;\n /** Fold one wire update into the held value; a revision gap means a missed patch — resync. */\n apply: (update: LiveUpdate<State>, resync: () => void) => void;\n};\n\nexport function createLiveStateStore<State>(): LiveStateStore<State> {\n let held: { revision: number; state: State | undefined } = { revision: -1, state: undefined };\n const listeners = new Set<() => void>();\n const notify = () => listeners.forEach((listener) => listener());\n return {\n getState: () => held.state,\n subscribe: (listener: () => void) => {\n listeners.add(listener);\n return () => void listeners.delete(listener);\n },\n reset: () => {\n held = { revision: -1, state: undefined };\n notify();\n },\n apply: (update: LiveUpdate<State>, resync: () => void) => {\n if (\"s\" in update) {\n held = { revision: update.s[0], state: update.s[1] };\n } else if (\"p\" in update) {\n if (update.p[0] !== held.revision) {\n resync();\n return;\n }\n held = { revision: update.p[1], state: applyCompactPatch(held.state, update.p[2]) };\n } else if (update.type === \"snapshot\") {\n held = { revision: update.revision, state: update.state };\n } else if (update.from !== held.revision) {\n resync();\n return;\n } else {\n held = { revision: update.to, state: applyPatch(held.state, update.patch) };\n }\n notify();\n },\n };\n}\n","/**\n * Wire types for the live-state channel: what a server engine pushes down a\n * subscription. A subscription always delivers one `snapshot` first (the full\n * current state), then a stream of `patch`es — each the minimal structural diff\n * since the previous revision. `useLiveState` reassembles these into the live\n * value, so feature code never touches these types directly.\n *\n * See `diff.ts` for how patches are produced (`diff`) and applied (`applyPatch`).\n */\n\n/**\n * A structural patch turning a previous JSON value into the next one. Three\n * shapes, discriminated by `set`, `array`, or an object patch:\n * - `{ set }` — replace this position wholesale. Used for primitives,\n * `null`, type changes,\n * and newly-added object keys.\n * - `{ fields?, drop? }` — descend into a plain object: `fields` maps each\n * changed key to its own patch; `drop` lists keys that disappeared. At least\n * one is present (an empty descend never gets emitted).\n * - `{ array }` — patch changed positions and set the resulting length. Kept\n * elements retain their identity, including immutable text blocks inside a\n * changed step. This form is sent only to subscribers requesting version 2.\n */\nexport type LiveStatePatch =\n | { set: unknown }\n | { array: { length: number; items: [number, LiveStatePatch][] } }\n | { fields?: Record<string, LiveStatePatch>; drop?: string[] };\n\n/** Explicit codec negotiation keeps already-open clients valid across deploys. */\nexport type LiveStateSubscriptionOptions = { patchVersion?: 2 | 3 };\n\n/**\n * Version 3 addresses existing object fields by their sorted baseline position;\n * new fields use `+name`. Arrays use indices and an optional `#` length.\n * Primitives replace directly, `[value]` replaces other values, `[length, text]`\n * appends to a string, and `[]` deletes an object field. No dictionary survives\n * an update: every address refers to the acknowledged baseline of that patch.\n */\nexport type CompactLiveStatePatch =\n | string\n | number\n | boolean\n | null\n | []\n | [unknown]\n | [number, string]\n | { [address: string]: CompactLiveStatePatch };\n\n/** A transient reader's position; a new engine incarnation always has a new epoch. */\nexport type LiveStateCursor = { epoch: string; revision: number };\n\n/** A pull transports one delta, a fresh snapshot, or no change. No callback is retained. */\nexport type LiveStateRead<State> = { epoch: string; update: LiveUpdate<State> | null };\n\n/**\n * One message pushed down a live-state subscription. The first is always a\n * `snapshot` (a resync sends a fresh one); every message after carries only the\n * diff from revision `from` to `to`. Revisions are monotonic for the life of one\n * subscription, so a gap (`from` ≠ the client's revision) means a message was\n * missed and the client should resubscribe.\n *\n * `State` is asserted by the caller of `useLiveState` — the wire itself is\n * structure-agnostic.\n */\nexport type LiveUpdate<State = unknown> =\n | { type: \"snapshot\"; revision: number; state: State }\n | { type: \"patch\"; from: number; to: number; patch: LiveStatePatch }\n | { s: [revision: number, state: State] }\n | { p: [from: number, to: number, patch: CompactLiveStatePatch] };\n\nexport function isLiveStateSnapshot<State>(\n update: LiveUpdate<State>,\n): update is Extract<LiveUpdate<State>, { type: \"snapshot\" } | { s: unknown }> {\n return \"s\" in update || (\"type\" in update && update.type === \"snapshot\");\n}\n\nexport function liveStateRevision(update: LiveUpdate): number {\n if (\"s\" in update) return update.s[0];\n if (\"p\" in update) return update.p[1];\n return update.type === \"snapshot\" ? update.revision : update.to;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CompactLiveStatePatch, LiveStatePatch } from "./protocol.ts";
|
|
2
|
+
/** Encode only the changed paths of an existing structural diff. */
|
|
3
|
+
export declare function compactPatch(previous: unknown, patch: LiveStatePatch): CompactLiveStatePatch;
|
|
4
|
+
/** Apply a version 3 patch, retaining every untouched object's identity. */
|
|
5
|
+
export declare function applyCompactPatch<State>(previous: State, patch: CompactLiveStatePatch): State;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { LiveStatePatch } from "./protocol.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Structural diff between two JSON values, producing the minimal `LiveStatePatch`
|
|
4
|
+
* — or `undefined` when nothing changed.
|
|
5
|
+
*
|
|
6
|
+
* The diff is REFERENCE-FIRST: identical references short-circuit immediately
|
|
7
|
+
* (`Object.is`), so the cost is O(changed), not O(size) — *as long as callers
|
|
8
|
+
* update state immutably* (unchanged sub-objects keep their identity). That one
|
|
9
|
+
* discipline is the whole performance story: a single touched row in a
|
|
10
|
+
* thousand-entry index yields one tiny patch instead of a full rescan.
|
|
11
|
+
*
|
|
12
|
+
* PLAIN objects (prototype `Object.prototype` or `null`) are treated as keyed
|
|
13
|
+
* maps and diffed per key. Dense arrays are diffed by position. Everything
|
|
14
|
+
* else — primitives, `null`, sparse arrays, and
|
|
15
|
+
* non-plain instances like `Date`/`Map`/`Set` — is a leaf, replaced wholesale.
|
|
16
|
+
* Descending into an instance would diff its own enumerable keys, which for a
|
|
17
|
+
* `Date` is NONE — two different Dates would read as "unchanged" and the
|
|
18
|
+
* subscriber would stay stale forever. Keyed objects remain preferable for
|
|
19
|
+
* collections whose entries frequently move; positional patches do not infer moves.
|
|
20
|
+
*/
|
|
21
|
+
export declare function diff(prev: unknown, next: unknown, options?: {
|
|
22
|
+
arrays?: boolean;
|
|
23
|
+
}): LiveStatePatch | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* Apply a `LiveStatePatch` to a previous value, returning the next value.
|
|
26
|
+
*
|
|
27
|
+
* Like `diff`, this preserves structural sharing: a fresh object is built only
|
|
28
|
+
* along changed paths, so untouched branches keep their previous reference.
|
|
29
|
+
* That's what lets a client selector (`s => s.streamsIndex`) skip re-rendering
|
|
30
|
+
* when an unrelated slice changed — the slice it reads stays `Object.is`-equal
|
|
31
|
+
* across applies.
|
|
32
|
+
*/
|
|
33
|
+
export declare function applyPatch<State>(prev: State, patch: LiveStatePatch): State;
|
|
34
|
+
/**
|
|
35
|
+
* A PLAIN object — prototype `Object.prototype` or `null` — the only thing
|
|
36
|
+
* `diff` descends into and `applyPatch` merges over. Arrays and class instances
|
|
37
|
+
* (`Date`, `Map`, `Set`, …) fail this on purpose: they carry state outside
|
|
38
|
+
* their own enumerable keys, so per-key diffing would misread them (see the
|
|
39
|
+
* `diff` docstring) — they are leaves, replaced wholesale.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { LiveStateCursor, LiveStateRead, LiveStateSubscriptionOptions, LiveUpdate } from "./protocol.ts";
|
|
2
|
+
/** Handle returned by `LiveState.subscribe` — the ownership + liveness surface for one subscriber. */
|
|
3
|
+
export type LiveStateSubscription = {
|
|
4
|
+
/** Still registered on a live engine? A dead DO incarnation also makes the call reject. */
|
|
5
|
+
ping(): boolean;
|
|
6
|
+
unsubscribe(): void;
|
|
7
|
+
[Symbol.dispose](): void;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* A source-agnostic live store: hold a state value, and when it changes push the
|
|
11
|
+
* minimal diff to every subscriber. It does not know or care where the state
|
|
12
|
+
* comes from — a Durable Object folds events into it, a stateless RpcTarget polls
|
|
13
|
+
* a third-party API into it — so ANY RpcTarget can expose live state by holding
|
|
14
|
+
* one and returning a read-only wrapper from a `.live` getter.
|
|
15
|
+
*
|
|
16
|
+
* Two rules make it cheap and correct:
|
|
17
|
+
* - Updates are IMMUTABLE (`setState`/`assign` build a new value, never mutate),
|
|
18
|
+
* so the diff short-circuits unchanged branches by identity — O(changed), not
|
|
19
|
+
* O(size). See `diff.ts`.
|
|
20
|
+
* - Push work runs only while a subscriber exists. Transient readers call
|
|
21
|
+
* `readSince` explicitly; a dormant engine schedules nothing.
|
|
22
|
+
*
|
|
23
|
+
* All subscribers share one revision line. Fast subscribers share a patch;
|
|
24
|
+
* slow subscribers receive a coalesced patch from their acknowledged state.
|
|
25
|
+
* A revision mismatch makes the client resync instead of applying stale data.
|
|
26
|
+
*/
|
|
27
|
+
export declare class LiveState<State extends object> {
|
|
28
|
+
#private;
|
|
29
|
+
constructor(initial: State, options?: {
|
|
30
|
+
debounceMs?: number;
|
|
31
|
+
});
|
|
32
|
+
/** The current state — reflects every `setState`/`assign`, even while dormant. */
|
|
33
|
+
getState(): State;
|
|
34
|
+
/** One retained delta bounds history while a single parent can pull incrementally.
|
|
35
|
+
* A late reader or a new incarnation receives an explicit snapshot instead. */
|
|
36
|
+
readSince(cursor?: LiveStateCursor, options?: LiveStateSubscriptionOptions): LiveStateRead<State>;
|
|
37
|
+
/** True while at least one live subscriber makes projection work observable. */
|
|
38
|
+
get observed(): boolean;
|
|
39
|
+
/** Replace the state. Build a NEW value (don't mutate) so the diff stays cheap. */
|
|
40
|
+
setState(next: State | ((prev: State) => State)): void;
|
|
41
|
+
/** Shallow-merge a partial into the state — sugar for `setState(p => ({ ...p, ...partial }))`. */
|
|
42
|
+
assign(partial: Partial<State>): void;
|
|
43
|
+
subscribe(sink: (update: LiveUpdate<State>) => unknown, options?: LiveStateSubscriptionOptions): LiveStateSubscription;
|
|
44
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { RpcTarget } from "@iterate-com/capnweb";
|
|
2
|
+
import type { LiveState as LiveStateEngine, LiveStateSubscription } from "./engine.ts";
|
|
3
|
+
import type { LiveStateSubscriptionOptions, LiveUpdate } from "./protocol.ts";
|
|
4
|
+
import type { LiveStateRpc, LiveStateSubscriptionHandle } from "./types.ts";
|
|
5
|
+
type LiveStateSource<State extends object> = Pick<LiveStateEngine<State>, "getState" | "subscribe">;
|
|
6
|
+
type RefreshingLiveStateSource<State extends object> = {
|
|
7
|
+
readonly live: LiveStateSource<State>;
|
|
8
|
+
loadAndRefreshLive(): void | PromiseLike<void>;
|
|
9
|
+
};
|
|
10
|
+
export { createLiveStateStore, type LiveStateStore } from "./store.ts";
|
|
11
|
+
export { applyPatch, diff } from "./diff.ts";
|
|
12
|
+
export { compactPatch, applyCompactPatch } from "./compact.ts";
|
|
13
|
+
export { isLiveStateSnapshot, liveStateRevision } from "./protocol.ts";
|
|
14
|
+
export type { CompactLiveStatePatch, LiveStateCursor, LiveStateRead, LiveStatePatch, LiveStateSubscriptionOptions, LiveUpdate, } from "./protocol.ts";
|
|
15
|
+
export { LiveState, type LiveStateSubscription } from "./engine.ts";
|
|
16
|
+
export type { LiveStateRpc, LiveStateSubscriptionHandle } from "./types.ts";
|
|
17
|
+
export { disposeIgnoredRpcResult, isThenable, retainCallback, type RetainedCallback, } from "./retain.ts";
|
|
18
|
+
/**
|
|
19
|
+
* Expose mutable server state as a read-only Cap'n Web capability.
|
|
20
|
+
* Pass an in-memory `LiveState`, or a stream-processor registry whose engine
|
|
21
|
+
* must hydrate before its first snapshot.
|
|
22
|
+
*/
|
|
23
|
+
export declare class LiveStateRpcTarget<State extends object> extends RpcTarget implements LiveStateRpc<State> {
|
|
24
|
+
#private;
|
|
25
|
+
constructor(source: LiveStateSource<State> | RefreshingLiveStateSource<State>);
|
|
26
|
+
get(): Promise<State>;
|
|
27
|
+
subscribe(onUpdate: (update: LiveUpdate<State>) => unknown, options?: LiveStateSubscriptionOptions): Promise<LiveStateSubscriptionRpcTarget>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The wire handle for one subscription — `ping`/`unsubscribe` over RPC.
|
|
31
|
+
* Exported for relays that build their subscription OUTSIDE a `LiveState`
|
|
32
|
+
* engine (the worker-local liveState-socket relay) but must hand back the
|
|
33
|
+
* same handle shape `LiveStateRpcTarget.subscribe` does.
|
|
34
|
+
*/
|
|
35
|
+
export declare class LiveStateSubscriptionRpcTarget extends RpcTarget implements LiveStateSubscriptionHandle {
|
|
36
|
+
#private;
|
|
37
|
+
constructor(subscription: LiveStateSubscription);
|
|
38
|
+
ping(): boolean;
|
|
39
|
+
unsubscribe(): void;
|
|
40
|
+
[Symbol.dispose](): void;
|
|
41
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire types for the live-state channel: what a server engine pushes down a
|
|
3
|
+
* subscription. A subscription always delivers one `snapshot` first (the full
|
|
4
|
+
* current state), then a stream of `patch`es — each the minimal structural diff
|
|
5
|
+
* since the previous revision. `useLiveState` reassembles these into the live
|
|
6
|
+
* value, so feature code never touches these types directly.
|
|
7
|
+
*
|
|
8
|
+
* See `diff.ts` for how patches are produced (`diff`) and applied (`applyPatch`).
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* A structural patch turning a previous JSON value into the next one. Three
|
|
12
|
+
* shapes, discriminated by `set`, `array`, or an object patch:
|
|
13
|
+
* - `{ set }` — replace this position wholesale. Used for primitives,
|
|
14
|
+
* `null`, type changes,
|
|
15
|
+
* and newly-added object keys.
|
|
16
|
+
* - `{ fields?, drop? }` — descend into a plain object: `fields` maps each
|
|
17
|
+
* changed key to its own patch; `drop` lists keys that disappeared. At least
|
|
18
|
+
* one is present (an empty descend never gets emitted).
|
|
19
|
+
* - `{ array }` — patch changed positions and set the resulting length. Kept
|
|
20
|
+
* elements retain their identity, including immutable text blocks inside a
|
|
21
|
+
* changed step. This form is sent only to subscribers requesting version 2.
|
|
22
|
+
*/
|
|
23
|
+
export type LiveStatePatch = {
|
|
24
|
+
set: unknown;
|
|
25
|
+
} | {
|
|
26
|
+
array: {
|
|
27
|
+
length: number;
|
|
28
|
+
items: [number, LiveStatePatch][];
|
|
29
|
+
};
|
|
30
|
+
} | {
|
|
31
|
+
fields?: Record<string, LiveStatePatch>;
|
|
32
|
+
drop?: string[];
|
|
33
|
+
};
|
|
34
|
+
/** Explicit codec negotiation keeps already-open clients valid across deploys. */
|
|
35
|
+
export type LiveStateSubscriptionOptions = {
|
|
36
|
+
patchVersion?: 2 | 3;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Version 3 addresses existing object fields by their sorted baseline position;
|
|
40
|
+
* new fields use `+name`. Arrays use indices and an optional `#` length.
|
|
41
|
+
* Primitives replace directly, `[value]` replaces other values, `[length, text]`
|
|
42
|
+
* appends to a string, and `[]` deletes an object field. No dictionary survives
|
|
43
|
+
* an update: every address refers to the acknowledged baseline of that patch.
|
|
44
|
+
*/
|
|
45
|
+
export type CompactLiveStatePatch = string | number | boolean | null | [] | [unknown] | [number, string] | {
|
|
46
|
+
[address: string]: CompactLiveStatePatch;
|
|
47
|
+
};
|
|
48
|
+
/** A transient reader's position; a new engine incarnation always has a new epoch. */
|
|
49
|
+
export type LiveStateCursor = {
|
|
50
|
+
epoch: string;
|
|
51
|
+
revision: number;
|
|
52
|
+
};
|
|
53
|
+
/** A pull transports one delta, a fresh snapshot, or no change. No callback is retained. */
|
|
54
|
+
export type LiveStateRead<State> = {
|
|
55
|
+
epoch: string;
|
|
56
|
+
update: LiveUpdate<State> | null;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* One message pushed down a live-state subscription. The first is always a
|
|
60
|
+
* `snapshot` (a resync sends a fresh one); every message after carries only the
|
|
61
|
+
* diff from revision `from` to `to`. Revisions are monotonic for the life of one
|
|
62
|
+
* subscription, so a gap (`from` ≠ the client's revision) means a message was
|
|
63
|
+
* missed and the client should resubscribe.
|
|
64
|
+
*
|
|
65
|
+
* `State` is asserted by the caller of `useLiveState` — the wire itself is
|
|
66
|
+
* structure-agnostic.
|
|
67
|
+
*/
|
|
68
|
+
export type LiveUpdate<State = unknown> = {
|
|
69
|
+
type: "snapshot";
|
|
70
|
+
revision: number;
|
|
71
|
+
state: State;
|
|
72
|
+
} | {
|
|
73
|
+
type: "patch";
|
|
74
|
+
from: number;
|
|
75
|
+
to: number;
|
|
76
|
+
patch: LiveStatePatch;
|
|
77
|
+
} | {
|
|
78
|
+
s: [revision: number, state: State];
|
|
79
|
+
} | {
|
|
80
|
+
p: [from: number, to: number, patch: CompactLiveStatePatch];
|
|
81
|
+
};
|
|
82
|
+
export declare function isLiveStateSnapshot<State>(update: LiveUpdate<State>): update is Extract<LiveUpdate<State>, {
|
|
83
|
+
type: "snapshot";
|
|
84
|
+
} | {
|
|
85
|
+
s: unknown;
|
|
86
|
+
}>;
|
|
87
|
+
export declare function liveStateRevision(update: LiveUpdate): number;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** An RPC callback after retention: still callable, plus disposable and (best-effort) broken-transport aware. */
|
|
2
|
+
export type RetainedCallback<Arg> = ((arg: Arg) => unknown) & Disposable & {
|
|
3
|
+
onRpcBroken?(handler: (error: unknown) => void): void;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Retain a single-argument RPC callback so it survives past the call that
|
|
7
|
+
* carried it. `dup()` duplicates the underlying remote stub (that dup is what
|
|
8
|
+
* `[Symbol.dispose]` releases); `onRpcBroken` is forwarded defensively — Cap'n
|
|
9
|
+
* Web stubs expose no own property descriptors and a Workers RPC property access
|
|
10
|
+
* can fabricate a pipelined method that rejects at call time, so we wire whatever
|
|
11
|
+
* the stub claims and swallow registration failures. `onRpcBroken` is only a
|
|
12
|
+
* prompt hint; callers that need a terminal verdict must model one explicitly
|
|
13
|
+
* (for example, the durable wake lane's independent settlement capability).
|
|
14
|
+
*/
|
|
15
|
+
export declare function retainCallback<Arg>(callback: (arg: Arg) => unknown): RetainedCallback<Arg>;
|
|
16
|
+
/** Thenable probe: RPC stubs and their call results are thenable-shaped. */
|
|
17
|
+
export declare function isThenable(value: unknown): value is PromiseLike<unknown>;
|
|
18
|
+
/**
|
|
19
|
+
* Dispose the result of an RPC call whose value the caller ignores. Reading a
|
|
20
|
+
* Cap'n Web / Workers RPC method yields a disposable stub even when unused, and
|
|
21
|
+
* dropping it without disposal leaks the remote reference.
|
|
22
|
+
*/
|
|
23
|
+
export declare function disposeIgnoredRpcResult(result: unknown): void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { LiveUpdate } from "./protocol.ts";
|
|
2
|
+
/**
|
|
3
|
+
* The client half of the live-state channel: a tiny external store that folds
|
|
4
|
+
* wire updates (one snapshot, then patches) back into the live value. The
|
|
5
|
+
* server's `LiveState` engine is the producing half; `useLiveState` renders
|
|
6
|
+
* this store via `useSyncExternalStore`.
|
|
7
|
+
*
|
|
8
|
+
* Revision discipline: a patch only applies when its `from` matches the held
|
|
9
|
+
* revision. A mismatch means a message was missed (or arrived from a stale
|
|
10
|
+
* subscription's revision line) — the store calls `resync` and holds its value
|
|
11
|
+
* until the fresh snapshot lands, so a gap can never silently corrupt state.
|
|
12
|
+
*/
|
|
13
|
+
export type LiveStateStore<State> = {
|
|
14
|
+
getState: () => State | undefined;
|
|
15
|
+
subscribe: (listener: () => void) => () => void;
|
|
16
|
+
reset: () => void;
|
|
17
|
+
/** Fold one wire update into the held value; a revision gap means a missed patch — resync. */
|
|
18
|
+
apply: (update: LiveUpdate<State>, resync: () => void) => void;
|
|
19
|
+
};
|
|
20
|
+
export declare function createLiveStateStore<State>(): LiveStateStore<State>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LiveStateSubscriptionOptions, LiveUpdate } from "./protocol.ts";
|
|
2
|
+
/** Owned handle for one live-state subscription. */
|
|
3
|
+
export type LiveStateSubscriptionHandle = Disposable & {
|
|
4
|
+
ping(): boolean | Promise<boolean>;
|
|
5
|
+
unsubscribe(): void;
|
|
6
|
+
};
|
|
7
|
+
/** Read-only live value exposed across a Cap'n Web capability boundary. */
|
|
8
|
+
export interface LiveStateRpc<State = unknown> {
|
|
9
|
+
get(): Promise<State>;
|
|
10
|
+
subscribe(onUpdate: (update: LiveUpdate<State>) => unknown, options?: LiveStateSubscriptionOptions): Promise<LiveStateSubscriptionHandle>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { LiveStateRpc } from "./live-state/types.ts";
|
|
3
|
+
export type CapnWebRoot = object & Partial<Disposable> & {
|
|
4
|
+
onRpcBroken?(callback: (error?: unknown) => void): void;
|
|
5
|
+
};
|
|
6
|
+
export type MakeCapnWebConnection<Root extends CapnWebRoot> = () => Root | PromiseLike<Root>;
|
|
7
|
+
export type LiveStateStatus = "connecting" | "live" | "error";
|
|
8
|
+
type ConnectionSnapshot = {
|
|
9
|
+
error?: string;
|
|
10
|
+
generation: number;
|
|
11
|
+
reconnect: () => void;
|
|
12
|
+
root?: CapnWebRoot;
|
|
13
|
+
/** Stable for one connection factory, across all of that factory's reconnects. */
|
|
14
|
+
scope: object | undefined;
|
|
15
|
+
status: "connecting" | "connected" | "error";
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Own one reconnectable Cap'n Web root for a React subtree. The factory is the
|
|
19
|
+
* only transport policy: it can dial a WebSocket directly or duplicate a root
|
|
20
|
+
* from a lower-level shared connection keeper. Broken roots are disposed and
|
|
21
|
+
* replaced with bounded exponential backoff.
|
|
22
|
+
*/
|
|
23
|
+
export declare function CapnWebProvider<Root extends CapnWebRoot>({ children, makeConnection, }: {
|
|
24
|
+
children?: ReactNode;
|
|
25
|
+
makeConnection: MakeCapnWebConnection<Root>;
|
|
26
|
+
}): import("react").FunctionComponentElement<import("react").ProviderProps<ConnectionSnapshot | undefined>>;
|
|
27
|
+
/** The current provider root, undefined while its connection is being replaced. */
|
|
28
|
+
export declare function useCapnWebRoot<Root extends CapnWebRoot>(): Root | undefined;
|
|
29
|
+
type LiveStateOptions<Root extends CapnWebRoot> = {
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
makeConnection?: MakeCapnWebConnection<Root>;
|
|
32
|
+
root?: Root | null;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Render a selected slice of any LiveStateRpc reachable from the provider root.
|
|
36
|
+
* Pass `{ root }` to borrow an explicit root instead; borrowed roots are never
|
|
37
|
+
* disposed or reconnected by this hook.
|
|
38
|
+
*/
|
|
39
|
+
export declare function useLiveState<Root extends CapnWebRoot, State, Selected = State>(live: (root: Root) => LiveStateRpc<State>, selector?: (state: State) => Selected, deps?: unknown[], options?: LiveStateOptions<Root>): {
|
|
40
|
+
error?: string;
|
|
41
|
+
refresh: () => void;
|
|
42
|
+
status: LiveStateStatus;
|
|
43
|
+
value: Selected | undefined;
|
|
44
|
+
};
|
|
45
|
+
export {};
|