experimental-a2 0.16.0 → 0.17.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 +360 -0
- package/dist/{actor-ohPC-81x.d.ts → actor-DBbe5xeE.d.ts} +21 -11
- package/dist/actor-DBbe5xeE.d.ts.map +1 -0
- package/dist/actor-client.d.ts +2 -5
- package/dist/actor-client.d.ts.map +1 -1
- package/dist/actor-client.js +13 -4
- package/dist/actor-client.js.map +1 -1
- package/dist/actor-react.d.ts +5 -3
- package/dist/actor-react.d.ts.map +1 -1
- package/dist/actor-react.js +20 -10
- package/dist/actor-react.js.map +1 -1
- package/dist/{actor-shared-B5tJfzt-.js → actor-shared-t7w8xIKP.js} +12 -7
- package/dist/actor-shared-t7w8xIKP.js.map +1 -0
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +42 -23
- package/dist/actor.js.map +1 -1
- package/dist/{ai-CrLYwNEx.js → ai-UrWfKeuv.js} +966 -541
- package/dist/ai-UrWfKeuv.js.map +1 -0
- package/dist/ai-client.d.ts +27 -0
- package/dist/ai-client.d.ts.map +1 -0
- package/dist/ai-client.js +29 -0
- package/dist/ai-client.js.map +1 -0
- package/dist/ai-react.d.ts +96 -0
- package/dist/ai-react.d.ts.map +1 -0
- package/dist/ai-react.js +142 -0
- package/dist/ai-react.js.map +1 -0
- package/dist/ai-server.browser.js +2 -2
- package/dist/ai-server.browser.js.map +1 -1
- package/dist/ai-server.d.ts +80 -36
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +466 -265
- package/dist/ai-server.js.map +1 -1
- package/dist/{ai-HJ9fHfYI.d.ts → ai-vpGtcaCB.d.ts} +115 -101
- package/dist/ai-vpGtcaCB.d.ts.map +1 -0
- package/dist/ai.d.ts +2 -2
- package/dist/ai.js +2 -2
- package/dist/{client-BYzHjkwU.js → client-COaiaDub.js} +447 -80
- package/dist/client-COaiaDub.js.map +1 -0
- package/dist/{client-CzyacQpJ.d.ts → client-qRnRUzBS.d.ts} +14 -13
- package/dist/client-qRnRUzBS.d.ts.map +1 -0
- package/dist/client.d.ts +4 -2
- package/dist/client.js +1 -1
- package/dist/{contract-CKRg_E4q.js → contract-BYKASlCV.js} +12 -21
- package/dist/contract-BYKASlCV.js.map +1 -0
- package/dist/{idempotent-replay-DVOlyYbx.js → idempotent-replay-JdgMQRRm.js} +2 -2
- package/dist/{idempotent-replay-DVOlyYbx.js.map → idempotent-replay-JdgMQRRm.js.map} +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/{errors-DvhSXnxk.d.ts → initial-snapshot-BrLso5x_.d.ts} +7 -3
- package/dist/initial-snapshot-BrLso5x_.d.ts.map +1 -0
- package/dist/{internal-Dq2qYxou.js → internal-CbVvEwad.js} +23 -2
- package/dist/internal-CbVvEwad.js.map +1 -0
- package/dist/otel.d.ts +1 -1
- package/dist/react-BbJ-g6Uu.d.ts +86 -0
- package/dist/react-BbJ-g6Uu.d.ts.map +1 -0
- package/dist/react.d.ts +2 -122
- package/dist/react.js +107 -100
- package/dist/react.js.map +1 -1
- package/dist/reducer-BAXc8JyF.js +87 -0
- package/dist/reducer-BAXc8JyF.js.map +1 -0
- package/dist/{reducer-otzHjuJj.d.ts → reducer-BYu6DT7h.d.ts} +45 -29
- package/dist/reducer-BYu6DT7h.d.ts.map +1 -0
- package/dist/reducer-runtime-DQwz_J59.js +28 -0
- package/dist/reducer-runtime-DQwz_J59.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +3 -3
- package/dist/scheduler-qstash.d.ts.map +1 -1
- package/dist/scheduler-qstash.js +33 -10
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/{scheduler-task-BpzhPnRS.js → scheduler-task-X1SToDjP.js} +18 -3
- package/dist/scheduler-task-X1SToDjP.js.map +1 -0
- package/dist/scheduler-vercel.d.ts +4 -9
- package/dist/scheduler-vercel.d.ts.map +1 -1
- package/dist/scheduler-vercel.js +25 -44
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-DbD7IJVK.d.ts → server-CGM8WzRG.d.ts} +87 -26
- package/dist/server-CGM8WzRG.d.ts.map +1 -0
- package/dist/{server-BD5ckxZb.js → server-Rj64fNvX.js} +854 -202
- package/dist/server-Rj64fNvX.js.map +1 -0
- package/dist/server.d.ts +4 -4
- package/dist/server.js +2 -2
- package/dist/{errors-DCk6ch5n.js → session-clear-DGI4jTQ5.js} +17 -2
- package/dist/session-clear-DGI4jTQ5.js.map +1 -0
- package/dist/session-clear-Dshpuhff.d.ts +12 -0
- package/dist/session-clear-Dshpuhff.d.ts.map +1 -0
- package/dist/{store-DyZM6fS5.d.ts → store-Gpkq7WBo.d.ts} +44 -3
- package/dist/store-Gpkq7WBo.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +182 -69
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-CmxUbV93.js → store-polling-4_y8lETm.js} +21 -4
- package/dist/store-polling-4_y8lETm.js.map +1 -0
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +399 -108
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-presence-polling-C7-XZyW9.js → store-presence-polling-Q1C-k2dd.js} +3 -3
- package/dist/{store-presence-polling-C7-XZyW9.js.map → store-presence-polling-Q1C-k2dd.js.map} +1 -1
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.d.ts.map +1 -1
- package/dist/store-redis-http.js +293 -11
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis-notify-tqWNnMRu.js +2374 -0
- package/dist/store-redis-notify-tqWNnMRu.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 +11 -11
- 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 +166 -32
- package/dist/store-sqlite.js.map +1 -1
- package/dist/stream-buffer-Cx7AAPGq.js +23 -0
- package/dist/stream-buffer-Cx7AAPGq.js.map +1 -0
- package/dist/{telemetry-B5jzpy6y.d.ts → telemetry-cZH-cibj.d.ts} +2 -2
- package/dist/telemetry-cZH-cibj.d.ts.map +1 -0
- package/dist/{wire-BO5wWCb1.js → wire-DjfHOvdW.js} +132 -10
- package/dist/wire-DjfHOvdW.js.map +1 -0
- package/docs/01-quickstart.mdx +2 -2
- package/docs/actors/01-introduction.mdx +4 -0
- package/docs/actors/04-routes.mdx +14 -1
- package/docs/concepts/01-contracts.mdx +95 -0
- package/docs/concepts/04-state.mdx +3 -3
- package/docs/guides/01-timers.mdx +22 -14
- package/docs/guides/02-cancellation.mdx +1 -1
- package/docs/guides/03-react.mdx +268 -328
- package/docs/guides/04-local-first.mdx +32 -73
- package/docs/guides/05-production.mdx +40 -27
- package/docs/guides/06-ai-agents.mdx +680 -580
- package/docs/guides/09-presence.mdx +3 -7
- package/docs/guides/10-transports.mdx +5 -5
- package/docs/index.mdx +10 -18
- package/docs/reference/01-api.mdx +662 -195
- package/docs/reference/02-errors.mdx +2 -1
- package/examples/playground/DEPLOY.md +52 -6
- package/examples/playground/app/a0/[appId]/loading.tsx +16 -0
- package/examples/playground/app/a0/[appId]/page.tsx +48 -0
- package/examples/playground/app/a0/a0.css +818 -0
- package/examples/playground/app/a0/app-list.tsx +211 -0
- package/examples/playground/app/a0/app-view.tsx +55 -0
- package/examples/playground/app/a0/apps/[appId]/preview/route.ts +27 -0
- package/examples/playground/app/a0/apps/[appId]/route.ts +23 -0
- package/examples/playground/app/a0/apps.test.ts +116 -0
- package/examples/playground/app/a0/apps.ts +87 -0
- package/examples/playground/app/a0/chat.tsx +364 -0
- package/examples/playground/app/a0/columns.tsx +109 -0
- package/examples/playground/app/a0/context-menu.tsx +116 -0
- package/examples/playground/app/a0/cookies.ts +46 -0
- package/examples/playground/app/a0/crash-on-request.ts +72 -0
- package/examples/playground/app/a0/crashes/route.ts +3 -0
- package/examples/playground/app/a0/crashes.test.ts +33 -0
- package/examples/playground/app/a0/crashes.ts +28 -0
- package/examples/playground/app/a0/devtools.tsx +164 -0
- package/examples/playground/app/a0/events/route.ts +6 -0
- package/examples/playground/app/a0/flags.ts +30 -0
- package/examples/playground/app/a0/layout.tsx +38 -0
- package/examples/playground/app/a0/list/route.ts +10 -0
- package/examples/playground/app/a0/model.test.ts +56 -0
- package/examples/playground/app/a0/model.ts +58 -0
- package/examples/playground/app/a0/page.tsx +14 -0
- package/examples/playground/app/a0/preview-pane.tsx +145 -0
- package/examples/playground/app/a0/preview.test.ts +53 -0
- package/examples/playground/app/a0/preview.ts +26 -0
- package/examples/playground/app/a0/sandbox-log.ts +69 -0
- package/examples/playground/app/a0/sandbox.ts +191 -0
- package/examples/playground/app/a0/sandboxes/scheduler/route.ts +6 -0
- package/examples/playground/app/a0/scheduler/route.ts +7 -0
- package/examples/playground/app/a0/server.ts +158 -0
- package/examples/playground/app/a0/session.ts +47 -0
- package/examples/playground/app/a0/shell.tsx +150 -0
- package/examples/playground/app/a0/short/events/route.ts +9 -0
- package/examples/playground/app/a0/splitter.tsx +120 -0
- package/examples/playground/app/agent/[agentId]/agent-client.tsx +144 -174
- package/examples/playground/app/agent/[agentId]/command-receipt.test.tsx +65 -0
- package/examples/playground/app/agent/[agentId]/command-receipt.tsx +27 -0
- package/examples/playground/app/agent/[agentId]/compaction-event.tsx +25 -24
- package/examples/playground/app/agent/[agentId]/compaction-panel.test.tsx +119 -0
- package/examples/playground/app/agent/[agentId]/compaction-panel.tsx +208 -165
- package/examples/playground/app/agent/[agentId]/page.tsx +2 -16
- package/examples/playground/app/agent/events/route.ts +5 -2
- package/examples/playground/app/agent/ingress.test.ts +98 -0
- package/examples/playground/app/agent/model-input.test.tsx +58 -0
- package/examples/playground/app/agent/model-input.tsx +115 -0
- package/examples/playground/app/agent/model.test.ts +73 -0
- package/examples/playground/app/agent/model.ts +58 -26
- package/examples/playground/app/agent/new-agent-session.tsx +37 -19
- package/examples/playground/app/agent/page.tsx +4 -4
- package/examples/playground/app/agent/server.ts +15 -14
- package/examples/playground/app/agent/session.ts +5 -6
- package/examples/playground/app/agent/telemetry.test.ts +168 -0
- package/examples/playground/app/agent/telemetry.ts +30 -0
- package/examples/playground/app/agent/trace.test.ts +23 -0
- package/examples/playground/app/agent/trace.ts +18 -0
- package/examples/playground/app/canvas/[canvasId]/canvas-client.tsx +11 -11
- package/examples/playground/app/canvas/[canvasId]/page.tsx +1 -2
- package/examples/playground/app/canvas/[canvasId]/session.ts +2 -2
- package/examples/playground/app/canvas/model.ts +53 -53
- package/examples/playground/app/chat/[chatId]/agent-state.test.ts +176 -0
- package/examples/playground/app/chat/[chatId]/agent-stream-drawer.tsx +16 -1
- package/examples/playground/app/chat/[chatId]/chat-client.tsx +55 -77
- package/examples/playground/app/chat/[chatId]/chat-view.ts +37 -3
- package/examples/playground/app/chat/[chatId]/page.tsx +7 -15
- package/examples/playground/app/chat/[chatId]/session.ts +2 -19
- package/examples/playground/app/chat/agent/route.ts +9 -0
- package/examples/playground/app/chat/model.test.ts +46 -119
- package/examples/playground/app/chat/model.ts +82 -128
- package/examples/playground/app/chat/scheduler/route.ts +2 -2
- package/examples/playground/app/chat/server.test.ts +108 -0
- package/examples/playground/app/chat/server.ts +69 -73
- package/examples/playground/app/components/activity-feed.tsx +30 -1
- package/examples/playground/app/components/activity-history.test.tsx +35 -0
- package/examples/playground/app/components/demo-icons.tsx +89 -0
- package/examples/playground/app/components/demos.tsx +33 -0
- package/examples/playground/app/components/fast-link.test.tsx +58 -0
- package/examples/playground/app/components/fast-link.tsx +55 -0
- package/examples/playground/app/components/site-header.tsx +96 -0
- package/examples/playground/app/counter/counter-client.tsx +3 -3
- package/examples/playground/app/counter/model.test.ts +3 -3
- package/examples/playground/app/counter/model.ts +11 -21
- package/examples/playground/app/counter/page.tsx +2 -15
- package/examples/playground/app/counter/session.ts +2 -2
- package/examples/playground/app/documents/[documentId]/code-editor.tsx +12 -1
- package/examples/playground/app/documents/[documentId]/document-client.tsx +10 -11
- package/examples/playground/app/documents/[documentId]/page.tsx +1 -2
- package/examples/playground/app/documents/[documentId]/session.ts +2 -2
- package/examples/playground/app/documents/model.ts +27 -27
- package/examples/playground/app/globals.css +420 -207
- package/examples/playground/app/initial-state.test.tsx +54 -0
- package/examples/playground/app/layout.tsx +7 -23
- package/examples/playground/app/orders/[orderId]/page.tsx +1 -2
- package/examples/playground/app/orders/[orderId]/session.ts +2 -2
- package/examples/playground/app/orders/model.ts +49 -49
- package/examples/playground/app/page.tsx +13 -102
- package/examples/playground/app/recovery/[recoveryId]/page.tsx +1 -2
- package/examples/playground/app/recovery/[recoveryId]/session.ts +2 -2
- package/examples/playground/app/recovery/model.ts +30 -30
- package/examples/playground/instrumentation.ts +5 -0
- package/examples/playground/package.json +10 -2
- package/examples/playground/scripts/a0-template.ts +77 -0
- package/examples/playground/vercel.json +18 -0
- package/package.json +3 -1
- package/src/actor-client.ts +16 -4
- package/src/actor-react.ts +25 -11
- package/src/actor-shared.ts +21 -6
- package/src/actor.ts +76 -31
- package/src/ai-agent-actions.ts +219 -0
- package/src/ai-agent.ts +2343 -0
- package/src/ai-app-state.ts +91 -0
- package/src/ai-client-state.ts +39 -2
- package/src/ai-client.ts +62 -0
- package/src/ai-context-schema.ts +25 -5
- package/src/ai-context.ts +90 -40
- package/src/ai-control-server.ts +94 -55
- package/src/ai-control.ts +2 -0
- package/src/ai-coordinator.ts +23 -22
- package/src/ai-message-base.ts +209 -0
- package/src/ai-message-projection.ts +382 -58
- package/src/ai-progress-batches.ts +26 -0
- package/src/ai-projector.ts +95 -14
- package/src/ai-react.ts +146 -0
- package/src/ai-server.browser.ts +2 -2
- package/src/ai-server.ts +20 -1951
- package/src/ai-stored-state.ts +48 -163
- package/src/ai.ts +343 -276
- package/src/bootstrap-stream.ts +72 -0
- package/src/bounded-cache.ts +55 -0
- package/src/client-render-session.ts +49 -0
- package/src/client.ts +688 -151
- package/src/contract.ts +92 -42
- package/src/deterministic-id.ts +37 -0
- package/src/equal-json.ts +32 -0
- package/src/errors.ts +13 -1
- package/src/index.ts +5 -1
- package/src/initial-snapshot.ts +39 -0
- package/src/internal.ts +45 -0
- package/src/postgres-notification-scope.ts +14 -3
- package/src/react.ts +171 -206
- package/src/redis-http-reply.ts +256 -0
- package/src/redis-http-stream.ts +73 -0
- package/src/redis-http-subscriptions.ts +13 -5
- package/src/reducer-runtime.ts +52 -0
- package/src/reducer.ts +92 -10
- package/src/scheduler-qstash.ts +49 -20
- package/src/scheduler-task.ts +34 -2
- package/src/scheduler-vercel.ts +32 -84
- package/src/server-fetch.ts +139 -35
- package/src/server.ts +957 -237
- package/src/session-clear.ts +23 -0
- package/src/session-socket.ts +154 -49
- package/src/sse.ts +71 -4
- package/src/store-memory.ts +220 -78
- package/src/store-polling.ts +18 -4
- package/src/store-postgres.ts +461 -114
- package/src/store-redis-core.ts +1260 -434
- package/src/store-redis-http.ts +18 -2
- package/src/store-redis-notify.ts +260 -85
- package/src/store-redis.ts +19 -9
- package/src/store-sqlite.ts +259 -45
- package/src/store.ts +46 -1
- package/src/stream-buffer.ts +43 -0
- package/src/telemetry.ts +13 -1
- package/src/wire.ts +241 -13
- package/dist/actor-ohPC-81x.d.ts.map +0 -1
- package/dist/actor-shared-B5tJfzt-.js.map +0 -1
- package/dist/ai-CrLYwNEx.js.map +0 -1
- package/dist/ai-HJ9fHfYI.d.ts.map +0 -1
- package/dist/client-BYzHjkwU.js.map +0 -1
- package/dist/client-CzyacQpJ.d.ts.map +0 -1
- package/dist/contract-CKRg_E4q.js.map +0 -1
- package/dist/errors-DCk6ch5n.js.map +0 -1
- package/dist/errors-DvhSXnxk.d.ts.map +0 -1
- package/dist/internal-Dq2qYxou.js.map +0 -1
- package/dist/react.d.ts.map +0 -1
- package/dist/reducer-DEMjEY_O.js +0 -29
- package/dist/reducer-DEMjEY_O.js.map +0 -1
- package/dist/reducer-otzHjuJj.d.ts.map +0 -1
- package/dist/scheduler-task-BpzhPnRS.js.map +0 -1
- package/dist/server-BD5ckxZb.js.map +0 -1
- package/dist/server-DbD7IJVK.d.ts.map +0 -1
- package/dist/store-DyZM6fS5.d.ts.map +0 -1
- package/dist/store-polling-CmxUbV93.js.map +0 -1
- package/dist/store-redis-notify-BVLUfI3j.js +0 -1583
- package/dist/store-redis-notify-BVLUfI3j.js.map +0 -1
- package/dist/telemetry-B5jzpy6y.d.ts.map +0 -1
- package/dist/validate-XKT4FSNn.js +0 -30
- package/dist/validate-XKT4FSNn.js.map +0 -1
- package/dist/wire-BO5wWCb1.js.map +0 -1
- package/examples/playground/app/agent/[agentId]/compaction/route.ts +0 -14
- package/examples/playground/app/agent/compaction-settings.test.ts +0 -144
- package/examples/playground/app/agent/compaction-settings.ts +0 -49
- package/examples/playground/app/agent/compaction-timeline.test.ts +0 -377
- package/examples/playground/app/agent/compaction-timeline.ts +0 -208
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { A2Error } from './errors.ts'
|
|
2
|
+
import type { Event } from './store.ts'
|
|
3
|
+
|
|
4
|
+
/** A2's own event types start with this; application contracts cannot. */
|
|
5
|
+
export const RESERVED_EVENT_PREFIX = 'a2.'
|
|
6
|
+
|
|
7
|
+
/** Reserved type of the marker a clear appends. */
|
|
8
|
+
export const CLEAR_EVENT_TYPE = 'a2.cleared'
|
|
9
|
+
|
|
10
|
+
/** The marker `session.stream()` yields where a clear ended a lifetime. */
|
|
11
|
+
export type SessionCleared = Event & {
|
|
12
|
+
type: typeof CLEAR_EVENT_TYPE
|
|
13
|
+
payload: null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Accepts any stream item, including presence patches and snapshots. */
|
|
17
|
+
export function isSessionCleared(item: object): item is SessionCleared {
|
|
18
|
+
return (item as { type?: unknown }).type === CLEAR_EVENT_TYPE
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function sessionClearedError(message: string): A2Error {
|
|
22
|
+
return new A2Error('SESSION_CLEARED', message)
|
|
23
|
+
}
|
package/src/session-socket.ts
CHANGED
|
@@ -15,10 +15,18 @@ import type { ParsedPushEvent, ParsedPushPresence } from './push-envelope.ts'
|
|
|
15
15
|
import type { Session } from './server.ts'
|
|
16
16
|
import { parsePresenceSibling, parsePushEvents } from './push-envelope.ts'
|
|
17
17
|
import {
|
|
18
|
+
type StateSnapshotFrame,
|
|
19
|
+
type SocketSubscribeEntry,
|
|
20
|
+
type SubscriptionStart,
|
|
21
|
+
type WireVersion,
|
|
18
22
|
SOCKET_PING_FRAME,
|
|
19
23
|
asA2Error,
|
|
24
|
+
compactEventJson,
|
|
25
|
+
heldEventBatch,
|
|
26
|
+
isStreamEvent,
|
|
20
27
|
socketAckFor,
|
|
21
28
|
socketErrorAckFor,
|
|
29
|
+
socketEventsFor,
|
|
22
30
|
socketFrameFor,
|
|
23
31
|
socketSubscribedFor,
|
|
24
32
|
socketUnsubscribedFor,
|
|
@@ -78,7 +86,9 @@ export type SocketSession = {
|
|
|
78
86
|
stream(opts: {
|
|
79
87
|
startAfter: number
|
|
80
88
|
presence?: boolean
|
|
81
|
-
}): AsyncIterable<
|
|
89
|
+
}): AsyncIterable<
|
|
90
|
+
Event | PresencePatch | PresenceSnapshot | StateSnapshotFrame
|
|
91
|
+
>
|
|
82
92
|
append(...events: ParsedPushEvent[]): Promise<Event[]>
|
|
83
93
|
setPresence?(patch: ParsedPushPresence): Promise<void>
|
|
84
94
|
}
|
|
@@ -238,8 +248,12 @@ export type SessionsSocketResolve<D extends EventDefs> = (
|
|
|
238
248
|
sessionId: string,
|
|
239
249
|
/** The entry's resume frontier — `stream({ startAfter })`. */
|
|
240
250
|
startAfter: number,
|
|
251
|
+
selection?: SubscriptionStart,
|
|
241
252
|
) =>
|
|
242
|
-
|
|
253
|
+
| Pick<Session<D>, 'stream'>
|
|
254
|
+
| Pick<SocketSession, 'stream'>
|
|
255
|
+
| null
|
|
256
|
+
| Promise<Pick<Session<D>, 'stream'> | Pick<SocketSession, 'stream'> | null>
|
|
243
257
|
|
|
244
258
|
export type SessionsSocketOptions = {
|
|
245
259
|
/** The `stream()` presence opt-in for every session on the socket —
|
|
@@ -248,6 +262,8 @@ export type SessionsSocketOptions = {
|
|
|
248
262
|
/** Epoch ms: close cleanly (code 1000) at this time, ahead of a known
|
|
249
263
|
* platform deadline, so clients reconnect on our schedule. */
|
|
250
264
|
deadline?: number
|
|
265
|
+
/** `2` sends live events as `events` batches (the client's `wire=2`). */
|
|
266
|
+
wire?: WireVersion
|
|
251
267
|
/** Runs after frame parsing and before append or presence I/O. */
|
|
252
268
|
gatePush?(
|
|
253
269
|
sessionId: string,
|
|
@@ -262,32 +278,48 @@ export type SessionSocketOptions = SessionsSocketOptions & {
|
|
|
262
278
|
|
|
263
279
|
type SocketSubscription = {
|
|
264
280
|
target: SocketSession
|
|
265
|
-
iterator: AsyncIterator<
|
|
281
|
+
iterator: AsyncIterator<
|
|
282
|
+
Event | PresencePatch | PresenceSnapshot | StateSnapshotFrame
|
|
283
|
+
>
|
|
266
284
|
stopped: boolean
|
|
285
|
+
attempt?: number
|
|
267
286
|
}
|
|
268
287
|
|
|
269
|
-
const socketSubscription = (
|
|
270
|
-
target: SocketSession
|
|
271
|
-
startAfter: number
|
|
272
|
-
presence: boolean
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
})
|
|
288
|
+
const socketSubscription = (options: {
|
|
289
|
+
target: SocketSession
|
|
290
|
+
startAfter: number
|
|
291
|
+
presence: boolean
|
|
292
|
+
attempt?: number | undefined
|
|
293
|
+
}): SocketSubscription => {
|
|
294
|
+
const { target, startAfter, presence, attempt } = options
|
|
295
|
+
return {
|
|
296
|
+
target,
|
|
297
|
+
iterator: (presence
|
|
298
|
+
? target.stream({ startAfter, presence: true })
|
|
299
|
+
: target.stream({ startAfter }))[Symbol.asyncIterator](),
|
|
300
|
+
stopped: false,
|
|
301
|
+
...(attempt === undefined ? {} : { attempt }),
|
|
302
|
+
}
|
|
303
|
+
}
|
|
280
304
|
|
|
281
305
|
const stopSubscription = (subscription: SocketSubscription): void => {
|
|
282
306
|
subscription.stopped = true
|
|
283
307
|
void subscription.iterator.return?.()?.catch(() => {})
|
|
284
308
|
}
|
|
285
309
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
310
|
+
/**
|
|
311
|
+
* Sends a subscription's items until it ends. With `batch`, an event and
|
|
312
|
+
* the events its iterator already holds go out as one frame.
|
|
313
|
+
*/
|
|
314
|
+
const pumpSubscription = async (options: {
|
|
315
|
+
shell: SocketShell
|
|
316
|
+
subscription: SocketSubscription
|
|
317
|
+
frameFor: (
|
|
318
|
+
item: Event | PresencePatch | PresenceSnapshot | StateSnapshotFrame,
|
|
319
|
+
) => string
|
|
320
|
+
batch?: (events: string[]) => string
|
|
321
|
+
}): Promise<void> => {
|
|
322
|
+
const { shell, subscription, frameFor, batch } = options
|
|
291
323
|
for (;;) {
|
|
292
324
|
while (
|
|
293
325
|
!shell.torn() &&
|
|
@@ -301,7 +333,16 @@ const pumpSubscription = async (
|
|
|
301
333
|
// oxlint-disable-next-line no-await-in-loop -- stream pump
|
|
302
334
|
const { value, done } = await subscription.iterator.next()
|
|
303
335
|
if (shell.torn() || subscription.stopped || done) return
|
|
304
|
-
|
|
336
|
+
if (!batch || !isStreamEvent(value)) {
|
|
337
|
+
shell.send(frameFor(value))
|
|
338
|
+
continue
|
|
339
|
+
}
|
|
340
|
+
const { events } = heldEventBatch({
|
|
341
|
+
first: value,
|
|
342
|
+
iterator: subscription.iterator,
|
|
343
|
+
encode: compactEventJson,
|
|
344
|
+
})
|
|
345
|
+
shell.send(batch(events))
|
|
305
346
|
}
|
|
306
347
|
}
|
|
307
348
|
|
|
@@ -370,16 +411,16 @@ const applySocketPresence = async (
|
|
|
370
411
|
|
|
371
412
|
export function sessionSocket<D extends EventDefs>(
|
|
372
413
|
sessionId: string,
|
|
373
|
-
session: Pick<Session<D>, 'stream'>,
|
|
414
|
+
session: Pick<Session<D>, 'stream'> | Pick<SocketSession, 'stream'>,
|
|
374
415
|
socket: A2Socket,
|
|
375
416
|
options?: SessionSocketOptions,
|
|
376
417
|
): void {
|
|
377
418
|
const target = session as unknown as SocketSession
|
|
378
|
-
const subscription = socketSubscription(
|
|
419
|
+
const subscription = socketSubscription({
|
|
379
420
|
target,
|
|
380
|
-
options?.startAfter ?? 0,
|
|
381
|
-
options?.presence === true,
|
|
382
|
-
)
|
|
421
|
+
startAfter: options?.startAfter ?? 0,
|
|
422
|
+
presence: options?.presence === true,
|
|
423
|
+
})
|
|
383
424
|
|
|
384
425
|
const handleFrame = (frame: Record<string, unknown>): void => {
|
|
385
426
|
switch (frame['kind']) {
|
|
@@ -458,7 +499,14 @@ export function sessionSocket<D extends EventDefs>(
|
|
|
458
499
|
options?.deadline,
|
|
459
500
|
)
|
|
460
501
|
|
|
461
|
-
void pumpSubscription(
|
|
502
|
+
void pumpSubscription({
|
|
503
|
+
shell,
|
|
504
|
+
subscription,
|
|
505
|
+
frameFor: socketFrameFor,
|
|
506
|
+
...(options?.wire === 2
|
|
507
|
+
? { batch: (events) => socketEventsFor(events) }
|
|
508
|
+
: {}),
|
|
509
|
+
}).then(
|
|
462
510
|
() => shell.shutdown(1000),
|
|
463
511
|
() => shell.shutdown(1011, 'stream failed'),
|
|
464
512
|
)
|
|
@@ -466,17 +514,46 @@ export function sessionSocket<D extends EventDefs>(
|
|
|
466
514
|
|
|
467
515
|
const parseSubscribeEntries = (
|
|
468
516
|
value: unknown,
|
|
469
|
-
):
|
|
517
|
+
): SocketSubscribeEntry[] | null => {
|
|
470
518
|
if (!Array.isArray(value) || value.length === 0) return null
|
|
471
|
-
const entries:
|
|
519
|
+
const entries: SocketSubscribeEntry[] = []
|
|
472
520
|
for (const entry of value) {
|
|
473
521
|
if (entry === null || typeof entry !== 'object') return null
|
|
474
|
-
const { id, index } = entry as Record<
|
|
522
|
+
const { id, attempt, index, bootstrap, reducer } = entry as Record<
|
|
523
|
+
string,
|
|
524
|
+
unknown
|
|
525
|
+
>
|
|
475
526
|
if (typeof id !== 'string' || id.length === 0) return null
|
|
476
|
-
if (
|
|
527
|
+
if (
|
|
528
|
+
attempt !== undefined &&
|
|
529
|
+
(typeof attempt !== 'number' ||
|
|
530
|
+
!Number.isSafeInteger(attempt) ||
|
|
531
|
+
attempt < 0)
|
|
532
|
+
)
|
|
477
533
|
return null
|
|
534
|
+
if (
|
|
535
|
+
reducer !== undefined &&
|
|
536
|
+
(typeof reducer !== 'string' || reducer.length === 0)
|
|
537
|
+
)
|
|
538
|
+
return null
|
|
539
|
+
const lane = { id, ...(attempt === undefined ? {} : { attempt }) }
|
|
540
|
+
if (bootstrap === true) {
|
|
541
|
+
if (index !== undefined || typeof reducer !== 'string') return null
|
|
542
|
+
entries.push({ ...lane, bootstrap: true, reducer })
|
|
543
|
+
} else {
|
|
544
|
+
if (
|
|
545
|
+
bootstrap !== undefined ||
|
|
546
|
+
typeof index !== 'number' ||
|
|
547
|
+
!Number.isSafeInteger(index) ||
|
|
548
|
+
index < 0
|
|
549
|
+
)
|
|
550
|
+
return null
|
|
551
|
+
entries.push({
|
|
552
|
+
...lane,
|
|
553
|
+
index,
|
|
554
|
+
...(reducer === undefined ? {} : { reducer: reducer as string }),
|
|
555
|
+
})
|
|
478
556
|
}
|
|
479
|
-
entries.push({ id, index })
|
|
480
557
|
}
|
|
481
558
|
return entries
|
|
482
559
|
}
|
|
@@ -509,53 +586,81 @@ export function sessionsSocket<D extends EventDefs>(
|
|
|
509
586
|
id: string,
|
|
510
587
|
subscription: SocketSubscription,
|
|
511
588
|
): Promise<void> => {
|
|
589
|
+
const { attempt } = subscription
|
|
512
590
|
try {
|
|
513
|
-
await pumpSubscription(
|
|
514
|
-
|
|
515
|
-
|
|
591
|
+
await pumpSubscription({
|
|
592
|
+
shell,
|
|
593
|
+
subscription,
|
|
594
|
+
frameFor: (item) => socketFrameFor(item, id),
|
|
595
|
+
...(options?.wire === 2
|
|
596
|
+
? { batch: (events) => socketEventsFor(events, id) }
|
|
597
|
+
: {}),
|
|
598
|
+
})
|
|
516
599
|
if (shell.torn() || subscription.stopped) return
|
|
517
600
|
retire(id, subscription)
|
|
518
|
-
shell.send(socketUnsubscribedFor(id))
|
|
601
|
+
shell.send(socketUnsubscribedFor({ sessionId: id, attempt }))
|
|
519
602
|
} catch (error) {
|
|
520
603
|
if (shell.torn() || subscription.stopped) return
|
|
521
604
|
retire(id, subscription)
|
|
522
|
-
shell.send(
|
|
605
|
+
shell.send(
|
|
606
|
+
socketUnsubscribedFor({
|
|
607
|
+
sessionId: id,
|
|
608
|
+
attempt,
|
|
609
|
+
reason: 'stream failed',
|
|
610
|
+
error: asA2Error(error),
|
|
611
|
+
}),
|
|
612
|
+
)
|
|
523
613
|
}
|
|
524
614
|
}
|
|
525
615
|
|
|
526
616
|
const startSubscription = async (
|
|
527
|
-
|
|
528
|
-
index: number,
|
|
617
|
+
entry: SocketSubscribeEntry,
|
|
529
618
|
): Promise<void> => {
|
|
619
|
+
const { id, attempt } = entry
|
|
620
|
+
const index = entry.index ?? 0
|
|
530
621
|
const existing = subscriptions.get(id)
|
|
531
622
|
if (existing) {
|
|
532
623
|
stopSubscription(existing)
|
|
533
624
|
subscriptions.delete(id)
|
|
534
625
|
}
|
|
535
|
-
let session:
|
|
626
|
+
let session:
|
|
627
|
+
Pick<Session<D>, 'stream'> | Pick<SocketSession, 'stream'> | null
|
|
536
628
|
try {
|
|
537
|
-
session = await resolve(id, index)
|
|
629
|
+
session = await resolve(id, index, entry)
|
|
538
630
|
} catch (error) {
|
|
539
631
|
if (!shell.torn()) {
|
|
540
|
-
shell.send(
|
|
632
|
+
shell.send(
|
|
633
|
+
socketUnsubscribedFor({
|
|
634
|
+
sessionId: id,
|
|
635
|
+
attempt,
|
|
636
|
+
reason: 'stream failed',
|
|
637
|
+
error: asA2Error(error),
|
|
638
|
+
}),
|
|
639
|
+
)
|
|
541
640
|
}
|
|
542
641
|
return
|
|
543
642
|
}
|
|
544
643
|
if (shell.torn()) return
|
|
545
644
|
if (session === null) {
|
|
546
645
|
shell.send(
|
|
547
|
-
socketUnsubscribedFor(
|
|
548
|
-
id,
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
646
|
+
socketUnsubscribedFor({
|
|
647
|
+
sessionId: id,
|
|
648
|
+
attempt,
|
|
649
|
+
reason: 'subscribe rejected',
|
|
650
|
+
error: new A2Error('FORBIDDEN', 'the subscription is not authorized'),
|
|
651
|
+
}),
|
|
552
652
|
)
|
|
553
653
|
return
|
|
554
654
|
}
|
|
555
655
|
const target = session as unknown as SocketSession
|
|
556
|
-
const subscription = socketSubscription(
|
|
656
|
+
const subscription = socketSubscription({
|
|
657
|
+
target,
|
|
658
|
+
startAfter: index,
|
|
659
|
+
presence,
|
|
660
|
+
attempt,
|
|
661
|
+
})
|
|
557
662
|
subscriptions.set(id, subscription)
|
|
558
|
-
shell.send(socketSubscribedFor(id))
|
|
663
|
+
shell.send(socketSubscribedFor({ sessionId: id, attempt }))
|
|
559
664
|
void runSubscription(id, subscription)
|
|
560
665
|
}
|
|
561
666
|
|
|
@@ -628,7 +733,7 @@ export function sessionsSocket<D extends EventDefs>(
|
|
|
628
733
|
return
|
|
629
734
|
}
|
|
630
735
|
for (const entry of entries) {
|
|
631
|
-
control(() => startSubscription(entry
|
|
736
|
+
control(() => startSubscription(entry))
|
|
632
737
|
}
|
|
633
738
|
return
|
|
634
739
|
}
|
package/src/sse.ts
CHANGED
|
@@ -12,14 +12,31 @@ import type { PresencePatch, PresenceSnapshot } from './contract.ts'
|
|
|
12
12
|
import { invocationDeadlineMs } from './platform.ts'
|
|
13
13
|
import { streamActivities } from './stream-activity.ts'
|
|
14
14
|
import {
|
|
15
|
+
isSerializedEvent,
|
|
16
|
+
parseSerializedEvent,
|
|
17
|
+
takeBuffered,
|
|
18
|
+
type HeldEvent,
|
|
19
|
+
} from './stream-buffer.ts'
|
|
20
|
+
import {
|
|
21
|
+
type StateSnapshotFrame,
|
|
22
|
+
type WireVersion,
|
|
23
|
+
EVENT_WRITE_CHARS,
|
|
24
|
+
compactEventJson,
|
|
25
|
+
heldEventBatch,
|
|
26
|
+
isStreamEvent,
|
|
27
|
+
stateSnapshotToWire,
|
|
15
28
|
eventToWire,
|
|
29
|
+
serializedEventToWireJson,
|
|
16
30
|
presencePatchToWire,
|
|
17
31
|
presenceSnapshotToWire,
|
|
18
32
|
} from './wire.ts'
|
|
19
33
|
|
|
20
34
|
/**
|
|
21
35
|
* Pipe a live event iterable into an SSE `Response`. Each event is one
|
|
22
|
-
* frame — `id:` carries the event-log index, `data:` the JSON event.
|
|
36
|
+
* frame — `id:` carries the event-log index, `data:` the JSON event.
|
|
37
|
+
* Frames for events the iterable already holds are written together. With
|
|
38
|
+
* `wire: 2` those events share one frame instead: `data:` is a JSON array of
|
|
39
|
+
* `CompactWireEvent`s and `id:` the last one's index. A
|
|
23
40
|
* `stream({ presence: true })` iterable also yields presence items,
|
|
24
41
|
* which ride as named frames old clients skip: `event:
|
|
25
42
|
* presence-snapshot` first (the map, ISO dates), then `event: presence`
|
|
@@ -34,8 +51,12 @@ import {
|
|
|
34
51
|
* a platform-timeout failure.
|
|
35
52
|
*/
|
|
36
53
|
export function sseResponse(
|
|
37
|
-
iterable: AsyncIterable<
|
|
54
|
+
iterable: AsyncIterable<
|
|
55
|
+
Event | PresencePatch | PresenceSnapshot | StateSnapshotFrame
|
|
56
|
+
>,
|
|
57
|
+
options: { wire?: WireVersion } = {},
|
|
38
58
|
): Response {
|
|
59
|
+
const write = options.wire === 2 ? batchedFrames : eventFrames
|
|
39
60
|
const iterator = iterable[Symbol.asyncIterator]()
|
|
40
61
|
const encoder = new TextEncoder()
|
|
41
62
|
let heartbeat: ReturnType<typeof setInterval> | undefined
|
|
@@ -121,7 +142,7 @@ export function sseResponse(
|
|
|
121
142
|
controller.close()
|
|
122
143
|
return
|
|
123
144
|
}
|
|
124
|
-
controller.enqueue(encoder.encode(
|
|
145
|
+
controller.enqueue(encoder.encode(write(value, iterator)))
|
|
125
146
|
} catch (error) {
|
|
126
147
|
if (closed) return
|
|
127
148
|
closed = true
|
|
@@ -147,12 +168,58 @@ export function sseResponse(
|
|
|
147
168
|
})
|
|
148
169
|
}
|
|
149
170
|
|
|
150
|
-
|
|
171
|
+
type StreamItem =
|
|
172
|
+
HeldEvent | PresencePatch | PresenceSnapshot | StateSnapshotFrame
|
|
173
|
+
|
|
174
|
+
/** Version 1: one frame per event, then each event the iterator holds. */
|
|
175
|
+
function eventFrames(first: StreamItem, iterator: object): string {
|
|
176
|
+
let frames = frameFor(first)
|
|
177
|
+
while (frames.length < EVENT_WRITE_CHARS) {
|
|
178
|
+
const event = takeBuffered(iterator)
|
|
179
|
+
if (!event) break
|
|
180
|
+
frames += frameFor(event)
|
|
181
|
+
}
|
|
182
|
+
return frames
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Version 2: events this write carries share one frame, a JSON array whose
|
|
187
|
+
* `id:` is its last index. Held events are always events, so only `first`
|
|
188
|
+
* can be a snapshot or presence frame.
|
|
189
|
+
*/
|
|
190
|
+
function batchedFrames(first: StreamItem, iterator: object): string {
|
|
191
|
+
const lead = isStreamEvent(first) ? '' : frameFor(first)
|
|
192
|
+
const head = isStreamEvent(first) ? first : takeBuffered(iterator)
|
|
193
|
+
if (!head) return lead
|
|
194
|
+
const { events, last } = heldEventBatch({
|
|
195
|
+
first: head,
|
|
196
|
+
iterator,
|
|
197
|
+
encode: compactJsonLine,
|
|
198
|
+
})
|
|
199
|
+
return `${lead}id: ${last.index}\ndata: [${events.join(',')}]\n\n`
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// JSON.stringify output has no raw line breaks; any other stored text is
|
|
203
|
+
// re-serialized so it stays on its data: line.
|
|
204
|
+
const compactJsonLine = (event: HeldEvent): string =>
|
|
205
|
+
isSerializedEvent(event) && /[\r\n]/.test(event.payloadJson)
|
|
206
|
+
? compactEventJson(parseSerializedEvent(event))
|
|
207
|
+
: compactEventJson(event)
|
|
208
|
+
|
|
209
|
+
function frameFor(item: StreamItem): string {
|
|
210
|
+
if ('kind' in item)
|
|
211
|
+
return `event: a2-snapshot\ndata: ${JSON.stringify(stateSnapshotToWire(item))}\n\n`
|
|
151
212
|
if ('snapshot' in item) {
|
|
152
213
|
return `event: presence-snapshot\ndata: ${JSON.stringify(presenceSnapshotToWire(item))}\n\n`
|
|
153
214
|
}
|
|
154
215
|
if ('participant' in item) {
|
|
155
216
|
return `event: presence\ndata: ${JSON.stringify(presencePatchToWire(item))}\n\n`
|
|
156
217
|
}
|
|
218
|
+
// JSON.stringify output has no raw line breaks; any other stored text is
|
|
219
|
+
// re-serialized so it stays on its data: line.
|
|
220
|
+
if (isSerializedEvent(item))
|
|
221
|
+
return /[\r\n]/.test(item.payloadJson)
|
|
222
|
+
? `id: ${item.index}\ndata: ${JSON.stringify(eventToWire(parseSerializedEvent(item)))}\n\n`
|
|
223
|
+
: `id: ${item.index}\ndata: ${serializedEventToWireJson(item)}\n\n`
|
|
157
224
|
return `id: ${item.index}\ndata: ${JSON.stringify(eventToWire(item))}\n\n`
|
|
158
225
|
}
|