experimental-a2 0.21.1 → 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 +21 -0
- package/dist/{actor-HnGmrOZZ.d.ts → actor-CqibLgjQ.d.ts} +13 -5
- package/dist/actor-CqibLgjQ.d.ts.map +1 -0
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +1 -1
- package/dist/actor-react.js +1 -1
- package/dist/actor-shared-DFET5q16.js.map +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +3 -3
- package/dist/{ai-CvEnqVfo.d.ts → ai-DAu77CDr.d.ts} +2 -2
- package/dist/{ai-CvEnqVfo.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
- package/dist/{ai-CzE672Q3.js → ai-EEfKyc-z.js} +2 -2
- package/dist/{ai-CzE672Q3.js.map → ai-EEfKyc-z.js.map} +1 -1
- package/dist/ai-client.d.ts +1 -1
- 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 +211 -61
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-Bfk-Lycn.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
- package/dist/{ai-harness-Bfk-Lycn.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
- package/dist/ai-harness-dQeJaR1t.js.map +1 -1
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.js +1 -1
- package/dist/ai-react.d.ts +1 -1
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +3 -3
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +10 -4
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/dist/{client-9IeTsiAs.js → client-BQS4i_V9.js} +27 -4
- package/dist/client-BQS4i_V9.js.map +1 -0
- package/dist/client-CUMbS_38.d.ts.map +1 -1
- package/dist/client.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/{internal-Bc4PvfHk.js → internal-CvsrprnU.js} +32 -15
- package/dist/{internal-Bc4PvfHk.js.map → internal-CvsrprnU.js.map} +1 -1
- package/dist/react.js +1 -1
- package/dist/{runtime-consumer-BWmht60z.js → runtime-consumer-CIeL33wj.js} +2 -2
- package/dist/{runtime-consumer-BWmht60z.js.map → runtime-consumer-CIeL33wj.js.map} +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +2 -2
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +2 -2
- package/dist/{server-DvIB8kj5.js → server-DtFrWxi3.js} +30 -8
- package/dist/server-DtFrWxi3.js.map +1 -0
- package/dist/{server-BshFZ0eL.d.ts → server-qgez0Xny.d.ts} +3 -3
- package/dist/{server-BshFZ0eL.d.ts.map → server-qgez0Xny.d.ts.map} +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/{session-clear-Bk_29eEw.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
- package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
- package/dist/{store-Z2qjw50n.d.ts → store-D0MhBvg6.d.ts} +15 -6
- package/dist/store-D0MhBvg6.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 +15 -17
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-DYNyxCev.js → store-polling-BZt2Rf3D.js} +2 -2
- package/dist/{store-polling-DYNyxCev.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 +31 -40
- 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 +3 -3
- package/dist/{store-redis-notify-IiVdlYHc.js → store-redis-notify-CsJw_hfD.js} +538 -397
- 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 +3 -4
- 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 +16 -18
- 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/{wire-CZGqNZRx.js → wire-CV-Z7wz-.js} +3 -3
- package/dist/{wire-CZGqNZRx.js.map → wire-CV-Z7wz-.js.map} +1 -1
- package/docs/actors/01-introduction.mdx +5 -2
- package/docs/agents/03-conversations.mdx +9 -8
- package/docs/agents/04-harnesses.mdx +7 -4
- package/docs/reference/01-core.mdx +3 -2
- package/docs/reference/02-server.mdx +5 -1
- package/docs/reference/04-ai.mdx +13 -4
- package/docs/reference/06-errors.mdx +1 -1
- package/docs/server/01-consumers.mdx +4 -1
- package/docs/server/06-guarantees.mdx +7 -4
- package/docs/stores/02-redis.mdx +16 -10
- package/docs/stores/03-redis-http.mdx +6 -4
- package/docs/stores/04-postgres.mdx +2 -2
- package/docs/upgrades/01-overview.mdx +1 -0
- package/docs/upgrades/78-0-22.mdx +115 -0
- package/examples/playground/app/a0/sandbox.ts +58 -13
- package/examples/playground/app/a2slack/db/a2.ts +7 -4
- package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
- package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +8 -1
- package/examples/playground/app/chat/server.ts +2 -2
- package/examples/playground/lib/store.ts +1 -1
- package/examples/playground/package.json +1 -1
- package/package.json +1 -1
- package/sql/postgres/drizzle-schema.ts +7 -4
- package/sql/postgres/schema.sql +24 -3
- package/src/actor-shared.ts +10 -2
- package/src/ai-agent.ts +13 -0
- package/src/ai-fx.ts +321 -77
- package/src/ai-harness.ts +7 -0
- package/src/client.ts +19 -2
- package/src/deterministic-id.ts +1 -1
- package/src/internal.ts +42 -11
- package/src/presence-recovery.ts +18 -11
- package/src/presence-tombstones.ts +52 -0
- package/src/push-envelope.ts +2 -2
- package/src/server.ts +49 -15
- package/src/store-memory.ts +24 -32
- package/src/store-postgres.ts +43 -53
- package/src/store-presence-polling.ts +59 -34
- package/src/store-redis-core.ts +595 -439
- package/src/store-redis-notify.ts +74 -26
- package/src/store-redis.ts +2 -3
- package/src/store-sqlite.ts +34 -40
- package/src/store.ts +14 -5
- package/dist/actor-HnGmrOZZ.d.ts.map +0 -1
- package/dist/client-9IeTsiAs.js.map +0 -1
- package/dist/server-DvIB8kj5.js.map +0 -1
- package/dist/session-clear-Bk_29eEw.d.ts.map +0 -1
- package/dist/store-Z2qjw50n.d.ts.map +0 -1
- package/dist/store-presence-polling-CzQ0wdtM.js +0 -94
- package/dist/store-presence-polling-CzQ0wdtM.js.map +0 -1
- package/dist/store-redis-notify-IiVdlYHc.js.map +0 -1
- package/dist/stream-buffer-Cx7AAPGq.js +0 -23
- package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wire-CZGqNZRx.js","names":[],"sources":["../src/wire.ts"],"sourcesContent":["/**\n * The wire format shared by server.fetch and experimental-a2/client\n * (browser side): events as JSON with ISO timestamps, and the A2Error\n * envelope. Internal module — both entry points re-export what's\n * public.\n */\n\nimport { A2Error, type A2ErrorCode, type A2ServerErrorCode } from './errors.ts'\nimport { nullProtoRecord } from './internal.ts'\nimport type { Event } from './store.ts'\nimport {\n isSerializedEvent,\n takeBuffered,\n type HeldEvent,\n type SerializedEvent,\n} from './stream-buffer.ts'\nimport type {\n PresenceMap,\n PresencePatch,\n PresenceSnapshot,\n} from './contract.ts'\n\nexport type WireEvent = {\n id: string\n type: string\n payload: unknown\n index: number\n sessionId: string\n /** ISO 8601 — revived to a Date on the client so reducers see the\n * same shape on both sides of the wire. */\n createdAt: string\n}\n\nexport function eventToWire(event: Event): WireEvent {\n return {\n id: event.id,\n type: event.type,\n payload: event.payload,\n index: event.index,\n sessionId: event.sessionId,\n createdAt: event.createdAt.toISOString(),\n }\n}\n\n// Consecutive frames usually repeat a stream's session, an event type, and\n// the timestamp of events appended together; each keeps its last encoding.\nconst lastEncoding = <T>(encode: (value: T) => string) => {\n let last: T | undefined\n let text = ''\n return (value: T): string => {\n if (value !== last) {\n text = encode(value)\n last = value\n }\n return text\n }\n}\nconst isoJson = lastEncoding((ms: number) =>\n JSON.stringify(new Date(ms).toISOString()),\n)\nconst sessionJson = lastEncoding((id: string) => JSON.stringify(id))\nconst typeJson = lastEncoding((type: string) => JSON.stringify(type))\n\n/**\n * `JSON.stringify(eventToWire(event))` for the parsed event, built around the\n * stored payload text instead of parsing and re-serializing it.\n */\nexport function serializedEventToWireJson(event: SerializedEvent): string {\n return `{\"id\":${JSON.stringify(event.id)},\"type\":${typeJson(event.type)},\"payload\":${event.payloadJson},\"index\":${event.index},\"sessionId\":${sessionJson(event.sessionId)},\"createdAt\":${isoJson(event.createdAtMs)}}`\n}\n\n/**\n * Wire version 2: what a client opts into with `wire=2` on the stream URL.\n * Live events travel in batches, without the `sessionId` the connection\n * already names, with `createdAt` as epoch milliseconds. Any other value,\n * or none, selects version 1.\n */\nexport type WireVersion = 1 | 2\n\nexport function wireVersion(value: string | null): WireVersion {\n return value === '2' ? 2 : 1\n}\n\n/**\n * Events already held by a stream join one write while it is shorter than\n * this many characters, so a write can exceed it by the event that crosses it.\n */\nexport const EVENT_WRITE_CHARS: number = 64 * 1024\n\n/** Stream items that are events, as opposed to snapshot or presence frames. */\nexport function isStreamEvent(item: object): item is HeldEvent {\n return !('kind' in item) && !('snapshot' in item) && !('participant' in item)\n}\n\n/**\n * `first` and the events `iterator` already holds, encoded, until the batch\n * reaches `EVENT_WRITE_CHARS`; `last` is the batch's final event.\n */\nexport function heldEventBatch(options: {\n first: HeldEvent\n iterator: object\n encode: (event: HeldEvent) => string\n}): { events: string[]; last: HeldEvent } {\n const { iterator, encode } = options\n let last = options.first\n const events = [encode(last)]\n let chars = events[0]!.length\n while (chars < EVENT_WRITE_CHARS) {\n const held = takeBuffered(iterator)\n if (!held) break\n const json = encode(held)\n events.push(json)\n chars += json.length + 1\n last = held\n }\n return { events, last }\n}\n\n/** A version 2 live event: `WireEvent` minus `sessionId`, ms timestamp. */\nexport type CompactWireEvent = {\n id: string\n type: string\n payload: unknown\n index: number\n createdAt: number\n}\n\n/** One version 2 event as JSON, splicing a held row's stored payload. */\nexport function compactEventJson(event: HeldEvent): string {\n if (!isSerializedEvent(event))\n return JSON.stringify({\n id: event.id,\n type: event.type,\n payload: event.payload,\n index: event.index,\n createdAt: event.createdAt.getTime(),\n })\n return `{\"id\":${JSON.stringify(event.id)},\"type\":${typeJson(event.type)},\"payload\":${event.payloadJson},\"index\":${event.index},\"createdAt\":${event.createdAtMs}}`\n}\n\nexport function isCompactWireEvent(value: unknown): value is CompactWireEvent {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n return (\n typeof v['id'] === 'string' &&\n typeof v['type'] === 'string' &&\n typeof v['index'] === 'number' &&\n typeof v['createdAt'] === 'number' &&\n Number.isFinite(v['createdAt'])\n )\n}\n\nexport function eventFromCompactWire(\n wire: CompactWireEvent,\n sessionId: string,\n): Event {\n return {\n id: wire.id,\n type: wire.type,\n payload: wire.payload,\n index: wire.index,\n sessionId,\n createdAt: new Date(wire.createdAt),\n }\n}\n\nexport function eventFromWire(wire: WireEvent): Event {\n return {\n id: wire.id,\n type: wire.type,\n payload: wire.payload,\n index: wire.index,\n sessionId: wire.sessionId,\n createdAt: new Date(wire.createdAt),\n }\n}\n\nexport function isWireEvent(value: unknown): value is WireEvent {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n return (\n typeof v['id'] === 'string' &&\n typeof v['type'] === 'string' &&\n typeof v['index'] === 'number' &&\n typeof v['sessionId'] === 'string' &&\n typeof v['createdAt'] === 'string'\n )\n}\n\n// ── the presence wire pair ───────────────────────────────────────────\n\n/** The `event: presence` SSE frame body: one patch as JSON. */\nexport type WirePresencePatch = {\n participant: string\n values: Record<string, unknown>\n seen: number\n /** ISO 8601 — revived to a Date on the client. */\n at: string\n}\n\n/**\n * The `event: presence-snapshot` SSE frame body: the in-memory\n * `PresenceSnapshot` shape verbatim, with each per-field `at`\n * serialized to ISO 8601.\n */\nexport type WirePresenceSnapshot = {\n snapshot: {\n [participant: string]: {\n [field: string]: { value: unknown; seen: number; at: string }\n }\n }\n}\n\nexport function presencePatchToWire(patch: PresencePatch): WirePresencePatch {\n return {\n participant: patch.participant,\n values: patch.values,\n seen: patch.seen,\n at: patch.at.toISOString(),\n }\n}\n\nexport function presencePatchFromWire(wire: WirePresencePatch): PresencePatch {\n return {\n participant: wire.participant,\n values: wire.values,\n seen: wire.seen,\n at: new Date(wire.at),\n }\n}\n\n// Participant and field keys come off the wire, so every object keyed\n// by them is built null-prototype — see `nullProtoRecord`.\nexport function presenceSnapshotToWire(\n snapshot: PresenceSnapshot,\n): WirePresenceSnapshot {\n const wire: WirePresenceSnapshot['snapshot'] = nullProtoRecord()\n for (const [participant, fields] of Object.entries(snapshot.snapshot)) {\n const wireFields: WirePresenceSnapshot['snapshot'][string] =\n nullProtoRecord()\n for (const [field, entry] of Object.entries(fields)) {\n if (entry === undefined) continue\n wireFields[field] = {\n value: entry.value,\n seen: entry.seen,\n at: entry.at.toISOString(),\n }\n }\n wire[participant] = wireFields\n }\n return { snapshot: wire }\n}\n\nexport function presenceSnapshotFromWire(\n wire: WirePresenceSnapshot,\n): PresenceSnapshot {\n const map: PresenceMap = nullProtoRecord()\n for (const [participant, fields] of Object.entries(wire.snapshot)) {\n const revived: PresenceMap[string] = nullProtoRecord()\n for (const [field, entry] of Object.entries(fields)) {\n revived[field] = {\n value: entry.value,\n seen: entry.seen,\n at: new Date(entry.at),\n }\n }\n map[participant] = revived\n }\n return { snapshot: map }\n}\n\nexport function isWirePresencePatch(\n value: unknown,\n): value is WirePresencePatch {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n return (\n typeof v['participant'] === 'string' &&\n v['values'] !== null &&\n typeof v['values'] === 'object' &&\n !Array.isArray(v['values']) &&\n typeof v['seen'] === 'number' &&\n typeof v['at'] === 'string'\n )\n}\n\nexport function isWirePresenceSnapshot(\n value: unknown,\n): value is WirePresenceSnapshot {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n return (\n v['snapshot'] !== null &&\n typeof v['snapshot'] === 'object' &&\n !Array.isArray(v['snapshot'])\n )\n}\n\n// ── the A2Error envelope ─────────────────────────────────────────────\n\nexport type WireError = {\n error: { code: A2ServerErrorCode; message: string; details?: unknown }\n}\n\nconst ERROR_STATUS: Record<A2ServerErrorCode, number> = {\n INVALID_PAYLOAD: 400,\n FORBIDDEN: 403,\n UNKNOWN_EVENT_TYPE: 400,\n PARTIAL_DUPLICATE_BATCH: 400,\n SUPERSEDED_ATTEMPT: 409,\n SESSION_CLEARED: 409,\n CLAIM_EXPIRED: 409,\n STORE_UNAVAILABLE: 503,\n STORE_NOT_CONFIGURED: 500,\n UNKNOWN_PRESENCE_FIELD: 400,\n PRESENCE_NOT_SUPPORTED: 500,\n CONSUMER_DEAD_LETTERED: 500,\n}\n\nexport function errorStatus(code: A2ErrorCode): number {\n return ERROR_STATUS[isServerErrorCode(code) ? code : 'STORE_UNAVAILABLE']\n}\n\nexport function errorToWire(error: A2Error): WireError {\n const failure = asA2Error(error)\n const body: WireError = {\n error: {\n code: failure.code as A2ServerErrorCode,\n message: failure.message,\n },\n }\n if (failure.details !== undefined) body.error.details = failure.details\n return body\n}\n\n/** Wrap an arbitrary thrown value for the wire: known server codes pass through,\n * anything else becomes STORE_UNAVAILABLE — from the client's\n * perspective an unknown server failure is retryable-once, not a\n * protocol contract. */\nexport function asA2Error(error: unknown): A2Error {\n return error instanceof A2Error && isServerErrorCode(error.code)\n ? error\n : new A2Error('STORE_UNAVAILABLE', 'internal error', { cause: error })\n}\n\n/** Rebuild an A2Error from a wire body; null if the body isn't one. */\nexport function errorFromWire(body: unknown): A2Error | null {\n if (body === null || typeof body !== 'object') return null\n const err = (body as { error?: unknown }).error\n if (err === null || typeof err !== 'object') return null\n const { code, message, details } = err as Record<string, unknown>\n if (typeof code !== 'string' || !isServerErrorCode(code)) return null\n return new A2Error(code, String(message ?? code), {\n details,\n })\n}\n\nconst isServerErrorCode = (code: string): code is A2ServerErrorCode =>\n Object.hasOwn(ERROR_STATUS, code)\n\n// ── the ws frame layer ───────────────────────────────────────────────\n// The SSE lanes reframed for a socket (specs/a2-api.md §13): every\n// message is one JSON text frame, and a frame is its wire payload plus\n// a `kind` discriminant (plus `req` where a reply must correlate).\n// Unknown kinds are skipped by both sides — the same\n// forward-compatibility rule as named SSE frames.\n//\n// The multiplexed superset rides the same frames: `subscribe` /\n// `unsubscribe` up-frames open and close per-session lanes on one\n// socket, `sessionId` tags route everything else. Single-session\n// frames (no tags) keep parsing unchanged — a route opts into\n// multiplexing by choosing the multi-session server handler, never by\n// breaking the old protocol.\n\n/**\n * Server → client, parsed: a live-stream item, a push ack, a\n * subscription lifecycle notice, or the heartbeat. `parseSocketFrame`\n * yields these; frames of unknown kind (or ones failing their shape\n * guard) come back `null`. `sessionId` is present on frames from a\n * multiplexed socket and absent on a single-session one.\n */\nexport type StateSnapshotFrame = {\n kind: 'snapshot'\n sessionId: string\n name: string\n index: number\n state: unknown\n stateUndefined?: true\n /** The session's latest clear marker, when it has been cleared. */\n clearIndex?: number\n}\n\nexport function stateSnapshotToWire(\n frame: StateSnapshotFrame,\n): StateSnapshotFrame {\n return frame.state === undefined\n ? { ...frame, state: null, stateUndefined: true }\n : frame\n}\n\nexport function isStateSnapshotFrame(\n value: unknown,\n): value is StateSnapshotFrame {\n if (value === null || typeof value !== 'object') return false\n const frame = value as Record<string, unknown>\n return (\n frame['kind'] === 'snapshot' &&\n typeof frame['sessionId'] === 'string' &&\n typeof frame['name'] === 'string' &&\n frame['name'].length > 0 &&\n typeof frame['index'] === 'number' &&\n Number.isSafeInteger(frame['index']) &&\n frame['index'] >= 0 &&\n Object.hasOwn(frame, 'state') &&\n (!Object.hasOwn(frame, 'stateUndefined') ||\n (frame['stateUndefined'] === true && frame['state'] === null)) &&\n (!Object.hasOwn(frame, 'clearIndex') ||\n (typeof frame['clearIndex'] === 'number' &&\n Number.isSafeInteger(frame['clearIndex']) &&\n frame['clearIndex'] > 0 &&\n frame['clearIndex'] <= frame['index']))\n )\n}\n\nexport type SubscriptionStart =\n | { index: number; bootstrap?: never; reducer?: string }\n | { bootstrap: true; reducer: string; index?: never }\n\nexport type SocketDownFrame =\n | StateSnapshotFrame\n | { kind: 'event'; event: WireEvent }\n | { kind: 'events'; events: CompactWireEvent[]; sessionId?: string }\n | { kind: 'presence'; patch: WirePresencePatch; sessionId?: string }\n | {\n kind: 'presence-snapshot'\n snapshot: WirePresenceSnapshot\n sessionId?: string\n }\n | { kind: 'ack'; req: number; events: WireEvent[]; sessionId?: string }\n | { kind: 'ack'; req: number; error: A2Error; sessionId?: string }\n | { kind: 'subscribed'; sessionId: string; attempt?: number }\n | {\n kind: 'unsubscribed'\n sessionId: string\n attempt?: number\n reason?: string\n error?: A2Error\n }\n | { kind: 'ping' }\n\n/**\n * Client → server: the push envelope's two planes. `sessionId` is\n * implied by the socket on a single-session connection and required by\n * the multiplexed handler. Plain JSON on the wire; the server parses\n * and validates them through the same seams as `parsePushBody`.\n */\nexport type SocketPushFrame = {\n kind: 'push'\n /** Client-local ack correlator — opaque to the server. */\n req: number\n events: Array<{ type: string; payload: unknown; id?: string }>\n sessionId?: string\n}\n\nexport type SocketPresenceFrame = {\n kind: 'presence'\n participant: string\n values: Record<string, unknown>\n seen?: number\n at?: number\n sessionId?: string\n}\n\n/** One session lane in a subscribe frame. The server echoes `attempt`\n * on that lane's `subscribed`/`unsubscribed` answers, so a client that\n * re-subscribed before an answer arrived can tell the answers apart. */\nexport type SocketSubscribeEntry = {\n id: string\n attempt?: number\n} & SubscriptionStart\n\n/** Open per-session lanes on a multiplexed socket. `index` is each\n * session's exclusive resume frontier — `stream({ startAfter })`. */\nexport type SocketSubscribeFrame = {\n kind: 'subscribe'\n sessions: SocketSubscribeEntry[]\n}\n\nexport type SocketUnsubscribeFrame = {\n kind: 'unsubscribe'\n sessions: string[]\n}\n\nexport type SocketUpFrame =\n | SocketPushFrame\n | SocketPresenceFrame\n | SocketSubscribeFrame\n | SocketUnsubscribeFrame\n\nexport const SOCKET_PING_FRAME: string = JSON.stringify({ kind: 'ping' })\n\n/** One stream item as a socket frame — `sseResponse`'s framing over\n * the same codecs, with `kind` instead of an SSE event name. On a\n * multiplexed socket presence frames carry the `sessionId` tag; events\n * already carry theirs in the wire event. */\nexport function socketFrameFor(\n item: Event | PresencePatch | PresenceSnapshot | StateSnapshotFrame,\n sessionId?: string,\n): string {\n const tag = sessionId === undefined ? {} : { sessionId }\n if ('kind' in item) return JSON.stringify(stateSnapshotToWire(item))\n if ('snapshot' in item) {\n return JSON.stringify({\n kind: 'presence-snapshot',\n ...presenceSnapshotToWire(item),\n ...tag,\n })\n }\n if ('participant' in item) {\n return JSON.stringify({\n kind: 'presence',\n ...presencePatchToWire(item),\n ...tag,\n })\n }\n return JSON.stringify({ kind: 'event', ...eventToWire(item) })\n}\n\n/** A version 2 batch frame from compact event JSON; a multiplexed\n * socket names the batch's session once. */\nexport function socketEventsFor(events: string[], sessionId?: string): string {\n const tag =\n sessionId === undefined ? '' : `\"sessionId\":${JSON.stringify(sessionId)},`\n return `{\"kind\":\"events\",${tag}\"events\":[${events.join(',')}]}`\n}\n\nexport function socketAckFor(\n req: number,\n events: Event[],\n sessionId?: string,\n): string {\n return JSON.stringify({\n kind: 'ack',\n req,\n events: events.map(eventToWire),\n ...(sessionId === undefined ? {} : { sessionId }),\n })\n}\n\nexport function socketErrorAckFor(\n req: number,\n error: A2Error,\n sessionId?: string,\n): string {\n return JSON.stringify({\n kind: 'ack',\n req,\n ...errorToWire(error),\n ...(sessionId === undefined ? {} : { sessionId }),\n })\n}\n\nexport function socketSubscribedFor(options: {\n sessionId: string\n attempt?: number | undefined\n}): string {\n const { sessionId, attempt } = options\n return JSON.stringify({\n kind: 'subscribed',\n sessionId,\n ...(attempt === undefined ? {} : { attempt }),\n })\n}\n\n/** The subscription-over notice: with a `reason` the server rejected\n * or lost the session's stream, without one it ended cleanly. */\nexport function socketUnsubscribedFor(options: {\n sessionId: string\n attempt?: number | undefined\n reason?: string\n error?: A2Error\n}): string {\n const { sessionId, attempt, reason, error } = options\n return JSON.stringify({\n kind: 'unsubscribed',\n sessionId,\n ...(attempt === undefined ? {} : { attempt }),\n ...(reason === undefined ? {} : { reason }),\n ...(error === undefined ? {} : errorToWire(error)),\n })\n}\n\nconst isAttempt = (value: unknown): value is number =>\n typeof value === 'number' && Number.isSafeInteger(value) && value >= 0\n\nexport function parseSocketFrame(data: string): SocketDownFrame | null {\n let parsed: unknown\n try {\n parsed = JSON.parse(data)\n } catch {\n return null\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return null\n }\n const frame = parsed as Record<string, unknown>\n switch (frame['kind']) {\n case 'snapshot':\n if (!isStateSnapshotFrame(parsed))\n throw new A2Error('INVALID_PAYLOAD', 'invalid bootstrap snapshot')\n return parsed\n case 'ping':\n return { kind: 'ping' }\n case 'event': {\n if (!isWireEvent(parsed)) return null\n const { id, type, payload, index, sessionId, createdAt } = parsed\n return {\n kind: 'event',\n event: { id, type, payload, index, sessionId, createdAt },\n }\n }\n case 'events': {\n // Dropping a batch would leave a gap the frontier then skips past.\n const events = frame['events']\n if (!Array.isArray(events) || !events.every(isCompactWireEvent))\n throw new A2Error('INVALID_PAYLOAD', 'invalid event batch')\n return { kind: 'events', events, ...sessionTag(frame) }\n }\n case 'presence': {\n if (!isWirePresencePatch(parsed)) return null\n const { participant, values, seen, at } = parsed\n return {\n kind: 'presence',\n patch: { participant, values, seen, at },\n ...sessionTag(frame),\n }\n }\n case 'presence-snapshot': {\n if (!isWirePresenceSnapshot(parsed)) return null\n return {\n kind: 'presence-snapshot',\n snapshot: { snapshot: parsed.snapshot },\n ...sessionTag(frame),\n }\n }\n case 'subscribed': {\n const { sessionId, attempt } = frame\n if (typeof sessionId !== 'string') return null\n if (attempt !== undefined && !isAttempt(attempt)) return null\n return {\n kind: 'subscribed',\n sessionId,\n ...(attempt === undefined ? {} : { attempt }),\n }\n }\n case 'unsubscribed': {\n const { sessionId, attempt } = frame\n if (typeof sessionId !== 'string') return null\n if (attempt !== undefined && !isAttempt(attempt)) return null\n const reason = frame['reason']\n const error =\n errorFromWire(parsed) ??\n ('error' in frame\n ? new A2Error(\n 'STORE_UNAVAILABLE',\n 'unintelligible subscription error',\n )\n : null)\n return {\n kind: 'unsubscribed',\n sessionId,\n ...(attempt === undefined ? {} : { attempt }),\n ...(typeof reason === 'string' ? { reason } : {}),\n ...(error === null ? {} : { error }),\n }\n }\n case 'ack': {\n const req = frame['req']\n if (typeof req !== 'number') return null\n const tag = sessionTag(frame)\n const events = frame['events']\n if (Array.isArray(events) && events.every(isWireEvent)) {\n return { kind: 'ack', req, events, ...tag }\n }\n const error = errorFromWire(parsed)\n if (error) return { kind: 'ack', req, error, ...tag }\n // A correlatable ack must never be dropped: its waiter would hang\n // forever behind a healthy socket (pings keep the watchdog fed).\n // An outcome this client cannot interpret — an error code from a\n // newer server, an event shape that fails a guard — degrades to\n // a lost ack: retryable, and the client-generated ids make the\n // retry idempotent even if the append actually committed.\n return {\n kind: 'ack',\n req,\n error: new A2Error('STORE_UNAVAILABLE', 'unintelligible ack'),\n ...tag,\n }\n }\n default:\n return null\n }\n}\n\nconst sessionTag = (frame: Record<string, unknown>): { sessionId?: string } => {\n const sessionId = frame['sessionId']\n return typeof sessionId === 'string' ? { sessionId } : {}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"wire-CV-Z7wz-.js","names":[],"sources":["../src/wire.ts"],"sourcesContent":["/**\n * The wire format shared by server.fetch and experimental-a2/client\n * (browser side): events as JSON with ISO timestamps, and the A2Error\n * envelope. Internal module — both entry points re-export what's\n * public.\n */\n\nimport { A2Error, type A2ErrorCode, type A2ServerErrorCode } from './errors.ts'\nimport { nullProtoRecord } from './internal.ts'\nimport type { Event } from './store.ts'\nimport {\n isSerializedEvent,\n takeBuffered,\n type HeldEvent,\n type SerializedEvent,\n} from './stream-buffer.ts'\nimport type {\n PresenceMap,\n PresencePatch,\n PresenceSnapshot,\n} from './contract.ts'\n\nexport type WireEvent = {\n id: string\n type: string\n payload: unknown\n index: number\n sessionId: string\n /** ISO 8601 — revived to a Date on the client so reducers see the\n * same shape on both sides of the wire. */\n createdAt: string\n}\n\nexport function eventToWire(event: Event): WireEvent {\n return {\n id: event.id,\n type: event.type,\n payload: event.payload,\n index: event.index,\n sessionId: event.sessionId,\n createdAt: event.createdAt.toISOString(),\n }\n}\n\n// Consecutive frames usually repeat a stream's session, an event type, and\n// the timestamp of events appended together; each keeps its last encoding.\nconst lastEncoding = <T>(encode: (value: T) => string) => {\n let last: T | undefined\n let text = ''\n return (value: T): string => {\n if (value !== last) {\n text = encode(value)\n last = value\n }\n return text\n }\n}\nconst isoJson = lastEncoding((ms: number) =>\n JSON.stringify(new Date(ms).toISOString()),\n)\nconst sessionJson = lastEncoding((id: string) => JSON.stringify(id))\nconst typeJson = lastEncoding((type: string) => JSON.stringify(type))\n\n/**\n * `JSON.stringify(eventToWire(event))` for the parsed event, built around the\n * stored payload text instead of parsing and re-serializing it.\n */\nexport function serializedEventToWireJson(event: SerializedEvent): string {\n return `{\"id\":${JSON.stringify(event.id)},\"type\":${typeJson(event.type)},\"payload\":${event.payloadJson},\"index\":${event.index},\"sessionId\":${sessionJson(event.sessionId)},\"createdAt\":${isoJson(event.createdAtMs)}}`\n}\n\n/**\n * Wire version 2: what a client opts into with `wire=2` on the stream URL.\n * Live events travel in batches, without the `sessionId` the connection\n * already names, with `createdAt` as epoch milliseconds. Any other value,\n * or none, selects version 1.\n */\nexport type WireVersion = 1 | 2\n\nexport function wireVersion(value: string | null): WireVersion {\n return value === '2' ? 2 : 1\n}\n\n/**\n * Events already held by a stream join one write while it is shorter than\n * this many characters, so a write can exceed it by the event that crosses it.\n */\nexport const EVENT_WRITE_CHARS: number = 64 * 1024\n\n/** Stream items that are events, as opposed to snapshot or presence frames. */\nexport function isStreamEvent(item: object): item is HeldEvent {\n return !('kind' in item) && !('snapshot' in item) && !('participant' in item)\n}\n\n/**\n * `first` and the events `iterator` already holds, encoded, until the batch\n * reaches `EVENT_WRITE_CHARS`; `last` is the batch's final event.\n */\nexport function heldEventBatch(options: {\n first: HeldEvent\n iterator: object\n encode: (event: HeldEvent) => string\n}): { events: string[]; last: HeldEvent } {\n const { iterator, encode } = options\n let last = options.first\n const events = [encode(last)]\n let chars = events[0]!.length\n while (chars < EVENT_WRITE_CHARS) {\n const held = takeBuffered(iterator)\n if (!held) break\n const json = encode(held)\n events.push(json)\n chars += json.length + 1\n last = held\n }\n return { events, last }\n}\n\n/** A version 2 live event: `WireEvent` minus `sessionId`, ms timestamp. */\nexport type CompactWireEvent = {\n id: string\n type: string\n payload: unknown\n index: number\n createdAt: number\n}\n\n/** One version 2 event as JSON, splicing a held row's stored payload. */\nexport function compactEventJson(event: HeldEvent): string {\n if (!isSerializedEvent(event))\n return JSON.stringify({\n id: event.id,\n type: event.type,\n payload: event.payload,\n index: event.index,\n createdAt: event.createdAt.getTime(),\n })\n return `{\"id\":${JSON.stringify(event.id)},\"type\":${typeJson(event.type)},\"payload\":${event.payloadJson},\"index\":${event.index},\"createdAt\":${event.createdAtMs}}`\n}\n\nexport function isCompactWireEvent(value: unknown): value is CompactWireEvent {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n return (\n typeof v['id'] === 'string' &&\n typeof v['type'] === 'string' &&\n typeof v['index'] === 'number' &&\n typeof v['createdAt'] === 'number' &&\n Number.isFinite(v['createdAt'])\n )\n}\n\nexport function eventFromCompactWire(\n wire: CompactWireEvent,\n sessionId: string,\n): Event {\n return {\n id: wire.id,\n type: wire.type,\n payload: wire.payload,\n index: wire.index,\n sessionId,\n createdAt: new Date(wire.createdAt),\n }\n}\n\nexport function eventFromWire(wire: WireEvent): Event {\n return {\n id: wire.id,\n type: wire.type,\n payload: wire.payload,\n index: wire.index,\n sessionId: wire.sessionId,\n createdAt: new Date(wire.createdAt),\n }\n}\n\nexport function isWireEvent(value: unknown): value is WireEvent {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n return (\n typeof v['id'] === 'string' &&\n typeof v['type'] === 'string' &&\n typeof v['index'] === 'number' &&\n typeof v['sessionId'] === 'string' &&\n typeof v['createdAt'] === 'string'\n )\n}\n\n// ── the presence wire pair ───────────────────────────────────────────\n\n/** The `event: presence` SSE frame body: one patch as JSON. */\nexport type WirePresencePatch = {\n participant: string\n values: Record<string, unknown>\n seen: number\n /** ISO 8601 — revived to a Date on the client. */\n at: string\n}\n\n/**\n * The `event: presence-snapshot` SSE frame body: the in-memory\n * `PresenceSnapshot` shape verbatim, with each per-field `at`\n * serialized to ISO 8601.\n */\nexport type WirePresenceSnapshot = {\n snapshot: {\n [participant: string]: {\n [field: string]: { value: unknown; seen: number; at: string }\n }\n }\n}\n\nexport function presencePatchToWire(patch: PresencePatch): WirePresencePatch {\n return {\n participant: patch.participant,\n values: patch.values,\n seen: patch.seen,\n at: patch.at.toISOString(),\n }\n}\n\nexport function presencePatchFromWire(wire: WirePresencePatch): PresencePatch {\n return {\n participant: wire.participant,\n values: wire.values,\n seen: wire.seen,\n at: new Date(wire.at),\n }\n}\n\n// Participant and field keys come off the wire, so every object keyed\n// by them is built null-prototype — see `nullProtoRecord`.\nexport function presenceSnapshotToWire(\n snapshot: PresenceSnapshot,\n): WirePresenceSnapshot {\n const wire: WirePresenceSnapshot['snapshot'] = nullProtoRecord()\n for (const [participant, fields] of Object.entries(snapshot.snapshot)) {\n const wireFields: WirePresenceSnapshot['snapshot'][string] =\n nullProtoRecord()\n for (const [field, entry] of Object.entries(fields)) {\n if (entry === undefined) continue\n wireFields[field] = {\n value: entry.value,\n seen: entry.seen,\n at: entry.at.toISOString(),\n }\n }\n wire[participant] = wireFields\n }\n return { snapshot: wire }\n}\n\nexport function presenceSnapshotFromWire(\n wire: WirePresenceSnapshot,\n): PresenceSnapshot {\n const map: PresenceMap = nullProtoRecord()\n for (const [participant, fields] of Object.entries(wire.snapshot)) {\n const revived: PresenceMap[string] = nullProtoRecord()\n for (const [field, entry] of Object.entries(fields)) {\n revived[field] = {\n value: entry.value,\n seen: entry.seen,\n at: new Date(entry.at),\n }\n }\n map[participant] = revived\n }\n return { snapshot: map }\n}\n\nexport function isWirePresencePatch(\n value: unknown,\n): value is WirePresencePatch {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n return (\n typeof v['participant'] === 'string' &&\n v['values'] !== null &&\n typeof v['values'] === 'object' &&\n !Array.isArray(v['values']) &&\n typeof v['seen'] === 'number' &&\n typeof v['at'] === 'string'\n )\n}\n\nexport function isWirePresenceSnapshot(\n value: unknown,\n): value is WirePresenceSnapshot {\n if (value === null || typeof value !== 'object') return false\n const v = value as Record<string, unknown>\n return (\n v['snapshot'] !== null &&\n typeof v['snapshot'] === 'object' &&\n !Array.isArray(v['snapshot'])\n )\n}\n\n// ── the A2Error envelope ─────────────────────────────────────────────\n\nexport type WireError = {\n error: { code: A2ServerErrorCode; message: string; details?: unknown }\n}\n\nconst ERROR_STATUS: Record<A2ServerErrorCode, number> = {\n INVALID_PAYLOAD: 400,\n FORBIDDEN: 403,\n UNKNOWN_EVENT_TYPE: 400,\n PARTIAL_DUPLICATE_BATCH: 400,\n SUPERSEDED_ATTEMPT: 409,\n SESSION_CLEARED: 409,\n CLAIM_EXPIRED: 409,\n STORE_UNAVAILABLE: 503,\n STORE_NOT_CONFIGURED: 500,\n UNKNOWN_PRESENCE_FIELD: 400,\n PRESENCE_NOT_SUPPORTED: 500,\n CONSUMER_DEAD_LETTERED: 500,\n}\n\nexport function errorStatus(code: A2ErrorCode): number {\n return ERROR_STATUS[isServerErrorCode(code) ? code : 'STORE_UNAVAILABLE']\n}\n\nexport function errorToWire(error: A2Error): WireError {\n const failure = asA2Error(error)\n const body: WireError = {\n error: {\n code: failure.code as A2ServerErrorCode,\n message: failure.message,\n },\n }\n if (failure.details !== undefined) body.error.details = failure.details\n return body\n}\n\n/** Wrap an arbitrary thrown value for the wire: known server codes pass through,\n * anything else becomes STORE_UNAVAILABLE — from the client's\n * perspective an unknown server failure is retryable-once, not a\n * protocol contract. */\nexport function asA2Error(error: unknown): A2Error {\n return error instanceof A2Error && isServerErrorCode(error.code)\n ? error\n : new A2Error('STORE_UNAVAILABLE', 'internal error', { cause: error })\n}\n\n/** Rebuild an A2Error from a wire body; null if the body isn't one. */\nexport function errorFromWire(body: unknown): A2Error | null {\n if (body === null || typeof body !== 'object') return null\n const err = (body as { error?: unknown }).error\n if (err === null || typeof err !== 'object') return null\n const { code, message, details } = err as Record<string, unknown>\n if (typeof code !== 'string' || !isServerErrorCode(code)) return null\n return new A2Error(code, String(message ?? code), {\n details,\n })\n}\n\nconst isServerErrorCode = (code: string): code is A2ServerErrorCode =>\n Object.hasOwn(ERROR_STATUS, code)\n\n// ── the ws frame layer ───────────────────────────────────────────────\n// The SSE lanes reframed for a socket (specs/a2-api.md §13): every\n// message is one JSON text frame, and a frame is its wire payload plus\n// a `kind` discriminant (plus `req` where a reply must correlate).\n// Unknown kinds are skipped by both sides — the same\n// forward-compatibility rule as named SSE frames.\n//\n// The multiplexed superset rides the same frames: `subscribe` /\n// `unsubscribe` up-frames open and close per-session lanes on one\n// socket, `sessionId` tags route everything else. Single-session\n// frames (no tags) keep parsing unchanged — a route opts into\n// multiplexing by choosing the multi-session server handler, never by\n// breaking the old protocol.\n\n/**\n * Server → client, parsed: a live-stream item, a push ack, a\n * subscription lifecycle notice, or the heartbeat. `parseSocketFrame`\n * yields these; frames of unknown kind (or ones failing their shape\n * guard) come back `null`. `sessionId` is present on frames from a\n * multiplexed socket and absent on a single-session one.\n */\nexport type StateSnapshotFrame = {\n kind: 'snapshot'\n sessionId: string\n name: string\n index: number\n state: unknown\n stateUndefined?: true\n /** The session's latest clear marker, when it has been cleared. */\n clearIndex?: number\n}\n\nexport function stateSnapshotToWire(\n frame: StateSnapshotFrame,\n): StateSnapshotFrame {\n return frame.state === undefined\n ? { ...frame, state: null, stateUndefined: true }\n : frame\n}\n\nexport function isStateSnapshotFrame(\n value: unknown,\n): value is StateSnapshotFrame {\n if (value === null || typeof value !== 'object') return false\n const frame = value as Record<string, unknown>\n return (\n frame['kind'] === 'snapshot' &&\n typeof frame['sessionId'] === 'string' &&\n typeof frame['name'] === 'string' &&\n frame['name'].length > 0 &&\n typeof frame['index'] === 'number' &&\n Number.isSafeInteger(frame['index']) &&\n frame['index'] >= 0 &&\n Object.hasOwn(frame, 'state') &&\n (!Object.hasOwn(frame, 'stateUndefined') ||\n (frame['stateUndefined'] === true && frame['state'] === null)) &&\n (!Object.hasOwn(frame, 'clearIndex') ||\n (typeof frame['clearIndex'] === 'number' &&\n Number.isSafeInteger(frame['clearIndex']) &&\n frame['clearIndex'] > 0 &&\n frame['clearIndex'] <= frame['index']))\n )\n}\n\nexport type SubscriptionStart =\n | { index: number; bootstrap?: never; reducer?: string }\n | { bootstrap: true; reducer: string; index?: never }\n\nexport type SocketDownFrame =\n | StateSnapshotFrame\n | { kind: 'event'; event: WireEvent }\n | { kind: 'events'; events: CompactWireEvent[]; sessionId?: string }\n | { kind: 'presence'; patch: WirePresencePatch; sessionId?: string }\n | {\n kind: 'presence-snapshot'\n snapshot: WirePresenceSnapshot\n sessionId?: string\n }\n | { kind: 'ack'; req: number; events: WireEvent[]; sessionId?: string }\n | { kind: 'ack'; req: number; error: A2Error; sessionId?: string }\n | { kind: 'subscribed'; sessionId: string; attempt?: number }\n | {\n kind: 'unsubscribed'\n sessionId: string\n attempt?: number\n reason?: string\n error?: A2Error\n }\n | { kind: 'ping' }\n\n/**\n * Client → server: the push envelope's two planes. `sessionId` is\n * implied by the socket on a single-session connection and required by\n * the multiplexed handler. Plain JSON on the wire; the server parses\n * and validates them through the same seams as `parsePushBody`.\n */\nexport type SocketPushFrame = {\n kind: 'push'\n /** Client-local ack correlator — opaque to the server. */\n req: number\n events: Array<{ type: string; payload: unknown; id?: string }>\n sessionId?: string\n}\n\nexport type SocketPresenceFrame = {\n kind: 'presence'\n participant: string\n values: Record<string, unknown>\n seen?: number\n at?: number\n sessionId?: string\n}\n\n/** One session lane in a subscribe frame. The server echoes `attempt`\n * on that lane's `subscribed`/`unsubscribed` answers, so a client that\n * re-subscribed before an answer arrived can tell the answers apart. */\nexport type SocketSubscribeEntry = {\n id: string\n attempt?: number\n} & SubscriptionStart\n\n/** Open per-session lanes on a multiplexed socket. `index` is each\n * session's exclusive resume frontier — `stream({ startAfter })`. */\nexport type SocketSubscribeFrame = {\n kind: 'subscribe'\n sessions: SocketSubscribeEntry[]\n}\n\nexport type SocketUnsubscribeFrame = {\n kind: 'unsubscribe'\n sessions: string[]\n}\n\nexport type SocketUpFrame =\n | SocketPushFrame\n | SocketPresenceFrame\n | SocketSubscribeFrame\n | SocketUnsubscribeFrame\n\nexport const SOCKET_PING_FRAME: string = JSON.stringify({ kind: 'ping' })\n\n/** One stream item as a socket frame — `sseResponse`'s framing over\n * the same codecs, with `kind` instead of an SSE event name. On a\n * multiplexed socket presence frames carry the `sessionId` tag; events\n * already carry theirs in the wire event. */\nexport function socketFrameFor(\n item: Event | PresencePatch | PresenceSnapshot | StateSnapshotFrame,\n sessionId?: string,\n): string {\n const tag = sessionId === undefined ? {} : { sessionId }\n if ('kind' in item) return JSON.stringify(stateSnapshotToWire(item))\n if ('snapshot' in item) {\n return JSON.stringify({\n kind: 'presence-snapshot',\n ...presenceSnapshotToWire(item),\n ...tag,\n })\n }\n if ('participant' in item) {\n return JSON.stringify({\n kind: 'presence',\n ...presencePatchToWire(item),\n ...tag,\n })\n }\n return JSON.stringify({ kind: 'event', ...eventToWire(item) })\n}\n\n/** A version 2 batch frame from compact event JSON; a multiplexed\n * socket names the batch's session once. */\nexport function socketEventsFor(events: string[], sessionId?: string): string {\n const tag =\n sessionId === undefined ? '' : `\"sessionId\":${JSON.stringify(sessionId)},`\n return `{\"kind\":\"events\",${tag}\"events\":[${events.join(',')}]}`\n}\n\nexport function socketAckFor(\n req: number,\n events: Event[],\n sessionId?: string,\n): string {\n return JSON.stringify({\n kind: 'ack',\n req,\n events: events.map(eventToWire),\n ...(sessionId === undefined ? {} : { sessionId }),\n })\n}\n\nexport function socketErrorAckFor(\n req: number,\n error: A2Error,\n sessionId?: string,\n): string {\n return JSON.stringify({\n kind: 'ack',\n req,\n ...errorToWire(error),\n ...(sessionId === undefined ? {} : { sessionId }),\n })\n}\n\nexport function socketSubscribedFor(options: {\n sessionId: string\n attempt?: number | undefined\n}): string {\n const { sessionId, attempt } = options\n return JSON.stringify({\n kind: 'subscribed',\n sessionId,\n ...(attempt === undefined ? {} : { attempt }),\n })\n}\n\n/** The subscription-over notice: with a `reason` the server rejected\n * or lost the session's stream, without one it ended cleanly. */\nexport function socketUnsubscribedFor(options: {\n sessionId: string\n attempt?: number | undefined\n reason?: string\n error?: A2Error\n}): string {\n const { sessionId, attempt, reason, error } = options\n return JSON.stringify({\n kind: 'unsubscribed',\n sessionId,\n ...(attempt === undefined ? {} : { attempt }),\n ...(reason === undefined ? {} : { reason }),\n ...(error === undefined ? {} : errorToWire(error)),\n })\n}\n\nconst isAttempt = (value: unknown): value is number =>\n typeof value === 'number' && Number.isSafeInteger(value) && value >= 0\n\nexport function parseSocketFrame(data: string): SocketDownFrame | null {\n let parsed: unknown\n try {\n parsed = JSON.parse(data)\n } catch {\n return null\n }\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return null\n }\n const frame = parsed as Record<string, unknown>\n switch (frame['kind']) {\n case 'snapshot':\n if (!isStateSnapshotFrame(parsed))\n throw new A2Error('INVALID_PAYLOAD', 'invalid bootstrap snapshot')\n return parsed\n case 'ping':\n return { kind: 'ping' }\n case 'event': {\n if (!isWireEvent(parsed)) return null\n const { id, type, payload, index, sessionId, createdAt } = parsed\n return {\n kind: 'event',\n event: { id, type, payload, index, sessionId, createdAt },\n }\n }\n case 'events': {\n // Dropping a batch would leave a gap the frontier then skips past.\n const events = frame['events']\n if (!Array.isArray(events) || !events.every(isCompactWireEvent))\n throw new A2Error('INVALID_PAYLOAD', 'invalid event batch')\n return { kind: 'events', events, ...sessionTag(frame) }\n }\n case 'presence': {\n if (!isWirePresencePatch(parsed)) return null\n const { participant, values, seen, at } = parsed\n return {\n kind: 'presence',\n patch: { participant, values, seen, at },\n ...sessionTag(frame),\n }\n }\n case 'presence-snapshot': {\n if (!isWirePresenceSnapshot(parsed)) return null\n return {\n kind: 'presence-snapshot',\n snapshot: { snapshot: parsed.snapshot },\n ...sessionTag(frame),\n }\n }\n case 'subscribed': {\n const { sessionId, attempt } = frame\n if (typeof sessionId !== 'string') return null\n if (attempt !== undefined && !isAttempt(attempt)) return null\n return {\n kind: 'subscribed',\n sessionId,\n ...(attempt === undefined ? {} : { attempt }),\n }\n }\n case 'unsubscribed': {\n const { sessionId, attempt } = frame\n if (typeof sessionId !== 'string') return null\n if (attempt !== undefined && !isAttempt(attempt)) return null\n const reason = frame['reason']\n const error =\n errorFromWire(parsed) ??\n ('error' in frame\n ? new A2Error(\n 'STORE_UNAVAILABLE',\n 'unintelligible subscription error',\n )\n : null)\n return {\n kind: 'unsubscribed',\n sessionId,\n ...(attempt === undefined ? {} : { attempt }),\n ...(typeof reason === 'string' ? { reason } : {}),\n ...(error === null ? {} : { error }),\n }\n }\n case 'ack': {\n const req = frame['req']\n if (typeof req !== 'number') return null\n const tag = sessionTag(frame)\n const events = frame['events']\n if (Array.isArray(events) && events.every(isWireEvent)) {\n return { kind: 'ack', req, events, ...tag }\n }\n const error = errorFromWire(parsed)\n if (error) return { kind: 'ack', req, error, ...tag }\n // A correlatable ack must never be dropped: its waiter would hang\n // forever behind a healthy socket (pings keep the watchdog fed).\n // An outcome this client cannot interpret — an error code from a\n // newer server, an event shape that fails a guard — degrades to\n // a lost ack: retryable, and the client-generated ids make the\n // retry idempotent even if the append actually committed.\n return {\n kind: 'ack',\n req,\n error: new A2Error('STORE_UNAVAILABLE', 'unintelligible ack'),\n ...tag,\n }\n }\n default:\n return null\n }\n}\n\nconst sessionTag = (frame: Record<string, unknown>): { sessionId?: string } => {\n const sessionId = frame['sessionId']\n return typeof sessionId === 'string' ? { sessionId } : {}\n}\n"],"mappings":"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"}
|
|
@@ -103,8 +103,11 @@ no retry. The message settles, and the caller receives
|
|
|
103
103
|
backoff while the instance's lane waits; see
|
|
104
104
|
[Errors](/reference/errors).
|
|
105
105
|
- **A crash** re-runs the handler from the pre-message state. `ctx.id`
|
|
106
|
-
is the message id, stable across re-runs
|
|
107
|
-
key for external I/O.
|
|
106
|
+
is the message id, stable across re-runs and unique within the
|
|
107
|
+
instance: use it as the idempotency key for external I/O. An explicit
|
|
108
|
+
message `id` that repeats in another instance repeats there too, so
|
|
109
|
+
derive explicit ids per instance, for example with
|
|
110
|
+
[`idempotentId()`](/reference/core#idempotentidparts).
|
|
108
111
|
- **A clear** by `handle.session.clear()` before the answer rejects the
|
|
109
112
|
call with an `A2Error` whose code is `SESSION_CLEARED`. The handler
|
|
110
113
|
may already have run. Over HTTP it's a `410`.
|
|
@@ -292,9 +292,10 @@ no `reducer`; the types enforce this through `AI_STATE_REDUCER`.
|
|
|
292
292
|
|
|
293
293
|
When the agent belongs inside something larger, such as a shared chat room,
|
|
294
294
|
keep two sessions. A consumer of the room appends each message the agent should
|
|
295
|
-
see to the agent's session with `inputs.message()`.
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
see to the agent's session with `inputs.message()`. Derive the message ID from
|
|
296
|
+
the room event's ID with `idempotentId()`, so a retried run converges on the
|
|
297
|
+
same message. Event IDs are unique per session, so when several rooms feed one
|
|
298
|
+
agent session, include the room's session ID too. A tool
|
|
298
299
|
the agent calls to speak appends to the room with an `idempotentId()` of its
|
|
299
300
|
`toolCallId`. Neither fold reads the other's state.
|
|
300
301
|
|
|
@@ -509,11 +510,11 @@ full handoff rules.
|
|
|
509
510
|
New-input commands from `inputs.message()`, `inputs.seed()`, and
|
|
510
511
|
`inputs.steer()` use the message ID as their event ID, and hand-written commands
|
|
511
512
|
must too. Caller IDs cannot start with the reserved `a2.ai:` prefix and must be
|
|
512
|
-
unique
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
513
|
+
unique within the session; the same ID in another session is another message.
|
|
514
|
+
An identical submission replays without new work, even after removal or
|
|
515
|
+
compaction. Reusing an ID in the session with different content or action
|
|
516
|
+
rejects with `PARTIAL_DUPLICATE_BATCH`: use `queue.edit()` to change a pending
|
|
517
|
+
input and a fresh ID for a new one.
|
|
517
518
|
|
|
518
519
|
### Optimistic conversation display
|
|
519
520
|
|
|
@@ -179,8 +179,11 @@ first model call sees it.
|
|
|
179
179
|
A2 records each model call and each tool result as the harness works. When a
|
|
180
180
|
process dies in the middle of a response, the next attempt resumes at the
|
|
181
181
|
interrupted step: finished model calls are not made again and finished tools
|
|
182
|
-
do not run again. A2
|
|
183
|
-
last response resumes the harness from its state at the end of that
|
|
184
|
-
FX's checkpoint or Pi's session, kept in memory, and any other
|
|
185
|
-
the session from the log.
|
|
182
|
+
do not run again. A2 keeps nothing durable outside the log: a process that ran
|
|
183
|
+
the last response resumes the harness from its state at the end of that
|
|
184
|
+
response, FX's checkpoint or Pi's session, kept in memory, and any other
|
|
185
|
+
process replays the session from the log. Under `fx()`, the process that ran a
|
|
186
|
+
step also keeps FX's kernel waiting on that step's tool calls until the session
|
|
187
|
+
has nothing left to run there, so the response's next step, or the first step
|
|
188
|
+
of a response that steers it, continues it without a replay. A tool call that a crash cut short reruns unless its
|
|
186
189
|
`toolRecovery` is `'report'`; see [Delivery semantics](/agents/conversations#delivery-semantics).
|
|
@@ -110,8 +110,9 @@ export const id = await idempotentId('orders', 'shop-7', 'request-42')
|
|
|
110
110
|
- String boundaries, order, and case matter; strings are not normalized. Empty
|
|
111
111
|
strings and an empty tuple are valid.
|
|
112
112
|
- Every argument has the same role; a prefix such as `'orders'` is a namespace
|
|
113
|
-
by convention. Include enough context to distinguish operations
|
|
114
|
-
|
|
113
|
+
by convention. Include enough context to distinguish operations within a
|
|
114
|
+
session: event IDs are unique per session, so the same ID in another session
|
|
115
|
+
names a different event.
|
|
115
116
|
- A2 preserves explicit IDs and does not change its automatic ID generation.
|
|
116
117
|
Changing an existing recipe changes retry identity, so retries of already
|
|
117
118
|
committed requests must keep their original IDs.
|
|
@@ -517,7 +517,8 @@ The only way to move a session forward. See [Guarantees](/server/guarantees).
|
|
|
517
517
|
- A multi-event append is atomic: all-or-nothing, consecutive positions, one
|
|
518
518
|
transaction.
|
|
519
519
|
- An explicit `id` makes it idempotent across retries; re-sending an identical
|
|
520
|
-
batch returns the original rows.
|
|
520
|
+
batch returns the original rows. Ids are unique per session: the same `id`
|
|
521
|
+
in another session is another event.
|
|
521
522
|
- Events pushed through `server.fetch` arrive through this same append.
|
|
522
523
|
- Only events some consumer takes are dispatched; the append itself settles
|
|
523
524
|
the rest. An append no consumer takes still starts session healing when
|
|
@@ -883,6 +884,9 @@ broadcasts. No log row, no dispatch, no scheduler arm; `history()` and
|
|
|
883
884
|
epoch-ms stamp; the browser client stamps its sends automatically. Without
|
|
884
885
|
`at`, receipt time stands in: a stampless caller is a single writer, so
|
|
885
886
|
receipt order is its send order.
|
|
887
|
+
- `null` clears a field. The store keeps the clear's `at` until the field's
|
|
888
|
+
TTL runs out, so an older value that reaches it later, such as one sent
|
|
889
|
+
through another server instance, still loses.
|
|
886
890
|
- A bad payload throws `INVALID_PAYLOAD`, an unknown field throws
|
|
887
891
|
`UNKNOWN_PRESENCE_FIELD`, and a non-finite or negative `at` throws a
|
|
888
892
|
`TypeError`.
|
package/docs/reference/04-ai.mdx
CHANGED
|
@@ -131,8 +131,8 @@ Pure typed builders of `ai.control.requested` commands, for `append()` and
|
|
|
131
131
|
event ID, and must. Approval, input-response, and retry builders use stable
|
|
132
132
|
interaction IDs; other commands use the ID `push()` or `append()` assigns,
|
|
133
133
|
kept across transport retries. Caller IDs are nonempty, never start with the
|
|
134
|
-
reserved `a2.ai:`, and are unique
|
|
135
|
-
and conflicting reuse rejects before append. A2's own IDs are opaque,
|
|
134
|
+
reserved `a2.ai:`, and are unique within the session; identical events
|
|
135
|
+
replay and conflicting reuse in the session rejects before append. A2's own IDs are opaque,
|
|
136
136
|
fixed-length, deterministic `a2.ai:` values; an assistant message's ID is its
|
|
137
137
|
initial model-request event's ID.
|
|
138
138
|
- **Passive input.** `generate: false` is valid browser input: it admits the
|
|
@@ -539,7 +539,9 @@ their `https:` or `data:` URLs; any other file fails the turn. See
|
|
|
539
539
|
|
|
540
540
|
A harness resumes after a crash at the interrupted step. Finished model calls
|
|
541
541
|
are not made again and finished tool calls do not run again. A2 rebuilds the
|
|
542
|
-
harness for each step from the log,
|
|
542
|
+
harness for each step from the log, unless [`fx()`](#fx) continues the kernel
|
|
543
|
+
its previous step left in the same process, and keeps nothing durable outside
|
|
544
|
+
the log. Each agent
|
|
543
545
|
keeps, in its process's memory, the harness's state at the end of the last
|
|
544
546
|
response it ran there, FX's checkpoint or Pi's session, so a warm process reads
|
|
545
547
|
and replays only the log since; a cold one reads and replays the session from
|
|
@@ -600,7 +602,14 @@ response so far from the log: recorded model responses and tool results answer
|
|
|
600
602
|
the kernel without network calls or tool runs, then FX makes its next model
|
|
601
603
|
call. Replay grows with the response, roughly 0.1 to 0.5 seconds for one of 80
|
|
602
604
|
to 200 tool calls. When libfx can't take a checkpoint, A2 replays from an
|
|
603
|
-
earlier one.
|
|
605
|
+
earlier one. The process that ran the step before skips the replay: it keeps
|
|
606
|
+
FX's kernel waiting on that step's tool calls, and the next step gives the
|
|
607
|
+
kernel any steering sent meanwhile, answers its tool calls from the log, and
|
|
608
|
+
lets it go on. A step that finds anything else first, such as changed model
|
|
609
|
+
settings or a model call another process committed, closes that kernel and
|
|
610
|
+
replays. A kept kernel holds its process open, so A2 closes it once the session
|
|
611
|
+
has nothing left to run in that process: however a response ends, its process
|
|
612
|
+
can exit.
|
|
604
613
|
|
|
605
614
|
FX's model calls carry the same [AI Gateway](#agentoptions) defaults as the
|
|
606
615
|
AI SDK harness: `caching: 'auto'`, under any Gateway options FX sets itself,
|
|
@@ -39,7 +39,7 @@ export async function POST(req: Request) {
|
|
|
39
39
|
| `UNKNOWN_EVENT_TYPE` | an event type isn't in the contract's `events` map | no |
|
|
40
40
|
| `UNKNOWN_PRESENCE_FIELD` | a presence field isn't in the contract's `presence` map | no |
|
|
41
41
|
| `PRESENCE_NOT_SUPPORTED` | the contract declares `presence` but the store has no presence capability. Thrown at construction; for environment-resolved stores, at first store use | no |
|
|
42
|
-
| `PARTIAL_DUPLICATE_BATCH` | a batch mixes already-appended and fresh events, contains the same id twice,
|
|
42
|
+
| `PARTIAL_DUPLICATE_BATCH` | a batch mixes already-appended and fresh events, or contains the same id twice. Event ids are unique per session, so an id used in another session never conflicts. Always a caller bug; nothing was written | no |
|
|
43
43
|
| `SUPERSEDED_ATTEMPT` | a consumer's append or clear comes from an attempt that a recovery claim or a session clear replaced; nothing was written. Also the abort reason on that attempt's `ctx.signal` | no |
|
|
44
44
|
| `SESSION_CLEARED` | a state read reaches below the latest clear, a consumer reads state past a clear of its event, or an actor call is cleared before it receives its answer | no |
|
|
45
45
|
| `CLAIM_EXPIRED` | never thrown; only the `ctx.signal` abort reason when the attempt's lease lapsed without renewal (assumed loss, decided locally, not proof) | no |
|
|
@@ -367,7 +367,10 @@ Consumers are at-least-once. A crash after your side effect but before A2
|
|
|
367
367
|
records completion runs the event again.
|
|
368
368
|
|
|
369
369
|
- **External side effects take an idempotency key.** `event.id` is stable
|
|
370
|
-
across attempts.
|
|
370
|
+
across attempts and unique within its session. Ids your callers pick, and
|
|
371
|
+
the ids A2 derives from them, can repeat in another session: when yours can,
|
|
372
|
+
add the session to the key, and the contract's name if several contracts
|
|
373
|
+
share it, as in `` `orders:${event.sessionId}:${event.id}` ``.
|
|
371
374
|
- **Named appends are deduplicated.** A retry gets the existing events back.
|
|
372
375
|
- **Returned events commit with completion.** They all exist with the
|
|
373
376
|
cursor's advance, or none do.
|
|
@@ -5,8 +5,9 @@ description: Appends are effectively-once, consumers are at-least-once, and retr
|
|
|
5
5
|
|
|
6
6
|
## In short
|
|
7
7
|
|
|
8
|
-
- **Appends are effectively-once.** Event ids deduplicate writes
|
|
9
|
-
before its consumers start; they may start before
|
|
8
|
+
- **Appends are effectively-once.** Event ids deduplicate writes within a
|
|
9
|
+
session. A batch commits before its consumers start; they may start before
|
|
10
|
+
`append` resolves.
|
|
10
11
|
- **Consumers are at-least-once.** They may run again after a crash, so
|
|
11
12
|
[side effects use idempotency keys](/server/consumers#consumers-can-run-twice).
|
|
12
13
|
- **The log stays ordered.** Every session has one immutable append order.
|
|
@@ -50,8 +51,10 @@ the committed rows back. A partial or changed batch is rejected.
|
|
|
50
51
|
|
|
51
52
|
The transaction covers only A2's log. An HTTP call, database write, or other
|
|
52
53
|
external effect can't join it, so a crash after the effect but before
|
|
53
|
-
completion repeats it. Use `event.id` as an idempotency key
|
|
54
|
-
|
|
54
|
+
completion repeats it. Use `event.id` as an idempotency key, with the session
|
|
55
|
+
when ids can repeat across sessions (see
|
|
56
|
+
[Consumers can run twice](/server/consumers#consumers-can-run-twice)), or add
|
|
57
|
+
fencing in the external system.
|
|
55
58
|
|
|
56
59
|
## Lanes
|
|
57
60
|
|
package/docs/stores/02-redis.mdx
CHANGED
|
@@ -21,9 +21,9 @@ export const ordersServer = createServer({
|
|
|
21
21
|
npm i ioredis
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Works on a single Redis instance and on non-cluster providers such as
|
|
25
|
-
which is durable by default (use its TCP `rediss://` URL). There's
|
|
26
|
-
set up: A2 creates its keys as it writes.
|
|
24
|
+
Works on a single Redis or Valkey instance and on non-cluster providers such as
|
|
25
|
+
Upstash, which is durable by default (use its TCP `rediss://` URL). There's
|
|
26
|
+
nothing to set up: A2 creates its keys as it writes.
|
|
27
27
|
|
|
28
28
|
## Options
|
|
29
29
|
|
|
@@ -31,22 +31,28 @@ set up: A2 creates its keys as it writes.
|
|
|
31
31
|
| --- | --- |
|
|
32
32
|
| `url` | creates an `ioredis` client |
|
|
33
33
|
| `client` | your own `ioredis` client instead |
|
|
34
|
-
| `keyPrefix` | isolates several apps on one Redis. Default `'a2'
|
|
34
|
+
| `keyPrefix` | isolates several apps on one Redis. Default `'a2'`; no braces |
|
|
35
35
|
| `clock`, `ids` | test seams for timestamps and generated event ids |
|
|
36
36
|
|
|
37
37
|
## Upgrade
|
|
38
38
|
|
|
39
39
|
The store records its key layout in `<keyPrefix>:schema`, and refuses a prefix
|
|
40
40
|
marked with another one with `STORE_NOT_CONFIGURED`. A release that changes
|
|
41
|
-
the layout says what to do on its [upgrade page](/upgrades/overview).
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
the layout says what to do on its [upgrade page](/upgrades/overview). This
|
|
42
|
+
release names every key differently from earlier ones, so give the store a
|
|
43
|
+
fresh `keyPrefix`.
|
|
44
44
|
|
|
45
45
|
## How it works
|
|
46
46
|
|
|
47
|
-
- Each session is a Redis Stream
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
- Each session is a Redis Stream, and every key of a session shares one hash
|
|
48
|
+
tag.
|
|
49
|
+
- Every write is one atomic script over its session's keys. On Upstash, which
|
|
50
|
+
runs a script under a database-wide lock by default, A2 marks each script to
|
|
51
|
+
lock only its session's keys. Redis and Valkey run one command at a time, so
|
|
52
|
+
A2 keeps scripts short instead.
|
|
53
|
+
- Every write publishes a wake-up after its script, so live streams are
|
|
54
|
+
push-driven, and connections scale with your processes, not with your
|
|
55
|
+
viewers.
|
|
50
56
|
- A wake-up is disposable: a safety re-read recovers a lost one within ten
|
|
51
57
|
seconds.
|
|
52
58
|
|
|
@@ -22,7 +22,8 @@ export const ordersServer = createServer({
|
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
Stores sessions exactly like [Redis](/stores/redis), with no client dependency
|
|
25
|
-
and nothing to set up. Use it when your provider only offers a REST API.
|
|
25
|
+
and nothing to set up. Use it when your provider only offers a REST API. On
|
|
26
|
+
Upstash, each script locks only its session's keys, never the whole database.
|
|
26
27
|
|
|
27
28
|
:::warning
|
|
28
29
|
Live streams trust the messages on the store's `:notify` channels. Browsers
|
|
@@ -37,7 +38,7 @@ credentials that can also write the store.
|
|
|
37
38
|
| Option | |
|
|
38
39
|
| --- | --- |
|
|
39
40
|
| `url`, `token` | your provider's REST endpoint and token |
|
|
40
|
-
| `keyPrefix` | isolates several apps on one Redis. Default `'a2'
|
|
41
|
+
| `keyPrefix` | isolates several apps on one Redis. Default `'a2'`; no braces |
|
|
41
42
|
| `clock`, `ids` | test seams for timestamps and generated event ids |
|
|
42
43
|
|
|
43
44
|
## Live updates
|
|
@@ -46,8 +47,9 @@ credentials that can also write the store.
|
|
|
46
47
|
same URL and token. Each active channel holds one outbound HTTP subscription
|
|
47
48
|
per store instance, shared by its viewers and closed when the last one
|
|
48
49
|
leaves.
|
|
49
|
-
-
|
|
50
|
-
rejected notification doesn't
|
|
50
|
+
- Each write publishes its wake-up after its script commits, so a slow
|
|
51
|
+
subscriber never holds the script's lock. A rejected notification doesn't
|
|
52
|
+
fail the write.
|
|
51
53
|
- Idle streams re-read every ten seconds to recover a missed notification.
|
|
52
54
|
Subscriptions reconnect on their own and catch up from the event cursor.
|
|
53
55
|
- After a reconnect, A2 re-reads presence and clears fields deleted during the
|
|
@@ -38,8 +38,8 @@ another role needs `grant execute on function a2_batch(text[], jsonb)` beside
|
|
|
38
38
|
its grants on the tables, and keeps it when you apply the file again.
|
|
39
39
|
|
|
40
40
|
A2 checks nothing up front. It relies on the table and column names, the
|
|
41
|
-
column types, the primary keys, the unique index on `a2_events
|
|
42
|
-
the `a2_batch` function; the other indexes keep its queries fast. A query that
|
|
41
|
+
column types, the primary keys, the unique index on `a2_events (session_id,
|
|
42
|
+
event_id)`, and the `a2_batch` function; the other indexes keep its queries fast. A query that
|
|
43
43
|
finds a table, column, or the function missing throws `STORE_NOT_CONFIGURED`,
|
|
44
44
|
naming the file. Once it's applied, the next request works, with no redeploy.
|
|
45
45
|
|
|
@@ -7,6 +7,7 @@ description: What to do when you move to a new release. One page per release tha
|
|
|
7
7
|
|
|
8
8
|
| Release | What you do |
|
|
9
9
|
| --- | --- |
|
|
10
|
+
| [0.22](/upgrades/0-22) | Event ids are unique per session. Postgres: apply the new `schema.sql`, then deploy. Redis: use a new `keyPrefix`. |
|
|
10
11
|
| [0.21](/upgrades/0-21) | Rewrite handlers as consumers. Let earlier deployments finish; on Postgres, then apply the new `schema.sql`. SQLite starts fresh. |
|
|
11
12
|
| [0.20](/upgrades/0-20) | Postgres: apply `schema.sql` yourself, before you deploy. |
|
|
12
13
|
| [0.19](/upgrades/0-19) | Start new agent sessions: earlier ones don't fold. |
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: '0.22'
|
|
3
|
+
description: Event ids are unique per session, not per store, and a presence clear keeps its stamp.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Event ids are unique per session
|
|
7
|
+
|
|
8
|
+
The same event id in two sessions is now two events. Before, appending an id
|
|
9
|
+
that another session already used threw `PARTIAL_DUPLICATE_BATCH`. Inside one
|
|
10
|
+
session nothing changes: an identical retry returns the original rows, a batch
|
|
11
|
+
that mixes sent and fresh events throws, and a consumer's retried completion
|
|
12
|
+
returns the events it committed.
|
|
13
|
+
|
|
14
|
+
- **Ids you derive need only the session's scope.** An
|
|
15
|
+
[`idempotentId()`](/reference/core#idempotentidparts) recipe that includes
|
|
16
|
+
the session still works; one that leaves it out no longer collides with
|
|
17
|
+
another session's events.
|
|
18
|
+
- **Idempotency keys built from ids.** `event.id` and an actor's `ctx.id` are
|
|
19
|
+
unique within their session. Ids your callers pick, and the ids A2 derives
|
|
20
|
+
from them, can now repeat in another session, so a key for an external side
|
|
21
|
+
effect built from one alone can too. When yours can repeat, add the session
|
|
22
|
+
to the key; see
|
|
23
|
+
[Consumers can run twice](/server/consumers#consumers-can-run-twice).
|
|
24
|
+
- **Clear markers** follow the same rule.
|
|
25
|
+
- **Custom stores** check ids against the session's own rows only: the
|
|
26
|
+
idempotent-replay and partial-batch checks in `append` and
|
|
27
|
+
`completeAttempt` ignore other sessions. See
|
|
28
|
+
[`A2Store`](/reference/server#a2store).
|
|
29
|
+
|
|
30
|
+
## Postgres
|
|
31
|
+
|
|
32
|
+
0.22's `schema.sql` replaces the unique index on `a2_events (event_id)`,
|
|
33
|
+
`a2_events_event_id`, with one on `a2_events (session_id, event_id)`,
|
|
34
|
+
`a2_events_session_event_id`. Every row is kept, and building the new index
|
|
35
|
+
can't fail: the old one already keeps those pairs unique.
|
|
36
|
+
|
|
37
|
+
1. **Apply 0.22's `schema.sql`**, as the role your app connects with:
|
|
38
|
+
`node_modules/experimental-a2/sql/postgres/schema.sql`.
|
|
39
|
+
2. **Deploy 0.22.**
|
|
40
|
+
|
|
41
|
+
The first time it runs, the swap locks `a2_events` against every read and write
|
|
42
|
+
until the new index is built, then drops the old one. On a large table, make
|
|
43
|
+
both changes by hand instead, with nothing blocked. Run each statement on its
|
|
44
|
+
own, outside a transaction:
|
|
45
|
+
|
|
46
|
+
```sql
|
|
47
|
+
create unique index concurrently if not exists a2_events_session_event_id
|
|
48
|
+
on a2_events (session_id, event_id);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A concurrent build that fails leaves an invalid index of the same name behind.
|
|
52
|
+
Check that this returns `true` before you drop the old index; if it doesn't,
|
|
53
|
+
drop the new one and build it again:
|
|
54
|
+
|
|
55
|
+
```sql
|
|
56
|
+
select indisvalid from pg_index
|
|
57
|
+
where indexrelid = 'a2_events_session_event_id'::regclass;
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```sql
|
|
61
|
+
drop index concurrently if exists a2_events_event_id;
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Then apply `schema.sql`: it finds both done and waits on nothing. It drops the
|
|
65
|
+
old index only once the new one is valid.
|
|
66
|
+
|
|
67
|
+
With Drizzle, update your copy of `drizzle-schema.ts`:
|
|
68
|
+
`a2_events_session_event_id` on `(session_id, event_id)` replaces
|
|
69
|
+
`a2_events_event_id`. On a large table, run the statements above by hand
|
|
70
|
+
instead, and take them out of the migration Drizzle generates.
|
|
71
|
+
|
|
72
|
+
## SQLite
|
|
73
|
+
|
|
74
|
+
Nothing to do. The store creates the new index and drops the old one when it
|
|
75
|
+
opens the file. Once 0.22 has opened a file, don't open it with an earlier
|
|
76
|
+
release: that release builds the old index again, and can't open the file at
|
|
77
|
+
all once two sessions share an id.
|
|
78
|
+
|
|
79
|
+
## Redis
|
|
80
|
+
|
|
81
|
+
Redis names every key differently in this release, so it starts fresh:
|
|
82
|
+
**point the store at a new `keyPrefix`**, then deploy. Under its new prefix,
|
|
83
|
+
0.22 never reads what an earlier release wrote; delete the old prefix's keys
|
|
84
|
+
whenever convenient. On a prefix 0.21 wrote, 0.22 refuses to start with
|
|
85
|
+
`STORE_NOT_CONFIGURED`. A `keyPrefix` can't contain braces.
|
|
86
|
+
|
|
87
|
+
What changes:
|
|
88
|
+
|
|
89
|
+
- **Scripts lock only their session on Upstash.** Every key of a session
|
|
90
|
+
shares one hash tag, so each script sets Upstash's `allow-key-locking` and
|
|
91
|
+
locks its session's keys instead of the whole database. The store asks the
|
|
92
|
+
server once whether it accepts the flag; Redis and Valkey don't, and run one
|
|
93
|
+
command at a time anyway.
|
|
94
|
+
- **No script publishes.** Each write publishes its wake-up after its script,
|
|
95
|
+
so a slow subscriber never holds a lock.
|
|
96
|
+
- **`clear()` works on Upstash.** It used to rename the log aside, which
|
|
97
|
+
Upstash refuses for streams. The marker now joins the same log, and the old
|
|
98
|
+
lifetime's entries stay below it, unread, until `clear()` finishes deleting
|
|
99
|
+
them. A session that stays empty after its clear keeps only its clear marker,
|
|
100
|
+
the log holding it, and its log consumers' positions.
|
|
101
|
+
|
|
102
|
+
## Memory
|
|
103
|
+
|
|
104
|
+
Nothing to do: the memory store starts empty in every process.
|
|
105
|
+
|
|
106
|
+
## Presence clears keep their stamp
|
|
107
|
+
|
|
108
|
+
A `null` from `setPresence()` now leaves a tombstone in the store, carrying the
|
|
109
|
+
clear's `at`, until the field's TTL runs out. An older value that reaches the
|
|
110
|
+
store after the clear, such as one sent through another server instance, still
|
|
111
|
+
loses. Before, it brought the field back. Nothing to do: Postgres and SQLite
|
|
112
|
+
keep tombstones in `a2_presence` as a JSON `null` value.
|
|
113
|
+
|
|
114
|
+
Custom stores with presence keep a tombstone for each `null`, compare it by
|
|
115
|
+
`at` like a value until it expires, and leave tombstones out of `read`.
|