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/src/store-memory.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { A2Error, supersededAttemptError } from './errors.ts'
|
|
|
11
11
|
import { idempotentReplay } from './idempotent-replay.ts'
|
|
12
12
|
import { nullProtoRecord } from './internal.ts'
|
|
13
13
|
import { CLEAR_EVENT_TYPE, sessionClearedError } from './session-clear.ts'
|
|
14
|
+
import { idleLogCursor } from './store-codec.ts'
|
|
14
15
|
import {
|
|
15
16
|
RANDOM_IDS,
|
|
16
17
|
SYSTEM_CLOCK,
|
|
@@ -19,8 +20,13 @@ import {
|
|
|
19
20
|
type Clock,
|
|
20
21
|
type Event,
|
|
21
22
|
type EventCause,
|
|
23
|
+
type HeadDispatch,
|
|
24
|
+
type HeadEvent,
|
|
25
|
+
type HeadRef,
|
|
22
26
|
type IdSource,
|
|
23
27
|
type PresenceRow,
|
|
28
|
+
type StoreConsumer,
|
|
29
|
+
type StoredCursor,
|
|
24
30
|
type StoredEvent,
|
|
25
31
|
type StoreStateRead,
|
|
26
32
|
} from './store.ts'
|
|
@@ -40,20 +46,32 @@ type Row = {
|
|
|
40
46
|
sessionId: string
|
|
41
47
|
createdAt: Date
|
|
42
48
|
cause: EventCause | null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type Dispatch = Omit<HeadDispatch, 'consumer' | 'lane'>
|
|
52
|
+
|
|
53
|
+
type Completion = { attempt: number; returnedEventIds: string[] }
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* One (consumer, lane) record. A log cursor reads the whole log by type, a
|
|
57
|
+
* lane cursor queues its lane's events, and a concurrent cursor (lane null)
|
|
58
|
+
* covers one event.
|
|
59
|
+
*/
|
|
60
|
+
type Cursor = {
|
|
61
|
+
consumer: string
|
|
43
62
|
lane: string | null
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
failedAt: Date | null
|
|
63
|
+
kind: 'log' | 'lane' | 'event'
|
|
64
|
+
position: number
|
|
65
|
+
head: number | null
|
|
66
|
+
dispatch: Dispatch
|
|
67
|
+
/** Lane cursors: the lane's event indexes in log order. */
|
|
68
|
+
queue: number[]
|
|
69
|
+
/** Lane cursors: where the head sits in `queue`. */
|
|
70
|
+
queued: number
|
|
71
|
+
/** Completions by head index; a log cursor keeps only its latest. */
|
|
72
|
+
completions: Map<number, Completion>
|
|
73
|
+
/** Log cursors: the types it takes, null for every type. */
|
|
74
|
+
types: readonly string[] | null
|
|
57
75
|
}
|
|
58
76
|
|
|
59
77
|
type PresenceEntry = {
|
|
@@ -63,12 +81,6 @@ type PresenceEntry = {
|
|
|
63
81
|
expiresAt: Date
|
|
64
82
|
}
|
|
65
83
|
|
|
66
|
-
type LaneQueue = { rows: Row[]; head: number }
|
|
67
|
-
type SessionDispatch = {
|
|
68
|
-
ready: Map<number, Row>
|
|
69
|
-
lanes: Map<string, LaneQueue>
|
|
70
|
-
}
|
|
71
|
-
|
|
72
84
|
/** Contiguous rows; `reclaim` removes from the front below `clearIndex`. */
|
|
73
85
|
type SessionLog = {
|
|
74
86
|
rows: Row[]
|
|
@@ -83,6 +95,22 @@ const RECLAIM_BATCH = 256
|
|
|
83
95
|
const headOf = (log: SessionLog | undefined): number =>
|
|
84
96
|
log ? log.offset + log.rows.length : 0
|
|
85
97
|
|
|
98
|
+
const freshDispatch = (): Dispatch => ({
|
|
99
|
+
firstClaimedAt: null,
|
|
100
|
+
lastClaimedAt: null,
|
|
101
|
+
attemptCount: 0,
|
|
102
|
+
claimHolder: null,
|
|
103
|
+
claimExpiresAt: null,
|
|
104
|
+
failureCount: 0,
|
|
105
|
+
lastFailedAt: null,
|
|
106
|
+
lastFailedAttempt: null,
|
|
107
|
+
lastError: null,
|
|
108
|
+
failedAt: null,
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const date = (value: Date | null): Date | null =>
|
|
112
|
+
value ? new Date(value) : null
|
|
113
|
+
|
|
86
114
|
const toStored = (row: Row): StoredEvent => ({
|
|
87
115
|
id: row.id,
|
|
88
116
|
type: row.type,
|
|
@@ -91,20 +119,6 @@ const toStored = (row: Row): StoredEvent => ({
|
|
|
91
119
|
sessionId: row.sessionId,
|
|
92
120
|
createdAt: new Date(row.createdAt),
|
|
93
121
|
cause: row.cause ? { ...row.cause } : null,
|
|
94
|
-
lane: row.lane,
|
|
95
|
-
processedAt: row.processedAt ? new Date(row.processedAt) : null,
|
|
96
|
-
processedByAttempt: row.processedByAttempt,
|
|
97
|
-
returnedEventIds: row.returnedEventIds ? [...row.returnedEventIds] : null,
|
|
98
|
-
firstClaimedAt: row.firstClaimedAt ? new Date(row.firstClaimedAt) : null,
|
|
99
|
-
lastClaimedAt: row.lastClaimedAt ? new Date(row.lastClaimedAt) : null,
|
|
100
|
-
attemptCount: row.attemptCount,
|
|
101
|
-
claimHolder: row.claimHolder,
|
|
102
|
-
claimExpiresAt: row.claimExpiresAt ? new Date(row.claimExpiresAt) : null,
|
|
103
|
-
failureCount: row.failureCount,
|
|
104
|
-
lastFailedAt: row.lastFailedAt ? new Date(row.lastFailedAt) : null,
|
|
105
|
-
lastFailedAttempt: row.lastFailedAttempt,
|
|
106
|
-
lastError: row.lastError,
|
|
107
|
-
failedAt: row.failedAt ? new Date(row.failedAt) : null,
|
|
108
122
|
})
|
|
109
123
|
|
|
110
124
|
const toEvent = (row: Row): Event => ({
|
|
@@ -116,18 +130,95 @@ const toEvent = (row: Row): Event => ({
|
|
|
116
130
|
createdAt: new Date(row.createdAt),
|
|
117
131
|
})
|
|
118
132
|
|
|
133
|
+
const toHead = (cursor: Cursor, row: Row): HeadEvent => ({
|
|
134
|
+
...toStored(row),
|
|
135
|
+
consumer: cursor.consumer,
|
|
136
|
+
lane: cursor.lane,
|
|
137
|
+
firstClaimedAt: date(cursor.dispatch.firstClaimedAt),
|
|
138
|
+
lastClaimedAt: date(cursor.dispatch.lastClaimedAt),
|
|
139
|
+
attemptCount: cursor.dispatch.attemptCount,
|
|
140
|
+
claimHolder: cursor.dispatch.claimHolder,
|
|
141
|
+
claimExpiresAt: date(cursor.dispatch.claimExpiresAt),
|
|
142
|
+
failureCount: cursor.dispatch.failureCount,
|
|
143
|
+
lastFailedAt: date(cursor.dispatch.lastFailedAt),
|
|
144
|
+
lastFailedAttempt: cursor.dispatch.lastFailedAttempt,
|
|
145
|
+
lastError: cursor.dispatch.lastError,
|
|
146
|
+
failedAt: date(cursor.dispatch.failedAt),
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const toCursor = (cursor: Cursor): StoredCursor => ({
|
|
150
|
+
consumer: cursor.consumer,
|
|
151
|
+
lane: cursor.lane,
|
|
152
|
+
position: cursor.position,
|
|
153
|
+
head: cursor.head,
|
|
154
|
+
firstClaimedAt: date(cursor.dispatch.firstClaimedAt),
|
|
155
|
+
lastClaimedAt: date(cursor.dispatch.lastClaimedAt),
|
|
156
|
+
attemptCount: cursor.dispatch.attemptCount,
|
|
157
|
+
claimHolder: cursor.dispatch.claimHolder,
|
|
158
|
+
claimExpiresAt: date(cursor.dispatch.claimExpiresAt),
|
|
159
|
+
failureCount: cursor.dispatch.failureCount,
|
|
160
|
+
lastFailedAt: date(cursor.dispatch.lastFailedAt),
|
|
161
|
+
lastFailedAttempt: cursor.dispatch.lastFailedAttempt,
|
|
162
|
+
lastError: cursor.dispatch.lastError,
|
|
163
|
+
failedAt: date(cursor.dispatch.failedAt),
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const compareText = (a: string, b: string): number =>
|
|
167
|
+
a < b ? -1 : a > b ? 1 : 0
|
|
168
|
+
|
|
169
|
+
/** Consumer, then lane, with concurrent cursors last by head. */
|
|
170
|
+
const byCursor = (a: StoredCursor, b: StoredCursor): number =>
|
|
171
|
+
compareText(a.consumer, b.consumer) ||
|
|
172
|
+
(a.lane === null
|
|
173
|
+
? b.lane === null
|
|
174
|
+
? (a.head ?? 0) - (b.head ?? 0)
|
|
175
|
+
: 1
|
|
176
|
+
: b.lane === null
|
|
177
|
+
? -1
|
|
178
|
+
: compareText(a.lane, b.lane))
|
|
179
|
+
|
|
180
|
+
const cursorKey = (
|
|
181
|
+
consumer: string,
|
|
182
|
+
lane: string | null,
|
|
183
|
+
index: number,
|
|
184
|
+
): string =>
|
|
185
|
+
lane === null ? `${consumer}\u0000#${index}` : `${consumer}\u0000L${lane}`
|
|
186
|
+
|
|
187
|
+
const takes = (types: readonly string[] | null, type: string): boolean =>
|
|
188
|
+
type !== CLEAR_EVENT_TYPE && (types === null || types.includes(type))
|
|
189
|
+
|
|
190
|
+
const byHead = (a: HeadEvent, b: HeadEvent): number =>
|
|
191
|
+
a.index - b.index || compareText(a.consumer, b.consumer)
|
|
192
|
+
|
|
119
193
|
const snapshotKey = (sessionId: string, reducerName: string): string =>
|
|
120
194
|
`${sessionId}\u0000${reducerName}`
|
|
121
195
|
|
|
122
|
-
const
|
|
123
|
-
`${sessionId}\u0000${index}`
|
|
196
|
+
const headPinKey = (sessionId: string, ref: HeadRef): string =>
|
|
197
|
+
`${sessionId}\u0000${cursorKey(ref.consumer, ref.lane, ref.index)}\u0000${ref.index}`
|
|
198
|
+
|
|
199
|
+
/** Whether a cursor belongs to a consumer the caller serves; omitted, every one. */
|
|
200
|
+
const servedBy = (
|
|
201
|
+
consumers: readonly StoreConsumer[] | undefined,
|
|
202
|
+
): ((cursor: Cursor) => boolean) => {
|
|
203
|
+
if (!consumers) return () => true
|
|
204
|
+
const names = new Set(consumers.map((consumer) => consumer.name))
|
|
205
|
+
return (cursor) => names.has(cursor.consumer)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** A cursor's current head. */
|
|
209
|
+
const refOf = (cursor: Cursor): HeadRef => ({
|
|
210
|
+
consumer: cursor.consumer,
|
|
211
|
+
lane: cursor.lane,
|
|
212
|
+
index: cursor.head!,
|
|
213
|
+
})
|
|
124
214
|
|
|
125
215
|
export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
126
216
|
const clock = options.clock ?? SYSTEM_CLOCK
|
|
127
217
|
const generateId = options.ids ?? RANDOM_IDS
|
|
128
218
|
|
|
129
219
|
const sessions = new Map<string, SessionLog>()
|
|
130
|
-
|
|
220
|
+
/** Session id → cursor key → cursor. */
|
|
221
|
+
const cursors = new Map<string, Map<string, Cursor>>()
|
|
131
222
|
/** Global unique index on event ids, like the SQL schema's. */
|
|
132
223
|
const byEventId = new Map<string, Row>()
|
|
133
224
|
/** Keyed by `${sessionId}\u0000${reducerName}` — a pure cache. */
|
|
@@ -187,14 +278,26 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
187
278
|
: row
|
|
188
279
|
}
|
|
189
280
|
|
|
281
|
+
const cursorsOf = (sessionId: string): Map<string, Cursor> => {
|
|
282
|
+
let map = cursors.get(sessionId)
|
|
283
|
+
if (!map) {
|
|
284
|
+
map = new Map()
|
|
285
|
+
cursors.set(sessionId, map)
|
|
286
|
+
}
|
|
287
|
+
return map
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const cursorAt = (sessionId: string, ref: HeadRef): Cursor | undefined =>
|
|
291
|
+
cursors.get(sessionId)?.get(cursorKey(ref.consumer, ref.lane, ref.index))
|
|
292
|
+
|
|
190
293
|
const checkpointKey = (
|
|
191
294
|
sessionId: string,
|
|
192
295
|
reducerName: string,
|
|
193
296
|
index: number,
|
|
194
297
|
): string => `${snapshotKey(sessionId, reducerName)}\u0000${index}`
|
|
195
298
|
|
|
196
|
-
const releaseSnapshotPins = (sessionId: string,
|
|
197
|
-
const pinKey =
|
|
299
|
+
const releaseSnapshotPins = (sessionId: string, ref: HeadRef): void => {
|
|
300
|
+
const pinKey = headPinKey(sessionId, ref)
|
|
198
301
|
const checkpoints = snapshotPins.get(pinKey)
|
|
199
302
|
if (!checkpoints) return
|
|
200
303
|
for (const key of checkpoints) {
|
|
@@ -209,114 +312,114 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
209
312
|
snapshotPins.delete(pinKey)
|
|
210
313
|
}
|
|
211
314
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return
|
|
227
|
-
}
|
|
228
|
-
let queue = state.lanes.get(row.lane)
|
|
229
|
-
if (!queue) {
|
|
230
|
-
queue = { rows: [], head: 0 }
|
|
231
|
-
state.lanes.set(row.lane, queue)
|
|
315
|
+
/** The next event after `after` that a log cursor takes, or null. */
|
|
316
|
+
const nextLogHead = (
|
|
317
|
+
sessionId: string,
|
|
318
|
+
after: number,
|
|
319
|
+
types: readonly string[] | null,
|
|
320
|
+
): number | null => {
|
|
321
|
+
const log = sessions.get(sessionId)
|
|
322
|
+
if (!log) return null
|
|
323
|
+
for (
|
|
324
|
+
let offset = Math.max(0, after - log.offset);
|
|
325
|
+
offset < log.rows.length;
|
|
326
|
+
offset += 1
|
|
327
|
+
) {
|
|
328
|
+
const row = log.rows[offset]!
|
|
329
|
+
if (takes(types, row.type)) return row.index
|
|
232
330
|
}
|
|
233
|
-
|
|
234
|
-
if (queue.rows.length === 1) state.ready.set(row.index, row)
|
|
331
|
+
return null
|
|
235
332
|
}
|
|
236
333
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
334
|
+
/** Moves a cursor past its head: the next head starts with fresh bookkeeping. */
|
|
335
|
+
const advance = (sessionId: string, cursor: Cursor): void => {
|
|
336
|
+
const done = cursor.head!
|
|
337
|
+
releaseSnapshotPins(sessionId, refOf(cursor))
|
|
338
|
+
cursor.position = done
|
|
339
|
+
cursor.dispatch = freshDispatch()
|
|
340
|
+
if (cursor.kind === 'event') {
|
|
341
|
+
cursor.head = null
|
|
342
|
+
} else if (cursor.kind === 'lane') {
|
|
343
|
+
cursor.queued += 1
|
|
344
|
+
cursor.head = cursor.queue[cursor.queued] ?? null
|
|
345
|
+
} else {
|
|
346
|
+
cursor.head = nextLogHead(sessionId, done, cursor.types)
|
|
247
347
|
}
|
|
248
|
-
const next = queue.rows[queue.head]!
|
|
249
|
-
if (next.failedAt === null) state.ready.set(next.index, next)
|
|
250
348
|
}
|
|
251
349
|
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
350
|
+
const hasPending = (
|
|
351
|
+
sessionId: string,
|
|
352
|
+
consumers: readonly StoreConsumer[] | undefined,
|
|
353
|
+
): boolean => {
|
|
354
|
+
const served = servedBy(consumers)
|
|
355
|
+
for (const cursor of cursors.get(sessionId)?.values() ?? []) {
|
|
356
|
+
if (
|
|
357
|
+
cursor.head !== null &&
|
|
358
|
+
cursor.dispatch.failedAt === null &&
|
|
359
|
+
served(cursor)
|
|
360
|
+
) {
|
|
361
|
+
return true
|
|
362
|
+
}
|
|
258
363
|
}
|
|
259
|
-
return
|
|
364
|
+
return false
|
|
260
365
|
}
|
|
261
366
|
|
|
262
|
-
|
|
263
|
-
const log = sessions.get(sessionId)
|
|
264
|
-
return (log?.rows ?? []).some(
|
|
265
|
-
(row) => row.index > log!.clearIndex && row.processedAt === null,
|
|
266
|
-
)
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const appendResult = (sessionId: string, events: StoredEvent[]) => ({
|
|
270
|
-
events,
|
|
271
|
-
hasPending: hasPending(sessionId),
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
/** The attempt-currency fence shared by handler appends and clears. */
|
|
367
|
+
/** The attempt-currency fence shared by consumer appends and clears. */
|
|
275
368
|
const assertCurrentCause = (sessionId: string, cause: EventCause): void => {
|
|
276
369
|
if (cause.index <= clearIndexOf(sessionId)) {
|
|
277
370
|
throw supersededAttemptError(sessionId, cause)
|
|
278
371
|
}
|
|
279
|
-
const
|
|
280
|
-
if (
|
|
372
|
+
const cursor = cursorAt(sessionId, cause)
|
|
373
|
+
if (
|
|
374
|
+
!cursor ||
|
|
375
|
+
cursor.head !== cause.index ||
|
|
376
|
+
cursor.dispatch.attemptCount !== cause.attempt ||
|
|
377
|
+
cursor.dispatch.failedAt !== null
|
|
378
|
+
) {
|
|
281
379
|
throw supersededAttemptError(sessionId, cause)
|
|
282
380
|
}
|
|
283
381
|
}
|
|
284
382
|
|
|
383
|
+
const deadLetter = (sessionId: string, cursor: Cursor, now: Date): void => {
|
|
384
|
+
cursor.dispatch.failedAt = new Date(now)
|
|
385
|
+
releaseSnapshotPins(sessionId, refOf(cursor))
|
|
386
|
+
}
|
|
387
|
+
|
|
285
388
|
const claim = ({
|
|
286
389
|
sessionId,
|
|
287
|
-
|
|
390
|
+
cursor,
|
|
288
391
|
holder,
|
|
289
392
|
now,
|
|
290
393
|
expiresAt,
|
|
291
394
|
maxFailures,
|
|
292
395
|
}: {
|
|
293
396
|
sessionId: string
|
|
294
|
-
|
|
397
|
+
cursor: Cursor
|
|
295
398
|
holder: string
|
|
296
399
|
now: Date
|
|
297
400
|
expiresAt: Date
|
|
298
401
|
maxFailures: number | undefined
|
|
299
|
-
}):
|
|
402
|
+
}): HeadEvent => {
|
|
403
|
+
const dispatch = cursor.dispatch
|
|
404
|
+
const row = rowAt(sessionId, cursor.head!)!
|
|
300
405
|
// A claim still held here lapsed: its attempt never finished.
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
if (maxFailures !== undefined &&
|
|
406
|
+
if (dispatch.claimHolder !== null) {
|
|
407
|
+
dispatch.failureCount += 1
|
|
408
|
+
if (maxFailures !== undefined && dispatch.failureCount >= maxFailures) {
|
|
304
409
|
// Advancing the attempt fences the lapsed one; nothing runs.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
releaseSnapshotPins(sessionId, row.index)
|
|
311
|
-
return toStored(row)
|
|
410
|
+
dispatch.attemptCount += 1
|
|
411
|
+
dispatch.claimHolder = null
|
|
412
|
+
dispatch.claimExpiresAt = null
|
|
413
|
+
deadLetter(sessionId, cursor, now)
|
|
414
|
+
return toHead(cursor, row)
|
|
312
415
|
}
|
|
313
416
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
return
|
|
417
|
+
dispatch.attemptCount += 1
|
|
418
|
+
dispatch.firstClaimedAt ??= new Date(now)
|
|
419
|
+
dispatch.lastClaimedAt = new Date(now)
|
|
420
|
+
dispatch.claimHolder = holder
|
|
421
|
+
dispatch.claimExpiresAt = new Date(expiresAt)
|
|
422
|
+
return toHead(cursor, row)
|
|
320
423
|
}
|
|
321
424
|
|
|
322
425
|
const checkBatchIds = (
|
|
@@ -347,10 +450,105 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
347
450
|
return existing
|
|
348
451
|
}
|
|
349
452
|
|
|
453
|
+
/** Records the types a declared log consumer takes on its cursor. */
|
|
454
|
+
const refreshLogTypes = (
|
|
455
|
+
sessionId: string,
|
|
456
|
+
consumers: readonly StoreConsumer[] | undefined,
|
|
457
|
+
): void => {
|
|
458
|
+
const map = cursors.get(sessionId)
|
|
459
|
+
if (!map) return
|
|
460
|
+
for (const consumer of consumers ?? []) {
|
|
461
|
+
if (!consumer.log) continue
|
|
462
|
+
const cursor = map.get(cursorKey(consumer.name, consumer.log.lane, 0))
|
|
463
|
+
if (cursor?.kind === 'log') cursor.types = consumer.log.types
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** Starts the cursor work an inserted batch carries. */
|
|
468
|
+
const enqueue = (
|
|
469
|
+
sessionId: string,
|
|
470
|
+
rows: readonly Row[],
|
|
471
|
+
events: readonly AppendEvent[],
|
|
472
|
+
base: number,
|
|
473
|
+
consumers: readonly StoreConsumer[] | undefined,
|
|
474
|
+
): void => {
|
|
475
|
+
const map = cursorsOf(sessionId)
|
|
476
|
+
rows.forEach((row, offset) => {
|
|
477
|
+
for (const target of events[offset]!.targets ?? []) {
|
|
478
|
+
const key = cursorKey(target.consumer, target.lane, row.index)
|
|
479
|
+
if (target.lane === null) {
|
|
480
|
+
map.set(key, {
|
|
481
|
+
consumer: target.consumer,
|
|
482
|
+
lane: null,
|
|
483
|
+
kind: 'event',
|
|
484
|
+
position: row.index - 1,
|
|
485
|
+
head: row.index,
|
|
486
|
+
dispatch: freshDispatch(),
|
|
487
|
+
queue: [],
|
|
488
|
+
queued: 0,
|
|
489
|
+
completions: new Map(),
|
|
490
|
+
types: null,
|
|
491
|
+
})
|
|
492
|
+
continue
|
|
493
|
+
}
|
|
494
|
+
let cursor = map.get(key)
|
|
495
|
+
if (!cursor) {
|
|
496
|
+
cursor = {
|
|
497
|
+
consumer: target.consumer,
|
|
498
|
+
lane: target.lane,
|
|
499
|
+
kind: 'lane',
|
|
500
|
+
position: clearIndexOf(sessionId),
|
|
501
|
+
head: null,
|
|
502
|
+
dispatch: freshDispatch(),
|
|
503
|
+
queue: [],
|
|
504
|
+
queued: 0,
|
|
505
|
+
completions: new Map(),
|
|
506
|
+
types: null,
|
|
507
|
+
}
|
|
508
|
+
map.set(key, cursor)
|
|
509
|
+
}
|
|
510
|
+
cursor.queue.push(row.index)
|
|
511
|
+
cursor.head ??= row.index
|
|
512
|
+
}
|
|
513
|
+
})
|
|
514
|
+
const clearIndex = clearIndexOf(sessionId)
|
|
515
|
+
for (const consumer of consumers ?? []) {
|
|
516
|
+
const log = consumer.log
|
|
517
|
+
if (!log) continue
|
|
518
|
+
const first = rows.find((row) => takes(log.types, row.type))
|
|
519
|
+
if (!first) continue
|
|
520
|
+
const key = cursorKey(consumer.name, log.lane, 0)
|
|
521
|
+
const cursor = map.get(key)
|
|
522
|
+
if (!cursor) {
|
|
523
|
+
const position = log.backfill ? clearIndex : base
|
|
524
|
+
map.set(key, {
|
|
525
|
+
consumer: consumer.name,
|
|
526
|
+
lane: log.lane,
|
|
527
|
+
kind: 'log',
|
|
528
|
+
position,
|
|
529
|
+
head: log.backfill
|
|
530
|
+
? nextLogHead(sessionId, position, log.types)
|
|
531
|
+
: first.index,
|
|
532
|
+
dispatch: freshDispatch(),
|
|
533
|
+
queue: [],
|
|
534
|
+
queued: 0,
|
|
535
|
+
completions: new Map(),
|
|
536
|
+
types: log.types,
|
|
537
|
+
})
|
|
538
|
+
} else {
|
|
539
|
+
cursor.types = log.types
|
|
540
|
+
// Not `first`: a deployment that doesn't serve this consumer may
|
|
541
|
+
// have appended events it takes since it went idle.
|
|
542
|
+
cursor.head ??= nextLogHead(sessionId, cursor.position, log.types)
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
350
547
|
const insert = (
|
|
351
548
|
sessionId: string,
|
|
352
549
|
events: readonly AppendEvent[],
|
|
353
550
|
now: Date,
|
|
551
|
+
consumers: readonly StoreConsumer[] | undefined,
|
|
354
552
|
cause?: EventCause,
|
|
355
553
|
): Row[] => {
|
|
356
554
|
const log = logOf(sessionId)
|
|
@@ -367,20 +565,6 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
367
565
|
: candidate.cause
|
|
368
566
|
? { ...candidate.cause }
|
|
369
567
|
: null,
|
|
370
|
-
lane: candidate.lane ?? null,
|
|
371
|
-
processedAt: candidate.settled ? new Date(now) : null,
|
|
372
|
-
processedByAttempt: null,
|
|
373
|
-
returnedEventIds: null,
|
|
374
|
-
firstClaimedAt: null,
|
|
375
|
-
lastClaimedAt: null,
|
|
376
|
-
attemptCount: 0,
|
|
377
|
-
claimHolder: null,
|
|
378
|
-
claimExpiresAt: null,
|
|
379
|
-
failureCount: 0,
|
|
380
|
-
lastFailedAt: null,
|
|
381
|
-
lastFailedAttempt: null,
|
|
382
|
-
lastError: null,
|
|
383
|
-
failedAt: null,
|
|
384
568
|
}))
|
|
385
569
|
for (const row of inserted) {
|
|
386
570
|
if (ownerOf(sessionId, row.id)) {
|
|
@@ -393,8 +577,8 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
393
577
|
for (const row of inserted) {
|
|
394
578
|
log.rows.push(row)
|
|
395
579
|
byEventId.set(row.id, row)
|
|
396
|
-
enqueue(sessionId, row)
|
|
397
580
|
}
|
|
581
|
+
enqueue(sessionId, inserted, events, base, consumers)
|
|
398
582
|
return inserted
|
|
399
583
|
}
|
|
400
584
|
|
|
@@ -462,9 +646,31 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
462
646
|
}
|
|
463
647
|
}
|
|
464
648
|
|
|
649
|
+
/** The dead-lettered head a redrive names, or undefined. */
|
|
650
|
+
const deadHeadOf = (
|
|
651
|
+
sessionId: string,
|
|
652
|
+
consumer: string,
|
|
653
|
+
index: number,
|
|
654
|
+
): Cursor | undefined => {
|
|
655
|
+
if (index <= clearIndexOf(sessionId)) return undefined
|
|
656
|
+
for (const cursor of cursors.get(sessionId)?.values() ?? []) {
|
|
657
|
+
if (
|
|
658
|
+
cursor.consumer === consumer &&
|
|
659
|
+
cursor.head === index &&
|
|
660
|
+
cursor.dispatch.failedAt !== null
|
|
661
|
+
) {
|
|
662
|
+
return cursor
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return undefined
|
|
666
|
+
}
|
|
667
|
+
|
|
465
668
|
return {
|
|
466
669
|
async append(sessionId, events, appendOptions) {
|
|
467
|
-
|
|
670
|
+
const consumers = appendOptions?.consumers
|
|
671
|
+
if (events.length === 0) {
|
|
672
|
+
return { events: [], hasPending: hasPending(sessionId, consumers) }
|
|
673
|
+
}
|
|
468
674
|
|
|
469
675
|
// Batch-scoped idempotency (a2-implementation.md §3): all ids
|
|
470
676
|
// already present → lost-ack retry, return the original rows;
|
|
@@ -472,10 +678,10 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
472
678
|
const existing = checkBatchIds(sessionId, events)
|
|
473
679
|
if (existing.length > 0) {
|
|
474
680
|
if (existing.length === events.length) {
|
|
475
|
-
return
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
681
|
+
return {
|
|
682
|
+
events: idempotentReplay(events, existing.map(toStored)),
|
|
683
|
+
hasPending: hasPending(sessionId, consumers),
|
|
684
|
+
}
|
|
479
685
|
}
|
|
480
686
|
throw new A2Error(
|
|
481
687
|
'PARTIAL_DUPLICATE_BATCH',
|
|
@@ -489,16 +695,18 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
489
695
|
`scheduled work from event ${originIndex} was cleared in session '${sessionId}'`,
|
|
490
696
|
)
|
|
491
697
|
}
|
|
492
|
-
// Attempt-currency fence: a fresh
|
|
493
|
-
// its causal attempt
|
|
698
|
+
// Attempt-currency fence: a fresh consumer append commits only while
|
|
699
|
+
// its causal attempt still owns the cursor's head (store.ts `append`).
|
|
494
700
|
for (const event of events) {
|
|
495
701
|
if (event.cause) assertCurrentCause(sessionId, event.cause)
|
|
496
702
|
}
|
|
497
703
|
|
|
498
|
-
const
|
|
499
|
-
const inserted = insert(sessionId, events, now)
|
|
704
|
+
const inserted = insert(sessionId, events, clock.now(), consumers)
|
|
500
705
|
notifyStreams(sessionId, inserted)
|
|
501
|
-
return
|
|
706
|
+
return {
|
|
707
|
+
events: inserted.map(toStored),
|
|
708
|
+
hasPending: hasPending(sessionId, consumers),
|
|
709
|
+
}
|
|
502
710
|
},
|
|
503
711
|
|
|
504
712
|
async clear({ sessionId, ifLastIndex, cause }) {
|
|
@@ -517,12 +725,25 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
517
725
|
}
|
|
518
726
|
const [marker] = insert(
|
|
519
727
|
sessionId,
|
|
520
|
-
[{ type: CLEAR_EVENT_TYPE, payload: null
|
|
728
|
+
[{ type: CLEAR_EVENT_TYPE, payload: null }],
|
|
521
729
|
clock.now(),
|
|
730
|
+
undefined,
|
|
522
731
|
)
|
|
523
732
|
logOf(sessionId).clearIndex = marker!.index
|
|
524
|
-
// Every
|
|
525
|
-
|
|
733
|
+
// Every event is at or below the marker: no head survives, and log
|
|
734
|
+
// cursors count from the marker.
|
|
735
|
+
const map = cursors.get(sessionId)
|
|
736
|
+
for (const [key, cursor] of map ?? []) {
|
|
737
|
+
if (cursor.head !== null) releaseSnapshotPins(sessionId, refOf(cursor))
|
|
738
|
+
if (cursor.kind !== 'log') {
|
|
739
|
+
map!.delete(key)
|
|
740
|
+
continue
|
|
741
|
+
}
|
|
742
|
+
cursor.position = marker!.index
|
|
743
|
+
cursor.head = null
|
|
744
|
+
cursor.dispatch = freshDispatch()
|
|
745
|
+
cursor.completions.clear()
|
|
746
|
+
}
|
|
526
747
|
notifyStreams(sessionId, [marker!])
|
|
527
748
|
return { cleared: true, index: marker!.index, clearIndex: marker!.index }
|
|
528
749
|
},
|
|
@@ -540,7 +761,6 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
540
761
|
for (const row of removed) {
|
|
541
762
|
// A reused id belongs to its new row now.
|
|
542
763
|
if (byEventId.get(row.id) === row) byEventId.delete(row.id)
|
|
543
|
-
releaseSnapshotPins(sessionId, row.index)
|
|
544
764
|
}
|
|
545
765
|
const done = log.offset === log.clearIndex - 1
|
|
546
766
|
// Writes below the marker are dropped, so one sweep at the end suffices.
|
|
@@ -548,6 +768,49 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
548
768
|
return { deleted: removed.length, done }
|
|
549
769
|
},
|
|
550
770
|
|
|
771
|
+
async wakeLog({ sessionId, consumer }) {
|
|
772
|
+
const { log } = consumer
|
|
773
|
+
const key = cursorKey(consumer.name, log.lane, 0)
|
|
774
|
+
const head = headOf(sessions.get(sessionId))
|
|
775
|
+
let cursor = cursors.get(sessionId)?.get(key)
|
|
776
|
+
if (!cursor) {
|
|
777
|
+
const marker = clearIndexOf(sessionId)
|
|
778
|
+
if (head === marker) {
|
|
779
|
+
return {
|
|
780
|
+
cursor: idleLogCursor(consumer.name, log.lane, marker),
|
|
781
|
+
head,
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
const position = log.backfill ? marker : head
|
|
785
|
+
cursor = {
|
|
786
|
+
consumer: consumer.name,
|
|
787
|
+
lane: log.lane,
|
|
788
|
+
kind: 'log',
|
|
789
|
+
position,
|
|
790
|
+
head: log.backfill
|
|
791
|
+
? nextLogHead(sessionId, position, log.types)
|
|
792
|
+
: null,
|
|
793
|
+
dispatch: freshDispatch(),
|
|
794
|
+
queue: [],
|
|
795
|
+
queued: 0,
|
|
796
|
+
completions: new Map(),
|
|
797
|
+
types: log.types,
|
|
798
|
+
}
|
|
799
|
+
cursorsOf(sessionId).set(key, cursor)
|
|
800
|
+
} else {
|
|
801
|
+
cursor.types = log.types
|
|
802
|
+
cursor.head ??= nextLogHead(sessionId, cursor.position, log.types)
|
|
803
|
+
}
|
|
804
|
+
return { cursor: toCursor(cursor), head }
|
|
805
|
+
},
|
|
806
|
+
|
|
807
|
+
async cursors(sessionId) {
|
|
808
|
+
return [...(cursors.get(sessionId)?.values() ?? [])]
|
|
809
|
+
.filter((cursor) => cursor.kind !== 'event' || cursor.head !== null)
|
|
810
|
+
.map(toCursor)
|
|
811
|
+
.toSorted(byCursor)
|
|
812
|
+
},
|
|
813
|
+
|
|
551
814
|
async read(sessionId, opts) {
|
|
552
815
|
const log = sessions.get(sessionId)
|
|
553
816
|
if (!log) return []
|
|
@@ -569,46 +832,64 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
569
832
|
holder,
|
|
570
833
|
ttlMs,
|
|
571
834
|
expiresAtMs,
|
|
572
|
-
|
|
835
|
+
exclude,
|
|
836
|
+
consumers,
|
|
573
837
|
maxFailures,
|
|
838
|
+
through,
|
|
574
839
|
}) {
|
|
840
|
+
refreshLogTypes(sessionId, consumers)
|
|
575
841
|
const claimedAt = clock.now()
|
|
576
842
|
const now = claimedAt.getTime()
|
|
577
843
|
const expiresAt = new Date(expiresAtMs ?? now + ttlMs)
|
|
578
|
-
const excluded = new Set(
|
|
579
|
-
|
|
844
|
+
const excluded = new Set(
|
|
845
|
+
(exclude ?? []).map((ref) =>
|
|
846
|
+
cursorKey(ref.consumer, ref.lane, ref.index),
|
|
847
|
+
),
|
|
848
|
+
)
|
|
849
|
+
const served = servedBy(consumers)
|
|
850
|
+
const eligible: Cursor[] = []
|
|
580
851
|
let retryAt: Date | null = null
|
|
581
852
|
|
|
582
|
-
for (const
|
|
583
|
-
if (
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
853
|
+
for (const [key, cursor] of cursors.get(sessionId) ?? []) {
|
|
854
|
+
if (
|
|
855
|
+
cursor.head === null ||
|
|
856
|
+
(through !== undefined && cursor.head > through) ||
|
|
857
|
+
cursor.dispatch.failedAt !== null ||
|
|
858
|
+
!served(cursor) ||
|
|
859
|
+
excluded.has(key)
|
|
860
|
+
) {
|
|
861
|
+
continue
|
|
862
|
+
}
|
|
863
|
+
const expiry = cursor.dispatch.claimExpiresAt
|
|
864
|
+
if (expiry && expiry.getTime() > now) {
|
|
865
|
+
if (!retryAt || expiry < retryAt) retryAt = new Date(expiry)
|
|
588
866
|
continue
|
|
589
867
|
}
|
|
590
|
-
eligible.push(
|
|
868
|
+
eligible.push(cursor)
|
|
591
869
|
}
|
|
592
870
|
|
|
593
871
|
if (eligible.length === 0) {
|
|
594
872
|
return retryAt ? { outcome: 'busy', retryAt } : { outcome: 'settled' }
|
|
595
873
|
}
|
|
596
|
-
eligible.sort((a, b) => a.index - b.index)
|
|
597
874
|
const clearIndex = clearIndexOf(sessionId)
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const claimed = claim({
|
|
875
|
+
const claimed = eligible
|
|
876
|
+
.map((cursor) => {
|
|
877
|
+
const head = claim({
|
|
602
878
|
sessionId,
|
|
603
|
-
|
|
879
|
+
cursor,
|
|
604
880
|
holder,
|
|
605
881
|
now: claimedAt,
|
|
606
882
|
expiresAt,
|
|
607
883
|
maxFailures,
|
|
608
884
|
})
|
|
609
|
-
if (clearIndex > 0)
|
|
610
|
-
return
|
|
611
|
-
})
|
|
885
|
+
if (clearIndex > 0) head.clearIndex = clearIndex
|
|
886
|
+
return head
|
|
887
|
+
})
|
|
888
|
+
.toSorted(byHead)
|
|
889
|
+
return {
|
|
890
|
+
outcome: 'claimed',
|
|
891
|
+
events: claimed,
|
|
892
|
+
head: headOf(sessions.get(sessionId)),
|
|
612
893
|
}
|
|
613
894
|
},
|
|
614
895
|
|
|
@@ -616,48 +897,69 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
616
897
|
const now = clock.now()
|
|
617
898
|
const expiresAt = new Date(expiresAtMs ?? now.getTime() + ttlMs)
|
|
618
899
|
const clearIndex = clearIndexOf(sessionId)
|
|
619
|
-
const renewed:
|
|
620
|
-
const superseded:
|
|
621
|
-
|
|
622
|
-
(a, b) => a.index - b.index,
|
|
623
|
-
)
|
|
624
|
-
|
|
625
|
-
|
|
900
|
+
const renewed: HeadRef[] = []
|
|
901
|
+
const superseded: HeadRef[] = []
|
|
902
|
+
const ordered = claims.toSorted(
|
|
903
|
+
(a, b) => a.index - b.index || compareText(a.consumer, b.consumer),
|
|
904
|
+
)
|
|
905
|
+
for (const { attempt, ...ref } of ordered) {
|
|
906
|
+
if (ref.index <= clearIndex) {
|
|
907
|
+
superseded.push(ref)
|
|
626
908
|
continue
|
|
627
909
|
}
|
|
628
|
-
const
|
|
629
|
-
if (!
|
|
630
|
-
|
|
631
|
-
|
|
910
|
+
const cursor = cursorAt(sessionId, ref)
|
|
911
|
+
if (!cursor) continue
|
|
912
|
+
const dispatch = cursor.dispatch
|
|
913
|
+
// An event the cursor has not reached yet holds no claim to renew.
|
|
914
|
+
if (cursor.head !== ref.index && cursor.position < ref.index) continue
|
|
915
|
+
if (cursor.head !== ref.index || dispatch.attemptCount > attempt) {
|
|
916
|
+
superseded.push(ref)
|
|
632
917
|
continue
|
|
633
918
|
}
|
|
634
919
|
if (
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
row.claimExpiresAt > now
|
|
920
|
+
dispatch.failedAt === null &&
|
|
921
|
+
dispatch.claimHolder === holder &&
|
|
922
|
+
dispatch.claimExpiresAt !== null &&
|
|
923
|
+
dispatch.claimExpiresAt > now
|
|
640
924
|
) {
|
|
641
|
-
|
|
642
|
-
renewed.push(
|
|
925
|
+
dispatch.claimExpiresAt = new Date(expiresAt)
|
|
926
|
+
renewed.push(ref)
|
|
643
927
|
}
|
|
644
928
|
}
|
|
645
929
|
return { renewed, superseded }
|
|
646
930
|
},
|
|
647
931
|
|
|
648
|
-
async completeAttempt({
|
|
932
|
+
async completeAttempt({
|
|
933
|
+
sessionId,
|
|
934
|
+
consumer,
|
|
935
|
+
lane,
|
|
936
|
+
index,
|
|
937
|
+
attempt,
|
|
938
|
+
events,
|
|
939
|
+
consumers,
|
|
940
|
+
}) {
|
|
649
941
|
if (index <= clearIndexOf(sessionId)) return { outcome: 'superseded' }
|
|
650
|
-
const
|
|
651
|
-
const
|
|
652
|
-
if (
|
|
653
|
-
if (
|
|
942
|
+
const ref = { consumer, lane, index }
|
|
943
|
+
const cursor = cursorAt(sessionId, ref)
|
|
944
|
+
if (!cursor) {
|
|
945
|
+
if (!rowAt(sessionId, index)) {
|
|
946
|
+
throw new TypeError(
|
|
947
|
+
`no event at index ${index} in session '${sessionId}'`,
|
|
948
|
+
)
|
|
949
|
+
}
|
|
950
|
+
return { outcome: 'superseded' }
|
|
951
|
+
}
|
|
952
|
+
refreshLogTypes(sessionId, consumers)
|
|
953
|
+
|
|
954
|
+
if (cursor.head !== index) {
|
|
955
|
+
const completion = cursor.completions.get(index)
|
|
956
|
+
if (!completion || completion.attempt !== attempt) {
|
|
654
957
|
return { outcome: 'superseded' }
|
|
655
958
|
}
|
|
656
959
|
const requestedIds = events.map((event) => event.id)
|
|
657
960
|
if (
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
parent.returnedEventIds.some(
|
|
961
|
+
completion.returnedEventIds.length !== requestedIds.length ||
|
|
962
|
+
completion.returnedEventIds.some(
|
|
661
963
|
(id, offset) => id !== requestedIds[offset],
|
|
662
964
|
)
|
|
663
965
|
) {
|
|
@@ -671,7 +973,10 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
671
973
|
existing.length !== events.length ||
|
|
672
974
|
existing.some(
|
|
673
975
|
(row) =>
|
|
674
|
-
row.cause?.
|
|
976
|
+
row.cause?.consumer !== consumer ||
|
|
977
|
+
row.cause.lane !== lane ||
|
|
978
|
+
row.cause.index !== index ||
|
|
979
|
+
row.cause.attempt !== attempt,
|
|
675
980
|
)
|
|
676
981
|
) {
|
|
677
982
|
throw new A2Error(
|
|
@@ -681,13 +986,17 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
681
986
|
}
|
|
682
987
|
return {
|
|
683
988
|
outcome: 'completed',
|
|
684
|
-
events:
|
|
989
|
+
events:
|
|
990
|
+
events.length === 0
|
|
991
|
+
? []
|
|
992
|
+
: idempotentReplay(events, existing.map(toStored)),
|
|
685
993
|
}
|
|
686
994
|
}
|
|
995
|
+
const dispatch = cursor.dispatch
|
|
687
996
|
if (
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
997
|
+
dispatch.attemptCount !== attempt ||
|
|
998
|
+
dispatch.failedAt !== null ||
|
|
999
|
+
dispatch.claimHolder === null
|
|
691
1000
|
) {
|
|
692
1001
|
return { outcome: 'superseded' }
|
|
693
1002
|
}
|
|
@@ -699,21 +1008,26 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
699
1008
|
'returned event batch contains already-appended ids',
|
|
700
1009
|
)
|
|
701
1010
|
}
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
1011
|
+
const inserted = insert(sessionId, events, clock.now(), consumers, {
|
|
1012
|
+
consumer,
|
|
1013
|
+
lane,
|
|
1014
|
+
index,
|
|
1015
|
+
attempt,
|
|
1016
|
+
})
|
|
1017
|
+
if (cursor.kind === 'log') cursor.completions.clear()
|
|
1018
|
+
cursor.completions.set(index, {
|
|
1019
|
+
attempt,
|
|
1020
|
+
returnedEventIds: events.map((event) => event.id),
|
|
1021
|
+
})
|
|
1022
|
+
advance(sessionId, cursor)
|
|
711
1023
|
notifyStreams(sessionId, inserted)
|
|
712
1024
|
return { outcome: 'completed', events: inserted.map(toStored) }
|
|
713
1025
|
},
|
|
714
1026
|
|
|
715
1027
|
async failAttempt({
|
|
716
1028
|
sessionId,
|
|
1029
|
+
consumer,
|
|
1030
|
+
lane,
|
|
717
1031
|
index,
|
|
718
1032
|
attempt,
|
|
719
1033
|
error,
|
|
@@ -723,76 +1037,88 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
723
1037
|
if (index <= clearIndexOf(sessionId)) {
|
|
724
1038
|
return { outcome: 'superseded', failureCount: 0 }
|
|
725
1039
|
}
|
|
726
|
-
const
|
|
1040
|
+
const cursor = cursorAt(sessionId, { consumer, lane, index })
|
|
1041
|
+
if (!cursor) {
|
|
1042
|
+
if (!rowAt(sessionId, index)) {
|
|
1043
|
+
throw new TypeError(
|
|
1044
|
+
`no event at index ${index} in session '${sessionId}'`,
|
|
1045
|
+
)
|
|
1046
|
+
}
|
|
1047
|
+
return { outcome: 'superseded', failureCount: 0 }
|
|
1048
|
+
}
|
|
1049
|
+
const dispatch = cursor.dispatch
|
|
727
1050
|
if (
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
(
|
|
1051
|
+
cursor.head !== index ||
|
|
1052
|
+
dispatch.attemptCount !== attempt ||
|
|
1053
|
+
(dispatch.claimHolder === null &&
|
|
1054
|
+
dispatch.lastFailedAttempt !== attempt)
|
|
731
1055
|
) {
|
|
732
|
-
return {
|
|
1056
|
+
return {
|
|
1057
|
+
outcome: 'superseded',
|
|
1058
|
+
failureCount: cursor.head === index ? dispatch.failureCount : 0,
|
|
1059
|
+
}
|
|
733
1060
|
}
|
|
734
|
-
if (
|
|
1061
|
+
if (
|
|
1062
|
+
dispatch.lastFailedAttempt === attempt &&
|
|
1063
|
+
dispatch.claimHolder === null
|
|
1064
|
+
) {
|
|
735
1065
|
return {
|
|
736
|
-
outcome:
|
|
737
|
-
failureCount:
|
|
1066
|
+
outcome: dispatch.failedAt ? 'dead_lettered' : 'failed',
|
|
1067
|
+
failureCount: dispatch.failureCount,
|
|
738
1068
|
}
|
|
739
1069
|
}
|
|
740
|
-
if (
|
|
741
|
-
return { outcome: 'dead_lettered', failureCount:
|
|
1070
|
+
if (dispatch.failedAt) {
|
|
1071
|
+
return { outcome: 'dead_lettered', failureCount: dispatch.failureCount }
|
|
742
1072
|
}
|
|
743
|
-
|
|
744
|
-
|
|
1073
|
+
dispatch.failureCount += 1
|
|
1074
|
+
dispatch.lastError = error
|
|
745
1075
|
const failedAt = clock.now()
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
retryAfterMs === undefined ||
|
|
1076
|
+
dispatch.lastFailedAt = new Date(failedAt)
|
|
1077
|
+
dispatch.lastFailedAttempt = attempt
|
|
1078
|
+
dispatch.claimHolder = null
|
|
1079
|
+
dispatch.claimExpiresAt =
|
|
1080
|
+
retryAfterMs === undefined || dispatch.failureCount >= maxFailures
|
|
751
1081
|
? null
|
|
752
1082
|
: new Date(failedAt.getTime() + retryAfterMs)
|
|
753
|
-
if (
|
|
754
|
-
|
|
755
|
-
dispatchOf(sessionId).ready.delete(row.index)
|
|
756
|
-
releaseSnapshotPins(sessionId, index)
|
|
1083
|
+
if (dispatch.failureCount >= maxFailures) {
|
|
1084
|
+
deadLetter(sessionId, cursor, failedAt)
|
|
757
1085
|
return {
|
|
758
1086
|
outcome: 'dead_lettered',
|
|
759
|
-
failureCount:
|
|
1087
|
+
failureCount: dispatch.failureCount,
|
|
760
1088
|
}
|
|
761
1089
|
}
|
|
762
|
-
return { outcome: 'failed', failureCount:
|
|
1090
|
+
return { outcome: 'failed', failureCount: dispatch.failureCount }
|
|
763
1091
|
},
|
|
764
1092
|
|
|
765
1093
|
async deadLetters(sessionId) {
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
)
|
|
774
|
-
|
|
1094
|
+
const clearIndex = clearIndexOf(sessionId)
|
|
1095
|
+
const dead: HeadEvent[] = []
|
|
1096
|
+
for (const cursor of cursors.get(sessionId)?.values() ?? []) {
|
|
1097
|
+
if (
|
|
1098
|
+
cursor.head === null ||
|
|
1099
|
+
cursor.head <= clearIndex ||
|
|
1100
|
+
cursor.dispatch.failedAt === null
|
|
1101
|
+
) {
|
|
1102
|
+
continue
|
|
1103
|
+
}
|
|
1104
|
+
dead.push(toHead(cursor, rowAt(sessionId, cursor.head)!))
|
|
1105
|
+
}
|
|
1106
|
+
return dead.toSorted(byHead)
|
|
775
1107
|
},
|
|
776
1108
|
|
|
777
|
-
async redrive({ sessionId, index, action }) {
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
return { redriven: false, hasPending: hasPending(sessionId) }
|
|
1109
|
+
async redrive({ sessionId, consumer, index, action, consumers }) {
|
|
1110
|
+
const cursor = deadHeadOf(sessionId, consumer, index)
|
|
1111
|
+
if (!cursor) {
|
|
1112
|
+
return { redriven: false, hasPending: hasPending(sessionId, consumers) }
|
|
782
1113
|
}
|
|
1114
|
+
refreshLogTypes(sessionId, consumers)
|
|
783
1115
|
if (action === 'skip') {
|
|
784
|
-
|
|
785
|
-
settle(sessionId, row)
|
|
1116
|
+
advance(sessionId, cursor)
|
|
786
1117
|
} else {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
const state = dispatchOf(sessionId)
|
|
790
|
-
const queue = row.lane === null ? undefined : state.lanes.get(row.lane)
|
|
791
|
-
if (row.lane === null || queue?.rows[queue.head] === row) {
|
|
792
|
-
state.ready.set(row.index, row)
|
|
793
|
-
}
|
|
1118
|
+
cursor.dispatch.failedAt = null
|
|
1119
|
+
cursor.dispatch.failureCount = 0
|
|
794
1120
|
}
|
|
795
|
-
return { redriven: true, hasPending: hasPending(sessionId) }
|
|
1121
|
+
return { redriven: true, hasPending: hasPending(sessionId, consumers) }
|
|
796
1122
|
},
|
|
797
1123
|
|
|
798
1124
|
async readState(sessionId, reducerName, stateOptions) {
|
|
@@ -821,17 +1147,17 @@ export function memory(options: MemoryStoreOptions = {}): A2Store {
|
|
|
821
1147
|
for (const write of writes.toSorted((a, b) => a.index - b.index)) {
|
|
822
1148
|
if (write.index < clearIndex) continue
|
|
823
1149
|
const checkpoint = checkpointKey(sessionId, reducerName, write.index)
|
|
824
|
-
for (const
|
|
825
|
-
const
|
|
1150
|
+
for (const pin of write.pins ?? []) {
|
|
1151
|
+
const cursor = cursorAt(sessionId, pin)
|
|
826
1152
|
if (
|
|
827
|
-
!
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1153
|
+
!cursor ||
|
|
1154
|
+
pin.index <= clearIndex ||
|
|
1155
|
+
cursor.head !== pin.index ||
|
|
1156
|
+
cursor.dispatch.failedAt !== null
|
|
831
1157
|
) {
|
|
832
1158
|
continue
|
|
833
1159
|
}
|
|
834
|
-
const pinKey =
|
|
1160
|
+
const pinKey = headPinKey(sessionId, pin)
|
|
835
1161
|
let pinned = snapshotPins.get(pinKey)
|
|
836
1162
|
if (!pinned) {
|
|
837
1163
|
pinned = new Set()
|