experimental-a2 0.6.0 → 0.8.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 (148) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/ai-server.d.ts +4 -5
  3. package/dist/ai-server.d.ts.map +1 -1
  4. package/dist/ai-server.js +20 -17
  5. package/dist/ai-server.js.map +1 -1
  6. package/dist/ai.d.ts +334 -2
  7. package/dist/ai.d.ts.map +1 -0
  8. package/dist/ai.js +1 -1
  9. package/dist/client.d.ts +202 -2
  10. package/dist/client.d.ts.map +1 -0
  11. package/dist/client.js +1025 -1
  12. package/dist/client.js.map +1 -0
  13. package/dist/errors-BQuJpe82.js.map +1 -1
  14. package/dist/index.d.ts +22 -3
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/{internal-DstsI6Re.js → internal-DRXJ56EI.js} +5 -28
  17. package/dist/internal-DRXJ56EI.js.map +1 -0
  18. package/dist/react.d.ts +1 -1
  19. package/dist/react.js +1 -1
  20. package/dist/scheduler-qstash.d.ts +3 -3
  21. package/dist/scheduler-qstash.js +4 -5
  22. package/dist/scheduler-qstash.js.map +1 -1
  23. package/dist/scheduler-vercel.d.ts +2 -2
  24. package/dist/scheduler-vercel.js +4 -4
  25. package/dist/scheduler-vercel.js.map +1 -1
  26. package/dist/{server-Duw6MVlB.js → server-B2XNevQA.js} +830 -131
  27. package/dist/server-B2XNevQA.js.map +1 -0
  28. package/dist/{server-DpvjhdoE.d.ts → server-DjPhHnbI.d.ts} +71 -50
  29. package/dist/server-DjPhHnbI.d.ts.map +1 -0
  30. package/dist/server.d.ts +3 -3
  31. package/dist/server.js +1 -1
  32. package/dist/store-N8PXxDAS.js.map +1 -1
  33. package/dist/{store-DysUkTH3.d.ts → store-RJO35BMj.d.ts} +24 -62
  34. package/dist/store-RJO35BMj.d.ts.map +1 -0
  35. package/dist/store-memory.d.ts +1 -1
  36. package/dist/store-memory.d.ts.map +1 -1
  37. package/dist/store-memory.js +80 -78
  38. package/dist/store-memory.js.map +1 -1
  39. package/dist/{store-polling-dSeLxzfb.js → store-polling-6DW7F1DT.js} +2 -2
  40. package/dist/{store-polling-dSeLxzfb.js.map → store-polling-6DW7F1DT.js.map} +1 -1
  41. package/dist/store-postgres.d.ts +1 -1
  42. package/dist/store-postgres.d.ts.map +1 -1
  43. package/dist/store-postgres.js +231 -182
  44. package/dist/store-postgres.js.map +1 -1
  45. package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DT01r4GZ.js} +213 -161
  46. package/dist/store-redis-core-DT01r4GZ.js.map +1 -0
  47. package/dist/store-redis-http.d.ts +1 -1
  48. package/dist/store-redis-http.js +3 -4
  49. package/dist/store-redis-http.js.map +1 -1
  50. package/dist/store-redis.d.ts +1 -1
  51. package/dist/store-redis.js +4 -5
  52. package/dist/store-redis.js.map +1 -1
  53. package/dist/store-sqlite.d.ts +1 -1
  54. package/dist/store-sqlite.d.ts.map +1 -1
  55. package/dist/store-sqlite.js +104 -91
  56. package/dist/store-sqlite.js.map +1 -1
  57. package/dist/{wire-BFQmSJ-9.js → wire-B6te_wns.js} +4 -3
  58. package/dist/wire-B6te_wns.js.map +1 -0
  59. package/docs/concepts/02-handlers.mdx +4 -0
  60. package/docs/concepts/04-state.mdx +57 -9
  61. package/docs/guides/03-react.mdx +20 -28
  62. package/docs/guides/05-production.mdx +9 -11
  63. package/docs/guides/06-ai-agents.mdx +10 -15
  64. package/docs/guides/09-presence.mdx +14 -19
  65. package/docs/guides/10-transports.mdx +104 -86
  66. package/docs/reference/01-api.mdx +182 -293
  67. package/docs/reference/02-errors.mdx +5 -7
  68. package/package.json +1 -14
  69. package/src/ai-server.ts +36 -15
  70. package/src/client.ts +2 -2
  71. package/src/errors.ts +1 -0
  72. package/src/internal.ts +3 -62
  73. package/src/push-envelope.ts +24 -21
  74. package/src/scheduler-qstash.ts +3 -3
  75. package/src/scheduler-vercel.ts +2 -2
  76. package/src/server-fetch.ts +344 -0
  77. package/src/server.ts +315 -312
  78. package/src/session-socket.ts +36 -20
  79. package/src/sse.ts +2 -2
  80. package/src/store-memory.ts +138 -101
  81. package/src/store-postgres.ts +355 -238
  82. package/src/store-redis-core.ts +247 -237
  83. package/src/store-redis-http.ts +1 -2
  84. package/src/store-redis.ts +1 -2
  85. package/src/store-sqlite.ts +191 -153
  86. package/src/store.ts +24 -66
  87. package/src/wire.ts +2 -1
  88. package/dist/ai-D_PGS-JR.d.ts +0 -334
  89. package/dist/ai-D_PGS-JR.d.ts.map +0 -1
  90. package/dist/cli-B3VuxoDe.js +0 -599
  91. package/dist/cli-B3VuxoDe.js.map +0 -1
  92. package/dist/cli-bin.d.ts +0 -1
  93. package/dist/cli-bin.js +0 -7
  94. package/dist/cli-bin.js.map +0 -1
  95. package/dist/cli.d.ts +0 -20
  96. package/dist/cli.d.ts.map +0 -1
  97. package/dist/cli.js +0 -2
  98. package/dist/client-CdMqi7mC.d.ts +0 -202
  99. package/dist/client-CdMqi7mC.d.ts.map +0 -1
  100. package/dist/client-Dj5d3SP_.js +0 -1026
  101. package/dist/client-Dj5d3SP_.js.map +0 -1
  102. package/dist/devtools-J_jZ2vQf.d.ts +0 -152
  103. package/dist/devtools-J_jZ2vQf.d.ts.map +0 -1
  104. package/dist/devtools-kJJaORn-.js +0 -340
  105. package/dist/devtools-kJJaORn-.js.map +0 -1
  106. package/dist/devtools-server.browser.d.ts +0 -1
  107. package/dist/devtools-server.browser.js +0 -6
  108. package/dist/devtools-server.browser.js.map +0 -1
  109. package/dist/devtools-server.d.ts +0 -23
  110. package/dist/devtools-server.d.ts.map +0 -1
  111. package/dist/devtools-server.js +0 -1270
  112. package/dist/devtools-server.js.map +0 -1
  113. package/dist/devtools.d.ts +0 -2
  114. package/dist/devtools.js +0 -2
  115. package/dist/errors-W6nwJ-fm.d.ts +0 -21
  116. package/dist/errors-W6nwJ-fm.d.ts.map +0 -1
  117. package/dist/http.d.ts +0 -151
  118. package/dist/http.d.ts.map +0 -1
  119. package/dist/http.js +0 -706
  120. package/dist/http.js.map +0 -1
  121. package/dist/inspection-DaxB5jM2.js +0 -13
  122. package/dist/inspection-DaxB5jM2.js.map +0 -1
  123. package/dist/internal-DstsI6Re.js.map +0 -1
  124. package/dist/platform-B4TnJtWu.js +0 -34
  125. package/dist/platform-B4TnJtWu.js.map +0 -1
  126. package/dist/server-DpvjhdoE.d.ts.map +0 -1
  127. package/dist/server-Duw6MVlB.js.map +0 -1
  128. package/dist/store-DysUkTH3.d.ts.map +0 -1
  129. package/dist/store-redis-core-BFLwz0Wj.js.map +0 -1
  130. package/dist/testing.browser.d.ts +0 -1
  131. package/dist/testing.browser.js +0 -6
  132. package/dist/testing.browser.js.map +0 -1
  133. package/dist/testing.d.ts +0 -32
  134. package/dist/testing.d.ts.map +0 -1
  135. package/dist/testing.js +0 -103
  136. package/dist/testing.js.map +0 -1
  137. package/dist/wire-BFQmSJ-9.js.map +0 -1
  138. package/docs/guides/07-devtools.mdx +0 -229
  139. package/src/cli-bin.ts +0 -5
  140. package/src/cli.ts +0 -1046
  141. package/src/devtools-app.ts +0 -989
  142. package/src/devtools-server.browser.ts +0 -5
  143. package/src/devtools-server.ts +0 -604
  144. package/src/devtools.ts +0 -716
  145. package/src/http.ts +0 -394
  146. package/src/inspection.ts +0 -39
  147. package/src/testing.browser.ts +0 -5
  148. package/src/testing.ts +0 -185
@@ -11,7 +11,8 @@ import { A2Error } from './errors.ts'
11
11
  import { SOCKET_TIMINGS, STREAM_TIMINGS } from './internal.ts'
12
12
  import type { Event } from './store.ts'
13
13
  import type { EventDefs, PresencePatch, PresenceSnapshot } from './contract.ts'
14
- import type { PushedEvent, PushedPresence, Session } from './server.ts'
14
+ import type { ParsedPushEvent, ParsedPushPresence } from './push-envelope.ts'
15
+ import type { Session } from './server.ts'
15
16
  import { parsePresenceSibling, parsePushEvents } from './push-envelope.ts'
16
17
  import {
17
18
  SOCKET_PING_FRAME,
@@ -71,16 +72,15 @@ const MAX_MALFORMED_FRAMES = 5
71
72
  * The pump's runtime view of a session. The presence members are
72
73
  * conditional on the public surface (`WithPresence`) and therefore
73
74
  * invisible on a generic `Session<D>`, so capability is checked at
74
- * runtime (`setPresence` present) — the same resolution as the docs'
75
- * route sample. The casts live in `sessionsSocket` and `handle`.
75
+ * runtime (`setPresence` present). The cast stays inside the transport.
76
76
  */
77
77
  export type SocketSession = {
78
78
  stream(opts: {
79
79
  startAfter: number
80
80
  presence?: boolean
81
81
  }): AsyncIterable<Event | PresencePatch | PresenceSnapshot>
82
- append(...events: PushedEvent[]): Promise<Event[]>
83
- setPresence?(patch: PushedPresence): Promise<void>
82
+ append(...events: ParsedPushEvent[]): Promise<Event[]>
83
+ setPresence?(patch: ParsedPushPresence): Promise<void>
84
84
  }
85
85
 
86
86
  /**
@@ -238,7 +238,8 @@ export type SessionsSocketResolve<D extends EventDefs> = (
238
238
  sessionId: string,
239
239
  /** The entry's resume frontier — `stream({ startAfter })`. */
240
240
  startAfter: number,
241
- ) => Session<D> | null | Promise<Session<D> | null>
241
+ ) =>
242
+ Pick<Session<D>, 'stream'> | null | Promise<Pick<Session<D>, 'stream'> | null>
242
243
 
243
244
  export type SessionsSocketOptions = {
244
245
  /** The `stream()` presence opt-in for every session on the socket —
@@ -247,17 +248,11 @@ export type SessionsSocketOptions = {
247
248
  /** Epoch ms: close cleanly (code 1000) at this time, ahead of a known
248
249
  * platform deadline, so clients reconnect on our schedule. */
249
250
  deadline?: number
250
- /**
251
- * Per-frame push gate — policy's seam on the up lane, the socket
252
- * counterpart of a POST route's checks (`handle`'s `before` rides
253
- * it). Runs after the frame parses and before `append`: return an
254
- * `A2Error` to answer the push with that error ack (nothing
255
- * appended), `null` to allow. Absent, every push on an accepted
256
- * subscription is allowed — the prior behavior, unchanged.
257
- */
251
+ /** Runs after frame parsing and before append or presence I/O. */
258
252
  gatePush?(
259
253
  sessionId: string,
260
- events: PushedEvent[],
254
+ events: ParsedPushEvent[],
255
+ presence?: ParsedPushPresence,
261
256
  ): A2Error | null | Promise<A2Error | null>
262
257
  }
263
258
 
@@ -359,11 +354,14 @@ export function sessionsSocket<D extends EventDefs>(
359
354
  id: string,
360
355
  index: number,
361
356
  ): Promise<void> => {
362
- let session: Session<D> | null
357
+ let session: Pick<Session<D>, 'stream'> | null
363
358
  try {
364
359
  session = await resolve(id, index)
365
360
  } catch {
366
- session = null
361
+ if (!shell.torn()) {
362
+ shell.send(socketUnsubscribedFor(id, 'stream failed'))
363
+ }
364
+ return
367
365
  }
368
366
  if (shell.torn()) return
369
367
  if (session === null) {
@@ -419,7 +417,8 @@ export function sessionsSocket<D extends EventDefs>(
419
417
  }
420
418
  try {
421
419
  const events = parsePushEvents(frame['events'])
422
- const denial = (await options?.gatePush?.(sessionId, events)) ?? null
420
+ const denial =
421
+ (await options?.gatePush?.(sessionId, events, undefined)) ?? null
423
422
  if (denial !== null) {
424
423
  shell.send(socketErrorAckFor(req, denial, sessionId))
425
424
  return
@@ -442,11 +441,28 @@ export function sessionsSocket<D extends EventDefs>(
442
441
  shell.malformedFrame('presence frame on a presence-less session')
443
442
  return
444
443
  }
444
+ let patch: ParsedPushPresence | undefined
445
445
  try {
446
- const patch = parsePresenceSibling(frame)
447
- if (patch) await subscription.target.setPresence(patch)
446
+ patch = parsePresenceSibling(frame)
448
447
  } catch {
449
448
  shell.malformedFrame('invalid presence frame')
449
+ return
450
+ }
451
+ if (!patch) return
452
+ try {
453
+ const denial = (await options?.gatePush?.(sessionId, [], patch)) ?? null
454
+ if (denial !== null) return
455
+ await subscription.target.setPresence(patch)
456
+ } catch (error) {
457
+ const failure = asA2Error(error)
458
+ if (
459
+ failure.code === 'INVALID_PAYLOAD' ||
460
+ failure.code === 'UNKNOWN_PRESENCE_FIELD'
461
+ ) {
462
+ shell.malformedFrame('invalid presence frame')
463
+ return
464
+ }
465
+ shell.shutdown(1011, 'internal error')
450
466
  }
451
467
  }
452
468
 
package/src/sse.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The SSE response protocol — framing, heartbeat, deadline, and
3
- * teardown for the live stream lane. Internal module: `handle`'s
4
- * stream branch is the one sanctioned caller; the framing rules stay
3
+ * teardown for the live stream lane. Internal module: `server.fetch` is
4
+ * the sanctioned caller; the framing rules stay
5
5
  * documented on `sseResponse` because the client's parser (and the
6
6
  * stall watchdog) depend on them.
7
7
  */
@@ -21,7 +21,6 @@ import {
21
21
  type IdSource,
22
22
  type PresenceRow,
23
23
  type StoredEvent,
24
- type StoredSessionSummary,
25
24
  type StoreStateRead,
26
25
  } from './store.ts'
27
26
 
@@ -102,6 +101,12 @@ const toEvent = (row: Row): Event => ({
102
101
  createdAt: new Date(row.createdAt),
103
102
  })
104
103
 
104
+ const snapshotKey = (sessionId: string, reducerName: string): string =>
105
+ `${sessionId}\u0000${reducerName}`
106
+
107
+ const eventPinKey = (sessionId: string, index: number): string =>
108
+ `${sessionId}\u0000${index}`
109
+
105
110
  export function memory(options: MemoryStoreOptions = {}): A2Store {
106
111
  const clock = options.clock ?? SYSTEM_CLOCK
107
112
  const generateId = options.ids ?? RANDOM_IDS
@@ -115,6 +120,12 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
115
120
  string,
116
121
  { index: number; state: unknown; updatedAt: Date }
117
122
  >()
123
+ const historicalSnapshots = new Map<
124
+ string,
125
+ { index: number; state: unknown; updatedAt: Date }
126
+ >()
127
+ const snapshotPins = new Map<string, Set<string>>()
128
+ const snapshotPinCounts = new Map<string, number>()
118
129
  const streamSubscribers = new Map<string, Set<(row: Row) => void>>()
119
130
  /** ns → participant → field → latest surviving write. */
120
131
  const presenceRows = new Map<
@@ -143,6 +154,28 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
143
154
  return rows
144
155
  }
145
156
 
157
+ const checkpointKey = (
158
+ sessionId: string,
159
+ reducerName: string,
160
+ index: number,
161
+ ): string => `${snapshotKey(sessionId, reducerName)}\u0000${index}`
162
+
163
+ const releaseSnapshotPins = (sessionId: string, eventIndex: number): void => {
164
+ const pinKey = eventPinKey(sessionId, eventIndex)
165
+ const checkpoints = snapshotPins.get(pinKey)
166
+ if (!checkpoints) return
167
+ for (const key of checkpoints) {
168
+ const next = (snapshotPinCounts.get(key) ?? 1) - 1
169
+ if (next === 0) {
170
+ snapshotPinCounts.delete(key)
171
+ historicalSnapshots.delete(key)
172
+ } else {
173
+ snapshotPinCounts.set(key, next)
174
+ }
175
+ }
176
+ snapshotPins.delete(pinKey)
177
+ }
178
+
146
179
  const dispatchOf = (sessionId: string): SessionDispatch => {
147
180
  let state = dispatch.get(sessionId)
148
181
  if (!state) {
@@ -296,16 +329,41 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
296
329
  const readStateOf = (
297
330
  sessionId: string,
298
331
  reducerName: string,
332
+ throughIndex?: number,
333
+ snapshotThroughIndex?: number,
299
334
  ): StoreStateRead => {
300
- const snap = snapshots.get(`${sessionId}\u0000${reducerName}`)
301
- const snapshot = snap
302
- ? { index: snap.index, state: structuredClone(snap.state) }
335
+ const key = snapshotKey(sessionId, reducerName)
336
+ const snapshotFrontier = snapshotThroughIndex ?? throughIndex
337
+ const head = snapshots.get(key)
338
+ let candidate =
339
+ head && (snapshotFrontier === undefined || head.index <= snapshotFrontier)
340
+ ? head
341
+ : null
342
+ if (snapshotFrontier !== undefined) {
343
+ const prefix = `${key}\u0000`
344
+ for (const [historicalKey, snapshot] of historicalSnapshots) {
345
+ if (
346
+ historicalKey.startsWith(prefix) &&
347
+ snapshot.index <= snapshotFrontier &&
348
+ (!candidate || snapshot.index > candidate.index)
349
+ ) {
350
+ candidate = snapshot
351
+ }
352
+ }
353
+ }
354
+ const snapshot = candidate
355
+ ? { index: candidate.index, state: structuredClone(candidate.state) }
303
356
  : null
304
357
  const afterIndex = snapshot?.index ?? 0
305
358
  return {
359
+ headIndex: head?.index ?? null,
306
360
  snapshot,
307
361
  events: (sessions.get(sessionId) ?? [])
308
- .filter((row) => row.index > afterIndex)
362
+ .filter(
363
+ (row) =>
364
+ row.index > afterIndex &&
365
+ (throughIndex === undefined || row.index <= throughIndex),
366
+ )
309
367
  .map(toEvent),
310
368
  }
311
369
  }
@@ -489,6 +547,7 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
489
547
  parent.claimHolder = null
490
548
  parent.claimExpiresAt = null
491
549
  settle(sessionId, parent)
550
+ releaseSnapshotPins(sessionId, index)
492
551
  notifyStreams(sessionId, inserted)
493
552
  return { outcome: 'completed', events: inserted.map(toStored) }
494
553
  },
@@ -521,6 +580,7 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
521
580
  if (row.failureCount >= maxFailures) {
522
581
  row.failedAt = new Date(failedAt)
523
582
  dispatchOf(sessionId).ready.delete(row.index)
583
+ releaseSnapshotPins(sessionId, index)
524
584
  return {
525
585
  outcome: 'dead_lettered',
526
586
  failureCount: row.failureCount,
@@ -529,26 +589,83 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
529
589
  return { outcome: 'failed', failureCount: row.failureCount }
530
590
  },
531
591
 
532
- async readState(sessionId, reducerName) {
533
- return readStateOf(sessionId, reducerName)
592
+ async readState(sessionId, reducerName, stateOptions) {
593
+ return readStateOf(
594
+ sessionId,
595
+ reducerName,
596
+ stateOptions?.throughIndex,
597
+ stateOptions?.snapshotThroughIndex,
598
+ )
534
599
  },
535
600
 
536
- async readStates(sessionIds, reducerName) {
537
- return sessionIds.map((sessionId) => readStateOf(sessionId, reducerName))
601
+ async readStates(requests, reducerName) {
602
+ return requests.map((request) =>
603
+ readStateOf(
604
+ request.sessionId,
605
+ reducerName,
606
+ request.throughIndex,
607
+ request.snapshotThroughIndex,
608
+ ),
609
+ )
538
610
  },
539
611
 
540
- async putSnapshot(sessionId, reducerName, index, state) {
541
- const key = `${sessionId}\u0000${reducerName}`
542
- const existing = snapshots.get(key)
543
- // Guarded upsert: a slower concurrent writer must never clobber a
544
- // further-along snapshot. A lost race costs a few refolded events
545
- // next read — never correctness.
546
- if (existing && existing.index >= index) return
547
- snapshots.set(key, {
548
- index,
549
- state: structuredClone(state),
550
- updatedAt: clock.now(),
551
- })
612
+ async putSnapshots(sessionId, reducerName, writes) {
613
+ const key = snapshotKey(sessionId, reducerName)
614
+ for (const write of writes.toSorted((a, b) => a.index - b.index)) {
615
+ const checkpoint = checkpointKey(sessionId, reducerName, write.index)
616
+ for (const eventIndex of write.pinEventIndexes ?? []) {
617
+ const event = sessions.get(sessionId)?.[eventIndex - 1]
618
+ if (!event || event.processedAt !== null || event.failedAt !== null) {
619
+ continue
620
+ }
621
+ const pinKey = eventPinKey(sessionId, eventIndex)
622
+ let pinned = snapshotPins.get(pinKey)
623
+ if (!pinned) {
624
+ pinned = new Set()
625
+ snapshotPins.set(pinKey, pinned)
626
+ }
627
+ if (!pinned.has(checkpoint)) {
628
+ pinned.add(checkpoint)
629
+ snapshotPinCounts.set(
630
+ checkpoint,
631
+ (snapshotPinCounts.get(checkpoint) ?? 0) + 1,
632
+ )
633
+ }
634
+ }
635
+
636
+ const current = snapshots.get(key)
637
+ if (!current || write.index >= current.index) {
638
+ if (current && write.index > current.index) {
639
+ const currentCheckpoint = checkpointKey(
640
+ sessionId,
641
+ reducerName,
642
+ current.index,
643
+ )
644
+ if ((snapshotPinCounts.get(currentCheckpoint) ?? 0) > 0) {
645
+ historicalSnapshots.set(currentCheckpoint, {
646
+ index: current.index,
647
+ state: structuredClone(current.state),
648
+ updatedAt: new Date(current.updatedAt),
649
+ })
650
+ }
651
+ }
652
+ snapshots.set(key, {
653
+ index: write.index,
654
+ state: structuredClone(write.state),
655
+ updatedAt: clock.now(),
656
+ })
657
+ historicalSnapshots.delete(checkpoint)
658
+ } else if (
659
+ write.index < current.index &&
660
+ (snapshotPinCounts.get(checkpoint) ?? 0) > 0
661
+ ) {
662
+ historicalSnapshots.set(checkpoint, {
663
+ index: write.index,
664
+ state: structuredClone(write.state),
665
+ updatedAt: clock.now(),
666
+ })
667
+ }
668
+ }
552
669
  },
553
670
 
554
671
  presence: {
@@ -648,86 +765,6 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
648
765
  },
649
766
  },
650
767
 
651
- inspect: {
652
- async listSessions(inspectionOptions) {
653
- const ids = [...sessions.keys()]
654
- .filter(
655
- (sessionId) =>
656
- sessionId.startsWith(inspectionOptions.prefix) &&
657
- (inspectionOptions.cursor === undefined ||
658
- sessionId > inspectionOptions.cursor),
659
- )
660
- .toSorted()
661
- const visible = ids.slice(0, inspectionOptions.limit)
662
- const summaries = visible.map((sessionId): StoredSessionSummary => {
663
- const rows = sessions.get(sessionId)!
664
- const first = rows[0]!
665
- let updatedAt = first.createdAt
666
- let pendingCount = 0
667
- let failedCount = 0
668
- let attemptCount = 0
669
- let failureCount = 0
670
- for (const row of rows) {
671
- attemptCount += row.attemptCount
672
- failureCount += row.failureCount
673
- if (row.processedAt === null && row.failedAt === null) {
674
- pendingCount += 1
675
- }
676
- if (row.failedAt !== null) failedCount += 1
677
- for (const timestamp of [
678
- row.createdAt,
679
- row.firstClaimedAt,
680
- row.lastClaimedAt,
681
- row.lastFailedAt,
682
- row.processedAt,
683
- row.failedAt,
684
- ]) {
685
- if (timestamp && timestamp > updatedAt) updatedAt = timestamp
686
- }
687
- }
688
- return {
689
- sessionId,
690
- eventCount: rows.length,
691
- pendingCount,
692
- failedCount,
693
- attemptCount,
694
- failureCount,
695
- firstEventAt: new Date(first.createdAt),
696
- updatedAt: new Date(updatedAt),
697
- }
698
- })
699
- return {
700
- sessions: summaries,
701
- cursor: ids.length > visible.length ? (visible.at(-1) ?? null) : null,
702
- }
703
- },
704
- async readEvents(sessionId, pageOptions) {
705
- const rows = sessions.get(sessionId) ?? []
706
- const throughIndex = Math.min(
707
- pageOptions.throughIndex ?? rows.length,
708
- rows.length,
709
- )
710
- return {
711
- events: rows
712
- .slice(pageOptions.afterIndex, throughIndex)
713
- .slice(0, pageOptions.limit)
714
- .map(toStored),
715
- throughIndex,
716
- }
717
- },
718
- async listSnapshots(sessionId) {
719
- const prefix = `${sessionId}\u0000`
720
- return [...snapshots.entries()]
721
- .filter(([key]) => key.startsWith(prefix))
722
- .map(([key, snapshot]) => ({
723
- reducerName: key.slice(prefix.length),
724
- index: snapshot.index,
725
- updatedAt: new Date(snapshot.updatedAt),
726
- }))
727
- .toSorted((a, b) => a.reducerName.localeCompare(b.reducerName))
728
- },
729
- },
730
-
731
768
  stream(sessionId, opts) {
732
769
  const startAfter = opts?.startAfter ?? 0
733
770
  return {