experimental-a2 0.4.0 → 0.5.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 +51 -0
- package/dist/{ai-B4YhEnfw.d.ts → ai-CFNeCrRl.d.ts} +3 -2
- package/dist/ai-CFNeCrRl.d.ts.map +1 -0
- package/dist/ai-server.browser.js +2 -0
- package/dist/ai-server.browser.js.map +1 -0
- package/dist/ai-server.d.ts +4 -3
- package/dist/ai-server.d.ts.map +1 -0
- package/dist/ai-server.js +4 -2
- package/dist/ai-server.js.map +1 -0
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +3 -1
- package/dist/ai.js.map +1 -0
- package/dist/cli-B3VuxoDe.js +2 -0
- package/dist/cli-B3VuxoDe.js.map +1 -0
- package/dist/cli-bin.js +2 -0
- package/dist/cli-bin.js.map +1 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -0
- package/dist/{client-Bt4tAKi9.js → client-BKlyLiOU.js} +295 -85
- package/dist/client-BKlyLiOU.js.map +1 -0
- package/dist/{client-BrfDXQ8A.d.ts → client-D7mvIXrF.d.ts} +40 -4
- package/dist/client-D7mvIXrF.d.ts.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/contract-48bUMgcL.js +2 -0
- package/dist/contract-48bUMgcL.js.map +1 -0
- package/dist/contract-jIfaR085.d.ts +2 -1
- package/dist/contract-jIfaR085.d.ts.map +1 -0
- package/dist/devtools-J_jZ2vQf.d.ts +2 -1
- package/dist/devtools-J_jZ2vQf.d.ts.map +1 -0
- package/dist/devtools-kJJaORn-.js +2 -0
- package/dist/devtools-kJJaORn-.js.map +1 -0
- package/dist/devtools-server.browser.js +2 -0
- package/dist/devtools-server.browser.js.map +1 -0
- package/dist/devtools-server.d.ts +2 -1
- package/dist/devtools-server.d.ts.map +1 -0
- package/dist/devtools-server.js +2 -0
- package/dist/devtools-server.js.map +1 -0
- package/dist/errors-BQuJpe82.js +2 -0
- package/dist/errors-BQuJpe82.js.map +1 -0
- package/dist/errors-W6nwJ-fm.d.ts +2 -1
- package/dist/errors-W6nwJ-fm.d.ts.map +1 -0
- package/dist/http.d.ts +121 -72
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +503 -178
- package/dist/http.js.map +1 -0
- package/dist/idempotent-replay-DuqEkYA7.js +2 -0
- package/dist/idempotent-replay-DuqEkYA7.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/inspection-DaxB5jM2.js +2 -0
- package/dist/inspection-DaxB5jM2.js.map +1 -0
- package/dist/{internal-aEotMzu_.js → internal-DstsI6Re.js} +3 -1
- package/dist/internal-DstsI6Re.js.map +1 -0
- package/dist/otel.d.ts +2 -1
- package/dist/otel.d.ts.map +1 -0
- package/dist/otel.js +2 -0
- package/dist/otel.js.map +1 -0
- package/dist/platform-B4TnJtWu.js +2 -0
- package/dist/platform-B4TnJtWu.js.map +1 -0
- package/dist/react.d.ts +12 -3
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +5 -1
- package/dist/react.js.map +1 -0
- package/dist/retryable-lazy-DZWmHpii.js +2 -0
- package/dist/retryable-lazy-DZWmHpii.js.map +1 -0
- package/dist/scheduler-qstash.d.ts +4 -3
- package/dist/scheduler-qstash.d.ts.map +1 -0
- package/dist/scheduler-qstash.js +4 -2
- package/dist/scheduler-qstash.js.map +1 -0
- package/dist/scheduler-task-BpzhPnRS.js +2 -0
- package/dist/scheduler-task-BpzhPnRS.js.map +1 -0
- package/dist/scheduler-vercel.d.ts +4 -3
- package/dist/scheduler-vercel.d.ts.map +1 -0
- package/dist/scheduler-vercel.js +4 -2
- package/dist/scheduler-vercel.js.map +1 -0
- package/dist/{server-CcNnFnoW.js → server-C72KOw51.js} +100 -48
- package/dist/server-C72KOw51.js.map +1 -0
- package/dist/{server-YtPq7hjw.d.ts → server-DUF9pjsx.d.ts} +5 -4
- package/dist/server-DUF9pjsx.d.ts.map +1 -0
- package/dist/server.browser.js +2 -0
- package/dist/server.browser.js.map +1 -0
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/{store-C3sNAaBT.d.ts → store-DysUkTH3.d.ts} +10 -1
- package/dist/store-DysUkTH3.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js +2 -0
- package/dist/store-N8PXxDAS.js.map +1 -0
- package/dist/store-codec-DTG0Ftek.js +2 -0
- package/dist/store-codec-DTG0Ftek.js.map +1 -0
- package/dist/store-memory.d.ts +3 -2
- package/dist/store-memory.d.ts.map +1 -0
- package/dist/store-memory.js +19 -11
- package/dist/store-memory.js.map +1 -0
- package/dist/{store-polling-DgrrAE3d.js → store-polling-dSeLxzfb.js} +3 -1
- package/dist/store-polling-dSeLxzfb.js.map +1 -0
- package/dist/store-postgres.d.ts +3 -2
- package/dist/store-postgres.d.ts.map +1 -0
- package/dist/store-postgres.js +57 -1
- package/dist/store-postgres.js.map +1 -0
- package/dist/{store-redis-core-DWqx3F47.js → store-redis-core-BFLwz0Wj.js} +3 -1
- package/dist/store-redis-core-BFLwz0Wj.js.map +1 -0
- package/dist/store-redis-http.d.ts +3 -2
- package/dist/store-redis-http.d.ts.map +1 -0
- package/dist/store-redis-http.js +4 -2
- package/dist/store-redis-http.js.map +1 -0
- package/dist/store-redis.d.ts +3 -2
- package/dist/store-redis.d.ts.map +1 -0
- package/dist/store-redis.js +5 -3
- package/dist/store-redis.js.map +1 -0
- package/dist/store-sqlite.d.ts +3 -2
- package/dist/store-sqlite.d.ts.map +1 -0
- package/dist/store-sqlite.js +3 -1
- package/dist/store-sqlite.js.map +1 -0
- package/dist/telemetry-BjYHTfh2.d.ts +2 -1
- package/dist/telemetry-BjYHTfh2.d.ts.map +1 -0
- package/dist/testing.browser.js +2 -0
- package/dist/testing.browser.js.map +1 -0
- package/dist/testing.d.ts +2 -1
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +2 -0
- package/dist/testing.js.map +1 -0
- package/dist/validate-XKT4FSNn.js +2 -0
- package/dist/validate-XKT4FSNn.js.map +1 -0
- package/dist/{wire-DCUZBUlT.js → wire-BFQmSJ-9.js} +77 -15
- package/dist/wire-BFQmSJ-9.js.map +1 -0
- package/docs/guides/03-react.mdx +59 -39
- package/docs/guides/06-ai-agents.mdx +5 -27
- package/docs/guides/09-presence.mdx +19 -40
- package/docs/guides/10-transports.mdx +49 -40
- package/docs/reference/01-api.mdx +107 -26
- package/docs/reference/02-errors.mdx +4 -2
- package/package.json +2 -1
- package/src/ai-coordinator.ts +358 -0
- package/src/ai-projector.ts +524 -0
- package/src/ai-sdk-step.ts +261 -0
- package/src/ai-server.browser.ts +5 -0
- package/src/ai-server.ts +1719 -0
- package/src/ai.ts +2155 -0
- package/src/cache-indexeddb.ts +10 -0
- package/src/cli-bin.ts +5 -0
- package/src/cli.ts +1046 -0
- package/src/client.ts +1826 -0
- package/src/contract.ts +206 -0
- package/src/deterministic-id.ts +72 -0
- package/src/devtools-app.ts +989 -0
- package/src/devtools-server.browser.ts +5 -0
- package/src/devtools-server.ts +604 -0
- package/src/devtools.ts +716 -0
- package/src/errors.ts +50 -0
- package/src/http.ts +394 -0
- package/src/idempotent-replay.ts +53 -0
- package/src/index.ts +37 -0
- package/src/inspection.ts +39 -0
- package/src/internal.ts +426 -0
- package/src/otel.ts +59 -0
- package/src/platform.ts +60 -0
- package/src/push-envelope.ts +137 -0
- package/src/react.ts +284 -0
- package/src/reducer.ts +108 -0
- package/src/retryable-lazy.ts +27 -0
- package/src/scheduler-qstash.ts +915 -0
- package/src/scheduler-task.ts +106 -0
- package/src/scheduler-vercel.ts +437 -0
- package/src/server.browser.ts +12 -0
- package/src/server.ts +2700 -0
- package/src/session-socket.ts +548 -0
- package/src/sse.ts +141 -0
- package/src/standard-schema.ts +77 -0
- package/src/store-codec.ts +10 -0
- package/src/store-memory.ts +788 -0
- package/src/store-polling.ts +102 -0
- package/src/store-postgres.ts +1212 -0
- package/src/store-redis-core.ts +1494 -0
- package/src/store-redis-http.ts +116 -0
- package/src/store-redis.ts +458 -0
- package/src/store-sqlite.ts +1108 -0
- package/src/store.ts +385 -0
- package/src/telemetry.ts +47 -0
- package/src/testing.browser.ts +5 -0
- package/src/testing.ts +185 -0
- package/src/validate.ts +39 -0
- package/src/wire.ts +454 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools-server.js","names":[],"sources":["../src/devtools-app.ts","../src/devtools-server.ts"],"sourcesContent":["/** Private browser application served by `experimental-a2/devtools/server`. */\n\nimport {\n DEVTOOLS_PROTOCOL_VERSION,\n DEVTOOLS_SESSION_PAGE_LIMIT,\n} from './devtools.ts'\n\nimport type {\n DevtoolsSessionDetail,\n DevtoolsWireSessionSummary,\n} from './devtools.ts'\n\nexport type DevtoolsBootstrap = {\n contracts: Array<{ name: string }>\n contract: string | null\n sessions: DevtoolsWireSessionSummary[]\n cursor: string | null\n session: string | null\n detail: DevtoolsSessionDetail | null\n error: string | null\n}\n\nconst escapeText = (value: string): string =>\n value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')\n\nconst escapeAttribute = (value: string): string =>\n escapeText(value).replaceAll('\"', '"')\n\nconst formatDuration = (milliseconds: number): string => {\n if (milliseconds < 1_000) {\n return `${Math.max(0, Math.round(milliseconds))} ms`\n }\n if (milliseconds < 60_000) {\n return `${(milliseconds / 1_000).toFixed(milliseconds < 10_000 ? 2 : 1)} s`\n }\n return `${(milliseconds / 60_000).toFixed(1)} min`\n}\n\nconst formatRelative = (value: string): string => {\n const delta = Date.now() - new Date(value).getTime()\n if (delta < 1_000) return 'now'\n if (delta < 60_000) return `${Math.floor(delta / 1_000)}s ago`\n if (delta < 3_600_000) return `${Math.floor(delta / 60_000)}m ago`\n if (delta < 86_400_000) return `${Math.floor(delta / 3_600_000)}h ago`\n return `${Math.floor(delta / 86_400_000)}d ago`\n}\n\nconst sessionStatus = (\n session: DevtoolsBootstrap['sessions'][number],\n): 'failed' | 'pending' | 'settled' =>\n session.failedCount > 0\n ? 'failed'\n : session.pendingCount > 0\n ? 'pending'\n : 'settled'\n\nconst metricHtml = (label: string, value: string): string =>\n `<div class=\"card\"><span>${escapeText(label)}</span><strong>${escapeText(value)}</strong></div>`\n\nconst shellHtml = (bootstrap: DevtoolsBootstrap): string => {\n const options = bootstrap.contracts\n .map(\n ({ name }) =>\n `<option value=\"${escapeAttribute(name)}\"${name === bootstrap.contract ? ' selected' : ''}>${escapeText(name)}</option>`,\n )\n .join('')\n const sessions = bootstrap.sessions.length\n ? bootstrap.sessions\n .map((session) => {\n const active =\n session.sessionId === bootstrap.session ? ' active' : ''\n const events = `${session.eventCount} ${session.eventCount === 1 ? 'event' : 'events'}`\n return `<button class=\"session-button${active}\" type=\"button\"><span class=\"session-name\">${escapeText(session.sessionId)}</span><span class=\"dot ${sessionStatus(session)}\"></span><span class=\"session-meta\">${events}</span><span class=\"session-meta\">${formatRelative(session.updatedAt)}</span></button>`\n })\n .join('')\n : '<div class=\"session-meta\">No durable sessions yet.</div>'\n const more = bootstrap.cursor\n ? '<button class=\"load-more\" type=\"button\">Load more</button>'\n : ''\n\n let main: string\n if (!bootstrap.session) {\n main =\n '<main class=\"main\"><div class=\"empty\"><div><strong>No session selected</strong><span>Choose a durable session to inspect its lifecycle.</span></div></div></main>'\n } else if (!bootstrap.detail) {\n const error = bootstrap.error\n ? `<div class=\"error-box\">${escapeText(bootstrap.error)}</div>`\n : ''\n main = `<main class=\"main\">${error}<div class=\"empty\"><div><strong>Session unavailable</strong><span>The selected durable session could not be read.</span></div></div></main>`\n } else {\n const { events } = bootstrap.detail\n const pending = events.filter(\n (event) => event.processedAt === null && event.failedAt === null,\n ).length\n const attempts = events.reduce(\n (total, event) => total + event.attemptCount,\n 0,\n )\n const failures = events.reduce(\n (total, event) => total + event.failureCount,\n 0,\n )\n const first = events[0]\n const last = events.at(-1)\n const lifetime =\n first && last\n ? formatDuration(\n new Date(\n last.failedAt ?? last.processedAt ?? Date.now(),\n ).getTime() - new Date(first.createdAt).getTime(),\n )\n : '0 ms'\n const error = bootstrap.error\n ? `<div class=\"error-box\">${escapeText(bootstrap.error)}</div>`\n : ''\n main = `<main class=\"main\">${error}<header class=\"session-header\"><div class=\"session-title\"><p class=\"eyebrow\">${escapeText(bootstrap.contract ?? '')} / session</p><h1>${escapeText(bootstrap.session)}</h1><p>Read-only · durable data · refreshed by SSE</p></div><span class=\"live\"><i></i>live</span></header><div class=\"cards\">${metricHtml('Events', String(events.length))}${metricHtml('Pending', String(pending))}${metricHtml('Dispatches', String(attempts))}${metricHtml('Caught failures', String(failures))}${metricHtml('Lifetime', lifetime)}</div></main>`\n }\n\n return `<div class=\"shell\"><aside class=\"sidebar\"><div class=\"brand\"><span class=\"mark\">A2</span><div class=\"brand-copy\"><strong>A2 Devtools</strong><span>Durable execution data</span></div></div><div class=\"side-controls\"><p class=\"eyebrow\">Contract</p><select class=\"contract-select\" aria-label=\"Contract\">${options}</select></div><div class=\"sessions\"><p class=\"eyebrow\">Sessions</p>${sessions}${more}</div></aside>${main}</div>`\n}\n\nconst bootstrapJson = (bootstrap: DevtoolsBootstrap): string =>\n JSON.stringify(bootstrap)\n .replaceAll('<', '\\\\u003c')\n .replaceAll('\\u2028', '\\\\u2028')\n .replaceAll('\\u2029', '\\\\u2029')\n\nexport const devtoolsHtml = (\n basePath: string,\n bootstrap: DevtoolsBootstrap,\n): string => `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n <meta name=\"color-scheme\" content=\"light\">\n <base href=\"${escapeAttribute(basePath)}\">\n <title>A2 Devtools</title>\n <link rel=\"stylesheet\" href=\"_a2/app.css\">\n </head>\n <body>\n <div id=\"app\">${shellHtml(bootstrap)}</div>\n <script id=\"a2-bootstrap\" type=\"application/json\">${bootstrapJson(bootstrap)}</script>\n <script type=\"module\" src=\"_a2/app.js\"></script>\n </body>\n</html>`\n\nexport const DEVTOOLS_APP_CSS: string = String.raw`\n:root {\n color: #1a1a1a;\n background: #fff;\n font-family: Geist, \"Geist Sans\", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n font-synthesis: none;\n --surface: #fafafa;\n --line: #eaeaea;\n --line-strong: #d4d4d4;\n --muted: #666;\n --faint: #8f8f8f;\n --success: #15803d;\n --warning: #b45309;\n --danger: #dc2626;\n}\n\n* { box-sizing: border-box; }\nhtml, body, #app { min-height: 100%; margin: 0; }\nbody { min-width: 320px; background: #fff; }\nbutton, select { font: inherit; }\nbutton { color: inherit; }\nbutton:focus-visible, select:focus-visible, summary:focus-visible { outline: 2px solid #000; outline-offset: 2px; }\n\n.mark {\n display: inline-grid;\n place-items: center;\n width: 30px;\n height: 30px;\n border-radius: 6px;\n background: #000;\n color: #fff;\n font-size: 12px;\n font-weight: 700;\n letter-spacing: -.04em;\n}\n\n.shell {\n min-height: 100vh;\n display: grid;\n grid-template-columns: 248px minmax(0, 1fr);\n}\n\n.sidebar {\n position: sticky;\n top: 0;\n height: 100vh;\n display: flex;\n flex-direction: column;\n border-right: 1px solid var(--line);\n background: var(--surface);\n}\n\n.brand {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 18px;\n border-bottom: 1px solid var(--line);\n}\n\n.brand-copy strong { display: block; font-size: 13px; font-weight: 600; letter-spacing: -.01em; }\n.brand-copy span { color: var(--muted); font-size: 10px; }\n.side-controls { padding: 14px; border-bottom: 1px solid var(--line); }\n.eyebrow { margin: 0 0 8px; color: var(--muted); font-size: 10px; font-weight: 500; letter-spacing: .08em; text-transform: uppercase; }\n\n.contract-select {\n width: 100%;\n padding: 8px 9px;\n color: #1a1a1a;\n border: 1px solid var(--line-strong);\n border-radius: 6px;\n outline: none;\n background: #fff;\n}\n.contract-select:focus { border-color: #000; }\n\n.sessions { min-height: 0; flex: 1; overflow: auto; padding: 12px 8px; }\n.session-button {\n width: 100%;\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n gap: 5px 9px;\n padding: 9px 10px;\n margin-bottom: 2px;\n text-align: left;\n border: 1px solid transparent;\n border-radius: 6px;\n background: transparent;\n cursor: pointer;\n}\n.session-button:hover { background: #f0f0f0; }\n.session-button.active { border-color: var(--line-strong); background: #fff; }\n.session-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }\n.session-meta { color: var(--muted); font-size: 10px; }\n.dot { align-self: center; width: 6px; height: 6px; border-radius: 99px; background: #999; }\n.dot.pending { background: var(--warning); }\n.dot.failed { background: var(--danger); }\n.dot.settled { background: var(--success); }\n.load-more { width: 100%; margin: 6px 0 10px; padding: 8px; border: 1px solid var(--line-strong); border-radius: 6px; background: #fff; cursor: pointer; color: var(--muted); }\n.load-more:hover { color: #000; border-color: #999; }\n\n.main { min-width: 0; width: 100%; max-width: 1440px; padding: 38px clamp(24px, 5vw, 72px) 72px; }\n.empty { min-height: 70vh; display: grid; place-items: center; color: var(--muted); text-align: center; }\n.empty strong { display: block; margin-bottom: 7px; color: #1a1a1a; font-size: 16px; font-weight: 500; }\n\n.session-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 24px; margin-bottom: 28px; }\n.session-title { min-width: 0; }\n.session-title h1 { margin: 4px 0 8px; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: clamp(22px, 3vw, 32px); font-weight: 500; letter-spacing: -.04em; }\n.session-title p { margin: 0; color: var(--muted); font-size: 12px; }\n.live { display: inline-flex; align-items: center; gap: 7px; padding: 6px 9px; color: var(--muted); border: 1px solid var(--line); border-radius: 99px; font-size: 11px; }\n.live i { width: 6px; height: 6px; border-radius: 99px; background: var(--success); }\n\n.cards { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); margin-bottom: 32px; overflow: hidden; border: 1px solid var(--line); border-radius: 8px; }\n.card { padding: 15px 16px; border-right: 1px solid var(--line); background: #fff; }\n.card:last-child { border-right: 0; }\n.card span { display: block; margin-bottom: 8px; color: var(--muted); font-size: 10px; font-weight: 500; }\n.card strong { font-size: 20px; font-weight: 500; letter-spacing: -.03em; }\n\n.section { margin-top: 32px; }\n.section-head { display: flex; align-items: end; justify-content: space-between; gap: 20px; margin-bottom: 11px; }\n.section-head h2 { margin: 0; font-size: 14px; font-weight: 500; letter-spacing: -.01em; }\n.section-head p { margin: 0; color: var(--muted); font-size: 11px; }\n\n.forest { overflow: hidden; border: 1px solid var(--line); border-radius: 8px; background: #fff; }\n.forest-row { --depth: 0; position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 16px; min-height: 42px; padding: 9px 13px 9px calc(13px + var(--depth) * 22px); border-bottom: 1px solid var(--line); }\n.forest-row:last-child { border-bottom: 0; }\n.forest-row::before { content: \"\"; position: absolute; top: 0; bottom: 0; left: calc(20px + (var(--depth) - 1) * 22px); width: 1px; background: var(--depth-line, transparent); }\n.forest-row.nested { --depth-line: var(--line-strong); }\n.forest-main { min-width: 0; display: flex; align-items: center; gap: 8px; }\n.forest-main .dot { flex: none; }\n.forest-type { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: 11px ui-monospace, SFMono-Regular, Menlo, monospace; }\n.forest-edge { color: var(--muted); font-size: 10px; white-space: nowrap; }\n.forest-edge.unknown { color: var(--warning); }\n\n.timeline { overflow: hidden; border: 1px solid var(--line); border-radius: 8px; background: #fff; }\n.timeline-axis, .event-row { display: grid; grid-template-columns: 310px minmax(320px, 1fr); }\n.timeline-axis { min-height: 37px; border-bottom: 1px solid var(--line); color: var(--muted); font-size: 9px; }\n.axis-label { padding: 12px 14px; border-right: 1px solid var(--line); font-weight: 500; }\n.axis-track { position: relative; display: flex; justify-content: space-between; padding: 12px 16px; }\n.event-row { min-height: 84px; border-bottom: 1px solid var(--line); }\n.event-row:last-child { border-bottom: 0; }\n.event-info { min-width: 0; padding: 11px 14px; border-right: 1px solid var(--line); }\n.event-line { display: flex; align-items: center; gap: 8px; min-width: 0; }\n.event-index { color: var(--muted); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; }\n.event-type { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }\n.event-sub { display: flex; flex-wrap: wrap; gap: 5px 9px; margin-top: 7px; color: var(--muted); font-size: 9px; }\n.event-sub .danger { color: var(--danger); }\n.event-sub .warning { color: var(--warning); }\n.event-facts { display: flex; flex-wrap: wrap; gap: 4px 9px; margin-top: 7px; color: var(--muted); font-size: 9px; }\n.event-facts .warning { color: var(--warning); }\n.event-details { margin-top: 8px; }\n.event-details summary { color: var(--muted); cursor: pointer; font-size: 9px; }\n.event-details pre { max-height: 220px; overflow: auto; margin: 8px 0 0; padding: 10px; border: 1px solid var(--line); border-radius: 6px; background: var(--surface); color: #333; font: 10px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }\n.event-track { position: relative; min-height: 84px; }\n.event-track::before { content: \"\"; position: absolute; top: 42px; right: 0; left: 0; height: 1px; background: var(--line); }\n.event-bar { position: absolute; top: 39px; height: 7px; min-width: 4px; border-radius: 99px; background: #111; }\n.event-bar.processed { background: #111; }\n.event-bar.pending { top: 38px; height: 9px; border: 1px solid #777; background: #fff; }\n.event-bar.failed { background: var(--danger); }\n.lifecycle-marker { position: absolute; z-index: 2; width: 11px; height: 11px; margin-left: -5px; border: 2px solid #fff; border-radius: 99px; background: #777; box-shadow: 0 0 0 1px #777; }\n.lifecycle-marker.created { top: 27px; border-radius: 2px; background: #fff; box-shadow: 0 0 0 1px #777; }\n.lifecycle-marker.claim { top: 37px; background: #111; box-shadow: 0 0 0 1px #111; }\n.lifecycle-marker.failure { top: 47px; background: var(--danger); box-shadow: 0 0 0 1px var(--danger); }\n.lifecycle-marker.complete { top: 47px; background: var(--success); box-shadow: 0 0 0 1px var(--success); }\n.lifecycle-marker.dead-letter { top: 57px; background: #fff; box-shadow: 0 0 0 1px var(--danger); }\n.timeline-legend { display: flex; flex-wrap: wrap; gap: 7px 14px; margin: 9px 2px 0; color: var(--muted); font-size: 9px; }\n.legend-item { display: inline-flex; align-items: center; gap: 6px; }\n.legend-key { width: 7px; height: 7px; border-radius: 99px; background: #777; }\n.legend-key.created { border: 1px solid #777; border-radius: 1px; background: #fff; }\n.legend-key.claim { background: #111; }\n.legend-key.failure { background: var(--danger); }\n.legend-key.dead-letter { border: 1px solid var(--danger); background: #fff; }\n.legend-key.complete { background: var(--success); }\n\n.snapshots { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 8px; }\n.snapshot { padding: 14px; border: 1px solid var(--line); border-radius: 8px; background: #fff; }\n.snapshot strong { display: block; margin-bottom: 8px; font: 12px ui-monospace, SFMono-Regular, Menlo, monospace; }\n.snapshot div { display: flex; justify-content: space-between; color: var(--muted); font-size: 10px; }\n.snapshot b { color: #111; font-weight: 500; }\n.error-box { margin-bottom: 20px; padding: 12px 14px; border: 1px solid #fecaca; border-radius: 7px; background: #fffafa; color: var(--danger); font: 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre-wrap; }\n\n@media (max-width: 850px) {\n .shell { grid-template-columns: 1fr; }\n .sidebar { position: static; height: auto; max-height: 310px; border-right: 0; border-bottom: 1px solid var(--line); }\n .sessions { max-height: 180px; }\n .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }\n .card { border-bottom: 1px solid var(--line); }\n .card:nth-child(2n) { border-right: 0; }\n .card:last-child { border-bottom: 0; }\n .timeline-axis, .event-row { grid-template-columns: 260px minmax(300px, 1fr); }\n .timeline { overflow-x: auto; }\n .timeline-axis, .event-row { min-width: 610px; }\n .main { padding: 28px 20px 60px; }\n}\n`\n\nexport const DEVTOOLS_APP_JS: string = String.raw`\nconst root = document.querySelector('#app')\nconst readBootstrap = () => {\n const element = document.querySelector('#a2-bootstrap')\n if (!element) return null\n try {\n const bootstrap = JSON.parse(element.textContent || 'null')\n element.remove()\n return bootstrap\n } catch {\n return null\n }\n}\nconst navigationFromUrl = () => {\n const params = new URL(window.location.href).searchParams\n return {\n contract: params.get('contract'),\n session: params.get('session'),\n }\n}\nconst initialNavigation = navigationFromUrl()\nconst bootstrap = readBootstrap()\nconst state = {\n contracts: bootstrap?.contracts || [],\n contract: bootstrap ? bootstrap.contract : initialNavigation.contract,\n sessions: bootstrap?.sessions || [],\n cursor: bootstrap?.cursor || null,\n session: bootstrap ? bootstrap.session : initialNavigation.session,\n detail: bootstrap?.detail || null,\n watch: null,\n error: bootstrap?.error || null,\n}\n\nconst syncUrl = (replace) => {\n const url = new URL(window.location.href)\n if (state.contract) url.searchParams.set('contract', state.contract)\n else url.searchParams.delete('contract')\n if (state.session) url.searchParams.set('session', state.session)\n else url.searchParams.delete('session')\n const historyState = { contract: state.contract, session: state.session }\n if (replace) window.history.replaceState(historyState, '', url)\n else window.history.pushState(historyState, '', url)\n}\n\nconst node = (tag, className, text) => {\n const element = document.createElement(tag)\n if (className) element.className = className\n if (text !== undefined) element.textContent = text\n return element\n}\n\nconst append = (parent, ...children) => {\n for (const child of children) if (child) parent.append(child)\n return parent\n}\n\nconst request = async (resource, params = {}) => {\n const url = new URL('_a2/' + resource, document.baseURI)\n for (const [key, value] of Object.entries(params)) {\n if (value !== null && value !== undefined) url.searchParams.set(key, String(value))\n }\n const response = await fetch(url, { headers: { accept: 'application/json' } })\n if (!response.ok) {\n let message = response.status + ' ' + response.statusText\n try { message = (await response.json()).error || message } catch {}\n throw new Error(message)\n }\n return response.json()\n}\n\nconst detailRevision = async (detail) => {\n const timestamp = (value) => value === null ? null : new Date(value).getTime()\n const bytes = new TextEncoder().encode(JSON.stringify({\n events: detail.events.map((event) => [\n event.id,\n event.index,\n event.cause?.index ?? null,\n event.cause?.attempt ?? null,\n timestamp(event.processedAt),\n event.processedByAttempt,\n timestamp(event.firstClaimedAt),\n timestamp(event.lastClaimedAt),\n event.attemptCount,\n event.failureCount,\n timestamp(event.lastFailedAt),\n event.lastFailedAttempt,\n event.lastError,\n timestamp(event.failedAt),\n ]),\n snapshots: detail.snapshots.map((snapshot) => [\n snapshot.reducerName,\n snapshot.index,\n snapshot.updatedAt === null ? 0 : new Date(snapshot.updatedAt).getTime(),\n ]),\n }))\n const digest = await crypto.subtle.digest('SHA-256', bytes)\n return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('')\n}\n\nconst requestDetail = async (contract, session) => {\n const events = []\n let snapshots = []\n let cursor = null\n let throughIndex = null\n const seenCursors = new Set()\n do {\n const page = await request('session', {\n contract,\n session,\n limit: ${DEVTOOLS_SESSION_PAGE_LIMIT},\n cursor,\n })\n if ('revision' in page) return page\n if (page.contract !== contract || page.sessionId !== session) {\n throw new Error('Session page identity changed during inspection.')\n }\n if (throughIndex === null) {\n throughIndex = page.throughIndex\n snapshots = page.snapshots\n } else if (page.throughIndex !== throughIndex || page.snapshots.length > 0) {\n throw new Error('Session page frontier changed during inspection.')\n }\n for (const event of page.events) {\n if (event.index !== events.length + 1 || event.index > throughIndex) {\n throw new Error('Session event pages are not contiguous.')\n }\n events.push(event)\n }\n if (page.cursor !== null) {\n if (\n page.events.length === 0 ||\n events.length === throughIndex ||\n seenCursors.has(page.cursor)\n ) {\n throw new Error('Session cursor did not advance.')\n }\n seenCursors.add(page.cursor)\n }\n cursor = page.cursor\n } while (cursor !== null)\n if (throughIndex !== events.length) {\n throw new Error('Session event pages ended before their frozen frontier.')\n }\n const detail = {\n protocolVersion: ${DEVTOOLS_PROTOCOL_VERSION},\n contract,\n sessionId: session,\n revision: '',\n events,\n snapshots,\n }\n detail.revision = await detailRevision(detail)\n return detail\n}\n\nconst watchUrl = () => {\n const url = new URL('_a2/watch', document.baseURI)\n url.searchParams.set('contract', state.contract)\n url.searchParams.set('session', state.session)\n if (state.detail?.revision) url.searchParams.set('revision', state.detail.revision)\n return url\n}\n\nconst formatTime = (value) => new Intl.DateTimeFormat(undefined, {\n hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3,\n}).format(new Date(value))\n\nconst formatRelative = (value) => {\n const delta = Date.now() - new Date(value).getTime()\n if (delta < 1000) return 'now'\n if (delta < 60000) return Math.floor(delta / 1000) + 's ago'\n if (delta < 3600000) return Math.floor(delta / 60000) + 'm ago'\n if (delta < 86400000) return Math.floor(delta / 3600000) + 'h ago'\n return Math.floor(delta / 86400000) + 'd ago'\n}\n\nconst formatDuration = (milliseconds) => {\n if (milliseconds < 1000) return Math.max(0, Math.round(milliseconds)) + ' ms'\n if (milliseconds < 60000) return (milliseconds / 1000).toFixed(milliseconds < 10000 ? 2 : 1) + ' s'\n return (milliseconds / 60000).toFixed(1) + ' min'\n}\n\nconst sessionStatus = (session) => session.failedCount > 0 ? 'failed' : session.pendingCount > 0 ? 'pending' : 'settled'\nconst eventStatus = (event) => event.failedAt ? 'failed' : event.processedAt ? 'processed' : 'pending'\nconst eventStatusLabel = (event) => event.failedAt ? 'dead-lettered' : event.processedAt ? 'completed' : 'pending'\n\nconst durableOutcomeGap = (event) => Math.max(\n 0,\n event.attemptCount - event.failureCount - (event.processedByAttempt === null ? 0 : 1),\n)\n\nconst causalEntries = (events) => {\n const byIndex = new Map(events.map((event) => [event.index, event]))\n const children = new Map()\n for (const event of events) {\n if (!event.cause || !byIndex.has(event.cause.index)) continue\n const siblings = children.get(event.cause.index) || []\n siblings.push(event)\n children.set(event.cause.index, siblings)\n }\n\n const roots = events.filter((event) => !event.cause || !byIndex.has(event.cause.index))\n const entries = []\n const visited = new Set()\n const visit = (event, depth) => {\n if (visited.has(event.index)) return\n visited.add(event.index)\n entries.push({ event, depth })\n for (const child of children.get(event.index) || []) visit(child, depth + 1)\n }\n for (const root of roots) visit(root, 0)\n for (const event of events) visit(event, 0)\n return entries\n}\n\nconst loadContracts = async () => {\n const data = await request('contracts')\n state.contracts = data.contracts\n if (!data.contracts.some((contract) => contract.name === state.contract)) {\n state.contract = data.contracts[0]?.name || null\n state.session = null\n }\n if (state.contract) await loadSessions(true)\n syncUrl(true)\n render()\n}\n\nconst loadSessions = async (reset) => {\n if (!state.contract) return\n const data = await request('sessions', {\n contract: state.contract,\n cursor: reset ? null : state.cursor,\n limit: 50,\n })\n state.sessions = reset ? data.sessions : state.sessions.concat(data.sessions)\n state.cursor = data.cursor\n if (reset && !state.session) {\n state.session = state.sessions[0]?.sessionId || null\n }\n if (state.session) await loadDetail()\n}\n\nconst loadDetail = async () => {\n if (!state.contract || !state.session) return\n state.detail = await requestDetail(state.contract, state.session)\n state.error = null\n render()\n}\n\nconst beginWatch = () => {\n state.watch?.close()\n if (!state.session) return\n const watchedContract = state.contract\n const watchedSession = state.session\n const source = new EventSource(watchUrl())\n source.addEventListener('invalidate', async () => {\n if (state.contract !== watchedContract || state.session !== watchedSession) return\n try {\n await Promise.all([loadDetail(), refreshSelectedSummary()])\n } catch (error) {\n state.error = error instanceof Error ? error.message : String(error)\n render()\n }\n })\n source.onerror = () => {\n // EventSource reconnects. Durable state is fetched on the next\n // invalidation, so a dropped frame cannot make the view incorrect.\n }\n state.watch = source\n}\n\nconst refreshSelectedSummary = async () => {\n const data = await request('sessions', { contract: state.contract, limit: 100 })\n const fresh = data.sessions.find((session) => session.sessionId === state.session)\n const index = state.sessions.findIndex((session) => session.sessionId === state.session)\n if (fresh && index !== -1) state.sessions[index] = fresh\n render()\n}\n\nconst selectContract = async (name) => {\n state.watch?.close()\n state.contract = name\n state.session = null\n state.detail = null\n state.sessions = []\n state.cursor = null\n state.error = null\n syncUrl(false)\n render()\n try {\n await loadSessions(true)\n syncUrl(true)\n beginWatch()\n } catch (error) {\n state.error = error instanceof Error ? error.message : String(error)\n render()\n }\n}\n\nconst selectSession = async (id) => {\n if (id === state.session) return\n\n state.watch?.close()\n state.session = id\n state.detail = null\n state.error = null\n syncUrl(false)\n render()\n try {\n await loadDetail()\n beginWatch()\n } catch (error) {\n state.error = error instanceof Error ? error.message : String(error)\n render()\n }\n}\n\nconst renderSidebar = () => {\n const sidebar = node('aside', 'sidebar')\n const brand = node('div', 'brand')\n const copy = node('div', 'brand-copy')\n append(copy, node('strong', '', 'A2 Devtools'), node('span', '', 'Durable execution data'))\n append(brand, node('span', 'mark', 'A2'), copy)\n sidebar.append(brand)\n\n const controls = node('div', 'side-controls')\n controls.append(node('p', 'eyebrow', 'Contract'))\n const select = node('select', 'contract-select')\n select.setAttribute('aria-label', 'Contract')\n for (const contract of state.contracts) {\n const option = node('option', '', contract.name)\n option.value = contract.name\n option.selected = contract.name === state.contract\n select.append(option)\n }\n select.addEventListener('change', () => void selectContract(select.value))\n controls.append(select)\n sidebar.append(controls)\n\n const sessions = node('div', 'sessions')\n sessions.append(node('p', 'eyebrow', 'Sessions'))\n if (state.sessions.length === 0) sessions.append(node('div', 'session-meta', 'No durable sessions yet.'))\n for (const session of state.sessions) {\n const status = sessionStatus(session)\n const button = node('button', 'session-button' + (session.sessionId === state.session ? ' active' : ''))\n button.type = 'button'\n button.addEventListener('click', () => void selectSession(session.sessionId))\n append(\n button,\n node('span', 'session-name', session.sessionId),\n node('span', 'dot ' + status),\n node('span', 'session-meta', session.eventCount + (session.eventCount === 1 ? ' event' : ' events')),\n node('span', 'session-meta', formatRelative(session.updatedAt)),\n )\n sessions.append(button)\n }\n if (state.cursor) {\n const more = node('button', 'load-more', 'Load more')\n more.type = 'button'\n more.addEventListener('click', async () => {\n more.disabled = true\n try { await loadSessions(false); render() } catch (error) {\n state.error = error instanceof Error ? error.message : String(error)\n render()\n }\n })\n sessions.append(more)\n }\n sidebar.append(sessions)\n return sidebar\n}\n\nconst metric = (label, value) => {\n const card = node('div', 'card')\n append(card, node('span', '', label), node('strong', '', value))\n return card\n}\n\nconst renderCausalForest = (events) => {\n const forest = node('div', 'forest')\n const indexes = new Set(events.map((event) => event.index))\n for (const { event, depth } of causalEntries(events)) {\n const row = node('div', 'forest-row' + (depth > 0 ? ' nested' : ''))\n row.style.setProperty('--depth', String(depth))\n const main = node('div', 'forest-main')\n append(\n main,\n node('span', 'dot ' + eventStatus(event)),\n node('span', 'event-index', '#' + event.index),\n node('span', 'forest-type', event.type),\n )\n row.append(main)\n\n let edge\n if (!event.cause) {\n edge = node('span', 'forest-edge unknown', 'origin unknown')\n edge.title = 'This may be a root append or an event written before causal metadata existed.'\n } else if (!indexes.has(event.cause.index)) {\n edge = node('span', 'forest-edge unknown', 'parent #' + event.cause.index + ' unavailable · attempt ' + event.cause.attempt)\n } else {\n edge = node('span', 'forest-edge', 'from #' + event.cause.index + ' · attempt ' + event.cause.attempt)\n }\n row.append(edge)\n forest.append(row)\n }\n return forest\n}\n\nconst renderTimeline = (events) => {\n const timeline = node('div', 'timeline')\n const now = Date.now()\n const times = events.flatMap((event) => [\n event.createdAt,\n event.firstClaimedAt,\n event.lastClaimedAt,\n event.lastFailedAt,\n event.processedAt,\n event.failedAt,\n eventStatus(event) === 'pending' ? now : null,\n ].filter((value) => value !== null).map((value) => new Date(value).getTime()))\n const minimum = Math.min(...events.map((event) => new Date(event.createdAt).getTime()))\n const maximum = Math.max(...times, minimum + 1)\n const range = maximum - minimum\n const position = (value) => Math.max(0, Math.min(100, (new Date(value).getTime() - minimum) / range * 100))\n const markerPosition = (value) => Math.max(.75, Math.min(99.25, position(value)))\n const lifecycleMarker = (kind, value, title) => {\n const marker = node('span', 'lifecycle-marker ' + kind)\n marker.style.left = markerPosition(value) + '%'\n marker.title = title + ' · ' + formatTime(value)\n return marker\n }\n\n const axis = node('div', 'timeline-axis')\n axis.append(node('div', 'axis-label', 'Event'))\n const track = node('div', 'axis-track')\n for (const percent of [0, 25, 50, 75, 100]) {\n track.append(node('span', '', formatDuration(range * percent / 100)))\n }\n axis.append(track)\n timeline.append(axis)\n\n for (const event of events) {\n const status = eventStatus(event)\n const start = new Date(event.createdAt).getTime()\n const end = new Date(event.failedAt || event.processedAt || now).getTime()\n const left = position(event.createdAt)\n const width = Math.max((end - start) / range * 100, .45)\n const row = node('div', 'event-row')\n const info = node('div', 'event-info')\n const line = node('div', 'event-line')\n append(line, node('span', 'event-index', '#' + event.index), node('span', 'event-type', event.type))\n info.append(line)\n const sub = node('div', 'event-sub')\n sub.append(node('span', status === 'failed' ? 'danger' : '', eventStatusLabel(event)))\n sub.append(node('span', '', event.attemptCount + (event.attemptCount === 1 ? ' dispatch' : ' dispatches')))\n sub.append(node('span', event.failureCount > 0 ? 'danger' : '', event.failureCount + (event.failureCount === 1 ? ' caught failure' : ' caught failures')))\n sub.append(node('span', '', 'created ' + formatTime(event.createdAt)))\n info.append(sub)\n\n const facts = node('div', 'event-facts')\n if (event.attemptCount === 0) {\n facts.append(node('span', '', 'not dispatched'))\n } else if (!event.firstClaimedAt) {\n facts.append(node('span', 'warning', 'first dispatch time unknown'))\n }\n if (event.attemptCount > 1) {\n facts.append(node('span', '', (event.attemptCount - 1) + (event.attemptCount === 2 ? ' redispatch' : ' redispatches')))\n }\n const outcomeGap = durableOutcomeGap(event)\n if (outcomeGap > 0) {\n const hint = node('span', 'warning', outcomeGap + (outcomeGap === 1 ? ' dispatch without outcome' : ' dispatches without outcomes'))\n hint.title = 'May indicate active work, a hard crash, or a superseded attempt.'\n facts.append(hint)\n }\n if (event.lastFailedAttempt !== null) {\n facts.append(node('span', 'danger', 'latest failure · attempt ' + event.lastFailedAttempt))\n }\n if (event.processedAt) {\n facts.append(node('span', '', event.processedByAttempt === null ? 'completing attempt not recorded' : 'completed by attempt ' + event.processedByAttempt))\n }\n if (facts.childNodes.length > 0) info.append(facts)\n\n const details = node('details', 'event-details')\n details.append(node('summary', '', 'payload'))\n details.append(node('pre', '', JSON.stringify(event.payload, null, 2)))\n info.append(details)\n if (event.lastError) {\n const failure = node('details', 'event-details')\n failure.append(node('summary', 'danger', 'last error'))\n failure.append(node('pre', '', event.lastError))\n info.append(failure)\n }\n row.append(info)\n\n const eventTrack = node('div', 'event-track')\n const bar = node('span', 'event-bar ' + status)\n bar.style.left = left + '%'\n bar.style.width = Math.min(width, 100 - left) + '%'\n bar.title = eventStatusLabel(event) + ' · ' + formatDuration(end - start)\n append(eventTrack, bar)\n eventTrack.append(lifecycleMarker('created', event.createdAt, 'Created'))\n if (event.firstClaimedAt && event.lastClaimedAt === event.firstClaimedAt) {\n eventTrack.append(lifecycleMarker('claim', event.firstClaimedAt, 'First and latest dispatch'))\n } else {\n if (event.firstClaimedAt) eventTrack.append(lifecycleMarker('claim', event.firstClaimedAt, 'First dispatch'))\n if (event.lastClaimedAt) eventTrack.append(lifecycleMarker('claim', event.lastClaimedAt, 'Latest dispatch'))\n }\n if (event.lastFailedAt) {\n eventTrack.append(lifecycleMarker('failure', event.lastFailedAt, 'Latest caught failure' + (event.lastFailedAttempt === null ? '' : ' · attempt ' + event.lastFailedAttempt)))\n }\n if (event.processedAt) {\n eventTrack.append(lifecycleMarker('complete', event.processedAt, 'Completed' + (event.processedByAttempt === null ? '' : ' · attempt ' + event.processedByAttempt)))\n }\n if (event.failedAt) eventTrack.append(lifecycleMarker('dead-letter', event.failedAt, 'Dead-lettered'))\n row.append(eventTrack)\n timeline.append(row)\n }\n\n const legend = node('div', 'timeline-legend')\n for (const [kind, label] of [\n ['created', 'Created'],\n ['claim', 'First / latest dispatch'],\n ['failure', 'Latest caught failure'],\n ['complete', 'Completion'],\n ['dead-letter', 'Dead-letter'],\n ]) {\n const item = node('span', 'legend-item')\n append(item, node('i', 'legend-key ' + kind), document.createTextNode(label))\n legend.append(item)\n }\n return append(node('div'), timeline, legend)\n}\n\nconst renderMain = () => {\n const main = node('main', 'main')\n if (state.error) main.append(node('div', 'error-box', state.error))\n if (!state.session) {\n const empty = node('div', 'empty')\n const copy = node('div')\n append(copy, node('strong', '', 'No session selected'), node('span', '', 'Choose a durable session to inspect its lifecycle.'))\n empty.append(copy)\n main.append(empty)\n return main\n }\n if (!state.detail) {\n main.append(node('div', 'empty', 'Reading the durable store…'))\n return main\n }\n\n const events = state.detail.events\n const snapshots = state.detail.snapshots\n const pending = events.filter((event) => eventStatus(event) === 'pending').length\n const failed = events.filter((event) => eventStatus(event) === 'failed').length\n const attempts = events.reduce((total, event) => total + event.attemptCount, 0)\n const failures = events.reduce((total, event) => total + event.failureCount, 0)\n const first = new Date(events[0].createdAt).getTime()\n const lastEvent = events[events.length - 1]\n const last = new Date(lastEvent.failedAt || lastEvent.processedAt || Date.now()).getTime()\n\n const header = node('header', 'session-header')\n const title = node('div', 'session-title')\n append(title, node('p', 'eyebrow', state.contract + ' / session'), node('h1', '', state.session), node('p', '', 'Read-only · durable data · refreshed by SSE'))\n const live = node('span', 'live')\n append(live, node('i'), document.createTextNode('live'))\n append(header, title, live)\n main.append(header)\n\n const cards = node('div', 'cards')\n append(cards, metric('Events', String(events.length)), metric('Pending', String(pending)), metric('Dispatches', String(attempts)), metric('Caught failures', String(failures)), metric('Lifetime', formatDuration(last - first)))\n main.append(cards)\n\n const forestSection = node('section', 'section')\n const forestHead = node('div', 'section-head')\n append(forestHead, node('h2', '', 'Causal forest'), node('p', '', 'Unknown origins may be roots or legacy events'))\n append(forestSection, forestHead, renderCausalForest(events))\n main.append(forestSection)\n\n const section = node('section', 'section')\n const head = node('div', 'section-head')\n append(head, node('h2', '', 'Event lifecycle'), node('p', '', 'Durable operation boundaries · missing history stays unknown'))\n append(section, head, renderTimeline(events))\n main.append(section)\n\n const snapshotSection = node('section', 'section')\n const snapshotHead = node('div', 'section-head')\n append(snapshotHead, node('h2', '', 'Snapshots'), node('p', '', failed ? 'Session is stalled' : snapshots.length ? 'Latest durable reducer frontiers' : 'No cached folds'))\n snapshotSection.append(snapshotHead)\n if (snapshots.length) {\n const grid = node('div', 'snapshots')\n for (const snapshot of snapshots) {\n const card = node('div', 'snapshot')\n card.append(node('strong', '', snapshot.reducerName))\n const detail = node('div')\n append(detail, node('span', '', snapshot.updatedAt ? formatRelative(snapshot.updatedAt) : 'time unknown'), node('b', '', '#' + snapshot.index))\n card.append(detail)\n grid.append(card)\n }\n snapshotSection.append(grid)\n }\n main.append(snapshotSection)\n return main\n}\n\nconst render = () => {\n root.replaceChildren(append(node('div', 'shell'), renderSidebar(), renderMain()))\n}\n\nconst restoreFromUrl = async () => {\n state.watch?.close()\n const navigation = navigationFromUrl()\n state.contract = state.contracts.some((contract) => contract.name === navigation.contract)\n ? navigation.contract\n : state.contracts[0]?.name || null\n state.session = navigation.session\n state.sessions = []\n state.cursor = null\n state.detail = null\n state.error = null\n render()\n try {\n if (state.contract) await loadSessions(true)\n syncUrl(true)\n beginWatch()\n } catch (error) {\n state.error = error instanceof Error ? error.message : String(error)\n render()\n }\n}\n\nwindow.addEventListener('popstate', () => void restoreFromUrl())\n\nsetInterval(() => {\n if (state.detail?.events.some((event) => eventStatus(event) === 'pending')) render()\n}, 1000)\n\nif (bootstrap) {\n syncUrl(true)\n render()\n beginWatch()\n} else {\n loadContracts().then(() => beginWatch()).catch((error) => {\n state.error = error instanceof Error ? error.message : String(error)\n render()\n })\n}\n`\n","/**\n * experimental-a2/devtools/server — a read-only dashboard over A2's durable data.\n * One Request handler serves the application, its private browser bundle,\n * JSON inspection endpoints, and live SSE invalidations.\n */\n\nimport {\n DEVTOOLS_APP_CSS,\n DEVTOOLS_APP_JS,\n devtoolsHtml,\n type DevtoolsBootstrap,\n} from './devtools-app.ts'\nimport {\n DEVTOOLS_CAPABILITIES,\n DEVTOOLS_CAPTURE_MEDIA_TYPE,\n DEVTOOLS_PROTOCOL_VERSION,\n DEVTOOLS_SESSION_PAGE_LIMIT,\n DEVTOOLS_SESSION_PAGE_MAX_LIMIT,\n devtoolsSessionRevision,\n encodeDevtoolsCapture,\n type DevtoolsSessionPage,\n type DevtoolsSessionDetail,\n type DevtoolsWireEvent,\n type DevtoolsWireSessionSummary,\n type DevtoolsWireSnapshot,\n} from './devtools.ts'\nimport {\n DEVTOOLS_TIMINGS,\n InspectionUnsupportedError,\n serverInspection,\n type ServerInspection,\n} from './inspection.ts'\nimport type {\n StoredEvent,\n StoredSessionSummary,\n StoredSnapshot,\n} from './store.ts'\n\nexport type DevtoolsServer = {\n readonly contract: { readonly name: string }\n}\n\nexport type DevtoolsAuthorization = (\n request: Request,\n) => boolean | Response | Promise<boolean | Response>\n\nexport type DevtoolsOptions = {\n servers: readonly DevtoolsServer[]\n /**\n * Application-owned access control. Returning a Response short-circuits\n * the request. Without this option, devtools is available only when\n * NODE_ENV is exactly \"development\" and otherwise returns 404.\n */\n authorize?: DevtoolsAuthorization\n}\n\nexport type Devtools = {\n handler(): (request: Request) => Promise<Response>\n}\n\nconst MARKER = '/_a2/'\nconst encoder = new TextEncoder()\n\ntype InspectionDetail = {\n events: StoredEvent[]\n snapshots: StoredSnapshot[]\n}\n\nconst isDevelopment = (): boolean =>\n typeof process !== 'undefined' && process.env?.['NODE_ENV'] === 'development'\n\nconst headers = (contentType: string): HeadersInit => ({\n 'content-type': contentType,\n 'cache-control': 'no-store',\n 'x-content-type-options': 'nosniff',\n 'referrer-policy': 'no-referrer',\n})\n\nconst json = (value: unknown, init?: ResponseInit): Response =>\n Response.json(value, {\n ...init,\n headers: {\n ...headers('application/json; charset=utf-8'),\n ...init?.headers,\n },\n })\n\nconst notFound = (): Response =>\n new Response('Not found', { status: 404, headers: headers('text/plain') })\n\nconst badRequest = (message: string): Response =>\n json({ error: message }, { status: 400 })\n\nconst unavailable = (message: string): Response =>\n json({ error: message }, { status: 501 })\n\nconst wireSummary = (\n session: StoredSessionSummary,\n): DevtoolsWireSessionSummary => ({\n sessionId: session.sessionId,\n eventCount: session.eventCount,\n pendingCount: session.pendingCount,\n failedCount: session.failedCount,\n attemptCount: session.attemptCount,\n failureCount: session.failureCount,\n firstEventAt: session.firstEventAt.toISOString(),\n updatedAt: session.updatedAt.toISOString(),\n})\n\nconst wireEvent = (event: StoredEvent): DevtoolsWireEvent => ({\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 cause:\n event.cause === null\n ? null\n : {\n index: event.cause.index,\n attempt: event.cause.attempt,\n ...(event.cause.batchSize === undefined\n ? {}\n : { batchSize: event.cause.batchSize }),\n },\n lane: event.lane,\n processedAt: event.processedAt?.toISOString() ?? null,\n processedByAttempt: event.processedByAttempt,\n returnedEventIds: event.returnedEventIds,\n firstClaimedAt: event.firstClaimedAt?.toISOString() ?? null,\n lastClaimedAt: event.lastClaimedAt?.toISOString() ?? null,\n attemptCount: event.attemptCount,\n claimHolder: event.claimHolder,\n claimExpiresAt: event.claimExpiresAt?.toISOString() ?? null,\n failureCount: event.failureCount,\n lastFailedAt: event.lastFailedAt?.toISOString() ?? null,\n lastFailedAttempt: event.lastFailedAttempt,\n lastError: event.lastError,\n failedAt: event.failedAt?.toISOString() ?? null,\n})\n\nconst wireSnapshot = (snapshot: StoredSnapshot): DevtoolsWireSnapshot => ({\n reducerName: snapshot.reducerName,\n index: snapshot.index,\n // Redis snapshots written before inspection metadata have no durable\n // timestamp. Epoch zero is the adapter's explicit \"unknown\" marker.\n updatedAt:\n snapshot.updatedAt.getTime() === 0\n ? null\n : snapshot.updatedAt.toISOString(),\n})\n\nconst sessionRevision = async (detail: InspectionDetail): Promise<string> => {\n return devtoolsSessionRevision({\n events: detail.events.map(wireEvent),\n snapshots: detail.snapshots.map(wireSnapshot),\n })\n}\n\nconst wireDetail = async (\n contract: string,\n sessionId: string,\n detail: InspectionDetail,\n): Promise<DevtoolsSessionDetail> => ({\n protocolVersion: DEVTOOLS_PROTOCOL_VERSION,\n contract,\n sessionId,\n revision: await sessionRevision(detail),\n events: detail.events.map(wireEvent),\n snapshots: detail.snapshots.map(wireSnapshot),\n})\n\nconst readCompleteSession = async (\n inspection: ServerInspection,\n sessionId: string,\n): Promise<InspectionDetail> => {\n const events: StoredEvent[] = []\n let snapshots: StoredSnapshot[] = []\n let afterIndex = 0\n let throughIndex: number | undefined\n for (;;) {\n // oxlint-disable-next-line no-await-in-loop -- each request advances one fixed-frontier page\n const page = await inspection.readSessionPage(sessionId, {\n afterIndex,\n limit: DEVTOOLS_SESSION_PAGE_LIMIT,\n ...(throughIndex === undefined ? {} : { throughIndex }),\n })\n if (throughIndex === undefined) {\n throughIndex = page.throughIndex\n snapshots = page.snapshots\n } else if (page.throughIndex !== throughIndex) {\n throw new Error('inspection session frontier changed between pages')\n }\n if (events.length + page.events.length > page.throughIndex) {\n throw new Error('inspection session page crossed its frozen frontier')\n }\n events.push(...page.events)\n if (page.nextIndex === null) break\n if (events.length === page.throughIndex) {\n throw new Error('inspection session cursor continued past its frontier')\n }\n if (page.nextIndex <= afterIndex) {\n throw new Error('inspection session cursor did not advance')\n }\n afterIndex = page.nextIndex\n }\n if (throughIndex !== events.length) {\n throw new Error('inspection session ended before its frozen frontier')\n }\n return { events, snapshots }\n}\n\nconst readRevision = async (\n inspection: ServerInspection,\n sessionId: string,\n): Promise<{\n detail: InspectionDetail\n revision: string\n}> => {\n const detail = await readCompleteSession(inspection, sessionId)\n return { detail, revision: await sessionRevision(detail) }\n}\n\nconst bootstrapState = async (\n url: URL,\n servers: ReadonlyMap<string, DevtoolsServer>,\n): Promise<DevtoolsBootstrap> => {\n const contracts = [...servers.keys()].map((name) => ({ name }))\n const requestedContract = url.searchParams.get('contract')\n const contract =\n requestedContract && servers.has(requestedContract)\n ? requestedContract\n : (contracts[0]?.name ?? null)\n const bootstrap: DevtoolsBootstrap = {\n contracts,\n contract,\n sessions: [],\n cursor: null,\n session: null,\n detail: null,\n error: null,\n }\n if (!contract) return bootstrap\n\n const server = servers.get(contract)\n const inspection = server ? serverInspection.get(server) : undefined\n if (!inspection) {\n bootstrap.error = `contract '${contract}' is not an inspectable A2 server`\n return bootstrap\n }\n\n try {\n const page = await inspection.listSessions({ limit: 50 })\n bootstrap.sessions = page.sessions.map(wireSummary)\n bootstrap.cursor = page.cursor\n const requestedSession =\n requestedContract === contract ? url.searchParams.get('session') : null\n bootstrap.session = requestedSession ?? page.sessions[0]?.sessionId ?? null\n if (!bootstrap.session) return bootstrap\n\n const detail = await readCompleteSession(inspection, bootstrap.session)\n if (detail.events.length === 0) {\n bootstrap.error = `session '${bootstrap.session}' was not found`\n return bootstrap\n }\n bootstrap.detail = await wireDetail(contract, bootstrap.session, detail)\n } catch (error) {\n bootstrap.error = error instanceof Error ? error.message : String(error)\n }\n return bootstrap\n}\n\nconst resourceOf = (pathname: string): string | null => {\n const marker = pathname.lastIndexOf(MARKER)\n return marker === -1 ? null : pathname.slice(marker + MARKER.length)\n}\n\nconst inspectionOf = (\n servers: ReadonlyMap<string, DevtoolsServer>,\n contractName: string | null,\n): ServerInspection | Response => {\n if (!contractName) return badRequest('contract is required')\n const server = servers.get(contractName)\n if (!server) return notFound()\n return (\n serverInspection.get(server) ??\n unavailable(`contract '${contractName}' is not an inspectable A2 server`)\n )\n}\n\ntype SessionCursor = { afterIndex: number; throughIndex: number }\n\nconst encodeSessionCursor = (\n contract: string,\n sessionId: string,\n cursor: SessionCursor,\n): string =>\n JSON.stringify([\n 1,\n contract,\n sessionId,\n cursor.throughIndex,\n cursor.afterIndex,\n ])\n\nconst parseSessionCursor = (\n value: string,\n contract: string,\n sessionId: string,\n): SessionCursor | Response => {\n let decoded: unknown\n try {\n decoded = JSON.parse(value) as unknown\n } catch {\n return badRequest('cursor is invalid')\n }\n if (\n !Array.isArray(decoded) ||\n decoded.length !== 5 ||\n decoded[0] !== 1 ||\n !Number.isSafeInteger(decoded[3]) ||\n !Number.isSafeInteger(decoded[4]) ||\n (decoded[3] as number) < 1 ||\n (decoded[4] as number) < 1 ||\n (decoded[4] as number) >= (decoded[3] as number)\n ) {\n return badRequest('cursor is invalid')\n }\n if (decoded[1] !== contract || decoded[2] !== sessionId) {\n return badRequest('cursor does not belong to this contract and session')\n }\n return {\n throughIndex: decoded[3] as number,\n afterIndex: decoded[4] as number,\n }\n}\n\nconst captureFilename = (contract: string, sessionId: string): string => {\n const safe = `${contract}-${sessionId}`\n .replaceAll(/[^a-zA-Z0-9._-]/g, '_')\n .slice(0, 160)\n return `${safe || 'a2-session'}.a2log`\n}\n\nconst watchResponse = (\n request: Request,\n inspection: ServerInspection,\n sessionId: string,\n initial: InspectionDetail,\n): Response => {\n let stopped = false\n let timeout: ReturnType<typeof setTimeout> | null = null\n let wake: (() => void) | null = null\n const stop = (): void => {\n stopped = true\n if (timeout !== null) clearTimeout(timeout)\n timeout = null\n wake?.()\n wake = null\n }\n const wait = (milliseconds: number): Promise<void> =>\n new Promise((resolve) => {\n wake = resolve\n timeout = setTimeout(() => {\n timeout = null\n wake = null\n resolve()\n }, milliseconds)\n })\n\n const stream = new ReadableStream<Uint8Array>({\n start(controller) {\n request.signal.addEventListener('abort', stop, { once: true })\n controller.enqueue(encoder.encode('retry: 1000\\n: connected\\n\\n'))\n void (async () => {\n let detail = initial\n let previous = new URL(request.url).searchParams.get('revision') ?? ''\n let lastHeartbeat = Date.now()\n try {\n let revision = await sessionRevision(detail)\n for (;;) {\n if (stopped) break\n if (revision !== previous) {\n previous = revision\n controller.enqueue(\n encoder.encode(\n `event: invalidate\\ndata: ${JSON.stringify({ revision })}\\n\\n`,\n ),\n )\n } else if (\n Date.now() - lastHeartbeat >=\n DEVTOOLS_TIMINGS.heartbeatMs\n ) {\n controller.enqueue(encoder.encode(': ping\\n\\n'))\n lastHeartbeat = Date.now()\n }\n const active = detail.events.some(\n (event) => event.processedAt === null && event.failedAt === null,\n )\n // oxlint-disable-next-line no-await-in-loop -- each revision read follows its poll interval\n await wait(\n active ? DEVTOOLS_TIMINGS.activeMs : DEVTOOLS_TIMINGS.idleMs,\n )\n if (!stopped) {\n // oxlint-disable-next-line no-await-in-loop -- poll state advances from the prior read\n const next = await readRevision(inspection, sessionId)\n detail = next.detail\n revision = next.revision\n }\n }\n controller.close()\n } catch (error) {\n if (!stopped) controller.error(error)\n }\n })()\n },\n cancel() {\n stop()\n },\n })\n\n return new Response(stream, {\n headers: {\n ...headers('text/event-stream; charset=utf-8'),\n connection: 'keep-alive',\n 'x-accel-buffering': 'no',\n },\n })\n}\n\nconst authorizeRequest = async (\n request: Request,\n authorize: DevtoolsAuthorization | undefined,\n): Promise<Response | null> => {\n if (!authorize) return isDevelopment() ? null : notFound()\n const result = await authorize(request)\n if (result instanceof Response) return result\n return result === true ? null : notFound()\n}\n\nexport function createDevtools(options: DevtoolsOptions): Devtools {\n if (\n !options ||\n !Array.isArray(options.servers) ||\n options.servers.length === 0\n ) {\n throw new TypeError('createDevtools expects at least one A2 server')\n }\n const servers = new Map<string, DevtoolsServer>()\n for (const server of options.servers) {\n const name = server?.contract?.name\n if (typeof name !== 'string' || name.length === 0) {\n throw new TypeError('createDevtools received a server without a contract')\n }\n if (servers.has(name)) {\n throw new TypeError(`createDevtools received contract '${name}' twice`)\n }\n servers.set(name, server)\n }\n\n return {\n handler() {\n return async (request) => {\n const denied = await authorizeRequest(request, options.authorize)\n if (denied) return denied\n if (request.method !== 'GET') {\n return new Response('Method not allowed', {\n status: 405,\n headers: { ...headers('text/plain'), allow: 'GET' },\n })\n }\n\n const url = new URL(request.url)\n const resource = resourceOf(url.pathname)\n if (resource === null) {\n const basePath = `${url.pathname.replace(/\\/$/, '')}/`\n const bootstrap = await bootstrapState(url, servers)\n return new Response(devtoolsHtml(basePath, bootstrap), {\n headers: {\n ...headers('text/html; charset=utf-8'),\n 'content-security-policy':\n \"default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; img-src 'self' data:; base-uri 'self'; frame-ancestors 'none'\",\n },\n })\n }\n if (resource === 'app.js') {\n return new Response(DEVTOOLS_APP_JS, {\n headers: headers('text/javascript; charset=utf-8'),\n })\n }\n if (resource === 'app.css') {\n return new Response(DEVTOOLS_APP_CSS, {\n headers: headers('text/css; charset=utf-8'),\n })\n }\n if (resource === 'contracts') {\n return json({\n protocolVersion: DEVTOOLS_PROTOCOL_VERSION,\n contracts: [...servers.keys()].map((name) => ({ name })),\n })\n }\n if (resource === 'capabilities') {\n return json(DEVTOOLS_CAPABILITIES)\n }\n\n const contractName = url.searchParams.get('contract')\n const inspection = inspectionOf(servers, contractName)\n if (inspection instanceof Response) return inspection\n try {\n if (resource === 'sessions') {\n const requestedLimit = Number(url.searchParams.get('limit') ?? 50)\n if (!Number.isInteger(requestedLimit) || requestedLimit < 1) {\n return badRequest('limit must be a positive integer')\n }\n const limit = Math.min(requestedLimit, 100)\n const cursor = url.searchParams.get('cursor')\n const page = await inspection.listSessions({\n limit,\n ...(cursor !== null ? { cursor } : {}),\n })\n return json({\n protocolVersion: DEVTOOLS_PROTOCOL_VERSION,\n cursor: page.cursor,\n sessions: page.sessions.map(wireSummary),\n })\n }\n\n const sessionId = url.searchParams.get('session')\n if (!sessionId) return badRequest('session is required')\n if (\n resource === 'session' &&\n (url.searchParams.has('limit') || url.searchParams.has('cursor'))\n ) {\n const requestedLimit = Number(\n url.searchParams.get('limit') ?? DEVTOOLS_SESSION_PAGE_LIMIT,\n )\n if (!Number.isSafeInteger(requestedLimit) || requestedLimit < 1) {\n return badRequest('limit must be a positive integer')\n }\n const limit = Math.min(\n requestedLimit,\n DEVTOOLS_SESSION_PAGE_MAX_LIMIT,\n )\n const cursorValue = url.searchParams.get('cursor')\n const cursor =\n cursorValue === null\n ? { afterIndex: 0, throughIndex: undefined }\n : parseSessionCursor(cursorValue, contractName!, sessionId)\n if (cursor instanceof Response) return cursor\n const page = await inspection.readSessionPage(sessionId, {\n afterIndex: cursor.afterIndex,\n limit,\n ...(cursor.throughIndex === undefined\n ? {}\n : { throughIndex: cursor.throughIndex }),\n })\n if (page.throughIndex === 0) return notFound()\n const response: DevtoolsSessionPage = {\n protocolVersion: DEVTOOLS_PROTOCOL_VERSION,\n contract: contractName!,\n sessionId,\n throughIndex: page.throughIndex,\n cursor:\n page.nextIndex === null\n ? null\n : encodeSessionCursor(contractName!, sessionId, {\n throughIndex: page.throughIndex,\n afterIndex: page.nextIndex,\n }),\n events: page.events.map(wireEvent),\n snapshots: page.snapshots.map(wireSnapshot),\n }\n return json(response)\n }\n\n const detail = await readCompleteSession(inspection, sessionId)\n if (detail.events.length === 0) return notFound()\n const wired = await wireDetail(contractName!, sessionId, detail)\n if (resource === 'session') {\n return json(wired)\n }\n if (resource === 'export') {\n return new Response(await encodeDevtoolsCapture(wired), {\n headers: {\n ...headers(`${DEVTOOLS_CAPTURE_MEDIA_TYPE}; charset=utf-8`),\n 'content-disposition': `attachment; filename=\"${captureFilename(wired.contract, sessionId)}\"`,\n },\n })\n }\n if (resource === 'watch') {\n return watchResponse(request, inspection, sessionId, detail)\n }\n } catch (error) {\n if (error instanceof InspectionUnsupportedError) {\n return unavailable(error.message)\n }\n throw error\n }\n return notFound()\n }\n },\n }\n}\n"],"mappings":"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"}
|
package/dist/errors-BQuJpe82.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors-BQuJpe82.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * a2 errors — one class, discriminated by `code`.\n *\n * See specs/a2-implementation.md §10 and docs/reference/02-errors.mdx.\n * A single class serializes cleanly across the push-route boundary and\n * keeps `instanceof` checks working after bundling.\n */\n\nexport type A2ErrorCode =\n | 'INVALID_PAYLOAD' // schema validation failed — thrown before anything is written\n | 'UNKNOWN_EVENT_TYPE' // event type not in the machine's `events` map\n | 'PARTIAL_DUPLICATE_BATCH' // batch mixed already-appended and fresh events\n | 'SUPERSEDED_ATTEMPT' // handler append from an attempt a recovery claim replaced\n | 'CLAIM_EXPIRED' // ctx.signal abort reason: the attempt's lease lapsed without renewal\n | 'STORE_UNAVAILABLE' // store backend failure — retryable, nothing was written\n | 'STORE_NOT_CONFIGURED' // production boot with no `store` passed\n | 'UNKNOWN_PRESENCE_FIELD' // presence field not in the contract's `presence` map\n | 'PRESENCE_NOT_SUPPORTED' // contract declares presence but the store lacks the capability\n\nexport class A2Error extends Error {\n readonly code: A2ErrorCode\n /** e.g. the Standard Schema issues for INVALID_PAYLOAD */\n readonly details: unknown\n\n constructor(\n code: A2ErrorCode,\n message: string,\n options?: { details?: unknown; cause?: unknown },\n ) {\n super(\n message,\n options && 'cause' in options ? { cause: options.cause } : undefined,\n )\n this.name = 'A2Error'\n this.code = code\n this.details = options?.details\n }\n}\n\n/**\n * Wrap an arbitrary thrown value as STORE_UNAVAILABLE, preserving the\n * original as `cause`. A2Errors pass through untouched — they already\n * carry their own meaning.\n */\nexport function asStoreUnavailable(err: unknown): A2Error {\n if (err instanceof A2Error) return err\n return new A2Error('STORE_UNAVAILABLE', 'the store backend failed', {\n cause: err,\n })\n}\n"],"mappings":";AAmBA,IAAa,UAAb,cAA6B,MAAM;CACjC;;CAEA;CAEA,YACE,MACA,SACA,SACA;EACA,MACE,SACA,WAAW,WAAW,UAAU,EAAE,OAAO,QAAQ,MAAM,IAAI,KAAA,CAC7D;EACA,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,UAAU,SAAS;CAC1B;AACF;;;;;;AAOA,SAAgB,mBAAmB,KAAuB;CACxD,IAAI,eAAe,SAAS,OAAO;CACnC,OAAO,IAAI,QAAQ,qBAAqB,4BAA4B,EAClE,OAAO,IACT,CAAC;AACH"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors-W6nwJ-fm.d.ts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;KAQY;cAWC,gBAAgB;WAClB,MAAM;;WAEN;EAET,YACE,MAAM,aACN,iBACA;IAAY;IAAmB"}
|
package/dist/http.d.ts
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
|
-
import { a as EventDefs,
|
|
2
|
-
import {
|
|
3
|
-
import { _ as Session, f as PushedEvent, i as DrainableServer, p as PushedPresence } from "./server-YtPq7hjw.js";
|
|
1
|
+
import { a as EventDefs, o as PresenceDefs, r as ContractEvent } from "./contract-jIfaR085.js";
|
|
2
|
+
import { f as PushedEvent, i as DrainableServer, n as A2Server, p as PushedPresence } from "./server-DUF9pjsx.js";
|
|
4
3
|
import { t as A2Error } from "./errors-W6nwJ-fm.js";
|
|
4
|
+
//#region src/session-socket.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The structural socket `sessionsSocket` speaks against — satisfied
|
|
7
|
+
* by `ws` and by `@vercel/functions`' upgraded socket alike, so the
|
|
8
|
+
* platform upgrade API stays out of the library. `message` data
|
|
9
|
+
* arrives as whatever the platform delivers (string, Buffer,
|
|
10
|
+
* ArrayBuffer, or fragments); the handler normalizes defensively.
|
|
11
|
+
*/
|
|
12
|
+
type A2Socket = {
|
|
13
|
+
send(data: string): void;
|
|
14
|
+
on(event: "message", listener: (data: unknown) => void): void;
|
|
15
|
+
on(event: "close", listener: () => void): void;
|
|
16
|
+
on(event: "error", listener: (error: unknown) => void): void;
|
|
17
|
+
close(code?: number, reason?: string): void;
|
|
18
|
+
/** Bytes queued but not yet transmitted — when exposed, the down
|
|
19
|
+
* pump uses it for backpressure (`SOCKET_TIMINGS`). */
|
|
20
|
+
bufferedAmount?: number;
|
|
21
|
+
};
|
|
22
|
+
//#endregion
|
|
5
23
|
//#region src/http.d.ts
|
|
6
24
|
/**
|
|
7
25
|
* Create one scheduler callback route for servers sharing the same configured
|
|
@@ -9,48 +27,14 @@ import { t as A2Error } from "./errors-W6nwJ-fm.js";
|
|
|
9
27
|
* is evaluated, before a delivery can be acknowledged by the wrong adapter.
|
|
10
28
|
*/
|
|
11
29
|
declare function schedulerHandler(firstServer: DrainableServer, ...servers: DrainableServer[]): (req: Request) => Promise<Response>;
|
|
12
|
-
/** The push envelope: what a client POSTs to append events. */
|
|
13
|
-
type PushBody = {
|
|
14
|
-
sessionId: string;
|
|
15
|
-
/** Branded: `session.append` accepts these directly (the push route
|
|
16
|
-
* path); schema validation still happens inside `append`. Empty only
|
|
17
|
-
* for a presence-only push. */
|
|
18
|
-
events: PushedEvent[];
|
|
19
|
-
/** Branded like `events`: `session.setPresence` accepts it whole;
|
|
20
|
-
* field validation still happens inside `setPresence`. */
|
|
21
|
-
presence?: PushedPresence;
|
|
22
|
-
};
|
|
23
|
-
/**
|
|
24
|
-
* Validate the push envelope — `{ sessionId, events, presence? }` —
|
|
25
|
-
* throwing `INVALID_PAYLOAD` on a malformed body. Payload validation
|
|
26
|
-
* against the machine's schemas happens in `append` and `setPresence`,
|
|
27
|
-
* not here. Either plane may appear alone: a presence-only push has no
|
|
28
|
-
* (or empty) `events`.
|
|
29
|
-
*/
|
|
30
|
-
declare function parsePushBody(req: Request): Promise<PushBody>;
|
|
31
|
-
/**
|
|
32
|
-
* Pipe a live event iterable into an SSE `Response`. Each event is one
|
|
33
|
-
* frame — `id:` carries the event-log index, `data:` the JSON event. A
|
|
34
|
-
* `stream({ presence: true })` iterable also yields presence items,
|
|
35
|
-
* which ride as named frames old clients skip: `event:
|
|
36
|
-
* presence-snapshot` first (the map, ISO dates), then `event: presence`
|
|
37
|
-
* per patch — no `id:`, presence never advances the resume frontier. A
|
|
38
|
-
* disconnecting client cancels the stream, which closes the underlying
|
|
39
|
-
* subscription. Two kinds of comment frames ride along: a `: connected`
|
|
40
|
-
* prelude that flushes headers immediately, and a `: ping` heartbeat
|
|
41
|
-
* every 15 seconds so clients (and proxies) can tell a quiet stream
|
|
42
|
-
* from a dead connection — the session client's stall watchdog counts
|
|
43
|
-
* on it. When the platform exposes an invocation deadline, the response
|
|
44
|
-
* closes cleanly one second before it so the client reconnects without
|
|
45
|
-
* a platform-timeout failure.
|
|
46
|
-
*/
|
|
47
|
-
declare function sseResponse(iterable: AsyncIterable<Event | PresencePatch | PresenceSnapshot>): Response;
|
|
48
30
|
/**
|
|
49
31
|
* Serialize an error into the documented wire shape,
|
|
50
32
|
* `{ error: { code, message, details } }`, with the mapped status
|
|
51
33
|
* (400 for caller bugs, 503 for STORE_UNAVAILABLE). Non-A2Errors become a
|
|
52
34
|
* 503 STORE_UNAVAILABLE — from the client's perspective an unknown server
|
|
53
|
-
* failure is retryable-once, not a protocol contract.
|
|
35
|
+
* failure is retryable-once, not a protocol contract. The natural
|
|
36
|
+
* return value of a `before` hook that wants the wire's own error
|
|
37
|
+
* shapes.
|
|
54
38
|
*/
|
|
55
39
|
declare function errorResponse(error: unknown): Response;
|
|
56
40
|
/**
|
|
@@ -60,43 +44,108 @@ declare function errorResponse(error: unknown): Response;
|
|
|
60
44
|
*/
|
|
61
45
|
declare function deserializeError(body: unknown): A2Error | null;
|
|
62
46
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
47
|
+
* What a request (or socket frame) asks of a session, parsed — the
|
|
48
|
+
* argument policy hooks branch on. `stream` covers the SSE GET and the
|
|
49
|
+
* socket's subscribe frames; `push` covers the HTTP POST and the
|
|
50
|
+
* socket's push frames; `history` is the bounded cold read the
|
|
51
|
+
* client's `loadHistory` rides; `ws-upgrade` is the upgrade GET before
|
|
52
|
+
* any frame flows.
|
|
68
53
|
*/
|
|
69
|
-
type
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
54
|
+
type A2Intent<D extends EventDefs = EventDefs> = {
|
|
55
|
+
type: "ws-upgrade";
|
|
56
|
+
} | {
|
|
57
|
+
type: "stream";
|
|
58
|
+
sessionId: string;
|
|
59
|
+
startAfter: number;
|
|
60
|
+
transport: "sse" | "ws";
|
|
61
|
+
} | {
|
|
62
|
+
type: "history";
|
|
63
|
+
sessionId: string;
|
|
64
|
+
gte: number;
|
|
65
|
+
lte: number;
|
|
66
|
+
} | {
|
|
67
|
+
type: "push";
|
|
68
|
+
sessionId: string;
|
|
69
|
+
/** Parsed and branded, payloads not yet validated — `type`
|
|
70
|
+
* autocompletes the contract's names but is any string off the
|
|
71
|
+
* wire; `append` rejects unknown types after the hooks run. */
|
|
72
|
+
events: Array<PushedEvent & {
|
|
73
|
+
type: SuggestedEventType<D>;
|
|
74
|
+
}>;
|
|
75
|
+
presence?: PushedPresence;
|
|
76
|
+
transport: "http" | "ws";
|
|
77
|
+
};
|
|
78
|
+
/** The contract's event names for editor completion, without excluding
|
|
79
|
+
* the wire's reality (any string can arrive; validation is later). */
|
|
80
|
+
type SuggestedEventType<D extends EventDefs> = (keyof D & string) | (string & Record<never, never>);
|
|
81
|
+
/** What the library did with an allowed intent — `after`'s view.
|
|
82
|
+
* `covered` = the closed range is fully present (an immutable slice of
|
|
83
|
+
* the append-only log), the one caching-relevant fact the library
|
|
84
|
+
* owns; policy over it belongs to the app's `after`. */
|
|
85
|
+
type A2Outcome<D extends EventDefs = EventDefs> = {
|
|
86
|
+
type: "stream";
|
|
87
|
+
} | {
|
|
88
|
+
type: "history";
|
|
89
|
+
covered: boolean;
|
|
90
|
+
events: ContractEvent<D>[];
|
|
91
|
+
} | {
|
|
92
|
+
type: "push";
|
|
93
|
+
appended: ContractEvent<D>[];
|
|
78
94
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
/**
|
|
96
|
+
* The platform's socket-upgrade seam: given the attach callback, return
|
|
97
|
+
* the 101 response. On Vercel:
|
|
98
|
+
* `upgrade: (attach) => experimental_upgradeWebSocket(attach)`.
|
|
99
|
+
*/
|
|
100
|
+
type UpgradeFn = (attach: (socket: A2Socket) => void) => Response | Promise<Response>;
|
|
101
|
+
/** One bag: the policy hooks and the capability options — the names
|
|
102
|
+
* distinguish them, so `handle(server, { presence: true })` and
|
|
103
|
+
* `handle(server, { before, upgrade })` both read naturally. */
|
|
104
|
+
type HandleOptions<D extends EventDefs = EventDefs> = {
|
|
105
|
+
/** Return a Response to deny; thrown errors are treated as
|
|
106
|
+
* infrastructure failures and answer with the retryable wire shape. */
|
|
107
|
+
before?(args: {
|
|
108
|
+
request: Request;
|
|
109
|
+
intent: A2Intent<D>;
|
|
110
|
+
}): Response | undefined | void | Promise<Response | undefined | void>;
|
|
111
|
+
after?(args: {
|
|
112
|
+
request: Request;
|
|
113
|
+
intent: A2Intent<D>;
|
|
114
|
+
outcome: A2Outcome<D>;
|
|
115
|
+
response: Response;
|
|
116
|
+
}): Response | undefined | void | Promise<Response | undefined | void>;
|
|
117
|
+
upgrade?: UpgradeFn;
|
|
85
118
|
presence?: boolean;
|
|
86
|
-
/** Epoch ms: close cleanly (code 1000) at this time, ahead of a known
|
|
87
|
-
* platform deadline, so clients reconnect on our schedule. */
|
|
88
119
|
deadline?: number;
|
|
89
120
|
};
|
|
90
121
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* `
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
122
|
+
* The session route pair as one call: `export const { GET, POST } =
|
|
123
|
+
* handle(server, options?)` serves the live stream (SSE GET, resumed
|
|
124
|
+
* at `index`), the history slice (GET with `gte`/`lte`), the push
|
|
125
|
+
* envelope (POST), and — with `options.upgrade` — the multiplexed
|
|
126
|
+
* WebSocket, all through one pair of policy hooks.
|
|
127
|
+
*
|
|
128
|
+
* Parsing is protocol, hooks are policy: a request that fails to parse
|
|
129
|
+
* (missing sessionId, malformed bounds, bad push envelope) answers
|
|
130
|
+
* INVALID_PAYLOAD on the wire before any hook runs. `before` sees
|
|
131
|
+
* every parsed intent — HTTP requests and socket frames alike, with
|
|
132
|
+
* `request` always the original (for socket frames: upgrade) Request —
|
|
133
|
+
* and short-circuits by returning a Response: over HTTP it is the
|
|
134
|
+
* response verbatim; over the socket it is translated to the wire's
|
|
135
|
+
* own vocabulary (subscribe → `unsubscribed { reason: 'subscribe
|
|
136
|
+
* rejected' }`, push → an INVALID_PAYLOAD error ack), because a
|
|
137
|
+
* Response body cannot cross a socket. `after` runs only where the
|
|
138
|
+
* library produced an HTTP response — never after a short-circuit,
|
|
139
|
+
* never for socket frames — and may mutate `response.headers` in place
|
|
140
|
+
* or return a replacement (history caching policy lives here, on
|
|
141
|
+
* `outcome.covered`). `options.deadline` (epoch ms) closes sockets
|
|
142
|
+
* cleanly ahead of a known platform deadline, so clients reconnect on
|
|
143
|
+
* our schedule.
|
|
99
144
|
*/
|
|
100
|
-
declare function
|
|
145
|
+
declare function handle<D extends EventDefs, P extends PresenceDefs = Record<never, never>>(server: A2Server<D, P>, options?: HandleOptions<D>): {
|
|
146
|
+
GET(req: Request): Promise<Response>;
|
|
147
|
+
POST(req: Request): Promise<Response>;
|
|
148
|
+
};
|
|
101
149
|
//#endregion
|
|
102
|
-
export { A2Socket,
|
|
150
|
+
export { A2Intent, A2Outcome, type A2Socket, HandleOptions, UpgradeFn, deserializeError, errorResponse, handle, schedulerHandler };
|
|
151
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","names":[],"sources":["../src/session-socket.ts","../src/http.ts"],"mappings":";;;;;;;;;;;KAkCY;EACV,KAAK;EACL,GAAG,kBAAkB,WAAW;EAChC,GAAG,gBAAgB;EACnB,GAAG,gBAAgB,WAAW;EAC9B,MAAM,eAAe;;;EAGrB;;;;;;;;;iBCUc,iBACd,aAAa,oBACV,SAAS,qBACV,KAAK,YAAY,QAAQ;;;;;;;;;;iBAoBb,cAAc,iBAAiB;;;;;;iBAY/B,iBAAiB,gBAAgB;;;;;;;;;KAcrC,SAAS,UAAU,YAAY;EACrC;;EAEA;EACA;EACA;EACA;;EAEA;EAAiB;EAAmB;EAAa;;EAEjD;EACA;;;;EAIA,QAAQ,MAAM;IAAgB,MAAM,mBAAmB;;EACvD,WAAW;EACX;;;;KAKD,mBAAmB,UAAU,oBACzB,wBAAwB;;;;;KAMrB,UAAU,UAAU,YAAY;EACtC;;EACA;EAAiB;EAAkB,QAAQ,cAAc;;EACzD;EAAc,UAAU,cAAc;;;;;;;KAOhC,aACV,SAAS,QAAQ,sBACd,WAAW,QAAQ;;;;KA6CZ,cAAc,UAAU,YAAY;;;EAG9C,QAAQ;IACN,SAAS;IACT,QAAQ,SAAS;MACf,8BAA8B,QAAQ;EAC1C,OAAO;IACL,SAAS;IACT,QAAQ,SAAS;IACjB,SAAS,UAAU;IACnB,UAAU;MACR,8BAA8B,QAAQ;EAC1C,UAAU;EACV;EACA;;;;;;;;;;;;;;;;;;;;;;;;;;iBA2Bc,OACd,UAAU,WACV,UAAU,eAAe,sBAEzB,QAAQ,SAAS,GAAG,IACpB,UAAU,cAAc;EAExB,IAAI,KAAK,UAAU,QAAQ;EAC3B,KAAK,KAAK,UAAU,QAAQ"}
|