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.
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 +291 -256
  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 +8 -17
  23. package/dist/host/http.js.map +1 -1
  24. package/dist/host/socket.d.ts +47 -23
  25. package/dist/host/socket.d.ts.map +1 -1
  26. package/dist/host/socket.js +389 -245
  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 -8
  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 -10
  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 +55 -41
  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 -19
  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 +434 -365
  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 +22 -23
  74. package/src/host/socket.ts +550 -321
  75. package/src/host/synced.ts +60 -0
  76. package/src/host-internal.ts +21 -3
  77. package/src/host.ts +40 -8
  78. package/src/index.ts +1 -0
  79. package/src/statewire.ts +8 -30
  80. package/src/storage.ts +96 -79
  81. package/src/transports/remote.ts +0 -7
  82. package/src/transports/sse.ts +8 -3
  83. package/src/transports/testing.ts +34 -19
  84. package/src/types.ts +3 -5
  85. package/src/wire/limits.ts +1 -1
  86. package/src/wire/protocol.ts +40 -11
package/dist/host.d.ts CHANGED
@@ -2,6 +2,7 @@ import { StatewireReject } from "./host/engine.js";
2
2
  import { useStatewireWork } from "./host/work.js";
3
3
  import { append } from "./host/draft.js";
4
4
  import { getStateHost, useStatewireCommands, useStatewireState } from "./host/host.js";
5
+ import { syncedDocument } from "./host/synced.js";
5
6
  import { Statewire } from "./types.js";
6
7
  //#region src/host.d.ts
7
8
  declare namespace StatewireHost {
@@ -16,19 +17,35 @@ declare namespace StatewireHost {
16
17
  /** The committed plain state — immutably replaced per op, so a captured value is a stable snapshot. */
17
18
  snapshot: () => TState;
18
19
  };
20
+ type Document = {
21
+ protocol: string;
22
+ key: readonly string[] | null;
23
+ main: boolean;
24
+ value: unknown;
25
+ };
19
26
  type StateHost = Statewire.StateSource & {
20
27
  /** Synchronous, per locally-recorded op; the host owns batching. */
21
28
  setOnOp: (listener: ((op: Statewire.Op) => void) | null) => void;
29
+ /** Mount or replace the shared document `(protocol, key)`; keyed documents reach an attach only once it `include`s them. */
30
+ mount: (protocol: string, value: unknown, options?: {
31
+ key?: readonly string[];
32
+ main?: boolean;
33
+ }) => void;
34
+ unmount: (protocol: string, key?: readonly string[]) => void;
35
+ documents: () => ReadonlyMap<string, Document>;
36
+ setOnDocuments: (listener: (() => void) | null) => void;
22
37
  };
23
38
  /**
24
39
  * Per-invocation handle bound as `this` of a command handler: `caller` is
25
- * the submitting client, `applied()` states the broadcast state now contains
26
- * the command's effects — the client drops its optimistic overlay — before
27
- * the handler settles (idempotent, a no-op after settle). The command
28
- * settles when the handler's returned promise does.
40
+ * the submitting client, `attach` the admitting attach's document view
41
+ * (in-process dispatch has none — reading it throws), `applied()` states the
42
+ * broadcast state now contains the command's effects — the client drops its
43
+ * optimistic overlay — before the handler settles (idempotent, a no-op after
44
+ * settle). The command settles when the handler's returned promise does.
29
45
  */
30
46
  type Ctx = {
31
47
  readonly caller: Caller;
48
+ readonly attach: Attach;
32
49
  readonly signal: AbortSignal;
33
50
  readonly applied: () => void;
34
51
  };
@@ -37,16 +54,23 @@ declare namespace StatewireHost {
37
54
  * across a long run: `clientId` is the client's `Statewire-Client-Id`
38
55
  * (in-process dispatch has none — reading it throws), `auth` is the value
39
56
  * `authorize` returned for the attach that admitted the command (undefined
40
- * for a void `authorize` and for in-process dispatch), `context` reads the
41
- * admitting attach's context live (null until that attach sends one),
42
- * `isConnected` reports whether the client has an active stream attachment.
57
+ * for a void `authorize` and for in-process dispatch), `isConnected`
58
+ * reports whether the client has an active stream attachment.
43
59
  */
44
60
  type Caller = {
45
61
  readonly clientId: string;
46
62
  readonly auth: unknown;
47
- readonly context: Record<string, unknown> | null;
48
63
  readonly isConnected: boolean;
49
64
  };
65
+ /** The admitting attach's view: its ephemeral documents plus the shared documents included for it. */
66
+ type Attach = {
67
+ document: (protocol: string, key?: readonly string[]) => unknown;
68
+ /** `protocol` must be ephemeral; the encoded value is bounded by `MAX_EPHEMERAL_DOCUMENT_BYTES`. */
69
+ mount: (protocol: string, value: unknown, key?: readonly string[], main?: boolean) => void;
70
+ unmount: (protocol: string, key?: readonly string[]) => void;
71
+ include: (protocol: string, key: readonly string[]) => void;
72
+ exclude: (protocol: string, key: readonly string[]) => void;
73
+ };
50
74
  type HandledCommands<THandlers extends Statewire.Commands> = { [K in keyof THandlers]: (...args: Parameters<THandlers[K]>) => Awaited<ReturnType<THandlers[K]>>; };
51
75
  type Fin = {
52
76
  reason: "reconnect" | "error" | "protocol-error" | "forbidden" | "unauthorized" | "gone" | "idle" | (string & {});
@@ -59,5 +83,5 @@ declare namespace StatewireHost {
59
83
  };
60
84
  }
61
85
  //#endregion
62
- export { StatewireHost, StatewireReject, append, getStateHost, useStatewireCommands, useStatewireState, useStatewireWork };
86
+ export { StatewireHost, StatewireReject, append, getStateHost, syncedDocument, useStatewireCommands, useStatewireState, useStatewireWork };
63
87
  //# sourceMappingURL=host.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"host.d.ts","names":[],"sources":["../src/host.ts"],"mappings":";;;;;;kBAWiB;OACH;aACD;IACT,QAAQ,GAAG,MAAM,QAAQ,OAAO,QAAQ;IACxC,WAAW;;OAGD,SAAS;;IAEnB,QAAQ,cAAc,UAAU;;IAEhC,gBAAgB;;OAGN,YAAY,UAAU;;IAEhC,UAAU,YAAY,IAAI,UAAU;;;;;;;;;OAU1B;aACD,QAAQ;aACR,QAAQ;aACR;;;;;;;;;;;OAYC;aACD;aACA;aACA,SAAS;aACT;;OAGC,gBAAgB,kBAAkB,UAAU,eACrD,WAAW,gBACP,MAAM,WAAW,UAAU,QAC3B,QAAQ,WAAW,UAAU;OAGxB;IACV;IASA;IACA;IACA;IACA;IACA;IACA"}
1
+ {"version":3,"file":"host.d.ts","names":[],"sources":["../src/host.ts"],"mappings":";;;;;;;kBAYiB;OACH;aACD;IACT,QAAQ,GAAG,MAAM,QAAQ,OAAO,QAAQ;IACxC,WAAW;;OAGD,SAAS;;IAEnB,QAAQ,cAAc,UAAU;;IAEhC,gBAAgB;;OAGN;IACV;IACA;IACA;IACA;;OAGU,YAAY,UAAU;;IAEhC,UAAU,YAAY,IAAI,UAAU;;IAEpC,QACE,kBACA,gBACA;MAAY;MAAyB;;IAEvC,UAAU,kBAAkB;IAC5B,iBAAiB,oBAAoB;IACrC,iBAAiB;;;;;;;;;;OAWP;aACD,QAAQ;aACR,QAAQ;aACR,QAAQ;aACR;;;;;;;;;;OAWC;aACD;aACA;aACA;;;OAIC;IACV,WAAW,kBAAkB;;IAE7B,QACE,kBACA,gBACA,yBACA;IAEF,UAAU,kBAAkB;IAC5B,UAAU,kBAAkB;IAC5B,UAAU,kBAAkB;;OAGlB,gBAAgB,kBAAkB,UAAU,eACrD,WAAW,gBACP,MAAM,WAAW,UAAU,QAC3B,QAAQ,WAAW,UAAU;OAGxB;IACV;IASA;IACA;IACA;IACA;IACA;IACA"}
package/dist/host.js CHANGED
@@ -2,4 +2,5 @@ import { StatewireReject } from "./host/engine.js";
2
2
  import { append } from "./host/draft.js";
3
3
  import { getStateHost, useStatewireCommands, useStatewireState } from "./host/host.js";
4
4
  import { useStatewireWork } from "./host/work.js";
5
- export { StatewireReject, append, getStateHost, useStatewireCommands, useStatewireState, useStatewireWork };
5
+ import { syncedDocument } from "./host/synced.js";
6
+ export { StatewireReject, append, getStateHost, syncedDocument, useStatewireCommands, useStatewireState, useStatewireWork };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { StatewireDocuments } from "./wire/documents.js";
2
- import { StatewireProtocol } from "./wire/protocol.js";
2
+ import { DEFAULT_PROTOCOL, StatewireProtocol } from "./wire/protocol.js";
3
3
  import { StatewireHttp } from "./transports/sse.js";
4
4
  import { StatewireClient, StatewireResource, useStatewire } from "./statewire.js";
5
5
  import { Statewire, StatewireSendError } from "./types.js";
6
6
  import { StatewireDocument, decodeDocument, encodeDocument } from "./document.js";
7
7
  import { StatewireWebsocket } from "./transports/ws.js";
8
- export { type Statewire, StatewireClient, type StatewireDocument, type StatewireDocuments, StatewireHttp, type StatewireProtocol, StatewireResource, StatewireSendError, StatewireWebsocket, decodeDocument, encodeDocument, useStatewire };
8
+ export { DEFAULT_PROTOCOL, type Statewire, StatewireClient, type StatewireDocument, type StatewireDocuments, StatewireHttp, type StatewireProtocol, StatewireResource, StatewireSendError, StatewireWebsocket, decodeDocument, encodeDocument, useStatewire };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { decodeDocument, encodeDocument } from "./document.js";
2
+ import { DEFAULT_PROTOCOL } from "./wire/protocol.js";
2
3
  import { StatewireSendError } from "./types.js";
3
4
  import { StatewireClient, StatewireResource, useStatewire } from "./statewire.js";
4
5
  import { StatewireHttp } from "./transports/sse.js";
5
6
  import { StatewireWebsocket } from "./transports/ws.js";
6
- export { StatewireClient, StatewireHttp, StatewireResource, StatewireSendError, StatewireWebsocket, decodeDocument, encodeDocument, useStatewire };
7
+ export { DEFAULT_PROTOCOL, StatewireClient, StatewireHttp, StatewireResource, StatewireSendError, StatewireWebsocket, decodeDocument, encodeDocument, useStatewire };
@@ -1,6 +1,6 @@
1
1
  import { StatewireDocuments } from "./wire/documents.js";
2
- import { StatewireProtocol } from "./wire/protocol.js";
3
2
  import { StatewireHost } from "./host.js";
3
+ import { StatewireProtocol } from "./wire/protocol.js";
4
4
  import { TERMINAL } from "./client/connection.js";
5
5
  import { Statewire } from "./types.js";
6
6
  import { ResourceElement } from "@assistant-ui/tap";
@@ -17,9 +17,7 @@ declare class StatewireClient<TState, TCommands extends Statewire.Commands = Sta
17
17
  get documents(): StatewireDocuments.Source | undefined;
18
18
  get selection(): StatewireProtocol.Selection | undefined;
19
19
  invoke(protocol: string, method: string, params: readonly unknown[]): Promise<unknown>;
20
- updateContext(ops: readonly unknown[]): void;
21
20
  get isBusy(): boolean;
22
- setContext(value: Record<string, unknown>): void;
23
21
  get failed(): readonly StatewireClient.FailedCommand[];
24
22
  retryFailed(entry: StatewireClient.FailedCommand): Promise<unknown>;
25
23
  discardFailed(entry: StatewireClient.FailedCommand): void;
@@ -59,15 +57,12 @@ declare namespace StatewireClient {
59
57
  documents: StatewireDocuments.Source | undefined;
60
58
  selection: StatewireProtocol.Selection | undefined;
61
59
  invoke: (protocol: string, method: string, params: readonly unknown[]) => Promise<unknown>;
62
- updateContext: (ops: readonly unknown[]) => void;
63
60
  /** Identity-stable across renders; reads the current canonical state without subscribing the caller. */
64
61
  getState: () => TState;
65
62
  connection: Connection;
66
63
  commands: Statewire.CommandsProxy<TCommands>;
67
64
  /** Negation of the server's broadcast idle level (a transport without idle is always busy). */
68
65
  isBusy: boolean;
69
- /** Replace this client's ambient context on the server; throws on transports without a client context channel. */
70
- setContext: (value: Record<string, unknown>) => void;
71
66
  /** Non-accepted sends retained in the session record; reload with it. */
72
67
  failed: readonly FailedCommand[];
73
68
  /** Re-enqueues the entry's command on fresh clocks and removes the entry. */
@@ -182,7 +177,6 @@ declare namespace StatewireClient {
182
177
  /** A command as persisted while un-durable: its lane seq plus the wire statement. */
183
178
  type SessionCommand = {
184
179
  seq: number;
185
- protocol: string;
186
180
  produced?: {
187
181
  wire: StatewireProtocol.Range;
188
182
  protocols: readonly StatewireProtocol.Offer[] | "*";
@@ -191,9 +185,8 @@ declare namespace StatewireClient {
191
185
  params: unknown[];
192
186
  meta?: unknown;
193
187
  };
194
- /** The persisted session: the lane identity, the cmd clock, and the commands the server has not marked durable. */
195
- type SessionRecord = {
196
- clientId: string;
188
+ /** A durable lane: its cmd clock and the commands the server has not marked durable. */
189
+ type SessionLane = {
197
190
  nextSeq: number;
198
191
  commands: readonly SessionCommand[];
199
192
  /** Terminal verdicts parked for the sharer that minted the seq (shared storage only). */
@@ -201,6 +194,11 @@ declare namespace StatewireClient {
201
194
  /** Non-accepted sends retained for the user; survive record rebases and resets. */
202
195
  failed?: readonly FailedCommand[];
203
196
  };
197
+ /** The persisted session: the client identity plus one lane per durable protocol. */
198
+ type SessionRecord = {
199
+ clientId: string;
200
+ lanes: Readonly<Record<string, SessionLane>>;
201
+ };
204
202
  /** A terminal verdict one sharer's connection carried, parked for the sharer awaiting the seq. */
205
203
  type SessionVerdict = {
206
204
  seq: number;
@@ -1 +1 @@
1
- {"version":3,"file":"statewire.d.ts","names":[],"sources":["../src/statewire.ts"],"mappings":"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"}
1
+ {"version":3,"file":"statewire.d.ts","names":[],"sources":["../src/statewire.ts"],"mappings":"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"}
package/dist/statewire.js CHANGED
@@ -41,18 +41,10 @@ const useStatewire = (options) => {
41
41
  if (!value.invoke) throw new Error("statewire: transport does not support protocol commands");
42
42
  return value.invoke(protocol_0, method, params);
43
43
  },
44
- updateContext: (ops) => {
45
- if (!value.updateContext) throw new Error("statewire: transport does not support context documents");
46
- value.updateContext(ops);
47
- },
48
44
  getState: core.getState,
49
45
  connection,
50
46
  commands: core.commands,
51
47
  isBusy,
52
- setContext: (contextValue) => {
53
- if (!value.setContext) throw new Error("statewire: this transport has no client context channel");
54
- value.setContext(contextValue);
55
- },
56
48
  failed,
57
49
  retryFailed: (entry) => failures().retry(entry),
58
50
  discardFailed: (entry_0) => failures().discard(entry_0)
@@ -92,15 +84,9 @@ var StatewireClient = class {
92
84
  invoke(protocol, method, params) {
93
85
  return this.root.getValue().invoke(protocol, method, params);
94
86
  }
95
- updateContext(ops) {
96
- this.root.getValue().updateContext(ops);
97
- }
98
87
  get isBusy() {
99
88
  return this.root.getValue().isBusy;
100
89
  }
101
- setContext(value) {
102
- this.root.getValue().setContext(value);
103
- }
104
90
  get failed() {
105
91
  return this.root.getValue().failed;
106
92
  }
@@ -1 +1 @@
1
- {"version":3,"file":"statewire.js","names":["StatewireDocuments","StatewireProtocol","StatewireHost","useMemo","useRef","useState","useSyncExternalStore","createTapRoot","flushTapSync","resource","useContextProvider","useResource","useResources","withKey","ResourceElement","CONNECTED_CONNECTION","useStatewireCore","Statewire","TERMINAL","DocumentStorageContext","ProtocolContext","SessionStorageContext","useSessionTransport","transport","session","documents","protocol","StatewireClient","SessionStorage","Storage","Options","SessionTransport","useStatewire","Commands","options","TState","TCommands","Helpers","latest","current","storage","undefined","wireVersion","applicationProtocols","keyPrefixes","value","core","subscribe","listener","state","getState","isBusy","getBusy","failed","getFailed","connection","failures","NonNullable","Error","selection","invoke","method","params","updateContext","ops","commands","CommandsProxy","setContext","contextValue","Record","retryFailed","entry","FailedCommand","retry","discardFailed","discard","StatewireResource","root","disposed","constructor","ClientRoot","getValue","Connection","Source","Selection","Promise","dispose","unmount","Readonly","Range","Offer","initialState","optimistic","Optimistic","materialize","Materialize","onStateChange","ctx","changes","Changes","onCommandChange","update","CommandUpdate","onConnectionChange","onError","error","ConnectionError","failureCovered","ConnectionState","status","reason","degraded","attempt","cause","code","nextRetryAt","lastError","message","payload","reconnect","ConnectionStatus","draft","command","Command","OptimisticContext","ts","seq","clientId","SendFailure","fate","predecessorLost","OnSessionLost","pending","ReadonlyArray","produced","SessionCommand","meta","failure","wire","protocols","SessionRecord","nextSeq","verdicts","SessionVerdict","type","ready","flush","load","save","record","migrate","lock","fn","T","fin","Fin","HeadersOption","HeadersInit","staleAuth","HttpOptions","sessionStore","url","isNew","headers","fetch","globalThis","onSessionLost","onChange","onDone","WebsocketOptions","query","webSocket","WebSocket"],"sources":["../src/statewire.ts"],"sourcesContent":["import type { StatewireDocuments } from \"./wire/documents\";\nimport type { StatewireProtocol } from \"./wire/protocol\";\nimport type { StatewireHost } from \"./host\";\nimport { useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport {\n createTapRoot,\n flushTapSync,\n resource,\n useContextProvider,\n useResource,\n useResources,\n withKey,\n} from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { CONNECTED_CONNECTION, useStatewireCore } from \"./client/core\";\nimport type { Statewire } from \"./types\";\nimport type { TERMINAL } from \"./client/connection\";\nimport {\n DocumentStorageContext,\n ProtocolContext,\n SessionStorageContext,\n} from \"./client/sessionStore\";\n\nconst useSessionTransport = ({\n transport,\n session,\n documents,\n protocol,\n}: {\n transport: ResourceElement<Statewire<any, any>>;\n session: StatewireClient.SessionStorage | undefined;\n documents: StatewireDocuments.Storage | undefined;\n protocol: StatewireProtocol.Options;\n}): Statewire<unknown> =>\n useContextProvider(SessionStorageContext, session, () =>\n // oxlint-disable-next-line react-hooks/rules-of-hooks\n useContextProvider(DocumentStorageContext, documents, () =>\n // oxlint-disable-next-line react-hooks/rules-of-hooks\n useContextProvider(ProtocolContext, protocol, () =>\n // oxlint-disable-next-line react-hooks/rules-of-hooks\n useResource(transport),\n ),\n ),\n );\n\n// The transport owns the session; it reads the storage off the context this\n// wrapper's own fiber provides, so plain-React callers get a provider too.\nconst SessionTransport = resource(useSessionTransport);\n\nexport const useStatewire = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n>(\n options: StatewireClient.Options<TState, TCommands>,\n): StatewireClient.Helpers<TState, TCommands> => {\n const latest = useRef(options);\n latest.current = options;\n\n const session = useResources(\n options.storage?.session !== undefined\n ? [withKey(\"session\", options.storage.session)]\n : [],\n )[0];\n const documents = useResources(\n options.storage?.documents === undefined\n ? []\n : [withKey(\"documents\", options.storage.documents)],\n )[0];\n const protocol: StatewireProtocol.Options = {\n ...(options.protocol !== undefined && { protocol: options.protocol }),\n ...(options.wireVersion !== undefined && {\n wireVersion: options.wireVersion,\n }),\n ...(options.applicationProtocols !== undefined && {\n applicationProtocols: options.applicationProtocols,\n }),\n ...(options.keyPrefixes !== undefined && {\n keyPrefixes: options.keyPrefixes,\n }),\n };\n const value = useResource(\n SessionTransport({\n transport: options.transport,\n session,\n documents,\n protocol,\n }),\n );\n const core = useStatewireCore(() => latest.current, value);\n const [subscribe] = useState(\n () => (listener: () => void) =>\n core.subscribe(() => flushTapSync(listener)),\n );\n\n const state = useSyncExternalStore(\n subscribe,\n core.getState,\n core.getState,\n ) as TState;\n const isBusy = useSyncExternalStore(subscribe, core.getBusy, core.getBusy);\n const failed = useSyncExternalStore(\n subscribe,\n core.getFailed,\n core.getFailed,\n );\n const connection = value.connection ?? CONNECTED_CONNECTION;\n\n return useMemo(() => {\n const failures = (): NonNullable<typeof value.failures> => {\n if (!value.failures) {\n throw new Error(\"statewire: this transport has no failed-send lane\");\n }\n return value.failures;\n };\n return {\n state,\n documents: value.documents,\n selection: value.selection,\n invoke: (\n protocol: string,\n method: string,\n params: readonly unknown[],\n ) => {\n if (!value.invoke)\n throw new Error(\n \"statewire: transport does not support protocol commands\",\n );\n return value.invoke(protocol, method, params);\n },\n updateContext: (ops: readonly unknown[]) => {\n if (!value.updateContext)\n throw new Error(\n \"statewire: transport does not support context documents\",\n );\n value.updateContext(ops);\n },\n getState: core.getState as () => TState,\n connection,\n commands: core.commands as Statewire.CommandsProxy<TCommands>,\n isBusy,\n setContext: (contextValue: Record<string, unknown>) => {\n if (!value.setContext) {\n throw new Error(\n \"statewire: this transport has no client context channel\",\n );\n }\n value.setContext(contextValue);\n },\n failed,\n retryFailed: (entry: StatewireClient.FailedCommand) =>\n failures().retry(entry),\n discardFailed: (entry: StatewireClient.FailedCommand) =>\n failures().discard(entry),\n };\n }, [state, isBusy, failed, connection, core, value]);\n};\n\nexport const StatewireResource = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n>(\n options: StatewireClient.Options<TState, TCommands>,\n): ResourceElement<StatewireClient.Helpers<TState, TCommands>> =>\n resource(useStatewire<TState, TCommands>)(options);\n\nexport class StatewireClient<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n> {\n private readonly root;\n private disposed = false;\n\n constructor(options: StatewireClient.Options<TState, TCommands>) {\n const ClientRoot = () => useStatewire<TState, TCommands>(options);\n this.root = createTapRoot(ClientRoot);\n }\n\n get state(): TState {\n return this.root.getValue().getState();\n }\n\n get connection(): StatewireClient.Connection {\n return this.root.getValue().connection;\n }\n\n get commands(): Statewire.CommandsProxy<TCommands> {\n return this.root.getValue().commands;\n }\n\n get documents(): StatewireDocuments.Source | undefined {\n return this.root.getValue().documents;\n }\n get selection(): StatewireProtocol.Selection | undefined {\n return this.root.getValue().selection;\n }\n invoke(\n protocol: string,\n method: string,\n params: readonly unknown[],\n ): Promise<unknown> {\n return this.root.getValue().invoke(protocol, method, params);\n }\n updateContext(ops: readonly unknown[]): void {\n this.root.getValue().updateContext(ops);\n }\n\n get isBusy(): boolean {\n return this.root.getValue().isBusy;\n }\n\n setContext(value: Record<string, unknown>): void {\n this.root.getValue().setContext(value);\n }\n\n get failed(): readonly StatewireClient.FailedCommand[] {\n return this.root.getValue().failed;\n }\n\n retryFailed(entry: StatewireClient.FailedCommand): Promise<unknown> {\n return this.root.getValue().retryFailed(entry);\n }\n\n discardFailed(entry: StatewireClient.FailedCommand): void {\n this.root.getValue().discardFailed(entry);\n }\n\n subscribe(listener: () => void): () => void {\n return this.root.subscribe(listener);\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n this.root.unmount();\n }\n}\n\nexport namespace StatewireClient {\n export type Options<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = {\n protocol?: string;\n keyPrefixes?: Readonly<Record<string, readonly string[]>>;\n wireVersion?: StatewireProtocol.Range;\n applicationProtocols?: readonly StatewireProtocol.Offer[] | \"*\";\n transport: ResourceElement<Statewire<TState, TCommands>>;\n /** Durable session: the mounted storage holds this client's lane identity and its un-durable commands, which resend on the next attach. */\n storage?:\n | {\n session?: ResourceElement<SessionStorage> | undefined;\n documents?: ResourceElement<StatewireDocuments.Storage> | undefined;\n }\n | undefined;\n /** Served as the canonical state until the attach snapshot; own commands fold over it. */\n initialState?: TState;\n optimistic?: Optimistic<NonNullable<TState>, TCommands>;\n materialize?: Materialize<NonNullable<TState>>;\n onStateChange?: (\n state: NonNullable<TState>,\n ctx: { changes: Statewire.Changes },\n ) => void;\n onCommandChange?: (\n update: Statewire.CommandUpdate<TCommands>,\n ctx: { state: NonNullable<TState> },\n ) => void;\n onConnectionChange?: (connection: Connection) => void;\n onError?: (error: ConnectionError) => void;\n /** Drops a retained failed entry once a canonical snapshot shows its effect landed; checked against each new snapshot. */\n failureCovered?: (\n state: NonNullable<TState>,\n entry: FailedCommand,\n ) => boolean;\n };\n\n export type Helpers<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = {\n /** The statewire's state type — remote statewires include `undefined` until the first attach snapshot applies. */\n state: TState;\n documents: StatewireDocuments.Source | undefined;\n selection: StatewireProtocol.Selection | undefined;\n invoke: (\n protocol: string,\n method: string,\n params: readonly unknown[],\n ) => Promise<unknown>;\n updateContext: (ops: readonly unknown[]) => void;\n /** Identity-stable across renders; reads the current canonical state without subscribing the caller. */\n getState: () => TState;\n connection: Connection;\n commands: Statewire.CommandsProxy<TCommands>;\n /** Negation of the server's broadcast idle level (a transport without idle is always busy). */\n isBusy: boolean;\n /** Replace this client's ambient context on the server; throws on transports without a client context channel. */\n setContext: (value: Record<string, unknown>) => void;\n /** Non-accepted sends retained in the session record; reload with it. */\n failed: readonly FailedCommand[];\n /** Re-enqueues the entry's command on fresh clocks and removes the entry. */\n retryFailed: (entry: FailedCommand) => Promise<unknown>;\n /** Removes the entry. */\n discardFailed: (entry: FailedCommand) => void;\n };\n\n /** Connection lifecycle and whether its current state warrants a problem indicator. */\n export type ConnectionState =\n | { status: \"standby\"; reason: \"deferred\" | \"idle\"; degraded: false }\n | { status: \"connecting\"; degraded: false }\n | { status: \"connected\"; degraded: false }\n | {\n status: \"retrying\";\n degraded: true;\n attempt: number;\n /** Backoff details are absent while the retry dial is in flight. */\n cause?: \"dropped\" | \"reconnect\" | \"error\" | \"unauthorized\";\n code?: string;\n nextRetryAt?: number;\n lastError?: string;\n message?: string;\n }\n | {\n status: \"stopped\";\n degraded: true;\n reason:\n | \"gone\"\n | \"forbidden\"\n | \"protocol-error\"\n | \"unauthorized\"\n | \"storage-error\";\n message?: string;\n payload?: unknown;\n };\n export type Connection = ConnectionState & {\n error?: ConnectionError;\n reconnect: () => void;\n };\n export type ConnectionStatus = Connection[\"status\"];\n\n /**\n * Mutate the draft in place; the client records the next state plus the exact\n * changed paths. Re-run against the new base on every rebase.\n */\n export type Optimistic<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = (\n draft: TState,\n command: Statewire.Command<TCommands>,\n ctx: OptimisticContext,\n ) => void;\n\n export type OptimisticContext = {\n ts: number;\n seq: number;\n clientId?: string;\n };\n\n /**\n * Mutate the draft to make server-accepted, still-uncommitted intents in\n * the state concrete (e.g. dispatch records into messages). Runs after the\n * optimistic folds on every published state.\n */\n export type Materialize<TState> = (draft: TState) => void;\n\n /**\n * fate: \"aborted\" = never sent, \"rejected\" = the server refused it\n * (`code` \"unknown-command\" | \"invalid-params\" for the static rejects,\n * code-less for handler rejects), \"lost\" = sent but no verdict will ever\n * arrive — the record is gone (a proven seq gap) or the channel died: it\n * may or may not have run, \"failed\" = the handler ran and threw,\n * \"crashed\" = the handler died involuntarily, \"expired\" = settled —\n * result, rejected, failed, or crashed — but the verdict outlived the\n * server's retention.\n * cause is which channel settled it: \"server\" = the server's answer;\n * the rest are channel deaths the client settled locally.\n */\n export type SendFailure =\n | {\n fate: \"rejected\";\n cause: \"server\";\n code?: string;\n message?: string;\n payload?: unknown;\n }\n | { fate: \"failed\"; cause: \"server\"; message?: string }\n | { fate: \"crashed\"; cause: \"server\" }\n | { fate: \"expired\"; cause: \"server\" }\n | {\n fate: \"aborted\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\";\n predecessorLost?: true;\n }\n | {\n fate: \"lost\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\";\n };\n\n /** Held commands a proven record gap settled `{fate: \"lost\", cause: \"session-lost\"}`; return \"requeue\" to re-enqueue them on the reset clocks. */\n export type OnSessionLost = (\n pending: ReadonlyArray<{\n method: string;\n params: readonly unknown[];\n ts: number;\n seq: number;\n clientId?: string;\n }>,\n ) => \"requeue\" | \"drop\";\n\n /** A non-accepted send retained for the user: the wire statement, its failure, and its original clocks. */\n export type FailedCommand = {\n readonly protocol: string;\n readonly produced?: SessionCommand[\"produced\"];\n readonly command: string;\n readonly params: readonly unknown[];\n readonly meta?: unknown;\n readonly failure: SendFailure;\n readonly ts: number;\n readonly seq: number;\n };\n\n /** A command as persisted while un-durable: its lane seq plus the wire statement. */\n export type SessionCommand = {\n seq: number;\n protocol: string;\n produced?: {\n wire: StatewireProtocol.Range;\n protocols: readonly StatewireProtocol.Offer[] | \"*\";\n };\n method: string;\n params: unknown[];\n meta?: unknown;\n };\n\n /** The persisted session: the lane identity, the cmd clock, and the commands the server has not marked durable. */\n export type SessionRecord = {\n clientId: string;\n nextSeq: number;\n commands: readonly SessionCommand[];\n /** Terminal verdicts parked for the sharer that minted the seq (shared storage only). */\n verdicts?: readonly SessionVerdict[];\n /** Non-accepted sends retained for the user; survive record rebases and resets. */\n failed?: readonly FailedCommand[];\n };\n\n /** A terminal verdict one sharer's connection carried, parked for the sharer awaiting the seq. */\n export type SessionVerdict = {\n seq: number;\n type: (typeof TERMINAL)[number];\n code?: string;\n message?: string;\n payload?: unknown;\n meta?: unknown;\n };\n\n /** Session storage seam with optional asynchronous hydration and persistence. */\n export type SessionStorage = {\n /** Resolves when a synchronous load can return the hydrated record. */\n ready?: Promise<void>;\n /** Resolves after all queued saves are persisted; awaited before sending statements. */\n flush?: () => void | Promise<void>;\n load: () => SessionRecord | null;\n save: (record: SessionRecord) => void;\n /** Upgrade incompatible stored commands; null discards the old record and starts a new client identity. */\n migrate?: (\n record: SessionRecord,\n current: NonNullable<SessionCommand[\"produced\"]>,\n ) => SessionRecord | null;\n /** Serializes record mutations across sharers; presence marks the storage shared. */\n lock?: <T>(fn: () => T | Promise<T>) => Promise<T>;\n /** Notifies of record saves by other sharers; returns unsubscribe. Required alongside `lock`. */\n subscribe?: (listener: () => void) => () => void;\n };\n\n /** Static headers, or a resolver invoked per request/attach; `staleAuth` is true right after an auth rejection. */\n export type ConnectionError = Error & { fin?: StatewireHost.Fin };\n\n export type HeadersOption =\n | HeadersInit\n | ((ctx: { staleAuth: boolean }) => HeadersInit | Promise<HeadersInit>);\n\n export type HttpOptions = StatewireProtocol.Options & {\n sessionStore?: SessionStorage | undefined;\n url: string;\n /** The session does not exist on the server yet: the first attach waits for the first command send (or an explicit `reconnect()`). */\n isNew?: boolean;\n headers?: HeadersOption;\n fetch?: typeof globalThis.fetch;\n onSessionLost?: OnSessionLost;\n onChange?: (connection: Connection) => void;\n onError?: (error: ConnectionError) => void;\n /** The broadcast idle flag's rising edge: the instance went quiet. */\n onDone?: () => void;\n };\n\n export type WebsocketOptions = StatewireProtocol.Options & {\n sessionStore?: SessionStorage | undefined;\n url: string;\n /** The session does not exist on the server yet: the first attach waits for the first command send (or an explicit `reconnect()`). */\n isNew?: boolean;\n headers?: HeadersOption;\n protocols?: string | string[];\n query?:\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>);\n webSocket?: new (url: string, protocols?: string | string[]) => WebSocket;\n onSessionLost?: OnSessionLost;\n onChange?: (connection: Connection) => void;\n onError?: (error: ConnectionError) => void;\n /** The broadcast idle flag's rising edge: the instance went quiet. */\n onDone?: () => void;\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"statewire.js","names":["StatewireDocuments","StatewireProtocol","StatewireHost","useMemo","useRef","useState","useSyncExternalStore","createTapRoot","flushTapSync","resource","useContextProvider","useResource","useResources","withKey","ResourceElement","CONNECTED_CONNECTION","useStatewireCore","Statewire","TERMINAL","DocumentStorageContext","ProtocolContext","SessionStorageContext","useSessionTransport","transport","session","documents","protocol","StatewireClient","SessionStorage","Storage","Options","SessionTransport","useStatewire","Commands","options","TState","TCommands","Helpers","latest","current","storage","undefined","wireVersion","applicationProtocols","keyPrefixes","value","core","subscribe","listener","state","getState","isBusy","getBusy","failed","getFailed","connection","failures","NonNullable","Error","selection","invoke","method","params","commands","CommandsProxy","retryFailed","entry","FailedCommand","retry","discardFailed","discard","StatewireResource","root","disposed","constructor","ClientRoot","getValue","Connection","Source","Selection","Promise","dispose","unmount","Readonly","Record","Range","Offer","initialState","optimistic","Optimistic","materialize","Materialize","onStateChange","ctx","changes","Changes","onCommandChange","update","CommandUpdate","onConnectionChange","onError","error","ConnectionError","failureCovered","ConnectionState","status","reason","degraded","attempt","cause","code","nextRetryAt","lastError","message","payload","reconnect","ConnectionStatus","draft","command","Command","OptimisticContext","ts","seq","clientId","SendFailure","fate","predecessorLost","OnSessionLost","pending","ReadonlyArray","produced","SessionCommand","meta","failure","wire","protocols","SessionLane","nextSeq","verdicts","SessionVerdict","SessionRecord","lanes","type","ready","flush","load","save","record","migrate","lock","fn","T","fin","Fin","HeadersOption","HeadersInit","staleAuth","HttpOptions","sessionStore","url","isNew","headers","fetch","globalThis","onSessionLost","onChange","onDone","WebsocketOptions","query","webSocket","WebSocket"],"sources":["../src/statewire.ts"],"sourcesContent":["import type { StatewireDocuments } from \"./wire/documents\";\nimport type { StatewireProtocol } from \"./wire/protocol\";\nimport type { StatewireHost } from \"./host\";\nimport { useMemo, useRef, useState, useSyncExternalStore } from \"react\";\nimport {\n createTapRoot,\n flushTapSync,\n resource,\n useContextProvider,\n useResource,\n useResources,\n withKey,\n} from \"@assistant-ui/tap\";\nimport type { ResourceElement } from \"@assistant-ui/tap\";\nimport { CONNECTED_CONNECTION, useStatewireCore } from \"./client/core\";\nimport type { Statewire } from \"./types\";\nimport type { TERMINAL } from \"./client/connection\";\nimport {\n DocumentStorageContext,\n ProtocolContext,\n SessionStorageContext,\n} from \"./client/sessionStore\";\n\nconst useSessionTransport = ({\n transport,\n session,\n documents,\n protocol,\n}: {\n transport: ResourceElement<Statewire<any, any>>;\n session: StatewireClient.SessionStorage | undefined;\n documents: StatewireDocuments.Storage | undefined;\n protocol: StatewireProtocol.Options;\n}): Statewire<unknown> =>\n useContextProvider(SessionStorageContext, session, () =>\n // oxlint-disable-next-line react-hooks/rules-of-hooks\n useContextProvider(DocumentStorageContext, documents, () =>\n // oxlint-disable-next-line react-hooks/rules-of-hooks\n useContextProvider(ProtocolContext, protocol, () =>\n // oxlint-disable-next-line react-hooks/rules-of-hooks\n useResource(transport),\n ),\n ),\n );\n\n// The transport owns the session; it reads the storage off the context this\n// wrapper's own fiber provides, so plain-React callers get a provider too.\nconst SessionTransport = resource(useSessionTransport);\n\nexport const useStatewire = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n>(\n options: StatewireClient.Options<TState, TCommands>,\n): StatewireClient.Helpers<TState, TCommands> => {\n const latest = useRef(options);\n latest.current = options;\n\n const session = useResources(\n options.storage?.session !== undefined\n ? [withKey(\"session\", options.storage.session)]\n : [],\n )[0];\n const documents = useResources(\n options.storage?.documents === undefined\n ? []\n : [withKey(\"documents\", options.storage.documents)],\n )[0];\n const protocol: StatewireProtocol.Options = {\n ...(options.protocol !== undefined && { protocol: options.protocol }),\n ...(options.wireVersion !== undefined && {\n wireVersion: options.wireVersion,\n }),\n ...(options.applicationProtocols !== undefined && {\n applicationProtocols: options.applicationProtocols,\n }),\n ...(options.keyPrefixes !== undefined && {\n keyPrefixes: options.keyPrefixes,\n }),\n };\n const value = useResource(\n SessionTransport({\n transport: options.transport,\n session,\n documents,\n protocol,\n }),\n );\n const core = useStatewireCore(() => latest.current, value);\n const [subscribe] = useState(\n () => (listener: () => void) =>\n core.subscribe(() => flushTapSync(listener)),\n );\n\n const state = useSyncExternalStore(\n subscribe,\n core.getState,\n core.getState,\n ) as TState;\n const isBusy = useSyncExternalStore(subscribe, core.getBusy, core.getBusy);\n const failed = useSyncExternalStore(\n subscribe,\n core.getFailed,\n core.getFailed,\n );\n const connection = value.connection ?? CONNECTED_CONNECTION;\n\n return useMemo(() => {\n const failures = (): NonNullable<typeof value.failures> => {\n if (!value.failures) {\n throw new Error(\"statewire: this transport has no failed-send lane\");\n }\n return value.failures;\n };\n return {\n state,\n documents: value.documents,\n selection: value.selection,\n invoke: (\n protocol: string,\n method: string,\n params: readonly unknown[],\n ) => {\n if (!value.invoke)\n throw new Error(\n \"statewire: transport does not support protocol commands\",\n );\n return value.invoke(protocol, method, params);\n },\n getState: core.getState as () => TState,\n connection,\n commands: core.commands as Statewire.CommandsProxy<TCommands>,\n isBusy,\n failed,\n retryFailed: (entry: StatewireClient.FailedCommand) =>\n failures().retry(entry),\n discardFailed: (entry: StatewireClient.FailedCommand) =>\n failures().discard(entry),\n };\n }, [state, isBusy, failed, connection, core, value]);\n};\n\nexport const StatewireResource = <\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n>(\n options: StatewireClient.Options<TState, TCommands>,\n): ResourceElement<StatewireClient.Helpers<TState, TCommands>> =>\n resource(useStatewire<TState, TCommands>)(options);\n\nexport class StatewireClient<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n> {\n private readonly root;\n private disposed = false;\n\n constructor(options: StatewireClient.Options<TState, TCommands>) {\n const ClientRoot = () => useStatewire<TState, TCommands>(options);\n this.root = createTapRoot(ClientRoot);\n }\n\n get state(): TState {\n return this.root.getValue().getState();\n }\n\n get connection(): StatewireClient.Connection {\n return this.root.getValue().connection;\n }\n\n get commands(): Statewire.CommandsProxy<TCommands> {\n return this.root.getValue().commands;\n }\n\n get documents(): StatewireDocuments.Source | undefined {\n return this.root.getValue().documents;\n }\n get selection(): StatewireProtocol.Selection | undefined {\n return this.root.getValue().selection;\n }\n invoke(\n protocol: string,\n method: string,\n params: readonly unknown[],\n ): Promise<unknown> {\n return this.root.getValue().invoke(protocol, method, params);\n }\n get isBusy(): boolean {\n return this.root.getValue().isBusy;\n }\n\n get failed(): readonly StatewireClient.FailedCommand[] {\n return this.root.getValue().failed;\n }\n\n retryFailed(entry: StatewireClient.FailedCommand): Promise<unknown> {\n return this.root.getValue().retryFailed(entry);\n }\n\n discardFailed(entry: StatewireClient.FailedCommand): void {\n this.root.getValue().discardFailed(entry);\n }\n\n subscribe(listener: () => void): () => void {\n return this.root.subscribe(listener);\n }\n\n dispose(): void {\n if (this.disposed) return;\n this.disposed = true;\n this.root.unmount();\n }\n}\n\nexport namespace StatewireClient {\n export type Options<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = {\n protocol?: string;\n keyPrefixes?: Readonly<Record<string, readonly string[]>>;\n wireVersion?: StatewireProtocol.Range;\n applicationProtocols?: readonly StatewireProtocol.Offer[] | \"*\";\n transport: ResourceElement<Statewire<TState, TCommands>>;\n /** Durable session: the mounted storage holds this client's lane identity and its un-durable commands, which resend on the next attach. */\n storage?:\n | {\n session?: ResourceElement<SessionStorage> | undefined;\n documents?: ResourceElement<StatewireDocuments.Storage> | undefined;\n }\n | undefined;\n /** Served as the canonical state until the attach snapshot; own commands fold over it. */\n initialState?: TState;\n optimistic?: Optimistic<NonNullable<TState>, TCommands>;\n materialize?: Materialize<NonNullable<TState>>;\n onStateChange?: (\n state: NonNullable<TState>,\n ctx: { changes: Statewire.Changes },\n ) => void;\n onCommandChange?: (\n update: Statewire.CommandUpdate<TCommands>,\n ctx: { state: NonNullable<TState> },\n ) => void;\n onConnectionChange?: (connection: Connection) => void;\n onError?: (error: ConnectionError) => void;\n /** Drops a retained failed entry once a canonical snapshot shows its effect landed; checked against each new snapshot. */\n failureCovered?: (\n state: NonNullable<TState>,\n entry: FailedCommand,\n ) => boolean;\n };\n\n export type Helpers<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = {\n /** The statewire's state type — remote statewires include `undefined` until the first attach snapshot applies. */\n state: TState;\n documents: StatewireDocuments.Source | undefined;\n selection: StatewireProtocol.Selection | undefined;\n invoke: (\n protocol: string,\n method: string,\n params: readonly unknown[],\n ) => Promise<unknown>;\n /** Identity-stable across renders; reads the current canonical state without subscribing the caller. */\n getState: () => TState;\n connection: Connection;\n commands: Statewire.CommandsProxy<TCommands>;\n /** Negation of the server's broadcast idle level (a transport without idle is always busy). */\n isBusy: boolean;\n /** Non-accepted sends retained in the session record; reload with it. */\n failed: readonly FailedCommand[];\n /** Re-enqueues the entry's command on fresh clocks and removes the entry. */\n retryFailed: (entry: FailedCommand) => Promise<unknown>;\n /** Removes the entry. */\n discardFailed: (entry: FailedCommand) => void;\n };\n\n /** Connection lifecycle and whether its current state warrants a problem indicator. */\n export type ConnectionState =\n | { status: \"standby\"; reason: \"deferred\" | \"idle\"; degraded: false }\n | { status: \"connecting\"; degraded: false }\n | { status: \"connected\"; degraded: false }\n | {\n status: \"retrying\";\n degraded: true;\n attempt: number;\n /** Backoff details are absent while the retry dial is in flight. */\n cause?: \"dropped\" | \"reconnect\" | \"error\" | \"unauthorized\";\n code?: string;\n nextRetryAt?: number;\n lastError?: string;\n message?: string;\n }\n | {\n status: \"stopped\";\n degraded: true;\n reason:\n | \"gone\"\n | \"forbidden\"\n | \"protocol-error\"\n | \"unauthorized\"\n | \"storage-error\";\n message?: string;\n payload?: unknown;\n };\n export type Connection = ConnectionState & {\n error?: ConnectionError;\n reconnect: () => void;\n };\n export type ConnectionStatus = Connection[\"status\"];\n\n /**\n * Mutate the draft in place; the client records the next state plus the exact\n * changed paths. Re-run against the new base on every rebase.\n */\n export type Optimistic<\n TState,\n TCommands extends Statewire.Commands = Statewire.Commands,\n > = (\n draft: TState,\n command: Statewire.Command<TCommands>,\n ctx: OptimisticContext,\n ) => void;\n\n export type OptimisticContext = {\n ts: number;\n seq: number;\n clientId?: string;\n };\n\n /**\n * Mutate the draft to make server-accepted, still-uncommitted intents in\n * the state concrete (e.g. dispatch records into messages). Runs after the\n * optimistic folds on every published state.\n */\n export type Materialize<TState> = (draft: TState) => void;\n\n /**\n * fate: \"aborted\" = never sent, \"rejected\" = the server refused it\n * (`code` \"unknown-command\" | \"invalid-params\" for the static rejects,\n * code-less for handler rejects), \"lost\" = sent but no verdict will ever\n * arrive — the record is gone (a proven seq gap) or the channel died: it\n * may or may not have run, \"failed\" = the handler ran and threw,\n * \"crashed\" = the handler died involuntarily, \"expired\" = settled —\n * result, rejected, failed, or crashed — but the verdict outlived the\n * server's retention.\n * cause is which channel settled it: \"server\" = the server's answer;\n * the rest are channel deaths the client settled locally.\n */\n export type SendFailure =\n | {\n fate: \"rejected\";\n cause: \"server\";\n code?: string;\n message?: string;\n payload?: unknown;\n }\n | { fate: \"failed\"; cause: \"server\"; message?: string }\n | { fate: \"crashed\"; cause: \"server\" }\n | { fate: \"expired\"; cause: \"server\" }\n | {\n fate: \"aborted\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\";\n predecessorLost?: true;\n }\n | {\n fate: \"lost\";\n cause: \"unmount\" | \"stream-gone\" | \"session-lost\";\n };\n\n /** Held commands a proven record gap settled `{fate: \"lost\", cause: \"session-lost\"}`; return \"requeue\" to re-enqueue them on the reset clocks. */\n export type OnSessionLost = (\n pending: ReadonlyArray<{\n method: string;\n params: readonly unknown[];\n ts: number;\n seq: number;\n clientId?: string;\n }>,\n ) => \"requeue\" | \"drop\";\n\n /** A non-accepted send retained for the user: the wire statement, its failure, and its original clocks. */\n export type FailedCommand = {\n readonly protocol: string;\n readonly produced?: SessionCommand[\"produced\"];\n readonly command: string;\n readonly params: readonly unknown[];\n readonly meta?: unknown;\n readonly failure: SendFailure;\n readonly ts: number;\n readonly seq: number;\n };\n\n /** A command as persisted while un-durable: its lane seq plus the wire statement. */\n export type SessionCommand = {\n seq: number;\n produced?: {\n wire: StatewireProtocol.Range;\n protocols: readonly StatewireProtocol.Offer[] | \"*\";\n };\n method: string;\n params: unknown[];\n meta?: unknown;\n };\n\n /** A durable lane: its cmd clock and the commands the server has not marked durable. */\n export type SessionLane = {\n nextSeq: number;\n commands: readonly SessionCommand[];\n /** Terminal verdicts parked for the sharer that minted the seq (shared storage only). */\n verdicts?: readonly SessionVerdict[];\n /** Non-accepted sends retained for the user; survive record rebases and resets. */\n failed?: readonly FailedCommand[];\n };\n\n /** The persisted session: the client identity plus one lane per durable protocol. */\n export type SessionRecord = {\n clientId: string;\n lanes: Readonly<Record<string, SessionLane>>;\n };\n\n /** A terminal verdict one sharer's connection carried, parked for the sharer awaiting the seq. */\n export type SessionVerdict = {\n seq: number;\n type: (typeof TERMINAL)[number];\n code?: string;\n message?: string;\n payload?: unknown;\n meta?: unknown;\n };\n\n /** Session storage seam with optional asynchronous hydration and persistence. */\n export type SessionStorage = {\n /** Resolves when a synchronous load can return the hydrated record. */\n ready?: Promise<void>;\n /** Resolves after all queued saves are persisted; awaited before sending statements. */\n flush?: () => void | Promise<void>;\n load: () => SessionRecord | null;\n save: (record: SessionRecord) => void;\n /** Upgrade incompatible stored commands; null discards the old record and starts a new client identity. */\n migrate?: (\n record: SessionRecord,\n current: NonNullable<SessionCommand[\"produced\"]>,\n ) => SessionRecord | null;\n /** Serializes record mutations across sharers; presence marks the storage shared. */\n lock?: <T>(fn: () => T | Promise<T>) => Promise<T>;\n /** Notifies of record saves by other sharers; returns unsubscribe. Required alongside `lock`. */\n subscribe?: (listener: () => void) => () => void;\n };\n\n /** Static headers, or a resolver invoked per request/attach; `staleAuth` is true right after an auth rejection. */\n export type ConnectionError = Error & { fin?: StatewireHost.Fin };\n\n export type HeadersOption =\n | HeadersInit\n | ((ctx: { staleAuth: boolean }) => HeadersInit | Promise<HeadersInit>);\n\n export type HttpOptions = StatewireProtocol.Options & {\n sessionStore?: SessionStorage | undefined;\n url: string;\n /** The session does not exist on the server yet: the first attach waits for the first command send (or an explicit `reconnect()`). */\n isNew?: boolean;\n headers?: HeadersOption;\n fetch?: typeof globalThis.fetch;\n onSessionLost?: OnSessionLost;\n onChange?: (connection: Connection) => void;\n onError?: (error: ConnectionError) => void;\n /** The broadcast idle flag's rising edge: the instance went quiet. */\n onDone?: () => void;\n };\n\n export type WebsocketOptions = StatewireProtocol.Options & {\n sessionStore?: SessionStorage | undefined;\n url: string;\n /** The session does not exist on the server yet: the first attach waits for the first command send (or an explicit `reconnect()`). */\n isNew?: boolean;\n headers?: HeadersOption;\n protocols?: string | string[];\n query?:\n | Record<string, string>\n | (() => Record<string, string> | Promise<Record<string, string>>);\n webSocket?: new (url: string, protocols?: string | string[]) => WebSocket;\n onSessionLost?: OnSessionLost;\n onChange?: (connection: Connection) => void;\n onError?: (error: ConnectionError) => void;\n /** The broadcast idle flag's rising edge: the instance went quiet. */\n onDone?: () => void;\n };\n}\n"],"mappings":"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"}
package/dist/storage.d.ts CHANGED
@@ -15,11 +15,8 @@ declare namespace KeyValueSessionStorage {
15
15
  backend: Backend;
16
16
  key: string;
17
17
  namespace?: string;
18
- /** The application format used by sessions saved before version negotiation. */
19
- legacyProtocol?: {
20
- name: string;
21
- version: string;
22
- };
18
+ /** The lane that sessions saved in a single-lane format lift into; defaults to the client's default protocol. */
19
+ protocol?: string;
23
20
  migrate?: StatewireClient.SessionStorage["migrate"];
24
21
  retention?: {
25
22
  maxAgeMs?: number;
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.ts","names":[],"sources":["../src/storage.ts"],"mappings":";;;cA+ea,oDAAsB,SAAA,gBAAA,iBAAA,SAAA,uBAAA;kBAElB;OACH;IACV,UAAU,gCAAgC;IAC1C,UAAU,aAAa,yBAAyB;IAChD,aAAa,uBAAuB;IACpC,uCAAuC;;IAEvC,cAAc,aAAa,cAAc,kBAAkB;;OAEjD;IACV,SAAS;IACT;IACA;;IAEA;MAAmB;MAAc;;IACjC,UAAU,gBAAgB;IAC1B;MAAc;MAAmB;;IACjC,WAAW"}
1
+ {"version":3,"file":"storage.d.ts","names":[],"sources":["../src/storage.ts"],"mappings":";;;cAggBa,oDAAsB,SAAA,gBAAA,iBAAA,SAAA,uBAAA;kBAElB;OACH;IACV,UAAU,gCAAgC;IAC1C,UAAU,aAAa,yBAAyB;IAChD,aAAa,uBAAuB;IACpC,uCAAuC;;IAEvC,cAAc,aAAa,cAAc,kBAAkB;;OAEjD;IACV,SAAS;IACT;IACA;;IAEA;IACA,UAAU,gBAAgB;IAC1B;MAAc;MAAmB;;IACjC,WAAW"}
package/dist/storage.js CHANGED
@@ -1,4 +1,4 @@
1
- import { negotiationHeaders, protocolToken } from "./wire/protocol.js";
1
+ import { DEFAULT_PROTOCOL, negotiationHeaders, protocolToken } from "./wire/protocol.js";
2
2
  import { wrapStorageError } from "./client/storageError.js";
3
3
  import { useEffect, useMemo, useRef } from "@assistant-ui/tap/react-shim";
4
4
  import { resource } from "@assistant-ui/tap";
@@ -21,7 +21,9 @@ const isSendFailure = (value) => {
21
21
  }
22
22
  };
23
23
  const isFailedCommand = (value) => isRecord(value) && isProtocol(value["protocol"]) && isProducer(value["produced"]) && typeof value["command"] === "string" && Array.isArray(value["params"]) && isSendFailure(value["failure"]) && typeof value["ts"] === "number" && Number.isFinite(value["ts"]) && isPositiveSafeInteger(value["seq"]);
24
- const isSessionRecord = (value) => isRecord(value) && typeof value["clientId"] === "string" && value["clientId"].length > 0 && isPositiveSafeInteger(value["nextSeq"]) && Array.isArray(value["commands"]) && value["commands"].every(isSessionCommand) && (value["verdicts"] === void 0 || Array.isArray(value["verdicts"]) && value["verdicts"].every(isSessionVerdict)) && (value["failed"] === void 0 || Array.isArray(value["failed"]) && value["failed"].every(isFailedCommand));
24
+ const isSessionLane = (value) => isRecord(value) && isPositiveSafeInteger(value["nextSeq"]) && Array.isArray(value["commands"]) && value["commands"].every(isSessionCommand) && (value["verdicts"] === void 0 || Array.isArray(value["verdicts"]) && value["verdicts"].every(isSessionVerdict)) && (value["failed"] === void 0 || Array.isArray(value["failed"]) && value["failed"].every(isFailedCommand));
25
+ const isClientId = (value) => typeof value === "string" && value.length > 0;
26
+ const isSessionRecord = (value) => isRecord(value) && isClientId(value["clientId"]) && isRecord(value["lanes"]) && Object.keys(value["lanes"]).every(isProtocol) && Object.values(value["lanes"]).every(isSessionLane);
25
27
  const isProtocol = (value) => value === void 0 || typeof value === "string" && value !== "*" && protocolToken.test(value);
26
28
  const isProducer = (value) => {
27
29
  if (value === void 0) return true;
@@ -36,32 +38,49 @@ const isProducer = (value) => {
36
38
  return false;
37
39
  }
38
40
  };
39
- const decode = (raw, legacyProtocol) => {
40
- if (raw === null) return null;
41
- const parsed = JSON.parse(raw);
42
- if (!isSessionRecord(parsed)) throw new Error("statewire: invalid stored session record");
43
- const { statewireStorageVersion: format, savedAt: _savedAt, ctxNextSeq: _ctxNextSeq, context: _context, ...value } = parsed;
44
- if (!isSessionRecord(value)) throw new Error("statewire: invalid stored session record");
45
- if (format !== void 0 && format !== 1 && format !== 2) throw new Error("statewire: unsupported session storage version");
46
- if (legacyProtocol === void 0 || format !== void 0) return value;
47
- const produced = {
48
- wire: { version: "2026-09-07" },
49
- protocols: [legacyProtocol]
41
+ const invalid = () => {
42
+ throw new Error("statewire: invalid stored session record");
43
+ };
44
+ const liftLegacy = (value, protocol) => {
45
+ const { clientId, nextSeq, commands, verdicts, failed } = value;
46
+ const lane = {
47
+ nextSeq,
48
+ commands: [],
49
+ ...verdicts !== void 0 && { verdicts }
50
50
  };
51
+ if (!isClientId(clientId) || !isSessionLane(lane) || !Array.isArray(commands)) return invalid();
52
+ const legacyCommands = commands;
53
+ if (!legacyCommands.every(isSessionCommand)) return invalid();
54
+ if (failed !== void 0 && (!Array.isArray(failed) || !failed.every(isFailedCommand))) return invalid();
55
+ const lanes = { [protocol]: {
56
+ ...lane,
57
+ commands: legacyCommands.filter((command) => (command.protocol ?? protocol) === protocol).map(({ protocol: _protocol, ...command }) => command)
58
+ } };
59
+ for (const entry of failed ?? []) {
60
+ const target = lanes[entry.protocol] ?? {
61
+ nextSeq: 1,
62
+ commands: []
63
+ };
64
+ lanes[entry.protocol] = {
65
+ ...target,
66
+ failed: [...target.failed ?? [], entry]
67
+ };
68
+ }
51
69
  return {
52
- ...value,
53
- commands: value.commands.map((command) => command.produced === void 0 ? {
54
- ...command,
55
- protocol: command.protocol ?? legacyProtocol.name,
56
- produced
57
- } : command),
58
- ...value.failed === void 0 ? {} : { failed: value.failed.map((entry) => ({
59
- ...entry,
60
- protocol: entry.protocol ?? legacyProtocol.name,
61
- produced: entry.produced ?? produced
62
- })) }
70
+ clientId,
71
+ lanes
63
72
  };
64
73
  };
74
+ const normalize = (parsed, protocol) => {
75
+ if (!isRecord(parsed)) return invalid();
76
+ const { statewireStorageVersion: format, savedAt: _savedAt, ...value } = parsed;
77
+ if (format === 3) return isSessionRecord(value) ? value : invalid();
78
+ if (format !== void 0 && format !== 1 && format !== 2) throw new Error("statewire: unsupported session storage version");
79
+ const { ctxNextSeq: _ctxNextSeq, context: _context, ...legacy } = value;
80
+ return liftLegacy(legacy, protocol);
81
+ };
82
+ const decode = (raw, protocol) => raw === null ? null : normalize(JSON.parse(raw), protocol);
83
+ const isEmpty = (record) => Object.values(record.lanes).every((lane) => lane.commands.length === 0 && !lane.failed?.length);
65
84
  const active = /* @__PURE__ */ new WeakMap();
66
85
  const sweep = async (options, currentKey, quota = false) => {
67
86
  const { backend, retention, namespace = "" } = options;
@@ -75,9 +94,11 @@ const sweep = async (options, currentKey, quota = false) => {
75
94
  } catch {
76
95
  return;
77
96
  }
78
- const format = isRecord(value) ? value["statewireStorageVersion"] : void 0;
79
- if (format !== void 0 && format !== 1 && format !== 2) return void 0;
80
- if (!isSessionRecord(value) || value.commands.length || value.failed?.length) return void 0;
97
+ try {
98
+ if (!isEmpty(normalize(value, "default"))) return void 0;
99
+ } catch {
100
+ return;
101
+ }
81
102
  const savedAt = value["savedAt"] ?? value["updatedAt"];
82
103
  return typeof savedAt === "number" && Number.isFinite(savedAt) ? savedAt : void 0;
83
104
  };
@@ -110,16 +131,10 @@ const sweep = async (options, currentKey, quota = false) => {
110
131
  };
111
132
  const isQuotaError = (error) => error instanceof Error && error.name === "QuotaExceededError";
112
133
  const useKeyValueSessionStorage = (options) => {
113
- const { backend, key, namespace = "", legacyProtocol: suppliedLegacyProtocol, migrate, retention: suppliedRetention, onError } = options;
134
+ const { backend, key, namespace = "", protocol = DEFAULT_PROTOCOL, migrate, retention: suppliedRetention, onError } = options;
114
135
  const latestErrorHandler = useRef(onError);
115
136
  latestErrorHandler.current = onError;
116
137
  const reportError = useMemo(() => (error) => latestErrorHandler.current?.(error), []);
117
- const legacyName = suppliedLegacyProtocol?.name;
118
- const legacyVersion = suppliedLegacyProtocol?.version;
119
- const legacyProtocol = useMemo(() => legacyName === void 0 ? void 0 : {
120
- name: legacyName,
121
- version: legacyVersion
122
- }, [legacyName, legacyVersion]);
123
138
  const retain = suppliedRetention !== void 0;
124
139
  const maxAgeMs = suppliedRetention?.maxAgeMs;
125
140
  const maxEntries = suppliedRetention?.maxEntries;
@@ -143,7 +158,6 @@ const useKeyValueSessionStorage = (options) => {
143
158
  retention
144
159
  ]);
145
160
  const storageKey = namespace + key;
146
- if (legacyProtocol !== void 0) negotiationHeaders({ applicationProtocols: [legacyProtocol] });
147
161
  if (retention !== void 0) {
148
162
  if (!namespace) throw new Error("statewire: session retention requires a namespace");
149
163
  if (!backend.getAllKeys) throw new Error("statewire: session retention requires getAllKeys");
@@ -170,7 +184,7 @@ const useKeyValueSessionStorage = (options) => {
170
184
  };
171
185
  if (state.asynchronous) {
172
186
  state.ready = Promise.resolve(raw).then((value) => {
173
- state.record = decode(value, legacyProtocol);
187
+ state.record = decode(value, protocol);
174
188
  state.hydrated = true;
175
189
  }).catch((cause_0) => {
176
190
  const error_1 = wrapStorageError(cause_0);
@@ -180,7 +194,7 @@ const useKeyValueSessionStorage = (options) => {
180
194
  });
181
195
  state.ready.catch(() => {});
182
196
  } else try {
183
- state.record = decode(raw, legacyProtocol);
197
+ state.record = decode(raw, protocol);
184
198
  } catch (cause_1) {
185
199
  const error_2 = wrapStorageError(cause_1);
186
200
  reportError(error_2);
@@ -190,7 +204,7 @@ const useKeyValueSessionStorage = (options) => {
190
204
  }, [
191
205
  backend,
192
206
  storageKey,
193
- legacyProtocol,
207
+ protocol,
194
208
  reportError
195
209
  ]);
196
210
  useEffect(() => {
@@ -232,7 +246,7 @@ const useKeyValueSessionStorage = (options) => {
232
246
  }
233
247
  if (typeof raw_0 !== "string" && raw_0 !== null) throw new Error("statewire: storage backend changed from synchronous to asynchronous");
234
248
  try {
235
- return decode(raw_0, legacyProtocol);
249
+ return decode(raw_0, protocol);
236
250
  } catch (cause_4) {
237
251
  const error_5 = wrapStorageError(cause_4);
238
252
  reportError(error_5);
@@ -245,7 +259,7 @@ const useKeyValueSessionStorage = (options) => {
245
259
  if (!isSessionRecord(record)) throw new Error("statewire: invalid session record");
246
260
  const encoded = JSON.stringify({
247
261
  ...record,
248
- statewireStorageVersion: 2,
262
+ statewireStorageVersion: 3,
249
263
  savedAt: Date.now()
250
264
  });
251
265
  state_0.record = record;
@@ -307,7 +321,7 @@ const useKeyValueSessionStorage = (options) => {
307
321
  state_0,
308
322
  backend,
309
323
  storageKey,
310
- legacyProtocol,
324
+ protocol,
311
325
  migrate,
312
326
  retention,
313
327
  configured,