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/src/store-sqlite.ts
CHANGED
|
@@ -18,16 +18,26 @@ import { DatabaseSync } from 'node:sqlite'
|
|
|
18
18
|
import { A2Error, supersededAttemptError } from './errors.ts'
|
|
19
19
|
import { CLEAR_EVENT_TYPE, sessionClearedError } from './session-clear.ts'
|
|
20
20
|
import { idempotentReplay } from './idempotent-replay.ts'
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
cursorKeyOf,
|
|
23
|
+
decodeCause,
|
|
24
|
+
decodeReturnedEventIds,
|
|
25
|
+
idleLogCursor,
|
|
26
|
+
} from './store-codec.ts'
|
|
22
27
|
import { pollingStream } from './store-polling.ts'
|
|
23
28
|
import {
|
|
24
29
|
RANDOM_IDS,
|
|
25
30
|
SYSTEM_CLOCK,
|
|
26
31
|
type A2Store,
|
|
32
|
+
type AppendEvent,
|
|
27
33
|
type Clock,
|
|
28
34
|
type Event,
|
|
29
35
|
type EventCause,
|
|
36
|
+
type HeadEvent,
|
|
37
|
+
type HeadRef,
|
|
30
38
|
type IdSource,
|
|
39
|
+
type StoreConsumer,
|
|
40
|
+
type StoredCursor,
|
|
31
41
|
type StoredEvent,
|
|
32
42
|
} from './store.ts'
|
|
33
43
|
|
|
@@ -45,7 +55,15 @@ export type SqliteStore = A2Store & {
|
|
|
45
55
|
close(): void
|
|
46
56
|
}
|
|
47
57
|
|
|
58
|
+
/** The cursor layout; databases from before it are rejected, not migrated. */
|
|
59
|
+
const LAYOUT = 'cursors-1'
|
|
60
|
+
|
|
48
61
|
const SCHEMA = `
|
|
62
|
+
create table if not exists a2_meta (
|
|
63
|
+
key text not null primary key,
|
|
64
|
+
value text not null
|
|
65
|
+
) strict;
|
|
66
|
+
|
|
49
67
|
create table if not exists a2_events (
|
|
50
68
|
session_id text not null,
|
|
51
69
|
idx integer not null,
|
|
@@ -54,35 +72,50 @@ create table if not exists a2_events (
|
|
|
54
72
|
event_id text not null,
|
|
55
73
|
created_at integer not null,
|
|
56
74
|
cause text,
|
|
75
|
+
primary key (session_id, idx)
|
|
76
|
+
) strict;
|
|
77
|
+
|
|
78
|
+
create unique index if not exists a2_events_event_id on a2_events (event_id);
|
|
79
|
+
create index if not exists a2_events_types on a2_events (session_id, event_type, idx);
|
|
80
|
+
|
|
81
|
+
create table if not exists a2_cursors (
|
|
82
|
+
session_id text not null,
|
|
83
|
+
consumer text not null,
|
|
84
|
+
cursor_key text not null,
|
|
57
85
|
lane text,
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
86
|
+
kind text not null,
|
|
87
|
+
position integer not null,
|
|
88
|
+
head integer,
|
|
89
|
+
types text,
|
|
90
|
+
attempt_count integer not null default 0,
|
|
62
91
|
first_claimed_at integer,
|
|
63
92
|
last_claimed_at integer,
|
|
64
|
-
|
|
93
|
+
claim_holder text,
|
|
94
|
+
claim_expires_at integer,
|
|
65
95
|
failure_count integer not null default 0,
|
|
66
96
|
last_failed_at integer,
|
|
67
97
|
last_failed_attempt integer,
|
|
68
98
|
last_error text,
|
|
69
99
|
failed_at integer,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
100
|
+
completed_idx integer,
|
|
101
|
+
completed_attempt integer,
|
|
102
|
+
returned_event_ids text,
|
|
103
|
+
primary key (session_id, consumer, cursor_key)
|
|
73
104
|
) strict;
|
|
74
105
|
|
|
75
|
-
create
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
create
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
106
|
+
create index if not exists a2_cursors_heads
|
|
107
|
+
on a2_cursors (session_id, head) where head is not null;
|
|
108
|
+
|
|
109
|
+
create table if not exists a2_lane_events (
|
|
110
|
+
session_id text not null,
|
|
111
|
+
consumer text not null,
|
|
112
|
+
lane text not null,
|
|
113
|
+
idx integer not null,
|
|
114
|
+
done integer not null default 0,
|
|
115
|
+
completed_attempt integer,
|
|
116
|
+
returned_event_ids text,
|
|
117
|
+
primary key (session_id, consumer, lane, idx)
|
|
118
|
+
) strict;
|
|
86
119
|
|
|
87
120
|
create table if not exists a2_snapshots (
|
|
88
121
|
session_id text not null,
|
|
@@ -104,10 +137,12 @@ create table if not exists a2_snapshot_history (
|
|
|
104
137
|
|
|
105
138
|
create table if not exists a2_snapshot_pins (
|
|
106
139
|
session_id text not null,
|
|
140
|
+
consumer text not null,
|
|
141
|
+
cursor_key text not null,
|
|
107
142
|
event_index integer not null,
|
|
108
143
|
reducer_name text not null,
|
|
109
144
|
up_to_index integer not null,
|
|
110
|
-
primary key (session_id, event_index, reducer_name, up_to_index)
|
|
145
|
+
primary key (session_id, consumer, cursor_key, event_index, reducer_name, up_to_index)
|
|
111
146
|
) strict;
|
|
112
147
|
|
|
113
148
|
create index if not exists a2_snapshot_pins_checkpoint
|
|
@@ -165,34 +200,51 @@ const wrap = <T>(fn: () => T): T => {
|
|
|
165
200
|
const toDate = (ms: number | bigint | null): Date | null =>
|
|
166
201
|
ms === null ? null : new Date(Number(ms))
|
|
167
202
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
203
|
+
const toNumber = (value: number | bigint | null): number | null =>
|
|
204
|
+
value === null ? null : Number(value)
|
|
205
|
+
|
|
206
|
+
type EventRow = {
|
|
207
|
+
session_id: string
|
|
208
|
+
idx: number | bigint
|
|
209
|
+
event_type: string
|
|
210
|
+
payload: string
|
|
211
|
+
event_id: string
|
|
212
|
+
created_at: number | bigint
|
|
213
|
+
cause: string | null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
type CursorRow = {
|
|
217
|
+
session_id: string
|
|
218
|
+
consumer: string
|
|
219
|
+
cursor_key: string
|
|
220
|
+
lane: string | null
|
|
221
|
+
kind: 'log' | 'lane' | 'event'
|
|
222
|
+
position: number | bigint
|
|
223
|
+
head: number | bigint | null
|
|
224
|
+
types: string | null
|
|
225
|
+
attempt_count: number | bigint
|
|
226
|
+
first_claimed_at: number | bigint | null
|
|
227
|
+
last_claimed_at: number | bigint | null
|
|
228
|
+
claim_holder: string | null
|
|
229
|
+
claim_expires_at: number | bigint | null
|
|
230
|
+
failure_count: number | bigint
|
|
231
|
+
last_failed_at: number | bigint | null
|
|
232
|
+
last_failed_attempt: number | bigint | null
|
|
233
|
+
last_error: string | null
|
|
234
|
+
failed_at: number | bigint | null
|
|
235
|
+
completed_idx: number | bigint | null
|
|
236
|
+
completed_attempt: number | bigint | null
|
|
237
|
+
returned_event_ids: string | null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** A cursor joined with its head event's columns, prefixed `e_`. */
|
|
241
|
+
type HeadRow = CursorRow & {
|
|
242
|
+
e_idx: number | bigint
|
|
243
|
+
e_event_type: string
|
|
244
|
+
e_payload: string
|
|
245
|
+
e_event_id: string
|
|
246
|
+
e_created_at: number | bigint
|
|
247
|
+
e_cause: string | null
|
|
196
248
|
}
|
|
197
249
|
|
|
198
250
|
const toStored = (row: EventRow): StoredEvent => ({
|
|
@@ -202,26 +254,7 @@ const toStored = (row: EventRow): StoredEvent => ({
|
|
|
202
254
|
index: Number(row.idx),
|
|
203
255
|
sessionId: row.session_id,
|
|
204
256
|
createdAt: new Date(Number(row.created_at)),
|
|
205
|
-
cause:
|
|
206
|
-
lane: row.lane,
|
|
207
|
-
processedAt: toDate(row.processed_at),
|
|
208
|
-
processedByAttempt:
|
|
209
|
-
row.processed_by_attempt === null ? null : Number(row.processed_by_attempt),
|
|
210
|
-
returnedEventIds:
|
|
211
|
-
row.returned_event_ids === null
|
|
212
|
-
? null
|
|
213
|
-
: decodeReturnedEventIds(row.returned_event_ids),
|
|
214
|
-
firstClaimedAt: toDate(row.first_claimed_at),
|
|
215
|
-
lastClaimedAt: toDate(row.last_claimed_at),
|
|
216
|
-
attemptCount: Number(row.attempt_count),
|
|
217
|
-
failureCount: Number(row.failure_count),
|
|
218
|
-
lastFailedAt: toDate(row.last_failed_at),
|
|
219
|
-
lastFailedAttempt:
|
|
220
|
-
row.last_failed_attempt === null ? null : Number(row.last_failed_attempt),
|
|
221
|
-
lastError: row.last_error,
|
|
222
|
-
failedAt: toDate(row.failed_at),
|
|
223
|
-
claimHolder: row.claim_holder,
|
|
224
|
-
claimExpiresAt: toDate(row.claim_expires_at),
|
|
257
|
+
cause: decodeCause(row.cause),
|
|
225
258
|
})
|
|
226
259
|
|
|
227
260
|
const toEvent = (row: EventRow): Event => ({
|
|
@@ -233,28 +266,37 @@ const toEvent = (row: EventRow): Event => ({
|
|
|
233
266
|
createdAt: new Date(Number(row.created_at)),
|
|
234
267
|
})
|
|
235
268
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
269
|
+
const toCursor = (row: CursorRow): StoredCursor => ({
|
|
270
|
+
consumer: row.consumer,
|
|
271
|
+
lane: row.lane,
|
|
272
|
+
position: Number(row.position),
|
|
273
|
+
head: toNumber(row.head),
|
|
274
|
+
firstClaimedAt: toDate(row.first_claimed_at),
|
|
275
|
+
lastClaimedAt: toDate(row.last_claimed_at),
|
|
276
|
+
attemptCount: Number(row.attempt_count),
|
|
277
|
+
claimHolder: row.claim_holder,
|
|
278
|
+
claimExpiresAt: toDate(row.claim_expires_at),
|
|
279
|
+
failureCount: Number(row.failure_count),
|
|
280
|
+
lastFailedAt: toDate(row.last_failed_at),
|
|
281
|
+
lastFailedAttempt: toNumber(row.last_failed_attempt),
|
|
282
|
+
lastError: row.last_error,
|
|
283
|
+
failedAt: toDate(row.failed_at),
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
const toHead = (row: HeadRow): HeadEvent => {
|
|
287
|
+
const { position: _position, head: _head, ...dispatch } = toCursor(row)
|
|
288
|
+
return {
|
|
289
|
+
...toStored({
|
|
290
|
+
session_id: row.session_id,
|
|
291
|
+
idx: row.e_idx,
|
|
292
|
+
event_type: row.e_event_type,
|
|
293
|
+
payload: row.e_payload,
|
|
294
|
+
event_id: row.e_event_id,
|
|
295
|
+
created_at: row.e_created_at,
|
|
296
|
+
cause: row.e_cause,
|
|
297
|
+
}),
|
|
298
|
+
...dispatch,
|
|
299
|
+
}
|
|
258
300
|
}
|
|
259
301
|
|
|
260
302
|
type PresenceDbRow = {
|
|
@@ -280,16 +322,30 @@ type SnapshotHeadRow = {
|
|
|
280
322
|
updated_at: number | bigint
|
|
281
323
|
}
|
|
282
324
|
|
|
283
|
-
|
|
284
|
-
max: number | bigint
|
|
285
|
-
has_pending: number | bigint
|
|
286
|
-
}
|
|
325
|
+
const RECLAIM_BATCH = 256
|
|
287
326
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
327
|
+
/** The head columns every claim, dead letter, and listing joins in. */
|
|
328
|
+
const HEAD_SELECT = `
|
|
329
|
+
select cursor.*,
|
|
330
|
+
event.idx as e_idx, event.event_type as e_event_type,
|
|
331
|
+
event.payload as e_payload, event.event_id as e_event_id,
|
|
332
|
+
event.created_at as e_created_at, event.cause as e_cause
|
|
333
|
+
from a2_cursors cursor
|
|
334
|
+
join a2_events event
|
|
335
|
+
on event.session_id = cursor.session_id and event.idx = cursor.head`
|
|
291
336
|
|
|
292
|
-
|
|
337
|
+
/** A fresh head's dispatch columns, for `set` clauses. */
|
|
338
|
+
const FRESH_DISPATCH = `
|
|
339
|
+
attempt_count = 0, first_claimed_at = null, last_claimed_at = null,
|
|
340
|
+
claim_holder = null, claim_expires_at = null, failure_count = 0,
|
|
341
|
+
last_failed_at = null, last_failed_attempt = null, last_error = null,
|
|
342
|
+
failed_at = null`
|
|
343
|
+
|
|
344
|
+
const placeholders = (count: number): string =>
|
|
345
|
+
Array.from({ length: count }, () => '?').join(', ')
|
|
346
|
+
|
|
347
|
+
const compareText = (a: string, b: string): number =>
|
|
348
|
+
a < b ? -1 : a > b ? 1 : 0
|
|
293
349
|
|
|
294
350
|
export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
295
351
|
const path = options.path ?? '.a2/dev.db'
|
|
@@ -311,43 +367,123 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
311
367
|
db.exec('pragma busy_timeout = 5000')
|
|
312
368
|
setWalJournalMode(db)
|
|
313
369
|
db.exec('pragma synchronous = normal')
|
|
370
|
+
const legacy = db
|
|
371
|
+
.prepare("select 1 from pragma_table_info('a2_events') where name = ?")
|
|
372
|
+
.get('processed_at')
|
|
373
|
+
if (legacy) {
|
|
374
|
+
db.close()
|
|
375
|
+
throw new A2Error(
|
|
376
|
+
'STORE_NOT_CONFIGURED',
|
|
377
|
+
`the sqlite database at '${path}' has the A2 layout from before consumers; delete it, or point the store at a fresh database file`,
|
|
378
|
+
)
|
|
379
|
+
}
|
|
314
380
|
db.exec(SCHEMA)
|
|
381
|
+
db.prepare(
|
|
382
|
+
"insert into a2_meta (key, value) values ('layout', ?) on conflict (key) do nothing",
|
|
383
|
+
).run(LAYOUT)
|
|
315
384
|
|
|
316
385
|
const insertEvent = db.prepare(
|
|
317
386
|
`insert into a2_events
|
|
318
|
-
(session_id, idx, event_type, payload, event_id, created_at,
|
|
319
|
-
|
|
320
|
-
select ?, ?, ?, ?, ?, ?, ?, ?,
|
|
321
|
-
case
|
|
322
|
-
when ? is null then 1
|
|
323
|
-
when not exists (
|
|
324
|
-
select 1 from a2_events
|
|
325
|
-
where session_id = ? and lane = ? and processed_at is null
|
|
326
|
-
and idx > ?
|
|
327
|
-
) then 1
|
|
328
|
-
else 0
|
|
329
|
-
end,
|
|
330
|
-
?`,
|
|
387
|
+
(session_id, idx, event_type, payload, event_id, created_at, cause)
|
|
388
|
+
values (?, ?, ?, ?, ?, ?, ?)`,
|
|
331
389
|
)
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
where session_id = ?
|
|
335
|
-
and idx = (
|
|
336
|
-
select idx from a2_events
|
|
337
|
-
where session_id = ? and lane = ? and processed_at is null
|
|
338
|
-
and idx > ?
|
|
339
|
-
order by idx limit 1
|
|
340
|
-
)`,
|
|
390
|
+
const selectHead = db.prepare(
|
|
391
|
+
'select coalesce(max(idx), 0) as max from a2_events where session_id = ?',
|
|
341
392
|
)
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
393
|
+
const headOf = (sessionId: string): number =>
|
|
394
|
+
Number((selectHead.get(sessionId) as { max: number | bigint }).max)
|
|
395
|
+
const selectEvent = db.prepare(
|
|
396
|
+
'select 1 from a2_events where session_id = ? and idx = ?',
|
|
397
|
+
)
|
|
398
|
+
const selectCursor = db.prepare(
|
|
399
|
+
'select * from a2_cursors where session_id = ? and consumer = ? and cursor_key = ?',
|
|
400
|
+
)
|
|
401
|
+
const cursorOf = (sessionId: string, ref: HeadRef): CursorRow | undefined =>
|
|
402
|
+
selectCursor.get(
|
|
403
|
+
sessionId,
|
|
404
|
+
ref.consumer,
|
|
405
|
+
cursorKeyOf(ref.lane, ref.index),
|
|
406
|
+
) as CursorRow | undefined
|
|
407
|
+
const insertEventCursor = db.prepare(
|
|
408
|
+
`insert into a2_cursors
|
|
409
|
+
(session_id, consumer, cursor_key, lane, kind, position, head)
|
|
410
|
+
values (?, ?, ?, null, 'event', ?, ?)`,
|
|
411
|
+
)
|
|
412
|
+
const insertLaneEvent = db.prepare(
|
|
413
|
+
`insert into a2_lane_events (session_id, consumer, lane, idx)
|
|
414
|
+
values (?, ?, ?, ?)`,
|
|
415
|
+
)
|
|
416
|
+
// A lane cursor whose head lies at or below the clear marker is idle: the
|
|
417
|
+
// marker superseded everything it had queued.
|
|
418
|
+
const queueLaneHead = db.prepare(
|
|
419
|
+
`insert into a2_cursors
|
|
420
|
+
(session_id, consumer, cursor_key, lane, kind, position, head)
|
|
421
|
+
values (?, ?, ?, ?, 'lane', ?, ?)
|
|
422
|
+
on conflict (session_id, consumer, cursor_key) do update set
|
|
423
|
+
head = excluded.head,
|
|
424
|
+
position = max(a2_cursors.position, excluded.position),
|
|
425
|
+
${FRESH_DISPATCH}
|
|
426
|
+
where a2_cursors.head is null or a2_cursors.head <= excluded.position`,
|
|
427
|
+
)
|
|
428
|
+
const openLogCursor = db.prepare(
|
|
429
|
+
`insert into a2_cursors
|
|
430
|
+
(session_id, consumer, cursor_key, lane, kind, position, head, types)
|
|
431
|
+
values (?, ?, ?, ?, 'log', ?, ?, ?)`,
|
|
432
|
+
)
|
|
433
|
+
const wakeLogCursor = db.prepare(
|
|
434
|
+
`update a2_cursors set types = ?, head = ?
|
|
435
|
+
where session_id = ? and consumer = ? and cursor_key = ?`,
|
|
436
|
+
)
|
|
437
|
+
const refreshTypes = db.prepare(
|
|
438
|
+
`update a2_cursors set types = ?
|
|
439
|
+
where session_id = ? and consumer = ? and kind = 'log'
|
|
440
|
+
and types is not ?`,
|
|
441
|
+
)
|
|
442
|
+
const nextLogHeadAll = db.prepare(
|
|
443
|
+
`select min(idx) as next from a2_events
|
|
444
|
+
where session_id = ? and idx > ? and event_type != ?`,
|
|
445
|
+
)
|
|
446
|
+
const nextLogHeadOf = db.prepare(
|
|
447
|
+
`select min(idx) as next from a2_events
|
|
448
|
+
where session_id = ? and idx > ? and event_type != ?
|
|
449
|
+
and event_type in (select value from json_each(?))`,
|
|
450
|
+
)
|
|
451
|
+
const nextLogHead = (
|
|
452
|
+
sessionId: string,
|
|
453
|
+
after: number,
|
|
454
|
+
types: string | null,
|
|
455
|
+
): number | null =>
|
|
456
|
+
toNumber(
|
|
457
|
+
(
|
|
458
|
+
(types === null
|
|
459
|
+
? nextLogHeadAll.get(sessionId, after, CLEAR_EVENT_TYPE)
|
|
460
|
+
: nextLogHeadOf.get(sessionId, after, CLEAR_EVENT_TYPE, types)) as {
|
|
461
|
+
next: number | bigint | null
|
|
462
|
+
}
|
|
463
|
+
).next,
|
|
464
|
+
)
|
|
465
|
+
const nextLaneHead = db.prepare(
|
|
466
|
+
`select min(idx) as next from a2_lane_events
|
|
467
|
+
where session_id = ? and consumer = ? and lane = ? and done = 0
|
|
468
|
+
and idx > ?`,
|
|
469
|
+
)
|
|
470
|
+
const selectLaneEvent = db.prepare(
|
|
471
|
+
`select completed_attempt, returned_event_ids from a2_lane_events
|
|
472
|
+
where session_id = ? and consumer = ? and lane = ? and idx = ?`,
|
|
473
|
+
)
|
|
474
|
+
const finishLaneEvent = db.prepare(
|
|
475
|
+
`update a2_lane_events
|
|
476
|
+
set done = 1, completed_attempt = ?, returned_event_ids = ?
|
|
477
|
+
where session_id = ? and consumer = ? and lane = ? and idx = ?`,
|
|
478
|
+
)
|
|
479
|
+
const moveCursor = db.prepare(
|
|
480
|
+
`update a2_cursors set position = ?, head = ?, ${FRESH_DISPATCH}
|
|
481
|
+
where session_id = ? and consumer = ? and cursor_key = ?`,
|
|
482
|
+
)
|
|
483
|
+
const recordCompletion = db.prepare(
|
|
484
|
+
`update a2_cursors
|
|
485
|
+
set completed_idx = ?, completed_attempt = ?, returned_event_ids = ?
|
|
486
|
+
where session_id = ? and consumer = ? and cursor_key = ?`,
|
|
351
487
|
)
|
|
352
488
|
const upsertPresence = db.prepare(
|
|
353
489
|
`insert into a2_presence (ns, participant, field, value, seen, at, expires_at)
|
|
@@ -374,16 +510,9 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
374
510
|
)
|
|
375
511
|
const selectByIds = (count: number) =>
|
|
376
512
|
db.prepare(
|
|
377
|
-
`select
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
where pending.session_id = ?
|
|
381
|
-
and pending.processed_at is null
|
|
382
|
-
and pending.idx > ?
|
|
383
|
-
) as session_has_pending
|
|
384
|
-
from a2_events stored
|
|
385
|
-
where stored.event_id in (${Array.from({ length: count }, () => '?').join(', ')})
|
|
386
|
-
order by stored.idx`,
|
|
513
|
+
`select * from a2_events
|
|
514
|
+
where event_id in (${placeholders(count)})
|
|
515
|
+
order by idx`,
|
|
387
516
|
)
|
|
388
517
|
|
|
389
518
|
const selectClearIndex = db.prepare(
|
|
@@ -400,6 +529,37 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
400
529
|
'delete from a2_events where session_id = ? and event_id = ?',
|
|
401
530
|
)
|
|
402
531
|
|
|
532
|
+
/** Served consumers as a SQL condition on `cursor.consumer`. */
|
|
533
|
+
const servedClause = (
|
|
534
|
+
consumers: readonly StoreConsumer[] | undefined,
|
|
535
|
+
): { sql: string; params: string[] } =>
|
|
536
|
+
consumers === undefined
|
|
537
|
+
? { sql: '', params: [] }
|
|
538
|
+
: consumers.length === 0
|
|
539
|
+
? { sql: 'and 0', params: [] }
|
|
540
|
+
: {
|
|
541
|
+
sql: `and cursor.consumer in (${placeholders(consumers.length)})`,
|
|
542
|
+
params: consumers.map((consumer) => consumer.name),
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const hasPending = (
|
|
546
|
+
sessionId: string,
|
|
547
|
+
clearIndex: number,
|
|
548
|
+
consumers: readonly StoreConsumer[] | undefined,
|
|
549
|
+
): boolean => {
|
|
550
|
+
const served = servedClause(consumers)
|
|
551
|
+
return (
|
|
552
|
+
db
|
|
553
|
+
.prepare(
|
|
554
|
+
`select 1 from a2_cursors cursor
|
|
555
|
+
where cursor.session_id = ? and cursor.head > ?
|
|
556
|
+
and cursor.failed_at is null ${served.sql}
|
|
557
|
+
limit 1`,
|
|
558
|
+
)
|
|
559
|
+
.get(sessionId, clearIndex, ...served.params) !== undefined
|
|
560
|
+
)
|
|
561
|
+
}
|
|
562
|
+
|
|
403
563
|
/**
|
|
404
564
|
* The rows owning `ids`. This session's rows below its clear marker do
|
|
405
565
|
* not own their ids: they are deleted here so the ids can be reused.
|
|
@@ -408,13 +568,9 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
408
568
|
sessionId: string,
|
|
409
569
|
clearIndex: number,
|
|
410
570
|
ids: readonly string[],
|
|
411
|
-
):
|
|
571
|
+
): EventRow[] => {
|
|
412
572
|
if (ids.length === 0) return []
|
|
413
|
-
const rows = selectByIds(ids.length).all(
|
|
414
|
-
sessionId,
|
|
415
|
-
clearIndex,
|
|
416
|
-
...ids,
|
|
417
|
-
) as unknown as ExistingEventRow[]
|
|
573
|
+
const rows = selectByIds(ids.length).all(...ids) as unknown as EventRow[]
|
|
418
574
|
return rows.filter((row) => {
|
|
419
575
|
if (row.session_id !== sessionId || Number(row.idx) >= clearIndex)
|
|
420
576
|
return true
|
|
@@ -423,10 +579,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
423
579
|
})
|
|
424
580
|
}
|
|
425
581
|
|
|
426
|
-
|
|
427
|
-
'select attempt_count, failed_at from a2_events where session_id = ? and idx = ?',
|
|
428
|
-
)
|
|
429
|
-
/** The attempt-currency fence shared by handler appends and clears. */
|
|
582
|
+
/** The attempt-currency fence shared by consumer appends and clears. */
|
|
430
583
|
const assertCurrentCause = ({
|
|
431
584
|
sessionId,
|
|
432
585
|
cause,
|
|
@@ -438,16 +591,12 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
438
591
|
}): void => {
|
|
439
592
|
if (cause.index <= clearIndex)
|
|
440
593
|
throw supersededAttemptError(sessionId, cause)
|
|
441
|
-
const
|
|
442
|
-
{ attempt_count: number | bigint; failed_at: unknown } | undefined
|
|
443
|
-
if (!parent) {
|
|
444
|
-
throw new TypeError(
|
|
445
|
-
`no event at index ${cause.index} in session '${sessionId}'`,
|
|
446
|
-
)
|
|
447
|
-
}
|
|
594
|
+
const cursor = cursorOf(sessionId, cause)
|
|
448
595
|
if (
|
|
449
|
-
|
|
450
|
-
|
|
596
|
+
!cursor ||
|
|
597
|
+
toNumber(cursor.head) !== cause.index ||
|
|
598
|
+
Number(cursor.attempt_count) !== cause.attempt ||
|
|
599
|
+
cursor.failed_at !== null
|
|
451
600
|
) {
|
|
452
601
|
throw supersededAttemptError(sessionId, cause)
|
|
453
602
|
}
|
|
@@ -470,11 +619,17 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
470
619
|
}
|
|
471
620
|
}
|
|
472
621
|
|
|
473
|
-
/** A dead
|
|
474
|
-
const releasePins = (
|
|
622
|
+
/** A finished or dead head's snapshot pins, and history no pin keeps. */
|
|
623
|
+
const releasePins = (
|
|
624
|
+
sessionId: string,
|
|
625
|
+
consumer: string,
|
|
626
|
+
cursorKey: string,
|
|
627
|
+
index: number,
|
|
628
|
+
): void => {
|
|
475
629
|
db.prepare(
|
|
476
|
-
|
|
477
|
-
|
|
630
|
+
`delete from a2_snapshot_pins
|
|
631
|
+
where session_id = ? and consumer = ? and cursor_key = ? and event_index = ?`,
|
|
632
|
+
).run(sessionId, consumer, cursorKey, index)
|
|
478
633
|
db.prepare(
|
|
479
634
|
`delete from a2_snapshot_history
|
|
480
635
|
where session_id = ?
|
|
@@ -486,20 +641,201 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
486
641
|
)`,
|
|
487
642
|
).run(sessionId)
|
|
488
643
|
}
|
|
644
|
+
|
|
645
|
+
/** Moves a cursor past its head; the next head starts fresh. */
|
|
646
|
+
const advance = (sessionId: string, cursor: CursorRow): void => {
|
|
647
|
+
const done = Number(cursor.head)
|
|
648
|
+
releasePins(sessionId, cursor.consumer, cursor.cursor_key, done)
|
|
649
|
+
let next: number | null
|
|
650
|
+
if (cursor.kind === 'event') {
|
|
651
|
+
next = null
|
|
652
|
+
} else if (cursor.kind === 'lane') {
|
|
653
|
+
next = toNumber(
|
|
654
|
+
(
|
|
655
|
+
nextLaneHead.get(
|
|
656
|
+
sessionId,
|
|
657
|
+
cursor.consumer,
|
|
658
|
+
cursor.lane,
|
|
659
|
+
Math.max(done, clearIndexOf(sessionId)),
|
|
660
|
+
) as { next: number | bigint | null }
|
|
661
|
+
).next,
|
|
662
|
+
)
|
|
663
|
+
} else {
|
|
664
|
+
const { types } = selectCursor.get(
|
|
665
|
+
sessionId,
|
|
666
|
+
cursor.consumer,
|
|
667
|
+
cursor.cursor_key,
|
|
668
|
+
) as CursorRow
|
|
669
|
+
next = nextLogHead(sessionId, done, types)
|
|
670
|
+
}
|
|
671
|
+
moveCursor.run(done, next, sessionId, cursor.consumer, cursor.cursor_key)
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Brings a log consumer's cursor up to date and records its types. A
|
|
676
|
+
* missing cursor opens at the clear marker with `backfill`, otherwise at
|
|
677
|
+
* `base`; an idle one takes the next event of its types after its
|
|
678
|
+
* position; one with a head keeps it, claim and all. Returns its key.
|
|
679
|
+
*/
|
|
680
|
+
const wakeLogAt = ({
|
|
681
|
+
sessionId,
|
|
682
|
+
consumer,
|
|
683
|
+
log,
|
|
684
|
+
base,
|
|
685
|
+
clearIndex,
|
|
686
|
+
}: {
|
|
687
|
+
sessionId: string
|
|
688
|
+
consumer: string
|
|
689
|
+
log: NonNullable<StoreConsumer['log']>
|
|
690
|
+
base: number
|
|
691
|
+
clearIndex: number
|
|
692
|
+
}): string => {
|
|
693
|
+
const types = log.types === null ? null : JSON.stringify(log.types)
|
|
694
|
+
const key = cursorKeyOf(log.lane, 0)
|
|
695
|
+
const existing = selectCursor.get(sessionId, consumer, key) as
|
|
696
|
+
CursorRow | undefined
|
|
697
|
+
if (existing) {
|
|
698
|
+
wakeLogCursor.run(
|
|
699
|
+
types,
|
|
700
|
+
existing.head === null
|
|
701
|
+
? // A deployment that doesn't serve this consumer may have
|
|
702
|
+
// appended events it takes since it went idle.
|
|
703
|
+
nextLogHead(sessionId, Number(existing.position), types)
|
|
704
|
+
: Number(existing.head),
|
|
705
|
+
sessionId,
|
|
706
|
+
consumer,
|
|
707
|
+
key,
|
|
708
|
+
)
|
|
709
|
+
return key
|
|
710
|
+
}
|
|
711
|
+
const position = log.backfill ? clearIndex : base
|
|
712
|
+
openLogCursor.run(
|
|
713
|
+
sessionId,
|
|
714
|
+
consumer,
|
|
715
|
+
key,
|
|
716
|
+
log.lane,
|
|
717
|
+
position,
|
|
718
|
+
nextLogHead(sessionId, position, types),
|
|
719
|
+
types,
|
|
720
|
+
)
|
|
721
|
+
return key
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** Starts the cursor work an inserted batch carries. */
|
|
725
|
+
const enqueue = ({
|
|
726
|
+
sessionId,
|
|
727
|
+
events,
|
|
728
|
+
base,
|
|
729
|
+
clearIndex,
|
|
730
|
+
consumers,
|
|
731
|
+
}: {
|
|
732
|
+
sessionId: string
|
|
733
|
+
events: readonly AppendEvent[]
|
|
734
|
+
base: number
|
|
735
|
+
clearIndex: number
|
|
736
|
+
consumers: readonly StoreConsumer[] | undefined
|
|
737
|
+
}): void => {
|
|
738
|
+
events.forEach((event, offset) => {
|
|
739
|
+
const index = base + 1 + offset
|
|
740
|
+
for (const target of event.targets ?? []) {
|
|
741
|
+
if (target.lane === null) {
|
|
742
|
+
insertEventCursor.run(
|
|
743
|
+
sessionId,
|
|
744
|
+
target.consumer,
|
|
745
|
+
cursorKeyOf(null, index),
|
|
746
|
+
index - 1,
|
|
747
|
+
index,
|
|
748
|
+
)
|
|
749
|
+
continue
|
|
750
|
+
}
|
|
751
|
+
insertLaneEvent.run(sessionId, target.consumer, target.lane, index)
|
|
752
|
+
queueLaneHead.run(
|
|
753
|
+
sessionId,
|
|
754
|
+
target.consumer,
|
|
755
|
+
cursorKeyOf(target.lane, index),
|
|
756
|
+
target.lane,
|
|
757
|
+
clearIndex,
|
|
758
|
+
index,
|
|
759
|
+
)
|
|
760
|
+
}
|
|
761
|
+
})
|
|
762
|
+
// The batch is inserted already, so a cursor it wakes finds the batch's
|
|
763
|
+
// first event of its types, or an earlier one.
|
|
764
|
+
for (const consumer of consumers ?? []) {
|
|
765
|
+
const log = consumer.log
|
|
766
|
+
if (!log) continue
|
|
767
|
+
const carries = events.some(
|
|
768
|
+
(event) =>
|
|
769
|
+
event.type !== CLEAR_EVENT_TYPE &&
|
|
770
|
+
(log.types === null || log.types.includes(event.type)),
|
|
771
|
+
)
|
|
772
|
+
if (!carries) continue
|
|
773
|
+
wakeLogAt({ sessionId, consumer: consumer.name, log, base, clearIndex })
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const refreshLogTypes = (
|
|
778
|
+
sessionId: string,
|
|
779
|
+
consumers: readonly StoreConsumer[] | undefined,
|
|
780
|
+
): void => {
|
|
781
|
+
for (const consumer of consumers ?? []) {
|
|
782
|
+
if (!consumer.log) continue
|
|
783
|
+
const types =
|
|
784
|
+
consumer.log.types === null ? null : JSON.stringify(consumer.log.types)
|
|
785
|
+
refreshTypes.run(types, sessionId, consumer.name, types)
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const insertBatch = ({
|
|
790
|
+
sessionId,
|
|
791
|
+
events,
|
|
792
|
+
clearIndex,
|
|
793
|
+
consumers,
|
|
794
|
+
cause,
|
|
795
|
+
}: {
|
|
796
|
+
sessionId: string
|
|
797
|
+
events: readonly AppendEvent[]
|
|
798
|
+
clearIndex: number
|
|
799
|
+
consumers: readonly StoreConsumer[] | undefined
|
|
800
|
+
cause?: EventCause
|
|
801
|
+
}): StoredEvent[] => {
|
|
802
|
+
const base = headOf(sessionId)
|
|
803
|
+
const now = clock.now().getTime()
|
|
804
|
+
const inserted = events.map((event, offset): StoredEvent => {
|
|
805
|
+
const id = event.id ?? generateId()
|
|
806
|
+
const index = base + 1 + offset
|
|
807
|
+
const eventCause = cause ?? event.cause ?? null
|
|
808
|
+
insertEvent.run(
|
|
809
|
+
sessionId,
|
|
810
|
+
index,
|
|
811
|
+
event.type,
|
|
812
|
+
JSON.stringify(event.payload) ?? 'null',
|
|
813
|
+
id,
|
|
814
|
+
now,
|
|
815
|
+
eventCause ? JSON.stringify(eventCause) : null,
|
|
816
|
+
)
|
|
817
|
+
return {
|
|
818
|
+
id,
|
|
819
|
+
type: event.type,
|
|
820
|
+
payload: structuredClone(event.payload),
|
|
821
|
+
index,
|
|
822
|
+
sessionId,
|
|
823
|
+
createdAt: new Date(now),
|
|
824
|
+
cause: eventCause ? { ...eventCause } : null,
|
|
825
|
+
}
|
|
826
|
+
})
|
|
827
|
+
enqueue({ sessionId, events, base, clearIndex, consumers })
|
|
828
|
+
return inserted
|
|
829
|
+
}
|
|
830
|
+
|
|
489
831
|
return {
|
|
490
832
|
async append(sessionId, events, appendOptions) {
|
|
833
|
+
const consumers = appendOptions?.consumers
|
|
491
834
|
if (events.length === 0) {
|
|
492
|
-
return wrap(() => {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
clearIndexOf(sessionId),
|
|
497
|
-
) as AppendStatusRow
|
|
498
|
-
return {
|
|
499
|
-
events: [],
|
|
500
|
-
hasPending: Number(status.has_pending) === 1,
|
|
501
|
-
}
|
|
502
|
-
})
|
|
835
|
+
return wrap(() => ({
|
|
836
|
+
events: [],
|
|
837
|
+
hasPending: hasPending(sessionId, clearIndexOf(sessionId), consumers),
|
|
838
|
+
}))
|
|
503
839
|
}
|
|
504
840
|
return wrap(() =>
|
|
505
841
|
tx(() => {
|
|
@@ -527,7 +863,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
527
863
|
if (existing.length === events.length) {
|
|
528
864
|
return {
|
|
529
865
|
events: idempotentReplay(events, existing.map(toStored)),
|
|
530
|
-
hasPending:
|
|
866
|
+
hasPending: hasPending(sessionId, clearIndex, consumers),
|
|
531
867
|
}
|
|
532
868
|
}
|
|
533
869
|
throw new A2Error(
|
|
@@ -543,8 +879,8 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
543
879
|
`scheduled work from event ${originIndex} was cleared in session '${sessionId}'`,
|
|
544
880
|
)
|
|
545
881
|
}
|
|
546
|
-
// Attempt-currency fence: a fresh
|
|
547
|
-
// while its causal attempt
|
|
882
|
+
// Attempt-currency fence: a fresh consumer append commits only
|
|
883
|
+
// while its causal attempt still owns the cursor's head
|
|
548
884
|
// (store.ts `append`). `begin immediate` serializes this read
|
|
549
885
|
// against a concurrent claim.
|
|
550
886
|
for (const e of events) {
|
|
@@ -552,65 +888,69 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
552
888
|
assertCurrentCause({ sessionId, cause: e.cause, clearIndex })
|
|
553
889
|
}
|
|
554
890
|
|
|
555
|
-
const
|
|
556
|
-
sessionId,
|
|
891
|
+
const inserted = insertBatch({
|
|
557
892
|
sessionId,
|
|
893
|
+
events,
|
|
558
894
|
clearIndex,
|
|
559
|
-
|
|
560
|
-
const base = Number(status.max)
|
|
561
|
-
const now = clock.now().getTime()
|
|
562
|
-
const inserted: StoredEvent[] = events.map((e, i) => {
|
|
563
|
-
const id = e.id ?? generateId()
|
|
564
|
-
const index = base + 1 + i
|
|
565
|
-
insertEvent.run(
|
|
566
|
-
sessionId,
|
|
567
|
-
index,
|
|
568
|
-
e.type,
|
|
569
|
-
JSON.stringify(e.payload) ?? 'null',
|
|
570
|
-
id,
|
|
571
|
-
now,
|
|
572
|
-
e.cause ? JSON.stringify(e.cause) : null,
|
|
573
|
-
e.lane ?? null,
|
|
574
|
-
e.lane ?? null,
|
|
575
|
-
sessionId,
|
|
576
|
-
e.lane ?? null,
|
|
577
|
-
clearIndex,
|
|
578
|
-
e.settled ? now : null,
|
|
579
|
-
)
|
|
580
|
-
return {
|
|
581
|
-
id,
|
|
582
|
-
type: e.type,
|
|
583
|
-
payload: structuredClone(e.payload),
|
|
584
|
-
index,
|
|
585
|
-
sessionId,
|
|
586
|
-
createdAt: new Date(now),
|
|
587
|
-
cause: e.cause ? { ...e.cause } : null,
|
|
588
|
-
lane: e.lane ?? null,
|
|
589
|
-
processedAt: e.settled ? new Date(now) : null,
|
|
590
|
-
processedByAttempt: null,
|
|
591
|
-
returnedEventIds: null,
|
|
592
|
-
firstClaimedAt: null,
|
|
593
|
-
lastClaimedAt: null,
|
|
594
|
-
attemptCount: 0,
|
|
595
|
-
failureCount: 0,
|
|
596
|
-
lastFailedAt: null,
|
|
597
|
-
lastFailedAttempt: null,
|
|
598
|
-
lastError: null,
|
|
599
|
-
failedAt: null,
|
|
600
|
-
claimHolder: null,
|
|
601
|
-
claimExpiresAt: null,
|
|
602
|
-
}
|
|
895
|
+
consumers,
|
|
603
896
|
})
|
|
604
897
|
return {
|
|
605
898
|
events: inserted,
|
|
606
|
-
hasPending:
|
|
607
|
-
Number(status.has_pending) === 1 ||
|
|
608
|
-
events.some((event) => event.settled !== true),
|
|
899
|
+
hasPending: hasPending(sessionId, clearIndex, consumers),
|
|
609
900
|
}
|
|
610
901
|
}),
|
|
611
902
|
)
|
|
612
903
|
},
|
|
613
904
|
|
|
905
|
+
async wakeLog({ sessionId, consumer }) {
|
|
906
|
+
return wrap(() => {
|
|
907
|
+
// Nothing after the latest clear: a missing cursor stays missing,
|
|
908
|
+
// and nothing takes the write lock.
|
|
909
|
+
const { lane } = consumer.log
|
|
910
|
+
const marker = clearIndexOf(sessionId)
|
|
911
|
+
if (
|
|
912
|
+
headOf(sessionId) === marker &&
|
|
913
|
+
!selectCursor.get(sessionId, consumer.name, cursorKeyOf(lane, 0))
|
|
914
|
+
) {
|
|
915
|
+
return {
|
|
916
|
+
cursor: idleLogCursor(consumer.name, lane, marker),
|
|
917
|
+
head: marker,
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return tx(() => {
|
|
921
|
+
const head = headOf(sessionId)
|
|
922
|
+
const key = wakeLogAt({
|
|
923
|
+
sessionId,
|
|
924
|
+
consumer: consumer.name,
|
|
925
|
+
log: consumer.log,
|
|
926
|
+
base: head,
|
|
927
|
+
clearIndex: clearIndexOf(sessionId),
|
|
928
|
+
})
|
|
929
|
+
const cursor = selectCursor.get(sessionId, consumer.name, key)
|
|
930
|
+
return { cursor: toCursor(cursor as CursorRow), head }
|
|
931
|
+
})
|
|
932
|
+
})
|
|
933
|
+
},
|
|
934
|
+
|
|
935
|
+
async cursors(sessionId) {
|
|
936
|
+
return wrap(() => {
|
|
937
|
+
const clearIndex = clearIndexOf(sessionId)
|
|
938
|
+
const rows = db
|
|
939
|
+
.prepare(
|
|
940
|
+
`select * from a2_cursors
|
|
941
|
+
where session_id = ?
|
|
942
|
+
and (
|
|
943
|
+
kind = 'log'
|
|
944
|
+
or head > ?
|
|
945
|
+
or (kind = 'lane' and head is null and position > ?)
|
|
946
|
+
)
|
|
947
|
+
order by consumer, lane is null, lane, head`,
|
|
948
|
+
)
|
|
949
|
+
.all(sessionId, clearIndex, clearIndex) as unknown as CursorRow[]
|
|
950
|
+
return rows.map(toCursor)
|
|
951
|
+
})
|
|
952
|
+
},
|
|
953
|
+
|
|
614
954
|
async read(sessionId, opts) {
|
|
615
955
|
return wrap(() => {
|
|
616
956
|
// Reads start at the latest clear marker.
|
|
@@ -641,44 +981,61 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
641
981
|
holder,
|
|
642
982
|
ttlMs,
|
|
643
983
|
expiresAtMs,
|
|
644
|
-
|
|
984
|
+
exclude = [],
|
|
985
|
+
consumers,
|
|
645
986
|
maxFailures,
|
|
987
|
+
through,
|
|
646
988
|
}) {
|
|
647
989
|
return wrap(() =>
|
|
648
990
|
tx(() => {
|
|
991
|
+
refreshLogTypes(sessionId, consumers)
|
|
649
992
|
const now = clock.now().getTime()
|
|
650
993
|
const expiresAt = expiresAtMs ?? now + ttlMs
|
|
651
994
|
const clearIndex = clearIndexOf(sessionId)
|
|
995
|
+
const served = servedClause(consumers)
|
|
996
|
+
const excludedKeys = exclude.map(
|
|
997
|
+
(ref) => `${ref.consumer}\u0000${cursorKeyOf(ref.lane, ref.index)}`,
|
|
998
|
+
)
|
|
652
999
|
const exclusion =
|
|
653
|
-
|
|
1000
|
+
excludedKeys.length === 0
|
|
654
1001
|
? ''
|
|
655
|
-
: `and
|
|
1002
|
+
: `and cursor.consumer || char(0) || cursor.cursor_key
|
|
1003
|
+
not in (${placeholders(excludedKeys.length)})`
|
|
1004
|
+
const bounded = through === undefined ? '' : 'and cursor.head <= ?'
|
|
1005
|
+
const filter = `
|
|
1006
|
+
where cursor.session_id = ?
|
|
1007
|
+
and cursor.head > ?
|
|
1008
|
+
and cursor.failed_at is null
|
|
1009
|
+
${bounded}
|
|
1010
|
+
${served.sql}
|
|
1011
|
+
${exclusion}`
|
|
1012
|
+
const params = [
|
|
1013
|
+
sessionId,
|
|
1014
|
+
clearIndex,
|
|
1015
|
+
...(through === undefined ? [] : [through]),
|
|
1016
|
+
...served.params,
|
|
1017
|
+
...excludedKeys,
|
|
1018
|
+
now,
|
|
1019
|
+
]
|
|
656
1020
|
const eligible = db
|
|
657
1021
|
.prepare(
|
|
658
|
-
`select
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
and
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
and (event.claim_expires_at is null or event.claim_expires_at <= ?)
|
|
666
|
-
${exclusion}
|
|
667
|
-
order by event.idx`,
|
|
1022
|
+
`select cursor.consumer, cursor.cursor_key,
|
|
1023
|
+
cursor.claim_holder, cursor.failure_count
|
|
1024
|
+
from a2_cursors cursor
|
|
1025
|
+
${filter}
|
|
1026
|
+
and (cursor.claim_expires_at is null
|
|
1027
|
+
or cursor.claim_expires_at <= ?)
|
|
1028
|
+
order by cursor.head, cursor.consumer`,
|
|
668
1029
|
)
|
|
669
|
-
.all(
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
idx: number | bigint
|
|
676
|
-
claim_holder: string | null
|
|
677
|
-
failure_count: number | bigint
|
|
678
|
-
}>
|
|
1030
|
+
.all(...params) as unknown as Array<
|
|
1031
|
+
Pick<
|
|
1032
|
+
CursorRow,
|
|
1033
|
+
'consumer' | 'cursor_key' | 'claim_holder' | 'failure_count'
|
|
1034
|
+
>
|
|
1035
|
+
>
|
|
679
1036
|
if (eligible.length > 0) {
|
|
680
|
-
const
|
|
681
|
-
`update
|
|
1037
|
+
const claimHead = db.prepare(
|
|
1038
|
+
`update a2_cursors set
|
|
682
1039
|
attempt_count = attempt_count + 1,
|
|
683
1040
|
failure_count = failure_count
|
|
684
1041
|
+ case when claim_holder is null then 0 else 1 end,
|
|
@@ -686,55 +1043,71 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
686
1043
|
last_claimed_at = ?,
|
|
687
1044
|
claim_holder = ?,
|
|
688
1045
|
claim_expires_at = ?
|
|
689
|
-
where session_id = ? and
|
|
690
|
-
returning *`,
|
|
1046
|
+
where session_id = ? and consumer = ? and cursor_key = ?`,
|
|
691
1047
|
)
|
|
692
1048
|
// Advancing the attempt fences the lapsed one; nothing runs.
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
1049
|
+
const deadLetterHead = db.prepare(
|
|
1050
|
+
`update a2_cursors set
|
|
1051
|
+
attempt_count = attempt_count + 1,
|
|
1052
|
+
failure_count = failure_count + 1,
|
|
1053
|
+
failed_at = ?,
|
|
1054
|
+
claim_holder = null,
|
|
1055
|
+
claim_expires_at = null
|
|
1056
|
+
where session_id = ? and consumer = ? and cursor_key = ?`,
|
|
1057
|
+
)
|
|
1058
|
+
const selectHeadRow = db.prepare(
|
|
1059
|
+
`${HEAD_SELECT}
|
|
1060
|
+
where cursor.session_id = ? and cursor.consumer = ?
|
|
1061
|
+
and cursor.cursor_key = ?`,
|
|
1062
|
+
)
|
|
704
1063
|
const events = eligible.map((row) => {
|
|
705
1064
|
// A lapsed claim's takeover that spends the last failed attempt.
|
|
706
1065
|
const exhausted =
|
|
707
1066
|
row.claim_holder !== null &&
|
|
708
1067
|
maxFailures !== undefined &&
|
|
709
1068
|
Number(row.failure_count) + 1 >= maxFailures
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1069
|
+
if (exhausted) {
|
|
1070
|
+
deadLetterHead.run(now, sessionId, row.consumer, row.cursor_key)
|
|
1071
|
+
} else {
|
|
1072
|
+
claimHead.run(
|
|
1073
|
+
now,
|
|
1074
|
+
now,
|
|
1075
|
+
holder,
|
|
1076
|
+
expiresAt,
|
|
1077
|
+
sessionId,
|
|
1078
|
+
row.consumer,
|
|
1079
|
+
row.cursor_key,
|
|
1080
|
+
)
|
|
1081
|
+
}
|
|
1082
|
+
const claimed = toHead(
|
|
1083
|
+
selectHeadRow.get(
|
|
1084
|
+
sessionId,
|
|
1085
|
+
row.consumer,
|
|
1086
|
+
row.cursor_key,
|
|
1087
|
+
) as HeadRow,
|
|
1088
|
+
)
|
|
1089
|
+
if (exhausted) {
|
|
1090
|
+
releasePins(
|
|
1091
|
+
sessionId,
|
|
1092
|
+
row.consumer,
|
|
1093
|
+
row.cursor_key,
|
|
1094
|
+
claimed.index,
|
|
1095
|
+
)
|
|
1096
|
+
}
|
|
1097
|
+
if (clearIndex > 0) claimed.clearIndex = clearIndex
|
|
1098
|
+
return claimed
|
|
719
1099
|
})
|
|
720
|
-
return { outcome: 'claimed', events }
|
|
1100
|
+
return { outcome: 'claimed', events, head: headOf(sessionId) }
|
|
721
1101
|
}
|
|
722
1102
|
|
|
723
1103
|
const active = db
|
|
724
1104
|
.prepare(
|
|
725
|
-
`select min(claim_expires_at) as retry_at
|
|
726
|
-
from
|
|
727
|
-
|
|
728
|
-
and
|
|
729
|
-
and failed_at is null
|
|
730
|
-
and lane_ready = 1
|
|
731
|
-
and idx > ?
|
|
732
|
-
and claim_expires_at > ?
|
|
733
|
-
${exclusion}`,
|
|
1105
|
+
`select min(cursor.claim_expires_at) as retry_at
|
|
1106
|
+
from a2_cursors cursor
|
|
1107
|
+
${filter}
|
|
1108
|
+
and cursor.claim_expires_at > ?`,
|
|
734
1109
|
)
|
|
735
|
-
.get(
|
|
736
|
-
retry_at: number | bigint | null
|
|
737
|
-
}
|
|
1110
|
+
.get(...params) as { retry_at: number | bigint | null }
|
|
738
1111
|
return active.retry_at === null
|
|
739
1112
|
? { outcome: 'settled' }
|
|
740
1113
|
: { outcome: 'busy', retryAt: new Date(Number(active.retry_at)) }
|
|
@@ -748,65 +1121,76 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
748
1121
|
tx(() => {
|
|
749
1122
|
const now = clock.now().getTime()
|
|
750
1123
|
const expiresAt = expiresAtMs ?? now + ttlMs
|
|
751
|
-
const renewed:
|
|
752
|
-
const superseded:
|
|
753
|
-
const currentAttempt = db.prepare(
|
|
754
|
-
'select attempt_count from a2_events where session_id = ? and idx = ?',
|
|
755
|
-
)
|
|
1124
|
+
const renewed: HeadRef[] = []
|
|
1125
|
+
const superseded: HeadRef[] = []
|
|
756
1126
|
const extend = db.prepare(
|
|
757
|
-
`update
|
|
758
|
-
where session_id = ?
|
|
759
|
-
and
|
|
1127
|
+
`update a2_cursors set claim_expires_at = ?
|
|
1128
|
+
where session_id = ? and consumer = ? and cursor_key = ?
|
|
1129
|
+
and head = ?
|
|
760
1130
|
and attempt_count = ?
|
|
761
|
-
and processed_at is null
|
|
762
1131
|
and failed_at is null
|
|
763
1132
|
and claim_holder = ?
|
|
764
1133
|
and claim_expires_at > ?`,
|
|
765
1134
|
)
|
|
766
1135
|
const clearIndex = clearIndexOf(sessionId)
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1136
|
+
const ordered = claims.toSorted(
|
|
1137
|
+
(a, b) => a.index - b.index || compareText(a.consumer, b.consumer),
|
|
1138
|
+
)
|
|
1139
|
+
for (const { attempt, ...ref } of ordered) {
|
|
1140
|
+
if (ref.index <= clearIndex) {
|
|
1141
|
+
superseded.push(ref)
|
|
770
1142
|
continue
|
|
771
1143
|
}
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1144
|
+
const cursor = cursorOf(sessionId, ref)
|
|
1145
|
+
if (!cursor) continue
|
|
1146
|
+
const head = toNumber(cursor.head)
|
|
1147
|
+
// An event the cursor has not reached yet holds no claim to renew.
|
|
1148
|
+
if (head !== ref.index && Number(cursor.position) < ref.index)
|
|
1149
|
+
continue
|
|
1150
|
+
if (head !== ref.index || Number(cursor.attempt_count) > attempt) {
|
|
1151
|
+
superseded.push(ref)
|
|
777
1152
|
continue
|
|
778
1153
|
}
|
|
779
1154
|
const updated = extend.run(
|
|
780
1155
|
expiresAt,
|
|
781
1156
|
sessionId,
|
|
782
|
-
|
|
783
|
-
|
|
1157
|
+
ref.consumer,
|
|
1158
|
+
cursor.cursor_key,
|
|
1159
|
+
ref.index,
|
|
1160
|
+
attempt,
|
|
784
1161
|
holder,
|
|
785
1162
|
now,
|
|
786
1163
|
)
|
|
787
|
-
if (Number(updated.changes) === 1) renewed.push(
|
|
788
|
-
}
|
|
789
|
-
return {
|
|
790
|
-
renewed: renewed.toSorted((a, b) => a - b),
|
|
791
|
-
superseded: superseded.toSorted((a, b) => a - b),
|
|
1164
|
+
if (Number(updated.changes) === 1) renewed.push(ref)
|
|
792
1165
|
}
|
|
1166
|
+
return { renewed, superseded }
|
|
793
1167
|
}),
|
|
794
1168
|
)
|
|
795
1169
|
},
|
|
796
1170
|
|
|
797
|
-
async completeAttempt({
|
|
1171
|
+
async completeAttempt({
|
|
1172
|
+
sessionId,
|
|
1173
|
+
consumer,
|
|
1174
|
+
lane,
|
|
1175
|
+
index,
|
|
1176
|
+
attempt,
|
|
1177
|
+
events,
|
|
1178
|
+
consumers,
|
|
1179
|
+
}) {
|
|
798
1180
|
return wrap(() =>
|
|
799
1181
|
tx(() => {
|
|
800
1182
|
const clearIndex = clearIndexOf(sessionId)
|
|
801
1183
|
if (index <= clearIndex) return { outcome: 'superseded' }
|
|
802
|
-
const
|
|
803
|
-
|
|
804
|
-
.get(sessionId, index)
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1184
|
+
const cursor = cursorOf(sessionId, { consumer, lane, index })
|
|
1185
|
+
if (!cursor) {
|
|
1186
|
+
if (!selectEvent.get(sessionId, index)) {
|
|
1187
|
+
throw new TypeError(
|
|
1188
|
+
`no event at index ${index} in session '${sessionId}'`,
|
|
1189
|
+
)
|
|
1190
|
+
}
|
|
1191
|
+
return { outcome: 'superseded' }
|
|
809
1192
|
}
|
|
1193
|
+
refreshLogTypes(sessionId, consumers)
|
|
810
1194
|
const ids = events.map((event) => event.id)
|
|
811
1195
|
if (new Set(ids).size !== ids.length) {
|
|
812
1196
|
throw new A2Error(
|
|
@@ -815,14 +1199,38 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
815
1199
|
)
|
|
816
1200
|
}
|
|
817
1201
|
|
|
818
|
-
if (
|
|
819
|
-
|
|
1202
|
+
if (toNumber(cursor.head) !== index) {
|
|
1203
|
+
let completion:
|
|
1204
|
+
{ attempt: number | null; ids: string | null } | undefined
|
|
1205
|
+
if (cursor.kind === 'lane') {
|
|
1206
|
+
const row = selectLaneEvent.get(
|
|
1207
|
+
sessionId,
|
|
1208
|
+
consumer,
|
|
1209
|
+
lane,
|
|
1210
|
+
index,
|
|
1211
|
+
) as
|
|
1212
|
+
| {
|
|
1213
|
+
completed_attempt: number | bigint | null
|
|
1214
|
+
returned_event_ids: string | null
|
|
1215
|
+
}
|
|
1216
|
+
| undefined
|
|
1217
|
+
completion = row && {
|
|
1218
|
+
attempt: toNumber(row.completed_attempt),
|
|
1219
|
+
ids: row.returned_event_ids,
|
|
1220
|
+
}
|
|
1221
|
+
} else if (toNumber(cursor.completed_idx) === index) {
|
|
1222
|
+
completion = {
|
|
1223
|
+
attempt: toNumber(cursor.completed_attempt),
|
|
1224
|
+
ids: cursor.returned_event_ids,
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
if (!completion || completion.attempt !== attempt) {
|
|
820
1228
|
return { outcome: 'superseded' }
|
|
821
1229
|
}
|
|
822
1230
|
const returnedIds =
|
|
823
|
-
|
|
1231
|
+
completion.ids === null
|
|
824
1232
|
? null
|
|
825
|
-
: decodeReturnedEventIds(
|
|
1233
|
+
: decodeReturnedEventIds(completion.ids)
|
|
826
1234
|
if (
|
|
827
1235
|
returnedIds === null ||
|
|
828
1236
|
returnedIds.length !== ids.length ||
|
|
@@ -836,22 +1244,21 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
836
1244
|
const existing =
|
|
837
1245
|
ids.length === 0
|
|
838
1246
|
? []
|
|
839
|
-
: (selectByIds(ids.length).all(
|
|
840
|
-
sessionId,
|
|
841
|
-
clearIndex,
|
|
842
|
-
...ids,
|
|
843
|
-
) as unknown as ExistingEventRow[])
|
|
1247
|
+
: (selectByIds(ids.length).all(...ids) as unknown as EventRow[])
|
|
844
1248
|
const byId = new Map(existing.map((row) => [row.event_id, row]))
|
|
845
1249
|
const ordered = ids.map((id) => byId.get(id))
|
|
846
1250
|
if (
|
|
847
|
-
ordered.some(
|
|
848
|
-
(row)
|
|
1251
|
+
ordered.some((row) => {
|
|
1252
|
+
const cause = row ? decodeCause(row.cause) : null
|
|
1253
|
+
return (
|
|
849
1254
|
!row ||
|
|
850
1255
|
row.session_id !== sessionId ||
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1256
|
+
cause?.consumer !== consumer ||
|
|
1257
|
+
cause.lane !== lane ||
|
|
1258
|
+
cause.index !== index ||
|
|
1259
|
+
cause.attempt !== attempt
|
|
1260
|
+
)
|
|
1261
|
+
})
|
|
855
1262
|
) {
|
|
856
1263
|
throw new A2Error(
|
|
857
1264
|
'PARTIAL_DUPLICATE_BATCH',
|
|
@@ -860,16 +1267,19 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
860
1267
|
}
|
|
861
1268
|
return {
|
|
862
1269
|
outcome: 'completed',
|
|
863
|
-
events:
|
|
864
|
-
events
|
|
865
|
-
|
|
866
|
-
|
|
1270
|
+
events:
|
|
1271
|
+
events.length === 0
|
|
1272
|
+
? []
|
|
1273
|
+
: idempotentReplay(
|
|
1274
|
+
events,
|
|
1275
|
+
(ordered as EventRow[]).map(toStored),
|
|
1276
|
+
),
|
|
867
1277
|
}
|
|
868
1278
|
}
|
|
869
1279
|
if (
|
|
870
|
-
Number(
|
|
871
|
-
|
|
872
|
-
|
|
1280
|
+
Number(cursor.attempt_count) !== attempt ||
|
|
1281
|
+
cursor.claim_holder === null ||
|
|
1282
|
+
cursor.failed_at !== null
|
|
873
1283
|
) {
|
|
874
1284
|
return { outcome: 'superseded' }
|
|
875
1285
|
}
|
|
@@ -881,65 +1291,34 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
881
1291
|
)
|
|
882
1292
|
}
|
|
883
1293
|
|
|
884
|
-
const
|
|
885
|
-
sessionId,
|
|
1294
|
+
const inserted = insertBatch({
|
|
886
1295
|
sessionId,
|
|
1296
|
+
events,
|
|
887
1297
|
clearIndex,
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
claim_holder = null,
|
|
897
|
-
claim_expires_at = null
|
|
898
|
-
where session_id = ? and idx = ?`,
|
|
899
|
-
).run(now, attempt, JSON.stringify(ids), sessionId, index)
|
|
900
|
-
db.prepare(
|
|
901
|
-
'delete from a2_snapshot_pins where session_id = ? and event_index = ?',
|
|
902
|
-
).run(sessionId, index)
|
|
903
|
-
db.prepare(
|
|
904
|
-
`delete from a2_snapshot_history
|
|
905
|
-
where session_id = ?
|
|
906
|
-
and not exists (
|
|
907
|
-
select 1 from a2_snapshot_pins pin
|
|
908
|
-
where pin.session_id = a2_snapshot_history.session_id
|
|
909
|
-
and pin.reducer_name = a2_snapshot_history.reducer_name
|
|
910
|
-
and pin.up_to_index = a2_snapshot_history.up_to_index
|
|
911
|
-
)`,
|
|
912
|
-
).run(sessionId)
|
|
913
|
-
if (parent.lane !== null) {
|
|
914
|
-
promoteLaneHead.run(sessionId, sessionId, parent.lane, clearIndex)
|
|
915
|
-
}
|
|
916
|
-
const inserted = events.map((event, offset) => {
|
|
917
|
-
const id = event.id
|
|
918
|
-
const childIndex = Number(status.max) + offset + 1
|
|
919
|
-
const cause = { index, attempt }
|
|
920
|
-
insertEvent.run(
|
|
921
|
-
sessionId,
|
|
922
|
-
childIndex,
|
|
923
|
-
event.type,
|
|
924
|
-
JSON.stringify(event.payload) ?? 'null',
|
|
925
|
-
id,
|
|
926
|
-
now,
|
|
927
|
-
JSON.stringify(cause),
|
|
928
|
-
event.lane ?? null,
|
|
929
|
-
event.lane ?? null,
|
|
1298
|
+
consumers,
|
|
1299
|
+
cause: { consumer, lane, index, attempt },
|
|
1300
|
+
})
|
|
1301
|
+
const returned = JSON.stringify(ids)
|
|
1302
|
+
if (cursor.kind === 'lane') {
|
|
1303
|
+
finishLaneEvent.run(
|
|
1304
|
+
attempt,
|
|
1305
|
+
returned,
|
|
930
1306
|
sessionId,
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1307
|
+
consumer,
|
|
1308
|
+
lane,
|
|
1309
|
+
index,
|
|
934
1310
|
)
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1311
|
+
} else {
|
|
1312
|
+
recordCompletion.run(
|
|
1313
|
+
index,
|
|
1314
|
+
attempt,
|
|
1315
|
+
returned,
|
|
1316
|
+
sessionId,
|
|
1317
|
+
consumer,
|
|
1318
|
+
cursor.cursor_key,
|
|
941
1319
|
)
|
|
942
|
-
}
|
|
1320
|
+
}
|
|
1321
|
+
advance(sessionId, cursor)
|
|
943
1322
|
return { outcome: 'completed', events: inserted }
|
|
944
1323
|
}),
|
|
945
1324
|
)
|
|
@@ -947,6 +1326,8 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
947
1326
|
|
|
948
1327
|
async failAttempt({
|
|
949
1328
|
sessionId,
|
|
1329
|
+
consumer,
|
|
1330
|
+
lane,
|
|
950
1331
|
index,
|
|
951
1332
|
attempt,
|
|
952
1333
|
error,
|
|
@@ -958,30 +1339,34 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
958
1339
|
if (index <= clearIndexOf(sessionId)) {
|
|
959
1340
|
return { outcome: 'superseded', failureCount: 0 }
|
|
960
1341
|
}
|
|
961
|
-
const current =
|
|
962
|
-
.prepare('select * from a2_events where session_id = ? and idx = ?')
|
|
963
|
-
.get(sessionId, index) as EventRow | undefined
|
|
1342
|
+
const current = cursorOf(sessionId, { consumer, lane, index })
|
|
964
1343
|
if (!current) {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1344
|
+
if (!selectEvent.get(sessionId, index)) {
|
|
1345
|
+
throw new TypeError(
|
|
1346
|
+
`no event at index ${index} in session '${sessionId}'`,
|
|
1347
|
+
)
|
|
1348
|
+
}
|
|
1349
|
+
return { outcome: 'superseded', failureCount: 0 }
|
|
1350
|
+
}
|
|
1351
|
+
if (toNumber(current.head) !== index) {
|
|
1352
|
+
return { outcome: 'superseded', failureCount: 0 }
|
|
968
1353
|
}
|
|
969
1354
|
const failureCount = Number(current.failure_count)
|
|
970
|
-
if (
|
|
971
|
-
current.processed_at !== null ||
|
|
972
|
-
Number(current.attempt_count) !== attempt
|
|
973
|
-
) {
|
|
1355
|
+
if (Number(current.attempt_count) !== attempt) {
|
|
974
1356
|
return { outcome: 'superseded', failureCount }
|
|
975
1357
|
}
|
|
976
|
-
if (current.failed_at !== null) {
|
|
977
|
-
return { outcome: 'dead_lettered', failureCount }
|
|
978
|
-
}
|
|
979
1358
|
if (
|
|
980
1359
|
current.claim_holder === null &&
|
|
981
1360
|
current.last_failed_attempt !== null &&
|
|
982
1361
|
Number(current.last_failed_attempt) === attempt
|
|
983
1362
|
) {
|
|
984
|
-
return {
|
|
1363
|
+
return {
|
|
1364
|
+
outcome: current.failed_at === null ? 'failed' : 'dead_lettered',
|
|
1365
|
+
failureCount,
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
if (current.failed_at !== null) {
|
|
1369
|
+
return { outcome: 'dead_lettered', failureCount }
|
|
985
1370
|
}
|
|
986
1371
|
if (current.claim_holder === null) {
|
|
987
1372
|
return { outcome: 'superseded', failureCount }
|
|
@@ -991,7 +1376,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
991
1376
|
const nextFailureCount = failureCount + 1
|
|
992
1377
|
const deadLettered = nextFailureCount >= maxFailures
|
|
993
1378
|
db.prepare(
|
|
994
|
-
`update
|
|
1379
|
+
`update a2_cursors set
|
|
995
1380
|
failure_count = ?,
|
|
996
1381
|
last_error = ?,
|
|
997
1382
|
last_failed_at = ?,
|
|
@@ -999,7 +1384,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
999
1384
|
failed_at = ?,
|
|
1000
1385
|
claim_holder = null,
|
|
1001
1386
|
claim_expires_at = ?
|
|
1002
|
-
where session_id = ? and
|
|
1387
|
+
where session_id = ? and consumer = ? and cursor_key = ?`,
|
|
1003
1388
|
).run(
|
|
1004
1389
|
nextFailureCount,
|
|
1005
1390
|
error,
|
|
@@ -1010,9 +1395,12 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
1010
1395
|
? null
|
|
1011
1396
|
: now + retryAfterMs,
|
|
1012
1397
|
sessionId,
|
|
1013
|
-
|
|
1398
|
+
consumer,
|
|
1399
|
+
current.cursor_key,
|
|
1014
1400
|
)
|
|
1015
|
-
if (deadLettered)
|
|
1401
|
+
if (deadLettered) {
|
|
1402
|
+
releasePins(sessionId, consumer, current.cursor_key, index)
|
|
1403
|
+
}
|
|
1016
1404
|
return {
|
|
1017
1405
|
outcome: deadLettered ? 'dead_lettered' : 'failed',
|
|
1018
1406
|
failureCount: nextFailureCount,
|
|
@@ -1025,58 +1413,58 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
1025
1413
|
return wrap(() => {
|
|
1026
1414
|
const rows = db
|
|
1027
1415
|
.prepare(
|
|
1028
|
-
|
|
1029
|
-
where session_id = ?
|
|
1030
|
-
and
|
|
1031
|
-
and
|
|
1032
|
-
and idx > coalesce(
|
|
1416
|
+
`${HEAD_SELECT}
|
|
1417
|
+
where cursor.session_id = ?
|
|
1418
|
+
and cursor.failed_at is not null
|
|
1419
|
+
and cursor.head > coalesce(
|
|
1033
1420
|
(select clear_index from a2_clears where session_id = ?), 0
|
|
1034
1421
|
)
|
|
1035
|
-
order by
|
|
1422
|
+
order by cursor.head, cursor.consumer`,
|
|
1036
1423
|
)
|
|
1037
|
-
.all(sessionId, sessionId) as unknown as
|
|
1038
|
-
return rows.map(
|
|
1424
|
+
.all(sessionId, sessionId) as unknown as HeadRow[]
|
|
1425
|
+
return rows.map(toHead)
|
|
1039
1426
|
})
|
|
1040
1427
|
},
|
|
1041
1428
|
|
|
1042
|
-
async redrive({ sessionId, index, action }) {
|
|
1429
|
+
async redrive({ sessionId, consumer, index, action, consumers }) {
|
|
1043
1430
|
return wrap(() =>
|
|
1044
1431
|
tx(() => {
|
|
1045
1432
|
const clearIndex = clearIndexOf(sessionId)
|
|
1046
|
-
const
|
|
1433
|
+
const cursor =
|
|
1047
1434
|
index > clearIndex
|
|
1048
1435
|
? (db
|
|
1049
1436
|
.prepare(
|
|
1050
|
-
|
|
1437
|
+
`select * from a2_cursors
|
|
1438
|
+
where session_id = ? and consumer = ? and head = ?
|
|
1439
|
+
and failed_at is not null`,
|
|
1051
1440
|
)
|
|
1052
|
-
.get(sessionId, index) as
|
|
1053
|
-
| Pick<EventRow, 'lane' | 'processed_at' | 'failed_at'>
|
|
1054
|
-
| undefined)
|
|
1441
|
+
.get(sessionId, consumer, index) as CursorRow | undefined)
|
|
1055
1442
|
: undefined
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1443
|
+
if (cursor) {
|
|
1444
|
+
refreshLogTypes(sessionId, consumers)
|
|
1445
|
+
if (action === 'skip') {
|
|
1446
|
+
if (cursor.kind === 'lane') {
|
|
1447
|
+
finishLaneEvent.run(
|
|
1448
|
+
null,
|
|
1449
|
+
null,
|
|
1450
|
+
sessionId,
|
|
1451
|
+
consumer,
|
|
1452
|
+
cursor.lane,
|
|
1453
|
+
index,
|
|
1454
|
+
)
|
|
1455
|
+
}
|
|
1456
|
+
advance(sessionId, cursor)
|
|
1457
|
+
} else {
|
|
1458
|
+
db.prepare(
|
|
1459
|
+
`update a2_cursors set failed_at = null, failure_count = 0
|
|
1460
|
+
where session_id = ? and consumer = ? and cursor_key = ?`,
|
|
1461
|
+
).run(sessionId, consumer, cursor.cursor_key)
|
|
1067
1462
|
}
|
|
1068
|
-
} else if (redriven) {
|
|
1069
|
-
db.prepare(
|
|
1070
|
-
`update a2_events set failed_at = null, failure_count = 0
|
|
1071
|
-
where session_id = ? and idx = ?`,
|
|
1072
|
-
).run(sessionId, index)
|
|
1073
1463
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
sessionId,
|
|
1077
|
-
|
|
1078
|
-
) as AppendStatusRow
|
|
1079
|
-
return { redriven, hasPending: Number(status.has_pending) === 1 }
|
|
1464
|
+
return {
|
|
1465
|
+
redriven: cursor !== undefined,
|
|
1466
|
+
hasPending: hasPending(sessionId, clearIndex, consumers),
|
|
1467
|
+
}
|
|
1080
1468
|
}),
|
|
1081
1469
|
)
|
|
1082
1470
|
},
|
|
@@ -1116,21 +1504,15 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
1116
1504
|
(select state from snapshot) as snapshot_state,
|
|
1117
1505
|
null as session_id, null as idx, null as event_type,
|
|
1118
1506
|
null as payload, null as event_id, null as created_at,
|
|
1119
|
-
null as cause
|
|
1120
|
-
null as processed_at,
|
|
1121
|
-
null as processed_by_attempt, null as returned_event_ids,
|
|
1122
|
-
null as first_claimed_at,
|
|
1123
|
-
null as last_claimed_at, null as attempt_count,
|
|
1124
|
-
null as failure_count, null as last_failed_at,
|
|
1125
|
-
null as last_failed_attempt, null as last_error,
|
|
1126
|
-
null as failed_at, null as claim_holder,
|
|
1127
|
-
null as claim_expires_at
|
|
1507
|
+
null as cause
|
|
1128
1508
|
union all
|
|
1129
1509
|
select 1 as row_order, 'event' as row_kind,
|
|
1130
1510
|
null as clear_index,
|
|
1131
1511
|
(select up_to_index from head) as head_index,
|
|
1132
1512
|
null as snapshot_index, null as snapshot_state,
|
|
1133
|
-
event
|
|
1513
|
+
event.session_id, event.idx, event.event_type,
|
|
1514
|
+
event.payload, event.event_id, event.created_at,
|
|
1515
|
+
event.cause
|
|
1134
1516
|
from a2_events as event
|
|
1135
1517
|
where event.session_id = ?
|
|
1136
1518
|
and event.idx > max(
|
|
@@ -1182,19 +1564,19 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
1182
1564
|
async putSnapshots(sessionId, reducerName, writes) {
|
|
1183
1565
|
await wrap(() =>
|
|
1184
1566
|
tx(() => {
|
|
1185
|
-
const
|
|
1567
|
+
const selectSnapshotHead = db.prepare(
|
|
1186
1568
|
`select up_to_index, state, updated_at from a2_snapshots
|
|
1187
1569
|
where session_id = ? and reducer_name = ?`,
|
|
1188
1570
|
)
|
|
1189
1571
|
const unfinished = db.prepare(
|
|
1190
|
-
`select 1 from
|
|
1191
|
-
where session_id = ? and
|
|
1192
|
-
and
|
|
1572
|
+
`select 1 from a2_cursors
|
|
1573
|
+
where session_id = ? and consumer = ? and cursor_key = ?
|
|
1574
|
+
and head = ? and head > ? and failed_at is null`,
|
|
1193
1575
|
)
|
|
1194
1576
|
const insertPin = db.prepare(
|
|
1195
1577
|
`insert or ignore into a2_snapshot_pins
|
|
1196
|
-
(session_id, event_index, reducer_name, up_to_index)
|
|
1197
|
-
values (?, ?, ?, ?)`,
|
|
1578
|
+
(session_id, consumer, cursor_key, event_index, reducer_name, up_to_index)
|
|
1579
|
+
values (?, ?, ?, ?, ?, ?)`,
|
|
1198
1580
|
)
|
|
1199
1581
|
const hasPin = db.prepare(
|
|
1200
1582
|
`select 1 from a2_snapshot_pins
|
|
@@ -1226,12 +1608,28 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
1226
1608
|
for (const write of writes.toSorted((a, b) => a.index - b.index)) {
|
|
1227
1609
|
// A fold from before the latest clear describes a lifetime that is gone.
|
|
1228
1610
|
if (write.index < clearIndex) continue
|
|
1229
|
-
for (const
|
|
1230
|
-
|
|
1231
|
-
|
|
1611
|
+
for (const pin of write.pins ?? []) {
|
|
1612
|
+
const cursorKey = cursorKeyOf(pin.lane, pin.index)
|
|
1613
|
+
if (
|
|
1614
|
+
unfinished.get(
|
|
1615
|
+
sessionId,
|
|
1616
|
+
pin.consumer,
|
|
1617
|
+
cursorKey,
|
|
1618
|
+
pin.index,
|
|
1619
|
+
clearIndex,
|
|
1620
|
+
)
|
|
1621
|
+
) {
|
|
1622
|
+
insertPin.run(
|
|
1623
|
+
sessionId,
|
|
1624
|
+
pin.consumer,
|
|
1625
|
+
cursorKey,
|
|
1626
|
+
pin.index,
|
|
1627
|
+
reducerName,
|
|
1628
|
+
write.index,
|
|
1629
|
+
)
|
|
1232
1630
|
}
|
|
1233
1631
|
}
|
|
1234
|
-
const head =
|
|
1632
|
+
const head = selectSnapshotHead.get(sessionId, reducerName) as
|
|
1235
1633
|
SnapshotHeadRow | undefined
|
|
1236
1634
|
const now = clock.now().getTime()
|
|
1237
1635
|
if (!head || write.index >= Number(head.up_to_index)) {
|
|
@@ -1349,15 +1747,7 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
1349
1747
|
tx(() => {
|
|
1350
1748
|
const clearIndex = clearIndexOf(sessionId)
|
|
1351
1749
|
if (cause) assertCurrentCause({ sessionId, cause, clearIndex })
|
|
1352
|
-
const head =
|
|
1353
|
-
(
|
|
1354
|
-
appendStatus.get(
|
|
1355
|
-
sessionId,
|
|
1356
|
-
sessionId,
|
|
1357
|
-
clearIndex,
|
|
1358
|
-
) as AppendStatusRow
|
|
1359
|
-
).max,
|
|
1360
|
-
)
|
|
1750
|
+
const head = headOf(sessionId)
|
|
1361
1751
|
if (ifLastIndex !== undefined && clearIndex === ifLastIndex + 1) {
|
|
1362
1752
|
return { cleared: true, index: head, clearIndex }
|
|
1363
1753
|
}
|
|
@@ -1368,26 +1758,32 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
1368
1758
|
return { cleared: true, index: head, clearIndex }
|
|
1369
1759
|
}
|
|
1370
1760
|
const marker = head + 1
|
|
1371
|
-
const now = clock.now().getTime()
|
|
1372
1761
|
insertEvent.run(
|
|
1373
1762
|
sessionId,
|
|
1374
1763
|
marker,
|
|
1375
1764
|
CLEAR_EVENT_TYPE,
|
|
1376
1765
|
'null',
|
|
1377
1766
|
generateId(),
|
|
1378
|
-
now,
|
|
1379
|
-
null,
|
|
1380
|
-
null,
|
|
1767
|
+
clock.now().getTime(),
|
|
1381
1768
|
null,
|
|
1382
|
-
sessionId,
|
|
1383
|
-
null,
|
|
1384
|
-
clearIndex,
|
|
1385
|
-
now,
|
|
1386
1769
|
)
|
|
1387
1770
|
db.prepare(
|
|
1388
1771
|
`insert into a2_clears (session_id, clear_index) values (?, ?)
|
|
1389
1772
|
on conflict (session_id) do update set clear_index = excluded.clear_index`,
|
|
1390
1773
|
).run(sessionId, marker)
|
|
1774
|
+
// One row per log consumer: they count from the marker. Lane and
|
|
1775
|
+
// concurrent cursors at or below it are superseded by the marker
|
|
1776
|
+
// alone, and reclaim deletes them.
|
|
1777
|
+
db.prepare(
|
|
1778
|
+
`update a2_cursors
|
|
1779
|
+
set position = ?, head = null, ${FRESH_DISPATCH},
|
|
1780
|
+
completed_idx = null, completed_attempt = null,
|
|
1781
|
+
returned_event_ids = null
|
|
1782
|
+
where session_id = ? and kind = 'log'`,
|
|
1783
|
+
).run(marker, sessionId)
|
|
1784
|
+
db.prepare(
|
|
1785
|
+
'delete from a2_snapshot_pins where session_id = ? and event_index < ?',
|
|
1786
|
+
).run(sessionId, marker)
|
|
1391
1787
|
return { cleared: true, index: marker, clearIndex: marker }
|
|
1392
1788
|
}),
|
|
1393
1789
|
)
|
|
@@ -1420,7 +1816,11 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
1420
1816
|
.run(sessionId, through).changes,
|
|
1421
1817
|
)
|
|
1422
1818
|
db.prepare(
|
|
1423
|
-
'delete from
|
|
1819
|
+
'delete from a2_lane_events where session_id = ? and idx <= ?',
|
|
1820
|
+
).run(sessionId, through)
|
|
1821
|
+
db.prepare(
|
|
1822
|
+
`delete from a2_cursors
|
|
1823
|
+
where session_id = ? and kind = 'event' and position < ?`,
|
|
1424
1824
|
).run(sessionId, through)
|
|
1425
1825
|
}
|
|
1426
1826
|
const done =
|
|
@@ -1431,6 +1831,11 @@ export function sqlite(options: SqliteStoreOptions = {}): SqliteStore {
|
|
|
1431
1831
|
.get(sessionId, clearIndex) === undefined
|
|
1432
1832
|
// Writes serialize with the clear, so none lands below the marker later.
|
|
1433
1833
|
if (done) {
|
|
1834
|
+
db.prepare(
|
|
1835
|
+
`delete from a2_cursors
|
|
1836
|
+
where session_id = ? and kind != 'log'
|
|
1837
|
+
and (head is null or head <= ?) and position <= ?`,
|
|
1838
|
+
).run(sessionId, clearIndex, clearIndex)
|
|
1434
1839
|
for (const table of [
|
|
1435
1840
|
'a2_snapshot_pins',
|
|
1436
1841
|
'a2_snapshot_history',
|