statewire 0.20.0 → 0.22.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 (143) hide show
  1. package/dist/client/changes.d.ts +3 -6
  2. package/dist/client/changes.d.ts.map +1 -1
  3. package/dist/client/changes.js +1 -2
  4. package/dist/client/connection.d.ts +80 -83
  5. package/dist/client/connection.d.ts.map +1 -1
  6. package/dist/client/connection.js +20 -7
  7. package/dist/client/connection.js.map +1 -1
  8. package/dist/client/core.d.ts +39 -41
  9. package/dist/client/core.d.ts.map +1 -1
  10. package/dist/client/core.js +2 -3
  11. package/dist/client/session.d.ts +93 -93
  12. package/dist/client/session.d.ts.map +1 -1
  13. package/dist/client/session.js +47 -23
  14. package/dist/client/session.js.map +1 -1
  15. package/dist/client/sessionStore.d.ts +11 -14
  16. package/dist/client/sessionStore.d.ts.map +1 -1
  17. package/dist/client/sessionStore.js +1 -2
  18. package/dist/client/storageError.d.ts +2 -5
  19. package/dist/client/storageError.d.ts.map +1 -1
  20. package/dist/client/storageError.js +1 -2
  21. package/dist/client/timing.d.ts +11 -14
  22. package/dist/client/timing.d.ts.map +1 -1
  23. package/dist/client/timing.js +1 -2
  24. package/dist/document.d.ts +17 -20
  25. package/dist/document.d.ts.map +1 -1
  26. package/dist/document.js +1 -2
  27. package/dist/host/adapter.d.ts +4 -7
  28. package/dist/host/adapter.d.ts.map +1 -1
  29. package/dist/host/adapter.js +1 -2
  30. package/dist/host/draft.d.ts +13 -16
  31. package/dist/host/draft.d.ts.map +1 -1
  32. package/dist/host/draft.js +1 -2
  33. package/dist/host/engine.d.ts +7 -10
  34. package/dist/host/engine.d.ts.map +1 -1
  35. package/dist/host/engine.js +1 -2
  36. package/dist/host/host.d.ts +42 -45
  37. package/dist/host/host.d.ts.map +1 -1
  38. package/dist/host/host.js +15 -13
  39. package/dist/host/host.js.map +1 -1
  40. package/dist/host/http.d.ts +4 -7
  41. package/dist/host/http.d.ts.map +1 -1
  42. package/dist/host/http.js +15 -11
  43. package/dist/host/http.js.map +1 -1
  44. package/dist/host/socket.d.ts +145 -140
  45. package/dist/host/socket.d.ts.map +1 -1
  46. package/dist/host/socket.js +99 -17
  47. package/dist/host/socket.js.map +1 -1
  48. package/dist/host/synced.d.ts +3 -6
  49. package/dist/host/synced.d.ts.map +1 -1
  50. package/dist/host/synced.js +1 -2
  51. package/dist/host/work.d.ts +2 -5
  52. package/dist/host/work.d.ts.map +1 -1
  53. package/dist/host/work.js +1 -2
  54. package/dist/host-internal.d.ts +99 -90
  55. package/dist/host-internal.d.ts.map +1 -1
  56. package/dist/host.d.ts +90 -91
  57. package/dist/host.d.ts.map +1 -1
  58. package/dist/index.d.ts +11 -8
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +1 -1
  61. package/dist/statewire.d.ts +265 -268
  62. package/dist/statewire.d.ts.map +1 -1
  63. package/dist/statewire.js +7 -7
  64. package/dist/statewire.js.map +1 -1
  65. package/dist/storage.d.ts +23 -26
  66. package/dist/storage.d.ts.map +1 -1
  67. package/dist/storage.js +1 -2
  68. package/dist/transports/fake-sse.d.ts +7 -10
  69. package/dist/transports/fake-sse.d.ts.map +1 -1
  70. package/dist/transports/fake-sse.js +1 -2
  71. package/dist/transports/fixtures.d.ts +19 -25
  72. package/dist/transports/fixtures.d.ts.map +1 -1
  73. package/dist/transports/fixtures.js +1 -2
  74. package/dist/transports/remote.d.ts +8 -11
  75. package/dist/transports/remote.d.ts.map +1 -1
  76. package/dist/transports/remote.js +1 -2
  77. package/dist/transports/sse-parser.d.ts +1 -4
  78. package/dist/transports/sse-parser.d.ts.map +1 -1
  79. package/dist/transports/sse-parser.js +1 -2
  80. package/dist/transports/sse.d.ts +50 -52
  81. package/dist/transports/sse.d.ts.map +1 -1
  82. package/dist/transports/sse.js +9 -3
  83. package/dist/transports/sse.js.map +1 -1
  84. package/dist/transports/testing.d.ts +31 -34
  85. package/dist/transports/testing.d.ts.map +1 -1
  86. package/dist/transports/testing.js +1 -2
  87. package/dist/transports/ws.d.ts +5 -7
  88. package/dist/transports/ws.d.ts.map +1 -1
  89. package/dist/transports/ws.js +3 -4
  90. package/dist/transports/ws.js.map +1 -1
  91. package/dist/types.d.ts +81 -78
  92. package/dist/types.d.ts.map +1 -1
  93. package/dist/types.js +1 -2
  94. package/dist/util/draftTraps.d.ts +2 -4
  95. package/dist/util/draftTraps.d.ts.map +1 -1
  96. package/dist/util/draftTraps.js +1 -2
  97. package/dist/util/macrotask.d.ts +1 -4
  98. package/dist/util/macrotask.d.ts.map +1 -1
  99. package/dist/util/macrotask.js +1 -2
  100. package/dist/util/methodProxy.d.ts +1 -4
  101. package/dist/util/methodProxy.d.ts.map +1 -1
  102. package/dist/util/methodProxy.js +1 -2
  103. package/dist/util/randomId.d.ts +1 -4
  104. package/dist/util/randomId.d.ts.map +1 -1
  105. package/dist/util/randomId.js +1 -2
  106. package/dist/util/recordMutations.d.ts +5 -7
  107. package/dist/util/recordMutations.d.ts.map +1 -1
  108. package/dist/util/recordMutations.js +1 -2
  109. package/dist/util/statePath.d.ts +9 -12
  110. package/dist/util/statePath.d.ts.map +1 -1
  111. package/dist/util/statePath.js +1 -2
  112. package/dist/wire/context.d.ts +7 -12
  113. package/dist/wire/context.d.ts.map +1 -1
  114. package/dist/wire/context.js +2 -3
  115. package/dist/wire/documents.d.ts +36 -39
  116. package/dist/wire/documents.d.ts.map +1 -1
  117. package/dist/wire/documents.js +2 -3
  118. package/dist/wire/documents.js.map +1 -1
  119. package/dist/wire/limits.d.ts +7 -10
  120. package/dist/wire/limits.d.ts.map +1 -1
  121. package/dist/wire/limits.js +1 -2
  122. package/dist/wire/meta.d.ts +9 -11
  123. package/dist/wire/meta.d.ts.map +1 -1
  124. package/dist/wire/meta.js +1 -2
  125. package/dist/wire/meta.js.map +1 -1
  126. package/dist/wire/ops.d.ts +9 -12
  127. package/dist/wire/ops.d.ts.map +1 -1
  128. package/dist/wire/ops.js +1 -2
  129. package/dist/wire/protocol.d.ts +53 -56
  130. package/dist/wire/protocol.d.ts.map +1 -1
  131. package/dist/wire/protocol.js +1 -2
  132. package/dist/wire/version.d.ts +1 -4
  133. package/dist/wire/version.d.ts.map +1 -1
  134. package/dist/wire/version.js +1 -2
  135. package/package.json +6 -6
  136. package/src/client/connection.ts +25 -15
  137. package/src/client/session.ts +54 -15
  138. package/src/host/host.ts +9 -3
  139. package/src/host/http.ts +37 -5
  140. package/src/host/socket.ts +158 -9
  141. package/src/host-internal.ts +14 -0
  142. package/src/statewire.ts +11 -6
  143. package/src/transports/sse.ts +12 -0
@@ -1,4 +1,4 @@
1
- //#region src/wire/protocol.ts
1
+ // src/wire/protocol.ts
2
2
  const WIRE_VERSION = "2026-09-30";
3
3
  const DEFAULT_PROTOCOL = "default";
4
4
  const PROTOCOL_HEADER = "Statewire-Protocol";
@@ -236,7 +236,6 @@ const negotiateProtocols = (headers, wire, definitions) => {
236
236
  })
237
237
  };
238
238
  };
239
- //#endregion
240
239
  export { DEFAULT_PROTOCOL, PROTOCOL_HEADER, VERSION_HEADER, WIRE_VERSION, inRange, negotiateProtocols, negotiationHeaders, parseNegotiation, protocolOffers, protocolToken, validateRange, validateSelection, validateVersion };
241
240
 
242
241
  //# sourceMappingURL=protocol.js.map
@@ -1,5 +1,2 @@
1
- //#region src/wire/version.d.ts
2
- declare const WS_SUBPROTOCOL = "statewire.v1";
3
- //#endregion
4
- export { WS_SUBPROTOCOL };
1
+ export declare const WS_SUBPROTOCOL = "statewire.v1";
5
2
  //# sourceMappingURL=version.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"version.d.ts","names":[],"sources":["../../src/wire/version.ts"],"mappings":";cAAa"}
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/wire/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,cAAc,iBAAiB,CAAC"}
@@ -1,6 +1,5 @@
1
- //#region src/wire/version.ts
1
+ // src/wire/version.ts
2
2
  const WS_SUBPROTOCOL = "statewire.v1";
3
- //#endregion
4
3
  export { WS_SUBPROTOCOL };
5
4
 
6
5
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "statewire",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "State over the wire: snapshot-first delta streaming and command submission for any state and any command",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -47,16 +47,16 @@
47
47
  "test": "vitest run"
48
48
  },
49
49
  "dependencies": {
50
- "@assistant-ui/tap": "^0.9.6"
50
+ "@assistant-ui/tap": "^0.9.24"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "react": "^18 || ^19"
54
54
  },
55
55
  "devDependencies": {
56
- "@assistant-ui/x-buildutils": "^0.0.20",
57
- "@types/react": "^19.2.17",
56
+ "@assistant-ui/x-buildutils": "^0.0.31",
57
+ "@types/react": "^19.3.0",
58
58
  "babel-plugin-react-compiler": "^1.0.0",
59
- "react": "^19.2.8",
60
- "vitest": "^4.1.10"
59
+ "react": "^19.3.1",
60
+ "vitest": "^5.0.5"
61
61
  }
62
62
  }
@@ -167,13 +167,21 @@ const setStatus = (c: Conn, next: ConnectionState) => {
167
167
  c.onChange(c.connection);
168
168
  };
169
169
 
170
- const storageFailed = (c: Conn, error: Error) => {
170
+ const stopForError = ({
171
+ connection: c,
172
+ error,
173
+ reason,
174
+ }: {
175
+ connection: Conn;
176
+ error: Error;
177
+ reason: "storage-error" | "protocol-error";
178
+ }) => {
171
179
  c.error = error;
172
180
  c.onError(error);
173
181
  flushTapSync(() =>
174
182
  setStatus(c, {
175
183
  status: "stopped",
176
- reason: "storage-error",
184
+ reason,
177
185
  degraded: true,
178
186
  message: error.message,
179
187
  }),
@@ -481,6 +489,7 @@ export const handleEnvelope = (
481
489
  return null;
482
490
  }
483
491
  deliverEnvelope(c, env);
492
+ if (c.connection.status === "stopped") return "stop";
484
493
  if (env.fin !== undefined) return settleFin(c, env.fin);
485
494
  if (c.connection.status !== "connected") {
486
495
  c.attempt = 0;
@@ -568,7 +577,7 @@ const run = async (c: Conn, lifetime: AbortController, gate: Gate) => {
568
577
  } catch (error) {
569
578
  if (!abort.signal.aborted) {
570
579
  if (isStorageError(error)) {
571
- storageFailed(c, error);
580
+ stopForError({ connection: c, error, reason: "storage-error" });
572
581
  outcome = "stop";
573
582
  } else if (
574
583
  error instanceof SyntaxError ||
@@ -709,18 +718,19 @@ export const useLink = (
709
718
  };
710
719
  return conn;
711
720
  });
712
- const [link] = useState(
713
- (): RemoteLink => ({
714
- get connection() {
715
- return c.connection;
716
- },
717
- get parked() {
718
- return c.parked;
719
- },
720
- send: (frame) => latest.current.send(c, frame),
721
- storageFailed: (error) => storageFailed(c, error),
722
- }),
723
- );
721
+ const [link] = useState((): RemoteLink => ({
722
+ get connection() {
723
+ return c.connection;
724
+ },
725
+ get parked() {
726
+ return c.parked;
727
+ },
728
+ send: (frame) => latest.current.send(c, frame),
729
+ storageFailed: (error) =>
730
+ stopForError({ connection: c, error, reason: "storage-error" }),
731
+ protocolFailed: (error) =>
732
+ stopForError({ connection: c, error, reason: "protocol-error" }),
733
+ }));
724
734
 
725
735
  const negotiation = JSON.stringify([
726
736
  props.protocol,
@@ -85,6 +85,7 @@ export type StatewireSessionOptions = StatewireProtocol.Options & {
85
85
 
86
86
  type Shared = {
87
87
  txq: Promise<void>;
88
+ syncQueued: boolean;
88
89
  parked: { protocol: string; verdict: StatewireClient.SessionVerdict }[];
89
90
  unsubscribe: (() => void) | null;
90
91
  };
@@ -204,7 +205,6 @@ const storageFailed = (s: Session, cause: unknown) => {
204
205
  };
205
206
 
206
207
  const setRecord = (s: Session, record: StatewireClient.SessionRecord) => {
207
- s.record = record;
208
208
  if (!s.deactivated) {
209
209
  try {
210
210
  s.store.save(record);
@@ -213,6 +213,7 @@ const setRecord = (s: Session, record: StatewireClient.SessionRecord) => {
213
213
  throw s.storageError;
214
214
  }
215
215
  }
216
+ s.record = record;
216
217
  };
217
218
 
218
219
  const toStored = (p: Pending): StatewireClient.SessionCommand => {
@@ -288,8 +289,16 @@ const syncBacklogInner = (s: Session) => {
288
289
  };
289
290
 
290
291
  const syncBacklog = (s: Session) => {
291
- if (!s.shared) return syncBacklogInner(s);
292
- transactBackground(s, () => syncBacklogInner(s));
292
+ const shared = s.shared;
293
+ if (!shared) return syncBacklogInner(s);
294
+ if (shared.syncQueued) return;
295
+ shared.syncQueued = true;
296
+ void transact(s, () => {
297
+ shared.syncQueued = false;
298
+ syncBacklogInner(s);
299
+ }).catch(() => {
300
+ shared.syncQueued = false;
301
+ });
293
302
  };
294
303
 
295
304
  // Every record mutation over shared storage is a locked read-modify-write.
@@ -305,7 +314,7 @@ const transact = (s: Session, fn: () => void): Promise<void> => {
305
314
  if (stored !== null && stored.clientId !== s.record.clientId) {
306
315
  adoptReset(s, stored);
307
316
  } else if (stored !== null) {
308
- s.record = stored;
317
+ s.record = boundRecord(stored);
309
318
  }
310
319
  fn();
311
320
  const persistence = s.store.flush?.();
@@ -357,6 +366,25 @@ export const failedCommands = (
357
366
  return s.failedList;
358
367
  };
359
368
 
369
+ export const MAX_FAILED_RETAINED = 256;
370
+
371
+ const boundFailed = (failed: readonly StatewireClient.FailedCommand[]) =>
372
+ failed.length > MAX_FAILED_RETAINED
373
+ ? [...failed].sort((a, b) => a.ts - b.ts).slice(-MAX_FAILED_RETAINED)
374
+ : failed;
375
+
376
+ const boundRecord = (record: StatewireClient.SessionRecord) => {
377
+ let lanes = record.lanes;
378
+ for (const [protocol, lane] of Object.entries(lanes)) {
379
+ if (lane.failed !== undefined && lane.failed.length > MAX_FAILED_RETAINED)
380
+ lanes = {
381
+ ...lanes,
382
+ [protocol]: { ...lane, failed: boundFailed(lane.failed) },
383
+ };
384
+ }
385
+ return lanes === record.lanes ? record : { ...record, lanes };
386
+ };
387
+
360
388
  const retainFailedInner = (
361
389
  s: Session,
362
390
  entry: Pending,
@@ -367,7 +395,7 @@ const retainFailedInner = (
367
395
  s,
368
396
  withLane(s.record, entry.command.protocol, {
369
397
  ...lane,
370
- failed: [
398
+ failed: boundFailed([
371
399
  ...(lane.failed ?? []),
372
400
  {
373
401
  protocol: entry.command.protocol,
@@ -381,7 +409,7 @@ const retainFailedInner = (
381
409
  ts: entry.inv.ts,
382
410
  seq: entry.seq,
383
411
  },
384
- ],
412
+ ]),
385
413
  }),
386
414
  );
387
415
  s.notify();
@@ -497,7 +525,7 @@ const rebaseRecord = (s: Session, adopt: () => void) => {
497
525
  ...lanes,
498
526
  [protocol]: {
499
527
  ...current,
500
- failed: [...(current.failed ?? []), ...failed],
528
+ failed: boundFailed([...(current.failed ?? []), ...failed]),
501
529
  },
502
530
  };
503
531
  }
@@ -515,7 +543,7 @@ const resetRecord = (s: Session, clientId: string) =>
515
543
  // attempted history and our survivors re-stamp on the stored clocks.
516
544
  const adoptReset = (s: Session, stored: StatewireClient.SessionRecord) => {
517
545
  rebaseRecord(s, () => {
518
- s.record = stored;
546
+ s.record = boundRecord(stored);
519
547
  });
520
548
  // The host attributes frames by lease, and the lease is bound to the id
521
549
  // this attach was opened with.
@@ -825,7 +853,7 @@ const handleOps = (
825
853
  ) => {
826
854
  ingestOps(s, ops);
827
855
  for (const cmd of cmds) handleCmd(s, cmd);
828
- syncBacklog(s);
856
+ if (cmds.length > 0) syncBacklog(s);
829
857
  publishIngested(s);
830
858
  };
831
859
 
@@ -1018,7 +1046,7 @@ const pump = async (s: Session) => {
1018
1046
  // Shared storage: the lock across the send keeps per-lane arrival
1019
1047
  // ordered across sharers during attach handover.
1020
1048
  const send = () => transport.send(lanes.frame);
1021
- result = await (s.shared && !s.deactivated
1049
+ result = await (s.shared && !s.deactivated && lanes.entries.length > 0
1022
1050
  ? s.store.lock!(send)
1023
1051
  : send());
1024
1052
  } catch {
@@ -1224,7 +1252,10 @@ const adoptStored = (s: Session) => {
1224
1252
  const params =
1225
1253
  command.meta === undefined
1226
1254
  ? command.params
1227
- : (reviveValue([...command.params], command.meta) as unknown[]);
1255
+ : (reviveValue(
1256
+ structuredClone(command.params),
1257
+ command.meta,
1258
+ ) as unknown[]);
1228
1259
  const entry: Pending = {
1229
1260
  seq,
1230
1261
  command: { protocol, ...command },
@@ -1364,7 +1395,7 @@ export const retryFailed = (
1364
1395
  const params =
1365
1396
  found.meta === undefined
1366
1397
  ? found.params
1367
- : (reviveValue([...found.params], found.meta) as unknown[]);
1398
+ : (reviveValue(structuredClone(found.params), found.meta) as unknown[]);
1368
1399
  const result = new Promise<unknown>((resolve, reject) => {
1369
1400
  const retry = () => {
1370
1401
  if (!s.running) {
@@ -1510,8 +1541,11 @@ const loadSessionRecord = (
1510
1541
  store.save(stored);
1511
1542
  }
1512
1543
  }
1513
- const record = stored ?? { clientId: newClientId(), lanes: {} };
1514
- if (stored === null) store.save(record);
1544
+ const record =
1545
+ stored === null
1546
+ ? { clientId: newClientId(), lanes: {} }
1547
+ : boundRecord(stored);
1548
+ if (record !== stored) store.save(record);
1515
1549
  return record;
1516
1550
  };
1517
1551
 
@@ -1590,7 +1624,12 @@ export const useStatewireSession = (
1590
1624
  documentStore,
1591
1625
  shared:
1592
1626
  store.lock !== undefined
1593
- ? { txq: Promise.resolve(), parked: [], unsubscribe: null }
1627
+ ? {
1628
+ txq: Promise.resolve(),
1629
+ syncQueued: false,
1630
+ parked: [],
1631
+ unsubscribe: null,
1632
+ }
1594
1633
  : false,
1595
1634
  ephemeral: false,
1596
1635
  deactivated: false,
package/src/host/host.ts CHANGED
@@ -231,8 +231,13 @@ const LOCAL_CALLER: StatewireHost.Caller = Object.freeze({
231
231
  isConnected: true,
232
232
  });
233
233
 
234
- const toSendError = (error: unknown): StatewireSendError =>
235
- error instanceof StatewireSendError
234
+ const toSendError = (error: unknown): StatewireSendError => {
235
+ if (
236
+ !(error instanceof StatewireSendError) &&
237
+ !(error instanceof StatewireReject)
238
+ )
239
+ console.error("statewire command failed", error);
240
+ return error instanceof StatewireSendError
236
241
  ? error
237
242
  : error instanceof StatewireReject
238
243
  ? new StatewireSendError({
@@ -245,8 +250,9 @@ const toSendError = (error: unknown): StatewireSendError =>
245
250
  : new StatewireSendError({
246
251
  fate: "failed",
247
252
  cause: "server",
248
- message: error instanceof Error ? error.message : String(error),
253
+ message: "the command failed",
249
254
  });
255
+ };
250
256
 
251
257
  const applyInvocation = (cell: CommandsCell, inv: ActiveInvocation) => {
252
258
  if (inv.applied) return;
package/src/host/http.ts CHANGED
@@ -8,6 +8,7 @@ import {
8
8
  authEquals,
9
9
  closeConn,
10
10
  flush,
11
+ expireLease,
11
12
  parseFrame,
12
13
  parseFrameValue,
13
14
  register,
@@ -94,9 +95,10 @@ const headersOf = (request: Request): Record<string, string> =>
94
95
  const authorize = async (
95
96
  h: SocketHostState,
96
97
  headers: Record<string, string>,
97
- ): Promise<{ auth: unknown } | { refused: Response }> => {
98
+ ): Promise<{ auth: unknown; authorizedAt: number } | { refused: Response }> => {
98
99
  try {
99
- return { auth: await h.authorize?.({ headers }) };
100
+ const auth = await h.authorize?.({ headers });
101
+ return { auth, authorizedAt: Date.now() };
100
102
  } catch (error) {
101
103
  if (error instanceof Response) return { refused: error };
102
104
  throw error;
@@ -140,6 +142,7 @@ const attachSse = (
140
142
  h: SocketHostState,
141
143
  clientId: string,
142
144
  auth: unknown,
145
+ authorizedAt: number,
143
146
  selection: StatewireProtocol.Selection,
144
147
  frame?: ParsedFrame,
145
148
  ): SseAttachment => {
@@ -203,6 +206,7 @@ const attachSse = (
203
206
  auth,
204
207
  selection,
205
208
  frame,
209
+ authorizedAt,
206
210
  );
207
211
  connection = registration?.connection ?? null;
208
212
  if (!open) connection?.close();
@@ -249,7 +253,13 @@ const getStream = async (
249
253
  ]);
250
254
  if ("fin" in selection)
251
255
  return Response.json(selection, { status: selection.fin.status });
252
- const attachment = attachSse(h, client.clientId, authorized.auth, selection);
256
+ const attachment = attachSse(
257
+ h,
258
+ client.clientId,
259
+ authorized.auth,
260
+ authorized.authorizedAt,
261
+ selection,
262
+ );
253
263
  const lease = attachment.registration === null ? null : mintLease();
254
264
  if (attachment.registration !== null)
255
265
  attachment.registration.conn.lease = lease;
@@ -301,6 +311,7 @@ const postStream = async (
301
311
  h,
302
312
  client.clientId,
303
313
  authorized.auth,
314
+ authorized.authorizedAt,
304
315
  selection,
305
316
  frame,
306
317
  );
@@ -365,11 +376,24 @@ export const frames = async (
365
376
  lease === null
366
377
  ? undefined
367
378
  : [...h.conns].find((conn) => conn.open && conn.lease === lease);
368
- if (h.disposed || h.finished !== null || leased === undefined) {
379
+ if (
380
+ h.disposed ||
381
+ h.finished !== null ||
382
+ leased === undefined ||
383
+ expireLease(h, leased)
384
+ ) {
369
385
  return Response.json({ error: "stale-lease" }, { status: 423 });
370
386
  }
371
387
  const clientId = leased.clientId;
372
388
  const raw = await readFrameBody(request);
389
+ if (
390
+ !leased.open ||
391
+ h.disposed ||
392
+ h.finished !== null ||
393
+ expireLease(h, leased)
394
+ ) {
395
+ return Response.json({ error: "stale-lease" }, { status: 423 });
396
+ }
373
397
  if (raw === null) {
374
398
  return Response.json({ error: "frame body too large" }, { status: 413 });
375
399
  }
@@ -382,7 +406,12 @@ export const frames = async (
382
406
  }
383
407
  return violationResponse(h, clientId, error.message);
384
408
  }
385
- if (!leased.open || h.disposed || h.finished !== null) {
409
+ if (
410
+ !leased.open ||
411
+ h.disposed ||
412
+ h.finished !== null ||
413
+ expireLease(h, leased)
414
+ ) {
386
415
  return Response.json({ error: "stale-lease" }, { status: 423 });
387
416
  }
388
417
  let admission: ReturnType<typeof admitFrame>;
@@ -393,6 +422,9 @@ export const frames = async (
393
422
  throw error;
394
423
  return violationResponse(h, clientId, error.message);
395
424
  }
425
+ if (admission.type === "expired") {
426
+ return Response.json({ error: "stale-lease" }, { status: 423 });
427
+ }
396
428
  if (admission.type === "refused") {
397
429
  const retry = admission.syn.syn.retry;
398
430
  return Response.json(admission.syn, {