statewire 0.16.0 → 0.17.1

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 (96) hide show
  1. package/README.md +12 -4
  2. package/dist/client/connection.d.ts +5 -1
  3. package/dist/client/connection.d.ts.map +1 -1
  4. package/dist/client/connection.js +22 -6
  5. package/dist/client/connection.js.map +1 -1
  6. package/dist/client/core.d.ts.map +1 -1
  7. package/dist/client/core.js +2 -0
  8. package/dist/client/core.js.map +1 -1
  9. package/dist/client/session.d.ts +15 -4
  10. package/dist/client/session.d.ts.map +1 -1
  11. package/dist/client/session.js +235 -52
  12. package/dist/client/session.js.map +1 -1
  13. package/dist/client/sessionStore.d.ts +5 -1
  14. package/dist/client/sessionStore.d.ts.map +1 -1
  15. package/dist/client/sessionStore.js +3 -1
  16. package/dist/client/sessionStore.js.map +1 -1
  17. package/dist/document.d.ts +25 -0
  18. package/dist/document.d.ts.map +1 -0
  19. package/dist/document.js +88 -0
  20. package/dist/document.js.map +1 -0
  21. package/dist/host/draft.js +1 -1
  22. package/dist/host/host.d.ts +1 -0
  23. package/dist/host/host.d.ts.map +1 -1
  24. package/dist/host/host.js.map +1 -1
  25. package/dist/host/http.d.ts.map +1 -1
  26. package/dist/host/http.js +27 -9
  27. package/dist/host/http.js.map +1 -1
  28. package/dist/host/socket.d.ts +12 -5
  29. package/dist/host/socket.d.ts.map +1 -1
  30. package/dist/host/socket.js +107 -43
  31. package/dist/host/socket.js.map +1 -1
  32. package/dist/host-internal.d.ts +9 -1
  33. package/dist/host-internal.d.ts.map +1 -1
  34. package/dist/host-internal.js +1 -1
  35. package/dist/index.d.ts +4 -1
  36. package/dist/index.js +2 -1
  37. package/dist/statewire.d.ts +33 -2
  38. package/dist/statewire.d.ts.map +1 -1
  39. package/dist/statewire.js +34 -3
  40. package/dist/statewire.js.map +1 -1
  41. package/dist/transports/fixtures.js +1 -0
  42. package/dist/transports/fixtures.js.map +1 -1
  43. package/dist/transports/remote.d.ts.map +1 -1
  44. package/dist/transports/remote.js +15 -8
  45. package/dist/transports/remote.js.map +1 -1
  46. package/dist/transports/sse.d.ts.map +1 -1
  47. package/dist/transports/sse.js +8 -8
  48. package/dist/transports/sse.js.map +1 -1
  49. package/dist/transports/testing.d.ts +1 -1
  50. package/dist/transports/testing.d.ts.map +1 -1
  51. package/dist/transports/testing.js +12 -4
  52. package/dist/transports/testing.js.map +1 -1
  53. package/dist/transports/ws.js +2 -2
  54. package/dist/transports/ws.js.map +1 -1
  55. package/dist/types.d.ts +7 -0
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/types.js.map +1 -1
  58. package/dist/wire/documents.d.ts +25 -2
  59. package/dist/wire/documents.d.ts.map +1 -1
  60. package/dist/wire/documents.js +28 -8
  61. package/dist/wire/documents.js.map +1 -1
  62. package/dist/wire/meta.d.ts +3 -18
  63. package/dist/wire/meta.d.ts.map +1 -1
  64. package/dist/wire/meta.js +8 -57
  65. package/dist/wire/meta.js.map +1 -1
  66. package/dist/wire/ops.js +1 -1
  67. package/dist/wire/protocol.d.ts +55 -0
  68. package/dist/wire/protocol.d.ts.map +1 -0
  69. package/dist/wire/protocol.js +221 -0
  70. package/dist/wire/protocol.js.map +1 -0
  71. package/dist/wire/version.d.ts +1 -3
  72. package/dist/wire/version.d.ts.map +1 -1
  73. package/dist/wire/version.js +1 -3
  74. package/dist/wire/version.js.map +1 -1
  75. package/package.json +1 -1
  76. package/src/client/connection.ts +43 -13
  77. package/src/client/core.ts +6 -0
  78. package/src/client/session.ts +340 -45
  79. package/src/client/sessionStore.ts +8 -0
  80. package/src/document.ts +185 -0
  81. package/src/host/host.ts +1 -0
  82. package/src/host/http.ts +36 -7
  83. package/src/host/socket.ts +149 -57
  84. package/src/host-internal.ts +12 -1
  85. package/src/index.ts +5 -0
  86. package/src/statewire.ts +109 -6
  87. package/src/transports/fixtures.ts +1 -1
  88. package/src/transports/remote.ts +16 -7
  89. package/src/transports/sse.ts +18 -8
  90. package/src/transports/testing.ts +10 -2
  91. package/src/transports/ws.ts +1 -1
  92. package/src/types.ts +11 -0
  93. package/src/wire/documents.ts +126 -8
  94. package/src/wire/meta.ts +11 -94
  95. package/src/wire/protocol.ts +327 -0
  96. package/src/wire/version.ts +0 -2
@@ -1,3 +1,12 @@
1
+ import { encodeDocument } from "../document";
2
+ import {
3
+ DEFAULT_PROTOCOL,
4
+ WIRE_VERSION,
5
+ negotiateProtocols,
6
+ } from "../wire/protocol";
7
+ import type { StatewireProtocol } from "../wire/protocol";
8
+ import { applyDocumentOps, mainDocument } from "../wire/documents";
9
+ import type { StatewireDocuments } from "../wire/documents";
1
10
  import { useEffect, useState } from "react";
2
11
  import { createTapRoot, flushTapSync, useResource } from "@assistant-ui/tap";
3
12
  import type { ResourceElement } from "@assistant-ui/tap";
@@ -7,7 +16,7 @@ import { StatewireSendError } from "../types";
7
16
  import type { Statewire } from "../types";
8
17
  import type { StatewireHost } from "../host";
9
18
  import type { StatewireHostInternal } from "../host-internal";
10
- import { encodeValue, reviveValue } from "../wire/meta";
19
+ import { reviveValue } from "../wire/meta";
11
20
  import {
12
21
  COMMAND_CAPACITY_RETRY_MS,
13
22
  MAX_COMMANDS_PER_FRAME,
@@ -17,7 +26,7 @@ import {
17
26
  MAX_INFLIGHT_COMMANDS_PER_HOST,
18
27
  } from "../wire/limits";
19
28
  import { createMacrotaskPoster } from "../util/macrotask";
20
- import type { StatewireMetaEntry } from "../wire/meta";
29
+ import type { StatewireDocument } from "../document";
21
30
  import type { StatewirePath } from "../wire/ops";
22
31
  import type { StatewireCmd, StatewireEnvelope } from "../client/connection";
23
32
  import { frames as serveFrames, stream as serveStream } from "./http";
@@ -71,7 +80,7 @@ type WireOp = {
71
80
  op: string;
72
81
  path?: StatewirePath;
73
82
  value?: unknown;
74
- meta?: StatewireMetaEntry[];
83
+ meta?: StatewireDocument.MetaEntry[];
75
84
  };
76
85
 
77
86
  type Admission = {
@@ -88,6 +97,7 @@ export type ClientRec = {
88
97
  ctxSeq: number;
89
98
  touched: number;
90
99
  context: Record<string, unknown> | null;
100
+ contextDocuments: StatewireDocuments.State;
91
101
  results: Map<number, { ts: number; cmd: StatewireCmd }>;
92
102
  inflight: Map<number, Admission>;
93
103
  /** The auth captured at the record's first attach; later attaches must match. */
@@ -95,6 +105,7 @@ export type ClientRec = {
95
105
  };
96
106
 
97
107
  export type Conn = {
108
+ selection: StatewireProtocol.Selection;
98
109
  clientId: string;
99
110
  socket: StatewireHostInternal.Socket;
100
111
  open: boolean;
@@ -125,8 +136,9 @@ const violationReason = (error: unknown): string | null =>
125
136
  ? error.message
126
137
  : null;
127
138
 
128
- type CtxStatement = { seq: number; value: Record<string, unknown> };
139
+ type CtxStatement = { seq: number; ops: unknown[] };
129
140
  type CmdStatement = {
141
+ protocol: string;
130
142
  seq: number;
131
143
  method: string;
132
144
  params: unknown;
@@ -161,30 +173,6 @@ const parseLane = (value: unknown): Record<string, unknown>[] => {
161
173
 
162
174
  const frameEncoder = new TextEncoder();
163
175
 
164
- /** v1 ctx statements state the context whole: one root replace with an object value. */
165
- const contextValue = (ops: unknown): Record<string, unknown> => {
166
- if (!Array.isArray(ops) || ops.length !== 1) {
167
- throw new Error("context must be a single-op array");
168
- }
169
- const op = ops[0] as Record<string, unknown> | null;
170
- if (
171
- !isPlainObject(op) ||
172
- op["op"] !== "replace" ||
173
- !Array.isArray(op["path"]) ||
174
- op["path"].length !== 0
175
- ) {
176
- throw new Error("context op must be a root replace");
177
- }
178
- const value =
179
- op["meta"] !== undefined
180
- ? reviveValue(op["value"], op["meta"], { rejectUnknown: true })
181
- : op["value"];
182
- if (!isPlainObject(value)) {
183
- throw new Error("context value must be an object");
184
- }
185
- return value;
186
- };
187
-
188
176
  export const parseFrameValue = (frame: unknown): ParsedFrame => {
189
177
  if (!isPlainObject(frame)) return violation("malformed frame");
190
178
  const keys = Object.keys(frame);
@@ -199,19 +187,15 @@ export const parseFrameValue = (frame: unknown): ParsedFrame => {
199
187
  return violation("too many context statements");
200
188
  }
201
189
  for (const statement of parseLane(lane)) {
202
- let value: Record<string, unknown>;
203
- try {
204
- value = contextValue(statement["ops"]);
205
- } catch {
190
+ const ops = statement["ops"];
191
+ if (!Array.isArray(ops) || !ops.length)
206
192
  return violation("invalid-context");
207
- }
208
193
  if (
209
- frameEncoder.encode(JSON.stringify(value)).byteLength >
194
+ frameEncoder.encode(JSON.stringify(ops)).byteLength >
210
195
  MAX_CONTEXT_VALUE_BYTES
211
- ) {
196
+ )
212
197
  return violation("oversized-context");
213
- }
214
- ctx.push({ seq: statement["seq"] as number, value });
198
+ ctx.push({ seq: statement["seq"] as number, ops });
215
199
  }
216
200
  }
217
201
  const cmd: CmdStatement[] = [];
@@ -222,10 +206,15 @@ export const parseFrameValue = (frame: unknown): ParsedFrame => {
222
206
  }
223
207
  for (const statement of parseLane(lane)) {
224
208
  const method = statement["method"];
225
- if (typeof method !== "string" || !("params" in statement)) {
209
+ if (
210
+ typeof method !== "string" ||
211
+ typeof statement["protocol"] !== "string" ||
212
+ !("params" in statement)
213
+ ) {
226
214
  return violation("malformed statement");
227
215
  }
228
216
  cmd.push({
217
+ protocol: statement["protocol"] as string,
229
218
  seq: statement["seq"] as number,
230
219
  method,
231
220
  params: statement["params"],
@@ -254,9 +243,9 @@ const frameIsOversized = (raw: string): boolean =>
254
243
 
255
244
  const encodePayload = (
256
245
  payload: unknown,
257
- ): { payload?: unknown; meta?: StatewireMetaEntry[] } => {
246
+ ): { payload?: unknown; meta?: StatewireDocument.MetaEntry[] } => {
258
247
  if (payload === undefined) return {};
259
- const { value, meta } = encodeValue(payload);
248
+ const { value, meta } = encodeDocument(payload);
260
249
  return { payload: value, ...(meta !== undefined && { meta }) };
261
250
  };
262
251
 
@@ -334,6 +323,8 @@ const parseInitialFrame = (raw: string) => {
334
323
  // ─── host ───────────────────────────────────────────────
335
324
 
336
325
  export type SocketHostState = {
326
+ protocol: StatewireProtocol.Definition;
327
+ wireVersion: StatewireProtocol.Range;
337
328
  heartbeatMs: number;
338
329
  wsInitTimeoutMs: number;
339
330
  maxSubscribers: number;
@@ -384,7 +375,7 @@ const recordOp = (
384
375
  },
385
376
  ) => {
386
377
  if (op.op === "replace" || op.op === "add") {
387
- const { value, meta } = encodeValue(op.value);
378
+ const { value, meta } = encodeDocument(op.value);
388
379
  h.pendingOps.push({ ...op, value, ...(meta !== undefined && { meta }) });
389
380
  } else {
390
381
  h.pendingOps.push(op);
@@ -518,7 +509,9 @@ export const flush = (h: SocketHostState, fin?: StatewireHost.Fin) => {
518
509
  ).map((seq) => ({ seq, type: "applied" }));
519
510
  if (ops.length > 0) {
520
511
  const packet: StatewireEnvelope = {
521
- ops,
512
+ ops: conn.selection.protocols.some((p) => p.name === h.protocol.name)
513
+ ? ops
514
+ : [],
522
515
  ...(appliedEntries.length > 0 && { cmd: appliedEntries }),
523
516
  ...(edge && { idle: level }),
524
517
  };
@@ -526,7 +519,11 @@ export const flush = (h: SocketHostState, fin?: StatewireHost.Fin) => {
526
519
  } else if (appliedEntries.length > 0) {
527
520
  sendTo(h, conn, JSON.stringify({ cmd: appliedEntries }));
528
521
  }
529
- const verdicts = cmds.get(conn.clientId) ?? [];
522
+ const verdicts = (cmds.get(conn.clientId) ?? []).map((cmd) =>
523
+ conn.selection.protocols.some((p) => p.name === h.protocol.name)
524
+ ? cmd
525
+ : { seq: cmd.seq, ...(cmd.dur === undefined ? {} : { dur: cmd.dur }) },
526
+ );
530
527
  if (verdicts.length > 0) {
531
528
  sendTo(h, conn, JSON.stringify({ cmd: verdicts }));
532
529
  }
@@ -562,6 +559,7 @@ const notifyClientRecords = (h: SocketHostState) => {
562
559
  ctxSeq: rec.ctxSeq,
563
560
  touched: rec.touched,
564
561
  context: rec.context,
562
+ contextDocuments: [...rec.contextDocuments.documents],
565
563
  results: [...rec.results].map(([seq, entry]) => ({
566
564
  seq,
567
565
  ts: entry.ts,
@@ -672,6 +670,7 @@ const admitCmd = (
672
670
  rec: ClientRec,
673
671
  statement: CmdStatement,
674
672
  auth: unknown,
673
+ selection: StatewireProtocol.Selection,
675
674
  ) => {
676
675
  const adm: Admission = {
677
676
  seq: statement.seq,
@@ -682,6 +681,18 @@ const admitCmd = (
682
681
  };
683
682
  rec.inflight.set(statement.seq, adm);
684
683
  notifyClientRecords(h);
684
+ if (
685
+ statement.protocol !== h.protocol.name ||
686
+ !selection.protocols.some((p) => p.name === statement.protocol)
687
+ ) {
688
+ settleCmd(h, rec, adm, {
689
+ seq: adm.seq,
690
+ type: "rejected",
691
+ code: "unsupported-protocol",
692
+ message: "command protocol was not negotiated",
693
+ });
694
+ return;
695
+ }
685
696
  if (!Array.isArray(statement.params)) {
686
697
  rejectStatement(h, rec, adm, "params must be an array");
687
698
  return;
@@ -749,6 +760,10 @@ export const admitFrame = (
749
760
  rec: ClientRec,
750
761
  frame: ParsedFrame,
751
762
  auth: unknown,
763
+ selection: StatewireProtocol.Selection = {
764
+ version: h.wireVersion.version,
765
+ protocols: [{ name: h.protocol.name, version: h.protocol.version }],
766
+ },
752
767
  ): FrameAdmission => {
753
768
  rec.touched = Date.now();
754
769
  const through = admittedThrough(rec);
@@ -801,11 +816,28 @@ export const admitFrame = (
801
816
  };
802
817
  }
803
818
  }
804
- for (const statement of ctxNew) {
805
- rec.context = statement.value;
806
- rec.ctxSeq = statement.seq;
819
+ let contextDocuments = rec.contextDocuments;
820
+ try {
821
+ for (const statement of ctxNew)
822
+ contextDocuments = applyDocumentOps(
823
+ contextDocuments,
824
+ statement.ops,
825
+ selection.protocols,
826
+ true,
827
+ );
828
+ if (
829
+ frameEncoder.encode(JSON.stringify([...contextDocuments.documents]))
830
+ .byteLength > MAX_CONTEXT_VALUE_BYTES
831
+ )
832
+ return violation("oversized-context");
833
+ } catch {
834
+ return violation("invalid-context");
807
835
  }
808
- for (const statement of cmdNew) admitCmd(h, rec, statement, auth);
836
+ rec.contextDocuments = contextDocuments;
837
+ rec.context = (mainDocument(contextDocuments, h.protocol.name)?.value ??
838
+ null) as Record<string, unknown> | null;
839
+ if (ctxNew.length) rec.ctxSeq = ctxNew.at(-1)!.seq;
840
+ for (const statement of cmdNew) admitCmd(h, rec, statement, auth, selection);
809
841
  if (cmdNew.length > 0) {
810
842
  // Memory host: admitted means retained — dur rides the admitting flush.
811
843
  queueCmd(h, rec.id, { seq: cmdNew.at(-1)!.seq, dur: true });
@@ -854,7 +886,20 @@ const message = (h: SocketHostState, conn: Conn, data: string) => {
854
886
  }
855
887
  if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
856
888
  const rec = h.clients.get(conn.clientId)!;
857
- const admission = admitFrame(h, rec, frame, conn.auth);
889
+ let admission: ReturnType<typeof admitFrame>;
890
+ try {
891
+ admission = admitFrame(h, rec, frame, conn.auth, conn.selection);
892
+ } catch (error) {
893
+ const reason = violationReason(error);
894
+ if (reason === null) throw error;
895
+ sendTo(
896
+ h,
897
+ conn,
898
+ JSON.stringify({ fin: { reason: "protocol-error", message: reason } }),
899
+ );
900
+ closeConn(h, conn, 1008, reason);
901
+ return;
902
+ }
858
903
  if (admission.type === "refused") {
859
904
  sendTo(h, conn, JSON.stringify(admission.syn));
860
905
  }
@@ -965,6 +1010,7 @@ const connectAuthorized = (
965
1010
  socket,
966
1011
  options.authorized === true,
967
1012
  options.auth,
1013
+ options.selection,
968
1014
  );
969
1015
  return registration?.connection ?? null;
970
1016
  }
@@ -1056,7 +1102,10 @@ const connectAuthorized = (
1056
1102
  const frame = initial.frame;
1057
1103
  let headers: Record<string, string>;
1058
1104
  try {
1059
- headers = normalizedHeaders(initial.headers);
1105
+ headers = {
1106
+ ...normalizedHeaders(initial.headers),
1107
+ ...normalizedHeaders(options.headers ?? {}),
1108
+ };
1060
1109
  } catch {
1061
1110
  sendPending(
1062
1111
  pending,
@@ -1089,17 +1138,39 @@ const connectAuthorized = (
1089
1138
  .then(() => authorize?.({ headers }))
1090
1139
  .then((auth) => {
1091
1140
  if (!pending.open || h.disposed || h.finished !== null) return;
1092
- options.onAuthorized?.(auth);
1141
+ const selection = negotiateProtocols(
1142
+ new Headers(headers),
1143
+ h.wireVersion,
1144
+ [h.protocol],
1145
+ );
1146
+ if ("fin" in selection) {
1147
+ sendPending(pending, JSON.stringify(selection));
1148
+ closePending(h, pending, 1008, selection.fin.message);
1149
+ return;
1150
+ }
1151
+ options.onAuthorized?.(auth, selection);
1093
1152
  pending.open = false;
1094
1153
  h.pendingConns.delete(pending);
1095
1154
  if (pending.timer !== null) clearTimeout(pending.timer);
1096
- const registration = register(h, clientId, socket, true, auth);
1155
+ const registration = register(
1156
+ h,
1157
+ clientId,
1158
+ socket,
1159
+ true,
1160
+ auth,
1161
+ selection,
1162
+ );
1097
1163
  if (registration === null) return;
1098
1164
  pending.connection = registration.connection;
1099
1165
  if (frame.ctx.length || frame.cmd.length) {
1100
- const admission = admitFrame(h, registration.rec, frame, auth);
1101
- if (admission.type === "refused")
1102
- sendTo(h, registration.conn, JSON.stringify(admission.syn));
1166
+ message(
1167
+ h,
1168
+ registration.conn,
1169
+ JSON.stringify({
1170
+ ...(frame.ctx.length ? { ctx: frame.ctx } : {}),
1171
+ ...(frame.cmd.length ? { cmd: frame.cmd } : {}),
1172
+ }),
1173
+ );
1103
1174
  }
1104
1175
  for (const data of pending.queued.splice(0))
1105
1176
  registration.connection.message(data);
@@ -1143,6 +1214,10 @@ export const register = (
1143
1214
  socket: StatewireHostInternal.Socket,
1144
1215
  sawFrame = false,
1145
1216
  auth: unknown = undefined,
1217
+ selection: StatewireProtocol.Selection = {
1218
+ version: h.wireVersion.version,
1219
+ protocols: [{ name: h.protocol.name, version: h.protocol.version }],
1220
+ },
1146
1221
  ): Registration | null => {
1147
1222
  const refuse = (code: number, reason: string) => {
1148
1223
  try {
@@ -1193,6 +1268,7 @@ export const register = (
1193
1268
  ctxSeq: 0,
1194
1269
  touched: Date.now(),
1195
1270
  context: null,
1271
+ contextDocuments: { mode: "mounted", documents: new Map() },
1196
1272
  results: new Map(),
1197
1273
  inflight: new Map(),
1198
1274
  principal: auth,
@@ -1202,6 +1278,7 @@ export const register = (
1202
1278
  })();
1203
1279
  rec.touched = Date.now();
1204
1280
  const conn: Conn = {
1281
+ selection,
1205
1282
  clientId,
1206
1283
  socket,
1207
1284
  open: true,
@@ -1214,22 +1291,26 @@ export const register = (
1214
1291
  gcClients(h);
1215
1292
  notifyClientRecords(h);
1216
1293
  ensureHeartbeat(h);
1217
- const { value, meta } = encodeValue(h.stateHost!.snapshot());
1294
+ const { value, meta } = encodeDocument(h.stateHost!.snapshot());
1218
1295
  const hello: StatewireEnvelope = {
1219
1296
  ops: [
1220
1297
  {
1221
- op: "replace",
1222
- path: [],
1298
+ op: "mount",
1299
+ protocol: h.protocol.name,
1300
+ main: true,
1223
1301
  value,
1224
1302
  ...(meta !== undefined && { meta }),
1225
1303
  },
1226
1304
  ],
1227
1305
  syn: {
1306
+ ...selection,
1228
1307
  seq: admittedThrough(rec),
1229
1308
  ctxSeq: rec.ctxSeq,
1230
1309
  ...(h.idleWire && { idle: true }),
1231
1310
  },
1232
1311
  };
1312
+ if (!selection.protocols.some((p) => p.name === h.protocol.name))
1313
+ hello.ops = [];
1233
1314
  sendTo(h, conn, JSON.stringify(hello));
1234
1315
  const connection: StatewireHostInternal.SocketConnection = {
1235
1316
  clientId,
@@ -1287,6 +1368,10 @@ const restoreClientRecords = (
1287
1368
  record.id,
1288
1369
  {
1289
1370
  ...record,
1371
+ contextDocuments: {
1372
+ mode: "mounted",
1373
+ documents: new Map(record.contextDocuments ?? []),
1374
+ },
1290
1375
  results: new Map(
1291
1376
  record.results.map(({ seq, ts, cmd }) => [seq, { ts, cmd }]),
1292
1377
  ),
@@ -1305,6 +1390,8 @@ const useSocketHost = (
1305
1390
  > => {
1306
1391
  const [h] = useState((): SocketHostState => {
1307
1392
  const host: SocketHostState = {
1393
+ protocol: { name: DEFAULT_PROTOCOL, version: WIRE_VERSION },
1394
+ wireVersion: { version: WIRE_VERSION },
1308
1395
  heartbeatMs: 15_000,
1309
1396
  wsInitTimeoutMs: 5_000,
1310
1397
  maxSubscribers: MAX_SUBSCRIBERS,
@@ -1341,6 +1428,11 @@ const useSocketHost = (
1341
1428
  };
1342
1429
  return host;
1343
1430
  });
1431
+ h.protocol = options?.protocol ?? {
1432
+ name: DEFAULT_PROTOCOL,
1433
+ version: WIRE_VERSION,
1434
+ };
1435
+ h.wireVersion = options?.wireVersion ?? { version: WIRE_VERSION };
1344
1436
  setHeartbeatMs(h, options?.heartbeatIntervalMs ?? 15_000);
1345
1437
  h.pushback = options?.pushback;
1346
1438
  h.wsInitTimeoutMs = options?.wsInitTimeoutMs ?? 5_000;
@@ -1,3 +1,5 @@
1
+ import type { StatewireProtocol } from "./wire/protocol";
2
+ import type { StatewireDocuments } from "./wire/documents";
1
3
  import type { StatewireHost } from "./host";
2
4
  import type { StatewireCmd } from "./client/connection";
3
5
 
@@ -17,12 +19,18 @@ export namespace StatewireHostInternal {
17
19
  };
18
20
 
19
21
  export type SocketConnectOptions = {
22
+ /** Upgrade headers take precedence over initialization headers. */
23
+ headers?: Record<string, string>;
20
24
  /** The adapter restored this socket from an already-authorized session. */
21
25
  authorized?: boolean;
26
+ selection?: StatewireProtocol.Selection;
22
27
  /** The restored session's captured auth value; it must satisfy the client record's principal binding. */
23
28
  auth?: unknown;
24
29
  /** Persist authorization (and the captured auth value) before the first snapshot is released. */
25
- onAuthorized?: (auth: unknown) => void;
30
+ onAuthorized?: (
31
+ auth: unknown,
32
+ selection: StatewireProtocol.Selection,
33
+ ) => void;
26
34
  };
27
35
 
28
36
  export type SocketHost = {
@@ -44,6 +52,7 @@ export namespace StatewireHostInternal {
44
52
  ctxSeq: number;
45
53
  touched: number;
46
54
  context: Record<string, unknown> | null;
55
+ contextDocuments?: readonly [number, StatewireDocuments.Document][];
47
56
  results: readonly { seq: number; ts: number; cmd: StatewireCmd }[];
48
57
  principal: unknown;
49
58
  };
@@ -54,6 +63,8 @@ export namespace StatewireHostInternal {
54
63
  };
55
64
 
56
65
  export type SocketHostOptions<TAuth = unknown> = {
66
+ protocol?: StatewireProtocol.Definition;
67
+ wireVersion?: StatewireProtocol.Range;
57
68
  /** Gates every attach; the awaited return value is the attach's auth, exposed to command handlers as `this.caller.auth`. The first attach binds the client id's record to this value; a later attach whose value is not structurally equal is refused (HTTP 403, socket close 1008). */
58
69
  authorize?: (ctx: {
59
70
  headers: Record<string, string>;
package/src/index.ts CHANGED
@@ -1,5 +1,10 @@
1
1
  export type { Statewire } from "./types";
2
+ export { encodeDocument, decodeDocument } from "./document";
3
+ export type { StatewireDocument } from "./document";
2
4
  export { StatewireClient, useStatewire, StatewireResource } from "./statewire";
3
5
  export { StatewireHttp } from "./transports/sse";
4
6
  export { StatewireWebsocket } from "./transports/ws";
5
7
  export { StatewireSendError } from "./types";
8
+
9
+ export type { StatewireProtocol } from "./wire/protocol";
10
+ export type { StatewireDocuments } from "./wire/documents";