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
@@ -1,8 +1,8 @@
1
1
  import { encodeDocument, reviveDocumentValue } from "../document.js";
2
2
  import { DEFAULT_PROTOCOL, WIRE_VERSION, negotiateProtocols } from "../wire/protocol.js";
3
- import { applyDocumentOps, mainDocument } from "../wire/documents.js";
3
+ import { sanitizeValue } from "../wire/ops.js";
4
4
  import { StatewireSendError } from "../types.js";
5
- import { getCommandsHost, getStateHost } from "./host.js";
5
+ import { documentIdentity, getCommandsHost, getStateHost } from "./host.js";
6
6
  import { COMMAND_CAPACITY_RETRY_MS, MAX_INFLIGHT_COMMANDS_PER_HOST } from "../wire/limits.js";
7
7
  import { createMacrotaskPoster } from "../util/macrotask.js";
8
8
  import { frames, stream } from "./http.js";
@@ -35,62 +35,37 @@ const violation = (reason) => {
35
35
  };
36
36
  const violationReason = (error) => error instanceof Error && error.name === "StatewireViolation" ? error.message : null;
37
37
  const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
38
- const parseLane = (value) => {
39
- if (!Array.isArray(value)) return violation("malformed frame");
40
- if (value.length === 0) return violation("empty lane");
41
- const statements = [];
42
- let prev = null;
43
- for (const statement of value) {
44
- if (!isPlainObject(statement)) return violation("malformed statement");
45
- const seq = statement["seq"];
46
- if (typeof seq !== "number" || !Number.isInteger(seq) || seq < 1) return violation("malformed statement");
47
- if (prev !== null && seq !== prev + 1) return violation("malformed frame");
48
- prev = seq;
49
- statements.push(statement);
50
- }
51
- return statements;
52
- };
53
38
  const frameEncoder = new TextEncoder();
54
39
  const parseFrameValue = (frame) => {
55
40
  if (!isPlainObject(frame)) return violation("malformed frame");
56
41
  const keys = Object.keys(frame);
57
42
  if (keys.length === 0) return violation("malformed frame");
58
- const unknown = keys.filter((key) => key !== "ctx" && key !== "cmd").sort();
43
+ const unknown = keys.filter((key) => key !== "cmd").sort();
59
44
  if (unknown.length > 0) return violation(`unknown frame key: '${unknown[0]}'`);
60
- const ctx = [];
61
- if (frame["ctx"] !== void 0) {
62
- const lane = frame["ctx"];
63
- if (Array.isArray(lane) && lane.length > 256) return violation("too many context statements");
64
- for (const statement of parseLane(lane)) {
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");
68
- ctx.push({
69
- seq: statement["seq"],
70
- ops
71
- });
72
- }
73
- }
45
+ const lane = frame["cmd"];
46
+ if (!Array.isArray(lane)) return violation("malformed frame");
47
+ if (lane.length === 0) return violation("empty lane");
48
+ if (lane.length > 256) return violation("too many commands");
49
+ const prev = /* @__PURE__ */ new Map();
74
50
  const cmd = [];
75
- if (frame["cmd"] !== void 0) {
76
- const lane = frame["cmd"];
77
- if (Array.isArray(lane) && lane.length > 256) return violation("too many commands");
78
- for (const statement of parseLane(lane)) {
79
- const method = statement["method"];
80
- if (typeof method !== "string" || typeof statement["protocol"] !== "string" || !("params" in statement)) return violation("malformed statement");
81
- cmd.push({
82
- protocol: statement["protocol"],
83
- seq: statement["seq"],
84
- method,
85
- params: statement["params"],
86
- ...statement["meta"] !== void 0 && { meta: statement["meta"] }
87
- });
88
- }
51
+ for (const statement of lane) {
52
+ if (!isPlainObject(statement)) return violation("malformed statement");
53
+ const seq = statement["seq"];
54
+ const method = statement["method"];
55
+ const protocol = statement["protocol"];
56
+ if (typeof seq !== "number" || !Number.isInteger(seq) || seq < 1 || typeof method !== "string" || typeof protocol !== "string" || !("params" in statement)) return violation("malformed statement");
57
+ const last = prev.get(protocol);
58
+ if (last !== void 0 && seq !== last + 1) return violation("malformed frame");
59
+ prev.set(protocol, seq);
60
+ cmd.push({
61
+ protocol,
62
+ seq,
63
+ method,
64
+ params: statement["params"],
65
+ ...statement["meta"] !== void 0 && { meta: statement["meta"] }
66
+ });
89
67
  }
90
- return {
91
- ctx,
92
- cmd
93
- };
68
+ return { cmd };
94
69
  };
95
70
  const parseFrame = (raw) => {
96
71
  let frame;
@@ -111,19 +86,23 @@ const encodePayload = (payload) => {
111
86
  };
112
87
  };
113
88
  const cmdOf = (adm, outcome) => {
89
+ const at = {
90
+ protocol: adm.protocol,
91
+ seq: adm.seq
92
+ };
114
93
  if (outcome.type === "accepted") return {
115
- seq: adm.seq,
94
+ ...at,
116
95
  type: "result",
117
96
  ...encodePayload(outcome.payload)
118
97
  };
119
98
  const failure = outcome.error instanceof StatewireSendError ? outcome.error.failure : null;
120
99
  switch (failure?.fate) {
121
100
  case "crashed": return {
122
- seq: adm.seq,
101
+ ...at,
123
102
  type: "crashed"
124
103
  };
125
104
  case "failed": return {
126
- seq: adm.seq,
105
+ ...at,
127
106
  type: "failed",
128
107
  ...failure.message !== void 0 && { message: failure.message }
129
108
  };
@@ -131,20 +110,20 @@ const cmdOf = (adm, outcome) => {
131
110
  if (failure.code !== void 0) {
132
111
  const message = failure.code === "unknown-command" ? `unknown method: '${adm.method}'` : failure.message;
133
112
  return {
134
- seq: adm.seq,
113
+ ...at,
135
114
  type: "rejected",
136
115
  code: failure.code,
137
116
  ...message !== void 0 && { message }
138
117
  };
139
118
  }
140
119
  return {
141
- seq: adm.seq,
120
+ ...at,
142
121
  type: "rejected",
143
122
  ...failure.message !== void 0 && { message: failure.message },
144
123
  ...encodePayload(failure.payload)
145
124
  };
146
125
  default: return {
147
- seq: adm.seq,
126
+ ...at,
148
127
  type: "failed",
149
128
  message: outcome.error instanceof Error ? outcome.error.message : String(outcome.error)
150
129
  };
@@ -163,10 +142,7 @@ const parseInitialFrame = (raw) => {
163
142
  if (!isPlainObject(headers) || Object.values(headers).some((v) => typeof v !== "string")) return violation("malformed headers");
164
143
  return {
165
144
  headers,
166
- frame: Object.keys(lanes).length ? parseFrameValue(lanes) : {
167
- ctx: [],
168
- cmd: []
169
- }
145
+ frame: Object.keys(lanes).length ? parseFrameValue(lanes) : { cmd: [] }
170
146
  };
171
147
  };
172
148
  const scheduleFlush = (h) => {
@@ -174,6 +150,10 @@ const scheduleFlush = (h) => {
174
150
  h.flushScheduled = true;
175
151
  h.post();
176
152
  };
153
+ const markViews = (h) => {
154
+ h.viewsDirty = true;
155
+ scheduleFlush(h);
156
+ };
177
157
  const recordOp = (h, op) => {
178
158
  if (op.op === "replace" || op.op === "add") {
179
159
  const { value, meta } = encodeDocument(op.value);
@@ -193,8 +173,10 @@ const attach = (h, next) => {
193
173
  if (!nextState || !nextCommands?.submit) throw new Error("StatewireSocketHost requires a statewire/host transport (useStatewireState with object state, useStatewireCommands)");
194
174
  if (nextState !== h.stateHost) {
195
175
  h.stateHost?.setOnOp(null);
176
+ h.stateHost?.setOnDocuments(null);
196
177
  h.stateHost = nextState;
197
178
  h.stateHost.setOnOp((op) => recordOp(h, op));
179
+ h.stateHost.setOnDocuments(() => markViews(h));
198
180
  }
199
181
  h.commandsHost = nextCommands;
200
182
  };
@@ -205,6 +187,7 @@ const closeConn = (h, conn, code, reason) => {
205
187
  try {
206
188
  conn.socket.close(code, reason);
207
189
  } catch {}
190
+ h.current?.detached?.(conn.attach);
208
191
  if (h.conns.size === 0 && h.heartbeatTimer !== null) {
209
192
  clearInterval(h.heartbeatTimer);
210
193
  h.heartbeatTimer = null;
@@ -219,6 +202,13 @@ const sendTo = (h, conn, data) => {
219
202
  closeConn(h, conn);
220
203
  }
221
204
  };
205
+ const violate = (h, conn, reason) => {
206
+ sendTo(h, conn, JSON.stringify({ fin: {
207
+ reason: "protocol-error",
208
+ message: reason
209
+ } }));
210
+ closeConn(h, conn, 1008, reason);
211
+ };
222
212
  const atSubscriberLimit = (h) => h.conns.size + [...h.pendingConns].filter((conn) => conn.authorizing).length >= h.maxSubscribers;
223
213
  const ensureHeartbeat = (h) => {
224
214
  if (h.heartbeatTimer !== null || h.heartbeatMs <= 0) return;
@@ -237,46 +227,146 @@ const setHeartbeatMs = (h, ms) => {
237
227
  ensureHeartbeat(h);
238
228
  }
239
229
  };
230
+ const durableLanes = function* (h) {
231
+ for (const rec of h.clients.values()) yield* rec.lanes.values();
232
+ };
233
+ const allLanes = function* (h) {
234
+ yield* durableLanes(h);
235
+ for (const conn of h.conns) yield* conn.lanes.values();
236
+ };
240
237
  const idleLevel = (h) => {
241
238
  if (h.current?.work?.isIdle === false) return false;
242
- for (const rec of h.clients.values()) if (rec.inflight.size > 0) return false;
239
+ for (const lane of allLanes(h)) if (lane.inflight.size > 0) return false;
243
240
  return true;
244
241
  };
242
+ const selects = (conn, protocol) => conn.selection.protocols.some((p) => p.name === protocol);
243
+ const viewOf = (h, conn) => {
244
+ const view = /* @__PURE__ */ new Map();
245
+ const shared = h.stateHost.documents();
246
+ const hostMain = documentIdentity(h.protocol.name);
247
+ for (const p of conn.selection.protocols) if (p.record === "ephemeral") {
248
+ for (const [id, doc] of conn.documents) if (doc.protocol === p.name) view.set(id, doc);
249
+ } else for (const [id, doc] of shared) if (doc.protocol === p.name && id !== hostMain && (doc.main || conn.included.has(id))) view.set(id, doc);
250
+ return view;
251
+ };
252
+ const diffView = (h, conn) => {
253
+ const current = viewOf(h, conn);
254
+ const ops = [];
255
+ for (const [id, entry] of conn.view) {
256
+ if (current.has(id)) continue;
257
+ conn.view.delete(id);
258
+ ops.push({
259
+ op: "unmount",
260
+ doc: entry.doc
261
+ });
262
+ }
263
+ for (const [id, doc] of current) {
264
+ const entry = conn.view.get(id);
265
+ if (entry !== void 0 && entry.value === doc.value) continue;
266
+ const { value, meta } = encodeDocument(doc.value);
267
+ if (entry === void 0) {
268
+ const number = conn.nextDoc++;
269
+ conn.view.set(id, {
270
+ doc: number,
271
+ value: doc.value
272
+ });
273
+ ops.push({
274
+ op: "mount",
275
+ doc: number,
276
+ protocol: doc.protocol,
277
+ ...doc.key !== null && { key: doc.key },
278
+ ...doc.main && { main: true },
279
+ value,
280
+ ...meta !== void 0 && { meta }
281
+ });
282
+ } else {
283
+ entry.value = doc.value;
284
+ ops.push({
285
+ op: "replace",
286
+ doc: entry.doc,
287
+ path: [],
288
+ value,
289
+ ...meta !== void 0 && { meta }
290
+ });
291
+ }
292
+ }
293
+ return ops;
294
+ };
295
+ const attachOf = (h, conn) => {
296
+ const requireSelected = (protocol) => {
297
+ const entry = conn.selection.protocols.find((p) => p.name === protocol);
298
+ if (entry === void 0) throw new Error(`statewire: protocol ${protocol} was not negotiated`);
299
+ return entry;
300
+ };
301
+ return {
302
+ document: (protocol, key) => {
303
+ if (key === void 0 && protocol === h.protocol.name) return h.stateHost.snapshot();
304
+ const view = viewOf(h, conn);
305
+ if (key !== void 0) return view.get(documentIdentity(protocol, key))?.value;
306
+ for (const doc of view.values()) if (doc.protocol === protocol && doc.main) return doc.value;
307
+ },
308
+ mount: (protocol, value, key, main = key === void 0) => {
309
+ if (requireSelected(protocol).record !== "ephemeral") throw new Error(`statewire: protocol ${protocol} is not ephemeral`);
310
+ const encoded = encodeDocument(value);
311
+ if (frameEncoder.encode(JSON.stringify(encoded)).byteLength > 65536) throw new Error("statewire: oversized ephemeral document");
312
+ const id = documentIdentity(protocol, key);
313
+ if (main) {
314
+ for (const [other, doc] of conn.documents) if (doc.protocol === protocol && doc.main && other !== id) throw new Error(`statewire: ${protocol} already has a main document`);
315
+ }
316
+ conn.documents.set(id, {
317
+ protocol,
318
+ key: key === void 0 ? null : [...key],
319
+ main,
320
+ value: sanitizeValue(value)
321
+ });
322
+ markViews(h);
323
+ },
324
+ unmount: (protocol, key) => {
325
+ if (!conn.documents.delete(documentIdentity(protocol, key))) throw new Error("statewire: document is not mounted");
326
+ markViews(h);
327
+ },
328
+ include: (protocol, key) => {
329
+ if (requireSelected(protocol).record !== void 0) throw new Error(`statewire: protocol ${protocol} is not durable`);
330
+ conn.included.add(documentIdentity(protocol, key));
331
+ markViews(h);
332
+ },
333
+ exclude: (protocol, key) => {
334
+ conn.included.delete(documentIdentity(protocol, key));
335
+ markViews(h);
336
+ }
337
+ };
338
+ };
245
339
  const flush = (h, fin) => {
246
340
  flushTapSync(() => {});
247
341
  h.stateHost?.takeChanged();
248
- for (const rec of h.clients.values()) for (const adm of rec.inflight.values()) if (adm.applied && !adm.settled && !adm.appliedSent) {
342
+ for (const lane of allLanes(h)) for (const adm of lane.inflight.values()) if (adm.applied && !adm.settled && !adm.appliedSent) {
249
343
  adm.appliedSent = true;
250
- queueApplied(h, rec.id, adm.seq);
344
+ queueApplied(h, lane, adm);
251
345
  }
252
346
  const level = idleLevel(h);
253
347
  const edge = level !== h.idleWire;
254
- if (h.pendingOps.length === 0 && h.pendingApplied.size === 0 && h.pendingCmds.size === 0 && fin === void 0 && !edge) return;
348
+ if (h.pendingOps.length === 0 && h.pendingApplied.size === 0 && h.pendingCmds.size === 0 && !h.viewsDirty && fin === void 0 && !edge && ![...h.conns].some((conn) => conn.pendingApplied.length > 0 || conn.pendingCmds.length > 0)) return;
255
349
  h.idleWire = level;
350
+ h.viewsDirty = false;
256
351
  if (fin === void 0 && edge && level && h.finOnIdle) fin = { reason: "idle" };
257
- const ops = h.pendingOps;
352
+ const mainOps = h.pendingOps;
258
353
  const applied = h.pendingApplied;
259
354
  const cmds = h.pendingCmds;
260
355
  h.pendingOps = [];
261
356
  h.pendingApplied = /* @__PURE__ */ new Map();
262
357
  h.pendingCmds = /* @__PURE__ */ new Map();
263
358
  for (const conn of [...h.conns]) {
264
- const appliedEntries = (applied.get(conn.clientId) ?? []).map((seq) => ({
265
- seq,
266
- type: "applied"
359
+ if (!conn.ready) continue;
360
+ const forConn = (list) => (list ?? []).filter((cmd) => selects(conn, cmd.protocol));
361
+ const ops = [...selects(conn, h.protocol.name) ? mainOps : [], ...diffView(h, conn)];
362
+ const appliedEntries = [...forConn(applied.get(conn.clientId)), ...conn.pendingApplied.splice(0)];
363
+ if (ops.length > 0) sendTo(h, conn, JSON.stringify({
364
+ ops,
365
+ ...appliedEntries.length > 0 && { cmd: appliedEntries },
366
+ ...edge && { idle: level }
267
367
  }));
268
- if (ops.length > 0) {
269
- const packet = {
270
- ops: conn.selection.protocols.some((p) => p.name === h.protocol.name) ? ops : [],
271
- ...appliedEntries.length > 0 && { cmd: appliedEntries },
272
- ...edge && { idle: level }
273
- };
274
- sendTo(h, conn, JSON.stringify(packet));
275
- } else if (appliedEntries.length > 0) sendTo(h, conn, JSON.stringify({ cmd: appliedEntries }));
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
- });
368
+ else if (appliedEntries.length > 0) sendTo(h, conn, JSON.stringify({ cmd: appliedEntries }));
369
+ const verdicts = [...forConn(cmds.get(conn.clientId)), ...conn.pendingCmds.splice(0)];
280
370
  if (verdicts.length > 0) sendTo(h, conn, JSON.stringify({ cmd: verdicts }));
281
371
  if (edge && ops.length === 0) sendTo(h, conn, JSON.stringify({ idle: level }));
282
372
  if (fin !== void 0) {
@@ -285,213 +375,237 @@ const flush = (h, fin) => {
285
375
  }
286
376
  }
287
377
  };
288
- const queueApplied = (h, clientId, seq) => {
289
- const list = h.pendingApplied.get(clientId);
290
- if (list) list.push(seq);
291
- else h.pendingApplied.set(clientId, [seq]);
378
+ const queueApplied = (h, lane, adm) => {
379
+ const cmd = {
380
+ protocol: adm.protocol,
381
+ seq: adm.seq,
382
+ type: "applied"
383
+ };
384
+ if (lane.conn !== null) lane.conn.pendingApplied.push(cmd);
385
+ else {
386
+ const list = h.pendingApplied.get(lane.clientId);
387
+ if (list) list.push(cmd);
388
+ else h.pendingApplied.set(lane.clientId, [cmd]);
389
+ }
292
390
  scheduleFlush(h);
293
391
  };
294
- const queueCmd = (h, clientId, cmd) => {
295
- const list = h.pendingCmds.get(clientId);
296
- if (list) list.push(cmd);
297
- else h.pendingCmds.set(clientId, [cmd]);
392
+ const queueCmd = (h, lane, cmd) => {
393
+ if (lane.conn !== null) lane.conn.pendingCmds.push(cmd);
394
+ else {
395
+ const list = h.pendingCmds.get(lane.clientId);
396
+ if (list) list.push(cmd);
397
+ else h.pendingCmds.set(lane.clientId, [cmd]);
398
+ }
298
399
  scheduleFlush(h);
299
400
  };
300
401
  const notifyClientRecords = (h) => {
301
402
  h.persistence?.onClientRecordsChange?.([...h.clients.values()].map((rec) => ({
302
403
  id: rec.id,
303
- lastSeq: admittedThrough(rec),
304
404
  touched: rec.touched,
305
- results: [...rec.results].map(([seq, entry]) => ({
306
- seq,
307
- ts: entry.ts,
308
- cmd: entry.cmd
309
- })),
405
+ lanes: Object.fromEntries([...rec.lanes.values()].map((lane) => [lane.protocol, {
406
+ lastSeq: admittedThrough(lane),
407
+ results: [...lane.results].map(([seq, entry]) => ({
408
+ seq,
409
+ ts: entry.ts,
410
+ cmd: entry.cmd
411
+ }))
412
+ }])),
310
413
  principal: rec.principal
311
414
  })));
312
415
  };
313
- const retain = (rec, cmd) => {
416
+ const retain = (lane, cmd) => {
314
417
  const now = Date.now();
315
- rec.results.set(cmd.seq, {
418
+ lane.results.set(cmd.seq, {
316
419
  ts: now,
317
420
  cmd
318
421
  });
319
- const newest = Math.max(...rec.results.keys());
320
- for (const [seq, entry] of rec.results) if (seq !== newest && now - entry.ts > RESULT_TTL_MS) rec.results.delete(seq);
321
- while (rec.results.size > RESULT_LIMIT) rec.results.delete(Math.min(...rec.results.keys()));
322
- };
323
- /** Cmd-lane watermark: the last statement admitted, settled or not. */
324
- const admittedThrough = (rec) => Math.max(rec.lastSeq, ...rec.inflight.keys());
325
- const reemit = (h, rec, seq) => {
326
- const cached = rec.results.get(seq);
422
+ const newest = Math.max(...lane.results.keys());
423
+ for (const [seq, entry] of lane.results) if (seq !== newest && now - entry.ts > RESULT_TTL_MS) lane.results.delete(seq);
424
+ while (lane.results.size > RESULT_LIMIT) lane.results.delete(Math.min(...lane.results.keys()));
425
+ };
426
+ /** Lane watermark: the last statement admitted, settled or not. */
427
+ const admittedThrough = (lane) => Math.max(lane.lastSeq, ...lane.inflight.keys());
428
+ const watermark = (h, conn, protocol) => {
429
+ const lane = conn.selection.protocols.find((p) => p.name === protocol).record === "ephemeral" ? conn.lanes.get(protocol) : h.clients.get(conn.clientId)?.lanes.get(protocol);
430
+ return lane === void 0 ? 0 : admittedThrough(lane);
431
+ };
432
+ const laneOf = (h, conn, protocol) => {
433
+ const entry = conn.selection.protocols.find((p) => p.name === protocol);
434
+ if (entry === void 0) return violation("unsupported protocol");
435
+ const ephemeral = entry.record === "ephemeral";
436
+ const lanes = ephemeral ? conn.lanes : h.clients.get(conn.clientId).lanes;
437
+ let lane = lanes.get(protocol);
438
+ if (lane === void 0) {
439
+ lane = {
440
+ protocol,
441
+ clientId: conn.clientId,
442
+ conn: ephemeral ? conn : null,
443
+ lastSeq: 0,
444
+ results: /* @__PURE__ */ new Map(),
445
+ inflight: /* @__PURE__ */ new Map()
446
+ };
447
+ lanes.set(protocol, lane);
448
+ }
449
+ return lane;
450
+ };
451
+ const reemit = (h, lane, seq) => {
452
+ const cached = lane.results.get(seq);
327
453
  if (cached) {
328
- if (seq === Math.max(...rec.results.keys()) || Date.now() - cached.ts <= RESULT_TTL_MS) {
329
- queueCmd(h, rec.id, cached.cmd);
454
+ if (seq === Math.max(...lane.results.keys()) || Date.now() - cached.ts <= RESULT_TTL_MS) {
455
+ queueCmd(h, lane, cached.cmd);
330
456
  return;
331
457
  }
332
- rec.results.delete(seq);
458
+ lane.results.delete(seq);
333
459
  notifyClientRecords(h);
334
460
  }
335
- const adm = rec.inflight.get(seq);
461
+ const adm = lane.inflight.get(seq);
336
462
  if (adm) {
337
- if (adm.applied) queueApplied(h, rec.id, seq);
463
+ if (adm.applied) queueApplied(h, lane, adm);
338
464
  return;
339
465
  }
340
- queueCmd(h, rec.id, {
466
+ queueCmd(h, lane, {
467
+ protocol: lane.protocol,
341
468
  seq,
342
469
  type: "expired"
343
470
  });
344
471
  };
345
- const settleCmd = (h, rec, adm, cmd) => {
472
+ const settleCmd = (h, lane, adm, cmd) => {
346
473
  if (adm.settled) return;
347
474
  adm.settled = true;
348
- rec.inflight.delete(adm.seq);
349
- retain(rec, cmd);
350
- rec.lastSeq = Math.max(rec.lastSeq, adm.seq);
475
+ lane.inflight.delete(adm.seq);
476
+ retain(lane, cmd);
477
+ lane.lastSeq = Math.max(lane.lastSeq, adm.seq);
351
478
  adm.applied = true;
352
- queueCmd(h, rec.id, cmd);
353
- notifyClientRecords(h);
479
+ queueCmd(h, lane, cmd);
480
+ if (lane.conn === null) notifyClientRecords(h);
354
481
  };
355
- const settleWire = (h, rec, adm, outcome) => settleCmd(h, rec, adm, cmdOf(adm, outcome));
482
+ const settleWire = (h, lane, adm, outcome) => settleCmd(h, lane, adm, cmdOf(adm, outcome));
356
483
  const callerOf = (h, rec, conn) => ({
357
484
  clientId: rec.id,
358
485
  auth: conn.auth,
359
- get context() {
360
- return conn.context;
361
- },
362
486
  get isConnected() {
363
487
  if (h.clients.get(rec.id) !== rec) return false;
364
488
  for (const conn of h.conns) if (conn.clientId === rec.id && conn.open) return true;
365
489
  return false;
366
490
  }
367
491
  });
368
- const rejectStatement = (h, rec, adm, message) => settleCmd(h, rec, adm, {
492
+ const rejectStatement = (h, lane, adm, message) => settleCmd(h, lane, adm, {
493
+ protocol: adm.protocol,
369
494
  seq: adm.seq,
370
495
  type: "rejected",
371
496
  code: "invalid-params",
372
497
  message
373
498
  });
374
- const admitCmd = (h, rec, conn, statement) => {
499
+ const admitCmd = (h, lane, conn, statement) => {
500
+ const rec = h.clients.get(conn.clientId);
375
501
  const adm = {
502
+ protocol: statement.protocol,
376
503
  seq: statement.seq,
377
504
  method: statement.method,
378
505
  applied: false,
379
506
  appliedSent: false,
380
507
  settled: false
381
508
  };
382
- rec.inflight.set(statement.seq, adm);
383
- notifyClientRecords(h);
384
- if (statement.protocol !== h.protocol.name || !conn.selection.protocols.some((p) => p.name === statement.protocol)) {
385
- settleCmd(h, rec, adm, {
386
- seq: adm.seq,
387
- type: "rejected",
388
- code: "unsupported-protocol",
389
- message: "command protocol was not negotiated"
390
- });
391
- return;
392
- }
509
+ lane.inflight.set(statement.seq, adm);
510
+ if (lane.conn === null) notifyClientRecords(h);
393
511
  if (!Array.isArray(statement.params)) {
394
- rejectStatement(h, rec, adm, "params must be an array");
512
+ rejectStatement(h, lane, adm, "params must be an array");
395
513
  return;
396
514
  }
397
515
  let params = statement.params;
398
516
  if (statement.meta !== void 0) try {
399
517
  params = reviveDocumentValue(params, statement.meta, { rejectUnknown: true });
400
518
  } catch (error) {
401
- rejectStatement(h, rec, adm, `invalid params: ${error.message}`);
519
+ rejectStatement(h, lane, adm, `invalid params: ${error.message}`);
402
520
  return;
403
521
  }
404
522
  if (statement.method === "statewire/commands") {
405
523
  if (params.length > 0) {
406
- rejectStatement(h, rec, adm, "statewire/commands takes no params");
524
+ rejectStatement(h, lane, adm, "statewire/commands takes no params");
407
525
  return;
408
526
  }
409
527
  const names = [...h.commandsHost?.methods?.() ?? [], "statewire/commands"].sort();
410
- settleCmd(h, rec, adm, {
528
+ settleCmd(h, lane, adm, {
529
+ protocol: adm.protocol,
411
530
  seq: adm.seq,
412
531
  type: "result",
413
532
  payload: names
414
533
  });
415
534
  return;
416
535
  }
536
+ const method = statement.protocol === h.protocol.name ? statement.method : `${statement.protocol}/${statement.method}`;
417
537
  h.commandsHost.submit({
418
- method: statement.method,
538
+ method,
419
539
  params,
420
540
  clientId: rec.id,
421
541
  caller: callerOf(h, rec, conn),
542
+ attach: conn.attach,
422
543
  onApplied: () => {
423
544
  if (adm.settled || adm.applied) return;
424
545
  adm.applied = true;
425
- rec.lastSeq = Math.max(rec.lastSeq, adm.seq);
426
- notifyClientRecords(h);
546
+ lane.lastSeq = Math.max(lane.lastSeq, adm.seq);
547
+ if (lane.conn === null) notifyClientRecords(h);
427
548
  scheduleFlush(h);
428
549
  },
429
- onSettle: (outcome) => settleWire(h, rec, adm, outcome)
550
+ onSettle: (outcome) => settleWire(h, lane, adm, outcome)
430
551
  }).catch((error) => {
431
- if (!adm.settled) settleWire(h, rec, adm, {
552
+ if (!adm.settled) settleWire(h, lane, adm, {
432
553
  type: "rejected",
433
554
  error
434
555
  });
435
556
  });
436
557
  };
558
+ const refusal = (h, conn, retry) => ({
559
+ type: "refused",
560
+ syn: { syn: {
561
+ protocols: conn.selection.protocols.map((p) => ({
562
+ name: p.name,
563
+ seq: watermark(h, conn, p.name)
564
+ })),
565
+ ...retry !== void 0 && { retry }
566
+ } }
567
+ });
437
568
  const admitFrame = (h, conn, frame) => {
438
569
  const rec = h.clients.get(conn.clientId);
439
570
  rec.touched = Date.now();
440
- const through = admittedThrough(rec);
441
- for (const statement of frame.cmd) if (statement.seq <= through) reemit(h, rec, statement.seq);
442
- const ctxNew = frame.ctx.filter((s) => s.seq > conn.ctxSeq);
443
- const cmdNew = frame.cmd.filter((s) => s.seq > through);
444
- if (ctxNew.length > 0 && ctxNew[0].seq !== conn.ctxSeq + 1 || cmdNew.length > 0 && cmdNew[0].seq !== through + 1) {
445
- notifyClientRecords(h);
446
- return {
447
- type: "refused",
448
- syn: { syn: {
449
- seq: through,
450
- ctxSeq: conn.ctxSeq
451
- } }
452
- };
571
+ const lanes = new Map(frame.cmd.map((s) => [s.protocol, laneOf(h, conn, s.protocol)]));
572
+ const through = new Map([...lanes].map(([protocol, lane]) => [protocol, admittedThrough(lane)]));
573
+ const fresh = frame.cmd.filter((s) => s.seq > through.get(s.protocol));
574
+ for (const s of frame.cmd) if (s.seq <= through.get(s.protocol)) reemit(h, lanes.get(s.protocol), s.seq);
575
+ let durableNew = 0;
576
+ for (const [protocol, lane] of lanes) {
577
+ const first = fresh.find((s) => s.protocol === protocol);
578
+ if (first === void 0) continue;
579
+ if (first.seq !== through.get(protocol) + 1) {
580
+ if (lane.conn !== null) return violation("ephemeral lane gap");
581
+ notifyClientRecords(h);
582
+ return refusal(h, conn);
583
+ }
584
+ if (lane.conn === null) durableNew += fresh.filter((s) => s.protocol === protocol).length;
453
585
  }
454
- if (cmdNew.length > 0) {
455
- let hostCommandLoad = 0;
456
- for (const client of h.clients.values()) hostCommandLoad += client.inflight.size;
457
- if (rec.inflight.size + cmdNew.length > 256 || hostCommandLoad + cmdNew.length > h.maxInflightCommands) return {
458
- type: "refused",
459
- syn: { syn: {
460
- seq: through,
461
- ctxSeq: conn.ctxSeq,
462
- retry: COMMAND_CAPACITY_RETRY_MS
463
- } }
464
- };
586
+ if (durableNew > 0) {
587
+ let clientLoad = 0;
588
+ for (const lane of rec.lanes.values()) clientLoad += lane.inflight.size;
589
+ let hostLoad = 0;
590
+ for (const lane of durableLanes(h)) hostLoad += lane.inflight.size;
591
+ if (clientLoad + durableNew > 256 || hostLoad + durableNew > h.maxInflightCommands) return refusal(h, conn, COMMAND_CAPACITY_RETRY_MS);
465
592
  }
466
- if ((ctxNew.length || cmdNew.length) && h.pushback) {
593
+ if (fresh.length > 0 && h.pushback) {
467
594
  const retry = h.pushback();
468
595
  if (retry !== null) {
469
596
  if (!Number.isSafeInteger(retry) || retry < 0) throw new TypeError("pushback must return a nonnegative safe integer or null");
470
- return {
471
- type: "refused",
472
- syn: { syn: {
473
- seq: through,
474
- ctxSeq: conn.ctxSeq,
475
- retry
476
- } }
477
- };
597
+ return refusal(h, conn, retry);
478
598
  }
479
599
  }
480
- let contextDocuments = conn.contextDocuments;
481
- try {
482
- for (const statement of ctxNew) contextDocuments = applyDocumentOps(contextDocuments, statement.ops, conn.selection.protocols, true);
483
- if (frameEncoder.encode(JSON.stringify([...contextDocuments.documents])).byteLength > 65536) return violation("oversized-context");
484
- } catch {
485
- return violation("invalid-context");
486
- }
487
- conn.contextDocuments = contextDocuments;
488
- conn.context = mainDocument(contextDocuments, h.protocol.name)?.value ?? null;
489
- if (ctxNew.length) conn.ctxSeq = ctxNew.at(-1).seq;
490
- for (const statement of cmdNew) admitCmd(h, rec, conn, statement);
491
- if (cmdNew.length > 0) queueCmd(h, rec.id, {
492
- seq: cmdNew.at(-1).seq,
493
- dur: true
494
- });
600
+ for (const s of fresh) admitCmd(h, lanes.get(s.protocol), conn, s);
601
+ for (const [protocol, lane] of lanes) {
602
+ const last = fresh.findLast((s) => s.protocol === protocol);
603
+ if (last !== void 0 && lane.conn === null) queueCmd(h, lane, {
604
+ protocol,
605
+ seq: last.seq,
606
+ dur: true
607
+ });
608
+ }
495
609
  notifyClientRecords(h);
496
610
  scheduleFlush(h);
497
611
  return { type: "ok" };
@@ -508,31 +622,15 @@ const message = (h, conn, data) => {
508
622
  }
509
623
  const first = !conn.sawFrame;
510
624
  conn.sawFrame = true;
511
- let frame;
512
- try {
513
- frame = first ? parseInitialFrame(data).frame : parseFrame(data);
514
- } catch (error) {
515
- const reason = violationReason(error);
516
- if (reason === null) throw error;
517
- sendTo(h, conn, JSON.stringify({ fin: {
518
- reason: "protocol-error",
519
- message: reason
520
- } }));
521
- closeConn(h, conn, 1008, reason);
522
- return;
523
- }
524
- if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
525
625
  let admission;
526
626
  try {
627
+ const frame = first ? parseInitialFrame(data).frame : parseFrame(data);
628
+ if (frame.cmd.length === 0) return;
527
629
  admission = admitFrame(h, conn, frame);
528
630
  } catch (error) {
529
631
  const reason = violationReason(error);
530
632
  if (reason === null) throw error;
531
- sendTo(h, conn, JSON.stringify({ fin: {
532
- reason: "protocol-error",
533
- message: reason
534
- } }));
535
- closeConn(h, conn, 1008, reason);
633
+ violate(h, conn, reason);
536
634
  return;
537
635
  }
538
636
  if (admission.type === "refused") sendTo(h, conn, JSON.stringify(admission.syn));
@@ -692,13 +790,15 @@ const connectAuthorized = (h, clientId, socket, options) => {
692
790
  pending.open = false;
693
791
  h.pendingConns.delete(pending);
694
792
  if (pending.timer !== null) clearTimeout(pending.timer);
695
- const registration = register(h, clientId, socket, true, auth, selection);
793
+ let registration;
794
+ try {
795
+ registration = register(h, clientId, socket, true, auth, selection, frame);
796
+ } catch (error) {
797
+ if (violationReason(error) === null) throw error;
798
+ return;
799
+ }
696
800
  if (registration === null) return;
697
801
  pending.connection = registration.connection;
698
- if (frame.ctx.length || frame.cmd.length) message(h, registration.conn, JSON.stringify({
699
- ...frame.ctx.length ? { ctx: frame.ctx } : {},
700
- ...frame.cmd.length ? { cmd: frame.cmd } : {}
701
- }));
702
802
  for (const data of pending.queued.splice(0)) registration.connection.message(data);
703
803
  }).catch((error) => {
704
804
  if (pending.open) rejectPendingAuthorization(h, pending, error);
@@ -718,20 +818,25 @@ const gcClients = (h) => {
718
818
  const oldest = [...h.clients.values()].sort((a, b) => a.touched - b.touched);
719
819
  for (const rec of oldest) {
720
820
  if (excess <= 0) break;
721
- if (live.has(rec.id) || rec.inflight.size > 0) continue;
821
+ if (live.has(rec.id) || [...rec.lanes.values()].some((lane) => lane.inflight.size > 0)) continue;
722
822
  h.clients.delete(rec.id);
723
823
  changed = true;
724
824
  excess -= 1;
725
825
  }
726
826
  if (changed) notifyClientRecords(h);
727
827
  };
828
+ /**
829
+ * Attach a socket: ephemeral-lane statements of the attach frame are admitted
830
+ * before the snapshot (their documents and answers ride the hello), durable
831
+ * ones after it. A violation in the attach frame closes the attach and throws.
832
+ */
728
833
  const register = (h, clientId, socket, sawFrame = false, auth = void 0, selection = {
729
834
  version: h.wireVersion.version,
730
835
  protocols: [{
731
836
  name: h.protocol.name,
732
837
  version: h.protocol.version
733
838
  }]
734
- }) => {
839
+ }, frame = { cmd: [] }) => {
735
840
  const refuse = (code, reason) => {
736
841
  try {
737
842
  socket.close(code, reason);
@@ -763,10 +868,8 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0, selectio
763
868
  const rec = known ?? (() => {
764
869
  const created = {
765
870
  id: clientId,
766
- lastSeq: 0,
767
871
  touched: Date.now(),
768
- results: /* @__PURE__ */ new Map(),
769
- inflight: /* @__PURE__ */ new Map(),
872
+ lanes: /* @__PURE__ */ new Map(),
770
873
  principal: auth
771
874
  };
772
875
  h.clients.set(clientId, created);
@@ -778,39 +881,71 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0, selectio
778
881
  clientId,
779
882
  socket,
780
883
  open: true,
884
+ ready: false,
781
885
  sawFrame,
782
886
  lastSentAt: 0,
783
887
  lease: null,
784
888
  auth,
785
- ctxSeq: 0,
786
- context: null,
787
- contextDocuments: {
788
- mode: "mounted",
789
- documents: /* @__PURE__ */ new Map()
790
- }
889
+ lanes: /* @__PURE__ */ new Map(),
890
+ documents: /* @__PURE__ */ new Map(),
891
+ included: /* @__PURE__ */ new Set(),
892
+ view: /* @__PURE__ */ new Map(),
893
+ nextDoc: 1,
894
+ pendingApplied: [],
895
+ pendingCmds: [],
896
+ attach: void 0
791
897
  };
898
+ conn.attach = attachOf(h, conn);
792
899
  h.conns.add(conn);
793
900
  gcClients(h);
794
- notifyClientRecords(h);
795
901
  ensureHeartbeat(h);
796
- const { value, meta } = encodeDocument(h.stateHost.snapshot());
797
- const hello = {
798
- ops: [{
902
+ const isEphemeral = (s) => selection.protocols.find((p) => p.name === s.protocol)?.record === "ephemeral";
903
+ const early = frame.cmd.filter(isEphemeral);
904
+ const late = frame.cmd.filter((s) => !isEphemeral(s));
905
+ const admit = (statements) => {
906
+ if (statements.length === 0) return null;
907
+ try {
908
+ return admitFrame(h, conn, { cmd: statements });
909
+ } catch (error) {
910
+ const reason = violationReason(error);
911
+ if (reason === null) throw error;
912
+ violate(h, conn, reason);
913
+ throw error;
914
+ }
915
+ };
916
+ const earlyAdmission = admit(early);
917
+ if (early.length > 0) flush(h);
918
+ notifyClientRecords(h);
919
+ const ops = [];
920
+ if (selects(conn, h.protocol.name)) {
921
+ const { value, meta } = encodeDocument(h.stateHost.snapshot());
922
+ ops.push({
799
923
  op: "mount",
800
924
  protocol: h.protocol.name,
801
925
  main: true,
802
926
  value,
803
927
  ...meta !== void 0 && { meta }
804
- }],
928
+ });
929
+ }
930
+ ops.push(...diffView(h, conn));
931
+ const answers = [...conn.pendingApplied.splice(0), ...conn.pendingCmds.splice(0)];
932
+ const hello = {
933
+ ops,
805
934
  syn: {
806
- ...selection,
807
- seq: admittedThrough(rec),
808
- ctxSeq: 0,
935
+ version: selection.version,
936
+ protocols: selection.protocols.map((p) => ({
937
+ ...p,
938
+ seq: p.record === "ephemeral" ? 0 : watermark(h, conn, p.name)
939
+ })),
809
940
  ...h.idleWire && { idle: true }
810
- }
941
+ },
942
+ ...answers.length > 0 && { cmd: answers }
811
943
  };
812
- if (!selection.protocols.some((p) => p.name === h.protocol.name)) hello.ops = [];
944
+ conn.ready = true;
813
945
  sendTo(h, conn, JSON.stringify(hello));
946
+ if (earlyAdmission?.type === "refused") sendTo(h, conn, JSON.stringify(earlyAdmission.syn));
947
+ const lateAdmission = admit(late);
948
+ if (lateAdmission?.type === "refused") sendTo(h, conn, JSON.stringify(lateAdmission.syn));
814
949
  return {
815
950
  conn,
816
951
  connection: {
@@ -837,6 +972,7 @@ const finish = (h, fin) => {
837
972
  const resume = (h) => {
838
973
  h.disposed = false;
839
974
  h.stateHost?.setOnOp((op) => recordOp(h, op));
975
+ h.stateHost?.setOnDocuments(() => markViews(h));
840
976
  };
841
977
  const suspend = (h) => {
842
978
  h.disposed = true;
@@ -845,16 +981,22 @@ const suspend = (h) => {
845
981
  h.heartbeatTimer = null;
846
982
  }
847
983
  h.stateHost?.setOnOp(null);
984
+ h.stateHost?.setOnDocuments(null);
848
985
  };
849
986
  const restoreClientRecords = (records = []) => new Map(records.map((record) => [record.id, {
850
987
  id: record.id,
851
- lastSeq: record.lastSeq,
852
988
  touched: record.touched,
853
- results: new Map(record.results.map(({ seq, ts, cmd }) => [seq, {
854
- ts,
855
- cmd
989
+ lanes: new Map(Object.entries(record.lanes).map(([protocol, lane]) => [protocol, {
990
+ protocol,
991
+ clientId: record.id,
992
+ conn: null,
993
+ lastSeq: lane.lastSeq,
994
+ results: new Map(lane.results.map(({ seq, ts, cmd }) => [seq, {
995
+ ts,
996
+ cmd
997
+ }])),
998
+ inflight: /* @__PURE__ */ new Map()
856
999
  }])),
857
- inflight: /* @__PURE__ */ new Map(),
858
1000
  principal: record.principal
859
1001
  }]));
860
1002
  const useSocketHost = (transport, options, persistence) => {
@@ -880,6 +1022,7 @@ const useSocketHost = (transport, options, persistence) => {
880
1022
  pendingOps: [],
881
1023
  pendingApplied: /* @__PURE__ */ new Map(),
882
1024
  pendingCmds: /* @__PURE__ */ new Map(),
1025
+ viewsDirty: false,
883
1026
  idleWire: true,
884
1027
  finished: null,
885
1028
  disposed: false,
@@ -902,9 +1045,9 @@ const useSocketHost = (transport, options, persistence) => {
902
1045
  });
903
1046
  h.protocol = options?.protocol ?? {
904
1047
  name: "default",
905
- version: "2026-09-07"
1048
+ version: "2026-09-13"
906
1049
  };
907
- h.wireVersion = options?.wireVersion ?? { version: "2026-09-07" };
1050
+ h.wireVersion = options?.wireVersion ?? { version: "2026-09-13" };
908
1051
  setHeartbeatMs(h, options?.heartbeatIntervalMs ?? 15e3);
909
1052
  h.pushback = options?.pushback;
910
1053
  h.wsInitTimeoutMs = options?.wsInitTimeoutMs ?? 5e3;
@@ -936,9 +1079,10 @@ const useSocketHost = (transport, options, persistence) => {
936
1079
  * Transport-agnostic WS binding over a statewire/host transport:
937
1080
  * feed it sockets (anything with `send`/`close`) and raw text frames, and it
938
1081
  * speaks the Statewire packet protocol — snapshot-first hello with `syn`,
939
- * statement-lane client frames under one admission law, `{cmd}` answer
940
- * arrays fanned out to every attach of a client id, idle edges, and `fin` on
941
- * finish.
1082
+ * per-protocol statement lanes under one admission law, `{cmd}` answer
1083
+ * arrays fanned out to every attach of a client id (durable lanes) or the
1084
+ * admitting attach (ephemeral lanes), per-attach document views, idle edges,
1085
+ * and `fin` on finish.
942
1086
  */
943
1087
  const StatewireSocketHost = (transport, options, persistence) => {
944
1088
  const SocketHostRoot = () => {