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,6 +1,8 @@
1
+ import { encodeDocument, reviveDocumentValue } from "../document.js";
2
+ import { DEFAULT_PROTOCOL, WIRE_VERSION, negotiateProtocols } from "../wire/protocol.js";
3
+ import { applyDocumentOps, mainDocument } from "../wire/documents.js";
1
4
  import { StatewireSendError } from "../types.js";
2
5
  import { getCommandsHost, getStateHost } from "./host.js";
3
- import { encodeValue, reviveValue } from "../wire/meta.js";
4
6
  import { COMMAND_CAPACITY_RETRY_MS, MAX_INFLIGHT_COMMANDS_PER_HOST } from "../wire/limits.js";
5
7
  import { createMacrotaskPoster } from "../util/macrotask.js";
6
8
  import { frames, stream } from "./http.js";
@@ -49,15 +51,6 @@ const parseLane = (value) => {
49
51
  return statements;
50
52
  };
51
53
  const frameEncoder = new TextEncoder();
52
- /** v1 ctx statements state the context whole: one root replace with an object value. */
53
- const contextValue = (ops) => {
54
- if (!Array.isArray(ops) || ops.length !== 1) throw new Error("context must be a single-op array");
55
- const op = ops[0];
56
- if (!isPlainObject(op) || op["op"] !== "replace" || !Array.isArray(op["path"]) || op["path"].length !== 0) throw new Error("context op must be a root replace");
57
- const value = op["meta"] !== void 0 ? reviveValue(op["value"], op["meta"], { rejectUnknown: true }) : op["value"];
58
- if (!isPlainObject(value)) throw new Error("context value must be an object");
59
- return value;
60
- };
61
54
  const parseFrameValue = (frame) => {
62
55
  if (!isPlainObject(frame)) return violation("malformed frame");
63
56
  const keys = Object.keys(frame);
@@ -69,16 +62,12 @@ const parseFrameValue = (frame) => {
69
62
  const lane = frame["ctx"];
70
63
  if (Array.isArray(lane) && lane.length > 256) return violation("too many context statements");
71
64
  for (const statement of parseLane(lane)) {
72
- let value;
73
- try {
74
- value = contextValue(statement["ops"]);
75
- } catch {
76
- return violation("invalid-context");
77
- }
78
- if (frameEncoder.encode(JSON.stringify(value)).byteLength > 65536) return violation("oversized-context");
65
+ const ops = statement["ops"];
66
+ if (!Array.isArray(ops) || !ops.length) return violation("invalid-context");
67
+ if (frameEncoder.encode(JSON.stringify(ops)).byteLength > 65536) return violation("oversized-context");
79
68
  ctx.push({
80
69
  seq: statement["seq"],
81
- value
70
+ ops
82
71
  });
83
72
  }
84
73
  }
@@ -88,8 +77,9 @@ const parseFrameValue = (frame) => {
88
77
  if (Array.isArray(lane) && lane.length > 256) return violation("too many commands");
89
78
  for (const statement of parseLane(lane)) {
90
79
  const method = statement["method"];
91
- if (typeof method !== "string" || !("params" in statement)) return violation("malformed statement");
80
+ if (typeof method !== "string" || typeof statement["protocol"] !== "string" || !("params" in statement)) return violation("malformed statement");
92
81
  cmd.push({
82
+ protocol: statement["protocol"],
93
83
  seq: statement["seq"],
94
84
  method,
95
85
  params: statement["params"],
@@ -114,7 +104,7 @@ const parseFrame = (raw) => {
114
104
  const frameIsOversized = (raw) => raw.length > 8388608 || frameEncoder.encode(raw).byteLength > 8388608;
115
105
  const encodePayload = (payload) => {
116
106
  if (payload === void 0) return {};
117
- const { value, meta } = encodeValue(payload);
107
+ const { value, meta } = encodeDocument(payload);
118
108
  return {
119
109
  payload: value,
120
110
  ...meta !== void 0 && { meta }
@@ -186,7 +176,7 @@ const scheduleFlush = (h) => {
186
176
  };
187
177
  const recordOp = (h, op) => {
188
178
  if (op.op === "replace" || op.op === "add") {
189
- const { value, meta } = encodeValue(op.value);
179
+ const { value, meta } = encodeDocument(op.value);
190
180
  h.pendingOps.push({
191
181
  ...op,
192
182
  value,
@@ -277,13 +267,16 @@ const flush = (h, fin) => {
277
267
  }));
278
268
  if (ops.length > 0) {
279
269
  const packet = {
280
- ops,
270
+ ops: conn.selection.protocols.some((p) => p.name === h.protocol.name) ? ops : [],
281
271
  ...appliedEntries.length > 0 && { cmd: appliedEntries },
282
272
  ...edge && { idle: level }
283
273
  };
284
274
  sendTo(h, conn, JSON.stringify(packet));
285
275
  } else if (appliedEntries.length > 0) sendTo(h, conn, JSON.stringify({ cmd: appliedEntries }));
286
- const verdicts = cmds.get(conn.clientId) ?? [];
276
+ const verdicts = (cmds.get(conn.clientId) ?? []).map((cmd) => conn.selection.protocols.some((p) => p.name === h.protocol.name) ? cmd : {
277
+ seq: cmd.seq,
278
+ ...cmd.dur === void 0 ? {} : { dur: cmd.dur }
279
+ });
287
280
  if (verdicts.length > 0) sendTo(h, conn, JSON.stringify({ cmd: verdicts }));
288
281
  if (edge && ops.length === 0) sendTo(h, conn, JSON.stringify({ idle: level }));
289
282
  if (fin !== void 0) {
@@ -311,6 +304,7 @@ const notifyClientRecords = (h) => {
311
304
  ctxSeq: rec.ctxSeq,
312
305
  touched: rec.touched,
313
306
  context: rec.context,
307
+ contextDocuments: [...rec.contextDocuments.documents],
314
308
  results: [...rec.results].map(([seq, entry]) => ({
315
309
  seq,
316
310
  ts: entry.ts,
@@ -380,7 +374,7 @@ const rejectStatement = (h, rec, adm, message) => settleCmd(h, rec, adm, {
380
374
  code: "invalid-params",
381
375
  message
382
376
  });
383
- const admitCmd = (h, rec, statement, auth) => {
377
+ const admitCmd = (h, rec, statement, auth, selection) => {
384
378
  const adm = {
385
379
  seq: statement.seq,
386
380
  method: statement.method,
@@ -390,13 +384,22 @@ const admitCmd = (h, rec, statement, auth) => {
390
384
  };
391
385
  rec.inflight.set(statement.seq, adm);
392
386
  notifyClientRecords(h);
387
+ if (statement.protocol !== h.protocol.name || !selection.protocols.some((p) => p.name === statement.protocol)) {
388
+ settleCmd(h, rec, adm, {
389
+ seq: adm.seq,
390
+ type: "rejected",
391
+ code: "unsupported-protocol",
392
+ message: "command protocol was not negotiated"
393
+ });
394
+ return;
395
+ }
393
396
  if (!Array.isArray(statement.params)) {
394
397
  rejectStatement(h, rec, adm, "params must be an array");
395
398
  return;
396
399
  }
397
400
  let params = statement.params;
398
401
  if (statement.meta !== void 0) try {
399
- params = reviveValue(params, statement.meta, { rejectUnknown: true });
402
+ params = reviveDocumentValue(params, statement.meta, { rejectUnknown: true });
400
403
  } catch (error) {
401
404
  rejectStatement(h, rec, adm, `invalid params: ${error.message}`);
402
405
  return;
@@ -434,7 +437,13 @@ const admitCmd = (h, rec, statement, auth) => {
434
437
  });
435
438
  });
436
439
  };
437
- const admitFrame = (h, rec, frame, auth) => {
440
+ const admitFrame = (h, rec, frame, auth, selection = {
441
+ version: h.wireVersion.version,
442
+ protocols: [{
443
+ name: h.protocol.name,
444
+ version: h.protocol.version
445
+ }]
446
+ }) => {
438
447
  rec.touched = Date.now();
439
448
  const through = admittedThrough(rec);
440
449
  for (const statement of frame.cmd) if (statement.seq <= through) reemit(h, rec, statement.seq);
@@ -476,11 +485,17 @@ const admitFrame = (h, rec, frame, auth) => {
476
485
  };
477
486
  }
478
487
  }
479
- for (const statement of ctxNew) {
480
- rec.context = statement.value;
481
- rec.ctxSeq = statement.seq;
488
+ let contextDocuments = rec.contextDocuments;
489
+ try {
490
+ for (const statement of ctxNew) contextDocuments = applyDocumentOps(contextDocuments, statement.ops, selection.protocols, true);
491
+ if (frameEncoder.encode(JSON.stringify([...contextDocuments.documents])).byteLength > 65536) return violation("oversized-context");
492
+ } catch {
493
+ return violation("invalid-context");
482
494
  }
483
- for (const statement of cmdNew) admitCmd(h, rec, statement, auth);
495
+ rec.contextDocuments = contextDocuments;
496
+ rec.context = mainDocument(contextDocuments, h.protocol.name)?.value ?? null;
497
+ if (ctxNew.length) rec.ctxSeq = ctxNew.at(-1).seq;
498
+ for (const statement of cmdNew) admitCmd(h, rec, statement, auth, selection);
484
499
  if (cmdNew.length > 0) queueCmd(h, rec.id, {
485
500
  seq: cmdNew.at(-1).seq,
486
501
  dur: true
@@ -516,7 +531,19 @@ const message = (h, conn, data) => {
516
531
  }
517
532
  if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
518
533
  const rec = h.clients.get(conn.clientId);
519
- const admission = admitFrame(h, rec, frame, conn.auth);
534
+ let admission;
535
+ try {
536
+ admission = admitFrame(h, rec, frame, conn.auth, conn.selection);
537
+ } catch (error) {
538
+ const reason = violationReason(error);
539
+ if (reason === null) throw error;
540
+ sendTo(h, conn, JSON.stringify({ fin: {
541
+ reason: "protocol-error",
542
+ message: reason
543
+ } }));
544
+ closeConn(h, conn, 1008, reason);
545
+ return;
546
+ }
520
547
  if (admission.type === "refused") sendTo(h, conn, JSON.stringify(admission.syn));
521
548
  };
522
549
  const closePending = (h, pending, code, reason) => {
@@ -573,7 +600,7 @@ const normalizedHeaders = (headers) => {
573
600
  };
574
601
  const connectAuthorized = (h, clientId, socket, options) => {
575
602
  const authorize = h.authorize;
576
- if (options.authorized === true) return register(h, clientId, socket, options.authorized === true, options.auth)?.connection ?? null;
603
+ if (options.authorized === true) return register(h, clientId, socket, options.authorized === true, options.auth, options.selection)?.connection ?? null;
577
604
  const refuse = (code, reason) => {
578
605
  try {
579
606
  socket.close(code, reason);
@@ -640,7 +667,10 @@ const connectAuthorized = (h, clientId, socket, options) => {
640
667
  const frame = initial.frame;
641
668
  let headers;
642
669
  try {
643
- headers = normalizedHeaders(initial.headers);
670
+ headers = {
671
+ ...normalizedHeaders(initial.headers),
672
+ ...normalizedHeaders(options.headers ?? {})
673
+ };
644
674
  } catch {
645
675
  sendPending(pending, JSON.stringify({ fin: {
646
676
  reason: "protocol-error",
@@ -661,17 +691,23 @@ const connectAuthorized = (h, clientId, socket, options) => {
661
691
  pending.authorizing = true;
662
692
  Promise.resolve().then(() => authorize?.({ headers })).then((auth) => {
663
693
  if (!pending.open || h.disposed || h.finished !== null) return;
664
- options.onAuthorized?.(auth);
694
+ const selection = negotiateProtocols(new Headers(headers), h.wireVersion, [h.protocol]);
695
+ if ("fin" in selection) {
696
+ sendPending(pending, JSON.stringify(selection));
697
+ closePending(h, pending, 1008, selection.fin.message);
698
+ return;
699
+ }
700
+ options.onAuthorized?.(auth, selection);
665
701
  pending.open = false;
666
702
  h.pendingConns.delete(pending);
667
703
  if (pending.timer !== null) clearTimeout(pending.timer);
668
- const registration = register(h, clientId, socket, true, auth);
704
+ const registration = register(h, clientId, socket, true, auth, selection);
669
705
  if (registration === null) return;
670
706
  pending.connection = registration.connection;
671
- if (frame.ctx.length || frame.cmd.length) {
672
- const admission = admitFrame(h, registration.rec, frame, auth);
673
- if (admission.type === "refused") sendTo(h, registration.conn, JSON.stringify(admission.syn));
674
- }
707
+ if (frame.ctx.length || frame.cmd.length) message(h, registration.conn, JSON.stringify({
708
+ ...frame.ctx.length ? { ctx: frame.ctx } : {},
709
+ ...frame.cmd.length ? { cmd: frame.cmd } : {}
710
+ }));
675
711
  for (const data of pending.queued.splice(0)) registration.connection.message(data);
676
712
  }).catch((error) => {
677
713
  if (pending.open) rejectPendingAuthorization(h, pending, error);
@@ -698,7 +734,13 @@ const gcClients = (h) => {
698
734
  }
699
735
  if (changed) notifyClientRecords(h);
700
736
  };
701
- const register = (h, clientId, socket, sawFrame = false, auth = void 0) => {
737
+ const register = (h, clientId, socket, sawFrame = false, auth = void 0, selection = {
738
+ version: h.wireVersion.version,
739
+ protocols: [{
740
+ name: h.protocol.name,
741
+ version: h.protocol.version
742
+ }]
743
+ }) => {
702
744
  const refuse = (code, reason) => {
703
745
  try {
704
746
  socket.close(code, reason);
@@ -734,6 +776,10 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0) => {
734
776
  ctxSeq: 0,
735
777
  touched: Date.now(),
736
778
  context: null,
779
+ contextDocuments: {
780
+ mode: "mounted",
781
+ documents: /* @__PURE__ */ new Map()
782
+ },
737
783
  results: /* @__PURE__ */ new Map(),
738
784
  inflight: /* @__PURE__ */ new Map(),
739
785
  principal: auth
@@ -743,6 +789,7 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0) => {
743
789
  })();
744
790
  rec.touched = Date.now();
745
791
  const conn = {
792
+ selection,
746
793
  clientId,
747
794
  socket,
748
795
  open: true,
@@ -755,20 +802,23 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0) => {
755
802
  gcClients(h);
756
803
  notifyClientRecords(h);
757
804
  ensureHeartbeat(h);
758
- const { value, meta } = encodeValue(h.stateHost.snapshot());
805
+ const { value, meta } = encodeDocument(h.stateHost.snapshot());
759
806
  const hello = {
760
807
  ops: [{
761
- op: "replace",
762
- path: [],
808
+ op: "mount",
809
+ protocol: h.protocol.name,
810
+ main: true,
763
811
  value,
764
812
  ...meta !== void 0 && { meta }
765
813
  }],
766
814
  syn: {
815
+ ...selection,
767
816
  seq: admittedThrough(rec),
768
817
  ctxSeq: rec.ctxSeq,
769
818
  ...h.idleWire && { idle: true }
770
819
  }
771
820
  };
821
+ if (!selection.protocols.some((p) => p.name === h.protocol.name)) hello.ops = [];
772
822
  sendTo(h, conn, JSON.stringify(hello));
773
823
  return {
774
824
  rec,
@@ -808,6 +858,10 @@ const suspend = (h) => {
808
858
  };
809
859
  const restoreClientRecords = (records = []) => new Map(records.map((record) => [record.id, {
810
860
  ...record,
861
+ contextDocuments: {
862
+ mode: "mounted",
863
+ documents: new Map(record.contextDocuments ?? [])
864
+ },
811
865
  results: new Map(record.results.map(({ seq, ts, cmd }) => [seq, {
812
866
  ts,
813
867
  cmd
@@ -817,6 +871,11 @@ const restoreClientRecords = (records = []) => new Map(records.map((record) => [
817
871
  const useSocketHost = (transport, options, persistence) => {
818
872
  const [h] = useState(() => {
819
873
  const host = {
874
+ protocol: {
875
+ name: DEFAULT_PROTOCOL,
876
+ version: WIRE_VERSION
877
+ },
878
+ wireVersion: { version: WIRE_VERSION },
820
879
  heartbeatMs: 15e3,
821
880
  wsInitTimeoutMs: 5e3,
822
881
  maxSubscribers: MAX_SUBSCRIBERS,
@@ -852,6 +911,11 @@ const useSocketHost = (transport, options, persistence) => {
852
911
  };
853
912
  return host;
854
913
  });
914
+ h.protocol = options?.protocol ?? {
915
+ name: "default",
916
+ version: "2026-09-07"
917
+ };
918
+ h.wireVersion = options?.wireVersion ?? { version: "2026-09-07" };
855
919
  setHeartbeatMs(h, options?.heartbeatIntervalMs ?? 15e3);
856
920
  h.pushback = options?.pushback;
857
921
  h.wsInitTimeoutMs = options?.wsInitTimeoutMs ?? 5e3;