experimental-a2 0.7.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 +19 -0
- package/dist/ai-server.d.ts +1 -1
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +13 -11
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +1 -1
- package/dist/{server-286j79Mt.js → server-B2XNevQA.js} +123 -53
- package/dist/server-B2XNevQA.js.map +1 -0
- package/dist/{server-DgXmORIq.d.ts → server-DjPhHnbI.d.ts} +7 -4
- 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-flRz1OWh.d.ts → store-RJO35BMj.d.ts} +25 -8
- 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 +79 -19
- package/dist/store-memory.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 +230 -101
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-redis-core-DEYO8Ryv.js → store-redis-core-DT01r4GZ.js} +167 -29
- 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 +2 -2
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +2 -2
- 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 +103 -19
- package/dist/store-sqlite.js.map +1 -1
- package/docs/concepts/02-handlers.mdx +4 -0
- package/docs/concepts/04-state.mdx +57 -9
- package/docs/guides/06-ai-agents.mdx +2 -1
- package/docs/reference/01-api.mdx +39 -16
- package/package.json +1 -1
- package/src/ai-server.ts +27 -10
- package/src/server.ts +242 -87
- package/src/store-memory.ts +138 -20
- package/src/store-postgres.ts +355 -138
- package/src/store-redis-core.ts +201 -27
- package/src/store-redis-http.ts +1 -1
- package/src/store-redis.ts +1 -1
- package/src/store-sqlite.ts +191 -34
- package/src/store.ts +27 -9
- package/dist/server-286j79Mt.js.map +0 -1
- package/dist/server-DgXmORIq.d.ts.map +0 -1
- package/dist/store-flRz1OWh.d.ts.map +0 -1
- package/dist/store-redis-core-DEYO8Ryv.js.map +0 -1
package/src/store-redis-core.ts
CHANGED
|
@@ -182,8 +182,24 @@ return { 'ok', tostring(base + 1), has_pending,
|
|
|
182
182
|
tostring(redis.call('EXISTS', KEYS[7])) }
|
|
183
183
|
`
|
|
184
184
|
|
|
185
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* KEYS: counter, meta, pending, ready, event snapshot pins, snapshot pin counts.
|
|
187
|
+
* ARGV: idx, attempt, error, maxFailures, nowMs.
|
|
188
|
+
*/
|
|
186
189
|
const FAIL_ATTEMPT_LUA = `
|
|
190
|
+
local function release_snapshot_pins()
|
|
191
|
+
local refs = redis.call('SMEMBERS', KEYS[5])
|
|
192
|
+
for _, ref in ipairs(refs) do
|
|
193
|
+
local count = redis.call('HINCRBY', KEYS[6], ref, -1)
|
|
194
|
+
if count <= 0 then
|
|
195
|
+
redis.call('HDEL', KEYS[6], ref)
|
|
196
|
+
local checkpoint = cjson.decode(ref)
|
|
197
|
+
redis.call('ZREMRANGEBYSCORE', checkpoint[1], checkpoint[2], checkpoint[2])
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
redis.call('DEL', KEYS[5])
|
|
201
|
+
if redis.call('HLEN', KEYS[6]) == 0 then redis.call('DEL', KEYS[6]) end
|
|
202
|
+
end
|
|
187
203
|
local function lane_field(lane, suffix)
|
|
188
204
|
return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix
|
|
189
205
|
end
|
|
@@ -237,6 +253,7 @@ redis.call('HDEL', KEYS[2], field .. ':ch', field .. ':ce')
|
|
|
237
253
|
if failures >= tonumber(ARGV[4]) then
|
|
238
254
|
redis.call('HSET', KEYS[2], field .. ':f', ARGV[5])
|
|
239
255
|
redis.call('ZREM', KEYS[4], field)
|
|
256
|
+
release_snapshot_pins()
|
|
240
257
|
return { 'dead_lettered', tostring(failures) }
|
|
241
258
|
end
|
|
242
259
|
return { 'failed', tostring(failures) }
|
|
@@ -330,11 +347,25 @@ return result
|
|
|
330
347
|
`
|
|
331
348
|
|
|
332
349
|
/**
|
|
333
|
-
* KEYS: store, counter, ids, meta, pending, ready, watched
|
|
350
|
+
* KEYS: store, counter, ids, meta, pending, ready, watched,
|
|
351
|
+
* event snapshot pins, snapshot pin counts.
|
|
334
352
|
* ARGV: sessionId, nowMs, parent index, attempt, n, then per child:
|
|
335
353
|
* id, type, payloadJson, lane, settled.
|
|
336
354
|
*/
|
|
337
355
|
const COMPLETE_ATTEMPT_LUA = `
|
|
356
|
+
local function release_snapshot_pins()
|
|
357
|
+
local refs = redis.call('SMEMBERS', KEYS[8])
|
|
358
|
+
for _, ref in ipairs(refs) do
|
|
359
|
+
local count = redis.call('HINCRBY', KEYS[9], ref, -1)
|
|
360
|
+
if count <= 0 then
|
|
361
|
+
redis.call('HDEL', KEYS[9], ref)
|
|
362
|
+
local checkpoint = cjson.decode(ref)
|
|
363
|
+
redis.call('ZREMRANGEBYSCORE', checkpoint[1], checkpoint[2], checkpoint[2])
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
redis.call('DEL', KEYS[8])
|
|
367
|
+
if redis.call('HLEN', KEYS[9]) == 0 then redis.call('DEL', KEYS[9]) end
|
|
368
|
+
end
|
|
338
369
|
local function lane_field(lane, suffix)
|
|
339
370
|
return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix
|
|
340
371
|
end
|
|
@@ -433,6 +464,7 @@ redis.call('HSET', KEYS[4],
|
|
|
433
464
|
parent .. ':ri', returned_json)
|
|
434
465
|
redis.call('HDEL', KEYS[4], parent .. ':ch', parent .. ':ce')
|
|
435
466
|
settle(parent)
|
|
467
|
+
release_snapshot_pins()
|
|
436
468
|
local cause = cjson.encode({ index = tonumber(parent), attempt = attempt })
|
|
437
469
|
for i = 1, n do
|
|
438
470
|
local id = ARGV[5 + (i - 1) * 5 + 1]
|
|
@@ -457,13 +489,61 @@ return { 'ok', tostring(total + 1),
|
|
|
457
489
|
tostring(redis.call('EXISTS', KEYS[7])) }
|
|
458
490
|
`
|
|
459
491
|
|
|
460
|
-
/**
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
492
|
+
/**
|
|
493
|
+
* KEYS: head, history, event counter, event meta, pin counts.
|
|
494
|
+
* ARGV: nowMs, event pin key prefix, write count, then per write:
|
|
495
|
+
* index, snapshot JSON, pin count, pin event indexes.
|
|
496
|
+
*/
|
|
497
|
+
const PUT_SNAPSHOTS_LUA = `
|
|
498
|
+
local total = tonumber(redis.call('GET', KEYS[3]) or '0')
|
|
499
|
+
local writes = {}
|
|
500
|
+
local offset = 4
|
|
501
|
+
for i = 1, tonumber(ARGV[3]) do
|
|
502
|
+
local index = tonumber(ARGV[offset])
|
|
503
|
+
local snapshot = ARGV[offset + 1]
|
|
504
|
+
local pin_count = tonumber(ARGV[offset + 2])
|
|
505
|
+
local ref = cjson.encode({ KEYS[2], tostring(index) })
|
|
506
|
+
for p = 1, pin_count do
|
|
507
|
+
local event_index = tonumber(ARGV[offset + 2 + p])
|
|
508
|
+
if event_index >= 1 and event_index <= total and
|
|
509
|
+
redis.call('HEXISTS', KEYS[4], tostring(event_index) .. ':p') == 0 and
|
|
510
|
+
redis.call('HEXISTS', KEYS[4], tostring(event_index) .. ':f') == 0 then
|
|
511
|
+
local pin_key = ARGV[2] .. tostring(event_index)
|
|
512
|
+
if redis.call('SADD', pin_key, ref) == 1 then
|
|
513
|
+
redis.call('HINCRBY', KEYS[5], ref, 1)
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
writes[#writes + 1] = { index = index, snapshot = snapshot, ref = ref }
|
|
518
|
+
offset = offset + 3 + pin_count
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
local head = redis.call('GET', KEYS[1])
|
|
522
|
+
local head_index = nil
|
|
523
|
+
if head then head_index = tonumber(cjson.decode(head)['index']) end
|
|
524
|
+
local latest = writes[#writes]
|
|
525
|
+
local writes_head = not head_index or latest.index >= head_index
|
|
526
|
+
local advances = not head_index or latest.index > head_index
|
|
527
|
+
local final_head_index = advances and latest.index or head_index
|
|
528
|
+
|
|
529
|
+
if advances and head and tonumber(redis.call('HGET', KEYS[5], cjson.encode({ KEYS[2], tostring(head_index) })) or '0') > 0 then
|
|
530
|
+
redis.call('ZREMRANGEBYSCORE', KEYS[2], head_index, head_index)
|
|
531
|
+
redis.call('ZADD', KEYS[2], head_index, head)
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
for _, write in ipairs(writes) do
|
|
535
|
+
if write.index < final_head_index then
|
|
536
|
+
redis.call('ZREMRANGEBYSCORE', KEYS[2], write.index, write.index)
|
|
537
|
+
if tonumber(redis.call('HGET', KEYS[5], write.ref) or '0') > 0 then
|
|
538
|
+
redis.call('ZADD', KEYS[2], write.index, write.snapshot)
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
if writes_head then
|
|
544
|
+
redis.call('SET', KEYS[1], latest.snapshot)
|
|
545
|
+
redis.call('ZREMRANGEBYSCORE', KEYS[2], latest.index, latest.index)
|
|
465
546
|
end
|
|
466
|
-
redis.call('SET', KEYS[1], ARGV[2])
|
|
467
547
|
return 1
|
|
468
548
|
`
|
|
469
549
|
|
|
@@ -520,11 +600,26 @@ end
|
|
|
520
600
|
return live
|
|
521
601
|
`
|
|
522
602
|
|
|
523
|
-
/**
|
|
603
|
+
/**
|
|
604
|
+
* KEYS: head, history, event counter. ARGV: snapshot frontier or ''.
|
|
605
|
+
* Returns one atomic multiversion-cache-plus-log-frontier read.
|
|
606
|
+
*/
|
|
524
607
|
const READ_STATE_LUA = `
|
|
525
|
-
local
|
|
526
|
-
local
|
|
527
|
-
|
|
608
|
+
local head = redis.call('GET', KEYS[1])
|
|
609
|
+
local cap = ARGV[1]
|
|
610
|
+
local snapshot = nil
|
|
611
|
+
if head and (cap == '' or tonumber(cjson.decode(head)['index']) <= tonumber(cap)) then
|
|
612
|
+
snapshot = head
|
|
613
|
+
end
|
|
614
|
+
local upper = cap == '' and '+inf' or cap
|
|
615
|
+
local historical = redis.call('ZREVRANGEBYSCORE', KEYS[2], upper, '-inf', 'LIMIT', 0, 1)
|
|
616
|
+
if #historical > 0 then
|
|
617
|
+
if not snapshot or tonumber(cjson.decode(historical[1])['index']) > tonumber(cjson.decode(snapshot)['index']) then
|
|
618
|
+
snapshot = historical[1]
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
local frontier = redis.call('GET', KEYS[3]) or '0'
|
|
622
|
+
return { head or '', snapshot or '', frontier }
|
|
528
623
|
`
|
|
529
624
|
|
|
530
625
|
// ── wire helpers ─────────────────────────────────────────────────────
|
|
@@ -681,6 +776,14 @@ export function createRedisStoreCore(
|
|
|
681
776
|
const pendingKey = (s: string): string => `${prefix}:${s}:pending`
|
|
682
777
|
const readyKey = (s: string): string => `${prefix}:${s}:ready`
|
|
683
778
|
const snapKey = (s: string, r: string): string => `${prefix}:${s}:snap:${r}`
|
|
779
|
+
const snapHistoryKey = (s: string, r: string): string =>
|
|
780
|
+
`${prefix}:${s}:snap-history:${r}`
|
|
781
|
+
const snapshotEventPinsKey = (s: string, index: number): string =>
|
|
782
|
+
`${prefix}:${s}:snapshot-pins:${index}`
|
|
783
|
+
const snapshotEventPinsPrefix = (s: string): string =>
|
|
784
|
+
`${prefix}:${s}:snapshot-pins:`
|
|
785
|
+
const snapshotPinCountsKey = (s: string): string =>
|
|
786
|
+
`${prefix}:${s}:snapshot-pin-counts`
|
|
684
787
|
const watchedKey = (s: string): string => `${prefix}:${s}:watched`
|
|
685
788
|
const presenceKey = (s: string): string => `${prefix}:${s}:presence`
|
|
686
789
|
const idsKey = `${prefix}:ids`
|
|
@@ -1068,6 +1171,8 @@ export function createRedisStoreCore(
|
|
|
1068
1171
|
pendingKey(sessionId),
|
|
1069
1172
|
readyKey(sessionId),
|
|
1070
1173
|
watchedKey(sessionId),
|
|
1174
|
+
snapshotEventPinsKey(sessionId, index),
|
|
1175
|
+
snapshotPinCountsKey(sessionId),
|
|
1071
1176
|
],
|
|
1072
1177
|
[
|
|
1073
1178
|
sessionId,
|
|
@@ -1160,6 +1265,8 @@ export function createRedisStoreCore(
|
|
|
1160
1265
|
metaKey(sessionId),
|
|
1161
1266
|
pendingKey(sessionId),
|
|
1162
1267
|
readyKey(sessionId),
|
|
1268
|
+
snapshotEventPinsKey(sessionId, index),
|
|
1269
|
+
snapshotPinCountsKey(sessionId),
|
|
1163
1270
|
],
|
|
1164
1271
|
[index, attempt, error, maxFailures, clock.now().getTime()],
|
|
1165
1272
|
)) as string[]
|
|
@@ -1176,22 +1283,37 @@ export function createRedisStoreCore(
|
|
|
1176
1283
|
})
|
|
1177
1284
|
},
|
|
1178
1285
|
|
|
1179
|
-
async readState(sessionId, reducerName) {
|
|
1286
|
+
async readState(sessionId, reducerName, stateOptions) {
|
|
1180
1287
|
return wrap(async () => {
|
|
1181
1288
|
const reply = (await evalScript(
|
|
1182
1289
|
READ_STATE_LUA,
|
|
1183
|
-
[
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1290
|
+
[
|
|
1291
|
+
snapKey(sessionId, reducerName),
|
|
1292
|
+
snapHistoryKey(sessionId, reducerName),
|
|
1293
|
+
countKey(sessionId),
|
|
1294
|
+
],
|
|
1295
|
+
[
|
|
1296
|
+
stateOptions?.snapshotThroughIndex ??
|
|
1297
|
+
stateOptions?.throughIndex ??
|
|
1298
|
+
'',
|
|
1299
|
+
],
|
|
1300
|
+
)) as [string, string, string]
|
|
1301
|
+
const rawHead = reply[0]
|
|
1302
|
+
const raw = reply[1]
|
|
1303
|
+
const head =
|
|
1304
|
+
rawHead === ''
|
|
1305
|
+
? null
|
|
1306
|
+
: (JSON.parse(rawHead) as { index: number; state: unknown })
|
|
1187
1307
|
const parsed =
|
|
1188
1308
|
raw === ''
|
|
1189
1309
|
? null
|
|
1190
1310
|
: (JSON.parse(raw) as { index: number; state: unknown })
|
|
1191
|
-
const
|
|
1311
|
+
const currentFrontier = Number(reply[2])
|
|
1192
1312
|
if (
|
|
1193
|
-
!Number.isSafeInteger(
|
|
1194
|
-
|
|
1313
|
+
!Number.isSafeInteger(currentFrontier) ||
|
|
1314
|
+
currentFrontier < 0 ||
|
|
1315
|
+
(head !== null &&
|
|
1316
|
+
(!Number.isSafeInteger(head.index) || head.index < 0)) ||
|
|
1195
1317
|
(parsed !== null &&
|
|
1196
1318
|
(!Number.isSafeInteger(parsed.index) || parsed.index < 0))
|
|
1197
1319
|
) {
|
|
@@ -1200,26 +1322,78 @@ export function createRedisStoreCore(
|
|
|
1200
1322
|
`redis state frontier for session '${sessionId}' is invalid`,
|
|
1201
1323
|
)
|
|
1202
1324
|
}
|
|
1325
|
+
const frontier = Math.min(
|
|
1326
|
+
currentFrontier,
|
|
1327
|
+
stateOptions?.throughIndex ?? currentFrontier,
|
|
1328
|
+
)
|
|
1329
|
+
const snapshot = parsed
|
|
1203
1330
|
const events = await readEventRange(
|
|
1204
1331
|
sessionId,
|
|
1205
|
-
|
|
1332
|
+
snapshot?.index ?? 0,
|
|
1206
1333
|
frontier,
|
|
1207
1334
|
)
|
|
1208
1335
|
return {
|
|
1209
|
-
|
|
1210
|
-
|
|
1336
|
+
headIndex: head?.index ?? null,
|
|
1337
|
+
snapshot: snapshot
|
|
1338
|
+
? { index: snapshot.index, state: snapshot.state }
|
|
1211
1339
|
: null,
|
|
1212
1340
|
events,
|
|
1213
1341
|
}
|
|
1214
1342
|
})
|
|
1215
1343
|
},
|
|
1216
1344
|
|
|
1217
|
-
async
|
|
1345
|
+
async putSnapshots(sessionId, reducerName, writes) {
|
|
1346
|
+
const byIndex = new Map<
|
|
1347
|
+
number,
|
|
1348
|
+
{ index: number; state: unknown; pinEventIndexes: Set<number> }
|
|
1349
|
+
>()
|
|
1350
|
+
for (const write of writes) {
|
|
1351
|
+
const existing = byIndex.get(write.index)
|
|
1352
|
+
if (existing) {
|
|
1353
|
+
existing.state = write.state
|
|
1354
|
+
for (const eventIndex of write.pinEventIndexes ?? []) {
|
|
1355
|
+
existing.pinEventIndexes.add(eventIndex)
|
|
1356
|
+
}
|
|
1357
|
+
} else {
|
|
1358
|
+
byIndex.set(write.index, {
|
|
1359
|
+
index: write.index,
|
|
1360
|
+
state: write.state,
|
|
1361
|
+
pinEventIndexes: new Set(write.pinEventIndexes ?? []),
|
|
1362
|
+
})
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
const normalized = [...byIndex.values()].toSorted(
|
|
1366
|
+
(a, b) => a.index - b.index,
|
|
1367
|
+
)
|
|
1368
|
+
if (normalized.length === 0) return
|
|
1218
1369
|
await wrap(async () => {
|
|
1370
|
+
const args: Array<string | number> = [
|
|
1371
|
+
clock.now().getTime(),
|
|
1372
|
+
snapshotEventPinsPrefix(sessionId),
|
|
1373
|
+
normalized.length,
|
|
1374
|
+
]
|
|
1375
|
+
for (const write of normalized) {
|
|
1376
|
+
args.push(
|
|
1377
|
+
write.index,
|
|
1378
|
+
json({
|
|
1379
|
+
index: write.index,
|
|
1380
|
+
state: write.state,
|
|
1381
|
+
updatedAt: clock.now().getTime(),
|
|
1382
|
+
}),
|
|
1383
|
+
write.pinEventIndexes.size,
|
|
1384
|
+
...write.pinEventIndexes,
|
|
1385
|
+
)
|
|
1386
|
+
}
|
|
1219
1387
|
await evalScript(
|
|
1220
|
-
|
|
1221
|
-
[
|
|
1222
|
-
|
|
1388
|
+
PUT_SNAPSHOTS_LUA,
|
|
1389
|
+
[
|
|
1390
|
+
snapKey(sessionId, reducerName),
|
|
1391
|
+
snapHistoryKey(sessionId, reducerName),
|
|
1392
|
+
countKey(sessionId),
|
|
1393
|
+
metaKey(sessionId),
|
|
1394
|
+
snapshotPinCountsKey(sessionId),
|
|
1395
|
+
],
|
|
1396
|
+
args,
|
|
1223
1397
|
)
|
|
1224
1398
|
})
|
|
1225
1399
|
},
|
package/src/store-redis-http.ts
CHANGED
|
@@ -95,7 +95,7 @@ export function redisHttp(options: RedisHttpStoreOptions): RedisHttpStore {
|
|
|
95
95
|
completeAttempt: core.completeAttempt,
|
|
96
96
|
failAttempt: core.failAttempt,
|
|
97
97
|
readState: core.readState,
|
|
98
|
-
|
|
98
|
+
putSnapshots: core.putSnapshots,
|
|
99
99
|
presence: {
|
|
100
100
|
async set(ns, participant, values, meta) {
|
|
101
101
|
await core.presence.set(ns, participant, values, meta)
|
package/src/store-redis.ts
CHANGED
|
@@ -291,7 +291,7 @@ export function redis(options: RedisStoreOptions = {}): RedisStore {
|
|
|
291
291
|
completeAttempt: core.completeAttempt,
|
|
292
292
|
failAttempt: core.failAttempt,
|
|
293
293
|
readState: core.readState,
|
|
294
|
-
|
|
294
|
+
putSnapshots: core.putSnapshots,
|
|
295
295
|
|
|
296
296
|
presence: {
|
|
297
297
|
async set(ns, participant, values, meta) {
|
package/src/store-sqlite.ts
CHANGED
|
@@ -92,6 +92,26 @@ create table if not exists a2_snapshots (
|
|
|
92
92
|
primary key (session_id, reducer_name)
|
|
93
93
|
) strict;
|
|
94
94
|
|
|
95
|
+
create table if not exists a2_snapshot_history (
|
|
96
|
+
session_id text not null,
|
|
97
|
+
reducer_name text not null,
|
|
98
|
+
up_to_index integer not null,
|
|
99
|
+
state text not null,
|
|
100
|
+
updated_at integer not null,
|
|
101
|
+
primary key (session_id, reducer_name, up_to_index)
|
|
102
|
+
) strict;
|
|
103
|
+
|
|
104
|
+
create table if not exists a2_snapshot_pins (
|
|
105
|
+
session_id text not null,
|
|
106
|
+
event_index integer not null,
|
|
107
|
+
reducer_name text not null,
|
|
108
|
+
up_to_index integer not null,
|
|
109
|
+
primary key (session_id, event_index, reducer_name, up_to_index)
|
|
110
|
+
) strict;
|
|
111
|
+
|
|
112
|
+
create index if not exists a2_snapshot_pins_checkpoint
|
|
113
|
+
on a2_snapshot_pins (session_id, reducer_name, up_to_index);
|
|
114
|
+
|
|
95
115
|
create table if not exists a2_presence (
|
|
96
116
|
ns text not null,
|
|
97
117
|
participant text not null,
|
|
@@ -242,10 +262,17 @@ type PresenceDbRow = {
|
|
|
242
262
|
|
|
243
263
|
type StateReadRow = Partial<EventRow> & {
|
|
244
264
|
row_kind: 'snapshot' | 'event'
|
|
265
|
+
head_index: number | bigint | null
|
|
245
266
|
snapshot_index: number | bigint | null
|
|
246
267
|
snapshot_state: string | null
|
|
247
268
|
}
|
|
248
269
|
|
|
270
|
+
type SnapshotHeadRow = {
|
|
271
|
+
up_to_index: number | bigint
|
|
272
|
+
state: string
|
|
273
|
+
updated_at: number | bigint
|
|
274
|
+
}
|
|
275
|
+
|
|
249
276
|
type AppendStatusRow = {
|
|
250
277
|
max: number | bigint
|
|
251
278
|
has_pending: number | bigint
|
|
@@ -747,6 +774,19 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
747
774
|
claim_expires_at = null
|
|
748
775
|
where session_id = ? and idx = ?`,
|
|
749
776
|
).run(now, attempt, JSON.stringify(ids), sessionId, index)
|
|
777
|
+
db.prepare(
|
|
778
|
+
'delete from a2_snapshot_pins where session_id = ? and event_index = ?',
|
|
779
|
+
).run(sessionId, index)
|
|
780
|
+
db.prepare(
|
|
781
|
+
`delete from a2_snapshot_history
|
|
782
|
+
where session_id = ?
|
|
783
|
+
and not exists (
|
|
784
|
+
select 1 from a2_snapshot_pins pin
|
|
785
|
+
where pin.session_id = a2_snapshot_history.session_id
|
|
786
|
+
and pin.reducer_name = a2_snapshot_history.reducer_name
|
|
787
|
+
and pin.up_to_index = a2_snapshot_history.up_to_index
|
|
788
|
+
)`,
|
|
789
|
+
).run(sessionId)
|
|
750
790
|
if (parent.lane !== null) {
|
|
751
791
|
promoteLaneHead.run(sessionId, sessionId, parent.lane)
|
|
752
792
|
}
|
|
@@ -835,6 +875,21 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
835
875
|
sessionId,
|
|
836
876
|
index,
|
|
837
877
|
)
|
|
878
|
+
if (deadLettered) {
|
|
879
|
+
db.prepare(
|
|
880
|
+
'delete from a2_snapshot_pins where session_id = ? and event_index = ?',
|
|
881
|
+
).run(sessionId, index)
|
|
882
|
+
db.prepare(
|
|
883
|
+
`delete from a2_snapshot_history
|
|
884
|
+
where session_id = ?
|
|
885
|
+
and not exists (
|
|
886
|
+
select 1 from a2_snapshot_pins pin
|
|
887
|
+
where pin.session_id = a2_snapshot_history.session_id
|
|
888
|
+
and pin.reducer_name = a2_snapshot_history.reducer_name
|
|
889
|
+
and pin.up_to_index = a2_snapshot_history.up_to_index
|
|
890
|
+
)`,
|
|
891
|
+
).run(sessionId)
|
|
892
|
+
}
|
|
838
893
|
return {
|
|
839
894
|
outcome: deadLettered ? 'dead_lettered' : 'failed',
|
|
840
895
|
failureCount: nextFailureCount,
|
|
@@ -843,18 +898,32 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
843
898
|
)
|
|
844
899
|
},
|
|
845
900
|
|
|
846
|
-
async readState(sessionId, reducerName) {
|
|
901
|
+
async readState(sessionId, reducerName, stateOptions) {
|
|
847
902
|
return wrap(() => {
|
|
903
|
+
const throughIndex = stateOptions?.throughIndex ?? null
|
|
904
|
+
const snapshotThroughIndex =
|
|
905
|
+
stateOptions?.snapshotThroughIndex ?? throughIndex
|
|
848
906
|
const rows = db
|
|
849
907
|
.prepare(
|
|
850
|
-
`with
|
|
851
|
-
select up_to_index, state
|
|
908
|
+
`with head as materialized (
|
|
909
|
+
select up_to_index, state, updated_at
|
|
852
910
|
from a2_snapshots
|
|
853
|
-
|
|
911
|
+
where session_id = ? and reducer_name = ?
|
|
912
|
+
), snapshot as materialized (
|
|
913
|
+
select up_to_index, state from (
|
|
914
|
+
select up_to_index, state from head
|
|
915
|
+
where (? is null or up_to_index <= ?)
|
|
916
|
+
union all
|
|
917
|
+
select up_to_index, state
|
|
918
|
+
from a2_snapshot_history
|
|
919
|
+
where session_id = ? and reducer_name = ?
|
|
920
|
+
and (? is null or up_to_index <= ?)
|
|
921
|
+
) order by up_to_index desc limit 1
|
|
854
922
|
)
|
|
855
923
|
select 0 as row_order, 'snapshot' as row_kind,
|
|
856
|
-
|
|
857
|
-
snapshot
|
|
924
|
+
(select up_to_index from head) as head_index,
|
|
925
|
+
(select up_to_index from snapshot) as snapshot_index,
|
|
926
|
+
(select state from snapshot) as snapshot_state,
|
|
858
927
|
null as session_id, null as idx, null as event_type,
|
|
859
928
|
null as payload, null as event_id, null as created_at,
|
|
860
929
|
null as cause, null as lane, null as lane_ready,
|
|
@@ -866,25 +935,44 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
866
935
|
null as last_failed_attempt, null as last_error,
|
|
867
936
|
null as failed_at, null as claim_holder,
|
|
868
937
|
null as claim_expires_at
|
|
869
|
-
from snapshot
|
|
870
938
|
union all
|
|
871
939
|
select 1 as row_order, 'event' as row_kind,
|
|
940
|
+
(select up_to_index from head) as head_index,
|
|
872
941
|
null as snapshot_index, null as snapshot_state,
|
|
873
942
|
event.*
|
|
874
943
|
from a2_events as event
|
|
875
944
|
where event.session_id = ?
|
|
876
945
|
and event.idx > coalesce((select up_to_index from snapshot), 0)
|
|
946
|
+
and (? is null or event.idx <= ?)
|
|
877
947
|
order by row_order, idx`,
|
|
878
948
|
)
|
|
879
|
-
.all(
|
|
949
|
+
.all(
|
|
950
|
+
sessionId,
|
|
951
|
+
reducerName,
|
|
952
|
+
snapshotThroughIndex,
|
|
953
|
+
snapshotThroughIndex,
|
|
954
|
+
sessionId,
|
|
955
|
+
reducerName,
|
|
956
|
+
snapshotThroughIndex,
|
|
957
|
+
snapshotThroughIndex,
|
|
958
|
+
sessionId,
|
|
959
|
+
throughIndex,
|
|
960
|
+
throughIndex,
|
|
961
|
+
) as unknown as StateReadRow[]
|
|
880
962
|
const snapshot = rows.find((row) => row.row_kind === 'snapshot')
|
|
881
963
|
return {
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
964
|
+
headIndex:
|
|
965
|
+
snapshot?.head_index === null || snapshot?.head_index === undefined
|
|
966
|
+
? null
|
|
967
|
+
: Number(snapshot.head_index),
|
|
968
|
+
snapshot:
|
|
969
|
+
snapshot?.snapshot_index !== null &&
|
|
970
|
+
snapshot?.snapshot_index !== undefined
|
|
971
|
+
? {
|
|
972
|
+
index: Number(snapshot.snapshot_index),
|
|
973
|
+
state: JSON.parse(snapshot.snapshot_state!),
|
|
974
|
+
}
|
|
975
|
+
: null,
|
|
888
976
|
events: rows
|
|
889
977
|
.filter(
|
|
890
978
|
(row): row is StateReadRow & EventRow => row.row_kind === 'event',
|
|
@@ -894,26 +982,95 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
894
982
|
})
|
|
895
983
|
},
|
|
896
984
|
|
|
897
|
-
async
|
|
898
|
-
wrap(() =>
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
985
|
+
async putSnapshots(sessionId, reducerName, writes) {
|
|
986
|
+
await wrap(() =>
|
|
987
|
+
tx(() => {
|
|
988
|
+
const selectHead = db.prepare(
|
|
989
|
+
`select up_to_index, state, updated_at from a2_snapshots
|
|
990
|
+
where session_id = ? and reducer_name = ?`,
|
|
991
|
+
)
|
|
992
|
+
const unfinished = db.prepare(
|
|
993
|
+
`select 1 from a2_events
|
|
994
|
+
where session_id = ? and idx = ?
|
|
995
|
+
and processed_at is null and failed_at is null`,
|
|
996
|
+
)
|
|
997
|
+
const insertPin = db.prepare(
|
|
998
|
+
`insert or ignore into a2_snapshot_pins
|
|
999
|
+
(session_id, event_index, reducer_name, up_to_index)
|
|
1000
|
+
values (?, ?, ?, ?)`,
|
|
1001
|
+
)
|
|
1002
|
+
const hasPin = db.prepare(
|
|
1003
|
+
`select 1 from a2_snapshot_pins
|
|
1004
|
+
where session_id = ? and reducer_name = ? and up_to_index = ?
|
|
1005
|
+
limit 1`,
|
|
1006
|
+
)
|
|
1007
|
+
const putHistory = db.prepare(
|
|
1008
|
+
`insert into a2_snapshot_history
|
|
1009
|
+
(session_id, reducer_name, up_to_index, state, updated_at)
|
|
1010
|
+
values (?, ?, ?, ?, ?)
|
|
1011
|
+
on conflict (session_id, reducer_name, up_to_index) do update set
|
|
1012
|
+
state = excluded.state, updated_at = excluded.updated_at`,
|
|
1013
|
+
)
|
|
1014
|
+
const putHead = db.prepare(
|
|
1015
|
+
`insert into a2_snapshots
|
|
1016
|
+
(session_id, reducer_name, up_to_index, state, updated_at)
|
|
1017
|
+
values (?, ?, ?, ?, ?)
|
|
1018
|
+
on conflict (session_id, reducer_name) do update set
|
|
1019
|
+
up_to_index = excluded.up_to_index,
|
|
1020
|
+
state = excluded.state,
|
|
1021
|
+
updated_at = excluded.updated_at
|
|
1022
|
+
where excluded.up_to_index >= a2_snapshots.up_to_index`,
|
|
1023
|
+
)
|
|
1024
|
+
const deleteHistory = db.prepare(
|
|
1025
|
+
`delete from a2_snapshot_history
|
|
1026
|
+
where session_id = ? and reducer_name = ? and up_to_index = ?`,
|
|
1027
|
+
)
|
|
1028
|
+
for (const write of writes.toSorted((a, b) => a.index - b.index)) {
|
|
1029
|
+
for (const eventIndex of write.pinEventIndexes ?? []) {
|
|
1030
|
+
if (unfinished.get(sessionId, eventIndex)) {
|
|
1031
|
+
insertPin.run(sessionId, eventIndex, reducerName, write.index)
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
const head = selectHead.get(sessionId, reducerName) as
|
|
1035
|
+
SnapshotHeadRow | undefined
|
|
1036
|
+
const now = clock.now().getTime()
|
|
1037
|
+
if (!head || write.index >= Number(head.up_to_index)) {
|
|
1038
|
+
if (
|
|
1039
|
+
head &&
|
|
1040
|
+
write.index > Number(head.up_to_index) &&
|
|
1041
|
+
hasPin.get(sessionId, reducerName, Number(head.up_to_index))
|
|
1042
|
+
) {
|
|
1043
|
+
putHistory.run(
|
|
1044
|
+
sessionId,
|
|
1045
|
+
reducerName,
|
|
1046
|
+
Number(head.up_to_index),
|
|
1047
|
+
head.state,
|
|
1048
|
+
Number(head.updated_at),
|
|
1049
|
+
)
|
|
1050
|
+
}
|
|
1051
|
+
putHead.run(
|
|
1052
|
+
sessionId,
|
|
1053
|
+
reducerName,
|
|
1054
|
+
write.index,
|
|
1055
|
+
JSON.stringify(write.state) ?? 'null',
|
|
1056
|
+
now,
|
|
1057
|
+
)
|
|
1058
|
+
deleteHistory.run(sessionId, reducerName, write.index)
|
|
1059
|
+
} else if (
|
|
1060
|
+
write.index < Number(head.up_to_index) &&
|
|
1061
|
+
hasPin.get(sessionId, reducerName, write.index)
|
|
1062
|
+
) {
|
|
1063
|
+
putHistory.run(
|
|
1064
|
+
sessionId,
|
|
1065
|
+
reducerName,
|
|
1066
|
+
write.index,
|
|
1067
|
+
JSON.stringify(write.state) ?? 'null',
|
|
1068
|
+
now,
|
|
1069
|
+
)
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}),
|
|
1073
|
+
)
|
|
917
1074
|
},
|
|
918
1075
|
|
|
919
1076
|
presence: {
|