experimental-a2 0.19.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/README.md +1 -1
- package/dist/{actor-C7b-1arn.d.ts → actor-HnGmrOZZ.d.ts} +5 -5
- package/dist/{actor-C7b-1arn.d.ts.map → actor-HnGmrOZZ.d.ts.map} +1 -1
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-client.js +2 -2
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +5 -5
- package/dist/{actor-shared-t7w8xIKP.js → actor-shared-DFET5q16.js} +3 -3
- package/dist/{actor-shared-t7w8xIKP.js.map → actor-shared-DFET5q16.js.map} +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +54 -43
- package/dist/actor.js.map +1 -1
- package/dist/{ai-Cz3hqVhk.d.ts → ai-CvEnqVfo.d.ts} +8 -8
- package/dist/{ai-Cz3hqVhk.d.ts.map → ai-CvEnqVfo.d.ts.map} +1 -1
- package/dist/{ai-mMiWul1T.js → ai-CzE672Q3.js} +9 -9
- package/dist/ai-CzE672Q3.js.map +1 -0
- package/dist/ai-client.d.ts +2 -2
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.d.ts +1 -1
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +36 -9
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-Bfk-Lycn.d.ts} +2 -2
- package/dist/ai-harness-Bfk-Lycn.d.ts.map +1 -0
- package/dist/{ai-harness-BuuC-gJ3.js → ai-harness-dQeJaR1t.js} +17 -4
- package/dist/ai-harness-dQeJaR1t.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.js +4 -4
- package/dist/ai-pi.js.map +1 -1
- package/dist/ai-react.d.ts +5 -5
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +14 -17
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +84 -75
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/{client-BhHLoNKF.js → client-9IeTsiAs.js} +5 -5
- package/dist/{client-BhHLoNKF.js.map → client-9IeTsiAs.js.map} +1 -1
- package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
- package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
- package/dist/contract-CWukt01d.js.map +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
- package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
- package/dist/{internal-CscSY7K_.js → internal-Bc4PvfHk.js} +10 -3
- package/dist/internal-Bc4PvfHk.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/otel.js +1 -1
- package/dist/otel.js.map +1 -1
- package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
- package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
- package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
- package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
- package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
- package/dist/runtime-consumer-BWmht60z.js +12 -0
- package/dist/runtime-consumer-BWmht60z.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.d.ts.map +1 -1
- package/dist/scheduler-qstash.js +7 -7
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-task-X1SToDjP.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +5 -4
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-DYAATFuT.d.ts → server-BshFZ0eL.d.ts} +108 -41
- package/dist/server-BshFZ0eL.d.ts.map +1 -0
- package/dist/{server-D-Y4pqwu.js → server-DvIB8kj5.js} +423 -135
- package/dist/server-DvIB8kj5.js.map +1 -0
- package/dist/server.browser.js +1 -1
- package/dist/server.browser.js.map +1 -1
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-Bk_29eEw.d.ts} +2 -2
- package/dist/session-clear-Bk_29eEw.d.ts.map +1 -0
- package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
- package/dist/session-clear-wHd5GCpZ.js.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-Z2qjw50n.d.ts +523 -0
- package/dist/store-Z2qjw50n.d.ts.map +1 -0
- package/dist/store-codec-DwrWlgQx.js +65 -0
- package/dist/store-codec-DwrWlgQx.js.map +1 -0
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +393 -210
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-7WJV0YWP.js → store-polling-DYNyxCev.js} +2 -2
- package/dist/{store-polling-7WJV0YWP.js.map → store-polling-DYNyxCev.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +1595 -673
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-B1ixeqIS.js → store-presence-polling-CzQ0wdtM.js} +3 -3
- package/dist/{store-presence-polling-B1ixeqIS.js.map → store-presence-polling-CzQ0wdtM.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +4 -4
- package/dist/store-redis-notify-IiVdlYHc.js +2919 -0
- package/dist/store-redis-notify-IiVdlYHc.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.js +2 -2
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +517 -287
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
- package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
- package/dist/{wire-BKxH1vbP.js → wire-CZGqNZRx.js} +5 -4
- package/dist/wire-CZGqNZRx.js.map +1 -0
- package/docs/01-quickstart.mdx +28 -19
- package/docs/02-examples.mdx +1 -1
- package/docs/actors/01-introduction.mdx +9 -1
- package/docs/actors/02-concurrency.mdx +1 -1
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/01-introduction.mdx +1 -1
- package/docs/agents/02-tools.mdx +13 -12
- package/docs/agents/03-conversations.mdx +31 -21
- package/docs/agents/04-harnesses.mdx +7 -3
- package/docs/agents/meta.ts +1 -1
- package/docs/client/03-presence.mdx +18 -14
- package/docs/client/meta.ts +1 -1
- package/docs/contract/01-events.mdx +2 -2
- package/docs/contract/02-state.mdx +18 -14
- package/docs/contract/03-sessions.mdx +32 -25
- package/docs/index.mdx +31 -26
- package/docs/reference/01-core.mdx +12 -12
- package/docs/reference/02-server.mdx +247 -192
- package/docs/reference/04-ai.mdx +29 -20
- package/docs/reference/05-telemetry.mdx +146 -0
- package/docs/reference/06-errors.mdx +24 -18
- package/docs/reference/meta.ts +1 -1
- package/docs/schedulers/01-overview.mdx +112 -0
- package/docs/schedulers/02-vercel-queues.mdx +87 -0
- package/docs/schedulers/03-qstash.mdx +123 -0
- package/docs/schedulers/meta.ts +1 -0
- package/docs/server/01-consumers.mdx +415 -0
- package/docs/server/02-timers.mdx +33 -23
- package/docs/server/03-cancellation.mdx +52 -44
- package/docs/server/04-routes.mdx +4 -1
- package/docs/server/05-sql-projections.mdx +391 -0
- package/docs/server/06-guarantees.mdx +60 -50
- package/docs/stores/01-overview.mdx +37 -0
- package/docs/stores/02-redis.mdx +57 -0
- package/docs/stores/03-redis-http.mdx +56 -0
- package/docs/stores/04-postgres.mdx +172 -0
- package/docs/stores/05-sqlite.mdx +36 -0
- package/docs/stores/06-memory.mdx +22 -0
- package/docs/stores/meta.ts +1 -0
- package/docs/upgrades/01-overview.mdx +30 -0
- package/docs/upgrades/79-0-21.mdx +165 -0
- package/docs/upgrades/80-0-20.mdx +43 -0
- package/docs/upgrades/81-0-19.mdx +33 -0
- package/docs/upgrades/82-0-18.mdx +33 -0
- package/docs/upgrades/83-0-17.mdx +69 -0
- package/docs/upgrades/meta.ts +3 -0
- package/examples/playground/DEPLOY.md +48 -11
- package/examples/playground/app/a0/apps/[appId]/route.ts +6 -10
- package/examples/playground/app/a0/chat.test.tsx +421 -330
- package/examples/playground/app/a0/sandbox-log.ts +17 -15
- package/examples/playground/app/a0/server.ts +4 -4
- package/examples/playground/app/a0/shell.tsx +1 -1
- package/examples/playground/app/a0/uploads/route.test.ts +59 -0
- package/examples/playground/app/a2slack/a2slack.css +1067 -0
- package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
- package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
- package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
- package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
- package/examples/playground/app/a2slack/consumers/search.ts +37 -0
- package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
- package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
- package/examples/playground/app/a2slack/db/a2.ts +206 -0
- package/examples/playground/app/a2slack/db/index.ts +73 -0
- package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
- package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
- package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
- package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
- package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +34 -0
- package/examples/playground/app/a2slack/db/queries.ts +263 -0
- package/examples/playground/app/a2slack/db/schema.ts +145 -0
- package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
- package/examples/playground/app/a2slack/drills/harness.ts +244 -0
- package/examples/playground/app/a2slack/events/route.ts +36 -0
- package/examples/playground/app/a2slack/identity.ts +44 -0
- package/examples/playground/app/a2slack/inbox/route.ts +8 -0
- package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
- package/examples/playground/app/a2slack/lag/page.tsx +101 -0
- package/examples/playground/app/a2slack/lag.ts +73 -0
- package/examples/playground/app/a2slack/layout.tsx +43 -0
- package/examples/playground/app/a2slack/messages/route.ts +17 -0
- package/examples/playground/app/a2slack/model.test.ts +208 -0
- package/examples/playground/app/a2slack/model.ts +369 -0
- package/examples/playground/app/a2slack/models.test.ts +72 -0
- package/examples/playground/app/a2slack/page.tsx +10 -0
- package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
- package/examples/playground/app/a2slack/search/route.ts +40 -0
- package/examples/playground/app/a2slack/search-index.ts +108 -0
- package/examples/playground/app/a2slack/server.test.ts +315 -0
- package/examples/playground/app/a2slack/server.ts +55 -0
- package/examples/playground/app/a2slack/sessions.ts +50 -0
- package/examples/playground/app/a2slack/thread/route.ts +8 -0
- package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
- package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
- package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
- package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
- package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
- package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
- package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
- package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
- package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
- package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
- package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
- package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
- package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
- package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
- package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
- package/examples/playground/app/agent/[agentId]/agent-queue.test.tsx +116 -90
- package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +107 -76
- package/examples/playground/app/agent/ingress.test.ts +41 -17
- package/examples/playground/app/agent/model-input.test.tsx +92 -43
- package/examples/playground/app/agent/server.ts +3 -3
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +2 -2
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
- package/examples/playground/app/chat/server.test.ts +38 -65
- package/examples/playground/app/chat/server.ts +45 -35
- package/examples/playground/app/components/activity-history.test.tsx +60 -18
- package/examples/playground/app/components/demo-icons.tsx +7 -0
- package/examples/playground/app/components/demos.tsx +7 -0
- package/examples/playground/app/components/fast-link.test.tsx +3 -5
- package/examples/playground/app/initial-state.test.tsx +86 -45
- package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
- package/examples/playground/app/orders/page.tsx +1 -1
- package/examples/playground/app/orders/server.ts +49 -30
- package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
- package/examples/playground/app/recovery/page.tsx +4 -4
- package/examples/playground/app/recovery/server.ts +23 -32
- package/examples/playground/drizzle.config.ts +14 -0
- package/examples/playground/next.config.ts +6 -1
- package/examples/playground/package.json +13 -1
- package/examples/playground/scripts/migrate.mjs +31 -0
- package/examples/playground/test/agent.ts +35 -0
- package/examples/playground/test/model.ts +149 -0
- package/examples/playground/test/serve.ts +184 -0
- package/examples/playground/test/server-only.ts +1 -0
- package/examples/playground/vercel.json +9 -0
- package/examples/playground/vitest.config.ts +27 -0
- package/examples/playground/vitest.drills.config.ts +32 -0
- package/package.json +7 -2
- package/sql/postgres/drizzle-schema.ts +206 -0
- package/sql/postgres/notifications.sql +24 -0
- package/sql/postgres/schema.sql +276 -0
- package/src/actor.ts +101 -71
- package/src/ai-agent.ts +154 -115
- package/src/ai-control-server.ts +8 -8
- package/src/ai-fx.ts +41 -9
- package/src/ai-harness.ts +28 -2
- package/src/ai-pi.ts +2 -2
- package/src/ai-server.ts +2 -2
- package/src/ai.ts +7 -7
- package/src/contract.ts +1 -1
- package/src/deterministic-id.ts +26 -3
- package/src/errors.ts +9 -6
- package/src/idempotent-replay.ts +0 -1
- package/src/index.ts +1 -1
- package/src/internal.ts +17 -6
- package/src/otel.ts +1 -1
- package/src/postgres-notifications.ts +0 -22
- package/src/postgres-resources.ts +6 -9
- package/src/postgres-schema.ts +121 -0
- package/src/push-envelope.ts +20 -0
- package/src/runtime-consumer.ts +10 -0
- package/src/scheduler-qstash.ts +12 -5
- package/src/scheduler-task.ts +2 -2
- package/src/scheduler-vercel.ts +10 -4
- package/src/server.browser.ts +1 -1
- package/src/server.ts +839 -238
- package/src/session-socket.ts +1 -1
- package/src/store-codec.ts +66 -0
- package/src/store-memory.ts +581 -255
- package/src/store-postgres.ts +1984 -943
- package/src/store-redis-core.ts +1624 -1152
- package/src/store-redis-notify.ts +2 -0
- package/src/store-sqlite.ts +891 -486
- package/src/store.ts +227 -97
- package/src/telemetry.ts +2 -1
- package/src/wire.ts +1 -0
- package/dist/ai-harness-BuuC-gJ3.js.map +0 -1
- package/dist/ai-harness-CV6rXbB3.d.ts.map +0 -1
- package/dist/ai-mMiWul1T.js.map +0 -1
- package/dist/contract-BYKASlCV.js.map +0 -1
- package/dist/idempotent-replay-JdgMQRRm.js +0 -21
- package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
- package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
- package/dist/internal-CscSY7K_.js.map +0 -1
- package/dist/server-D-Y4pqwu.js.map +0 -1
- package/dist/server-DYAATFuT.d.ts.map +0 -1
- package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
- package/dist/session-clear-DGI4jTQ5.js.map +0 -1
- package/dist/store-CyLUy86v.d.ts +0 -404
- package/dist/store-CyLUy86v.d.ts.map +0 -1
- package/dist/store-codec-DTG0Ftek.js +0 -10
- package/dist/store-codec-DTG0Ftek.js.map +0 -1
- package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
- package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
- package/dist/wire-BKxH1vbP.js.map +0 -1
- package/docs/reference/05-adapters.mdx +0 -351
- package/docs/server/01-handlers.mdx +0 -200
- package/docs/server/05-production.mdx +0 -323
package/dist/store-sqlite.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { i as A2Error, r as sessionClearedError, s as supersededAttemptError, t as CLEAR_EVENT_TYPE } from "./session-clear-
|
|
2
|
-
import { n as pollingStream } from "./store-polling-
|
|
3
|
-
import {
|
|
1
|
+
import { i as A2Error, r as sessionClearedError, s as supersededAttemptError, t as CLEAR_EVENT_TYPE } from "./session-clear-wHd5GCpZ.js";
|
|
2
|
+
import { n as pollingStream } from "./store-polling-DYNyxCev.js";
|
|
3
|
+
import { a as idempotentReplay, i as idleLogCursor, n as decodeCause, r as decodeReturnedEventIds, t as cursorKeyOf } from "./store-codec-DwrWlgQx.js";
|
|
4
4
|
import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS.js";
|
|
5
|
-
import { t as decodeReturnedEventIds } from "./store-codec-DTG0Ftek.js";
|
|
6
5
|
import { dirname, resolve } from "node:path";
|
|
7
6
|
import { mkdirSync } from "node:fs";
|
|
8
7
|
import { DatabaseSync } from "node:sqlite";
|
|
@@ -20,7 +19,14 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
20
19
|
* injected clock — never SQL `now()` — so tests can time-travel against
|
|
21
20
|
* real storage.
|
|
22
21
|
*/
|
|
22
|
+
/** The cursor layout; databases from before it are rejected, not migrated. */
|
|
23
|
+
const LAYOUT = "cursors-1";
|
|
23
24
|
const SCHEMA = `
|
|
25
|
+
create table if not exists a2_meta (
|
|
26
|
+
key text not null primary key,
|
|
27
|
+
value text not null
|
|
28
|
+
) strict;
|
|
29
|
+
|
|
24
30
|
create table if not exists a2_events (
|
|
25
31
|
session_id text not null,
|
|
26
32
|
idx integer not null,
|
|
@@ -29,35 +35,50 @@ create table if not exists a2_events (
|
|
|
29
35
|
event_id text not null,
|
|
30
36
|
created_at integer not null,
|
|
31
37
|
cause text,
|
|
38
|
+
primary key (session_id, idx)
|
|
39
|
+
) strict;
|
|
40
|
+
|
|
41
|
+
create unique index if not exists a2_events_event_id on a2_events (event_id);
|
|
42
|
+
create index if not exists a2_events_types on a2_events (session_id, event_type, idx);
|
|
43
|
+
|
|
44
|
+
create table if not exists a2_cursors (
|
|
45
|
+
session_id text not null,
|
|
46
|
+
consumer text not null,
|
|
47
|
+
cursor_key text not null,
|
|
32
48
|
lane text,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
kind text not null,
|
|
50
|
+
position integer not null,
|
|
51
|
+
head integer,
|
|
52
|
+
types text,
|
|
53
|
+
attempt_count integer not null default 0,
|
|
37
54
|
first_claimed_at integer,
|
|
38
55
|
last_claimed_at integer,
|
|
39
|
-
|
|
56
|
+
claim_holder text,
|
|
57
|
+
claim_expires_at integer,
|
|
40
58
|
failure_count integer not null default 0,
|
|
41
59
|
last_failed_at integer,
|
|
42
60
|
last_failed_attempt integer,
|
|
43
61
|
last_error text,
|
|
44
62
|
failed_at integer,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
completed_idx integer,
|
|
64
|
+
completed_attempt integer,
|
|
65
|
+
returned_event_ids text,
|
|
66
|
+
primary key (session_id, consumer, cursor_key)
|
|
48
67
|
) strict;
|
|
49
68
|
|
|
50
|
-
create
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
create
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
create index if not exists a2_cursors_heads
|
|
70
|
+
on a2_cursors (session_id, head) where head is not null;
|
|
71
|
+
|
|
72
|
+
create table if not exists a2_lane_events (
|
|
73
|
+
session_id text not null,
|
|
74
|
+
consumer text not null,
|
|
75
|
+
lane text not null,
|
|
76
|
+
idx integer not null,
|
|
77
|
+
done integer not null default 0,
|
|
78
|
+
completed_attempt integer,
|
|
79
|
+
returned_event_ids text,
|
|
80
|
+
primary key (session_id, consumer, lane, idx)
|
|
81
|
+
) strict;
|
|
61
82
|
|
|
62
83
|
create table if not exists a2_snapshots (
|
|
63
84
|
session_id text not null,
|
|
@@ -79,10 +100,12 @@ create table if not exists a2_snapshot_history (
|
|
|
79
100
|
|
|
80
101
|
create table if not exists a2_snapshot_pins (
|
|
81
102
|
session_id text not null,
|
|
103
|
+
consumer text not null,
|
|
104
|
+
cursor_key text not null,
|
|
82
105
|
event_index integer not null,
|
|
83
106
|
reducer_name text not null,
|
|
84
107
|
up_to_index integer not null,
|
|
85
|
-
primary key (session_id, event_index, reducer_name, up_to_index)
|
|
108
|
+
primary key (session_id, consumer, cursor_key, event_index, reducer_name, up_to_index)
|
|
86
109
|
) strict;
|
|
87
110
|
|
|
88
111
|
create index if not exists a2_snapshot_pins_checkpoint
|
|
@@ -129,17 +152,7 @@ const wrap = (fn) => {
|
|
|
129
152
|
}
|
|
130
153
|
};
|
|
131
154
|
const toDate = (ms) => ms === null ? null : new Date(Number(ms));
|
|
132
|
-
const
|
|
133
|
-
if (json === null) return null;
|
|
134
|
-
const value = JSON.parse(json);
|
|
135
|
-
if (!Number.isInteger(value.index) || Number(value.index) < 1 || !Number.isInteger(value.attempt) || Number(value.attempt) < 1) throw new TypeError("stored event has an invalid cause");
|
|
136
|
-
if (value.batchSize !== void 0 && (!Number.isInteger(value.batchSize) || Number(value.batchSize) < 1)) throw new TypeError("stored event has an invalid cause");
|
|
137
|
-
return {
|
|
138
|
-
index: Number(value.index),
|
|
139
|
-
attempt: Number(value.attempt),
|
|
140
|
-
...value.batchSize === void 0 ? {} : { batchSize: Number(value.batchSize) }
|
|
141
|
-
};
|
|
142
|
-
};
|
|
155
|
+
const toNumber = (value) => value === null ? null : Number(value);
|
|
143
156
|
const toStored = (row) => ({
|
|
144
157
|
id: row.event_id,
|
|
145
158
|
type: row.event_type,
|
|
@@ -147,21 +160,7 @@ const toStored = (row) => ({
|
|
|
147
160
|
index: Number(row.idx),
|
|
148
161
|
sessionId: row.session_id,
|
|
149
162
|
createdAt: new Date(Number(row.created_at)),
|
|
150
|
-
cause:
|
|
151
|
-
lane: row.lane,
|
|
152
|
-
processedAt: toDate(row.processed_at),
|
|
153
|
-
processedByAttempt: row.processed_by_attempt === null ? null : Number(row.processed_by_attempt),
|
|
154
|
-
returnedEventIds: row.returned_event_ids === null ? null : decodeReturnedEventIds(row.returned_event_ids),
|
|
155
|
-
firstClaimedAt: toDate(row.first_claimed_at),
|
|
156
|
-
lastClaimedAt: toDate(row.last_claimed_at),
|
|
157
|
-
attemptCount: Number(row.attempt_count),
|
|
158
|
-
failureCount: Number(row.failure_count),
|
|
159
|
-
lastFailedAt: toDate(row.last_failed_at),
|
|
160
|
-
lastFailedAttempt: row.last_failed_attempt === null ? null : Number(row.last_failed_attempt),
|
|
161
|
-
lastError: row.last_error,
|
|
162
|
-
failedAt: toDate(row.failed_at),
|
|
163
|
-
claimHolder: row.claim_holder,
|
|
164
|
-
claimExpiresAt: toDate(row.claim_expires_at)
|
|
163
|
+
cause: decodeCause(row.cause)
|
|
165
164
|
});
|
|
166
165
|
const toEvent = (row) => ({
|
|
167
166
|
id: row.event_id,
|
|
@@ -171,7 +170,55 @@ const toEvent = (row) => ({
|
|
|
171
170
|
sessionId: row.session_id,
|
|
172
171
|
createdAt: new Date(Number(row.created_at))
|
|
173
172
|
});
|
|
173
|
+
const toCursor = (row) => ({
|
|
174
|
+
consumer: row.consumer,
|
|
175
|
+
lane: row.lane,
|
|
176
|
+
position: Number(row.position),
|
|
177
|
+
head: toNumber(row.head),
|
|
178
|
+
firstClaimedAt: toDate(row.first_claimed_at),
|
|
179
|
+
lastClaimedAt: toDate(row.last_claimed_at),
|
|
180
|
+
attemptCount: Number(row.attempt_count),
|
|
181
|
+
claimHolder: row.claim_holder,
|
|
182
|
+
claimExpiresAt: toDate(row.claim_expires_at),
|
|
183
|
+
failureCount: Number(row.failure_count),
|
|
184
|
+
lastFailedAt: toDate(row.last_failed_at),
|
|
185
|
+
lastFailedAttempt: toNumber(row.last_failed_attempt),
|
|
186
|
+
lastError: row.last_error,
|
|
187
|
+
failedAt: toDate(row.failed_at)
|
|
188
|
+
});
|
|
189
|
+
const toHead = (row) => {
|
|
190
|
+
const { position: _position, head: _head, ...dispatch } = toCursor(row);
|
|
191
|
+
return {
|
|
192
|
+
...toStored({
|
|
193
|
+
session_id: row.session_id,
|
|
194
|
+
idx: row.e_idx,
|
|
195
|
+
event_type: row.e_event_type,
|
|
196
|
+
payload: row.e_payload,
|
|
197
|
+
event_id: row.e_event_id,
|
|
198
|
+
created_at: row.e_created_at,
|
|
199
|
+
cause: row.e_cause
|
|
200
|
+
}),
|
|
201
|
+
...dispatch
|
|
202
|
+
};
|
|
203
|
+
};
|
|
174
204
|
const RECLAIM_BATCH = 256;
|
|
205
|
+
/** The head columns every claim, dead letter, and listing joins in. */
|
|
206
|
+
const HEAD_SELECT = `
|
|
207
|
+
select cursor.*,
|
|
208
|
+
event.idx as e_idx, event.event_type as e_event_type,
|
|
209
|
+
event.payload as e_payload, event.event_id as e_event_id,
|
|
210
|
+
event.created_at as e_created_at, event.cause as e_cause
|
|
211
|
+
from a2_cursors cursor
|
|
212
|
+
join a2_events event
|
|
213
|
+
on event.session_id = cursor.session_id and event.idx = cursor.head`;
|
|
214
|
+
/** A fresh head's dispatch columns, for `set` clauses. */
|
|
215
|
+
const FRESH_DISPATCH = `
|
|
216
|
+
attempt_count = 0, first_claimed_at = null, last_claimed_at = null,
|
|
217
|
+
claim_holder = null, claim_expires_at = null, failure_count = 0,
|
|
218
|
+
last_failed_at = null, last_failed_attempt = null, last_error = null,
|
|
219
|
+
failed_at = null`;
|
|
220
|
+
const placeholders = (count) => Array.from({ length: count }, () => "?").join(", ");
|
|
221
|
+
const compareText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
175
222
|
function sqlite(options = {}) {
|
|
176
223
|
const path = options.path ?? ".a2/dev.db";
|
|
177
224
|
const clock = options.clock ?? SYSTEM_CLOCK;
|
|
@@ -184,37 +231,60 @@ function sqlite(options = {}) {
|
|
|
184
231
|
db.exec("pragma busy_timeout = 5000");
|
|
185
232
|
setWalJournalMode(db);
|
|
186
233
|
db.exec("pragma synchronous = normal");
|
|
234
|
+
if (db.prepare("select 1 from pragma_table_info('a2_events') where name = ?").get("processed_at")) {
|
|
235
|
+
db.close();
|
|
236
|
+
throw new A2Error("STORE_NOT_CONFIGURED", `the sqlite database at '${path}' has the A2 layout from before consumers; delete it, or point the store at a fresh database file`);
|
|
237
|
+
}
|
|
187
238
|
db.exec(SCHEMA);
|
|
239
|
+
db.prepare("insert into a2_meta (key, value) values ('layout', ?) on conflict (key) do nothing").run(LAYOUT);
|
|
188
240
|
const insertEvent = db.prepare(`insert into a2_events
|
|
189
|
-
(session_id, idx, event_type, payload, event_id, created_at,
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
241
|
+
(session_id, idx, event_type, payload, event_id, created_at, cause)
|
|
242
|
+
values (?, ?, ?, ?, ?, ?, ?)`);
|
|
243
|
+
const selectHead = db.prepare("select coalesce(max(idx), 0) as max from a2_events where session_id = ?");
|
|
244
|
+
const headOf = (sessionId) => Number(selectHead.get(sessionId).max);
|
|
245
|
+
const selectEvent = db.prepare("select 1 from a2_events where session_id = ? and idx = ?");
|
|
246
|
+
const selectCursor = db.prepare("select * from a2_cursors where session_id = ? and consumer = ? and cursor_key = ?");
|
|
247
|
+
const cursorOf = (sessionId, ref) => selectCursor.get(sessionId, ref.consumer, cursorKeyOf(ref.lane, ref.index));
|
|
248
|
+
const insertEventCursor = db.prepare(`insert into a2_cursors
|
|
249
|
+
(session_id, consumer, cursor_key, lane, kind, position, head)
|
|
250
|
+
values (?, ?, ?, null, 'event', ?, ?)`);
|
|
251
|
+
const insertLaneEvent = db.prepare(`insert into a2_lane_events (session_id, consumer, lane, idx)
|
|
252
|
+
values (?, ?, ?, ?)`);
|
|
253
|
+
const queueLaneHead = db.prepare(`insert into a2_cursors
|
|
254
|
+
(session_id, consumer, cursor_key, lane, kind, position, head)
|
|
255
|
+
values (?, ?, ?, ?, 'lane', ?, ?)
|
|
256
|
+
on conflict (session_id, consumer, cursor_key) do update set
|
|
257
|
+
head = excluded.head,
|
|
258
|
+
position = max(a2_cursors.position, excluded.position),
|
|
259
|
+
${FRESH_DISPATCH}
|
|
260
|
+
where a2_cursors.head is null or a2_cursors.head <= excluded.position`);
|
|
261
|
+
const openLogCursor = db.prepare(`insert into a2_cursors
|
|
262
|
+
(session_id, consumer, cursor_key, lane, kind, position, head, types)
|
|
263
|
+
values (?, ?, ?, ?, 'log', ?, ?, ?)`);
|
|
264
|
+
const wakeLogCursor = db.prepare(`update a2_cursors set types = ?, head = ?
|
|
265
|
+
where session_id = ? and consumer = ? and cursor_key = ?`);
|
|
266
|
+
const refreshTypes = db.prepare(`update a2_cursors set types = ?
|
|
267
|
+
where session_id = ? and consumer = ? and kind = 'log'
|
|
268
|
+
and types is not ?`);
|
|
269
|
+
const nextLogHeadAll = db.prepare(`select min(idx) as next from a2_events
|
|
270
|
+
where session_id = ? and idx > ? and event_type != ?`);
|
|
271
|
+
const nextLogHeadOf = db.prepare(`select min(idx) as next from a2_events
|
|
272
|
+
where session_id = ? and idx > ? and event_type != ?
|
|
273
|
+
and event_type in (select value from json_each(?))`);
|
|
274
|
+
const nextLogHead = (sessionId, after, types) => toNumber((types === null ? nextLogHeadAll.get(sessionId, after, CLEAR_EVENT_TYPE) : nextLogHeadOf.get(sessionId, after, CLEAR_EVENT_TYPE, types)).next);
|
|
275
|
+
const nextLaneHead = db.prepare(`select min(idx) as next from a2_lane_events
|
|
276
|
+
where session_id = ? and consumer = ? and lane = ? and done = 0
|
|
277
|
+
and idx > ?`);
|
|
278
|
+
const selectLaneEvent = db.prepare(`select completed_attempt, returned_event_ids from a2_lane_events
|
|
279
|
+
where session_id = ? and consumer = ? and lane = ? and idx = ?`);
|
|
280
|
+
const finishLaneEvent = db.prepare(`update a2_lane_events
|
|
281
|
+
set done = 1, completed_attempt = ?, returned_event_ids = ?
|
|
282
|
+
where session_id = ? and consumer = ? and lane = ? and idx = ?`);
|
|
283
|
+
const moveCursor = db.prepare(`update a2_cursors set position = ?, head = ?, ${FRESH_DISPATCH}
|
|
284
|
+
where session_id = ? and consumer = ? and cursor_key = ?`);
|
|
285
|
+
const recordCompletion = db.prepare(`update a2_cursors
|
|
286
|
+
set completed_idx = ?, completed_attempt = ?, returned_event_ids = ?
|
|
287
|
+
where session_id = ? and consumer = ? and cursor_key = ?`);
|
|
218
288
|
const upsertPresence = db.prepare(`insert into a2_presence (ns, participant, field, value, seen, at, expires_at)
|
|
219
289
|
values (?, ?, ?, ?, ?, ?, ?)
|
|
220
290
|
on conflict (ns, participant, field) do update set
|
|
@@ -230,38 +300,47 @@ function sqlite(options = {}) {
|
|
|
230
300
|
from a2_presence
|
|
231
301
|
where ns = ? and expires_at > ?
|
|
232
302
|
order by participant, field`);
|
|
233
|
-
const selectByIds = (count) => db.prepare(`select
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
where pending.session_id = ?
|
|
237
|
-
and pending.processed_at is null
|
|
238
|
-
and pending.idx > ?
|
|
239
|
-
) as session_has_pending
|
|
240
|
-
from a2_events stored
|
|
241
|
-
where stored.event_id in (${Array.from({ length: count }, () => "?").join(", ")})
|
|
242
|
-
order by stored.idx`);
|
|
303
|
+
const selectByIds = (count) => db.prepare(`select * from a2_events
|
|
304
|
+
where event_id in (${placeholders(count)})
|
|
305
|
+
order by idx`);
|
|
243
306
|
const selectClearIndex = db.prepare("select clear_index from a2_clears where session_id = ?");
|
|
244
307
|
const clearIndexOf = (sessionId) => Number(selectClearIndex.get(sessionId)?.clear_index ?? 0);
|
|
245
308
|
const deleteEventById = db.prepare("delete from a2_events where session_id = ? and event_id = ?");
|
|
309
|
+
/** Served consumers as a SQL condition on `cursor.consumer`. */
|
|
310
|
+
const servedClause = (consumers) => consumers === void 0 ? {
|
|
311
|
+
sql: "",
|
|
312
|
+
params: []
|
|
313
|
+
} : consumers.length === 0 ? {
|
|
314
|
+
sql: "and 0",
|
|
315
|
+
params: []
|
|
316
|
+
} : {
|
|
317
|
+
sql: `and cursor.consumer in (${placeholders(consumers.length)})`,
|
|
318
|
+
params: consumers.map((consumer) => consumer.name)
|
|
319
|
+
};
|
|
320
|
+
const hasPending = (sessionId, clearIndex, consumers) => {
|
|
321
|
+
const served = servedClause(consumers);
|
|
322
|
+
return db.prepare(`select 1 from a2_cursors cursor
|
|
323
|
+
where cursor.session_id = ? and cursor.head > ?
|
|
324
|
+
and cursor.failed_at is null ${served.sql}
|
|
325
|
+
limit 1`).get(sessionId, clearIndex, ...served.params) !== void 0;
|
|
326
|
+
};
|
|
246
327
|
/**
|
|
247
328
|
* The rows owning `ids`. This session's rows below its clear marker do
|
|
248
329
|
* not own their ids: they are deleted here so the ids can be reused.
|
|
249
330
|
*/
|
|
250
331
|
const liveOwnersOf = (sessionId, clearIndex, ids) => {
|
|
251
332
|
if (ids.length === 0) return [];
|
|
252
|
-
return selectByIds(ids.length).all(
|
|
333
|
+
return selectByIds(ids.length).all(...ids).filter((row) => {
|
|
253
334
|
if (row.session_id !== sessionId || Number(row.idx) >= clearIndex) return true;
|
|
254
335
|
deleteEventById.run(sessionId, row.event_id);
|
|
255
336
|
return false;
|
|
256
337
|
});
|
|
257
338
|
};
|
|
258
|
-
|
|
259
|
-
/** The attempt-currency fence shared by handler appends and clears. */
|
|
339
|
+
/** The attempt-currency fence shared by consumer appends and clears. */
|
|
260
340
|
const assertCurrentCause = ({ sessionId, cause, clearIndex }) => {
|
|
261
341
|
if (cause.index <= clearIndex) throw supersededAttemptError(sessionId, cause);
|
|
262
|
-
const
|
|
263
|
-
if (!
|
|
264
|
-
if (Number(parent.attempt_count) !== cause.attempt || parent.failed_at !== null) throw supersededAttemptError(sessionId, cause);
|
|
342
|
+
const cursor = cursorOf(sessionId, cause);
|
|
343
|
+
if (!cursor || toNumber(cursor.head) !== cause.index || Number(cursor.attempt_count) !== cause.attempt || cursor.failed_at !== null) throw supersededAttemptError(sessionId, cause);
|
|
265
344
|
};
|
|
266
345
|
const tx = (fn) => {
|
|
267
346
|
db.exec("begin immediate");
|
|
@@ -276,9 +355,10 @@ function sqlite(options = {}) {
|
|
|
276
355
|
throw err;
|
|
277
356
|
}
|
|
278
357
|
};
|
|
279
|
-
/** A dead
|
|
280
|
-
const releasePins = (sessionId, index) => {
|
|
281
|
-
db.prepare(
|
|
358
|
+
/** A finished or dead head's snapshot pins, and history no pin keeps. */
|
|
359
|
+
const releasePins = (sessionId, consumer, cursorKey, index) => {
|
|
360
|
+
db.prepare(`delete from a2_snapshot_pins
|
|
361
|
+
where session_id = ? and consumer = ? and cursor_key = ? and event_index = ?`).run(sessionId, consumer, cursorKey, index);
|
|
282
362
|
db.prepare(`delete from a2_snapshot_history
|
|
283
363
|
where session_id = ?
|
|
284
364
|
and not exists (
|
|
@@ -288,15 +368,104 @@ function sqlite(options = {}) {
|
|
|
288
368
|
and pin.up_to_index = a2_snapshot_history.up_to_index
|
|
289
369
|
)`).run(sessionId);
|
|
290
370
|
};
|
|
371
|
+
/** Moves a cursor past its head; the next head starts fresh. */
|
|
372
|
+
const advance = (sessionId, cursor) => {
|
|
373
|
+
const done = Number(cursor.head);
|
|
374
|
+
releasePins(sessionId, cursor.consumer, cursor.cursor_key, done);
|
|
375
|
+
let next;
|
|
376
|
+
if (cursor.kind === "event") next = null;
|
|
377
|
+
else if (cursor.kind === "lane") next = toNumber(nextLaneHead.get(sessionId, cursor.consumer, cursor.lane, Math.max(done, clearIndexOf(sessionId))).next);
|
|
378
|
+
else {
|
|
379
|
+
const { types } = selectCursor.get(sessionId, cursor.consumer, cursor.cursor_key);
|
|
380
|
+
next = nextLogHead(sessionId, done, types);
|
|
381
|
+
}
|
|
382
|
+
moveCursor.run(done, next, sessionId, cursor.consumer, cursor.cursor_key);
|
|
383
|
+
};
|
|
384
|
+
/**
|
|
385
|
+
* Brings a log consumer's cursor up to date and records its types. A
|
|
386
|
+
* missing cursor opens at the clear marker with `backfill`, otherwise at
|
|
387
|
+
* `base`; an idle one takes the next event of its types after its
|
|
388
|
+
* position; one with a head keeps it, claim and all. Returns its key.
|
|
389
|
+
*/
|
|
390
|
+
const wakeLogAt = ({ sessionId, consumer, log, base, clearIndex }) => {
|
|
391
|
+
const types = log.types === null ? null : JSON.stringify(log.types);
|
|
392
|
+
const key = cursorKeyOf(log.lane, 0);
|
|
393
|
+
const existing = selectCursor.get(sessionId, consumer, key);
|
|
394
|
+
if (existing) {
|
|
395
|
+
wakeLogCursor.run(types, existing.head === null ? nextLogHead(sessionId, Number(existing.position), types) : Number(existing.head), sessionId, consumer, key);
|
|
396
|
+
return key;
|
|
397
|
+
}
|
|
398
|
+
const position = log.backfill ? clearIndex : base;
|
|
399
|
+
openLogCursor.run(sessionId, consumer, key, log.lane, position, nextLogHead(sessionId, position, types), types);
|
|
400
|
+
return key;
|
|
401
|
+
};
|
|
402
|
+
/** Starts the cursor work an inserted batch carries. */
|
|
403
|
+
const enqueue = ({ sessionId, events, base, clearIndex, consumers }) => {
|
|
404
|
+
events.forEach((event, offset) => {
|
|
405
|
+
const index = base + 1 + offset;
|
|
406
|
+
for (const target of event.targets ?? []) {
|
|
407
|
+
if (target.lane === null) {
|
|
408
|
+
insertEventCursor.run(sessionId, target.consumer, cursorKeyOf(null, index), index - 1, index);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
insertLaneEvent.run(sessionId, target.consumer, target.lane, index);
|
|
412
|
+
queueLaneHead.run(sessionId, target.consumer, cursorKeyOf(target.lane, index), target.lane, clearIndex, index);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
for (const consumer of consumers ?? []) {
|
|
416
|
+
const log = consumer.log;
|
|
417
|
+
if (!log) continue;
|
|
418
|
+
if (!events.some((event) => event.type !== "a2.cleared" && (log.types === null || log.types.includes(event.type)))) continue;
|
|
419
|
+
wakeLogAt({
|
|
420
|
+
sessionId,
|
|
421
|
+
consumer: consumer.name,
|
|
422
|
+
log,
|
|
423
|
+
base,
|
|
424
|
+
clearIndex
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
const refreshLogTypes = (sessionId, consumers) => {
|
|
429
|
+
for (const consumer of consumers ?? []) {
|
|
430
|
+
if (!consumer.log) continue;
|
|
431
|
+
const types = consumer.log.types === null ? null : JSON.stringify(consumer.log.types);
|
|
432
|
+
refreshTypes.run(types, sessionId, consumer.name, types);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
const insertBatch = ({ sessionId, events, clearIndex, consumers, cause }) => {
|
|
436
|
+
const base = headOf(sessionId);
|
|
437
|
+
const now = clock.now().getTime();
|
|
438
|
+
const inserted = events.map((event, offset) => {
|
|
439
|
+
const id = event.id ?? generateId();
|
|
440
|
+
const index = base + 1 + offset;
|
|
441
|
+
const eventCause = cause ?? event.cause ?? null;
|
|
442
|
+
insertEvent.run(sessionId, index, event.type, JSON.stringify(event.payload) ?? "null", id, now, eventCause ? JSON.stringify(eventCause) : null);
|
|
443
|
+
return {
|
|
444
|
+
id,
|
|
445
|
+
type: event.type,
|
|
446
|
+
payload: structuredClone(event.payload),
|
|
447
|
+
index,
|
|
448
|
+
sessionId,
|
|
449
|
+
createdAt: new Date(now),
|
|
450
|
+
cause: eventCause ? { ...eventCause } : null
|
|
451
|
+
};
|
|
452
|
+
});
|
|
453
|
+
enqueue({
|
|
454
|
+
sessionId,
|
|
455
|
+
events,
|
|
456
|
+
base,
|
|
457
|
+
clearIndex,
|
|
458
|
+
consumers
|
|
459
|
+
});
|
|
460
|
+
return inserted;
|
|
461
|
+
};
|
|
291
462
|
return {
|
|
292
463
|
async append(sessionId, events, appendOptions) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
};
|
|
299
|
-
});
|
|
464
|
+
const consumers = appendOptions?.consumers;
|
|
465
|
+
if (events.length === 0) return wrap(() => ({
|
|
466
|
+
events: [],
|
|
467
|
+
hasPending: hasPending(sessionId, clearIndexOf(sessionId), consumers)
|
|
468
|
+
}));
|
|
300
469
|
return wrap(() => tx(() => {
|
|
301
470
|
const clearIndex = clearIndexOf(sessionId);
|
|
302
471
|
const suppliedIds = events.filter((e) => e.id !== void 0).map((e) => e.id);
|
|
@@ -308,7 +477,7 @@ function sqlite(options = {}) {
|
|
|
308
477
|
if (foreign) throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${foreign.event_id}' already exists in another session`);
|
|
309
478
|
if (existing.length === events.length) return {
|
|
310
479
|
events: idempotentReplay(events, existing.map(toStored)),
|
|
311
|
-
hasPending:
|
|
480
|
+
hasPending: hasPending(sessionId, clearIndex, consumers)
|
|
312
481
|
};
|
|
313
482
|
throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`);
|
|
314
483
|
}
|
|
@@ -320,42 +489,55 @@ function sqlite(options = {}) {
|
|
|
320
489
|
cause: e.cause,
|
|
321
490
|
clearIndex
|
|
322
491
|
});
|
|
323
|
-
const status = appendStatus.get(sessionId, sessionId, clearIndex);
|
|
324
|
-
const base = Number(status.max);
|
|
325
|
-
const now = clock.now().getTime();
|
|
326
492
|
return {
|
|
327
|
-
events:
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
id,
|
|
333
|
-
type: e.type,
|
|
334
|
-
payload: structuredClone(e.payload),
|
|
335
|
-
index,
|
|
336
|
-
sessionId,
|
|
337
|
-
createdAt: new Date(now),
|
|
338
|
-
cause: e.cause ? { ...e.cause } : null,
|
|
339
|
-
lane: e.lane ?? null,
|
|
340
|
-
processedAt: e.settled ? new Date(now) : null,
|
|
341
|
-
processedByAttempt: null,
|
|
342
|
-
returnedEventIds: null,
|
|
343
|
-
firstClaimedAt: null,
|
|
344
|
-
lastClaimedAt: null,
|
|
345
|
-
attemptCount: 0,
|
|
346
|
-
failureCount: 0,
|
|
347
|
-
lastFailedAt: null,
|
|
348
|
-
lastFailedAttempt: null,
|
|
349
|
-
lastError: null,
|
|
350
|
-
failedAt: null,
|
|
351
|
-
claimHolder: null,
|
|
352
|
-
claimExpiresAt: null
|
|
353
|
-
};
|
|
493
|
+
events: insertBatch({
|
|
494
|
+
sessionId,
|
|
495
|
+
events,
|
|
496
|
+
clearIndex,
|
|
497
|
+
consumers
|
|
354
498
|
}),
|
|
355
|
-
hasPending:
|
|
499
|
+
hasPending: hasPending(sessionId, clearIndex, consumers)
|
|
356
500
|
};
|
|
357
501
|
}));
|
|
358
502
|
},
|
|
503
|
+
async wakeLog({ sessionId, consumer }) {
|
|
504
|
+
return wrap(() => {
|
|
505
|
+
const { lane } = consumer.log;
|
|
506
|
+
const marker = clearIndexOf(sessionId);
|
|
507
|
+
if (headOf(sessionId) === marker && !selectCursor.get(sessionId, consumer.name, cursorKeyOf(lane, 0))) return {
|
|
508
|
+
cursor: idleLogCursor(consumer.name, lane, marker),
|
|
509
|
+
head: marker
|
|
510
|
+
};
|
|
511
|
+
return tx(() => {
|
|
512
|
+
const head = headOf(sessionId);
|
|
513
|
+
const key = wakeLogAt({
|
|
514
|
+
sessionId,
|
|
515
|
+
consumer: consumer.name,
|
|
516
|
+
log: consumer.log,
|
|
517
|
+
base: head,
|
|
518
|
+
clearIndex: clearIndexOf(sessionId)
|
|
519
|
+
});
|
|
520
|
+
const cursor = selectCursor.get(sessionId, consumer.name, key);
|
|
521
|
+
return {
|
|
522
|
+
cursor: toCursor(cursor),
|
|
523
|
+
head
|
|
524
|
+
};
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
},
|
|
528
|
+
async cursors(sessionId) {
|
|
529
|
+
return wrap(() => {
|
|
530
|
+
const clearIndex = clearIndexOf(sessionId);
|
|
531
|
+
return db.prepare(`select * from a2_cursors
|
|
532
|
+
where session_id = ?
|
|
533
|
+
and (
|
|
534
|
+
kind = 'log'
|
|
535
|
+
or head > ?
|
|
536
|
+
or (kind = 'lane' and head is null and position > ?)
|
|
537
|
+
)
|
|
538
|
+
order by consumer, lane is null, lane, head`).all(sessionId, clearIndex, clearIndex).map(toCursor);
|
|
539
|
+
});
|
|
540
|
+
},
|
|
359
541
|
async read(sessionId, opts) {
|
|
360
542
|
return wrap(() => {
|
|
361
543
|
const conditions = ["session_id = ?", "idx >= coalesce((select clear_index from a2_clears where session_id = ?), 0)"];
|
|
@@ -371,24 +553,40 @@ function sqlite(options = {}) {
|
|
|
371
553
|
return db.prepare(`select * from a2_events where ${conditions.join(" and ")} order by idx`).all(...params).map(toStored);
|
|
372
554
|
});
|
|
373
555
|
},
|
|
374
|
-
async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs,
|
|
556
|
+
async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs, exclude = [], consumers, maxFailures, through }) {
|
|
375
557
|
return wrap(() => tx(() => {
|
|
558
|
+
refreshLogTypes(sessionId, consumers);
|
|
376
559
|
const now = clock.now().getTime();
|
|
377
560
|
const expiresAt = expiresAtMs ?? now + ttlMs;
|
|
378
561
|
const clearIndex = clearIndexOf(sessionId);
|
|
379
|
-
const
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
562
|
+
const served = servedClause(consumers);
|
|
563
|
+
const excludedKeys = exclude.map((ref) => `${ref.consumer}\u0000${cursorKeyOf(ref.lane, ref.index)}`);
|
|
564
|
+
const exclusion = excludedKeys.length === 0 ? "" : `and cursor.consumer || char(0) || cursor.cursor_key
|
|
565
|
+
not in (${placeholders(excludedKeys.length)})`;
|
|
566
|
+
const filter = `
|
|
567
|
+
where cursor.session_id = ?
|
|
568
|
+
and cursor.head > ?
|
|
569
|
+
and cursor.failed_at is null
|
|
570
|
+
${through === void 0 ? "" : "and cursor.head <= ?"}
|
|
571
|
+
${served.sql}
|
|
572
|
+
${exclusion}`;
|
|
573
|
+
const params = [
|
|
574
|
+
sessionId,
|
|
575
|
+
clearIndex,
|
|
576
|
+
...through === void 0 ? [] : [through],
|
|
577
|
+
...served.params,
|
|
578
|
+
...excludedKeys,
|
|
579
|
+
now
|
|
580
|
+
];
|
|
581
|
+
const eligible = db.prepare(`select cursor.consumer, cursor.cursor_key,
|
|
582
|
+
cursor.claim_holder, cursor.failure_count
|
|
583
|
+
from a2_cursors cursor
|
|
584
|
+
${filter}
|
|
585
|
+
and (cursor.claim_expires_at is null
|
|
586
|
+
or cursor.claim_expires_at <= ?)
|
|
587
|
+
order by cursor.head, cursor.consumer`).all(...params);
|
|
390
588
|
if (eligible.length > 0) {
|
|
391
|
-
const
|
|
589
|
+
const claimHead = db.prepare(`update a2_cursors set
|
|
392
590
|
attempt_count = attempt_count + 1,
|
|
393
591
|
failure_count = failure_count
|
|
394
592
|
+ case when claim_holder is null then 0 else 1 end,
|
|
@@ -396,37 +594,35 @@ function sqlite(options = {}) {
|
|
|
396
594
|
last_claimed_at = ?,
|
|
397
595
|
claim_holder = ?,
|
|
398
596
|
claim_expires_at = ?
|
|
399
|
-
where session_id = ? and
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
597
|
+
where session_id = ? and consumer = ? and cursor_key = ?`);
|
|
598
|
+
const deadLetterHead = db.prepare(`update a2_cursors set
|
|
599
|
+
attempt_count = attempt_count + 1,
|
|
600
|
+
failure_count = failure_count + 1,
|
|
601
|
+
failed_at = ?,
|
|
602
|
+
claim_holder = null,
|
|
603
|
+
claim_expires_at = null
|
|
604
|
+
where session_id = ? and consumer = ? and cursor_key = ?`);
|
|
605
|
+
const selectHeadRow = db.prepare(`${HEAD_SELECT}
|
|
606
|
+
where cursor.session_id = ? and cursor.consumer = ?
|
|
607
|
+
and cursor.cursor_key = ?`);
|
|
409
608
|
return {
|
|
410
609
|
outcome: "claimed",
|
|
411
610
|
events: eligible.map((row) => {
|
|
412
611
|
const exhausted = row.claim_holder !== null && maxFailures !== void 0 && Number(row.failure_count) + 1 >= maxFailures;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const
|
|
416
|
-
if (
|
|
417
|
-
|
|
418
|
-
|
|
612
|
+
if (exhausted) deadLetterHead.run(now, sessionId, row.consumer, row.cursor_key);
|
|
613
|
+
else claimHead.run(now, now, holder, expiresAt, sessionId, row.consumer, row.cursor_key);
|
|
614
|
+
const claimed = toHead(selectHeadRow.get(sessionId, row.consumer, row.cursor_key));
|
|
615
|
+
if (exhausted) releasePins(sessionId, row.consumer, row.cursor_key, claimed.index);
|
|
616
|
+
if (clearIndex > 0) claimed.clearIndex = clearIndex;
|
|
617
|
+
return claimed;
|
|
618
|
+
}),
|
|
619
|
+
head: headOf(sessionId)
|
|
419
620
|
};
|
|
420
621
|
}
|
|
421
|
-
const active = db.prepare(`select min(claim_expires_at) as retry_at
|
|
422
|
-
from
|
|
423
|
-
|
|
424
|
-
and
|
|
425
|
-
and failed_at is null
|
|
426
|
-
and lane_ready = 1
|
|
427
|
-
and idx > ?
|
|
428
|
-
and claim_expires_at > ?
|
|
429
|
-
${exclusion}`).get(sessionId, clearIndex, now, ...excludeIndexes);
|
|
622
|
+
const active = db.prepare(`select min(cursor.claim_expires_at) as retry_at
|
|
623
|
+
from a2_cursors cursor
|
|
624
|
+
${filter}
|
|
625
|
+
and cursor.claim_expires_at > ?`).get(...params);
|
|
430
626
|
return active.retry_at === null ? { outcome: "settled" } : {
|
|
431
627
|
outcome: "busy",
|
|
432
628
|
retryAt: new Date(Number(active.retry_at))
|
|
@@ -443,113 +639,137 @@ function sqlite(options = {}) {
|
|
|
443
639
|
const expiresAt = expiresAtMs ?? now + ttlMs;
|
|
444
640
|
const renewed = [];
|
|
445
641
|
const superseded = [];
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
and idx = ?
|
|
642
|
+
const extend = db.prepare(`update a2_cursors set claim_expires_at = ?
|
|
643
|
+
where session_id = ? and consumer = ? and cursor_key = ?
|
|
644
|
+
and head = ?
|
|
450
645
|
and attempt_count = ?
|
|
451
|
-
and processed_at is null
|
|
452
646
|
and failed_at is null
|
|
453
647
|
and claim_holder = ?
|
|
454
648
|
and claim_expires_at > ?`);
|
|
455
649
|
const clearIndex = clearIndexOf(sessionId);
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
650
|
+
const ordered = claims.toSorted((a, b) => a.index - b.index || compareText(a.consumer, b.consumer));
|
|
651
|
+
for (const { attempt, ...ref } of ordered) {
|
|
652
|
+
if (ref.index <= clearIndex) {
|
|
653
|
+
superseded.push(ref);
|
|
459
654
|
continue;
|
|
460
655
|
}
|
|
461
|
-
const
|
|
462
|
-
if (!
|
|
463
|
-
|
|
464
|
-
|
|
656
|
+
const cursor = cursorOf(sessionId, ref);
|
|
657
|
+
if (!cursor) continue;
|
|
658
|
+
const head = toNumber(cursor.head);
|
|
659
|
+
if (head !== ref.index && Number(cursor.position) < ref.index) continue;
|
|
660
|
+
if (head !== ref.index || Number(cursor.attempt_count) > attempt) {
|
|
661
|
+
superseded.push(ref);
|
|
465
662
|
continue;
|
|
466
663
|
}
|
|
467
|
-
const updated = extend.run(expiresAt, sessionId,
|
|
468
|
-
if (Number(updated.changes) === 1) renewed.push(
|
|
664
|
+
const updated = extend.run(expiresAt, sessionId, ref.consumer, cursor.cursor_key, ref.index, attempt, holder, now);
|
|
665
|
+
if (Number(updated.changes) === 1) renewed.push(ref);
|
|
469
666
|
}
|
|
470
667
|
return {
|
|
471
|
-
renewed
|
|
472
|
-
superseded
|
|
668
|
+
renewed,
|
|
669
|
+
superseded
|
|
473
670
|
};
|
|
474
671
|
}));
|
|
475
672
|
},
|
|
476
|
-
async completeAttempt({ sessionId, index, attempt, events }) {
|
|
673
|
+
async completeAttempt({ sessionId, consumer, lane, index, attempt, events, consumers }) {
|
|
477
674
|
return wrap(() => tx(() => {
|
|
478
675
|
const clearIndex = clearIndexOf(sessionId);
|
|
479
676
|
if (index <= clearIndex) return { outcome: "superseded" };
|
|
480
|
-
const
|
|
481
|
-
|
|
677
|
+
const cursor = cursorOf(sessionId, {
|
|
678
|
+
consumer,
|
|
679
|
+
lane,
|
|
680
|
+
index
|
|
681
|
+
});
|
|
682
|
+
if (!cursor) {
|
|
683
|
+
if (!selectEvent.get(sessionId, index)) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
|
|
684
|
+
return { outcome: "superseded" };
|
|
685
|
+
}
|
|
686
|
+
refreshLogTypes(sessionId, consumers);
|
|
482
687
|
const ids = events.map((event) => event.id);
|
|
483
688
|
if (new Set(ids).size !== ids.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains the same event id more than once");
|
|
484
|
-
if (
|
|
485
|
-
|
|
486
|
-
|
|
689
|
+
if (toNumber(cursor.head) !== index) {
|
|
690
|
+
let completion;
|
|
691
|
+
if (cursor.kind === "lane") {
|
|
692
|
+
const row = selectLaneEvent.get(sessionId, consumer, lane, index);
|
|
693
|
+
completion = row && {
|
|
694
|
+
attempt: toNumber(row.completed_attempt),
|
|
695
|
+
ids: row.returned_event_ids
|
|
696
|
+
};
|
|
697
|
+
} else if (toNumber(cursor.completed_idx) === index) completion = {
|
|
698
|
+
attempt: toNumber(cursor.completed_attempt),
|
|
699
|
+
ids: cursor.returned_event_ids
|
|
700
|
+
};
|
|
701
|
+
if (!completion || completion.attempt !== attempt) return { outcome: "superseded" };
|
|
702
|
+
const returnedIds = completion.ids === null ? null : decodeReturnedEventIds(completion.ids);
|
|
487
703
|
if (returnedIds === null || returnedIds.length !== ids.length || returnedIds.some((id, offset) => id !== ids[offset])) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "completed attempt does not match the returned event batch");
|
|
488
|
-
const existing = ids.length === 0 ? [] : selectByIds(ids.length).all(
|
|
704
|
+
const existing = ids.length === 0 ? [] : selectByIds(ids.length).all(...ids);
|
|
489
705
|
const byId = new Map(existing.map((row) => [row.event_id, row]));
|
|
490
706
|
const ordered = ids.map((id) => byId.get(id));
|
|
491
|
-
if (ordered.some((row) =>
|
|
707
|
+
if (ordered.some((row) => {
|
|
708
|
+
const cause = row ? decodeCause(row.cause) : null;
|
|
709
|
+
return !row || row.session_id !== sessionId || cause?.consumer !== consumer || cause.lane !== lane || cause.index !== index || cause.attempt !== attempt;
|
|
710
|
+
})) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event retry does not match the committed batch");
|
|
492
711
|
return {
|
|
493
712
|
outcome: "completed",
|
|
494
|
-
events: idempotentReplay(events, ordered.map(toStored))
|
|
713
|
+
events: events.length === 0 ? [] : idempotentReplay(events, ordered.map(toStored))
|
|
495
714
|
};
|
|
496
715
|
}
|
|
497
|
-
if (Number(
|
|
716
|
+
if (Number(cursor.attempt_count) !== attempt || cursor.claim_holder === null || cursor.failed_at !== null) return { outcome: "superseded" };
|
|
498
717
|
if (liveOwnersOf(sessionId, clearIndex, ids).length > 0) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains an already-appended event id");
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
and pin.reducer_name = a2_snapshot_history.reducer_name
|
|
516
|
-
and pin.up_to_index = a2_snapshot_history.up_to_index
|
|
517
|
-
)`).run(sessionId);
|
|
518
|
-
if (parent.lane !== null) promoteLaneHead.run(sessionId, sessionId, parent.lane, clearIndex);
|
|
718
|
+
const inserted = insertBatch({
|
|
719
|
+
sessionId,
|
|
720
|
+
events,
|
|
721
|
+
clearIndex,
|
|
722
|
+
consumers,
|
|
723
|
+
cause: {
|
|
724
|
+
consumer,
|
|
725
|
+
lane,
|
|
726
|
+
index,
|
|
727
|
+
attempt
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
const returned = JSON.stringify(ids);
|
|
731
|
+
if (cursor.kind === "lane") finishLaneEvent.run(attempt, returned, sessionId, consumer, lane, index);
|
|
732
|
+
else recordCompletion.run(index, attempt, returned, sessionId, consumer, cursor.cursor_key);
|
|
733
|
+
advance(sessionId, cursor);
|
|
519
734
|
return {
|
|
520
735
|
outcome: "completed",
|
|
521
|
-
events:
|
|
522
|
-
const id = event.id;
|
|
523
|
-
const childIndex = Number(status.max) + offset + 1;
|
|
524
|
-
const cause = {
|
|
525
|
-
index,
|
|
526
|
-
attempt
|
|
527
|
-
};
|
|
528
|
-
insertEvent.run(sessionId, childIndex, event.type, JSON.stringify(event.payload) ?? "null", id, now, JSON.stringify(cause), event.lane ?? null, event.lane ?? null, sessionId, event.lane ?? null, clearIndex, event.settled ? now : null);
|
|
529
|
-
return toStored(db.prepare("select * from a2_events where session_id = ? and idx = ?").get(sessionId, childIndex));
|
|
530
|
-
})
|
|
736
|
+
events: inserted
|
|
531
737
|
};
|
|
532
738
|
}));
|
|
533
739
|
},
|
|
534
|
-
async failAttempt({ sessionId, index, attempt, error, maxFailures, retryAfterMs }) {
|
|
740
|
+
async failAttempt({ sessionId, consumer, lane, index, attempt, error, maxFailures, retryAfterMs }) {
|
|
535
741
|
return wrap(() => tx(() => {
|
|
536
742
|
if (index <= clearIndexOf(sessionId)) return {
|
|
537
743
|
outcome: "superseded",
|
|
538
744
|
failureCount: 0
|
|
539
745
|
};
|
|
540
|
-
const current =
|
|
541
|
-
|
|
746
|
+
const current = cursorOf(sessionId, {
|
|
747
|
+
consumer,
|
|
748
|
+
lane,
|
|
749
|
+
index
|
|
750
|
+
});
|
|
751
|
+
if (!current) {
|
|
752
|
+
if (!selectEvent.get(sessionId, index)) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
|
|
753
|
+
return {
|
|
754
|
+
outcome: "superseded",
|
|
755
|
+
failureCount: 0
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
if (toNumber(current.head) !== index) return {
|
|
759
|
+
outcome: "superseded",
|
|
760
|
+
failureCount: 0
|
|
761
|
+
};
|
|
542
762
|
const failureCount = Number(current.failure_count);
|
|
543
|
-
if (
|
|
763
|
+
if (Number(current.attempt_count) !== attempt) return {
|
|
544
764
|
outcome: "superseded",
|
|
545
765
|
failureCount
|
|
546
766
|
};
|
|
547
|
-
if (current.
|
|
548
|
-
outcome: "dead_lettered",
|
|
767
|
+
if (current.claim_holder === null && current.last_failed_attempt !== null && Number(current.last_failed_attempt) === attempt) return {
|
|
768
|
+
outcome: current.failed_at === null ? "failed" : "dead_lettered",
|
|
549
769
|
failureCount
|
|
550
770
|
};
|
|
551
|
-
if (current.
|
|
552
|
-
outcome: "
|
|
771
|
+
if (current.failed_at !== null) return {
|
|
772
|
+
outcome: "dead_lettered",
|
|
553
773
|
failureCount
|
|
554
774
|
};
|
|
555
775
|
if (current.claim_holder === null) return {
|
|
@@ -559,7 +779,7 @@ function sqlite(options = {}) {
|
|
|
559
779
|
const now = clock.now().getTime();
|
|
560
780
|
const nextFailureCount = failureCount + 1;
|
|
561
781
|
const deadLettered = nextFailureCount >= maxFailures;
|
|
562
|
-
db.prepare(`update
|
|
782
|
+
db.prepare(`update a2_cursors set
|
|
563
783
|
failure_count = ?,
|
|
564
784
|
last_error = ?,
|
|
565
785
|
last_failed_at = ?,
|
|
@@ -567,8 +787,8 @@ function sqlite(options = {}) {
|
|
|
567
787
|
failed_at = ?,
|
|
568
788
|
claim_holder = null,
|
|
569
789
|
claim_expires_at = ?
|
|
570
|
-
where session_id = ? and
|
|
571
|
-
if (deadLettered) releasePins(sessionId, index);
|
|
790
|
+
where session_id = ? and consumer = ? and cursor_key = ?`).run(nextFailureCount, error, now, attempt, deadLettered ? now : null, deadLettered || retryAfterMs === void 0 ? null : now + retryAfterMs, sessionId, consumer, current.cursor_key);
|
|
791
|
+
if (deadLettered) releasePins(sessionId, consumer, current.cursor_key, index);
|
|
572
792
|
return {
|
|
573
793
|
outcome: deadLettered ? "dead_lettered" : "failed",
|
|
574
794
|
failureCount: nextFailureCount
|
|
@@ -577,31 +797,32 @@ function sqlite(options = {}) {
|
|
|
577
797
|
},
|
|
578
798
|
async deadLetters(sessionId) {
|
|
579
799
|
return wrap(() => {
|
|
580
|
-
return db.prepare(
|
|
581
|
-
where session_id = ?
|
|
582
|
-
and
|
|
583
|
-
and
|
|
584
|
-
and idx > coalesce(
|
|
800
|
+
return db.prepare(`${HEAD_SELECT}
|
|
801
|
+
where cursor.session_id = ?
|
|
802
|
+
and cursor.failed_at is not null
|
|
803
|
+
and cursor.head > coalesce(
|
|
585
804
|
(select clear_index from a2_clears where session_id = ?), 0
|
|
586
805
|
)
|
|
587
|
-
order by
|
|
806
|
+
order by cursor.head, cursor.consumer`).all(sessionId, sessionId).map(toHead);
|
|
588
807
|
});
|
|
589
808
|
},
|
|
590
|
-
async redrive({ sessionId, index, action }) {
|
|
809
|
+
async redrive({ sessionId, consumer, index, action, consumers }) {
|
|
591
810
|
return wrap(() => tx(() => {
|
|
592
811
|
const clearIndex = clearIndexOf(sessionId);
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if (
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
812
|
+
const cursor = index > clearIndex ? db.prepare(`select * from a2_cursors
|
|
813
|
+
where session_id = ? and consumer = ? and head = ?
|
|
814
|
+
and failed_at is not null`).get(sessionId, consumer, index) : void 0;
|
|
815
|
+
if (cursor) {
|
|
816
|
+
refreshLogTypes(sessionId, consumers);
|
|
817
|
+
if (action === "skip") {
|
|
818
|
+
if (cursor.kind === "lane") finishLaneEvent.run(null, null, sessionId, consumer, cursor.lane, index);
|
|
819
|
+
advance(sessionId, cursor);
|
|
820
|
+
} else db.prepare(`update a2_cursors set failed_at = null, failure_count = 0
|
|
821
|
+
where session_id = ? and consumer = ? and cursor_key = ?`).run(sessionId, consumer, cursor.cursor_key);
|
|
822
|
+
}
|
|
602
823
|
return {
|
|
603
|
-
redriven,
|
|
604
|
-
hasPending:
|
|
824
|
+
redriven: cursor !== void 0,
|
|
825
|
+
hasPending: hasPending(sessionId, clearIndex, consumers)
|
|
605
826
|
};
|
|
606
827
|
}));
|
|
607
828
|
},
|
|
@@ -637,21 +858,15 @@ function sqlite(options = {}) {
|
|
|
637
858
|
(select state from snapshot) as snapshot_state,
|
|
638
859
|
null as session_id, null as idx, null as event_type,
|
|
639
860
|
null as payload, null as event_id, null as created_at,
|
|
640
|
-
null as cause
|
|
641
|
-
null as processed_at,
|
|
642
|
-
null as processed_by_attempt, null as returned_event_ids,
|
|
643
|
-
null as first_claimed_at,
|
|
644
|
-
null as last_claimed_at, null as attempt_count,
|
|
645
|
-
null as failure_count, null as last_failed_at,
|
|
646
|
-
null as last_failed_attempt, null as last_error,
|
|
647
|
-
null as failed_at, null as claim_holder,
|
|
648
|
-
null as claim_expires_at
|
|
861
|
+
null as cause
|
|
649
862
|
union all
|
|
650
863
|
select 1 as row_order, 'event' as row_kind,
|
|
651
864
|
null as clear_index,
|
|
652
865
|
(select up_to_index from head) as head_index,
|
|
653
866
|
null as snapshot_index, null as snapshot_state,
|
|
654
|
-
event
|
|
867
|
+
event.session_id, event.idx, event.event_type,
|
|
868
|
+
event.payload, event.event_id, event.created_at,
|
|
869
|
+
event.cause
|
|
655
870
|
from a2_events as event
|
|
656
871
|
where event.session_id = ?
|
|
657
872
|
and event.idx > max(
|
|
@@ -675,14 +890,14 @@ function sqlite(options = {}) {
|
|
|
675
890
|
},
|
|
676
891
|
async putSnapshots(sessionId, reducerName, writes) {
|
|
677
892
|
await wrap(() => tx(() => {
|
|
678
|
-
const
|
|
893
|
+
const selectSnapshotHead = db.prepare(`select up_to_index, state, updated_at from a2_snapshots
|
|
679
894
|
where session_id = ? and reducer_name = ?`);
|
|
680
|
-
const unfinished = db.prepare(`select 1 from
|
|
681
|
-
where session_id = ? and
|
|
682
|
-
and
|
|
895
|
+
const unfinished = db.prepare(`select 1 from a2_cursors
|
|
896
|
+
where session_id = ? and consumer = ? and cursor_key = ?
|
|
897
|
+
and head = ? and head > ? and failed_at is null`);
|
|
683
898
|
const insertPin = db.prepare(`insert or ignore into a2_snapshot_pins
|
|
684
|
-
(session_id, event_index, reducer_name, up_to_index)
|
|
685
|
-
values (?, ?, ?, ?)`);
|
|
899
|
+
(session_id, consumer, cursor_key, event_index, reducer_name, up_to_index)
|
|
900
|
+
values (?, ?, ?, ?, ?, ?)`);
|
|
686
901
|
const hasPin = db.prepare(`select 1 from a2_snapshot_pins
|
|
687
902
|
where session_id = ? and reducer_name = ? and up_to_index = ?
|
|
688
903
|
limit 1`);
|
|
@@ -704,8 +919,11 @@ function sqlite(options = {}) {
|
|
|
704
919
|
const clearIndex = clearIndexOf(sessionId);
|
|
705
920
|
for (const write of writes.toSorted((a, b) => a.index - b.index)) {
|
|
706
921
|
if (write.index < clearIndex) continue;
|
|
707
|
-
for (const
|
|
708
|
-
|
|
922
|
+
for (const pin of write.pins ?? []) {
|
|
923
|
+
const cursorKey = cursorKeyOf(pin.lane, pin.index);
|
|
924
|
+
if (unfinished.get(sessionId, pin.consumer, cursorKey, pin.index, clearIndex)) insertPin.run(sessionId, pin.consumer, cursorKey, pin.index, reducerName, write.index);
|
|
925
|
+
}
|
|
926
|
+
const head = selectSnapshotHead.get(sessionId, reducerName);
|
|
709
927
|
const now = clock.now().getTime();
|
|
710
928
|
if (!head || write.index >= Number(head.up_to_index)) {
|
|
711
929
|
if (head && write.index > Number(head.up_to_index) && Number(head.up_to_index) >= clearIndex && hasPin.get(sessionId, reducerName, Number(head.up_to_index))) putHistory.run(sessionId, reducerName, Number(head.up_to_index), head.state, Number(head.updated_at));
|
|
@@ -758,7 +976,7 @@ function sqlite(options = {}) {
|
|
|
758
976
|
cause,
|
|
759
977
|
clearIndex
|
|
760
978
|
});
|
|
761
|
-
const head =
|
|
979
|
+
const head = headOf(sessionId);
|
|
762
980
|
if (ifLastIndex !== void 0 && clearIndex === ifLastIndex + 1) return {
|
|
763
981
|
cleared: true,
|
|
764
982
|
index: head,
|
|
@@ -775,10 +993,15 @@ function sqlite(options = {}) {
|
|
|
775
993
|
clearIndex
|
|
776
994
|
};
|
|
777
995
|
const marker = head + 1;
|
|
778
|
-
|
|
779
|
-
insertEvent.run(sessionId, marker, CLEAR_EVENT_TYPE, "null", generateId(), now, null, null, null, sessionId, null, clearIndex, now);
|
|
996
|
+
insertEvent.run(sessionId, marker, CLEAR_EVENT_TYPE, "null", generateId(), clock.now().getTime(), null);
|
|
780
997
|
db.prepare(`insert into a2_clears (session_id, clear_index) values (?, ?)
|
|
781
998
|
on conflict (session_id) do update set clear_index = excluded.clear_index`).run(sessionId, marker);
|
|
999
|
+
db.prepare(`update a2_cursors
|
|
1000
|
+
set position = ?, head = null, ${FRESH_DISPATCH},
|
|
1001
|
+
completed_idx = null, completed_attempt = null,
|
|
1002
|
+
returned_event_ids = null
|
|
1003
|
+
where session_id = ? and kind = 'log'`).run(marker, sessionId);
|
|
1004
|
+
db.prepare("delete from a2_snapshot_pins where session_id = ? and event_index < ?").run(sessionId, marker);
|
|
782
1005
|
return {
|
|
783
1006
|
cleared: true,
|
|
784
1007
|
index: marker,
|
|
@@ -802,14 +1025,21 @@ function sqlite(options = {}) {
|
|
|
802
1025
|
if (last.last !== null) {
|
|
803
1026
|
const through = Number(last.last);
|
|
804
1027
|
deleted = Number(db.prepare("delete from a2_events where session_id = ? and idx <= ?").run(sessionId, through).changes);
|
|
805
|
-
db.prepare("delete from
|
|
1028
|
+
db.prepare("delete from a2_lane_events where session_id = ? and idx <= ?").run(sessionId, through);
|
|
1029
|
+
db.prepare(`delete from a2_cursors
|
|
1030
|
+
where session_id = ? and kind = 'event' and position < ?`).run(sessionId, through);
|
|
806
1031
|
}
|
|
807
1032
|
const done = db.prepare("select 1 from a2_events where session_id = ? and idx < ? limit 1").get(sessionId, clearIndex) === void 0;
|
|
808
|
-
if (done)
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1033
|
+
if (done) {
|
|
1034
|
+
db.prepare(`delete from a2_cursors
|
|
1035
|
+
where session_id = ? and kind != 'log'
|
|
1036
|
+
and (head is null or head <= ?) and position <= ?`).run(sessionId, clearIndex, clearIndex);
|
|
1037
|
+
for (const table of [
|
|
1038
|
+
"a2_snapshot_pins",
|
|
1039
|
+
"a2_snapshot_history",
|
|
1040
|
+
"a2_snapshots"
|
|
1041
|
+
]) db.prepare(`delete from ${table} where session_id = ? and up_to_index < ?`).run(sessionId, clearIndex);
|
|
1042
|
+
}
|
|
813
1043
|
return {
|
|
814
1044
|
deleted,
|
|
815
1045
|
done
|