experimental-a2 0.20.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 +34 -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.js +3 -6
- 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-CV6rXbB3.d.ts.map → ai-harness-Bfk-Lycn.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 +80 -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-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.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-DYAATFuT.d.ts → server-BshFZ0eL.d.ts} +108 -41
- package/dist/server-BshFZ0eL.d.ts.map +1 -0
- package/dist/{server-qyb5aJJG.js → server-DvIB8kj5.js} +387 -113
- 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 +1586 -566
- 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 +21 -15
- package/docs/02-examples.mdx +1 -1
- package/docs/actors/01-introduction.mdx +8 -0
- 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 +30 -20
- package/docs/agents/04-harnesses.mdx +7 -3
- 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 +4 -4
- package/docs/reference/02-server.mdx +245 -190
- package/docs/reference/04-ai.mdx +20 -19
- package/docs/reference/05-telemetry.mdx +27 -13
- package/docs/reference/06-errors.mdx +24 -18
- 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 +415 -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 +55 -48
- package/docs/stores/01-overview.mdx +3 -3
- package/docs/stores/02-redis.mdx +11 -3
- package/docs/stores/03-redis-http.mdx +2 -2
- package/docs/stores/04-postgres.mdx +65 -14
- package/docs/stores/05-sqlite.mdx +5 -3
- 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 +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/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 +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/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/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 +206 -0
- package/sql/postgres/schema.sql +206 -67
- package/src/actor.ts +101 -71
- package/src/ai-agent.ts +149 -111
- package/src/ai-control-server.ts +8 -8
- package/src/ai-fx.ts +4 -6
- package/src/ai-harness.ts +10 -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 +8 -5
- package/src/idempotent-replay.ts +0 -1
- package/src/index.ts +1 -1
- package/src/internal.ts +15 -5
- package/src/otel.ts +1 -1
- package/src/postgres-schema.ts +98 -57
- 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 +774 -207
- 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 -825
- 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-Clbg5ei7.js.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-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-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/server/01-handlers.mdx +0 -227
package/dist/store-memory.js
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
import { i as A2Error, r as sessionClearedError, s as supersededAttemptError, t as CLEAR_EVENT_TYPE } from "./session-clear-
|
|
2
|
-
import { _ as nullProtoRecord } from "./internal-
|
|
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 { _ as nullProtoRecord } from "./internal-Bc4PvfHk.js";
|
|
3
|
+
import { a as idempotentReplay, i as idleLogCursor } from "./store-codec-DwrWlgQx.js";
|
|
4
4
|
import { n as SYSTEM_CLOCK, t as RANDOM_IDS } from "./store-N8PXxDAS.js";
|
|
5
5
|
//#region src/store-memory.ts
|
|
6
6
|
const RECLAIM_BATCH = 256;
|
|
7
7
|
const headOf = (log) => log ? log.offset + log.rows.length : 0;
|
|
8
|
+
const freshDispatch = () => ({
|
|
9
|
+
firstClaimedAt: null,
|
|
10
|
+
lastClaimedAt: null,
|
|
11
|
+
attemptCount: 0,
|
|
12
|
+
claimHolder: null,
|
|
13
|
+
claimExpiresAt: null,
|
|
14
|
+
failureCount: 0,
|
|
15
|
+
lastFailedAt: null,
|
|
16
|
+
lastFailedAttempt: null,
|
|
17
|
+
lastError: null,
|
|
18
|
+
failedAt: null
|
|
19
|
+
});
|
|
20
|
+
const date = (value) => value ? new Date(value) : null;
|
|
8
21
|
const toStored = (row) => ({
|
|
9
22
|
id: row.id,
|
|
10
23
|
type: row.type,
|
|
@@ -12,21 +25,7 @@ const toStored = (row) => ({
|
|
|
12
25
|
index: row.index,
|
|
13
26
|
sessionId: row.sessionId,
|
|
14
27
|
createdAt: new Date(row.createdAt),
|
|
15
|
-
cause: row.cause ? { ...row.cause } : null
|
|
16
|
-
lane: row.lane,
|
|
17
|
-
processedAt: row.processedAt ? new Date(row.processedAt) : null,
|
|
18
|
-
processedByAttempt: row.processedByAttempt,
|
|
19
|
-
returnedEventIds: row.returnedEventIds ? [...row.returnedEventIds] : null,
|
|
20
|
-
firstClaimedAt: row.firstClaimedAt ? new Date(row.firstClaimedAt) : null,
|
|
21
|
-
lastClaimedAt: row.lastClaimedAt ? new Date(row.lastClaimedAt) : null,
|
|
22
|
-
attemptCount: row.attemptCount,
|
|
23
|
-
claimHolder: row.claimHolder,
|
|
24
|
-
claimExpiresAt: row.claimExpiresAt ? new Date(row.claimExpiresAt) : null,
|
|
25
|
-
failureCount: row.failureCount,
|
|
26
|
-
lastFailedAt: row.lastFailedAt ? new Date(row.lastFailedAt) : null,
|
|
27
|
-
lastFailedAttempt: row.lastFailedAttempt,
|
|
28
|
-
lastError: row.lastError,
|
|
29
|
-
failedAt: row.failedAt ? new Date(row.failedAt) : null
|
|
28
|
+
cause: row.cause ? { ...row.cause } : null
|
|
30
29
|
});
|
|
31
30
|
const toEvent = (row) => ({
|
|
32
31
|
id: row.id,
|
|
@@ -36,13 +35,63 @@ const toEvent = (row) => ({
|
|
|
36
35
|
sessionId: row.sessionId,
|
|
37
36
|
createdAt: new Date(row.createdAt)
|
|
38
37
|
});
|
|
38
|
+
const toHead = (cursor, row) => ({
|
|
39
|
+
...toStored(row),
|
|
40
|
+
consumer: cursor.consumer,
|
|
41
|
+
lane: cursor.lane,
|
|
42
|
+
firstClaimedAt: date(cursor.dispatch.firstClaimedAt),
|
|
43
|
+
lastClaimedAt: date(cursor.dispatch.lastClaimedAt),
|
|
44
|
+
attemptCount: cursor.dispatch.attemptCount,
|
|
45
|
+
claimHolder: cursor.dispatch.claimHolder,
|
|
46
|
+
claimExpiresAt: date(cursor.dispatch.claimExpiresAt),
|
|
47
|
+
failureCount: cursor.dispatch.failureCount,
|
|
48
|
+
lastFailedAt: date(cursor.dispatch.lastFailedAt),
|
|
49
|
+
lastFailedAttempt: cursor.dispatch.lastFailedAttempt,
|
|
50
|
+
lastError: cursor.dispatch.lastError,
|
|
51
|
+
failedAt: date(cursor.dispatch.failedAt)
|
|
52
|
+
});
|
|
53
|
+
const toCursor = (cursor) => ({
|
|
54
|
+
consumer: cursor.consumer,
|
|
55
|
+
lane: cursor.lane,
|
|
56
|
+
position: cursor.position,
|
|
57
|
+
head: cursor.head,
|
|
58
|
+
firstClaimedAt: date(cursor.dispatch.firstClaimedAt),
|
|
59
|
+
lastClaimedAt: date(cursor.dispatch.lastClaimedAt),
|
|
60
|
+
attemptCount: cursor.dispatch.attemptCount,
|
|
61
|
+
claimHolder: cursor.dispatch.claimHolder,
|
|
62
|
+
claimExpiresAt: date(cursor.dispatch.claimExpiresAt),
|
|
63
|
+
failureCount: cursor.dispatch.failureCount,
|
|
64
|
+
lastFailedAt: date(cursor.dispatch.lastFailedAt),
|
|
65
|
+
lastFailedAttempt: cursor.dispatch.lastFailedAttempt,
|
|
66
|
+
lastError: cursor.dispatch.lastError,
|
|
67
|
+
failedAt: date(cursor.dispatch.failedAt)
|
|
68
|
+
});
|
|
69
|
+
const compareText = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
70
|
+
/** Consumer, then lane, with concurrent cursors last by head. */
|
|
71
|
+
const byCursor = (a, b) => compareText(a.consumer, b.consumer) || (a.lane === null ? b.lane === null ? (a.head ?? 0) - (b.head ?? 0) : 1 : b.lane === null ? -1 : compareText(a.lane, b.lane));
|
|
72
|
+
const cursorKey = (consumer, lane, index) => lane === null ? `${consumer}\u0000#${index}` : `${consumer}\u0000L${lane}`;
|
|
73
|
+
const takes = (types, type) => type !== "a2.cleared" && (types === null || types.includes(type));
|
|
74
|
+
const byHead = (a, b) => a.index - b.index || compareText(a.consumer, b.consumer);
|
|
39
75
|
const snapshotKey = (sessionId, reducerName) => `${sessionId}\u0000${reducerName}`;
|
|
40
|
-
const
|
|
76
|
+
const headPinKey = (sessionId, ref) => `${sessionId}\u0000${cursorKey(ref.consumer, ref.lane, ref.index)}\u0000${ref.index}`;
|
|
77
|
+
/** Whether a cursor belongs to a consumer the caller serves; omitted, every one. */
|
|
78
|
+
const servedBy = (consumers) => {
|
|
79
|
+
if (!consumers) return () => true;
|
|
80
|
+
const names = new Set(consumers.map((consumer) => consumer.name));
|
|
81
|
+
return (cursor) => names.has(cursor.consumer);
|
|
82
|
+
};
|
|
83
|
+
/** A cursor's current head. */
|
|
84
|
+
const refOf = (cursor) => ({
|
|
85
|
+
consumer: cursor.consumer,
|
|
86
|
+
lane: cursor.lane,
|
|
87
|
+
index: cursor.head
|
|
88
|
+
});
|
|
41
89
|
function memory(options = {}) {
|
|
42
90
|
const clock = options.clock ?? SYSTEM_CLOCK;
|
|
43
91
|
const generateId = options.ids ?? RANDOM_IDS;
|
|
44
92
|
const sessions = /* @__PURE__ */ new Map();
|
|
45
|
-
|
|
93
|
+
/** Session id → cursor key → cursor. */
|
|
94
|
+
const cursors = /* @__PURE__ */ new Map();
|
|
46
95
|
/** Global unique index on event ids, like the SQL schema's. */
|
|
47
96
|
const byEventId = /* @__PURE__ */ new Map();
|
|
48
97
|
/** Keyed by `${sessionId}\u0000${reducerName}` — a pure cache. */
|
|
@@ -81,9 +130,18 @@ function memory(options = {}) {
|
|
|
81
130
|
const row = byEventId.get(id);
|
|
82
131
|
return row?.sessionId === sessionId && row.index < clearIndexOf(sessionId) ? void 0 : row;
|
|
83
132
|
};
|
|
133
|
+
const cursorsOf = (sessionId) => {
|
|
134
|
+
let map = cursors.get(sessionId);
|
|
135
|
+
if (!map) {
|
|
136
|
+
map = /* @__PURE__ */ new Map();
|
|
137
|
+
cursors.set(sessionId, map);
|
|
138
|
+
}
|
|
139
|
+
return map;
|
|
140
|
+
};
|
|
141
|
+
const cursorAt = (sessionId, ref) => cursors.get(sessionId)?.get(cursorKey(ref.consumer, ref.lane, ref.index));
|
|
84
142
|
const checkpointKey = (sessionId, reducerName, index) => `${snapshotKey(sessionId, reducerName)}\u0000${index}`;
|
|
85
|
-
const releaseSnapshotPins = (sessionId,
|
|
86
|
-
const pinKey =
|
|
143
|
+
const releaseSnapshotPins = (sessionId, ref) => {
|
|
144
|
+
const pinKey = headPinKey(sessionId, ref);
|
|
87
145
|
const checkpoints = snapshotPins.get(pinKey);
|
|
88
146
|
if (!checkpoints) return;
|
|
89
147
|
for (const key of checkpoints) {
|
|
@@ -95,87 +153,62 @@ function memory(options = {}) {
|
|
|
95
153
|
}
|
|
96
154
|
snapshotPins.delete(pinKey);
|
|
97
155
|
};
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
dispatch.set(sessionId, state);
|
|
106
|
-
}
|
|
107
|
-
return state;
|
|
108
|
-
};
|
|
109
|
-
const enqueue = (sessionId, row) => {
|
|
110
|
-
if (row.processedAt !== null) return;
|
|
111
|
-
const state = dispatchOf(sessionId);
|
|
112
|
-
if (row.lane === null) {
|
|
113
|
-
state.ready.set(row.index, row);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
let queue = state.lanes.get(row.lane);
|
|
117
|
-
if (!queue) {
|
|
118
|
-
queue = {
|
|
119
|
-
rows: [],
|
|
120
|
-
head: 0
|
|
121
|
-
};
|
|
122
|
-
state.lanes.set(row.lane, queue);
|
|
123
|
-
}
|
|
124
|
-
queue.rows.push(row);
|
|
125
|
-
if (queue.rows.length === 1) state.ready.set(row.index, row);
|
|
126
|
-
};
|
|
127
|
-
const settle = (sessionId, row) => {
|
|
128
|
-
const state = dispatchOf(sessionId);
|
|
129
|
-
state.ready.delete(row.index);
|
|
130
|
-
if (row.lane === null) return;
|
|
131
|
-
const queue = state.lanes.get(row.lane);
|
|
132
|
-
if (queue.rows[queue.head] !== row) return;
|
|
133
|
-
queue.head += 1;
|
|
134
|
-
if (queue.head === queue.rows.length) {
|
|
135
|
-
state.lanes.delete(row.lane);
|
|
136
|
-
return;
|
|
156
|
+
/** The next event after `after` that a log cursor takes, or null. */
|
|
157
|
+
const nextLogHead = (sessionId, after, types) => {
|
|
158
|
+
const log = sessions.get(sessionId);
|
|
159
|
+
if (!log) return null;
|
|
160
|
+
for (let offset = Math.max(0, after - log.offset); offset < log.rows.length; offset += 1) {
|
|
161
|
+
const row = log.rows[offset];
|
|
162
|
+
if (takes(types, row.type)) return row.index;
|
|
137
163
|
}
|
|
138
|
-
|
|
139
|
-
if (next.failedAt === null) state.ready.set(next.index, next);
|
|
164
|
+
return null;
|
|
140
165
|
};
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
166
|
+
/** Moves a cursor past its head: the next head starts with fresh bookkeeping. */
|
|
167
|
+
const advance = (sessionId, cursor) => {
|
|
168
|
+
const done = cursor.head;
|
|
169
|
+
releaseSnapshotPins(sessionId, refOf(cursor));
|
|
170
|
+
cursor.position = done;
|
|
171
|
+
cursor.dispatch = freshDispatch();
|
|
172
|
+
if (cursor.kind === "event") cursor.head = null;
|
|
173
|
+
else if (cursor.kind === "lane") {
|
|
174
|
+
cursor.queued += 1;
|
|
175
|
+
cursor.head = cursor.queue[cursor.queued] ?? null;
|
|
176
|
+
} else cursor.head = nextLogHead(sessionId, done, cursor.types);
|
|
145
177
|
};
|
|
146
|
-
const hasPending = (sessionId) => {
|
|
147
|
-
const
|
|
148
|
-
|
|
178
|
+
const hasPending = (sessionId, consumers) => {
|
|
179
|
+
const served = servedBy(consumers);
|
|
180
|
+
for (const cursor of cursors.get(sessionId)?.values() ?? []) if (cursor.head !== null && cursor.dispatch.failedAt === null && served(cursor)) return true;
|
|
181
|
+
return false;
|
|
149
182
|
};
|
|
150
|
-
|
|
151
|
-
events,
|
|
152
|
-
hasPending: hasPending(sessionId)
|
|
153
|
-
});
|
|
154
|
-
/** The attempt-currency fence shared by handler appends and clears. */
|
|
183
|
+
/** The attempt-currency fence shared by consumer appends and clears. */
|
|
155
184
|
const assertCurrentCause = (sessionId, cause) => {
|
|
156
185
|
if (cause.index <= clearIndexOf(sessionId)) throw supersededAttemptError(sessionId, cause);
|
|
157
|
-
const
|
|
158
|
-
if (
|
|
186
|
+
const cursor = cursorAt(sessionId, cause);
|
|
187
|
+
if (!cursor || cursor.head !== cause.index || cursor.dispatch.attemptCount !== cause.attempt || cursor.dispatch.failedAt !== null) throw supersededAttemptError(sessionId, cause);
|
|
159
188
|
};
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
189
|
+
const deadLetter = (sessionId, cursor, now) => {
|
|
190
|
+
cursor.dispatch.failedAt = new Date(now);
|
|
191
|
+
releaseSnapshotPins(sessionId, refOf(cursor));
|
|
192
|
+
};
|
|
193
|
+
const claim = ({ sessionId, cursor, holder, now, expiresAt, maxFailures }) => {
|
|
194
|
+
const dispatch = cursor.dispatch;
|
|
195
|
+
const row = rowAt(sessionId, cursor.head);
|
|
196
|
+
if (dispatch.claimHolder !== null) {
|
|
197
|
+
dispatch.failureCount += 1;
|
|
198
|
+
if (maxFailures !== void 0 && dispatch.failureCount >= maxFailures) {
|
|
199
|
+
dispatch.attemptCount += 1;
|
|
200
|
+
dispatch.claimHolder = null;
|
|
201
|
+
dispatch.claimExpiresAt = null;
|
|
202
|
+
deadLetter(sessionId, cursor, now);
|
|
203
|
+
return toHead(cursor, row);
|
|
171
204
|
}
|
|
172
205
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return
|
|
206
|
+
dispatch.attemptCount += 1;
|
|
207
|
+
dispatch.firstClaimedAt ??= new Date(now);
|
|
208
|
+
dispatch.lastClaimedAt = new Date(now);
|
|
209
|
+
dispatch.claimHolder = holder;
|
|
210
|
+
dispatch.claimExpiresAt = new Date(expiresAt);
|
|
211
|
+
return toHead(cursor, row);
|
|
179
212
|
};
|
|
180
213
|
const checkBatchIds = (sessionId, events) => {
|
|
181
214
|
const supplied = events.filter((candidate) => candidate.id !== void 0);
|
|
@@ -185,7 +218,86 @@ function memory(options = {}) {
|
|
|
185
218
|
if (foreign) throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${foreign.id}' already exists in another session`);
|
|
186
219
|
return existing;
|
|
187
220
|
};
|
|
188
|
-
|
|
221
|
+
/** Records the types a declared log consumer takes on its cursor. */
|
|
222
|
+
const refreshLogTypes = (sessionId, consumers) => {
|
|
223
|
+
const map = cursors.get(sessionId);
|
|
224
|
+
if (!map) return;
|
|
225
|
+
for (const consumer of consumers ?? []) {
|
|
226
|
+
if (!consumer.log) continue;
|
|
227
|
+
const cursor = map.get(cursorKey(consumer.name, consumer.log.lane, 0));
|
|
228
|
+
if (cursor?.kind === "log") cursor.types = consumer.log.types;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
/** Starts the cursor work an inserted batch carries. */
|
|
232
|
+
const enqueue = (sessionId, rows, events, base, consumers) => {
|
|
233
|
+
const map = cursorsOf(sessionId);
|
|
234
|
+
rows.forEach((row, offset) => {
|
|
235
|
+
for (const target of events[offset].targets ?? []) {
|
|
236
|
+
const key = cursorKey(target.consumer, target.lane, row.index);
|
|
237
|
+
if (target.lane === null) {
|
|
238
|
+
map.set(key, {
|
|
239
|
+
consumer: target.consumer,
|
|
240
|
+
lane: null,
|
|
241
|
+
kind: "event",
|
|
242
|
+
position: row.index - 1,
|
|
243
|
+
head: row.index,
|
|
244
|
+
dispatch: freshDispatch(),
|
|
245
|
+
queue: [],
|
|
246
|
+
queued: 0,
|
|
247
|
+
completions: /* @__PURE__ */ new Map(),
|
|
248
|
+
types: null
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
let cursor = map.get(key);
|
|
253
|
+
if (!cursor) {
|
|
254
|
+
cursor = {
|
|
255
|
+
consumer: target.consumer,
|
|
256
|
+
lane: target.lane,
|
|
257
|
+
kind: "lane",
|
|
258
|
+
position: clearIndexOf(sessionId),
|
|
259
|
+
head: null,
|
|
260
|
+
dispatch: freshDispatch(),
|
|
261
|
+
queue: [],
|
|
262
|
+
queued: 0,
|
|
263
|
+
completions: /* @__PURE__ */ new Map(),
|
|
264
|
+
types: null
|
|
265
|
+
};
|
|
266
|
+
map.set(key, cursor);
|
|
267
|
+
}
|
|
268
|
+
cursor.queue.push(row.index);
|
|
269
|
+
cursor.head ??= row.index;
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
const clearIndex = clearIndexOf(sessionId);
|
|
273
|
+
for (const consumer of consumers ?? []) {
|
|
274
|
+
const log = consumer.log;
|
|
275
|
+
if (!log) continue;
|
|
276
|
+
const first = rows.find((row) => takes(log.types, row.type));
|
|
277
|
+
if (!first) continue;
|
|
278
|
+
const key = cursorKey(consumer.name, log.lane, 0);
|
|
279
|
+
const cursor = map.get(key);
|
|
280
|
+
if (!cursor) {
|
|
281
|
+
const position = log.backfill ? clearIndex : base;
|
|
282
|
+
map.set(key, {
|
|
283
|
+
consumer: consumer.name,
|
|
284
|
+
lane: log.lane,
|
|
285
|
+
kind: "log",
|
|
286
|
+
position,
|
|
287
|
+
head: log.backfill ? nextLogHead(sessionId, position, log.types) : first.index,
|
|
288
|
+
dispatch: freshDispatch(),
|
|
289
|
+
queue: [],
|
|
290
|
+
queued: 0,
|
|
291
|
+
completions: /* @__PURE__ */ new Map(),
|
|
292
|
+
types: log.types
|
|
293
|
+
});
|
|
294
|
+
} else {
|
|
295
|
+
cursor.types = log.types;
|
|
296
|
+
cursor.head ??= nextLogHead(sessionId, cursor.position, log.types);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const insert = (sessionId, events, now, consumers, cause) => {
|
|
189
301
|
const log = logOf(sessionId);
|
|
190
302
|
const base = headOf(log);
|
|
191
303
|
const inserted = events.map((candidate, offset) => ({
|
|
@@ -195,28 +307,14 @@ function memory(options = {}) {
|
|
|
195
307
|
index: base + 1 + offset,
|
|
196
308
|
sessionId,
|
|
197
309
|
createdAt: new Date(now),
|
|
198
|
-
cause: cause ? { ...cause } : candidate.cause ? { ...candidate.cause } : null
|
|
199
|
-
lane: candidate.lane ?? null,
|
|
200
|
-
processedAt: candidate.settled ? new Date(now) : null,
|
|
201
|
-
processedByAttempt: null,
|
|
202
|
-
returnedEventIds: null,
|
|
203
|
-
firstClaimedAt: null,
|
|
204
|
-
lastClaimedAt: null,
|
|
205
|
-
attemptCount: 0,
|
|
206
|
-
claimHolder: null,
|
|
207
|
-
claimExpiresAt: null,
|
|
208
|
-
failureCount: 0,
|
|
209
|
-
lastFailedAt: null,
|
|
210
|
-
lastFailedAttempt: null,
|
|
211
|
-
lastError: null,
|
|
212
|
-
failedAt: null
|
|
310
|
+
cause: cause ? { ...cause } : candidate.cause ? { ...candidate.cause } : null
|
|
213
311
|
}));
|
|
214
312
|
for (const row of inserted) if (ownerOf(sessionId, row.id)) throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${row.id}' already exists`);
|
|
215
313
|
for (const row of inserted) {
|
|
216
314
|
log.rows.push(row);
|
|
217
315
|
byEventId.set(row.id, row);
|
|
218
|
-
enqueue(sessionId, row);
|
|
219
316
|
}
|
|
317
|
+
enqueue(sessionId, inserted, events, base, consumers);
|
|
220
318
|
return inserted;
|
|
221
319
|
};
|
|
222
320
|
/** Every cached fold of this session below its clear, head or historical. */
|
|
@@ -256,21 +354,35 @@ function memory(options = {}) {
|
|
|
256
354
|
...clearIndex > 0 ? { clearIndex } : {}
|
|
257
355
|
};
|
|
258
356
|
};
|
|
357
|
+
/** The dead-lettered head a redrive names, or undefined. */
|
|
358
|
+
const deadHeadOf = (sessionId, consumer, index) => {
|
|
359
|
+
if (index <= clearIndexOf(sessionId)) return void 0;
|
|
360
|
+
for (const cursor of cursors.get(sessionId)?.values() ?? []) if (cursor.consumer === consumer && cursor.head === index && cursor.dispatch.failedAt !== null) return cursor;
|
|
361
|
+
};
|
|
259
362
|
return {
|
|
260
363
|
async append(sessionId, events, appendOptions) {
|
|
261
|
-
|
|
364
|
+
const consumers = appendOptions?.consumers;
|
|
365
|
+
if (events.length === 0) return {
|
|
366
|
+
events: [],
|
|
367
|
+
hasPending: hasPending(sessionId, consumers)
|
|
368
|
+
};
|
|
262
369
|
const existing = checkBatchIds(sessionId, events);
|
|
263
370
|
if (existing.length > 0) {
|
|
264
|
-
if (existing.length === events.length) return
|
|
371
|
+
if (existing.length === events.length) return {
|
|
372
|
+
events: idempotentReplay(events, existing.map(toStored)),
|
|
373
|
+
hasPending: hasPending(sessionId, consumers)
|
|
374
|
+
};
|
|
265
375
|
throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`);
|
|
266
376
|
}
|
|
267
377
|
const originIndex = appendOptions?.originIndex;
|
|
268
378
|
if (originIndex !== void 0 && originIndex <= clearIndexOf(sessionId)) throw sessionClearedError(`scheduled work from event ${originIndex} was cleared in session '${sessionId}'`);
|
|
269
379
|
for (const event of events) if (event.cause) assertCurrentCause(sessionId, event.cause);
|
|
270
|
-
const
|
|
271
|
-
const inserted = insert(sessionId, events, now);
|
|
380
|
+
const inserted = insert(sessionId, events, clock.now(), consumers);
|
|
272
381
|
notifyStreams(sessionId, inserted);
|
|
273
|
-
return
|
|
382
|
+
return {
|
|
383
|
+
events: inserted.map(toStored),
|
|
384
|
+
hasPending: hasPending(sessionId, consumers)
|
|
385
|
+
};
|
|
274
386
|
},
|
|
275
387
|
async clear({ sessionId, ifLastIndex, cause }) {
|
|
276
388
|
if (cause) assertCurrentCause(sessionId, cause);
|
|
@@ -294,11 +406,21 @@ function memory(options = {}) {
|
|
|
294
406
|
};
|
|
295
407
|
const [marker] = insert(sessionId, [{
|
|
296
408
|
type: CLEAR_EVENT_TYPE,
|
|
297
|
-
payload: null
|
|
298
|
-
|
|
299
|
-
}], clock.now());
|
|
409
|
+
payload: null
|
|
410
|
+
}], clock.now(), void 0);
|
|
300
411
|
logOf(sessionId).clearIndex = marker.index;
|
|
301
|
-
|
|
412
|
+
const map = cursors.get(sessionId);
|
|
413
|
+
for (const [key, cursor] of map ?? []) {
|
|
414
|
+
if (cursor.head !== null) releaseSnapshotPins(sessionId, refOf(cursor));
|
|
415
|
+
if (cursor.kind !== "log") {
|
|
416
|
+
map.delete(key);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
cursor.position = marker.index;
|
|
420
|
+
cursor.head = null;
|
|
421
|
+
cursor.dispatch = freshDispatch();
|
|
422
|
+
cursor.completions.clear();
|
|
423
|
+
}
|
|
302
424
|
notifyStreams(sessionId, [marker]);
|
|
303
425
|
return {
|
|
304
426
|
cleared: true,
|
|
@@ -314,10 +436,7 @@ function memory(options = {}) {
|
|
|
314
436
|
};
|
|
315
437
|
const removed = log.rows.splice(0, Math.min(RECLAIM_BATCH, log.clearIndex - 1 - log.offset));
|
|
316
438
|
log.offset += removed.length;
|
|
317
|
-
for (const row of removed)
|
|
318
|
-
if (byEventId.get(row.id) === row) byEventId.delete(row.id);
|
|
319
|
-
releaseSnapshotPins(sessionId, row.index);
|
|
320
|
-
}
|
|
439
|
+
for (const row of removed) if (byEventId.get(row.id) === row) byEventId.delete(row.id);
|
|
321
440
|
const done = log.offset === log.clearIndex - 1;
|
|
322
441
|
if (done) dropCheckpointsBelow(sessionId, log.clearIndex);
|
|
323
442
|
return {
|
|
@@ -325,6 +444,43 @@ function memory(options = {}) {
|
|
|
325
444
|
done
|
|
326
445
|
};
|
|
327
446
|
},
|
|
447
|
+
async wakeLog({ sessionId, consumer }) {
|
|
448
|
+
const { log } = consumer;
|
|
449
|
+
const key = cursorKey(consumer.name, log.lane, 0);
|
|
450
|
+
const head = headOf(sessions.get(sessionId));
|
|
451
|
+
let cursor = cursors.get(sessionId)?.get(key);
|
|
452
|
+
if (!cursor) {
|
|
453
|
+
const marker = clearIndexOf(sessionId);
|
|
454
|
+
if (head === marker) return {
|
|
455
|
+
cursor: idleLogCursor(consumer.name, log.lane, marker),
|
|
456
|
+
head
|
|
457
|
+
};
|
|
458
|
+
const position = log.backfill ? marker : head;
|
|
459
|
+
cursor = {
|
|
460
|
+
consumer: consumer.name,
|
|
461
|
+
lane: log.lane,
|
|
462
|
+
kind: "log",
|
|
463
|
+
position,
|
|
464
|
+
head: log.backfill ? nextLogHead(sessionId, position, log.types) : null,
|
|
465
|
+
dispatch: freshDispatch(),
|
|
466
|
+
queue: [],
|
|
467
|
+
queued: 0,
|
|
468
|
+
completions: /* @__PURE__ */ new Map(),
|
|
469
|
+
types: log.types
|
|
470
|
+
};
|
|
471
|
+
cursorsOf(sessionId).set(key, cursor);
|
|
472
|
+
} else {
|
|
473
|
+
cursor.types = log.types;
|
|
474
|
+
cursor.head ??= nextLogHead(sessionId, cursor.position, log.types);
|
|
475
|
+
}
|
|
476
|
+
return {
|
|
477
|
+
cursor: toCursor(cursor),
|
|
478
|
+
head
|
|
479
|
+
};
|
|
480
|
+
},
|
|
481
|
+
async cursors(sessionId) {
|
|
482
|
+
return [...cursors.get(sessionId)?.values() ?? []].filter((cursor) => cursor.kind !== "event" || cursor.head !== null).map(toCursor).toSorted(byCursor);
|
|
483
|
+
},
|
|
328
484
|
async read(sessionId, opts) {
|
|
329
485
|
const log = sessions.get(sessionId);
|
|
330
486
|
if (!log) return [];
|
|
@@ -334,41 +490,44 @@ function memory(options = {}) {
|
|
|
334
490
|
if (through <= after) return [];
|
|
335
491
|
return log.rows.slice(after - log.offset, through - log.offset).map(toStored);
|
|
336
492
|
},
|
|
337
|
-
async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs,
|
|
493
|
+
async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs, exclude, consumers, maxFailures, through }) {
|
|
494
|
+
refreshLogTypes(sessionId, consumers);
|
|
338
495
|
const claimedAt = clock.now();
|
|
339
496
|
const now = claimedAt.getTime();
|
|
340
497
|
const expiresAt = new Date(expiresAtMs ?? now + ttlMs);
|
|
341
|
-
const excluded = new Set(
|
|
498
|
+
const excluded = new Set((exclude ?? []).map((ref) => cursorKey(ref.consumer, ref.lane, ref.index)));
|
|
499
|
+
const served = servedBy(consumers);
|
|
342
500
|
const eligible = [];
|
|
343
501
|
let retryAt = null;
|
|
344
|
-
for (const
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
502
|
+
for (const [key, cursor] of cursors.get(sessionId) ?? []) {
|
|
503
|
+
if (cursor.head === null || through !== void 0 && cursor.head > through || cursor.dispatch.failedAt !== null || !served(cursor) || excluded.has(key)) continue;
|
|
504
|
+
const expiry = cursor.dispatch.claimExpiresAt;
|
|
505
|
+
if (expiry && expiry.getTime() > now) {
|
|
506
|
+
if (!retryAt || expiry < retryAt) retryAt = new Date(expiry);
|
|
348
507
|
continue;
|
|
349
508
|
}
|
|
350
|
-
eligible.push(
|
|
509
|
+
eligible.push(cursor);
|
|
351
510
|
}
|
|
352
511
|
if (eligible.length === 0) return retryAt ? {
|
|
353
512
|
outcome: "busy",
|
|
354
513
|
retryAt
|
|
355
514
|
} : { outcome: "settled" };
|
|
356
|
-
eligible.sort((a, b) => a.index - b.index);
|
|
357
515
|
const clearIndex = clearIndexOf(sessionId);
|
|
358
516
|
return {
|
|
359
517
|
outcome: "claimed",
|
|
360
|
-
events: eligible.map((
|
|
361
|
-
const
|
|
518
|
+
events: eligible.map((cursor) => {
|
|
519
|
+
const head = claim({
|
|
362
520
|
sessionId,
|
|
363
|
-
|
|
521
|
+
cursor,
|
|
364
522
|
holder,
|
|
365
523
|
now: claimedAt,
|
|
366
524
|
expiresAt,
|
|
367
525
|
maxFailures
|
|
368
526
|
});
|
|
369
|
-
if (clearIndex > 0)
|
|
370
|
-
return
|
|
371
|
-
})
|
|
527
|
+
if (clearIndex > 0) head.clearIndex = clearIndex;
|
|
528
|
+
return head;
|
|
529
|
+
}).toSorted(byHead),
|
|
530
|
+
head: headOf(sessions.get(sessionId))
|
|
372
531
|
};
|
|
373
532
|
},
|
|
374
533
|
async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {
|
|
@@ -377,20 +536,23 @@ function memory(options = {}) {
|
|
|
377
536
|
const clearIndex = clearIndexOf(sessionId);
|
|
378
537
|
const renewed = [];
|
|
379
538
|
const superseded = [];
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
539
|
+
const ordered = claims.toSorted((a, b) => a.index - b.index || compareText(a.consumer, b.consumer));
|
|
540
|
+
for (const { attempt, ...ref } of ordered) {
|
|
541
|
+
if (ref.index <= clearIndex) {
|
|
542
|
+
superseded.push(ref);
|
|
383
543
|
continue;
|
|
384
544
|
}
|
|
385
|
-
const
|
|
386
|
-
if (!
|
|
387
|
-
|
|
388
|
-
|
|
545
|
+
const cursor = cursorAt(sessionId, ref);
|
|
546
|
+
if (!cursor) continue;
|
|
547
|
+
const dispatch = cursor.dispatch;
|
|
548
|
+
if (cursor.head !== ref.index && cursor.position < ref.index) continue;
|
|
549
|
+
if (cursor.head !== ref.index || dispatch.attemptCount > attempt) {
|
|
550
|
+
superseded.push(ref);
|
|
389
551
|
continue;
|
|
390
552
|
}
|
|
391
|
-
if (
|
|
392
|
-
|
|
393
|
-
renewed.push(
|
|
553
|
+
if (dispatch.failedAt === null && dispatch.claimHolder === holder && dispatch.claimExpiresAt !== null && dispatch.claimExpiresAt > now) {
|
|
554
|
+
dispatch.claimExpiresAt = new Date(expiresAt);
|
|
555
|
+
renewed.push(ref);
|
|
394
556
|
}
|
|
395
557
|
}
|
|
396
558
|
return {
|
|
@@ -398,103 +560,124 @@ function memory(options = {}) {
|
|
|
398
560
|
superseded
|
|
399
561
|
};
|
|
400
562
|
},
|
|
401
|
-
async completeAttempt({ sessionId, index, attempt, events }) {
|
|
563
|
+
async completeAttempt({ sessionId, consumer, lane, index, attempt, events, consumers }) {
|
|
402
564
|
if (index <= clearIndexOf(sessionId)) return { outcome: "superseded" };
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
};
|
|
408
|
-
if (
|
|
409
|
-
if (
|
|
565
|
+
const cursor = cursorAt(sessionId, {
|
|
566
|
+
consumer,
|
|
567
|
+
lane,
|
|
568
|
+
index
|
|
569
|
+
});
|
|
570
|
+
if (!cursor) {
|
|
571
|
+
if (!rowAt(sessionId, index)) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
|
|
572
|
+
return { outcome: "superseded" };
|
|
573
|
+
}
|
|
574
|
+
refreshLogTypes(sessionId, consumers);
|
|
575
|
+
if (cursor.head !== index) {
|
|
576
|
+
const completion = cursor.completions.get(index);
|
|
577
|
+
if (!completion || completion.attempt !== attempt) return { outcome: "superseded" };
|
|
410
578
|
const requestedIds = events.map((event) => event.id);
|
|
411
|
-
if (
|
|
579
|
+
if (completion.returnedEventIds.length !== requestedIds.length || completion.returnedEventIds.some((id, offset) => id !== requestedIds[offset])) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "completed attempt does not match the returned event batch");
|
|
412
580
|
const existing = checkBatchIds(sessionId, events);
|
|
413
|
-
if (existing.length !== events.length || existing.some((row) => row.cause?.index !== index || row.cause.attempt !== attempt)) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "completed attempt does not match the returned event batch");
|
|
581
|
+
if (existing.length !== events.length || existing.some((row) => row.cause?.consumer !== consumer || row.cause.lane !== lane || row.cause.index !== index || row.cause.attempt !== attempt)) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "completed attempt does not match the returned event batch");
|
|
414
582
|
return {
|
|
415
583
|
outcome: "completed",
|
|
416
|
-
events: idempotentReplay(events, existing.map(toStored))
|
|
584
|
+
events: events.length === 0 ? [] : idempotentReplay(events, existing.map(toStored))
|
|
417
585
|
};
|
|
418
586
|
}
|
|
419
|
-
|
|
587
|
+
const dispatch = cursor.dispatch;
|
|
588
|
+
if (dispatch.attemptCount !== attempt || dispatch.failedAt !== null || dispatch.claimHolder === null) return { outcome: "superseded" };
|
|
420
589
|
if (checkBatchIds(sessionId, events).length > 0) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "returned event batch contains already-appended ids");
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
590
|
+
const inserted = insert(sessionId, events, clock.now(), consumers, {
|
|
591
|
+
consumer,
|
|
592
|
+
lane,
|
|
593
|
+
index,
|
|
594
|
+
attempt
|
|
595
|
+
});
|
|
596
|
+
if (cursor.kind === "log") cursor.completions.clear();
|
|
597
|
+
cursor.completions.set(index, {
|
|
598
|
+
attempt,
|
|
599
|
+
returnedEventIds: events.map((event) => event.id)
|
|
600
|
+
});
|
|
601
|
+
advance(sessionId, cursor);
|
|
430
602
|
notifyStreams(sessionId, inserted);
|
|
431
603
|
return {
|
|
432
604
|
outcome: "completed",
|
|
433
605
|
events: inserted.map(toStored)
|
|
434
606
|
};
|
|
435
607
|
},
|
|
436
|
-
async failAttempt({ sessionId, index, attempt, error, maxFailures, retryAfterMs }) {
|
|
608
|
+
async failAttempt({ sessionId, consumer, lane, index, attempt, error, maxFailures, retryAfterMs }) {
|
|
437
609
|
if (index <= clearIndexOf(sessionId)) return {
|
|
438
610
|
outcome: "superseded",
|
|
439
611
|
failureCount: 0
|
|
440
612
|
};
|
|
441
|
-
const
|
|
442
|
-
|
|
613
|
+
const cursor = cursorAt(sessionId, {
|
|
614
|
+
consumer,
|
|
615
|
+
lane,
|
|
616
|
+
index
|
|
617
|
+
});
|
|
618
|
+
if (!cursor) {
|
|
619
|
+
if (!rowAt(sessionId, index)) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
|
|
620
|
+
return {
|
|
621
|
+
outcome: "superseded",
|
|
622
|
+
failureCount: 0
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
const dispatch = cursor.dispatch;
|
|
626
|
+
if (cursor.head !== index || dispatch.attemptCount !== attempt || dispatch.claimHolder === null && dispatch.lastFailedAttempt !== attempt) return {
|
|
443
627
|
outcome: "superseded",
|
|
444
|
-
failureCount:
|
|
628
|
+
failureCount: cursor.head === index ? dispatch.failureCount : 0
|
|
445
629
|
};
|
|
446
|
-
if (
|
|
447
|
-
outcome:
|
|
448
|
-
failureCount:
|
|
630
|
+
if (dispatch.lastFailedAttempt === attempt && dispatch.claimHolder === null) return {
|
|
631
|
+
outcome: dispatch.failedAt ? "dead_lettered" : "failed",
|
|
632
|
+
failureCount: dispatch.failureCount
|
|
449
633
|
};
|
|
450
|
-
if (
|
|
634
|
+
if (dispatch.failedAt) return {
|
|
451
635
|
outcome: "dead_lettered",
|
|
452
|
-
failureCount:
|
|
636
|
+
failureCount: dispatch.failureCount
|
|
453
637
|
};
|
|
454
|
-
|
|
455
|
-
|
|
638
|
+
dispatch.failureCount += 1;
|
|
639
|
+
dispatch.lastError = error;
|
|
456
640
|
const failedAt = clock.now();
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (
|
|
462
|
-
|
|
463
|
-
dispatchOf(sessionId).ready.delete(row.index);
|
|
464
|
-
releaseSnapshotPins(sessionId, index);
|
|
641
|
+
dispatch.lastFailedAt = new Date(failedAt);
|
|
642
|
+
dispatch.lastFailedAttempt = attempt;
|
|
643
|
+
dispatch.claimHolder = null;
|
|
644
|
+
dispatch.claimExpiresAt = retryAfterMs === void 0 || dispatch.failureCount >= maxFailures ? null : new Date(failedAt.getTime() + retryAfterMs);
|
|
645
|
+
if (dispatch.failureCount >= maxFailures) {
|
|
646
|
+
deadLetter(sessionId, cursor, failedAt);
|
|
465
647
|
return {
|
|
466
648
|
outcome: "dead_lettered",
|
|
467
|
-
failureCount:
|
|
649
|
+
failureCount: dispatch.failureCount
|
|
468
650
|
};
|
|
469
651
|
}
|
|
470
652
|
return {
|
|
471
653
|
outcome: "failed",
|
|
472
|
-
failureCount:
|
|
654
|
+
failureCount: dispatch.failureCount
|
|
473
655
|
};
|
|
474
656
|
},
|
|
475
657
|
async deadLetters(sessionId) {
|
|
476
|
-
const
|
|
477
|
-
|
|
658
|
+
const clearIndex = clearIndexOf(sessionId);
|
|
659
|
+
const dead = [];
|
|
660
|
+
for (const cursor of cursors.get(sessionId)?.values() ?? []) {
|
|
661
|
+
if (cursor.head === null || cursor.head <= clearIndex || cursor.dispatch.failedAt === null) continue;
|
|
662
|
+
dead.push(toHead(cursor, rowAt(sessionId, cursor.head)));
|
|
663
|
+
}
|
|
664
|
+
return dead.toSorted(byHead);
|
|
478
665
|
},
|
|
479
|
-
async redrive({ sessionId, index, action }) {
|
|
480
|
-
const
|
|
481
|
-
if (!
|
|
666
|
+
async redrive({ sessionId, consumer, index, action, consumers }) {
|
|
667
|
+
const cursor = deadHeadOf(sessionId, consumer, index);
|
|
668
|
+
if (!cursor) return {
|
|
482
669
|
redriven: false,
|
|
483
|
-
hasPending: hasPending(sessionId)
|
|
670
|
+
hasPending: hasPending(sessionId, consumers)
|
|
484
671
|
};
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
row.failureCount = 0;
|
|
491
|
-
const state = dispatchOf(sessionId);
|
|
492
|
-
const queue = row.lane === null ? void 0 : state.lanes.get(row.lane);
|
|
493
|
-
if (row.lane === null || queue?.rows[queue.head] === row) state.ready.set(row.index, row);
|
|
672
|
+
refreshLogTypes(sessionId, consumers);
|
|
673
|
+
if (action === "skip") advance(sessionId, cursor);
|
|
674
|
+
else {
|
|
675
|
+
cursor.dispatch.failedAt = null;
|
|
676
|
+
cursor.dispatch.failureCount = 0;
|
|
494
677
|
}
|
|
495
678
|
return {
|
|
496
679
|
redriven: true,
|
|
497
|
-
hasPending: hasPending(sessionId)
|
|
680
|
+
hasPending: hasPending(sessionId, consumers)
|
|
498
681
|
};
|
|
499
682
|
},
|
|
500
683
|
async readState(sessionId, reducerName, stateOptions) {
|
|
@@ -509,10 +692,10 @@ function memory(options = {}) {
|
|
|
509
692
|
for (const write of writes.toSorted((a, b) => a.index - b.index)) {
|
|
510
693
|
if (write.index < clearIndex) continue;
|
|
511
694
|
const checkpoint = checkpointKey(sessionId, reducerName, write.index);
|
|
512
|
-
for (const
|
|
513
|
-
const
|
|
514
|
-
if (!
|
|
515
|
-
const pinKey =
|
|
695
|
+
for (const pin of write.pins ?? []) {
|
|
696
|
+
const cursor = cursorAt(sessionId, pin);
|
|
697
|
+
if (!cursor || pin.index <= clearIndex || cursor.head !== pin.index || cursor.dispatch.failedAt !== null) continue;
|
|
698
|
+
const pinKey = headPinKey(sessionId, pin);
|
|
516
699
|
let pinned = snapshotPins.get(pinKey);
|
|
517
700
|
if (!pinned) {
|
|
518
701
|
pinned = /* @__PURE__ */ new Set();
|