statewire 0.18.1 → 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 +296 -401
  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 +9 -19
  23. package/dist/host/http.js.map +1 -1
  24. package/dist/host/socket.d.ts +48 -25
  25. package/dist/host/socket.d.ts.map +1 -1
  26. package/dist/host/socket.js +399 -266
  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 -12
  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 -20
  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 +54 -89
  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 -18
  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 +442 -517
  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 +23 -31
  74. package/src/host/socket.ts +560 -339
  75. package/src/host/synced.ts +60 -0
  76. package/src/host-internal.ts +21 -7
  77. package/src/host.ts +40 -8
  78. package/src/index.ts +1 -0
  79. package/src/statewire.ts +8 -37
  80. package/src/storage.ts +90 -157
  81. package/src/transports/remote.ts +0 -7
  82. package/src/transports/sse.ts +8 -3
  83. package/src/transports/testing.ts +34 -18
  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,221 +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
- ctxSeq: rec.ctxSeq,
305
404
  touched: rec.touched,
306
- context: rec.context,
307
- contextDocuments: [...rec.contextDocuments.documents],
308
- results: [...rec.results].map(([seq, entry]) => ({
309
- seq,
310
- ts: entry.ts,
311
- cmd: entry.cmd
312
- })),
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
+ }])),
313
413
  principal: rec.principal
314
414
  })));
315
415
  };
316
- const retain = (rec, cmd) => {
416
+ const retain = (lane, cmd) => {
317
417
  const now = Date.now();
318
- rec.results.set(cmd.seq, {
418
+ lane.results.set(cmd.seq, {
319
419
  ts: now,
320
420
  cmd
321
421
  });
322
- const newest = Math.max(...rec.results.keys());
323
- for (const [seq, entry] of rec.results) if (seq !== newest && now - entry.ts > RESULT_TTL_MS) rec.results.delete(seq);
324
- while (rec.results.size > RESULT_LIMIT) rec.results.delete(Math.min(...rec.results.keys()));
325
- };
326
- /** Cmd-lane watermark: the last statement admitted, settled or not. */
327
- const admittedThrough = (rec) => Math.max(rec.lastSeq, ...rec.inflight.keys());
328
- const reemit = (h, rec, seq) => {
329
- 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);
330
453
  if (cached) {
331
- if (seq === Math.max(...rec.results.keys()) || Date.now() - cached.ts <= RESULT_TTL_MS) {
332
- 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);
333
456
  return;
334
457
  }
335
- rec.results.delete(seq);
458
+ lane.results.delete(seq);
336
459
  notifyClientRecords(h);
337
460
  }
338
- const adm = rec.inflight.get(seq);
461
+ const adm = lane.inflight.get(seq);
339
462
  if (adm) {
340
- if (adm.applied) queueApplied(h, rec.id, seq);
463
+ if (adm.applied) queueApplied(h, lane, adm);
341
464
  return;
342
465
  }
343
- queueCmd(h, rec.id, {
466
+ queueCmd(h, lane, {
467
+ protocol: lane.protocol,
344
468
  seq,
345
469
  type: "expired"
346
470
  });
347
471
  };
348
- const settleCmd = (h, rec, adm, cmd) => {
472
+ const settleCmd = (h, lane, adm, cmd) => {
349
473
  if (adm.settled) return;
350
474
  adm.settled = true;
351
- rec.inflight.delete(adm.seq);
352
- retain(rec, cmd);
353
- 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);
354
478
  adm.applied = true;
355
- queueCmd(h, rec.id, cmd);
356
- notifyClientRecords(h);
479
+ queueCmd(h, lane, cmd);
480
+ if (lane.conn === null) notifyClientRecords(h);
357
481
  };
358
- const settleWire = (h, rec, adm, outcome) => settleCmd(h, rec, adm, cmdOf(adm, outcome));
359
- const callerOf = (h, rec, auth) => ({
482
+ const settleWire = (h, lane, adm, outcome) => settleCmd(h, lane, adm, cmdOf(adm, outcome));
483
+ const callerOf = (h, rec, conn) => ({
360
484
  clientId: rec.id,
361
- auth,
362
- get context() {
363
- return rec.context;
364
- },
485
+ auth: conn.auth,
365
486
  get isConnected() {
366
487
  if (h.clients.get(rec.id) !== rec) return false;
367
488
  for (const conn of h.conns) if (conn.clientId === rec.id && conn.open) return true;
368
489
  return false;
369
490
  }
370
491
  });
371
- 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,
372
494
  seq: adm.seq,
373
495
  type: "rejected",
374
496
  code: "invalid-params",
375
497
  message
376
498
  });
377
- const admitCmd = (h, rec, statement, auth, selection) => {
499
+ const admitCmd = (h, lane, conn, statement) => {
500
+ const rec = h.clients.get(conn.clientId);
378
501
  const adm = {
502
+ protocol: statement.protocol,
379
503
  seq: statement.seq,
380
504
  method: statement.method,
381
505
  applied: false,
382
506
  appliedSent: false,
383
507
  settled: false
384
508
  };
385
- rec.inflight.set(statement.seq, adm);
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
- }
509
+ lane.inflight.set(statement.seq, adm);
510
+ if (lane.conn === null) notifyClientRecords(h);
396
511
  if (!Array.isArray(statement.params)) {
397
- rejectStatement(h, rec, adm, "params must be an array");
512
+ rejectStatement(h, lane, adm, "params must be an array");
398
513
  return;
399
514
  }
400
515
  let params = statement.params;
401
516
  if (statement.meta !== void 0) try {
402
517
  params = reviveDocumentValue(params, statement.meta, { rejectUnknown: true });
403
518
  } catch (error) {
404
- rejectStatement(h, rec, adm, `invalid params: ${error.message}`);
519
+ rejectStatement(h, lane, adm, `invalid params: ${error.message}`);
405
520
  return;
406
521
  }
407
522
  if (statement.method === "statewire/commands") {
408
523
  if (params.length > 0) {
409
- rejectStatement(h, rec, adm, "statewire/commands takes no params");
524
+ rejectStatement(h, lane, adm, "statewire/commands takes no params");
410
525
  return;
411
526
  }
412
527
  const names = [...h.commandsHost?.methods?.() ?? [], "statewire/commands"].sort();
413
- settleCmd(h, rec, adm, {
528
+ settleCmd(h, lane, adm, {
529
+ protocol: adm.protocol,
414
530
  seq: adm.seq,
415
531
  type: "result",
416
532
  payload: names
417
533
  });
418
534
  return;
419
535
  }
536
+ const method = statement.protocol === h.protocol.name ? statement.method : `${statement.protocol}/${statement.method}`;
420
537
  h.commandsHost.submit({
421
- method: statement.method,
538
+ method,
422
539
  params,
423
540
  clientId: rec.id,
424
- caller: callerOf(h, rec, auth),
541
+ caller: callerOf(h, rec, conn),
542
+ attach: conn.attach,
425
543
  onApplied: () => {
426
544
  if (adm.settled || adm.applied) return;
427
545
  adm.applied = true;
428
- rec.lastSeq = Math.max(rec.lastSeq, adm.seq);
429
- notifyClientRecords(h);
546
+ lane.lastSeq = Math.max(lane.lastSeq, adm.seq);
547
+ if (lane.conn === null) notifyClientRecords(h);
430
548
  scheduleFlush(h);
431
549
  },
432
- onSettle: (outcome) => settleWire(h, rec, adm, outcome)
550
+ onSettle: (outcome) => settleWire(h, lane, adm, outcome)
433
551
  }).catch((error) => {
434
- if (!adm.settled) settleWire(h, rec, adm, {
552
+ if (!adm.settled) settleWire(h, lane, adm, {
435
553
  type: "rejected",
436
554
  error
437
555
  });
438
556
  });
439
557
  };
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
- }) => {
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
+ });
568
+ const admitFrame = (h, conn, frame) => {
569
+ const rec = h.clients.get(conn.clientId);
447
570
  rec.touched = Date.now();
448
- const through = admittedThrough(rec);
449
- for (const statement of frame.cmd) if (statement.seq <= through) reemit(h, rec, statement.seq);
450
- const ctxNew = frame.ctx.filter((s) => s.seq > rec.ctxSeq);
451
- const cmdNew = frame.cmd.filter((s) => s.seq > through);
452
- if (ctxNew.length > 0 && ctxNew[0].seq !== rec.ctxSeq + 1 || cmdNew.length > 0 && cmdNew[0].seq !== through + 1) {
453
- notifyClientRecords(h);
454
- return {
455
- type: "refused",
456
- syn: { syn: {
457
- seq: through,
458
- ctxSeq: rec.ctxSeq
459
- } }
460
- };
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;
461
585
  }
462
- if (cmdNew.length > 0) {
463
- let hostCommandLoad = 0;
464
- for (const client of h.clients.values()) hostCommandLoad += client.inflight.size;
465
- if (rec.inflight.size + cmdNew.length > 256 || hostCommandLoad + cmdNew.length > h.maxInflightCommands) return {
466
- type: "refused",
467
- syn: { syn: {
468
- seq: through,
469
- ctxSeq: rec.ctxSeq,
470
- retry: COMMAND_CAPACITY_RETRY_MS
471
- } }
472
- };
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);
473
592
  }
474
- if ((ctxNew.length || cmdNew.length) && h.pushback) {
593
+ if (fresh.length > 0 && h.pushback) {
475
594
  const retry = h.pushback();
476
595
  if (retry !== null) {
477
596
  if (!Number.isSafeInteger(retry) || retry < 0) throw new TypeError("pushback must return a nonnegative safe integer or null");
478
- return {
479
- type: "refused",
480
- syn: { syn: {
481
- seq: through,
482
- ctxSeq: rec.ctxSeq,
483
- retry
484
- } }
485
- };
597
+ return refusal(h, conn, retry);
486
598
  }
487
599
  }
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");
494
- }
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);
499
- if (cmdNew.length > 0) queueCmd(h, rec.id, {
500
- seq: cmdNew.at(-1).seq,
501
- dur: true
502
- });
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
+ }
503
609
  notifyClientRecords(h);
504
610
  scheduleFlush(h);
505
611
  return { type: "ok" };
@@ -516,32 +622,15 @@ const message = (h, conn, data) => {
516
622
  }
517
623
  const first = !conn.sawFrame;
518
624
  conn.sawFrame = true;
519
- let frame;
520
- try {
521
- frame = first ? parseInitialFrame(data).frame : parseFrame(data);
522
- } catch (error) {
523
- const reason = violationReason(error);
524
- if (reason === null) throw error;
525
- sendTo(h, conn, JSON.stringify({ fin: {
526
- reason: "protocol-error",
527
- message: reason
528
- } }));
529
- closeConn(h, conn, 1008, reason);
530
- return;
531
- }
532
- if (frame.ctx.length === 0 && frame.cmd.length === 0) return;
533
- const rec = h.clients.get(conn.clientId);
534
625
  let admission;
535
626
  try {
536
- admission = admitFrame(h, rec, frame, conn.auth, conn.selection);
627
+ const frame = first ? parseInitialFrame(data).frame : parseFrame(data);
628
+ if (frame.cmd.length === 0) return;
629
+ admission = admitFrame(h, conn, frame);
537
630
  } catch (error) {
538
631
  const reason = violationReason(error);
539
632
  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);
633
+ violate(h, conn, reason);
545
634
  return;
546
635
  }
547
636
  if (admission.type === "refused") sendTo(h, conn, JSON.stringify(admission.syn));
@@ -701,13 +790,15 @@ const connectAuthorized = (h, clientId, socket, options) => {
701
790
  pending.open = false;
702
791
  h.pendingConns.delete(pending);
703
792
  if (pending.timer !== null) clearTimeout(pending.timer);
704
- 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
+ }
705
800
  if (registration === null) return;
706
801
  pending.connection = registration.connection;
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
- }));
711
802
  for (const data of pending.queued.splice(0)) registration.connection.message(data);
712
803
  }).catch((error) => {
713
804
  if (pending.open) rejectPendingAuthorization(h, pending, error);
@@ -727,20 +818,25 @@ const gcClients = (h) => {
727
818
  const oldest = [...h.clients.values()].sort((a, b) => a.touched - b.touched);
728
819
  for (const rec of oldest) {
729
820
  if (excess <= 0) break;
730
- 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;
731
822
  h.clients.delete(rec.id);
732
823
  changed = true;
733
824
  excess -= 1;
734
825
  }
735
826
  if (changed) notifyClientRecords(h);
736
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
+ */
737
833
  const register = (h, clientId, socket, sawFrame = false, auth = void 0, selection = {
738
834
  version: h.wireVersion.version,
739
835
  protocols: [{
740
836
  name: h.protocol.name,
741
837
  version: h.protocol.version
742
838
  }]
743
- }) => {
839
+ }, frame = { cmd: [] }) => {
744
840
  const refuse = (code, reason) => {
745
841
  try {
746
842
  socket.close(code, reason);
@@ -772,16 +868,8 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0, selectio
772
868
  const rec = known ?? (() => {
773
869
  const created = {
774
870
  id: clientId,
775
- lastSeq: 0,
776
- ctxSeq: 0,
777
871
  touched: Date.now(),
778
- context: null,
779
- contextDocuments: {
780
- mode: "mounted",
781
- documents: /* @__PURE__ */ new Map()
782
- },
783
- results: /* @__PURE__ */ new Map(),
784
- inflight: /* @__PURE__ */ new Map(),
872
+ lanes: /* @__PURE__ */ new Map(),
785
873
  principal: auth
786
874
  };
787
875
  h.clients.set(clientId, created);
@@ -793,35 +881,72 @@ const register = (h, clientId, socket, sawFrame = false, auth = void 0, selectio
793
881
  clientId,
794
882
  socket,
795
883
  open: true,
884
+ ready: false,
796
885
  sawFrame,
797
886
  lastSentAt: 0,
798
887
  lease: null,
799
- auth
888
+ auth,
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
800
897
  };
898
+ conn.attach = attachOf(h, conn);
801
899
  h.conns.add(conn);
802
900
  gcClients(h);
803
- notifyClientRecords(h);
804
901
  ensureHeartbeat(h);
805
- const { value, meta } = encodeDocument(h.stateHost.snapshot());
806
- const hello = {
807
- 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({
808
923
  op: "mount",
809
924
  protocol: h.protocol.name,
810
925
  main: true,
811
926
  value,
812
927
  ...meta !== void 0 && { meta }
813
- }],
928
+ });
929
+ }
930
+ ops.push(...diffView(h, conn));
931
+ const answers = [...conn.pendingApplied.splice(0), ...conn.pendingCmds.splice(0)];
932
+ const hello = {
933
+ ops,
814
934
  syn: {
815
- ...selection,
816
- seq: admittedThrough(rec),
817
- ctxSeq: rec.ctxSeq,
935
+ version: selection.version,
936
+ protocols: selection.protocols.map((p) => ({
937
+ ...p,
938
+ seq: p.record === "ephemeral" ? 0 : watermark(h, conn, p.name)
939
+ })),
818
940
  ...h.idleWire && { idle: true }
819
- }
941
+ },
942
+ ...answers.length > 0 && { cmd: answers }
820
943
  };
821
- if (!selection.protocols.some((p) => p.name === h.protocol.name)) hello.ops = [];
944
+ conn.ready = true;
822
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));
823
949
  return {
824
- rec,
825
950
  conn,
826
951
  connection: {
827
952
  clientId,
@@ -847,6 +972,7 @@ const finish = (h, fin) => {
847
972
  const resume = (h) => {
848
973
  h.disposed = false;
849
974
  h.stateHost?.setOnOp((op) => recordOp(h, op));
975
+ h.stateHost?.setOnDocuments(() => markViews(h));
850
976
  };
851
977
  const suspend = (h) => {
852
978
  h.disposed = true;
@@ -855,18 +981,23 @@ const suspend = (h) => {
855
981
  h.heartbeatTimer = null;
856
982
  }
857
983
  h.stateHost?.setOnOp(null);
984
+ h.stateHost?.setOnDocuments(null);
858
985
  };
859
986
  const restoreClientRecords = (records = []) => new Map(records.map((record) => [record.id, {
860
- ...record,
861
- contextDocuments: {
862
- mode: "mounted",
863
- documents: new Map(record.contextDocuments ?? [])
864
- },
865
- results: new Map(record.results.map(({ seq, ts, cmd }) => [seq, {
866
- ts,
867
- cmd
987
+ id: record.id,
988
+ touched: record.touched,
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()
868
999
  }])),
869
- inflight: /* @__PURE__ */ new Map()
1000
+ principal: record.principal
870
1001
  }]));
871
1002
  const useSocketHost = (transport, options, persistence) => {
872
1003
  const [h] = useState(() => {
@@ -891,6 +1022,7 @@ const useSocketHost = (transport, options, persistence) => {
891
1022
  pendingOps: [],
892
1023
  pendingApplied: /* @__PURE__ */ new Map(),
893
1024
  pendingCmds: /* @__PURE__ */ new Map(),
1025
+ viewsDirty: false,
894
1026
  idleWire: true,
895
1027
  finished: null,
896
1028
  disposed: false,
@@ -913,9 +1045,9 @@ const useSocketHost = (transport, options, persistence) => {
913
1045
  });
914
1046
  h.protocol = options?.protocol ?? {
915
1047
  name: "default",
916
- version: "2026-09-07"
1048
+ version: "2026-09-13"
917
1049
  };
918
- h.wireVersion = options?.wireVersion ?? { version: "2026-09-07" };
1050
+ h.wireVersion = options?.wireVersion ?? { version: "2026-09-13" };
919
1051
  setHeartbeatMs(h, options?.heartbeatIntervalMs ?? 15e3);
920
1052
  h.pushback = options?.pushback;
921
1053
  h.wsInitTimeoutMs = options?.wsInitTimeoutMs ?? 5e3;
@@ -947,9 +1079,10 @@ const useSocketHost = (transport, options, persistence) => {
947
1079
  * Transport-agnostic WS binding over a statewire/host transport:
948
1080
  * feed it sockets (anything with `send`/`close`) and raw text frames, and it
949
1081
  * speaks the Statewire packet protocol — snapshot-first hello with `syn`,
950
- * statement-lane client frames under one admission law, `{cmd}` answer
951
- * arrays fanned out to every attach of a client id, idle edges, and `fin` on
952
- * 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.
953
1086
  */
954
1087
  const StatewireSocketHost = (transport, options, persistence) => {
955
1088
  const SocketHostRoot = () => {