experimental-a2 0.19.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 +50 -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.d.ts.map +1 -1
- package/dist/ai-fx.js +36 -9
- 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-Bfk-Lycn.d.ts.map +1 -0
- package/dist/{ai-harness-BuuC-gJ3.js → ai-harness-dQeJaR1t.js} +17 -4
- package/dist/ai-harness-dQeJaR1t.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.js +4 -4
- 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 +84 -75
- 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.d.ts.map +1 -1
- package/dist/scheduler-qstash.js +7 -7
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-task-X1SToDjP.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +5 -4
- 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-D-Y4pqwu.js → server-DvIB8kj5.js} +423 -135
- 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 +1595 -673
- 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 +28 -19
- package/docs/02-examples.mdx +1 -1
- package/docs/actors/01-introduction.mdx +9 -1
- package/docs/actors/02-concurrency.mdx +1 -1
- package/docs/actors/meta.ts +1 -1
- package/docs/agents/01-introduction.mdx +1 -1
- package/docs/agents/02-tools.mdx +13 -12
- package/docs/agents/03-conversations.mdx +31 -21
- package/docs/agents/04-harnesses.mdx +7 -3
- package/docs/agents/meta.ts +1 -1
- package/docs/client/03-presence.mdx +18 -14
- package/docs/client/meta.ts +1 -1
- 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 +31 -26
- package/docs/reference/01-core.mdx +12 -12
- package/docs/reference/02-server.mdx +247 -192
- package/docs/reference/04-ai.mdx +29 -20
- package/docs/reference/05-telemetry.mdx +146 -0
- package/docs/reference/06-errors.mdx +24 -18
- package/docs/reference/meta.ts +1 -1
- package/docs/schedulers/01-overview.mdx +112 -0
- package/docs/schedulers/02-vercel-queues.mdx +87 -0
- package/docs/schedulers/03-qstash.mdx +123 -0
- package/docs/schedulers/meta.ts +1 -0
- package/docs/server/01-consumers.mdx +415 -0
- package/docs/server/02-timers.mdx +33 -23
- 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 +60 -50
- package/docs/stores/01-overview.mdx +37 -0
- package/docs/stores/02-redis.mdx +57 -0
- package/docs/stores/03-redis-http.mdx +56 -0
- package/docs/stores/04-postgres.mdx +172 -0
- package/docs/stores/05-sqlite.mdx +36 -0
- package/docs/stores/06-memory.mdx +22 -0
- package/docs/stores/meta.ts +1 -0
- 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 +48 -11
- package/examples/playground/app/a0/apps/[appId]/route.ts +6 -10
- package/examples/playground/app/a0/chat.test.tsx +421 -330
- package/examples/playground/app/a0/sandbox-log.ts +17 -15
- package/examples/playground/app/a0/server.ts +4 -4
- package/examples/playground/app/a0/shell.tsx +1 -1
- package/examples/playground/app/a0/uploads/route.test.ts +59 -0
- 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/[agentId]/agent-queue.test.tsx +116 -90
- package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +107 -76
- package/examples/playground/app/agent/ingress.test.ts +41 -17
- package/examples/playground/app/agent/model-input.test.tsx +92 -43
- package/examples/playground/app/agent/server.ts +3 -3
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.test.tsx +2 -2
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
- package/examples/playground/app/chat/server.test.ts +38 -65
- package/examples/playground/app/chat/server.ts +45 -35
- package/examples/playground/app/components/activity-history.test.tsx +60 -18
- package/examples/playground/app/components/demo-icons.tsx +7 -0
- package/examples/playground/app/components/demos.tsx +7 -0
- package/examples/playground/app/components/fast-link.test.tsx +3 -5
- package/examples/playground/app/initial-state.test.tsx +86 -45
- 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 +13 -1
- package/examples/playground/scripts/migrate.mjs +31 -0
- package/examples/playground/test/agent.ts +35 -0
- package/examples/playground/test/model.ts +149 -0
- package/examples/playground/test/serve.ts +184 -0
- package/examples/playground/test/server-only.ts +1 -0
- package/examples/playground/vercel.json +9 -0
- package/examples/playground/vitest.config.ts +27 -0
- package/examples/playground/vitest.drills.config.ts +32 -0
- package/package.json +7 -2
- package/sql/postgres/drizzle-schema.ts +206 -0
- package/sql/postgres/notifications.sql +24 -0
- package/sql/postgres/schema.sql +276 -0
- package/src/actor.ts +101 -71
- package/src/ai-agent.ts +154 -115
- package/src/ai-control-server.ts +8 -8
- package/src/ai-fx.ts +41 -9
- package/src/ai-harness.ts +28 -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 +9 -6
- package/src/idempotent-replay.ts +0 -1
- package/src/index.ts +1 -1
- package/src/internal.ts +17 -6
- package/src/otel.ts +1 -1
- package/src/postgres-notifications.ts +0 -22
- package/src/postgres-resources.ts +6 -9
- package/src/postgres-schema.ts +121 -0
- package/src/push-envelope.ts +20 -0
- package/src/runtime-consumer.ts +10 -0
- package/src/scheduler-qstash.ts +12 -5
- package/src/scheduler-task.ts +2 -2
- package/src/scheduler-vercel.ts +10 -4
- package/src/server.browser.ts +1 -1
- package/src/server.ts +839 -238
- 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 -943
- 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-BuuC-gJ3.js.map +0 -1
- package/dist/ai-harness-CV6rXbB3.d.ts.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-D-Y4pqwu.js.map +0 -1
- package/dist/server-DYAATFuT.d.ts.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/reference/05-adapters.mdx +0 -351
- package/docs/server/01-handlers.mdx +0 -200
- package/docs/server/05-production.mdx +0 -323
|
@@ -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 { _ as nullProtoRecord, c as STATE_CACHE, g as markSchedulerSendFailure, i as POLL_TIMINGS, l as STREAM_TIMINGS, m as failedPreviousAttempt, o as RESERVED_PARTICIPANT_IDS, s as SOCKET_TIMINGS, t as DRAIN_TIMINGS, v as
|
|
4
|
+
import { _ as nullProtoRecord, b as serverInternals, c as STATE_CACHE, g as markSchedulerSendFailure, i as POLL_TIMINGS, l as STREAM_TIMINGS, m as failedPreviousAttempt, o as RESERVED_PARTICIPANT_IDS, s as SOCKET_TIMINGS, t as DRAIN_TIMINGS, v as runtimeConsumerFactory, x as stateTailReaders, y as runtimeConsumers } from "./internal-Bc4PvfHk.js";
|
|
5
5
|
import { t as boundedCache } from "./bounded-cache-CPHWF-8h.js";
|
|
6
|
-
import { i as streamActivities, r as createStreamActivity, t as defaultSleep } from "./store-polling-
|
|
6
|
+
import { i as streamActivities, r as createStreamActivity, t as defaultSleep } from "./store-polling-DYNyxCev.js";
|
|
7
7
|
import { i as takeBuffered, n as isSerializedEvent, r as parseSerializedEvent, t as bufferedEvents } from "./stream-buffer-Cx7AAPGq.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-CZGqNZRx.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,7 +988,7 @@ 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
|
|
@@ -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);
|
|
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
|
+
} };
|
|
1106
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);
|
|
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
|
+
}
|
|
1320
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);
|
|
@@ -1386,7 +1528,10 @@ function createServer(options) {
|
|
|
1386
1528
|
const signal = createDrainSignal();
|
|
1387
1529
|
const p = new Promise((resolve) => {
|
|
1388
1530
|
queueMicrotask(() => {
|
|
1389
|
-
drainSession(sessionId,
|
|
1531
|
+
drainSession(sessionId, {
|
|
1532
|
+
watchdogDueAt,
|
|
1533
|
+
signal
|
|
1534
|
+
}).then(() => resolve()).catch(() => resolve());
|
|
1390
1535
|
});
|
|
1391
1536
|
}).finally(() => {
|
|
1392
1537
|
if (scheduledDrains.get(sessionId)?.promise === p) scheduledDrains.delete(sessionId);
|
|
@@ -1411,20 +1556,15 @@ function createServer(options) {
|
|
|
1411
1556
|
};
|
|
1412
1557
|
if (e.id !== void 0) validated.id = e.id;
|
|
1413
1558
|
if (!resolveDispatchMetadata) return validated;
|
|
1414
|
-
const
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
payload: structuredClone(result.value),
|
|
1422
|
-
...e.id === void 0 ? {} : { id: e.id }
|
|
1423
|
-
}
|
|
1424
|
-
}) : registration.lane;
|
|
1425
|
-
if (typeof lane !== "string" || lane.length === 0) throw new TypeError(`lane for '${e.type}' must resolve to a non-empty string`);
|
|
1426
|
-
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
|
+
});
|
|
1427
1566
|
}
|
|
1567
|
+
if (targets.length > 0) validated.targets = targets;
|
|
1428
1568
|
return validated;
|
|
1429
1569
|
});
|
|
1430
1570
|
};
|
|
@@ -1452,9 +1592,12 @@ function createServer(options) {
|
|
|
1452
1592
|
let rows;
|
|
1453
1593
|
let shouldHealSession;
|
|
1454
1594
|
try {
|
|
1455
|
-
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
|
+
});
|
|
1456
1599
|
rows = result.events;
|
|
1457
|
-
shouldHealSession = result.hasPending && !(source === "
|
|
1600
|
+
shouldHealSession = result.hasPending && !(source === "consumer" && !validated.some(startsWork));
|
|
1458
1601
|
} catch (err) {
|
|
1459
1602
|
throw asStoreUnavailable(err);
|
|
1460
1603
|
}
|
|
@@ -1563,7 +1706,7 @@ function createServer(options) {
|
|
|
1563
1706
|
state: write.state
|
|
1564
1707
|
})
|
|
1565
1708
|
};
|
|
1566
|
-
if (write.
|
|
1709
|
+
if (write.pins.size > 0) snapshot.pins = [...write.pins.values()];
|
|
1567
1710
|
return snapshot;
|
|
1568
1711
|
});
|
|
1569
1712
|
if (snapshots.length > 0) persistSnapshots({
|
|
@@ -1632,7 +1775,7 @@ function createServer(options) {
|
|
|
1632
1775
|
/**
|
|
1633
1776
|
* Serves a plan from the cached state when every read is at or past the
|
|
1634
1777
|
* cached index. A read bounded at the cached index by an event whose id
|
|
1635
|
-
* matches the cached one (a
|
|
1778
|
+
* matches the cached one (a consumer's own trigger) needs no store request;
|
|
1636
1779
|
* any other read fetches the tail from the cached event onward and checks
|
|
1637
1780
|
* that event's id. Returns false when the store must answer instead.
|
|
1638
1781
|
*/
|
|
@@ -1662,14 +1805,14 @@ function createServer(options) {
|
|
|
1662
1805
|
clearIndex: cached.clearIndex,
|
|
1663
1806
|
snapshot: "memory",
|
|
1664
1807
|
visit: (entry, state, index) => {
|
|
1665
|
-
if (entry.
|
|
1808
|
+
if (entry.pin !== void 0) addSnapshotWrite(writes, index, state, entry.pin);
|
|
1666
1809
|
}
|
|
1667
1810
|
});
|
|
1668
1811
|
const added = Math.max(0, folded.position - 1);
|
|
1669
1812
|
const rewritesHead = folded.index - cached.persistedIndex >= STATE_CACHE.snapshotEvery;
|
|
1670
1813
|
if (rewritesHead && !writes.has(folded.index)) writes.set(folded.index, {
|
|
1671
1814
|
state: folded.state,
|
|
1672
|
-
|
|
1815
|
+
pins: /* @__PURE__ */ new Map()
|
|
1673
1816
|
});
|
|
1674
1817
|
const snapshots = writeSnapshots({
|
|
1675
1818
|
store,
|
|
@@ -1743,11 +1886,11 @@ function createServer(options) {
|
|
|
1743
1886
|
snapshot: snapshotOutcome,
|
|
1744
1887
|
visit: (entry, entryState, entryIndex, count) => {
|
|
1745
1888
|
if (entryIndex === 0) return;
|
|
1746
|
-
if (count === 0 && entry.
|
|
1889
|
+
if (count === 0 && entry.pin === void 0) return;
|
|
1747
1890
|
const advancesHead = stateRead.headIndex === null || entryIndex > stateRead.headIndex;
|
|
1748
1891
|
const repairsHead = snapshotOutcome === "rejected" && entryIndex === stateRead.headIndex;
|
|
1749
|
-
if (!advancesHead && !repairsHead && entry.
|
|
1750
|
-
addSnapshotWrite(writes, entryIndex, entryState, entry.
|
|
1892
|
+
if (!advancesHead && !repairsHead && entry.pin === void 0) return;
|
|
1893
|
+
addSnapshotWrite(writes, entryIndex, entryState, entry.pin);
|
|
1751
1894
|
}
|
|
1752
1895
|
});
|
|
1753
1896
|
const snapshots = writeSnapshots({
|
|
@@ -1829,6 +1972,8 @@ function createServer(options) {
|
|
|
1829
1972
|
};
|
|
1830
1973
|
const makeSchedule = (sessionId, trigger) => {
|
|
1831
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;
|
|
1832
1977
|
const triggerCreatedAt = trigger?.createdAt.getTime();
|
|
1833
1978
|
const clearIndex = trigger?.clearIndex ?? 0;
|
|
1834
1979
|
const schedule = async (scheduleName, timing, ...events) => {
|
|
@@ -1846,7 +1991,7 @@ function createServer(options) {
|
|
|
1846
1991
|
id: event.id
|
|
1847
1992
|
});
|
|
1848
1993
|
const validated = validateEvents(sessionId, structuredClone(snapshottedEvents), false);
|
|
1849
|
-
const scheduleId = await deterministicScheduleId(name, sessionId, triggerEventId, scheduleName, clearIndex);
|
|
1994
|
+
const scheduleId = await deterministicScheduleId(name, sessionId, triggerEventId, scheduleName, clearIndex, triggerConsumer);
|
|
1850
1995
|
const scheduledEvents = await Promise.all(validated.map(async (event, item) => ({
|
|
1851
1996
|
id: event.id ?? await deterministicScheduledEventId(scheduleId, item),
|
|
1852
1997
|
type: event.type,
|
|
@@ -1883,7 +2028,8 @@ function createServer(options) {
|
|
|
1883
2028
|
triggerEventId,
|
|
1884
2029
|
clearIndex,
|
|
1885
2030
|
name: scheduleName,
|
|
1886
|
-
ifLastIndex
|
|
2031
|
+
ifLastIndex,
|
|
2032
|
+
...triggerConsumer === void 0 ? {} : { consumer: triggerConsumer }
|
|
1887
2033
|
});
|
|
1888
2034
|
try {
|
|
1889
2035
|
await scheduler.schedule({
|
|
@@ -2329,6 +2475,7 @@ function createServer(options) {
|
|
|
2329
2475
|
}
|
|
2330
2476
|
return rows.flatMap((row) => row.failedAt === null ? [] : [{
|
|
2331
2477
|
...toPublic(row),
|
|
2478
|
+
consumer: row.consumer,
|
|
2332
2479
|
lane: row.lane,
|
|
2333
2480
|
attemptCount: row.attemptCount,
|
|
2334
2481
|
failureCount: row.failureCount,
|
|
@@ -2336,14 +2483,18 @@ function createServer(options) {
|
|
|
2336
2483
|
failedAt: row.failedAt
|
|
2337
2484
|
}]);
|
|
2338
2485
|
};
|
|
2339
|
-
const redriveEvent = async (sessionId,
|
|
2486
|
+
const redriveEvent = async (sessionId, target, redriveOptions) => {
|
|
2487
|
+
const index = target?.index;
|
|
2488
|
+
const redriven = target?.consumer;
|
|
2340
2489
|
assertRedriveIndex(index);
|
|
2490
|
+
if (typeof redriven !== "string" || redriven.length === 0) throw new TypeError("redrive requires a consumer name");
|
|
2341
2491
|
const action = redriveOptions?.action;
|
|
2342
2492
|
if (action !== "retry" && action !== "skip") throw new TypeError("redrive requires action 'retry' or 'skip'");
|
|
2343
2493
|
return telemetry.span("a2.redrive", {
|
|
2344
2494
|
"a2.contract": name,
|
|
2345
2495
|
"a2.session_id": sessionId,
|
|
2346
2496
|
"a2.redrive.index": index,
|
|
2497
|
+
"a2.redrive.consumer": redriven,
|
|
2347
2498
|
"a2.redrive.action": action
|
|
2348
2499
|
}, async (span) => {
|
|
2349
2500
|
const store = await redrivingStore("redrive");
|
|
@@ -2351,8 +2502,10 @@ function createServer(options) {
|
|
|
2351
2502
|
try {
|
|
2352
2503
|
result = await store.redrive({
|
|
2353
2504
|
sessionId: nsId(sessionId),
|
|
2505
|
+
consumer: redriven,
|
|
2354
2506
|
index,
|
|
2355
|
-
action
|
|
2507
|
+
action,
|
|
2508
|
+
consumers: storeConsumers
|
|
2356
2509
|
});
|
|
2357
2510
|
} catch (err) {
|
|
2358
2511
|
throw asStoreUnavailable(err);
|
|
@@ -2372,9 +2525,100 @@ function createServer(options) {
|
|
|
2372
2525
|
return { redriven: result.redriven };
|
|
2373
2526
|
});
|
|
2374
2527
|
};
|
|
2528
|
+
/**
|
|
2529
|
+
* `session.consume`: wakes the consumer's cursor, then drains only that
|
|
2530
|
+
* consumer until its head lies past `through`. A head another invocation
|
|
2531
|
+
* holds, or one waiting out a failure, is waited for; a dead-lettered head
|
|
2532
|
+
* throws.
|
|
2533
|
+
*/
|
|
2534
|
+
/** Per session, which consumer's running head is waiting in `consume` on which: a cycle never ends. */
|
|
2535
|
+
const consumeWaits = /* @__PURE__ */ new Map();
|
|
2536
|
+
const consumeSession = (sessionId, defined, consumeOptions = {}, trigger = null) => {
|
|
2537
|
+
assertSessionId(sessionId);
|
|
2538
|
+
if (defined?.contract?.name !== name || !defined.spec) throw new TypeError(`session.consume for '${name}' got a consumer of another contract`);
|
|
2539
|
+
const own = definitions.get(defined.name);
|
|
2540
|
+
if (own !== void 0 && own !== defined) throw new TypeError(`this server runs a different consumer named '${defined.name}'`);
|
|
2541
|
+
if (defined.name === trigger?.consumer) throw new TypeError(`consumer '${defined.name}' can't consume itself: its own head is running`);
|
|
2542
|
+
const registered = own ? registeredConsumers.get(defined.name) : registrationOf(defined);
|
|
2543
|
+
const log = registered.log;
|
|
2544
|
+
if (!log) throw new TypeError(`consumer '${defined.name}' needs a string lane to be consumed: only then does it have one position`);
|
|
2545
|
+
const { through: requested, signal } = consumeOptions;
|
|
2546
|
+
if (requested !== void 0) assertConsumeIndex(requested);
|
|
2547
|
+
const waiter = trigger?.consumer;
|
|
2548
|
+
const waits = waiter === void 0 ? void 0 : consumeWaits.get(sessionId) ?? consumeWaits.set(sessionId, /* @__PURE__ */ new Map()).get(sessionId);
|
|
2549
|
+
let cycle = false;
|
|
2550
|
+
if (waiter !== void 0 && waits) {
|
|
2551
|
+
for (let next = defined.name; next !== void 0 && !cycle; next = waits.get(next)) cycle = next === waiter;
|
|
2552
|
+
if (!cycle) waits.set(waiter, defined.name);
|
|
2553
|
+
}
|
|
2554
|
+
const storeConsumer = {
|
|
2555
|
+
name: registered.name,
|
|
2556
|
+
log
|
|
2557
|
+
};
|
|
2558
|
+
const consumed = telemetry.span("a2.consume", {
|
|
2559
|
+
"a2.contract": name,
|
|
2560
|
+
"a2.session_id": sessionId,
|
|
2561
|
+
"a2.consume.consumer": registered.name
|
|
2562
|
+
}, async (span) => {
|
|
2563
|
+
const store = await resolveStore();
|
|
2564
|
+
const ns = nsId(sessionId);
|
|
2565
|
+
let woken = await store.wakeLog({
|
|
2566
|
+
sessionId: ns,
|
|
2567
|
+
consumer: storeConsumer
|
|
2568
|
+
});
|
|
2569
|
+
const through = Math.min(requested ?? woken.head, woken.head);
|
|
2570
|
+
span.setAttribute("a2.consume.through", through);
|
|
2571
|
+
const done = () => woken.cursor.head === null || woken.cursor.head > through;
|
|
2572
|
+
if (cycle && !done()) throw new TypeError(`consumer '${waiter}' can't consume '${defined.name}' through ${through}: '${defined.name}' is waiting on it in session '${sessionId}'`);
|
|
2573
|
+
let drains = 0;
|
|
2574
|
+
let wait = CONSUME_WAIT_MIN_MS;
|
|
2575
|
+
while (!done()) {
|
|
2576
|
+
signal?.throwIfAborted();
|
|
2577
|
+
const { cursor } = woken;
|
|
2578
|
+
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"}`);
|
|
2579
|
+
drains += 1;
|
|
2580
|
+
const drained = drainSession(sessionId, {
|
|
2581
|
+
only: registered,
|
|
2582
|
+
through
|
|
2583
|
+
});
|
|
2584
|
+
try {
|
|
2585
|
+
await untilAborted(drained, signal);
|
|
2586
|
+
} catch (error) {
|
|
2587
|
+
if (signal?.aborted) drained.then(() => scheduleDrain(sessionId), () => {});
|
|
2588
|
+
throw error;
|
|
2589
|
+
}
|
|
2590
|
+
const before = cursor;
|
|
2591
|
+
woken = await store.wakeLog({
|
|
2592
|
+
sessionId: ns,
|
|
2593
|
+
consumer: storeConsumer
|
|
2594
|
+
});
|
|
2595
|
+
const moved = woken.cursor.head !== before.head || woken.cursor.attemptCount !== before.attemptCount;
|
|
2596
|
+
if (done() || moved) {
|
|
2597
|
+
wait = CONSUME_WAIT_MIN_MS;
|
|
2598
|
+
continue;
|
|
2599
|
+
}
|
|
2600
|
+
const sleep = defaultSleep(wait);
|
|
2601
|
+
const abort = () => sleep.cancel();
|
|
2602
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
2603
|
+
await sleep.promise;
|
|
2604
|
+
signal?.removeEventListener("abort", abort);
|
|
2605
|
+
wait = Math.min(wait * 2, CONSUME_WAIT_MAX_MS);
|
|
2606
|
+
}
|
|
2607
|
+
if (drains > 0 || own && woken.cursor.head !== null) scheduleDrain(sessionId);
|
|
2608
|
+
const index = woken.cursor.head === null ? woken.head : woken.cursor.head - 1;
|
|
2609
|
+
span.setAttribute("a2.consume.index", index);
|
|
2610
|
+
span.setAttribute("a2.consume.drains", drains);
|
|
2611
|
+
return { index };
|
|
2612
|
+
});
|
|
2613
|
+
return waiter === void 0 || cycle ? consumed : consumed.finally(() => {
|
|
2614
|
+
waits?.delete(waiter);
|
|
2615
|
+
if (waits?.size === 0) consumeWaits.delete(sessionId);
|
|
2616
|
+
});
|
|
2617
|
+
};
|
|
2375
2618
|
const makeSession = (id, append, trigger, attempt) => ({
|
|
2376
2619
|
id,
|
|
2377
2620
|
append,
|
|
2621
|
+
consume: (defined, consumeOptions) => consumeSession(id, defined, consumeOptions, trigger),
|
|
2378
2622
|
schedule: makeSchedule(id, trigger),
|
|
2379
2623
|
clear: (clearOptions) => clearSession(id, clearOptions, attempt),
|
|
2380
2624
|
history: async (bounds) => (await readHistory(id, bounds)).filter((row) => !isSessionCleared(row)).map(toPublic),
|
|
@@ -2385,7 +2629,7 @@ function createServer(options) {
|
|
|
2385
2629
|
const bounds = {};
|
|
2386
2630
|
if (through !== void 0 && through !== "latest") {
|
|
2387
2631
|
bounds.throughIndex = through;
|
|
2388
|
-
if (trigger) bounds.
|
|
2632
|
+
if (trigger) bounds.pin = headRefOf(trigger);
|
|
2389
2633
|
if (through === trigger?.index && !attempt?.signal.aborted) bounds.throughEventId = trigger.id;
|
|
2390
2634
|
}
|
|
2391
2635
|
if (trigger) bounds.triggerIndex = trigger.index;
|
|
@@ -2401,32 +2645,35 @@ function createServer(options) {
|
|
|
2401
2645
|
presence: () => readPresenceMap(id)
|
|
2402
2646
|
} : {}
|
|
2403
2647
|
});
|
|
2648
|
+
/** The consumer name a trigger's child ids key on, if any. */
|
|
2649
|
+
const idConsumerOf = (trigger) => registeredConsumers.get(trigger.consumer)?.anonymousIds ? void 0 : trigger.consumer;
|
|
2404
2650
|
const makeCtx = (trigger, attempt, signal, endAttempt) => {
|
|
2405
2651
|
const publicSessionId = stripNs(trigger.sessionId);
|
|
2652
|
+
const idConsumer = idConsumerOf(trigger);
|
|
2653
|
+
const cause = {
|
|
2654
|
+
consumer: trigger.consumer,
|
|
2655
|
+
lane: trigger.lane,
|
|
2656
|
+
index: trigger.index,
|
|
2657
|
+
attempt
|
|
2658
|
+
};
|
|
2406
2659
|
const append = async (appendName, ...events) => {
|
|
2407
2660
|
assertAppendName(appendName);
|
|
2408
2661
|
const generatedIds = events.map((event) => event.id === void 0);
|
|
2409
2662
|
const withIds = await Promise.all(events.map(async (event, item) => {
|
|
2410
2663
|
if (event.id !== void 0) return event;
|
|
2411
|
-
const id = await deterministicEventId(trigger.id, appendName, item);
|
|
2664
|
+
const id = await deterministicEventId(trigger.id, appendName, item, idConsumer);
|
|
2412
2665
|
return {
|
|
2413
2666
|
...event,
|
|
2414
2667
|
id
|
|
2415
2668
|
};
|
|
2416
2669
|
}));
|
|
2417
|
-
return appendCore(publicSessionId, withIds, "
|
|
2418
|
-
index: trigger.index,
|
|
2419
|
-
attempt
|
|
2420
|
-
}, generatedIds);
|
|
2670
|
+
return appendCore(publicSessionId, withIds, "consumer", "inline", (_rows, watchdogDueAt) => scheduleDrain(publicSessionId, watchdogDueAt), cause, generatedIds);
|
|
2421
2671
|
};
|
|
2422
2672
|
const ctx = {
|
|
2423
2673
|
event: toPublic(trigger),
|
|
2424
2674
|
attempt,
|
|
2425
2675
|
session: makeSession(publicSessionId, append, trigger, {
|
|
2426
|
-
cause
|
|
2427
|
-
index: trigger.index,
|
|
2428
|
-
attempt
|
|
2429
|
-
},
|
|
2676
|
+
cause,
|
|
2430
2677
|
end: endAttempt,
|
|
2431
2678
|
signal
|
|
2432
2679
|
}),
|
|
@@ -2504,21 +2751,21 @@ function createServer(options) {
|
|
|
2504
2751
|
type: "completed",
|
|
2505
2752
|
value
|
|
2506
2753
|
}), (error) => ({
|
|
2507
|
-
type: "
|
|
2754
|
+
type: "run-failed",
|
|
2508
2755
|
error
|
|
2509
2756
|
})), hub.watch.then(() => hub.error === null ? { type: "monitor-closed" } : {
|
|
2510
2757
|
type: "monitor-failed",
|
|
2511
2758
|
error: hub.error
|
|
2512
2759
|
})]);
|
|
2513
2760
|
if (outcome.type === "completed") return outcome.value;
|
|
2514
|
-
if (outcome.type === "
|
|
2761
|
+
if (outcome.type === "run-failed") throw outcome.error;
|
|
2515
2762
|
if (outcome.type === "monitor-failed") {
|
|
2516
2763
|
for (const active of hub.subscriptions) active.controller.abort(outcome.error);
|
|
2517
2764
|
await operation.catch(() => {});
|
|
2518
2765
|
throw outcome.error;
|
|
2519
2766
|
}
|
|
2520
2767
|
await operation.catch(() => {});
|
|
2521
|
-
throw new Error("abort monitor closed while
|
|
2768
|
+
throw new Error("abort monitor closed while consumers were active");
|
|
2522
2769
|
},
|
|
2523
2770
|
close: async () => {
|
|
2524
2771
|
hub.subscriptions.delete(subscription);
|
|
@@ -2538,28 +2785,32 @@ function createServer(options) {
|
|
|
2538
2785
|
if (result === void 0) return [];
|
|
2539
2786
|
const events = Array.isArray(result) ? [...result] : [result];
|
|
2540
2787
|
if (events.length === 0) return [];
|
|
2788
|
+
const idConsumer = idConsumerOf(trigger);
|
|
2541
2789
|
const withIds = await Promise.all(events.map(async (event, item) => {
|
|
2542
2790
|
const withId = { ...event };
|
|
2543
|
-
withId.id = event.id ?? await deterministicReturnedEventId(trigger.id, item);
|
|
2791
|
+
withId.id = event.id ?? await deterministicReturnedEventId(trigger.id, item, idConsumer);
|
|
2544
2792
|
return withId;
|
|
2545
2793
|
}));
|
|
2546
2794
|
return validateEvents(sessionId, withIds).map((event) => Object.assign(event, {
|
|
2547
2795
|
id: event.id,
|
|
2548
2796
|
cause: {
|
|
2797
|
+
consumer: trigger.consumer,
|
|
2798
|
+
lane: trigger.lane,
|
|
2549
2799
|
index: trigger.index,
|
|
2550
2800
|
attempt: trigger.attemptCount
|
|
2551
2801
|
}
|
|
2552
2802
|
}));
|
|
2553
2803
|
};
|
|
2554
|
-
const drainSession = (sessionId, watchdogDueAt, signal) => telemetry.span("a2.drain", {
|
|
2804
|
+
const drainSession = (sessionId, { watchdogDueAt, signal, handOffFailure = false, only, through } = {}) => telemetry.span("a2.drain", {
|
|
2555
2805
|
"a2.contract": name,
|
|
2556
2806
|
"a2.session_id": sessionId
|
|
2557
2807
|
}, async (span) => {
|
|
2558
2808
|
const store = await resolveStore();
|
|
2559
2809
|
const ns = nsId(sessionId);
|
|
2560
2810
|
const holder = crypto.randomUUID();
|
|
2811
|
+
/** Running heads, keyed by `headKey`: one event can run for several consumers. */
|
|
2561
2812
|
const active = /* @__PURE__ */ new Map();
|
|
2562
|
-
const excluded = /* @__PURE__ */ new
|
|
2813
|
+
const excluded = /* @__PURE__ */ new Map();
|
|
2563
2814
|
let processed = 0;
|
|
2564
2815
|
let sawFailure = false;
|
|
2565
2816
|
let schedulerArm;
|
|
@@ -2571,10 +2822,10 @@ function createServer(options) {
|
|
|
2571
2822
|
const lapseDue = () => {
|
|
2572
2823
|
lapseTimer = null;
|
|
2573
2824
|
const nowMs = Date.now();
|
|
2574
|
-
for (const
|
|
2825
|
+
for (const entry of active.values()) {
|
|
2575
2826
|
if (entry.lapsed || entry.expiresAtMs > nowMs) continue;
|
|
2576
2827
|
entry.lapsed = true;
|
|
2577
|
-
entry.supersede.abort(new A2Error("CLAIM_EXPIRED", `the claim on event ${index} in session '${sessionId}' lapsed without renewal`));
|
|
2828
|
+
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`));
|
|
2578
2829
|
}
|
|
2579
2830
|
scheduleLapse();
|
|
2580
2831
|
};
|
|
@@ -2612,8 +2863,8 @@ function createServer(options) {
|
|
|
2612
2863
|
if (stopRenewing || renewal || active.size === 0) return renewal ?? Promise.resolve();
|
|
2613
2864
|
const window = nextClaimWindow();
|
|
2614
2865
|
requestWatchdog(window.watchdogAtMs);
|
|
2615
|
-
const claims = [...active.
|
|
2616
|
-
|
|
2866
|
+
const claims = [...active.values()].map((entry) => ({
|
|
2867
|
+
...entry.ref,
|
|
2617
2868
|
attempt: entry.attempt
|
|
2618
2869
|
}));
|
|
2619
2870
|
const operation = store.renewClaims({
|
|
@@ -2623,14 +2874,14 @@ function createServer(options) {
|
|
|
2623
2874
|
ttlMs: window.ttlMs,
|
|
2624
2875
|
...window.deadlineCapped ? { expiresAtMs: window.expiresAtMs } : {}
|
|
2625
2876
|
}).then((result) => {
|
|
2626
|
-
for (const
|
|
2627
|
-
const entry = active.get(
|
|
2877
|
+
for (const ref of result.renewed) {
|
|
2878
|
+
const entry = active.get(headKey(ref));
|
|
2628
2879
|
if (entry && !entry.lapsed) entry.expiresAtMs = window.expiresAtMs;
|
|
2629
2880
|
}
|
|
2630
|
-
for (const
|
|
2631
|
-
const entry = active.get(
|
|
2632
|
-
if (!entry) continue;
|
|
2633
|
-
endSuperseded(entry, `a newer attempt owns event ${index} in session '${sessionId}'`);
|
|
2881
|
+
for (const ref of result.superseded) {
|
|
2882
|
+
const entry = active.get(headKey(ref));
|
|
2883
|
+
if (!entry || entry.completing) continue;
|
|
2884
|
+
endSuperseded(entry, `a newer attempt owns event ${ref.index} for consumer '${ref.consumer}' in session '${sessionId}'`);
|
|
2634
2885
|
}
|
|
2635
2886
|
if (window.deadlineCapped) stopRenewing = true;
|
|
2636
2887
|
scheduleLapse();
|
|
@@ -2644,46 +2895,74 @@ function createServer(options) {
|
|
|
2644
2895
|
renewal = tracked;
|
|
2645
2896
|
return tracked;
|
|
2646
2897
|
};
|
|
2647
|
-
/** Records
|
|
2898
|
+
/** Records a head the store dead-lettered on takeover; it never runs. */
|
|
2648
2899
|
const reportExhausted = (event) => telemetry.span("a2.event", {
|
|
2649
2900
|
"a2.contract": name,
|
|
2650
2901
|
"a2.session_id": sessionId,
|
|
2651
2902
|
"a2.event.type": event.type,
|
|
2652
2903
|
"a2.event.index": event.index,
|
|
2653
2904
|
"a2.event.id": event.id,
|
|
2905
|
+
"a2.event.consumer": event.consumer,
|
|
2654
2906
|
"a2.event.attempt": event.attemptCount,
|
|
2655
|
-
"a2.event.handled":
|
|
2907
|
+
"a2.event.handled": registeredConsumers.has(event.consumer),
|
|
2656
2908
|
...event.lane === null ? {} : { "a2.event.lane": event.lane }
|
|
2657
2909
|
}, async (eventSpan) => {
|
|
2658
2910
|
eventSpan.setAttribute("a2.event.outcome", "dead_lettered");
|
|
2659
2911
|
});
|
|
2660
|
-
const runEvent = async (event, supersedeSignal, endAttempt) => {
|
|
2661
|
-
const registration =
|
|
2912
|
+
const runEvent = async (event, supersedeSignal, endAttempt, beginCompleting, sessionHead) => {
|
|
2913
|
+
const registration = only?.name === event.consumer ? only : registeredConsumers.get(event.consumer);
|
|
2914
|
+
const abortOn = registration?.abortOn(event.type) ?? null;
|
|
2662
2915
|
return telemetry.span("a2.event", {
|
|
2663
2916
|
"a2.contract": name,
|
|
2664
2917
|
"a2.session_id": sessionId,
|
|
2665
2918
|
"a2.event.type": event.type,
|
|
2666
2919
|
"a2.event.index": event.index,
|
|
2667
2920
|
"a2.event.id": event.id,
|
|
2921
|
+
"a2.event.consumer": event.consumer,
|
|
2668
2922
|
"a2.event.attempt": event.attemptCount,
|
|
2669
2923
|
"a2.event.handled": registration !== void 0,
|
|
2924
|
+
"a2.event.age_ms": Math.max(0, Date.now() - event.createdAt.getTime()),
|
|
2925
|
+
...sessionHead === void 0 ? {} : { "a2.event.lag": Math.max(0, sessionHead - event.index) },
|
|
2670
2926
|
...event.lane === null ? {} : { "a2.event.lane": event.lane }
|
|
2671
2927
|
}, async (eventSpan) => {
|
|
2672
2928
|
let subscription;
|
|
2673
|
-
let
|
|
2674
|
-
let
|
|
2929
|
+
let runSignal = supersedeSignal;
|
|
2930
|
+
let completion;
|
|
2931
|
+
let completing = false;
|
|
2675
2932
|
try {
|
|
2676
|
-
if (
|
|
2677
|
-
subscription = await subscribeAbort(store, ns, event,
|
|
2678
|
-
|
|
2933
|
+
if (abortOn) {
|
|
2934
|
+
subscription = await subscribeAbort(store, ns, event, abortOn, event.attemptCount);
|
|
2935
|
+
runSignal = AbortSignal.any([supersedeSignal, subscription.signal]);
|
|
2679
2936
|
}
|
|
2680
|
-
const operation = registration?.
|
|
2937
|
+
const operation = registration?.run(makeCtx(event, event.attemptCount, runSignal, endAttempt));
|
|
2681
2938
|
const result = operation ? await (subscription ? subscription.monitor(operation) : operation) : void 0;
|
|
2682
2939
|
if (subscription) {
|
|
2683
2940
|
await subscription.close();
|
|
2684
2941
|
subscription = void 0;
|
|
2685
2942
|
}
|
|
2686
|
-
returned = await returnedEvents(sessionId, event, result);
|
|
2943
|
+
const returned = await returnedEvents(sessionId, event, result);
|
|
2944
|
+
await renewal;
|
|
2945
|
+
completing = true;
|
|
2946
|
+
beginCompleting();
|
|
2947
|
+
completion = await telemetry.span("a2.commit", {
|
|
2948
|
+
"a2.contract": name,
|
|
2949
|
+
"a2.session_id": sessionId,
|
|
2950
|
+
"a2.commit.operation": "complete",
|
|
2951
|
+
"a2.commit.index": event.index,
|
|
2952
|
+
"a2.commit.attempt": event.attemptCount
|
|
2953
|
+
}, async (commitSpan) => {
|
|
2954
|
+
const committed = await store.completeAttempt({
|
|
2955
|
+
sessionId: ns,
|
|
2956
|
+
consumer: event.consumer,
|
|
2957
|
+
lane: event.lane,
|
|
2958
|
+
index: event.index,
|
|
2959
|
+
attempt: event.attemptCount,
|
|
2960
|
+
events: returned,
|
|
2961
|
+
consumers: storeConsumers
|
|
2962
|
+
});
|
|
2963
|
+
commitSpan.setAttribute("a2.commit.outcome", committed.outcome);
|
|
2964
|
+
return committed;
|
|
2965
|
+
});
|
|
2687
2966
|
} catch (err) {
|
|
2688
2967
|
if (supersedeSignal.aborted && err === supersedeSignal.reason) {
|
|
2689
2968
|
eventSpan.setAttribute("a2.event.outcome", "surrendered");
|
|
@@ -2693,6 +2972,8 @@ function createServer(options) {
|
|
|
2693
2972
|
appendsEvents: false
|
|
2694
2973
|
};
|
|
2695
2974
|
}
|
|
2975
|
+
const refused = completing && err instanceof A2Error && err.code === "PARTIAL_DUPLICATE_BATCH";
|
|
2976
|
+
if (completing && !refused) throw err;
|
|
2696
2977
|
eventSpan.recordError(err);
|
|
2697
2978
|
const failure = await telemetry.span("a2.commit", {
|
|
2698
2979
|
"a2.contract": name,
|
|
@@ -2703,10 +2984,12 @@ function createServer(options) {
|
|
|
2703
2984
|
}, async (commitSpan) => {
|
|
2704
2985
|
const result = await store.failAttempt({
|
|
2705
2986
|
sessionId: ns,
|
|
2987
|
+
consumer: event.consumer,
|
|
2988
|
+
lane: event.lane,
|
|
2706
2989
|
index: event.index,
|
|
2707
2990
|
attempt: event.attemptCount,
|
|
2708
2991
|
error: describeError(err),
|
|
2709
|
-
maxFailures: err instanceof NonRetriableError ? 1 : MAX_FAILURES,
|
|
2992
|
+
maxFailures: err instanceof NonRetriableError || refused ? 1 : MAX_FAILURES,
|
|
2710
2993
|
retryAfterMs: failureRetryDelayMs(event.failureCount + 1)
|
|
2711
2994
|
});
|
|
2712
2995
|
commitSpan.setAttribute("a2.commit.outcome", result.outcome);
|
|
@@ -2721,41 +3004,26 @@ function createServer(options) {
|
|
|
2721
3004
|
} finally {
|
|
2722
3005
|
await subscription?.close();
|
|
2723
3006
|
}
|
|
2724
|
-
await renewal;
|
|
2725
|
-
const completion = await telemetry.span("a2.commit", {
|
|
2726
|
-
"a2.contract": name,
|
|
2727
|
-
"a2.session_id": sessionId,
|
|
2728
|
-
"a2.commit.operation": "complete",
|
|
2729
|
-
"a2.commit.index": event.index,
|
|
2730
|
-
"a2.commit.attempt": event.attemptCount
|
|
2731
|
-
}, async (commitSpan) => {
|
|
2732
|
-
const result = await store.completeAttempt({
|
|
2733
|
-
sessionId: ns,
|
|
2734
|
-
index: event.index,
|
|
2735
|
-
attempt: event.attemptCount,
|
|
2736
|
-
events: returned
|
|
2737
|
-
});
|
|
2738
|
-
commitSpan.setAttribute("a2.commit.outcome", result.outcome);
|
|
2739
|
-
return result;
|
|
2740
|
-
});
|
|
2741
3007
|
eventSpan.setAttribute("a2.event.outcome", completion.outcome === "completed" ? "processed" : completion.outcome);
|
|
2742
3008
|
if (completion.outcome === "superseded") return {
|
|
2743
3009
|
status: "superseded",
|
|
2744
3010
|
opensLane: false,
|
|
2745
3011
|
appendsEvents: false
|
|
2746
3012
|
};
|
|
2747
|
-
if (
|
|
3013
|
+
if (runSignal.aborted) eventSpan.setAttribute("a2.event.aborted", true);
|
|
2748
3014
|
return {
|
|
2749
3015
|
status: "processed",
|
|
2750
3016
|
opensLane: event.lane !== null,
|
|
2751
|
-
appendsEvents:
|
|
3017
|
+
appendsEvents: completion.events.length > 0
|
|
2752
3018
|
};
|
|
2753
3019
|
});
|
|
2754
3020
|
};
|
|
2755
3021
|
let claimAgain = false;
|
|
2756
|
-
const start = (event, expiresAtMs) => {
|
|
2757
|
-
const
|
|
2758
|
-
|
|
3022
|
+
const start = (event, expiresAtMs, sessionHead) => {
|
|
3023
|
+
const ref = headRefOf(event);
|
|
3024
|
+
const key = headKey(ref);
|
|
3025
|
+
const previous = active.get(key);
|
|
3026
|
+
if (previous) endSuperseded(previous, `a newer attempt owns event ${event.index} for consumer '${event.consumer}' in session '${sessionId}'`);
|
|
2759
3027
|
const supersede = new AbortController();
|
|
2760
3028
|
let fence;
|
|
2761
3029
|
const fenced = new Promise((resolve) => {
|
|
@@ -2764,27 +3032,31 @@ function createServer(options) {
|
|
|
2764
3032
|
const endAttempt = () => endSuperseded({
|
|
2765
3033
|
supersede,
|
|
2766
3034
|
fence
|
|
2767
|
-
}, `event ${event.index} in session '${sessionId}' was cleared`);
|
|
3035
|
+
}, `event ${event.index} for consumer '${event.consumer}' in session '${sessionId}' was cleared`);
|
|
2768
3036
|
let entry;
|
|
2769
|
-
const execution = Promise.race([runEvent(event, supersede.signal, endAttempt
|
|
2770
|
-
|
|
3037
|
+
const execution = Promise.race([runEvent(event, supersede.signal, endAttempt, () => {
|
|
3038
|
+
entry.completing = true;
|
|
3039
|
+
}, sessionHead), fenced]).then((eventOutcome) => {
|
|
3040
|
+
if (active.get(key) === entry) active.delete(key);
|
|
2771
3041
|
if (eventOutcome.status === "processed") processed += 1;
|
|
2772
3042
|
if (eventOutcome.status === "failed") {
|
|
2773
|
-
excluded.
|
|
3043
|
+
excluded.set(key, ref);
|
|
2774
3044
|
sawFailure = true;
|
|
2775
3045
|
}
|
|
2776
3046
|
claimAgain ||= eventOutcome.opensLane || eventOutcome.appendsEvents;
|
|
2777
3047
|
return eventOutcome;
|
|
2778
3048
|
});
|
|
2779
3049
|
entry = {
|
|
3050
|
+
ref,
|
|
2780
3051
|
attempt: event.attemptCount,
|
|
2781
3052
|
execution,
|
|
2782
3053
|
supersede,
|
|
2783
3054
|
fence,
|
|
2784
3055
|
expiresAtMs,
|
|
2785
|
-
lapsed: false
|
|
3056
|
+
lapsed: false,
|
|
3057
|
+
completing: false
|
|
2786
3058
|
};
|
|
2787
|
-
active.set(
|
|
3059
|
+
active.set(key, entry);
|
|
2788
3060
|
scheduleLapse();
|
|
2789
3061
|
if (!heartbeat && !stopRenewing) {
|
|
2790
3062
|
heartbeat = setInterval(() => void renew(), DRAIN_TIMINGS.claimHeartbeatMs);
|
|
@@ -2813,7 +3085,9 @@ function createServer(options) {
|
|
|
2813
3085
|
ttlMs: window.ttlMs,
|
|
2814
3086
|
maxFailures: MAX_FAILURES,
|
|
2815
3087
|
...window.deadlineCapped ? { expiresAtMs: window.expiresAtMs } : {},
|
|
2816
|
-
|
|
3088
|
+
consumers: only ? [storeConsumerOf(only)] : storeConsumers,
|
|
3089
|
+
...through === void 0 ? {} : { through },
|
|
3090
|
+
...excluded.size === 0 ? {} : { exclude: [...excluded.values()] }
|
|
2817
3091
|
});
|
|
2818
3092
|
claimSpan.setAttribute("a2.claim.outcome", result.outcome);
|
|
2819
3093
|
claimSpan.setAttribute("a2.claim.count", result.outcome === "claimed" ? result.events.length : 0);
|
|
@@ -2824,7 +3098,7 @@ function createServer(options) {
|
|
|
2824
3098
|
for (const event of claim.events) if (event.failedAt !== null) await reportExhausted(event);
|
|
2825
3099
|
if (runnable.length > 0) requestWatchdog(window.watchdogAtMs);
|
|
2826
3100
|
if (window.deadlineCapped) stopRenewing = true;
|
|
2827
|
-
for (const event of runnable) start(event, window.expiresAtMs);
|
|
3101
|
+
for (const event of runnable) start(event, window.expiresAtMs, claim.head);
|
|
2828
3102
|
shouldClaim = false;
|
|
2829
3103
|
}
|
|
2830
3104
|
}
|
|
@@ -2853,6 +3127,10 @@ function createServer(options) {
|
|
|
2853
3127
|
}
|
|
2854
3128
|
if (claim === void 0) continue;
|
|
2855
3129
|
if (signal && signal.version !== signalVersion) continue;
|
|
3130
|
+
if (claimAgain) {
|
|
3131
|
+
claimAgain = false;
|
|
3132
|
+
continue;
|
|
3133
|
+
}
|
|
2856
3134
|
if (claim.outcome === "busy") {
|
|
2857
3135
|
requestWatchdog(claim.retryAt.getTime() + DRAIN_TIMINGS.watchdogGraceMs);
|
|
2858
3136
|
outcome = "busy";
|
|
@@ -2860,6 +3138,10 @@ function createServer(options) {
|
|
|
2860
3138
|
if (signal) signal.closed = true;
|
|
2861
3139
|
break;
|
|
2862
3140
|
}
|
|
3141
|
+
} catch (error) {
|
|
3142
|
+
if (!handOffFailure || schedulerArm === void 0) throw error;
|
|
3143
|
+
span.recordError(error);
|
|
3144
|
+
outcome = "failed";
|
|
2863
3145
|
} finally {
|
|
2864
3146
|
if (signal) signal.closed = true;
|
|
2865
3147
|
stopRenewing = true;
|
|
@@ -2888,7 +3170,7 @@ function createServer(options) {
|
|
|
2888
3170
|
const append = Object.assign(appendInline, { dispatch: appendDispatch });
|
|
2889
3171
|
return Object.assign(makeSession(id, append, null), {
|
|
2890
3172
|
deadLetters: () => listDeadLetters(id),
|
|
2891
|
-
redrive: (
|
|
3173
|
+
redrive: (target, redriveOptions) => redriveEvent(id, target, redriveOptions)
|
|
2892
3174
|
});
|
|
2893
3175
|
};
|
|
2894
3176
|
const historyPage = async (sessionId, bounds) => {
|
|
@@ -2920,7 +3202,10 @@ function createServer(options) {
|
|
|
2920
3202
|
},
|
|
2921
3203
|
schedulerDrain: (sessionId, opts) => {
|
|
2922
3204
|
assertSessionId(sessionId);
|
|
2923
|
-
return drainSession(sessionId,
|
|
3205
|
+
return drainSession(sessionId, {
|
|
3206
|
+
watchdogDueAt: opts?.watchdogDueAt,
|
|
3207
|
+
handOffFailure: true
|
|
3208
|
+
});
|
|
2924
3209
|
},
|
|
2925
3210
|
schedulerAppend: async (sessionId, events, originIndex) => {
|
|
2926
3211
|
if (!scheduler) throw new TypeError("scheduled append delivery requires a configured scheduler adapter");
|
|
@@ -2954,6 +3239,9 @@ function assertStreamIndex(value) {
|
|
|
2954
3239
|
function assertStateIndex(value) {
|
|
2955
3240
|
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("state.through must be a non-negative safe integer");
|
|
2956
3241
|
}
|
|
3242
|
+
function assertConsumeIndex(value) {
|
|
3243
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("consume through must be a non-negative safe integer");
|
|
3244
|
+
}
|
|
2957
3245
|
function assertRedriveIndex(value) {
|
|
2958
3246
|
if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("redrive requires a positive safe-integer index");
|
|
2959
3247
|
}
|
|
@@ -2961,7 +3249,7 @@ function assertClearIndex(value) {
|
|
|
2961
3249
|
if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("ifLastIndex must be a non-negative safe integer");
|
|
2962
3250
|
}
|
|
2963
3251
|
function assertAppendName(name) {
|
|
2964
|
-
if (typeof name !== "string" || name.length === 0) throw new TypeError("
|
|
3252
|
+
if (typeof name !== "string" || name.length === 0) throw new TypeError("consumer append name must be a non-empty string");
|
|
2965
3253
|
}
|
|
2966
3254
|
function assertScheduleName(name) {
|
|
2967
3255
|
if (typeof name !== "string" || name.length === 0) throw new TypeError("schedule name must be a non-empty string");
|
|
@@ -3047,6 +3335,6 @@ const measuredBytes = (reducer, state, index, snapshots) => JSON.stringify(snaps
|
|
|
3047
3335
|
}))?.length ?? 0;
|
|
3048
3336
|
const stateCacheKey = (sessionId, reducerName) => `${sessionId}\u0000${reducerName}`;
|
|
3049
3337
|
//#endregion
|
|
3050
|
-
export { CONSOLE_TELEMETRY as a, parsePresenceSibling as c,
|
|
3338
|
+
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 };
|
|
3051
3339
|
|
|
3052
|
-
//# sourceMappingURL=server-
|
|
3340
|
+
//# sourceMappingURL=server-DvIB8kj5.js.map
|