experimental-a2 0.17.0 → 0.18.1
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 +32 -0
- package/README.md +1 -1
- package/dist/{actor-DBbe5xeE.d.ts → actor-C7b-1arn.d.ts} +5 -5
- package/dist/{actor-DBbe5xeE.d.ts.map → actor-C7b-1arn.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +2 -2
- package/dist/{ai-vpGtcaCB.d.ts → ai-BMLlI2Ff.d.ts} +3 -3
- package/dist/{ai-vpGtcaCB.d.ts.map → ai-BMLlI2Ff.d.ts.map} +1 -1
- package/dist/{ai-UrWfKeuv.js → ai-D6mGlO8j.js} +2 -2
- package/dist/{ai-UrWfKeuv.js.map → ai-D6mGlO8j.js.map} +1 -1
- package/dist/ai-client.d.ts +2 -2
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.browser.d.ts +1 -0
- package/dist/ai-fx.browser.js +6 -0
- package/dist/ai-fx.browser.js.map +1 -0
- package/dist/ai-fx.d.ts +10 -0
- package/dist/ai-fx.d.ts.map +1 -0
- package/dist/ai-fx.js +692 -0
- package/dist/ai-fx.js.map +1 -0
- package/dist/ai-harness-B2suP2Qh.d.ts +26 -0
- package/dist/ai-harness-B2suP2Qh.d.ts.map +1 -0
- package/dist/ai-harness-BuLh_EYi.js +273 -0
- package/dist/ai-harness-BuLh_EYi.js.map +1 -0
- package/dist/ai-pi.browser.d.ts +1 -0
- package/dist/ai-pi.browser.js +6 -0
- package/dist/ai-pi.browser.js.map +1 -0
- package/dist/ai-pi.d.ts +17 -0
- package/dist/ai-pi.d.ts.map +1 -0
- package/dist/ai-pi.js +950 -0
- package/dist/ai-pi.js.map +1 -0
- package/dist/ai-react.d.ts +5 -5
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +47 -25
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +113 -40
- package/dist/ai-server.js.map +1 -1
- package/dist/ai-tool-input-DmSqMUQi.js +40 -0
- package/dist/ai-tool-input-DmSqMUQi.js.map +1 -0
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/dist/bounded-cache-CPHWF-8h.js +41 -0
- package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
- package/dist/{client-COaiaDub.js → client-BhHLoNKF.js} +3 -3
- package/dist/{client-COaiaDub.js.map → client-BhHLoNKF.js.map} +1 -1
- package/dist/{client-qRnRUzBS.d.ts → client-h_DKFAjj.d.ts} +3 -3
- package/dist/{client-qRnRUzBS.d.ts.map → client-h_DKFAjj.d.ts.map} +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/{initial-snapshot-BrLso5x_.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +2 -2
- package/dist/{initial-snapshot-BrLso5x_.d.ts.map → initial-snapshot-Bx-c0pDP.d.ts.map} +1 -1
- package/dist/{internal-CbVvEwad.js → internal-CscSY7K_.js} +11 -3
- package/dist/internal-CscSY7K_.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/{react-BbJ-g6Uu.d.ts → react-BBXgHWFc.d.ts} +4 -4
- package/dist/{react-BbJ-g6Uu.d.ts.map → react-BBXgHWFc.d.ts.map} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/reducer-BAXc8JyF.js.map +1 -1
- package/dist/{reducer-BYu6DT7h.d.ts → reducer-CqA59Q6U.d.ts} +2 -2
- package/dist/{reducer-BYu6DT7h.d.ts.map → reducer-CqA59Q6U.d.ts.map} +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -3
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +7 -7
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +9 -10
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Rj64fNvX.js → server-D-Y4pqwu.js} +126 -67
- package/dist/server-D-Y4pqwu.js.map +1 -0
- package/dist/{server-CGM8WzRG.d.ts → server-DYAATFuT.d.ts} +42 -7
- package/dist/server-DYAATFuT.d.ts.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +1 -1
- package/dist/{session-clear-Dshpuhff.d.ts → session-clear-C-QwTXV1.d.ts} +2 -2
- package/dist/session-clear-C-QwTXV1.d.ts.map +1 -0
- package/dist/{store-Gpkq7WBo.d.ts → store-CyLUy86v.d.ts} +53 -5
- package/dist/store-CyLUy86v.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.js +56 -11
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-4_y8lETm.js → store-polling-7WJV0YWP.js} +2 -2
- package/dist/{store-polling-4_y8lETm.js.map → store-polling-7WJV0YWP.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 +106 -25
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-Q1C-k2dd.js → store-presence-polling-B1ixeqIS.js} +3 -3
- package/dist/{store-presence-polling-Q1C-k2dd.js.map → store-presence-polling-B1ixeqIS.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -3
- package/dist/{store-redis-notify-tqWNnMRu.js → store-redis-notify-CM0AfLY0.js} +271 -101
- package/dist/store-redis-notify-CM0AfLY0.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.js +66 -21
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{telemetry-cZH-cibj.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
- package/dist/{wire-DjfHOvdW.js → wire-BKxH1vbP.js} +2 -2
- package/dist/{wire-DjfHOvdW.js.map → wire-BKxH1vbP.js.map} +1 -1
- package/docs/01-quickstart.mdx +49 -83
- package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
- package/docs/actors/01-introduction.mdx +48 -86
- package/docs/actors/02-concurrency.mdx +43 -70
- package/docs/actors/03-timers.mdx +28 -42
- package/docs/actors/04-routes.mdx +110 -150
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/01-introduction.mdx +257 -0
- package/docs/agents/02-tools.mdx +247 -0
- package/docs/agents/03-conversations.mdx +538 -0
- package/docs/agents/04-harnesses.mdx +136 -0
- package/docs/agents/meta.ts +1 -0
- package/docs/client/01-react.mdx +294 -0
- package/docs/client/02-transports.mdx +86 -0
- package/docs/client/03-presence.mdx +224 -0
- package/docs/client/meta.ts +1 -0
- package/docs/contract/01-events.mdx +57 -0
- package/docs/contract/02-state.mdx +142 -0
- package/docs/contract/03-sessions.mdx +175 -0
- package/docs/index.mdx +109 -145
- package/docs/reference/01-core.mdx +139 -0
- package/docs/reference/02-server.mdx +860 -0
- package/docs/reference/03-client.mdx +301 -0
- package/docs/reference/04-ai.mdx +731 -0
- package/docs/reference/05-adapters.mdx +351 -0
- package/docs/reference/06-errors.mdx +138 -0
- package/docs/reference/meta.ts +1 -1
- package/docs/server/01-handlers.mdx +200 -0
- package/docs/server/02-timers.mdx +130 -0
- package/docs/server/03-cancellation.mdx +142 -0
- package/docs/server/04-routes.mdx +243 -0
- package/docs/server/05-production.mdx +323 -0
- package/docs/server/06-guarantees.mdx +132 -0
- package/examples/playground/DEPLOY.md +3 -5
- package/examples/playground/app/a0/[appId]/page.tsx +12 -13
- package/examples/playground/app/a0/app-list.tsx +9 -2
- package/examples/playground/app/a0/app-view.tsx +9 -1
- package/examples/playground/app/a0/apps/[appId]/route.ts +10 -5
- package/examples/playground/app/a0/apps.test.ts +12 -0
- package/examples/playground/app/a0/apps.ts +22 -7
- package/examples/playground/app/a0/chat.tsx +47 -1
- package/examples/playground/app/a0/devtools.tsx +2 -8
- package/examples/playground/app/a0/fx/events/route.ts +9 -0
- package/examples/playground/app/a0/fx/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/fx/short/events/route.ts +11 -0
- package/examples/playground/app/a0/model.test.ts +18 -1
- package/examples/playground/app/a0/model.ts +47 -10
- package/examples/playground/app/a0/no-photon.cjs +4 -0
- package/examples/playground/app/a0/no-quickjs.cjs +3 -0
- package/examples/playground/app/a0/pi/events/route.ts +9 -0
- package/examples/playground/app/a0/pi/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/pi/short/events/route.ts +11 -0
- package/examples/playground/app/a0/sandbox-log.ts +10 -4
- package/examples/playground/app/a0/sandbox-session.ts +152 -0
- package/examples/playground/app/a0/scheduler/route.ts +2 -2
- package/examples/playground/app/a0/server.ts +98 -54
- package/examples/playground/app/a0/session.ts +17 -5
- package/examples/playground/app/a0/shell.tsx +11 -3
- package/examples/playground/app/agent/ingress.test.ts +6 -3
- package/examples/playground/app/agent/server.ts +9 -6
- package/examples/playground/app/agent/telemetry.test.ts +71 -50
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +1 -1
- package/examples/playground/app/chat/server.test.ts +30 -26
- package/examples/playground/app/chat/server.ts +46 -46
- package/examples/playground/next.config.ts +17 -5
- package/examples/playground/package.json +4 -1
- package/examples/playground/vercel.json +18 -0
- package/package.json +27 -3
- package/src/ai-agent.ts +271 -90
- package/src/ai-fx.browser.ts +5 -0
- package/src/ai-fx.ts +912 -0
- package/src/ai-harness.ts +518 -0
- package/src/ai-model-metadata.ts +11 -6
- package/src/ai-pi.browser.ts +5 -0
- package/src/ai-pi.ts +1322 -0
- package/src/ai-sdk-step.ts +2 -2
- package/src/ai-server.ts +3 -1
- package/src/ai-tool-input.ts +45 -0
- package/src/internal.ts +13 -0
- package/src/libfx.d.ts +34 -0
- package/src/reducer.ts +1 -1
- package/src/scheduler-qstash.ts +5 -5
- package/src/scheduler-vercel.ts +14 -15
- package/src/server.ts +233 -29
- package/src/store-memory.ts +92 -16
- package/src/store-postgres.ts +156 -25
- package/src/store-redis-core.ts +322 -110
- package/src/store-redis-notify.ts +2 -0
- package/src/store-sqlite.ts +121 -28
- package/src/store.ts +55 -3
- package/src/telemetry.ts +1 -0
- package/dist/internal-CbVvEwad.js.map +0 -1
- package/dist/server-CGM8WzRG.d.ts.map +0 -1
- package/dist/server-Rj64fNvX.js.map +0 -1
- package/dist/session-clear-Dshpuhff.d.ts.map +0 -1
- package/dist/store-Gpkq7WBo.d.ts.map +0 -1
- package/dist/store-redis-notify-tqWNnMRu.js.map +0 -1
- package/dist/telemetry-cZH-cibj.d.ts.map +0 -1
- package/docs/concepts/01-contracts.mdx +0 -237
- package/docs/concepts/02-handlers.mdx +0 -284
- package/docs/concepts/03-durability.mdx +0 -314
- package/docs/concepts/04-state.mdx +0 -209
- package/docs/guides/01-timers.mdx +0 -193
- package/docs/guides/02-cancellation.mdx +0 -161
- package/docs/guides/03-react.mdx +0 -324
- package/docs/guides/04-local-first.mdx +0 -47
- package/docs/guides/05-production.mdx +0 -534
- package/docs/guides/06-ai-agents.mdx +0 -1408
- package/docs/guides/08-application-data.mdx +0 -113
- package/docs/guides/09-presence.mdx +0 -262
- package/docs/guides/10-transports.mdx +0 -221
- package/docs/reference/01-api.mdx +0 -2358
- package/docs/reference/02-errors.mdx +0 -150
- /package/docs/{concepts → contract}/meta.ts +0 -0
- /package/docs/{guides → server}/meta.ts +0 -0
package/src/store-memory.ts
CHANGED
|
@@ -259,16 +259,18 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
259
259
|
return row
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
const
|
|
262
|
+
const hasPending = (sessionId: string): boolean => {
|
|
263
263
|
const log = sessions.get(sessionId)
|
|
264
|
-
return
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
(row) => row.index > log!.clearIndex && row.processedAt === null,
|
|
268
|
-
),
|
|
269
|
-
}
|
|
264
|
+
return (log?.rows ?? []).some(
|
|
265
|
+
(row) => row.index > log!.clearIndex && row.processedAt === null,
|
|
266
|
+
)
|
|
270
267
|
}
|
|
271
268
|
|
|
269
|
+
const appendResult = (sessionId: string, events: StoredEvent[]) => ({
|
|
270
|
+
events,
|
|
271
|
+
hasPending: hasPending(sessionId),
|
|
272
|
+
})
|
|
273
|
+
|
|
272
274
|
/** The attempt-currency fence shared by handler appends and clears. */
|
|
273
275
|
const assertCurrentCause = (sessionId: string, cause: EventCause): void => {
|
|
274
276
|
if (cause.index <= clearIndexOf(sessionId)) {
|
|
@@ -280,12 +282,35 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
280
282
|
}
|
|
281
283
|
}
|
|
282
284
|
|
|
283
|
-
const claim = (
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
285
|
+
const claim = ({
|
|
286
|
+
sessionId,
|
|
287
|
+
row,
|
|
288
|
+
holder,
|
|
289
|
+
now,
|
|
290
|
+
expiresAt,
|
|
291
|
+
maxFailures,
|
|
292
|
+
}: {
|
|
293
|
+
sessionId: string
|
|
294
|
+
row: Row
|
|
295
|
+
holder: string
|
|
296
|
+
now: Date
|
|
297
|
+
expiresAt: Date
|
|
298
|
+
maxFailures: number | undefined
|
|
299
|
+
}): StoredEvent => {
|
|
300
|
+
// A claim still held here lapsed: its attempt never finished.
|
|
301
|
+
if (row.claimHolder !== null) {
|
|
302
|
+
row.failureCount += 1
|
|
303
|
+
if (maxFailures !== undefined && row.failureCount >= maxFailures) {
|
|
304
|
+
// Advancing the attempt fences the lapsed one; nothing runs.
|
|
305
|
+
row.attemptCount += 1
|
|
306
|
+
row.failedAt = new Date(now)
|
|
307
|
+
row.claimHolder = null
|
|
308
|
+
row.claimExpiresAt = null
|
|
309
|
+
dispatchOf(sessionId).ready.delete(row.index)
|
|
310
|
+
releaseSnapshotPins(sessionId, row.index)
|
|
311
|
+
return toStored(row)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
289
314
|
row.attemptCount += 1
|
|
290
315
|
row.firstClaimedAt ??= new Date(now)
|
|
291
316
|
row.lastClaimedAt = new Date(now)
|
|
@@ -545,6 +570,7 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
545
570
|
ttlMs,
|
|
546
571
|
expiresAtMs,
|
|
547
572
|
excludeIndexes,
|
|
573
|
+
maxFailures,
|
|
548
574
|
}) {
|
|
549
575
|
const claimedAt = clock.now()
|
|
550
576
|
const now = claimedAt.getTime()
|
|
@@ -572,7 +598,14 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
572
598
|
return {
|
|
573
599
|
outcome: 'claimed',
|
|
574
600
|
events: eligible.map((row) => {
|
|
575
|
-
const claimed = claim(
|
|
601
|
+
const claimed = claim({
|
|
602
|
+
sessionId,
|
|
603
|
+
row,
|
|
604
|
+
holder,
|
|
605
|
+
now: claimedAt,
|
|
606
|
+
expiresAt,
|
|
607
|
+
maxFailures,
|
|
608
|
+
})
|
|
576
609
|
if (clearIndex > 0) claimed.clearIndex = clearIndex
|
|
577
610
|
return claimed
|
|
578
611
|
}),
|
|
@@ -679,7 +712,14 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
679
712
|
return { outcome: 'completed', events: inserted.map(toStored) }
|
|
680
713
|
},
|
|
681
714
|
|
|
682
|
-
async failAttempt({
|
|
715
|
+
async failAttempt({
|
|
716
|
+
sessionId,
|
|
717
|
+
index,
|
|
718
|
+
attempt,
|
|
719
|
+
error,
|
|
720
|
+
maxFailures,
|
|
721
|
+
retryAfterMs,
|
|
722
|
+
}) {
|
|
683
723
|
if (index <= clearIndexOf(sessionId)) {
|
|
684
724
|
return { outcome: 'superseded', failureCount: 0 }
|
|
685
725
|
}
|
|
@@ -706,7 +746,10 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
706
746
|
row.lastFailedAt = new Date(failedAt)
|
|
707
747
|
row.lastFailedAttempt = attempt
|
|
708
748
|
row.claimHolder = null
|
|
709
|
-
row.claimExpiresAt =
|
|
749
|
+
row.claimExpiresAt =
|
|
750
|
+
retryAfterMs === undefined || row.failureCount >= maxFailures
|
|
751
|
+
? null
|
|
752
|
+
: new Date(failedAt.getTime() + retryAfterMs)
|
|
710
753
|
if (row.failureCount >= maxFailures) {
|
|
711
754
|
row.failedAt = new Date(failedAt)
|
|
712
755
|
dispatchOf(sessionId).ready.delete(row.index)
|
|
@@ -719,6 +762,39 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
719
762
|
return { outcome: 'failed', failureCount: row.failureCount }
|
|
720
763
|
},
|
|
721
764
|
|
|
765
|
+
async deadLetters(sessionId) {
|
|
766
|
+
const log = sessions.get(sessionId)
|
|
767
|
+
return (log?.rows ?? [])
|
|
768
|
+
.filter(
|
|
769
|
+
(row) =>
|
|
770
|
+
row.index > log!.clearIndex &&
|
|
771
|
+
row.processedAt === null &&
|
|
772
|
+
row.failedAt !== null,
|
|
773
|
+
)
|
|
774
|
+
.map(toStored)
|
|
775
|
+
},
|
|
776
|
+
|
|
777
|
+
async redrive({ sessionId, index, action }) {
|
|
778
|
+
const row =
|
|
779
|
+
index > clearIndexOf(sessionId) ? rowAt(sessionId, index) : undefined
|
|
780
|
+
if (!row || row.processedAt !== null || row.failedAt === null) {
|
|
781
|
+
return { redriven: false, hasPending: hasPending(sessionId) }
|
|
782
|
+
}
|
|
783
|
+
if (action === 'skip') {
|
|
784
|
+
row.processedAt = clock.now()
|
|
785
|
+
settle(sessionId, row)
|
|
786
|
+
} else {
|
|
787
|
+
row.failedAt = null
|
|
788
|
+
row.failureCount = 0
|
|
789
|
+
const state = dispatchOf(sessionId)
|
|
790
|
+
const queue = row.lane === null ? undefined : state.lanes.get(row.lane)
|
|
791
|
+
if (row.lane === null || queue?.rows[queue.head] === row) {
|
|
792
|
+
state.ready.set(row.index, row)
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return { redriven: true, hasPending: hasPending(sessionId) }
|
|
796
|
+
},
|
|
797
|
+
|
|
722
798
|
async readState(sessionId, reducerName, stateOptions) {
|
|
723
799
|
return readStateOf(
|
|
724
800
|
sessionId,
|
package/src/store-postgres.ts
CHANGED
|
@@ -350,6 +350,34 @@ const createListener = ({
|
|
|
350
350
|
return listenerConnection
|
|
351
351
|
})
|
|
352
352
|
|
|
353
|
+
/** A dead-lettered event's snapshot pins, and history no pin keeps. */
|
|
354
|
+
const releasePins = async ({
|
|
355
|
+
query,
|
|
356
|
+
sessionId,
|
|
357
|
+
index,
|
|
358
|
+
}: {
|
|
359
|
+
query: PostgresConnection['query']
|
|
360
|
+
sessionId: string
|
|
361
|
+
index: number
|
|
362
|
+
}): Promise<void> => {
|
|
363
|
+
await query(
|
|
364
|
+
`delete from a2_snapshot_pins
|
|
365
|
+
where session_id = $1 and event_index = $2`,
|
|
366
|
+
[sessionId, index],
|
|
367
|
+
)
|
|
368
|
+
await query(
|
|
369
|
+
`delete from a2_snapshot_history history
|
|
370
|
+
where history.session_id = $1
|
|
371
|
+
and not exists (
|
|
372
|
+
select 1 from a2_snapshot_pins pin
|
|
373
|
+
where pin.session_id = history.session_id
|
|
374
|
+
and pin.reducer_name = history.reducer_name
|
|
375
|
+
and pin.up_to_index = history.up_to_index
|
|
376
|
+
)`,
|
|
377
|
+
[sessionId],
|
|
378
|
+
)
|
|
379
|
+
}
|
|
380
|
+
|
|
353
381
|
export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
354
382
|
const clock = options.clock ?? SYSTEM_CLOCK
|
|
355
383
|
const generateId = options.ids ?? RANDOM_IDS
|
|
@@ -942,11 +970,16 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
942
970
|
ttlMs,
|
|
943
971
|
expiresAtMs,
|
|
944
972
|
excludeIndexes = [],
|
|
973
|
+
maxFailures,
|
|
945
974
|
}) {
|
|
946
975
|
return wrap(() =>
|
|
947
976
|
withTx(async (query) => {
|
|
948
977
|
const now = clock.now().getTime()
|
|
949
978
|
const expiresAt = expiresAtMs ?? now + ttlMs
|
|
979
|
+
// A lapsed claim's takeover that spends the last failed attempt
|
|
980
|
+
// dead-letters the event instead of claiming it.
|
|
981
|
+
const exhausted = `(claimed.claim_holder is not null
|
|
982
|
+
and claimed.failure_count + 1 >= $6::int)`
|
|
950
983
|
const { rows } = await query(
|
|
951
984
|
`with eligible as materialized (
|
|
952
985
|
select event.idx
|
|
@@ -962,15 +995,36 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
962
995
|
)
|
|
963
996
|
update a2_events claimed set
|
|
964
997
|
attempt_count = claimed.attempt_count + 1,
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
998
|
+
failure_count = claimed.failure_count
|
|
999
|
+
+ case when claimed.claim_holder is null then 0 else 1 end,
|
|
1000
|
+
failed_at = case when ${exhausted} then $2::bigint else claimed.failed_at end,
|
|
1001
|
+
first_claimed_at = case when ${exhausted}
|
|
1002
|
+
then claimed.first_claimed_at
|
|
1003
|
+
else coalesce(claimed.first_claimed_at, $2::bigint) end,
|
|
1004
|
+
last_claimed_at = case when ${exhausted}
|
|
1005
|
+
then claimed.last_claimed_at else $2::bigint end,
|
|
1006
|
+
claim_holder = case when ${exhausted} then null else $3::text end,
|
|
1007
|
+
claim_expires_at = case when ${exhausted} then null else $4::bigint end
|
|
969
1008
|
from eligible
|
|
970
1009
|
where claimed.session_id = $1 and claimed.idx = eligible.idx
|
|
971
1010
|
returning claimed.*, ${clearIndexOf('$1')} as clear_index`,
|
|
972
|
-
[
|
|
1011
|
+
[
|
|
1012
|
+
sessionId,
|
|
1013
|
+
now,
|
|
1014
|
+
holder,
|
|
1015
|
+
expiresAt,
|
|
1016
|
+
[...excludeIndexes],
|
|
1017
|
+
maxFailures ?? null,
|
|
1018
|
+
],
|
|
973
1019
|
)
|
|
1020
|
+
for (const row of rows) {
|
|
1021
|
+
if (row['failed_at'] !== null)
|
|
1022
|
+
await releasePins({
|
|
1023
|
+
query,
|
|
1024
|
+
sessionId,
|
|
1025
|
+
index: asNumber(row['idx']),
|
|
1026
|
+
})
|
|
1027
|
+
}
|
|
974
1028
|
if (rows.length > 0) {
|
|
975
1029
|
const events = rows
|
|
976
1030
|
.map((row) => {
|
|
@@ -990,8 +1044,9 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
990
1044
|
and failed_at is null
|
|
991
1045
|
and lane_ready
|
|
992
1046
|
and idx > ${clearIndexOf('$1')}
|
|
993
|
-
and claim_expires_at > $2
|
|
994
|
-
|
|
1047
|
+
and claim_expires_at > $2
|
|
1048
|
+
and not (idx = any($3::bigint[]))`,
|
|
1049
|
+
[sessionId, now, [...excludeIndexes]],
|
|
995
1050
|
)
|
|
996
1051
|
const retryAt = activeRows[0]?.['retry_at']
|
|
997
1052
|
return retryAt === null || retryAt === undefined
|
|
@@ -1262,7 +1317,15 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1262
1317
|
)
|
|
1263
1318
|
},
|
|
1264
1319
|
|
|
1265
|
-
async failAttempt({
|
|
1320
|
+
async failAttempt({
|
|
1321
|
+
sessionId,
|
|
1322
|
+
index,
|
|
1323
|
+
attempt,
|
|
1324
|
+
error,
|
|
1325
|
+
maxFailures,
|
|
1326
|
+
retryAfterMs,
|
|
1327
|
+
}) {
|
|
1328
|
+
const now = clock.now().getTime()
|
|
1266
1329
|
return wrap(() =>
|
|
1267
1330
|
withTx(async (query) => {
|
|
1268
1331
|
const { rows } = await query(
|
|
@@ -1282,7 +1345,10 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1282
1345
|
else e.failed_at
|
|
1283
1346
|
end,
|
|
1284
1347
|
claim_holder = null,
|
|
1285
|
-
claim_expires_at =
|
|
1348
|
+
claim_expires_at = case
|
|
1349
|
+
when e.failure_count + 1 >= $5 then null
|
|
1350
|
+
else $7::bigint
|
|
1351
|
+
end
|
|
1286
1352
|
from current
|
|
1287
1353
|
where e.session_id = $1
|
|
1288
1354
|
and e.idx = $2
|
|
@@ -1322,7 +1388,8 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1322
1388
|
attempt,
|
|
1323
1389
|
error,
|
|
1324
1390
|
maxFailures,
|
|
1325
|
-
|
|
1391
|
+
now,
|
|
1392
|
+
retryAfterMs === undefined ? null : now + retryAfterMs,
|
|
1326
1393
|
],
|
|
1327
1394
|
)
|
|
1328
1395
|
const row = rows[0]
|
|
@@ -1337,28 +1404,92 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1337
1404
|
`no event at index ${index} in session '${sessionId}'`,
|
|
1338
1405
|
)
|
|
1339
1406
|
}
|
|
1340
|
-
if (row['outcome'] === 'dead_lettered')
|
|
1407
|
+
if (row['outcome'] === 'dead_lettered')
|
|
1408
|
+
await releasePins({ query, sessionId, index })
|
|
1409
|
+
return {
|
|
1410
|
+
outcome: row['outcome'] as
|
|
1411
|
+
'failed' | 'dead_lettered' | 'superseded',
|
|
1412
|
+
failureCount: asNumber(row['failure_count']),
|
|
1413
|
+
}
|
|
1414
|
+
}),
|
|
1415
|
+
)
|
|
1416
|
+
},
|
|
1417
|
+
|
|
1418
|
+
async deadLetters(sessionId) {
|
|
1419
|
+
return wrap(async () => {
|
|
1420
|
+
const c = await client()
|
|
1421
|
+
const { rows } = await c.query(
|
|
1422
|
+
`select * from a2_events
|
|
1423
|
+
where session_id = $1
|
|
1424
|
+
and processed_at is null
|
|
1425
|
+
and failed_at is not null
|
|
1426
|
+
and idx > ${clearIndexOf('$1')}
|
|
1427
|
+
order by idx`,
|
|
1428
|
+
[sessionId],
|
|
1429
|
+
)
|
|
1430
|
+
return rows.map(toStored)
|
|
1431
|
+
})
|
|
1432
|
+
},
|
|
1433
|
+
|
|
1434
|
+
async redrive({ sessionId, index, action }) {
|
|
1435
|
+
return wrap(() =>
|
|
1436
|
+
withTx(async (query) => {
|
|
1437
|
+
// Lane promotion must see every append that committed before it,
|
|
1438
|
+
// so a skip takes the session lock first, in append's order. The
|
|
1439
|
+
// row lock serializes both actions with claims and handler appends.
|
|
1440
|
+
if (action === 'skip') {
|
|
1441
|
+
await query('select pg_advisory_xact_lock(hashtext($1))', [
|
|
1442
|
+
sessionId,
|
|
1443
|
+
])
|
|
1444
|
+
}
|
|
1445
|
+
const { rows } = await query(
|
|
1446
|
+
`select lane from a2_events
|
|
1447
|
+
where session_id = $1 and idx = $2
|
|
1448
|
+
and idx > ${clearIndexOf('$1')}
|
|
1449
|
+
and processed_at is null
|
|
1450
|
+
and failed_at is not null
|
|
1451
|
+
for update`,
|
|
1452
|
+
[sessionId, index],
|
|
1453
|
+
)
|
|
1454
|
+
const row = rows[0]
|
|
1455
|
+
if (row && action === 'retry') {
|
|
1341
1456
|
await query(
|
|
1342
|
-
`
|
|
1343
|
-
where session_id = $1 and
|
|
1457
|
+
`update a2_events set failed_at = null, failure_count = 0
|
|
1458
|
+
where session_id = $1 and idx = $2`,
|
|
1344
1459
|
[sessionId, index],
|
|
1345
1460
|
)
|
|
1461
|
+
} else if (row) {
|
|
1346
1462
|
await query(
|
|
1347
|
-
`
|
|
1348
|
-
where
|
|
1349
|
-
|
|
1350
|
-
select 1 from a2_snapshot_pins pin
|
|
1351
|
-
where pin.session_id = history.session_id
|
|
1352
|
-
and pin.reducer_name = history.reducer_name
|
|
1353
|
-
and pin.up_to_index = history.up_to_index
|
|
1354
|
-
)`,
|
|
1355
|
-
[sessionId],
|
|
1463
|
+
`update a2_events set processed_at = $3, lane_ready = false
|
|
1464
|
+
where session_id = $1 and idx = $2`,
|
|
1465
|
+
[sessionId, index, clock.now().getTime()],
|
|
1356
1466
|
)
|
|
1467
|
+
const lane = (row['lane'] as string | null) ?? null
|
|
1468
|
+
if (lane !== null) {
|
|
1469
|
+
await query(
|
|
1470
|
+
`update a2_events set lane_ready = true
|
|
1471
|
+
where session_id = $1
|
|
1472
|
+
and idx = (
|
|
1473
|
+
select idx from a2_events
|
|
1474
|
+
where session_id = $1 and lane = $2 and processed_at is null
|
|
1475
|
+
and idx > ${clearIndexOf('$1')}
|
|
1476
|
+
order by idx limit 1
|
|
1477
|
+
)`,
|
|
1478
|
+
[sessionId, lane],
|
|
1479
|
+
)
|
|
1480
|
+
}
|
|
1357
1481
|
}
|
|
1482
|
+
const { rows: status } = await query(
|
|
1483
|
+
`select exists(
|
|
1484
|
+
select 1 from a2_events
|
|
1485
|
+
where session_id = $1 and processed_at is null
|
|
1486
|
+
and idx > ${clearIndexOf('$1')}
|
|
1487
|
+
) as has_pending`,
|
|
1488
|
+
[sessionId],
|
|
1489
|
+
)
|
|
1358
1490
|
return {
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
failureCount: asNumber(row['failure_count']),
|
|
1491
|
+
redriven: row !== undefined,
|
|
1492
|
+
hasPending: status[0]?.['has_pending'] === true,
|
|
1362
1493
|
}
|
|
1363
1494
|
}),
|
|
1364
1495
|
)
|