statewire 0.11.0 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/client/connection.d.ts +6 -4
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +10 -2
- package/dist/client/connection.js.map +1 -1
- package/dist/client/core.d.ts +1 -1
- package/dist/client/session.d.ts +4 -2
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +102 -102
- package/dist/client/session.js.map +1 -1
- package/dist/host/draft.d.ts.map +1 -1
- package/dist/host/draft.js +9 -1
- package/dist/host/draft.js.map +1 -1
- package/dist/host/host.d.ts +1 -1
- package/dist/host/http.js +4 -3
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +6 -5
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +12 -20
- package/dist/host/socket.js.map +1 -1
- package/dist/host-internal.d.ts +2 -1
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host.d.ts +2 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/statewire.d.ts +15 -4
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js.map +1 -1
- package/dist/transports/fake-sse.js +4 -4
- package/dist/transports/fake-sse.js.map +1 -1
- package/dist/transports/fixtures.d.ts +1 -1
- package/dist/transports/remote.d.ts +1 -1
- package/dist/transports/remote.js +1 -0
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +2 -2
- package/dist/transports/testing.d.ts +1 -1
- package/dist/transports/testing.js +2 -4
- package/dist/transports/testing.js.map +1 -1
- package/dist/wire/meta.d.ts +5 -3
- package/dist/wire/meta.d.ts.map +1 -1
- package/dist/wire/meta.js +7 -4
- package/dist/wire/meta.js.map +1 -1
- package/dist/wire/ops.d.ts.map +1 -1
- package/dist/wire/ops.js +26 -2
- package/dist/wire/ops.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +12 -6
- package/src/client/session.ts +145 -139
- package/src/host/draft.ts +15 -1
- package/src/host/http.ts +10 -9
- package/src/host/socket.ts +18 -28
- package/src/host-internal.ts +2 -1
- package/src/host.ts +0 -1
- package/src/statewire.ts +18 -4
- package/src/transports/fake-sse.ts +6 -6
- package/src/transports/remote.ts +1 -0
- package/src/transports/testing.ts +2 -7
- package/src/wire/meta.ts +12 -4
- package/src/wire/ops.ts +30 -3
package/src/statewire.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
import type { ResourceElement } from "@assistant-ui/tap";
|
|
12
12
|
import { LIVE_CONNECTION, useStatewireCore } from "./client/core";
|
|
13
13
|
import type { Statewire } from "./types";
|
|
14
|
+
import type { TERMINAL } from "./client/connection";
|
|
14
15
|
import { SessionStorageContext } from "./client/sessionStore";
|
|
15
16
|
|
|
16
17
|
const useSessionTransport = ({
|
|
@@ -218,7 +219,7 @@ export namespace StatewireClient {
|
|
|
218
219
|
* fate: "aborted" = never sent, "rejected" = the server refused it
|
|
219
220
|
* (`code` "unknown-command" | "invalid-params" for the static rejects,
|
|
220
221
|
* code-less for handler rejects), "lost" = sent but no verdict will ever
|
|
221
|
-
* arrive — the record is gone (
|
|
222
|
+
* arrive — the record is gone (a proven seq gap) or the channel died: it
|
|
222
223
|
* may or may not have run, "failed" = the handler ran and threw,
|
|
223
224
|
* "crashed" = the handler died involuntarily, "expired" = settled —
|
|
224
225
|
* result, rejected, failed, or crashed — but the verdict outlived the
|
|
@@ -239,14 +240,25 @@ export namespace StatewireClient {
|
|
|
239
240
|
| { fate: "expired"; cause: "server" }
|
|
240
241
|
| {
|
|
241
242
|
fate: "aborted";
|
|
242
|
-
cause: "unmount" | "stream-gone" | "session-lost"
|
|
243
|
+
cause: "unmount" | "stream-gone" | "session-lost";
|
|
243
244
|
predecessorLost?: true;
|
|
244
245
|
}
|
|
245
246
|
| {
|
|
246
247
|
fate: "lost";
|
|
247
|
-
cause: "unmount" | "stream-gone" | "session-lost"
|
|
248
|
+
cause: "unmount" | "stream-gone" | "session-lost";
|
|
248
249
|
};
|
|
249
250
|
|
|
251
|
+
/** Held commands a proven record gap settled `{fate: "lost", cause: "session-lost"}`; return "requeue" to re-enqueue them on the reset clocks. */
|
|
252
|
+
export type OnSessionLost = (
|
|
253
|
+
pending: ReadonlyArray<{
|
|
254
|
+
method: string;
|
|
255
|
+
params: readonly unknown[];
|
|
256
|
+
ts: number;
|
|
257
|
+
seq: number;
|
|
258
|
+
clientId?: string;
|
|
259
|
+
}>,
|
|
260
|
+
) => "requeue" | "drop";
|
|
261
|
+
|
|
250
262
|
/** A command as persisted while un-durable: its lane seq plus the wire statement. */
|
|
251
263
|
export type SessionCommand = {
|
|
252
264
|
seq: number;
|
|
@@ -268,7 +280,7 @@ export namespace StatewireClient {
|
|
|
268
280
|
/** A terminal verdict one sharer's connection carried, parked for the sharer awaiting the seq. */
|
|
269
281
|
export type SessionVerdict = {
|
|
270
282
|
seq: number;
|
|
271
|
-
type:
|
|
283
|
+
type: (typeof TERMINAL)[number];
|
|
272
284
|
code?: string;
|
|
273
285
|
message?: string;
|
|
274
286
|
payload?: unknown;
|
|
@@ -295,6 +307,7 @@ export namespace StatewireClient {
|
|
|
295
307
|
url: string;
|
|
296
308
|
headers?: HeadersOption;
|
|
297
309
|
fetch?: typeof globalThis.fetch;
|
|
310
|
+
onSessionLost?: OnSessionLost;
|
|
298
311
|
onChange?: (connection: Connection) => void;
|
|
299
312
|
/** The broadcast idle flag's rising edge: the instance went quiet. */
|
|
300
313
|
onDone?: () => void;
|
|
@@ -309,6 +322,7 @@ export namespace StatewireClient {
|
|
|
309
322
|
| Record<string, string>
|
|
310
323
|
| (() => Record<string, string> | Promise<Record<string, string>>);
|
|
311
324
|
webSocket?: new (url: string, protocols?: string | string[]) => WebSocket;
|
|
325
|
+
onSessionLost?: OnSessionLost;
|
|
312
326
|
onChange?: (connection: Connection) => void;
|
|
313
327
|
/** The broadcast idle flag's rising edge: the instance went quiet. */
|
|
314
328
|
onDone?: () => void;
|
|
@@ -30,7 +30,11 @@ export const openSSEStream = (
|
|
|
30
30
|
if (!open) return;
|
|
31
31
|
open = false;
|
|
32
32
|
streams.delete(handle);
|
|
33
|
-
|
|
33
|
+
try {
|
|
34
|
+
controller.close();
|
|
35
|
+
} catch {
|
|
36
|
+
// the reader already canceled
|
|
37
|
+
}
|
|
34
38
|
},
|
|
35
39
|
};
|
|
36
40
|
streams.add(handle);
|
|
@@ -40,11 +44,7 @@ export const openSSEStream = (
|
|
|
40
44
|
streams.delete(handle);
|
|
41
45
|
},
|
|
42
46
|
});
|
|
43
|
-
signal?.addEventListener("abort", () =>
|
|
44
|
-
for (const stream of [...streams]) {
|
|
45
|
-
if (stream.clientId === clientId) stream.close();
|
|
46
|
-
}
|
|
47
|
-
});
|
|
47
|
+
signal?.addEventListener("abort", () => handle.close());
|
|
48
48
|
return new Response(body, {
|
|
49
49
|
status: 200,
|
|
50
50
|
headers: { "content-type": "text/event-stream", ...headers },
|
package/src/transports/remote.ts
CHANGED
|
@@ -77,8 +77,7 @@ export const createMockTransport = <TState>(
|
|
|
77
77
|
const id = headers.get("Statewire-Client-Id") ?? "";
|
|
78
78
|
if (url.endsWith("/stream")) {
|
|
79
79
|
clientId = id;
|
|
80
|
-
|
|
81
|
-
if (!known) clients.set(id, { seq: 0, ctxSeq: 0 });
|
|
80
|
+
if (!clients.has(id)) clients.set(id, { seq: 0, ctxSeq: 0 });
|
|
82
81
|
const rec = clients.get(id)!;
|
|
83
82
|
const frame = JSON.parse(String(init?.body)) as StatewireFrame;
|
|
84
83
|
admit(id, frame);
|
|
@@ -89,11 +88,7 @@ export const createMockTransport = <TState>(
|
|
|
89
88
|
handle.send(
|
|
90
89
|
sseFrame({
|
|
91
90
|
ops: [{ op: "replace", path: [], value: state }],
|
|
92
|
-
syn: {
|
|
93
|
-
seq: rec.seq,
|
|
94
|
-
ctxSeq: rec.ctxSeq,
|
|
95
|
-
record: known ? "resumed" : "fresh",
|
|
96
|
-
},
|
|
91
|
+
syn: { seq: rec.seq, ctxSeq: rec.ctxSeq },
|
|
97
92
|
}),
|
|
98
93
|
);
|
|
99
94
|
},
|
package/src/wire/meta.ts
CHANGED
|
@@ -17,15 +17,23 @@ const isPath = (path: unknown): path is (string | number)[] =>
|
|
|
17
17
|
(typeof seg === "number" && Number.isInteger(seg)),
|
|
18
18
|
);
|
|
19
19
|
|
|
20
|
-
/** Revive meta-typed locations inside a wire value (in place).
|
|
21
|
-
*
|
|
22
|
-
export const reviveValue = (
|
|
20
|
+
/** Revive meta-typed locations inside a wire value (in place). Clients skip
|
|
21
|
+
* unknown types; hosts pass `rejectUnknown` to fault on them instead. */
|
|
22
|
+
export const reviveValue = (
|
|
23
|
+
value: unknown,
|
|
24
|
+
meta: unknown,
|
|
25
|
+
{ rejectUnknown = false } = {},
|
|
26
|
+
): unknown => {
|
|
23
27
|
if (!Array.isArray(meta)) return fault("meta must be an array");
|
|
24
28
|
for (const entry of meta as { path?: unknown; type?: unknown }[]) {
|
|
25
29
|
const path =
|
|
26
30
|
typeof entry === "object" && entry !== null ? entry.path : null;
|
|
27
31
|
if (!isPath(path)) return fault("meta entry must carry a path array");
|
|
28
|
-
if (entry.type !== "date")
|
|
32
|
+
if (entry.type !== "date") {
|
|
33
|
+
if (rejectUnknown)
|
|
34
|
+
return fault(`unknown meta type: ${String(entry.type)}`);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
29
37
|
const walked = walkStatePath(value, path, "resolve");
|
|
30
38
|
if (walked === undefined) {
|
|
31
39
|
return fault(`meta path ${path.join(".")} does not resolve`);
|
package/src/wire/ops.ts
CHANGED
|
@@ -64,15 +64,42 @@ export const sanitizeValue = (value: unknown, parent?: string): unknown =>
|
|
|
64
64
|
const invalid = (op: Statewire.Op) =>
|
|
65
65
|
new Error(`Statewire cannot apply ${op.op} at path: ${op.path.join(".")}`);
|
|
66
66
|
|
|
67
|
+
// Text-insert offsets are Unicode code points on the wire, not UTF-16 units.
|
|
68
|
+
const utf16Index = (text: string, offset: number): number => {
|
|
69
|
+
let index = 0;
|
|
70
|
+
for (let remaining = offset; remaining > 0; remaining -= 1) {
|
|
71
|
+
if (index >= text.length) return -1;
|
|
72
|
+
index += text.codePointAt(index)! > 0xffff ? 2 : 1;
|
|
73
|
+
}
|
|
74
|
+
return index;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const countCodePoints = (text: string): number => {
|
|
78
|
+
let count = 0;
|
|
79
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
80
|
+
if (text.codePointAt(index)! > 0xffff) index += 1;
|
|
81
|
+
count += 1;
|
|
82
|
+
}
|
|
83
|
+
return count;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
let last: { str: string; count: number } | null = null;
|
|
87
|
+
|
|
67
88
|
const insertedText = (
|
|
68
89
|
current: string,
|
|
69
90
|
op: Extract<Statewire.Op, { op: "add" }>,
|
|
70
91
|
offset: number,
|
|
71
92
|
): string => {
|
|
72
|
-
if (typeof op.value !== "string"
|
|
73
|
-
|
|
93
|
+
if (typeof op.value !== "string") throw invalid(op);
|
|
94
|
+
const memoized =
|
|
95
|
+
last !== null && last.str === current && last.count === offset;
|
|
96
|
+
const index = memoized ? current.length : utf16Index(current, offset);
|
|
97
|
+
if (index < 0) throw invalid(op);
|
|
98
|
+
const next = current.slice(0, index) + op.value + current.slice(index);
|
|
99
|
+
if (index === current.length) {
|
|
100
|
+
last = { str: next, count: offset + countCodePoints(op.value) };
|
|
74
101
|
}
|
|
75
|
-
return
|
|
102
|
+
return next;
|
|
76
103
|
};
|
|
77
104
|
|
|
78
105
|
const applyAdd = (
|