experimental-a2 0.16.1 → 0.18.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 +373 -0
- package/README.md +1 -1
- package/dist/{actor-ohPC-81x.d.ts → actor-Bjce9_5U.d.ts} +21 -11
- package/dist/actor-Bjce9_5U.d.ts.map +1 -0
- package/dist/actor-client.d.ts +2 -5
- package/dist/actor-client.d.ts.map +1 -1
- package/dist/actor-client.js +13 -4
- package/dist/actor-client.js.map +1 -1
- package/dist/actor-react.d.ts +5 -3
- package/dist/actor-react.d.ts.map +1 -1
- package/dist/actor-react.js +20 -10
- package/dist/actor-react.js.map +1 -1
- package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
- package/dist/actor-shared-t7w8xIKP.js.map +1 -0
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +42 -23
- package/dist/actor.js.map +1 -1
- package/dist/{ai-DgOBltJ_.js → ai-BpOGm-zB.js} +621 -216
- package/dist/ai-BpOGm-zB.js.map +1 -0
- package/dist/{ai-CrEf6p_W.d.ts → ai-DrUaMyRt.d.ts} +114 -100
- package/dist/ai-DrUaMyRt.d.ts.map +1 -0
- package/dist/ai-client.d.ts +27 -0
- package/dist/ai-client.d.ts.map +1 -0
- package/dist/ai-client.js +29 -0
- package/dist/ai-client.js.map +1 -0
- 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 +634 -0
- package/dist/ai-fx.js.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-harness-DvsPeJeB.d.ts +26 -0
- package/dist/ai-harness-DvsPeJeB.d.ts.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 +904 -0
- package/dist/ai-pi.js.map +1 -0
- package/dist/ai-react.d.ts +96 -0
- package/dist/ai-react.d.ts.map +1 -0
- package/dist/ai-react.js +142 -0
- package/dist/ai-react.js.map +1 -0
- package/dist/ai-server.browser.js +2 -2
- package/dist/ai-server.browser.js.map +1 -1
- package/dist/ai-server.d.ts +109 -43
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +526 -283
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/bounded-cache-CPHWF-8h.js +41 -0
- package/dist/bounded-cache-CPHWF-8h.js.map +1 -0
- package/dist/{client-BYzHjkwU.js → client-BReKqwZC.js} +447 -80
- package/dist/client-BReKqwZC.js.map +1 -0
- package/dist/{client-CzyacQpJ.d.ts → client-h_DKFAjj.d.ts} +14 -13
- package/dist/client-h_DKFAjj.d.ts.map +1 -0
- package/dist/client.d.ts +4 -2
- package/dist/client.js +1 -1
- package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
- package/dist/contract-BYKASlCV.js.map +1 -0
- package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
- package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-Bx-c0pDP.d.ts} +7 -3
- package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +1 -0
- package/dist/{internal-Dq2qYxou.js → internal-B2T3U4Rq.js} +29 -2
- package/dist/internal-B2T3U4Rq.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/react-BBXgHWFc.d.ts +86 -0
- package/dist/react-BBXgHWFc.d.ts.map +1 -0
- package/dist/react.d.ts +2 -122
- package/dist/react.js +107 -100
- package/dist/react.js.map +1 -1
- package/dist/reducer-BAXc8JyF.js +87 -0
- package/dist/reducer-BAXc8JyF.js.map +1 -0
- package/dist/{reducer-otzHjuJj.d.ts → reducer-CqA59Q6U.d.ts} +46 -30
- package/dist/reducer-CqA59Q6U.d.ts.map +1 -0
- package/dist/reducer-runtime-DQwz_J59.js +28 -0
- package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +3 -3
- package/dist/scheduler-qstash.d.ts.map +1 -1
- package/dist/scheduler-qstash.js +33 -10
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
- package/dist/scheduler-task-X1SToDjP.js.map +1 -0
- package/dist/scheduler-vercel.d.ts +4 -9
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +25 -44
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-BD5ckxZb.js → server-Dw_k_7kO.js} +904 -219
- package/dist/server-Dw_k_7kO.js.map +1 -0
- package/dist/{server-DbD7IJVK.d.ts → server-En7cbi3v.d.ts} +116 -26
- package/dist/server-En7cbi3v.d.ts.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/session-clear-Bo-XTFEy.d.ts +12 -0
- package/dist/session-clear-Bo-XTFEy.d.ts.map +1 -0
- package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
- package/dist/session-clear-DGI4jTQ5.js.map +1 -0
- package/dist/{store-DyZM6fS5.d.ts → store-Djdv0bnZ.d.ts} +73 -3
- package/dist/store-Djdv0bnZ.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.d.ts.map +1 -1
- package/dist/store-memory.js +205 -66
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-CmxUbV93.js → store-polling-B5FcwJfo.js} +21 -4
- package/dist/store-polling-B5FcwJfo.js.map +1 -0
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +450 -108
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-BAwS8RWn.js} +3 -3
- package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-BAwS8RWn.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.d.ts.map +1 -1
- package/dist/store-redis-http.js +293 -11
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis-notify-BRITti_o.js +2493 -0
- package/dist/store-redis-notify-BRITti_o.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +11 -11
- 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 +197 -33
- package/dist/store-sqlite.js.map +1 -1
- package/dist/stream-buffer-Cx7AAPGq.js +23 -0
- package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
- package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-Cyfqo6Qt.d.ts} +2 -2
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +1 -0
- package/dist/{wire-BO5wWCb1.js → wire-rSUglYo8.js} +132 -10
- package/dist/wire-rSUglYo8.js.map +1 -0
- package/docs/01-quickstart.mdx +50 -84
- package/docs/{guides/07-examples.mdx → 02-examples.mdx} +10 -20
- package/docs/actors/01-introduction.mdx +48 -82
- package/docs/actors/02-concurrency.mdx +43 -70
- package/docs/actors/03-timers.mdx +28 -42
- package/docs/actors/04-routes.mdx +110 -137
- 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 +112 -156
- package/docs/reference/01-core.mdx +139 -0
- package/docs/reference/02-server.mdx +855 -0
- package/docs/reference/03-client.mdx +301 -0
- package/docs/reference/04-ai.mdx +722 -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 +141 -0
- package/docs/server/04-routes.mdx +243 -0
- package/docs/server/05-production.mdx +322 -0
- package/docs/server/06-guarantees.mdx +123 -0
- package/examples/playground/DEPLOY.md +53 -7
- package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
- package/examples/playground/app/a0/[appId]/page.tsx +53 -0
- package/examples/playground/app/a0/a0.css +818 -0
- package/examples/playground/app/a0/app-list.tsx +218 -0
- package/examples/playground/app/a0/app-view.tsx +63 -0
- package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
- package/examples/playground/app/a0/apps/[appId]/route.ts +28 -0
- package/examples/playground/app/a0/apps.test.ts +128 -0
- package/examples/playground/app/a0/apps.ts +102 -0
- package/examples/playground/app/a0/chat.tsx +410 -0
- package/examples/playground/app/a0/columns.tsx +109 -0
- package/examples/playground/app/a0/context-menu.tsx +116 -0
- package/examples/playground/app/a0/cookies.ts +46 -0
- package/examples/playground/app/a0/crash-on-request.ts +72 -0
- package/examples/playground/app/a0/crashes/route.ts +3 -0
- package/examples/playground/app/a0/crashes.test.ts +33 -0
- package/examples/playground/app/a0/crashes.ts +28 -0
- package/examples/playground/app/a0/devtools.tsx +164 -0
- package/examples/playground/app/a0/events/route.ts +6 -0
- package/examples/playground/app/a0/flags.ts +30 -0
- 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/layout.tsx +38 -0
- package/examples/playground/app/a0/list/route.ts +10 -0
- package/examples/playground/app/a0/model.test.ts +73 -0
- package/examples/playground/app/a0/model.ts +95 -0
- 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/page.tsx +14 -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/preview-pane.tsx +145 -0
- package/examples/playground/app/a0/preview.test.ts +53 -0
- package/examples/playground/app/a0/preview.ts +26 -0
- package/examples/playground/app/a0/sandbox-log.ts +75 -0
- package/examples/playground/app/a0/sandbox-session.ts +152 -0
- package/examples/playground/app/a0/sandbox.ts +191 -0
- package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
- package/examples/playground/app/a0/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/server.ts +213 -0
- package/examples/playground/app/a0/session.ts +59 -0
- package/examples/playground/app/a0/shell.tsx +158 -0
- package/examples/playground/app/a0/short/events/route.ts +9 -0
- package/examples/playground/app/a0/splitter.tsx +120 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
- package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
- package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
- package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
- package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
- package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
- package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
- package/examples/playground/app/agent/events/route.ts +5 -2
- package/examples/playground/app/agent/ingress.test.ts +101 -0
- package/examples/playground/app/agent/model-input.test.tsx +58 -0
- package/examples/playground/app/agent/model-input.tsx +115 -0
- package/examples/playground/app/agent/model.test.ts +73 -0
- package/examples/playground/app/agent/model.ts +58 -26
- package/examples/playground/app/agent/new-agent-session.tsx +37 -19
- package/examples/playground/app/agent/page.tsx +4 -4
- package/examples/playground/app/agent/server.ts +19 -15
- package/examples/playground/app/agent/session.ts +5 -6
- package/examples/playground/app/agent/telemetry.test.ts +189 -0
- package/examples/playground/app/agent/telemetry.ts +30 -0
- package/examples/playground/app/agent/trace.test.ts +23 -0
- package/examples/playground/app/agent/trace.ts +18 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +12 -12
- package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
- package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
- package/examples/playground/app/canvas/model.ts +53 -53
- package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
- package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
- package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
- package/examples/playground/app/chat/[chatId]/session.ts +2 -19
- package/examples/playground/app/chat/agent/route.ts +9 -0
- package/examples/playground/app/chat/model.test.ts +46 -119
- package/examples/playground/app/chat/model.ts +82 -128
- package/examples/playground/app/chat/scheduler/route.ts +2 -2
- package/examples/playground/app/chat/server.test.ts +112 -0
- package/examples/playground/app/chat/server.ts +113 -117
- package/examples/playground/app/components/activity-feed.tsx +30 -1
- package/examples/playground/app/components/activity-history.test.tsx +35 -0
- package/examples/playground/app/components/demo-icons.tsx +89 -0
- package/examples/playground/app/components/demos.tsx +33 -0
- package/examples/playground/app/components/fast-link.test.tsx +58 -0
- package/examples/playground/app/components/fast-link.tsx +55 -0
- package/examples/playground/app/components/site-header.tsx +96 -0
- package/examples/playground/app/counter/counter-client.tsx +3 -3
- package/examples/playground/app/counter/model.test.ts +3 -3
- package/examples/playground/app/counter/model.ts +11 -21
- package/examples/playground/app/counter/page.tsx +2 -15
- package/examples/playground/app/counter/session.ts +2 -2
- package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
- package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
- package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
- package/examples/playground/app/documents/[documentId]/session.ts +2 -2
- package/examples/playground/app/documents/model.ts +27 -27
- package/examples/playground/app/globals.css +420 -207
- package/examples/playground/app/initial-state.test.tsx +54 -0
- package/examples/playground/app/layout.tsx +7 -23
- package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
- package/examples/playground/app/orders/[orderId]/session.ts +2 -2
- package/examples/playground/app/orders/model.ts +49 -49
- package/examples/playground/app/page.tsx +13 -102
- package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
- package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
- package/examples/playground/app/recovery/model.ts +30 -30
- package/examples/playground/instrumentation.ts +5 -0
- package/examples/playground/next.config.ts +17 -5
- package/examples/playground/package.json +13 -2
- package/examples/playground/scripts/a0-template.ts +77 -0
- package/examples/playground/vercel.json +36 -0
- package/package.json +29 -3
- package/src/actor-client.ts +16 -4
- package/src/actor-react.ts +25 -11
- package/src/actor-shared.ts +21 -6
- package/src/actor.ts +76 -31
- package/src/ai-agent-actions.ts +219 -0
- package/src/ai-agent.ts +2517 -0
- package/src/ai-app-state.ts +91 -0
- package/src/ai-client-state.ts +6 -3
- package/src/ai-client.ts +62 -0
- package/src/ai-context-schema.ts +25 -5
- package/src/ai-context.ts +90 -40
- package/src/ai-control-server.ts +92 -72
- package/src/ai-coordinator.ts +23 -22
- package/src/ai-fx.browser.ts +5 -0
- package/src/ai-fx.ts +833 -0
- package/src/ai-harness.ts +518 -0
- package/src/ai-message-base.ts +209 -0
- package/src/ai-message-projection.ts +262 -64
- package/src/ai-model-metadata.ts +11 -6
- package/src/ai-pi.browser.ts +5 -0
- package/src/ai-pi.ts +1263 -0
- package/src/ai-progress-batches.ts +26 -0
- package/src/ai-projector.ts +95 -14
- package/src/ai-react.ts +146 -0
- package/src/ai-sdk-step.ts +2 -2
- package/src/ai-server.browser.ts +2 -2
- package/src/ai-server.ts +23 -1965
- package/src/ai-stored-state.ts +48 -163
- package/src/ai.ts +339 -155
- package/src/bootstrap-stream.ts +72 -0
- package/src/bounded-cache.ts +55 -0
- package/src/client-render-session.ts +49 -0
- package/src/client.ts +688 -151
- package/src/contract.ts +92 -42
- package/src/deterministic-id.ts +37 -0
- package/src/equal-json.ts +32 -0
- package/src/errors.ts +13 -1
- package/src/index.ts +5 -1
- package/src/initial-snapshot.ts +39 -0
- package/src/internal.ts +52 -0
- package/src/libfx.d.ts +34 -0
- package/src/postgres-notification-scope.ts +14 -3
- package/src/react.ts +171 -206
- package/src/redis-http-reply.ts +256 -0
- package/src/redis-http-stream.ts +73 -0
- package/src/redis-http-subscriptions.ts +13 -5
- package/src/reducer-runtime.ts +52 -0
- package/src/reducer.ts +93 -11
- package/src/scheduler-qstash.ts +49 -20
- package/src/scheduler-task.ts +34 -2
- package/src/scheduler-vercel.ts +32 -84
- package/src/server-fetch.ts +139 -35
- package/src/server.ts +1133 -255
- package/src/session-clear.ts +23 -0
- package/src/session-socket.ts +154 -49
- package/src/sse.ts +71 -4
- package/src/store-memory.ts +252 -75
- package/src/store-polling.ts +18 -4
- package/src/store-postgres.ts +541 -114
- package/src/store-redis-core.ts +1450 -493
- package/src/store-redis-http.ts +18 -2
- package/src/store-redis-notify.ts +262 -85
- package/src/store-redis.ts +19 -9
- package/src/store-sqlite.ts +319 -45
- package/src/store.ts +79 -1
- package/src/stream-buffer.ts +43 -0
- package/src/telemetry.ts +14 -1
- package/src/wire.ts +241 -13
- package/dist/actor-ohPC-81x.d.ts.map +0 -1
- package/dist/actor-shared-B5tJfzt-.js.map +0 -1
- package/dist/ai-CrEf6p_W.d.ts.map +0 -1
- package/dist/ai-DgOBltJ_.js.map +0 -1
- package/dist/client-BYzHjkwU.js.map +0 -1
- package/dist/client-CzyacQpJ.d.ts.map +0 -1
- package/dist/contract-CKRg_E4q.js.map +0 -1
- package/dist/errors-DCk6ch5n.js.map +0 -1
- package/dist/errors-DvhSXnxk.d.ts.map +0 -1
- package/dist/internal-Dq2qYxou.js.map +0 -1
- package/dist/react.d.ts.map +0 -1
- package/dist/reducer-DEMjEY_O.js +0 -29
- package/dist/reducer-DEMjEY_O.js.map +0 -1
- package/dist/reducer-otzHjuJj.d.ts.map +0 -1
- package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
- package/dist/server-BD5ckxZb.js.map +0 -1
- package/dist/server-DbD7IJVK.d.ts.map +0 -1
- package/dist/store-DyZM6fS5.d.ts.map +0 -1
- package/dist/store-polling-CmxUbV93.js.map +0 -1
- package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
- package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
- package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
- package/dist/validate-XKT4FSNn.js +0 -30
- package/dist/validate-XKT4FSNn.js.map +0 -1
- package/dist/wire-BO5wWCb1.js.map +0 -1
- package/docs/concepts/01-contracts.mdx +0 -142
- 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 -185
- package/docs/guides/02-cancellation.mdx +0 -161
- package/docs/guides/03-react.mdx +0 -384
- package/docs/guides/04-local-first.mdx +0 -88
- package/docs/guides/05-production.mdx +0 -521
- package/docs/guides/06-ai-agents.mdx +0 -1318
- package/docs/guides/08-application-data.mdx +0 -113
- package/docs/guides/09-presence.mdx +0 -266
- package/docs/guides/10-transports.mdx +0 -221
- package/docs/reference/01-api.mdx +0 -1896
- package/docs/reference/02-errors.mdx +0 -149
- package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
- package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
- package/examples/playground/app/agent/compaction-settings.ts +0 -49
- package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
- package/examples/playground/app/agent/compaction-timeline.ts +0 -208
- /package/docs/{concepts → contract}/meta.ts +0 -0
- /package/docs/{guides → server}/meta.ts +0 -0
package/src/store-postgres.ts
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
* assignment.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { A2Error } from './errors.ts'
|
|
19
|
+
import { A2Error, supersededAttemptError } from './errors.ts'
|
|
20
20
|
import { idempotentReplay } from './idempotent-replay.ts'
|
|
21
|
+
import { CLEAR_EVENT_TYPE, sessionClearedError } from './session-clear.ts'
|
|
21
22
|
import { decodeReturnedEventIds } from './store-codec.ts'
|
|
22
23
|
import { pollingStream, defaultSleep, type Sleeper } from './store-polling.ts'
|
|
23
24
|
import { NOTIFY_TIMINGS, nullProtoRecord } from './internal.ts'
|
|
@@ -159,6 +160,11 @@ create table if not exists a2_snapshot_pins (
|
|
|
159
160
|
create index if not exists a2_snapshot_pins_checkpoint
|
|
160
161
|
on a2_snapshot_pins (session_id, reducer_name, up_to_index);
|
|
161
162
|
|
|
163
|
+
create table if not exists a2_clears (
|
|
164
|
+
session_id text primary key,
|
|
165
|
+
clear_index bigint not null
|
|
166
|
+
);
|
|
167
|
+
|
|
162
168
|
create table if not exists a2_presence (
|
|
163
169
|
ns text not null,
|
|
164
170
|
participant text not null,
|
|
@@ -173,6 +179,17 @@ create table if not exists a2_presence (
|
|
|
173
179
|
|
|
174
180
|
const asNumber = (value: unknown): number => Number(value) // bigint columns arrive as strings
|
|
175
181
|
|
|
182
|
+
/**
|
|
183
|
+
* The session's latest clear marker, or 0, as a primary-key subquery. Read
|
|
184
|
+
* it in a statement that runs after the operation's lock: a statement's
|
|
185
|
+
* snapshot starts with the statement, so one that waited for the lock could
|
|
186
|
+
* miss a clear that committed meanwhile.
|
|
187
|
+
*/
|
|
188
|
+
const clearIndexOf = (session: string): string =>
|
|
189
|
+
`coalesce((select clear_index from a2_clears where session_id = ${session}), 0)`
|
|
190
|
+
|
|
191
|
+
const RECLAIM_BATCH = 256
|
|
192
|
+
|
|
176
193
|
const toDate = (ms: unknown): Date | null =>
|
|
177
194
|
ms === null || ms === undefined ? null : new Date(asNumber(ms))
|
|
178
195
|
|
|
@@ -440,17 +457,23 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
440
457
|
"throughIndex" bigint,
|
|
441
458
|
"snapshotThroughIndex" bigint
|
|
442
459
|
)
|
|
460
|
+
), bounded as materialized (
|
|
461
|
+
select request.*, coalesce(clears.clear_index, 0) as clear_index
|
|
462
|
+
from request
|
|
463
|
+
left join a2_clears clears on clears.session_id = request."sessionId"
|
|
443
464
|
), selected as materialized (
|
|
444
465
|
select request.position,
|
|
445
466
|
request."sessionId" as session_id,
|
|
446
467
|
request."throughIndex" as through_index,
|
|
468
|
+
request.clear_index,
|
|
447
469
|
head.up_to_index as head_index,
|
|
448
470
|
candidate.up_to_index as snapshot_index,
|
|
449
471
|
candidate.state as snapshot_state
|
|
450
|
-
from request
|
|
472
|
+
from bounded request
|
|
451
473
|
left join a2_snapshots head
|
|
452
474
|
on head.session_id = request."sessionId"
|
|
453
475
|
and head.reducer_name = $2
|
|
476
|
+
and head.up_to_index >= request.clear_index
|
|
454
477
|
left join lateral (
|
|
455
478
|
select up_to_index, state from (
|
|
456
479
|
select head.up_to_index, head.state
|
|
@@ -461,6 +484,7 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
461
484
|
from a2_snapshot_history history
|
|
462
485
|
where history.session_id = request."sessionId"
|
|
463
486
|
and history.reducer_name = $2
|
|
487
|
+
and history.up_to_index >= request.clear_index
|
|
464
488
|
and (
|
|
465
489
|
request."snapshotThroughIndex" is null
|
|
466
490
|
or history.up_to_index <= request."snapshotThroughIndex"
|
|
@@ -472,7 +496,7 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
472
496
|
)
|
|
473
497
|
select selected.position, 0 as row_order, selected.session_id,
|
|
474
498
|
selected.head_index, selected.snapshot_index,
|
|
475
|
-
selected.snapshot_state,
|
|
499
|
+
selected.snapshot_state, selected.clear_index,
|
|
476
500
|
null::bigint as idx, null::text as event_type,
|
|
477
501
|
null::jsonb as payload, null::text as event_id,
|
|
478
502
|
null::bigint as created_at
|
|
@@ -480,12 +504,15 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
480
504
|
union all
|
|
481
505
|
select selected.position, 1 as row_order, event.session_id,
|
|
482
506
|
selected.head_index, null::bigint as snapshot_index,
|
|
483
|
-
null::jsonb as snapshot_state,
|
|
507
|
+
null::jsonb as snapshot_state, selected.clear_index,
|
|
484
508
|
event.idx, event.event_type, event.payload,
|
|
485
509
|
event.event_id, event.created_at
|
|
486
510
|
from selected
|
|
487
511
|
join a2_events event on event.session_id = selected.session_id
|
|
488
|
-
where event.idx >
|
|
512
|
+
where event.idx > greatest(
|
|
513
|
+
coalesce(selected.snapshot_index, 0),
|
|
514
|
+
selected.clear_index - 1
|
|
515
|
+
)
|
|
489
516
|
and (
|
|
490
517
|
selected.through_index is null
|
|
491
518
|
or event.idx <= selected.through_index
|
|
@@ -501,6 +528,8 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
501
528
|
for (const row of rows) {
|
|
502
529
|
const read = reads[asNumber(row['position'])]!
|
|
503
530
|
if (row['row_order'] === 0 || row['row_order'] === '0') {
|
|
531
|
+
const clearIndex = asNumber(row['clear_index'])
|
|
532
|
+
if (clearIndex > 0) read.clearIndex = clearIndex
|
|
504
533
|
read.headIndex =
|
|
505
534
|
row['head_index'] === null ? null : asNumber(row['head_index'])
|
|
506
535
|
if (row['snapshot_index'] !== null) {
|
|
@@ -552,8 +581,126 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
552
581
|
})
|
|
553
582
|
}
|
|
554
583
|
|
|
584
|
+
type Query = PostgresConnection['query']
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* The rows owning `ids`. This session's rows below its clear marker do
|
|
588
|
+
* not own their ids: they are deleted here so the ids can be reused.
|
|
589
|
+
*/
|
|
590
|
+
const liveOwnersOf = async ({
|
|
591
|
+
query,
|
|
592
|
+
sessionId,
|
|
593
|
+
clearIndex,
|
|
594
|
+
ids,
|
|
595
|
+
}: {
|
|
596
|
+
query: Query
|
|
597
|
+
sessionId: string
|
|
598
|
+
clearIndex: number
|
|
599
|
+
ids: readonly string[]
|
|
600
|
+
}): Promise<Record<string, unknown>[]> => {
|
|
601
|
+
if (ids.length === 0) return []
|
|
602
|
+
const { rows } = await query(
|
|
603
|
+
'select * from a2_events where event_id = any($1::text[]) order by idx',
|
|
604
|
+
[ids],
|
|
605
|
+
)
|
|
606
|
+
const stale = rows.filter(
|
|
607
|
+
(row) =>
|
|
608
|
+
row['session_id'] === sessionId && asNumber(row['idx']) < clearIndex,
|
|
609
|
+
)
|
|
610
|
+
if (stale.length === 0) return rows
|
|
611
|
+
// Never wait on a stale row: cleared work holding it may itself be
|
|
612
|
+
// waiting for the session lock this append holds.
|
|
613
|
+
const { rows: deleted } = await query(
|
|
614
|
+
`with stale as materialized (
|
|
615
|
+
select idx from a2_events
|
|
616
|
+
where session_id = $1 and event_id = any($2::text[])
|
|
617
|
+
order by idx
|
|
618
|
+
for update skip locked
|
|
619
|
+
)
|
|
620
|
+
delete from a2_events event using stale
|
|
621
|
+
where event.session_id = $1 and event.idx = stale.idx
|
|
622
|
+
returning event.idx`,
|
|
623
|
+
[sessionId, stale.map((row) => row['event_id'])],
|
|
624
|
+
)
|
|
625
|
+
if (deleted.length < stale.length) {
|
|
626
|
+
throw new A2Error(
|
|
627
|
+
'STORE_UNAVAILABLE',
|
|
628
|
+
`an event id being reused is still held by cleared work in session '${sessionId}'`,
|
|
629
|
+
)
|
|
630
|
+
}
|
|
631
|
+
return rows.filter((row) => !stale.includes(row))
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** Whether `index` is at or below the boundary, read in a fresh statement. */
|
|
635
|
+
const clearedAt = async ({
|
|
636
|
+
query,
|
|
637
|
+
sessionId,
|
|
638
|
+
index,
|
|
639
|
+
}: {
|
|
640
|
+
query: Query
|
|
641
|
+
sessionId: string
|
|
642
|
+
index: number
|
|
643
|
+
}): Promise<boolean> => {
|
|
644
|
+
const { rows } = await query(
|
|
645
|
+
`select ${clearIndexOf('$1')} as clear_index`,
|
|
646
|
+
[sessionId],
|
|
647
|
+
)
|
|
648
|
+
return index <= asNumber(rows[0]?.['clear_index'])
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** Insert one event; its lane is ready unless an unfinished lane event of this lifetime precedes it. */
|
|
652
|
+
const insertEvent = async ({
|
|
653
|
+
query,
|
|
654
|
+
row,
|
|
655
|
+
clearIndex,
|
|
656
|
+
returning = false,
|
|
657
|
+
}: {
|
|
658
|
+
query: Query
|
|
659
|
+
row: {
|
|
660
|
+
sessionId: string
|
|
661
|
+
index: number
|
|
662
|
+
type: string
|
|
663
|
+
payload: unknown
|
|
664
|
+
id: string
|
|
665
|
+
createdAt: number
|
|
666
|
+
cause: EventCause | null
|
|
667
|
+
lane: string | null
|
|
668
|
+
processedAt: number | null
|
|
669
|
+
}
|
|
670
|
+
clearIndex: number
|
|
671
|
+
returning?: boolean
|
|
672
|
+
}): Promise<Record<string, unknown> | undefined> => {
|
|
673
|
+
const { rows } = await query(
|
|
674
|
+
`insert into a2_events
|
|
675
|
+
(session_id, idx, event_type, payload, event_id, created_at,
|
|
676
|
+
cause, lane, lane_ready, processed_at)
|
|
677
|
+
values (
|
|
678
|
+
$1, $2, $3, $4::jsonb, $5, $6, $7::jsonb, $8,
|
|
679
|
+
$8::text is null or not exists (
|
|
680
|
+
select 1 from a2_events
|
|
681
|
+
where session_id = $1 and lane = $8 and processed_at is null
|
|
682
|
+
and idx > $10
|
|
683
|
+
),
|
|
684
|
+
$9
|
|
685
|
+
)${returning ? ' returning *' : ''}`,
|
|
686
|
+
[
|
|
687
|
+
row.sessionId,
|
|
688
|
+
row.index,
|
|
689
|
+
row.type,
|
|
690
|
+
jsonb(row.payload),
|
|
691
|
+
row.id,
|
|
692
|
+
row.createdAt,
|
|
693
|
+
row.cause ? jsonb(row.cause) : null,
|
|
694
|
+
row.lane,
|
|
695
|
+
row.processedAt,
|
|
696
|
+
clearIndex,
|
|
697
|
+
],
|
|
698
|
+
)
|
|
699
|
+
return rows[0]
|
|
700
|
+
}
|
|
701
|
+
|
|
555
702
|
const store: PostgresStore = {
|
|
556
|
-
async append(sessionId, events) {
|
|
703
|
+
async append(sessionId, events, appendOptions) {
|
|
557
704
|
if (events.length === 0) {
|
|
558
705
|
return wrap(async () => {
|
|
559
706
|
const c = await client()
|
|
@@ -561,6 +708,7 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
561
708
|
`select exists(
|
|
562
709
|
select 1 from a2_events
|
|
563
710
|
where session_id = $1 and processed_at is null
|
|
711
|
+
and idx > ${clearIndexOf('$1')}
|
|
564
712
|
) as has_pending`,
|
|
565
713
|
[sessionId],
|
|
566
714
|
)
|
|
@@ -575,6 +723,23 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
575
723
|
// Serialize appends per session — index assignment must not
|
|
576
724
|
// race across writers. Transaction-scoped, self-releasing.
|
|
577
725
|
await query('select pg_advisory_xact_lock(hashtext($1))', [sessionId])
|
|
726
|
+
const { rows: statusRows } = await query(
|
|
727
|
+
`select coalesce(max(idx), 0) as max,
|
|
728
|
+
${clearIndexOf('$1')} as clear_index,
|
|
729
|
+
exists(
|
|
730
|
+
select 1 from a2_events pending
|
|
731
|
+
where pending.session_id = $1
|
|
732
|
+
and pending.processed_at is null
|
|
733
|
+
and pending.idx > ${clearIndexOf('$1')}
|
|
734
|
+
) as session_has_pending
|
|
735
|
+
from a2_events
|
|
736
|
+
where session_id = $1`,
|
|
737
|
+
[sessionId],
|
|
738
|
+
)
|
|
739
|
+
const base = asNumber(statusRows[0]?.['max'])
|
|
740
|
+
const clearIndex = asNumber(statusRows[0]?.['clear_index'])
|
|
741
|
+
const sessionHasPending =
|
|
742
|
+
statusRows[0]?.['session_has_pending'] === true
|
|
578
743
|
|
|
579
744
|
const supplied = events.filter((e) => e.id !== undefined)
|
|
580
745
|
const suppliedIds = supplied.map((e) => e.id as string)
|
|
@@ -585,18 +750,12 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
585
750
|
)
|
|
586
751
|
}
|
|
587
752
|
if (suppliedIds.length > 0) {
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
) as session_has_pending
|
|
595
|
-
from a2_events stored
|
|
596
|
-
where stored.event_id = any($1)
|
|
597
|
-
order by stored.idx`,
|
|
598
|
-
[suppliedIds, sessionId],
|
|
599
|
-
)
|
|
753
|
+
const existing = await liveOwnersOf({
|
|
754
|
+
query,
|
|
755
|
+
sessionId,
|
|
756
|
+
clearIndex,
|
|
757
|
+
ids: suppliedIds,
|
|
758
|
+
})
|
|
600
759
|
if (existing.length > 0) {
|
|
601
760
|
const foreign = existing.find(
|
|
602
761
|
(row) => row['session_id'] !== sessionId,
|
|
@@ -610,7 +769,7 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
610
769
|
if (existing.length === events.length) {
|
|
611
770
|
return {
|
|
612
771
|
events: idempotentReplay(events, existing.map(toStored)),
|
|
613
|
-
hasPending:
|
|
772
|
+
hasPending: sessionHasPending,
|
|
614
773
|
}
|
|
615
774
|
}
|
|
616
775
|
throw new A2Error(
|
|
@@ -620,6 +779,16 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
620
779
|
}
|
|
621
780
|
}
|
|
622
781
|
|
|
782
|
+
const originIndex = appendOptions?.originIndex
|
|
783
|
+
if (originIndex !== undefined && originIndex <= clearIndex) {
|
|
784
|
+
throw sessionClearedError(
|
|
785
|
+
`scheduled work from event ${originIndex} was cleared in session '${sessionId}'`,
|
|
786
|
+
)
|
|
787
|
+
}
|
|
788
|
+
for (const e of events) {
|
|
789
|
+
if (e.cause && e.cause.index <= clearIndex)
|
|
790
|
+
throw supersededAttemptError(sessionId, e.cause)
|
|
791
|
+
}
|
|
623
792
|
// Attempt-currency fence: a fresh handler append commits only
|
|
624
793
|
// while its causal attempt is still the parent's latest
|
|
625
794
|
// (store.ts `append`). `for update` serializes this read against
|
|
@@ -654,26 +823,11 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
654
823
|
asNumber(parent['attempt_count']) !== e.cause.attempt ||
|
|
655
824
|
parent['failed_at'] !== null
|
|
656
825
|
) {
|
|
657
|
-
throw
|
|
658
|
-
'SUPERSEDED_ATTEMPT',
|
|
659
|
-
`attempt ${e.cause.attempt} no longer owns event ${e.cause.index} in session '${sessionId}'`,
|
|
660
|
-
)
|
|
826
|
+
throw supersededAttemptError(sessionId, e.cause)
|
|
661
827
|
}
|
|
662
828
|
}
|
|
663
829
|
}
|
|
664
830
|
|
|
665
|
-
const { rows: maxRows } = await query(
|
|
666
|
-
`select coalesce(max(idx), 0) as max,
|
|
667
|
-
exists(
|
|
668
|
-
select 1 from a2_events pending
|
|
669
|
-
where pending.session_id = $1
|
|
670
|
-
and pending.processed_at is null
|
|
671
|
-
) as session_has_pending
|
|
672
|
-
from a2_events
|
|
673
|
-
where session_id = $1`,
|
|
674
|
-
[sessionId],
|
|
675
|
-
)
|
|
676
|
-
const base = asNumber(maxRows[0]?.['max'])
|
|
677
831
|
const now = clock.now().getTime()
|
|
678
832
|
const inserted: StoredEvent[] = []
|
|
679
833
|
for (let i = 0; i < events.length; i += 1) {
|
|
@@ -681,30 +835,21 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
681
835
|
const id = e.id ?? generateId()
|
|
682
836
|
const index = base + 1 + i
|
|
683
837
|
// oxlint-disable-next-line no-await-in-loop -- one tx, ordered inserts
|
|
684
|
-
await
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
cause, lane, lane_ready, processed_at)
|
|
688
|
-
values (
|
|
689
|
-
$1, $2, $3, $4::jsonb, $5, $6, $7::jsonb, $8,
|
|
690
|
-
$8::text is null or not exists (
|
|
691
|
-
select 1 from a2_events
|
|
692
|
-
where session_id = $1 and lane = $8 and processed_at is null
|
|
693
|
-
),
|
|
694
|
-
$9
|
|
695
|
-
)`,
|
|
696
|
-
[
|
|
838
|
+
await insertEvent({
|
|
839
|
+
query,
|
|
840
|
+
row: {
|
|
697
841
|
sessionId,
|
|
698
842
|
index,
|
|
699
|
-
e.type,
|
|
700
|
-
|
|
843
|
+
type: e.type,
|
|
844
|
+
payload: e.payload,
|
|
701
845
|
id,
|
|
702
|
-
now,
|
|
703
|
-
|
|
704
|
-
e.lane ?? null,
|
|
705
|
-
e.settled ? now : null,
|
|
706
|
-
|
|
707
|
-
|
|
846
|
+
createdAt: now,
|
|
847
|
+
cause: e.cause ?? null,
|
|
848
|
+
lane: e.lane ?? null,
|
|
849
|
+
processedAt: e.settled ? now : null,
|
|
850
|
+
},
|
|
851
|
+
clearIndex,
|
|
852
|
+
})
|
|
708
853
|
inserted.push({
|
|
709
854
|
id,
|
|
710
855
|
type: e.type,
|
|
@@ -732,7 +877,7 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
732
877
|
return {
|
|
733
878
|
events: inserted,
|
|
734
879
|
hasPending:
|
|
735
|
-
|
|
880
|
+
sessionHasPending ||
|
|
736
881
|
events.some((event) => event.settled !== true),
|
|
737
882
|
}
|
|
738
883
|
}),
|
|
@@ -752,6 +897,8 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
752
897
|
params.push(opts.throughIndex)
|
|
753
898
|
conditions.push(`idx <= $${params.length}`)
|
|
754
899
|
}
|
|
900
|
+
// Reads start at the latest clear marker.
|
|
901
|
+
conditions.push(`idx >= ${clearIndexOf('$1')}`)
|
|
755
902
|
const { rows } = await c.query(
|
|
756
903
|
`select * from a2_events where ${conditions.join(' and ')} order by idx`,
|
|
757
904
|
params,
|
|
@@ -778,6 +925,8 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
778
925
|
params.push(opts.throughIndex)
|
|
779
926
|
conditions.push(`idx <= $${params.length}`)
|
|
780
927
|
}
|
|
928
|
+
// Reads start at the latest clear marker.
|
|
929
|
+
conditions.push(`idx >= ${clearIndexOf('$1')}`)
|
|
781
930
|
const { rows } = await c.query(
|
|
782
931
|
`select session_id, idx, event_id, event_type, payload, created_at
|
|
783
932
|
from a2_events where ${conditions.join(' and ')} order by idx`,
|
|
@@ -806,6 +955,7 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
806
955
|
and event.processed_at is null
|
|
807
956
|
and event.failed_at is null
|
|
808
957
|
and event.lane_ready
|
|
958
|
+
and event.idx > ${clearIndexOf('$1')}
|
|
809
959
|
and (event.claim_expires_at is null or event.claim_expires_at <= $2)
|
|
810
960
|
and not (event.idx = any($5::bigint[]))
|
|
811
961
|
for update of event
|
|
@@ -818,12 +968,17 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
818
968
|
claim_expires_at = $4
|
|
819
969
|
from eligible
|
|
820
970
|
where claimed.session_id = $1 and claimed.idx = eligible.idx
|
|
821
|
-
returning claimed
|
|
971
|
+
returning claimed.*, ${clearIndexOf('$1')} as clear_index`,
|
|
822
972
|
[sessionId, now, holder, expiresAt, [...excludeIndexes]],
|
|
823
973
|
)
|
|
824
974
|
if (rows.length > 0) {
|
|
825
975
|
const events = rows
|
|
826
|
-
.map(
|
|
976
|
+
.map((row) => {
|
|
977
|
+
const stored = toStored(row)
|
|
978
|
+
const clearIndex = asNumber(row['clear_index'])
|
|
979
|
+
if (clearIndex > 0) stored.clearIndex = clearIndex
|
|
980
|
+
return stored
|
|
981
|
+
})
|
|
827
982
|
.toSorted((a, b) => a.index - b.index)
|
|
828
983
|
return { outcome: 'claimed', events }
|
|
829
984
|
}
|
|
@@ -834,6 +989,7 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
834
989
|
and processed_at is null
|
|
835
990
|
and failed_at is null
|
|
836
991
|
and lane_ready
|
|
992
|
+
and idx > ${clearIndexOf('$1')}
|
|
837
993
|
and claim_expires_at > $2`,
|
|
838
994
|
[sessionId, now],
|
|
839
995
|
)
|
|
@@ -865,6 +1021,7 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
865
1021
|
from listed
|
|
866
1022
|
where event.session_id = $2
|
|
867
1023
|
and event.idx = listed.idx
|
|
1024
|
+
and event.idx > ${clearIndexOf('$2')}
|
|
868
1025
|
and event.attempt_count = listed.attempt
|
|
869
1026
|
and event.processed_at is null
|
|
870
1027
|
and event.failed_at is null
|
|
@@ -872,11 +1029,13 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
872
1029
|
and event.claim_expires_at > $5
|
|
873
1030
|
returning event.idx
|
|
874
1031
|
)
|
|
875
|
-
select
|
|
876
|
-
exists(select 1 from renewed where renewed.idx =
|
|
877
|
-
as renewed
|
|
878
|
-
|
|
879
|
-
|
|
1032
|
+
select listed.idx, event.attempt_count,
|
|
1033
|
+
exists(select 1 from renewed where renewed.idx = listed.idx)
|
|
1034
|
+
as renewed,
|
|
1035
|
+
listed.idx <= ${clearIndexOf('$2')} as cleared
|
|
1036
|
+
from listed
|
|
1037
|
+
left join a2_events event
|
|
1038
|
+
on event.session_id = $2 and event.idx = listed.idx`,
|
|
880
1039
|
[
|
|
881
1040
|
expiresAt,
|
|
882
1041
|
sessionId,
|
|
@@ -892,7 +1051,9 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
892
1051
|
const index = asNumber(row['idx'])
|
|
893
1052
|
if (row['renewed'] === true) renewed.push(index)
|
|
894
1053
|
else if (
|
|
895
|
-
|
|
1054
|
+
row['cleared'] === true ||
|
|
1055
|
+
(row['attempt_count'] !== null &&
|
|
1056
|
+
asNumber(row['attempt_count']) > (attempts.get(index) ?? 0))
|
|
896
1057
|
) {
|
|
897
1058
|
superseded.push(index)
|
|
898
1059
|
}
|
|
@@ -908,13 +1069,23 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
908
1069
|
return wrap(() =>
|
|
909
1070
|
withTx(async (query) => {
|
|
910
1071
|
const { rows: parentRows } = await query(
|
|
911
|
-
`select
|
|
912
|
-
|
|
913
|
-
|
|
1072
|
+
`select boundary.clear_index, parent.*
|
|
1073
|
+
from (select ${clearIndexOf('$1')} as clear_index) boundary
|
|
1074
|
+
left join lateral (
|
|
1075
|
+
select * from a2_events
|
|
1076
|
+
where session_id = $1 and idx = $2 and idx > boundary.clear_index
|
|
1077
|
+
for update
|
|
1078
|
+
) parent on true`,
|
|
914
1079
|
[sessionId, index],
|
|
915
1080
|
)
|
|
916
|
-
|
|
1081
|
+
if (index <= asNumber(parentRows[0]?.['clear_index']))
|
|
1082
|
+
return { outcome: 'superseded' }
|
|
1083
|
+
const parent =
|
|
1084
|
+
parentRows[0]?.['idx'] == null ? undefined : parentRows[0]
|
|
917
1085
|
if (!parent) {
|
|
1086
|
+
// Deleted while this statement waited for its lock, after a clear.
|
|
1087
|
+
if (await clearedAt({ query, sessionId, index }))
|
|
1088
|
+
return { outcome: 'superseded' }
|
|
918
1089
|
throw new TypeError(
|
|
919
1090
|
`no event at index ${index} in session '${sessionId}'`,
|
|
920
1091
|
)
|
|
@@ -957,6 +1128,11 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
957
1128
|
existing.map((row) => [row['event_id'] as string, row]),
|
|
958
1129
|
)
|
|
959
1130
|
const ordered = ids.map((id) => byId.get(id))
|
|
1131
|
+
if (
|
|
1132
|
+
ordered.some((row) => !row) &&
|
|
1133
|
+
(await clearedAt({ query, sessionId, index }))
|
|
1134
|
+
)
|
|
1135
|
+
return { outcome: 'superseded' }
|
|
960
1136
|
if (
|
|
961
1137
|
ordered.some((row) => {
|
|
962
1138
|
const cause = row ? toCause(row['cause']) : null
|
|
@@ -990,12 +1166,23 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
990
1166
|
return { outcome: 'superseded' }
|
|
991
1167
|
}
|
|
992
1168
|
|
|
1169
|
+
// The parent lock came first; only a statement after the session
|
|
1170
|
+
// lock sees every clear that committed while this one waited.
|
|
993
1171
|
await query('select pg_advisory_xact_lock(hashtext($1))', [sessionId])
|
|
1172
|
+
const { rows: maxRows } = await query(
|
|
1173
|
+
`select coalesce(max(idx), 0) as max, ${clearIndexOf('$1')} as clear_index
|
|
1174
|
+
from a2_events where session_id = $1`,
|
|
1175
|
+
[sessionId],
|
|
1176
|
+
)
|
|
1177
|
+
const clearIndex = asNumber(maxRows[0]?.['clear_index'])
|
|
1178
|
+
if (index <= clearIndex) return { outcome: 'superseded' }
|
|
994
1179
|
if (ids.length > 0) {
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1180
|
+
const existing = await liveOwnersOf({
|
|
1181
|
+
query,
|
|
1182
|
+
sessionId,
|
|
1183
|
+
clearIndex,
|
|
1184
|
+
ids,
|
|
1185
|
+
})
|
|
999
1186
|
if (existing.length > 0) {
|
|
1000
1187
|
throw new A2Error(
|
|
1001
1188
|
'PARTIAL_DUPLICATE_BATCH',
|
|
@@ -1004,10 +1191,6 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1004
1191
|
}
|
|
1005
1192
|
}
|
|
1006
1193
|
|
|
1007
|
-
const { rows: maxRows } = await query(
|
|
1008
|
-
'select coalesce(max(idx), 0) as max from a2_events where session_id = $1',
|
|
1009
|
-
[sessionId],
|
|
1010
|
-
)
|
|
1011
1194
|
const base = asNumber(maxRows[0]?.['max'])
|
|
1012
1195
|
const now = clock.now().getTime()
|
|
1013
1196
|
const inserted: StoredEvent[] = []
|
|
@@ -1015,32 +1198,23 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1015
1198
|
const event = events[offset]!
|
|
1016
1199
|
const childIndex = base + offset + 1
|
|
1017
1200
|
// oxlint-disable-next-line no-await-in-loop -- one tx, ordered inserts
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
cause, lane, lane_ready, processed_at)
|
|
1022
|
-
values (
|
|
1023
|
-
$1, $2, $3, $4::jsonb, $5, $6, $7::jsonb, $8,
|
|
1024
|
-
$8::text is null or not exists (
|
|
1025
|
-
select 1 from a2_events
|
|
1026
|
-
where session_id = $1 and lane = $8 and processed_at is null
|
|
1027
|
-
),
|
|
1028
|
-
$9
|
|
1029
|
-
)
|
|
1030
|
-
returning *`,
|
|
1031
|
-
[
|
|
1201
|
+
const row = await insertEvent({
|
|
1202
|
+
query,
|
|
1203
|
+
row: {
|
|
1032
1204
|
sessionId,
|
|
1033
|
-
childIndex,
|
|
1034
|
-
event.type,
|
|
1035
|
-
|
|
1036
|
-
event.id,
|
|
1037
|
-
now,
|
|
1038
|
-
|
|
1039
|
-
event.lane ?? null,
|
|
1040
|
-
event.settled ? now : null,
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1205
|
+
index: childIndex,
|
|
1206
|
+
type: event.type,
|
|
1207
|
+
payload: event.payload,
|
|
1208
|
+
id: event.id,
|
|
1209
|
+
createdAt: now,
|
|
1210
|
+
cause: { index, attempt },
|
|
1211
|
+
lane: event.lane ?? null,
|
|
1212
|
+
processedAt: event.settled ? now : null,
|
|
1213
|
+
},
|
|
1214
|
+
clearIndex,
|
|
1215
|
+
returning: true,
|
|
1216
|
+
})
|
|
1217
|
+
inserted.push(toStored(row!))
|
|
1044
1218
|
}
|
|
1045
1219
|
await query(
|
|
1046
1220
|
`update a2_events set
|
|
@@ -1077,9 +1251,10 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1077
1251
|
and idx = (
|
|
1078
1252
|
select idx from a2_events
|
|
1079
1253
|
where session_id = $1 and lane = $2 and processed_at is null
|
|
1254
|
+
and idx > $3
|
|
1080
1255
|
order by idx limit 1
|
|
1081
1256
|
)`,
|
|
1082
|
-
[sessionId, parentLane],
|
|
1257
|
+
[sessionId, parentLane, clearIndex],
|
|
1083
1258
|
)
|
|
1084
1259
|
}
|
|
1085
1260
|
return { outcome: 'completed', events: inserted }
|
|
@@ -1094,6 +1269,7 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1094
1269
|
`with current as materialized (
|
|
1095
1270
|
select e.* from a2_events e
|
|
1096
1271
|
where e.session_id = $1 and e.idx = $2
|
|
1272
|
+
and e.idx > ${clearIndexOf('$1')}
|
|
1097
1273
|
for update of e
|
|
1098
1274
|
), updated as (
|
|
1099
1275
|
update a2_events e set
|
|
@@ -1117,6 +1293,7 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1117
1293
|
returning e.failure_count, e.failed_at
|
|
1118
1294
|
)
|
|
1119
1295
|
select case
|
|
1296
|
+
when $2 <= ${clearIndexOf('$1')} then 'cleared'
|
|
1120
1297
|
when not exists (select 1 from current) then 'missing'
|
|
1121
1298
|
when exists (
|
|
1122
1299
|
select 1 from updated where failed_at is not null
|
|
@@ -1149,7 +1326,13 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1149
1326
|
],
|
|
1150
1327
|
)
|
|
1151
1328
|
const row = rows[0]
|
|
1329
|
+
if (row?.['outcome'] === 'cleared') {
|
|
1330
|
+
return { outcome: 'superseded', failureCount: 0 }
|
|
1331
|
+
}
|
|
1152
1332
|
if (!row || row['outcome'] === 'missing') {
|
|
1333
|
+
// Deleted while this statement waited for its lock, after a clear.
|
|
1334
|
+
if (await clearedAt({ query, sessionId, index }))
|
|
1335
|
+
return { outcome: 'superseded', failureCount: 0 }
|
|
1153
1336
|
throw new TypeError(
|
|
1154
1337
|
`no event at index ${index} in session '${sessionId}'`,
|
|
1155
1338
|
)
|
|
@@ -1181,6 +1364,86 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1181
1364
|
)
|
|
1182
1365
|
},
|
|
1183
1366
|
|
|
1367
|
+
async deadLetters(sessionId) {
|
|
1368
|
+
return wrap(async () => {
|
|
1369
|
+
const c = await client()
|
|
1370
|
+
const { rows } = await c.query(
|
|
1371
|
+
`select * from a2_events
|
|
1372
|
+
where session_id = $1
|
|
1373
|
+
and processed_at is null
|
|
1374
|
+
and failed_at is not null
|
|
1375
|
+
and idx > ${clearIndexOf('$1')}
|
|
1376
|
+
order by idx`,
|
|
1377
|
+
[sessionId],
|
|
1378
|
+
)
|
|
1379
|
+
return rows.map(toStored)
|
|
1380
|
+
})
|
|
1381
|
+
},
|
|
1382
|
+
|
|
1383
|
+
async redrive({ sessionId, index, action }) {
|
|
1384
|
+
return wrap(() =>
|
|
1385
|
+
withTx(async (query) => {
|
|
1386
|
+
// Lane promotion must see every append that committed before it,
|
|
1387
|
+
// so a skip takes the session lock first, in append's order. The
|
|
1388
|
+
// row lock serializes both actions with claims and handler appends.
|
|
1389
|
+
if (action === 'skip') {
|
|
1390
|
+
await query('select pg_advisory_xact_lock(hashtext($1))', [
|
|
1391
|
+
sessionId,
|
|
1392
|
+
])
|
|
1393
|
+
}
|
|
1394
|
+
const { rows } = await query(
|
|
1395
|
+
`select lane from a2_events
|
|
1396
|
+
where session_id = $1 and idx = $2
|
|
1397
|
+
and idx > ${clearIndexOf('$1')}
|
|
1398
|
+
and processed_at is null
|
|
1399
|
+
and failed_at is not null
|
|
1400
|
+
for update`,
|
|
1401
|
+
[sessionId, index],
|
|
1402
|
+
)
|
|
1403
|
+
const row = rows[0]
|
|
1404
|
+
if (row && action === 'retry') {
|
|
1405
|
+
await query(
|
|
1406
|
+
`update a2_events set failed_at = null, failure_count = 0
|
|
1407
|
+
where session_id = $1 and idx = $2`,
|
|
1408
|
+
[sessionId, index],
|
|
1409
|
+
)
|
|
1410
|
+
} else if (row) {
|
|
1411
|
+
await query(
|
|
1412
|
+
`update a2_events set processed_at = $3, lane_ready = false
|
|
1413
|
+
where session_id = $1 and idx = $2`,
|
|
1414
|
+
[sessionId, index, clock.now().getTime()],
|
|
1415
|
+
)
|
|
1416
|
+
const lane = (row['lane'] as string | null) ?? null
|
|
1417
|
+
if (lane !== null) {
|
|
1418
|
+
await query(
|
|
1419
|
+
`update a2_events set lane_ready = true
|
|
1420
|
+
where session_id = $1
|
|
1421
|
+
and idx = (
|
|
1422
|
+
select idx from a2_events
|
|
1423
|
+
where session_id = $1 and lane = $2 and processed_at is null
|
|
1424
|
+
and idx > ${clearIndexOf('$1')}
|
|
1425
|
+
order by idx limit 1
|
|
1426
|
+
)`,
|
|
1427
|
+
[sessionId, lane],
|
|
1428
|
+
)
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
const { rows: status } = await query(
|
|
1432
|
+
`select exists(
|
|
1433
|
+
select 1 from a2_events
|
|
1434
|
+
where session_id = $1 and processed_at is null
|
|
1435
|
+
and idx > ${clearIndexOf('$1')}
|
|
1436
|
+
) as has_pending`,
|
|
1437
|
+
[sessionId],
|
|
1438
|
+
)
|
|
1439
|
+
return {
|
|
1440
|
+
redriven: row !== undefined,
|
|
1441
|
+
hasPending: status[0]?.['has_pending'] === true,
|
|
1442
|
+
}
|
|
1443
|
+
}),
|
|
1444
|
+
)
|
|
1445
|
+
},
|
|
1446
|
+
|
|
1184
1447
|
async readState(sessionId, reducerName, stateOptions) {
|
|
1185
1448
|
return wrap(
|
|
1186
1449
|
async () =>
|
|
@@ -1212,15 +1475,33 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1212
1475
|
async putSnapshots(sessionId, reducerName, writes) {
|
|
1213
1476
|
await wrap(() =>
|
|
1214
1477
|
withTx(async (query) => {
|
|
1215
|
-
const
|
|
1478
|
+
const requested = [
|
|
1216
1479
|
...new Map(writes.map((write) => [write.index, write])).values(),
|
|
1217
1480
|
].toSorted((a, b) => a.index - b.index)
|
|
1218
|
-
if (
|
|
1481
|
+
if (requested.length === 0) return
|
|
1219
1482
|
|
|
1220
1483
|
await query(
|
|
1221
1484
|
'select pg_advisory_xact_lock(hashtext($1), hashtext($2))',
|
|
1222
1485
|
[sessionId, reducerName],
|
|
1223
1486
|
)
|
|
1487
|
+
const { rows: headRows } = await query(
|
|
1488
|
+
`select boundary.clear_index, head.up_to_index, head.state, head.updated_at
|
|
1489
|
+
from (select ${clearIndexOf('$1')} as clear_index) boundary
|
|
1490
|
+
left join lateral (
|
|
1491
|
+
select up_to_index, state, updated_at from a2_snapshots
|
|
1492
|
+
where session_id = $1 and reducer_name = $2
|
|
1493
|
+
for update
|
|
1494
|
+
) head on true`,
|
|
1495
|
+
[sessionId, reducerName],
|
|
1496
|
+
)
|
|
1497
|
+
const clearIndex = asNumber(headRows[0]?.['clear_index'])
|
|
1498
|
+
// A fold from before the latest clear describes a lifetime that is gone.
|
|
1499
|
+
const normalized = requested.filter(
|
|
1500
|
+
(write) => write.index >= clearIndex,
|
|
1501
|
+
)
|
|
1502
|
+
if (normalized.length === 0) return
|
|
1503
|
+
const currentHead =
|
|
1504
|
+
headRows[0]?.['up_to_index'] == null ? undefined : headRows[0]
|
|
1224
1505
|
|
|
1225
1506
|
const requestedPins = normalized.flatMap((write) =>
|
|
1226
1507
|
(write.pinEventIndexes ?? []).map((eventIndex) => ({
|
|
@@ -1234,9 +1515,9 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1234
1515
|
]
|
|
1235
1516
|
const { rows: events } = await query(
|
|
1236
1517
|
`select idx, processed_at, failed_at from a2_events
|
|
1237
|
-
where session_id = $1 and idx = any($2::bigint[])
|
|
1518
|
+
where session_id = $1 and idx = any($2::bigint[]) and idx > $3
|
|
1238
1519
|
for update`,
|
|
1239
|
-
[sessionId, eventIndexes],
|
|
1520
|
+
[sessionId, eventIndexes, clearIndex],
|
|
1240
1521
|
)
|
|
1241
1522
|
const unfinished = new Set(
|
|
1242
1523
|
events
|
|
@@ -1264,13 +1545,6 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1264
1545
|
}
|
|
1265
1546
|
}
|
|
1266
1547
|
|
|
1267
|
-
const { rows: headRows } = await query(
|
|
1268
|
-
`select up_to_index, state, updated_at from a2_snapshots
|
|
1269
|
-
where session_id = $1 and reducer_name = $2
|
|
1270
|
-
for update`,
|
|
1271
|
-
[sessionId, reducerName],
|
|
1272
|
-
)
|
|
1273
|
-
const currentHead = headRows[0]
|
|
1274
1548
|
const latestWrite = normalized.at(-1)!
|
|
1275
1549
|
const writesHead =
|
|
1276
1550
|
!currentHead ||
|
|
@@ -1303,6 +1577,7 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1303
1577
|
if (
|
|
1304
1578
|
advancesHead &&
|
|
1305
1579
|
currentHead &&
|
|
1580
|
+
asNumber(currentHead['up_to_index']) >= clearIndex &&
|
|
1306
1581
|
pinned.has(asNumber(currentHead['up_to_index']))
|
|
1307
1582
|
) {
|
|
1308
1583
|
const index = asNumber(currentHead['up_to_index'])
|
|
@@ -1467,10 +1742,12 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1467
1742
|
const readAfter = (afterIndex: number): Promise<Event[]> =>
|
|
1468
1743
|
wrap(async () => {
|
|
1469
1744
|
const c = await client()
|
|
1745
|
+
// A cursor below the latest marker resumes at it.
|
|
1470
1746
|
const { rows } = await c.query(
|
|
1471
1747
|
`select session_id, idx, event_type, payload, event_id, created_at
|
|
1472
1748
|
from a2_events
|
|
1473
1749
|
where session_id = $1 and idx > $2
|
|
1750
|
+
and idx >= ${clearIndexOf('$1')}
|
|
1474
1751
|
order by idx`,
|
|
1475
1752
|
[sessionId, afterIndex],
|
|
1476
1753
|
)
|
|
@@ -1488,6 +1765,156 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1488
1765
|
)
|
|
1489
1766
|
},
|
|
1490
1767
|
|
|
1768
|
+
async clear({ sessionId, ifLastIndex, cause }) {
|
|
1769
|
+
return wrap(() =>
|
|
1770
|
+
withTx(async (query) => {
|
|
1771
|
+
await query('select pg_advisory_xact_lock(hashtext($1))', [sessionId])
|
|
1772
|
+
const { rows: headRows } = await query(
|
|
1773
|
+
`select coalesce(max(idx), 0) as head,
|
|
1774
|
+
${clearIndexOf('$1')} as clear_index
|
|
1775
|
+
from a2_events where session_id = $1`,
|
|
1776
|
+
[sessionId],
|
|
1777
|
+
)
|
|
1778
|
+
const head = asNumber(headRows[0]?.['head'])
|
|
1779
|
+
const clearIndex = asNumber(headRows[0]?.['clear_index'])
|
|
1780
|
+
if (cause) {
|
|
1781
|
+
if (cause.index <= clearIndex)
|
|
1782
|
+
throw supersededAttemptError(sessionId, cause)
|
|
1783
|
+
const { rows: parents } = await query(
|
|
1784
|
+
`select attempt_count, failed_at from a2_events
|
|
1785
|
+
where session_id = $1 and idx = $2
|
|
1786
|
+
for update`,
|
|
1787
|
+
[sessionId, cause.index],
|
|
1788
|
+
)
|
|
1789
|
+
const parent = parents[0]
|
|
1790
|
+
if (!parent) {
|
|
1791
|
+
throw new TypeError(
|
|
1792
|
+
`no event at index ${cause.index} in session '${sessionId}'`,
|
|
1793
|
+
)
|
|
1794
|
+
}
|
|
1795
|
+
if (
|
|
1796
|
+
asNumber(parent['attempt_count']) !== cause.attempt ||
|
|
1797
|
+
parent['failed_at'] !== null
|
|
1798
|
+
) {
|
|
1799
|
+
throw supersededAttemptError(sessionId, cause)
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
if (ifLastIndex !== undefined && clearIndex === ifLastIndex + 1) {
|
|
1803
|
+
return { cleared: true, index: head, clearIndex }
|
|
1804
|
+
}
|
|
1805
|
+
if (ifLastIndex !== undefined && ifLastIndex !== head) {
|
|
1806
|
+
return { cleared: false, index: head, clearIndex }
|
|
1807
|
+
}
|
|
1808
|
+
if (head === 0 || clearIndex === head) {
|
|
1809
|
+
return { cleared: true, index: head, clearIndex }
|
|
1810
|
+
}
|
|
1811
|
+
const marker = head + 1
|
|
1812
|
+
const now = clock.now().getTime()
|
|
1813
|
+
await insertEvent({
|
|
1814
|
+
query,
|
|
1815
|
+
row: {
|
|
1816
|
+
sessionId,
|
|
1817
|
+
index: marker,
|
|
1818
|
+
type: CLEAR_EVENT_TYPE,
|
|
1819
|
+
payload: null,
|
|
1820
|
+
id: generateId(),
|
|
1821
|
+
createdAt: now,
|
|
1822
|
+
cause: null,
|
|
1823
|
+
lane: null,
|
|
1824
|
+
processedAt: now,
|
|
1825
|
+
},
|
|
1826
|
+
clearIndex,
|
|
1827
|
+
})
|
|
1828
|
+
// Releasing a clearing handler's claim makes a claim waiting on its
|
|
1829
|
+
// row skip it once this commits, instead of re-running the handler.
|
|
1830
|
+
await query(
|
|
1831
|
+
`with released as (
|
|
1832
|
+
update a2_events
|
|
1833
|
+
set lane_ready = false, claim_holder = null, claim_expires_at = null
|
|
1834
|
+
where session_id = $1 and idx = $3
|
|
1835
|
+
)
|
|
1836
|
+
insert into a2_clears (session_id, clear_index) values ($1, $2)
|
|
1837
|
+
on conflict (session_id) do update set clear_index = excluded.clear_index`,
|
|
1838
|
+
[sessionId, marker, cause?.index ?? null],
|
|
1839
|
+
)
|
|
1840
|
+
return { cleared: true, index: marker, clearIndex: marker }
|
|
1841
|
+
}),
|
|
1842
|
+
)
|
|
1843
|
+
},
|
|
1844
|
+
|
|
1845
|
+
async reclaim(sessionId) {
|
|
1846
|
+
return wrap(() =>
|
|
1847
|
+
withTx(async (query) => {
|
|
1848
|
+
// Rows other work holds are skipped, never waited on, so deletion
|
|
1849
|
+
// cannot join a deadlock; a later batch takes them.
|
|
1850
|
+
const { rows } = await query(
|
|
1851
|
+
`with boundary as materialized (
|
|
1852
|
+
select ${clearIndexOf('$1')} as clear_index
|
|
1853
|
+
), batch as materialized (
|
|
1854
|
+
select idx from a2_events
|
|
1855
|
+
where session_id = $1
|
|
1856
|
+
and idx < (select clear_index from boundary)
|
|
1857
|
+
order by idx
|
|
1858
|
+
limit $2
|
|
1859
|
+
for update skip locked
|
|
1860
|
+
), deleted as (
|
|
1861
|
+
delete from a2_events event using batch
|
|
1862
|
+
where event.session_id = $1 and event.idx = batch.idx
|
|
1863
|
+
returning event.idx
|
|
1864
|
+
), pins as (
|
|
1865
|
+
delete from a2_snapshot_pins pin using deleted
|
|
1866
|
+
where pin.session_id = $1 and pin.event_index = deleted.idx
|
|
1867
|
+
)
|
|
1868
|
+
select (select count(*) from deleted) as deleted,
|
|
1869
|
+
(select clear_index from boundary) as clear_index`,
|
|
1870
|
+
[sessionId, RECLAIM_BATCH],
|
|
1871
|
+
)
|
|
1872
|
+
const deleted = asNumber(rows[0]?.['deleted'])
|
|
1873
|
+
const clearIndex = asNumber(rows[0]?.['clear_index'])
|
|
1874
|
+
if (clearIndex === 0) return { deleted: 0, done: true }
|
|
1875
|
+
if (deleted > 0) return { deleted, done: false }
|
|
1876
|
+
const { rows: remaining } = await query(
|
|
1877
|
+
'select 1 from a2_events where session_id = $1 and idx < $2 limit 1',
|
|
1878
|
+
[sessionId, clearIndex],
|
|
1879
|
+
)
|
|
1880
|
+
if (remaining.length > 0) return { deleted: 0, done: false }
|
|
1881
|
+
const { rows: swept } = await query(
|
|
1882
|
+
`with pins as materialized (
|
|
1883
|
+
select ctid from a2_snapshot_pins
|
|
1884
|
+
where session_id = $1 and up_to_index < $2
|
|
1885
|
+
for update skip locked
|
|
1886
|
+
), history as materialized (
|
|
1887
|
+
select ctid from a2_snapshot_history
|
|
1888
|
+
where session_id = $1 and up_to_index < $2
|
|
1889
|
+
for update skip locked
|
|
1890
|
+
), heads as materialized (
|
|
1891
|
+
select ctid from a2_snapshots
|
|
1892
|
+
where session_id = $1 and up_to_index < $2
|
|
1893
|
+
for update skip locked
|
|
1894
|
+
), gone_pins as (
|
|
1895
|
+
delete from a2_snapshot_pins target using pins where target.ctid = pins.ctid
|
|
1896
|
+
), gone_history as (
|
|
1897
|
+
delete from a2_snapshot_history target using history
|
|
1898
|
+
where target.ctid = history.ctid
|
|
1899
|
+
), gone_heads as (
|
|
1900
|
+
delete from a2_snapshots target using heads where target.ctid = heads.ctid
|
|
1901
|
+
)
|
|
1902
|
+
select (select count(*) from a2_snapshot_pins
|
|
1903
|
+
where session_id = $1 and up_to_index < $2)
|
|
1904
|
+
+ (select count(*) from a2_snapshot_history
|
|
1905
|
+
where session_id = $1 and up_to_index < $2)
|
|
1906
|
+
+ (select count(*) from a2_snapshots
|
|
1907
|
+
where session_id = $1 and up_to_index < $2)
|
|
1908
|
+
- (select count(*) from pins)
|
|
1909
|
+
- (select count(*) from history)
|
|
1910
|
+
- (select count(*) from heads) as skipped`,
|
|
1911
|
+
[sessionId, clearIndex],
|
|
1912
|
+
)
|
|
1913
|
+
return { deleted: 0, done: asNumber(swept[0]?.['skipped']) === 0 }
|
|
1914
|
+
}),
|
|
1915
|
+
)
|
|
1916
|
+
},
|
|
1917
|
+
|
|
1491
1918
|
async close() {
|
|
1492
1919
|
if (closed) return
|
|
1493
1920
|
closed = true
|