statewire 0.18.1 → 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.
Files changed (86) hide show
  1. package/dist/client/connection.d.ts +8 -5
  2. package/dist/client/connection.d.ts.map +1 -1
  3. package/dist/client/connection.js +24 -13
  4. package/dist/client/connection.js.map +1 -1
  5. package/dist/client/session.d.ts +13 -15
  6. package/dist/client/session.d.ts.map +1 -1
  7. package/dist/client/session.js +296 -401
  8. package/dist/client/session.js.map +1 -1
  9. package/dist/client/timing.d.ts +2 -1
  10. package/dist/client/timing.d.ts.map +1 -1
  11. package/dist/client/timing.js +2 -1
  12. package/dist/client/timing.js.map +1 -1
  13. package/dist/host/engine.d.ts +2 -0
  14. package/dist/host/engine.d.ts.map +1 -1
  15. package/dist/host/engine.js +2 -0
  16. package/dist/host/engine.js.map +1 -1
  17. package/dist/host/host.d.ts +3 -1
  18. package/dist/host/host.d.ts.map +1 -1
  19. package/dist/host/host.js +38 -7
  20. package/dist/host/host.js.map +1 -1
  21. package/dist/host/http.d.ts.map +1 -1
  22. package/dist/host/http.js +9 -19
  23. package/dist/host/http.js.map +1 -1
  24. package/dist/host/socket.d.ts +48 -25
  25. package/dist/host/socket.d.ts.map +1 -1
  26. package/dist/host/socket.js +399 -266
  27. package/dist/host/socket.js.map +1 -1
  28. package/dist/host/synced.d.ts +9 -0
  29. package/dist/host/synced.d.ts.map +1 -0
  30. package/dist/host/synced.js +45 -0
  31. package/dist/host/synced.js.map +1 -0
  32. package/dist/host-internal.d.ts +20 -12
  33. package/dist/host-internal.d.ts.map +1 -1
  34. package/dist/host.d.ts +33 -9
  35. package/dist/host.d.ts.map +1 -1
  36. package/dist/host.js +2 -1
  37. package/dist/index.d.ts +2 -2
  38. package/dist/index.js +2 -1
  39. package/dist/statewire.d.ts +8 -20
  40. package/dist/statewire.d.ts.map +1 -1
  41. package/dist/statewire.js +0 -14
  42. package/dist/statewire.js.map +1 -1
  43. package/dist/storage.d.ts +2 -5
  44. package/dist/storage.d.ts.map +1 -1
  45. package/dist/storage.js +54 -89
  46. package/dist/storage.js.map +1 -1
  47. package/dist/transports/remote.d.ts.map +1 -1
  48. package/dist/transports/remote.js +1 -6
  49. package/dist/transports/remote.js.map +1 -1
  50. package/dist/transports/sse.d.ts +2 -5
  51. package/dist/transports/sse.d.ts.map +1 -1
  52. package/dist/transports/sse.js.map +1 -1
  53. package/dist/transports/testing.d.ts +7 -5
  54. package/dist/transports/testing.d.ts.map +1 -1
  55. package/dist/transports/testing.js +19 -18
  56. package/dist/transports/testing.js.map +1 -1
  57. package/dist/types.d.ts +3 -5
  58. package/dist/types.d.ts.map +1 -1
  59. package/dist/types.js.map +1 -1
  60. package/dist/wire/limits.d.ts +2 -2
  61. package/dist/wire/limits.js +2 -2
  62. package/dist/wire/limits.js.map +1 -1
  63. package/dist/wire/protocol.d.ts +6 -1
  64. package/dist/wire/protocol.d.ts.map +1 -1
  65. package/dist/wire/protocol.js +33 -12
  66. package/dist/wire/protocol.js.map +1 -1
  67. package/package.json +1 -1
  68. package/src/client/connection.ts +46 -33
  69. package/src/client/session.ts +442 -517
  70. package/src/client/timing.ts +2 -0
  71. package/src/host/engine.ts +3 -1
  72. package/src/host/host.ts +50 -3
  73. package/src/host/http.ts +23 -31
  74. package/src/host/socket.ts +560 -339
  75. package/src/host/synced.ts +60 -0
  76. package/src/host-internal.ts +21 -7
  77. package/src/host.ts +40 -8
  78. package/src/index.ts +1 -0
  79. package/src/statewire.ts +8 -37
  80. package/src/storage.ts +90 -157
  81. package/src/transports/remote.ts +0 -7
  82. package/src/transports/sse.ts +8 -3
  83. package/src/transports/testing.ts +34 -18
  84. package/src/types.ts +3 -5
  85. package/src/wire/limits.ts +1 -1
  86. package/src/wire/protocol.ts +40 -11
@@ -4,11 +4,9 @@ import type { Statewire } from "../types";
4
4
  import {
5
5
  documentSource,
6
6
  invokeProtocol,
7
- updateContext,
8
7
  discardFailed,
9
8
  failedCommands,
10
9
  retryFailed,
11
- setContext,
12
10
  setTransport,
13
11
  useStatewireSession,
14
12
  } from "../client/session";
@@ -40,17 +38,12 @@ export const useStatewireRemote = <
40
38
  },
41
39
  invoke: (protocol, method, params) =>
42
40
  invokeProtocol(s, protocol, method, params),
43
- updateContext: (ops) => updateContext(s, ops),
44
41
  commands: s.commands,
45
42
  connection: conn.connection,
46
43
  source: s.source,
47
- get durableSeq() {
48
- return s.durableSeq;
49
- },
50
44
  get isBusy() {
51
45
  return !s.idle;
52
46
  },
53
- setContext: (value) => setContext(s, value),
54
47
  failures: {
55
48
  list: () => failedCommands(s),
56
49
  retry: (entry) => retryFailed(s, entry),
@@ -25,14 +25,19 @@ import { negotiationHeaders, parseNegotiation } from "../wire/protocol";
25
25
 
26
26
  /** A client frame's lanes: stamped statements, strictly consecutive per lane. */
27
27
  export type StatewireFrame = {
28
- ctx?: { ops: unknown[]; seq: number }[];
29
- cmd?: { method: string; params: unknown[]; meta?: unknown; seq: number }[];
28
+ cmd?: {
29
+ protocol: string;
30
+ method: string;
31
+ params: unknown[];
32
+ meta?: unknown;
33
+ seq: number;
34
+ }[];
30
35
  };
31
36
 
32
37
  export type StatewireRemoteProps = {
33
38
  beforeAttach?: () => void | Promise<void>;
34
39
  clientId: string;
35
- /** Lanes to ride the attach frame (no-lease supersession only); records the included seqs. */
40
+ /** Lanes to ride the attach frame: ephemeral-lane statements, plus durable cmds on no-lease supersession; records the included entries. */
36
41
  takeAttachLanes: () => StatewireFrame | null;
37
42
  onHello: (args: { ops: unknown[]; syn: StatewireSyn }) => void;
38
43
  onOps: (args: { ops: unknown[]; cmds: StatewireCmd[] }) => void;
@@ -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: cmd as `{method, params}`, ctx as `{context}`. */
20
- commands: { method?: string; params?: unknown[]; context?: unknown }[];
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, { seq: number; ctxSeq: number }>();
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.seq) continue;
68
- rec.seq = Math.max(rec.seq, statement.seq);
69
- commands.push({ method: statement.method, params: statement.params });
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,8 +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, { seq: 0, ctxSeq: 0 });
86
+ if (!clients.has(id)) clients.set(id, {});
84
87
  const rec = clients.get(id)!;
88
+ for (const lane of ephemeral) delete rec[lane];
85
89
  const frame = JSON.parse(String(init?.body ?? "{}")) as StatewireFrame;
86
90
  admit(id, frame);
87
91
  return openSSEStream(
@@ -92,10 +96,20 @@ export const createMockTransport = <TState>(
92
96
  sseFrame({
93
97
  ops: [{ op: "mount", protocol, main: true, value: state }],
94
98
  syn: {
95
- seq: rec.seq,
96
- ctxSeq: rec.ctxSeq,
97
- version: "2026-09-07",
98
- protocols: [{ name: protocol, version: "2026-09-07" }],
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
+ ],
99
113
  },
100
114
  }),
101
115
  );
@@ -121,11 +135,13 @@ export const createMockTransport = <TState>(
121
135
  state = next;
122
136
  return sendEnvelope({ ops: [{ op: "replace", path: [], value: next }] });
123
137
  },
124
- applied: (seq) => sendEnvelope({ cmd: [{ seq, type: "applied" }] }),
138
+ applied: (seq) =>
139
+ sendEnvelope({ cmd: [{ protocol, seq, type: "applied" }] }),
125
140
  settle: (seq, payload) =>
126
141
  sendEnvelope({
127
142
  cmd: [
128
143
  {
144
+ protocol,
129
145
  seq,
130
146
  type: "result",
131
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
- /** Replace this client's ambient context on the server; it rides the ctx lane (no verdict). */
73
- setContext?: (value: Record<string, unknown>) => void;
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[];
@@ -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 MAX_CONTEXT_VALUE_BYTES = 64 * 1024;
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;
@@ -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-07";
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 Set<string>();
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
- seen.add(name);
215
- if (offers !== "*") {
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
- return { name, version };
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!.has(o.name)
323
- ? [{ name: o.name, version: selected!.get(o.name)! }]
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
  };