experimental-a2 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +55 -0
- package/README.md +1 -1
- package/dist/{actor-C7b-1arn.d.ts → actor-CqibLgjQ.d.ts} +15 -7
- package/dist/actor-CqibLgjQ.d.ts.map +1 -0
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-client.js +2 -2
- package/dist/actor-react.d.ts +3 -3
- package/dist/actor-react.js +5 -5
- package/dist/{actor-shared-t7w8xIKP.js → actor-shared-DFET5q16.js} +3 -3
- package/dist/actor-shared-DFET5q16.js.map +1 -0
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +54 -43
- package/dist/actor.js.map +1 -1
- package/dist/{ai-Cz3hqVhk.d.ts → ai-DAu77CDr.d.ts} +8 -8
- package/dist/{ai-Cz3hqVhk.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
- package/dist/{ai-mMiWul1T.js → ai-EEfKyc-z.js} +9 -9
- package/dist/ai-EEfKyc-z.js.map +1 -0
- package/dist/ai-client.d.ts +2 -2
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.d.ts +4 -2
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +214 -67
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-CV6rXbB3.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
- package/dist/{ai-harness-CV6rXbB3.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
- package/dist/{ai-harness-Clbg5ei7.js → ai-harness-dQeJaR1t.js} +7 -3
- package/dist/ai-harness-dQeJaR1t.js.map +1 -0
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.js +3 -3
- package/dist/ai-pi.js.map +1 -1
- package/dist/ai-react.d.ts +5 -5
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +14 -17
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +86 -73
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/{client-BhHLoNKF.js → client-BQS4i_V9.js} +29 -6
- package/dist/client-BQS4i_V9.js.map +1 -0
- package/dist/{client-h_DKFAjj.d.ts → client-CUMbS_38.d.ts} +3 -3
- package/dist/{client-h_DKFAjj.d.ts.map → client-CUMbS_38.d.ts.map} +1 -1
- package/dist/client.d.ts +3 -3
- package/dist/client.js +1 -1
- package/dist/{contract-BYKASlCV.js → contract-CWukt01d.js} +2 -2
- package/dist/contract-CWukt01d.js.map +1 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/{initial-snapshot-Bx-c0pDP.d.ts → initial-snapshot-D3Lve43o.d.ts} +4 -4
- package/dist/initial-snapshot-D3Lve43o.d.ts.map +1 -0
- package/dist/{internal-CscSY7K_.js → internal-CvsrprnU.js} +40 -16
- package/dist/internal-CvsrprnU.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/otel.js +1 -1
- package/dist/otel.js.map +1 -1
- package/dist/{react-BBXgHWFc.d.ts → react-BoHvC3v9.d.ts} +4 -4
- package/dist/{react-BBXgHWFc.d.ts.map → react-BoHvC3v9.d.ts.map} +1 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{reducer-CqA59Q6U.d.ts → reducer-CYPHsSmd.d.ts} +2 -2
- package/dist/{reducer-CqA59Q6U.d.ts.map → reducer-CYPHsSmd.d.ts.map} +1 -1
- package/dist/{reducer-BAXc8JyF.js → reducer-CjmvZSAb.js} +2 -2
- package/dist/{reducer-BAXc8JyF.js.map → reducer-CjmvZSAb.js.map} +1 -1
- package/dist/runtime-consumer-CIeL33wj.js +12 -0
- package/dist/runtime-consumer-CIeL33wj.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +3 -3
- package/dist/scheduler-task-X1SToDjP.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +3 -3
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-qyb5aJJG.js → server-DtFrWxi3.js} +412 -116
- package/dist/server-DtFrWxi3.js.map +1 -0
- package/dist/{server-DYAATFuT.d.ts → server-qgez0Xny.d.ts} +108 -41
- package/dist/server-qgez0Xny.d.ts.map +1 -0
- package/dist/server.browser.js +1 -1
- package/dist/server.browser.js.map +1 -1
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/{session-clear-C-QwTXV1.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
- package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
- package/dist/{session-clear-DGI4jTQ5.js → session-clear-wHd5GCpZ.js} +5 -4
- package/dist/session-clear-wHd5GCpZ.js.map +1 -0
- package/dist/store-D0MhBvg6.d.ts +532 -0
- package/dist/store-D0MhBvg6.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-codec-DwrWlgQx.js +65 -0
- package/dist/store-codec-DwrWlgQx.js.map +1 -0
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +406 -225
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-7WJV0YWP.js → store-polling-BZt2Rf3D.js} +2 -2
- package/dist/{store-polling-7WJV0YWP.js.map → store-polling-BZt2Rf3D.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +1598 -587
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
- package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +4 -4
- package/dist/store-redis-notify-CsJw_hfD.js +3060 -0
- package/dist/store-redis-notify-CsJw_hfD.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +4 -5
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +527 -299
- package/dist/store-sqlite.js.map +1 -1
- package/dist/stream-buffer-DrTyiIuo.js +57 -0
- package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
- package/dist/{telemetry-Cyfqo6Qt.d.ts → telemetry-CWwsh-Gx.d.ts} +3 -3
- package/dist/telemetry-CWwsh-Gx.d.ts.map +1 -0
- package/dist/{wire-BKxH1vbP.js → wire-CV-Z7wz-.js} +6 -5
- package/dist/wire-CV-Z7wz-.js.map +1 -0
- package/docs/01-quickstart.mdx +21 -15
- package/docs/02-examples.mdx +1 -1
- package/docs/actors/01-introduction.mdx +13 -2
- package/docs/actors/02-concurrency.mdx +1 -1
- package/docs/agents/01-introduction.mdx +1 -1
- package/docs/agents/02-tools.mdx +12 -11
- package/docs/agents/03-conversations.mdx +36 -25
- package/docs/agents/04-harnesses.mdx +14 -7
- package/docs/client/03-presence.mdx +18 -14
- package/docs/contract/01-events.mdx +2 -2
- package/docs/contract/02-state.mdx +18 -14
- package/docs/contract/03-sessions.mdx +32 -25
- package/docs/index.mdx +29 -24
- package/docs/reference/01-core.mdx +7 -6
- package/docs/reference/02-server.mdx +250 -191
- package/docs/reference/04-ai.mdx +33 -23
- package/docs/reference/05-telemetry.mdx +27 -13
- package/docs/reference/06-errors.mdx +25 -19
- package/docs/reference/meta.ts +1 -1
- package/docs/schedulers/01-overview.mdx +8 -8
- package/docs/schedulers/02-vercel-queues.mdx +2 -2
- package/docs/schedulers/03-qstash.mdx +4 -4
- package/docs/server/01-consumers.mdx +418 -0
- package/docs/server/02-timers.mdx +31 -21
- package/docs/server/03-cancellation.mdx +52 -44
- package/docs/server/04-routes.mdx +4 -1
- package/docs/server/05-sql-projections.mdx +391 -0
- package/docs/server/06-guarantees.mdx +61 -51
- package/docs/stores/01-overview.mdx +3 -3
- package/docs/stores/02-redis.mdx +24 -10
- package/docs/stores/03-redis-http.mdx +8 -6
- package/docs/stores/04-postgres.mdx +65 -14
- package/docs/stores/05-sqlite.mdx +5 -3
- package/docs/upgrades/01-overview.mdx +31 -0
- package/docs/upgrades/78-0-22.mdx +115 -0
- package/docs/upgrades/79-0-21.mdx +165 -0
- package/docs/upgrades/80-0-20.mdx +43 -0
- package/docs/upgrades/81-0-19.mdx +33 -0
- package/docs/upgrades/82-0-18.mdx +33 -0
- package/docs/upgrades/83-0-17.mdx +69 -0
- package/docs/upgrades/meta.ts +3 -0
- package/examples/playground/DEPLOY.md +39 -7
- package/examples/playground/app/a0/chat.test.tsx +2 -2
- package/examples/playground/app/a0/sandbox-log.ts +17 -15
- package/examples/playground/app/a0/sandbox.ts +58 -13
- package/examples/playground/app/a0/server.ts +4 -4
- package/examples/playground/app/a2slack/a2slack.css +1067 -0
- package/examples/playground/app/a2slack/c/[channel]/page.tsx +20 -0
- package/examples/playground/app/a2slack/consumers/agent.ts +63 -0
- package/examples/playground/app/a2slack/consumers/feed.ts +164 -0
- package/examples/playground/app/a2slack/consumers/notify.ts +116 -0
- package/examples/playground/app/a2slack/consumers/search.ts +37 -0
- package/examples/playground/app/a2slack/consumers/triage.ts +31 -0
- package/examples/playground/app/a2slack/consumers/welcome.ts +25 -0
- package/examples/playground/app/a2slack/db/a2.ts +209 -0
- package/examples/playground/app/a2slack/db/index.ts +73 -0
- package/examples/playground/app/a2slack/db/migrations/0000_extensions.sql +4 -0
- package/examples/playground/app/a2slack/db/migrations/0001_workspace.sql +74 -0
- package/examples/playground/app/a2slack/db/migrations/0002_a2.sql +215 -0
- package/examples/playground/app/a2slack/db/migrations/0003_a2_batch.sql +67 -0
- package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0000_snapshot.json +18 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0001_snapshot.json +500 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0002_snapshot.json +1159 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0003_snapshot.json +1159 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
- package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +41 -0
- package/examples/playground/app/a2slack/db/queries.ts +263 -0
- package/examples/playground/app/a2slack/db/schema.ts +145 -0
- package/examples/playground/app/a2slack/drills/a2slack.drill.ts +708 -0
- package/examples/playground/app/a2slack/drills/harness.ts +244 -0
- package/examples/playground/app/a2slack/events/route.ts +36 -0
- package/examples/playground/app/a2slack/identity.ts +44 -0
- package/examples/playground/app/a2slack/inbox/route.ts +8 -0
- package/examples/playground/app/a2slack/lag/data/route.ts +8 -0
- package/examples/playground/app/a2slack/lag/page.tsx +101 -0
- package/examples/playground/app/a2slack/lag.ts +73 -0
- package/examples/playground/app/a2slack/layout.tsx +43 -0
- package/examples/playground/app/a2slack/messages/route.ts +17 -0
- package/examples/playground/app/a2slack/model.test.ts +208 -0
- package/examples/playground/app/a2slack/model.ts +369 -0
- package/examples/playground/app/a2slack/models.test.ts +72 -0
- package/examples/playground/app/a2slack/page.tsx +10 -0
- package/examples/playground/app/a2slack/scheduler/route.ts +5 -0
- package/examples/playground/app/a2slack/search/route.ts +40 -0
- package/examples/playground/app/a2slack/search-index.ts +108 -0
- package/examples/playground/app/a2slack/server.test.ts +315 -0
- package/examples/playground/app/a2slack/server.ts +55 -0
- package/examples/playground/app/a2slack/sessions.ts +50 -0
- package/examples/playground/app/a2slack/thread/route.ts +8 -0
- package/examples/playground/app/a2slack/ui/ChannelView.tsx +87 -0
- package/examples/playground/app/a2slack/ui/Composer.tsx +117 -0
- package/examples/playground/app/a2slack/ui/DotsShadow.tsx +42 -0
- package/examples/playground/app/a2slack/ui/Feed.tsx +157 -0
- package/examples/playground/app/a2slack/ui/InboxContext.tsx +63 -0
- package/examples/playground/app/a2slack/ui/InboxView.tsx +82 -0
- package/examples/playground/app/a2slack/ui/Menu.tsx +128 -0
- package/examples/playground/app/a2slack/ui/MessageRow.tsx +314 -0
- package/examples/playground/app/a2slack/ui/NameGate.tsx +35 -0
- package/examples/playground/app/a2slack/ui/SearchContext.tsx +146 -0
- package/examples/playground/app/a2slack/ui/SearchResults.tsx +97 -0
- package/examples/playground/app/a2slack/ui/Shell.tsx +34 -0
- package/examples/playground/app/a2slack/ui/Sidebar.tsx +199 -0
- package/examples/playground/app/a2slack/ui/ThreadPanel.tsx +134 -0
- package/examples/playground/app/a2slack/ui/useInbox.ts +71 -0
- package/examples/playground/app/agent/server.ts +3 -3
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +1 -1
- package/examples/playground/app/chat/server.ts +45 -35
- package/examples/playground/app/components/demo-icons.tsx +7 -0
- package/examples/playground/app/components/demos.tsx +7 -0
- package/examples/playground/app/orders/[orderId]/order-client.tsx +2 -2
- package/examples/playground/app/orders/page.tsx +1 -1
- package/examples/playground/app/orders/server.ts +49 -30
- package/examples/playground/app/recovery/[recoveryId]/recovery-client.tsx +14 -15
- package/examples/playground/app/recovery/page.tsx +4 -4
- package/examples/playground/app/recovery/server.ts +23 -32
- package/examples/playground/drizzle.config.ts +14 -0
- package/examples/playground/lib/store.ts +1 -1
- package/examples/playground/next.config.ts +6 -1
- package/examples/playground/package.json +12 -1
- package/examples/playground/scripts/migrate.mjs +31 -0
- package/examples/playground/vercel.json +9 -0
- package/examples/playground/vitest.drills.config.ts +32 -0
- package/package.json +5 -2
- package/sql/postgres/drizzle-schema.ts +209 -0
- package/sql/postgres/schema.sql +227 -67
- package/src/actor-shared.ts +10 -2
- package/src/actor.ts +101 -71
- package/src/ai-agent.ts +162 -111
- package/src/ai-control-server.ts +8 -8
- package/src/ai-fx.ts +324 -82
- package/src/ai-harness.ts +17 -2
- package/src/ai-pi.ts +2 -2
- package/src/ai-server.ts +2 -2
- package/src/ai.ts +7 -7
- package/src/client.ts +19 -2
- package/src/contract.ts +1 -1
- package/src/deterministic-id.ts +27 -4
- package/src/errors.ts +8 -5
- package/src/idempotent-replay.ts +0 -1
- package/src/index.ts +1 -1
- package/src/internal.ts +57 -16
- package/src/otel.ts +1 -1
- package/src/postgres-schema.ts +98 -57
- package/src/presence-recovery.ts +18 -11
- package/src/presence-tombstones.ts +52 -0
- package/src/push-envelope.ts +20 -0
- package/src/runtime-consumer.ts +10 -0
- package/src/scheduler-task.ts +2 -2
- package/src/scheduler-vercel.ts +1 -1
- package/src/server.browser.ts +1 -1
- package/src/server.ts +819 -218
- package/src/session-socket.ts +1 -1
- package/src/store-codec.ts +66 -0
- package/src/store-memory.ts +604 -286
- package/src/store-postgres.ts +2006 -857
- package/src/store-presence-polling.ts +59 -34
- package/src/store-redis-core.ts +1961 -1333
- package/src/store-redis-notify.ts +76 -26
- package/src/store-redis.ts +2 -3
- package/src/store-sqlite.ts +915 -516
- package/src/store.ts +241 -102
- package/src/telemetry.ts +2 -1
- package/src/wire.ts +1 -0
- package/dist/actor-C7b-1arn.d.ts.map +0 -1
- package/dist/actor-shared-t7w8xIKP.js.map +0 -1
- package/dist/ai-harness-Clbg5ei7.js.map +0 -1
- package/dist/ai-mMiWul1T.js.map +0 -1
- package/dist/client-BhHLoNKF.js.map +0 -1
- package/dist/contract-BYKASlCV.js.map +0 -1
- package/dist/idempotent-replay-JdgMQRRm.js +0 -21
- package/dist/idempotent-replay-JdgMQRRm.js.map +0 -1
- package/dist/initial-snapshot-Bx-c0pDP.d.ts.map +0 -1
- package/dist/internal-CscSY7K_.js.map +0 -1
- package/dist/server-DYAATFuT.d.ts.map +0 -1
- package/dist/server-qyb5aJJG.js.map +0 -1
- package/dist/session-clear-C-QwTXV1.d.ts.map +0 -1
- package/dist/session-clear-DGI4jTQ5.js.map +0 -1
- package/dist/store-CyLUy86v.d.ts +0 -404
- package/dist/store-CyLUy86v.d.ts.map +0 -1
- package/dist/store-codec-DTG0Ftek.js +0 -10
- package/dist/store-codec-DTG0Ftek.js.map +0 -1
- package/dist/store-presence-polling-B1ixeqIS.js +0 -94
- package/dist/store-presence-polling-B1ixeqIS.js.map +0 -1
- package/dist/store-redis-notify-CM0AfLY0.js +0 -2544
- package/dist/store-redis-notify-CM0AfLY0.js.map +0 -1
- package/dist/stream-buffer-Cx7AAPGq.js +0 -23
- package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
- package/dist/telemetry-Cyfqo6Qt.d.ts.map +0 -1
- package/dist/wire-BKxH1vbP.js.map +0 -1
- package/docs/server/01-handlers.mdx +0 -227
package/sql/postgres/schema.sql
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
|
-
-- A2's tables for experimental-a2/store-postgres.
|
|
1
|
+
-- A2's tables for experimental-a2/store-postgres. drizzle-schema.ts, next to
|
|
2
|
+
-- this file, shows the same tables as Drizzle definitions, as an example.
|
|
2
3
|
--
|
|
3
|
-
-- Apply this file
|
|
4
|
-
--
|
|
5
|
-
-- leaves existing data alone, so the file is safe to re-run on any
|
|
6
|
-
-- database, whichever release created it.
|
|
4
|
+
-- Apply this file before the first deploy, and again when a release's
|
|
5
|
+
-- changelog says the tables changed. Every statement creates only what is
|
|
6
|
+
-- missing and leaves existing data alone, so the file is safe to re-run on any
|
|
7
|
+
-- A2 database, whichever release created it. A2 checks nothing up front: a
|
|
8
|
+
-- query that finds a table or column missing throws STORE_NOT_CONFIGURED.
|
|
9
|
+
--
|
|
10
|
+
-- On a database from a release that ran handlers, it drops the work those
|
|
11
|
+
-- handlers hadn't finished, and the columns they dispatched by, so drain
|
|
12
|
+
-- earlier deployments and apply it while none is running.
|
|
7
13
|
--
|
|
8
14
|
-- Each index sits behind a catalog check: `create index if not exists` takes
|
|
9
15
|
-- a share lock on its table, and requires owning it, even when the index
|
|
10
16
|
-- exists. The check makes a re-run on a live database wait for no writer.
|
|
17
|
+
--
|
|
18
|
+
-- The file works as a migration as it ships: copy it into your migration
|
|
19
|
+
-- tool's folder. The breakpoint comment after each statement splits it for
|
|
20
|
+
-- tools that send one statement per query, like Drizzle on PGlite; to every
|
|
21
|
+
-- other tool, psql included, it's only a comment.
|
|
11
22
|
|
|
12
23
|
create table if not exists a2_events (
|
|
13
24
|
session_id text not null,
|
|
@@ -17,47 +28,53 @@ create table if not exists a2_events (
|
|
|
17
28
|
event_id text not null,
|
|
18
29
|
created_at bigint not null,
|
|
19
30
|
cause jsonb,
|
|
31
|
+
primary key (session_id, idx)
|
|
32
|
+
);
|
|
33
|
+
--> statement-breakpoint
|
|
34
|
+
|
|
35
|
+
-- One row per run of a cursor: an event cursor's one event, a lane's run of
|
|
36
|
+
-- queued events, or a log cursor's position. A run is keyed by the head it
|
|
37
|
+
-- started at (`epoch`), so a clear starts new runs instead of rewriting old
|
|
38
|
+
-- ones.
|
|
39
|
+
create table if not exists a2_cursors (
|
|
40
|
+
session_id text not null,
|
|
41
|
+
consumer text not null,
|
|
42
|
+
cursor_key text not null,
|
|
43
|
+
epoch bigint not null,
|
|
20
44
|
lane text,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
45
|
+
kind text not null,
|
|
46
|
+
position bigint not null,
|
|
47
|
+
head bigint,
|
|
48
|
+
types jsonb,
|
|
49
|
+
attempt_count integer not null default 0,
|
|
25
50
|
first_claimed_at bigint,
|
|
26
51
|
last_claimed_at bigint,
|
|
27
|
-
|
|
52
|
+
claim_holder text,
|
|
53
|
+
claim_expires_at bigint,
|
|
28
54
|
failure_count integer not null default 0,
|
|
29
55
|
last_failed_at bigint,
|
|
30
56
|
last_failed_attempt integer,
|
|
31
57
|
last_error text,
|
|
32
58
|
failed_at bigint,
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
59
|
+
completed_idx bigint,
|
|
60
|
+
completed_attempt integer,
|
|
61
|
+
returned_event_ids jsonb,
|
|
62
|
+
primary key (session_id, cursor_key, consumer, epoch)
|
|
36
63
|
);
|
|
64
|
+
--> statement-breakpoint
|
|
37
65
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if to_regclass('a2_events_dispatch_ready') is null then
|
|
51
|
-
create index if not exists a2_events_dispatch_ready
|
|
52
|
-
on a2_events (session_id, idx)
|
|
53
|
-
where lane_ready and processed_at is null and failed_at is null;
|
|
54
|
-
end if;
|
|
55
|
-
if to_regclass('a2_events_claim_expiry') is null then
|
|
56
|
-
create index if not exists a2_events_claim_expiry
|
|
57
|
-
on a2_events (session_id, claim_expires_at)
|
|
58
|
-
where lane_ready and processed_at is null and failed_at is null;
|
|
59
|
-
end if;
|
|
60
|
-
end $$;
|
|
66
|
+
-- A lane cursor's queue: the events appended for the lane, in log order.
|
|
67
|
+
create table if not exists a2_lane_events (
|
|
68
|
+
session_id text not null,
|
|
69
|
+
consumer text not null,
|
|
70
|
+
lane text not null,
|
|
71
|
+
idx bigint not null,
|
|
72
|
+
done boolean not null default false,
|
|
73
|
+
completed_attempt integer,
|
|
74
|
+
returned_event_ids jsonb,
|
|
75
|
+
primary key (session_id, consumer, lane, idx)
|
|
76
|
+
);
|
|
77
|
+
--> statement-breakpoint
|
|
61
78
|
|
|
62
79
|
create table if not exists a2_snapshots (
|
|
63
80
|
session_id text not null,
|
|
@@ -67,6 +84,7 @@ create table if not exists a2_snapshots (
|
|
|
67
84
|
updated_at bigint not null,
|
|
68
85
|
primary key (session_id, reducer_name)
|
|
69
86
|
);
|
|
87
|
+
--> statement-breakpoint
|
|
70
88
|
|
|
71
89
|
create table if not exists a2_snapshot_history (
|
|
72
90
|
session_id text not null,
|
|
@@ -76,26 +94,37 @@ create table if not exists a2_snapshot_history (
|
|
|
76
94
|
updated_at bigint not null,
|
|
77
95
|
primary key (session_id, reducer_name, up_to_index)
|
|
78
96
|
);
|
|
97
|
+
--> statement-breakpoint
|
|
98
|
+
|
|
99
|
+
-- Earlier releases pinned snapshots per event; this one pins them per
|
|
100
|
+
-- cursor head. Pins only hold history for work in flight, so old ones go.
|
|
101
|
+
do $$ begin
|
|
102
|
+
if to_regclass('a2_snapshot_pins') is not null and not exists (
|
|
103
|
+
select 1 from pg_attribute
|
|
104
|
+
where attrelid = to_regclass('a2_snapshot_pins')
|
|
105
|
+
and attname = 'cursor_key' and not attisdropped
|
|
106
|
+
) then
|
|
107
|
+
drop table a2_snapshot_pins;
|
|
108
|
+
end if;
|
|
109
|
+
end $$;
|
|
110
|
+
--> statement-breakpoint
|
|
79
111
|
|
|
80
112
|
create table if not exists a2_snapshot_pins (
|
|
81
113
|
session_id text not null,
|
|
82
114
|
event_index bigint not null,
|
|
115
|
+
consumer text not null,
|
|
116
|
+
cursor_key text not null,
|
|
83
117
|
reducer_name text not null,
|
|
84
118
|
up_to_index bigint not null,
|
|
85
|
-
primary key (session_id, event_index, reducer_name, up_to_index)
|
|
119
|
+
primary key (session_id, event_index, consumer, cursor_key, reducer_name, up_to_index)
|
|
86
120
|
);
|
|
87
|
-
|
|
88
|
-
do $$ begin
|
|
89
|
-
if to_regclass('a2_snapshot_pins_checkpoint') is null then
|
|
90
|
-
create index if not exists a2_snapshot_pins_checkpoint
|
|
91
|
-
on a2_snapshot_pins (session_id, reducer_name, up_to_index);
|
|
92
|
-
end if;
|
|
93
|
-
end $$;
|
|
121
|
+
--> statement-breakpoint
|
|
94
122
|
|
|
95
123
|
create table if not exists a2_clears (
|
|
96
124
|
session_id text primary key,
|
|
97
125
|
clear_index bigint not null
|
|
98
126
|
);
|
|
127
|
+
--> statement-breakpoint
|
|
99
128
|
|
|
100
129
|
create table if not exists a2_presence (
|
|
101
130
|
ns text not null,
|
|
@@ -107,31 +136,162 @@ create table if not exists a2_presence (
|
|
|
107
136
|
expires_at bigint not null,
|
|
108
137
|
primary key (ns, participant, field)
|
|
109
138
|
);
|
|
139
|
+
--> statement-breakpoint
|
|
110
140
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
)
|
|
141
|
+
-- From an earlier release: work its handlers hadn't finished is dropped,
|
|
142
|
+
-- since this release runs no handlers to finish it. Every event is kept.
|
|
143
|
+
-- Causes gain the dispatcher that appended them, the handlers table
|
|
144
|
+
-- (`a2.handlers`), and its lane. Then the old dispatch columns go, and
|
|
145
|
+
-- their indexes with them. The block locks the events first, so an earlier
|
|
146
|
+
-- release still writing waits and then fails, rather than committing between
|
|
147
|
+
-- its statements, and it commits whole, or not at all.
|
|
148
|
+
do $$ begin
|
|
149
|
+
if exists (
|
|
150
|
+
select 1 from pg_attribute
|
|
151
|
+
where attrelid = to_regclass('a2_events')
|
|
152
|
+
and attname = 'processed_at' and not attisdropped
|
|
153
|
+
) then
|
|
154
|
+
lock table a2_events in access exclusive mode;
|
|
155
|
+
|
|
156
|
+
update a2_events child
|
|
157
|
+
set cause = jsonb_build_object(
|
|
158
|
+
'consumer', 'a2.handlers',
|
|
159
|
+
'lane', (select parent.lane from a2_events parent
|
|
160
|
+
where parent.session_id = child.session_id
|
|
161
|
+
and parent.idx = (child.cause ->> 'index')::bigint)
|
|
162
|
+
) || child.cause
|
|
163
|
+
where child.cause is not null and not child.cause ? 'consumer';
|
|
115
164
|
|
|
116
|
-
|
|
117
|
-
|
|
165
|
+
alter table a2_events
|
|
166
|
+
drop column lane,
|
|
167
|
+
drop column lane_ready,
|
|
168
|
+
drop column processed_at,
|
|
169
|
+
drop column processed_by_attempt,
|
|
170
|
+
drop column returned_event_ids,
|
|
171
|
+
drop column first_claimed_at,
|
|
172
|
+
drop column last_claimed_at,
|
|
173
|
+
drop column attempt_count,
|
|
174
|
+
drop column failure_count,
|
|
175
|
+
drop column last_failed_at,
|
|
176
|
+
drop column last_failed_attempt,
|
|
177
|
+
drop column last_error,
|
|
178
|
+
drop column failed_at,
|
|
179
|
+
drop column claim_holder,
|
|
180
|
+
drop column claim_expires_at;
|
|
181
|
+
end if;
|
|
182
|
+
end $$;
|
|
183
|
+
--> statement-breakpoint
|
|
184
|
+
|
|
185
|
+
-- Earlier releases recorded a schema version here and checked it before
|
|
186
|
+
-- their first query. This one checks nothing up front.
|
|
187
|
+
drop table if exists a2_schema;
|
|
188
|
+
--> statement-breakpoint
|
|
189
|
+
|
|
190
|
+
-- Runs an operation's statements in one round trip: A2 sends each write that
|
|
191
|
+
-- takes a session's lock as one call, the lock and then the statements that
|
|
192
|
+
-- need it, so it holds the lock only while the server runs them. Each
|
|
193
|
+
-- statement takes its snapshot when it starts, so those after the lock see
|
|
194
|
+
-- what its last holder committed; a single statement can't, since its
|
|
195
|
+
-- snapshot predates the lock. Each statement is a select of jsonb rows: a row
|
|
196
|
+
-- with an `abort` key fails the call and rolls it back, and one with a `stop`
|
|
197
|
+
-- key ends it. It needs read committed, the default: under a stricter level
|
|
198
|
+
-- every statement would share the first one's snapshot, so it refuses to run.
|
|
199
|
+
-- Created only if missing, as the indexes are: a later release that changes it
|
|
200
|
+
-- adds a function with a new name.
|
|
201
|
+
do $$ declare grantee text; begin
|
|
202
|
+
if to_regprocedure('a2_batch(text[], jsonb)') is null then
|
|
203
|
+
create function a2_batch(statements text[], params jsonb)
|
|
204
|
+
returns setof jsonb
|
|
205
|
+
language plpgsql
|
|
206
|
+
volatile
|
|
207
|
+
as $batch$
|
|
208
|
+
declare
|
|
209
|
+
statement text;
|
|
210
|
+
result jsonb;
|
|
211
|
+
begin
|
|
212
|
+
if current_setting('transaction_isolation') <> 'read committed' then
|
|
213
|
+
raise exception using errcode = 'A2002',
|
|
214
|
+
message = 'a2_batch needs the read committed isolation level';
|
|
215
|
+
end if;
|
|
216
|
+
foreach statement in array statements loop
|
|
217
|
+
for result in execute statement using params loop
|
|
218
|
+
if result ? 'abort' then
|
|
219
|
+
raise exception using errcode = 'A2001',
|
|
220
|
+
message = 'a2_batch aborted', detail = result::text;
|
|
221
|
+
end if;
|
|
222
|
+
return next result;
|
|
223
|
+
if result ? 'stop' then
|
|
224
|
+
return;
|
|
225
|
+
end if;
|
|
226
|
+
end loop;
|
|
227
|
+
end loop;
|
|
228
|
+
end
|
|
229
|
+
$batch$;
|
|
230
|
+
-- Default privileges can grant a new function to roles besides PUBLIC, as
|
|
231
|
+
-- platforms that serve functions over an API grant theirs: it starts with
|
|
232
|
+
-- none but its owner's. Grants made after it exist are left alone.
|
|
233
|
+
for grantee in
|
|
234
|
+
select acl.grantee::regrole::text
|
|
235
|
+
from pg_proc proc, aclexplode(proc.proacl) acl
|
|
236
|
+
where proc.oid = to_regprocedure('a2_batch(text[], jsonb)')
|
|
237
|
+
and acl.grantee not in (0, proc.proowner)
|
|
238
|
+
loop
|
|
239
|
+
execute format('revoke execute on function a2_batch(text[], jsonb) from %s', grantee);
|
|
240
|
+
end loop;
|
|
241
|
+
end if;
|
|
242
|
+
-- It runs any statement it's handed, as its caller: only its owner, the role
|
|
243
|
+
-- that applies this file, may call it, and not every role, as Postgres lets
|
|
244
|
+
-- by default. Platforms that serve functions over an API would serve it too.
|
|
245
|
+
if pg_has_role(
|
|
246
|
+
current_user,
|
|
247
|
+
(select proowner from pg_proc
|
|
248
|
+
where oid = to_regprocedure('a2_batch(text[], jsonb)')),
|
|
249
|
+
'usage'
|
|
250
|
+
) then
|
|
251
|
+
revoke execute on function a2_batch(text[], jsonb) from public;
|
|
252
|
+
end if;
|
|
253
|
+
end $$;
|
|
254
|
+
--> statement-breakpoint
|
|
255
|
+
|
|
256
|
+
do $$ begin
|
|
257
|
+
if to_regclass('a2_events_types') is null then
|
|
258
|
+
create index if not exists a2_events_types
|
|
259
|
+
on a2_events (session_id, event_type, idx);
|
|
260
|
+
end if;
|
|
261
|
+
if to_regclass('a2_cursors_heads') is null then
|
|
262
|
+
create index if not exists a2_cursors_heads
|
|
263
|
+
on a2_cursors (session_id, head) where head is not null;
|
|
264
|
+
end if;
|
|
265
|
+
if to_regclass('a2_cursors_lanes') is null then
|
|
266
|
+
create index if not exists a2_cursors_lanes
|
|
267
|
+
on a2_cursors (session_id) where kind = 'lane';
|
|
268
|
+
end if;
|
|
269
|
+
if to_regclass('a2_snapshot_pins_checkpoint') is null then
|
|
270
|
+
create index if not exists a2_snapshot_pins_checkpoint
|
|
271
|
+
on a2_snapshot_pins (session_id, reducer_name, up_to_index);
|
|
272
|
+
end if;
|
|
273
|
+
end $$;
|
|
274
|
+
--> statement-breakpoint
|
|
275
|
+
|
|
276
|
+
-- Event ids are unique per session, through the index on (session_id,
|
|
277
|
+
-- event_id). It replaces a2_events_event_id, on event_id alone. The swap
|
|
278
|
+
-- locks the table before it builds, so it waits for running queries instead
|
|
279
|
+
-- of deadlocking with a writer when the whole file runs as one transaction.
|
|
280
|
+
-- The old index goes only once the new one is valid, so a session's ids
|
|
281
|
+
-- always have a unique index.
|
|
118
282
|
do $$ begin
|
|
119
|
-
if to_regclass('
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
raise exception 'A2 schema incomplete: an earlier statement in schema.sql failed. Fix it and apply the file again.';
|
|
283
|
+
if to_regclass('a2_events_session_event_id') is null then
|
|
284
|
+
if to_regclass('a2_events_event_id') is not null then
|
|
285
|
+
lock table a2_events in access exclusive mode;
|
|
286
|
+
end if;
|
|
287
|
+
create unique index if not exists a2_events_session_event_id
|
|
288
|
+
on a2_events (session_id, event_id);
|
|
289
|
+
end if;
|
|
290
|
+
if to_regclass('a2_events_event_id') is not null and exists (
|
|
291
|
+
select 1 from pg_index
|
|
292
|
+
where indexrelid = to_regclass('a2_events_session_event_id')
|
|
293
|
+
and indisvalid and indisunique
|
|
294
|
+
) then
|
|
295
|
+
drop index a2_events_event_id;
|
|
133
296
|
end if;
|
|
134
|
-
insert into a2_schema (version) values (1)
|
|
135
|
-
on conflict (only_row)
|
|
136
|
-
do update set version = greatest(a2_schema.version, excluded.version);
|
|
137
297
|
end $$;
|
package/src/actor-shared.ts
CHANGED
|
@@ -131,7 +131,11 @@ export type ActorScheduleSend<E> = {
|
|
|
131
131
|
export type ActorContext<D extends ActorProtocol> = {
|
|
132
132
|
/** Mutable draft, committed atomically with the handler's completion. */
|
|
133
133
|
state: D['state']
|
|
134
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* The message id — stable across re-runs and unique within the instance;
|
|
136
|
+
* the idempotency key for external I/O. An explicit send id that repeats
|
|
137
|
+
* in another instance repeats here too.
|
|
138
|
+
*/
|
|
135
139
|
id: string
|
|
136
140
|
/** Durable 1-based dispatch ordinal of this message. */
|
|
137
141
|
attempt: number
|
|
@@ -150,7 +154,11 @@ export type ActorContext<D extends ActorProtocol> = {
|
|
|
150
154
|
* whose serial handlers decide against fresh state.
|
|
151
155
|
*/
|
|
152
156
|
export type ActorConcurrentContext<D extends ActorProtocol> = {
|
|
153
|
-
/**
|
|
157
|
+
/**
|
|
158
|
+
* The message id — stable across re-runs and unique within the instance;
|
|
159
|
+
* the idempotency key for external I/O. An explicit send id that repeats
|
|
160
|
+
* in another instance repeats here too.
|
|
161
|
+
*/
|
|
154
162
|
id: string
|
|
155
163
|
/** Durable 1-based dispatch ordinal of this message. */
|
|
156
164
|
attempt: number
|
package/src/actor.ts
CHANGED
|
@@ -82,7 +82,14 @@ import { setServerFetchHooks } from './server-fetch.ts'
|
|
|
82
82
|
import { sseResponse } from './sse.ts'
|
|
83
83
|
import { isSessionCleared, sessionClearedError } from './session-clear.ts'
|
|
84
84
|
import type { SessionCleared } from './session-clear.ts'
|
|
85
|
-
import type {
|
|
85
|
+
import type {
|
|
86
|
+
A2Scheduler,
|
|
87
|
+
A2Server,
|
|
88
|
+
ConsumerContext,
|
|
89
|
+
ConsumerResult,
|
|
90
|
+
Session,
|
|
91
|
+
} from './server.ts'
|
|
92
|
+
import { runtimeConsumer } from './runtime-consumer.ts'
|
|
86
93
|
import type { A2Store } from './store.ts'
|
|
87
94
|
import type { StandardSchemaV1 } from './standard-schema.ts'
|
|
88
95
|
import type { A2Telemetry } from './telemetry.ts'
|
|
@@ -460,14 +467,16 @@ export function actor<D extends ActorProtocol>(
|
|
|
460
467
|
return send as ActorSend<D['events']>
|
|
461
468
|
}
|
|
462
469
|
|
|
463
|
-
|
|
470
|
+
type Run = (ctx: ConsumerContext<EventDefs>) => ConsumerResult<EventDefs>
|
|
471
|
+
const concurrentRuns = new Map<string, Run>()
|
|
472
|
+
const serialRuns = new Map<string, Run>()
|
|
464
473
|
for (const [eventName, def] of handlersByEvent) {
|
|
465
474
|
if (def.kind === 'concurrent') {
|
|
466
475
|
// No lane: concurrent handlers run in parallel — with the lane
|
|
467
476
|
// and with each other. They cannot write state (no draft to
|
|
468
477
|
// commit), so no serialization is needed for correctness; slow
|
|
469
478
|
// I/O lives here without blocking the actor.
|
|
470
|
-
|
|
479
|
+
concurrentRuns.set(eventName, async (ctx) => {
|
|
471
480
|
const sends: { type: string; payload: unknown }[] = []
|
|
472
481
|
try {
|
|
473
482
|
await def.handle(
|
|
@@ -509,78 +518,70 @@ export function actor<D extends ActorProtocol>(
|
|
|
509
518
|
}
|
|
510
519
|
// Sends commit atomically with settlement; re-runs converge on
|
|
511
520
|
// the same deterministic message ids.
|
|
512
|
-
return sends
|
|
513
|
-
}
|
|
521
|
+
return sends as never
|
|
522
|
+
})
|
|
514
523
|
continue
|
|
515
524
|
}
|
|
516
|
-
|
|
517
|
-
//
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
//
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
signal: ctx.signal,
|
|
542
|
-
send: makeSend(sends),
|
|
543
|
-
schedule: makeSchedule((taskName, timing, event) =>
|
|
544
|
-
ctx.session.schedule(
|
|
545
|
-
taskName,
|
|
546
|
-
timing as Parameters<typeof ctx.session.schedule>[1],
|
|
547
|
-
event,
|
|
548
|
-
),
|
|
525
|
+
serialRuns.set(eventName, async (ctx) => {
|
|
526
|
+
// Explicitly 'latest', not the event-relative default: prior
|
|
527
|
+
// lane messages commit *after* this message was appended, so
|
|
528
|
+
// their state commits carry higher indexes than the trigger —
|
|
529
|
+
// an event-relative read would miss them and lose updates.
|
|
530
|
+
// 'latest' is deterministic here because the lane makes this
|
|
531
|
+
// attempt the only possible writer while it runs.
|
|
532
|
+
const { state, clearIndex } = await ctx.session.state(serverReducer, {
|
|
533
|
+
through: 'latest',
|
|
534
|
+
})
|
|
535
|
+
const draft = structuredClone(state)
|
|
536
|
+
const sends: { type: string; payload: unknown }[] = []
|
|
537
|
+
try {
|
|
538
|
+
await def.handle(
|
|
539
|
+
{
|
|
540
|
+
state: draft,
|
|
541
|
+
id: ctx.event.id,
|
|
542
|
+
attempt: ctx.attempt,
|
|
543
|
+
signal: ctx.signal,
|
|
544
|
+
send: makeSend(sends),
|
|
545
|
+
schedule: makeSchedule((taskName, timing, event) =>
|
|
546
|
+
ctx.session.schedule(
|
|
547
|
+
taskName,
|
|
548
|
+
timing as Parameters<typeof ctx.session.schedule>[1],
|
|
549
|
+
event,
|
|
549
550
|
),
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
event: eventName,
|
|
566
|
-
error: describeError(error),
|
|
567
|
-
}
|
|
568
|
-
return { type: ACTOR_FAILED_EVENT, payload }
|
|
569
|
-
}
|
|
570
|
-
// One returned batch: buffered sends plus the state commit —
|
|
571
|
-
// committed atomically with completion, so a reserve and its
|
|
572
|
-
// trigger can never half-happen. The state event stays the
|
|
573
|
-
// outcome marker a waiting call resolves on. (Unconditional
|
|
574
|
-
// for now; a dirty check is a compatible later optimization.)
|
|
575
|
-
const payload: ActorStatePayload<D['state']> = {
|
|
576
|
-
state: draft,
|
|
577
|
-
event: eventName,
|
|
551
|
+
),
|
|
552
|
+
},
|
|
553
|
+
ctx.event.payload,
|
|
554
|
+
)
|
|
555
|
+
} catch (error) {
|
|
556
|
+
// A NonRetriableError is an answer: settle the message,
|
|
557
|
+
// record the refusal, deliver it to the waiting caller —
|
|
558
|
+
// no retry, no failure budget spent (buffered sends are
|
|
559
|
+
// discarded with the draft: refusals are total). Every
|
|
560
|
+
// other throw is an ordinary A2 handler failure: rethrown
|
|
561
|
+
// into core's retry machinery (backoff, ten-failure
|
|
562
|
+
// budget, dead letter) — including surrendered lease
|
|
563
|
+
// aborts, which core recognizes by identity.
|
|
564
|
+
if (!(error instanceof NonRetriableError)) throw error
|
|
565
|
+
const payload: ActorFailedPayload = {
|
|
578
566
|
message: ctx.event.id,
|
|
579
|
-
|
|
567
|
+
event: eventName,
|
|
568
|
+
error: describeError(error),
|
|
580
569
|
}
|
|
581
|
-
return
|
|
582
|
-
}
|
|
583
|
-
|
|
570
|
+
return { type: ACTOR_FAILED_EVENT, payload }
|
|
571
|
+
}
|
|
572
|
+
// One returned batch: buffered sends plus the state commit —
|
|
573
|
+
// committed atomically with completion, so a reserve and its
|
|
574
|
+
// trigger can never half-happen. The state event stays the
|
|
575
|
+
// outcome marker a waiting call resolves on. (Unconditional
|
|
576
|
+
// for now; a dirty check is a compatible later optimization.)
|
|
577
|
+
const payload: ActorStatePayload<D['state']> = {
|
|
578
|
+
state: draft,
|
|
579
|
+
event: eventName,
|
|
580
|
+
message: ctx.event.id,
|
|
581
|
+
...(clearIndex === undefined ? {} : { clearIndex }),
|
|
582
|
+
}
|
|
583
|
+
return [...sends, { type: ACTOR_STATE_EVENT, payload }] as never
|
|
584
|
+
})
|
|
584
585
|
}
|
|
585
586
|
|
|
586
587
|
const presenceEnabled = (options as { presence?: unknown }).presence === true
|
|
@@ -590,9 +591,38 @@ export function actor<D extends ActorProtocol>(
|
|
|
590
591
|
const contract = presenceEnabled
|
|
591
592
|
? createContract({ name, events, presence: openPresenceDefs })
|
|
592
593
|
: createContract({ name, events })
|
|
594
|
+
// Serial messages share one lane per instance: the next starts only after
|
|
595
|
+
// the previous one completed, and completion commits the state event
|
|
596
|
+
// atomically, so read-modify-write on `ctx.state` is safe by exclusion,
|
|
597
|
+
// not by liveness. The lane is a function, so each message is queued on
|
|
598
|
+
// it as it's appended. Concurrent messages run alongside it and each
|
|
599
|
+
// other: they write no state.
|
|
600
|
+
const serialTypes = [...serialRuns.keys()]
|
|
601
|
+
const concurrentTypes = [...concurrentRuns.keys()]
|
|
602
|
+
const consumers = [
|
|
603
|
+
...(serialTypes.length === 0
|
|
604
|
+
? []
|
|
605
|
+
: [
|
|
606
|
+
runtimeConsumer({
|
|
607
|
+
contract,
|
|
608
|
+
name: 'a2.actor',
|
|
609
|
+
types: serialTypes,
|
|
610
|
+
lane: () => ACTOR_LANE,
|
|
611
|
+
}).run((ctx) => serialRuns.get(ctx.event.type)!(ctx as never)),
|
|
612
|
+
]),
|
|
613
|
+
...(concurrentTypes.length === 0
|
|
614
|
+
? []
|
|
615
|
+
: [
|
|
616
|
+
runtimeConsumer({
|
|
617
|
+
contract,
|
|
618
|
+
name: 'a2.actor.concurrent',
|
|
619
|
+
types: concurrentTypes,
|
|
620
|
+
}).run((ctx) => concurrentRuns.get(ctx.event.type)!(ctx as never)),
|
|
621
|
+
]),
|
|
622
|
+
]
|
|
593
623
|
const server = createServer({
|
|
594
624
|
contract,
|
|
595
|
-
|
|
625
|
+
consumers,
|
|
596
626
|
...(options.store ? { store: options.store } : {}),
|
|
597
627
|
...(options.scheduler ? { scheduler: options.scheduler } : {}),
|
|
598
628
|
...(options.telemetry ? { telemetry: options.telemetry } : {}),
|