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-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 {
|
|
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 { y as nullProtoRecord } from "./internal-CvsrprnU.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,15 +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
|
-
|
|
46
|
-
|
|
47
|
-
const byEventId = /* @__PURE__ */ new Map();
|
|
93
|
+
/** Session id → cursor key → cursor. */
|
|
94
|
+
const cursors = /* @__PURE__ */ new Map();
|
|
48
95
|
/** Keyed by `${sessionId}\u0000${reducerName}` — a pure cache. */
|
|
49
96
|
const snapshots = /* @__PURE__ */ new Map();
|
|
50
97
|
const historicalSnapshots = /* @__PURE__ */ new Map();
|
|
@@ -65,7 +112,8 @@ function memory(options = {}) {
|
|
|
65
112
|
log = {
|
|
66
113
|
rows: [],
|
|
67
114
|
offset: 0,
|
|
68
|
-
clearIndex: 0
|
|
115
|
+
clearIndex: 0,
|
|
116
|
+
byEventId: /* @__PURE__ */ new Map()
|
|
69
117
|
};
|
|
70
118
|
sessions.set(sessionId, log);
|
|
71
119
|
}
|
|
@@ -76,14 +124,24 @@ function memory(options = {}) {
|
|
|
76
124
|
return log && index > log.offset ? log.rows[index - log.offset - 1] : void 0;
|
|
77
125
|
};
|
|
78
126
|
const clearIndexOf = (sessionId) => sessions.get(sessionId)?.clearIndex ?? 0;
|
|
79
|
-
/** The row owning `id`, ignoring
|
|
127
|
+
/** The session's row owning `id`, ignoring rows below its clear. */
|
|
80
128
|
const ownerOf = (sessionId, id) => {
|
|
81
|
-
const
|
|
82
|
-
|
|
129
|
+
const log = sessions.get(sessionId);
|
|
130
|
+
const row = log?.byEventId.get(id);
|
|
131
|
+
return log && row && row.index >= log.clearIndex ? row : void 0;
|
|
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;
|
|
83
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,97 +153,148 @@ 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);
|
|
188
|
+
};
|
|
189
|
+
const deadLetter = (sessionId, cursor, now) => {
|
|
190
|
+
cursor.dispatch.failedAt = new Date(now);
|
|
191
|
+
releaseSnapshotPins(sessionId, refOf(cursor));
|
|
159
192
|
};
|
|
160
|
-
const claim = ({ sessionId,
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return
|
|
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);
|
|
182
215
|
if (new Set(supplied.map((candidate) => candidate.id)).size !== supplied.length) throw new A2Error("PARTIAL_DUPLICATE_BATCH", "batch contains the same event id more than once");
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
216
|
+
return supplied.map((candidate) => ownerOf(sessionId, candidate.id)).filter((row) => row !== void 0);
|
|
217
|
+
};
|
|
218
|
+
/** Records the types a declared log consumer takes on its cursor. */
|
|
219
|
+
const refreshLogTypes = (sessionId, consumers) => {
|
|
220
|
+
const map = cursors.get(sessionId);
|
|
221
|
+
if (!map) return;
|
|
222
|
+
for (const consumer of consumers ?? []) {
|
|
223
|
+
if (!consumer.log) continue;
|
|
224
|
+
const cursor = map.get(cursorKey(consumer.name, consumer.log.lane, 0));
|
|
225
|
+
if (cursor?.kind === "log") cursor.types = consumer.log.types;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
/** Starts the cursor work an inserted batch carries. */
|
|
229
|
+
const enqueue = (sessionId, rows, events, base, consumers) => {
|
|
230
|
+
const map = cursorsOf(sessionId);
|
|
231
|
+
rows.forEach((row, offset) => {
|
|
232
|
+
for (const target of events[offset].targets ?? []) {
|
|
233
|
+
const key = cursorKey(target.consumer, target.lane, row.index);
|
|
234
|
+
if (target.lane === null) {
|
|
235
|
+
map.set(key, {
|
|
236
|
+
consumer: target.consumer,
|
|
237
|
+
lane: null,
|
|
238
|
+
kind: "event",
|
|
239
|
+
position: row.index - 1,
|
|
240
|
+
head: row.index,
|
|
241
|
+
dispatch: freshDispatch(),
|
|
242
|
+
queue: [],
|
|
243
|
+
queued: 0,
|
|
244
|
+
completions: /* @__PURE__ */ new Map(),
|
|
245
|
+
types: null
|
|
246
|
+
});
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
let cursor = map.get(key);
|
|
250
|
+
if (!cursor) {
|
|
251
|
+
cursor = {
|
|
252
|
+
consumer: target.consumer,
|
|
253
|
+
lane: target.lane,
|
|
254
|
+
kind: "lane",
|
|
255
|
+
position: clearIndexOf(sessionId),
|
|
256
|
+
head: null,
|
|
257
|
+
dispatch: freshDispatch(),
|
|
258
|
+
queue: [],
|
|
259
|
+
queued: 0,
|
|
260
|
+
completions: /* @__PURE__ */ new Map(),
|
|
261
|
+
types: null
|
|
262
|
+
};
|
|
263
|
+
map.set(key, cursor);
|
|
264
|
+
}
|
|
265
|
+
cursor.queue.push(row.index);
|
|
266
|
+
cursor.head ??= row.index;
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
const clearIndex = clearIndexOf(sessionId);
|
|
270
|
+
for (const consumer of consumers ?? []) {
|
|
271
|
+
const log = consumer.log;
|
|
272
|
+
if (!log) continue;
|
|
273
|
+
const first = rows.find((row) => takes(log.types, row.type));
|
|
274
|
+
if (!first) continue;
|
|
275
|
+
const key = cursorKey(consumer.name, log.lane, 0);
|
|
276
|
+
const cursor = map.get(key);
|
|
277
|
+
if (!cursor) {
|
|
278
|
+
const position = log.backfill ? clearIndex : base;
|
|
279
|
+
map.set(key, {
|
|
280
|
+
consumer: consumer.name,
|
|
281
|
+
lane: log.lane,
|
|
282
|
+
kind: "log",
|
|
283
|
+
position,
|
|
284
|
+
head: log.backfill ? nextLogHead(sessionId, position, log.types) : first.index,
|
|
285
|
+
dispatch: freshDispatch(),
|
|
286
|
+
queue: [],
|
|
287
|
+
queued: 0,
|
|
288
|
+
completions: /* @__PURE__ */ new Map(),
|
|
289
|
+
types: log.types
|
|
290
|
+
});
|
|
291
|
+
} else {
|
|
292
|
+
cursor.types = log.types;
|
|
293
|
+
cursor.head ??= nextLogHead(sessionId, cursor.position, log.types);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
187
296
|
};
|
|
188
|
-
const insert = (sessionId, events, now, cause) => {
|
|
297
|
+
const insert = (sessionId, events, now, consumers, cause) => {
|
|
189
298
|
const log = logOf(sessionId);
|
|
190
299
|
const base = headOf(log);
|
|
191
300
|
const inserted = events.map((candidate, offset) => ({
|
|
@@ -195,28 +304,14 @@ function memory(options = {}) {
|
|
|
195
304
|
index: base + 1 + offset,
|
|
196
305
|
sessionId,
|
|
197
306
|
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
|
|
307
|
+
cause: cause ? { ...cause } : candidate.cause ? { ...candidate.cause } : null
|
|
213
308
|
}));
|
|
214
309
|
for (const row of inserted) if (ownerOf(sessionId, row.id)) throw new A2Error("PARTIAL_DUPLICATE_BATCH", `event id '${row.id}' already exists`);
|
|
215
310
|
for (const row of inserted) {
|
|
216
311
|
log.rows.push(row);
|
|
217
|
-
byEventId.set(row.id, row);
|
|
218
|
-
enqueue(sessionId, row);
|
|
312
|
+
log.byEventId.set(row.id, row);
|
|
219
313
|
}
|
|
314
|
+
enqueue(sessionId, inserted, events, base, consumers);
|
|
220
315
|
return inserted;
|
|
221
316
|
};
|
|
222
317
|
/** Every cached fold of this session below its clear, head or historical. */
|
|
@@ -256,21 +351,35 @@ function memory(options = {}) {
|
|
|
256
351
|
...clearIndex > 0 ? { clearIndex } : {}
|
|
257
352
|
};
|
|
258
353
|
};
|
|
354
|
+
/** The dead-lettered head a redrive names, or undefined. */
|
|
355
|
+
const deadHeadOf = (sessionId, consumer, index) => {
|
|
356
|
+
if (index <= clearIndexOf(sessionId)) return void 0;
|
|
357
|
+
for (const cursor of cursors.get(sessionId)?.values() ?? []) if (cursor.consumer === consumer && cursor.head === index && cursor.dispatch.failedAt !== null) return cursor;
|
|
358
|
+
};
|
|
259
359
|
return {
|
|
260
360
|
async append(sessionId, events, appendOptions) {
|
|
261
|
-
|
|
361
|
+
const consumers = appendOptions?.consumers;
|
|
362
|
+
if (events.length === 0) return {
|
|
363
|
+
events: [],
|
|
364
|
+
hasPending: hasPending(sessionId, consumers)
|
|
365
|
+
};
|
|
262
366
|
const existing = checkBatchIds(sessionId, events);
|
|
263
367
|
if (existing.length > 0) {
|
|
264
|
-
if (existing.length === events.length) return
|
|
368
|
+
if (existing.length === events.length) return {
|
|
369
|
+
events: idempotentReplay(events, existing.map(toStored)),
|
|
370
|
+
hasPending: hasPending(sessionId, consumers)
|
|
371
|
+
};
|
|
265
372
|
throw new A2Error("PARTIAL_DUPLICATE_BATCH", `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`);
|
|
266
373
|
}
|
|
267
374
|
const originIndex = appendOptions?.originIndex;
|
|
268
375
|
if (originIndex !== void 0 && originIndex <= clearIndexOf(sessionId)) throw sessionClearedError(`scheduled work from event ${originIndex} was cleared in session '${sessionId}'`);
|
|
269
376
|
for (const event of events) if (event.cause) assertCurrentCause(sessionId, event.cause);
|
|
270
|
-
const
|
|
271
|
-
const inserted = insert(sessionId, events, now);
|
|
377
|
+
const inserted = insert(sessionId, events, clock.now(), consumers);
|
|
272
378
|
notifyStreams(sessionId, inserted);
|
|
273
|
-
return
|
|
379
|
+
return {
|
|
380
|
+
events: inserted.map(toStored),
|
|
381
|
+
hasPending: hasPending(sessionId, consumers)
|
|
382
|
+
};
|
|
274
383
|
},
|
|
275
384
|
async clear({ sessionId, ifLastIndex, cause }) {
|
|
276
385
|
if (cause) assertCurrentCause(sessionId, cause);
|
|
@@ -294,11 +403,21 @@ function memory(options = {}) {
|
|
|
294
403
|
};
|
|
295
404
|
const [marker] = insert(sessionId, [{
|
|
296
405
|
type: CLEAR_EVENT_TYPE,
|
|
297
|
-
payload: null
|
|
298
|
-
|
|
299
|
-
}], clock.now());
|
|
406
|
+
payload: null
|
|
407
|
+
}], clock.now(), void 0);
|
|
300
408
|
logOf(sessionId).clearIndex = marker.index;
|
|
301
|
-
|
|
409
|
+
const map = cursors.get(sessionId);
|
|
410
|
+
for (const [key, cursor] of map ?? []) {
|
|
411
|
+
if (cursor.head !== null) releaseSnapshotPins(sessionId, refOf(cursor));
|
|
412
|
+
if (cursor.kind !== "log") {
|
|
413
|
+
map.delete(key);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
cursor.position = marker.index;
|
|
417
|
+
cursor.head = null;
|
|
418
|
+
cursor.dispatch = freshDispatch();
|
|
419
|
+
cursor.completions.clear();
|
|
420
|
+
}
|
|
302
421
|
notifyStreams(sessionId, [marker]);
|
|
303
422
|
return {
|
|
304
423
|
cleared: true,
|
|
@@ -314,10 +433,7 @@ function memory(options = {}) {
|
|
|
314
433
|
};
|
|
315
434
|
const removed = log.rows.splice(0, Math.min(RECLAIM_BATCH, log.clearIndex - 1 - log.offset));
|
|
316
435
|
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
|
-
}
|
|
436
|
+
for (const row of removed) if (log.byEventId.get(row.id) === row) log.byEventId.delete(row.id);
|
|
321
437
|
const done = log.offset === log.clearIndex - 1;
|
|
322
438
|
if (done) dropCheckpointsBelow(sessionId, log.clearIndex);
|
|
323
439
|
return {
|
|
@@ -325,6 +441,43 @@ function memory(options = {}) {
|
|
|
325
441
|
done
|
|
326
442
|
};
|
|
327
443
|
},
|
|
444
|
+
async wakeLog({ sessionId, consumer }) {
|
|
445
|
+
const { log } = consumer;
|
|
446
|
+
const key = cursorKey(consumer.name, log.lane, 0);
|
|
447
|
+
const head = headOf(sessions.get(sessionId));
|
|
448
|
+
let cursor = cursors.get(sessionId)?.get(key);
|
|
449
|
+
if (!cursor) {
|
|
450
|
+
const marker = clearIndexOf(sessionId);
|
|
451
|
+
if (head === marker) return {
|
|
452
|
+
cursor: idleLogCursor(consumer.name, log.lane, marker),
|
|
453
|
+
head
|
|
454
|
+
};
|
|
455
|
+
const position = log.backfill ? marker : head;
|
|
456
|
+
cursor = {
|
|
457
|
+
consumer: consumer.name,
|
|
458
|
+
lane: log.lane,
|
|
459
|
+
kind: "log",
|
|
460
|
+
position,
|
|
461
|
+
head: log.backfill ? nextLogHead(sessionId, position, log.types) : null,
|
|
462
|
+
dispatch: freshDispatch(),
|
|
463
|
+
queue: [],
|
|
464
|
+
queued: 0,
|
|
465
|
+
completions: /* @__PURE__ */ new Map(),
|
|
466
|
+
types: log.types
|
|
467
|
+
};
|
|
468
|
+
cursorsOf(sessionId).set(key, cursor);
|
|
469
|
+
} else {
|
|
470
|
+
cursor.types = log.types;
|
|
471
|
+
cursor.head ??= nextLogHead(sessionId, cursor.position, log.types);
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
cursor: toCursor(cursor),
|
|
475
|
+
head
|
|
476
|
+
};
|
|
477
|
+
},
|
|
478
|
+
async cursors(sessionId) {
|
|
479
|
+
return [...cursors.get(sessionId)?.values() ?? []].filter((cursor) => cursor.kind !== "event" || cursor.head !== null).map(toCursor).toSorted(byCursor);
|
|
480
|
+
},
|
|
328
481
|
async read(sessionId, opts) {
|
|
329
482
|
const log = sessions.get(sessionId);
|
|
330
483
|
if (!log) return [];
|
|
@@ -334,41 +487,44 @@ function memory(options = {}) {
|
|
|
334
487
|
if (through <= after) return [];
|
|
335
488
|
return log.rows.slice(after - log.offset, through - log.offset).map(toStored);
|
|
336
489
|
},
|
|
337
|
-
async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs,
|
|
490
|
+
async claimAvailable({ sessionId, holder, ttlMs, expiresAtMs, exclude, consumers, maxFailures, through }) {
|
|
491
|
+
refreshLogTypes(sessionId, consumers);
|
|
338
492
|
const claimedAt = clock.now();
|
|
339
493
|
const now = claimedAt.getTime();
|
|
340
494
|
const expiresAt = new Date(expiresAtMs ?? now + ttlMs);
|
|
341
|
-
const excluded = new Set(
|
|
495
|
+
const excluded = new Set((exclude ?? []).map((ref) => cursorKey(ref.consumer, ref.lane, ref.index)));
|
|
496
|
+
const served = servedBy(consumers);
|
|
342
497
|
const eligible = [];
|
|
343
498
|
let retryAt = null;
|
|
344
|
-
for (const
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
499
|
+
for (const [key, cursor] of cursors.get(sessionId) ?? []) {
|
|
500
|
+
if (cursor.head === null || through !== void 0 && cursor.head > through || cursor.dispatch.failedAt !== null || !served(cursor) || excluded.has(key)) continue;
|
|
501
|
+
const expiry = cursor.dispatch.claimExpiresAt;
|
|
502
|
+
if (expiry && expiry.getTime() > now) {
|
|
503
|
+
if (!retryAt || expiry < retryAt) retryAt = new Date(expiry);
|
|
348
504
|
continue;
|
|
349
505
|
}
|
|
350
|
-
eligible.push(
|
|
506
|
+
eligible.push(cursor);
|
|
351
507
|
}
|
|
352
508
|
if (eligible.length === 0) return retryAt ? {
|
|
353
509
|
outcome: "busy",
|
|
354
510
|
retryAt
|
|
355
511
|
} : { outcome: "settled" };
|
|
356
|
-
eligible.sort((a, b) => a.index - b.index);
|
|
357
512
|
const clearIndex = clearIndexOf(sessionId);
|
|
358
513
|
return {
|
|
359
514
|
outcome: "claimed",
|
|
360
|
-
events: eligible.map((
|
|
361
|
-
const
|
|
515
|
+
events: eligible.map((cursor) => {
|
|
516
|
+
const head = claim({
|
|
362
517
|
sessionId,
|
|
363
|
-
|
|
518
|
+
cursor,
|
|
364
519
|
holder,
|
|
365
520
|
now: claimedAt,
|
|
366
521
|
expiresAt,
|
|
367
522
|
maxFailures
|
|
368
523
|
});
|
|
369
|
-
if (clearIndex > 0)
|
|
370
|
-
return
|
|
371
|
-
})
|
|
524
|
+
if (clearIndex > 0) head.clearIndex = clearIndex;
|
|
525
|
+
return head;
|
|
526
|
+
}).toSorted(byHead),
|
|
527
|
+
head: headOf(sessions.get(sessionId))
|
|
372
528
|
};
|
|
373
529
|
},
|
|
374
530
|
async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {
|
|
@@ -377,20 +533,23 @@ function memory(options = {}) {
|
|
|
377
533
|
const clearIndex = clearIndexOf(sessionId);
|
|
378
534
|
const renewed = [];
|
|
379
535
|
const superseded = [];
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
536
|
+
const ordered = claims.toSorted((a, b) => a.index - b.index || compareText(a.consumer, b.consumer));
|
|
537
|
+
for (const { attempt, ...ref } of ordered) {
|
|
538
|
+
if (ref.index <= clearIndex) {
|
|
539
|
+
superseded.push(ref);
|
|
383
540
|
continue;
|
|
384
541
|
}
|
|
385
|
-
const
|
|
386
|
-
if (!
|
|
387
|
-
|
|
388
|
-
|
|
542
|
+
const cursor = cursorAt(sessionId, ref);
|
|
543
|
+
if (!cursor) continue;
|
|
544
|
+
const dispatch = cursor.dispatch;
|
|
545
|
+
if (cursor.head !== ref.index && cursor.position < ref.index) continue;
|
|
546
|
+
if (cursor.head !== ref.index || dispatch.attemptCount > attempt) {
|
|
547
|
+
superseded.push(ref);
|
|
389
548
|
continue;
|
|
390
549
|
}
|
|
391
|
-
if (
|
|
392
|
-
|
|
393
|
-
renewed.push(
|
|
550
|
+
if (dispatch.failedAt === null && dispatch.claimHolder === holder && dispatch.claimExpiresAt !== null && dispatch.claimExpiresAt > now) {
|
|
551
|
+
dispatch.claimExpiresAt = new Date(expiresAt);
|
|
552
|
+
renewed.push(ref);
|
|
394
553
|
}
|
|
395
554
|
}
|
|
396
555
|
return {
|
|
@@ -398,103 +557,124 @@ function memory(options = {}) {
|
|
|
398
557
|
superseded
|
|
399
558
|
};
|
|
400
559
|
},
|
|
401
|
-
async completeAttempt({ sessionId, index, attempt, events }) {
|
|
560
|
+
async completeAttempt({ sessionId, consumer, lane, index, attempt, events, consumers }) {
|
|
402
561
|
if (index <= clearIndexOf(sessionId)) return { outcome: "superseded" };
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
};
|
|
408
|
-
if (
|
|
409
|
-
if (
|
|
562
|
+
const cursor = cursorAt(sessionId, {
|
|
563
|
+
consumer,
|
|
564
|
+
lane,
|
|
565
|
+
index
|
|
566
|
+
});
|
|
567
|
+
if (!cursor) {
|
|
568
|
+
if (!rowAt(sessionId, index)) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
|
|
569
|
+
return { outcome: "superseded" };
|
|
570
|
+
}
|
|
571
|
+
refreshLogTypes(sessionId, consumers);
|
|
572
|
+
if (cursor.head !== index) {
|
|
573
|
+
const completion = cursor.completions.get(index);
|
|
574
|
+
if (!completion || completion.attempt !== attempt) return { outcome: "superseded" };
|
|
410
575
|
const requestedIds = events.map((event) => event.id);
|
|
411
|
-
if (
|
|
576
|
+
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
577
|
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");
|
|
578
|
+
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
579
|
return {
|
|
415
580
|
outcome: "completed",
|
|
416
|
-
events: idempotentReplay(events, existing.map(toStored))
|
|
581
|
+
events: events.length === 0 ? [] : idempotentReplay(events, existing.map(toStored))
|
|
417
582
|
};
|
|
418
583
|
}
|
|
419
|
-
|
|
584
|
+
const dispatch = cursor.dispatch;
|
|
585
|
+
if (dispatch.attemptCount !== attempt || dispatch.failedAt !== null || dispatch.claimHolder === null) return { outcome: "superseded" };
|
|
420
586
|
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
|
-
|
|
587
|
+
const inserted = insert(sessionId, events, clock.now(), consumers, {
|
|
588
|
+
consumer,
|
|
589
|
+
lane,
|
|
590
|
+
index,
|
|
591
|
+
attempt
|
|
592
|
+
});
|
|
593
|
+
if (cursor.kind === "log") cursor.completions.clear();
|
|
594
|
+
cursor.completions.set(index, {
|
|
595
|
+
attempt,
|
|
596
|
+
returnedEventIds: events.map((event) => event.id)
|
|
597
|
+
});
|
|
598
|
+
advance(sessionId, cursor);
|
|
430
599
|
notifyStreams(sessionId, inserted);
|
|
431
600
|
return {
|
|
432
601
|
outcome: "completed",
|
|
433
602
|
events: inserted.map(toStored)
|
|
434
603
|
};
|
|
435
604
|
},
|
|
436
|
-
async failAttempt({ sessionId, index, attempt, error, maxFailures, retryAfterMs }) {
|
|
605
|
+
async failAttempt({ sessionId, consumer, lane, index, attempt, error, maxFailures, retryAfterMs }) {
|
|
437
606
|
if (index <= clearIndexOf(sessionId)) return {
|
|
438
607
|
outcome: "superseded",
|
|
439
608
|
failureCount: 0
|
|
440
609
|
};
|
|
441
|
-
const
|
|
442
|
-
|
|
610
|
+
const cursor = cursorAt(sessionId, {
|
|
611
|
+
consumer,
|
|
612
|
+
lane,
|
|
613
|
+
index
|
|
614
|
+
});
|
|
615
|
+
if (!cursor) {
|
|
616
|
+
if (!rowAt(sessionId, index)) throw new TypeError(`no event at index ${index} in session '${sessionId}'`);
|
|
617
|
+
return {
|
|
618
|
+
outcome: "superseded",
|
|
619
|
+
failureCount: 0
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
const dispatch = cursor.dispatch;
|
|
623
|
+
if (cursor.head !== index || dispatch.attemptCount !== attempt || dispatch.claimHolder === null && dispatch.lastFailedAttempt !== attempt) return {
|
|
443
624
|
outcome: "superseded",
|
|
444
|
-
failureCount:
|
|
625
|
+
failureCount: cursor.head === index ? dispatch.failureCount : 0
|
|
445
626
|
};
|
|
446
|
-
if (
|
|
447
|
-
outcome:
|
|
448
|
-
failureCount:
|
|
627
|
+
if (dispatch.lastFailedAttempt === attempt && dispatch.claimHolder === null) return {
|
|
628
|
+
outcome: dispatch.failedAt ? "dead_lettered" : "failed",
|
|
629
|
+
failureCount: dispatch.failureCount
|
|
449
630
|
};
|
|
450
|
-
if (
|
|
631
|
+
if (dispatch.failedAt) return {
|
|
451
632
|
outcome: "dead_lettered",
|
|
452
|
-
failureCount:
|
|
633
|
+
failureCount: dispatch.failureCount
|
|
453
634
|
};
|
|
454
|
-
|
|
455
|
-
|
|
635
|
+
dispatch.failureCount += 1;
|
|
636
|
+
dispatch.lastError = error;
|
|
456
637
|
const failedAt = clock.now();
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (
|
|
462
|
-
|
|
463
|
-
dispatchOf(sessionId).ready.delete(row.index);
|
|
464
|
-
releaseSnapshotPins(sessionId, index);
|
|
638
|
+
dispatch.lastFailedAt = new Date(failedAt);
|
|
639
|
+
dispatch.lastFailedAttempt = attempt;
|
|
640
|
+
dispatch.claimHolder = null;
|
|
641
|
+
dispatch.claimExpiresAt = retryAfterMs === void 0 || dispatch.failureCount >= maxFailures ? null : new Date(failedAt.getTime() + retryAfterMs);
|
|
642
|
+
if (dispatch.failureCount >= maxFailures) {
|
|
643
|
+
deadLetter(sessionId, cursor, failedAt);
|
|
465
644
|
return {
|
|
466
645
|
outcome: "dead_lettered",
|
|
467
|
-
failureCount:
|
|
646
|
+
failureCount: dispatch.failureCount
|
|
468
647
|
};
|
|
469
648
|
}
|
|
470
649
|
return {
|
|
471
650
|
outcome: "failed",
|
|
472
|
-
failureCount:
|
|
651
|
+
failureCount: dispatch.failureCount
|
|
473
652
|
};
|
|
474
653
|
},
|
|
475
654
|
async deadLetters(sessionId) {
|
|
476
|
-
const
|
|
477
|
-
|
|
655
|
+
const clearIndex = clearIndexOf(sessionId);
|
|
656
|
+
const dead = [];
|
|
657
|
+
for (const cursor of cursors.get(sessionId)?.values() ?? []) {
|
|
658
|
+
if (cursor.head === null || cursor.head <= clearIndex || cursor.dispatch.failedAt === null) continue;
|
|
659
|
+
dead.push(toHead(cursor, rowAt(sessionId, cursor.head)));
|
|
660
|
+
}
|
|
661
|
+
return dead.toSorted(byHead);
|
|
478
662
|
},
|
|
479
|
-
async redrive({ sessionId, index, action }) {
|
|
480
|
-
const
|
|
481
|
-
if (!
|
|
663
|
+
async redrive({ sessionId, consumer, index, action, consumers }) {
|
|
664
|
+
const cursor = deadHeadOf(sessionId, consumer, index);
|
|
665
|
+
if (!cursor) return {
|
|
482
666
|
redriven: false,
|
|
483
|
-
hasPending: hasPending(sessionId)
|
|
667
|
+
hasPending: hasPending(sessionId, consumers)
|
|
484
668
|
};
|
|
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);
|
|
669
|
+
refreshLogTypes(sessionId, consumers);
|
|
670
|
+
if (action === "skip") advance(sessionId, cursor);
|
|
671
|
+
else {
|
|
672
|
+
cursor.dispatch.failedAt = null;
|
|
673
|
+
cursor.dispatch.failureCount = 0;
|
|
494
674
|
}
|
|
495
675
|
return {
|
|
496
676
|
redriven: true,
|
|
497
|
-
hasPending: hasPending(sessionId)
|
|
677
|
+
hasPending: hasPending(sessionId, consumers)
|
|
498
678
|
};
|
|
499
679
|
},
|
|
500
680
|
async readState(sessionId, reducerName, stateOptions) {
|
|
@@ -509,10 +689,10 @@ function memory(options = {}) {
|
|
|
509
689
|
for (const write of writes.toSorted((a, b) => a.index - b.index)) {
|
|
510
690
|
if (write.index < clearIndex) continue;
|
|
511
691
|
const checkpoint = checkpointKey(sessionId, reducerName, write.index);
|
|
512
|
-
for (const
|
|
513
|
-
const
|
|
514
|
-
if (!
|
|
515
|
-
const pinKey =
|
|
692
|
+
for (const pin of write.pins ?? []) {
|
|
693
|
+
const cursor = cursorAt(sessionId, pin);
|
|
694
|
+
if (!cursor || pin.index <= clearIndex || cursor.head !== pin.index || cursor.dispatch.failedAt !== null) continue;
|
|
695
|
+
const pinKey = headPinKey(sessionId, pin);
|
|
516
696
|
let pinned = snapshotPins.get(pinKey);
|
|
517
697
|
if (!pinned) {
|
|
518
698
|
pinned = /* @__PURE__ */ new Set();
|
|
@@ -558,14 +738,14 @@ function memory(options = {}) {
|
|
|
558
738
|
fields = /* @__PURE__ */ new Map();
|
|
559
739
|
participants.set(participant, fields);
|
|
560
740
|
}
|
|
561
|
-
const
|
|
741
|
+
const now = clock.now();
|
|
742
|
+
const expiresAtMs = now.getTime() + meta.ttlMs;
|
|
562
743
|
const applied = nullProtoRecord();
|
|
563
744
|
let appliedCount = 0;
|
|
564
745
|
for (const [field, value] of Object.entries(values)) {
|
|
565
746
|
const existing = fields.get(field);
|
|
566
|
-
if (existing && existing.at > meta.at) continue;
|
|
567
|
-
|
|
568
|
-
else fields.set(field, {
|
|
747
|
+
if (existing && existing.expiresAt > now && existing.at > meta.at) continue;
|
|
748
|
+
fields.set(field, {
|
|
569
749
|
value: structuredClone(value),
|
|
570
750
|
seen: meta.seen,
|
|
571
751
|
at: new Date(meta.at),
|
|
@@ -598,6 +778,7 @@ function memory(options = {}) {
|
|
|
598
778
|
fields.delete(field);
|
|
599
779
|
continue;
|
|
600
780
|
}
|
|
781
|
+
if (entry.value === null) continue;
|
|
601
782
|
rows.push({
|
|
602
783
|
participant,
|
|
603
784
|
field,
|