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
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { a as NonRetriableError, i as A2Error, n as isSessionCleared, o as asStoreUnavailable, r as sessionClearedError } from "./session-clear-
|
|
2
|
-
import { n as foldEvent, s as validateSync, t as cloneInitial } from "./reducer-
|
|
1
|
+
import { a as NonRetriableError, i as A2Error, n as isSessionCleared, o as asStoreUnavailable, r as sessionClearedError } from "./session-clear-wHd5GCpZ.js";
|
|
2
|
+
import { n as foldEvent, s as validateSync, t as cloneInitial } from "./reducer-CjmvZSAb.js";
|
|
3
3
|
import { n as encodeState, t as decodeState } from "./reducer-runtime-DQwz_J59.js";
|
|
4
|
-
import {
|
|
4
|
+
import { C as stateTailReaders, S as serverInternals, a as POLL_TIMINGS, b as runtimeConsumerFactory, c as SOCKET_TIMINGS, g as failedPreviousAttempt, h as drainSettledHooks, l as STATE_CACHE, s as RESERVED_PARTICIPANT_IDS, t as DRAIN_TIMINGS, u as STREAM_TIMINGS, v as markSchedulerSendFailure, x as runtimeConsumers, y as nullProtoRecord } from "./internal-CvsrprnU.js";
|
|
5
5
|
import { t as boundedCache } from "./bounded-cache-CPHWF-8h.js";
|
|
6
|
-
import { i as
|
|
7
|
-
import { i as
|
|
6
|
+
import { a as presenceTombstones, i as takeBuffered, n as isSerializedEvent, r as parseSerializedEvent, t as bufferedEvents } from "./stream-buffer-DrTyiIuo.js";
|
|
7
|
+
import { i as streamActivities, r as createStreamActivity, t as defaultSleep } from "./store-polling-BZt2Rf3D.js";
|
|
8
8
|
import { r as platformWaitUntil, t as invocationDeadlineMs } from "./platform-B4TnJtWu.js";
|
|
9
|
-
import { A as stateSnapshotToWire, C as serializedEventToWireJson, D as socketFrameFor, E as socketEventsFor, O as socketSubscribedFor, S as presenceSnapshotToWire, T as socketErrorAckFor, b as presencePatchToWire, d as heldEventBatch, i as compactEventJson, j as wireVersion, k as socketUnsubscribedFor, m as isStreamEvent, n as SOCKET_PING_FRAME, o as errorStatus, r as asA2Error, s as errorToWire, t as EVENT_WRITE_CHARS, u as eventToWire, w as socketAckFor } from "./wire-
|
|
9
|
+
import { A as stateSnapshotToWire, C as serializedEventToWireJson, D as socketFrameFor, E as socketEventsFor, O as socketSubscribedFor, S as presenceSnapshotToWire, T as socketErrorAckFor, b as presencePatchToWire, d as heldEventBatch, i as compactEventJson, j as wireVersion, k as socketUnsubscribedFor, m as isStreamEvent, n as SOCKET_PING_FRAME, o as errorStatus, r as asA2Error, s as errorToWire, t as EVENT_WRITE_CHARS, u as eventToWire, w as socketAckFor } from "./wire-CV-Z7wz-.js";
|
|
10
10
|
import { t as retryableLazy } from "./retryable-lazy-DZWmHpii.js";
|
|
11
11
|
//#region src/push-envelope.ts
|
|
12
12
|
/**
|
|
@@ -34,6 +34,10 @@ function parsePresenceSibling(value) {
|
|
|
34
34
|
if (at !== void 0) out.at = at;
|
|
35
35
|
return out;
|
|
36
36
|
}
|
|
37
|
+
/** The longest event id a push may carry: ids are indexed, and Postgres's index rows have a limit. */
|
|
38
|
+
const MAX_PUSHED_ID = 256;
|
|
39
|
+
/** A UUIDv5, the shape of the ids A2 derives (deterministic-id.ts). */
|
|
40
|
+
const DERIVED_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
37
41
|
/** The events half of the envelope, shared by the POST body and socket
|
|
38
42
|
* `push` frames so INVALID_PAYLOAD shapes have one implementation. */
|
|
39
43
|
function parsePushEvents(events) {
|
|
@@ -43,6 +47,8 @@ function parsePushEvents(events) {
|
|
|
43
47
|
const { type, payload, id } = event;
|
|
44
48
|
if (typeof type !== "string" || type.length === 0) throw invalid(`events[${i}].type must be a non-empty string`);
|
|
45
49
|
if (id !== void 0 && typeof id !== "string") throw invalid(`events[${i}].id must be a string when present`);
|
|
50
|
+
if (id !== void 0 && (id.length === 0 || id.length > MAX_PUSHED_ID)) throw invalid(`events[${i}].id must be 1-${MAX_PUSHED_ID} characters when present`);
|
|
51
|
+
if (id !== void 0 && DERIVED_ID.test(id)) throw invalid(`events[${i}].id is shaped like the ids A2 derives for consumers (a UUIDv5); use crypto.randomUUID() or idempotentId()`);
|
|
46
52
|
const out = {
|
|
47
53
|
type,
|
|
48
54
|
payload
|
|
@@ -982,11 +988,11 @@ function createServerFetch(server) {
|
|
|
982
988
|
//#endregion
|
|
983
989
|
//#region src/deterministic-id.ts
|
|
984
990
|
/**
|
|
985
|
-
* Deterministic default ids for
|
|
991
|
+
* Deterministic default ids for consumer `session.append` calls. A consumer
|
|
986
992
|
* re-run after a crash calls `session.append` again; giving
|
|
987
993
|
* each call a deterministic id — derived from the triggering event's id
|
|
988
994
|
* and the caller's stable name — makes the re-run a no-op via the store's
|
|
989
|
-
* unique index on event ids.
|
|
995
|
+
* per-session unique index on event ids.
|
|
990
996
|
*
|
|
991
997
|
* The id is a UUIDv5-shaped SHA-1 over
|
|
992
998
|
* `(namespace, triggerEventId, name, itemOrdinal)`, computed with
|
|
@@ -1008,20 +1014,27 @@ async function deterministicIdFromInput(input) {
|
|
|
1008
1014
|
async function deterministicId(parts) {
|
|
1009
1015
|
return deterministicIdFromInput(parts.join("\0"));
|
|
1010
1016
|
}
|
|
1011
|
-
|
|
1017
|
+
/**
|
|
1018
|
+
* `consumer` keys the id on the consumer that appends, so two consumers of
|
|
1019
|
+
* one event never collide. A2's own consumers pass none: their types are
|
|
1020
|
+
* disjoint, so the event alone keys them.
|
|
1021
|
+
*/
|
|
1022
|
+
async function deterministicEventId(triggerEventId, name, itemOrdinal, consumer) {
|
|
1012
1023
|
if (name.length === 0) throw new TypeError("append name must not be empty");
|
|
1013
1024
|
return deterministicId([
|
|
1014
1025
|
APPEND_NAMESPACE,
|
|
1015
1026
|
triggerEventId,
|
|
1016
1027
|
name,
|
|
1017
|
-
itemOrdinal
|
|
1028
|
+
itemOrdinal,
|
|
1029
|
+
...consumer === void 0 ? [] : [consumer]
|
|
1018
1030
|
]);
|
|
1019
1031
|
}
|
|
1020
|
-
async function deterministicReturnedEventId(triggerEventId, itemOrdinal) {
|
|
1032
|
+
async function deterministicReturnedEventId(triggerEventId, itemOrdinal, consumer) {
|
|
1021
1033
|
return deterministicId([
|
|
1022
1034
|
RETURN_NAMESPACE,
|
|
1023
1035
|
triggerEventId,
|
|
1024
|
-
itemOrdinal
|
|
1036
|
+
itemOrdinal,
|
|
1037
|
+
...consumer === void 0 ? [] : [consumer]
|
|
1025
1038
|
]);
|
|
1026
1039
|
}
|
|
1027
1040
|
/**
|
|
@@ -1030,17 +1043,18 @@ async function deterministicReturnedEventId(triggerEventId, itemOrdinal) {
|
|
|
1030
1043
|
* after a clear schedules a distinct task while sessions never cleared keep
|
|
1031
1044
|
* the ids they always had.
|
|
1032
1045
|
*/
|
|
1033
|
-
async function deterministicScheduleId(contract, sessionId, triggerEventId, name, clearIndex = 0) {
|
|
1046
|
+
async function deterministicScheduleId(contract, sessionId, triggerEventId, name, clearIndex = 0, consumer) {
|
|
1034
1047
|
return deterministicIdFromInput(JSON.stringify([
|
|
1035
1048
|
SCHEDULE_NAMESPACE,
|
|
1036
1049
|
contract,
|
|
1037
1050
|
sessionId,
|
|
1038
1051
|
triggerEventId,
|
|
1039
1052
|
name,
|
|
1040
|
-
...clearIndex > 0 ? [clearIndex] : []
|
|
1053
|
+
...clearIndex > 0 ? [clearIndex] : [],
|
|
1054
|
+
...consumer === void 0 ? [] : [{ consumer }]
|
|
1041
1055
|
]));
|
|
1042
1056
|
}
|
|
1043
|
-
async function deterministicScheduleClearId({ contract, sessionId, triggerEventId, clearIndex, name, ifLastIndex }) {
|
|
1057
|
+
async function deterministicScheduleClearId({ contract, sessionId, triggerEventId, clearIndex, name, ifLastIndex, consumer }) {
|
|
1044
1058
|
return deterministicIdFromInput(JSON.stringify([
|
|
1045
1059
|
SCHEDULE_CLEAR_NAMESPACE,
|
|
1046
1060
|
contract,
|
|
@@ -1048,7 +1062,8 @@ async function deterministicScheduleClearId({ contract, sessionId, triggerEventI
|
|
|
1048
1062
|
triggerEventId,
|
|
1049
1063
|
clearIndex,
|
|
1050
1064
|
name,
|
|
1051
|
-
ifLastIndex
|
|
1065
|
+
ifLastIndex,
|
|
1066
|
+
...consumer === void 0 ? [] : [{ consumer }]
|
|
1052
1067
|
]));
|
|
1053
1068
|
}
|
|
1054
1069
|
async function deterministicScheduledEventId(scheduleId, itemOrdinal) {
|
|
@@ -1101,13 +1116,76 @@ async function deliverSchedulerClear(server, task) {
|
|
|
1101
1116
|
if (!internals) throw new TypeError(`a2 scheduler: server for contract '${task.contract}' cannot receive scheduled clears`);
|
|
1102
1117
|
await internals.schedulerClear(task.sessionId, task.ifLastIndex);
|
|
1103
1118
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1119
|
+
const CONSUMER_NAME = /^[a-z0-9][a-z0-9._:-]{0,127}$/i;
|
|
1120
|
+
/** Define a consumer: a named reader of each session's log. */
|
|
1121
|
+
function consumer(options) {
|
|
1122
|
+
return defineConsumer(options, false);
|
|
1106
1123
|
}
|
|
1124
|
+
/**
|
|
1125
|
+
* `consumer()`, and for A2's own runtimes (`runtime`) a name under `a2.` and
|
|
1126
|
+
* child ids without the name: their types are disjoint, so the event alone
|
|
1127
|
+
* keys them, and regrouping A2's consumers never changes an id.
|
|
1128
|
+
*/
|
|
1129
|
+
function defineConsumer(options, runtime) {
|
|
1130
|
+
const contract = options?.contract;
|
|
1131
|
+
if (!contract || typeof contract.name !== "string") throw new TypeError("consumer expects options with a contract");
|
|
1132
|
+
const consumerName = options.name;
|
|
1133
|
+
if (typeof consumerName !== "string" || !CONSUMER_NAME.test(consumerName)) throw new TypeError("consumer name must be 1-128 letters, digits, and . _ : - characters");
|
|
1134
|
+
if (consumerName.startsWith("a2.") !== runtime) throw new TypeError(runtime ? `A2's own consumers are named under 'a2.'` : `consumer names starting with 'a2.' are reserved`);
|
|
1135
|
+
const types = options.types === void 0 ? null : [...options.types];
|
|
1136
|
+
if (types?.length === 0) throw new TypeError(`consumer '${consumerName}' lists no types; leave types out to take every type`);
|
|
1137
|
+
for (const type of types ?? []) if (!Object.hasOwn(contract.events, type)) throw new TypeError(`contract '${contract.name}' has no event type '${String(type)}'`);
|
|
1138
|
+
const lane = options.lane ?? null;
|
|
1139
|
+
if (lane !== null && typeof lane !== "function" && (typeof lane !== "string" || lane.length === 0)) throw new TypeError(`lane for consumer '${consumerName}' must be a non-empty string or a function`);
|
|
1140
|
+
const backfill = options.backfill ?? false;
|
|
1141
|
+
if (backfill && typeof lane !== "string") throw new TypeError(`consumer '${consumerName}' backfills, so its lane must be a string`);
|
|
1142
|
+
if (backfill && options.abortOn !== void 0) throw new TypeError(`consumer '${consumerName}' backfills, so it can't use abortOn`);
|
|
1143
|
+
return { run(fn) {
|
|
1144
|
+
if (typeof fn !== "function") throw new TypeError(`consumer '${consumerName}' needs a run function`);
|
|
1145
|
+
const defined = {
|
|
1146
|
+
name: consumerName,
|
|
1147
|
+
contract: { name: contract.name },
|
|
1148
|
+
spec: {
|
|
1149
|
+
types,
|
|
1150
|
+
lane,
|
|
1151
|
+
backfill,
|
|
1152
|
+
abortOn: options.abortOn ?? null,
|
|
1153
|
+
run: fn
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
if (runtime) runtimeConsumers.add(defined);
|
|
1157
|
+
return defined;
|
|
1158
|
+
} };
|
|
1159
|
+
}
|
|
1160
|
+
runtimeConsumerFactory.define = (options) => defineConsumer(options, true);
|
|
1161
|
+
/** A registered consumer as the store needs to know it. */
|
|
1162
|
+
const storeConsumerOf = (registered) => registered.log ? {
|
|
1163
|
+
name: registered.name,
|
|
1164
|
+
log: registered.log
|
|
1165
|
+
} : { name: registered.name };
|
|
1107
1166
|
const MAX_FAILURES = 10;
|
|
1167
|
+
/** How long `consume` waits before looking again at a head it can't run. */
|
|
1168
|
+
const CONSUME_WAIT_MIN_MS = 25;
|
|
1169
|
+
const CONSUME_WAIT_MAX_MS = 1e3;
|
|
1170
|
+
/**
|
|
1171
|
+
* `task`, or a rejection with `signal`'s reason once it aborts. The task runs
|
|
1172
|
+
* on either way, and its own failure is caught, so nothing goes unhandled.
|
|
1173
|
+
*/
|
|
1174
|
+
const untilAborted = (task, signal) => {
|
|
1175
|
+
if (!signal) return task;
|
|
1176
|
+
task.catch(() => {});
|
|
1177
|
+
if (signal.aborted) return Promise.reject(signal.reason);
|
|
1178
|
+
return new Promise((resolve, reject) => {
|
|
1179
|
+
const abort = () => reject(signal.reason);
|
|
1180
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1181
|
+
task.then(resolve, reject).finally(() => {
|
|
1182
|
+
signal.removeEventListener("abort", abort);
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
1185
|
+
};
|
|
1108
1186
|
/** How long an event waits after its `failureCount`th failed attempt. */
|
|
1109
1187
|
const failureRetryDelayMs = (failureCount) => Math.min(DRAIN_TIMINGS.failureRetryBaseMs * 2 ** Math.max(failureCount - 1, 0), DRAIN_TIMINGS.failureRetryMaxMs);
|
|
1110
|
-
/** Abort a lost attempt's
|
|
1188
|
+
/** Abort a lost attempt's consumer and release its drain slot. */
|
|
1111
1189
|
function endSuperseded(entry, message) {
|
|
1112
1190
|
entry.supersede.abort(new A2Error("SUPERSEDED_ATTEMPT", message));
|
|
1113
1191
|
entry.fence({
|
|
@@ -1242,21 +1320,35 @@ function foldPresenceRows(rows) {
|
|
|
1242
1320
|
};
|
|
1243
1321
|
return map;
|
|
1244
1322
|
}
|
|
1245
|
-
|
|
1323
|
+
/** One cursor head, as a map key. */
|
|
1324
|
+
const headKey = (ref) => JSON.stringify([
|
|
1325
|
+
ref.consumer,
|
|
1326
|
+
ref.lane,
|
|
1327
|
+
ref.index
|
|
1328
|
+
]);
|
|
1329
|
+
const headRefOf = (head) => ({
|
|
1330
|
+
consumer: head.consumer,
|
|
1331
|
+
lane: head.lane,
|
|
1332
|
+
index: head.index
|
|
1333
|
+
});
|
|
1334
|
+
const addSnapshotWrite = (writes, index, state, pin) => {
|
|
1246
1335
|
let write = writes.get(index);
|
|
1247
1336
|
if (!write) {
|
|
1248
1337
|
write = {
|
|
1249
1338
|
state: cloneInitial(state),
|
|
1250
|
-
|
|
1339
|
+
pins: /* @__PURE__ */ new Map()
|
|
1251
1340
|
};
|
|
1252
1341
|
writes.set(index, write);
|
|
1253
1342
|
}
|
|
1254
|
-
if (
|
|
1343
|
+
if (pin !== void 0) write.pins.set(headKey(pin), pin);
|
|
1255
1344
|
};
|
|
1345
|
+
/** Whether a registered consumer runs events of this type. */
|
|
1346
|
+
const takes = (registered, type) => registered.types === null || registered.types.has(type);
|
|
1256
1347
|
/** Implement a contract: bind its vocabulary to storage and reactions. */
|
|
1257
1348
|
function createServer(options) {
|
|
1258
1349
|
const serverContract = options?.contract;
|
|
1259
1350
|
if (serverContract === null || typeof serverContract !== "object" || typeof serverContract.name !== "string" || serverContract.events === null) throw new TypeError("createServer expects options with a contract (see a2.contract)");
|
|
1351
|
+
if (Object.hasOwn(options, "handlers")) throw new TypeError("createServer no longer takes handlers: write each as a consumer in `consumers` (see https://www.a2.dev/upgrades/0-21)");
|
|
1260
1352
|
const name = serverContract.name;
|
|
1261
1353
|
const defs = serverContract.events;
|
|
1262
1354
|
const presenceDefs = serverContract.presence;
|
|
@@ -1292,15 +1384,9 @@ function createServer(options) {
|
|
|
1292
1384
|
const resolveStore = retryableLazy(makeStore).get;
|
|
1293
1385
|
const telemetry = options.telemetry ?? CONSOLE_TELEMETRY;
|
|
1294
1386
|
const scheduler = options.scheduler;
|
|
1295
|
-
const
|
|
1296
|
-
for (const [type, entry] of Object.entries(options.handlers ?? {})) {
|
|
1297
|
-
if (entry === void 0) continue;
|
|
1298
|
-
if (!Object.hasOwn(defs, type)) throw new TypeError(`contract '${name}' has no event type '${type}' — cannot register a handler for it`);
|
|
1299
|
-
const callback = typeof entry === "function" ? entry : entry?.handler;
|
|
1300
|
-
if (typeof callback !== "function") throw new TypeError(`handler for '${type}' must be a function`);
|
|
1387
|
+
const abortMatcherOf = (spec) => {
|
|
1301
1388
|
let abortOn = null;
|
|
1302
|
-
|
|
1303
|
-
if (spec !== void 0) {
|
|
1389
|
+
if (spec !== void 0 && spec !== null) {
|
|
1304
1390
|
abortOn = /* @__PURE__ */ new Map();
|
|
1305
1391
|
const pairs = Array.isArray(spec) ? spec.map((abortType) => [abortType, true]) : Object.entries(spec);
|
|
1306
1392
|
for (const [abortType, matcher] of pairs) {
|
|
@@ -1310,14 +1396,70 @@ function createServer(options) {
|
|
|
1310
1396
|
}
|
|
1311
1397
|
if (abortOn.size === 0) abortOn = null;
|
|
1312
1398
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1399
|
+
return abortOn;
|
|
1400
|
+
};
|
|
1401
|
+
const laneContext = (sessionId, event) => ({
|
|
1402
|
+
sessionId,
|
|
1403
|
+
event: {
|
|
1404
|
+
type: event.type,
|
|
1405
|
+
payload: structuredClone(event.payload),
|
|
1406
|
+
...event.id === void 0 ? {} : { id: event.id }
|
|
1407
|
+
}
|
|
1408
|
+
});
|
|
1409
|
+
const registeredConsumers = /* @__PURE__ */ new Map();
|
|
1410
|
+
/** A consumer definition as the server runs it. */
|
|
1411
|
+
const registrationOf = (defined) => {
|
|
1412
|
+
const { types, lane, backfill, abortOn, run } = defined.spec;
|
|
1413
|
+
const matcher = abortMatcherOf(abortOn);
|
|
1414
|
+
return {
|
|
1415
|
+
name: defined.name,
|
|
1416
|
+
types: types === null ? null : new Set(types),
|
|
1417
|
+
laneOf: typeof lane === "string" ? null : lane === null ? () => null : (sessionId, event) => {
|
|
1418
|
+
const resolved = lane(laneContext(sessionId, event));
|
|
1419
|
+
if (resolved !== null && (typeof resolved !== "string" || resolved.length === 0)) throw new TypeError(`lane for consumer '${defined.name}' must resolve to a non-empty string or null`);
|
|
1420
|
+
return resolved;
|
|
1421
|
+
},
|
|
1422
|
+
log: typeof lane === "string" ? {
|
|
1423
|
+
lane,
|
|
1424
|
+
types: types === null ? null : [...types],
|
|
1425
|
+
backfill
|
|
1426
|
+
} : void 0,
|
|
1427
|
+
abortOn: () => matcher,
|
|
1428
|
+
run,
|
|
1429
|
+
anonymousIds: runtimeConsumers.has(defined)
|
|
1430
|
+
};
|
|
1431
|
+
};
|
|
1432
|
+
/** The definitions the server runs, so `consume` can tell its own from another. */
|
|
1433
|
+
const definitions = /* @__PURE__ */ new Map();
|
|
1434
|
+
/** A consumer written inline is `consumer()`'s options and `run`, on this contract. */
|
|
1435
|
+
const definitionOf = (entry) => {
|
|
1436
|
+
if (entry === null || typeof entry !== "object" || "spec" in entry || !("run" in entry)) return entry;
|
|
1437
|
+
const { run, ...rest } = entry;
|
|
1438
|
+
return consumer({
|
|
1439
|
+
...rest,
|
|
1440
|
+
contract: serverContract
|
|
1441
|
+
}).run(run);
|
|
1442
|
+
};
|
|
1443
|
+
for (const entry of options.consumers ?? []) {
|
|
1444
|
+
const defined = definitionOf(entry);
|
|
1445
|
+
if (defined?.contract?.name !== name || !defined.spec) throw new TypeError(`createServer for '${name}' got a consumer of another contract`);
|
|
1446
|
+
if (registeredConsumers.has(defined.name)) throw new TypeError(`two consumers are named '${defined.name}'`);
|
|
1447
|
+
registeredConsumers.set(defined.name, registrationOf(defined));
|
|
1448
|
+
definitions.set(defined.name, defined);
|
|
1320
1449
|
}
|
|
1450
|
+
const anonymousTypes = /* @__PURE__ */ new Map();
|
|
1451
|
+
for (const registered of registeredConsumers.values()) {
|
|
1452
|
+
if (!registered.anonymousIds) continue;
|
|
1453
|
+
for (const type of registered.types ?? Object.keys(defs)) {
|
|
1454
|
+
const other = anonymousTypes.get(type);
|
|
1455
|
+
if (other !== void 0) throw new TypeError(`A2's consumers '${other}' and '${registered.name}' both take '${type}'`);
|
|
1456
|
+
anonymousTypes.set(type, registered.name);
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
/** The consumers this server serves, as the store needs to know them. */
|
|
1460
|
+
const storeConsumers = [...registeredConsumers.values()].map(storeConsumerOf);
|
|
1461
|
+
/** Whether an event starts cursor work for any consumer. */
|
|
1462
|
+
const startsWork = (event) => (event.targets?.length ?? 0) > 0 || [...registeredConsumers.values()].some((registered) => registered.log && takes(registered, event.type));
|
|
1321
1463
|
const prefix = `${name}${NS}`;
|
|
1322
1464
|
const nsId = (sessionId) => `${prefix}${sessionId}`;
|
|
1323
1465
|
const stripNs = (stored) => stored.slice(prefix.length);
|
|
@@ -1414,20 +1556,15 @@ function createServer(options) {
|
|
|
1414
1556
|
};
|
|
1415
1557
|
if (e.id !== void 0) validated.id = e.id;
|
|
1416
1558
|
if (!resolveDispatchMetadata) return validated;
|
|
1417
|
-
const
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
payload: structuredClone(result.value),
|
|
1425
|
-
...e.id === void 0 ? {} : { id: e.id }
|
|
1426
|
-
}
|
|
1427
|
-
}) : registration.lane;
|
|
1428
|
-
if (typeof lane !== "string" || lane.length === 0) throw new TypeError(`lane for '${e.type}' must resolve to a non-empty string`);
|
|
1429
|
-
validated.lane = lane;
|
|
1559
|
+
const targets = [];
|
|
1560
|
+
for (const registered of registeredConsumers.values()) {
|
|
1561
|
+
if (!registered.laneOf || !takes(registered, e.type)) continue;
|
|
1562
|
+
targets.push({
|
|
1563
|
+
consumer: registered.name,
|
|
1564
|
+
lane: registered.laneOf(sessionId, validated)
|
|
1565
|
+
});
|
|
1430
1566
|
}
|
|
1567
|
+
if (targets.length > 0) validated.targets = targets;
|
|
1431
1568
|
return validated;
|
|
1432
1569
|
});
|
|
1433
1570
|
};
|
|
@@ -1455,9 +1592,12 @@ function createServer(options) {
|
|
|
1455
1592
|
let rows;
|
|
1456
1593
|
let shouldHealSession;
|
|
1457
1594
|
try {
|
|
1458
|
-
const result = await store.append(nsId(sessionId), validated,
|
|
1595
|
+
const result = await store.append(nsId(sessionId), validated, {
|
|
1596
|
+
consumers: storeConsumers,
|
|
1597
|
+
...originIndex === void 0 ? {} : { originIndex }
|
|
1598
|
+
});
|
|
1459
1599
|
rows = result.events;
|
|
1460
|
-
shouldHealSession = result.hasPending && !(source === "
|
|
1600
|
+
shouldHealSession = result.hasPending && !(source === "consumer" && !validated.some(startsWork));
|
|
1461
1601
|
} catch (err) {
|
|
1462
1602
|
throw asStoreUnavailable(err);
|
|
1463
1603
|
}
|
|
@@ -1566,7 +1706,7 @@ function createServer(options) {
|
|
|
1566
1706
|
state: write.state
|
|
1567
1707
|
})
|
|
1568
1708
|
};
|
|
1569
|
-
if (write.
|
|
1709
|
+
if (write.pins.size > 0) snapshot.pins = [...write.pins.values()];
|
|
1570
1710
|
return snapshot;
|
|
1571
1711
|
});
|
|
1572
1712
|
if (snapshots.length > 0) persistSnapshots({
|
|
@@ -1635,7 +1775,7 @@ function createServer(options) {
|
|
|
1635
1775
|
/**
|
|
1636
1776
|
* Serves a plan from the cached state when every read is at or past the
|
|
1637
1777
|
* cached index. A read bounded at the cached index by an event whose id
|
|
1638
|
-
* matches the cached one (a
|
|
1778
|
+
* matches the cached one (a consumer's own trigger) needs no store request;
|
|
1639
1779
|
* any other read fetches the tail from the cached event onward and checks
|
|
1640
1780
|
* that event's id. Returns false when the store must answer instead.
|
|
1641
1781
|
*/
|
|
@@ -1665,14 +1805,14 @@ function createServer(options) {
|
|
|
1665
1805
|
clearIndex: cached.clearIndex,
|
|
1666
1806
|
snapshot: "memory",
|
|
1667
1807
|
visit: (entry, state, index) => {
|
|
1668
|
-
if (entry.
|
|
1808
|
+
if (entry.pin !== void 0) addSnapshotWrite(writes, index, state, entry.pin);
|
|
1669
1809
|
}
|
|
1670
1810
|
});
|
|
1671
1811
|
const added = Math.max(0, folded.position - 1);
|
|
1672
1812
|
const rewritesHead = folded.index - cached.persistedIndex >= STATE_CACHE.snapshotEvery;
|
|
1673
1813
|
if (rewritesHead && !writes.has(folded.index)) writes.set(folded.index, {
|
|
1674
1814
|
state: folded.state,
|
|
1675
|
-
|
|
1815
|
+
pins: /* @__PURE__ */ new Map()
|
|
1676
1816
|
});
|
|
1677
1817
|
const snapshots = writeSnapshots({
|
|
1678
1818
|
store,
|
|
@@ -1746,11 +1886,11 @@ function createServer(options) {
|
|
|
1746
1886
|
snapshot: snapshotOutcome,
|
|
1747
1887
|
visit: (entry, entryState, entryIndex, count) => {
|
|
1748
1888
|
if (entryIndex === 0) return;
|
|
1749
|
-
if (count === 0 && entry.
|
|
1889
|
+
if (count === 0 && entry.pin === void 0) return;
|
|
1750
1890
|
const advancesHead = stateRead.headIndex === null || entryIndex > stateRead.headIndex;
|
|
1751
1891
|
const repairsHead = snapshotOutcome === "rejected" && entryIndex === stateRead.headIndex;
|
|
1752
|
-
if (!advancesHead && !repairsHead && entry.
|
|
1753
|
-
addSnapshotWrite(writes, entryIndex, entryState, entry.
|
|
1892
|
+
if (!advancesHead && !repairsHead && entry.pin === void 0) return;
|
|
1893
|
+
addSnapshotWrite(writes, entryIndex, entryState, entry.pin);
|
|
1754
1894
|
}
|
|
1755
1895
|
});
|
|
1756
1896
|
const snapshots = writeSnapshots({
|
|
@@ -1832,6 +1972,8 @@ function createServer(options) {
|
|
|
1832
1972
|
};
|
|
1833
1973
|
const makeSchedule = (sessionId, trigger) => {
|
|
1834
1974
|
const triggerEventId = trigger?.id ?? null;
|
|
1975
|
+
/** Timers a consumer sets key on its name, except A2's own. */
|
|
1976
|
+
const triggerConsumer = trigger && !registeredConsumers.get(trigger.consumer)?.anonymousIds ? trigger.consumer : void 0;
|
|
1835
1977
|
const triggerCreatedAt = trigger?.createdAt.getTime();
|
|
1836
1978
|
const clearIndex = trigger?.clearIndex ?? 0;
|
|
1837
1979
|
const schedule = async (scheduleName, timing, ...events) => {
|
|
@@ -1849,7 +1991,7 @@ function createServer(options) {
|
|
|
1849
1991
|
id: event.id
|
|
1850
1992
|
});
|
|
1851
1993
|
const validated = validateEvents(sessionId, structuredClone(snapshottedEvents), false);
|
|
1852
|
-
const scheduleId = await deterministicScheduleId(name, sessionId, triggerEventId, scheduleName, clearIndex);
|
|
1994
|
+
const scheduleId = await deterministicScheduleId(name, sessionId, triggerEventId, scheduleName, clearIndex, triggerConsumer);
|
|
1853
1995
|
const scheduledEvents = await Promise.all(validated.map(async (event, item) => ({
|
|
1854
1996
|
id: event.id ?? await deterministicScheduledEventId(scheduleId, item),
|
|
1855
1997
|
type: event.type,
|
|
@@ -1886,7 +2028,8 @@ function createServer(options) {
|
|
|
1886
2028
|
triggerEventId,
|
|
1887
2029
|
clearIndex,
|
|
1888
2030
|
name: scheduleName,
|
|
1889
|
-
ifLastIndex
|
|
2031
|
+
ifLastIndex,
|
|
2032
|
+
...triggerConsumer === void 0 ? {} : { consumer: triggerConsumer }
|
|
1890
2033
|
});
|
|
1891
2034
|
try {
|
|
1892
2035
|
await scheduler.schedule({
|
|
@@ -2065,6 +2208,7 @@ function createServer(options) {
|
|
|
2065
2208
|
}
|
|
2066
2209
|
};
|
|
2067
2210
|
const painted = /* @__PURE__ */ new Map();
|
|
2211
|
+
const paintedDeletes = presenceTombstones();
|
|
2068
2212
|
const diffRows = (rows) => {
|
|
2069
2213
|
const patches = [];
|
|
2070
2214
|
const live = /* @__PURE__ */ new Map();
|
|
@@ -2103,7 +2247,7 @@ function createServer(options) {
|
|
|
2103
2247
|
participant,
|
|
2104
2248
|
values: { [field]: null },
|
|
2105
2249
|
seen: prior.seen,
|
|
2106
|
-
at:
|
|
2250
|
+
at: new Date(prior.atMs)
|
|
2107
2251
|
});
|
|
2108
2252
|
}
|
|
2109
2253
|
if (fields.size === 0) painted.delete(participant);
|
|
@@ -2113,10 +2257,17 @@ function createServer(options) {
|
|
|
2113
2257
|
const paint = (patch) => {
|
|
2114
2258
|
let fields = painted.get(patch.participant);
|
|
2115
2259
|
for (const [field, value] of Object.entries(patch.values)) {
|
|
2260
|
+
const write = {
|
|
2261
|
+
participant: patch.participant,
|
|
2262
|
+
field,
|
|
2263
|
+
at: patch.at
|
|
2264
|
+
};
|
|
2116
2265
|
if (value === null || value === void 0) {
|
|
2117
2266
|
fields?.delete(field);
|
|
2267
|
+
if (value === null) paintedDeletes.keep(write);
|
|
2118
2268
|
continue;
|
|
2119
2269
|
}
|
|
2270
|
+
paintedDeletes.drop(write);
|
|
2120
2271
|
if (!fields) {
|
|
2121
2272
|
fields = /* @__PURE__ */ new Map();
|
|
2122
2273
|
painted.set(patch.participant, fields);
|
|
@@ -2150,11 +2301,20 @@ function createServer(options) {
|
|
|
2150
2301
|
}
|
|
2151
2302
|
wakeQueue?.();
|
|
2152
2303
|
};
|
|
2304
|
+
const outdated = (write) => {
|
|
2305
|
+
const prior = painted.get(write.participant)?.get(write.field);
|
|
2306
|
+
return prior !== void 0 && prior.atMs > write.at.getTime() || paintedDeletes.outranks(write);
|
|
2307
|
+
};
|
|
2153
2308
|
const dequeue = () => {
|
|
2154
2309
|
for (const [participant, fields] of queue) {
|
|
2155
2310
|
for (const [field, stamp] of fields) {
|
|
2156
2311
|
fields.delete(field);
|
|
2157
2312
|
if (fields.size === 0) queue.delete(participant);
|
|
2313
|
+
if (outdated({
|
|
2314
|
+
participant,
|
|
2315
|
+
field,
|
|
2316
|
+
at: stamp.at
|
|
2317
|
+
})) continue;
|
|
2158
2318
|
return {
|
|
2159
2319
|
participant,
|
|
2160
2320
|
values: { [field]: stamp.value },
|
|
@@ -2332,6 +2492,7 @@ function createServer(options) {
|
|
|
2332
2492
|
}
|
|
2333
2493
|
return rows.flatMap((row) => row.failedAt === null ? [] : [{
|
|
2334
2494
|
...toPublic(row),
|
|
2495
|
+
consumer: row.consumer,
|
|
2335
2496
|
lane: row.lane,
|
|
2336
2497
|
attemptCount: row.attemptCount,
|
|
2337
2498
|
failureCount: row.failureCount,
|
|
@@ -2339,14 +2500,18 @@ function createServer(options) {
|
|
|
2339
2500
|
failedAt: row.failedAt
|
|
2340
2501
|
}]);
|
|
2341
2502
|
};
|
|
2342
|
-
const redriveEvent = async (sessionId,
|
|
2503
|
+
const redriveEvent = async (sessionId, target, redriveOptions) => {
|
|
2504
|
+
const index = target?.index;
|
|
2505
|
+
const redriven = target?.consumer;
|
|
2343
2506
|
assertRedriveIndex(index);
|
|
2507
|
+
if (typeof redriven !== "string" || redriven.length === 0) throw new TypeError("redrive requires a consumer name");
|
|
2344
2508
|
const action = redriveOptions?.action;
|
|
2345
2509
|
if (action !== "retry" && action !== "skip") throw new TypeError("redrive requires action 'retry' or 'skip'");
|
|
2346
2510
|
return telemetry.span("a2.redrive", {
|
|
2347
2511
|
"a2.contract": name,
|
|
2348
2512
|
"a2.session_id": sessionId,
|
|
2349
2513
|
"a2.redrive.index": index,
|
|
2514
|
+
"a2.redrive.consumer": redriven,
|
|
2350
2515
|
"a2.redrive.action": action
|
|
2351
2516
|
}, async (span) => {
|
|
2352
2517
|
const store = await redrivingStore("redrive");
|
|
@@ -2354,8 +2519,10 @@ function createServer(options) {
|
|
|
2354
2519
|
try {
|
|
2355
2520
|
result = await store.redrive({
|
|
2356
2521
|
sessionId: nsId(sessionId),
|
|
2522
|
+
consumer: redriven,
|
|
2357
2523
|
index,
|
|
2358
|
-
action
|
|
2524
|
+
action,
|
|
2525
|
+
consumers: storeConsumers
|
|
2359
2526
|
});
|
|
2360
2527
|
} catch (err) {
|
|
2361
2528
|
throw asStoreUnavailable(err);
|
|
@@ -2375,9 +2542,100 @@ function createServer(options) {
|
|
|
2375
2542
|
return { redriven: result.redriven };
|
|
2376
2543
|
});
|
|
2377
2544
|
};
|
|
2545
|
+
/**
|
|
2546
|
+
* `session.consume`: wakes the consumer's cursor, then drains only that
|
|
2547
|
+
* consumer until its head lies past `through`. A head another invocation
|
|
2548
|
+
* holds, or one waiting out a failure, is waited for; a dead-lettered head
|
|
2549
|
+
* throws.
|
|
2550
|
+
*/
|
|
2551
|
+
/** Per session, which consumer's running head is waiting in `consume` on which: a cycle never ends. */
|
|
2552
|
+
const consumeWaits = /* @__PURE__ */ new Map();
|
|
2553
|
+
const consumeSession = (sessionId, defined, consumeOptions = {}, trigger = null) => {
|
|
2554
|
+
assertSessionId(sessionId);
|
|
2555
|
+
if (defined?.contract?.name !== name || !defined.spec) throw new TypeError(`session.consume for '${name}' got a consumer of another contract`);
|
|
2556
|
+
const own = definitions.get(defined.name);
|
|
2557
|
+
if (own !== void 0 && own !== defined) throw new TypeError(`this server runs a different consumer named '${defined.name}'`);
|
|
2558
|
+
if (defined.name === trigger?.consumer) throw new TypeError(`consumer '${defined.name}' can't consume itself: its own head is running`);
|
|
2559
|
+
const registered = own ? registeredConsumers.get(defined.name) : registrationOf(defined);
|
|
2560
|
+
const log = registered.log;
|
|
2561
|
+
if (!log) throw new TypeError(`consumer '${defined.name}' needs a string lane to be consumed: only then does it have one position`);
|
|
2562
|
+
const { through: requested, signal } = consumeOptions;
|
|
2563
|
+
if (requested !== void 0) assertConsumeIndex(requested);
|
|
2564
|
+
const waiter = trigger?.consumer;
|
|
2565
|
+
const waits = waiter === void 0 ? void 0 : consumeWaits.get(sessionId) ?? consumeWaits.set(sessionId, /* @__PURE__ */ new Map()).get(sessionId);
|
|
2566
|
+
let cycle = false;
|
|
2567
|
+
if (waiter !== void 0 && waits) {
|
|
2568
|
+
for (let next = defined.name; next !== void 0 && !cycle; next = waits.get(next)) cycle = next === waiter;
|
|
2569
|
+
if (!cycle) waits.set(waiter, defined.name);
|
|
2570
|
+
}
|
|
2571
|
+
const storeConsumer = {
|
|
2572
|
+
name: registered.name,
|
|
2573
|
+
log
|
|
2574
|
+
};
|
|
2575
|
+
const consumed = telemetry.span("a2.consume", {
|
|
2576
|
+
"a2.contract": name,
|
|
2577
|
+
"a2.session_id": sessionId,
|
|
2578
|
+
"a2.consume.consumer": registered.name
|
|
2579
|
+
}, async (span) => {
|
|
2580
|
+
const store = await resolveStore();
|
|
2581
|
+
const ns = nsId(sessionId);
|
|
2582
|
+
let woken = await store.wakeLog({
|
|
2583
|
+
sessionId: ns,
|
|
2584
|
+
consumer: storeConsumer
|
|
2585
|
+
});
|
|
2586
|
+
const through = Math.min(requested ?? woken.head, woken.head);
|
|
2587
|
+
span.setAttribute("a2.consume.through", through);
|
|
2588
|
+
const done = () => woken.cursor.head === null || woken.cursor.head > through;
|
|
2589
|
+
if (cycle && !done()) throw new TypeError(`consumer '${waiter}' can't consume '${defined.name}' through ${through}: '${defined.name}' is waiting on it in session '${sessionId}'`);
|
|
2590
|
+
let drains = 0;
|
|
2591
|
+
let wait = CONSUME_WAIT_MIN_MS;
|
|
2592
|
+
while (!done()) {
|
|
2593
|
+
signal?.throwIfAborted();
|
|
2594
|
+
const { cursor } = woken;
|
|
2595
|
+
if (cursor.failedAt !== null) throw new A2Error("CONSUMER_DEAD_LETTERED", `consumer '${registered.name}' is dead-lettered at event ${cursor.head} in session '${sessionId}': ${cursor.lastError ?? "every attempt ended before it finished"}`);
|
|
2596
|
+
drains += 1;
|
|
2597
|
+
const drained = drainSession(sessionId, {
|
|
2598
|
+
only: registered,
|
|
2599
|
+
through
|
|
2600
|
+
});
|
|
2601
|
+
try {
|
|
2602
|
+
await untilAborted(drained, signal);
|
|
2603
|
+
} catch (error) {
|
|
2604
|
+
if (signal?.aborted) drained.then(() => scheduleDrain(sessionId), () => {});
|
|
2605
|
+
throw error;
|
|
2606
|
+
}
|
|
2607
|
+
const before = cursor;
|
|
2608
|
+
woken = await store.wakeLog({
|
|
2609
|
+
sessionId: ns,
|
|
2610
|
+
consumer: storeConsumer
|
|
2611
|
+
});
|
|
2612
|
+
const moved = woken.cursor.head !== before.head || woken.cursor.attemptCount !== before.attemptCount;
|
|
2613
|
+
if (done() || moved) {
|
|
2614
|
+
wait = CONSUME_WAIT_MIN_MS;
|
|
2615
|
+
continue;
|
|
2616
|
+
}
|
|
2617
|
+
const sleep = defaultSleep(wait);
|
|
2618
|
+
const abort = () => sleep.cancel();
|
|
2619
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
2620
|
+
await sleep.promise;
|
|
2621
|
+
signal?.removeEventListener("abort", abort);
|
|
2622
|
+
wait = Math.min(wait * 2, CONSUME_WAIT_MAX_MS);
|
|
2623
|
+
}
|
|
2624
|
+
if (drains > 0 || own && woken.cursor.head !== null) scheduleDrain(sessionId);
|
|
2625
|
+
const index = woken.cursor.head === null ? woken.head : woken.cursor.head - 1;
|
|
2626
|
+
span.setAttribute("a2.consume.index", index);
|
|
2627
|
+
span.setAttribute("a2.consume.drains", drains);
|
|
2628
|
+
return { index };
|
|
2629
|
+
});
|
|
2630
|
+
return waiter === void 0 || cycle ? consumed : consumed.finally(() => {
|
|
2631
|
+
waits?.delete(waiter);
|
|
2632
|
+
if (waits?.size === 0) consumeWaits.delete(sessionId);
|
|
2633
|
+
});
|
|
2634
|
+
};
|
|
2378
2635
|
const makeSession = (id, append, trigger, attempt) => ({
|
|
2379
2636
|
id,
|
|
2380
2637
|
append,
|
|
2638
|
+
consume: (defined, consumeOptions) => consumeSession(id, defined, consumeOptions, trigger),
|
|
2381
2639
|
schedule: makeSchedule(id, trigger),
|
|
2382
2640
|
clear: (clearOptions) => clearSession(id, clearOptions, attempt),
|
|
2383
2641
|
history: async (bounds) => (await readHistory(id, bounds)).filter((row) => !isSessionCleared(row)).map(toPublic),
|
|
@@ -2388,7 +2646,7 @@ function createServer(options) {
|
|
|
2388
2646
|
const bounds = {};
|
|
2389
2647
|
if (through !== void 0 && through !== "latest") {
|
|
2390
2648
|
bounds.throughIndex = through;
|
|
2391
|
-
if (trigger) bounds.
|
|
2649
|
+
if (trigger) bounds.pin = headRefOf(trigger);
|
|
2392
2650
|
if (through === trigger?.index && !attempt?.signal.aborted) bounds.throughEventId = trigger.id;
|
|
2393
2651
|
}
|
|
2394
2652
|
if (trigger) bounds.triggerIndex = trigger.index;
|
|
@@ -2404,32 +2662,35 @@ function createServer(options) {
|
|
|
2404
2662
|
presence: () => readPresenceMap(id)
|
|
2405
2663
|
} : {}
|
|
2406
2664
|
});
|
|
2665
|
+
/** The consumer name a trigger's child ids key on, if any. */
|
|
2666
|
+
const idConsumerOf = (trigger) => registeredConsumers.get(trigger.consumer)?.anonymousIds ? void 0 : trigger.consumer;
|
|
2407
2667
|
const makeCtx = (trigger, attempt, signal, endAttempt) => {
|
|
2408
2668
|
const publicSessionId = stripNs(trigger.sessionId);
|
|
2669
|
+
const idConsumer = idConsumerOf(trigger);
|
|
2670
|
+
const cause = {
|
|
2671
|
+
consumer: trigger.consumer,
|
|
2672
|
+
lane: trigger.lane,
|
|
2673
|
+
index: trigger.index,
|
|
2674
|
+
attempt
|
|
2675
|
+
};
|
|
2409
2676
|
const append = async (appendName, ...events) => {
|
|
2410
2677
|
assertAppendName(appendName);
|
|
2411
2678
|
const generatedIds = events.map((event) => event.id === void 0);
|
|
2412
2679
|
const withIds = await Promise.all(events.map(async (event, item) => {
|
|
2413
2680
|
if (event.id !== void 0) return event;
|
|
2414
|
-
const id = await deterministicEventId(trigger.id, appendName, item);
|
|
2681
|
+
const id = await deterministicEventId(trigger.id, appendName, item, idConsumer);
|
|
2415
2682
|
return {
|
|
2416
2683
|
...event,
|
|
2417
2684
|
id
|
|
2418
2685
|
};
|
|
2419
2686
|
}));
|
|
2420
|
-
return appendCore(publicSessionId, withIds, "
|
|
2421
|
-
index: trigger.index,
|
|
2422
|
-
attempt
|
|
2423
|
-
}, generatedIds);
|
|
2687
|
+
return appendCore(publicSessionId, withIds, "consumer", "inline", (_rows, watchdogDueAt) => scheduleDrain(publicSessionId, watchdogDueAt), cause, generatedIds);
|
|
2424
2688
|
};
|
|
2425
2689
|
const ctx = {
|
|
2426
2690
|
event: toPublic(trigger),
|
|
2427
2691
|
attempt,
|
|
2428
2692
|
session: makeSession(publicSessionId, append, trigger, {
|
|
2429
|
-
cause
|
|
2430
|
-
index: trigger.index,
|
|
2431
|
-
attempt
|
|
2432
|
-
},
|
|
2693
|
+
cause,
|
|
2433
2694
|
end: endAttempt,
|
|
2434
2695
|
signal
|
|
2435
2696
|
}),
|
|
@@ -2507,21 +2768,21 @@ function createServer(options) {
|
|
|
2507
2768
|
type: "completed",
|
|
2508
2769
|
value
|
|
2509
2770
|
}), (error) => ({
|
|
2510
|
-
type: "
|
|
2771
|
+
type: "run-failed",
|
|
2511
2772
|
error
|
|
2512
2773
|
})), hub.watch.then(() => hub.error === null ? { type: "monitor-closed" } : {
|
|
2513
2774
|
type: "monitor-failed",
|
|
2514
2775
|
error: hub.error
|
|
2515
2776
|
})]);
|
|
2516
2777
|
if (outcome.type === "completed") return outcome.value;
|
|
2517
|
-
if (outcome.type === "
|
|
2778
|
+
if (outcome.type === "run-failed") throw outcome.error;
|
|
2518
2779
|
if (outcome.type === "monitor-failed") {
|
|
2519
2780
|
for (const active of hub.subscriptions) active.controller.abort(outcome.error);
|
|
2520
2781
|
await operation.catch(() => {});
|
|
2521
2782
|
throw outcome.error;
|
|
2522
2783
|
}
|
|
2523
2784
|
await operation.catch(() => {});
|
|
2524
|
-
throw new Error("abort monitor closed while
|
|
2785
|
+
throw new Error("abort monitor closed while consumers were active");
|
|
2525
2786
|
},
|
|
2526
2787
|
close: async () => {
|
|
2527
2788
|
hub.subscriptions.delete(subscription);
|
|
@@ -2541,28 +2802,37 @@ function createServer(options) {
|
|
|
2541
2802
|
if (result === void 0) return [];
|
|
2542
2803
|
const events = Array.isArray(result) ? [...result] : [result];
|
|
2543
2804
|
if (events.length === 0) return [];
|
|
2805
|
+
const idConsumer = idConsumerOf(trigger);
|
|
2544
2806
|
const withIds = await Promise.all(events.map(async (event, item) => {
|
|
2545
2807
|
const withId = { ...event };
|
|
2546
|
-
withId.id = event.id ?? await deterministicReturnedEventId(trigger.id, item);
|
|
2808
|
+
withId.id = event.id ?? await deterministicReturnedEventId(trigger.id, item, idConsumer);
|
|
2547
2809
|
return withId;
|
|
2548
2810
|
}));
|
|
2549
2811
|
return validateEvents(sessionId, withIds).map((event) => Object.assign(event, {
|
|
2550
2812
|
id: event.id,
|
|
2551
2813
|
cause: {
|
|
2814
|
+
consumer: trigger.consumer,
|
|
2815
|
+
lane: trigger.lane,
|
|
2552
2816
|
index: trigger.index,
|
|
2553
2817
|
attempt: trigger.attemptCount
|
|
2554
2818
|
}
|
|
2555
2819
|
}));
|
|
2556
2820
|
};
|
|
2557
|
-
const drainSession = (sessionId,
|
|
2821
|
+
const drainSession = async (sessionId, drainOptions = {}) => {
|
|
2822
|
+
const result = await runDrain(sessionId, drainOptions);
|
|
2823
|
+
if (result.settled && drainOptions.only === void 0) drainSettledHooks.get(self)?.(sessionId);
|
|
2824
|
+
return result;
|
|
2825
|
+
};
|
|
2826
|
+
const runDrain = (sessionId, { watchdogDueAt, signal, handOffFailure = false, only, through } = {}) => telemetry.span("a2.drain", {
|
|
2558
2827
|
"a2.contract": name,
|
|
2559
2828
|
"a2.session_id": sessionId
|
|
2560
2829
|
}, async (span) => {
|
|
2561
2830
|
const store = await resolveStore();
|
|
2562
2831
|
const ns = nsId(sessionId);
|
|
2563
2832
|
const holder = crypto.randomUUID();
|
|
2833
|
+
/** Running heads, keyed by `headKey`: one event can run for several consumers. */
|
|
2564
2834
|
const active = /* @__PURE__ */ new Map();
|
|
2565
|
-
const excluded = /* @__PURE__ */ new
|
|
2835
|
+
const excluded = /* @__PURE__ */ new Map();
|
|
2566
2836
|
let processed = 0;
|
|
2567
2837
|
let sawFailure = false;
|
|
2568
2838
|
let schedulerArm;
|
|
@@ -2574,10 +2844,10 @@ function createServer(options) {
|
|
|
2574
2844
|
const lapseDue = () => {
|
|
2575
2845
|
lapseTimer = null;
|
|
2576
2846
|
const nowMs = Date.now();
|
|
2577
|
-
for (const
|
|
2847
|
+
for (const entry of active.values()) {
|
|
2578
2848
|
if (entry.lapsed || entry.expiresAtMs > nowMs) continue;
|
|
2579
2849
|
entry.lapsed = true;
|
|
2580
|
-
entry.supersede.abort(new A2Error("CLAIM_EXPIRED", `the claim on event ${index} in session '${sessionId}' lapsed without renewal`));
|
|
2850
|
+
entry.supersede.abort(new A2Error("CLAIM_EXPIRED", `the claim on event ${entry.ref.index} for consumer '${entry.ref.consumer}' in session '${sessionId}' lapsed without renewal`));
|
|
2581
2851
|
}
|
|
2582
2852
|
scheduleLapse();
|
|
2583
2853
|
};
|
|
@@ -2615,8 +2885,8 @@ function createServer(options) {
|
|
|
2615
2885
|
if (stopRenewing || renewal || active.size === 0) return renewal ?? Promise.resolve();
|
|
2616
2886
|
const window = nextClaimWindow();
|
|
2617
2887
|
requestWatchdog(window.watchdogAtMs);
|
|
2618
|
-
const claims = [...active.
|
|
2619
|
-
|
|
2888
|
+
const claims = [...active.values()].map((entry) => ({
|
|
2889
|
+
...entry.ref,
|
|
2620
2890
|
attempt: entry.attempt
|
|
2621
2891
|
}));
|
|
2622
2892
|
const operation = store.renewClaims({
|
|
@@ -2626,14 +2896,14 @@ function createServer(options) {
|
|
|
2626
2896
|
ttlMs: window.ttlMs,
|
|
2627
2897
|
...window.deadlineCapped ? { expiresAtMs: window.expiresAtMs } : {}
|
|
2628
2898
|
}).then((result) => {
|
|
2629
|
-
for (const
|
|
2630
|
-
const entry = active.get(
|
|
2899
|
+
for (const ref of result.renewed) {
|
|
2900
|
+
const entry = active.get(headKey(ref));
|
|
2631
2901
|
if (entry && !entry.lapsed) entry.expiresAtMs = window.expiresAtMs;
|
|
2632
2902
|
}
|
|
2633
|
-
for (const
|
|
2634
|
-
const entry = active.get(
|
|
2635
|
-
if (!entry) continue;
|
|
2636
|
-
endSuperseded(entry, `a newer attempt owns event ${index} in session '${sessionId}'`);
|
|
2903
|
+
for (const ref of result.superseded) {
|
|
2904
|
+
const entry = active.get(headKey(ref));
|
|
2905
|
+
if (!entry || entry.completing) continue;
|
|
2906
|
+
endSuperseded(entry, `a newer attempt owns event ${ref.index} for consumer '${ref.consumer}' in session '${sessionId}'`);
|
|
2637
2907
|
}
|
|
2638
2908
|
if (window.deadlineCapped) stopRenewing = true;
|
|
2639
2909
|
scheduleLapse();
|
|
@@ -2647,41 +2917,46 @@ function createServer(options) {
|
|
|
2647
2917
|
renewal = tracked;
|
|
2648
2918
|
return tracked;
|
|
2649
2919
|
};
|
|
2650
|
-
/** Records
|
|
2920
|
+
/** Records a head the store dead-lettered on takeover; it never runs. */
|
|
2651
2921
|
const reportExhausted = (event) => telemetry.span("a2.event", {
|
|
2652
2922
|
"a2.contract": name,
|
|
2653
2923
|
"a2.session_id": sessionId,
|
|
2654
2924
|
"a2.event.type": event.type,
|
|
2655
2925
|
"a2.event.index": event.index,
|
|
2656
2926
|
"a2.event.id": event.id,
|
|
2927
|
+
"a2.event.consumer": event.consumer,
|
|
2657
2928
|
"a2.event.attempt": event.attemptCount,
|
|
2658
|
-
"a2.event.handled":
|
|
2929
|
+
"a2.event.handled": registeredConsumers.has(event.consumer),
|
|
2659
2930
|
...event.lane === null ? {} : { "a2.event.lane": event.lane }
|
|
2660
2931
|
}, async (eventSpan) => {
|
|
2661
2932
|
eventSpan.setAttribute("a2.event.outcome", "dead_lettered");
|
|
2662
2933
|
});
|
|
2663
|
-
const runEvent = async (event, supersedeSignal, endAttempt) => {
|
|
2664
|
-
const registration =
|
|
2934
|
+
const runEvent = async (event, supersedeSignal, endAttempt, beginCompleting, sessionHead) => {
|
|
2935
|
+
const registration = only?.name === event.consumer ? only : registeredConsumers.get(event.consumer);
|
|
2936
|
+
const abortOn = registration?.abortOn(event.type) ?? null;
|
|
2665
2937
|
return telemetry.span("a2.event", {
|
|
2666
2938
|
"a2.contract": name,
|
|
2667
2939
|
"a2.session_id": sessionId,
|
|
2668
2940
|
"a2.event.type": event.type,
|
|
2669
2941
|
"a2.event.index": event.index,
|
|
2670
2942
|
"a2.event.id": event.id,
|
|
2943
|
+
"a2.event.consumer": event.consumer,
|
|
2671
2944
|
"a2.event.attempt": event.attemptCount,
|
|
2672
2945
|
"a2.event.handled": registration !== void 0,
|
|
2946
|
+
"a2.event.age_ms": Math.max(0, Date.now() - event.createdAt.getTime()),
|
|
2947
|
+
...sessionHead === void 0 ? {} : { "a2.event.lag": Math.max(0, sessionHead - event.index) },
|
|
2673
2948
|
...event.lane === null ? {} : { "a2.event.lane": event.lane }
|
|
2674
2949
|
}, async (eventSpan) => {
|
|
2675
2950
|
let subscription;
|
|
2676
|
-
let
|
|
2951
|
+
let runSignal = supersedeSignal;
|
|
2677
2952
|
let completion;
|
|
2678
2953
|
let completing = false;
|
|
2679
2954
|
try {
|
|
2680
|
-
if (
|
|
2681
|
-
subscription = await subscribeAbort(store, ns, event,
|
|
2682
|
-
|
|
2955
|
+
if (abortOn) {
|
|
2956
|
+
subscription = await subscribeAbort(store, ns, event, abortOn, event.attemptCount);
|
|
2957
|
+
runSignal = AbortSignal.any([supersedeSignal, subscription.signal]);
|
|
2683
2958
|
}
|
|
2684
|
-
const operation = registration?.
|
|
2959
|
+
const operation = registration?.run(makeCtx(event, event.attemptCount, runSignal, endAttempt));
|
|
2685
2960
|
const result = operation ? await (subscription ? subscription.monitor(operation) : operation) : void 0;
|
|
2686
2961
|
if (subscription) {
|
|
2687
2962
|
await subscription.close();
|
|
@@ -2690,6 +2965,7 @@ function createServer(options) {
|
|
|
2690
2965
|
const returned = await returnedEvents(sessionId, event, result);
|
|
2691
2966
|
await renewal;
|
|
2692
2967
|
completing = true;
|
|
2968
|
+
beginCompleting();
|
|
2693
2969
|
completion = await telemetry.span("a2.commit", {
|
|
2694
2970
|
"a2.contract": name,
|
|
2695
2971
|
"a2.session_id": sessionId,
|
|
@@ -2699,9 +2975,12 @@ function createServer(options) {
|
|
|
2699
2975
|
}, async (commitSpan) => {
|
|
2700
2976
|
const committed = await store.completeAttempt({
|
|
2701
2977
|
sessionId: ns,
|
|
2978
|
+
consumer: event.consumer,
|
|
2979
|
+
lane: event.lane,
|
|
2702
2980
|
index: event.index,
|
|
2703
2981
|
attempt: event.attemptCount,
|
|
2704
|
-
events: returned
|
|
2982
|
+
events: returned,
|
|
2983
|
+
consumers: storeConsumers
|
|
2705
2984
|
});
|
|
2706
2985
|
commitSpan.setAttribute("a2.commit.outcome", committed.outcome);
|
|
2707
2986
|
return committed;
|
|
@@ -2727,6 +3006,8 @@ function createServer(options) {
|
|
|
2727
3006
|
}, async (commitSpan) => {
|
|
2728
3007
|
const result = await store.failAttempt({
|
|
2729
3008
|
sessionId: ns,
|
|
3009
|
+
consumer: event.consumer,
|
|
3010
|
+
lane: event.lane,
|
|
2730
3011
|
index: event.index,
|
|
2731
3012
|
attempt: event.attemptCount,
|
|
2732
3013
|
error: describeError(err),
|
|
@@ -2751,7 +3032,7 @@ function createServer(options) {
|
|
|
2751
3032
|
opensLane: false,
|
|
2752
3033
|
appendsEvents: false
|
|
2753
3034
|
};
|
|
2754
|
-
if (
|
|
3035
|
+
if (runSignal.aborted) eventSpan.setAttribute("a2.event.aborted", true);
|
|
2755
3036
|
return {
|
|
2756
3037
|
status: "processed",
|
|
2757
3038
|
opensLane: event.lane !== null,
|
|
@@ -2760,9 +3041,11 @@ function createServer(options) {
|
|
|
2760
3041
|
});
|
|
2761
3042
|
};
|
|
2762
3043
|
let claimAgain = false;
|
|
2763
|
-
const start = (event, expiresAtMs) => {
|
|
2764
|
-
const
|
|
2765
|
-
|
|
3044
|
+
const start = (event, expiresAtMs, sessionHead) => {
|
|
3045
|
+
const ref = headRefOf(event);
|
|
3046
|
+
const key = headKey(ref);
|
|
3047
|
+
const previous = active.get(key);
|
|
3048
|
+
if (previous) endSuperseded(previous, `a newer attempt owns event ${event.index} for consumer '${event.consumer}' in session '${sessionId}'`);
|
|
2766
3049
|
const supersede = new AbortController();
|
|
2767
3050
|
let fence;
|
|
2768
3051
|
const fenced = new Promise((resolve) => {
|
|
@@ -2771,27 +3054,31 @@ function createServer(options) {
|
|
|
2771
3054
|
const endAttempt = () => endSuperseded({
|
|
2772
3055
|
supersede,
|
|
2773
3056
|
fence
|
|
2774
|
-
}, `event ${event.index} in session '${sessionId}' was cleared`);
|
|
3057
|
+
}, `event ${event.index} for consumer '${event.consumer}' in session '${sessionId}' was cleared`);
|
|
2775
3058
|
let entry;
|
|
2776
|
-
const execution = Promise.race([runEvent(event, supersede.signal, endAttempt
|
|
2777
|
-
|
|
3059
|
+
const execution = Promise.race([runEvent(event, supersede.signal, endAttempt, () => {
|
|
3060
|
+
entry.completing = true;
|
|
3061
|
+
}, sessionHead), fenced]).then((eventOutcome) => {
|
|
3062
|
+
if (active.get(key) === entry) active.delete(key);
|
|
2778
3063
|
if (eventOutcome.status === "processed") processed += 1;
|
|
2779
3064
|
if (eventOutcome.status === "failed") {
|
|
2780
|
-
excluded.
|
|
3065
|
+
excluded.set(key, ref);
|
|
2781
3066
|
sawFailure = true;
|
|
2782
3067
|
}
|
|
2783
3068
|
claimAgain ||= eventOutcome.opensLane || eventOutcome.appendsEvents;
|
|
2784
3069
|
return eventOutcome;
|
|
2785
3070
|
});
|
|
2786
3071
|
entry = {
|
|
3072
|
+
ref,
|
|
2787
3073
|
attempt: event.attemptCount,
|
|
2788
3074
|
execution,
|
|
2789
3075
|
supersede,
|
|
2790
3076
|
fence,
|
|
2791
3077
|
expiresAtMs,
|
|
2792
|
-
lapsed: false
|
|
3078
|
+
lapsed: false,
|
|
3079
|
+
completing: false
|
|
2793
3080
|
};
|
|
2794
|
-
active.set(
|
|
3081
|
+
active.set(key, entry);
|
|
2795
3082
|
scheduleLapse();
|
|
2796
3083
|
if (!heartbeat && !stopRenewing) {
|
|
2797
3084
|
heartbeat = setInterval(() => void renew(), DRAIN_TIMINGS.claimHeartbeatMs);
|
|
@@ -2820,7 +3107,9 @@ function createServer(options) {
|
|
|
2820
3107
|
ttlMs: window.ttlMs,
|
|
2821
3108
|
maxFailures: MAX_FAILURES,
|
|
2822
3109
|
...window.deadlineCapped ? { expiresAtMs: window.expiresAtMs } : {},
|
|
2823
|
-
|
|
3110
|
+
consumers: only ? [storeConsumerOf(only)] : storeConsumers,
|
|
3111
|
+
...through === void 0 ? {} : { through },
|
|
3112
|
+
...excluded.size === 0 ? {} : { exclude: [...excluded.values()] }
|
|
2824
3113
|
});
|
|
2825
3114
|
claimSpan.setAttribute("a2.claim.outcome", result.outcome);
|
|
2826
3115
|
claimSpan.setAttribute("a2.claim.count", result.outcome === "claimed" ? result.events.length : 0);
|
|
@@ -2831,7 +3120,7 @@ function createServer(options) {
|
|
|
2831
3120
|
for (const event of claim.events) if (event.failedAt !== null) await reportExhausted(event);
|
|
2832
3121
|
if (runnable.length > 0) requestWatchdog(window.watchdogAtMs);
|
|
2833
3122
|
if (window.deadlineCapped) stopRenewing = true;
|
|
2834
|
-
for (const event of runnable) start(event, window.expiresAtMs);
|
|
3123
|
+
for (const event of runnable) start(event, window.expiresAtMs, claim.head);
|
|
2835
3124
|
shouldClaim = false;
|
|
2836
3125
|
}
|
|
2837
3126
|
}
|
|
@@ -2860,6 +3149,10 @@ function createServer(options) {
|
|
|
2860
3149
|
}
|
|
2861
3150
|
if (claim === void 0) continue;
|
|
2862
3151
|
if (signal && signal.version !== signalVersion) continue;
|
|
3152
|
+
if (claimAgain) {
|
|
3153
|
+
claimAgain = false;
|
|
3154
|
+
continue;
|
|
3155
|
+
}
|
|
2863
3156
|
if (claim.outcome === "busy") {
|
|
2864
3157
|
requestWatchdog(claim.retryAt.getTime() + DRAIN_TIMINGS.watchdogGraceMs);
|
|
2865
3158
|
outcome = "busy";
|
|
@@ -2899,7 +3192,7 @@ function createServer(options) {
|
|
|
2899
3192
|
const append = Object.assign(appendInline, { dispatch: appendDispatch });
|
|
2900
3193
|
return Object.assign(makeSession(id, append, null), {
|
|
2901
3194
|
deadLetters: () => listDeadLetters(id),
|
|
2902
|
-
redrive: (
|
|
3195
|
+
redrive: (target, redriveOptions) => redriveEvent(id, target, redriveOptions)
|
|
2903
3196
|
});
|
|
2904
3197
|
};
|
|
2905
3198
|
const historyPage = async (sessionId, bounds) => {
|
|
@@ -2968,6 +3261,9 @@ function assertStreamIndex(value) {
|
|
|
2968
3261
|
function assertStateIndex(value) {
|
|
2969
3262
|
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("state.through must be a non-negative safe integer");
|
|
2970
3263
|
}
|
|
3264
|
+
function assertConsumeIndex(value) {
|
|
3265
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("consume through must be a non-negative safe integer");
|
|
3266
|
+
}
|
|
2971
3267
|
function assertRedriveIndex(value) {
|
|
2972
3268
|
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("redrive requires a positive safe-integer index");
|
|
2973
3269
|
}
|
|
@@ -2975,7 +3271,7 @@ function assertClearIndex(value) {
|
|
|
2975
3271
|
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("ifLastIndex must be a non-negative safe integer");
|
|
2976
3272
|
}
|
|
2977
3273
|
function assertAppendName(name) {
|
|
2978
|
-
if (typeof name !== "string" || name.length === 0) throw new TypeError("
|
|
3274
|
+
if (typeof name !== "string" || name.length === 0) throw new TypeError("consumer append name must be a non-empty string");
|
|
2979
3275
|
}
|
|
2980
3276
|
function assertScheduleName(name) {
|
|
2981
3277
|
if (typeof name !== "string" || name.length === 0) throw new TypeError("schedule name must be a non-empty string");
|
|
@@ -3061,6 +3357,6 @@ const measuredBytes = (reducer, state, index, snapshots) => JSON.stringify(snaps
|
|
|
3061
3357
|
}))?.length ?? 0;
|
|
3062
3358
|
const stateCacheKey = (sessionId, reducerName) => `${sessionId}\u0000${reducerName}`;
|
|
3063
3359
|
//#endregion
|
|
3064
|
-
export { CONSOLE_TELEMETRY as a, parsePresenceSibling as c,
|
|
3360
|
+
export { CONSOLE_TELEMETRY as a, parsePresenceSibling as c, deliverSchedulerClear as i, createServer as n, setServerFetchHooks as o, deliverSchedulerAppend as r, sseResponse as s, consumer as t };
|
|
3065
3361
|
|
|
3066
|
-
//# sourceMappingURL=server-
|
|
3362
|
+
//# sourceMappingURL=server-DtFrWxi3.js.map
|