statewire 0.18.0 → 0.18.2

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.
@@ -65,7 +65,7 @@ type Session = {
65
65
  idle: boolean;
66
66
  onSessionLost: StatewireClient.OnSessionLost | undefined;
67
67
  onDone: (() => void) | undefined;
68
- ctxOps: unknown[] | undefined;
68
+ ctxNextSeq: number;
69
69
  ctxPending: CtxPending[];
70
70
  lanesOnAttach: boolean;
71
71
  attachLaneSeqs: readonly [ReadonlySet<number>, ReadonlySet<number>];
@@ -1 +1 @@
1
- {"version":3,"file":"session.d.ts","names":[],"sources":["../../src/client/session.ts"],"mappings":";;;;;;;;KA0CK;EACH;EACA,WAAW,YAAY,gBAAgB;EACvC;EACA;EACA;;KAGG;WACM;WACA,SAAS;WACT,KAAK;;EAEd;;EAEA;;EAEA;EACA,UAAU;EACV,SAAS;;KAGN;WACM;WACA;EACT;;KAeG;KAEO,0BAA0B,kBAAkB;EACtD,eAAe,gBAAgB;EAC/B,gBAAgB,gBAAgB;EAChC;;KAGG;EACH,KAAK;EACL,QAAQ,gBAAgB;EACxB;;KAGG;EACH,SAAS;EACT,WAAW,kBAAkB;EAC7B,kBAAkB,mBAAmB;EACrC,QAAQ,gBAAgB;EACxB,KAAK;EACL;EACA;EACA,OAAO;EACP;EACA,iBAAiB,gBAAgB;EACjC,OAAO,gBAAgB;EACvB,eAAe,mBAAmB;EAClC,QAAQ;EACR;;EAEA;EACA;EACA;EACA;EACA;EACA,eAAe,gBAAgB;EAC/B;EACA;EACA,YAAY;EACZ;EACA,0BAA0B,qBAAqB;EAC/C;EACA,SAAS;EACT;EACA,mBAAmB;EACnB,WAAW,mBAAmB;EAC9B;EACA,SAAS;EACT,QAAQ,UAAU;EAClB,QAAQ;EACR;EACA;EACA;EACA,WAAW;EACX;EACA,WAAW;EACX;EACA,UAAU,mBAAmB,sBAAsB;;cA8JxC,iBACX,GAAG,qBACO,gBAAgB;cA4tBtB,eAAgB,GAAG,SAAS,WAAW;cAmLhC,gBACX,GAAG,SACH,OAAO,gBAAgB;cAYZ,cACX,GAAG,SACH,OAAO,gBAAgB,kBACtB;cAyJU,gBAAiB,GAAG,SAAS;cAGpC,aACJ,GAAG,SACH,OAAO,yBACP;cAuBW,iBAAkB,GAAG,YAAU,mBAAmB;cA+BlD,iBACX,GAAG,SACH,kBACA,gBACA,+BAA0B;cAuEf,sBACX,SAAS,4BACR"}
1
+ {"version":3,"file":"session.d.ts","names":[],"sources":["../../src/client/session.ts"],"mappings":";;;;;;;;KA0CK;EACH;EACA,WAAW,YAAY,gBAAgB;EACvC;EACA;EACA;;KAGG;WACM;WACA,SAAS;WACT,KAAK;;EAEd;;EAEA;;EAEA;EACA,UAAU;EACV,SAAS;;KAGN;WACM;WACA;EACT;;KAeG;KAEO,0BAA0B,kBAAkB;EACtD,eAAe,gBAAgB;EAC/B,gBAAgB,gBAAgB;EAChC;;KAGG;EACH,KAAK;EACL,QAAQ,gBAAgB;EACxB;;KAGG;EACH,SAAS;EACT,WAAW,kBAAkB;EAC7B,kBAAkB,mBAAmB;EACrC,QAAQ,gBAAgB;EACxB,KAAK;EACL;EACA;EACA,OAAO;EACP;EACA,iBAAiB,gBAAgB;EACjC,OAAO,gBAAgB;EACvB,eAAe,mBAAmB;EAClC,QAAQ;EACR;;EAEA;EACA;EACA;EACA;EACA;EACA,eAAe,gBAAgB;EAC/B;EACA;EACA,YAAY;EACZ;EACA,0BAA0B,qBAAqB;EAC/C;EACA,SAAS;EACT;EACA,mBAAmB;EACnB,WAAW,mBAAmB;EAC9B;EACA,SAAS;EACT,QAAQ,UAAU;EAClB,QAAQ;EACR;EACA;EACA;EACA,WAAW;EACX;EACA,WAAW;EACX;EACA,UAAU,mBAAmB,sBAAsB;;cAgJxC,iBACX,GAAG,qBACO,gBAAgB;cAqqBtB,eAAgB,GAAG,SAAS,WAAW;cAmLhC,gBACX,GAAG,SACH,OAAO,gBAAgB;cAYZ,cACX,GAAG,SACH,OAAO,gBAAgB,kBACtB;cA4GU,gBAAiB,GAAG,SAAS;cAGpC,aACJ,GAAG,SACH,OAAO,yBACP;cAuBW,iBAAkB,GAAG,YAAU,mBAAmB;cA+BlD,iBACX,GAAG,SACH,kBACA,gBACA,+BAA0B;cAsEf,sBACX,SAAS,4BACR"}
@@ -93,21 +93,7 @@ const transact = (s, fn) => {
93
93
  const result = shared.txq.then(() => s.store.lock(async () => {
94
94
  const stored = s.store.load();
95
95
  if (stored !== null && stored.clientId !== s.record.clientId) adoptReset(s, stored);
96
- else if (stored !== null) {
97
- s.record = stored;
98
- if (stored.context !== void 0) {
99
- const posted = new Set(s.ctxPending.filter((p) => p.posted).map((p) => p.seq));
100
- s.ctxOps = stored.context.ops;
101
- s.contextDocuments = applyDocumentOps({
102
- mode: "mounted",
103
- documents: /* @__PURE__ */ new Map()
104
- }, stored.context.ops);
105
- s.ctxPending = stored.context.pending.map((p) => ({
106
- ...p,
107
- posted: posted.has(p.seq)
108
- }));
109
- }
110
- }
96
+ else if (stored !== null) s.record = stored;
111
97
  fn();
112
98
  const persistence = s.store.flush?.();
113
99
  if (persistence) await persistence;
@@ -203,20 +189,6 @@ const settleAll = (s, detach) => {
203
189
  });
204
190
  }
205
191
  };
206
- const restateContext = (s) => {
207
- s.ctxPending = [];
208
- if (s.ctxOps === void 0) return;
209
- const seq = s.record.ctxNextSeq;
210
- setRecord(s, {
211
- ...s.record,
212
- ctxNextSeq: seq + 1
213
- });
214
- s.ctxPending.push({
215
- seq,
216
- ops: s.ctxOps,
217
- posted: false
218
- });
219
- };
220
192
  const settleLost = (s, entries) => {
221
193
  for (const entry of entries) {
222
194
  s.lifecycle?.onCovered?.(entry.inv);
@@ -245,7 +217,6 @@ const rebaseRecord = (s, adopt) => {
245
217
  s.lifecycle?.onCovered?.(old.inv);
246
218
  enqueueInner(s, old.command, old.inv.params, old.resolve, old.reject);
247
219
  }
248
- restateContext(s);
249
220
  publish(s);
250
221
  wake(s);
251
222
  pump(s);
@@ -253,7 +224,6 @@ const rebaseRecord = (s, adopt) => {
253
224
  const resetRecord = (s, clientId) => rebaseRecord(s, () => setRecord(s, {
254
225
  clientId,
255
226
  nextSeq: 1,
256
- ctxNextSeq: 1,
257
227
  commands: []
258
228
  }));
259
229
  const adoptReset = (s, stored) => rebaseRecord(s, () => {
@@ -389,47 +359,7 @@ const publishIngested = (s) => {
389
359
  for (const listener of s.documentListeners) listener();
390
360
  wake(s);
391
361
  };
392
- const snapCtxClock = (s, ctxWm) => {
393
- if (s.record.context?.recover === "version-1") {
394
- const document = s.contextDocuments.documents.get(0);
395
- if (s.contextDocuments.documents.size !== 1 || !document?.main) throw new Error("statewire: version-1 context recovery requires one main document at doc 0");
396
- const encoded = encodeDocument(document.value);
397
- const ops = [{
398
- ...ctxWm === 0 ? {
399
- op: "mount",
400
- protocol: document.protocol,
401
- main: true
402
- } : {
403
- op: "replace",
404
- path: []
405
- },
406
- ...encoded
407
- }];
408
- s.ctxPending = [{
409
- seq: ctxWm + 1,
410
- ops,
411
- posted: false
412
- }];
413
- setRecord(s, {
414
- ...s.record,
415
- ctxNextSeq: ctxWm + 2,
416
- context: {
417
- ops: contextSnapshot(s.contextDocuments),
418
- pending: s.ctxPending
419
- }
420
- });
421
- return;
422
- }
423
- if (!(s.ctxPending.every((c, i) => c.seq === ctxWm + 1 + i) && s.record.ctxNextSeq === ctxWm + s.ctxPending.length + 1)) {
424
- if (ctxWm > 0) throw new Error("statewire: context history is missing; use a new client id");
425
- setRecord(s, {
426
- ...s.record,
427
- ctxNextSeq: ctxWm + 1
428
- });
429
- restateContext(s);
430
- }
431
- };
432
- const sessionLostInner = (s, seqWm, ctxWm) => {
362
+ const sessionLostInner = (s, seqWm) => {
433
363
  const held = [...s.pending];
434
364
  settleLost(s, held);
435
365
  s.pending = [];
@@ -437,17 +367,16 @@ const sessionLostInner = (s, seqWm, ctxWm) => {
437
367
  setRecord(s, {
438
368
  clientId: s.record.clientId,
439
369
  nextSeq: seqWm + 1,
440
- ctxNextSeq: s.record.ctxNextSeq,
441
370
  commands: [],
442
371
  ...s.record.failed !== void 0 && { failed: s.record.failed }
443
372
  });
444
- snapCtxClock(s, ctxWm);
445
373
  if (held.length > 0 && s.onSessionLost?.(held.map((p) => p.inv)) === "requeue") for (const old of held) enqueueInner(s, old.command, old.inv.params, () => {}, () => {});
446
374
  publish(s);
447
375
  wake(s);
448
376
  };
449
377
  const handleHello = (s, { ops, syn }) => {
450
378
  if (syn.record !== void 0 && syn.record !== "ephemeral") protocolError(`unknown record: ${String(syn.record)}`);
379
+ if (syn.ctxSeq !== void 0 && syn.ctxSeq !== 0) protocolError(`hello ctxSeq must be 0, got ${syn.ctxSeq}`);
451
380
  s.selection = syn.version === void 0 || syn.protocols === void 0 ? void 0 : {
452
381
  version: syn.version,
453
382
  protocols: syn.protocols
@@ -469,11 +398,7 @@ const handleHello = (s, { ops, syn }) => {
469
398
  if (s.shared) adoptStored(s);
470
399
  s.durableSeq = syn.seq ?? 0;
471
400
  for (const entry of s.pending) entry.posted = attachCmd.has(entry.seq);
472
- const ctxWatermark = syn.ctxSeq ?? 0;
473
- s.ctxPending = s.ctxPending.filter((c) => c.seq > ctxWatermark);
474
401
  for (const c of s.ctxPending) c.posted = attachCtx.has(c.seq);
475
- snapCtxClock(s, ctxWatermark);
476
- if (s.ctxOps !== void 0) persistContext(s);
477
402
  syncBacklogInner(s);
478
403
  s.sessionKnown = true;
479
404
  };
@@ -503,7 +428,8 @@ const handleRefusal = (s, syn) => {
503
428
  if (typeof ctxSeq === "number") {
504
429
  s.ctxPending = s.ctxPending.filter((c) => c.seq > ctxSeq);
505
430
  for (const c of s.ctxPending) c.posted = false;
506
- if (s.ctxOps !== void 0) persistContext(s);
431
+ const firstCtx = s.ctxPending.find((c) => !c.posted);
432
+ if (firstCtx !== void 0 && firstCtx.seq > ctxSeq + 1) protocolError(`ctx gap on a live attach: seq ${firstCtx.seq} after watermark ${ctxSeq}`);
507
433
  }
508
434
  if (typeof syn.retry === "number") {
509
435
  s.resumeAt = Date.now() + syn.retry;
@@ -512,14 +438,8 @@ const handleRefusal = (s, syn) => {
512
438
  return;
513
439
  }
514
440
  const firstCmd = s.pending.find((p) => !p.posted);
515
- const firstCtx = s.ctxPending.find((c) => !c.posted);
516
- const cmdGap = typeof seq === "number" && firstCmd !== void 0 && firstCmd.seq > Math.max(seq, s.durableSeq) + 1;
517
- const ctxGap = typeof ctxSeq === "number" && firstCtx !== void 0 && firstCtx.seq > ctxSeq + 1;
518
- if (cmdGap || ctxGap) {
519
- transactBackground(s, () => {
520
- if (cmdGap) sessionLostInner(s, seq, ctxSeq ?? 0);
521
- else snapCtxClock(s, ctxSeq);
522
- }, () => {
441
+ if (typeof seq === "number" && firstCmd !== void 0 && firstCmd.seq > Math.max(seq, s.durableSeq) + 1) {
442
+ transactBackground(s, () => sessionLostInner(s, seq), () => {
523
443
  wake(s);
524
444
  pump(s);
525
445
  });
@@ -541,19 +461,19 @@ const handleFinish = (s, { reason }) => {
541
461
  wake(s);
542
462
  };
543
463
  const isFinished = (s) => s.attach === "finished";
544
- const buildLanes = (s, persisted = false) => {
464
+ const buildLanes = (s, cmdGate = "all") => {
545
465
  const ctxEntries = [];
546
466
  for (const entry of s.ctxPending) {
547
467
  if (entry.posted) continue;
548
- if (persisted && (s.persistedRecord?.clientId !== s.record.clientId || entry.seq >= s.persistedRecord.ctxNextSeq)) break;
549
468
  if (ctxEntries.length >= 256) break;
550
469
  if (ctxEntries.length > 0 && entry.seq !== ctxEntries.at(-1).seq + 1) break;
551
470
  ctxEntries.push(entry);
552
471
  }
553
472
  const cmdEntries = [];
554
473
  for (const entry of s.pending) {
474
+ if (cmdGate === "none") break;
555
475
  if (entry.posted) continue;
556
- if (persisted && (s.persistedRecord?.clientId !== s.record.clientId || entry.seq >= s.persistedRecord.nextSeq)) break;
476
+ if (cmdGate === "persisted" && (s.persistedRecord?.clientId !== s.record.clientId || entry.seq >= s.persistedRecord.nextSeq)) break;
557
477
  if (cmdEntries.length >= 256) break;
558
478
  if (cmdEntries.length > 0 && entry.seq !== cmdEntries.at(-1).seq + 1) break;
559
479
  cmdEntries.push(entry);
@@ -577,12 +497,21 @@ const buildLanes = (s, persisted = false) => {
577
497
  ctxEntries
578
498
  };
579
499
  };
500
+ const resetContextLane = (s) => {
501
+ const ops = contextSnapshot(s.contextDocuments);
502
+ s.ctxPending = ops.length === 0 ? [] : [{
503
+ seq: 1,
504
+ ops,
505
+ posted: false
506
+ }];
507
+ s.ctxNextSeq = s.ctxPending.length + 1;
508
+ };
580
509
  const takeAttachLanes = (s) => {
581
510
  s.attachLaneSeqs = [/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set()];
511
+ resetContextLane(s);
582
512
  const wanted = s.lanesOnAttach;
583
513
  s.lanesOnAttach = false;
584
- if (!wanted || !s.sessionKnown || s.record.context?.recover !== void 0) return null;
585
- const lanes = buildLanes(s, true);
514
+ const lanes = buildLanes(s, wanted && s.sessionKnown ? "persisted" : "none");
586
515
  if (!lanes) return null;
587
516
  s.attachLaneSeqs = [new Set(lanes.cmdEntries.map((p) => p.seq)), new Set(lanes.ctxEntries.map((c) => c.seq))];
588
517
  for (const entry of lanes.cmdEntries) entry.attempted = true;
@@ -715,7 +644,7 @@ const setTransport = (s, transport) => {
715
644
  wake(s);
716
645
  };
717
646
  const enqueueInner = (s, command, params, resolve, reject) => {
718
- const seq = s.record.nextSeq;
647
+ const seq = Math.max(s.record.nextSeq, s.durableSeq + 1);
719
648
  const entry = {
720
649
  seq,
721
650
  command,
@@ -925,67 +854,25 @@ const contextSnapshot = (documents) => [...documents.documents].map(([doc, d]) =
925
854
  ...encoded.meta === void 0 ? {} : { meta: encoded.meta }
926
855
  };
927
856
  });
928
- const persistContext = (s) => {
929
- const ops = contextSnapshot(s.contextDocuments);
930
- s.ctxOps = ops;
931
- setRecord(s, {
932
- ...s.record,
933
- context: {
934
- ops,
935
- ...s.record.context?.recover === void 0 ? {} : { recover: s.record.context.recover },
936
- pending: s.ctxPending.map(({ seq, ops }) => ({
937
- seq,
938
- ops
939
- }))
940
- }
941
- });
942
- };
943
857
  const changeContext = (s, build) => {
944
- if (!s.hydrated) {
945
- s.ready.then(() => changeContext(s, build)).catch((cause) => {
946
- const error = wrapStorageError(cause);
947
- s.storageError = error;
948
- s.transport?.storageFailed(error);
949
- });
950
- return;
951
- }
952
858
  if (!s.running || isFinished(s)) return;
953
- const submit = () => {
954
- if (!s.running || isFinished(s)) return;
955
- const previous = s.record.context === void 0 ? s.contextDocuments : applyDocumentOps({
956
- mode: "mounted",
957
- documents: /* @__PURE__ */ new Map()
958
- }, s.record.context.ops);
959
- const operations = build(previous);
960
- if (!operations.length) throw new Error("statewire: context operations must not be empty");
961
- const offers = protocolOffers(s.options);
962
- const protocols = s.selection?.protocols ?? (offers === "*" ? void 0 : offers);
963
- s.contextDocuments = applyDocumentOps(previous, operations, protocols, true);
964
- const seq = s.record.ctxNextSeq;
965
- s.ctxPending = [...s.ctxPending, {
966
- seq,
967
- ops: operations.map((op) => {
968
- const value = op;
969
- if (value["doc"] !== 0) return value;
970
- const { doc: _doc, ...rest } = value;
971
- return rest;
972
- }),
973
- posted: false
974
- }];
975
- s.record = {
976
- ...s.record,
977
- ctxNextSeq: seq + 1
978
- };
979
- persistContext(s);
980
- };
981
- if (!s.shared) {
982
- submit();
983
- wake(s);
984
- pump(s);
985
- } else transactBackground(s, submit, () => {
986
- wake(s);
987
- pump(s);
988
- });
859
+ const operations = build(s.contextDocuments);
860
+ if (!operations.length) throw new Error("statewire: context operations must not be empty");
861
+ const offers = protocolOffers(s.options);
862
+ const protocols = s.selection?.protocols ?? (offers === "*" ? void 0 : offers);
863
+ s.contextDocuments = applyDocumentOps(s.contextDocuments, operations, protocols, true);
864
+ s.ctxPending = [...s.ctxPending, {
865
+ seq: s.ctxNextSeq++,
866
+ ops: operations.map((op) => {
867
+ const value = op;
868
+ if (value["doc"] !== 0) return value;
869
+ const { doc: _doc, ...rest } = value;
870
+ return rest;
871
+ }),
872
+ posted: false
873
+ }];
874
+ wake(s);
875
+ pump(s);
989
876
  };
990
877
  const updateContext = (s, operations) => changeContext(s, () => operations);
991
878
  const setContext = (s, value, protocol = s.options.protocol ?? "default") => {
@@ -1061,14 +948,10 @@ const loadSessionRecord = (store, options) => {
1061
948
  const record = stored ?? {
1062
949
  clientId: randomId(),
1063
950
  nextSeq: 1,
1064
- ctxNextSeq: 1,
1065
951
  commands: []
1066
952
  };
1067
953
  if (stored === null) store.save(record);
1068
- return {
1069
- record,
1070
- stored
1071
- };
954
+ return record;
1072
955
  };
1073
956
  const useStatewireSession = (options) => {
1074
957
  const [defaultStore] = useState(createMemorySessionStore);
@@ -1078,25 +961,18 @@ const useStatewireSession = (options) => {
1078
961
  const store = options.sessionStore ?? mounted ?? defaultStore;
1079
962
  if (store.lock === void 0 !== (store.subscribe === void 0)) throw new Error("statewire: session storage must provide lock and subscribe together");
1080
963
  const [s] = useState(() => {
1081
- const { record, stored } = store.ready === void 0 ? loadSessionRecord(store, options) : {
1082
- record: {
1083
- clientId: randomId(),
1084
- nextSeq: 1,
1085
- ctxNextSeq: 1,
1086
- commands: []
1087
- },
1088
- stored: null
964
+ const record = store.ready === void 0 ? loadSessionRecord(store, options) : {
965
+ clientId: randomId(),
966
+ nextSeq: 1,
967
+ commands: []
1089
968
  };
1090
969
  const session = {
1091
970
  options,
1092
971
  selection: void 0,
1093
- contextDocuments: stored?.context === void 0 ? {
972
+ contextDocuments: {
1094
973
  mode: "mounted",
1095
974
  documents: /* @__PURE__ */ new Map()
1096
- } : applyDocumentOps({
1097
- mode: "mounted",
1098
- documents: /* @__PURE__ */ new Map()
1099
- }, stored.context.ops),
975
+ },
1100
976
  record,
1101
977
  ctx: {
1102
978
  beforeAttach: () => persistBeforeSend(session),
@@ -1132,11 +1008,8 @@ const useStatewireSession = (options) => {
1132
1008
  idle: false,
1133
1009
  onSessionLost: void 0,
1134
1010
  onDone: void 0,
1135
- ctxOps: stored?.context?.ops,
1136
- ctxPending: stored?.context?.pending.map((p) => ({
1137
- ...p,
1138
- posted: false
1139
- })) ?? [],
1011
+ ctxNextSeq: 1,
1012
+ ctxPending: [],
1140
1013
  lanesOnAttach: false,
1141
1014
  attachLaneSeqs: [/* @__PURE__ */ new Set(), /* @__PURE__ */ new Set()],
1142
1015
  frameFailures: 0,
@@ -1172,20 +1045,7 @@ const useStatewireSession = (options) => {
1172
1045
  };
1173
1046
  if (store.ready !== void 0) {
1174
1047
  session.ready = store.ready.then(() => {
1175
- const { record: record_0 } = loadSessionRecord(store, session.options);
1176
- session.record = record_0;
1177
- session.contextDocuments = record_0.context === void 0 ? {
1178
- mode: "mounted",
1179
- documents: /* @__PURE__ */ new Map()
1180
- } : applyDocumentOps({
1181
- mode: "mounted",
1182
- documents: /* @__PURE__ */ new Map()
1183
- }, record_0.context.ops);
1184
- session.ctxOps = record_0.context?.ops;
1185
- session.ctxPending = record_0.context?.pending.map((entry) => ({
1186
- ...entry,
1187
- posted: false
1188
- })) ?? [];
1048
+ session.record = loadSessionRecord(store, session.options);
1189
1049
  session.recordPersisted = true;
1190
1050
  session.hydrated = true;
1191
1051
  if (session.running) session.notify();