experimental-a2 0.6.0 → 0.7.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 +22 -0
- package/dist/ai-server.d.ts +4 -5
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +8 -7
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +334 -2
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +1 -1
- package/dist/client.d.ts +202 -2
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1025 -1
- package/dist/client.js.map +1 -0
- package/dist/errors-BQuJpe82.js.map +1 -1
- package/dist/index.d.ts +22 -3
- package/dist/index.d.ts.map +1 -0
- package/dist/{internal-DstsI6Re.js → internal-DRXJ56EI.js} +5 -28
- package/dist/internal-DRXJ56EI.js.map +1 -0
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/scheduler-qstash.d.ts +3 -3
- package/dist/scheduler-qstash.js +4 -5
- package/dist/scheduler-qstash.js.map +1 -1
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +4 -4
- package/dist/scheduler-vercel.js.map +1 -1
- package/dist/{server-Duw6MVlB.js → server-286j79Mt.js} +708 -79
- package/dist/server-286j79Mt.js.map +1 -0
- package/dist/{server-DpvjhdoE.d.ts → server-DgXmORIq.d.ts} +67 -49
- package/dist/server-DgXmORIq.d.ts.map +1 -0
- package/dist/server.d.ts +3 -3
- package/dist/server.js +1 -1
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/{store-DysUkTH3.d.ts → store-flRz1OWh.d.ts} +2 -57
- package/dist/store-flRz1OWh.d.ts.map +1 -0
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +1 -59
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-dSeLxzfb.js → store-polling-6DW7F1DT.js} +2 -2
- package/dist/{store-polling-dSeLxzfb.js.map → store-polling-6DW7F1DT.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.js +1 -81
- package/dist/store-postgres.js.map +1 -1
- package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DEYO8Ryv.js} +48 -134
- package/dist/store-redis-core-DEYO8Ryv.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +2 -3
- package/dist/store-redis-http.js.map +1 -1
- package/dist/store-redis.d.ts +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 +1 -72
- package/dist/store-sqlite.js.map +1 -1
- package/dist/{wire-BFQmSJ-9.js → wire-B6te_wns.js} +4 -3
- package/dist/wire-B6te_wns.js.map +1 -0
- package/docs/guides/03-react.mdx +20 -28
- package/docs/guides/05-production.mdx +9 -11
- package/docs/guides/06-ai-agents.mdx +8 -14
- package/docs/guides/09-presence.mdx +14 -19
- package/docs/guides/10-transports.mdx +104 -86
- package/docs/reference/01-api.mdx +143 -277
- package/docs/reference/02-errors.mdx +5 -7
- package/package.json +1 -14
- package/src/ai-server.ts +9 -5
- package/src/client.ts +2 -2
- package/src/errors.ts +1 -0
- package/src/internal.ts +3 -62
- package/src/push-envelope.ts +24 -21
- package/src/scheduler-qstash.ts +3 -3
- package/src/scheduler-vercel.ts +2 -2
- package/src/server-fetch.ts +344 -0
- package/src/server.ts +73 -225
- package/src/session-socket.ts +36 -20
- package/src/sse.ts +2 -2
- package/src/store-memory.ts +0 -81
- package/src/store-postgres.ts +0 -100
- package/src/store-redis-core.ts +47 -211
- package/src/store-redis-http.ts +0 -1
- package/src/store-redis.ts +0 -1
- package/src/store-sqlite.ts +0 -119
- package/src/store.ts +0 -60
- package/src/wire.ts +2 -1
- package/dist/ai-D_PGS-JR.d.ts +0 -334
- package/dist/ai-D_PGS-JR.d.ts.map +0 -1
- package/dist/cli-B3VuxoDe.js +0 -599
- package/dist/cli-B3VuxoDe.js.map +0 -1
- package/dist/cli-bin.d.ts +0 -1
- package/dist/cli-bin.js +0 -7
- package/dist/cli-bin.js.map +0 -1
- package/dist/cli.d.ts +0 -20
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -2
- package/dist/client-CdMqi7mC.d.ts +0 -202
- package/dist/client-CdMqi7mC.d.ts.map +0 -1
- package/dist/client-Dj5d3SP_.js +0 -1026
- package/dist/client-Dj5d3SP_.js.map +0 -1
- package/dist/devtools-J_jZ2vQf.d.ts +0 -152
- package/dist/devtools-J_jZ2vQf.d.ts.map +0 -1
- package/dist/devtools-kJJaORn-.js +0 -340
- package/dist/devtools-kJJaORn-.js.map +0 -1
- package/dist/devtools-server.browser.d.ts +0 -1
- package/dist/devtools-server.browser.js +0 -6
- package/dist/devtools-server.browser.js.map +0 -1
- package/dist/devtools-server.d.ts +0 -23
- package/dist/devtools-server.d.ts.map +0 -1
- package/dist/devtools-server.js +0 -1270
- package/dist/devtools-server.js.map +0 -1
- package/dist/devtools.d.ts +0 -2
- package/dist/devtools.js +0 -2
- package/dist/errors-W6nwJ-fm.d.ts +0 -21
- package/dist/errors-W6nwJ-fm.d.ts.map +0 -1
- package/dist/http.d.ts +0 -151
- package/dist/http.d.ts.map +0 -1
- package/dist/http.js +0 -706
- package/dist/http.js.map +0 -1
- package/dist/inspection-DaxB5jM2.js +0 -13
- package/dist/inspection-DaxB5jM2.js.map +0 -1
- package/dist/internal-DstsI6Re.js.map +0 -1
- package/dist/platform-B4TnJtWu.js +0 -34
- package/dist/platform-B4TnJtWu.js.map +0 -1
- package/dist/server-DpvjhdoE.d.ts.map +0 -1
- package/dist/server-Duw6MVlB.js.map +0 -1
- package/dist/store-DysUkTH3.d.ts.map +0 -1
- package/dist/store-redis-core-BFLwz0Wj.js.map +0 -1
- package/dist/testing.browser.d.ts +0 -1
- package/dist/testing.browser.js +0 -6
- package/dist/testing.browser.js.map +0 -1
- package/dist/testing.d.ts +0 -32
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js +0 -103
- package/dist/testing.js.map +0 -1
- package/dist/wire-BFQmSJ-9.js.map +0 -1
- package/docs/guides/07-devtools.mdx +0 -229
- package/src/cli-bin.ts +0 -5
- package/src/cli.ts +0 -1046
- package/src/devtools-app.ts +0 -989
- package/src/devtools-server.browser.ts +0 -5
- package/src/devtools-server.ts +0 -604
- package/src/devtools.ts +0 -716
- package/src/http.ts +0 -394
- package/src/inspection.ts +0 -39
- package/src/testing.browser.ts +0 -5
- package/src/testing.ts +0 -185
package/dist/store-sqlite.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store-sqlite.js","names":[],"sources":["../src/store-sqlite.ts"],"sourcesContent":["/**\n * experimental-a2/store-sqlite — sqlite store backend (the dev default, `.a2/dev.db`).\n *\n * Built on `node:sqlite` (Node ≥ 22.13) so it ships with zero\n * dependencies. Implements the A2Store interface; the conformance suite\n * in test/conformance is the executable contract.\n *\n * The spec's `index` column is stored as `idx` (a2-implementation.md\n * §11 — `index` collides with a reserved word) and mapped back at the\n * API boundary. Timestamps are epoch milliseconds written from the\n * injected clock — never SQL `now()` — so tests can time-travel against\n * real storage.\n */\n\nimport { mkdirSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport { DatabaseSync } from 'node:sqlite'\nimport { A2Error } from './errors.ts'\nimport { idempotentReplay } from './idempotent-replay.ts'\nimport { decodeReturnedEventIds } from './store-codec.ts'\nimport { pollingStream } from './store-polling.ts'\nimport {\n RANDOM_IDS,\n SYSTEM_CLOCK,\n type A2Store,\n type Clock,\n type Event,\n type EventCause,\n type IdSource,\n type StoredEvent,\n} from './store.ts'\n\nexport type SqliteStoreOptions = {\n /** Database file path. Defaults to `.a2/dev.db`; `:memory:` works. */\n path?: string\n /** Injectable clock — every stored timestamp comes from here. */\n clock?: Clock\n /** Injectable id source for generated event ids. */\n ids?: IdSource\n}\n\nexport type SqliteStore = A2Store & {\n /** Close the underlying database handle. */\n close(): void\n}\n\nconst SCHEMA = `\ncreate table if not exists a2_events (\n session_id text not null,\n idx integer not null,\n event_type text not null,\n payload text not null,\n event_id text not null,\n created_at integer not null,\n cause text,\n lane text,\n lane_ready integer not null default 0,\n processed_at integer,\n processed_by_attempt integer,\n returned_event_ids text,\n first_claimed_at integer,\n last_claimed_at integer,\n attempt_count integer not null default 0,\n failure_count integer not null default 0,\n last_failed_at integer,\n last_failed_attempt integer,\n last_error text,\n failed_at integer,\n claim_holder text,\n claim_expires_at integer,\n primary key (session_id, idx)\n) strict;\n\ncreate unique index if not exists a2_events_event_id on a2_events (event_id);\ncreate index if not exists a2_events_unprocessed\n on a2_events (session_id, idx) where processed_at is null;\ncreate index if not exists a2_events_lane_pending\n on a2_events (session_id, lane, idx) where processed_at is null;\ncreate index if not exists a2_events_dispatch_ready\n on a2_events (session_id, idx)\n where lane_ready = 1 and processed_at is null and failed_at is null;\ncreate index if not exists a2_events_claim_expiry\n on a2_events (session_id, claim_expires_at)\n where lane_ready = 1 and processed_at is null and failed_at is null;\n\ncreate table if not exists a2_snapshots (\n session_id text not null,\n reducer_name text not null,\n up_to_index integer not null,\n state text not null,\n updated_at integer not null,\n primary key (session_id, reducer_name)\n) strict;\n\ncreate table if not exists a2_presence (\n ns text not null,\n participant text not null,\n field text not null,\n value text not null,\n seen integer not null,\n at integer not null,\n expires_at integer not null,\n primary key (ns, participant, field)\n) strict;\n`\n\n/**\n * Switching journal modes takes an exclusive lock, and — unlike normal\n * statements — the switch can return SQLITE_BUSY without consulting the\n * busy handler while several connections race it (instances cold-booting\n * against one file). Bounded synchronous retry; the window is boot-only\n * and tiny. Found by the multi-process torture test.\n */\nconst setWalJournalMode = (db: DatabaseSync): void => {\n for (let attempt = 0; ; attempt += 1) {\n try {\n db.exec('pragma journal_mode = wal')\n return\n } catch (err) {\n const busy = (err as { errcode?: number }).errcode === 5\n if (!busy || attempt >= 100) throw err\n // Synchronous 5ms sleep — sqlite() is a sync constructor.\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5)\n }\n }\n}\n\nconst wrap = <T>(fn: () => T): T => {\n try {\n return fn()\n } catch (err) {\n if (err instanceof A2Error || err instanceof TypeError) throw err\n throw new A2Error('STORE_UNAVAILABLE', 'sqlite store operation failed', {\n cause: err,\n })\n }\n}\n\nconst toDate = (ms: number | bigint | null): Date | null =>\n ms === null ? null : new Date(Number(ms))\n\nconst toCause = (json: string | null): EventCause | null => {\n if (json === null) return null\n const value = JSON.parse(json) as {\n index?: unknown\n attempt?: unknown\n batchSize?: unknown\n }\n if (\n !Number.isInteger(value.index) ||\n Number(value.index) < 1 ||\n !Number.isInteger(value.attempt) ||\n Number(value.attempt) < 1\n ) {\n throw new TypeError('stored event has an invalid cause')\n }\n if (\n value.batchSize !== undefined &&\n (!Number.isInteger(value.batchSize) || Number(value.batchSize) < 1)\n ) {\n throw new TypeError('stored event has an invalid cause')\n }\n return {\n index: Number(value.index),\n attempt: Number(value.attempt),\n ...(value.batchSize === undefined\n ? {}\n : { batchSize: Number(value.batchSize) }),\n }\n}\n\nconst toStored = (row: EventRow): StoredEvent => ({\n id: row.event_id,\n type: row.event_type,\n payload: JSON.parse(row.payload),\n index: Number(row.idx),\n sessionId: row.session_id,\n createdAt: new Date(Number(row.created_at)),\n cause: toCause(row.cause),\n lane: row.lane,\n processedAt: toDate(row.processed_at),\n processedByAttempt:\n row.processed_by_attempt === null ? null : Number(row.processed_by_attempt),\n returnedEventIds:\n row.returned_event_ids === null\n ? null\n : decodeReturnedEventIds(row.returned_event_ids),\n firstClaimedAt: toDate(row.first_claimed_at),\n lastClaimedAt: toDate(row.last_claimed_at),\n attemptCount: Number(row.attempt_count),\n failureCount: Number(row.failure_count),\n lastFailedAt: toDate(row.last_failed_at),\n lastFailedAttempt:\n row.last_failed_attempt === null ? null : Number(row.last_failed_attempt),\n lastError: row.last_error,\n failedAt: toDate(row.failed_at),\n claimHolder: row.claim_holder,\n claimExpiresAt: toDate(row.claim_expires_at),\n})\n\nconst toEvent = (row: EventRow): Event => ({\n id: row.event_id,\n type: row.event_type,\n payload: JSON.parse(row.payload),\n index: Number(row.idx),\n sessionId: row.session_id,\n createdAt: new Date(Number(row.created_at)),\n})\n\ntype EventRow = {\n session_id: string\n idx: number | bigint\n event_type: string\n payload: string\n event_id: string\n created_at: number | bigint\n cause: string | null\n lane: string | null\n processed_at: number | bigint | null\n processed_by_attempt: number | bigint | null\n returned_event_ids: string | null\n first_claimed_at: number | bigint | null\n last_claimed_at: number | bigint | null\n attempt_count: number | bigint\n failure_count: number | bigint\n last_failed_at: number | bigint | null\n last_failed_attempt: number | bigint | null\n last_error: string | null\n failed_at: number | bigint | null\n claim_holder: string | null\n claim_expires_at: number | bigint | null\n}\n\ntype SessionSummaryRow = {\n session_id: string\n event_count: number | bigint\n pending_count: number | bigint\n failed_count: number | bigint\n attempt_count: number | bigint\n failure_count: number | bigint\n first_event_at: number | bigint\n updated_at: number | bigint\n}\n\ntype SnapshotRow = {\n reducer_name: string\n up_to_index: number | bigint\n updated_at: number | bigint\n}\n\ntype PresenceDbRow = {\n participant: string\n field: string\n value: string\n seen: number | bigint\n at: number | bigint\n expires_at: number | bigint\n}\n\ntype StateReadRow = Partial<EventRow> & {\n row_kind: 'snapshot' | 'event'\n snapshot_index: number | bigint | null\n snapshot_state: string | null\n}\n\ntype AppendStatusRow = {\n max: number | bigint\n has_pending: number | bigint\n}\n\ntype ExistingEventRow = EventRow & {\n session_has_pending: number | bigint\n}\n\nexport function sqlite(options: SqliteStoreOptions = {}): SqliteStore {\n const path = options.path ?? '.a2/dev.db'\n const clock = options.clock ?? SYSTEM_CLOCK\n const generateId = options.ids ?? RANDOM_IDS\n\n if (path !== ':memory:') {\n // The dev-default backend only ever runs outside production, but\n // bundlers can't know that: without the ignore, Turbopack sees a\n // dynamic fs path and traces the entire project into the output.\n mkdirSync(dirname(resolve(/* turbopackIgnore: true */ path)), {\n recursive: true,\n })\n }\n const db = new DatabaseSync(path)\n // busy_timeout before anything that takes locks: concurrent\n // cold-opens (several instances booting at once) race the WAL switch\n // and schema creation.\n db.exec('pragma busy_timeout = 5000')\n setWalJournalMode(db)\n db.exec('pragma synchronous = normal')\n db.exec(SCHEMA)\n\n const insertEvent = db.prepare(\n `insert into a2_events\n (session_id, idx, event_type, payload, event_id, created_at,\n cause, lane, lane_ready, processed_at)\n select ?, ?, ?, ?, ?, ?, ?, ?,\n case\n when ? is null then 1\n when not exists (\n select 1 from a2_events\n where session_id = ? and lane = ? and processed_at is null\n ) then 1\n else 0\n end,\n ?`,\n )\n const promoteLaneHead = db.prepare(\n `update a2_events set lane_ready = 1\n where session_id = ?\n and idx = (\n select idx from a2_events\n where session_id = ? and lane = ? and processed_at is null\n order by idx limit 1\n )`,\n )\n const appendStatus = db.prepare(\n `select coalesce(\n (select max(idx) from a2_events where session_id = ?),\n 0\n ) as max,\n exists(\n select 1 from a2_events\n where session_id = ? and processed_at is null\n ) as has_pending`,\n )\n const upsertPresence = db.prepare(\n `insert into a2_presence (ns, participant, field, value, seen, at, expires_at)\n values (?, ?, ?, ?, ?, ?, ?)\n on conflict (ns, participant, field) do update set\n value = excluded.value,\n seen = excluded.seen,\n at = excluded.at,\n expires_at = excluded.expires_at\n where a2_presence.at <= excluded.at`,\n )\n const deletePresence = db.prepare(\n `delete from a2_presence\n where ns = ? and participant = ? and field = ? and at <= ?`,\n )\n const sweepPresence = db.prepare(\n 'delete from a2_presence where ns = ? and expires_at <= ?',\n )\n const selectPresence = db.prepare(\n `select participant, field, value, seen, at, expires_at\n from a2_presence\n where ns = ? and expires_at > ?\n order by participant, field`,\n )\n const selectByIds = (count: number) =>\n db.prepare(\n `select stored.*,\n exists(\n select 1 from a2_events pending\n where pending.session_id = ?\n and pending.processed_at is null\n ) as session_has_pending\n from a2_events stored\n where stored.event_id in (${Array.from({ length: count }, () => '?').join(', ')})\n order by stored.idx`,\n )\n\n const tx = <T>(fn: () => T): T => {\n db.exec('begin immediate')\n try {\n const result = fn()\n db.exec('commit')\n return result\n } catch (err) {\n try {\n db.exec('rollback')\n } catch {\n // Rollback can fail if the transaction never started; the\n // original error is what matters.\n }\n throw err\n }\n }\n\n return {\n async append(sessionId, events) {\n if (events.length === 0) {\n return wrap(() => {\n const status = appendStatus.get(\n sessionId,\n sessionId,\n ) as AppendStatusRow\n return {\n events: [],\n hasPending: Number(status.has_pending) === 1,\n }\n })\n }\n return wrap(() =>\n tx(() => {\n const supplied = events.filter((e) => e.id !== undefined)\n const suppliedIds = supplied.map((e) => e.id as string)\n if (new Set(suppliedIds).size !== suppliedIds.length) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'batch contains the same event id more than once',\n )\n }\n if (suppliedIds.length > 0) {\n const existing = selectByIds(suppliedIds.length).all(\n sessionId,\n ...suppliedIds,\n ) as unknown as ExistingEventRow[]\n if (existing.length > 0) {\n const foreign = existing.find(\n (row) => row.session_id !== sessionId,\n )\n if (foreign) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `event id '${foreign.event_id}' already exists in another session`,\n )\n }\n if (existing.length === events.length) {\n return {\n events: idempotentReplay(events, existing.map(toStored)),\n hasPending: Number(existing[0]!.session_has_pending) === 1,\n }\n }\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`,\n )\n }\n }\n\n // Attempt-currency fence: a fresh handler append commits only\n // while its causal attempt is still the parent's latest\n // (store.ts `append`). `begin immediate` serializes this read\n // against a concurrent claim.\n for (const e of events) {\n if (!e.cause) continue\n const parent = db\n .prepare(\n 'select attempt_count, failed_at from a2_events where session_id = ? and idx = ?',\n )\n .get(sessionId, e.cause.index) as\n { attempt_count: number | bigint; failed_at: unknown } | undefined\n if (!parent) {\n throw new TypeError(\n `no event at index ${e.cause.index} in session '${sessionId}'`,\n )\n }\n if (\n Number(parent.attempt_count) !== e.cause.attempt ||\n parent.failed_at !== null\n ) {\n throw new A2Error(\n 'SUPERSEDED_ATTEMPT',\n `attempt ${e.cause.attempt} no longer owns event ${e.cause.index} in session '${sessionId}'`,\n )\n }\n }\n\n const status = appendStatus.get(\n sessionId,\n sessionId,\n ) as AppendStatusRow\n const base = Number(status.max)\n const now = clock.now().getTime()\n const inserted: StoredEvent[] = events.map((e, i) => {\n const id = e.id ?? generateId()\n const index = base + 1 + i\n insertEvent.run(\n sessionId,\n index,\n e.type,\n JSON.stringify(e.payload) ?? 'null',\n id,\n now,\n e.cause ? JSON.stringify(e.cause) : null,\n e.lane ?? null,\n e.lane ?? null,\n sessionId,\n e.lane ?? null,\n e.settled ? now : null,\n )\n return {\n id,\n type: e.type,\n payload: structuredClone(e.payload),\n index,\n sessionId,\n createdAt: new Date(now),\n cause: e.cause ? { ...e.cause } : null,\n lane: e.lane ?? null,\n processedAt: e.settled ? new Date(now) : null,\n processedByAttempt: null,\n returnedEventIds: null,\n firstClaimedAt: null,\n lastClaimedAt: null,\n attemptCount: 0,\n failureCount: 0,\n lastFailedAt: null,\n lastFailedAttempt: null,\n lastError: null,\n failedAt: null,\n claimHolder: null,\n claimExpiresAt: null,\n }\n })\n return {\n events: inserted,\n hasPending:\n Number(status.has_pending) === 1 ||\n events.some((event) => event.settled !== true),\n }\n }),\n )\n },\n\n async read(sessionId, opts) {\n return wrap(() => {\n const conditions = ['session_id = ?']\n const params: (string | number)[] = [sessionId]\n if (opts?.afterIndex !== undefined) {\n conditions.push('idx > ?')\n params.push(opts.afterIndex)\n }\n if (opts?.throughIndex !== undefined) {\n conditions.push('idx <= ?')\n params.push(opts.throughIndex)\n }\n const rows = db\n .prepare(\n `select * from a2_events where ${conditions.join(' and ')} order by idx`,\n )\n .all(...params) as unknown as EventRow[]\n return rows.map(toStored)\n })\n },\n\n async claimAvailable({\n sessionId,\n holder,\n ttlMs,\n expiresAtMs,\n excludeIndexes = [],\n }) {\n return wrap(() =>\n tx(() => {\n const now = clock.now().getTime()\n const expiresAt = expiresAtMs ?? now + ttlMs\n const exclusion =\n excludeIndexes.length === 0\n ? ''\n : `and event.idx not in (${excludeIndexes.map(() => '?').join(', ')})`\n const eligible = db\n .prepare(\n `select event.idx\n from a2_events event\n where event.session_id = ?\n and event.processed_at is null\n and event.failed_at is null\n and event.lane_ready = 1\n and (event.claim_expires_at is null or event.claim_expires_at <= ?)\n ${exclusion}\n order by event.idx`,\n )\n .all(sessionId, now, ...excludeIndexes) as unknown as Array<{\n idx: number | bigint\n }>\n if (eligible.length > 0) {\n const update = db.prepare(\n `update a2_events set\n attempt_count = attempt_count + 1,\n first_claimed_at = coalesce(first_claimed_at, ?),\n last_claimed_at = ?,\n claim_holder = ?,\n claim_expires_at = ?\n where session_id = ? and idx = ?\n returning *`,\n )\n const events = eligible.map((row) => {\n const claimed = update.get(\n now,\n now,\n holder,\n expiresAt,\n sessionId,\n row.idx,\n ) as EventRow\n return toStored(claimed)\n })\n return { outcome: 'claimed', events }\n }\n\n const active = db\n .prepare(\n `select min(claim_expires_at) as retry_at\n from a2_events\n where session_id = ?\n and processed_at is null\n and failed_at is null\n and lane_ready = 1\n and claim_expires_at > ?`,\n )\n .get(sessionId, now) as {\n retry_at: number | bigint | null\n }\n return active.retry_at === null\n ? { outcome: 'settled' }\n : { outcome: 'busy', retryAt: new Date(Number(active.retry_at)) }\n }),\n )\n },\n\n async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {\n if (claims.length === 0) return { renewed: [], superseded: [] }\n return wrap(() =>\n tx(() => {\n const now = clock.now().getTime()\n const expiresAt = expiresAtMs ?? now + ttlMs\n const renewed: number[] = []\n const superseded: number[] = []\n const currentAttempt = db.prepare(\n 'select attempt_count from a2_events where session_id = ? and idx = ?',\n )\n const extend = db.prepare(\n `update a2_events set claim_expires_at = ?\n where session_id = ?\n and idx = ?\n and attempt_count = ?\n and processed_at is null\n and failed_at is null\n and claim_holder = ?\n and claim_expires_at > ?`,\n )\n for (const claim of claims) {\n const row = currentAttempt.get(sessionId, claim.index) as\n { attempt_count: number | bigint } | undefined\n if (!row) continue\n if (Number(row.attempt_count) > claim.attempt) {\n superseded.push(claim.index)\n continue\n }\n const updated = extend.run(\n expiresAt,\n sessionId,\n claim.index,\n claim.attempt,\n holder,\n now,\n )\n if (Number(updated.changes) === 1) renewed.push(claim.index)\n }\n return {\n renewed: renewed.toSorted((a, b) => a - b),\n superseded: superseded.toSorted((a, b) => a - b),\n }\n }),\n )\n },\n\n async completeAttempt({ sessionId, index, attempt, events }) {\n return wrap(() =>\n tx(() => {\n const parent = db\n .prepare('select * from a2_events where session_id = ? and idx = ?')\n .get(sessionId, index) as EventRow | undefined\n if (!parent) {\n throw new TypeError(\n `no event at index ${index} in session '${sessionId}'`,\n )\n }\n const ids = events.map((event) => event.id)\n if (new Set(ids).size !== ids.length) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'returned event batch contains the same event id more than once',\n )\n }\n\n if (parent.processed_at !== null) {\n if (Number(parent.processed_by_attempt) !== attempt) {\n return { outcome: 'superseded' }\n }\n const returnedIds =\n parent.returned_event_ids === null\n ? null\n : decodeReturnedEventIds(parent.returned_event_ids)\n if (\n returnedIds === null ||\n returnedIds.length !== ids.length ||\n returnedIds.some((id, offset) => id !== ids[offset])\n ) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'completed attempt does not match the returned event batch',\n )\n }\n const existing =\n ids.length === 0\n ? []\n : (selectByIds(ids.length).all(\n sessionId,\n ...ids,\n ) as unknown as ExistingEventRow[])\n const byId = new Map(existing.map((row) => [row.event_id, row]))\n const ordered = ids.map((id) => byId.get(id))\n if (\n ordered.some(\n (row) =>\n !row ||\n row.session_id !== sessionId ||\n row.cause === null ||\n toCause(row.cause)?.index !== index ||\n toCause(row.cause)?.attempt !== attempt,\n )\n ) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'returned event retry does not match the committed batch',\n )\n }\n return {\n outcome: 'completed',\n events: idempotentReplay(\n events,\n (ordered as ExistingEventRow[]).map(toStored),\n ),\n }\n }\n if (\n Number(parent.attempt_count) !== attempt ||\n parent.claim_holder === null ||\n parent.failed_at !== null\n ) {\n return { outcome: 'superseded' }\n }\n\n if (\n ids.length > 0 &&\n (selectByIds(ids.length).all(sessionId, ...ids) as unknown[])\n .length > 0\n ) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'returned event batch contains an already-appended event id',\n )\n }\n\n const status = appendStatus.get(\n sessionId,\n sessionId,\n ) as AppendStatusRow\n const now = clock.now().getTime()\n db.prepare(\n `update a2_events set\n processed_at = ?,\n processed_by_attempt = ?,\n returned_event_ids = ?,\n lane_ready = 0,\n claim_holder = null,\n claim_expires_at = null\n where session_id = ? and idx = ?`,\n ).run(now, attempt, JSON.stringify(ids), sessionId, index)\n if (parent.lane !== null) {\n promoteLaneHead.run(sessionId, sessionId, parent.lane)\n }\n const inserted = events.map((event, offset) => {\n const id = event.id\n const childIndex = Number(status.max) + offset + 1\n const cause = { index, attempt }\n insertEvent.run(\n sessionId,\n childIndex,\n event.type,\n JSON.stringify(event.payload) ?? 'null',\n id,\n now,\n JSON.stringify(cause),\n event.lane ?? null,\n event.lane ?? null,\n sessionId,\n event.lane ?? null,\n event.settled ? now : null,\n )\n return toStored(\n db\n .prepare(\n 'select * from a2_events where session_id = ? and idx = ?',\n )\n .get(sessionId, childIndex) as EventRow,\n )\n })\n return { outcome: 'completed', events: inserted }\n }),\n )\n },\n\n async failAttempt({ sessionId, index, attempt, error, maxFailures }) {\n return wrap(() =>\n tx(() => {\n const current = db\n .prepare('select * from a2_events where session_id = ? and idx = ?')\n .get(sessionId, index) as EventRow | undefined\n if (!current) {\n throw new TypeError(\n `no event at index ${index} in session '${sessionId}'`,\n )\n }\n const failureCount = Number(current.failure_count)\n if (\n current.processed_at !== null ||\n Number(current.attempt_count) !== attempt\n ) {\n return { outcome: 'superseded', failureCount }\n }\n if (current.failed_at !== null) {\n return { outcome: 'dead_lettered', failureCount }\n }\n if (\n current.claim_holder === null &&\n current.last_failed_attempt !== null &&\n Number(current.last_failed_attempt) === attempt\n ) {\n return { outcome: 'failed', failureCount }\n }\n if (current.claim_holder === null) {\n return { outcome: 'superseded', failureCount }\n }\n\n const now = clock.now().getTime()\n const nextFailureCount = failureCount + 1\n const deadLettered = nextFailureCount >= maxFailures\n db.prepare(\n `update a2_events set\n failure_count = ?,\n last_error = ?,\n last_failed_at = ?,\n last_failed_attempt = ?,\n failed_at = ?,\n claim_holder = null,\n claim_expires_at = null\n where session_id = ? and idx = ?`,\n ).run(\n nextFailureCount,\n error,\n now,\n attempt,\n deadLettered ? now : null,\n sessionId,\n index,\n )\n return {\n outcome: deadLettered ? 'dead_lettered' : 'failed',\n failureCount: nextFailureCount,\n }\n }),\n )\n },\n\n async readState(sessionId, reducerName) {\n return wrap(() => {\n const rows = db\n .prepare(\n `with snapshot as materialized (\n select up_to_index, state\n from a2_snapshots\n where session_id = ? and reducer_name = ?\n )\n select 0 as row_order, 'snapshot' as row_kind,\n snapshot.up_to_index as snapshot_index,\n snapshot.state as snapshot_state,\n null as session_id, null as idx, null as event_type,\n null as payload, null as event_id, null as created_at,\n null as cause, null as lane, null as lane_ready,\n null as processed_at,\n null as processed_by_attempt, null as returned_event_ids,\n null as first_claimed_at,\n null as last_claimed_at, null as attempt_count,\n null as failure_count, null as last_failed_at,\n null as last_failed_attempt, null as last_error,\n null as failed_at, null as claim_holder,\n null as claim_expires_at\n from snapshot\n union all\n select 1 as row_order, 'event' as row_kind,\n null as snapshot_index, null as snapshot_state,\n event.*\n from a2_events as event\n where event.session_id = ?\n and event.idx > coalesce((select up_to_index from snapshot), 0)\n order by row_order, idx`,\n )\n .all(sessionId, reducerName, sessionId) as unknown as StateReadRow[]\n const snapshot = rows.find((row) => row.row_kind === 'snapshot')\n return {\n snapshot: snapshot\n ? {\n index: Number(snapshot.snapshot_index),\n state: JSON.parse(snapshot.snapshot_state!),\n }\n : null,\n events: rows\n .filter(\n (row): row is StateReadRow & EventRow => row.row_kind === 'event',\n )\n .map(toEvent),\n }\n })\n },\n\n async putSnapshot(sessionId, reducerName, index, state) {\n wrap(() => {\n // Guarded upsert: a slower concurrent writer must never clobber\n // a further-along snapshot (spec §3).\n db.prepare(\n `insert into a2_snapshots (session_id, reducer_name, up_to_index, state, updated_at)\n values (?, ?, ?, ?, ?)\n on conflict (session_id, reducer_name) do update set\n up_to_index = excluded.up_to_index,\n state = excluded.state,\n updated_at = excluded.updated_at\n where excluded.up_to_index > a2_snapshots.up_to_index`,\n ).run(\n sessionId,\n reducerName,\n index,\n JSON.stringify(state) ?? 'null',\n clock.now().getTime(),\n )\n })\n },\n\n presence: {\n async set(ns, participant, values, meta) {\n const entries = Object.entries(values)\n if (entries.length === 0) return\n wrap(() =>\n tx(() => {\n const atMs = meta.at.getTime()\n // Expiry anchors on the storage clock — the sender's `at`\n // orders writes but never anchors their lifetime.\n const expiresAtMs = clock.now().getTime() + meta.ttlMs\n for (const [field, value] of entries) {\n // Field-wise LWW: a stored row with a strictly newer `at`\n // survives; ties go to the incoming write (memory parity).\n if (value === null) {\n deletePresence.run(ns, participant, field, atMs)\n } else {\n upsertPresence.run(\n ns,\n participant,\n field,\n JSON.stringify(value) ?? 'null',\n meta.seen,\n atMs,\n expiresAtMs,\n )\n }\n }\n }),\n )\n },\n\n async read(ns) {\n return wrap(() => {\n const now = clock.now().getTime()\n // The sweep is opportunistic cleanup; the select's filter\n // alone keeps expired rows out of this result.\n sweepPresence.run(ns, now)\n const rows = selectPresence.all(ns, now) as unknown as PresenceDbRow[]\n return rows.map((row) => ({\n participant: row.participant,\n field: row.field,\n value: JSON.parse(row.value) as unknown,\n seen: Number(row.seen),\n at: new Date(Number(row.at)),\n expiresAt: new Date(Number(row.expires_at)),\n }))\n })\n },\n },\n\n inspect: {\n async listSessions(inspectionOptions) {\n return wrap(() => {\n const conditions = ['substr(session_id, 1, ?) = ?']\n const params: (string | number)[] = [\n inspectionOptions.prefix.length,\n inspectionOptions.prefix,\n ]\n if (inspectionOptions.cursor !== undefined) {\n conditions.push('session_id > ?')\n params.push(inspectionOptions.cursor)\n }\n params.push(inspectionOptions.limit + 1)\n const rows = db\n .prepare(\n `select\n session_id,\n count(*) as event_count,\n sum(case when processed_at is null and failed_at is null then 1 else 0 end) as pending_count,\n sum(case when failed_at is not null then 1 else 0 end) as failed_count,\n sum(attempt_count) as attempt_count,\n sum(failure_count) as failure_count,\n min(created_at) as first_event_at,\n max(max(\n created_at,\n coalesce(first_claimed_at, 0),\n coalesce(last_claimed_at, 0),\n coalesce(last_failed_at, 0),\n coalesce(processed_at, 0),\n coalesce(failed_at, 0)\n )) as updated_at\n from a2_events\n where ${conditions.join(' and ')}\n group by session_id\n order by session_id\n limit ?`,\n )\n .all(...params) as unknown as SessionSummaryRow[]\n const hasMore = rows.length > inspectionOptions.limit\n const visible = rows.slice(0, inspectionOptions.limit)\n return {\n sessions: visible.map((row) => ({\n sessionId: row.session_id,\n eventCount: Number(row.event_count),\n pendingCount: Number(row.pending_count),\n failedCount: Number(row.failed_count),\n attemptCount: Number(row.attempt_count),\n failureCount: Number(row.failure_count),\n firstEventAt: new Date(Number(row.first_event_at)),\n updatedAt: new Date(Number(row.updated_at)),\n })),\n cursor: hasMore ? (visible.at(-1)?.session_id ?? null) : null,\n }\n })\n },\n async readEvents(sessionId, pageOptions) {\n return wrap(() => {\n const currentFrontier = Number(\n (\n db\n .prepare(\n 'select coalesce(max(idx), 0) as frontier from a2_events where session_id = ?',\n )\n .get(sessionId) as { frontier: number }\n ).frontier,\n )\n const throughIndex = Math.min(\n pageOptions.throughIndex ?? currentFrontier,\n currentFrontier,\n )\n const rows = db\n .prepare(\n `select * from a2_events\n where session_id = ? and idx > ? and idx <= ?\n order by idx limit ?`,\n )\n .all(\n sessionId,\n pageOptions.afterIndex,\n throughIndex,\n pageOptions.limit,\n ) as unknown as EventRow[]\n return { events: rows.map(toStored), throughIndex }\n })\n },\n async listSnapshots(sessionId) {\n return wrap(() => {\n const rows = db\n .prepare(\n `select reducer_name, up_to_index, updated_at\n from a2_snapshots where session_id = ? order by reducer_name`,\n )\n .all(sessionId) as unknown as SnapshotRow[]\n return rows.map((row) => ({\n reducerName: row.reducer_name,\n index: Number(row.up_to_index),\n updatedAt: new Date(Number(row.updated_at)),\n }))\n })\n },\n },\n\n stream(sessionId, opts) {\n const readAfter = (afterIndex: number): StoredEvent[] =>\n wrap(() => {\n const rows = db\n .prepare(\n 'select * from a2_events where session_id = ? and idx > ? order by idx',\n )\n .all(sessionId, afterIndex) as unknown as EventRow[]\n return rows.map(toStored)\n })\n return pollingStream(\n readAfter,\n opts?.startAfter !== undefined ? { startAfter: opts.startAfter } : {},\n )\n },\n\n close() {\n db.close()\n },\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"store-sqlite.js","names":[],"sources":["../src/store-sqlite.ts"],"sourcesContent":["/**\n * experimental-a2/store-sqlite — sqlite store backend (the dev default, `.a2/dev.db`).\n *\n * Built on `node:sqlite` (Node ≥ 22.13) so it ships with zero\n * dependencies. Implements the A2Store interface; the conformance suite\n * in test/conformance is the executable contract.\n *\n * The spec's `index` column is stored as `idx` (a2-implementation.md\n * §11 — `index` collides with a reserved word) and mapped back at the\n * API boundary. Timestamps are epoch milliseconds written from the\n * injected clock — never SQL `now()` — so tests can time-travel against\n * real storage.\n */\n\nimport { mkdirSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport { DatabaseSync } from 'node:sqlite'\nimport { A2Error } from './errors.ts'\nimport { idempotentReplay } from './idempotent-replay.ts'\nimport { decodeReturnedEventIds } from './store-codec.ts'\nimport { pollingStream } from './store-polling.ts'\nimport {\n RANDOM_IDS,\n SYSTEM_CLOCK,\n type A2Store,\n type Clock,\n type Event,\n type EventCause,\n type IdSource,\n type StoredEvent,\n} from './store.ts'\n\nexport type SqliteStoreOptions = {\n /** Database file path. Defaults to `.a2/dev.db`; `:memory:` works. */\n path?: string\n /** Injectable clock — every stored timestamp comes from here. */\n clock?: Clock\n /** Injectable id source for generated event ids. */\n ids?: IdSource\n}\n\nexport type SqliteStore = A2Store & {\n /** Close the underlying database handle. */\n close(): void\n}\n\nconst SCHEMA = `\ncreate table if not exists a2_events (\n session_id text not null,\n idx integer not null,\n event_type text not null,\n payload text not null,\n event_id text not null,\n created_at integer not null,\n cause text,\n lane text,\n lane_ready integer not null default 0,\n processed_at integer,\n processed_by_attempt integer,\n returned_event_ids text,\n first_claimed_at integer,\n last_claimed_at integer,\n attempt_count integer not null default 0,\n failure_count integer not null default 0,\n last_failed_at integer,\n last_failed_attempt integer,\n last_error text,\n failed_at integer,\n claim_holder text,\n claim_expires_at integer,\n primary key (session_id, idx)\n) strict;\n\ncreate unique index if not exists a2_events_event_id on a2_events (event_id);\ncreate index if not exists a2_events_unprocessed\n on a2_events (session_id, idx) where processed_at is null;\ncreate index if not exists a2_events_lane_pending\n on a2_events (session_id, lane, idx) where processed_at is null;\ncreate index if not exists a2_events_dispatch_ready\n on a2_events (session_id, idx)\n where lane_ready = 1 and processed_at is null and failed_at is null;\ncreate index if not exists a2_events_claim_expiry\n on a2_events (session_id, claim_expires_at)\n where lane_ready = 1 and processed_at is null and failed_at is null;\n\ncreate table if not exists a2_snapshots (\n session_id text not null,\n reducer_name text not null,\n up_to_index integer not null,\n state text not null,\n updated_at integer not null,\n primary key (session_id, reducer_name)\n) strict;\n\ncreate table if not exists a2_presence (\n ns text not null,\n participant text not null,\n field text not null,\n value text not null,\n seen integer not null,\n at integer not null,\n expires_at integer not null,\n primary key (ns, participant, field)\n) strict;\n`\n\n/**\n * Switching journal modes takes an exclusive lock, and — unlike normal\n * statements — the switch can return SQLITE_BUSY without consulting the\n * busy handler while several connections race it (instances cold-booting\n * against one file). Bounded synchronous retry; the window is boot-only\n * and tiny. Found by the multi-process torture test.\n */\nconst setWalJournalMode = (db: DatabaseSync): void => {\n for (let attempt = 0; ; attempt += 1) {\n try {\n db.exec('pragma journal_mode = wal')\n return\n } catch (err) {\n const busy = (err as { errcode?: number }).errcode === 5\n if (!busy || attempt >= 100) throw err\n // Synchronous 5ms sleep — sqlite() is a sync constructor.\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5)\n }\n }\n}\n\nconst wrap = <T>(fn: () => T): T => {\n try {\n return fn()\n } catch (err) {\n if (err instanceof A2Error || err instanceof TypeError) throw err\n throw new A2Error('STORE_UNAVAILABLE', 'sqlite store operation failed', {\n cause: err,\n })\n }\n}\n\nconst toDate = (ms: number | bigint | null): Date | null =>\n ms === null ? null : new Date(Number(ms))\n\nconst toCause = (json: string | null): EventCause | null => {\n if (json === null) return null\n const value = JSON.parse(json) as {\n index?: unknown\n attempt?: unknown\n batchSize?: unknown\n }\n if (\n !Number.isInteger(value.index) ||\n Number(value.index) < 1 ||\n !Number.isInteger(value.attempt) ||\n Number(value.attempt) < 1\n ) {\n throw new TypeError('stored event has an invalid cause')\n }\n if (\n value.batchSize !== undefined &&\n (!Number.isInteger(value.batchSize) || Number(value.batchSize) < 1)\n ) {\n throw new TypeError('stored event has an invalid cause')\n }\n return {\n index: Number(value.index),\n attempt: Number(value.attempt),\n ...(value.batchSize === undefined\n ? {}\n : { batchSize: Number(value.batchSize) }),\n }\n}\n\nconst toStored = (row: EventRow): StoredEvent => ({\n id: row.event_id,\n type: row.event_type,\n payload: JSON.parse(row.payload),\n index: Number(row.idx),\n sessionId: row.session_id,\n createdAt: new Date(Number(row.created_at)),\n cause: toCause(row.cause),\n lane: row.lane,\n processedAt: toDate(row.processed_at),\n processedByAttempt:\n row.processed_by_attempt === null ? null : Number(row.processed_by_attempt),\n returnedEventIds:\n row.returned_event_ids === null\n ? null\n : decodeReturnedEventIds(row.returned_event_ids),\n firstClaimedAt: toDate(row.first_claimed_at),\n lastClaimedAt: toDate(row.last_claimed_at),\n attemptCount: Number(row.attempt_count),\n failureCount: Number(row.failure_count),\n lastFailedAt: toDate(row.last_failed_at),\n lastFailedAttempt:\n row.last_failed_attempt === null ? null : Number(row.last_failed_attempt),\n lastError: row.last_error,\n failedAt: toDate(row.failed_at),\n claimHolder: row.claim_holder,\n claimExpiresAt: toDate(row.claim_expires_at),\n})\n\nconst toEvent = (row: EventRow): Event => ({\n id: row.event_id,\n type: row.event_type,\n payload: JSON.parse(row.payload),\n index: Number(row.idx),\n sessionId: row.session_id,\n createdAt: new Date(Number(row.created_at)),\n})\n\ntype EventRow = {\n session_id: string\n idx: number | bigint\n event_type: string\n payload: string\n event_id: string\n created_at: number | bigint\n cause: string | null\n lane: string | null\n processed_at: number | bigint | null\n processed_by_attempt: number | bigint | null\n returned_event_ids: string | null\n first_claimed_at: number | bigint | null\n last_claimed_at: number | bigint | null\n attempt_count: number | bigint\n failure_count: number | bigint\n last_failed_at: number | bigint | null\n last_failed_attempt: number | bigint | null\n last_error: string | null\n failed_at: number | bigint | null\n claim_holder: string | null\n claim_expires_at: number | bigint | null\n}\n\ntype PresenceDbRow = {\n participant: string\n field: string\n value: string\n seen: number | bigint\n at: number | bigint\n expires_at: number | bigint\n}\n\ntype StateReadRow = Partial<EventRow> & {\n row_kind: 'snapshot' | 'event'\n snapshot_index: number | bigint | null\n snapshot_state: string | null\n}\n\ntype AppendStatusRow = {\n max: number | bigint\n has_pending: number | bigint\n}\n\ntype ExistingEventRow = EventRow & {\n session_has_pending: number | bigint\n}\n\nexport function sqlite(options: SqliteStoreOptions = {}): SqliteStore {\n const path = options.path ?? '.a2/dev.db'\n const clock = options.clock ?? SYSTEM_CLOCK\n const generateId = options.ids ?? RANDOM_IDS\n\n if (path !== ':memory:') {\n // The dev-default backend only ever runs outside production, but\n // bundlers can't know that: without the ignore, Turbopack sees a\n // dynamic fs path and traces the entire project into the output.\n mkdirSync(dirname(resolve(/* turbopackIgnore: true */ path)), {\n recursive: true,\n })\n }\n const db = new DatabaseSync(path)\n // busy_timeout before anything that takes locks: concurrent\n // cold-opens (several instances booting at once) race the WAL switch\n // and schema creation.\n db.exec('pragma busy_timeout = 5000')\n setWalJournalMode(db)\n db.exec('pragma synchronous = normal')\n db.exec(SCHEMA)\n\n const insertEvent = db.prepare(\n `insert into a2_events\n (session_id, idx, event_type, payload, event_id, created_at,\n cause, lane, lane_ready, processed_at)\n select ?, ?, ?, ?, ?, ?, ?, ?,\n case\n when ? is null then 1\n when not exists (\n select 1 from a2_events\n where session_id = ? and lane = ? and processed_at is null\n ) then 1\n else 0\n end,\n ?`,\n )\n const promoteLaneHead = db.prepare(\n `update a2_events set lane_ready = 1\n where session_id = ?\n and idx = (\n select idx from a2_events\n where session_id = ? and lane = ? and processed_at is null\n order by idx limit 1\n )`,\n )\n const appendStatus = db.prepare(\n `select coalesce(\n (select max(idx) from a2_events where session_id = ?),\n 0\n ) as max,\n exists(\n select 1 from a2_events\n where session_id = ? and processed_at is null\n ) as has_pending`,\n )\n const upsertPresence = db.prepare(\n `insert into a2_presence (ns, participant, field, value, seen, at, expires_at)\n values (?, ?, ?, ?, ?, ?, ?)\n on conflict (ns, participant, field) do update set\n value = excluded.value,\n seen = excluded.seen,\n at = excluded.at,\n expires_at = excluded.expires_at\n where a2_presence.at <= excluded.at`,\n )\n const deletePresence = db.prepare(\n `delete from a2_presence\n where ns = ? and participant = ? and field = ? and at <= ?`,\n )\n const sweepPresence = db.prepare(\n 'delete from a2_presence where ns = ? and expires_at <= ?',\n )\n const selectPresence = db.prepare(\n `select participant, field, value, seen, at, expires_at\n from a2_presence\n where ns = ? and expires_at > ?\n order by participant, field`,\n )\n const selectByIds = (count: number) =>\n db.prepare(\n `select stored.*,\n exists(\n select 1 from a2_events pending\n where pending.session_id = ?\n and pending.processed_at is null\n ) as session_has_pending\n from a2_events stored\n where stored.event_id in (${Array.from({ length: count }, () => '?').join(', ')})\n order by stored.idx`,\n )\n\n const tx = <T>(fn: () => T): T => {\n db.exec('begin immediate')\n try {\n const result = fn()\n db.exec('commit')\n return result\n } catch (err) {\n try {\n db.exec('rollback')\n } catch {\n // Rollback can fail if the transaction never started; the\n // original error is what matters.\n }\n throw err\n }\n }\n\n return {\n async append(sessionId, events) {\n if (events.length === 0) {\n return wrap(() => {\n const status = appendStatus.get(\n sessionId,\n sessionId,\n ) as AppendStatusRow\n return {\n events: [],\n hasPending: Number(status.has_pending) === 1,\n }\n })\n }\n return wrap(() =>\n tx(() => {\n const supplied = events.filter((e) => e.id !== undefined)\n const suppliedIds = supplied.map((e) => e.id as string)\n if (new Set(suppliedIds).size !== suppliedIds.length) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'batch contains the same event id more than once',\n )\n }\n if (suppliedIds.length > 0) {\n const existing = selectByIds(suppliedIds.length).all(\n sessionId,\n ...suppliedIds,\n ) as unknown as ExistingEventRow[]\n if (existing.length > 0) {\n const foreign = existing.find(\n (row) => row.session_id !== sessionId,\n )\n if (foreign) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `event id '${foreign.event_id}' already exists in another session`,\n )\n }\n if (existing.length === events.length) {\n return {\n events: idempotentReplay(events, existing.map(toStored)),\n hasPending: Number(existing[0]!.session_has_pending) === 1,\n }\n }\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `batch mixes ${existing.length} already-appended and ${events.length - existing.length} fresh events`,\n )\n }\n }\n\n // Attempt-currency fence: a fresh handler append commits only\n // while its causal attempt is still the parent's latest\n // (store.ts `append`). `begin immediate` serializes this read\n // against a concurrent claim.\n for (const e of events) {\n if (!e.cause) continue\n const parent = db\n .prepare(\n 'select attempt_count, failed_at from a2_events where session_id = ? and idx = ?',\n )\n .get(sessionId, e.cause.index) as\n { attempt_count: number | bigint; failed_at: unknown } | undefined\n if (!parent) {\n throw new TypeError(\n `no event at index ${e.cause.index} in session '${sessionId}'`,\n )\n }\n if (\n Number(parent.attempt_count) !== e.cause.attempt ||\n parent.failed_at !== null\n ) {\n throw new A2Error(\n 'SUPERSEDED_ATTEMPT',\n `attempt ${e.cause.attempt} no longer owns event ${e.cause.index} in session '${sessionId}'`,\n )\n }\n }\n\n const status = appendStatus.get(\n sessionId,\n sessionId,\n ) as AppendStatusRow\n const base = Number(status.max)\n const now = clock.now().getTime()\n const inserted: StoredEvent[] = events.map((e, i) => {\n const id = e.id ?? generateId()\n const index = base + 1 + i\n insertEvent.run(\n sessionId,\n index,\n e.type,\n JSON.stringify(e.payload) ?? 'null',\n id,\n now,\n e.cause ? JSON.stringify(e.cause) : null,\n e.lane ?? null,\n e.lane ?? null,\n sessionId,\n e.lane ?? null,\n e.settled ? now : null,\n )\n return {\n id,\n type: e.type,\n payload: structuredClone(e.payload),\n index,\n sessionId,\n createdAt: new Date(now),\n cause: e.cause ? { ...e.cause } : null,\n lane: e.lane ?? null,\n processedAt: e.settled ? new Date(now) : null,\n processedByAttempt: null,\n returnedEventIds: null,\n firstClaimedAt: null,\n lastClaimedAt: null,\n attemptCount: 0,\n failureCount: 0,\n lastFailedAt: null,\n lastFailedAttempt: null,\n lastError: null,\n failedAt: null,\n claimHolder: null,\n claimExpiresAt: null,\n }\n })\n return {\n events: inserted,\n hasPending:\n Number(status.has_pending) === 1 ||\n events.some((event) => event.settled !== true),\n }\n }),\n )\n },\n\n async read(sessionId, opts) {\n return wrap(() => {\n const conditions = ['session_id = ?']\n const params: (string | number)[] = [sessionId]\n if (opts?.afterIndex !== undefined) {\n conditions.push('idx > ?')\n params.push(opts.afterIndex)\n }\n if (opts?.throughIndex !== undefined) {\n conditions.push('idx <= ?')\n params.push(opts.throughIndex)\n }\n const rows = db\n .prepare(\n `select * from a2_events where ${conditions.join(' and ')} order by idx`,\n )\n .all(...params) as unknown as EventRow[]\n return rows.map(toStored)\n })\n },\n\n async claimAvailable({\n sessionId,\n holder,\n ttlMs,\n expiresAtMs,\n excludeIndexes = [],\n }) {\n return wrap(() =>\n tx(() => {\n const now = clock.now().getTime()\n const expiresAt = expiresAtMs ?? now + ttlMs\n const exclusion =\n excludeIndexes.length === 0\n ? ''\n : `and event.idx not in (${excludeIndexes.map(() => '?').join(', ')})`\n const eligible = db\n .prepare(\n `select event.idx\n from a2_events event\n where event.session_id = ?\n and event.processed_at is null\n and event.failed_at is null\n and event.lane_ready = 1\n and (event.claim_expires_at is null or event.claim_expires_at <= ?)\n ${exclusion}\n order by event.idx`,\n )\n .all(sessionId, now, ...excludeIndexes) as unknown as Array<{\n idx: number | bigint\n }>\n if (eligible.length > 0) {\n const update = db.prepare(\n `update a2_events set\n attempt_count = attempt_count + 1,\n first_claimed_at = coalesce(first_claimed_at, ?),\n last_claimed_at = ?,\n claim_holder = ?,\n claim_expires_at = ?\n where session_id = ? and idx = ?\n returning *`,\n )\n const events = eligible.map((row) => {\n const claimed = update.get(\n now,\n now,\n holder,\n expiresAt,\n sessionId,\n row.idx,\n ) as EventRow\n return toStored(claimed)\n })\n return { outcome: 'claimed', events }\n }\n\n const active = db\n .prepare(\n `select min(claim_expires_at) as retry_at\n from a2_events\n where session_id = ?\n and processed_at is null\n and failed_at is null\n and lane_ready = 1\n and claim_expires_at > ?`,\n )\n .get(sessionId, now) as {\n retry_at: number | bigint | null\n }\n return active.retry_at === null\n ? { outcome: 'settled' }\n : { outcome: 'busy', retryAt: new Date(Number(active.retry_at)) }\n }),\n )\n },\n\n async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {\n if (claims.length === 0) return { renewed: [], superseded: [] }\n return wrap(() =>\n tx(() => {\n const now = clock.now().getTime()\n const expiresAt = expiresAtMs ?? now + ttlMs\n const renewed: number[] = []\n const superseded: number[] = []\n const currentAttempt = db.prepare(\n 'select attempt_count from a2_events where session_id = ? and idx = ?',\n )\n const extend = db.prepare(\n `update a2_events set claim_expires_at = ?\n where session_id = ?\n and idx = ?\n and attempt_count = ?\n and processed_at is null\n and failed_at is null\n and claim_holder = ?\n and claim_expires_at > ?`,\n )\n for (const claim of claims) {\n const row = currentAttempt.get(sessionId, claim.index) as\n { attempt_count: number | bigint } | undefined\n if (!row) continue\n if (Number(row.attempt_count) > claim.attempt) {\n superseded.push(claim.index)\n continue\n }\n const updated = extend.run(\n expiresAt,\n sessionId,\n claim.index,\n claim.attempt,\n holder,\n now,\n )\n if (Number(updated.changes) === 1) renewed.push(claim.index)\n }\n return {\n renewed: renewed.toSorted((a, b) => a - b),\n superseded: superseded.toSorted((a, b) => a - b),\n }\n }),\n )\n },\n\n async completeAttempt({ sessionId, index, attempt, events }) {\n return wrap(() =>\n tx(() => {\n const parent = db\n .prepare('select * from a2_events where session_id = ? and idx = ?')\n .get(sessionId, index) as EventRow | undefined\n if (!parent) {\n throw new TypeError(\n `no event at index ${index} in session '${sessionId}'`,\n )\n }\n const ids = events.map((event) => event.id)\n if (new Set(ids).size !== ids.length) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'returned event batch contains the same event id more than once',\n )\n }\n\n if (parent.processed_at !== null) {\n if (Number(parent.processed_by_attempt) !== attempt) {\n return { outcome: 'superseded' }\n }\n const returnedIds =\n parent.returned_event_ids === null\n ? null\n : decodeReturnedEventIds(parent.returned_event_ids)\n if (\n returnedIds === null ||\n returnedIds.length !== ids.length ||\n returnedIds.some((id, offset) => id !== ids[offset])\n ) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'completed attempt does not match the returned event batch',\n )\n }\n const existing =\n ids.length === 0\n ? []\n : (selectByIds(ids.length).all(\n sessionId,\n ...ids,\n ) as unknown as ExistingEventRow[])\n const byId = new Map(existing.map((row) => [row.event_id, row]))\n const ordered = ids.map((id) => byId.get(id))\n if (\n ordered.some(\n (row) =>\n !row ||\n row.session_id !== sessionId ||\n row.cause === null ||\n toCause(row.cause)?.index !== index ||\n toCause(row.cause)?.attempt !== attempt,\n )\n ) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'returned event retry does not match the committed batch',\n )\n }\n return {\n outcome: 'completed',\n events: idempotentReplay(\n events,\n (ordered as ExistingEventRow[]).map(toStored),\n ),\n }\n }\n if (\n Number(parent.attempt_count) !== attempt ||\n parent.claim_holder === null ||\n parent.failed_at !== null\n ) {\n return { outcome: 'superseded' }\n }\n\n if (\n ids.length > 0 &&\n (selectByIds(ids.length).all(sessionId, ...ids) as unknown[])\n .length > 0\n ) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'returned event batch contains an already-appended event id',\n )\n }\n\n const status = appendStatus.get(\n sessionId,\n sessionId,\n ) as AppendStatusRow\n const now = clock.now().getTime()\n db.prepare(\n `update a2_events set\n processed_at = ?,\n processed_by_attempt = ?,\n returned_event_ids = ?,\n lane_ready = 0,\n claim_holder = null,\n claim_expires_at = null\n where session_id = ? and idx = ?`,\n ).run(now, attempt, JSON.stringify(ids), sessionId, index)\n if (parent.lane !== null) {\n promoteLaneHead.run(sessionId, sessionId, parent.lane)\n }\n const inserted = events.map((event, offset) => {\n const id = event.id\n const childIndex = Number(status.max) + offset + 1\n const cause = { index, attempt }\n insertEvent.run(\n sessionId,\n childIndex,\n event.type,\n JSON.stringify(event.payload) ?? 'null',\n id,\n now,\n JSON.stringify(cause),\n event.lane ?? null,\n event.lane ?? null,\n sessionId,\n event.lane ?? null,\n event.settled ? now : null,\n )\n return toStored(\n db\n .prepare(\n 'select * from a2_events where session_id = ? and idx = ?',\n )\n .get(sessionId, childIndex) as EventRow,\n )\n })\n return { outcome: 'completed', events: inserted }\n }),\n )\n },\n\n async failAttempt({ sessionId, index, attempt, error, maxFailures }) {\n return wrap(() =>\n tx(() => {\n const current = db\n .prepare('select * from a2_events where session_id = ? and idx = ?')\n .get(sessionId, index) as EventRow | undefined\n if (!current) {\n throw new TypeError(\n `no event at index ${index} in session '${sessionId}'`,\n )\n }\n const failureCount = Number(current.failure_count)\n if (\n current.processed_at !== null ||\n Number(current.attempt_count) !== attempt\n ) {\n return { outcome: 'superseded', failureCount }\n }\n if (current.failed_at !== null) {\n return { outcome: 'dead_lettered', failureCount }\n }\n if (\n current.claim_holder === null &&\n current.last_failed_attempt !== null &&\n Number(current.last_failed_attempt) === attempt\n ) {\n return { outcome: 'failed', failureCount }\n }\n if (current.claim_holder === null) {\n return { outcome: 'superseded', failureCount }\n }\n\n const now = clock.now().getTime()\n const nextFailureCount = failureCount + 1\n const deadLettered = nextFailureCount >= maxFailures\n db.prepare(\n `update a2_events set\n failure_count = ?,\n last_error = ?,\n last_failed_at = ?,\n last_failed_attempt = ?,\n failed_at = ?,\n claim_holder = null,\n claim_expires_at = null\n where session_id = ? and idx = ?`,\n ).run(\n nextFailureCount,\n error,\n now,\n attempt,\n deadLettered ? now : null,\n sessionId,\n index,\n )\n return {\n outcome: deadLettered ? 'dead_lettered' : 'failed',\n failureCount: nextFailureCount,\n }\n }),\n )\n },\n\n async readState(sessionId, reducerName) {\n return wrap(() => {\n const rows = db\n .prepare(\n `with snapshot as materialized (\n select up_to_index, state\n from a2_snapshots\n where session_id = ? and reducer_name = ?\n )\n select 0 as row_order, 'snapshot' as row_kind,\n snapshot.up_to_index as snapshot_index,\n snapshot.state as snapshot_state,\n null as session_id, null as idx, null as event_type,\n null as payload, null as event_id, null as created_at,\n null as cause, null as lane, null as lane_ready,\n null as processed_at,\n null as processed_by_attempt, null as returned_event_ids,\n null as first_claimed_at,\n null as last_claimed_at, null as attempt_count,\n null as failure_count, null as last_failed_at,\n null as last_failed_attempt, null as last_error,\n null as failed_at, null as claim_holder,\n null as claim_expires_at\n from snapshot\n union all\n select 1 as row_order, 'event' as row_kind,\n null as snapshot_index, null as snapshot_state,\n event.*\n from a2_events as event\n where event.session_id = ?\n and event.idx > coalesce((select up_to_index from snapshot), 0)\n order by row_order, idx`,\n )\n .all(sessionId, reducerName, sessionId) as unknown as StateReadRow[]\n const snapshot = rows.find((row) => row.row_kind === 'snapshot')\n return {\n snapshot: snapshot\n ? {\n index: Number(snapshot.snapshot_index),\n state: JSON.parse(snapshot.snapshot_state!),\n }\n : null,\n events: rows\n .filter(\n (row): row is StateReadRow & EventRow => row.row_kind === 'event',\n )\n .map(toEvent),\n }\n })\n },\n\n async putSnapshot(sessionId, reducerName, index, state) {\n wrap(() => {\n // Guarded upsert: a slower concurrent writer must never clobber\n // a further-along snapshot (spec §3).\n db.prepare(\n `insert into a2_snapshots (session_id, reducer_name, up_to_index, state, updated_at)\n values (?, ?, ?, ?, ?)\n on conflict (session_id, reducer_name) do update set\n up_to_index = excluded.up_to_index,\n state = excluded.state,\n updated_at = excluded.updated_at\n where excluded.up_to_index > a2_snapshots.up_to_index`,\n ).run(\n sessionId,\n reducerName,\n index,\n JSON.stringify(state) ?? 'null',\n clock.now().getTime(),\n )\n })\n },\n\n presence: {\n async set(ns, participant, values, meta) {\n const entries = Object.entries(values)\n if (entries.length === 0) return\n wrap(() =>\n tx(() => {\n const atMs = meta.at.getTime()\n // Expiry anchors on the storage clock — the sender's `at`\n // orders writes but never anchors their lifetime.\n const expiresAtMs = clock.now().getTime() + meta.ttlMs\n for (const [field, value] of entries) {\n // Field-wise LWW: a stored row with a strictly newer `at`\n // survives; ties go to the incoming write (memory parity).\n if (value === null) {\n deletePresence.run(ns, participant, field, atMs)\n } else {\n upsertPresence.run(\n ns,\n participant,\n field,\n JSON.stringify(value) ?? 'null',\n meta.seen,\n atMs,\n expiresAtMs,\n )\n }\n }\n }),\n )\n },\n\n async read(ns) {\n return wrap(() => {\n const now = clock.now().getTime()\n // The sweep is opportunistic cleanup; the select's filter\n // alone keeps expired rows out of this result.\n sweepPresence.run(ns, now)\n const rows = selectPresence.all(ns, now) as unknown as PresenceDbRow[]\n return rows.map((row) => ({\n participant: row.participant,\n field: row.field,\n value: JSON.parse(row.value) as unknown,\n seen: Number(row.seen),\n at: new Date(Number(row.at)),\n expiresAt: new Date(Number(row.expires_at)),\n }))\n })\n },\n },\n\n stream(sessionId, opts) {\n const readAfter = (afterIndex: number): StoredEvent[] =>\n wrap(() => {\n const rows = db\n .prepare(\n 'select * from a2_events where session_id = ? and idx > ? order by idx',\n )\n .all(sessionId, afterIndex) as unknown as EventRow[]\n return rows.map(toStored)\n })\n return pollingStream(\n readAfter,\n opts?.startAfter !== undefined ? { startAfter: opts.startAfter } : {},\n )\n },\n\n close() {\n db.close()\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as A2Error } from "./errors-BQuJpe82.js";
|
|
2
|
-
import {
|
|
2
|
+
import { h as nullProtoRecord } from "./internal-DRXJ56EI.js";
|
|
3
3
|
//#region src/wire.ts
|
|
4
4
|
/**
|
|
5
|
-
* The wire format shared by
|
|
5
|
+
* The wire format shared by server.fetch and experimental-a2/client
|
|
6
6
|
* (browser side): events as JSON with ISO timestamps, and the A2Error
|
|
7
7
|
* envelope. Internal module — both entry points re-export what's
|
|
8
8
|
* public.
|
|
@@ -89,6 +89,7 @@ function isWirePresenceSnapshot(value) {
|
|
|
89
89
|
}
|
|
90
90
|
const ERROR_STATUS = {
|
|
91
91
|
INVALID_PAYLOAD: 400,
|
|
92
|
+
FORBIDDEN: 403,
|
|
92
93
|
UNKNOWN_EVENT_TYPE: 400,
|
|
93
94
|
PARTIAL_DUPLICATE_BATCH: 400,
|
|
94
95
|
SUPERSEDED_ATTEMPT: 409,
|
|
@@ -281,4 +282,4 @@ const sessionTag = (frame) => {
|
|
|
281
282
|
//#endregion
|
|
282
283
|
export { socketErrorAckFor as _, errorToWire as a, socketUnsubscribedFor as b, isWireEvent as c, parseSocketFrame as d, presencePatchFromWire as f, socketAckFor as g, presenceSnapshotToWire as h, errorStatus as i, isWirePresencePatch as l, presenceSnapshotFromWire as m, asA2Error as n, eventFromWire as o, presencePatchToWire as p, errorFromWire as r, eventToWire as s, SOCKET_PING_FRAME as t, isWirePresenceSnapshot as u, socketFrameFor as v, socketSubscribedFor as y };
|
|
283
284
|
|
|
284
|
-
//# sourceMappingURL=wire-
|
|
285
|
+
//# sourceMappingURL=wire-B6te_wns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire-B6te_wns.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 } from './errors.ts'\nimport { nullProtoRecord } from './internal.ts'\nimport type { Event } from './store.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\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: A2ErrorCode; message: string; details?: unknown }\n}\n\nconst ERROR_STATUS: Record<A2ErrorCode, number> = {\n INVALID_PAYLOAD: 400,\n FORBIDDEN: 403,\n UNKNOWN_EVENT_TYPE: 400,\n PARTIAL_DUPLICATE_BATCH: 400,\n SUPERSEDED_ATTEMPT: 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}\n\nexport function errorStatus(code: A2ErrorCode): number {\n return ERROR_STATUS[code]\n}\n\nexport function errorToWire(error: A2Error): WireError {\n const body: WireError = {\n error: { code: error.code, message: error.message },\n }\n if (error.details !== undefined) body.error.details = error.details\n return body\n}\n\n/** Wrap an arbitrary thrown value for the wire: A2Errors 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\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' || !Object.hasOwn(ERROR_STATUS, code))\n return null\n return new A2Error(code as A2ErrorCode, String(message ?? code), {\n details,\n })\n}\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 SocketDownFrame =\n | { kind: 'event'; event: WireEvent }\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 }\n | { kind: 'unsubscribed'; sessionId: string; reason?: string }\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/** 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: Array<{ id: string; index: number }>\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,\n sessionId?: string,\n): string {\n const tag = sessionId === undefined ? {} : { sessionId }\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\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(sessionId: string): string {\n return JSON.stringify({ kind: 'subscribed', sessionId })\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(\n sessionId: string,\n reason?: string,\n): string {\n return JSON.stringify({\n kind: 'unsubscribed',\n sessionId,\n ...(reason === undefined ? {} : { reason }),\n })\n}\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 '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 '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 = frame['sessionId']\n if (typeof sessionId !== 'string') return null\n return { kind: 'subscribed', sessionId }\n }\n case 'unsubscribed': {\n const sessionId = frame['sessionId']\n if (typeof sessionId !== 'string') return null\n const reason = frame['reason']\n return typeof reason === 'string'\n ? { kind: 'unsubscribed', sessionId, reason }\n : { kind: 'unsubscribed', sessionId }\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":";;;;;;;;;AA2BA,SAAgB,YAAY,OAAyB;CACnD,OAAO;EACL,IAAI,MAAM;EACV,MAAM,MAAM;EACZ,SAAS,MAAM;EACf,OAAO,MAAM;EACb,WAAW,MAAM;EACjB,WAAW,MAAM,UAAU,YAAY;CACzC;AACF;AAEA,SAAgB,cAAc,MAAwB;CACpD,OAAO;EACL,IAAI,KAAK;EACT,MAAM,KAAK;EACX,SAAS,KAAK;EACd,OAAO,KAAK;EACZ,WAAW,KAAK;EAChB,WAAW,IAAI,KAAK,KAAK,SAAS;CACpC;AACF;AAEA,SAAgB,YAAY,OAAoC;CAC9D,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO;CACxD,MAAM,IAAI;CACV,OACE,OAAO,EAAE,UAAU,YACnB,OAAO,EAAE,YAAY,YACrB,OAAO,EAAE,aAAa,YACtB,OAAO,EAAE,iBAAiB,YAC1B,OAAO,EAAE,iBAAiB;AAE9B;AA0BA,SAAgB,oBAAoB,OAAyC;CAC3E,OAAO;EACL,aAAa,MAAM;EACnB,QAAQ,MAAM;EACd,MAAM,MAAM;EACZ,IAAI,MAAM,GAAG,YAAY;CAC3B;AACF;AAEA,SAAgB,sBAAsB,MAAwC;CAC5E,OAAO;EACL,aAAa,KAAK;EAClB,QAAQ,KAAK;EACb,MAAM,KAAK;EACX,IAAI,IAAI,KAAK,KAAK,EAAE;CACtB;AACF;AAIA,SAAgB,uBACd,UACsB;CACtB,MAAM,OAAyC,gBAAgB;CAC/D,KAAK,MAAM,CAAC,aAAa,WAAW,OAAO,QAAQ,SAAS,QAAQ,GAAG;EACrE,MAAM,aACJ,gBAAgB;EAClB,KAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,MAAM,GAAG;GACnD,IAAI,UAAU,KAAA,GAAW;GACzB,WAAW,SAAS;IAClB,OAAO,MAAM;IACb,MAAM,MAAM;IACZ,IAAI,MAAM,GAAG,YAAY;GAC3B;EACF;EACA,KAAK,eAAe;CACtB;CACA,OAAO,EAAE,UAAU,KAAK;AAC1B;AAEA,SAAgB,yBACd,MACkB;CAClB,MAAM,MAAmB,gBAAgB;CACzC,KAAK,MAAM,CAAC,aAAa,WAAW,OAAO,QAAQ,KAAK,QAAQ,GAAG;EACjE,MAAM,UAA+B,gBAAgB;EACrD,KAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,MAAM,GAChD,QAAQ,SAAS;GACf,OAAO,MAAM;GACb,MAAM,MAAM;GACZ,IAAI,IAAI,KAAK,MAAM,EAAE;EACvB;EAEF,IAAI,eAAe;CACrB;CACA,OAAO,EAAE,UAAU,IAAI;AACzB;AAEA,SAAgB,oBACd,OAC4B;CAC5B,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO;CACxD,MAAM,IAAI;CACV,OACE,OAAO,EAAE,mBAAmB,YAC5B,EAAE,cAAc,QAChB,OAAO,EAAE,cAAc,YACvB,CAAC,MAAM,QAAQ,EAAE,SAAS,KAC1B,OAAO,EAAE,YAAY,YACrB,OAAO,EAAE,UAAU;AAEvB;AAEA,SAAgB,uBACd,OAC+B;CAC/B,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU,OAAO;CACxD,MAAM,IAAI;CACV,OACE,EAAE,gBAAgB,QAClB,OAAO,EAAE,gBAAgB,YACzB,CAAC,MAAM,QAAQ,EAAE,WAAW;AAEhC;AAQA,MAAM,eAA4C;CAChD,iBAAiB;CACjB,WAAW;CACX,oBAAoB;CACpB,yBAAyB;CACzB,oBAAoB;CACpB,eAAe;CACf,mBAAmB;CACnB,sBAAsB;CACtB,wBAAwB;CACxB,wBAAwB;AAC1B;AAEA,SAAgB,YAAY,MAA2B;CACrD,OAAO,aAAa;AACtB;AAEA,SAAgB,YAAY,OAA2B;CACrD,MAAM,OAAkB,EACtB,OAAO;EAAE,MAAM,MAAM;EAAM,SAAS,MAAM;CAAQ,EACpD;CACA,IAAI,MAAM,YAAY,KAAA,GAAW,KAAK,MAAM,UAAU,MAAM;CAC5D,OAAO;AACT;;;;;AAMA,SAAgB,UAAU,OAAyB;CACjD,OAAO,iBAAiB,UACpB,QACA,IAAI,QAAQ,qBAAqB,kBAAkB,EAAE,OAAO,MAAM,CAAC;AACzE;;AAGA,SAAgB,cAAc,MAA+B;CAC3D,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU,OAAO;CACtD,MAAM,MAAO,KAA6B;CAC1C,IAAI,QAAQ,QAAQ,OAAO,QAAQ,UAAU,OAAO;CACpD,MAAM,EAAE,MAAM,SAAS,YAAY;CACnC,IAAI,OAAO,SAAS,YAAY,CAAC,OAAO,OAAO,cAAc,IAAI,GAC/D,OAAO;CACT,OAAO,IAAI,QAAQ,MAAqB,OAAO,WAAW,IAAI,GAAG,EAC/D,QACF,CAAC;AACH;AA8EA,MAAa,oBAA4B,KAAK,UAAU,EAAE,MAAM,OAAO,CAAC;;;;;AAMxE,SAAgB,eACd,MACA,WACQ;CACR,MAAM,MAAM,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU;CACvD,IAAI,cAAc,MAChB,OAAO,KAAK,UAAU;EACpB,MAAM;EACN,GAAG,uBAAuB,IAAI;EAC9B,GAAG;CACL,CAAC;CAEH,IAAI,iBAAiB,MACnB,OAAO,KAAK,UAAU;EACpB,MAAM;EACN,GAAG,oBAAoB,IAAI;EAC3B,GAAG;CACL,CAAC;CAEH,OAAO,KAAK,UAAU;EAAE,MAAM;EAAS,GAAG,YAAY,IAAI;CAAE,CAAC;AAC/D;AAEA,SAAgB,aACd,KACA,QACA,WACQ;CACR,OAAO,KAAK,UAAU;EACpB,MAAM;EACN;EACA,QAAQ,OAAO,IAAI,WAAW;EAC9B,GAAI,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU;CACjD,CAAC;AACH;AAEA,SAAgB,kBACd,KACA,OACA,WACQ;CACR,OAAO,KAAK,UAAU;EACpB,MAAM;EACN;EACA,GAAG,YAAY,KAAK;EACpB,GAAI,cAAc,KAAA,IAAY,CAAC,IAAI,EAAE,UAAU;CACjD,CAAC;AACH;AAEA,SAAgB,oBAAoB,WAA2B;CAC7D,OAAO,KAAK,UAAU;EAAE,MAAM;EAAc;CAAU,CAAC;AACzD;;;AAIA,SAAgB,sBACd,WACA,QACQ;CACR,OAAO,KAAK,UAAU;EACpB,MAAM;EACN;EACA,GAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO;CAC3C,CAAC;AACH;AAEA,SAAgB,iBAAiB,MAAsC;CACrE,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,IAAI;CAC1B,QAAQ;EACN,OAAO;CACT;CACA,IAAI,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GACvE,OAAO;CAET,MAAM,QAAQ;CACd,QAAQ,MAAM,SAAd;EACE,KAAK,QACH,OAAO,EAAE,MAAM,OAAO;EACxB,KAAK,SAAS;GACZ,IAAI,CAAC,YAAY,MAAM,GAAG,OAAO;GACjC,MAAM,EAAE,IAAI,MAAM,SAAS,OAAO,WAAW,cAAc;GAC3D,OAAO;IACL,MAAM;IACN,OAAO;KAAE;KAAI;KAAM;KAAS;KAAO;KAAW;IAAU;GAC1D;EACF;EACA,KAAK,YAAY;GACf,IAAI,CAAC,oBAAoB,MAAM,GAAG,OAAO;GACzC,MAAM,EAAE,aAAa,QAAQ,MAAM,OAAO;GAC1C,OAAO;IACL,MAAM;IACN,OAAO;KAAE;KAAa;KAAQ;KAAM;IAAG;IACvC,GAAG,WAAW,KAAK;GACrB;EACF;EACA,KAAK;GACH,IAAI,CAAC,uBAAuB,MAAM,GAAG,OAAO;GAC5C,OAAO;IACL,MAAM;IACN,UAAU,EAAE,UAAU,OAAO,SAAS;IACtC,GAAG,WAAW,KAAK;GACrB;EAEF,KAAK,cAAc;GACjB,MAAM,YAAY,MAAM;GACxB,IAAI,OAAO,cAAc,UAAU,OAAO;GAC1C,OAAO;IAAE,MAAM;IAAc;GAAU;EACzC;EACA,KAAK,gBAAgB;GACnB,MAAM,YAAY,MAAM;GACxB,IAAI,OAAO,cAAc,UAAU,OAAO;GAC1C,MAAM,SAAS,MAAM;GACrB,OAAO,OAAO,WAAW,WACrB;IAAE,MAAM;IAAgB;IAAW;GAAO,IAC1C;IAAE,MAAM;IAAgB;GAAU;EACxC;EACA,KAAK,OAAO;GACV,MAAM,MAAM,MAAM;GAClB,IAAI,OAAO,QAAQ,UAAU,OAAO;GACpC,MAAM,MAAM,WAAW,KAAK;GAC5B,MAAM,SAAS,MAAM;GACrB,IAAI,MAAM,QAAQ,MAAM,KAAK,OAAO,MAAM,WAAW,GACnD,OAAO;IAAE,MAAM;IAAO;IAAK;IAAQ,GAAG;GAAI;GAE5C,MAAM,QAAQ,cAAc,MAAM;GAClC,IAAI,OAAO,OAAO;IAAE,MAAM;IAAO;IAAK;IAAO,GAAG;GAAI;GAOpD,OAAO;IACL,MAAM;IACN;IACA,OAAO,IAAI,QAAQ,qBAAqB,oBAAoB;IAC5D,GAAG;GACL;EACF;EACA,SACE,OAAO;CACX;AACF;AAEA,MAAM,cAAc,UAA2D;CAC7E,MAAM,YAAY,MAAM;CACxB,OAAO,OAAO,cAAc,WAAW,EAAE,UAAU,IAAI,CAAC;AAC1D"}
|
package/docs/guides/03-react.mdx
CHANGED
|
@@ -23,22 +23,15 @@ reducer, and the provider + hook it returns.
|
|
|
23
23
|
|
|
24
24
|
## The API route
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
`server.fetch` exposes a session over HTTP. It is bound, so the route can
|
|
27
|
+
export it directly: `GET` streams events and serves history slices;
|
|
28
|
+
`POST` appends.
|
|
29
29
|
|
|
30
30
|
```ts app/api/order-events/route.ts
|
|
31
|
-
import { handle } from 'experimental-a2/http'
|
|
32
31
|
import { ordersServer } from '@/server/orders'
|
|
33
32
|
|
|
34
|
-
export const
|
|
35
|
-
|
|
36
|
-
// here's where you'd do auth, or any other checks. Every lane
|
|
37
|
-
// arrives parsed: intent.type is 'stream', 'history', 'push', or
|
|
38
|
-
// 'ws-upgrade'. Return a Response to refuse, e.g.:
|
|
39
|
-
// if (!canRead(request, intent)) return new Response(null, { status: 403 })
|
|
40
|
-
},
|
|
41
|
-
})
|
|
33
|
+
export const GET = ordersServer.fetch
|
|
34
|
+
export const POST = ordersServer.fetch
|
|
42
35
|
```
|
|
43
36
|
|
|
44
37
|
`GET` is the read path. A plain `GET` is the live stream: a server-sent
|
|
@@ -48,31 +41,30 @@ paint or a dropped connection. A `GET` carrying `gte`/`lte` query
|
|
|
48
41
|
parameters is a history slice instead: the bounded log range as JSON,
|
|
49
42
|
the cold read [`loadHistory`](#the-client-component) rides.
|
|
50
43
|
|
|
51
|
-
`POST` is the write path. The push envelope is validated
|
|
52
|
-
answers `INVALID_PAYLOAD` before your hooks run), then `append` does the
|
|
44
|
+
`POST` is the write path. The push envelope is validated, then `append` does the
|
|
53
45
|
rest. The response is the appended events: an ack, not a stream. Thrown
|
|
54
46
|
[`A2Error`s](/reference/errors#over-the-wire) serialize onto the wire so
|
|
55
47
|
the client can branch on the same codes.
|
|
56
48
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
immutable slice of an append-only log.
|
|
49
|
+
Wrap `server.fetch` when the route needs authentication, per-operation
|
|
50
|
+
authorization, or response decoration. `authorize` receives the parsed
|
|
51
|
+
A2 operation after envelope parsing and before schema validation or I/O.
|
|
52
|
+
Return `false` to answer `FORBIDDEN`. A thrown error is treated as an
|
|
53
|
+
infrastructure failure.
|
|
63
54
|
|
|
64
55
|
```ts
|
|
65
56
|
// app/api/order-events/route.ts, now with response decoration:
|
|
66
|
-
import { handle } from 'experimental-a2/http'
|
|
67
57
|
import { ordersServer } from '@/server/orders'
|
|
68
58
|
|
|
69
|
-
export
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
59
|
+
export async function GET(request: Request): Promise<Response> {
|
|
60
|
+
const response = await ordersServer.fetch(request)
|
|
61
|
+
if (response.headers.get('a2-history-covered') === 'true') {
|
|
62
|
+
response.headers.set('cache-control', 'private, max-age=31536000')
|
|
63
|
+
}
|
|
64
|
+
return response
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const POST = ordersServer.fetch
|
|
76
68
|
```
|
|
77
69
|
|
|
78
70
|
## The session module
|
|
@@ -89,21 +89,19 @@ export const ordersServer = createServer({
|
|
|
89
89
|
})
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
One scheduler instance is shared by every server on this route.
|
|
93
|
-
|
|
92
|
+
One scheduler instance is shared by every server on this route. Mount its
|
|
93
|
+
handler with every server it may deliver to:
|
|
94
94
|
|
|
95
95
|
```ts app/api/a2/scheduler/route.ts
|
|
96
|
-
import {
|
|
97
|
-
import { ordersServer, billingServer } from '@/server'
|
|
96
|
+
import { billingServer, ordersServer, scheduler } from '@/server'
|
|
98
97
|
|
|
99
|
-
export const POST =
|
|
98
|
+
export const POST = scheduler.handler(ordersServer, billingServer)
|
|
100
99
|
```
|
|
101
100
|
|
|
102
|
-
`
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
can accept a delivery.
|
|
101
|
+
`scheduler.handler(...)` checks the server list synchronously when this module
|
|
102
|
+
loads. It requires at least one drainable server and unique contract names. A
|
|
103
|
+
bad route fails during startup or a build that evaluates route modules, before
|
|
104
|
+
it can accept a delivery.
|
|
107
105
|
|
|
108
106
|
```json vercel.json
|
|
109
107
|
{
|
|
@@ -177,7 +175,7 @@ export const ordersServer = createServer({
|
|
|
177
175
|
})
|
|
178
176
|
```
|
|
179
177
|
|
|
180
|
-
Mount `
|
|
178
|
+
Mount `scheduler.handler(ordersServer, billingServer)` once with the same
|
|
181
179
|
`app/api/a2/scheduler/route.ts` module shown above. Local Node.js development
|
|
182
180
|
needs no QStash environment variables. With `@upstash/qstash` 2.11.0 or newer,
|
|
183
181
|
A2 uses `http://localhost:${PORT ?? 3000}/api/a2/scheduler` and enables the
|
|
@@ -129,14 +129,10 @@ One HTTP route gives the browser a read and write path. `GET` streams events;
|
|
|
129
129
|
`POST` accepts optimistic pushes:
|
|
130
130
|
|
|
131
131
|
```ts app/api/agent-events/route.ts
|
|
132
|
-
import { handle } from 'experimental-a2/http'
|
|
133
132
|
import { assistantServer } from '@/server/assistant'
|
|
134
133
|
|
|
135
|
-
export const
|
|
136
|
-
|
|
137
|
-
// here's where you'd do auth, or any other checks
|
|
138
|
-
},
|
|
139
|
-
})
|
|
134
|
+
export const GET = assistantServer.fetch
|
|
135
|
+
export const POST = assistantServer.fetch
|
|
140
136
|
```
|
|
141
137
|
|
|
142
138
|
The route never calls the model directly. The browser appends user facts such
|
|
@@ -814,10 +810,7 @@ different server assembly:
|
|
|
814
810
|
```ts server/custom.ts
|
|
815
811
|
import { z } from 'zod'
|
|
816
812
|
import { agent } from 'experimental-a2/ai'
|
|
817
|
-
import {
|
|
818
|
-
createHandlers,
|
|
819
|
-
validateAgentPush,
|
|
820
|
-
} from 'experimental-a2/ai/server'
|
|
813
|
+
import { createHandlers } from 'experimental-a2/ai/server'
|
|
821
814
|
import { createServer } from 'experimental-a2/server'
|
|
822
815
|
|
|
823
816
|
const supportAgent = agent({
|
|
@@ -834,7 +827,6 @@ const aiHandlers = createHandlers({
|
|
|
834
827
|
|
|
835
828
|
export const customAssistantServer = createServer({
|
|
836
829
|
contract: supportAgent.contract,
|
|
837
|
-
validatePush: validateAgentPush,
|
|
838
830
|
handlers: {
|
|
839
831
|
...aiHandlers,
|
|
840
832
|
'ticket.linked': async ({ event }) => {
|
|
@@ -847,9 +839,11 @@ export const customAssistantServer = createServer({
|
|
|
847
839
|
|
|
848
840
|
Application events stay fully typed. The standard AI reducer ignores unknown
|
|
849
841
|
events, so another reducer can project application state without forking the AI
|
|
850
|
-
protocol. `
|
|
851
|
-
`
|
|
852
|
-
|
|
842
|
+
protocol. A custom `createServer()` assembly owns its browser ingress policy.
|
|
843
|
+
Use `server.fetch(request, { authorize })` to authorize operations, and reject
|
|
844
|
+
server-only AI event names there before they reach the custom server.
|
|
845
|
+
`createAgentServer()` applies its stricter built-in ingress allowlist
|
|
846
|
+
automatically.
|
|
853
847
|
|
|
854
848
|
### Replace generation, not durability
|
|
855
849
|
|
|
@@ -63,29 +63,25 @@ explicit leave required).
|
|
|
63
63
|
|
|
64
64
|
## The route
|
|
65
65
|
|
|
66
|
-
The same `
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
`setPresence`.
|
|
66
|
+
The same `server.fetch` route as [Live UI](/guides/react). Presence is
|
|
67
|
+
inferred from the contract. A contract with presence fields streams
|
|
68
|
+
presence patches with events, starting with a snapshot of the current
|
|
69
|
+
map. Its push body also accepts an optional `presence` sibling to
|
|
70
|
+
`events`, forwarded to `setPresence`.
|
|
71
71
|
|
|
72
72
|
```ts app/api/canvas-events/route.ts
|
|
73
|
-
import { handle } from 'experimental-a2/http'
|
|
74
73
|
import { canvasServer } from '@/server/canvas'
|
|
75
74
|
|
|
76
|
-
export const
|
|
77
|
-
|
|
78
|
-
before({ request, intent }) {
|
|
79
|
-
// here's where you'd do auth, or any other checks. On a push,
|
|
80
|
-
// intent.presence carries the whole patch; the participant id
|
|
81
|
-
// is caller-supplied, so authorize it like you authorize events.
|
|
82
|
-
// createServer's validatePush is the same seam and covers every
|
|
83
|
-
// transport (socket presence frames never become intents): on the
|
|
84
|
-
// presence plane it receives { sessionId, events: [], presence }.
|
|
85
|
-
},
|
|
86
|
-
})
|
|
75
|
+
export const GET = canvasServer.fetch
|
|
76
|
+
export const POST = canvasServer.fetch
|
|
87
77
|
```
|
|
88
78
|
|
|
79
|
+
Wrap `server.fetch` when the route needs authentication. Pass
|
|
80
|
+
`authorize` to check the typed A2 operation before field validation or
|
|
81
|
+
I/O. A push operation carries the complete presence patch, including
|
|
82
|
+
its caller-supplied participant id. The same hook runs for HTTP and
|
|
83
|
+
WebSocket presence.
|
|
84
|
+
|
|
89
85
|
`setPresence` validates each field against the contract, then
|
|
90
86
|
broadcasts. No append transaction, no dispatch, no scheduler arm, no log
|
|
91
87
|
row. A bad field throws `INVALID_PAYLOAD`; an unknown field throws
|
|
@@ -177,8 +173,7 @@ Both members are typed from the contract's `presence` map, through the
|
|
|
177
173
|
reducer, with no type arguments: field names, payloads, and `null`
|
|
178
174
|
clears all autocomplete. On a contract without `presence` the members
|
|
179
175
|
do not exist, so reaching for them is a compile error, not a silent
|
|
180
|
-
no-op.
|
|
181
|
-
echo while others are active, the GET route forgot `presence: true`.
|
|
176
|
+
no-op.
|
|
182
177
|
|
|
183
178
|
## Reconciling the planes: `seen`
|
|
184
179
|
|