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.
Files changed (145) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/ai-server.d.ts +4 -5
  3. package/dist/ai-server.d.ts.map +1 -1
  4. package/dist/ai-server.js +8 -7
  5. package/dist/ai-server.js.map +1 -1
  6. package/dist/ai.d.ts +334 -2
  7. package/dist/ai.d.ts.map +1 -0
  8. package/dist/ai.js +1 -1
  9. package/dist/client.d.ts +202 -2
  10. package/dist/client.d.ts.map +1 -0
  11. package/dist/client.js +1025 -1
  12. package/dist/client.js.map +1 -0
  13. package/dist/errors-BQuJpe82.js.map +1 -1
  14. package/dist/index.d.ts +22 -3
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/{internal-DstsI6Re.js → internal-DRXJ56EI.js} +5 -28
  17. package/dist/internal-DRXJ56EI.js.map +1 -0
  18. package/dist/react.d.ts +1 -1
  19. package/dist/react.js +1 -1
  20. package/dist/scheduler-qstash.d.ts +3 -3
  21. package/dist/scheduler-qstash.js +4 -5
  22. package/dist/scheduler-qstash.js.map +1 -1
  23. package/dist/scheduler-vercel.d.ts +2 -2
  24. package/dist/scheduler-vercel.js +4 -4
  25. package/dist/scheduler-vercel.js.map +1 -1
  26. package/dist/{server-Duw6MVlB.js → server-286j79Mt.js} +708 -79
  27. package/dist/server-286j79Mt.js.map +1 -0
  28. package/dist/{server-DpvjhdoE.d.ts → server-DgXmORIq.d.ts} +67 -49
  29. package/dist/server-DgXmORIq.d.ts.map +1 -0
  30. package/dist/server.d.ts +3 -3
  31. package/dist/server.js +1 -1
  32. package/dist/store-N8PXxDAS.js.map +1 -1
  33. package/dist/{store-DysUkTH3.d.ts → store-flRz1OWh.d.ts} +2 -57
  34. package/dist/store-flRz1OWh.d.ts.map +1 -0
  35. package/dist/store-memory.d.ts +1 -1
  36. package/dist/store-memory.d.ts.map +1 -1
  37. package/dist/store-memory.js +1 -59
  38. package/dist/store-memory.js.map +1 -1
  39. package/dist/{store-polling-dSeLxzfb.js → store-polling-6DW7F1DT.js} +2 -2
  40. package/dist/{store-polling-dSeLxzfb.js.map → store-polling-6DW7F1DT.js.map} +1 -1
  41. package/dist/store-postgres.d.ts +1 -1
  42. package/dist/store-postgres.js +1 -81
  43. package/dist/store-postgres.js.map +1 -1
  44. package/dist/{store-redis-core-BFLwz0Wj.js → store-redis-core-DEYO8Ryv.js} +48 -134
  45. package/dist/store-redis-core-DEYO8Ryv.js.map +1 -0
  46. package/dist/store-redis-http.d.ts +1 -1
  47. package/dist/store-redis-http.js +2 -3
  48. package/dist/store-redis-http.js.map +1 -1
  49. package/dist/store-redis.d.ts +1 -1
  50. package/dist/store-redis.js +3 -4
  51. package/dist/store-redis.js.map +1 -1
  52. package/dist/store-sqlite.d.ts +1 -1
  53. package/dist/store-sqlite.d.ts.map +1 -1
  54. package/dist/store-sqlite.js +1 -72
  55. package/dist/store-sqlite.js.map +1 -1
  56. package/dist/{wire-BFQmSJ-9.js → wire-B6te_wns.js} +4 -3
  57. package/dist/wire-B6te_wns.js.map +1 -0
  58. package/docs/guides/03-react.mdx +20 -28
  59. package/docs/guides/05-production.mdx +9 -11
  60. package/docs/guides/06-ai-agents.mdx +8 -14
  61. package/docs/guides/09-presence.mdx +14 -19
  62. package/docs/guides/10-transports.mdx +104 -86
  63. package/docs/reference/01-api.mdx +143 -277
  64. package/docs/reference/02-errors.mdx +5 -7
  65. package/package.json +1 -14
  66. package/src/ai-server.ts +9 -5
  67. package/src/client.ts +2 -2
  68. package/src/errors.ts +1 -0
  69. package/src/internal.ts +3 -62
  70. package/src/push-envelope.ts +24 -21
  71. package/src/scheduler-qstash.ts +3 -3
  72. package/src/scheduler-vercel.ts +2 -2
  73. package/src/server-fetch.ts +344 -0
  74. package/src/server.ts +73 -225
  75. package/src/session-socket.ts +36 -20
  76. package/src/sse.ts +2 -2
  77. package/src/store-memory.ts +0 -81
  78. package/src/store-postgres.ts +0 -100
  79. package/src/store-redis-core.ts +47 -211
  80. package/src/store-redis-http.ts +0 -1
  81. package/src/store-redis.ts +0 -1
  82. package/src/store-sqlite.ts +0 -119
  83. package/src/store.ts +0 -60
  84. package/src/wire.ts +2 -1
  85. package/dist/ai-D_PGS-JR.d.ts +0 -334
  86. package/dist/ai-D_PGS-JR.d.ts.map +0 -1
  87. package/dist/cli-B3VuxoDe.js +0 -599
  88. package/dist/cli-B3VuxoDe.js.map +0 -1
  89. package/dist/cli-bin.d.ts +0 -1
  90. package/dist/cli-bin.js +0 -7
  91. package/dist/cli-bin.js.map +0 -1
  92. package/dist/cli.d.ts +0 -20
  93. package/dist/cli.d.ts.map +0 -1
  94. package/dist/cli.js +0 -2
  95. package/dist/client-CdMqi7mC.d.ts +0 -202
  96. package/dist/client-CdMqi7mC.d.ts.map +0 -1
  97. package/dist/client-Dj5d3SP_.js +0 -1026
  98. package/dist/client-Dj5d3SP_.js.map +0 -1
  99. package/dist/devtools-J_jZ2vQf.d.ts +0 -152
  100. package/dist/devtools-J_jZ2vQf.d.ts.map +0 -1
  101. package/dist/devtools-kJJaORn-.js +0 -340
  102. package/dist/devtools-kJJaORn-.js.map +0 -1
  103. package/dist/devtools-server.browser.d.ts +0 -1
  104. package/dist/devtools-server.browser.js +0 -6
  105. package/dist/devtools-server.browser.js.map +0 -1
  106. package/dist/devtools-server.d.ts +0 -23
  107. package/dist/devtools-server.d.ts.map +0 -1
  108. package/dist/devtools-server.js +0 -1270
  109. package/dist/devtools-server.js.map +0 -1
  110. package/dist/devtools.d.ts +0 -2
  111. package/dist/devtools.js +0 -2
  112. package/dist/errors-W6nwJ-fm.d.ts +0 -21
  113. package/dist/errors-W6nwJ-fm.d.ts.map +0 -1
  114. package/dist/http.d.ts +0 -151
  115. package/dist/http.d.ts.map +0 -1
  116. package/dist/http.js +0 -706
  117. package/dist/http.js.map +0 -1
  118. package/dist/inspection-DaxB5jM2.js +0 -13
  119. package/dist/inspection-DaxB5jM2.js.map +0 -1
  120. package/dist/internal-DstsI6Re.js.map +0 -1
  121. package/dist/platform-B4TnJtWu.js +0 -34
  122. package/dist/platform-B4TnJtWu.js.map +0 -1
  123. package/dist/server-DpvjhdoE.d.ts.map +0 -1
  124. package/dist/server-Duw6MVlB.js.map +0 -1
  125. package/dist/store-DysUkTH3.d.ts.map +0 -1
  126. package/dist/store-redis-core-BFLwz0Wj.js.map +0 -1
  127. package/dist/testing.browser.d.ts +0 -1
  128. package/dist/testing.browser.js +0 -6
  129. package/dist/testing.browser.js.map +0 -1
  130. package/dist/testing.d.ts +0 -32
  131. package/dist/testing.d.ts.map +0 -1
  132. package/dist/testing.js +0 -103
  133. package/dist/testing.js.map +0 -1
  134. package/dist/wire-BFQmSJ-9.js.map +0 -1
  135. package/docs/guides/07-devtools.mdx +0 -229
  136. package/src/cli-bin.ts +0 -5
  137. package/src/cli.ts +0 -1046
  138. package/src/devtools-app.ts +0 -989
  139. package/src/devtools-server.browser.ts +0 -5
  140. package/src/devtools-server.ts +0 -604
  141. package/src/devtools.ts +0 -716
  142. package/src/http.ts +0 -394
  143. package/src/inspection.ts +0 -39
  144. package/src/testing.browser.ts +0 -5
  145. package/src/testing.ts +0 -185
@@ -1 +0,0 @@
1
- {"version":3,"file":"store-redis-core-BFLwz0Wj.js","names":[],"sources":["../src/store-redis-core.ts"],"sourcesContent":["/**\n * The shared Redis store core — every operation except `stream` and\n * `close`, expressed over one injected `call` function so both\n * transports reuse it: `store-redis` (Redis protocol via `ioredis`) and\n * `store-redis-http` (provider REST APIs).\n *\n * The stream IS the store: each event is a stream entry whose ID is its\n * dense a2 index (`${index}-0`), so `XRANGE` is the catch-up read.\n * Mutable drain/failure bookkeeping lives in a side hash (stream\n * entries are immutable); every write path is one atomic Lua script.\n * The conformance suite in test/conformance is the executable\n * contract, run against a real spawned `redis-server`.\n *\n * Timestamps and claim expiry come from the injected clock so tests\n * can time-travel against a real server. Cluster mode is out: the\n * atomic scripts span keys.\n */\n\nimport type { PresencePatch } from './contract.ts'\nimport { A2Error } from './errors.ts'\nimport { idempotentReplay } from './idempotent-replay.ts'\nimport { nullProtoRecord } from './internal.ts'\nimport { decodeReturnedEventIds } from './store-codec.ts'\nimport type {\n A2Store,\n A2StoreInspection,\n Clock,\n CompleteAttemptResult,\n Event,\n EventCause,\n IdSource,\n StoreClaimAvailableResult,\n PresenceRow,\n StoredEvent,\n} from './store.ts'\n\n/** Issues any Redis command; the seam both transports implement. */\nexport type RedisCall = (\n command: string,\n ...args: Array<string | number>\n) => Promise<unknown>\n\nexport type RedisStoreCoreOptions = {\n call: RedisCall\n clock: Clock\n ids: IdSource\n keyPrefix: string\n /**\n * Disposable wake-up fired after a write appends new events to a\n * watched session — `count` is the session's new event count. The\n * watched check runs inside the write script, so unwatched sessions\n * cost no wake-up command at all. Losing one is harmless: live\n * feeds re-read on a safety cadence, so notifications only affect\n * latency, never delivery.\n */\n notify?: (sessionId: string, count: number) => void\n}\n\nexport type RedisStoreCore = Omit<\n A2Store,\n 'stream' | 'inspect' | 'presence'\n> & {\n inspect: A2StoreInspection\n /**\n * The storage half of the presence capability, shared by both\n * transports. `set` returns the applied patch — null when every\n * field lost the LWW race — so a push-capable transport broadcasts\n * exactly what `read` now reflects; delivery is its business.\n */\n presence: {\n set(\n ns: string,\n participant: string,\n values: Record<string, unknown | null>,\n meta: { seen: number; at: Date; ttlMs: number },\n ): Promise<PresencePatch | null>\n read(ns: string): Promise<PresenceRow[]>\n }\n /** XRANGE + meta merge — the full-row catch-up read. */\n readRange(\n sessionId: string,\n afterIndex: number,\n throughIndex?: number,\n ): Promise<StoredEvent[]>\n /** XRANGE only — the light catch-up read live feeds use. */\n readEvents(sessionId: string, afterIndex: number): Promise<Event[]>\n /**\n * Declare the session watched for `ttlMs` — writes publish wake-ups\n * only while this presence marker lives. Live feeds refresh it\n * ahead of expiry; an expired marker degrades latency to the safety\n * cadence, never delivery.\n */\n markWatched(sessionId: string, ttlMs: number): Promise<void>\n}\n\nconst READ_PAGE_SIZE = 256\n\n// ── Lua scripts (each runs atomically) ───────────────────────────────\n\n/**\n * KEYS: store stream, counter, global id hash, session index, metadata,\n * pending, ready, watched.\n * ARGV: sessionId, nowMs, n, then per event:\n * id, type, payloadJson, causeJson, lane, settled.\n * Returns ['empty', hasPending] | ['ok', firstIndex, hasPending, watched] |\n * ['dup', hasPending, ...indexes] | ['partial', count] |\n * ['foreign', eventId] | ['no_parent', idx] | ['superseded', idx].\n */\nconst APPEND_LUA = `\nlocal function lane_field(lane, suffix)\n return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix\nend\nlocal function enqueue(field, idx, lane)\n redis.call('ZADD', KEYS[6], idx, field)\n if lane == '' then\n redis.call('ZADD', KEYS[7], idx, field)\n return\n end\n local tail_key = lane_field(lane, 'tail')\n local tail = redis.call('HGET', KEYS[5], tail_key)\n if tail then\n redis.call('HSET', KEYS[5], tail .. ':next', field)\n else\n redis.call('HSET', KEYS[5], lane_field(lane, 'head'), field)\n redis.call('ZADD', KEYS[7], idx, field)\n end\n redis.call('HSET', KEYS[5], tail_key, field)\nend\nlocal n = tonumber(ARGV[3])\nlocal base = tonumber(redis.call('GET', KEYS[2]) or '0')\nlocal has_pending = redis.call('ZCARD', KEYS[6]) > 0 and '1' or '0'\nif n == 0 then return { 'empty', has_pending } end\nlocal dups = {}\nfor i = 1, n do\n local id = ARGV[3 + (i - 1) * 6 + 1]\n local existing = redis.call('HGET', KEYS[3], id)\n if existing then\n local rec = cjson.decode(existing)\n if rec[1] ~= ARGV[1] then return { 'foreign', id } end\n dups[#dups + 1] = tostring(rec[2])\n end\nend\nif #dups == n then\n local res = { 'dup', has_pending }\n for i = 1, #dups do res[#res + 1] = dups[i] end\n return res\nend\nif #dups > 0 then return { 'partial', tostring(#dups) } end\nfor i = 1, n do\n local cause = ARGV[3 + (i - 1) * 6 + 4]\n if cause ~= '' then\n local parsed = cjson.decode(cause)\n local pidx = tonumber(parsed.index)\n if pidx < 1 or pidx > base then\n return { 'no_parent', tostring(parsed.index) }\n end\n local field = tostring(pidx)\n local dispatches = tonumber(redis.call('HGET', KEYS[5], field .. ':d') or '0')\n if dispatches ~= tonumber(parsed.attempt) or\n redis.call('HEXISTS', KEYS[5], field .. ':f') == 1 then\n return { 'superseded', field }\n end\n end\nend\nfor i = 1, n do\n local id = ARGV[3 + (i - 1) * 6 + 1]\n local typ = ARGV[3 + (i - 1) * 6 + 2]\n local payload = ARGV[3 + (i - 1) * 6 + 3]\n local cause = ARGV[3 + (i - 1) * 6 + 4]\n local lane = ARGV[3 + (i - 1) * 6 + 5]\n local settled = ARGV[3 + (i - 1) * 6 + 6]\n local idx = base + i\n redis.call('XADD', KEYS[1], tostring(idx) .. '-0',\n 'id', id, 'type', typ, 'payload', payload, 'created_at', ARGV[2],\n 'cause', cause, 'lane', lane)\n redis.call('HSET', KEYS[3], id, cjson.encode({ ARGV[1], idx }))\n redis.call('HSET', KEYS[5], tostring(idx) .. ':l', lane)\n if settled == '1' then\n redis.call('HSET', KEYS[5], tostring(idx) .. ':p', ARGV[2])\n else\n enqueue(tostring(idx), idx, lane)\n end\nend\nredis.call('SET', KEYS[2], tostring(base + n))\nredis.call('ZADD', KEYS[4], 0, ARGV[1])\nhas_pending = redis.call('ZCARD', KEYS[6]) > 0 and '1' or '0'\nreturn { 'ok', tostring(base + 1), has_pending,\n tostring(redis.call('EXISTS', KEYS[8])) }\n`\n\n/** KEYS: counter, meta, pending, ready. ARGV: idx, attempt, error, maxFailures, nowMs. */\nconst FAIL_ATTEMPT_LUA = `\nlocal function lane_field(lane, suffix)\n return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix\nend\nlocal function settle(field)\n redis.call('ZREM', KEYS[3], field)\n redis.call('ZREM', KEYS[4], field)\n local lane = redis.call('HGET', KEYS[2], field .. ':l') or ''\n if lane == '' then return end\n local head_key = lane_field(lane, 'head')\n if redis.call('HGET', KEYS[2], head_key) ~= field then return end\n local next_field = redis.call('HGET', KEYS[2], field .. ':next')\n while next_field and redis.call('HEXISTS', KEYS[2], next_field .. ':p') == 1 do\n next_field = redis.call('HGET', KEYS[2], next_field .. ':next')\n end\n if next_field then\n redis.call('HSET', KEYS[2], head_key, next_field)\n if redis.call('HEXISTS', KEYS[2], next_field .. ':f') == 0 then\n redis.call('ZADD', KEYS[4], tonumber(next_field), next_field)\n end\n else\n redis.call('HDEL', KEYS[2], head_key, lane_field(lane, 'tail'))\n end\nend\nlocal idx = tonumber(ARGV[1])\nif idx < 1 or idx > tonumber(redis.call('GET', KEYS[1]) or '0') then\n return { 'missing', '0' }\nend\nlocal field = ARGV[1]\nlocal failures = tonumber(redis.call('HGET', KEYS[2], field .. ':a') or '0')\nlocal dispatches = tonumber(redis.call('HGET', KEYS[2], field .. ':d') or '0')\nif redis.call('HEXISTS', KEYS[2], field .. ':p') == 1 or dispatches ~= tonumber(ARGV[2]) then\n return { 'superseded', tostring(failures) }\nend\nif redis.call('HEXISTS', KEYS[2], field .. ':f') == 1 then\n return { 'dead_lettered', tostring(failures) }\nend\nif redis.call('HEXISTS', KEYS[2], field .. ':lfa') == 1 and\n tonumber(redis.call('HGET', KEYS[2], field .. ':lfa')) == tonumber(ARGV[2]) and\n redis.call('HEXISTS', KEYS[2], field .. ':ch') == 0 then\n return { 'failed', tostring(failures) }\nend\nif redis.call('HEXISTS', KEYS[2], field .. ':ch') == 0 then\n return { 'superseded', tostring(failures) }\nend\nfailures = redis.call('HINCRBY', KEYS[2], field .. ':a', 1)\nredis.call('HSET', KEYS[2],\n field .. ':e', ARGV[3],\n field .. ':lf', ARGV[5],\n field .. ':lfa', ARGV[2])\nredis.call('HDEL', KEYS[2], field .. ':ch', field .. ':ce')\nif failures >= tonumber(ARGV[4]) then\n redis.call('HSET', KEYS[2], field .. ':f', ARGV[5])\n redis.call('ZREM', KEYS[4], field)\n return { 'dead_lettered', tostring(failures) }\nend\nreturn { 'failed', tostring(failures) }\n`\n\n/**\n * KEYS: store, counter, meta, pending, ready.\n * ARGV: holder, nowMs, expiresAtMs, excludedCount, excluded indexes.\n */\nconst CLAIM_AVAILABLE_LUA = `\nif redis.call('ZCARD', KEYS[4]) == 0 then return { 'settled' } end\nlocal ready = redis.call('ZRANGE', KEYS[5], 0, -1)\nlocal excluded = {}\nfor i = 1, tonumber(ARGV[4]) do excluded[ARGV[4 + i]] = true end\nlocal claimed = {}\nlocal retry_at = nil\nfor _, field in ipairs(ready) do\n if not excluded[field] then\n local expires = tonumber(redis.call('HGET', KEYS[3], field .. ':ce') or '0')\n if expires > tonumber(ARGV[2]) then\n if not retry_at or expires < retry_at then retry_at = expires end\n else\n local entries = redis.call('XRANGE', KEYS[1], field .. '-0', field .. '-0')\n if #entries == 0 then return { 'missing', field } end\n local attempt = redis.call('HINCRBY', KEYS[3], field .. ':d', 1)\n redis.call('HSETNX', KEYS[3], field .. ':fc', ARGV[2])\n redis.call('HSET', KEYS[3],\n field .. ':lc', ARGV[2],\n field .. ':ch', ARGV[1],\n field .. ':ce', ARGV[3])\n claimed[#claimed + 1] = {\n tostring(attempt),\n redis.call('HGET', KEYS[3], field .. ':a') or '0',\n redis.call('HGET', KEYS[3], field .. ':e') or false,\n redis.call('HGET', KEYS[3], field .. ':fc') or false,\n ARGV[2],\n redis.call('HGET', KEYS[3], field .. ':lf') or false,\n redis.call('HGET', KEYS[3], field .. ':lfa') or false,\n ARGV[1],\n ARGV[3],\n entries[1]\n }\n end\n end\nend\nif #claimed > 0 then\n local result = { 'claimed', tostring(#claimed) }\n for _, claim in ipairs(claimed) do\n for _, value in ipairs(claim) do result[#result + 1] = value end\n end\n return result\nend\nif retry_at then return { 'busy', tostring(retry_at) } end\nreturn { 'settled' }\n`\n\n/**\n * KEYS: counter, meta.\n * ARGV: holder, nowMs, expiresAtMs, n, then per claim: index, attempt.\n * Returns [renewedCount, ...renewed, ...superseded].\n */\nconst RENEW_CLAIMS_LUA = `\nlocal total = tonumber(redis.call('GET', KEYS[1]) or '0')\nlocal renewed = {}\nlocal superseded = {}\nfor i = 1, tonumber(ARGV[4]) do\n local field = ARGV[4 + (i - 1) * 2 + 1]\n local attempt = tonumber(ARGV[4 + (i - 1) * 2 + 2])\n local idx = tonumber(field)\n if idx >= 1 and idx <= total then\n local dispatches = tonumber(redis.call('HGET', KEYS[2], field .. ':d') or '0')\n if dispatches > attempt then\n superseded[#superseded + 1] = field\n else\n local expires = tonumber(redis.call('HGET', KEYS[2], field .. ':ce') or '0')\n if tonumber(ARGV[3]) > tonumber(ARGV[2]) and\n redis.call('HEXISTS', KEYS[2], field .. ':p') == 0 and\n redis.call('HEXISTS', KEYS[2], field .. ':f') == 0 and\n redis.call('HGET', KEYS[2], field .. ':ch') == ARGV[1] and\n expires > tonumber(ARGV[2]) then\n redis.call('HSET', KEYS[2], field .. ':ce', ARGV[3])\n renewed[#renewed + 1] = field\n end\n end\n end\nend\nlocal result = { tostring(#renewed) }\nfor i = 1, #renewed do result[#result + 1] = renewed[i] end\nfor i = 1, #superseded do result[#result + 1] = superseded[i] end\nreturn result\n`\n\n/**\n * KEYS: store, counter, ids, session index, meta, pending, ready, watched.\n * ARGV: sessionId, nowMs, parent index, attempt, n, then per child:\n * id, type, payloadJson, lane, settled.\n */\nconst COMPLETE_ATTEMPT_LUA = `\nlocal function lane_field(lane, suffix)\n return '__lane:' .. tostring(string.len(lane)) .. ':' .. lane .. ':' .. suffix\nend\nlocal function settle(field)\n redis.call('ZREM', KEYS[6], field)\n redis.call('ZREM', KEYS[7], field)\n local lane = redis.call('HGET', KEYS[5], field .. ':l') or ''\n if lane == '' then return end\n local head_key = lane_field(lane, 'head')\n if redis.call('HGET', KEYS[5], head_key) ~= field then return end\n local next_field = redis.call('HGET', KEYS[5], field .. ':next')\n while next_field and redis.call('HEXISTS', KEYS[5], next_field .. ':p') == 1 do\n next_field = redis.call('HGET', KEYS[5], next_field .. ':next')\n end\n if next_field then\n redis.call('HSET', KEYS[5], head_key, next_field)\n if redis.call('HEXISTS', KEYS[5], next_field .. ':f') == 0 then\n redis.call('ZADD', KEYS[7], tonumber(next_field), next_field)\n end\n else\n redis.call('HDEL', KEYS[5], head_key, lane_field(lane, 'tail'))\n end\nend\nlocal function enqueue(field, idx, lane)\n redis.call('ZADD', KEYS[6], idx, field)\n if lane == '' then\n redis.call('ZADD', KEYS[7], idx, field)\n return\n end\n local tail_key = lane_field(lane, 'tail')\n local tail = redis.call('HGET', KEYS[5], tail_key)\n if tail then\n redis.call('HSET', KEYS[5], tail .. ':next', field)\n else\n redis.call('HSET', KEYS[5], lane_field(lane, 'head'), field)\n redis.call('ZADD', KEYS[7], idx, field)\n end\n redis.call('HSET', KEYS[5], tail_key, field)\nend\nlocal parent = ARGV[3]\nlocal attempt = tonumber(ARGV[4])\nlocal n = tonumber(ARGV[5])\nlocal total = tonumber(redis.call('GET', KEYS[2]) or '0')\nif tonumber(parent) < 1 or tonumber(parent) > total then return { 'missing' } end\nlocal dispatches = tonumber(redis.call('HGET', KEYS[5], parent .. ':d') or '0')\nlocal processed = redis.call('HGET', KEYS[5], parent .. ':p')\nlocal processed_attempt = tonumber(redis.call('HGET', KEYS[5], parent .. ':pa') or '0')\nif processed then\n if processed_attempt ~= attempt then return { 'superseded' } end\n local raw_recorded_ids = redis.call('HGET', KEYS[5], parent .. ':ri')\n if not raw_recorded_ids then return { 'partial' } end\n local recorded_ids = cjson.decode(raw_recorded_ids)\n if #recorded_ids ~= n then return { 'partial' } end\n local indexes = {}\n for i = 1, n do\n local id = ARGV[5 + (i - 1) * 5 + 1]\n if recorded_ids[i] ~= id then return { 'partial' } end\n local existing = redis.call('HGET', KEYS[3], id)\n if not existing then return { 'partial' } end\n local rec = cjson.decode(existing)\n if rec[1] ~= ARGV[1] then return { 'partial' } end\n local entries = redis.call('XRANGE', KEYS[1], tostring(rec[2]) .. '-0', tostring(rec[2]) .. '-0')\n if #entries == 0 then return { 'partial' } end\n local values = entries[1][2]\n local cause = nil\n for j = 1, #values, 2 do\n if values[j] == 'cause' then cause = values[j + 1] break end\n end\n if not cause or cause == '' then return { 'partial' } end\n local decoded = cjson.decode(cause)\n if tonumber(decoded['index']) ~= tonumber(parent) or\n tonumber(decoded['attempt']) ~= attempt then return { 'partial' } end\n indexes[#indexes + 1] = tostring(rec[2])\n end\n local result = { 'duplicate' }\n for _, idx in ipairs(indexes) do result[#result + 1] = idx end\n return result\nend\nif dispatches ~= attempt then return { 'superseded' } end\nif redis.call('HEXISTS', KEYS[5], parent .. ':ch') == 0 or\n redis.call('HEXISTS', KEYS[5], parent .. ':f') == 1 or\n tonumber(redis.call('HGET', KEYS[5], parent .. ':lfa') or '0') == attempt then\n return { 'superseded' }\nend\nfor i = 1, n do\n local id = ARGV[5 + (i - 1) * 5 + 1]\n if redis.call('HEXISTS', KEYS[3], id) == 1 then return { 'partial' } end\nend\nlocal returned_ids = {}\nfor i = 1, n do returned_ids[i] = ARGV[5 + (i - 1) * 5 + 1] end\nlocal returned_json = '[]'\nif n > 0 then returned_json = cjson.encode(returned_ids) end\nredis.call('HSET', KEYS[5],\n parent .. ':p', ARGV[2],\n parent .. ':pa', ARGV[4],\n parent .. ':ri', returned_json)\nredis.call('HDEL', KEYS[5], parent .. ':ch', parent .. ':ce')\nsettle(parent)\nlocal cause = cjson.encode({ index = tonumber(parent), attempt = attempt })\nfor i = 1, n do\n local id = ARGV[5 + (i - 1) * 5 + 1]\n local typ = ARGV[5 + (i - 1) * 5 + 2]\n local payload = ARGV[5 + (i - 1) * 5 + 3]\n local lane = ARGV[5 + (i - 1) * 5 + 4]\n local settled = ARGV[5 + (i - 1) * 5 + 5]\n local idx = total + i\n redis.call('XADD', KEYS[1], tostring(idx) .. '-0',\n 'id', id, 'type', typ, 'payload', payload, 'created_at', ARGV[2],\n 'cause', cause, 'lane', lane)\n redis.call('HSET', KEYS[3], id, cjson.encode({ ARGV[1], idx }))\n redis.call('HSET', KEYS[5], tostring(idx) .. ':l', lane)\n if settled == '1' then\n redis.call('HSET', KEYS[5], tostring(idx) .. ':p', ARGV[2])\n else\n enqueue(tostring(idx), idx, lane)\n end\nend\nredis.call('SET', KEYS[2], tostring(total + n))\nredis.call('ZADD', KEYS[4], 0, ARGV[1])\nreturn { 'ok', tostring(total + 1),\n tostring(redis.call('EXISTS', KEYS[8])) }\n`\n\n/** KEYS: snapshot. ARGV: upToIndex, json. Guarded: never move backward. */\nconst PUT_SNAPSHOT_LUA = `\nlocal cur = redis.call('GET', KEYS[1])\nif cur and tonumber(cjson.decode(cur)['index']) >= tonumber(ARGV[1]) then\n return 0\nend\nredis.call('SET', KEYS[1], ARGV[2])\nreturn 1\n`\n\n/**\n * KEYS: presence hash. ARGV: atMs, ttlMs, n, then per entry:\n * hashField, valueJson ('' deletes). Field-wise LWW by the sender's\n * `at`: a stored entry with a strictly newer stamp survives; ties go\n * to the incoming write (memory-tier parity). Returns the applied\n * 1-based entry positions. The whole-key PEXPIRE is the coarse bound,\n * sound because core sends one uniform ttlMs; the per-entry `e` stamp\n * is the fine bound (the injected storage clock at the applied set\n * plus ttlMs — never the sender stamp), pruned on read against the\n * same clock.\n */\nconst PRESENCE_SET_LUA = `\nlocal at = tonumber(ARGV[1])\nlocal applied = {}\nfor i = 1, tonumber(ARGV[3]) do\n local field = ARGV[3 + (i - 1) * 2 + 1]\n local value = ARGV[3 + (i - 1) * 2 + 2]\n local existing = redis.call('HGET', KEYS[1], field)\n if not existing or tonumber(cjson.decode(existing)['a']) <= at then\n if value == '' then\n redis.call('HDEL', KEYS[1], field)\n else\n redis.call('HSET', KEYS[1], field, value)\n end\n applied[#applied + 1] = tostring(i)\n end\nend\nif #applied > 0 then redis.call('PEXPIRE', KEYS[1], ARGV[2]) end\nreturn applied\n`\n\n/**\n * KEYS: presence hash. ARGV: nowMs (the injected clock, so tests can\n * time-travel expiry against a real server). Returns the live\n * [field, value, ...] pairs; entries at or past their fine-bound\n * expiry are HDELed in the same atomic step, so a stale reader can\n * never sweep a concurrently refreshed field.\n */\nconst PRESENCE_READ_LUA = `\nlocal now = tonumber(ARGV[1])\nlocal rows = redis.call('HGETALL', KEYS[1])\nlocal live = {}\nfor i = 1, #rows, 2 do\n if tonumber(cjson.decode(rows[i + 1])['e']) <= now then\n redis.call('HDEL', KEYS[1], rows[i])\n else\n live[#live + 1] = rows[i]\n live[#live + 1] = rows[i + 1]\n end\nend\nreturn live\n`\n\n/** KEYS: snapshot, event counter. Returns one atomic cache-plus-frontier read. */\nconst READ_STATE_LUA = `\nlocal snapshot = redis.call('GET', KEYS[1])\nlocal frontier = redis.call('GET', KEYS[2]) or '0'\nreturn { snapshot or '', frontier }\n`\n\n// ── wire helpers ─────────────────────────────────────────────────────\n\ntype StreamEntry = [id: string, fields: string[]]\n\nconst fieldMap = (fields: string[]): Record<string, string> => {\n const map: Record<string, string> = {}\n for (let i = 0; i + 1 < fields.length; i += 2) {\n map[fields[i]!] = fields[i + 1]!\n }\n return map\n}\n\nconst STORED_EVENT_META_SUFFIXES = [\n 'p',\n 'pa',\n 'ri',\n 'ch',\n 'ce',\n 'f',\n 'fc',\n 'lc',\n 'lf',\n 'lfa',\n 'd',\n 'a',\n 'e',\n] as const\n\nconst entryIndex = (entryId: string): number =>\n Number(entryId.slice(0, entryId.indexOf('-')))\n\nconst toEvent = (sessionId: string, entry: StreamEntry): Event => {\n const fields = fieldMap(entry[1])\n return {\n id: fields['id']!,\n type: fields['type']!,\n payload: JSON.parse(fields['payload']!),\n index: entryIndex(entry[0]),\n sessionId,\n createdAt: new Date(Number(fields['created_at'])),\n }\n}\n\nconst json = (value: unknown): string => JSON.stringify(value) ?? 'null'\n\nconst escapeGlob = (value: string): string =>\n value\n .replaceAll('\\\\', '\\\\\\\\')\n .replaceAll('*', '\\\\*')\n .replaceAll('?', '\\\\?')\n .replaceAll('[', '\\\\[')\n .replaceAll(']', '\\\\]')\n\nconst toCause = (raw: string | undefined): EventCause | null => {\n if (raw === '' || raw === undefined) return null\n const value = JSON.parse(raw) 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 toClaimAvailableResult = (\n sessionId: string,\n reply: unknown[],\n): StoreClaimAvailableResult => {\n const outcome = String(reply[0])\n if (outcome === 'claimed') {\n const count = Number(reply[1])\n const events: StoredEvent[] = []\n for (let i = 0; i < count; i++) {\n const offset = 2 + i * 10\n const entry = reply[offset + 9] as StreamEntry\n const event = toEvent(sessionId, entry)\n const fields = fieldMap(entry[1])\n events.push(\n Object.assign(event, {\n cause: toCause(fields['cause']),\n lane: fields['lane'] === '' ? null : fields['lane']!,\n processedAt: null,\n processedByAttempt: null,\n returnedEventIds: null,\n firstClaimedAt:\n reply[offset + 3] === null\n ? null\n : new Date(Number(reply[offset + 3])),\n lastClaimedAt: new Date(Number(reply[offset + 4])),\n attemptCount: Number(reply[offset]),\n claimHolder: String(reply[offset + 7]),\n claimExpiresAt: new Date(Number(reply[offset + 8])),\n failureCount: Number(reply[offset + 1]),\n lastFailedAt:\n reply[offset + 5] === null\n ? null\n : new Date(Number(reply[offset + 5])),\n lastFailedAttempt:\n reply[offset + 6] === null ? null : Number(reply[offset + 6]),\n lastError:\n reply[offset + 2] === null ? null : String(reply[offset + 2]),\n failedAt: null,\n }),\n )\n }\n return {\n outcome: 'claimed',\n events,\n }\n }\n if (outcome === 'busy') {\n return { outcome, retryAt: new Date(Number(reply[1])) }\n }\n if (outcome === 'settled') return { outcome }\n throw new TypeError(\n `claim referenced a missing event in session '${sessionId}'`,\n )\n}\n\nconst wrap = async <T>(fn: () => Promise<T>): Promise<T> => {\n try {\n return await fn()\n } catch (err) {\n if (err instanceof A2Error || err instanceof TypeError) throw err\n throw new A2Error('STORE_UNAVAILABLE', 'redis store operation failed', {\n cause: err,\n })\n }\n}\n\nexport function createRedisStoreCore(\n options: RedisStoreCoreOptions,\n): RedisStoreCore {\n const { call, clock, notify } = options\n const generateId = options.ids\n const prefix = options.keyPrefix\n\n const storeKey = (s: string): string => `${prefix}:${s}:log`\n const countKey = (s: string): string => `${prefix}:${s}:count`\n const metaKey = (s: string): string => `${prefix}:${s}:meta`\n const pendingKey = (s: string): string => `${prefix}:${s}:pending`\n const readyKey = (s: string): string => `${prefix}:${s}:ready`\n const snapKey = (s: string, r: string): string => `${prefix}:${s}:snap:${r}`\n const watchedKey = (s: string): string => `${prefix}:${s}:watched`\n const presenceKey = (s: string): string => `${prefix}:${s}:presence`\n const idsKey = `${prefix}:ids`\n const sessionsKey = `${prefix}:sessions`\n\n const evalScript = (\n script: string,\n keys: string[],\n args: Array<string | number>,\n ): Promise<unknown> => call('EVAL', script, keys.length, ...keys, ...args)\n\n const readFrontier = async (sessionId: string): Promise<number> => {\n const frontier = Number((await call('GET', countKey(sessionId))) ?? 0)\n if (!Number.isSafeInteger(frontier) || frontier < 0) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event count for session '${sessionId}' is invalid`,\n )\n }\n return frontier\n }\n\n const readRangePage = async (\n sessionId: string,\n afterIndex: number,\n throughIndex: number,\n limit: number,\n ): Promise<StoredEvent[]> => {\n if (throughIndex <= afterIndex) return []\n const entries = (await call(\n 'XRANGE',\n storeKey(sessionId),\n `${afterIndex + 1}-0`,\n `${throughIndex}-0`,\n 'COUNT',\n limit,\n )) as StreamEntry[]\n if (entries.length === 0) return []\n\n const metaFields = entries.flatMap((entry) => {\n const index = entryIndex(entry[0])\n return STORED_EVENT_META_SUFFIXES.map((suffix) => `${index}:${suffix}`)\n })\n const metaValues =\n ((await call('HMGET', metaKey(sessionId), ...metaFields)) as Array<\n string | null\n > | null) ?? []\n const meta: Record<string, string> = {}\n for (let i = 0; i < metaFields.length; i += 1) {\n const value = metaValues[i]\n if (value !== null && value !== undefined) {\n meta[metaFields[i]!] = value\n }\n }\n return entries.map((entry) => {\n const event = toEvent(sessionId, entry)\n const idx = event.index\n const fields = fieldMap(entry[1])\n const processedAt = meta[`${idx}:p`]\n const processedByAttempt = meta[`${idx}:pa`]\n const returnedEventIds = meta[`${idx}:ri`]\n const claimHolder = meta[`${idx}:ch`]\n const claimExpiresAt = meta[`${idx}:ce`]\n const failedAt = meta[`${idx}:f`]\n const firstClaimedAt = meta[`${idx}:fc`]\n const lastClaimedAt = meta[`${idx}:lc`]\n const lastFailedAt = meta[`${idx}:lf`]\n const lastFailedAttempt = meta[`${idx}:lfa`]\n return Object.assign(event, {\n cause: toCause(fields['cause']),\n lane: fields['lane'] === '' ? null : fields['lane']!,\n processedAt:\n processedAt === undefined ? null : new Date(Number(processedAt)),\n processedByAttempt:\n processedByAttempt === undefined ? null : Number(processedByAttempt),\n returnedEventIds:\n returnedEventIds === undefined\n ? null\n : decodeReturnedEventIds(returnedEventIds),\n firstClaimedAt:\n firstClaimedAt === undefined\n ? null\n : new Date(Number(firstClaimedAt)),\n lastClaimedAt:\n lastClaimedAt === undefined ? null : new Date(Number(lastClaimedAt)),\n attemptCount: Number(meta[`${idx}:d`] ?? 0),\n claimHolder: claimHolder ?? null,\n claimExpiresAt:\n claimExpiresAt === undefined\n ? null\n : new Date(Number(claimExpiresAt)),\n failureCount: Number(meta[`${idx}:a`] ?? 0),\n lastFailedAt:\n lastFailedAt === undefined ? null : new Date(Number(lastFailedAt)),\n lastFailedAttempt:\n lastFailedAttempt === undefined ? null : Number(lastFailedAttempt),\n lastError: meta[`${idx}:e`] ?? null,\n failedAt: failedAt === undefined ? null : new Date(Number(failedAt)),\n })\n })\n }\n\n const readEventRange = async (\n sessionId: string,\n afterIndex: number,\n throughIndex: number,\n ): Promise<Event[]> => {\n if (throughIndex <= afterIndex) return []\n const events: Event[] = []\n let nextIndex = afterIndex + 1\n while (nextIndex <= throughIndex) {\n // oxlint-disable-next-line no-await-in-loop -- each page advances the dense frozen range\n const entries = (await call(\n 'XRANGE',\n storeKey(sessionId),\n `${nextIndex}-0`,\n `${throughIndex}-0`,\n 'COUNT',\n READ_PAGE_SIZE,\n )) as StreamEntry[]\n if (entries.length === 0) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' ended before index ${throughIndex}`,\n )\n }\n for (let position = 0; position < entries.length; position += 1) {\n const index = entryIndex(entries[position]![0])\n if (index !== nextIndex + position || index > throughIndex) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' is not contiguous`,\n )\n }\n }\n events.push(...entries.map((entry) => toEvent(sessionId, entry)))\n nextIndex = events.at(-1)!.index + 1\n }\n return events\n }\n\n /** Bounded XRANGE pages restore the full-read contract on capped providers. */\n const readRange = async (\n sessionId: string,\n afterIndex: number,\n throughIndex?: number,\n ): Promise<StoredEvent[]> => {\n if (throughIndex !== undefined && throughIndex <= afterIndex) return []\n const currentFrontier = await readFrontier(sessionId)\n const frontier = Math.min(throughIndex ?? currentFrontier, currentFrontier)\n if (frontier <= afterIndex) return []\n\n const rows: StoredEvent[] = []\n let nextIndex = afterIndex + 1\n while (nextIndex <= frontier) {\n // oxlint-disable-next-line no-await-in-loop -- each page advances the dense frozen range\n const page = await readRangePage(\n sessionId,\n nextIndex - 1,\n frontier,\n READ_PAGE_SIZE,\n )\n if (page.length === 0) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' ended before index ${frontier}`,\n )\n }\n for (let position = 0; position < page.length; position += 1) {\n if (page[position]!.index !== nextIndex + position) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' is not contiguous`,\n )\n }\n }\n const lastIndex = page.at(-1)!.index\n if (lastIndex < nextIndex || lastIndex > frontier) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' did not advance`,\n )\n }\n rows.push(...page)\n nextIndex = lastIndex + 1\n }\n return rows\n }\n\n return {\n async append(sessionId, events) {\n return wrap(async () => {\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 const now = clock.now().getTime()\n const withIds = events.map((e) => ({\n id: e.id ?? generateId(),\n type: e.type,\n payload: e.payload,\n cause: e.cause ? { ...e.cause } : null,\n lane: e.lane ?? null,\n settled: e.settled === true,\n }))\n const args: Array<string | number> = [\n sessionId,\n now,\n withIds.length,\n ...withIds.flatMap((e) => [\n e.id,\n e.type,\n json(e.payload),\n e.cause ? json(e.cause) : '',\n e.lane ?? '',\n e.settled ? '1' : '0',\n ]),\n ]\n const reply = (await evalScript(\n APPEND_LUA,\n [\n storeKey(sessionId),\n countKey(sessionId),\n idsKey,\n sessionsKey,\n metaKey(sessionId),\n pendingKey(sessionId),\n readyKey(sessionId),\n watchedKey(sessionId),\n ],\n args,\n )) as string[]\n\n const [verdict] = reply\n if (verdict === 'empty') {\n return { events: [], hasPending: reply[1] === '1' }\n }\n if (verdict === 'foreign') {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `event id '${reply[1]}' already exists in another session`,\n )\n }\n if (verdict === 'no_parent') {\n throw new TypeError(\n `no event at index ${reply[1]} in session '${sessionId}'`,\n )\n }\n if (verdict === 'superseded') {\n throw new A2Error(\n 'SUPERSEDED_ATTEMPT',\n `a newer attempt owns event ${reply[1]} in session '${sessionId}'`,\n )\n }\n if (verdict === 'partial') {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `batch mixes ${reply[1]} already-appended and ${events.length - Number(reply[1])} fresh events`,\n )\n }\n if (verdict === 'dup') {\n // A retry of a committed batch whose ack was lost — return\n // the original rows, ordered by index like every adapter.\n const hasPending = reply[1] === '1'\n const wanted = new Set(reply.slice(2).map(Number))\n const min = Math.min(...wanted)\n const rows = await readRange(sessionId, min - 1)\n const existing = rows.filter((row) => wanted.has(row.index))\n return {\n events: idempotentReplay(events, existing),\n hasPending,\n }\n }\n const base = Number(reply[1]) - 1\n if (reply[3] === '1') notify?.(sessionId, base + withIds.length)\n return {\n events: withIds.map((e, i) => ({\n id: e.id,\n type: e.type,\n payload: structuredClone(e.payload),\n index: base + 1 + i,\n sessionId,\n createdAt: new Date(now),\n cause: e.cause,\n lane: e.lane,\n processedAt: e.settled ? new Date(now) : null,\n processedByAttempt: null,\n returnedEventIds: null,\n firstClaimedAt: null,\n lastClaimedAt: null,\n attemptCount: 0,\n claimHolder: null,\n claimExpiresAt: null,\n failureCount: 0,\n lastFailedAt: null,\n lastFailedAttempt: null,\n lastError: null,\n failedAt: null,\n })),\n hasPending: reply[2] === '1',\n }\n })\n },\n\n async read(sessionId, opts) {\n return wrap(async () => {\n const rows = await readRange(\n sessionId,\n opts?.afterIndex ?? 0,\n opts?.throughIndex,\n )\n return rows\n })\n },\n\n async claimAvailable({\n sessionId,\n holder,\n ttlMs,\n expiresAtMs,\n excludeIndexes,\n }) {\n return wrap(async () => {\n const now = clock.now().getTime()\n const expiresAt = expiresAtMs ?? now + ttlMs\n const excluded = excludeIndexes ?? []\n const reply = (await evalScript(\n CLAIM_AVAILABLE_LUA,\n [\n storeKey(sessionId),\n countKey(sessionId),\n metaKey(sessionId),\n pendingKey(sessionId),\n readyKey(sessionId),\n ],\n [holder, now, expiresAt, excluded.length, ...excluded],\n )) as unknown[]\n return toClaimAvailableResult(sessionId, reply)\n })\n },\n\n async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {\n return wrap(async () => {\n const now = clock.now().getTime()\n const expiresAt = expiresAtMs ?? now + ttlMs\n const reply = (await evalScript(\n RENEW_CLAIMS_LUA,\n [countKey(sessionId), metaKey(sessionId)],\n [\n holder,\n now,\n expiresAt,\n claims.length,\n ...claims.flatMap((claim) => [claim.index, claim.attempt]),\n ],\n )) as string[]\n const renewedCount = Number(reply[0])\n const values = reply.slice(1).map(Number)\n return {\n renewed: values.slice(0, renewedCount).toSorted((a, b) => a - b),\n superseded: values.slice(renewedCount).toSorted((a, b) => a - b),\n }\n })\n },\n\n async completeAttempt({ sessionId, index, attempt, events }) {\n return wrap(async () => {\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 'batch contains the same event id more than once',\n )\n }\n const now = clock.now().getTime()\n const reply = (await evalScript(\n COMPLETE_ATTEMPT_LUA,\n [\n storeKey(sessionId),\n countKey(sessionId),\n idsKey,\n sessionsKey,\n metaKey(sessionId),\n pendingKey(sessionId),\n readyKey(sessionId),\n watchedKey(sessionId),\n ],\n [\n sessionId,\n now,\n index,\n attempt,\n events.length,\n ...events.flatMap((event) => [\n event.id,\n event.type,\n json(event.payload),\n event.lane ?? '',\n event.settled === true ? '1' : '0',\n ]),\n ],\n )) as string[]\n const outcome = reply[0]\n if (outcome === 'missing') {\n throw new TypeError(\n `no event at index ${index} in session '${sessionId}'`,\n )\n }\n if (outcome === 'partial') {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'returned event ids do not match the completed attempt',\n )\n }\n if (outcome === 'superseded') return { outcome }\n\n const cause = { index, attempt }\n if (outcome === 'ok') {\n const base = Number(reply[1]) - 1\n if (events.length > 0 && reply[2] === '1') {\n notify?.(sessionId, base + events.length)\n }\n return {\n outcome: 'completed',\n events: events.map((event, i) => ({\n id: event.id,\n type: event.type,\n payload: structuredClone(event.payload),\n index: base + 1 + i,\n sessionId,\n createdAt: new Date(now),\n cause,\n lane: event.lane ?? null,\n processedAt: event.settled === true ? new Date(now) : null,\n processedByAttempt: null,\n returnedEventIds: null,\n firstClaimedAt: null,\n lastClaimedAt: null,\n attemptCount: 0,\n claimHolder: null,\n claimExpiresAt: null,\n failureCount: 0,\n lastFailedAt: null,\n lastFailedAttempt: null,\n lastError: null,\n failedAt: null,\n })),\n } satisfies CompleteAttemptResult\n }\n if (outcome === 'duplicate') {\n if (events.length === 0) {\n return { outcome: 'completed', events: [] }\n }\n const wanted = new Set(reply.slice(1).map(Number))\n const min = Math.min(...wanted)\n const rows = await readRange(sessionId, min - 1)\n const byIndex = new Map(rows.map((row) => [row.index, row]))\n const existing = reply\n .slice(1)\n .map((value) => byIndex.get(Number(value))!)\n return {\n outcome: 'completed',\n events: idempotentReplay(events, existing),\n }\n }\n throw new TypeError(`unexpected completion outcome '${outcome}'`)\n })\n },\n\n async failAttempt({ sessionId, index, attempt, error, maxFailures }) {\n return wrap(async () => {\n const reply = (await evalScript(\n FAIL_ATTEMPT_LUA,\n [\n countKey(sessionId),\n metaKey(sessionId),\n pendingKey(sessionId),\n readyKey(sessionId),\n ],\n [index, attempt, error, maxFailures, clock.now().getTime()],\n )) as string[]\n const outcome = reply[0]\n if (outcome === 'missing') {\n throw new TypeError(\n `no event at index ${index} in session '${sessionId}'`,\n )\n }\n return {\n outcome: outcome as 'failed' | 'dead_lettered' | 'superseded',\n failureCount: Number(reply[1]),\n }\n })\n },\n\n async readState(sessionId, reducerName) {\n return wrap(async () => {\n const reply = (await evalScript(\n READ_STATE_LUA,\n [snapKey(sessionId, reducerName), countKey(sessionId)],\n [],\n )) as [string, string]\n const raw = reply[0]\n const parsed =\n raw === ''\n ? null\n : (JSON.parse(raw) as { index: number; state: unknown })\n const frontier = Number(reply[1])\n if (\n !Number.isSafeInteger(frontier) ||\n frontier < 0 ||\n (parsed !== null &&\n (!Number.isSafeInteger(parsed.index) || parsed.index < 0))\n ) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis state frontier for session '${sessionId}' is invalid`,\n )\n }\n const events = await readEventRange(\n sessionId,\n parsed?.index ?? 0,\n frontier,\n )\n return {\n snapshot: parsed\n ? { index: parsed.index, state: parsed.state }\n : null,\n events,\n }\n })\n },\n\n async putSnapshot(sessionId, reducerName, index, state) {\n await wrap(async () => {\n await evalScript(\n PUT_SNAPSHOT_LUA,\n [snapKey(sessionId, reducerName)],\n [index, json({ index, state, updatedAt: clock.now().getTime() })],\n )\n })\n },\n\n presence: {\n async set(ns, participant, values, meta) {\n return wrap(async () => {\n const entries = Object.entries(values)\n if (entries.length === 0) return null\n const atMs = meta.at.getTime()\n // Expiry anchors on the injected storage clock — the sender's\n // `a` stamp orders writes, the `e` stamp bounds their\n // lifetime, and the two clocks never mix.\n const expiresAtMs = clock.now().getTime() + meta.ttlMs\n // The hash field is JSON `[participant, field]`: both are\n // caller strings, so no separator character is\n // collision-safe — the JSON array is.\n const applied = (await evalScript(\n PRESENCE_SET_LUA,\n [presenceKey(ns)],\n [\n atMs,\n meta.ttlMs,\n entries.length,\n ...entries.flatMap(([field, value]) => [\n JSON.stringify([participant, field]),\n value === null\n ? ''\n : json({\n v: value,\n s: meta.seen,\n a: atMs,\n e: expiresAtMs,\n }),\n ]),\n ],\n )) as string[]\n if (applied.length === 0) return null\n // Field-keyed and caller-named — null-prototype, like every\n // presence map (see `nullProtoRecord`).\n const appliedValues: Record<string, unknown> = nullProtoRecord()\n for (const position of applied) {\n const [field, value] = entries[Number(position) - 1]!\n appliedValues[field] = structuredClone(value)\n }\n return {\n participant,\n values: appliedValues,\n seen: meta.seen,\n at: new Date(atMs),\n }\n })\n },\n\n async read(ns) {\n return wrap(async () => {\n const reply = (await evalScript(\n PRESENCE_READ_LUA,\n [presenceKey(ns)],\n [clock.now().getTime()],\n )) as string[]\n const rows: PresenceRow[] = []\n for (let i = 0; i + 1 < reply.length; i += 2) {\n const [participant, field] = JSON.parse(reply[i]!) as [\n string,\n string,\n ]\n const entry = JSON.parse(reply[i + 1]!) as {\n v: unknown\n s: number\n a: number\n e: number\n }\n rows.push({\n participant,\n field,\n value: entry.v,\n seen: entry.s,\n at: new Date(entry.a),\n expiresAt: new Date(entry.e),\n })\n }\n return rows\n })\n },\n },\n\n inspect: {\n async listSessions(inspectionOptions) {\n return wrap(async () => {\n const minimum =\n inspectionOptions.cursor === undefined\n ? `[${inspectionOptions.prefix}`\n : `(${inspectionOptions.cursor}`\n const candidates = (await call(\n 'ZRANGEBYLEX',\n sessionsKey,\n minimum,\n '+',\n 'LIMIT',\n 0,\n inspectionOptions.limit + 1,\n )) as string[]\n const sessionIds = candidates\n .filter((id) => id.startsWith(inspectionOptions.prefix))\n .slice(0, inspectionOptions.limit)\n\n const sessions = await Promise.all(\n sessionIds.map(async (sessionId) => {\n const rows = await readRange(sessionId, 0)\n const first = rows[0]!\n let updatedAt = first.createdAt\n let pendingCount = 0\n let failedCount = 0\n let attemptCount = 0\n let failureCount = 0\n for (const row of rows) {\n attemptCount += row.attemptCount\n failureCount += row.failureCount\n if (row.processedAt === null && row.failedAt === null) {\n pendingCount += 1\n }\n if (row.failedAt !== null) failedCount += 1\n for (const timestamp of [\n row.createdAt,\n row.firstClaimedAt,\n row.lastClaimedAt,\n row.lastFailedAt,\n row.processedAt,\n row.failedAt,\n ]) {\n if (timestamp && timestamp > updatedAt) updatedAt = timestamp\n }\n }\n return {\n sessionId,\n eventCount: rows.length,\n pendingCount,\n failedCount,\n attemptCount,\n failureCount,\n firstEventAt: first.createdAt,\n updatedAt,\n }\n }),\n )\n return {\n sessions: sessions.toSorted((a, b) =>\n a.sessionId.localeCompare(b.sessionId),\n ),\n cursor:\n candidates.length > inspectionOptions.limit &&\n candidates[inspectionOptions.limit]?.startsWith(\n inspectionOptions.prefix,\n )\n ? (sessionIds.at(-1) ?? null)\n : null,\n }\n })\n },\n async readEvents(sessionId, pageOptions) {\n return wrap(async () => {\n const currentFrontier = await readFrontier(sessionId)\n const throughIndex = Math.min(\n pageOptions.throughIndex ?? currentFrontier,\n currentFrontier,\n )\n const events = await readRangePage(\n sessionId,\n pageOptions.afterIndex,\n throughIndex,\n pageOptions.limit,\n )\n if (throughIndex > pageOptions.afterIndex && events.length === 0) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' ended before index ${throughIndex}`,\n )\n }\n for (let position = 0; position < events.length; position += 1) {\n if (\n events[position]!.index !==\n pageOptions.afterIndex + position + 1\n ) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' is not contiguous`,\n )\n }\n }\n return { events, throughIndex }\n })\n },\n async listSnapshots(sessionId) {\n return wrap(async () => {\n const start = `${prefix}:${sessionId}:snap:`\n let cursor = '0'\n const keys: string[] = []\n do {\n // oxlint-disable-next-line no-await-in-loop -- each SCAN cursor comes from the prior reply\n const reply = (await call(\n 'SCAN',\n cursor,\n 'MATCH',\n `${escapeGlob(start)}*`,\n 'COUNT',\n 100,\n )) as [string, string[]]\n cursor = reply[0]\n keys.push(...reply[1].filter((key) => key.startsWith(start)))\n } while (cursor !== '0')\n const snapshots = await Promise.all(\n keys.map(async (key) => {\n const raw = (await call('GET', key)) as string | null\n if (raw === null) return null\n const parsed = JSON.parse(raw) as {\n index: number\n updatedAt?: number\n }\n return {\n reducerName: key.slice(start.length),\n index: parsed.index,\n updatedAt: new Date(parsed.updatedAt ?? 0),\n }\n }),\n )\n return snapshots\n .filter((snapshot) => snapshot !== null)\n .toSorted((a, b) => a.reducerName.localeCompare(b.reducerName))\n })\n },\n },\n\n async readRange(sessionId, afterIndex, throughIndex) {\n return wrap(() => readRange(sessionId, afterIndex, throughIndex))\n },\n\n async readEvents(sessionId, afterIndex) {\n return wrap(async () => {\n const entries = (await call(\n 'XRANGE',\n storeKey(sessionId),\n `${afterIndex + 1}-0`,\n '+',\n )) as StreamEntry[]\n return entries.map((entry) => toEvent(sessionId, entry))\n })\n },\n\n async markWatched(sessionId, ttlMs) {\n await wrap(() => call('SET', watchedKey(sessionId), '1', 'PX', ttlMs))\n },\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- export {}
@@ -1,6 +0,0 @@
1
- //#region src/testing.browser.ts
2
- /** experimental-a2/testing loads local capture files and is Node-only. */
3
- throw new Error("experimental-a2/testing is Node-only. Load captures in a local test or script.");
4
- //#endregion
5
-
6
- //# sourceMappingURL=testing.browser.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"testing.browser.js","names":[],"sources":["../src/testing.browser.ts"],"sourcesContent":["/** experimental-a2/testing loads local capture files and is Node-only. */\nthrow new Error(\n 'experimental-a2/testing is Node-only. Load captures in a local test or script.',\n)\nexport {}\n"],"mappings":";;AACA,MAAM,IAAI,MACR,gFACF"}
package/dist/testing.d.ts DELETED
@@ -1,32 +0,0 @@
1
- import { a as EventDefs, n as Contract, p as Reducer, r as ContractEvent } from "./contract-jIfaR085.js";
2
- import { c as DevtoolsCapture, l as DevtoolsCaptureEvent } from "./devtools-J_jZ2vQf.js";
3
- //#region src/testing.d.ts
4
- type CaptureSource = string | URL;
5
- type LoadedCapture<D extends EventDefs> = {
6
- /** The verified capture, including its footer and operational records. */
7
- readonly capture: DevtoolsCapture;
8
- /** The contract used to validate and type the captured event history. */
9
- readonly contract: Contract<D>;
10
- /** Contract-typed public events with `createdAt` revived to `Date`. */
11
- readonly events: readonly ContractEvent<D>[];
12
- /** Unmodified wire events, including dispatch and failure metadata. */
13
- readonly rawEvents: readonly DevtoolsCaptureEvent[];
14
- };
15
- type ReplayCaptureOptions = {
16
- /** Fold events through this log index, inclusive. Zero means no events. */
17
- throughIndex?: number;
18
- };
19
- type CaptureReplay<S> = {
20
- readonly state: S;
21
- /** The last event index actually folded, or zero for the initial state. */
22
- readonly index: number;
23
- };
24
- /** Validate an already-parsed capture against an application's contract. */
25
- declare function prepareCapture<D extends EventDefs>(capture: DevtoolsCapture, contract: Contract<D>): Promise<LoadedCapture<D>>;
26
- /** Load, integrity-check, and contract-type a `.a2log` file or URL. */
27
- declare function loadCapture<D extends EventDefs>(source: CaptureSource, contract: Contract<D>): Promise<LoadedCapture<D>>;
28
- /** Purely fold captured events through a reducer, optionally to a cutpoint. */
29
- declare function replayCapture<D extends EventDefs, S>(loaded: LoadedCapture<D>, reducer: Reducer<D, S>, options?: ReplayCaptureOptions): CaptureReplay<S>;
30
- //#endregion
31
- export { CaptureReplay, CaptureSource, LoadedCapture, ReplayCaptureOptions, loadCapture, prepareCapture, replayCapture };
32
- //# sourceMappingURL=testing.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"testing.d.ts","names":[],"sources":["../src/testing.ts"],"mappings":";;;KAkBY,yBAAyB;KAEzB,cAAc,UAAU;;WAEzB,SAAS;;WAET,UAAU,SAAS;;WAEnB,iBAAiB,cAAc;;WAE/B,oBAAoB;;KAGnB;;EAEV;;KAGU,cAAc;WACf,OAAO;;WAEP;;;iBA6CW,eAAe,UAAU,WAC7C,SAAS,iBACT,UAAU,SAAS,KAClB,QAAQ,cAAc;;iBA0DH,YAAY,UAAU,WAC1C,QAAQ,eACR,UAAU,SAAS,KAClB,QAAQ,cAAc;;iBAeT,cAAc,UAAU,WAAW,GACjD,QAAQ,cAAc,IACtB,SAAS,QAAQ,GAAG,IACpB,UAAS,uBACR,cAAc"}
package/dist/testing.js DELETED
@@ -1,103 +0,0 @@
1
- import { n as validateSync } from "./validate-XKT4FSNn.js";
2
- import { l as parseDevtoolsCapture, u as verifyDevtoolsCapture } from "./devtools-kJJaORn-.js";
3
- import { readFile } from "node:fs/promises";
4
- import { fileURLToPath } from "node:url";
5
- //#region src/testing.ts
6
- /**
7
- * Offline helpers for loading a captured A2 session and replaying its events
8
- * through the application's current reducers. This module never constructs a
9
- * server or dispatches handlers.
10
- */
11
- const captureError = (message) => /* @__PURE__ */ new TypeError(`invalid A2 capture: ${message}`);
12
- const typedEvents = (capture, contract) => {
13
- const events = [];
14
- for (const event of capture.events) {
15
- const schema = Object.hasOwn(contract.events, event.type) ? contract.events[event.type] : void 0;
16
- if (schema === void 0) throw captureError(`contract '${contract.name}' has no event type '${event.type}' at index ${event.index}`);
17
- const result = validateSync(schema, structuredClone(event.payload), `captured event '${event.type}'`);
18
- if (result.issues !== void 0) throw captureError(`event #${event.index} '${event.type}' has an invalid payload: ${result.issues.map((issue) => issue.message).join("; ")}`);
19
- events.push({
20
- id: event.id,
21
- type: event.type,
22
- payload: result.value,
23
- index: event.index,
24
- sessionId: event.sessionId,
25
- createdAt: new Date(event.createdAt)
26
- });
27
- }
28
- return events;
29
- };
30
- /** Validate an already-parsed capture against an application's contract. */
31
- async function prepareCapture(capture, contract) {
32
- await verifyDevtoolsCapture(capture);
33
- if (capture.contract !== contract.name) throw captureError(`expected contract '${contract.name}', received '${capture.contract}'`);
34
- return {
35
- capture,
36
- contract,
37
- events: typedEvents(capture, contract),
38
- rawEvents: capture.events
39
- };
40
- }
41
- const displayUrl = (url) => {
42
- if (url.protocol === "data:") return "data: URL";
43
- const safe = new URL(url);
44
- safe.username = "";
45
- safe.password = "";
46
- safe.search = "";
47
- safe.hash = "";
48
- return safe.href;
49
- };
50
- const fetchCapture = async (url) => {
51
- const location = displayUrl(url);
52
- let response;
53
- try {
54
- response = await fetch(url);
55
- } catch {
56
- throw new Error(`failed to load A2 capture from '${location}'`);
57
- }
58
- if (!response.ok) throw new Error(`failed to load A2 capture from '${location}': ${response.status} ${response.statusText}`);
59
- return new Uint8Array(await response.arrayBuffer());
60
- };
61
- const readCapture = async (source) => {
62
- if (source instanceof URL) return source.protocol === "file:" ? readFile(fileURLToPath(source)) : fetchCapture(source);
63
- if (/^[a-z]:[\\/]/i.test(source)) return readFile(source);
64
- let url;
65
- try {
66
- url = new URL(source);
67
- } catch {
68
- return readFile(source);
69
- }
70
- return readCapture(url);
71
- };
72
- /** Load, integrity-check, and contract-type a `.a2log` file or URL. */
73
- async function loadCapture(source, contract) {
74
- const encoded = await readCapture(source);
75
- return prepareCapture(parseDevtoolsCapture(encoded), contract);
76
- }
77
- const cloneInitial = (initialState) => {
78
- try {
79
- return structuredClone(initialState);
80
- } catch {
81
- return initialState;
82
- }
83
- };
84
- /** Purely fold captured events through a reducer, optionally to a cutpoint. */
85
- function replayCapture(loaded, reducer, options = {}) {
86
- const { throughIndex } = options;
87
- if (throughIndex !== void 0 && (!Number.isInteger(throughIndex) || throughIndex < 0)) throw new TypeError("throughIndex must be a non-negative integer");
88
- let state = cloneInitial(reducer.initialState);
89
- let index = 0;
90
- for (const event of loaded.events) {
91
- if (throughIndex !== void 0 && event.index > throughIndex) break;
92
- state = reducer.fold(state, event);
93
- index = event.index;
94
- }
95
- return {
96
- state,
97
- index
98
- };
99
- }
100
- //#endregion
101
- export { loadCapture, prepareCapture, replayCapture };
102
-
103
- //# sourceMappingURL=testing.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"testing.js","names":[],"sources":["../src/testing.ts"],"sourcesContent":["/**\n * Offline helpers for loading a captured A2 session and replaying its events\n * through the application's current reducers. This module never constructs a\n * server or dispatches handlers.\n */\n\nimport { readFile } from 'node:fs/promises'\nimport { fileURLToPath } from 'node:url'\nimport type { Contract, ContractEvent, EventDefs } from './contract.ts'\nimport {\n parseDevtoolsCapture,\n verifyDevtoolsCapture,\n type DevtoolsCapture,\n type DevtoolsCaptureEvent,\n} from './devtools.ts'\nimport type { Reducer } from './reducer.ts'\nimport { validateSync } from './validate.ts'\n\nexport type CaptureSource = string | URL\n\nexport type LoadedCapture<D extends EventDefs> = {\n /** The verified capture, including its footer and operational records. */\n readonly capture: DevtoolsCapture\n /** The contract used to validate and type the captured event history. */\n readonly contract: Contract<D>\n /** Contract-typed public events with `createdAt` revived to `Date`. */\n readonly events: readonly ContractEvent<D>[]\n /** Unmodified wire events, including dispatch and failure metadata. */\n readonly rawEvents: readonly DevtoolsCaptureEvent[]\n}\n\nexport type ReplayCaptureOptions = {\n /** Fold events through this log index, inclusive. Zero means no events. */\n throughIndex?: number\n}\n\nexport type CaptureReplay<S> = {\n readonly state: S\n /** The last event index actually folded, or zero for the initial state. */\n readonly index: number\n}\n\nconst captureError = (message: string): TypeError =>\n new TypeError(`invalid A2 capture: ${message}`)\n\nconst typedEvents = <D extends EventDefs>(\n capture: DevtoolsCapture,\n contract: Contract<D>,\n): ContractEvent<D>[] => {\n const events: ContractEvent<D>[] = []\n for (const event of capture.events) {\n const schema = Object.hasOwn(contract.events, event.type)\n ? contract.events[event.type]\n : undefined\n if (schema === undefined) {\n throw captureError(\n `contract '${contract.name}' has no event type '${event.type}' at index ${event.index}`,\n )\n }\n const result = validateSync(\n schema,\n structuredClone(event.payload),\n `captured event '${event.type}'`,\n )\n if (result.issues !== undefined) {\n throw captureError(\n `event #${event.index} '${event.type}' has an invalid payload: ${result.issues\n .map((issue) => issue.message)\n .join('; ')}`,\n )\n }\n events.push({\n id: event.id,\n type: event.type,\n payload: result.value,\n index: event.index,\n sessionId: event.sessionId,\n createdAt: new Date(event.createdAt),\n } as ContractEvent<D>)\n }\n return events\n}\n\n/** Validate an already-parsed capture against an application's contract. */\nexport async function prepareCapture<D extends EventDefs>(\n capture: DevtoolsCapture,\n contract: Contract<D>,\n): Promise<LoadedCapture<D>> {\n await verifyDevtoolsCapture(capture)\n if (capture.contract !== contract.name) {\n throw captureError(\n `expected contract '${contract.name}', received '${capture.contract}'`,\n )\n }\n return {\n capture,\n contract,\n events: typedEvents(capture, contract),\n rawEvents: capture.events,\n }\n}\n\nconst displayUrl = (url: URL): string => {\n if (url.protocol === 'data:') return 'data: URL'\n const safe = new URL(url)\n safe.username = ''\n safe.password = ''\n safe.search = ''\n safe.hash = ''\n return safe.href\n}\n\nconst fetchCapture = async (url: URL): Promise<Uint8Array> => {\n const location = displayUrl(url)\n let response: Response\n try {\n response = await fetch(url)\n } catch {\n throw new Error(`failed to load A2 capture from '${location}'`)\n }\n if (!response.ok) {\n throw new Error(\n `failed to load A2 capture from '${location}': ${response.status} ${response.statusText}`,\n )\n }\n return new Uint8Array(await response.arrayBuffer())\n}\n\nconst readCapture = async (source: CaptureSource): Promise<Uint8Array> => {\n if (source instanceof URL) {\n return source.protocol === 'file:'\n ? readFile(fileURLToPath(source))\n : fetchCapture(source)\n }\n if (/^[a-z]:[\\\\/]/i.test(source)) return readFile(source)\n let url: URL | undefined\n try {\n url = new URL(source)\n } catch {\n return readFile(source)\n }\n return readCapture(url)\n}\n\n/** Load, integrity-check, and contract-type a `.a2log` file or URL. */\nexport async function loadCapture<D extends EventDefs>(\n source: CaptureSource,\n contract: Contract<D>,\n): Promise<LoadedCapture<D>> {\n const encoded = await readCapture(source)\n const capture = parseDevtoolsCapture(encoded)\n return prepareCapture(capture, contract)\n}\n\nconst cloneInitial = <S>(initialState: S): S => {\n try {\n return structuredClone(initialState)\n } catch {\n return initialState\n }\n}\n\n/** Purely fold captured events through a reducer, optionally to a cutpoint. */\nexport function replayCapture<D extends EventDefs, S>(\n loaded: LoadedCapture<D>,\n reducer: Reducer<D, S>,\n options: ReplayCaptureOptions = {},\n): CaptureReplay<S> {\n const { throughIndex } = options\n if (\n throughIndex !== undefined &&\n (!Number.isInteger(throughIndex) || throughIndex < 0)\n ) {\n throw new TypeError('throughIndex must be a non-negative integer')\n }\n\n let state = cloneInitial(reducer.initialState)\n let index = 0\n for (const event of loaded.events) {\n if (throughIndex !== undefined && event.index > throughIndex) break\n state = reducer.fold(state, event)\n index = event.index\n }\n return { state, index }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"wire-BFQmSJ-9.js","names":[],"sources":["../src/wire.ts"],"sourcesContent":["/**\n * The wire format shared by experimental-a2/http (server side) 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 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":"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"}
@@ -1,229 +0,0 @@
1
- ---
2
- title: Devtools
3
- description: Mount one authenticated, read-only handler to inspect and export durable sessions from the browser, CLI, or a test.
4
- ---
5
-
6
- ## Mount the dashboard
7
-
8
- The devtools application is one HTTP handler. Give it every A2 server you want
9
- to inspect, then mount it on a route that matches the root and its child paths.
10
- In Next.js, an optional catch-all route does both:
11
-
12
- ```ts app/api/a2/devtools/[[...path]]/route.ts
13
- import { createDevtools } from 'experimental-a2/devtools/server'
14
- import { billingServer, ordersServer } from '@/server'
15
-
16
- const devtools = createDevtools({
17
- servers: [ordersServer, billingServer],
18
- authorize: async (_request) => {
19
- // your authentication and authorization check:
20
- // return await canInspectA2(_request)
21
- return process.env.NODE_ENV === 'development'
22
- },
23
- })
24
-
25
- export const GET = devtools.handler()
26
- ```
27
-
28
- Open `/api/a2/devtools`. There is no client factory, provider, React component,
29
- or separate datastore. The handler serves the browser application itself and
30
- reads the same log as the servers.
31
-
32
- The initial HTML contains the first page of the selected contract's sessions
33
- and the selected session's durable detail. The browser starts from that
34
- bootstrap data without a loading screen or an initial API request waterfall,
35
- then takes over navigation and live updates.
36
-
37
- The selected contract and session stay in the URL. Refreshing preserves the
38
- view, links open the same session, and browser back and forward navigation work.
39
-
40
- The mounted route is the policy boundary for the complete Devtools surface.
41
- `authorize` runs before the browser application, JSON inspection endpoints,
42
- live stream, capture export, and CLI requests. A2 does not maintain a second
43
- credential store. Use your application's authentication and authorization at
44
- this route, and give Devtools the same protection as the durable data it reads.
45
-
46
- Omit `authorize` only for local development. An unguarded handler is available
47
- when `NODE_ENV` is exactly `development`. It returns 404 when the value is
48
- anything else, including unset. In a deployed dashboard, provide your
49
- application's access check. Only a literal `true` grants access. Returning
50
- `false` hides the route with a 404. Returning a `Response` supports a redirect
51
- or authentication challenge.
52
-
53
- ## Inspect from the CLI
54
-
55
- The `a2 devtools` CLI reads the same mounted route. Set its URL once, then list
56
- contracts and sessions or inspect one session:
57
-
58
- ```bash terminal
59
- export A2_DEVTOOLS_URL='https://example.com/api/a2/devtools'
60
- export A2_DEVTOOLS_TOKEN="$DEVTOOLS_BEARER_TOKEN"
61
-
62
- a2 devtools contracts
63
- a2 devtools sessions --contract orders --limit 50
64
- a2 devtools show --contract orders --session order-42
65
- a2 devtools show --contract orders --session order-42 --format json
66
- ```
67
-
68
- `A2_DEVTOOLS_TOKEN` becomes an `Authorization: Bearer` header. For a different
69
- environment variable, pass `--bearer-env DEVTOOLS_TOKEN`. Custom headers use
70
- `--header-env HEADER=ENV`, which is repeatable. The CLI accepts credential
71
- values only through environment variables. It rejects literal token and header
72
- flags, credentials embedded in `--url`, and URLs with query strings. Requests
73
- do not follow redirects, so an authentication redirect cannot forward those
74
- headers to another origin.
75
-
76
- Human-readable output is the default. `--format json` emits one JSON value and
77
- `--format ndjson` emits one record per line. Session listing also accepts
78
- `--cursor` for the next page. `--url` overrides `A2_DEVTOOLS_URL`.
79
-
80
- ## Export and check a capture
81
-
82
- Export writes one exact `.a2log` capture. `check` verifies a live capture and
83
- can assert operational conditions. `verify` checks a saved file without a
84
- server URL or credentials:
85
-
86
- ```bash terminal
87
- a2 devtools export \
88
- --contract orders \
89
- --session order-42 \
90
- --output artifacts/order-42.a2log
91
-
92
- a2 devtools check \
93
- --contract orders \
94
- --session order-42 \
95
- --settled \
96
- --no-dead-letters \
97
- --no-caught-failures \
98
- --max-redispatches 0
99
-
100
- a2 devtools verify artifacts/order-42.a2log
101
- ```
102
-
103
- An export contains a versioned manifest, every durable event in log order,
104
- snapshot metadata, and a footer with counts and a SHA-256 digest. Event records
105
- retain payloads, causal edges, lanes, dispatch attempts, failures, claims, and
106
- completion metadata. Snapshot state is not included. The CLI checks the digest
107
- and structure before writing the capture. It refuses to replace an existing
108
- file unless you pass `--force`, and creates capture files with owner-only
109
- permissions (`0600`).
110
-
111
- The CLI and browser read large sessions in bounded pages. The first page
112
- freezes a finite event frontier, so messages appended during the download do
113
- not extend it. Export includes every event through that frontier. If a store or
114
- provider returns a gap or ends early, the read fails instead of producing a
115
- partial capture with a valid footer.
116
-
117
- The capture is canonical UTF-8 NDJSON. If a durable payload contains a value
118
- that JSON would coerce or omit, export fails instead of signing a changed
119
- representation. Keep event payloads JSON-compatible when they need to be
120
- portable.
121
-
122
- Capture verification and failed `check` assertions exit with status 2. Usage,
123
- network, and filesystem errors exit with status 1. The live commands support
124
- the same human, JSON, and NDJSON output modes, which makes `check` useful in CI.
125
-
126
- An exact capture is sensitive production data. It can contain user payloads,
127
- AI prompts and tool results, provider metadata, usage, and failure messages
128
- when those values exist in the event log. Route authorization controls who can
129
- download a capture. It does not redact or encrypt the downloaded file. Store,
130
- share, and expire `.a2log` files under the same policy as the source log.
131
-
132
- ## Replay reducer state offline
133
-
134
- `experimental-a2/testing` loads a capture in Node.js, verifies it against the
135
- current contract, and folds its events through a reducer:
136
-
137
- ```ts test/replay-order.ts
138
- import { loadCapture, replayCapture } from 'experimental-a2/testing'
139
- import { orders } from '@/contracts'
140
- import { ordersReducer } from '@/reducer'
141
-
142
- export async function replayOrder(path: string) {
143
- const loaded = await loadCapture(path, orders)
144
-
145
- const afterFirstEvent = replayCapture(loaded, ordersReducer, {
146
- throughIndex: 1,
147
- })
148
- const current = replayCapture(loaded, ordersReducer)
149
-
150
- return { afterFirstEvent, current }
151
- }
152
- ```
153
-
154
- `loadCapture()` verifies the capture digest and identity, validates every event
155
- type and payload through the current contract schemas, and revives the public
156
- `createdAt` timestamp as `Date`. The loaded value also keeps `rawEvents` with
157
- the exact wire payloads and operational metadata.
158
-
159
- `throughIndex` is inclusive. Pass `0` to return the reducer's initial state at
160
- index 0. Replay always folds immutable events. Captured snapshots contain
161
- metadata only and are not used as reducer state.
162
-
163
- This is pure reducer replay. It does not construct a server, run handlers,
164
- invoke a model or tool, or repeat an external side effect. Test handler or model
165
- behavior with a fresh log and explicit test doubles instead.
166
-
167
- ## Causal forest
168
-
169
- Every stored `cause` connects a child to the event index and handler attempt
170
- whose `ctx.session.append` first persisted it. The dashboard builds that forest
171
- directly from the session log. It does not need a trace table or another write
172
- on the append path.
173
-
174
- A null cause appears as **origin unknown**. New top-level appends write null,
175
- but events written before causal metadata also read as null. The dashboard does
176
- not guess which one it is. If a cause points outside the visible log, the row
177
- keeps the stored edge and marks its parent unavailable.
178
-
179
- ## What the timeline means
180
-
181
- Each row is one durable event. Its bar begins at `createdAt`, when the event
182
- entered the log, and ends at `processedAt` or `failedAt`. A pending event stays
183
- open. Markers show the operation boundaries A2 stores:
184
-
185
- - first and latest dispatch from `firstClaimedAt` and `lastClaimedAt`
186
- - latest caught failure from `lastFailedAt` and `lastFailedAttempt`
187
- - completion from `processedAt` and `processedByAttempt`
188
- - dead-lettering from `failedAt`
189
-
190
- `attemptCount` counts every durable dispatch claim. `failureCount` counts only
191
- caught handler failures. A hard crash can increase the first without
192
- increasing the second. The dashboard calls that difference a dispatch without
193
- an outcome because it may be active work, a hard crash, or a superseded
194
- attempt. It does not claim to know which one occurred.
195
-
196
- Migrated events can have an `attemptCount` without a `firstClaimedAt`. The
197
- dashboard shows the first dispatch time as unknown instead of moving it to
198
- `createdAt` or `lastClaimedAt`.
199
-
200
- The bar is event lifetime, not handler execution time. A2 stores lifecycle
201
- summaries, not one span row for every attempt. Time spent waiting for a lane,
202
- running a handler, holding a claim, or waiting for the scheduler remains one honest
203
- interval.
204
-
205
- An event type without a handler is complete at append. Its bar has no waiting
206
- time or dispatch marker, and its attempt count stays at zero.
207
-
208
- Snapshots appear as their reducer name, event frontier, and last update time.
209
- The dashboard does not send cached snapshot state to the browser.
210
-
211
- ## Live updates
212
-
213
- The selected session opens an SSE connection to the same handler. The stream
214
- sends invalidations, not a second copy of state. On each invalidation the
215
- browser reads the durable session again. A disconnected browser can miss every
216
- frame and still become correct immediately after reconnecting.
217
-
218
- While work is pending, the handler checks processing metadata quickly. It
219
- backs off after the session settles and emits heartbeats for intermediaries.
220
- Only sessions someone is viewing have a live inspection loop.
221
-
222
- ## Custom stores
223
-
224
- The memory, SQLite, Postgres, and Redis adapters provide the optional read-only
225
- inspection interface. A custom `A2Store` can implement `inspect.listSessions`
226
- and `inspect.listSnapshots` to appear in the dashboard. Add
227
- `inspect.readEvents` for bounded storage-native event pages. Without it, A2
228
- reads the complete session for each page and slices it in memory. Stores without
229
- inspection continue to work normally; their devtools endpoint returns 501.
package/src/cli-bin.ts DELETED
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { runCli } from './cli.ts'
4
-
5
- process.exitCode = await runCli(process.argv.slice(2))