experimental-a2 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +1 -1
- package/dist/{actor-C7b-1arn.d.ts → actor-CqibLgjQ.d.ts} +15 -7
- package/dist/actor-CqibLgjQ.d.ts.map +1 -0
- 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-DFET5q16.js.map +1 -0
- 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-DAu77CDr.d.ts} +8 -8
- package/dist/{ai-Cz3hqVhk.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
- package/dist/{ai-mMiWul1T.js → ai-EEfKyc-z.js} +9 -9
- package/dist/ai-EEfKyc-z.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 +4 -2
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +214 -67
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
- package/dist/{ai-harness-CV6rXbB3.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
- package/dist/{ai-harness-Clbg5ei7.js → ai-harness-dQeJaR1t.js} +7 -3
- package/dist/ai-harness-dQeJaR1t.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.js +3 -3
- 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 +86 -73
- 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-BQS4i_V9.js} +29 -6
- package/dist/client-BQS4i_V9.js.map +1 -0
- 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-CvsrprnU.js} +40 -16
- package/dist/internal-CvsrprnU.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-CIeL33wj.js +12 -0
- package/dist/runtime-consumer-CIeL33wj.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -3
- package/dist/scheduler-task-X1SToDjP.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +3 -3
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-qyb5aJJG.js → server-DtFrWxi3.js} +412 -116
- package/dist/server-DtFrWxi3.js.map +1 -0
- package/dist/{server-DYAATFuT.d.ts → server-qgez0Xny.d.ts} +108 -41
- package/dist/server-qgez0Xny.d.ts.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-D1PFe7xl.d.ts} +2 -2
- package/dist/session-clear-D1PFe7xl.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-D0MhBvg6.d.ts +532 -0
- package/dist/store-D0MhBvg6.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- 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 +406 -225
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-7WJV0YWP.js → store-polling-BZt2Rf3D.js} +2 -2
- package/dist/{store-polling-7WJV0YWP.js.map → store-polling-BZt2Rf3D.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 +1598 -587
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
- package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +4 -4
- package/dist/store-redis-notify-CsJw_hfD.js +3060 -0
- package/dist/store-redis-notify-CsJw_hfD.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +4 -5
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +527 -299
- package/dist/store-sqlite.js.map +1 -1
- package/dist/stream-buffer-DrTyiIuo.js +57 -0
- package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
- 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-CV-Z7wz-.js} +6 -5
- package/dist/wire-CV-Z7wz-.js.map +1 -0
- package/docs/01-quickstart.mdx +21 -15
- package/docs/02-examples.mdx +1 -1
- package/docs/actors/01-introduction.mdx +13 -2
- package/docs/actors/02-concurrency.mdx +1 -1
- package/docs/agents/01-introduction.mdx +1 -1
- package/docs/agents/02-tools.mdx +12 -11
- package/docs/agents/03-conversations.mdx +36 -25
- package/docs/agents/04-harnesses.mdx +14 -7
- package/docs/client/03-presence.mdx +18 -14
- 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 +29 -24
- package/docs/reference/01-core.mdx +7 -6
- package/docs/reference/02-server.mdx +250 -191
- package/docs/reference/04-ai.mdx +33 -23
- package/docs/reference/05-telemetry.mdx +27 -13
- package/docs/reference/06-errors.mdx +25 -19
- package/docs/reference/meta.ts +1 -1
- package/docs/schedulers/01-overview.mdx +8 -8
- package/docs/schedulers/02-vercel-queues.mdx +2 -2
- package/docs/schedulers/03-qstash.mdx +4 -4
- package/docs/server/01-consumers.mdx +418 -0
- package/docs/server/02-timers.mdx +31 -21
- 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 +61 -51
- package/docs/stores/01-overview.mdx +3 -3
- package/docs/stores/02-redis.mdx +24 -10
- package/docs/stores/03-redis-http.mdx +8 -6
- package/docs/stores/04-postgres.mdx +65 -14
- package/docs/stores/05-sqlite.mdx +5 -3
- package/docs/upgrades/01-overview.mdx +31 -0
- package/docs/upgrades/78-0-22.mdx +115 -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 +39 -7
- package/examples/playground/app/a0/chat.test.tsx +2 -2
- package/examples/playground/app/a0/sandbox-log.ts +17 -15
- package/examples/playground/app/a0/sandbox.ts +58 -13
- package/examples/playground/app/a0/server.ts +4 -4
- 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 +209 -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/0004_a2_session_event_ids.sql +2 -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/0004_snapshot.json +1165 -0
- package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +41 -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/server.ts +3 -3
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
- package/examples/playground/app/chat/server.ts +45 -35
- package/examples/playground/app/components/demo-icons.tsx +7 -0
- package/examples/playground/app/components/demos.tsx +7 -0
- 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/lib/store.ts +1 -1
- package/examples/playground/next.config.ts +6 -1
- package/examples/playground/package.json +12 -1
- package/examples/playground/scripts/migrate.mjs +31 -0
- package/examples/playground/vercel.json +9 -0
- package/examples/playground/vitest.drills.config.ts +32 -0
- package/package.json +5 -2
- package/sql/postgres/drizzle-schema.ts +209 -0
- package/sql/postgres/schema.sql +227 -67
- package/src/actor-shared.ts +10 -2
- package/src/actor.ts +101 -71
- package/src/ai-agent.ts +162 -111
- package/src/ai-control-server.ts +8 -8
- package/src/ai-fx.ts +324 -82
- package/src/ai-harness.ts +17 -2
- package/src/ai-pi.ts +2 -2
- package/src/ai-server.ts +2 -2
- package/src/ai.ts +7 -7
- package/src/client.ts +19 -2
- package/src/contract.ts +1 -1
- package/src/deterministic-id.ts +27 -4
- package/src/errors.ts +8 -5
- package/src/idempotent-replay.ts +0 -1
- package/src/index.ts +1 -1
- package/src/internal.ts +57 -16
- package/src/otel.ts +1 -1
- package/src/postgres-schema.ts +98 -57
- package/src/presence-recovery.ts +18 -11
- package/src/presence-tombstones.ts +52 -0
- package/src/push-envelope.ts +20 -0
- package/src/runtime-consumer.ts +10 -0
- package/src/scheduler-task.ts +2 -2
- package/src/scheduler-vercel.ts +1 -1
- package/src/server.browser.ts +1 -1
- package/src/server.ts +819 -218
- package/src/session-socket.ts +1 -1
- package/src/store-codec.ts +66 -0
- package/src/store-memory.ts +604 -286
- package/src/store-postgres.ts +2006 -857
- package/src/store-presence-polling.ts +59 -34
- package/src/store-redis-core.ts +1961 -1333
- package/src/store-redis-notify.ts +76 -26
- package/src/store-redis.ts +2 -3
- package/src/store-sqlite.ts +915 -516
- package/src/store.ts +241 -102
- package/src/telemetry.ts +2 -1
- package/src/wire.ts +1 -0
- package/dist/actor-C7b-1arn.d.ts.map +0 -1
- package/dist/actor-shared-t7w8xIKP.js.map +0 -1
- package/dist/ai-harness-Clbg5ei7.js.map +0 -1
- package/dist/ai-mMiWul1T.js.map +0 -1
- package/dist/client-BhHLoNKF.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-DYAATFuT.d.ts.map +0 -1
- package/dist/server-qyb5aJJG.js.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-presence-polling-B1ixeqIS.js +0 -94
- package/dist/store-presence-polling-B1ixeqIS.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/stream-buffer-Cx7AAPGq.js +0 -23
- package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
- package/dist/wire-BKxH1vbP.js.map +0 -1
- package/docs/server/01-handlers.mdx +0 -227
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-BZt2Rf3D.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,52 @@ 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_session_event_id
|
|
42
|
+
on a2_events (session_id, event_id);
|
|
43
|
+
drop index if exists a2_events_event_id;
|
|
44
|
+
create index if not exists a2_events_types on a2_events (session_id, event_type, idx);
|
|
45
|
+
|
|
46
|
+
create table if not exists a2_cursors (
|
|
47
|
+
session_id text not null,
|
|
48
|
+
consumer text not null,
|
|
49
|
+
cursor_key text not null,
|
|
32
50
|
lane text,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
51
|
+
kind text not null,
|
|
52
|
+
position integer not null,
|
|
53
|
+
head integer,
|
|
54
|
+
types text,
|
|
55
|
+
attempt_count integer not null default 0,
|
|
37
56
|
first_claimed_at integer,
|
|
38
57
|
last_claimed_at integer,
|
|
39
|
-
|
|
58
|
+
claim_holder text,
|
|
59
|
+
claim_expires_at integer,
|
|
40
60
|
failure_count integer not null default 0,
|
|
41
61
|
last_failed_at integer,
|
|
42
62
|
last_failed_attempt integer,
|
|
43
63
|
last_error text,
|
|
44
64
|
failed_at integer,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
65
|
+
completed_idx integer,
|
|
66
|
+
completed_attempt integer,
|
|
67
|
+
returned_event_ids text,
|
|
68
|
+
primary key (session_id, consumer, cursor_key)
|
|
48
69
|
) strict;
|
|
49
70
|
|
|
50
|
-
create
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
create
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
create index if not exists a2_cursors_heads
|
|
72
|
+
on a2_cursors (session_id, head) where head is not null;
|
|
73
|
+
|
|
74
|
+
create table if not exists a2_lane_events (
|
|
75
|
+
session_id text not null,
|
|
76
|
+
consumer text not null,
|
|
77
|
+
lane text not null,
|
|
78
|
+
idx integer not null,
|
|
79
|
+
done integer not null default 0,
|
|
80
|
+
completed_attempt integer,
|
|
81
|
+
returned_event_ids text,
|
|
82
|
+
primary key (session_id, consumer, lane, idx)
|
|
83
|
+
) strict;
|
|
61
84
|
|
|
62
85
|
create table if not exists a2_snapshots (
|
|
63
86
|
session_id text not null,
|
|
@@ -79,10 +102,12 @@ create table if not exists a2_snapshot_history (
|
|
|
79
102
|
|
|
80
103
|
create table if not exists a2_snapshot_pins (
|
|
81
104
|
session_id text not null,
|
|
105
|
+
consumer text not null,
|
|
106
|
+
cursor_key text not null,
|
|
82
107
|
event_index integer not null,
|
|
83
108
|
reducer_name text not null,
|
|
84
109
|
up_to_index integer not null,
|
|
85
|
-
primary key (session_id, event_index, reducer_name, up_to_index)
|
|
110
|
+
primary key (session_id, consumer, cursor_key, event_index, reducer_name, up_to_index)
|
|
86
111
|
) strict;
|
|
87
112
|
|
|
88
113
|
create index if not exists a2_snapshot_pins_checkpoint
|
|
@@ -129,17 +154,7 @@ const wrap = (fn) => {
|
|
|
129
154
|
}
|
|
130
155
|
};
|
|
131
156
|
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
|
-
};
|
|
157
|
+
const toNumber = (value) => value === null ? null : Number(value);
|
|
143
158
|
const toStored = (row) => ({
|
|
144
159
|
id: row.event_id,
|
|
145
160
|
type: row.event_type,
|
|
@@ -147,21 +162,7 @@ const toStored = (row) => ({
|
|
|
147
162
|
index: Number(row.idx),
|
|
148
163
|
sessionId: row.session_id,
|
|
149
164
|
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)
|
|
165
|
+
cause: decodeCause(row.cause)
|
|
165
166
|
});
|
|
166
167
|
const toEvent = (row) => ({
|
|
167
168
|
id: row.event_id,
|
|
@@ -171,7 +172,55 @@ const toEvent = (row) => ({
|
|
|
171
172
|
sessionId: row.session_id,
|
|
172
173
|
createdAt: new Date(Number(row.created_at))
|
|
173
174
|
});
|
|
175
|
+
const toCursor = (row) => ({
|
|
176
|
+
consumer: row.consumer,
|
|
177
|
+
lane: row.lane,
|
|
178
|
+
position: Number(row.position),
|
|
179
|
+
head: toNumber(row.head),
|
|
180
|
+
firstClaimedAt: toDate(row.first_claimed_at),
|
|
181
|
+
lastClaimedAt: toDate(row.last_claimed_at),
|
|
182
|
+
attemptCount: Number(row.attempt_count),
|
|
183
|
+
claimHolder: row.claim_holder,
|
|
184
|
+
claimExpiresAt: toDate(row.claim_expires_at),
|
|
185
|
+
failureCount: Number(row.failure_count),
|
|
186
|
+
lastFailedAt: toDate(row.last_failed_at),
|
|
187
|
+
lastFailedAttempt: toNumber(row.last_failed_attempt),
|
|
188
|
+
lastError: row.last_error,
|
|
189
|
+
failedAt: toDate(row.failed_at)
|
|
190
|
+
});
|
|
191
|
+
const toHead = (row) => {
|
|
192
|
+
const { position: _position, head: _head, ...dispatch } = toCursor(row);
|
|
193
|
+
return {
|
|
194
|
+
...toStored({
|
|
195
|
+
session_id: row.session_id,
|
|
196
|
+
idx: row.e_idx,
|
|
197
|
+
event_type: row.e_event_type,
|
|
198
|
+
payload: row.e_payload,
|
|
199
|
+
event_id: row.e_event_id,
|
|
200
|
+
created_at: row.e_created_at,
|
|
201
|
+
cause: row.e_cause
|
|
202
|
+
}),
|
|
203
|
+
...dispatch
|
|
204
|
+
};
|
|
205
|
+
};
|
|
174
206
|
const RECLAIM_BATCH = 256;
|
|
207
|
+
/** The head columns every claim, dead letter, and listing joins in. */
|
|
208
|
+
const HEAD_SELECT = `
|
|
209
|
+
select cursor.*,
|
|
210
|
+
event.idx as e_idx, event.event_type as e_event_type,
|
|
211
|
+
event.payload as e_payload, event.event_id as e_event_id,
|
|
212
|
+
event.created_at as e_created_at, event.cause as e_cause
|
|
213
|
+
from a2_cursors cursor
|
|
214
|
+
join a2_events event
|
|
215
|
+
on event.session_id = cursor.session_id and event.idx = cursor.head`;
|
|
216
|
+
/** A fresh head's dispatch columns, for `set` clauses. */
|
|
217
|
+
const FRESH_DISPATCH = `
|
|
218
|
+
attempt_count = 0, first_claimed_at = null, last_claimed_at = null,
|
|
219
|
+
claim_holder = null, claim_expires_at = null, failure_count = 0,
|
|
220
|
+
last_failed_at = null, last_failed_attempt = null, last_error = null,
|
|
221
|
+
failed_at = null`;
|
|
222
|
+
const placeholders = (count) => Array.from({ length: count }, () => "?").join(", ");
|
|
223
|
+
const compareText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
175
224
|
function sqlite(options = {}) {
|
|
176
225
|
const path = options.path ?? ".a2/dev.db";
|
|
177
226
|
const clock = options.clock ?? SYSTEM_CLOCK;
|
|
@@ -184,37 +233,60 @@ function sqlite(options = {}) {
|
|
|
184
233
|
db.exec("pragma busy_timeout = 5000");
|
|
185
234
|
setWalJournalMode(db);
|
|
186
235
|
db.exec("pragma synchronous = normal");
|
|
236
|
+
if (db.prepare("select 1 from pragma_table_info('a2_events') where name = ?").get("processed_at")) {
|
|
237
|
+
db.close();
|
|
238
|
+
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`);
|
|
239
|
+
}
|
|
187
240
|
db.exec(SCHEMA);
|
|
241
|
+
db.prepare("insert into a2_meta (key, value) values ('layout', ?) on conflict (key) do nothing").run(LAYOUT);
|
|
188
242
|
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
|
-
|
|
243
|
+
(session_id, idx, event_type, payload, event_id, created_at, cause)
|
|
244
|
+
values (?, ?, ?, ?, ?, ?, ?)`);
|
|
245
|
+
const selectHead = db.prepare("select coalesce(max(idx), 0) as max from a2_events where session_id = ?");
|
|
246
|
+
const headOf = (sessionId) => Number(selectHead.get(sessionId).max);
|
|
247
|
+
const selectEvent = db.prepare("select 1 from a2_events where session_id = ? and idx = ?");
|
|
248
|
+
const selectCursor = db.prepare("select * from a2_cursors where session_id = ? and consumer = ? and cursor_key = ?");
|
|
249
|
+
const cursorOf = (sessionId, ref) => selectCursor.get(sessionId, ref.consumer, cursorKeyOf(ref.lane, ref.index));
|
|
250
|
+
const insertEventCursor = db.prepare(`insert into a2_cursors
|
|
251
|
+
(session_id, consumer, cursor_key, lane, kind, position, head)
|
|
252
|
+
values (?, ?, ?, null, 'event', ?, ?)`);
|
|
253
|
+
const insertLaneEvent = db.prepare(`insert into a2_lane_events (session_id, consumer, lane, idx)
|
|
254
|
+
values (?, ?, ?, ?)`);
|
|
255
|
+
const queueLaneHead = db.prepare(`insert into a2_cursors
|
|
256
|
+
(session_id, consumer, cursor_key, lane, kind, position, head)
|
|
257
|
+
values (?, ?, ?, ?, 'lane', ?, ?)
|
|
258
|
+
on conflict (session_id, consumer, cursor_key) do update set
|
|
259
|
+
head = excluded.head,
|
|
260
|
+
position = max(a2_cursors.position, excluded.position),
|
|
261
|
+
${FRESH_DISPATCH}
|
|
262
|
+
where a2_cursors.head is null or a2_cursors.head <= excluded.position`);
|
|
263
|
+
const openLogCursor = db.prepare(`insert into a2_cursors
|
|
264
|
+
(session_id, consumer, cursor_key, lane, kind, position, head, types)
|
|
265
|
+
values (?, ?, ?, ?, 'log', ?, ?, ?)`);
|
|
266
|
+
const wakeLogCursor = db.prepare(`update a2_cursors set types = ?, head = ?
|
|
267
|
+
where session_id = ? and consumer = ? and cursor_key = ?`);
|
|
268
|
+
const refreshTypes = db.prepare(`update a2_cursors set types = ?
|
|
269
|
+
where session_id = ? and consumer = ? and kind = 'log'
|
|
270
|
+
and types is not ?`);
|
|
271
|
+
const nextLogHeadAll = db.prepare(`select min(idx) as next from a2_events
|
|
272
|
+
where session_id = ? and idx > ? and event_type != ?`);
|
|
273
|
+
const nextLogHeadOf = db.prepare(`select min(idx) as next from a2_events
|
|
274
|
+
where session_id = ? and idx > ? and event_type != ?
|
|
275
|
+
and event_type in (select value from json_each(?))`);
|
|
276
|
+
const nextLogHead = (sessionId, after, types) => toNumber((types === null ? nextLogHeadAll.get(sessionId, after, CLEAR_EVENT_TYPE) : nextLogHeadOf.get(sessionId, after, CLEAR_EVENT_TYPE, types)).next);
|
|
277
|
+
const nextLaneHead = db.prepare(`select min(idx) as next from a2_lane_events
|
|
278
|
+
where session_id = ? and consumer = ? and lane = ? and done = 0
|
|
279
|
+
and idx > ?`);
|
|
280
|
+
const selectLaneEvent = db.prepare(`select completed_attempt, returned_event_ids from a2_lane_events
|
|
281
|
+
where session_id = ? and consumer = ? and lane = ? and idx = ?`);
|
|
282
|
+
const finishLaneEvent = db.prepare(`update a2_lane_events
|
|
283
|
+
set done = 1, completed_attempt = ?, returned_event_ids = ?
|
|
284
|
+
where session_id = ? and consumer = ? and lane = ? and idx = ?`);
|
|
285
|
+
const moveCursor = db.prepare(`update a2_cursors set position = ?, head = ?, ${FRESH_DISPATCH}
|
|
286
|
+
where session_id = ? and consumer = ? and cursor_key = ?`);
|
|
287
|
+
const recordCompletion = db.prepare(`update a2_cursors
|
|
288
|
+
set completed_idx = ?, completed_attempt = ?, returned_event_ids = ?
|
|
289
|
+
where session_id = ? and consumer = ? and cursor_key = ?`);
|
|
218
290
|
const upsertPresence = db.prepare(`insert into a2_presence (ns, participant, field, value, seen, at, expires_at)
|
|
219
291
|
values (?, ?, ?, ?, ?, ?, ?)
|
|
220
292
|
on conflict (ns, participant, field) do update set
|
|
@@ -222,46 +294,53 @@ function sqlite(options = {}) {
|
|
|
222
294
|
seen = excluded.seen,
|
|
223
295
|
at = excluded.at,
|
|
224
296
|
expires_at = excluded.expires_at
|
|
225
|
-
where a2_presence.at <= excluded.at
|
|
226
|
-
const deletePresence = db.prepare(`delete from a2_presence
|
|
227
|
-
where ns = ? and participant = ? and field = ? and at <= ?`);
|
|
297
|
+
where a2_presence.at <= excluded.at or a2_presence.expires_at <= ?`);
|
|
228
298
|
const sweepPresence = db.prepare("delete from a2_presence where ns = ? and expires_at <= ?");
|
|
229
299
|
const selectPresence = db.prepare(`select participant, field, value, seen, at, expires_at
|
|
230
300
|
from a2_presence
|
|
231
|
-
where ns = ? and expires_at > ?
|
|
301
|
+
where ns = ? and expires_at > ? and value != 'null'
|
|
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 session_id = ? and 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
|
-
* The rows owning `ids`.
|
|
248
|
-
*
|
|
328
|
+
* The session's rows owning `ids`. Rows below its clear marker do not
|
|
329
|
+
* 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(sessionId,
|
|
253
|
-
if (
|
|
333
|
+
return selectByIds(ids.length).all(sessionId, ...ids).filter((row) => {
|
|
334
|
+
if (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);
|
|
@@ -304,11 +473,9 @@ function sqlite(options = {}) {
|
|
|
304
473
|
if (suppliedIds.length > 0) {
|
|
305
474
|
const existing = liveOwnersOf(sessionId, clearIndex, suppliedIds);
|
|
306
475
|
if (existing.length > 0) {
|
|
307
|
-
const foreign = existing.find((row) => row.session_id !== sessionId);
|
|
308
|
-
if (foreign) throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${foreign.event_id}' already exists in another session`);
|
|
309
476
|
if (existing.length === events.length) return {
|
|
310
477
|
events: idempotentReplay(events, existing.map(toStored)),
|
|
311
|
-
hasPending:
|
|
478
|
+
hasPending: hasPending(sessionId, clearIndex, consumers)
|
|
312
479
|
};
|
|
313
480
|
throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`);
|
|
314
481
|
}
|
|
@@ -320,42 +487,55 @@ function sqlite(options = {}) {
|
|
|
320
487
|
cause: e.cause,
|
|
321
488
|
clearIndex
|
|
322
489
|
});
|
|
323
|
-
const status = appendStatus.get(sessionId, sessionId, clearIndex);
|
|
324
|
-
const base = Number(status.max);
|
|
325
|
-
const now = clock.now().getTime();
|
|
326
490
|
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
|
-
};
|
|
491
|
+
events: insertBatch({
|
|
492
|
+
sessionId,
|
|
493
|
+
events,
|
|
494
|
+
clearIndex,
|
|
495
|
+
consumers
|
|
354
496
|
}),
|
|
355
|
-
hasPending:
|
|
497
|
+
hasPending: hasPending(sessionId, clearIndex, consumers)
|
|
356
498
|
};
|
|
357
499
|
}));
|
|
358
500
|
},
|
|
501
|
+
async wakeLog({ sessionId, consumer }) {
|
|
502
|
+
return wrap(() => {
|
|
503
|
+
const { lane } = consumer.log;
|
|
504
|
+
const marker = clearIndexOf(sessionId);
|
|
505
|
+
if (headOf(sessionId) === marker && !selectCursor.get(sessionId, consumer.name, cursorKeyOf(lane, 0))) return {
|
|
506
|
+
cursor: idleLogCursor(consumer.name, lane, marker),
|
|
507
|
+
head: marker
|
|
508
|
+
};
|
|
509
|
+
return tx(() => {
|
|
510
|
+
const head = headOf(sessionId);
|
|
511
|
+
const key = wakeLogAt({
|
|
512
|
+
sessionId,
|
|
513
|
+
consumer: consumer.name,
|
|
514
|
+
log: consumer.log,
|
|
515
|
+
base: head,
|
|
516
|
+
clearIndex: clearIndexOf(sessionId)
|
|
517
|
+
});
|
|
518
|
+
const cursor = selectCursor.get(sessionId, consumer.name, key);
|
|
519
|
+
return {
|
|
520
|
+
cursor: toCursor(cursor),
|
|
521
|
+
head
|
|
522
|
+
};
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
},
|
|
526
|
+
async cursors(sessionId) {
|
|
527
|
+
return wrap(() => {
|
|
528
|
+
const clearIndex = clearIndexOf(sessionId);
|
|
529
|
+
return db.prepare(`select * from a2_cursors
|
|
530
|
+
where session_id = ?
|
|
531
|
+
and (
|
|
532
|
+
kind = 'log'
|
|
533
|
+
or head > ?
|
|
534
|
+
or (kind = 'lane' and head is null and position > ?)
|
|
535
|
+
)
|
|
536
|
+
order by consumer, lane is null, lane, head`).all(sessionId, clearIndex, clearIndex).map(toCursor);
|
|
537
|
+
});
|
|
538
|
+
},
|
|
359
539
|
async read(sessionId, opts) {
|
|
360
540
|
return wrap(() => {
|
|
361
541
|
const conditions = ["session_id = ?", "idx >= coalesce((select clear_index from a2_clears where session_id = ?), 0)"];
|
|
@@ -371,24 +551,40 @@ function sqlite(options = {}) {
|
|
|
371
551
|
return db.prepare(`select * from a2_events where ${conditions.join(" and ")} order by idx`).all(...params).map(toStored);
|
|
372
552
|
});
|
|
373
553
|
},
|
|
374
|
-
async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs,
|
|
554
|
+
async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs, exclude = [], consumers, maxFailures, through }) {
|
|
375
555
|
return wrap(() => tx(() => {
|
|
556
|
+
refreshLogTypes(sessionId, consumers);
|
|
376
557
|
const now = clock.now().getTime();
|
|
377
558
|
const expiresAt = expiresAtMs ?? now + ttlMs;
|
|
378
559
|
const clearIndex = clearIndexOf(sessionId);
|
|
379
|
-
const
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
560
|
+
const served = servedClause(consumers);
|
|
561
|
+
const excludedKeys = exclude.map((ref) => `${ref.consumer}\u0000${cursorKeyOf(ref.lane, ref.index)}`);
|
|
562
|
+
const exclusion = excludedKeys.length === 0 ? "" : `and cursor.consumer || char(0) || cursor.cursor_key
|
|
563
|
+
not in (${placeholders(excludedKeys.length)})`;
|
|
564
|
+
const filter = `
|
|
565
|
+
where cursor.session_id = ?
|
|
566
|
+
and cursor.head > ?
|
|
567
|
+
and cursor.failed_at is null
|
|
568
|
+
${through === void 0 ? "" : "and cursor.head <= ?"}
|
|
569
|
+
${served.sql}
|
|
570
|
+
${exclusion}`;
|
|
571
|
+
const params = [
|
|
572
|
+
sessionId,
|
|
573
|
+
clearIndex,
|
|
574
|
+
...through === void 0 ? [] : [through],
|
|
575
|
+
...served.params,
|
|
576
|
+
...excludedKeys,
|
|
577
|
+
now
|
|
578
|
+
];
|
|
579
|
+
const eligible = db.prepare(`select cursor.consumer, cursor.cursor_key,
|
|
580
|
+
cursor.claim_holder, cursor.failure_count
|
|
581
|
+
from a2_cursors cursor
|
|
582
|
+
${filter}
|
|
583
|
+
and (cursor.claim_expires_at is null
|
|
584
|
+
or cursor.claim_expires_at <= ?)
|
|
585
|
+
order by cursor.head, cursor.consumer`).all(...params);
|
|
390
586
|
if (eligible.length > 0) {
|
|
391
|
-
const
|
|
587
|
+
const claimHead = db.prepare(`update a2_cursors set
|
|
392
588
|
attempt_count = attempt_count + 1,
|
|
393
589
|
failure_count = failure_count
|
|
394
590
|
+ case when claim_holder is null then 0 else 1 end,
|
|
@@ -396,37 +592,35 @@ function sqlite(options = {}) {
|
|
|
396
592
|
last_claimed_at = ?,
|
|
397
593
|
claim_holder = ?,
|
|
398
594
|
claim_expires_at = ?
|
|
399
|
-
where session_id = ? and
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
595
|
+
where session_id = ? and consumer = ? and cursor_key = ?`);
|
|
596
|
+
const deadLetterHead = db.prepare(`update a2_cursors set
|
|
597
|
+
attempt_count = attempt_count + 1,
|
|
598
|
+
failure_count = failure_count + 1,
|
|
599
|
+
failed_at = ?,
|
|
600
|
+
claim_holder = null,
|
|
601
|
+
claim_expires_at = null
|
|
602
|
+
where session_id = ? and consumer = ? and cursor_key = ?`);
|
|
603
|
+
const selectHeadRow = db.prepare(`${HEAD_SELECT}
|
|
604
|
+
where cursor.session_id = ? and cursor.consumer = ?
|
|
605
|
+
and cursor.cursor_key = ?`);
|
|
409
606
|
return {
|
|
410
607
|
outcome: "claimed",
|
|
411
608
|
events: eligible.map((row) => {
|
|
412
609
|
const exhausted = row.claim_holder !== null && maxFailures !== void 0 && Number(row.failure_count) + 1 >= maxFailures;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const
|
|
416
|
-
if (
|
|
417
|
-
|
|
418
|
-
|
|
610
|
+
if (exhausted) deadLetterHead.run(now, sessionId, row.consumer, row.cursor_key);
|
|
611
|
+
else claimHead.run(now, now, holder, expiresAt, sessionId, row.consumer, row.cursor_key);
|
|
612
|
+
const claimed = toHead(selectHeadRow.get(sessionId, row.consumer, row.cursor_key));
|
|
613
|
+
if (exhausted) releasePins(sessionId, row.consumer, row.cursor_key, claimed.index);
|
|
614
|
+
if (clearIndex > 0) claimed.clearIndex = clearIndex;
|
|
615
|
+
return claimed;
|
|
616
|
+
}),
|
|
617
|
+
head: headOf(sessionId)
|
|
419
618
|
};
|
|
420
619
|
}
|
|
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);
|
|
620
|
+
const active = db.prepare(`select min(cursor.claim_expires_at) as retry_at
|
|
621
|
+
from a2_cursors cursor
|
|
622
|
+
${filter}
|
|
623
|
+
and cursor.claim_expires_at > ?`).get(...params);
|
|
430
624
|
return active.retry_at === null ? { outcome: "settled" } : {
|
|
431
625
|
outcome: "busy",
|
|
432
626
|
retryAt: new Date(Number(active.retry_at))
|
|
@@ -443,113 +637,137 @@ function sqlite(options = {}) {
|
|
|
443
637
|
const expiresAt = expiresAtMs ?? now + ttlMs;
|
|
444
638
|
const renewed = [];
|
|
445
639
|
const superseded = [];
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
and idx = ?
|
|
640
|
+
const extend = db.prepare(`update a2_cursors set claim_expires_at = ?
|
|
641
|
+
where session_id = ? and consumer = ? and cursor_key = ?
|
|
642
|
+
and head = ?
|
|
450
643
|
and attempt_count = ?
|
|
451
|
-
and processed_at is null
|
|
452
644
|
and failed_at is null
|
|
453
645
|
and claim_holder = ?
|
|
454
646
|
and claim_expires_at > ?`);
|
|
455
647
|
const clearIndex = clearIndexOf(sessionId);
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
648
|
+
const ordered = claims.toSorted((a, b) => a.index - b.index || compareText(a.consumer, b.consumer));
|
|
649
|
+
for (const { attempt, ...ref } of ordered) {
|
|
650
|
+
if (ref.index <= clearIndex) {
|
|
651
|
+
superseded.push(ref);
|
|
459
652
|
continue;
|
|
460
653
|
}
|
|
461
|
-
const
|
|
462
|
-
if (!
|
|
463
|
-
|
|
464
|
-
|
|
654
|
+
const cursor = cursorOf(sessionId, ref);
|
|
655
|
+
if (!cursor) continue;
|
|
656
|
+
const head = toNumber(cursor.head);
|
|
657
|
+
if (head !== ref.index && Number(cursor.position) < ref.index) continue;
|
|
658
|
+
if (head !== ref.index || Number(cursor.attempt_count) > attempt) {
|
|
659
|
+
superseded.push(ref);
|
|
465
660
|
continue;
|
|
466
661
|
}
|
|
467
|
-
const updated = extend.run(expiresAt, sessionId,
|
|
468
|
-
if (Number(updated.changes) === 1) renewed.push(
|
|
662
|
+
const updated = extend.run(expiresAt, sessionId, ref.consumer, cursor.cursor_key, ref.index, attempt, holder, now);
|
|
663
|
+
if (Number(updated.changes) === 1) renewed.push(ref);
|
|
469
664
|
}
|
|
470
665
|
return {
|
|
471
|
-
renewed
|
|
472
|
-
superseded
|
|
666
|
+
renewed,
|
|
667
|
+
superseded
|
|
473
668
|
};
|
|
474
669
|
}));
|
|
475
670
|
},
|
|
476
|
-
async completeAttempt({ sessionId, index, attempt, events }) {
|
|
671
|
+
async completeAttempt({ sessionId, consumer, lane, index, attempt, events, consumers }) {
|
|
477
672
|
return wrap(() => tx(() => {
|
|
478
673
|
const clearIndex = clearIndexOf(sessionId);
|
|
479
674
|
if (index <= clearIndex) return { outcome: "superseded" };
|
|
480
|
-
const
|
|
481
|
-
|
|
675
|
+
const cursor = cursorOf(sessionId, {
|
|
676
|
+
consumer,
|
|
677
|
+
lane,
|
|
678
|
+
index
|
|
679
|
+
});
|
|
680
|
+
if (!cursor) {
|
|
681
|
+
if (!selectEvent.get(sessionId, index)) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
|
|
682
|
+
return { outcome: "superseded" };
|
|
683
|
+
}
|
|
684
|
+
refreshLogTypes(sessionId, consumers);
|
|
482
685
|
const ids = events.map((event) => event.id);
|
|
483
686
|
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
|
-
|
|
687
|
+
if (toNumber(cursor.head) !== index) {
|
|
688
|
+
let completion;
|
|
689
|
+
if (cursor.kind === "lane") {
|
|
690
|
+
const row = selectLaneEvent.get(sessionId, consumer, lane, index);
|
|
691
|
+
completion = row && {
|
|
692
|
+
attempt: toNumber(row.completed_attempt),
|
|
693
|
+
ids: row.returned_event_ids
|
|
694
|
+
};
|
|
695
|
+
} else if (toNumber(cursor.completed_idx) === index) completion = {
|
|
696
|
+
attempt: toNumber(cursor.completed_attempt),
|
|
697
|
+
ids: cursor.returned_event_ids
|
|
698
|
+
};
|
|
699
|
+
if (!completion || completion.attempt !== attempt) return { outcome: "superseded" };
|
|
700
|
+
const returnedIds = completion.ids === null ? null : decodeReturnedEventIds(completion.ids);
|
|
487
701
|
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(sessionId,
|
|
702
|
+
const existing = ids.length === 0 ? [] : selectByIds(ids.length).all(sessionId, ...ids);
|
|
489
703
|
const byId = new Map(existing.map((row) => [row.event_id, row]));
|
|
490
704
|
const ordered = ids.map((id) => byId.get(id));
|
|
491
|
-
if (ordered.some((row) =>
|
|
705
|
+
if (ordered.some((row) => {
|
|
706
|
+
const cause = row ? decodeCause(row.cause) : null;
|
|
707
|
+
return !row || cause?.consumer !== consumer || cause.lane !== lane || cause.index !== index || cause.attempt !== attempt;
|
|
708
|
+
})) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event retry does not match the committed batch");
|
|
492
709
|
return {
|
|
493
710
|
outcome: "completed",
|
|
494
|
-
events: idempotentReplay(events, ordered.map(toStored))
|
|
711
|
+
events: events.length === 0 ? [] : idempotentReplay(events, ordered.map(toStored))
|
|
495
712
|
};
|
|
496
713
|
}
|
|
497
|
-
if (Number(
|
|
714
|
+
if (Number(cursor.attempt_count) !== attempt || cursor.claim_holder === null || cursor.failed_at !== null) return { outcome: "superseded" };
|
|
498
715
|
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);
|
|
716
|
+
const inserted = insertBatch({
|
|
717
|
+
sessionId,
|
|
718
|
+
events,
|
|
719
|
+
clearIndex,
|
|
720
|
+
consumers,
|
|
721
|
+
cause: {
|
|
722
|
+
consumer,
|
|
723
|
+
lane,
|
|
724
|
+
index,
|
|
725
|
+
attempt
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
const returned = JSON.stringify(ids);
|
|
729
|
+
if (cursor.kind === "lane") finishLaneEvent.run(attempt, returned, sessionId, consumer, lane, index);
|
|
730
|
+
else recordCompletion.run(index, attempt, returned, sessionId, consumer, cursor.cursor_key);
|
|
731
|
+
advance(sessionId, cursor);
|
|
519
732
|
return {
|
|
520
733
|
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
|
-
})
|
|
734
|
+
events: inserted
|
|
531
735
|
};
|
|
532
736
|
}));
|
|
533
737
|
},
|
|
534
|
-
async failAttempt({ sessionId, index, attempt, error, maxFailures, retryAfterMs }) {
|
|
738
|
+
async failAttempt({ sessionId, consumer, lane, index, attempt, error, maxFailures, retryAfterMs }) {
|
|
535
739
|
return wrap(() => tx(() => {
|
|
536
740
|
if (index <= clearIndexOf(sessionId)) return {
|
|
537
741
|
outcome: "superseded",
|
|
538
742
|
failureCount: 0
|
|
539
743
|
};
|
|
540
|
-
const current =
|
|
541
|
-
|
|
744
|
+
const current = cursorOf(sessionId, {
|
|
745
|
+
consumer,
|
|
746
|
+
lane,
|
|
747
|
+
index
|
|
748
|
+
});
|
|
749
|
+
if (!current) {
|
|
750
|
+
if (!selectEvent.get(sessionId, index)) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
|
|
751
|
+
return {
|
|
752
|
+
outcome: "superseded",
|
|
753
|
+
failureCount: 0
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
if (toNumber(current.head) !== index) return {
|
|
757
|
+
outcome: "superseded",
|
|
758
|
+
failureCount: 0
|
|
759
|
+
};
|
|
542
760
|
const failureCount = Number(current.failure_count);
|
|
543
|
-
if (
|
|
761
|
+
if (Number(current.attempt_count) !== attempt) return {
|
|
544
762
|
outcome: "superseded",
|
|
545
763
|
failureCount
|
|
546
764
|
};
|
|
547
|
-
if (current.
|
|
548
|
-
outcome: "dead_lettered",
|
|
765
|
+
if (current.claim_holder === null && current.last_failed_attempt !== null && Number(current.last_failed_attempt) === attempt) return {
|
|
766
|
+
outcome: current.failed_at === null ? "failed" : "dead_lettered",
|
|
549
767
|
failureCount
|
|
550
768
|
};
|
|
551
|
-
if (current.
|
|
552
|
-
outcome: "
|
|
769
|
+
if (current.failed_at !== null) return {
|
|
770
|
+
outcome: "dead_lettered",
|
|
553
771
|
failureCount
|
|
554
772
|
};
|
|
555
773
|
if (current.claim_holder === null) return {
|
|
@@ -559,7 +777,7 @@ function sqlite(options = {}) {
|
|
|
559
777
|
const now = clock.now().getTime();
|
|
560
778
|
const nextFailureCount = failureCount + 1;
|
|
561
779
|
const deadLettered = nextFailureCount >= maxFailures;
|
|
562
|
-
db.prepare(`update
|
|
780
|
+
db.prepare(`update a2_cursors set
|
|
563
781
|
failure_count = ?,
|
|
564
782
|
last_error = ?,
|
|
565
783
|
last_failed_at = ?,
|
|
@@ -567,8 +785,8 @@ function sqlite(options = {}) {
|
|
|
567
785
|
failed_at = ?,
|
|
568
786
|
claim_holder = null,
|
|
569
787
|
claim_expires_at = ?
|
|
570
|
-
where session_id = ? and
|
|
571
|
-
if (deadLettered) releasePins(sessionId, index);
|
|
788
|
+
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);
|
|
789
|
+
if (deadLettered) releasePins(sessionId, consumer, current.cursor_key, index);
|
|
572
790
|
return {
|
|
573
791
|
outcome: deadLettered ? "dead_lettered" : "failed",
|
|
574
792
|
failureCount: nextFailureCount
|
|
@@ -577,31 +795,32 @@ function sqlite(options = {}) {
|
|
|
577
795
|
},
|
|
578
796
|
async deadLetters(sessionId) {
|
|
579
797
|
return wrap(() => {
|
|
580
|
-
return db.prepare(
|
|
581
|
-
where session_id = ?
|
|
582
|
-
and
|
|
583
|
-
and
|
|
584
|
-
and idx > coalesce(
|
|
798
|
+
return db.prepare(`${HEAD_SELECT}
|
|
799
|
+
where cursor.session_id = ?
|
|
800
|
+
and cursor.failed_at is not null
|
|
801
|
+
and cursor.head > coalesce(
|
|
585
802
|
(select clear_index from a2_clears where session_id = ?), 0
|
|
586
803
|
)
|
|
587
|
-
order by
|
|
804
|
+
order by cursor.head, cursor.consumer`).all(sessionId, sessionId).map(toHead);
|
|
588
805
|
});
|
|
589
806
|
},
|
|
590
|
-
async redrive({ sessionId, index, action }) {
|
|
807
|
+
async redrive({ sessionId, consumer, index, action, consumers }) {
|
|
591
808
|
return wrap(() => tx(() => {
|
|
592
809
|
const clearIndex = clearIndexOf(sessionId);
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
if (
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
810
|
+
const cursor = index > clearIndex ? db.prepare(`select * from a2_cursors
|
|
811
|
+
where session_id = ? and consumer = ? and head = ?
|
|
812
|
+
and failed_at is not null`).get(sessionId, consumer, index) : void 0;
|
|
813
|
+
if (cursor) {
|
|
814
|
+
refreshLogTypes(sessionId, consumers);
|
|
815
|
+
if (action === "skip") {
|
|
816
|
+
if (cursor.kind === "lane") finishLaneEvent.run(null, null, sessionId, consumer, cursor.lane, index);
|
|
817
|
+
advance(sessionId, cursor);
|
|
818
|
+
} else db.prepare(`update a2_cursors set failed_at = null, failure_count = 0
|
|
819
|
+
where session_id = ? and consumer = ? and cursor_key = ?`).run(sessionId, consumer, cursor.cursor_key);
|
|
820
|
+
}
|
|
602
821
|
return {
|
|
603
|
-
redriven,
|
|
604
|
-
hasPending:
|
|
822
|
+
redriven: cursor !== void 0,
|
|
823
|
+
hasPending: hasPending(sessionId, clearIndex, consumers)
|
|
605
824
|
};
|
|
606
825
|
}));
|
|
607
826
|
},
|
|
@@ -637,21 +856,15 @@ function sqlite(options = {}) {
|
|
|
637
856
|
(select state from snapshot) as snapshot_state,
|
|
638
857
|
null as session_id, null as idx, null as event_type,
|
|
639
858
|
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
|
|
859
|
+
null as cause
|
|
649
860
|
union all
|
|
650
861
|
select 1 as row_order, 'event' as row_kind,
|
|
651
862
|
null as clear_index,
|
|
652
863
|
(select up_to_index from head) as head_index,
|
|
653
864
|
null as snapshot_index, null as snapshot_state,
|
|
654
|
-
event
|
|
865
|
+
event.session_id, event.idx, event.event_type,
|
|
866
|
+
event.payload, event.event_id, event.created_at,
|
|
867
|
+
event.cause
|
|
655
868
|
from a2_events as event
|
|
656
869
|
where event.session_id = ?
|
|
657
870
|
and event.idx > max(
|
|
@@ -675,14 +888,14 @@ function sqlite(options = {}) {
|
|
|
675
888
|
},
|
|
676
889
|
async putSnapshots(sessionId, reducerName, writes) {
|
|
677
890
|
await wrap(() => tx(() => {
|
|
678
|
-
const
|
|
891
|
+
const selectSnapshotHead = db.prepare(`select up_to_index, state, updated_at from a2_snapshots
|
|
679
892
|
where session_id = ? and reducer_name = ?`);
|
|
680
|
-
const unfinished = db.prepare(`select 1 from
|
|
681
|
-
where session_id = ? and
|
|
682
|
-
and
|
|
893
|
+
const unfinished = db.prepare(`select 1 from a2_cursors
|
|
894
|
+
where session_id = ? and consumer = ? and cursor_key = ?
|
|
895
|
+
and head = ? and head > ? and failed_at is null`);
|
|
683
896
|
const insertPin = db.prepare(`insert or ignore into a2_snapshot_pins
|
|
684
|
-
(session_id, event_index, reducer_name, up_to_index)
|
|
685
|
-
values (?, ?, ?, ?)`);
|
|
897
|
+
(session_id, consumer, cursor_key, event_index, reducer_name, up_to_index)
|
|
898
|
+
values (?, ?, ?, ?, ?, ?)`);
|
|
686
899
|
const hasPin = db.prepare(`select 1 from a2_snapshot_pins
|
|
687
900
|
where session_id = ? and reducer_name = ? and up_to_index = ?
|
|
688
901
|
limit 1`);
|
|
@@ -704,8 +917,11 @@ function sqlite(options = {}) {
|
|
|
704
917
|
const clearIndex = clearIndexOf(sessionId);
|
|
705
918
|
for (const write of writes.toSorted((a, b) => a.index - b.index)) {
|
|
706
919
|
if (write.index < clearIndex) continue;
|
|
707
|
-
for (const
|
|
708
|
-
|
|
920
|
+
for (const pin of write.pins ?? []) {
|
|
921
|
+
const cursorKey = cursorKeyOf(pin.lane, pin.index);
|
|
922
|
+
if (unfinished.get(sessionId, pin.consumer, cursorKey, pin.index, clearIndex)) insertPin.run(sessionId, pin.consumer, cursorKey, pin.index, reducerName, write.index);
|
|
923
|
+
}
|
|
924
|
+
const head = selectSnapshotHead.get(sessionId, reducerName);
|
|
709
925
|
const now = clock.now().getTime();
|
|
710
926
|
if (!head || write.index >= Number(head.up_to_index)) {
|
|
711
927
|
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));
|
|
@@ -721,9 +937,9 @@ function sqlite(options = {}) {
|
|
|
721
937
|
if (entries.length === 0) return;
|
|
722
938
|
wrap(() => tx(() => {
|
|
723
939
|
const atMs = meta.at.getTime();
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
940
|
+
const nowMs = clock.now().getTime();
|
|
941
|
+
const expiresAtMs = nowMs + meta.ttlMs;
|
|
942
|
+
for (const [field, value] of entries) upsertPresence.run(ns, participant, field, JSON.stringify(value) ?? "null", meta.seen, atMs, expiresAtMs, nowMs);
|
|
727
943
|
}));
|
|
728
944
|
},
|
|
729
945
|
async read(ns) {
|
|
@@ -758,7 +974,7 @@ function sqlite(options = {}) {
|
|
|
758
974
|
cause,
|
|
759
975
|
clearIndex
|
|
760
976
|
});
|
|
761
|
-
const head =
|
|
977
|
+
const head = headOf(sessionId);
|
|
762
978
|
if (ifLastIndex !== void 0 && clearIndex === ifLastIndex + 1) return {
|
|
763
979
|
cleared: true,
|
|
764
980
|
index: head,
|
|
@@ -775,10 +991,15 @@ function sqlite(options = {}) {
|
|
|
775
991
|
clearIndex
|
|
776
992
|
};
|
|
777
993
|
const marker = head + 1;
|
|
778
|
-
|
|
779
|
-
insertEvent.run(sessionId, marker, CLEAR_EVENT_TYPE, "null", generateId(), now, null, null, null, sessionId, null, clearIndex, now);
|
|
994
|
+
insertEvent.run(sessionId, marker, CLEAR_EVENT_TYPE, "null", generateId(), clock.now().getTime(), null);
|
|
780
995
|
db.prepare(`insert into a2_clears (session_id, clear_index) values (?, ?)
|
|
781
996
|
on conflict (session_id) do update set clear_index = excluded.clear_index`).run(sessionId, marker);
|
|
997
|
+
db.prepare(`update a2_cursors
|
|
998
|
+
set position = ?, head = null, ${FRESH_DISPATCH},
|
|
999
|
+
completed_idx = null, completed_attempt = null,
|
|
1000
|
+
returned_event_ids = null
|
|
1001
|
+
where session_id = ? and kind = 'log'`).run(marker, sessionId);
|
|
1002
|
+
db.prepare("delete from a2_snapshot_pins where session_id = ? and event_index < ?").run(sessionId, marker);
|
|
782
1003
|
return {
|
|
783
1004
|
cleared: true,
|
|
784
1005
|
index: marker,
|
|
@@ -802,14 +1023,21 @@ function sqlite(options = {}) {
|
|
|
802
1023
|
if (last.last !== null) {
|
|
803
1024
|
const through = Number(last.last);
|
|
804
1025
|
deleted = Number(db.prepare("delete from a2_events where session_id = ? and idx <= ?").run(sessionId, through).changes);
|
|
805
|
-
db.prepare("delete from
|
|
1026
|
+
db.prepare("delete from a2_lane_events where session_id = ? and idx <= ?").run(sessionId, through);
|
|
1027
|
+
db.prepare(`delete from a2_cursors
|
|
1028
|
+
where session_id = ? and kind = 'event' and position < ?`).run(sessionId, through);
|
|
806
1029
|
}
|
|
807
1030
|
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
|
-
|
|
1031
|
+
if (done) {
|
|
1032
|
+
db.prepare(`delete from a2_cursors
|
|
1033
|
+
where session_id = ? and kind != 'log'
|
|
1034
|
+
and (head is null or head <= ?) and position <= ?`).run(sessionId, clearIndex, clearIndex);
|
|
1035
|
+
for (const table of [
|
|
1036
|
+
"a2_snapshot_pins",
|
|
1037
|
+
"a2_snapshot_history",
|
|
1038
|
+
"a2_snapshots"
|
|
1039
|
+
]) db.prepare(`delete from ${table} where session_id = ? and up_to_index < ?`).run(sessionId, clearIndex);
|
|
1040
|
+
}
|
|
813
1041
|
return {
|
|
814
1042
|
deleted,
|
|
815
1043
|
done
|