experimental-a2 0.16.1 → 0.17.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 +352 -0
- package/dist/{actor-ohPC-81x.d.ts → actor-DBbe5xeE.d.ts} +21 -11
- package/dist/actor-DBbe5xeE.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-UrWfKeuv.js} +621 -216
- package/dist/ai-UrWfKeuv.js.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-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 +80 -36
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +432 -258
- package/dist/ai-server.js.map +1 -1
- package/dist/{ai-CrEf6p_W.d.ts → ai-vpGtcaCB.d.ts} +114 -100
- package/dist/ai-vpGtcaCB.d.ts.map +1 -0
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/{client-BYzHjkwU.js → client-COaiaDub.js} +447 -80
- package/dist/client-COaiaDub.js.map +1 -0
- package/dist/{client-CzyacQpJ.d.ts → client-qRnRUzBS.d.ts} +14 -13
- package/dist/client-qRnRUzBS.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-BrLso5x_.d.ts} +7 -3
- package/dist/initial-snapshot-BrLso5x_.d.ts.map +1 -0
- package/dist/{internal-Dq2qYxou.js → internal-CbVvEwad.js} +23 -2
- package/dist/internal-CbVvEwad.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/react-BbJ-g6Uu.d.ts +86 -0
- package/dist/react-BbJ-g6Uu.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-BYu6DT7h.d.ts} +45 -29
- package/dist/reducer-BYu6DT7h.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-DbD7IJVK.d.ts → server-CGM8WzRG.d.ts} +87 -26
- package/dist/server-CGM8WzRG.d.ts.map +1 -0
- package/dist/{server-BD5ckxZb.js → server-Rj64fNvX.js} +854 -202
- package/dist/server-Rj64fNvX.js.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
- package/dist/session-clear-DGI4jTQ5.js.map +1 -0
- package/dist/session-clear-Dshpuhff.d.ts +12 -0
- package/dist/session-clear-Dshpuhff.d.ts.map +1 -0
- package/dist/{store-DyZM6fS5.d.ts → store-Gpkq7WBo.d.ts} +44 -3
- package/dist/store-Gpkq7WBo.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 +182 -69
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-CmxUbV93.js → store-polling-4_y8lETm.js} +21 -4
- package/dist/store-polling-4_y8lETm.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 +399 -108
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-Q1C-k2dd.js} +3 -3
- package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-Q1C-k2dd.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-tqWNnMRu.js +2374 -0
- package/dist/store-redis-notify-tqWNnMRu.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 +166 -32
- 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-cZH-cibj.d.ts} +2 -2
- package/dist/telemetry-cZH-cibj.d.ts.map +1 -0
- package/dist/{wire-BO5wWCb1.js → wire-DjfHOvdW.js} +132 -10
- package/dist/wire-DjfHOvdW.js.map +1 -0
- package/docs/01-quickstart.mdx +2 -2
- package/docs/actors/01-introduction.mdx +4 -0
- package/docs/actors/04-routes.mdx +14 -1
- package/docs/concepts/01-contracts.mdx +95 -0
- package/docs/concepts/04-state.mdx +3 -3
- package/docs/guides/01-timers.mdx +22 -14
- package/docs/guides/02-cancellation.mdx +1 -1
- package/docs/guides/03-react.mdx +268 -328
- package/docs/guides/04-local-first.mdx +32 -73
- package/docs/guides/05-production.mdx +40 -27
- package/docs/guides/06-ai-agents.mdx +671 -581
- package/docs/guides/09-presence.mdx +3 -7
- package/docs/guides/10-transports.mdx +5 -5
- package/docs/index.mdx +10 -18
- package/docs/reference/01-api.mdx +656 -194
- package/docs/reference/02-errors.mdx +2 -1
- package/examples/playground/DEPLOY.md +52 -6
- package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
- package/examples/playground/app/a0/[appId]/page.tsx +48 -0
- package/examples/playground/app/a0/a0.css +818 -0
- package/examples/playground/app/a0/app-list.tsx +211 -0
- package/examples/playground/app/a0/app-view.tsx +55 -0
- package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
- package/examples/playground/app/a0/apps/[appId]/route.ts +23 -0
- package/examples/playground/app/a0/apps.test.ts +116 -0
- package/examples/playground/app/a0/apps.ts +87 -0
- package/examples/playground/app/a0/chat.tsx +364 -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/layout.tsx +38 -0
- package/examples/playground/app/a0/list/route.ts +10 -0
- package/examples/playground/app/a0/model.test.ts +56 -0
- package/examples/playground/app/a0/model.ts +58 -0
- package/examples/playground/app/a0/page.tsx +14 -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 +69 -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 +158 -0
- package/examples/playground/app/a0/session.ts +47 -0
- package/examples/playground/app/a0/shell.tsx +150 -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 +98 -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 +15 -14
- package/examples/playground/app/agent/session.ts +5 -6
- package/examples/playground/app/agent/telemetry.test.ts +168 -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 +11 -11
- 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 +108 -0
- package/examples/playground/app/chat/server.ts +69 -73
- 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/package.json +10 -2
- package/examples/playground/scripts/a0-template.ts +77 -0
- package/examples/playground/vercel.json +18 -0
- package/package.json +3 -1
- 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 +2343 -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-message-base.ts +209 -0
- package/src/ai-message-projection.ts +262 -64
- 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-server.browser.ts +2 -2
- package/src/ai-server.ts +20 -1964
- 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 +45 -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 +92 -10
- 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 +957 -237
- 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 +220 -78
- package/src/store-polling.ts +18 -4
- package/src/store-postgres.ts +461 -114
- package/src/store-redis-core.ts +1260 -434
- package/src/store-redis-http.ts +18 -2
- package/src/store-redis-notify.ts +260 -85
- package/src/store-redis.ts +19 -9
- package/src/store-sqlite.ts +259 -45
- package/src/store.ts +46 -1
- package/src/stream-buffer.ts +43 -0
- package/src/telemetry.ts +13 -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/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/dist/store-postgres.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { n as pollingStream, t as defaultSleep } from "./store-polling-
|
|
1
|
+
import { i as A2Error, r as sessionClearedError, s as supersededAttemptError, t as CLEAR_EVENT_TYPE } from "./session-clear-DGI4jTQ5.js";
|
|
2
|
+
import { g as nullProtoRecord, r as NOTIFY_TIMINGS } from "./internal-CbVvEwad.js";
|
|
3
|
+
import { n as pollingStream, t as defaultSleep } from "./store-polling-4_y8lETm.js";
|
|
4
4
|
import { r as platformWaitUntil } from "./platform-B4TnJtWu.js";
|
|
5
5
|
import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
|
|
6
|
-
import { t as idempotentReplay } from "./idempotent-replay-
|
|
6
|
+
import { t as idempotentReplay } from "./idempotent-replay-JdgMQRRm.js";
|
|
7
7
|
import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS.js";
|
|
8
8
|
import { t as decodeReturnedEventIds } from "./store-codec-DTG0Ftek.js";
|
|
9
|
-
import { t as pollPresence } from "./store-presence-polling-
|
|
9
|
+
import { t as pollPresence } from "./store-presence-polling-Q1C-k2dd.js";
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
//#region src/postgres-notifications.ts
|
|
12
12
|
const postgresNotificationChannel = ({ kind, sessionId }) => `a2_${kind}_${createHash("md5").update(sessionId).digest("hex")}`;
|
|
@@ -232,11 +232,14 @@ function postgresNotificationScope(load) {
|
|
|
232
232
|
let stopped = false;
|
|
233
233
|
let after = startAfter;
|
|
234
234
|
let buffer = [];
|
|
235
|
+
let position = 0;
|
|
235
236
|
let version = 0;
|
|
236
237
|
let waiting;
|
|
237
238
|
let lease;
|
|
238
239
|
const stop = () => {
|
|
239
240
|
stopped = true;
|
|
241
|
+
buffer = [];
|
|
242
|
+
position = 0;
|
|
240
243
|
waiting?.cancel();
|
|
241
244
|
lease?.stop();
|
|
242
245
|
state.stops.delete(stop);
|
|
@@ -262,20 +265,28 @@ function postgresNotificationScope(load) {
|
|
|
262
265
|
done: true,
|
|
263
266
|
value: void 0
|
|
264
267
|
};
|
|
265
|
-
const row = buffer
|
|
268
|
+
const row = buffer[position];
|
|
266
269
|
if (row) {
|
|
267
270
|
after = row.index;
|
|
271
|
+
buffer[position] = void 0;
|
|
272
|
+
position += 1;
|
|
273
|
+
if (position === buffer.length) {
|
|
274
|
+
buffer = [];
|
|
275
|
+
position = 0;
|
|
276
|
+
}
|
|
268
277
|
return {
|
|
269
278
|
done: false,
|
|
270
279
|
value: row
|
|
271
280
|
};
|
|
272
281
|
}
|
|
273
282
|
const before = version;
|
|
274
|
-
|
|
283
|
+
const rows = await read(after);
|
|
275
284
|
if (stopped || state.closed) return {
|
|
276
285
|
done: true,
|
|
277
286
|
value: void 0
|
|
278
287
|
};
|
|
288
|
+
buffer = rows;
|
|
289
|
+
position = 0;
|
|
279
290
|
if (buffer.length || before !== version) continue;
|
|
280
291
|
waiting = defaultSleep(NOTIFY_TIMINGS.safetyReadMs);
|
|
281
292
|
await waiting.promise;
|
|
@@ -563,6 +574,11 @@ create table if not exists a2_snapshot_pins (
|
|
|
563
574
|
create index if not exists a2_snapshot_pins_checkpoint
|
|
564
575
|
on a2_snapshot_pins (session_id, reducer_name, up_to_index);
|
|
565
576
|
|
|
577
|
+
create table if not exists a2_clears (
|
|
578
|
+
session_id text primary key,
|
|
579
|
+
clear_index bigint not null
|
|
580
|
+
);
|
|
581
|
+
|
|
566
582
|
create table if not exists a2_presence (
|
|
567
583
|
ns text not null,
|
|
568
584
|
participant text not null,
|
|
@@ -575,6 +591,14 @@ create table if not exists a2_presence (
|
|
|
575
591
|
);
|
|
576
592
|
`;
|
|
577
593
|
const asNumber = (value) => Number(value);
|
|
594
|
+
/**
|
|
595
|
+
* The session's latest clear marker, or 0, as a primary-key subquery. Read
|
|
596
|
+
* it in a statement that runs after the operation's lock: a statement's
|
|
597
|
+
* snapshot starts with the statement, so one that waited for the lock could
|
|
598
|
+
* miss a clear that committed meanwhile.
|
|
599
|
+
*/
|
|
600
|
+
const clearIndexOf = (session) => `coalesce((select clear_index from a2_clears where session_id = ${session}), 0)`;
|
|
601
|
+
const RECLAIM_BATCH = 256;
|
|
578
602
|
const toDate = (ms) => ms === null || ms === void 0 ? null : new Date(asNumber(ms));
|
|
579
603
|
const toCause = (raw) => {
|
|
580
604
|
if (raw === null || raw === void 0) return null;
|
|
@@ -755,17 +779,23 @@ function postgres(options = {}) {
|
|
|
755
779
|
"throughIndex" bigint,
|
|
756
780
|
"snapshotThroughIndex" bigint
|
|
757
781
|
)
|
|
782
|
+
), bounded as materialized (
|
|
783
|
+
select request.*, coalesce(clears.clear_index, 0) as clear_index
|
|
784
|
+
from request
|
|
785
|
+
left join a2_clears clears on clears.session_id = request."sessionId"
|
|
758
786
|
), selected as materialized (
|
|
759
787
|
select request.position,
|
|
760
788
|
request."sessionId" as session_id,
|
|
761
789
|
request."throughIndex" as through_index,
|
|
790
|
+
request.clear_index,
|
|
762
791
|
head.up_to_index as head_index,
|
|
763
792
|
candidate.up_to_index as snapshot_index,
|
|
764
793
|
candidate.state as snapshot_state
|
|
765
|
-
from request
|
|
794
|
+
from bounded request
|
|
766
795
|
left join a2_snapshots head
|
|
767
796
|
on head.session_id = request."sessionId"
|
|
768
797
|
and head.reducer_name = $2
|
|
798
|
+
and head.up_to_index >= request.clear_index
|
|
769
799
|
left join lateral (
|
|
770
800
|
select up_to_index, state from (
|
|
771
801
|
select head.up_to_index, head.state
|
|
@@ -776,6 +806,7 @@ function postgres(options = {}) {
|
|
|
776
806
|
from a2_snapshot_history history
|
|
777
807
|
where history.session_id = request."sessionId"
|
|
778
808
|
and history.reducer_name = $2
|
|
809
|
+
and history.up_to_index >= request.clear_index
|
|
779
810
|
and (
|
|
780
811
|
request."snapshotThroughIndex" is null
|
|
781
812
|
or history.up_to_index <= request."snapshotThroughIndex"
|
|
@@ -787,7 +818,7 @@ function postgres(options = {}) {
|
|
|
787
818
|
)
|
|
788
819
|
select selected.position, 0 as row_order, selected.session_id,
|
|
789
820
|
selected.head_index, selected.snapshot_index,
|
|
790
|
-
selected.snapshot_state,
|
|
821
|
+
selected.snapshot_state, selected.clear_index,
|
|
791
822
|
null::bigint as idx, null::text as event_type,
|
|
792
823
|
null::jsonb as payload, null::text as event_id,
|
|
793
824
|
null::bigint as created_at
|
|
@@ -795,12 +826,15 @@ function postgres(options = {}) {
|
|
|
795
826
|
union all
|
|
796
827
|
select selected.position, 1 as row_order, event.session_id,
|
|
797
828
|
selected.head_index, null::bigint as snapshot_index,
|
|
798
|
-
null::jsonb as snapshot_state,
|
|
829
|
+
null::jsonb as snapshot_state, selected.clear_index,
|
|
799
830
|
event.idx, event.event_type, event.payload,
|
|
800
831
|
event.event_id, event.created_at
|
|
801
832
|
from selected
|
|
802
833
|
join a2_events event on event.session_id = selected.session_id
|
|
803
|
-
where event.idx >
|
|
834
|
+
where event.idx > greatest(
|
|
835
|
+
coalesce(selected.snapshot_index, 0),
|
|
836
|
+
selected.clear_index - 1
|
|
837
|
+
)
|
|
804
838
|
and (
|
|
805
839
|
selected.through_index is null
|
|
806
840
|
or event.idx <= selected.through_index
|
|
@@ -814,6 +848,8 @@ function postgres(options = {}) {
|
|
|
814
848
|
for (const row of rows) {
|
|
815
849
|
const read = reads[asNumber(row["position"])];
|
|
816
850
|
if (row["row_order"] === 0 || row["row_order"] === "0") {
|
|
851
|
+
const clearIndex = asNumber(row["clear_index"]);
|
|
852
|
+
if (clearIndex > 0) read.clearIndex = clearIndex;
|
|
817
853
|
read.headIndex = row["head_index"] === null ? null : asNumber(row["head_index"]);
|
|
818
854
|
if (row["snapshot_index"] !== null) read.snapshot = {
|
|
819
855
|
index: asNumber(row["snapshot_index"]),
|
|
@@ -854,12 +890,66 @@ function postgres(options = {}) {
|
|
|
854
890
|
}
|
|
855
891
|
});
|
|
856
892
|
};
|
|
893
|
+
/**
|
|
894
|
+
* The rows owning `ids`. This session's rows below its clear marker do
|
|
895
|
+
* not own their ids: they are deleted here so the ids can be reused.
|
|
896
|
+
*/
|
|
897
|
+
const liveOwnersOf = async ({ query, sessionId, clearIndex, ids }) => {
|
|
898
|
+
if (ids.length === 0) return [];
|
|
899
|
+
const { rows } = await query("select * from a2_events where event_id = any($1::text[]) order by idx", [ids]);
|
|
900
|
+
const stale = rows.filter((row) => row["session_id"] === sessionId && asNumber(row["idx"]) < clearIndex);
|
|
901
|
+
if (stale.length === 0) return rows;
|
|
902
|
+
const { rows: deleted } = await query(`with stale as materialized (
|
|
903
|
+
select idx from a2_events
|
|
904
|
+
where session_id = $1 and event_id = any($2::text[])
|
|
905
|
+
order by idx
|
|
906
|
+
for update skip locked
|
|
907
|
+
)
|
|
908
|
+
delete from a2_events event using stale
|
|
909
|
+
where event.session_id = $1 and event.idx = stale.idx
|
|
910
|
+
returning event.idx`, [sessionId, stale.map((row) => row["event_id"])]);
|
|
911
|
+
if (deleted.length < stale.length) throw new A2Error("STORE_UNAVAILABLE", `an event id being reused is still held by cleared work in session '${sessionId}'`);
|
|
912
|
+
return rows.filter((row) => !stale.includes(row));
|
|
913
|
+
};
|
|
914
|
+
/** Whether `index` is at or below the boundary, read in a fresh statement. */
|
|
915
|
+
const clearedAt = async ({ query, sessionId, index }) => {
|
|
916
|
+
const { rows } = await query(`select ${clearIndexOf("$1")} as clear_index`, [sessionId]);
|
|
917
|
+
return index <= asNumber(rows[0]?.["clear_index"]);
|
|
918
|
+
};
|
|
919
|
+
/** Insert one event; its lane is ready unless an unfinished lane event of this lifetime precedes it. */
|
|
920
|
+
const insertEvent = async ({ query, row, clearIndex, returning = false }) => {
|
|
921
|
+
const { rows } = await query(`insert into a2_events
|
|
922
|
+
(session_id, idx, event_type, payload, event_id, created_at,
|
|
923
|
+
cause, lane, lane_ready, processed_at)
|
|
924
|
+
values (
|
|
925
|
+
$1, $2, $3, $4::jsonb, $5, $6, $7::jsonb, $8,
|
|
926
|
+
$8::text is null or not exists (
|
|
927
|
+
select 1 from a2_events
|
|
928
|
+
where session_id = $1 and lane = $8 and processed_at is null
|
|
929
|
+
and idx > $10
|
|
930
|
+
),
|
|
931
|
+
$9
|
|
932
|
+
)${returning ? " returning *" : ""}`, [
|
|
933
|
+
row.sessionId,
|
|
934
|
+
row.index,
|
|
935
|
+
row.type,
|
|
936
|
+
jsonb(row.payload),
|
|
937
|
+
row.id,
|
|
938
|
+
row.createdAt,
|
|
939
|
+
row.cause ? jsonb(row.cause) : null,
|
|
940
|
+
row.lane,
|
|
941
|
+
row.processedAt,
|
|
942
|
+
clearIndex
|
|
943
|
+
]);
|
|
944
|
+
return rows[0];
|
|
945
|
+
};
|
|
857
946
|
const store = {
|
|
858
|
-
async append(sessionId, events) {
|
|
947
|
+
async append(sessionId, events, appendOptions) {
|
|
859
948
|
if (events.length === 0) return wrap(async () => {
|
|
860
949
|
const { rows } = await (await client()).query(`select exists(
|
|
861
950
|
select 1 from a2_events
|
|
862
951
|
where session_id = $1 and processed_at is null
|
|
952
|
+
and idx > ${clearIndexOf("$1")}
|
|
863
953
|
) as has_pending`, [sessionId]);
|
|
864
954
|
return {
|
|
865
955
|
events: [],
|
|
@@ -868,28 +958,41 @@ function postgres(options = {}) {
|
|
|
868
958
|
});
|
|
869
959
|
return wrap(() => withTx(async (query) => {
|
|
870
960
|
await query("select pg_advisory_xact_lock(hashtext($1))", [sessionId]);
|
|
961
|
+
const { rows: statusRows } = await query(`select coalesce(max(idx), 0) as max,
|
|
962
|
+
${clearIndexOf("$1")} as clear_index,
|
|
963
|
+
exists(
|
|
964
|
+
select 1 from a2_events pending
|
|
965
|
+
where pending.session_id = $1
|
|
966
|
+
and pending.processed_at is null
|
|
967
|
+
and pending.idx > ${clearIndexOf("$1")}
|
|
968
|
+
) as session_has_pending
|
|
969
|
+
from a2_events
|
|
970
|
+
where session_id = $1`, [sessionId]);
|
|
971
|
+
const base = asNumber(statusRows[0]?.["max"]);
|
|
972
|
+
const clearIndex = asNumber(statusRows[0]?.["clear_index"]);
|
|
973
|
+
const sessionHasPending = statusRows[0]?.["session_has_pending"] === true;
|
|
871
974
|
const suppliedIds = events.filter((e) => e.id !== void 0).map((e) => e.id);
|
|
872
975
|
if (new Set(suppliedIds).size !== suppliedIds.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "batch contains the same event id more than once");
|
|
873
976
|
if (suppliedIds.length > 0) {
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
from a2_events stored
|
|
881
|
-
where stored.event_id = any($1)
|
|
882
|
-
order by stored.idx`, [suppliedIds, sessionId]);
|
|
977
|
+
const existing = await liveOwnersOf({
|
|
978
|
+
query,
|
|
979
|
+
sessionId,
|
|
980
|
+
clearIndex,
|
|
981
|
+
ids: suppliedIds
|
|
982
|
+
});
|
|
883
983
|
if (existing.length > 0) {
|
|
884
984
|
const foreign = existing.find((row) => row["session_id"] !== sessionId);
|
|
885
985
|
if (foreign) throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${String(foreign["event_id"])}' already exists in another session`);
|
|
886
986
|
if (existing.length === events.length) return {
|
|
887
987
|
events: idempotentReplay(events, existing.map(toStored)),
|
|
888
|
-
hasPending:
|
|
988
|
+
hasPending: sessionHasPending
|
|
889
989
|
};
|
|
890
990
|
throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`);
|
|
891
991
|
}
|
|
892
992
|
}
|
|
993
|
+
const originIndex = appendOptions?.originIndex;
|
|
994
|
+
if (originIndex !== void 0 && originIndex <= clearIndex) throw sessionClearedError(`scheduled work from event ${originIndex} was cleared in session '${sessionId}'`);
|
|
995
|
+
for (const e of events) if (e.cause && e.cause.index <= clearIndex) throw supersededAttemptError(sessionId, e.cause);
|
|
893
996
|
const causeIndexes = [...new Set(events.filter((e) => e.cause !== void 0).map((e) => e.cause.index))];
|
|
894
997
|
if (causeIndexes.length > 0) {
|
|
895
998
|
const { rows: parents } = await query(`select idx, attempt_count, failed_at from a2_events
|
|
@@ -900,45 +1003,30 @@ function postgres(options = {}) {
|
|
|
900
1003
|
if (!e.cause) continue;
|
|
901
1004
|
const parent = byIndex.get(e.cause.index);
|
|
902
1005
|
if (!parent) throw new TypeError(`no event at index ${e.cause.index} in session '${sessionId}'`);
|
|
903
|
-
if (asNumber(parent["attempt_count"]) !== e.cause.attempt || parent["failed_at"] !== null) throw
|
|
1006
|
+
if (asNumber(parent["attempt_count"]) !== e.cause.attempt || parent["failed_at"] !== null) throw supersededAttemptError(sessionId, e.cause);
|
|
904
1007
|
}
|
|
905
1008
|
}
|
|
906
|
-
const { rows: maxRows } = await query(`select coalesce(max(idx), 0) as max,
|
|
907
|
-
exists(
|
|
908
|
-
select 1 from a2_events pending
|
|
909
|
-
where pending.session_id = $1
|
|
910
|
-
and pending.processed_at is null
|
|
911
|
-
) as session_has_pending
|
|
912
|
-
from a2_events
|
|
913
|
-
where session_id = $1`, [sessionId]);
|
|
914
|
-
const base = asNumber(maxRows[0]?.["max"]);
|
|
915
1009
|
const now = clock.now().getTime();
|
|
916
1010
|
const inserted = [];
|
|
917
1011
|
for (let i = 0; i < events.length; i += 1) {
|
|
918
1012
|
const e = events[i];
|
|
919
1013
|
const id = e.id ?? generateId();
|
|
920
1014
|
const index = base + 1 + i;
|
|
921
|
-
await
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
id,
|
|
937
|
-
now,
|
|
938
|
-
e.cause ? jsonb(e.cause) : null,
|
|
939
|
-
e.lane ?? null,
|
|
940
|
-
e.settled ? now : null
|
|
941
|
-
]);
|
|
1015
|
+
await insertEvent({
|
|
1016
|
+
query,
|
|
1017
|
+
row: {
|
|
1018
|
+
sessionId,
|
|
1019
|
+
index,
|
|
1020
|
+
type: e.type,
|
|
1021
|
+
payload: e.payload,
|
|
1022
|
+
id,
|
|
1023
|
+
createdAt: now,
|
|
1024
|
+
cause: e.cause ?? null,
|
|
1025
|
+
lane: e.lane ?? null,
|
|
1026
|
+
processedAt: e.settled ? now : null
|
|
1027
|
+
},
|
|
1028
|
+
clearIndex
|
|
1029
|
+
});
|
|
942
1030
|
inserted.push({
|
|
943
1031
|
id,
|
|
944
1032
|
type: e.type,
|
|
@@ -965,7 +1053,7 @@ function postgres(options = {}) {
|
|
|
965
1053
|
}
|
|
966
1054
|
return {
|
|
967
1055
|
events: inserted,
|
|
968
|
-
hasPending:
|
|
1056
|
+
hasPending: sessionHasPending || events.some((event) => event.settled !== true)
|
|
969
1057
|
};
|
|
970
1058
|
}));
|
|
971
1059
|
},
|
|
@@ -982,6 +1070,7 @@ function postgres(options = {}) {
|
|
|
982
1070
|
params.push(opts.throughIndex);
|
|
983
1071
|
conditions.push(`idx <= $${params.length}`);
|
|
984
1072
|
}
|
|
1073
|
+
conditions.push(`idx >= ${clearIndexOf("$1")}`);
|
|
985
1074
|
const { rows } = await c.query(`select * from a2_events where ${conditions.join(" and ")} order by idx`, params);
|
|
986
1075
|
return rows.map(toStored);
|
|
987
1076
|
});
|
|
@@ -1000,6 +1089,7 @@ function postgres(options = {}) {
|
|
|
1000
1089
|
params.push(opts.throughIndex);
|
|
1001
1090
|
conditions.push(`idx <= $${params.length}`);
|
|
1002
1091
|
}
|
|
1092
|
+
conditions.push(`idx >= ${clearIndexOf("$1")}`);
|
|
1003
1093
|
const { rows } = await c.query(`select session_id, idx, event_id, event_type, payload, created_at
|
|
1004
1094
|
from a2_events where ${conditions.join(" and ")} order by idx`, params);
|
|
1005
1095
|
return rows.map(toEvent);
|
|
@@ -1008,6 +1098,7 @@ function postgres(options = {}) {
|
|
|
1008
1098
|
async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs, excludeIndexes = [] }) {
|
|
1009
1099
|
return wrap(() => withTx(async (query) => {
|
|
1010
1100
|
const now = clock.now().getTime();
|
|
1101
|
+
const expiresAt = expiresAtMs ?? now + ttlMs;
|
|
1011
1102
|
const { rows } = await query(`with eligible as materialized (
|
|
1012
1103
|
select event.idx
|
|
1013
1104
|
from a2_events event
|
|
@@ -1015,6 +1106,7 @@ function postgres(options = {}) {
|
|
|
1015
1106
|
and event.processed_at is null
|
|
1016
1107
|
and event.failed_at is null
|
|
1017
1108
|
and event.lane_ready
|
|
1109
|
+
and event.idx > ${clearIndexOf("$1")}
|
|
1018
1110
|
and (event.claim_expires_at is null or event.claim_expires_at <= $2)
|
|
1019
1111
|
and not (event.idx = any($5::bigint[]))
|
|
1020
1112
|
for update of event
|
|
@@ -1027,16 +1119,21 @@ function postgres(options = {}) {
|
|
|
1027
1119
|
claim_expires_at = $4
|
|
1028
1120
|
from eligible
|
|
1029
1121
|
where claimed.session_id = $1 and claimed.idx = eligible.idx
|
|
1030
|
-
returning claimed
|
|
1122
|
+
returning claimed.*, ${clearIndexOf("$1")} as clear_index`, [
|
|
1031
1123
|
sessionId,
|
|
1032
1124
|
now,
|
|
1033
1125
|
holder,
|
|
1034
|
-
|
|
1126
|
+
expiresAt,
|
|
1035
1127
|
[...excludeIndexes]
|
|
1036
1128
|
]);
|
|
1037
1129
|
if (rows.length > 0) return {
|
|
1038
1130
|
outcome: "claimed",
|
|
1039
|
-
events: rows.map(
|
|
1131
|
+
events: rows.map((row) => {
|
|
1132
|
+
const stored = toStored(row);
|
|
1133
|
+
const clearIndex = asNumber(row["clear_index"]);
|
|
1134
|
+
if (clearIndex > 0) stored.clearIndex = clearIndex;
|
|
1135
|
+
return stored;
|
|
1136
|
+
}).toSorted((a, b) => a.index - b.index)
|
|
1040
1137
|
};
|
|
1041
1138
|
const { rows: activeRows } = await query(`select min(claim_expires_at) as retry_at
|
|
1042
1139
|
from a2_events
|
|
@@ -1044,6 +1141,7 @@ function postgres(options = {}) {
|
|
|
1044
1141
|
and processed_at is null
|
|
1045
1142
|
and failed_at is null
|
|
1046
1143
|
and lane_ready
|
|
1144
|
+
and idx > ${clearIndexOf("$1")}
|
|
1047
1145
|
and claim_expires_at > $2`, [sessionId, now]);
|
|
1048
1146
|
const retryAt = activeRows[0]?.["retry_at"];
|
|
1049
1147
|
return retryAt === null || retryAt === void 0 ? { outcome: "settled" } : {
|
|
@@ -1072,6 +1170,7 @@ function postgres(options = {}) {
|
|
|
1072
1170
|
from listed
|
|
1073
1171
|
where event.session_id = $2
|
|
1074
1172
|
and event.idx = listed.idx
|
|
1173
|
+
and event.idx > ${clearIndexOf("$2")}
|
|
1075
1174
|
and event.attempt_count = listed.attempt
|
|
1076
1175
|
and event.processed_at is null
|
|
1077
1176
|
and event.failed_at is null
|
|
@@ -1079,11 +1178,13 @@ function postgres(options = {}) {
|
|
|
1079
1178
|
and event.claim_expires_at > $5
|
|
1080
1179
|
returning event.idx
|
|
1081
1180
|
)
|
|
1082
|
-
select
|
|
1083
|
-
exists(select 1 from renewed where renewed.idx =
|
|
1084
|
-
as renewed
|
|
1085
|
-
|
|
1086
|
-
|
|
1181
|
+
select listed.idx, event.attempt_count,
|
|
1182
|
+
exists(select 1 from renewed where renewed.idx = listed.idx)
|
|
1183
|
+
as renewed,
|
|
1184
|
+
listed.idx <= ${clearIndexOf("$2")} as cleared
|
|
1185
|
+
from listed
|
|
1186
|
+
left join a2_events event
|
|
1187
|
+
on event.session_id = $2 and event.idx = listed.idx`, [
|
|
1087
1188
|
expiresAt,
|
|
1088
1189
|
sessionId,
|
|
1089
1190
|
indexes,
|
|
@@ -1096,7 +1197,7 @@ function postgres(options = {}) {
|
|
|
1096
1197
|
for (const row of rows) {
|
|
1097
1198
|
const index = asNumber(row["idx"]);
|
|
1098
1199
|
if (row["renewed"] === true) renewed.push(index);
|
|
1099
|
-
else if (asNumber(row["attempt_count"]) > (attempts.get(index) ?? 0)) superseded.push(index);
|
|
1200
|
+
else if (row["cleared"] === true || row["attempt_count"] !== null && asNumber(row["attempt_count"]) > (attempts.get(index) ?? 0)) superseded.push(index);
|
|
1100
1201
|
}
|
|
1101
1202
|
return {
|
|
1102
1203
|
renewed: renewed.toSorted((a, b) => a - b),
|
|
@@ -1106,11 +1207,23 @@ function postgres(options = {}) {
|
|
|
1106
1207
|
},
|
|
1107
1208
|
async completeAttempt({ sessionId, index, attempt, events }) {
|
|
1108
1209
|
return wrap(() => withTx(async (query) => {
|
|
1109
|
-
const { rows: parentRows } = await query(`select
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1210
|
+
const { rows: parentRows } = await query(`select boundary.clear_index, parent.*
|
|
1211
|
+
from (select ${clearIndexOf("$1")} as clear_index) boundary
|
|
1212
|
+
left join lateral (
|
|
1213
|
+
select * from a2_events
|
|
1214
|
+
where session_id = $1 and idx = $2 and idx > boundary.clear_index
|
|
1215
|
+
for update
|
|
1216
|
+
) parent on true`, [sessionId, index]);
|
|
1217
|
+
if (index <= asNumber(parentRows[0]?.["clear_index"])) return { outcome: "superseded" };
|
|
1218
|
+
const parent = parentRows[0]?.["idx"] == null ? void 0 : parentRows[0];
|
|
1219
|
+
if (!parent) {
|
|
1220
|
+
if (await clearedAt({
|
|
1221
|
+
query,
|
|
1222
|
+
sessionId,
|
|
1223
|
+
index
|
|
1224
|
+
})) return { outcome: "superseded" };
|
|
1225
|
+
throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
|
|
1226
|
+
}
|
|
1114
1227
|
const ids = events.map((event) => event.id);
|
|
1115
1228
|
if (new Set(ids).size !== ids.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains the same event id more than once");
|
|
1116
1229
|
if (parent["processed_at"] !== null) {
|
|
@@ -1125,6 +1238,11 @@ function postgres(options = {}) {
|
|
|
1125
1238
|
where event_id = any($1::text[])`, [ids]);
|
|
1126
1239
|
const byId = new Map(existing.map((row) => [row["event_id"], row]));
|
|
1127
1240
|
const ordered = ids.map((id) => byId.get(id));
|
|
1241
|
+
if (ordered.some((row) => !row) && await clearedAt({
|
|
1242
|
+
query,
|
|
1243
|
+
sessionId,
|
|
1244
|
+
index
|
|
1245
|
+
})) return { outcome: "superseded" };
|
|
1128
1246
|
if (ordered.some((row) => {
|
|
1129
1247
|
const cause = row ? toCause(row["cause"]) : null;
|
|
1130
1248
|
return !row || row["session_id"] !== sessionId || cause?.index !== index || cause.attempt !== attempt;
|
|
@@ -1136,42 +1254,44 @@ function postgres(options = {}) {
|
|
|
1136
1254
|
}
|
|
1137
1255
|
if (asNumber(parent["attempt_count"]) !== attempt || parent["claim_holder"] === null || parent["claim_holder"] === void 0 || parent["failed_at"] !== null) return { outcome: "superseded" };
|
|
1138
1256
|
await query("select pg_advisory_xact_lock(hashtext($1))", [sessionId]);
|
|
1257
|
+
const { rows: maxRows } = await query(`select coalesce(max(idx), 0) as max, ${clearIndexOf("$1")} as clear_index
|
|
1258
|
+
from a2_events where session_id = $1`, [sessionId]);
|
|
1259
|
+
const clearIndex = asNumber(maxRows[0]?.["clear_index"]);
|
|
1260
|
+
if (index <= clearIndex) return { outcome: "superseded" };
|
|
1139
1261
|
if (ids.length > 0) {
|
|
1140
|
-
|
|
1141
|
-
|
|
1262
|
+
if ((await liveOwnersOf({
|
|
1263
|
+
query,
|
|
1264
|
+
sessionId,
|
|
1265
|
+
clearIndex,
|
|
1266
|
+
ids
|
|
1267
|
+
})).length > 0) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains an already-appended event id");
|
|
1142
1268
|
}
|
|
1143
|
-
const { rows: maxRows } = await query("select coalesce(max(idx), 0) as max from a2_events where session_id = $1", [sessionId]);
|
|
1144
1269
|
const base = asNumber(maxRows[0]?.["max"]);
|
|
1145
1270
|
const now = clock.now().getTime();
|
|
1146
1271
|
const inserted = [];
|
|
1147
1272
|
for (let offset = 0; offset < events.length; offset += 1) {
|
|
1148
1273
|
const event = events[offset];
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
}),
|
|
1171
|
-
event.lane ?? null,
|
|
1172
|
-
event.settled ? now : null
|
|
1173
|
-
]);
|
|
1174
|
-
inserted.push(toStored(rows[0]));
|
|
1274
|
+
const childIndex = base + offset + 1;
|
|
1275
|
+
const row = await insertEvent({
|
|
1276
|
+
query,
|
|
1277
|
+
row: {
|
|
1278
|
+
sessionId,
|
|
1279
|
+
index: childIndex,
|
|
1280
|
+
type: event.type,
|
|
1281
|
+
payload: event.payload,
|
|
1282
|
+
id: event.id,
|
|
1283
|
+
createdAt: now,
|
|
1284
|
+
cause: {
|
|
1285
|
+
index,
|
|
1286
|
+
attempt
|
|
1287
|
+
},
|
|
1288
|
+
lane: event.lane ?? null,
|
|
1289
|
+
processedAt: event.settled ? now : null
|
|
1290
|
+
},
|
|
1291
|
+
clearIndex,
|
|
1292
|
+
returning: true
|
|
1293
|
+
});
|
|
1294
|
+
inserted.push(toStored(row));
|
|
1175
1295
|
}
|
|
1176
1296
|
await query(`update a2_events set
|
|
1177
1297
|
processed_at = $1,
|
|
@@ -1203,8 +1323,13 @@ function postgres(options = {}) {
|
|
|
1203
1323
|
and idx = (
|
|
1204
1324
|
select idx from a2_events
|
|
1205
1325
|
where session_id = $1 and lane = $2 and processed_at is null
|
|
1326
|
+
and idx > $3
|
|
1206
1327
|
order by idx limit 1
|
|
1207
|
-
)`, [
|
|
1328
|
+
)`, [
|
|
1329
|
+
sessionId,
|
|
1330
|
+
parentLane,
|
|
1331
|
+
clearIndex
|
|
1332
|
+
]);
|
|
1208
1333
|
return {
|
|
1209
1334
|
outcome: "completed",
|
|
1210
1335
|
events: inserted
|
|
@@ -1216,6 +1341,7 @@ function postgres(options = {}) {
|
|
|
1216
1341
|
const { rows } = await query(`with current as materialized (
|
|
1217
1342
|
select e.* from a2_events e
|
|
1218
1343
|
where e.session_id = $1 and e.idx = $2
|
|
1344
|
+
and e.idx > ${clearIndexOf("$1")}
|
|
1219
1345
|
for update of e
|
|
1220
1346
|
), updated as (
|
|
1221
1347
|
update a2_events e set
|
|
@@ -1239,6 +1365,7 @@ function postgres(options = {}) {
|
|
|
1239
1365
|
returning e.failure_count, e.failed_at
|
|
1240
1366
|
)
|
|
1241
1367
|
select case
|
|
1368
|
+
when $2 <= ${clearIndexOf("$1")} then 'cleared'
|
|
1242
1369
|
when not exists (select 1 from current) then 'missing'
|
|
1243
1370
|
when exists (
|
|
1244
1371
|
select 1 from updated where failed_at is not null
|
|
@@ -1269,7 +1396,21 @@ function postgres(options = {}) {
|
|
|
1269
1396
|
clock.now().getTime()
|
|
1270
1397
|
]);
|
|
1271
1398
|
const row = rows[0];
|
|
1272
|
-
if (
|
|
1399
|
+
if (row?.["outcome"] === "cleared") return {
|
|
1400
|
+
outcome: "superseded",
|
|
1401
|
+
failureCount: 0
|
|
1402
|
+
};
|
|
1403
|
+
if (!row || row["outcome"] === "missing") {
|
|
1404
|
+
if (await clearedAt({
|
|
1405
|
+
query,
|
|
1406
|
+
sessionId,
|
|
1407
|
+
index
|
|
1408
|
+
})) return {
|
|
1409
|
+
outcome: "superseded",
|
|
1410
|
+
failureCount: 0
|
|
1411
|
+
};
|
|
1412
|
+
throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
|
|
1413
|
+
}
|
|
1273
1414
|
if (row["outcome"] === "dead_lettered") {
|
|
1274
1415
|
await query(`delete from a2_snapshot_pins
|
|
1275
1416
|
where session_id = $1 and event_index = $2`, [sessionId, index]);
|
|
@@ -1300,17 +1441,32 @@ function postgres(options = {}) {
|
|
|
1300
1441
|
},
|
|
1301
1442
|
async putSnapshots(sessionId, reducerName, writes) {
|
|
1302
1443
|
await wrap(() => withTx(async (query) => {
|
|
1303
|
-
const
|
|
1304
|
-
if (
|
|
1444
|
+
const requested = [...new Map(writes.map((write) => [write.index, write])).values()].toSorted((a, b) => a.index - b.index);
|
|
1445
|
+
if (requested.length === 0) return;
|
|
1305
1446
|
await query("select pg_advisory_xact_lock(hashtext($1), hashtext($2))", [sessionId, reducerName]);
|
|
1447
|
+
const { rows: headRows } = await query(`select boundary.clear_index, head.up_to_index, head.state, head.updated_at
|
|
1448
|
+
from (select ${clearIndexOf("$1")} as clear_index) boundary
|
|
1449
|
+
left join lateral (
|
|
1450
|
+
select up_to_index, state, updated_at from a2_snapshots
|
|
1451
|
+
where session_id = $1 and reducer_name = $2
|
|
1452
|
+
for update
|
|
1453
|
+
) head on true`, [sessionId, reducerName]);
|
|
1454
|
+
const clearIndex = asNumber(headRows[0]?.["clear_index"]);
|
|
1455
|
+
const normalized = requested.filter((write) => write.index >= clearIndex);
|
|
1456
|
+
if (normalized.length === 0) return;
|
|
1457
|
+
const currentHead = headRows[0]?.["up_to_index"] == null ? void 0 : headRows[0];
|
|
1306
1458
|
const requestedPins = normalized.flatMap((write) => (write.pinEventIndexes ?? []).map((eventIndex) => ({
|
|
1307
1459
|
eventIndex,
|
|
1308
1460
|
snapshotIndex: write.index
|
|
1309
1461
|
})));
|
|
1310
1462
|
if (requestedPins.length > 0) {
|
|
1311
1463
|
const { rows: events } = await query(`select idx, processed_at, failed_at from a2_events
|
|
1312
|
-
where session_id = $1 and idx = any($2::bigint[])
|
|
1313
|
-
for update`, [
|
|
1464
|
+
where session_id = $1 and idx = any($2::bigint[]) and idx > $3
|
|
1465
|
+
for update`, [
|
|
1466
|
+
sessionId,
|
|
1467
|
+
[...new Set(requestedPins.map((pin) => pin.eventIndex))],
|
|
1468
|
+
clearIndex
|
|
1469
|
+
]);
|
|
1314
1470
|
const unfinished = new Set(events.filter((row) => row["processed_at"] === null && row["failed_at"] === null).map((row) => asNumber(row["idx"])));
|
|
1315
1471
|
const accepted = requestedPins.filter((pin) => unfinished.has(pin.eventIndex));
|
|
1316
1472
|
if (accepted.length > 0) await query(`insert into a2_snapshot_pins
|
|
@@ -1326,10 +1482,6 @@ function postgres(options = {}) {
|
|
|
1326
1482
|
jsonb(accepted)
|
|
1327
1483
|
]);
|
|
1328
1484
|
}
|
|
1329
|
-
const { rows: headRows } = await query(`select up_to_index, state, updated_at from a2_snapshots
|
|
1330
|
-
where session_id = $1 and reducer_name = $2
|
|
1331
|
-
for update`, [sessionId, reducerName]);
|
|
1332
|
-
const currentHead = headRows[0];
|
|
1333
1485
|
const latestWrite = normalized.at(-1);
|
|
1334
1486
|
const writesHead = !currentHead || latestWrite.index >= asNumber(currentHead["up_to_index"]);
|
|
1335
1487
|
const advancesHead = !currentHead || latestWrite.index > asNumber(currentHead["up_to_index"]);
|
|
@@ -1343,7 +1495,7 @@ function postgres(options = {}) {
|
|
|
1343
1495
|
]);
|
|
1344
1496
|
const pinned = new Set(pinRows.map((row) => asNumber(row["up_to_index"])));
|
|
1345
1497
|
const historical = /* @__PURE__ */ new Map();
|
|
1346
|
-
if (advancesHead && currentHead && pinned.has(asNumber(currentHead["up_to_index"]))) {
|
|
1498
|
+
if (advancesHead && currentHead && asNumber(currentHead["up_to_index"]) >= clearIndex && pinned.has(asNumber(currentHead["up_to_index"]))) {
|
|
1347
1499
|
const index = asNumber(currentHead["up_to_index"]);
|
|
1348
1500
|
historical.set(index, {
|
|
1349
1501
|
index,
|
|
@@ -1476,6 +1628,7 @@ function postgres(options = {}) {
|
|
|
1476
1628
|
const { rows } = await (await client()).query(`select session_id, idx, event_type, payload, event_id, created_at
|
|
1477
1629
|
from a2_events
|
|
1478
1630
|
where session_id = $1 and idx > $2
|
|
1631
|
+
and idx >= ${clearIndexOf("$1")}
|
|
1479
1632
|
order by idx`, [sessionId, afterIndex]);
|
|
1480
1633
|
return rows.map(toEvent);
|
|
1481
1634
|
});
|
|
@@ -1489,6 +1642,144 @@ function postgres(options = {}) {
|
|
|
1489
1642
|
});
|
|
1490
1643
|
return pollingStream(readAfter, opts?.startAfter !== void 0 ? { startAfter: opts.startAfter } : {});
|
|
1491
1644
|
},
|
|
1645
|
+
async clear({ sessionId, ifLastIndex, cause }) {
|
|
1646
|
+
return wrap(() => withTx(async (query) => {
|
|
1647
|
+
await query("select pg_advisory_xact_lock(hashtext($1))", [sessionId]);
|
|
1648
|
+
const { rows: headRows } = await query(`select coalesce(max(idx), 0) as head,
|
|
1649
|
+
${clearIndexOf("$1")} as clear_index
|
|
1650
|
+
from a2_events where session_id = $1`, [sessionId]);
|
|
1651
|
+
const head = asNumber(headRows[0]?.["head"]);
|
|
1652
|
+
const clearIndex = asNumber(headRows[0]?.["clear_index"]);
|
|
1653
|
+
if (cause) {
|
|
1654
|
+
if (cause.index <= clearIndex) throw supersededAttemptError(sessionId, cause);
|
|
1655
|
+
const { rows: parents } = await query(`select attempt_count, failed_at from a2_events
|
|
1656
|
+
where session_id = $1 and idx = $2
|
|
1657
|
+
for update`, [sessionId, cause.index]);
|
|
1658
|
+
const parent = parents[0];
|
|
1659
|
+
if (!parent) throw new TypeError(`no event at index ${cause.index} in session '${sessionId}'`);
|
|
1660
|
+
if (asNumber(parent["attempt_count"]) !== cause.attempt || parent["failed_at"] !== null) throw supersededAttemptError(sessionId, cause);
|
|
1661
|
+
}
|
|
1662
|
+
if (ifLastIndex !== void 0 && clearIndex === ifLastIndex + 1) return {
|
|
1663
|
+
cleared: true,
|
|
1664
|
+
index: head,
|
|
1665
|
+
clearIndex
|
|
1666
|
+
};
|
|
1667
|
+
if (ifLastIndex !== void 0 && ifLastIndex !== head) return {
|
|
1668
|
+
cleared: false,
|
|
1669
|
+
index: head,
|
|
1670
|
+
clearIndex
|
|
1671
|
+
};
|
|
1672
|
+
if (head === 0 || clearIndex === head) return {
|
|
1673
|
+
cleared: true,
|
|
1674
|
+
index: head,
|
|
1675
|
+
clearIndex
|
|
1676
|
+
};
|
|
1677
|
+
const marker = head + 1;
|
|
1678
|
+
const now = clock.now().getTime();
|
|
1679
|
+
await insertEvent({
|
|
1680
|
+
query,
|
|
1681
|
+
row: {
|
|
1682
|
+
sessionId,
|
|
1683
|
+
index: marker,
|
|
1684
|
+
type: CLEAR_EVENT_TYPE,
|
|
1685
|
+
payload: null,
|
|
1686
|
+
id: generateId(),
|
|
1687
|
+
createdAt: now,
|
|
1688
|
+
cause: null,
|
|
1689
|
+
lane: null,
|
|
1690
|
+
processedAt: now
|
|
1691
|
+
},
|
|
1692
|
+
clearIndex
|
|
1693
|
+
});
|
|
1694
|
+
await query(`with released as (
|
|
1695
|
+
update a2_events
|
|
1696
|
+
set lane_ready = false, claim_holder = null, claim_expires_at = null
|
|
1697
|
+
where session_id = $1 and idx = $3
|
|
1698
|
+
)
|
|
1699
|
+
insert into a2_clears (session_id, clear_index) values ($1, $2)
|
|
1700
|
+
on conflict (session_id) do update set clear_index = excluded.clear_index`, [
|
|
1701
|
+
sessionId,
|
|
1702
|
+
marker,
|
|
1703
|
+
cause?.index ?? null
|
|
1704
|
+
]);
|
|
1705
|
+
return {
|
|
1706
|
+
cleared: true,
|
|
1707
|
+
index: marker,
|
|
1708
|
+
clearIndex: marker
|
|
1709
|
+
};
|
|
1710
|
+
}));
|
|
1711
|
+
},
|
|
1712
|
+
async reclaim(sessionId) {
|
|
1713
|
+
return wrap(() => withTx(async (query) => {
|
|
1714
|
+
const { rows } = await query(`with boundary as materialized (
|
|
1715
|
+
select ${clearIndexOf("$1")} as clear_index
|
|
1716
|
+
), batch as materialized (
|
|
1717
|
+
select idx from a2_events
|
|
1718
|
+
where session_id = $1
|
|
1719
|
+
and idx < (select clear_index from boundary)
|
|
1720
|
+
order by idx
|
|
1721
|
+
limit $2
|
|
1722
|
+
for update skip locked
|
|
1723
|
+
), deleted as (
|
|
1724
|
+
delete from a2_events event using batch
|
|
1725
|
+
where event.session_id = $1 and event.idx = batch.idx
|
|
1726
|
+
returning event.idx
|
|
1727
|
+
), pins as (
|
|
1728
|
+
delete from a2_snapshot_pins pin using deleted
|
|
1729
|
+
where pin.session_id = $1 and pin.event_index = deleted.idx
|
|
1730
|
+
)
|
|
1731
|
+
select (select count(*) from deleted) as deleted,
|
|
1732
|
+
(select clear_index from boundary) as clear_index`, [sessionId, RECLAIM_BATCH]);
|
|
1733
|
+
const deleted = asNumber(rows[0]?.["deleted"]);
|
|
1734
|
+
const clearIndex = asNumber(rows[0]?.["clear_index"]);
|
|
1735
|
+
if (clearIndex === 0) return {
|
|
1736
|
+
deleted: 0,
|
|
1737
|
+
done: true
|
|
1738
|
+
};
|
|
1739
|
+
if (deleted > 0) return {
|
|
1740
|
+
deleted,
|
|
1741
|
+
done: false
|
|
1742
|
+
};
|
|
1743
|
+
const { rows: remaining } = await query("select 1 from a2_events where session_id = $1 and idx < $2 limit 1", [sessionId, clearIndex]);
|
|
1744
|
+
if (remaining.length > 0) return {
|
|
1745
|
+
deleted: 0,
|
|
1746
|
+
done: false
|
|
1747
|
+
};
|
|
1748
|
+
const { rows: swept } = await query(`with pins as materialized (
|
|
1749
|
+
select ctid from a2_snapshot_pins
|
|
1750
|
+
where session_id = $1 and up_to_index < $2
|
|
1751
|
+
for update skip locked
|
|
1752
|
+
), history as materialized (
|
|
1753
|
+
select ctid from a2_snapshot_history
|
|
1754
|
+
where session_id = $1 and up_to_index < $2
|
|
1755
|
+
for update skip locked
|
|
1756
|
+
), heads as materialized (
|
|
1757
|
+
select ctid from a2_snapshots
|
|
1758
|
+
where session_id = $1 and up_to_index < $2
|
|
1759
|
+
for update skip locked
|
|
1760
|
+
), gone_pins as (
|
|
1761
|
+
delete from a2_snapshot_pins target using pins where target.ctid = pins.ctid
|
|
1762
|
+
), gone_history as (
|
|
1763
|
+
delete from a2_snapshot_history target using history
|
|
1764
|
+
where target.ctid = history.ctid
|
|
1765
|
+
), gone_heads as (
|
|
1766
|
+
delete from a2_snapshots target using heads where target.ctid = heads.ctid
|
|
1767
|
+
)
|
|
1768
|
+
select (select count(*) from a2_snapshot_pins
|
|
1769
|
+
where session_id = $1 and up_to_index < $2)
|
|
1770
|
+
+ (select count(*) from a2_snapshot_history
|
|
1771
|
+
where session_id = $1 and up_to_index < $2)
|
|
1772
|
+
+ (select count(*) from a2_snapshots
|
|
1773
|
+
where session_id = $1 and up_to_index < $2)
|
|
1774
|
+
- (select count(*) from pins)
|
|
1775
|
+
- (select count(*) from history)
|
|
1776
|
+
- (select count(*) from heads) as skipped`, [sessionId, clearIndex]);
|
|
1777
|
+
return {
|
|
1778
|
+
deleted: 0,
|
|
1779
|
+
done: asNumber(swept[0]?.["skipped"]) === 0
|
|
1780
|
+
};
|
|
1781
|
+
}));
|
|
1782
|
+
},
|
|
1492
1783
|
async close() {
|
|
1493
1784
|
if (closed) return;
|
|
1494
1785
|
closed = true;
|