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.
- package/CHANGELOG.md +41 -0
- package/dist/ai-server.d.ts +4 -5
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +20 -17
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +334 -2
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +1 -1
- package/dist/client.d.ts +202 -2
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1025 -1
- package/dist/client.js.map +1 -0
- package/dist/errors-BQuJpe82.js.map +1 -1
- package/dist/index.d.ts +22 -3
- package/dist/index.d.ts.map +1 -0
- package/dist/{internal-DstsI6Re.js → internal-DRXJ56EI.js} +5 -28
- package/dist/internal-DRXJ56EI.js.map +1 -0
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/scheduler-qstash.d.ts +3 -3
- package/dist/scheduler-qstash.js +4 -5
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +4 -4
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Duw6MVlB.js → server-B2XNevQA.js} +830 -131
- package/dist/server-B2XNevQA.js.map +1 -0
- package/dist/{server-DpvjhdoE.d.ts → server-DjPhHnbI.d.ts} +71 -50
- package/dist/server-DjPhHnbI.d.ts.map +1 -0
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/{store-DysUkTH3.d.ts → store-RJO35BMj.d.ts} +24 -62
- package/dist/store-RJO35BMj.d.ts.map +1 -0
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +80 -78
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-dSeLxzfb.js → store-polling-6DW7F1DT.js} +2 -2
- package/dist/{store-polling-dSeLxzfb.js.map → store-polling-6DW7F1DT.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +231 -182
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DT01r4GZ.js} +213 -161
- package/dist/store-redis-core-DT01r4GZ.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -4
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +4 -5
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +104 -91
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{wire-BFQmSJ-9.js → wire-B6te_wns.js} +4 -3
- package/dist/wire-B6te_wns.js.map +1 -0
- package/docs/concepts/02-handlers.mdx +4 -0
- package/docs/concepts/04-state.mdx +57 -9
- package/docs/guides/03-react.mdx +20 -28
- package/docs/guides/05-production.mdx +9 -11
- package/docs/guides/06-ai-agents.mdx +10 -15
- package/docs/guides/09-presence.mdx +14 -19
- package/docs/guides/10-transports.mdx +104 -86
- package/docs/reference/01-api.mdx +182 -293
- package/docs/reference/02-errors.mdx +5 -7
- package/package.json +1 -14
- package/src/ai-server.ts +36 -15
- package/src/client.ts +2 -2
- package/src/errors.ts +1 -0
- package/src/internal.ts +3 -62
- package/src/push-envelope.ts +24 -21
- package/src/scheduler-qstash.ts +3 -3
- package/src/scheduler-vercel.ts +2 -2
- package/src/server-fetch.ts +344 -0
- package/src/server.ts +315 -312
- package/src/session-socket.ts +36 -20
- package/src/sse.ts +2 -2
- package/src/store-memory.ts +138 -101
- package/src/store-postgres.ts +355 -238
- package/src/store-redis-core.ts +247 -237
- package/src/store-redis-http.ts +1 -2
- package/src/store-redis.ts +1 -2
- package/src/store-sqlite.ts +191 -153
- package/src/store.ts +24 -66
- package/src/wire.ts +2 -1
- package/dist/ai-D_PGS-JR.d.ts +0 -334
- package/dist/ai-D_PGS-JR.d.ts.map +0 -1
- package/dist/cli-B3VuxoDe.js +0 -599
- package/dist/cli-B3VuxoDe.js.map +0 -1
- package/dist/cli-bin.d.ts +0 -1
- package/dist/cli-bin.js +0 -7
- package/dist/cli-bin.js.map +0 -1
- package/dist/cli.d.ts +0 -20
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -2
- package/dist/client-CdMqi7mC.d.ts +0 -202
- package/dist/client-CdMqi7mC.d.ts.map +0 -1
- package/dist/client-Dj5d3SP_.js +0 -1026
- package/dist/client-Dj5d3SP_.js.map +0 -1
- package/dist/devtools-J_jZ2vQf.d.ts +0 -152
- package/dist/devtools-J_jZ2vQf.d.ts.map +0 -1
- package/dist/devtools-kJJaORn-.js +0 -340
- package/dist/devtools-kJJaORn-.js.map +0 -1
- package/dist/devtools-server.browser.d.ts +0 -1
- package/dist/devtools-server.browser.js +0 -6
- package/dist/devtools-server.browser.js.map +0 -1
- package/dist/devtools-server.d.ts +0 -23
- package/dist/devtools-server.d.ts.map +0 -1
- package/dist/devtools-server.js +0 -1270
- package/dist/devtools-server.js.map +0 -1
- package/dist/devtools.d.ts +0 -2
- package/dist/devtools.js +0 -2
- package/dist/errors-W6nwJ-fm.d.ts +0 -21
- package/dist/errors-W6nwJ-fm.d.ts.map +0 -1
- package/dist/http.d.ts +0 -151
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js +0 -706
- package/dist/http.js.map +0 -1
- package/dist/inspection-DaxB5jM2.js +0 -13
- package/dist/inspection-DaxB5jM2.js.map +0 -1
- package/dist/internal-DstsI6Re.js.map +0 -1
- package/dist/platform-B4TnJtWu.js +0 -34
- package/dist/platform-B4TnJtWu.js.map +0 -1
- package/dist/server-DpvjhdoE.d.ts.map +0 -1
- package/dist/server-Duw6MVlB.js.map +0 -1
- package/dist/store-DysUkTH3.d.ts.map +0 -1
- package/dist/store-redis-core-BFLwz0Wj.js.map +0 -1
- package/dist/testing.browser.d.ts +0 -1
- package/dist/testing.browser.js +0 -6
- package/dist/testing.browser.js.map +0 -1
- package/dist/testing.d.ts +0 -32
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js +0 -103
- package/dist/testing.js.map +0 -1
- package/dist/wire-BFQmSJ-9.js.map +0 -1
- package/docs/guides/07-devtools.mdx +0 -229
- package/src/cli-bin.ts +0 -5
- package/src/cli.ts +0 -1046
- package/src/devtools-app.ts +0 -989
- package/src/devtools-server.browser.ts +0 -5
- package/src/devtools-server.ts +0 -604
- package/src/devtools.ts +0 -716
- package/src/http.ts +0 -394
- package/src/inspection.ts +0 -39
- package/src/testing.browser.ts +0 -5
- package/src/testing.ts +0 -185
package/src/session-socket.ts
CHANGED
|
@@ -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 {
|
|
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)
|
|
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:
|
|
83
|
-
setPresence?(patch:
|
|
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
|
-
) =>
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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: `
|
|
4
|
-
*
|
|
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
|
*/
|
package/src/store-memory.ts
CHANGED
|
@@ -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
|
|
301
|
-
const
|
|
302
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
537
|
-
return
|
|
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
|
|
541
|
-
const key =
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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 {
|