statewire 0.18.2 → 0.19.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/dist/client/connection.d.ts +8 -5
- package/dist/client/connection.d.ts.map +1 -1
- package/dist/client/connection.js +24 -13
- package/dist/client/connection.js.map +1 -1
- package/dist/client/session.d.ts +13 -15
- package/dist/client/session.d.ts.map +1 -1
- package/dist/client/session.js +291 -256
- package/dist/client/session.js.map +1 -1
- package/dist/client/timing.d.ts +2 -1
- package/dist/client/timing.d.ts.map +1 -1
- package/dist/client/timing.js +2 -1
- package/dist/client/timing.js.map +1 -1
- package/dist/host/engine.d.ts +2 -0
- package/dist/host/engine.d.ts.map +1 -1
- package/dist/host/engine.js +2 -0
- package/dist/host/engine.js.map +1 -1
- package/dist/host/host.d.ts +3 -1
- package/dist/host/host.d.ts.map +1 -1
- package/dist/host/host.js +38 -7
- package/dist/host/host.js.map +1 -1
- package/dist/host/http.d.ts.map +1 -1
- package/dist/host/http.js +8 -17
- package/dist/host/http.js.map +1 -1
- package/dist/host/socket.d.ts +47 -23
- package/dist/host/socket.d.ts.map +1 -1
- package/dist/host/socket.js +389 -245
- package/dist/host/socket.js.map +1 -1
- package/dist/host/synced.d.ts +9 -0
- package/dist/host/synced.d.ts.map +1 -0
- package/dist/host/synced.js +45 -0
- package/dist/host/synced.js.map +1 -0
- package/dist/host-internal.d.ts +20 -8
- package/dist/host-internal.d.ts.map +1 -1
- package/dist/host.d.ts +33 -9
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +2 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/statewire.d.ts +8 -10
- package/dist/statewire.d.ts.map +1 -1
- package/dist/statewire.js +0 -14
- package/dist/statewire.js.map +1 -1
- package/dist/storage.d.ts +2 -5
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +55 -41
- package/dist/storage.js.map +1 -1
- package/dist/transports/remote.d.ts.map +1 -1
- package/dist/transports/remote.js +1 -6
- package/dist/transports/remote.js.map +1 -1
- package/dist/transports/sse.d.ts +2 -5
- package/dist/transports/sse.d.ts.map +1 -1
- package/dist/transports/sse.js.map +1 -1
- package/dist/transports/testing.d.ts +7 -5
- package/dist/transports/testing.d.ts.map +1 -1
- package/dist/transports/testing.js +19 -19
- package/dist/transports/testing.js.map +1 -1
- package/dist/types.d.ts +3 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/wire/limits.d.ts +2 -2
- package/dist/wire/limits.js +2 -2
- package/dist/wire/limits.js.map +1 -1
- package/dist/wire/protocol.d.ts +6 -1
- package/dist/wire/protocol.d.ts.map +1 -1
- package/dist/wire/protocol.js +33 -12
- package/dist/wire/protocol.js.map +1 -1
- package/package.json +1 -1
- package/src/client/connection.ts +46 -33
- package/src/client/session.ts +434 -365
- package/src/client/timing.ts +2 -0
- package/src/host/engine.ts +3 -1
- package/src/host/host.ts +50 -3
- package/src/host/http.ts +22 -23
- package/src/host/socket.ts +550 -321
- package/src/host/synced.ts +60 -0
- package/src/host-internal.ts +21 -3
- package/src/host.ts +40 -8
- package/src/index.ts +1 -0
- package/src/statewire.ts +8 -30
- package/src/storage.ts +96 -79
- package/src/transports/remote.ts +0 -7
- package/src/transports/sse.ts +8 -3
- package/src/transports/testing.ts +34 -19
- package/src/types.ts +3 -5
- package/src/wire/limits.ts +1 -1
- package/src/wire/protocol.ts +40 -11
|
@@ -5,6 +5,7 @@ import type { StatewireFrame } from "./sse";
|
|
|
5
5
|
import type { Statewire } from "../types";
|
|
6
6
|
import { openSSEStream, sseFrame } from "./fake-sse";
|
|
7
7
|
import type { FakeStreamHandle } from "./fake-sse";
|
|
8
|
+
import { WIRE_VERSION } from "../wire/protocol";
|
|
8
9
|
|
|
9
10
|
export * from "./fixtures";
|
|
10
11
|
|
|
@@ -16,8 +17,8 @@ export { applyOptimistic } from "../util/recordMutations";
|
|
|
16
17
|
export type MockTransport<TState> = {
|
|
17
18
|
/** Pass as the `transport:` option of useStatewire / Harness. */
|
|
18
19
|
transport: ResourceElement<Statewire<TState | undefined, any>>;
|
|
19
|
-
/** Captured statements in arrival order
|
|
20
|
-
commands: { method
|
|
20
|
+
/** Captured statements in arrival order. */
|
|
21
|
+
commands: { protocol: string; method: string; params: unknown[] }[];
|
|
21
22
|
/** Replace the replicated state and broadcast it as a root snapshot op. */
|
|
22
23
|
setState: (state: TState) => Promise<void>;
|
|
23
24
|
/** Deliver a bare ack cmd packet on the stream. */
|
|
@@ -42,11 +43,15 @@ let mockCount = 0;
|
|
|
42
43
|
export const createMockTransport = <TState>(
|
|
43
44
|
initialState: TState,
|
|
44
45
|
protocol = "default",
|
|
46
|
+
options: { ephemeral?: readonly string[] } = {},
|
|
45
47
|
): MockTransport<TState> => {
|
|
48
|
+
const ephemeral = (options.ephemeral ?? []).map(
|
|
49
|
+
(child) => `${protocol}/${child}`,
|
|
50
|
+
);
|
|
46
51
|
let state: unknown = initialState;
|
|
47
52
|
let clientId = "";
|
|
48
53
|
let finished = false;
|
|
49
|
-
const clients = new Map<string,
|
|
54
|
+
const clients = new Map<string, Record<string, number>>();
|
|
50
55
|
const streams = new Set<FakeStreamHandle>();
|
|
51
56
|
const commands: MockTransport<unknown>["commands"] = [];
|
|
52
57
|
|
|
@@ -57,16 +62,14 @@ export const createMockTransport = <TState>(
|
|
|
57
62
|
|
|
58
63
|
const admit = (id: string, frame: StatewireFrame) => {
|
|
59
64
|
const rec = clients.get(id)!;
|
|
60
|
-
for (const statement of frame.ctx ?? []) {
|
|
61
|
-
if (statement.seq <= rec.ctxSeq) continue;
|
|
62
|
-
rec.ctxSeq = Math.max(rec.ctxSeq, statement.seq);
|
|
63
|
-
const root = statement.ops[0] as { value?: unknown } | undefined;
|
|
64
|
-
commands.push({ context: root?.value });
|
|
65
|
-
}
|
|
66
65
|
for (const statement of frame.cmd ?? []) {
|
|
67
|
-
if (statement.seq <= rec.
|
|
68
|
-
rec.
|
|
69
|
-
commands.push({
|
|
66
|
+
if (statement.seq <= (rec[statement.protocol] ?? 0)) continue;
|
|
67
|
+
rec[statement.protocol] = statement.seq;
|
|
68
|
+
commands.push({
|
|
69
|
+
protocol: statement.protocol,
|
|
70
|
+
method: statement.method,
|
|
71
|
+
params: statement.params,
|
|
72
|
+
});
|
|
70
73
|
}
|
|
71
74
|
};
|
|
72
75
|
|
|
@@ -80,9 +83,9 @@ export const createMockTransport = <TState>(
|
|
|
80
83
|
const id = headers.get("Statewire-Client-Id") ?? "";
|
|
81
84
|
if (url.endsWith("/stream")) {
|
|
82
85
|
clientId = id;
|
|
83
|
-
if (!clients.has(id)) clients.set(id, {
|
|
86
|
+
if (!clients.has(id)) clients.set(id, {});
|
|
84
87
|
const rec = clients.get(id)!;
|
|
85
|
-
|
|
88
|
+
for (const lane of ephemeral) delete rec[lane];
|
|
86
89
|
const frame = JSON.parse(String(init?.body ?? "{}")) as StatewireFrame;
|
|
87
90
|
admit(id, frame);
|
|
88
91
|
return openSSEStream(
|
|
@@ -93,10 +96,20 @@ export const createMockTransport = <TState>(
|
|
|
93
96
|
sseFrame({
|
|
94
97
|
ops: [{ op: "mount", protocol, main: true, value: state }],
|
|
95
98
|
syn: {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
version: WIRE_VERSION,
|
|
100
|
+
protocols: [
|
|
101
|
+
{
|
|
102
|
+
name: protocol,
|
|
103
|
+
version: WIRE_VERSION,
|
|
104
|
+
seq: rec[protocol] ?? 0,
|
|
105
|
+
},
|
|
106
|
+
...ephemeral.map((name) => ({
|
|
107
|
+
name,
|
|
108
|
+
version: WIRE_VERSION,
|
|
109
|
+
record: "ephemeral" as const,
|
|
110
|
+
seq: 0,
|
|
111
|
+
})),
|
|
112
|
+
],
|
|
100
113
|
},
|
|
101
114
|
}),
|
|
102
115
|
);
|
|
@@ -122,11 +135,13 @@ export const createMockTransport = <TState>(
|
|
|
122
135
|
state = next;
|
|
123
136
|
return sendEnvelope({ ops: [{ op: "replace", path: [], value: next }] });
|
|
124
137
|
},
|
|
125
|
-
applied: (seq) =>
|
|
138
|
+
applied: (seq) =>
|
|
139
|
+
sendEnvelope({ cmd: [{ protocol, seq, type: "applied" }] }),
|
|
126
140
|
settle: (seq, payload) =>
|
|
127
141
|
sendEnvelope({
|
|
128
142
|
cmd: [
|
|
129
143
|
{
|
|
144
|
+
protocol,
|
|
130
145
|
seq,
|
|
131
146
|
type: "result",
|
|
132
147
|
...(payload !== undefined && { payload }),
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { StatewireDocuments } from "./wire/documents";
|
|
2
|
+
import type { StatewireHost } from "./host";
|
|
2
3
|
import type { StatewireProtocol } from "./wire/protocol";
|
|
3
4
|
import type { StatewireClient } from "./statewire";
|
|
4
5
|
import type { StatewirePath } from "./wire/ops";
|
|
@@ -60,17 +61,14 @@ export interface Statewire<
|
|
|
60
61
|
method: string,
|
|
61
62
|
params: readonly unknown[],
|
|
62
63
|
) => Promise<unknown>;
|
|
63
|
-
updateContext?: (ops: readonly unknown[]) => void;
|
|
64
64
|
commands: Statewire.CommandsProxy<TCommands>;
|
|
65
65
|
connection?: StatewireClient.Connection;
|
|
66
66
|
source?: Statewire.StateSource;
|
|
67
|
-
/** Highest command seq the server's dur flag states durably admitted — never lost, never run again — and released from the resend queue. */
|
|
68
|
-
durableSeq?: number;
|
|
69
67
|
/** Negation of the server's broadcast idle level; absent means always busy. */
|
|
70
68
|
isBusy?: boolean;
|
|
71
69
|
work?: { readonly isIdle: boolean };
|
|
72
|
-
/**
|
|
73
|
-
|
|
70
|
+
/** Called by the serving host when an attach ends; its ephemeral documents and includes are gone. */
|
|
71
|
+
detached?: (attach: StatewireHost.Attach) => void;
|
|
74
72
|
/** The retained failed-send lane; present on transports with a durable session. */
|
|
75
73
|
failures?: {
|
|
76
74
|
list: () => readonly StatewireClient.FailedCommand[];
|
package/src/wire/limits.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const MAX_COMMANDS_PER_FRAME = 256;
|
|
2
2
|
export const MAX_INFLIGHT_COMMANDS_PER_CLIENT = 256;
|
|
3
3
|
export const MAX_FRAME_BODY_BYTES = 8 * 1024 * 1024;
|
|
4
|
-
export const
|
|
4
|
+
export const MAX_EPHEMERAL_DOCUMENT_BYTES = 64 * 1024;
|
|
5
5
|
export const MAX_INFLIGHT_COMMANDS_PER_HOST = 1024;
|
|
6
6
|
export const COMMAND_CAPACITY_RETRY_MS = 1_000;
|
package/src/wire/protocol.ts
CHANGED
|
@@ -3,7 +3,7 @@ export namespace StatewireProtocol {
|
|
|
3
3
|
export type Offer = Range & { name: string; optional?: boolean };
|
|
4
4
|
export type Selection = {
|
|
5
5
|
version: string;
|
|
6
|
-
protocols: { name: string; version: string }[];
|
|
6
|
+
protocols: { name: string; version: string; record?: "ephemeral" }[];
|
|
7
7
|
};
|
|
8
8
|
export type Options = {
|
|
9
9
|
wireVersion?: Range;
|
|
@@ -14,10 +14,11 @@ export namespace StatewireProtocol {
|
|
|
14
14
|
export type Definition = Range & {
|
|
15
15
|
name: string;
|
|
16
16
|
requires?: Readonly<Record<string, Range>>;
|
|
17
|
+
subprotocols?: readonly { name: string; record?: "ephemeral" }[];
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
export const WIRE_VERSION = "2026-09-
|
|
21
|
+
export const WIRE_VERSION = "2026-09-13";
|
|
21
22
|
export const DEFAULT_PROTOCOL = "default";
|
|
22
23
|
export const PROTOCOL_HEADER = "Statewire-Protocol";
|
|
23
24
|
export const VERSION_HEADER = "Statewire-Version";
|
|
@@ -197,7 +198,7 @@ export const validateSelection = (
|
|
|
197
198
|
if (!Array.isArray(syn.protocols))
|
|
198
199
|
return invalid("syn.protocols must be an array");
|
|
199
200
|
const offers = protocolOffers(options);
|
|
200
|
-
const seen = new
|
|
201
|
+
const seen = new Map<string, string>();
|
|
201
202
|
const protocols = syn.protocols.map((entry: unknown) => {
|
|
202
203
|
if (typeof entry !== "object" || entry === null)
|
|
203
204
|
return invalid("invalid selected protocol");
|
|
@@ -211,13 +212,21 @@ export const validateSelection = (
|
|
|
211
212
|
)
|
|
212
213
|
return invalid("invalid selected protocol name");
|
|
213
214
|
const version = validateVersion(item.version);
|
|
214
|
-
|
|
215
|
-
|
|
215
|
+
if (item.record !== undefined && item.record !== "ephemeral")
|
|
216
|
+
invalid("invalid selected protocol record");
|
|
217
|
+
const record = item.record as "ephemeral" | undefined;
|
|
218
|
+
const parent = name.slice(0, name.lastIndexOf("/"));
|
|
219
|
+
const child = parent !== "" && seen.has(parent);
|
|
220
|
+
if (child) {
|
|
221
|
+
if (seen.get(parent) !== version)
|
|
222
|
+
invalid("subprotocol version differs from its parent");
|
|
223
|
+
} else if (offers !== "*") {
|
|
216
224
|
const offer = offers.find((o) => o.name === name);
|
|
217
225
|
if (!offer || !inRange(version, offer))
|
|
218
226
|
invalid("selected protocol is outside offer");
|
|
219
227
|
}
|
|
220
|
-
|
|
228
|
+
seen.set(name, version);
|
|
229
|
+
return { name, version, ...(record !== undefined && { record }) };
|
|
221
230
|
});
|
|
222
231
|
if (offers !== "*" && offers.some((o) => !o.optional && !seen.has(o.name)))
|
|
223
232
|
invalid("required protocol missing from selection");
|
|
@@ -275,6 +284,19 @@ export const negotiateProtocols = (
|
|
|
275
284
|
const registry = new Map(definitions.map((d) => [d.name, d]));
|
|
276
285
|
if (registry.size !== definitions.length)
|
|
277
286
|
throw new Error("statewire: duplicate registered protocol");
|
|
287
|
+
for (const definition of definitions) {
|
|
288
|
+
const children = new Set<string>();
|
|
289
|
+
for (const child of definition.subprotocols ?? []) {
|
|
290
|
+
if (
|
|
291
|
+
!protocolToken.test(child.name) ||
|
|
292
|
+
child.name.includes("/") ||
|
|
293
|
+
children.has(child.name) ||
|
|
294
|
+
(child.record !== undefined && child.record !== "ephemeral")
|
|
295
|
+
)
|
|
296
|
+
throw new Error("statewire: invalid subprotocol registration");
|
|
297
|
+
children.add(child.name);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
278
300
|
const offered = new Map(offers.map((o) => [o.name, o]));
|
|
279
301
|
const solve = (names: Set<string>) => {
|
|
280
302
|
const active = new Set(names);
|
|
@@ -318,10 +340,17 @@ export const negotiateProtocols = (
|
|
|
318
340
|
}
|
|
319
341
|
return {
|
|
320
342
|
version,
|
|
321
|
-
protocols: offers.flatMap((o) =>
|
|
322
|
-
selected!.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
343
|
+
protocols: offers.flatMap((o) => {
|
|
344
|
+
const version = selected!.get(o.name);
|
|
345
|
+
if (version === undefined) return [];
|
|
346
|
+
return [
|
|
347
|
+
{ name: o.name, version },
|
|
348
|
+
...(registry.get(o.name)!.subprotocols ?? []).map((child) => ({
|
|
349
|
+
name: `${o.name}/${child.name}`,
|
|
350
|
+
version,
|
|
351
|
+
...(child.record !== undefined && { record: child.record }),
|
|
352
|
+
})),
|
|
353
|
+
];
|
|
354
|
+
}),
|
|
326
355
|
};
|
|
327
356
|
};
|