experimental-a2 0.22.0 → 0.23.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.
@@ -1 +1 @@
1
- {"version":3,"file":"store-redis-notify-CsJw_hfD.js","names":[],"sources":["../src/presence-recovery.ts","../src/store-redis-core.ts","../src/store-redis-notify.ts"],"sourcesContent":["import type { PresencePatch } from './contract.ts'\nimport { presenceTombstones } from './presence-tombstones.ts'\nimport type { PresenceRow } from './store.ts'\n\ntype Field = Pick<PresenceRow, 'participant' | 'field' | 'seen' | 'at'>\nconst key = ({\n participant,\n field,\n}: Pick<Field, 'participant' | 'field'>): string =>\n JSON.stringify([participant, field])\n\nexport function presenceRecovery({\n read,\n deliver,\n isActive,\n}: {\n read: () => Promise<PresenceRow[]>\n deliver: (patch: PresencePatch) => void\n isActive: () => boolean\n}): {\n read(): Promise<PresenceRow[]>\n reconnect(): Promise<void>\n update(patch: PresencePatch): void\n} {\n const fields = new Map<string, Field>()\n const deleted = presenceTombstones()\n const reading = new Set<Set<string>>()\n let generation = 0\n let latest: Promise<PresenceRow[]> | undefined\n\n const remember = ({ participant, field, seen, at }: Field): void => {\n const id = key({ participant, field })\n const previous = fields.get(id)\n if (\n !previous ||\n previous.at.getTime() < at.getTime() ||\n (previous.at.getTime() === at.getTime() && previous.seen !== seen)\n )\n fields.set(id, { participant, field, seen, at })\n }\n const snapshot = async ({\n reconcile,\n }: {\n reconcile: boolean\n }): Promise<PresenceRow[]> => {\n const run = generation\n const before = new Map(fields)\n const changed = new Set<string>()\n reading.add(changed)\n try {\n const rows = await read()\n if (!isActive()) return rows\n if (run !== generation) return latest!\n // A row older than its field's deletion must not resurrect it.\n const current = rows.filter((row) => !deleted.outranks(row))\n const live = new Set<string>()\n for (const row of current) {\n const id = key(row)\n live.add(id)\n if (changed.has(id)) continue\n deleted.drop(row)\n remember(row)\n if (reconcile)\n deliver({\n participant: row.participant,\n values: { [row.field]: row.value },\n seen: row.seen,\n at: row.at,\n })\n }\n if (reconcile) {\n for (const [id, prior] of before) {\n if (live.has(id) || changed.has(id) || fields.get(id) !== prior)\n continue\n fields.delete(id)\n deleted.keep(prior)\n deliver({\n participant: prior.participant,\n values: { [prior.field]: null },\n seen: prior.seen,\n at: prior.at,\n })\n }\n }\n return current\n } finally {\n reading.delete(changed)\n if (reading.size === 0) latest = undefined\n }\n }\n\n return {\n async read() {\n let pending = latest\n while (pending) {\n // oxlint-disable-next-line no-await-in-loop -- follow replacement reconnects before starting a fresh read\n await pending\n if (latest === pending) break\n pending = latest\n }\n return snapshot({ reconcile: false })\n },\n async reconnect() {\n generation += 1\n latest = snapshot({ reconcile: true })\n await latest\n },\n update(patch) {\n for (const [field, value] of Object.entries(patch.values)) {\n if (value === undefined) continue\n const write = { participant: patch.participant, field, at: patch.at }\n const id = key(write)\n const previous = fields.get(id)\n if (previous && previous.at.getTime() > patch.at.getTime()) continue\n if (deleted.outranks(write)) continue\n for (const changed of reading) changed.add(id)\n if (value === null) {\n fields.delete(id)\n deleted.keep(write)\n } else {\n deleted.drop(write)\n remember({ ...write, seen: patch.seen })\n }\n }\n },\n }\n}\n","/**\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. Stream\n * entries are immutable; dispatch bookkeeping lives on consumer cursors.\n * Every write path is one atomic Lua script.\n *\n * Every key of a session is `{p}:{\"<session id as JSON>\"}:<name>`, one hash\n * tag per session (`redisSessionKey`). Each script declares keys of one\n * session only and derives the rest, so on Upstash a script that sets\n * `allow-key-locking` locks only its session instead of the database. Redis\n * and Valkey reject that flag, so the store asks the server once whether it\n * accepts it.\n *\n * The cursor layout, per session:\n *\n * - `heads:{chunk}` holds one row per (head index, consumer) in hashes of\n * META_CHUNK_SIZE consecutive indexes: a head's attempt and failure\n * fields, its kind (`l`), its lane successor (`next`), its snapshot pins,\n * and, once the cursor moves past it, its completion record. A new head\n * is a new row, so moving a cursor resets its attempt fields for free,\n * and an event cursor costs one row, as a handled event did.\n * - `cursors` is one hash of every lane and log cursor: its head (`h`),\n * position (`p`), lane tail (`t`), or log types (`ty`). A lane's queue is\n * linked through the rows' `next` fields.\n * - `runnable` is a zset of every cursor with a head, scored by the head's\n * index, negated while the head is dead-lettered.\n * - `pins` counts, per snapshot, the heads that pin it.\n *\n * A cursor id names a cursor within its session: the consumer's byte\n * length, `:`, the consumer, then `#{index}` for an event cursor, `L{lane}`\n * for a lane cursor, or `G{lane}` for a log cursor. A clear supersedes every\n * head at or below its marker at once (the `runnable` set is dropped); lane and\n * log cursor records are reset lazily against the marker and swept by\n * reclaim.\n *\n * `{p}:schema` records the layout. A store claims it once, before its first\n * script, and refuses a prefix holding another. Keys of an unrecorded layout\n * have other names, so they are never read.\n *\n * Event ids are unique per session: `id:{event id}` holds the index an id\n * names, and leaves with its session. The conformance suite in\n * test/conformance is the executable contract, run against a real spawned\n * `redis-server`, and over REST behind a shim that applies Upstash's\n * key-locking rules.\n *\n * Timestamps and claim expiry come from the injected clock so tests can\n * time-travel against a real server.\n */\n\nimport type { PresencePatch } from './contract.ts'\nimport { A2Error, supersededAttemptError } from './errors.ts'\nimport { CLEAR_EVENT_TYPE, sessionClearedError } from './session-clear.ts'\nimport { idempotentReplay } from './idempotent-replay.ts'\nimport { NOTIFY_TIMINGS, nullProtoRecord } from './internal.ts'\nimport { retryableLazy } from './retryable-lazy.ts'\nimport { decodeCause } from './store-codec.ts'\nimport type { SerializedEvent } from './stream-buffer.ts'\nimport type {\n A2Store,\n Clock,\n CompleteAttemptResult,\n Event,\n EventCause,\n EventTarget,\n HeadDispatch,\n HeadEvent,\n HeadRef,\n IdSource,\n StoreClaimAvailableResult,\n StoreConsumer,\n StoredCursor,\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 /**\n * Issues a command whose bulk replies arrive as raw bytes. Live feed\n * catch-up pages use it to decode only the fields a row needs.\n */\n callBytes?: RedisCall\n clock: Clock\n ids: IdSource\n keyPrefix: string\n presenceSource: string\n readStream?: RedisStreamRead\n}\n\n/** Stream entries from `from` onward, in nonempty ordered batches. */\nexport type RedisStreamRead = (options: {\n key: string\n from: string\n signal: AbortSignal\n}) => AsyncGenerator<StreamEntry[], void>\n\nexport type RedisStoreCore = Omit<A2Store, 'stream' | 'presence'> & {\n /**\n * The storage half of the presence capability, shared by both\n * transports. `set` returns the applied patch for local loopback, with\n * the publish to remote subscribers already under way, or null when\n * every field lost the LWW race.\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<{ patch: PresencePatch; published: Promise<void> } | null>\n read(ns: string): Promise<PresenceRow[]>\n }\n /**\n * The latest clear marker's index, or 0. Entries below it are an old\n * lifetime's until reclaim deletes them, so a feed starts no lower.\n */\n clearIndex(sessionId: string): Promise<number>\n /** XRANGE pages — the full-row catch-up read. */\n readRange(\n sessionId: string,\n afterIndex: number,\n throughIndex?: number,\n ): Promise<StoredEvent[]>\n /** One bounded XRANGE page of public events for live feeds. */\n readEvents(sessionId: string, afterIndex: number): Promise<Event[]>\n /**\n * One bounded XRANGE page of catch-up rows for a live feed without a\n * streaming reader, payloads left as stored JSON. The reply is held whole,\n * so a page retains up to `FEED_PAGE_SIZE` entries.\n */\n readFeedPage(\n sessionId: string,\n afterIndex: number,\n ): Promise<SerializedEvent[]>\n /** Catch-up events after `afterIndex`, in nonempty ordered batches. */\n streamEvents?: (options: {\n sessionId: string\n afterIndex: number\n signal: AbortSignal\n }) => AsyncGenerator<SerializedEvent[], void>\n /**\n * Events from `fromIndex` through `throughIndex` or the newest entry in\n * one newest-first page, or null when the range doesn't fit in one or\n * starts below the latest clear.\n */\n readTail(\n sessionId: string,\n fromIndex: number,\n throughIndex?: number,\n ): Promise<Event[] | null>\n}\n\nexport const READ_PAGE_SIZE = 256\nexport const FEED_PAGE_SIZE = 1000\nconst READ_CONCURRENCY = 16\n\n/** The key layout this core writes; any other is rejected. */\nexport const REDIS_LAYOUT = 'tagged-1'\n\n/**\n * Upstash runs a script under a database-wide lock unless it opts into\n * locking only its keys' hash tags. Redis and Valkey reject the flag, and\n * have no such lock to opt out of: one thread runs every command.\n */\nconst KEY_LOCKING = '#!lua flags=allow-key-locking'\nconst READ_ONLY_KEY_LOCKING = '#!lua flags=no-writes,allow-key-locking'\nconst SHEBANG_REJECTED = /shebang|unexpected symbol near '#'/i\n\nconst META_CHUNK_SIZE = 1000\n\n/** A head row's dispatch columns, in the order scripts return them. */\nconst HEAD_COLUMNS = `{ 'd', 'fc', 'lc', 'ch', 'ce', 'a', 'lf', 'lfa', 'e', 'f' }`\n\n// ── Lua scripts (each runs atomically) ───────────────────────────────\n\n/**\n * Issues a variadic command in calls of at most 1000 arguments, since Lua's\n * `unpack` is bounded by its stack, and concatenates table replies. An even\n * slice size keeps HSET's field/value pairs together.\n */\nconst SLICED_LUA = `\nlocal function sliced(command, key, args)\n local results = {}\n for first = 1, #args, 1000 do\n local last = math.min(first + 999, #args)\n local reply\n if key then\n reply = redis.call(command, key, unpack(args, first, last))\n else\n reply = redis.call(command, unpack(args, first, last))\n end\n if type(reply) == 'table' then\n for _, value in ipairs(reply) do results[#results + 1] = value end\n end\n end\n return results\nend\n`\n\n/**\n * Upstash bills every call a script makes, so scripts read the counter,\n * clear marker, typed-type index, and event-id owners with one MGET\n * (`read_bounds`), read rows with one HMGET per chunk\n * (`read_rows`), and buffer hash writes (`put`, `drop`) into one HSET and\n * one HDEL per hash (`flush`). Buffered writes are invisible to reads until\n * flushed, so a script must not read a field it has buffered. Every script\n * binds KEYS[1] to the session's counter and derives the session's other\n * keys, which share its hash tag.\n */\nconst BASE_LUA = `${SLICED_LUA}\nlocal COUNTER = KEYS[1]\nlocal SESSION = string.sub(COUNTER, 1, -6)\nlocal LOG, CLEAR = SESSION .. 'log', SESSION .. 'clear'\nlocal META, READY, CURSORS = SESSION .. 'heads', SESSION .. 'runnable', SESSION .. 'cursors'\nlocal PIN_COUNTS, TYPED = SESSION .. 'pins', SESSION .. 'typed'\nlocal CLEAR_TYPE = '${CLEAR_EVENT_TYPE}'\nlocal total, boundary = 0, 0\n-- The event types typed log cursors read: type → { from, last }. The type's\n-- index holds every event of it above \\`from\\`, and no event of it lies above\n-- \\`last\\`.\nlocal typed, typed_dirty = {}, false\n-- One key per event id holds the index it names in this session. The\n-- \\`id:\\` namespace keeps a raw id from spelling another session key.\nlocal function id_key(id) return SESSION .. 'id:' .. id end\n-- Reads the counter, the latest clear marker, the typed types, and the\n-- owners of \\`ids\\` in one MGET. Returns each id's index, or false.\nlocal function read_bounds(ids)\n local keys = { COUNTER, CLEAR, TYPED }\n for _, id in ipairs(ids or {}) do keys[#keys + 1] = id_key(id) end\n local values = sliced('MGET', nil, keys)\n total = tonumber(values[1] or values[2] or '0')\n boundary = tonumber(values[2] or '0')\n if values[3] then typed = cjson.decode(values[3]) end\n local owners = {}\n for i = 4, #keys do owners[i - 3] = values[i] end\n return owners\nend\n-- Event-id owners from read_bounds: an id recorded below the latest clear\n-- is free.\nlocal function owners_of(values)\n local owners = {}\n for i = 1, #values do\n local owner = values[i]\n if owner and tonumber(owner) < boundary then owner = false end\n owners[i] = owner\n end\n return owners\nend\nlocal function entry_index(id) return tonumber(string.match(id, '^(%d+)')) end\nlocal function cid_of(consumer, tag)\n return tostring(string.len(consumer)) .. ':' .. consumer .. tag\nend\nlocal function parse_cid(cid)\n local colon = string.find(cid, ':', 1, true)\n local n = tonumber(string.sub(cid, 1, colon - 1))\n return string.sub(cid, colon + 1, colon + n), string.sub(cid, colon + n + 1)\nend\n-- A cursor's rows: its consumer, then '#' for an event cursor or 'L' .. lane\n-- for a lane or log cursor, as requests name it.\nlocal function row_of(consumer, tag)\n local kind = string.sub(tag, 1, 1)\n if kind == '#' then return cid_of(consumer, '#') end\n if kind == 'G' then return cid_of(consumer, 'L' .. string.sub(tag, 2)) end\n return cid_of(consumer, tag)\nend\nlocal function chunk_key(idx)\n return META .. ':' .. tostring(math.floor((idx - 1) / ${META_CHUNK_SIZE}))\nend\nlocal function row_name(idx, row, suffix)\n return tostring(idx) .. ':' .. row .. ':' .. suffix\nend\n-- Rows of (index, row) refs, aligned with refs; one HMGET per chunk.\nlocal function read_rows(refs, suffixes)\n local groups, order = {}, {}\n for position, ref in ipairs(refs) do\n local key = chunk_key(ref[1])\n local group = groups[key]\n if not group then\n group = { positions = {}, names = {} }\n groups[key] = group\n order[#order + 1] = key\n end\n group.positions[#group.positions + 1] = position\n for _, suffix in ipairs(suffixes) do\n group.names[#group.names + 1] = row_name(ref[1], ref[2], suffix)\n end\n end\n local rows = {}\n for _, key in ipairs(order) do\n local group = groups[key]\n local values = sliced('HMGET', key, group.names)\n for n, position in ipairs(group.positions) do\n local row = {}\n for column = 1, #suffixes do row[column] = values[(n - 1) * #suffixes + column] end\n rows[position] = row\n end\n end\n return rows\nend\nlocal writes, write_order, deletes, delete_order = {}, {}, {}, {}\nlocal function put(key, field, value)\n local fields = writes[key]\n if not fields then\n fields = {}\n writes[key] = fields\n write_order[#write_order + 1] = key\n end\n fields[#fields + 1] = field\n fields[#fields + 1] = value\nend\nlocal function drop(key, field)\n local fields = deletes[key]\n if not fields then\n fields = {}\n deletes[key] = fields\n delete_order[#delete_order + 1] = key\n end\n fields[#fields + 1] = field\nend\nlocal function flush()\n for _, key in ipairs(write_order) do sliced('HSET', key, writes[key]) end\n for _, key in ipairs(delete_order) do sliced('HDEL', key, deletes[key]) end\n writes, write_order, deletes, delete_order = {}, {}, {}, {}\nend\nlocal function put_row(idx, row, suffix, value)\n put(chunk_key(idx), row_name(idx, row, suffix), value)\nend\nlocal function drop_row(idx, row, suffix)\n drop(chunk_key(idx), row_name(idx, row, suffix))\nend\n-- A request names a cursor by tag: '#' for an event cursor, 'L' .. lane for a\n-- lane or log cursor. A claimed row records its kind: '#', or the cursor's tag.\nlocal function owns(kind, tag)\n if not kind then return false end\n if tag == '#' then return kind == '#' end\n return kind == tag or kind == 'G' .. string.sub(tag, 2)\nend\nlocal function cid_for(consumer, kind, idx)\n if kind == '#' then return cid_of(consumer, '#' .. tostring(idx)) end\n return cid_of(consumer, kind)\nend\n-- One type's index, named by the type's bytes in hex so no type can spell\n-- another key of the session.\nlocal function type_key(name)\n return SESSION .. 'type:' .. (string.gsub(name, '.', function(c)\n return string.format('%02x', string.byte(c))\n end))\nend\nlocal ready_changes, ready_order = {}, {}\nlocal function ready_set(cid, score)\n if ready_changes[cid] == nil then ready_order[#ready_order + 1] = cid end\n ready_changes[cid] = score\nend\nlocal function flush_ready()\n local adds, removes = {}, {}\n for _, cid in ipairs(ready_order) do\n local score = ready_changes[cid]\n if score then\n adds[#adds + 1] = score\n adds[#adds + 1] = cid\n else\n removes[#removes + 1] = cid\n end\n end\n sliced('ZADD', READY, adds)\n sliced('ZREM', READY, removes)\n ready_changes, ready_order = {}, {}\nend\nlocal function release_pins(idx, row, pins)\n if not pins or pins == '' then return end\n for _, ref in ipairs(cjson.decode(pins)) do\n local count = redis.call('HINCRBY', PIN_COUNTS, ref, -1)\n if count <= 0 then\n redis.call('HDEL', PIN_COUNTS, ref)\n local checkpoint = cjson.decode(ref)\n redis.call('ZREMRANGEBYSCORE', checkpoint[1], checkpoint[2], checkpoint[2])\n end\n end\n put_row(idx, row, 'pins', '')\nend\n`\n\n/**\n * Cursor movement for scripts that add events or move cursors. Lane and log\n * cursor records are read once per script (`load_states`) and written back\n * once (`flush_states`). A record whose head lies at or below the latest clear\n * marker is idle, and a log cursor counts from the marker.\n *\n * A typed log cursor finds its next head in its types' indexes, one bounded\n * lookup per type, skipped when `typed` shows no newer event of it. When an\n * index does not reach back far enough yet (a typed backfill, or a type a\n * cursor newly reads), the lookup returns the type, and the script returns\n * `{ 'index', type, after }` before writing anything: the caller indexes\n * that type down to `after` (INDEX_TYPE_LUA, one page per call) and runs the\n * operation again.\n */\nconst CURSOR_LUA = `\nlocal served_all, served_set, served_logs = true, {}, {}\nlocal pending_fast = false\nlocal function types_of(raw)\n if not raw or raw == '*' then return nil end\n local set = {}\n for _, name in ipairs(cjson.decode(raw)) do set[name] = true end\n return set\nend\nlocal function takes(types, kind)\n return kind ~= CLEAR_TYPE and (types == nil or types[kind] == true)\nend\n-- The served consumers: '' for every consumer, else [[name] or\n-- [name, lane, typesJson or '*', backfill]].\nlocal function use_served(raw)\n if raw == '' then return end\n served_all = false\n for _, entry in ipairs(cjson.decode(raw)) do\n served_set[entry[1]] = true\n if entry[2] then\n served_logs[#served_logs + 1] = {\n name = entry[1], lane = entry[2], ty = entry[3],\n types = types_of(entry[3]), backfill = entry[4] == 1,\n }\n end\n end\nend\nlocal function served_has(name) return served_all or served_set[name] == true end\n-- Whether a served consumer has a live head. Newest heads come first: a\n-- removed consumer's backlog is older than the work a deployment serves.\nlocal function has_pending()\n if pending_fast then return '1' end\n if served_all then return redis.call('ZCOUNT', READY, 1, '+inf') > 0 and '1' or '0' end\n if next(served_set) == nil then return '0' end\n local page = redis.call('ZREVRANGEBYSCORE', READY, '+inf', 1, 'LIMIT', 0, 32)\n for _, cid in ipairs(page) do\n if served_set[(parse_cid(cid))] then return '1' end\n end\n if #page < 32 then return '0' end\n for _, cid in ipairs(redis.call('ZREVRANGEBYSCORE', READY, '+inf', 1, 'LIMIT', 32, -1)) do\n if served_set[(parse_cid(cid))] then return '1' end\n end\n return '0'\nend\nlocal function targets_of(raw)\n if raw == '' then return {} end\n return cjson.decode(raw)\nend\n-- Starts indexing a type: every later event of it.\nlocal function register(name, from)\n typed[name] = { from, from }\n typed_dirty = true\nend\n-- The first stored event in (after, total] of a type in \\`types\\` (nil: any\n-- type), or false. Nil and a type when that type's index can't answer yet.\nlocal function stored_head(after, types)\n if types == nil then\n if after + 1 <= total then return after + 1 end\n return false\n end\n local best = false\n for name in pairs(types) do\n local entry = typed[name]\n if entry == nil then\n if after < total then return nil, name end\n register(name, total)\n elseif entry[2] > after then\n if entry[1] > after then return nil, name end\n local found = redis.call('ZRANGEBYSCORE', type_key(name),\n '(' .. tostring(after), tostring(total), 'LIMIT', 0, 1)\n if #found > 0 then\n local idx = tonumber(found[1])\n if not best or idx < best then best = idx end\n end\n end\n end\n return best\nend\n-- The first event after \\`after\\` a log cursor takes: stored, then in the\n-- batch being appended.\nlocal function next_log_head(after, types, batch)\n local head, missing = stored_head(after, types)\n if head == nil then return nil, missing end\n if head then return head end\n for _, event in ipairs(batch) do\n if event.idx > after and takes(types, event.type) then return event.idx end\n end\n return false\nend\n-- Indexes a batch's events of the types typed log cursors read.\nlocal function index_batch(batch)\n local adds, order = {}, {}\n for _, event in ipairs(batch) do\n local entry = typed[event.type]\n if entry then\n local list = adds[event.type]\n if not list then\n list = {}\n adds[event.type] = list\n order[#order + 1] = event.type\n end\n list[#list + 1] = event.idx\n list[#list + 1] = tostring(event.idx)\n entry[2] = event.idx\n typed_dirty = true\n end\n end\n for _, name in ipairs(order) do sliced('ZADD', type_key(name), adds[name]) end\nend\nlocal states, state_order = {}, {}\nlocal function load_states(cids)\n local need, names = {}, {}\n for _, cid in ipairs(cids) do\n local state = states[cid]\n if (state == nil or state.partial) and not need[cid] then\n need[cid] = true\n need[#need + 1] = cid\n names[#names + 1] = cid .. ':h'\n names[#names + 1] = cid .. ':p'\n names[#names + 1] = cid .. ':t'\n names[#names + 1] = cid .. ':ty'\n end\n end\n if #need == 0 then return end\n local values = sliced('HMGET', CURSORS, names)\n for n, cid in ipairs(need) do\n local h, p, t, ty = values[n * 4 - 3], values[n * 4 - 2], values[n * 4 - 1], values[n * 4]\n local tail = (t and t ~= '') and tonumber(t) or false\n local state = states[cid]\n if state then\n state.t = tail\n state.partial = nil\n else\n local _, tag = parse_cid(cid)\n state = {\n kind = string.sub(tag, 1, 1), exists = p ~= false,\n h = (h and h ~= '') and tonumber(h) or false,\n p = tonumber(p or '0'), t = tail, ty = ty or false,\n }\n if state.h and state.h <= boundary then state.h = false end\n if state.kind == 'G' and state.exists and state.p < boundary then state.p = boundary end\n states[cid] = state\n state_order[#state_order + 1] = cid\n end\n end\nend\nlocal function flush_states()\n for _, cid in ipairs(state_order) do\n local state = states[cid]\n if state.dirty then\n put(CURSORS, cid .. ':h', state.h and tostring(state.h) or '')\n put(CURSORS, cid .. ':p', tostring(state.p))\n if state.kind == 'L' then\n if not state.partial then put(CURSORS, cid .. ':t', state.t and tostring(state.t) or '') end\n elseif state.ty then\n put(CURSORS, cid .. ':ty', state.ty)\n end\n end\n end\nend\n-- Moves a cursor past its head \\`idx\\`. \\`kind\\` is the head row's kind;\n-- \\`batch\\` is the returned events, appended after the stored log. Writes\n-- nothing; returns nil and a type when that type's index must reach back\n-- to \\`idx\\` first.\nlocal function advance(consumer, kind, idx, row_next, batch)\n if kind == '#' then\n ready_set(cid_of(consumer, '#' .. tostring(idx)), false)\n return true\n end\n local cid = cid_of(consumer, kind)\n local state = states[cid]\n local next_head\n if string.sub(kind, 1, 1) == 'L' then\n next_head = row_next and tonumber(row_next) or false\n if not state then\n state = { kind = 'L', exists = true, p = idx, h = false, t = false, ty = false, partial = true }\n states[cid] = state\n state_order[#state_order + 1] = cid\n end\n if not next_head then\n state.t = false\n state.partial = nil\n end\n else\n local lane, raw = string.sub(kind, 2), nil\n for _, spec in ipairs(served_logs) do\n if spec.name == consumer and spec.lane == lane then raw = spec.ty end\n end\n if not raw then\n load_states({ cid })\n raw = states[cid].ty or '*'\n end\n local head, missing = next_log_head(idx, types_of(raw), batch)\n if head == nil then return nil, missing end\n next_head = head\n state = states[cid]\n if not state then\n state = { kind = 'G', exists = true, p = idx, h = false, t = false, ty = raw }\n states[cid] = state\n state_order[#state_order + 1] = cid\n end\n state.ty = raw\n end\n state.h = next_head\n state.p = idx\n state.exists = true\n state.dirty = true\n ready_set(cid, next_head)\n return true\nend\n-- Reads what an appended batch's cursor work needs: its lane targets'\n-- records, and the head of each served log consumer whose types it carries.\n-- Writes nothing; returns nil, a type, and an index when that type's index\n-- must reach back to the index first.\nlocal log_plan = {}\nlocal function plan(batch, base)\n local need = {}\n for _, event in ipairs(batch) do\n for _, target in ipairs(event.targets) do\n if target[2] ~= '#' then need[#need + 1] = cid_of(target[1], target[2]) end\n end\n end\n for _, spec in ipairs(served_logs) do\n for _, event in ipairs(batch) do\n if takes(spec.types, event.type) then\n local cid = cid_of(spec.name, 'G' .. spec.lane)\n log_plan[#log_plan + 1] = { spec = spec, first = event.idx, cid = cid }\n need[#need + 1] = cid\n break\n end\n end\n end\n load_states(need)\n for _, log in ipairs(log_plan) do\n local state, spec = states[log.cid], log.spec\n if not state.exists then\n if spec.backfill then\n local head, missing = stored_head(boundary, spec.types)\n if head == nil then return nil, missing, boundary end\n log.p, log.h = boundary, head or log.first\n else\n -- Nothing it takes is stored after its start: index its types from there.\n for name in pairs(spec.types or {}) do\n if typed[name] == nil then register(name, base) end\n end\n log.p, log.h = base, log.first\n end\n elseif not state.h then\n -- Not the batch's first: a deployment that doesn't serve this consumer\n -- may have appended events it takes since it went idle.\n local head, missing = stored_head(state.p, spec.types)\n if head == nil then return nil, missing, state.p end\n log.h = head or log.first\n end\n end\n return true\nend\n-- Starts the cursor work \\`plan\\` read: each event's targets, and each\n-- planned log cursor.\nlocal function enqueue(batch)\n for _, event in ipairs(batch) do\n for _, target in ipairs(event.targets) do\n local consumer, tag = target[1], target[2]\n if served_has(consumer) then pending_fast = true end\n if tag == '#' then\n ready_set(cid_of(consumer, '#' .. tostring(event.idx)), event.idx)\n else\n local cid = cid_of(consumer, tag)\n local state = states[cid]\n if state.h and state.t then\n put_row(state.t, row_of(consumer, tag), 'next', tostring(event.idx))\n else\n state.h = event.idx\n if state.p < boundary then state.p = boundary end\n ready_set(cid, event.idx)\n end\n state.t = event.idx\n state.exists = true\n state.dirty = true\n end\n end\n end\n for _, log in ipairs(log_plan) do\n pending_fast = true\n local state = states[log.cid]\n if not state.exists then\n state.exists, state.p, state.h = true, log.p, log.h\n ready_set(log.cid, log.h)\n elseif not state.h then\n state.h = log.h\n ready_set(log.cid, log.h)\n end\n state.ty = log.spec.ty\n state.dirty = true\n end\nend\n`\n\n/**\n * KEYS: counter.\n * ARGV: sessionId, nowMs, n, served consumers, origin index or '', then per\n * event: id, type, payloadJson, causeJson or '', targets.\n * Returns ['empty', hasPending] | ['ok', firstIndex, hasPending] |\n * ['dup', hasPending, ...indexes] | ['partial', count] |\n * ['superseded', position] | ['cleared', originIndex] |\n * ['index', type, after].\n */\nconst APPEND_LUA = `${BASE_LUA}${CURSOR_LUA}\nlocal now, n = ARGV[2], tonumber(ARGV[3])\nuse_served(ARGV[4])\nif n == 0 then return { 'empty', has_pending() } end\nlocal ids = {}\nfor i = 1, n do ids[i] = ARGV[5 + (i - 1) * 5 + 1] end\nlocal owners = owners_of(read_bounds(ids))\nlocal base = total\nlocal dups = {}\nfor i = 1, n do\n if owners[i] then dups[#dups + 1] = owners[i] 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\nlocal origin = ARGV[5]\nif origin ~= '' and tonumber(origin) <= boundary then return { 'cleared', origin } end\n-- The attempt-currency fence: a cause's head row must still be its cursor's\n-- head, at that attempt, not dead-lettered.\nlocal refs, fenced = {}, {}\nfor i = 1, n do\n local raw = ARGV[5 + (i - 1) * 5 + 4]\n if raw ~= '' then\n local cause = cjson.decode(raw)\n local index = tonumber(cause.index)\n if index <= boundary then return { 'superseded', tostring(i) } end\n local tag = type(cause.lane) == 'string' and ('L' .. cause.lane) or '#'\n refs[#refs + 1] = { index, row_of(cause.consumer, tag) }\n fenced[#fenced + 1] = {\n position = i, consumer = cause.consumer, attempt = tonumber(cause.attempt), tag = tag,\n }\n end\nend\nlocal rows = read_rows(refs, { 'd', 'f', 'p', 'l' })\nfor k, cause in ipairs(fenced) do\n local row = rows[k]\n if tonumber(row[1] or '0') ~= cause.attempt or row[2] or row[3] or not owns(row[4], cause.tag) then\n return { 'superseded', tostring(cause.position) }\n end\n if served_has(cause.consumer) then pending_fast = true end\nend\nlocal batch = {}\nfor i = 1, n do\n local o = 5 + (i - 1) * 5\n batch[i] = { idx = base + i, type = ARGV[o + 2], targets = targets_of(ARGV[o + 5]) }\nend\nlocal planned, missing, after = plan(batch, base)\nif not planned then return { 'index', missing, tostring(after) } end\nlocal marks = {}\nfor i = 1, n do\n local o = 5 + (i - 1) * 5\n local field = tostring(base + i)\n redis.call('XADD', LOG, field .. '-0',\n 'id', ARGV[o + 1], 'type', ARGV[o + 2], 'payload', ARGV[o + 3],\n 'created_at', now, 'cause', ARGV[o + 4])\n marks[#marks + 1] = id_key(ARGV[o + 1])\n marks[#marks + 1] = field\nend\nindex_batch(batch)\nenqueue(batch)\nmarks[#marks + 1] = COUNTER\nmarks[#marks + 1] = tostring(base + n)\nif typed_dirty then\n marks[#marks + 1] = TYPED\n marks[#marks + 1] = cjson.encode(typed)\nend\nsliced('MSET', nil, marks)\nflush_states()\nflush()\nflush_ready()\nreturn { 'ok', tostring(base + 1), has_pending() }\n`\n\n/**\n * KEYS: counter.\n * ARGV: type, after. Indexes one page of the stored log below the type's\n * index, toward `after`, registering the type first if no cursor read it\n * yet. Returns ['1'] once the index reaches `after`, else ['0'].\n */\nconst INDEX_TYPE_LUA = `${BASE_LUA}\nread_bounds()\nlocal name, after = ARGV[1], tonumber(ARGV[2])\nlocal entry = typed[name]\nif not entry then\n entry = { total, total }\n typed[name] = entry\nend\n-- Entries below the latest clear belong to an old lifetime: never indexed.\nlocal floor = math.max(after, boundary - 1)\nif entry[1] > floor then\n local entries = redis.call('XREVRANGE', LOG, tostring(entry[1]) .. '-0',\n tostring(floor + 1) .. '-0', 'COUNT', ${READ_PAGE_SIZE})\n local adds = {}\n for _, stored in ipairs(entries) do\n local fields = stored[2]\n for j = 1, #fields, 2 do\n if fields[j] == 'type' then\n if fields[j + 1] == name then\n local idx = entry_index(stored[1])\n adds[#adds + 1] = idx\n adds[#adds + 1] = tostring(idx)\n end\n break\n end\n end\n end\n sliced('ZADD', type_key(name), adds)\n if #entries < ${READ_PAGE_SIZE} then\n entry[1] = after\n else\n entry[1] = entry_index(entries[#entries][1]) - 1\n end\nelseif entry[1] > after then\n entry[1] = after\nend\nredis.call('SET', TYPED, cjson.encode(typed))\nreturn { entry[1] <= after and '1' or '0' }\n`\n\n/**\n * KEYS: counter.\n * ARGV: holder, nowMs, expiresAtMs, maxFailures or '', served consumers,\n * the highest head to claim or '', excluded count, then per excluded head:\n * consumer, tag.\n * Live heads are the ready set's positive scores, so an empty one is enough\n * to report 'settled'. A claimed result ends with the clear marker.\n */\nconst CLAIM_AVAILABLE_LUA = `${BASE_LUA}${CURSOR_LUA}\nlocal ready = redis.call('ZRANGEBYSCORE', READY, 1, ARGV[6] == '' and '+inf' or ARGV[6], 'WITHSCORES')\nif #ready == 0 then return { 'settled' } end\nread_bounds()\nlocal holder, now, expires_at = ARGV[1], tonumber(ARGV[2]), ARGV[3]\nlocal max_failures = tonumber(ARGV[4])\nuse_served(ARGV[5])\nlocal excluded = {}\nfor i = 1, tonumber(ARGV[7]) do\n excluded[ARGV[7 + (i - 1) * 2 + 1] .. '\\\\0' .. ARGV[7 + (i - 1) * 2 + 2]] = true\nend\nlocal candidates = {}\nfor i = 1, #ready, 2 do\n local cid, idx = ready[i], tonumber(ready[i + 1])\n local consumer, tag = parse_cid(cid)\n local kind = string.sub(tag, 1, 1)\n local key = consumer .. '\\\\0' .. (kind == 'G' and ('L' .. string.sub(tag, 2)) or tag)\n if served_has(consumer) and not excluded[key] then\n candidates[#candidates + 1] = {\n idx, row_of(consumer, tag), consumer = consumer, cid = cid,\n kind = kind == '#' and '#' or tag,\n }\n end\nend\nlocal rows = read_rows(candidates, { 'd', 'fc', 'lc', 'ch', 'ce', 'a', 'lf', 'lfa', 'e', 'f', 'pins' })\nlocal takes_over, retry_at, wanted = {}, nil, {}\nfor position, candidate in ipairs(candidates) do\n local row = rows[position]\n local expires = tonumber(row[5] or '0')\n if expires > now then\n if not retry_at or expires < retry_at then retry_at = expires end\n else\n takes_over[#takes_over + 1] = { candidate = candidate, row = row }\n wanted[candidate[1]] = true\n end\nend\nif #takes_over == 0 then\n if retry_at then return { 'busy', tostring(retry_at) } end\n return { 'settled' }\nend\n-- One XRANGE per run of consecutive claimed indexes.\nlocal indexes, entries = {}, {}\nfor idx in pairs(wanted) do indexes[#indexes + 1] = idx end\ntable.sort(indexes)\nlocal first = 1\nwhile first <= #indexes do\n local last = first\n while last < #indexes and indexes[last + 1] == indexes[last] + 1 do last = last + 1 end\n local range = redis.call('XRANGE', LOG, tostring(indexes[first]) .. '-0', tostring(indexes[last]) .. '-0')\n for _, entry in ipairs(range) do entries[entry_index(entry[1])] = entry end\n first = last + 1\nend\nfor _, idx in ipairs(indexes) do\n if not entries[idx] then return { 'missing', tostring(idx) } end\nend\nlocal result = { 'claimed', tostring(#takes_over) }\nfor _, take in ipairs(takes_over) do\n local candidate, row = take.candidate, take.row\n local idx, at = candidate[1], candidate[2]\n local attempt = tostring(tonumber(row[1] or '0') + 1)\n -- A claim still held here lapsed: its attempt never finished.\n local lapsed = row[4] ~= false\n local failures = tonumber(row[6] or '0')\n if lapsed then\n failures = failures + 1\n put_row(idx, at, 'a', tostring(failures))\n end\n put_row(idx, at, 'd', attempt)\n local columns\n if lapsed and max_failures and failures >= max_failures then\n -- That takeover spent the last failed attempt: dead-letter, unclaimed.\n -- Advancing the attempt fences the lapsed one; nothing runs.\n put_row(idx, at, 'f', ARGV[2])\n drop_row(idx, at, 'ch')\n drop_row(idx, at, 'ce')\n ready_set(candidate.cid, -idx)\n release_pins(idx, at, row[11])\n columns = { attempt, row[2], row[3], false, false, tostring(failures), row[7], row[8], row[9], ARGV[2] }\n else\n local first_claimed = row[2] or ARGV[2]\n put_row(idx, at, 'fc', first_claimed)\n put_row(idx, at, 'lc', ARGV[2])\n put_row(idx, at, 'ch', holder)\n put_row(idx, at, 'ce', expires_at)\n put_row(idx, at, 'l', candidate.kind)\n columns = { attempt, first_claimed, ARGV[2], holder, expires_at, tostring(failures), row[7], row[8], row[9], false }\n end\n result[#result + 1] = { candidate.consumer, candidate.kind, tostring(idx), columns, entries[idx] }\nend\nflush()\nflush_ready()\nresult[#result + 1] = tostring(boundary)\nresult[#result + 1] = tostring(total)\nreturn result\n`\n\n/**\n * KEYS: counter.\n * ARGV: holder, nowMs, expiresAtMs, n, then per claim: consumer, tag, index,\n * attempt. Returns [renewedCount, ...renewed, ...superseded] as 1-based\n * positions in the request.\n */\nconst RENEW_CLAIMS_LUA = `${BASE_LUA}\nread_bounds()\nlocal holder, now, expires_at = ARGV[1], tonumber(ARGV[2]), ARGV[3]\nlocal refs, items, superseded = {}, {}, {}\nfor i = 1, tonumber(ARGV[4]) do\n local o = 4 + (i - 1) * 4\n local idx = tonumber(ARGV[o + 3])\n if idx <= boundary then\n superseded[#superseded + 1] = tostring(i)\n else\n local row = row_of(ARGV[o + 1], ARGV[o + 2])\n refs[#refs + 1] = { idx, row }\n items[#items + 1] = { position = i, idx = idx, row = row, tag = ARGV[o + 2], attempt = tonumber(ARGV[o + 4]) }\n end\nend\nlocal rows = read_rows(refs, { 'd', 'ce', 'p', 'f', 'ch', 'l' })\nlocal renewed = {}\nfor k, item in ipairs(items) do\n local row = rows[k]\n local attempt = tonumber(row[1] or '0')\n if row[6] and not owns(row[6], item.tag) then\n -- Another cursor's head: this one never claimed it.\n elseif row[3] or attempt > item.attempt then\n superseded[#superseded + 1] = tostring(item.position)\n elseif attempt == item.attempt and tonumber(expires_at) > now and\n not row[4] and row[5] == holder and tonumber(row[2] or '0') > now then\n put_row(item.idx, item.row, 'ce', expires_at)\n renewed[#renewed + 1] = tostring(item.position)\n end\nend\nflush()\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: counter.\n * ARGV: sessionId, nowMs, consumer, tag, index, attempt, child causeJson,\n * served consumers, returned idsJson, n, then per child: id, type,\n * payloadJson, targets.\n */\nconst COMPLETE_ATTEMPT_LUA = `${BASE_LUA}${CURSOR_LUA}\nlocal now = ARGV[2]\nlocal consumer, tag, idx, attempt = ARGV[3], ARGV[4], tonumber(ARGV[5]), tonumber(ARGV[6])\nlocal at = row_of(consumer, tag)\nlocal n = tonumber(ARGV[10])\nlocal ids = {}\nfor i = 1, n do ids[i] = ARGV[10 + (i - 1) * 4 + 1] end\nlocal owners = owners_of(read_bounds(ids))\nif idx <= boundary then return { 'superseded' } end\nif idx < 1 or idx > total then return { 'missing' } end\nlocal row = read_rows({ { idx, at } }, { 'd', 'p', 'pa', 'ri', 'ch', 'f', 'l', 'next', 'pins' })[1]\nif row[2] then\n -- The cursor moved past this head: a retry of its completion replays.\n if not owns(row[7], tag) then return { 'superseded' } end\n if not row[3] or tonumber(row[3]) ~= attempt then return { 'superseded' } end\n local recorded = cjson.decode(row[4] or '[]')\n if #recorded ~= n then return { 'partial' } end\n for i = 1, n do\n if recorded[i] ~= ARGV[10 + (i - 1) * 4 + 1] then return { 'partial' } end\n end\n local result = { 'duplicate' }\n for i = 1, n do\n if not owners[i] then return { 'partial' } end\n result[#result + 1] = owners[i]\n end\n return result\nend\nif tonumber(row[1] or '0') ~= attempt or not row[5] or row[6] or not owns(row[7], tag) then\n return { 'superseded' }\nend\nfor i = 1, n do\n if owners[i] then return { 'partial' } end\nend\nuse_served(ARGV[8])\nlocal batch = {}\nfor i = 1, n do\n local o = 10 + (i - 1) * 4\n batch[i] = { idx = total + i, type = ARGV[o + 2], targets = targets_of(ARGV[o + 4]) }\nend\nlocal moved, missing = advance(consumer, row[7], idx, row[8], batch)\nif not moved then return { 'index', missing, tostring(idx) } end\nlocal planned, unindexed, after = plan(batch, total)\nif not planned then return { 'index', unindexed, tostring(after) } end\nput_row(idx, at, 'p', now)\nput_row(idx, at, 'pa', ARGV[6])\nput_row(idx, at, 'ri', ARGV[9])\nrelease_pins(idx, at, row[9])\nlocal marks = {}\nfor i = 1, n do\n local o = 10 + (i - 1) * 4\n local field = tostring(total + i)\n redis.call('XADD', LOG, field .. '-0',\n 'id', ARGV[o + 1], 'type', ARGV[o + 2], 'payload', ARGV[o + 3],\n 'created_at', now, 'cause', ARGV[7])\n marks[#marks + 1] = id_key(ARGV[o + 1])\n marks[#marks + 1] = field\nend\nindex_batch(batch)\nenqueue(batch)\nif n > 0 then\n marks[#marks + 1] = COUNTER\n marks[#marks + 1] = tostring(total + n)\nend\nif typed_dirty then\n marks[#marks + 1] = TYPED\n marks[#marks + 1] = cjson.encode(typed)\nend\nsliced('MSET', nil, marks)\nflush_states()\nflush()\nflush_ready()\nreturn { 'ok', tostring(total + 1) }\n`\n\n/**\n * KEYS: counter.\n * ARGV: consumer, tag, index, attempt, error, maxFailures, nowMs, retryAtMs\n * or ''.\n */\nconst FAIL_ATTEMPT_LUA = `${BASE_LUA}\nread_bounds()\nlocal consumer, tag, idx, attempt = ARGV[1], ARGV[2], tonumber(ARGV[3]), tonumber(ARGV[4])\nlocal at = row_of(consumer, tag)\nif idx <= boundary then return { 'superseded', '0' } end\nif idx < 1 or idx > total then return { 'missing', '0' } end\nlocal row = read_rows({ { idx, at } }, { 'a', 'd', 'p', 'f', 'lfa', 'ch', 'l', 'pins' })[1]\nif row[3] or (row[7] and not owns(row[7], tag)) then return { 'superseded', '0' } end\nlocal failures = tonumber(row[1] or '0')\nif tonumber(row[2] or '0') ~= attempt then return { 'superseded', tostring(failures) } end\nif not row[6] and row[5] and tonumber(row[5]) == attempt then\n return { row[4] and 'dead_lettered' or 'failed', tostring(failures) }\nend\nif row[4] then return { 'dead_lettered', tostring(failures) } end\nif not row[6] then return { 'superseded', tostring(failures) } end\nfailures = failures + 1\nlocal dead = failures >= tonumber(ARGV[6])\nput_row(idx, at, 'a', tostring(failures))\nput_row(idx, at, 'e', ARGV[5])\nput_row(idx, at, 'lf', ARGV[7])\nput_row(idx, at, 'lfa', ARGV[4])\ndrop_row(idx, at, 'ch')\nif dead then\n put_row(idx, at, 'f', ARGV[7])\n drop_row(idx, at, 'ce')\n ready_set(cid_for(consumer, row[7], idx), -idx)\n release_pins(idx, at, row[8])\nelseif ARGV[8] ~= '' then\n put_row(idx, at, 'ce', ARGV[8])\nelse\n drop_row(idx, at, 'ce')\nend\nflush()\nflush_ready()\nreturn { dead and 'dead_lettered' or 'failed', tostring(failures) }\n`\n\n/**\n * KEYS: counter. ARGV: consumer, index, action, nowMs, served\n * consumers. Returns [redriven, hasPending] | ['index', type, after]. Acts\n * only on the consumer's dead-lettered head at the index, above the clear\n * marker.\n */\nconst REDRIVE_LUA = `${BASE_LUA}${CURSOR_LUA}\nread_bounds()\nuse_served(ARGV[5])\nlocal consumer, idx = ARGV[1], tonumber(ARGV[2])\nlocal function result(redriven) return { redriven, has_pending() } end\nif idx <= boundary or idx < 1 or idx > total then return result('0') end\nlocal cid, tag = nil, nil\nfor _, member in ipairs(redis.call('ZRANGEBYSCORE', READY, -idx, -idx)) do\n local owner, member_tag = parse_cid(member)\n if owner == consumer then\n cid, tag = member, member_tag\n break\n end\nend\nif not cid then return result('0') end\nlocal kind = string.sub(tag, 1, 1) == '#' and '#' or tag\nlocal at = row_of(consumer, tag)\nif ARGV[3] == 'skip' then\n local row = read_rows({ { idx, at } }, { 'next' })[1]\n local moved, missing = advance(consumer, kind, idx, row[1], {})\n if not moved then return { 'index', missing, tostring(idx) } end\n put_row(idx, at, 'p', ARGV[4])\n flush_states()\n if typed_dirty then redis.call('SET', TYPED, cjson.encode(typed)) end\nelse\n drop_row(idx, at, 'f')\n drop_row(idx, at, 'a')\n ready_set(cid, idx)\nend\nflush()\nflush_ready()\nreturn result('1')\n`\n\n/**\n * KEYS: counter. ARGV: consumer, lane, types JSON or '*', backfill\n * ('1' or '0'). Brings one log cursor up to date without an append, as\n * `plan` and `enqueue` would for an append carrying its types: a missing\n * cursor starts at the clear marker with backfill, else at the head with\n * nothing to run; an idle one takes the next stored event of its types; one\n * with a head, claimed or not, keeps it. Records the types, and writes\n * nothing when nothing changed. Returns ['ok', position, head or nil, head\n * columns, session head] | ['index', type, after].\n */\nconst WAKE_LOG_LUA = `${BASE_LUA}${CURSOR_LUA}\nread_bounds()\nlocal consumer, lane, ty = ARGV[1], ARGV[2], ARGV[3]\nlocal types = types_of(ty)\nlocal cid = cid_of(consumer, 'G' .. lane)\nload_states({ cid })\nlocal state = states[cid]\nlocal had_head = state.h\n-- Nothing after the latest clear: a missing cursor stays missing.\nif not state.exists and total == boundary then\n return { 'ok', tostring(boundary), false, {}, tostring(total) }\nend\nif not state.exists then\n state.exists, state.p = true, total\n if ARGV[4] == '1' then\n local head, missing = stored_head(boundary, types)\n if head == nil then return { 'index', missing, tostring(boundary) } end\n state.p, state.h = boundary, head\n else\n -- Nothing it takes is stored after its start: index its types from there.\n for name in pairs(types or {}) do\n if typed[name] == nil then register(name, total) end\n end\n end\n state.dirty = true\nelseif not state.h then\n -- A deployment that doesn't serve this consumer may have appended events\n -- it takes since it went idle.\n local head, missing = stored_head(state.p, types)\n if head == nil then return { 'index', missing, tostring(state.p) } end\n if head then state.h, state.dirty = head, true end\nend\nif state.ty ~= ty then state.ty, state.dirty = ty, true end\nlocal columns = {}\nif had_head then\n columns = read_rows({ { state.h, row_of(consumer, 'G' .. lane) } }, ${HEAD_COLUMNS})[1]\nelseif state.h then\n ready_set(cid, state.h)\nend\nflush_states()\nif typed_dirty then redis.call('SET', TYPED, cjson.encode(typed)) end\nflush()\nflush_ready()\nreturn { 'ok', tostring(state.p), state.h and tostring(state.h) or false, columns, tostring(total) }\n`\n\n/**\n * KEYS: counter. Returns the dead-lettered heads above the clear\n * marker as [consumer, tag, index, columns, entry] tuples.\n */\nconst DEAD_LETTERS_LUA = `${BASE_LUA}\nlocal dead = redis.call('ZRANGEBYSCORE', READY, '-inf', '(0', 'WITHSCORES')\nif #dead == 0 then return {} end\nread_bounds()\nlocal refs, items = {}, {}\nfor i = 1, #dead, 2 do\n local consumer, tag = parse_cid(dead[i])\n local idx = -tonumber(dead[i + 1])\n if idx > boundary then\n refs[#refs + 1] = { idx, row_of(consumer, tag) }\n items[#items + 1] = { consumer, tag, idx }\n end\nend\nlocal rows = read_rows(refs, ${HEAD_COLUMNS})\nlocal result = {}\nfor k, item in ipairs(items) do\n local entries = redis.call('XRANGE', LOG, tostring(item[3]) .. '-0', tostring(item[3]) .. '-0')\n if #entries > 0 then\n result[#result + 1] = { item[1], item[2], tostring(item[3]), rows[k], entries[1] }\n end\nend\nreturn result\n`\n\n/**\n * KEYS: counter. Returns every listed cursor as [consumer, tag,\n * position, head or nil, columns]: lane and log cursors from the cursor\n * hash, read against the clear marker, then the event cursors with a head.\n */\nconst CURSORS_LUA = `${BASE_LUA}\nread_bounds()\nlocal listed = {}\nlocal fields = redis.call('HGETALL', CURSORS)\nlocal records, order = {}, {}\nfor i = 1, #fields, 2 do\n local at = string.find(fields[i], ':[^:]*$')\n local cid, suffix = string.sub(fields[i], 1, at - 1), string.sub(fields[i], at + 1)\n if not records[cid] then\n records[cid] = {}\n order[#order + 1] = cid\n end\n records[cid][suffix] = fields[i + 1]\nend\nfor _, cid in ipairs(order) do\n local record = records[cid]\n local consumer, tag = parse_cid(cid)\n local head = (record.h and record.h ~= '') and tonumber(record.h) or false\n local position = tonumber(record.p or '0')\n if head and head <= boundary then head = false end\n local kind = string.sub(tag, 1, 1)\n if kind == 'G' and position < boundary then position = boundary end\n if kind == 'G' or head or position > boundary then\n listed[#listed + 1] = { consumer, tag, position, head }\n end\nend\nlocal ready = redis.call('ZRANGE', READY, 0, -1, 'WITHSCORES')\nfor i = 1, #ready, 2 do\n local consumer, tag = parse_cid(ready[i])\n if string.sub(tag, 1, 1) == '#' then\n local head = math.abs(tonumber(ready[i + 1]))\n listed[#listed + 1] = { consumer, '#', head - 1, head }\n end\nend\nlocal refs, positions = {}, {}\nfor position, cursor in ipairs(listed) do\n if cursor[4] then\n refs[#refs + 1] = { cursor[4], row_of(cursor[1], cursor[2]) }\n positions[#refs] = position\n end\nend\nlocal rows = read_rows(refs, ${HEAD_COLUMNS})\nlocal columns = {}\nfor k, position in ipairs(positions) do columns[position] = rows[k] end\nlocal result = {}\nfor position, cursor in ipairs(listed) do\n result[#result + 1] = {\n cursor[1], cursor[2], tostring(cursor[3]),\n cursor[4] and tostring(cursor[4]) or false, columns[position] or {},\n }\nend\nreturn result\n`\n\n/**\n * KEYS: counter, head, history.\n * ARGV: write count, then per write: index, snapshot JSON, pin count, then\n * per pin: consumer, tag, index; finally the reducer name. Writes from\n * before the latest clear are dropped, and a head from before it is\n * replaced as if absent. A pin holds only on an unfinished, live head: a\n * claimed row says so itself, an unclaimed head by its cursor's ready score.\n */\nconst PUT_SNAPSHOTS_LUA = `${BASE_LUA}\nread_bounds()\nlocal HEAD, HISTORY = KEYS[2], KEYS[3]\nlocal entries, refs, pins = {}, {}, {}\nlocal offset = 2\nfor w = 1, tonumber(ARGV[1]) do\n local index = tonumber(ARGV[offset])\n local pin_count = tonumber(ARGV[offset + 2])\n local entry = { index = index, snapshot = ARGV[offset + 1], pins = {} }\n for p = 1, pin_count do\n local o = offset + 2 + (p - 1) * 3\n local pin = { consumer = ARGV[o + 1], tag = ARGV[o + 2], idx = tonumber(ARGV[o + 3]) }\n if index >= boundary and pin.idx > boundary and pin.idx <= total then\n pin.row = row_of(pin.consumer, pin.tag)\n refs[#refs + 1] = { pin.idx, pin.row }\n pins[#pins + 1] = pin\n entry.pins[#entry.pins + 1] = pin\n end\n end\n entries[w] = entry\n offset = offset + 3 + pin_count * 3\nend\nlocal function live(consumer, tag, idx)\n return tonumber(redis.call('ZSCORE', READY, cid_of(consumer, tag)) or '0') == idx\nend\nlocal rows = read_rows(refs, { 'pins', 'p', 'f', 'l' })\nlocal held, held_order = {}, {}\nfor k, pin in ipairs(pins) do\n local row = rows[k]\n local valid\n if row[2] or row[3] then\n valid = false\n elseif row[4] then\n valid = owns(row[4], pin.tag)\n elseif pin.tag == '#' then\n valid = live(pin.consumer, '#' .. tostring(pin.idx), pin.idx)\n else\n valid = live(pin.consumer, pin.tag, pin.idx) or\n live(pin.consumer, 'G' .. string.sub(pin.tag, 2), pin.idx)\n end\n if valid then\n local key = tostring(pin.idx) .. ':' .. pin.row\n if not held[key] then\n local list = (row[1] and row[1] ~= '') and cjson.decode(row[1]) or {}\n local set = {}\n for _, ref in ipairs(list) do set[ref] = true end\n held[key] = { list = list, set = set, idx = pin.idx, row = pin.row }\n held_order[#held_order + 1] = key\n end\n pin.held = held[key]\n end\nend\nlocal snapshots = {}\nfor _, entry in ipairs(entries) do\n local ref = cjson.encode({ HISTORY, tostring(entry.index) })\n for _, pin in ipairs(entry.pins) do\n if pin.held and not pin.held.set[ref] then\n pin.held.set[ref] = true\n pin.held.list[#pin.held.list + 1] = ref\n pin.held.dirty = true\n redis.call('HINCRBY', PIN_COUNTS, ref, 1)\n end\n end\n if entry.index >= boundary then\n snapshots[#snapshots + 1] = { index = entry.index, snapshot = entry.snapshot, ref = ref }\n end\nend\nfor _, key in ipairs(held_order) do\n local row = held[key]\n if row.dirty then put_row(row.idx, row.row, 'pins', cjson.encode(row.list)) end\nend\nflush()\nif #snapshots == 0 then return 1 end\n\nlocal head = redis.call('GET', HEAD)\nlocal head_index = nil\nlocal registered = false\nif head then\n local record = cjson.decode(head)\n head_index = tonumber(record['index'])\n registered = record['r'] == 1\nend\nif head_index and head_index < boundary then\n head, head_index, registered = nil, nil, false\n redis.call('ZREMRANGEBYSCORE', HISTORY, '-inf', '(' .. boundary)\nend\n-- Deletion finds each reducer's keys through this set instead of a scan.\n-- Heads written since carry the mark, so a reducer registers once.\nif not registered then\n redis.call('SADD', SESSION .. 'snap-reducers', ARGV[#ARGV])\nend\nlocal latest = snapshots[#snapshots]\nlocal writes_head = not head_index or latest.index >= head_index\nlocal advances = not head_index or latest.index > head_index\nlocal final_head_index = advances and latest.index or head_index\n\nif advances and head and tonumber(redis.call('HGET', PIN_COUNTS, cjson.encode({ HISTORY, tostring(head_index) })) or '0') > 0 then\n redis.call('ZREMRANGEBYSCORE', HISTORY, head_index, head_index)\n redis.call('ZADD', HISTORY, head_index, head)\nend\n\nfor _, write in ipairs(snapshots) do\n if write.index < final_head_index then\n redis.call('ZREMRANGEBYSCORE', HISTORY, write.index, write.index)\n if tonumber(redis.call('HGET', PIN_COUNTS, write.ref) or '0') > 0 then\n redis.call('ZADD', HISTORY, write.index, write.snapshot)\n end\n end\nend\n\nif writes_head then\n redis.call('SET', HEAD, latest.snapshot)\n redis.call('ZREMRANGEBYSCORE', HISTORY, latest.index, latest.index)\nend\nreturn 1\n`\n\n/**\n * Presence entries are `{\"a\":at,\"e\":expiresAt,\"s\":seen,\"v\":value}`, stamps\n * first and both integers, so scripts read them with a pattern instead of\n * decoding the value.\n */\nconst PRESENCE_STAMPS_LUA = `\nlocal function stamps(entry)\n local a, e = string.match(entry, '^{\"a\":(%-?%d+),\"e\":(%-?%d+),')\n return tonumber(a), tonumber(e)\nend\n`\n\n/**\n * KEYS: presence hash. ARGV: atMs, ttlMs, nowMs, n, then per entry:\n * hashField, entryJson (a null value is a tombstone). Field-wise LWW by the\n * sender's `at`: an unexpired stored entry with a strictly newer stamp\n * survives; ties go to the incoming write (memory-tier parity). Returns the applied 1-based entry\n * positions. The whole-key PEXPIRE is the coarse bound, sound because core\n * sends one uniform ttlMs; the per-entry `e` stamp is the fine bound (the\n * injected storage clock at the applied set plus ttlMs — never the sender\n * stamp), filtered on read against the same clock.\n */\nconst PRESENCE_SET_LUA = `${SLICED_LUA}${PRESENCE_STAMPS_LUA}\nlocal at, now, n = tonumber(ARGV[1]), tonumber(ARGV[3]), tonumber(ARGV[4])\nlocal fields = {}\nfor i = 1, n do fields[i] = ARGV[4 + (i - 1) * 2 + 1] end\nlocal stored = sliced('HMGET', KEYS[1], fields)\nlocal applied, sets = {}, {}\nfor i = 1, n do\n local a, e = false, false\n if stored[i] then a, e = stamps(stored[i]) end\n if not stored[i] or a <= at or e <= now then\n sets[#sets + 1] = fields[i]\n sets[#sets + 1] = ARGV[4 + (i - 1) * 2 + 2]\n applied[#applied + 1] = tostring(i)\n end\nend\nsliced('HSET', KEYS[1], sets)\nif #applied > 0 then redis.call('PEXPIRE', KEYS[1], ARGV[2]) end\nreturn applied\n`\n\n/**\n * KEYS: presence hash. ARGV: nowMs, then the fields a read saw expired.\n * Deletes those still at or past their expiry, re-checked in this atomic\n * step so a stale reader never sweeps a concurrently refreshed field.\n */\nconst PRESENCE_PRUNE_LUA = `${SLICED_LUA}${PRESENCE_STAMPS_LUA}\nlocal now = tonumber(ARGV[1])\nlocal fields = {}\nfor i = 2, #ARGV do fields[#fields + 1] = ARGV[i] end\nlocal stored = sliced('HMGET', KEYS[1], fields)\nlocal stale = {}\nfor i = 1, #fields do\n if stored[i] then\n local _, e = stamps(stored[i])\n if e <= now then stale[#stale + 1] = fields[i] end\n end\nend\nsliced('HDEL', KEYS[1], stale)\nreturn 1\n`\n\n/**\n * KEYS: counter, head, history. ARGV: snapshot frontier or ''.\n * Returns one atomic multiversion-cache-plus-log-frontier read, with the\n * clear boundary; checkpoints from before it are ignored.\n */\nconst READ_STATE_LUA = `\nlocal session = string.sub(KEYS[1], 1, -6)\nlocal counts = redis.call('MGET', KEYS[1], session .. 'clear')\nlocal boundary = tonumber(counts[2] or '0')\nlocal head = redis.call('GET', KEYS[2])\nlocal head_index = head and cjson.decode(head)['index'] or nil\nif head and (type(head_index) ~= 'number' or head_index < 0 or head_index > 9007199254740991 or head_index ~= math.floor(head_index)) then\n return redis.error_reply('invalid snapshot head index')\nend\nif head_index and head_index < boundary then\n head = nil\n head_index = nil\nend\nlocal cap = ARGV[1]\nlocal snapshot = nil\nif head_index and (cap == '' or head_index <= tonumber(cap)) then\n snapshot = head\nelse\n local upper = cap == '' and '+inf' or cap\n local historical = redis.call('ZREVRANGEBYSCORE', KEYS[3], upper, boundary, 'LIMIT', 0, 1)\n if #historical > 0 then snapshot = historical[1] end\nend\nreturn { head_index or '', snapshot or '', counts[1] or counts[2] or '0', tostring(boundary) }\n`\n\n/**\n * What reclaim removes once no entry is left below the latest clear: every\n * registered reducer's checkpoints below the latest clear and the pin counts\n * naming them (snapshot writes register each reducer in\n * {prefix}:{session}:snap-reducers, so no key scan is needed), every head\n * row below it (whole chunks, then the boundary chunk's older rows), the\n * type indexes' older entries, and the lane cursors it left idle.\n *\n * A lifetime that ended empty then collapses to the clear marker, the log\n * holding it (readers resuming before the marker still read it), and the log\n * cursors the sweep reset to the marker. The counter reads from the marker\n * when absent, type indexes rebuild lazily, and the marker's own id key goes\n * too.\n */\nconst SWEEP_LUA = `\nlocal function sweep(boundary)\n for _, reducer in ipairs(redis.call('SMEMBERS', SESSION .. 'snap-reducers')) do\n local head_key = SESSION .. 'snap:' .. reducer\n local head = redis.call('GET', head_key)\n if head and tonumber(cjson.decode(head)['index']) < boundary then\n redis.call('DEL', head_key)\n end\n redis.call('ZREMRANGEBYSCORE', SESSION .. 'snap-history:' .. reducer, '-inf', '(' .. boundary)\n end\n local counts = redis.call('HGETALL', PIN_COUNTS)\n local stale = {}\n for i = 1, #counts, 2 do\n if tonumber(cjson.decode(counts[i])[2]) < boundary then stale[#stale + 1] = counts[i] end\n end\n sliced('HDEL', PIN_COUNTS, stale)\n local raw = redis.call('GET', TYPED)\n if raw then\n for name in pairs(cjson.decode(raw)) do\n redis.call('ZREMRANGEBYSCORE', type_key(name), '-inf', '(' .. boundary)\n end\n end\n local last = math.floor((boundary - 1) / ${META_CHUNK_SIZE})\n local chunks = {}\n for chunk = 0, last - 1 do chunks[#chunks + 1] = META .. ':' .. tostring(chunk) end\n sliced('UNLINK', nil, chunks)\n local older = {}\n for _, field in ipairs(redis.call('HKEYS', META .. ':' .. tostring(last))) do\n local idx = tonumber(string.match(field, '^(%d+):'))\n if idx and idx < boundary then older[#older + 1] = field end\n end\n sliced('HDEL', META .. ':' .. tostring(last), older)\n local fields = redis.call('HGETALL', CURSORS)\n local records, order = {}, {}\n for i = 1, #fields, 2 do\n local at = string.find(fields[i], ':[^:]*$')\n local cid, suffix = string.sub(fields[i], 1, at - 1), string.sub(fields[i], at + 1)\n if not records[cid] then\n records[cid] = {}\n order[#order + 1] = cid\n end\n records[cid][suffix] = fields[i + 1]\n end\n local idle, moved = {}, {}\n for _, cid in ipairs(order) do\n local record = records[cid]\n local _, tag = parse_cid(cid)\n local head = (record.h and record.h ~= '') and tonumber(record.h) or false\n local position = tonumber(record.p or '0')\n if head and head <= boundary then head = false end\n if string.sub(tag, 1, 1) == 'G' then\n if position < boundary then position = boundary end\n moved[#moved + 1] = cid .. ':h'\n moved[#moved + 1] = head and tostring(head) or ''\n moved[#moved + 1] = cid .. ':p'\n moved[#moved + 1] = tostring(position)\n elseif not head and position <= boundary then\n for suffix in pairs(record) do idle[#idle + 1] = cid .. ':' .. suffix end\n elseif not head and record.h ~= '' then\n moved[#moved + 1] = cid .. ':h'\n moved[#moved + 1] = ''\n end\n end\n sliced('HDEL', CURSORS, idle)\n sliced('HSET', CURSORS, moved)\n local count = redis.call('GET', COUNTER)\n if not count or tonumber(count) ~= boundary or redis.call('ZCARD', READY) > 0 then\n return\n end\n local stale = { COUNTER, TYPED, PIN_COUNTS, chunk_key(boundary) }\n local reducers = SESSION .. 'snap-reducers'\n for _, reducer in ipairs(redis.call('SMEMBERS', reducers)) do\n stale[#stale + 1] = SESSION .. 'snap:' .. reducer\n stale[#stale + 1] = SESSION .. 'snap-history:' .. reducer\n end\n stale[#stale + 1] = reducers\n local types = redis.call('GET', TYPED)\n if types then\n for name in pairs(cjson.decode(types)) do stale[#stale + 1] = type_key(name) end\n end\n local marker = redis.call('XRANGE', LOG, boundary .. '-0', boundary .. '-0')[1]\n if marker then\n for i = 1, #marker[2], 2 do\n if marker[2][i] == 'id' then stale[#stale + 1] = id_key(marker[2][i + 1]) end\n end\n end\n sliced('UNLINK', nil, stale)\nend\n`\n\n/**\n * KEYS: counter.\n * ARGV: sessionId, nowMs, ifLastIndex or '', cause consumer or '', cause tag,\n * cause index, cause attempt, marker id.\n * Returns ['ok', cleared, head, boundary, appended?] | ['superseded'] |\n * ['foreign']; `appended` is '1' when this call appended the marker.\n * The marker joins the same log: the old lifetime's entries stay below it\n * until reclaim deletes them, and every read of the log starts at the\n * marker. (A stream can't be renamed aside: Upstash refuses RENAME on one.)\n * The ready set is unlinked, so claims and pending checks see only the new\n * lifetime. Type indexes keep their older entries until reclaim too:\n * lookups start above the marker.\n */\nconst CLEAR_LUA = `${BASE_LUA}\nlocal marker_owner = owners_of(read_bounds({ ARGV[8] }))[1]\nlocal head = total\nif ARGV[4] ~= '' then\n local idx = tonumber(ARGV[6])\n if idx <= boundary then return { 'superseded' } end\n local row = read_rows({ { idx, row_of(ARGV[4], ARGV[5]) } }, { 'd', 'f', 'p', 'l' })[1]\n if tonumber(row[1] or '0') ~= tonumber(ARGV[7]) or row[2] or row[3] or not owns(row[4], ARGV[5]) then\n return { 'superseded' }\n end\nend\nlocal expected = ARGV[3]\nif expected ~= '' and boundary == tonumber(expected) + 1 then\n return { 'ok', '1', tostring(head), tostring(boundary) }\nend\nif expected ~= '' and tonumber(expected) ~= head then\n return { 'ok', '0', tostring(head), tostring(boundary) }\nend\nif head == 0 or boundary == head then\n return { 'ok', '1', tostring(head), tostring(boundary) }\nend\nif marker_owner then return { 'foreign' } end\nlocal field = tostring(head + 1)\nredis.call('UNLINK', READY)\nredis.call('XADD', LOG, field .. '-0',\n 'id', ARGV[8], 'type', CLEAR_TYPE, 'payload', 'null',\n 'created_at', ARGV[2], 'cause', '')\nredis.call('MSET', id_key(ARGV[8]), field, COUNTER, field, CLEAR, field)\nreturn { 'ok', '1', field, field, '1' }\n`\n\n/**\n * KEYS: counter. ARGV: page size.\n * The next deletion batch: up to a page of [index, id] pairs of the log's\n * entries below the latest clear. With none left it sweeps what the old\n * lifetimes left and returns nothing, so a retry finishes an interrupted\n * deletion.\n */\nconst RECLAIM_BATCH_LUA = `${BASE_LUA}${SWEEP_LUA}\nlocal boundary = tonumber(redis.call('GET', CLEAR) or '0')\nif boundary == 0 then return {} end\nlocal entries = redis.call('XRANGE', LOG, '-', tostring(boundary - 1) .. '-0', 'COUNT', ARGV[1])\nif #entries == 0 then\n sweep(boundary)\n return {}\nend\nlocal batch = {}\nfor _, entry in ipairs(entries) do\n local fields = entry[2]\n for i = 1, #fields, 2 do\n if fields[i] == 'id' then\n batch[#batch + 1] = string.match(entry[1], '^(%d+)')\n batch[#batch + 1] = fields[i + 1]\n break\n end\n end\nend\nreturn batch\n`\n\n/**\n * KEYS: counter.\n * ARGV: sessionId, n, then per entry: index, id.\n * Deletes the listed entries below the latest clear and their id records.\n * An id record survives when it names another event, so an id reused in the\n * new lifetime keeps its owner. Returns [deleted, done].\n */\nconst RECLAIM_LUA = `${BASE_LUA}${SWEEP_LUA}\nlocal boundary = tonumber(redis.call('GET', CLEAR) or '0')\nlocal ids, indexes, stream_ids = {}, {}, {}\n-- A concurrent reclaim may list the same batch; only XDEL says who removed it.\nlocal removed = 0\nfor i = 1, tonumber(ARGV[2]) do\n local field = ARGV[2 + (i - 1) * 2 + 1]\n if tonumber(field) < boundary then\n ids[#ids + 1] = ARGV[2 + (i - 1) * 2 + 2]\n indexes[#ids] = tonumber(field)\n stream_ids[#ids] = field .. '-0'\n end\nend\nif #ids > 0 then\n local keys = {}\n for i = 1, #ids do keys[i] = id_key(ids[i]) end\n local owners, stale = sliced('MGET', nil, keys), {}\n for i = 1, #ids do\n if owners[i] and tonumber(owners[i]) == indexes[i] then\n stale[#stale + 1] = keys[i]\n end\n end\n sliced('DEL', nil, stale)\n removed = redis.call('XDEL', LOG, unpack(stream_ids))\nend\nlocal done = 0\nif #redis.call('XRANGE', LOG, '-', tostring(boundary - 1) .. '-0', 'COUNT', 1) == 0 then\n sweep(boundary)\n done = 1\nend\nreturn { removed, done }\n`\n\n// ── wire helpers ─────────────────────────────────────────────────────\n\nconst WAKE_PAYLOAD_LIMIT = 32_768\nconst encoder = new TextEncoder()\n\n/**\n * A wake-up announcing events through `frontier`. It carries them, so a\n * caught-up reader applies them without reading the stream:\n * `{\"f\":frontier,\"t\":createdAt,\"e\":[[index,id,type,payload],...]}`, with\n * payloads inlined as their stored JSON text. One over WAKE_PAYLOAD_LIMIT\n * bytes, or one without entries, announces only `{\"f\":frontier}`, and\n * readers catch up with XRANGE.\n */\nconst wakeMessage = (\n frontier: number,\n createdAt: number,\n entries: ReadonlyArray<\n readonly [index: number, id: string, type: string, payloadJson: string]\n >,\n): string => {\n const bare = `{\"f\":${frontier}}`\n if (entries.length === 0) return bare\n const carried = `{\"f\":${frontier},\"t\":${createdAt},\"e\":[${entries\n .map(\n ([index, id, type, payload]) =>\n `[${index},${JSON.stringify(id)},${JSON.stringify(type)},${payload}]`,\n )\n .join(',')}]}`\n return encoder.encode(carried).byteLength > WAKE_PAYLOAD_LIMIT\n ? bare\n : carried\n}\n\nexport type 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 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\n/**\n * A stream entry's causal edge. Releases before consumers (0.17 to 0.20)\n * recorded only the parent's index and attempt: their edge reads as the\n * handlers table's, `a2.handlers`, as Postgres's upgrade names it, with no\n * lane, since nothing reads an earlier release's lane.\n */\nconst decodeStreamCause = (raw: string): EventCause | null => {\n const parsed = JSON.parse(raw) as { consumer?: unknown }\n return decodeCause(\n parsed.consumer === undefined\n ? { consumer: 'a2.handlers', lane: null, ...parsed }\n : parsed,\n )\n}\n\n/** A stream entry with its causal edge. */\nconst toStoredEvent = (sessionId: string, entry: StreamEntry): StoredEvent => {\n const cause = fieldMap(entry[1])['cause']\n return Object.assign(toEvent(sessionId, entry), {\n cause:\n cause === undefined || cause === '' ? null : decodeStreamCause(cause),\n })\n}\n\ntype ByteStreamEntry = [Uint8Array, Uint8Array[]]\n\nconst fieldBytes = (name: string): Uint8Array => new TextEncoder().encode(name)\nconst ID_FIELD = fieldBytes('id')\nconst TYPE_FIELD = fieldBytes('type')\nconst PAYLOAD_FIELD = fieldBytes('payload')\nconst CREATED_AT_FIELD = fieldBytes('created_at')\n// A leading U+FEFF is part of a stored id or type, never a byte-order mark.\nconst utf8 = new TextDecoder('utf-8', { ignoreBOM: true })\n\nconst isField = (name: Uint8Array, field: Uint8Array): boolean => {\n if (name.length !== field.length) return false\n for (let at = 0; at < field.length; at++)\n if (name[at] !== field[at]) return false\n return true\n}\n\nconst bytesToSerializedEvent = (\n sessionId: string,\n entry: ByteStreamEntry,\n): SerializedEvent => {\n const values = entry[1]\n let id: string | undefined\n let type: string | undefined\n let payloadJson: string | undefined\n let createdAt: string | undefined\n for (let at = 0; at + 1 < values.length; at += 2) {\n const name = values[at]!\n if (isField(name, ID_FIELD)) id = utf8.decode(values[at + 1])\n else if (isField(name, TYPE_FIELD)) type = utf8.decode(values[at + 1])\n else if (isField(name, PAYLOAD_FIELD))\n payloadJson = utf8.decode(values[at + 1])\n else if (isField(name, CREATED_AT_FIELD))\n createdAt = utf8.decode(values[at + 1])\n }\n return {\n id: id!,\n type: type!,\n payloadJson: payloadJson!,\n index: entryIndex(utf8.decode(entry[0])),\n sessionId,\n createdAtMs: Number(createdAt),\n }\n}\n\nconst toSerializedEvent = (\n sessionId: string,\n entry: StreamEntry,\n): SerializedEvent => {\n const fields = fieldMap(entry[1])\n return {\n id: fields['id']!,\n type: fields['type']!,\n payloadJson: fields['payload']!,\n index: entryIndex(entry[0]),\n sessionId,\n createdAtMs: Number(fields['created_at']),\n }\n}\n\nconst json = (value: unknown): string => JSON.stringify(value) ?? 'null'\n\n/** How scripts name a cursor within its consumer: `#`, or `L` and its lane. */\nconst tagOf = (lane: string | null): string =>\n lane === null ? '#' : `L${lane}`\n\n/** The lane a cursor tag names: null for an event cursor. */\nconst laneOf = (tag: string): string | null =>\n tag.startsWith('#') ? null : tag.slice(1)\n\nconst targetsArg = (targets: readonly EventTarget[] | undefined): string =>\n targets === undefined || targets.length === 0\n ? ''\n : JSON.stringify(\n targets.map((target) => [target.consumer, tagOf(target.lane)]),\n )\n\n/** A log consumer's types, as scripts read them; '*' takes every type. */\nconst logTypesArg = (types: readonly string[] | null): string =>\n types === null ? '*' : JSON.stringify(types)\n\n/** The served consumers, as scripts read them; '' serves every consumer. */\nconst servedArg = (consumers: readonly StoreConsumer[] | undefined): string =>\n consumers === undefined\n ? ''\n : JSON.stringify(\n consumers.map((consumer) =>\n consumer.log\n ? [\n consumer.name,\n consumer.log.lane,\n logTypesArg(consumer.log.types),\n consumer.log.backfill ? 1 : 0,\n ]\n : [consumer.name],\n ),\n )\n\ntype Column = string | number | null | undefined\n\n/** A head's dispatch state from a row's HEAD_COLUMNS. */\nconst dispatchOf = (\n consumer: string,\n lane: string | null,\n columns: readonly Column[] | null | undefined,\n): HeadDispatch => {\n const column = (at: number): string | null => {\n const value = columns?.[at]\n return value === null || value === undefined ? null : String(value)\n }\n const date = (at: number): Date | null => {\n const value = column(at)\n return value === null ? null : new Date(Number(value))\n }\n const count = (at: number): number | null => {\n const value = column(at)\n return value === null ? null : Number(value)\n }\n return {\n consumer,\n lane,\n firstClaimedAt: date(1),\n lastClaimedAt: date(2),\n attemptCount: count(0) ?? 0,\n claimHolder: column(3),\n claimExpiresAt: date(4),\n failureCount: count(5) ?? 0,\n lastFailedAt: date(6),\n lastFailedAttempt: count(7),\n lastError: column(8),\n failedAt: date(9),\n }\n}\n\ntype HeadTuple = [\n consumer: string,\n tag: string,\n index: string,\n columns: Column[],\n entry: StreamEntry,\n]\n\nconst toHeadEvent = (sessionId: string, head: HeadTuple): HeadEvent => ({\n ...toStoredEvent(sessionId, head[4]),\n ...dispatchOf(head[0], laneOf(head[1]), head[3]),\n})\n\nconst compareText = (a: string, b: string): number =>\n a < b ? -1 : a > b ? 1 : 0\n\nconst byHead = (a: HeadRef, b: HeadRef): number =>\n a.index - b.index || compareText(a.consumer, b.consumer)\n\n/** Consumer, then lane, with concurrent cursors last by head. */\nconst byCursor = (a: StoredCursor, b: StoredCursor): number =>\n compareText(a.consumer, b.consumer) ||\n (a.lane === null\n ? b.lane === null\n ? (a.head ?? 0) - (b.head ?? 0)\n : 1\n : b.lane === null\n ? -1\n : compareText(a.lane, b.lane))\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 = (reply.slice(2, 2 + count) as HeadTuple[])\n .map((head) => toHeadEvent(sessionId, head))\n .toSorted(byHead)\n const clearIndex = Number(reply[2 + count] ?? 0)\n if (clearIndex > 0)\n for (const event of events) event.clearIndex = clearIndex\n return { outcome: 'claimed', events, head: Number(reply[3 + count]) }\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 toIndex = ({\n raw,\n sessionId,\n name,\n}: {\n raw: unknown\n sessionId: string\n name: string\n}): number => {\n const index = Number(raw ?? 0)\n if (!Number.isSafeInteger(index) || index < 0) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis ${name} for session '${sessionId}' is invalid`,\n )\n }\n return index\n}\n\n/**\n * A range read that met a gap. Reclaim deletes the entries below a clear, so\n * a read whose bounds predate the clear finds nothing where it expected\n * events; anywhere else a gap is corruption.\n */\nconst rangeGap = (sessionId: string, message: string): A2Error =>\n Object.assign(\n new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' ${message}`,\n ),\n { rangeGap: true },\n )\n\nconst isRangeGap = (error: unknown): boolean =>\n error instanceof A2Error &&\n (error as { rangeGap?: boolean }).rangeGap === true\n\n/**\n * Run a whole read, bounds included. A gap means a clear raced it, so the\n * read runs once more from fresh bounds. A second gap, from another clear or\n * from corruption, surfaces as the retryable STORE_UNAVAILABLE.\n */\nconst retryAfterGap = async <T>(read: () => Promise<T>): Promise<T> => {\n try {\n return await read()\n } catch (error) {\n if (!isRangeGap(error)) throw error\n return read()\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\n/**\n * A session's key. Every key of a session shares one hash tag, so a script\n * locks only its session on Upstash and stays in one slot on a cluster. The\n * tag is the session id as JSON with its braces escaped: it can't end early,\n * and no session's tag spells another's.\n */\nexport const redisSessionKey = ({\n prefix,\n sessionId,\n suffix,\n}: {\n prefix: string\n sessionId: string\n suffix: string\n}): string =>\n `${prefix}:{${JSON.stringify(sessionId).replaceAll('{', '\\\\u007b').replaceAll('}', '\\\\u007d')}}:${suffix}`\n\nexport function createRedisStoreCore(\n options: RedisStoreCoreOptions,\n): RedisStoreCore {\n const { call, callBytes, clock } = options\n const generateId = options.ids\n const prefix = options.keyPrefix\n if (prefix.includes('{') || prefix.includes('}')) {\n throw new TypeError(\n 'a redis keyPrefix cannot contain braces: they would set the hash tag of every key',\n )\n }\n\n const sessionKey = (sessionId: string, suffix: string): string =>\n redisSessionKey({ prefix, sessionId, suffix })\n const storeKey = (s: string): string => sessionKey(s, 'log')\n const countKey = (s: string): string => sessionKey(s, 'count')\n // Scripts derive the session's other keys from the counter; keep them in step.\n const clearKey = (s: string): string => sessionKey(s, 'clear')\n const schemaKey = `${prefix}:schema`\n const snapKey = (s: string, r: string): string => sessionKey(s, `snap:${r}`)\n const snapHistoryKey = (s: string, r: string): string =>\n sessionKey(s, `snap-history:${r}`)\n const presenceKey = (s: string): string => sessionKey(s, 'presence')\n // Channels aren't keys; the notify store subscribes by these names.\n const notifyChannel = (s: string): string => `${prefix}:${s}:notify`\n const presenceChannel = (s: string): string => `${prefix}:${s}:presence`\n\n const layoutError = (): A2Error =>\n new A2Error(\n 'STORE_NOT_CONFIGURED',\n `the redis keys under prefix '${prefix}' hold an A2 layout other than '${REDIS_LAYOUT}', from another release; point the store at a fresh keyPrefix, or give the contract a new name so it starts new sessions`,\n )\n\n /**\n * Once per store, before its first script: claim or check the prefix's\n * layout, and ask the server whether scripts may lock only their keys.\n * Only the server's own rejection of the flag means no; a transport error\n * fails this attempt, and the next operation asks again.\n */\n const prepared = retryableLazy(async () => {\n const [, keyLocking] = await Promise.all([\n (async () => {\n await call('SET', schemaKey, REDIS_LAYOUT, 'NX')\n if ((await call('GET', schemaKey)) !== REDIS_LAYOUT) throw layoutError()\n })(),\n call('EVAL', `${KEY_LOCKING}\\nreturn 1`, 1, schemaKey).then(\n () => true,\n (error: unknown) => {\n if (SHEBANG_REJECTED.test(String((error as Error)?.message)))\n return false\n throw error\n },\n ),\n ])\n return { keyLocking }\n })\n\n /** Runs a script; a `readOnly` one takes Upstash's shared lock. */\n const runScript = async ({\n script,\n keys,\n args,\n readOnly,\n }: {\n script: string\n keys: string[]\n args: Array<string | number>\n readOnly: boolean\n }): Promise<unknown> => {\n const { keyLocking } = await prepared.get()\n let source = script\n if (keyLocking)\n source = `${readOnly ? READ_ONLY_KEY_LOCKING : KEY_LOCKING}\\n${script}`\n return call('EVAL', source, keys.length, ...keys, ...args)\n }\n\n /** Runs a writing script over a session: KEYS start with its counter. */\n const evalScript = (\n script: string,\n sessionId: string,\n keys: string[],\n args: Array<string | number>,\n ): Promise<unknown> =>\n runScript({\n script,\n keys: [countKey(sessionId), ...keys],\n args,\n readOnly: false,\n })\n\n /**\n * Publishes a write's wake-up after its script, never inside it: a script\n * holds its lock while PUBLISH waits on subscribers. Wake-ups are hints (a\n * lost one costs subscribers a catch-up read, never an event), so a failed\n * publish is dropped and the write waits for it at most\n * NOTIFY_TIMINGS.publishMs.\n */\n const publish = async (channel: string, message: string): Promise<void> => {\n let timer: ReturnType<typeof setTimeout> | undefined\n await Promise.race([\n call('PUBLISH', channel, message).catch(() => undefined),\n new Promise<void>((resolve) => {\n timer = setTimeout(resolve, NOTIFY_TIMINGS.publishMs)\n ;(timer as { unref?: () => void }).unref?.()\n }),\n ])\n clearTimeout(timer)\n }\n\n /**\n * Runs a write that may need a type's index to reach further back first.\n * Such a write returns ['index', type, after] having written nothing, and\n * runs again once INDEX_TYPE_LUA has indexed that history, a page per call.\n */\n const indexing = async (\n sessionId: string,\n run: () => Promise<unknown>,\n ): Promise<unknown[]> => {\n for (;;) {\n // oxlint-disable-next-line no-await-in-loop -- each run follows the indexing it asked for\n const reply = (await run()) as unknown[]\n if (reply[0] !== 'index') return reply\n for (;;) {\n // oxlint-disable-next-line no-await-in-loop -- each page lowers the index's floor\n const [done] = (await evalScript(\n INDEX_TYPE_LUA,\n sessionId,\n [],\n [String(reply[1]), Number(reply[2])],\n )) as [string]\n if (done === '1') break\n }\n }\n }\n\n /** The frontier and clear marker, in one MGET. */\n const readBounds = async (\n sessionId: string,\n ): Promise<{ frontier: number; clearIndex: number }> => {\n await prepared.get()\n const [count, cleared] = (await call(\n 'MGET',\n countKey(sessionId),\n clearKey(sessionId),\n )) as [unknown, unknown]\n return {\n // A collapsed session keeps only its clear marker.\n frontier: toIndex({\n raw: count ?? cleared,\n sessionId,\n name: 'event count',\n }),\n clearIndex: toIndex({ raw: cleared, sessionId, name: 'clear boundary' }),\n }\n }\n\n const readClearIndex = async (sessionId: string): Promise<number> => {\n await prepared.get()\n return toIndex({\n raw: await call('GET', clearKey(sessionId)),\n sessionId,\n name: 'clear boundary',\n })\n }\n\n const readEventPages = 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 rangeGap(sessionId, `ended before index ${throughIndex}`)\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 rangeGap(sessionId, 'is not contiguous')\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 const readEventRange = async (\n sessionId: string,\n afterIndex: number,\n throughIndex: number,\n ): Promise<Event[]> => {\n const events: Event[] = []\n for (\n let start = afterIndex;\n start < throughIndex;\n start += READ_PAGE_SIZE * READ_CONCURRENCY\n ) {\n const ranges = Array.from(\n {\n length: Math.min(\n READ_CONCURRENCY,\n Math.ceil((throughIndex - start) / READ_PAGE_SIZE),\n ),\n },\n (_, position) => {\n const after = start + position * READ_PAGE_SIZE\n return readEventPages(\n sessionId,\n after,\n Math.min(after + READ_PAGE_SIZE, throughIndex),\n )\n },\n )\n // oxlint-disable-next-line no-await-in-loop -- bound in-flight reads and settle every page before returning\n const results = await Promise.allSettled(ranges)\n for (const result of results) {\n if (result.status === 'rejected') throw result.reason\n events.push(...result.value)\n }\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 (await readBoundedRange(sessionId, afterIndex, throughIndex)).rows\n\n /** `readRange`, with the clear boundary its rows start at. */\n const readBoundedRange = async (\n sessionId: string,\n afterIndex: number,\n throughIndex?: number,\n ): Promise<{ rows: StoredEvent[]; clearIndex: number }> => {\n if (throughIndex !== undefined && throughIndex <= afterIndex)\n return { rows: [], clearIndex: 0 }\n const { frontier: currentFrontier, clearIndex } =\n await readBounds(sessionId)\n const frontier = Math.min(throughIndex ?? currentFrontier, currentFrontier)\n // Reads start at the latest clear marker.\n const after = Math.max(afterIndex, clearIndex - 1)\n if (frontier <= after) return { rows: [], clearIndex }\n\n const rows: StoredEvent[] = []\n let nextIndex = after + 1\n while (nextIndex <= frontier) {\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 `${frontier}-0`,\n 'COUNT',\n READ_PAGE_SIZE,\n )) as StreamEntry[]\n if (entries.length === 0) {\n throw rangeGap(sessionId, `ended before index ${frontier}`)\n }\n const page = entries.map((entry) => toStoredEvent(sessionId, entry))\n for (let position = 0; position < page.length; position += 1) {\n if (page[position]!.index !== nextIndex + position) {\n throw rangeGap(sessionId, 'is not contiguous')\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, clearIndex }\n }\n\n const readStream = options.readStream\n return {\n ...(readStream\n ? {\n streamEvents: async function* ({\n sessionId,\n afterIndex,\n signal,\n }: {\n sessionId: string\n afterIndex: number\n signal: AbortSignal\n }): AsyncGenerator<SerializedEvent[], void> {\n try {\n for await (const entries of readStream({\n key: storeKey(sessionId),\n from: `${afterIndex + 1}-0`,\n signal,\n })) {\n signal.throwIfAborted()\n yield entries.map((entry) =>\n toSerializedEvent(sessionId, entry),\n )\n }\n } catch (cause) {\n if (cause instanceof A2Error) throw cause\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'redis stream read failed',\n { cause },\n )\n }\n },\n }\n : {}),\n async append(sessionId, events, appendOptions) {\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 targets: e.targets,\n }))\n const payloads = withIds.map((e) => json(e.payload))\n const reply = (await indexing(sessionId, () =>\n evalScript(\n APPEND_LUA,\n sessionId,\n [],\n [\n sessionId,\n now,\n withIds.length,\n servedArg(appendOptions?.consumers),\n appendOptions?.originIndex ?? '',\n ...withIds.flatMap((e, i) => [\n e.id,\n e.type,\n payloads[i]!,\n e.cause ? json(e.cause) : '',\n targetsArg(e.targets),\n ]),\n ],\n ),\n )) as string[]\n\n const [verdict] = reply\n if (verdict === 'empty') {\n return { events: [], hasPending: reply[1] === '1' }\n }\n if (verdict === 'cleared') {\n throw sessionClearedError(\n `scheduled work from event ${reply[1]} was cleared in session '${sessionId}'`,\n )\n }\n if (verdict === 'superseded') {\n throw supersededAttemptError(\n sessionId,\n withIds[Number(reply[1]) - 1]!.cause!,\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 const hasPending = reply[1] === '1'\n const firstIndex = Math.min(...reply.slice(2).map(Number))\n // A valid retry occupies exactly events.length consecutive indexes.\n const { rows: existing, clearIndex } = await retryAfterGap(() =>\n readBoundedRange(\n sessionId,\n firstIndex - 1,\n firstIndex + events.length - 1,\n ),\n )\n if (firstIndex < clearIndex) {\n // Cleared after the script matched the retry: the ids are free.\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `session '${sessionId}' was cleared while an append retry replayed; retry it`,\n )\n }\n const replayed = idempotentReplay(events, existing)\n // The first attempt may have committed without publishing.\n await publish(\n notifyChannel(sessionId),\n wakeMessage(firstIndex + events.length - 1, now, []),\n )\n return { events: replayed, hasPending }\n }\n const base = Number(reply[1]) - 1\n await publish(\n notifyChannel(sessionId),\n wakeMessage(\n base + withIds.length,\n now,\n withIds.map((e, i) => [base + 1 + i, e.id, e.type, payloads[i]!]),\n ),\n )\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 })),\n hasPending: reply[2] === '1',\n }\n })\n },\n\n async cursors(sessionId) {\n return wrap(async () => {\n const reply = (await runScript({\n script: CURSORS_LUA,\n keys: [countKey(sessionId)],\n args: [],\n readOnly: true,\n })) as Array<\n [\n consumer: string,\n tag: string,\n position: string,\n head: string | null,\n columns: Column[],\n ]\n >\n return reply\n .map(([consumer, tag, position, head, columns]): StoredCursor =>\n Object.assign(dispatchOf(consumer, laneOf(tag), columns), {\n position: Number(position),\n head: head === null ? null : Number(head),\n }),\n )\n .toSorted(byCursor)\n })\n },\n\n async read(sessionId, opts) {\n return wrap(() =>\n retryAfterGap(() =>\n readRange(sessionId, opts?.afterIndex ?? 0, opts?.throughIndex),\n ),\n )\n },\n\n async readHistory(sessionId, opts) {\n const after = opts?.afterIndex ?? 0\n if (opts?.throughIndex !== undefined && opts.throughIndex <= after)\n return []\n return wrap(() =>\n retryAfterGap(async () => {\n const { frontier, clearIndex } = await readBounds(sessionId)\n // Reads start at the latest clear marker.\n return readEventRange(\n sessionId,\n Math.max(after, clearIndex - 1),\n Math.min(opts?.throughIndex ?? frontier, frontier),\n )\n }),\n )\n },\n\n async claimAvailable({\n sessionId,\n holder,\n ttlMs,\n expiresAtMs,\n exclude,\n consumers,\n maxFailures,\n through,\n }) {\n return wrap(async () => {\n const now = clock.now().getTime()\n const expiresAt = expiresAtMs ?? now + ttlMs\n const excluded = exclude ?? []\n const reply = (await evalScript(\n CLAIM_AVAILABLE_LUA,\n sessionId,\n [],\n [\n holder,\n now,\n expiresAt,\n maxFailures ?? '',\n servedArg(consumers),\n through ?? '',\n excluded.length,\n ...excluded.flatMap((ref) => [\n ref.consumer,\n ref.lane === null ? `#${ref.index}` : `L${ref.lane}`,\n ]),\n ],\n )) as unknown[]\n return toClaimAvailableResult(sessionId, reply)\n })\n },\n\n async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {\n if (claims.length === 0) return { renewed: [], superseded: [] }\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 sessionId,\n [],\n [\n holder,\n now,\n expiresAt,\n claims.length,\n ...claims.flatMap((claim) => [\n claim.consumer,\n tagOf(claim.lane),\n claim.index,\n claim.attempt,\n ]),\n ],\n )) as string[]\n const renewedCount = Number(reply[0])\n const refs = reply.slice(1).map((position): HeadRef => {\n const { consumer, lane, index } = claims[Number(position) - 1]!\n return { consumer, lane, index }\n })\n return {\n renewed: refs.slice(0, renewedCount).toSorted(byHead),\n superseded: refs.slice(renewedCount).toSorted(byHead),\n }\n })\n },\n\n async completeAttempt({\n sessionId,\n consumer,\n lane,\n index,\n attempt,\n events,\n consumers,\n }) {\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 cause: EventCause = { consumer, lane, index, attempt }\n const payloads = events.map((event) => json(event.payload))\n const reply = (await indexing(sessionId, () =>\n evalScript(\n COMPLETE_ATTEMPT_LUA,\n sessionId,\n [],\n [\n sessionId,\n now,\n consumer,\n tagOf(lane),\n index,\n attempt,\n json(cause),\n servedArg(consumers),\n json(ids),\n events.length,\n ...events.flatMap((event, i) => [\n event.id,\n event.type,\n payloads[i]!,\n targetsArg(event.targets),\n ]),\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 if (outcome === 'ok') {\n const base = Number(reply[1]) - 1\n if (events.length > 0) {\n await publish(\n notifyChannel(sessionId),\n wakeMessage(\n base + events.length,\n now,\n events.map((event, i) => [\n base + 1 + i,\n event.id,\n event.type,\n payloads[i]!,\n ]),\n ),\n )\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: { ...cause },\n })),\n } satisfies CompleteAttemptResult\n }\n if (outcome === 'duplicate') {\n if (events.length === 0) {\n return { outcome: 'completed', events: [] }\n }\n const firstIndex = Math.min(...reply.slice(1).map(Number))\n const { rows: existing, clearIndex } = await retryAfterGap(() =>\n readBoundedRange(\n sessionId,\n firstIndex - 1,\n firstIndex + events.length - 1,\n ),\n )\n // Cleared after the script matched the retry.\n if (index <= clearIndex) return { outcome: 'superseded' }\n if (\n existing.some(\n (row) =>\n row.cause?.consumer !== consumer ||\n row.cause.lane !== lane ||\n row.cause.index !== index ||\n row.cause.attempt !== attempt,\n )\n ) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'returned event retry does not match the committed batch',\n )\n }\n const replayed = idempotentReplay(events, existing)\n // The first attempt may have committed without publishing.\n await publish(\n notifyChannel(sessionId),\n wakeMessage(firstIndex + events.length - 1, now, []),\n )\n return { outcome: 'completed', events: replayed }\n }\n throw new TypeError(`unexpected completion outcome '${outcome}'`)\n })\n },\n\n async failAttempt({\n sessionId,\n consumer,\n lane,\n index,\n attempt,\n error,\n maxFailures,\n retryAfterMs,\n }) {\n return wrap(async () => {\n const now = clock.now().getTime()\n const reply = (await evalScript(\n FAIL_ATTEMPT_LUA,\n sessionId,\n [],\n [\n consumer,\n tagOf(lane),\n index,\n attempt,\n error,\n maxFailures,\n now,\n retryAfterMs === undefined ? '' : now + retryAfterMs,\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 return {\n outcome: outcome as 'failed' | 'dead_lettered' | 'superseded',\n failureCount: Number(reply[1]),\n }\n })\n },\n\n async deadLetters(sessionId) {\n return wrap(async () => {\n const reply = (await runScript({\n script: DEAD_LETTERS_LUA,\n keys: [countKey(sessionId)],\n args: [],\n readOnly: true,\n })) as HeadTuple[]\n return reply\n .map((head) => toHeadEvent(sessionId, head))\n .toSorted(byHead)\n })\n },\n\n async redrive({ sessionId, consumer, index, action, consumers }) {\n return wrap(async () => {\n const [redriven, hasPending] = (await indexing(sessionId, () =>\n evalScript(\n REDRIVE_LUA,\n sessionId,\n [],\n [\n consumer,\n index,\n action,\n clock.now().getTime(),\n servedArg(consumers),\n ],\n ),\n )) as [string, string]\n return { redriven: redriven === '1', hasPending: hasPending === '1' }\n })\n },\n\n async wakeLog({ sessionId, consumer }) {\n return wrap(async () => {\n const { name, log } = consumer\n const [, position, head, columns, sessionHead] = (await indexing(\n sessionId,\n () =>\n evalScript(\n WAKE_LOG_LUA,\n sessionId,\n [],\n [name, log.lane, logTypesArg(log.types), log.backfill ? 1 : 0],\n ),\n )) as [\n ok: string,\n position: string,\n head: string | null,\n columns: Column[],\n sessionHead: string,\n ]\n return {\n cursor: Object.assign(dispatchOf(name, log.lane, columns), {\n position: Number(position),\n head: head === null ? null : Number(head),\n }),\n head: Number(sessionHead),\n }\n })\n },\n\n async readState(sessionId, reducerName, stateOptions) {\n return wrap(() =>\n retryAfterGap(async () => {\n const reply = (await runScript({\n script: READ_STATE_LUA,\n keys: [\n countKey(sessionId),\n snapKey(sessionId, reducerName),\n snapHistoryKey(sessionId, reducerName),\n ],\n args: [\n stateOptions?.snapshotThroughIndex ??\n stateOptions?.throughIndex ??\n '',\n ],\n readOnly: true,\n })) as [number | string, string, string, string]\n const rawHead = reply[0]\n const raw = reply[1]\n const headIndex = rawHead === '' ? null : Number(rawHead)\n const parsed =\n raw === ''\n ? null\n : (JSON.parse(raw) as { index: number; state: unknown })\n const currentFrontier = Number(reply[2])\n if (\n !Number.isSafeInteger(currentFrontier) ||\n currentFrontier < 0 ||\n (headIndex !== null &&\n (!Number.isSafeInteger(headIndex) || headIndex < 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 frontier = Math.min(\n currentFrontier,\n stateOptions?.throughIndex ?? currentFrontier,\n )\n const snapshot = parsed\n const clearIndex = Number(reply[3] ?? 0)\n // The fold starts at the latest clear marker when no checkpoint is newer.\n const events = await readEventRange(\n sessionId,\n Math.max(snapshot?.index ?? 0, clearIndex - 1),\n frontier,\n )\n return {\n ...(clearIndex > 0 ? { clearIndex } : {}),\n headIndex,\n snapshot: snapshot\n ? { index: snapshot.index, state: snapshot.state }\n : null,\n events,\n }\n }),\n )\n },\n\n async putSnapshots(sessionId, reducerName, writes) {\n const byIndex = new Map<\n number,\n { index: number; state: unknown; pins: Map<string, HeadRef> }\n >()\n for (const write of writes) {\n const existing = byIndex.get(write.index)\n const pins = existing?.pins ?? new Map<string, HeadRef>()\n for (const pin of write.pins ?? []) {\n pins.set(JSON.stringify([pin.consumer, pin.lane, pin.index]), pin)\n }\n if (existing) existing.state = write.state\n else\n byIndex.set(write.index, {\n index: write.index,\n state: write.state,\n pins,\n })\n }\n const normalized = [...byIndex.values()].toSorted(\n (a, b) => a.index - b.index,\n )\n if (normalized.length === 0) return\n await wrap(async () => {\n const args: Array<string | number> = [normalized.length]\n for (const write of normalized) {\n args.push(\n write.index,\n json({\n index: write.index,\n state: write.state,\n updatedAt: clock.now().getTime(),\n // This reducer is in the session's registry (PUT_SNAPSHOTS_LUA).\n r: 1,\n }),\n write.pins.size,\n ...[...write.pins.values()].flatMap((pin) => [\n pin.consumer,\n tagOf(pin.lane),\n pin.index,\n ]),\n )\n }\n await evalScript(\n PUT_SNAPSHOTS_LUA,\n sessionId,\n [\n snapKey(sessionId, reducerName),\n snapHistoryKey(sessionId, reducerName),\n ],\n [...args, reducerName],\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 nowMs = clock.now().getTime()\n const expiresAtMs = nowMs + 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 runScript({\n script: PRESENCE_SET_LUA,\n keys: [presenceKey(ns)],\n args: [\n atMs,\n meta.ttlMs,\n nowMs,\n entries.length,\n // A null is stored as a tombstone: it keeps its stamp, so an\n // older set that arrives after it still loses.\n ...entries.flatMap(([field, value]) => [\n JSON.stringify([participant, field]),\n json({ a: atMs, e: expiresAtMs, s: meta.seen, v: value }),\n ]),\n ],\n readOnly: false,\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 const published: string[] = []\n for (const position of applied) {\n const [field, value] = entries[Number(position) - 1]!\n appliedValues[field] = structuredClone(value)\n published.push(`${json(field)}:${json(value)}`)\n }\n return {\n patch: {\n participant,\n values: appliedValues,\n seen: meta.seen,\n at: new Date(atMs),\n },\n published: publish(\n presenceChannel(ns),\n `${json({ src: options.presenceSource, participant, seen: meta.seen, at: atMs }).slice(0, -1)},\"values\":{${published.join(',')}}}`,\n ),\n }\n })\n },\n\n async read(ns) {\n return wrap(async () => {\n const key = presenceKey(ns)\n const reply = (await call('HGETALL', key)) as string[]\n const nowMs = clock.now().getTime()\n const rows: PresenceRow[] = []\n const expired: string[] = []\n for (let i = 0; i + 1 < reply.length; i += 2) {\n const entry = JSON.parse(reply[i + 1]!) as {\n v: unknown\n s: number\n a: number\n e: number\n }\n if (entry.e <= nowMs) {\n expired.push(reply[i]!)\n continue\n }\n if (entry.v === null) continue\n const [participant, field] = JSON.parse(reply[i]!) as [\n string,\n string,\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 // The rows are already filtered; a failed prune only defers the\n // deletion to the next read.\n if (expired.length > 0) {\n await runScript({\n script: PRESENCE_PRUNE_LUA,\n keys: [key],\n args: [nowMs, ...expired],\n readOnly: false,\n }).catch(() => undefined)\n }\n return rows\n })\n },\n },\n\n async readRange(sessionId, afterIndex, throughIndex) {\n return wrap(() => readRange(sessionId, afterIndex, throughIndex))\n },\n\n async readTail(sessionId, fromIndex, throughIndex) {\n return wrap(async () => {\n // Entries below the latest clear are an old lifetime's, awaiting\n // reclaim: a tail that reaches them is the boundary-aware reads' job.\n const [newest, clearIndex] = await Promise.all([\n call(\n 'XREVRANGE',\n storeKey(sessionId),\n throughIndex === undefined ? '+' : `${throughIndex}-0`,\n `${fromIndex}-0`,\n 'COUNT',\n READ_PAGE_SIZE,\n ) as Promise<StreamEntry[]>,\n readClearIndex(sessionId),\n ])\n if (clearIndex > fromIndex) return null\n const page = newest\n .toReversed()\n .map((entry) => toEvent(sessionId, entry))\n for (let position = 1; position < page.length; position += 1) {\n if (page[position]!.index !== page[position - 1]!.index + 1) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' is not contiguous`,\n )\n }\n }\n const oldest = page[0]?.index\n return oldest === undefined || oldest <= fromIndex ? page : null\n })\n },\n\n async clear({ sessionId, ifLastIndex, cause }) {\n return wrap(async () => {\n const markerId = generateId()\n const now = clock.now().getTime()\n const reply = (await evalScript(\n CLEAR_LUA,\n sessionId,\n [],\n [\n sessionId,\n now,\n ifLastIndex ?? '',\n cause?.consumer ?? '',\n cause ? tagOf(cause.lane) : '',\n cause?.index ?? '',\n cause?.attempt ?? '',\n markerId,\n ],\n )) as string[]\n if (reply[0] === 'superseded') {\n throw supersededAttemptError(sessionId, cause!)\n }\n if (reply[0] === 'foreign') {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `the clear marker id '${markerId}' already exists`,\n )\n }\n const head = Number(reply[2])\n if (reply[4] === '1') {\n await publish(\n notifyChannel(sessionId),\n wakeMessage(head, now, [\n [head, markerId, CLEAR_EVENT_TYPE, 'null'],\n ]),\n )\n } else if (reply[1] === '1' && head > 0) {\n // An earlier attempt may have appended the marker without publishing.\n await publish(notifyChannel(sessionId), wakeMessage(head, now, []))\n }\n return {\n cleared: reply[1] === '1',\n index: Number(reply[2]),\n clearIndex: Number(reply[3]),\n }\n })\n },\n\n async reclaim(sessionId) {\n return wrap(async () => {\n const pairs = (await evalScript(\n RECLAIM_BATCH_LUA,\n sessionId,\n [],\n [READ_PAGE_SIZE],\n )) as string[]\n if (pairs.length === 0) return { deleted: 0, done: true }\n const [deleted, done] = (await evalScript(\n RECLAIM_LUA,\n sessionId,\n [],\n [sessionId, pairs.length / 2, ...pairs],\n )) as [number, number]\n return { deleted: Number(deleted), done: Number(done) === 1 }\n })\n },\n\n clearIndex: (sessionId) => wrap(() => readClearIndex(sessionId)),\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 'COUNT',\n READ_PAGE_SIZE,\n )) as StreamEntry[]\n return entries.map((entry) => toEvent(sessionId, entry))\n })\n },\n async readFeedPage(sessionId, afterIndex) {\n return wrap(async () => {\n const range = [\n storeKey(sessionId),\n `${afterIndex + 1}-0`,\n '+',\n 'COUNT',\n FEED_PAGE_SIZE,\n ] as const\n if (callBytes) {\n const entries = (await callBytes(\n 'XRANGE',\n ...range,\n )) as ByteStreamEntry[]\n return entries.map((entry) =>\n bytesToSerializedEvent(sessionId, entry),\n )\n }\n const entries = (await call('XRANGE', ...range)) as StreamEntry[]\n return entries.map((entry) => toSerializedEvent(sessionId, entry))\n })\n },\n }\n}\n","import type { PresencePatch } from './contract.ts'\nimport { A2Error } from './errors.ts'\nimport { NOTIFY_TIMINGS, stateTailReaders } from './internal.ts'\nimport { defaultSleep, pollingStream, type Sleeper } from './store-polling.ts'\nimport { pollPresence } from './store-presence-polling.ts'\nimport { presenceRecovery } from './presence-recovery.ts'\nimport { createStreamActivity, streamActivities } from './stream-activity.ts'\nimport {\n bufferedEvents,\n isSerializedEvent,\n parseSerializedEvent,\n type HeldEvent,\n type SerializedEvent,\n} from './stream-buffer.ts'\nimport {\n createRedisStoreCore,\n FEED_PAGE_SIZE,\n type RedisCall,\n type RedisStreamRead,\n} from './store-redis-core.ts'\nimport type { A2Store, Clock, Event, IdSource } from './store.ts'\n\nexport type RedisConnection = {\n call(command: string, ...args: Array<string | number>): Promise<unknown>\n duplicate(): RedisConnection\n on(\n event: 'message',\n listener: (channel: string, message: string) => void,\n ): unknown\n disconnect(): void\n}\n\ntype PresenceWireMessage = {\n src: string\n participant: string\n values: Record<string, unknown>\n seen: number\n at: number\n}\n\ntype PresenceSubscription = {\n listeners: Set<(patch: PresencePatch) => void>\n poll?: ReturnType<typeof pollPresence> | undefined\n recovery?: ReturnType<typeof presenceRecovery> | undefined\n}\n\n/**\n * One live event feed's view of its channel: wake-up messages received\n * since it subscribed, whether it must catch up with XRANGE before trusting\n * them, and the resolver of its park, if parked.\n */\ntype Feed = {\n inbox: string[]\n bytes: number\n dirty: boolean\n /** Wake-ups past a gap, waiting up to NOTIFY_TIMINGS.reorderMs for it. */\n held: Array<{ wake: Wake; bytes: number }>\n gapSince: number | undefined\n wake: (() => void) | null\n stopRead: () => Promise<void>\n}\n\n/** A feed that falls this far behind drops its inbox and catches up. */\nconst INBOX_BYTES = 512 * 1024\n\ntype WakeEntry = [index: number, id: string, type: string, payload: unknown]\ntype Wake = { frontier: number; createdAt: number; entries: WakeEntry[] }\n\n/**\n * Parses a wake-up message: `{\"f\":frontier}` or a bare frontier number\n * announces events without carrying them; `{\"f\",\"t\",\"e\"}` carries them.\n * Returns null for anything else, which callers treat as a hint to catch up.\n */\nconst parseWake = (message: string): Wake | null => {\n let parsed: unknown\n try {\n parsed = JSON.parse(message)\n } catch {\n return null\n }\n if (typeof parsed === 'number') {\n return Number.isSafeInteger(parsed)\n ? { frontier: parsed, createdAt: 0, entries: [] }\n : null\n }\n if (typeof parsed !== 'object' || parsed === null) return null\n const { f, t, e } = parsed as { f?: unknown; t?: unknown; e?: unknown }\n if (typeof f !== 'number' || !Number.isSafeInteger(f)) return null\n if (e === undefined) return { frontier: f, createdAt: 0, entries: [] }\n if (!Array.isArray(e) || typeof t !== 'number' || !Number.isFinite(t))\n return null\n for (const entry of e) {\n if (\n !Array.isArray(entry) ||\n entry.length !== 4 ||\n !Number.isSafeInteger(entry[0]) ||\n typeof entry[1] !== 'string' ||\n typeof entry[2] !== 'string'\n )\n return null\n }\n return { frontier: f, createdAt: t, entries: e as WakeEntry[] }\n}\n\nexport function createRedisNotifyStore(options: {\n client: () => Promise<RedisConnection>\n callBytes?: RedisCall\n closeClient: () => Promise<void>\n clock: Clock\n ids: IdSource\n keyPrefix: string\n onReconnect?: (listener: (channel: string) => void) => void\n polling?: () => boolean\n readStream?: RedisStreamRead\n}): A2Store & { close(): Promise<void> } {\n const { client, clock, ids, keyPrefix: prefix } = options\n const notifyChannel = (sessionId: string): string =>\n `${prefix}:${sessionId}:notify`\n const presenceChannel = (ns: string): string => `${prefix}:${ns}:presence`\n\n const instanceId = crypto.randomUUID()\n const core = createRedisStoreCore({\n call: async (command, ...args) => {\n const c = await client()\n return c.call(command, ...args)\n },\n clock,\n ids,\n keyPrefix: prefix,\n presenceSource: instanceId,\n ...(options.callBytes ? { callBytes: options.callBytes } : {}),\n ...(options.readStream ? { readStream: options.readStream } : {}),\n })\n\n let severed = false\n let subscriber: Promise<RedisConnection> | null = null\n /** channel → live event feeds, registered before they subscribe. */\n const feeds = new Map<string, Set<Feed>>()\n /** channel → refcounted subscription held for each live iterator. */\n const subscriptions = new Map<\n string,\n { refs: number; ready: Promise<unknown> }\n >()\n const presenceSubscriptions = new Map<string, PresenceSubscription>()\n const pollingStreams = new Set<AsyncIterator<Event>>()\n\n options.onReconnect?.((channel) => {\n for (const feed of feeds.get(channel) ?? []) {\n feed.dirty = true\n feed.wake?.()\n }\n void presenceSubscriptions\n .get(channel)\n ?.recovery?.reconnect()\n .catch(() => {})\n })\n\n const deliverRemotePresence = ({\n subscription,\n message,\n }: {\n subscription: PresenceSubscription\n message: string\n }): void => {\n let parsed: PresenceWireMessage\n try {\n parsed = JSON.parse(message) as PresenceWireMessage\n } catch {\n return\n }\n if (parsed.src === instanceId) return\n const patch: PresencePatch = {\n participant: parsed.participant,\n values: parsed.values,\n seen: parsed.seen,\n at: new Date(parsed.at),\n }\n subscription.recovery?.update(patch)\n for (const listener of subscription.listeners) listener(patch)\n }\n\n const getSubscriber = (): Promise<RedisConnection> => {\n subscriber ??= (async () => {\n const conn = (await client()).duplicate()\n conn.on('message', (channel, message) => {\n for (const feed of feeds.get(channel) ?? []) {\n if (feed.bytes + message.length <= INBOX_BYTES) {\n feed.inbox.push(message)\n feed.bytes += message.length\n } else {\n feed.inbox.length = 0\n feed.held.length = 0\n feed.gapSince = undefined\n feed.bytes = 0\n feed.dirty = true\n }\n feed.wake?.()\n }\n const subscription = presenceSubscriptions.get(channel)\n if (subscription) deliverRemotePresence({ subscription, message })\n })\n return conn\n })()\n return subscriber\n }\n\n /**\n * The subscriber, gated on connection readiness — for channel\n * commands only (`close` uses the ungated promise, so it can never\n * hang on an unreachable server). SUBSCRIBE carries redis's\n * ok-loading flag, so ioredis writes it mid-handshake, ahead of the\n * ready check's INFO — which then finds the connection already in\n * subscriber mode and fails the check (a spurious error event plus\n * a reconnect). ECHO has no such flag: it resolves only once the\n * connection is ready, so every channel command chained behind it\n * lands after the check.\n */\n let subscriberGate: Promise<RedisConnection> | null = null\n const gatedSubscriber = (): Promise<RedisConnection> => {\n subscriberGate ??= getSubscriber().then(async (conn) => {\n await conn.call('echo', 'ready')\n return conn\n })\n return subscriberGate\n }\n\n const acquireChannel = async (channel: string): Promise<void> => {\n const existing = subscriptions.get(channel)\n if (existing) {\n existing.refs += 1\n try {\n await existing.ready\n } catch (err) {\n existing.refs -= 1\n throw err\n }\n return\n }\n const lease: { refs: number; ready: Promise<void> } = {\n refs: 1,\n // Lowercase on purpose: ioredis keys its subscriber-mode\n // bookkeeping (and reconnect resubscription) on the exact\n // command name.\n ready: gatedSubscriber().then(async (conn) => {\n await conn.call('subscribe', channel)\n if (subscriptions.get(channel) !== lease || severed) return\n const presence = presenceSubscriptions.get(channel)\n if (options.polling?.() && presence) {\n presence.recovery = undefined\n const ns = channel.slice(prefix.length + 1, -':presence'.length)\n presence.poll = pollPresence({\n read: () => core.presence.read(ns),\n deliver: (patch) => {\n for (const listener of presence.listeners) listener(patch)\n },\n })\n await presence.poll.ready\n }\n return undefined\n }),\n }\n subscriptions.set(channel, lease)\n try {\n await lease.ready\n } catch (err) {\n if (subscriptions.get(channel) === lease) {\n subscriptions.delete(channel)\n const presence = presenceSubscriptions.get(channel)\n presence?.poll?.stop()\n if (presence) presence.poll = undefined\n }\n throw err\n }\n }\n\n const releaseChannel = (channel: string): void => {\n const lease = subscriptions.get(channel)\n if (!lease) return\n lease.refs -= 1\n if (lease.refs > 0) return\n subscriptions.delete(channel)\n void gatedSubscriber()\n .then((conn) => conn.call('unsubscribe', channel))\n .catch(() => {})\n }\n\n const store: A2Store & { close(): Promise<void> } = {\n append: core.append,\n cursors: core.cursors,\n read: core.read,\n readHistory: core.readHistory!,\n claimAvailable: core.claimAvailable,\n renewClaims: core.renewClaims,\n completeAttempt: core.completeAttempt,\n failAttempt: core.failAttempt,\n readState: core.readState,\n putSnapshots: core.putSnapshots,\n clear: core.clear!,\n reclaim: core.reclaim!,\n deadLetters: core.deadLetters!,\n redrive: core.redrive!,\n wakeLog: core.wakeLog,\n\n presence: {\n async set(ns, participant, values, meta) {\n const applied = await core.presence.set(ns, participant, values, meta)\n if (!applied) return\n const { patch } = applied\n const channel = presenceChannel(ns)\n const subscription = presenceSubscriptions.get(channel)\n if (subscription) {\n subscription.poll?.update(patch)\n subscription.recovery?.update(patch)\n for (const listener of subscription.listeners) listener(patch)\n }\n await applied.published\n },\n\n async read(ns) {\n const channel = presenceChannel(ns)\n await subscriptions.get(channel)?.ready\n const subscription = presenceSubscriptions.get(channel)\n return (\n subscription?.recovery?.read() ??\n subscription?.poll?.snapshot(() => core.presence.read(ns)) ??\n core.presence.read(ns)\n )\n },\n\n subscribe(ns, onPatch) {\n const channel = presenceChannel(ns)\n let subscription = presenceSubscriptions.get(channel)\n if (!subscription) {\n subscription = { listeners: new Set() }\n presenceSubscriptions.set(channel, subscription)\n if (options.onReconnect && !options.polling?.()) {\n const current = subscription\n subscription.recovery = presenceRecovery({\n read: () => core.presence.read(ns),\n deliver: (patch) => {\n for (const listener of current.listeners) listener(patch)\n },\n isActive: () =>\n !severed && presenceSubscriptions.get(channel) === current,\n })\n }\n }\n subscription.listeners.add(onPatch)\n // Fire-and-forget on the shared refcounted subscription: it\n // only carries remote patches, so settling late (or failing)\n // degrades cross-instance latency, never same-process\n // delivery.\n void acquireChannel(channel).catch(() => {})\n let stopped = false\n return () => {\n if (stopped) return\n stopped = true\n subscription.listeners.delete(onPatch)\n if (subscription.listeners.size === 0) {\n subscription.poll?.stop()\n presenceSubscriptions.delete(channel)\n }\n releaseChannel(channel)\n }\n },\n },\n\n stream(sessionId, opts) {\n const startAfter = opts?.startAfter ?? 0\n const channel = notifyChannel(sessionId)\n\n return {\n [Symbol.asyncIterator](): AsyncIterator<Event> {\n let last = startAfter\n let buffer: HeldEvent[] = []\n let closed = false\n let subscribed = false\n let pending: Sleeper | null = null\n let poller: AsyncIterator<Event> | undefined\n let stopActivity: (() => void) | undefined\n let catchup:\n | {\n controller: AbortController\n iterator: AsyncGenerator<SerializedEvent[], void>\n delivered: boolean\n }\n | undefined\n let stoppingRead: Promise<void> | undefined\n const stopRead = (): Promise<void> => {\n const reading = catchup\n catchup = undefined\n if (reading) {\n reading.controller.abort()\n stoppingRead = reading.iterator\n .return(undefined)\n .then(() => undefined)\n .catch(() => {})\n }\n return stoppingRead ?? Promise.resolve()\n }\n const nextRead = async (\n after: number,\n ): Promise<IteratorResult<SerializedEvent[], void>> => {\n if (!catchup) {\n const controller = new AbortController()\n catchup = {\n controller,\n delivered: false,\n iterator: core.streamEvents!({\n sessionId,\n afterIndex: after,\n signal: controller.signal,\n })[Symbol.asyncIterator](),\n }\n }\n const reading = catchup\n const result = await reading.iterator.next()\n if (result.done && catchup === reading) {\n catchup = undefined\n if (reading.delivered) feed.dirty = true\n } else if (!result.done) reading.delivered = true\n return result\n }\n const feed: Feed = {\n inbox: [],\n bytes: 0,\n dirty: true,\n held: [],\n gapSince: undefined,\n wake: null,\n stopRead,\n }\n const activity = createStreamActivity(() => {\n if (poller) streamActivities.get(poller)?.activate()\n })\n\n const release = (): void => {\n void stopRead()\n stopActivity?.()\n if (poller) {\n void poller.return?.()\n pollingStreams.delete(poller)\n }\n if (!subscribed) return\n subscribed = false\n const set = feeds.get(channel)\n set?.delete(feed)\n if (set?.size === 0) feeds.delete(channel)\n releaseChannel(channel)\n }\n\n /**\n * Moves carried events that continue this feed's sequence into\n * the buffer. Writers publish after their scripts, so concurrent\n * writers' wake-ups can arrive out of order: they apply in index\n * order, and one past a gap is held until the gap fills or\n * NOTIFY_TIMINGS.reorderMs passes. A message without events past\n * the buffer, or an unreadable one, marks the feed dirty so it\n * catches up with XRANGE.\n */\n const applyInbox = (): void => {\n const carried = feed.held.splice(0)\n let announced = 0\n for (const message of feed.inbox.splice(0)) {\n const wake = parseWake(message)\n if (!wake) feed.dirty = true\n else if (wake.entries.length === 0)\n announced = Math.max(announced, wake.frontier)\n else carried.push({ wake, bytes: message.length })\n }\n carried.sort(\n (a, b) => a.wake.entries[0]![0] - b.wake.entries[0]![0],\n )\n feed.bytes = 0\n for (const held of carried) {\n if (\n held.wake.entries[0]![0] >\n (buffer.at(-1)?.index ?? last) + 1\n ) {\n feed.held.push(held)\n feed.bytes += held.bytes\n continue\n }\n for (const [index, id, type, payload] of held.wake.entries) {\n if (index <= (buffer.at(-1)?.index ?? last)) continue\n buffer.push({\n id,\n type,\n payload,\n index,\n sessionId,\n createdAt: new Date(held.wake.createdAt),\n })\n }\n }\n if (announced > (buffer.at(-1)?.index ?? last)) feed.dirty = true\n if (feed.held.length === 0) feed.gapSince = undefined\n else feed.gapSince ??= Date.now()\n }\n\n const iterator: AsyncIterator<Event> = {\n async next(): Promise<IteratorResult<Event>> {\n for (;;) {\n if (closed) {\n release()\n return { value: undefined, done: true }\n }\n const row = buffer.shift()\n if (row) {\n let event: Event\n try {\n event = isSerializedEvent(row)\n ? parseSerializedEvent(row)\n : row\n } catch (cause) {\n // A retry re-reads from the last delivered event.\n buffer = []\n feed.dirty = true\n release()\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'redis stream read failed',\n { cause },\n )\n }\n last = row.index\n return { value: event, done: false }\n }\n if (severed) {\n release()\n throw new A2Error('STORE_UNAVAILABLE', 'the store was closed')\n }\n try {\n if (!subscribed) {\n // Register and subscribe before the first read:\n // anything appended after it arrives in the inbox,\n // so nothing slips between catch-up and park.\n subscribed = true\n let set = feeds.get(channel)\n if (!set) {\n set = new Set()\n feeds.set(channel, set)\n }\n set.add(feed)\n // A feed starts at the latest clear at the earliest:\n // entries below it await reclaim.\n // oxlint-disable-next-line no-await-in-loop -- one-time setup\n const [, clearIndex] = await Promise.all([\n acquireChannel(channel),\n core.clearIndex(sessionId),\n ])\n if (clearIndex - 1 > last) last = clearIndex - 1\n if (closed || severed) continue\n }\n if (options.polling?.()) {\n if (!poller) {\n poller = pollingStream(\n (after) => core.readEvents(sessionId, after),\n { startAfter: last },\n )[Symbol.asyncIterator]()\n pollingStreams.add(poller)\n stopActivity = streamActivities\n .get(poller)\n ?.subscribe(activity.setIdle)\n }\n // oxlint-disable-next-line no-await-in-loop -- delegate the fallback to the shared poller\n const result = await poller.next()\n if (!result.done)\n presenceSubscriptions\n .get(presenceChannel(sessionId))\n ?.poll?.wake()\n if (severed)\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'the store was closed',\n )\n return result\n }\n if (catchup) {\n // oxlint-disable-next-line no-await-in-loop -- decode the next catch-up batch on demand\n const result = await nextRead(last)\n if (closed || severed) continue\n if (!result.done) buffer = result.value\n continue\n }\n applyInbox()\n if (buffer.length > 0) continue\n if (\n feed.gapSince !== undefined &&\n Date.now() - feed.gapSince >= NOTIFY_TIMINGS.reorderMs\n )\n feed.dirty = true\n if (feed.dirty) {\n feed.dirty = false\n // A read covers whatever the held wake-ups carry.\n feed.held.length = 0\n feed.gapSince = undefined\n if (core.streamEvents) {\n // oxlint-disable-next-line no-await-in-loop -- begin an incremental catch-up\n const result = await nextRead(last)\n if (closed || severed) continue\n if (!result.done) buffer = result.value\n continue\n }\n // oxlint-disable-next-line no-await-in-loop -- live-feed loop\n buffer = await core.readFeedPage(sessionId, last)\n if (buffer.length >= FEED_PAGE_SIZE) feed.dirty = true\n continue\n }\n // Caught up: park until a message, a reconnect, the end of\n // a held gap, or the safety read that heals a lost message.\n const woken = new Promise<'wake'>((resolve) => {\n feed.wake = () => resolve('wake')\n })\n const holding = feed.gapSince !== undefined\n pending = defaultSleep(\n holding\n ? Math.max(\n 0,\n NOTIFY_TIMINGS.reorderMs -\n (Date.now() - feed.gapSince!),\n )\n : NOTIFY_TIMINGS.safetyReadMs,\n )\n // oxlint-disable-next-line no-await-in-loop -- live-feed loop\n const outcome = await Promise.race([\n woken,\n pending.promise.then(() => 'timer' as const),\n ])\n pending.cancel()\n pending = null\n if (outcome === 'timer' && !holding) feed.dirty = true\n } catch (err) {\n release()\n if (closed) return { value: undefined, done: true }\n if (err instanceof A2Error) throw err\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'redis store operation failed',\n { cause: err },\n )\n } finally {\n feed.wake = null\n }\n }\n },\n async return(): Promise<IteratorResult<Event>> {\n closed = true\n pending?.cancel()\n feed.wake?.() // wakes an in-flight next() immediately\n release()\n await stopRead()\n return { value: undefined, done: true }\n },\n }\n streamActivities.set(iterator, activity)\n bufferedEvents.set(iterator, () => {\n if (closed) return undefined\n const row = buffer.shift()\n if (row) last = row.index\n return row\n })\n return iterator\n },\n }\n },\n\n async close() {\n severed = true\n for (const subscription of presenceSubscriptions.values())\n subscription.poll?.stop()\n presenceSubscriptions.clear()\n for (const poller of pollingStreams) void poller.return?.()\n pollingStreams.clear()\n const reads: Promise<void>[] = []\n for (const set of feeds.values()) {\n for (const feed of set) {\n feed.wake?.()\n reads.push(feed.stopRead())\n }\n }\n await Promise.allSettled(reads)\n if (subscriber) {\n const sub = await subscriber.catch(() => null)\n sub?.disconnect()\n }\n await options.closeClient()\n },\n }\n stateTailReaders.set(store, core.readTail)\n return store\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"store-redis-notify-CBDVJxA_.js","names":[],"sources":["../src/presence-recovery.ts","../src/store-redis-core.ts","../src/store-redis-notify.ts"],"sourcesContent":["import type { PresencePatch } from './contract.ts'\nimport { presenceTombstones } from './presence-tombstones.ts'\nimport type { PresenceRow } from './store.ts'\n\ntype Field = Pick<PresenceRow, 'participant' | 'field' | 'seen' | 'at'>\nconst key = ({\n participant,\n field,\n}: Pick<Field, 'participant' | 'field'>): string =>\n JSON.stringify([participant, field])\n\nexport function presenceRecovery({\n read,\n deliver,\n isActive,\n}: {\n read: () => Promise<PresenceRow[]>\n deliver: (patch: PresencePatch) => void\n isActive: () => boolean\n}): {\n read(): Promise<PresenceRow[]>\n reconnect(): Promise<void>\n update(patch: PresencePatch): void\n} {\n const fields = new Map<string, Field>()\n const deleted = presenceTombstones()\n const reading = new Set<Set<string>>()\n let generation = 0\n let latest: Promise<PresenceRow[]> | undefined\n\n const remember = ({ participant, field, seen, at }: Field): void => {\n const id = key({ participant, field })\n const previous = fields.get(id)\n if (\n !previous ||\n previous.at.getTime() < at.getTime() ||\n (previous.at.getTime() === at.getTime() && previous.seen !== seen)\n )\n fields.set(id, { participant, field, seen, at })\n }\n const snapshot = async ({\n reconcile,\n }: {\n reconcile: boolean\n }): Promise<PresenceRow[]> => {\n const run = generation\n const before = new Map(fields)\n const changed = new Set<string>()\n reading.add(changed)\n try {\n const rows = await read()\n if (!isActive()) return rows\n if (run !== generation) return latest!\n // A row older than its field's deletion must not resurrect it.\n const current = rows.filter((row) => !deleted.outranks(row))\n const live = new Set<string>()\n for (const row of current) {\n const id = key(row)\n live.add(id)\n if (changed.has(id)) continue\n deleted.drop(row)\n remember(row)\n if (reconcile)\n deliver({\n participant: row.participant,\n values: { [row.field]: row.value },\n seen: row.seen,\n at: row.at,\n })\n }\n if (reconcile) {\n for (const [id, prior] of before) {\n if (live.has(id) || changed.has(id) || fields.get(id) !== prior)\n continue\n fields.delete(id)\n deleted.keep(prior)\n deliver({\n participant: prior.participant,\n values: { [prior.field]: null },\n seen: prior.seen,\n at: prior.at,\n })\n }\n }\n return current\n } finally {\n reading.delete(changed)\n if (reading.size === 0) latest = undefined\n }\n }\n\n return {\n async read() {\n let pending = latest\n while (pending) {\n // oxlint-disable-next-line no-await-in-loop -- follow replacement reconnects before starting a fresh read\n await pending\n if (latest === pending) break\n pending = latest\n }\n return snapshot({ reconcile: false })\n },\n async reconnect() {\n generation += 1\n latest = snapshot({ reconcile: true })\n await latest\n },\n update(patch) {\n for (const [field, value] of Object.entries(patch.values)) {\n if (value === undefined) continue\n const write = { participant: patch.participant, field, at: patch.at }\n const id = key(write)\n const previous = fields.get(id)\n if (previous && previous.at.getTime() > patch.at.getTime()) continue\n if (deleted.outranks(write)) continue\n for (const changed of reading) changed.add(id)\n if (value === null) {\n fields.delete(id)\n deleted.keep(write)\n } else {\n deleted.drop(write)\n remember({ ...write, seen: patch.seen })\n }\n }\n },\n }\n}\n","/**\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. Stream\n * entries are immutable; dispatch bookkeeping lives on consumer cursors.\n * Every write path is one atomic Lua script.\n *\n * Every key of a session is `{p}:{\"<session id as JSON>\"}:<name>`, one hash\n * tag per session (`redisSessionKey`). Each script declares keys of one\n * session only and derives the rest, so on Upstash a script that sets\n * `allow-key-locking` locks only its session instead of the database. Redis\n * and Valkey reject that flag, so the store asks the server once whether it\n * accepts it.\n *\n * The cursor layout, per session:\n *\n * - `heads:{chunk}` holds one row per (head index, consumer) in hashes of\n * META_CHUNK_SIZE consecutive indexes: a head's attempt and failure\n * fields, its kind (`l`), its lane successor (`next`), its snapshot pins,\n * and, once the cursor moves past it, its completion record. A new head\n * is a new row, so moving a cursor resets its attempt fields for free,\n * and an event cursor costs one row, as a handled event did.\n * - `cursors` is one hash of every lane and log cursor: its head (`h`),\n * position (`p`), lane tail (`t`), or log types (`ty`). A lane's queue is\n * linked through the rows' `next` fields.\n * - `runnable` is a zset of every cursor with a head, scored by the head's\n * index, negated while the head is dead-lettered.\n * - `pins` counts, per snapshot, the heads that pin it.\n *\n * A cursor id names a cursor within its session: the consumer's byte\n * length, `:`, the consumer, then `#{index}` for an event cursor, `L{lane}`\n * for a lane cursor, or `G{lane}` for a log cursor. A clear supersedes every\n * head at or below its marker at once (the `runnable` set is dropped); lane and\n * log cursor records are reset lazily against the marker and swept by\n * reclaim.\n *\n * `{p}:schema` records the layout. A store claims it once, before its first\n * script, and refuses a prefix holding another. Keys of an unrecorded layout\n * have other names, so they are never read.\n *\n * Event ids are unique per session: `id:{event id}` holds the index an id\n * names, and leaves with its session. The conformance suite in\n * test/conformance is the executable contract, run against a real spawned\n * `redis-server`, and over REST behind a shim that applies Upstash's\n * key-locking rules.\n *\n * Timestamps and claim expiry come from the injected clock so tests can\n * time-travel against a real server.\n */\n\nimport type { PresencePatch } from './contract.ts'\nimport { A2Error, supersededAttemptError } from './errors.ts'\nimport { CLEAR_EVENT_TYPE, sessionClearedError } from './session-clear.ts'\nimport { idempotentReplay } from './idempotent-replay.ts'\nimport { NOTIFY_TIMINGS, nullProtoRecord } from './internal.ts'\nimport { retryableLazy } from './retryable-lazy.ts'\nimport { decodeCause } from './store-codec.ts'\nimport type { SerializedEvent } from './stream-buffer.ts'\nimport type {\n A2Store,\n Clock,\n CompleteAttemptResult,\n Event,\n EventCause,\n EventTarget,\n HeadDispatch,\n HeadEvent,\n HeadRef,\n IdSource,\n StoreClaimAvailableResult,\n StoreConsumer,\n StoredCursor,\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 /**\n * Issues a command whose bulk replies arrive as raw bytes. Live feed\n * catch-up pages use it to decode only the fields a row needs.\n */\n callBytes?: RedisCall\n clock: Clock\n ids: IdSource\n keyPrefix: string\n presenceSource: string\n readStream?: RedisStreamRead\n}\n\n/** Stream entries from `from` onward, in nonempty ordered batches. */\nexport type RedisStreamRead = (options: {\n key: string\n from: string\n signal: AbortSignal\n}) => AsyncGenerator<StreamEntry[], void>\n\nexport type RedisStoreCore = Omit<A2Store, 'stream' | 'presence'> & {\n /**\n * The storage half of the presence capability, shared by both\n * transports. `set` returns the applied patch for local loopback, with\n * the publish to remote subscribers already under way, or null when\n * every field lost the LWW race.\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<{ patch: PresencePatch; published: Promise<void> } | null>\n read(ns: string): Promise<PresenceRow[]>\n }\n /**\n * The latest clear marker's index, or 0. Entries below it are an old\n * lifetime's until reclaim deletes them, so a feed starts no lower.\n */\n clearIndex(sessionId: string): Promise<number>\n /** XRANGE pages — the full-row catch-up read. */\n readRange(\n sessionId: string,\n afterIndex: number,\n throughIndex?: number,\n ): Promise<StoredEvent[]>\n /** One bounded XRANGE page of public events for live feeds. */\n readEvents(sessionId: string, afterIndex: number): Promise<Event[]>\n /**\n * One bounded XRANGE page of catch-up rows for a live feed without a\n * streaming reader, payloads left as stored JSON. The reply is held whole,\n * so a page retains up to `FEED_PAGE_SIZE` entries.\n */\n readFeedPage(\n sessionId: string,\n afterIndex: number,\n ): Promise<SerializedEvent[]>\n /** Catch-up events after `afterIndex`, in nonempty ordered batches. */\n streamEvents?: (options: {\n sessionId: string\n afterIndex: number\n signal: AbortSignal\n }) => AsyncGenerator<SerializedEvent[], void>\n /**\n * Events from `fromIndex` through `throughIndex` or the newest entry in\n * one newest-first page, or null when the range doesn't fit in one or\n * starts below the latest clear.\n */\n readTail(\n sessionId: string,\n fromIndex: number,\n throughIndex?: number,\n ): Promise<Event[] | null>\n}\n\nexport const READ_PAGE_SIZE = 256\nexport const FEED_PAGE_SIZE = 1000\nconst READ_CONCURRENCY = 16\n\n/** The key layout this core writes; any other is rejected. */\nexport const REDIS_LAYOUT = 'tagged-1'\n\n/**\n * Upstash runs a script under a database-wide lock unless it opts into\n * locking only its keys' hash tags. Redis and Valkey reject the flag, and\n * have no such lock to opt out of: one thread runs every command.\n */\nconst KEY_LOCKING = '#!lua flags=allow-key-locking'\nconst READ_ONLY_KEY_LOCKING = '#!lua flags=no-writes,allow-key-locking'\nconst SHEBANG_REJECTED = /shebang|unexpected symbol near '#'/i\n\nconst META_CHUNK_SIZE = 1000\n\n/** A head row's dispatch columns, in the order scripts return them. */\nconst HEAD_COLUMNS = `{ 'd', 'fc', 'lc', 'ch', 'ce', 'a', 'lf', 'lfa', 'e', 'f' }`\n\n// ── Lua scripts (each runs atomically) ───────────────────────────────\n\n/**\n * Issues a variadic command in calls of at most 1000 arguments, since Lua's\n * `unpack` is bounded by its stack, and concatenates table replies. An even\n * slice size keeps HSET's field/value pairs together.\n */\nconst SLICED_LUA = `\nlocal function sliced(command, key, args)\n local results = {}\n for first = 1, #args, 1000 do\n local last = math.min(first + 999, #args)\n local reply\n if key then\n reply = redis.call(command, key, unpack(args, first, last))\n else\n reply = redis.call(command, unpack(args, first, last))\n end\n if type(reply) == 'table' then\n for _, value in ipairs(reply) do results[#results + 1] = value end\n end\n end\n return results\nend\n`\n\n/**\n * Upstash bills every call a script makes, so scripts read the counter,\n * clear marker, typed-type index, and event-id owners with one MGET\n * (`read_bounds`), read rows with one HMGET per chunk\n * (`read_rows`), and buffer hash writes (`put`, `drop`) into one HSET and\n * one HDEL per hash (`flush`). Buffered writes are invisible to reads until\n * flushed, so a script must not read a field it has buffered. Every script\n * binds KEYS[1] to the session's counter and derives the session's other\n * keys, which share its hash tag.\n */\nconst BASE_LUA = `${SLICED_LUA}\nlocal COUNTER = KEYS[1]\nlocal SESSION = string.sub(COUNTER, 1, -6)\nlocal LOG, CLEAR = SESSION .. 'log', SESSION .. 'clear'\nlocal META, READY, CURSORS = SESSION .. 'heads', SESSION .. 'runnable', SESSION .. 'cursors'\nlocal PIN_COUNTS, TYPED = SESSION .. 'pins', SESSION .. 'typed'\nlocal CLEAR_TYPE = '${CLEAR_EVENT_TYPE}'\nlocal total, boundary = 0, 0\n-- The event types typed log cursors read: type → { from, last }. The type's\n-- index holds every event of it above \\`from\\`, and no event of it lies above\n-- \\`last\\`.\nlocal typed, typed_dirty = {}, false\n-- One key per event id holds the index it names in this session. The\n-- \\`id:\\` namespace keeps a raw id from spelling another session key.\nlocal function id_key(id) return SESSION .. 'id:' .. id end\n-- Reads the counter, the latest clear marker, the typed types, and the\n-- owners of \\`ids\\` in one MGET. Returns each id's index, or false.\nlocal function read_bounds(ids)\n local keys = { COUNTER, CLEAR, TYPED }\n for _, id in ipairs(ids or {}) do keys[#keys + 1] = id_key(id) end\n local values = sliced('MGET', nil, keys)\n total = tonumber(values[1] or values[2] or '0')\n boundary = tonumber(values[2] or '0')\n if values[3] then typed = cjson.decode(values[3]) end\n local owners = {}\n for i = 4, #keys do owners[i - 3] = values[i] end\n return owners\nend\n-- Event-id owners from read_bounds: an id recorded below the latest clear\n-- is free.\nlocal function owners_of(values)\n local owners = {}\n for i = 1, #values do\n local owner = values[i]\n if owner and tonumber(owner) < boundary then owner = false end\n owners[i] = owner\n end\n return owners\nend\nlocal function entry_index(id) return tonumber(string.match(id, '^(%d+)')) end\nlocal function cid_of(consumer, tag)\n return tostring(string.len(consumer)) .. ':' .. consumer .. tag\nend\nlocal function parse_cid(cid)\n local colon = string.find(cid, ':', 1, true)\n local n = tonumber(string.sub(cid, 1, colon - 1))\n return string.sub(cid, colon + 1, colon + n), string.sub(cid, colon + n + 1)\nend\n-- A cursor's rows: its consumer, then '#' for an event cursor or 'L' .. lane\n-- for a lane or log cursor, as requests name it.\nlocal function row_of(consumer, tag)\n local kind = string.sub(tag, 1, 1)\n if kind == '#' then return cid_of(consumer, '#') end\n if kind == 'G' then return cid_of(consumer, 'L' .. string.sub(tag, 2)) end\n return cid_of(consumer, tag)\nend\nlocal function chunk_key(idx)\n return META .. ':' .. tostring(math.floor((idx - 1) / ${META_CHUNK_SIZE}))\nend\nlocal function row_name(idx, row, suffix)\n return tostring(idx) .. ':' .. row .. ':' .. suffix\nend\n-- Rows of (index, row) refs, aligned with refs; one HMGET per chunk.\nlocal function read_rows(refs, suffixes)\n local groups, order = {}, {}\n for position, ref in ipairs(refs) do\n local key = chunk_key(ref[1])\n local group = groups[key]\n if not group then\n group = { positions = {}, names = {} }\n groups[key] = group\n order[#order + 1] = key\n end\n group.positions[#group.positions + 1] = position\n for _, suffix in ipairs(suffixes) do\n group.names[#group.names + 1] = row_name(ref[1], ref[2], suffix)\n end\n end\n local rows = {}\n for _, key in ipairs(order) do\n local group = groups[key]\n local values = sliced('HMGET', key, group.names)\n for n, position in ipairs(group.positions) do\n local row = {}\n for column = 1, #suffixes do row[column] = values[(n - 1) * #suffixes + column] end\n rows[position] = row\n end\n end\n return rows\nend\nlocal writes, write_order, deletes, delete_order = {}, {}, {}, {}\nlocal function put(key, field, value)\n local fields = writes[key]\n if not fields then\n fields = {}\n writes[key] = fields\n write_order[#write_order + 1] = key\n end\n fields[#fields + 1] = field\n fields[#fields + 1] = value\nend\nlocal function drop(key, field)\n local fields = deletes[key]\n if not fields then\n fields = {}\n deletes[key] = fields\n delete_order[#delete_order + 1] = key\n end\n fields[#fields + 1] = field\nend\nlocal function flush()\n for _, key in ipairs(write_order) do sliced('HSET', key, writes[key]) end\n for _, key in ipairs(delete_order) do sliced('HDEL', key, deletes[key]) end\n writes, write_order, deletes, delete_order = {}, {}, {}, {}\nend\nlocal function put_row(idx, row, suffix, value)\n put(chunk_key(idx), row_name(idx, row, suffix), value)\nend\nlocal function drop_row(idx, row, suffix)\n drop(chunk_key(idx), row_name(idx, row, suffix))\nend\n-- A request names a cursor by tag: '#' for an event cursor, 'L' .. lane for a\n-- lane or log cursor. A claimed row records its kind: '#', or the cursor's tag.\nlocal function owns(kind, tag)\n if not kind then return false end\n if tag == '#' then return kind == '#' end\n return kind == tag or kind == 'G' .. string.sub(tag, 2)\nend\nlocal function cid_for(consumer, kind, idx)\n if kind == '#' then return cid_of(consumer, '#' .. tostring(idx)) end\n return cid_of(consumer, kind)\nend\n-- One type's index, named by the type's bytes in hex so no type can spell\n-- another key of the session.\nlocal function type_key(name)\n return SESSION .. 'type:' .. (string.gsub(name, '.', function(c)\n return string.format('%02x', string.byte(c))\n end))\nend\nlocal ready_changes, ready_order = {}, {}\nlocal function ready_set(cid, score)\n if ready_changes[cid] == nil then ready_order[#ready_order + 1] = cid end\n ready_changes[cid] = score\nend\nlocal function flush_ready()\n local adds, removes = {}, {}\n for _, cid in ipairs(ready_order) do\n local score = ready_changes[cid]\n if score then\n adds[#adds + 1] = score\n adds[#adds + 1] = cid\n else\n removes[#removes + 1] = cid\n end\n end\n sliced('ZADD', READY, adds)\n sliced('ZREM', READY, removes)\n ready_changes, ready_order = {}, {}\nend\nlocal function release_pins(idx, row, pins)\n if not pins or pins == '' then return end\n for _, ref in ipairs(cjson.decode(pins)) do\n local count = redis.call('HINCRBY', PIN_COUNTS, ref, -1)\n if count <= 0 then\n redis.call('HDEL', PIN_COUNTS, ref)\n local checkpoint = cjson.decode(ref)\n redis.call('ZREMRANGEBYSCORE', checkpoint[1], checkpoint[2], checkpoint[2])\n end\n end\n put_row(idx, row, 'pins', '')\nend\n`\n\n/**\n * Cursor movement for scripts that add events or move cursors. Lane and log\n * cursor records are read once per script (`load_states`) and written back\n * once (`flush_states`). A record whose head lies at or below the latest clear\n * marker is idle, and a log cursor counts from the marker.\n *\n * A typed log cursor finds its next head in its types' indexes, one bounded\n * lookup per type, skipped when `typed` shows no newer event of it. When an\n * index does not reach back far enough yet (a typed backfill, or a type a\n * cursor newly reads), the lookup returns the type, and the script returns\n * `{ 'index', type, after }` before writing anything: the caller indexes\n * that type down to `after` (INDEX_TYPE_LUA, one page per call) and runs the\n * operation again.\n */\nconst CURSOR_LUA = `\nlocal served_all, served_set, served_logs = true, {}, {}\nlocal pending_fast = false\nlocal function types_of(raw)\n if not raw or raw == '*' then return nil end\n local set = {}\n for _, name in ipairs(cjson.decode(raw)) do set[name] = true end\n return set\nend\nlocal function takes(types, kind)\n return kind ~= CLEAR_TYPE and (types == nil or types[kind] == true)\nend\n-- The served consumers: '' for every consumer, else [[name] or\n-- [name, lane, typesJson or '*', backfill]].\nlocal function use_served(raw)\n if raw == '' then return end\n served_all = false\n for _, entry in ipairs(cjson.decode(raw)) do\n served_set[entry[1]] = true\n if entry[2] then\n served_logs[#served_logs + 1] = {\n name = entry[1], lane = entry[2], ty = entry[3],\n types = types_of(entry[3]), backfill = entry[4] == 1,\n }\n end\n end\nend\nlocal function served_has(name) return served_all or served_set[name] == true end\n-- Whether a served consumer has a live head. Newest heads come first: a\n-- removed consumer's backlog is older than the work a deployment serves.\nlocal function has_pending()\n if pending_fast then return '1' end\n if served_all then return redis.call('ZCOUNT', READY, 1, '+inf') > 0 and '1' or '0' end\n if next(served_set) == nil then return '0' end\n local page = redis.call('ZREVRANGEBYSCORE', READY, '+inf', 1, 'LIMIT', 0, 32)\n for _, cid in ipairs(page) do\n if served_set[(parse_cid(cid))] then return '1' end\n end\n if #page < 32 then return '0' end\n for _, cid in ipairs(redis.call('ZREVRANGEBYSCORE', READY, '+inf', 1, 'LIMIT', 32, -1)) do\n if served_set[(parse_cid(cid))] then return '1' end\n end\n return '0'\nend\nlocal function targets_of(raw)\n if raw == '' then return {} end\n return cjson.decode(raw)\nend\n-- Starts indexing a type: every later event of it.\nlocal function register(name, from)\n typed[name] = { from, from }\n typed_dirty = true\nend\n-- The first stored event in (after, total] of a type in \\`types\\` (nil: any\n-- type), or false. Nil and a type when that type's index can't answer yet.\nlocal function stored_head(after, types)\n if types == nil then\n if after + 1 <= total then return after + 1 end\n return false\n end\n local best = false\n for name in pairs(types) do\n local entry = typed[name]\n if entry == nil then\n if after < total then return nil, name end\n register(name, total)\n elseif entry[2] > after then\n if entry[1] > after then return nil, name end\n local found = redis.call('ZRANGEBYSCORE', type_key(name),\n '(' .. tostring(after), tostring(total), 'LIMIT', 0, 1)\n if #found > 0 then\n local idx = tonumber(found[1])\n if not best or idx < best then best = idx end\n end\n end\n end\n return best\nend\n-- The first event after \\`after\\` a log cursor takes: stored, then in the\n-- batch being appended.\nlocal function next_log_head(after, types, batch)\n local head, missing = stored_head(after, types)\n if head == nil then return nil, missing end\n if head then return head end\n for _, event in ipairs(batch) do\n if event.idx > after and takes(types, event.type) then return event.idx end\n end\n return false\nend\n-- Indexes a batch's events of the types typed log cursors read.\nlocal function index_batch(batch)\n local adds, order = {}, {}\n for _, event in ipairs(batch) do\n local entry = typed[event.type]\n if entry then\n local list = adds[event.type]\n if not list then\n list = {}\n adds[event.type] = list\n order[#order + 1] = event.type\n end\n list[#list + 1] = event.idx\n list[#list + 1] = tostring(event.idx)\n entry[2] = event.idx\n typed_dirty = true\n end\n end\n for _, name in ipairs(order) do sliced('ZADD', type_key(name), adds[name]) end\nend\nlocal states, state_order = {}, {}\nlocal function load_states(cids)\n local need, names = {}, {}\n for _, cid in ipairs(cids) do\n local state = states[cid]\n if (state == nil or state.partial) and not need[cid] then\n need[cid] = true\n need[#need + 1] = cid\n names[#names + 1] = cid .. ':h'\n names[#names + 1] = cid .. ':p'\n names[#names + 1] = cid .. ':t'\n names[#names + 1] = cid .. ':ty'\n end\n end\n if #need == 0 then return end\n local values = sliced('HMGET', CURSORS, names)\n for n, cid in ipairs(need) do\n local h, p, t, ty = values[n * 4 - 3], values[n * 4 - 2], values[n * 4 - 1], values[n * 4]\n local tail = (t and t ~= '') and tonumber(t) or false\n local state = states[cid]\n if state then\n state.t = tail\n state.partial = nil\n else\n local _, tag = parse_cid(cid)\n state = {\n kind = string.sub(tag, 1, 1), exists = p ~= false,\n h = (h and h ~= '') and tonumber(h) or false,\n p = tonumber(p or '0'), t = tail, ty = ty or false,\n }\n if state.h and state.h <= boundary then state.h = false end\n if state.kind == 'G' and state.exists and state.p < boundary then state.p = boundary end\n states[cid] = state\n state_order[#state_order + 1] = cid\n end\n end\nend\nlocal function flush_states()\n for _, cid in ipairs(state_order) do\n local state = states[cid]\n if state.dirty then\n put(CURSORS, cid .. ':h', state.h and tostring(state.h) or '')\n put(CURSORS, cid .. ':p', tostring(state.p))\n if state.kind == 'L' then\n if not state.partial then put(CURSORS, cid .. ':t', state.t and tostring(state.t) or '') end\n elseif state.ty then\n put(CURSORS, cid .. ':ty', state.ty)\n end\n end\n end\nend\n-- Moves a cursor past its head \\`idx\\`. \\`kind\\` is the head row's kind;\n-- \\`batch\\` is the returned events, appended after the stored log. Writes\n-- nothing; returns nil and a type when that type's index must reach back\n-- to \\`idx\\` first.\nlocal function advance(consumer, kind, idx, row_next, batch)\n if kind == '#' then\n ready_set(cid_of(consumer, '#' .. tostring(idx)), false)\n return true\n end\n local cid = cid_of(consumer, kind)\n local state = states[cid]\n local next_head\n if string.sub(kind, 1, 1) == 'L' then\n next_head = row_next and tonumber(row_next) or false\n if not state then\n state = { kind = 'L', exists = true, p = idx, h = false, t = false, ty = false, partial = true }\n states[cid] = state\n state_order[#state_order + 1] = cid\n end\n if not next_head then\n state.t = false\n state.partial = nil\n end\n else\n local lane, raw = string.sub(kind, 2), nil\n for _, spec in ipairs(served_logs) do\n if spec.name == consumer and spec.lane == lane then raw = spec.ty end\n end\n if not raw then\n load_states({ cid })\n raw = states[cid].ty or '*'\n end\n local head, missing = next_log_head(idx, types_of(raw), batch)\n if head == nil then return nil, missing end\n next_head = head\n state = states[cid]\n if not state then\n state = { kind = 'G', exists = true, p = idx, h = false, t = false, ty = raw }\n states[cid] = state\n state_order[#state_order + 1] = cid\n end\n state.ty = raw\n end\n state.h = next_head\n state.p = idx\n state.exists = true\n state.dirty = true\n ready_set(cid, next_head)\n return true\nend\n-- Reads what an appended batch's cursor work needs: its lane targets'\n-- records, and the head of each served log consumer whose types it carries.\n-- Writes nothing; returns nil, a type, and an index when that type's index\n-- must reach back to the index first.\nlocal log_plan = {}\nlocal function plan(batch, base)\n local need = {}\n for _, event in ipairs(batch) do\n for _, target in ipairs(event.targets) do\n if target[2] ~= '#' then need[#need + 1] = cid_of(target[1], target[2]) end\n end\n end\n for _, spec in ipairs(served_logs) do\n for _, event in ipairs(batch) do\n if takes(spec.types, event.type) then\n local cid = cid_of(spec.name, 'G' .. spec.lane)\n log_plan[#log_plan + 1] = { spec = spec, first = event.idx, cid = cid }\n need[#need + 1] = cid\n break\n end\n end\n end\n load_states(need)\n for _, log in ipairs(log_plan) do\n local state, spec = states[log.cid], log.spec\n if not state.exists then\n if spec.backfill then\n local head, missing = stored_head(boundary, spec.types)\n if head == nil then return nil, missing, boundary end\n log.p, log.h = boundary, head or log.first\n else\n -- Nothing it takes is stored after its start: index its types from there.\n for name in pairs(spec.types or {}) do\n if typed[name] == nil then register(name, base) end\n end\n log.p, log.h = base, log.first\n end\n elseif not state.h then\n -- Not the batch's first: a deployment that doesn't serve this consumer\n -- may have appended events it takes since it went idle.\n local head, missing = stored_head(state.p, spec.types)\n if head == nil then return nil, missing, state.p end\n log.h = head or log.first\n end\n end\n return true\nend\n-- Starts the cursor work \\`plan\\` read: each event's targets, and each\n-- planned log cursor.\nlocal function enqueue(batch)\n for _, event in ipairs(batch) do\n for _, target in ipairs(event.targets) do\n local consumer, tag = target[1], target[2]\n if served_has(consumer) then pending_fast = true end\n if tag == '#' then\n ready_set(cid_of(consumer, '#' .. tostring(event.idx)), event.idx)\n else\n local cid = cid_of(consumer, tag)\n local state = states[cid]\n if state.h and state.t then\n put_row(state.t, row_of(consumer, tag), 'next', tostring(event.idx))\n else\n state.h = event.idx\n if state.p < boundary then state.p = boundary end\n ready_set(cid, event.idx)\n end\n state.t = event.idx\n state.exists = true\n state.dirty = true\n end\n end\n end\n for _, log in ipairs(log_plan) do\n pending_fast = true\n local state = states[log.cid]\n if not state.exists then\n state.exists, state.p, state.h = true, log.p, log.h\n ready_set(log.cid, log.h)\n elseif not state.h then\n state.h = log.h\n ready_set(log.cid, log.h)\n end\n state.ty = log.spec.ty\n state.dirty = true\n end\nend\n`\n\n/**\n * KEYS: counter.\n * ARGV: sessionId, nowMs, n, served consumers, origin index or '', then per\n * event: id, type, payloadJson, causeJson or '', targets.\n * Returns ['empty', hasPending] | ['ok', firstIndex, hasPending] |\n * ['dup', hasPending, ...indexes] | ['partial', count] |\n * ['superseded', position] | ['cleared', originIndex] |\n * ['index', type, after].\n */\nconst APPEND_LUA = `${BASE_LUA}${CURSOR_LUA}\nlocal now, n = ARGV[2], tonumber(ARGV[3])\nuse_served(ARGV[4])\nif n == 0 then return { 'empty', has_pending() } end\nlocal ids = {}\nfor i = 1, n do ids[i] = ARGV[5 + (i - 1) * 5 + 1] end\nlocal owners = owners_of(read_bounds(ids))\nlocal base = total\nlocal dups = {}\nfor i = 1, n do\n if owners[i] then dups[#dups + 1] = owners[i] 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\nlocal origin = ARGV[5]\nif origin ~= '' and tonumber(origin) <= boundary then return { 'cleared', origin } end\n-- The attempt-currency fence: a cause's head row must still be its cursor's\n-- head, at that attempt, not dead-lettered.\nlocal refs, fenced = {}, {}\nfor i = 1, n do\n local raw = ARGV[5 + (i - 1) * 5 + 4]\n if raw ~= '' then\n local cause = cjson.decode(raw)\n local index = tonumber(cause.index)\n if index <= boundary then return { 'superseded', tostring(i) } end\n local tag = type(cause.lane) == 'string' and ('L' .. cause.lane) or '#'\n refs[#refs + 1] = { index, row_of(cause.consumer, tag) }\n fenced[#fenced + 1] = {\n position = i, consumer = cause.consumer, attempt = tonumber(cause.attempt), tag = tag,\n }\n end\nend\nlocal rows = read_rows(refs, { 'd', 'f', 'p', 'l' })\nfor k, cause in ipairs(fenced) do\n local row = rows[k]\n if tonumber(row[1] or '0') ~= cause.attempt or row[2] or row[3] or not owns(row[4], cause.tag) then\n return { 'superseded', tostring(cause.position) }\n end\n if served_has(cause.consumer) then pending_fast = true end\nend\nlocal batch = {}\nfor i = 1, n do\n local o = 5 + (i - 1) * 5\n batch[i] = { idx = base + i, type = ARGV[o + 2], targets = targets_of(ARGV[o + 5]) }\nend\nlocal planned, missing, after = plan(batch, base)\nif not planned then return { 'index', missing, tostring(after) } end\nlocal marks = {}\nfor i = 1, n do\n local o = 5 + (i - 1) * 5\n local field = tostring(base + i)\n redis.call('XADD', LOG, field .. '-0',\n 'id', ARGV[o + 1], 'type', ARGV[o + 2], 'payload', ARGV[o + 3],\n 'created_at', now, 'cause', ARGV[o + 4])\n marks[#marks + 1] = id_key(ARGV[o + 1])\n marks[#marks + 1] = field\nend\nindex_batch(batch)\nenqueue(batch)\nmarks[#marks + 1] = COUNTER\nmarks[#marks + 1] = tostring(base + n)\nif typed_dirty then\n marks[#marks + 1] = TYPED\n marks[#marks + 1] = cjson.encode(typed)\nend\nsliced('MSET', nil, marks)\nflush_states()\nflush()\nflush_ready()\nreturn { 'ok', tostring(base + 1), has_pending() }\n`\n\n/**\n * KEYS: counter.\n * ARGV: type, after. Indexes one page of the stored log below the type's\n * index, toward `after`, registering the type first if no cursor read it\n * yet. Returns ['1'] once the index reaches `after`, else ['0'].\n */\nconst INDEX_TYPE_LUA = `${BASE_LUA}\nread_bounds()\nlocal name, after = ARGV[1], tonumber(ARGV[2])\nlocal entry = typed[name]\nif not entry then\n entry = { total, total }\n typed[name] = entry\nend\n-- Entries below the latest clear belong to an old lifetime: never indexed.\nlocal floor = math.max(after, boundary - 1)\nif entry[1] > floor then\n local entries = redis.call('XREVRANGE', LOG, tostring(entry[1]) .. '-0',\n tostring(floor + 1) .. '-0', 'COUNT', ${READ_PAGE_SIZE})\n local adds = {}\n for _, stored in ipairs(entries) do\n local fields = stored[2]\n for j = 1, #fields, 2 do\n if fields[j] == 'type' then\n if fields[j + 1] == name then\n local idx = entry_index(stored[1])\n adds[#adds + 1] = idx\n adds[#adds + 1] = tostring(idx)\n end\n break\n end\n end\n end\n sliced('ZADD', type_key(name), adds)\n if #entries < ${READ_PAGE_SIZE} then\n entry[1] = after\n else\n entry[1] = entry_index(entries[#entries][1]) - 1\n end\nelseif entry[1] > after then\n entry[1] = after\nend\nredis.call('SET', TYPED, cjson.encode(typed))\nreturn { entry[1] <= after and '1' or '0' }\n`\n\n/**\n * KEYS: counter.\n * ARGV: holder, nowMs, expiresAtMs, maxFailures or '', served consumers,\n * the highest head to claim or '', excluded count, then per excluded head:\n * consumer, tag.\n * Live heads are the ready set's positive scores, so an empty one is enough\n * to report 'settled'. A claimed result ends with the clear marker.\n */\nconst CLAIM_AVAILABLE_LUA = `${BASE_LUA}${CURSOR_LUA}\nlocal ready = redis.call('ZRANGEBYSCORE', READY, 1, ARGV[6] == '' and '+inf' or ARGV[6], 'WITHSCORES')\nif #ready == 0 then return { 'settled' } end\nread_bounds()\nlocal holder, now, expires_at = ARGV[1], tonumber(ARGV[2]), ARGV[3]\nlocal max_failures = tonumber(ARGV[4])\nuse_served(ARGV[5])\nlocal excluded = {}\nfor i = 1, tonumber(ARGV[7]) do\n excluded[ARGV[7 + (i - 1) * 2 + 1] .. '\\\\0' .. ARGV[7 + (i - 1) * 2 + 2]] = true\nend\nlocal candidates = {}\nfor i = 1, #ready, 2 do\n local cid, idx = ready[i], tonumber(ready[i + 1])\n local consumer, tag = parse_cid(cid)\n local kind = string.sub(tag, 1, 1)\n local key = consumer .. '\\\\0' .. (kind == 'G' and ('L' .. string.sub(tag, 2)) or tag)\n if served_has(consumer) and not excluded[key] then\n candidates[#candidates + 1] = {\n idx, row_of(consumer, tag), consumer = consumer, cid = cid,\n kind = kind == '#' and '#' or tag,\n }\n end\nend\nlocal rows = read_rows(candidates, { 'd', 'fc', 'lc', 'ch', 'ce', 'a', 'lf', 'lfa', 'e', 'f', 'pins' })\nlocal takes_over, retry_at, wanted = {}, nil, {}\nfor position, candidate in ipairs(candidates) do\n local row = rows[position]\n local expires = tonumber(row[5] or '0')\n if expires > now then\n if not retry_at or expires < retry_at then retry_at = expires end\n else\n takes_over[#takes_over + 1] = { candidate = candidate, row = row }\n wanted[candidate[1]] = true\n end\nend\nif #takes_over == 0 then\n if retry_at then return { 'busy', tostring(retry_at) } end\n return { 'settled' }\nend\n-- One XRANGE per run of consecutive claimed indexes.\nlocal indexes, entries = {}, {}\nfor idx in pairs(wanted) do indexes[#indexes + 1] = idx end\ntable.sort(indexes)\nlocal first = 1\nwhile first <= #indexes do\n local last = first\n while last < #indexes and indexes[last + 1] == indexes[last] + 1 do last = last + 1 end\n local range = redis.call('XRANGE', LOG, tostring(indexes[first]) .. '-0', tostring(indexes[last]) .. '-0')\n for _, entry in ipairs(range) do entries[entry_index(entry[1])] = entry end\n first = last + 1\nend\nfor _, idx in ipairs(indexes) do\n if not entries[idx] then return { 'missing', tostring(idx) } end\nend\nlocal result = { 'claimed', tostring(#takes_over) }\nfor _, take in ipairs(takes_over) do\n local candidate, row = take.candidate, take.row\n local idx, at = candidate[1], candidate[2]\n local attempt = tostring(tonumber(row[1] or '0') + 1)\n -- A claim still held here lapsed: its attempt never finished.\n local lapsed = row[4] ~= false\n local failures = tonumber(row[6] or '0')\n if lapsed then\n failures = failures + 1\n put_row(idx, at, 'a', tostring(failures))\n end\n put_row(idx, at, 'd', attempt)\n local columns\n if lapsed and max_failures and failures >= max_failures then\n -- That takeover spent the last failed attempt: dead-letter, unclaimed.\n -- Advancing the attempt fences the lapsed one; nothing runs.\n put_row(idx, at, 'f', ARGV[2])\n drop_row(idx, at, 'ch')\n drop_row(idx, at, 'ce')\n ready_set(candidate.cid, -idx)\n release_pins(idx, at, row[11])\n columns = { attempt, row[2], row[3], false, false, tostring(failures), row[7], row[8], row[9], ARGV[2] }\n else\n local first_claimed = row[2] or ARGV[2]\n put_row(idx, at, 'fc', first_claimed)\n put_row(idx, at, 'lc', ARGV[2])\n put_row(idx, at, 'ch', holder)\n put_row(idx, at, 'ce', expires_at)\n put_row(idx, at, 'l', candidate.kind)\n columns = { attempt, first_claimed, ARGV[2], holder, expires_at, tostring(failures), row[7], row[8], row[9], false }\n end\n result[#result + 1] = { candidate.consumer, candidate.kind, tostring(idx), columns, entries[idx] }\nend\nflush()\nflush_ready()\nresult[#result + 1] = tostring(boundary)\nresult[#result + 1] = tostring(total)\nreturn result\n`\n\n/**\n * KEYS: counter.\n * ARGV: holder, nowMs, expiresAtMs, n, then per claim: consumer, tag, index,\n * attempt. Returns [renewedCount, ...renewed, ...superseded] as 1-based\n * positions in the request.\n */\nconst RENEW_CLAIMS_LUA = `${BASE_LUA}\nread_bounds()\nlocal holder, now, expires_at = ARGV[1], tonumber(ARGV[2]), ARGV[3]\nlocal refs, items, superseded = {}, {}, {}\nfor i = 1, tonumber(ARGV[4]) do\n local o = 4 + (i - 1) * 4\n local idx = tonumber(ARGV[o + 3])\n if idx <= boundary then\n superseded[#superseded + 1] = tostring(i)\n else\n local row = row_of(ARGV[o + 1], ARGV[o + 2])\n refs[#refs + 1] = { idx, row }\n items[#items + 1] = { position = i, idx = idx, row = row, tag = ARGV[o + 2], attempt = tonumber(ARGV[o + 4]) }\n end\nend\nlocal rows = read_rows(refs, { 'd', 'ce', 'p', 'f', 'ch', 'l' })\nlocal renewed = {}\nfor k, item in ipairs(items) do\n local row = rows[k]\n local attempt = tonumber(row[1] or '0')\n if row[6] and not owns(row[6], item.tag) then\n -- Another cursor's head: this one never claimed it.\n elseif row[3] or attempt > item.attempt then\n superseded[#superseded + 1] = tostring(item.position)\n elseif attempt == item.attempt and tonumber(expires_at) > now and\n not row[4] and row[5] == holder and tonumber(row[2] or '0') > now then\n put_row(item.idx, item.row, 'ce', expires_at)\n renewed[#renewed + 1] = tostring(item.position)\n end\nend\nflush()\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: counter.\n * ARGV: sessionId, nowMs, consumer, tag, index, attempt, child causeJson,\n * served consumers, returned idsJson, n, then per child: id, type,\n * payloadJson, targets.\n */\nconst COMPLETE_ATTEMPT_LUA = `${BASE_LUA}${CURSOR_LUA}\nlocal now = ARGV[2]\nlocal consumer, tag, idx, attempt = ARGV[3], ARGV[4], tonumber(ARGV[5]), tonumber(ARGV[6])\nlocal at = row_of(consumer, tag)\nlocal n = tonumber(ARGV[10])\nlocal ids = {}\nfor i = 1, n do ids[i] = ARGV[10 + (i - 1) * 4 + 1] end\nlocal owners = owners_of(read_bounds(ids))\nif idx <= boundary then return { 'superseded' } end\nif idx < 1 or idx > total then return { 'missing' } end\nlocal row = read_rows({ { idx, at } }, { 'd', 'p', 'pa', 'ri', 'ch', 'f', 'l', 'next', 'pins' })[1]\nif row[2] then\n -- The cursor moved past this head: a retry of its completion replays.\n if not owns(row[7], tag) then return { 'superseded' } end\n if not row[3] or tonumber(row[3]) ~= attempt then return { 'superseded' } end\n local recorded = cjson.decode(row[4] or '[]')\n if #recorded ~= n then return { 'partial' } end\n for i = 1, n do\n if recorded[i] ~= ARGV[10 + (i - 1) * 4 + 1] then return { 'partial' } end\n end\n local result = { 'duplicate' }\n for i = 1, n do\n if not owners[i] then return { 'partial' } end\n result[#result + 1] = owners[i]\n end\n return result\nend\nif tonumber(row[1] or '0') ~= attempt or not row[5] or row[6] or not owns(row[7], tag) then\n return { 'superseded' }\nend\nfor i = 1, n do\n if owners[i] then return { 'partial' } end\nend\nuse_served(ARGV[8])\nlocal batch = {}\nfor i = 1, n do\n local o = 10 + (i - 1) * 4\n batch[i] = { idx = total + i, type = ARGV[o + 2], targets = targets_of(ARGV[o + 4]) }\nend\nlocal moved, missing = advance(consumer, row[7], idx, row[8], batch)\nif not moved then return { 'index', missing, tostring(idx) } end\nlocal planned, unindexed, after = plan(batch, total)\nif not planned then return { 'index', unindexed, tostring(after) } end\nput_row(idx, at, 'p', now)\nput_row(idx, at, 'pa', ARGV[6])\nput_row(idx, at, 'ri', ARGV[9])\nrelease_pins(idx, at, row[9])\nlocal marks = {}\nfor i = 1, n do\n local o = 10 + (i - 1) * 4\n local field = tostring(total + i)\n redis.call('XADD', LOG, field .. '-0',\n 'id', ARGV[o + 1], 'type', ARGV[o + 2], 'payload', ARGV[o + 3],\n 'created_at', now, 'cause', ARGV[7])\n marks[#marks + 1] = id_key(ARGV[o + 1])\n marks[#marks + 1] = field\nend\nindex_batch(batch)\nenqueue(batch)\nif n > 0 then\n marks[#marks + 1] = COUNTER\n marks[#marks + 1] = tostring(total + n)\nend\nif typed_dirty then\n marks[#marks + 1] = TYPED\n marks[#marks + 1] = cjson.encode(typed)\nend\nsliced('MSET', nil, marks)\nflush_states()\nflush()\nflush_ready()\nreturn { 'ok', tostring(total + 1) }\n`\n\n/**\n * KEYS: counter.\n * ARGV: consumer, tag, index, attempt, error, maxFailures, nowMs, retryAtMs\n * or ''.\n */\nconst FAIL_ATTEMPT_LUA = `${BASE_LUA}\nread_bounds()\nlocal consumer, tag, idx, attempt = ARGV[1], ARGV[2], tonumber(ARGV[3]), tonumber(ARGV[4])\nlocal at = row_of(consumer, tag)\nif idx <= boundary then return { 'superseded', '0' } end\nif idx < 1 or idx > total then return { 'missing', '0' } end\nlocal row = read_rows({ { idx, at } }, { 'a', 'd', 'p', 'f', 'lfa', 'ch', 'l', 'pins' })[1]\nif row[3] or (row[7] and not owns(row[7], tag)) then return { 'superseded', '0' } end\nlocal failures = tonumber(row[1] or '0')\nif tonumber(row[2] or '0') ~= attempt then return { 'superseded', tostring(failures) } end\nif not row[6] and row[5] and tonumber(row[5]) == attempt then\n return { row[4] and 'dead_lettered' or 'failed', tostring(failures) }\nend\nif row[4] then return { 'dead_lettered', tostring(failures) } end\nif not row[6] then return { 'superseded', tostring(failures) } end\nfailures = failures + 1\nlocal dead = failures >= tonumber(ARGV[6])\nput_row(idx, at, 'a', tostring(failures))\nput_row(idx, at, 'e', ARGV[5])\nput_row(idx, at, 'lf', ARGV[7])\nput_row(idx, at, 'lfa', ARGV[4])\ndrop_row(idx, at, 'ch')\nif dead then\n put_row(idx, at, 'f', ARGV[7])\n drop_row(idx, at, 'ce')\n ready_set(cid_for(consumer, row[7], idx), -idx)\n release_pins(idx, at, row[8])\nelseif ARGV[8] ~= '' then\n put_row(idx, at, 'ce', ARGV[8])\nelse\n drop_row(idx, at, 'ce')\nend\nflush()\nflush_ready()\nreturn { dead and 'dead_lettered' or 'failed', tostring(failures) }\n`\n\n/**\n * KEYS: counter. ARGV: consumer, index, action, nowMs, served\n * consumers. Returns [redriven, hasPending] | ['index', type, after]. Acts\n * only on the consumer's dead-lettered head at the index, above the clear\n * marker.\n */\nconst REDRIVE_LUA = `${BASE_LUA}${CURSOR_LUA}\nread_bounds()\nuse_served(ARGV[5])\nlocal consumer, idx = ARGV[1], tonumber(ARGV[2])\nlocal function result(redriven) return { redriven, has_pending() } end\nif idx <= boundary or idx < 1 or idx > total then return result('0') end\nlocal cid, tag = nil, nil\nfor _, member in ipairs(redis.call('ZRANGEBYSCORE', READY, -idx, -idx)) do\n local owner, member_tag = parse_cid(member)\n if owner == consumer then\n cid, tag = member, member_tag\n break\n end\nend\nif not cid then return result('0') end\nlocal kind = string.sub(tag, 1, 1) == '#' and '#' or tag\nlocal at = row_of(consumer, tag)\nif ARGV[3] == 'skip' then\n local row = read_rows({ { idx, at } }, { 'next' })[1]\n local moved, missing = advance(consumer, kind, idx, row[1], {})\n if not moved then return { 'index', missing, tostring(idx) } end\n put_row(idx, at, 'p', ARGV[4])\n flush_states()\n if typed_dirty then redis.call('SET', TYPED, cjson.encode(typed)) end\nelse\n drop_row(idx, at, 'f')\n drop_row(idx, at, 'a')\n ready_set(cid, idx)\nend\nflush()\nflush_ready()\nreturn result('1')\n`\n\n/**\n * KEYS: counter. ARGV: consumer, lane, types JSON or '*', backfill\n * ('1' or '0'). Brings one log cursor up to date without an append, as\n * `plan` and `enqueue` would for an append carrying its types: a missing\n * cursor starts at the clear marker with backfill, else at the head with\n * nothing to run; an idle one takes the next stored event of its types; one\n * with a head, claimed or not, keeps it. Records the types, and writes\n * nothing when nothing changed. Returns ['ok', position, head or nil, head\n * columns, session head] | ['index', type, after].\n */\nconst WAKE_LOG_LUA = `${BASE_LUA}${CURSOR_LUA}\nread_bounds()\nlocal consumer, lane, ty = ARGV[1], ARGV[2], ARGV[3]\nlocal types = types_of(ty)\nlocal cid = cid_of(consumer, 'G' .. lane)\nload_states({ cid })\nlocal state = states[cid]\nlocal had_head = state.h\n-- Nothing after the latest clear: a missing cursor stays missing.\nif not state.exists and total == boundary then\n return { 'ok', tostring(boundary), false, {}, tostring(total) }\nend\nif not state.exists then\n state.exists, state.p = true, total\n if ARGV[4] == '1' then\n local head, missing = stored_head(boundary, types)\n if head == nil then return { 'index', missing, tostring(boundary) } end\n state.p, state.h = boundary, head\n else\n -- Nothing it takes is stored after its start: index its types from there.\n for name in pairs(types or {}) do\n if typed[name] == nil then register(name, total) end\n end\n end\n state.dirty = true\nelseif not state.h then\n -- A deployment that doesn't serve this consumer may have appended events\n -- it takes since it went idle.\n local head, missing = stored_head(state.p, types)\n if head == nil then return { 'index', missing, tostring(state.p) } end\n if head then state.h, state.dirty = head, true end\nend\nif state.ty ~= ty then state.ty, state.dirty = ty, true end\nlocal columns = {}\nif had_head then\n columns = read_rows({ { state.h, row_of(consumer, 'G' .. lane) } }, ${HEAD_COLUMNS})[1]\nelseif state.h then\n ready_set(cid, state.h)\nend\nflush_states()\nif typed_dirty then redis.call('SET', TYPED, cjson.encode(typed)) end\nflush()\nflush_ready()\nreturn { 'ok', tostring(state.p), state.h and tostring(state.h) or false, columns, tostring(total) }\n`\n\n/**\n * KEYS: counter. Returns the dead-lettered heads above the clear\n * marker as [consumer, tag, index, columns, entry] tuples.\n */\nconst DEAD_LETTERS_LUA = `${BASE_LUA}\nlocal dead = redis.call('ZRANGEBYSCORE', READY, '-inf', '(0', 'WITHSCORES')\nif #dead == 0 then return {} end\nread_bounds()\nlocal refs, items = {}, {}\nfor i = 1, #dead, 2 do\n local consumer, tag = parse_cid(dead[i])\n local idx = -tonumber(dead[i + 1])\n if idx > boundary then\n refs[#refs + 1] = { idx, row_of(consumer, tag) }\n items[#items + 1] = { consumer, tag, idx }\n end\nend\nlocal rows = read_rows(refs, ${HEAD_COLUMNS})\nlocal result = {}\nfor k, item in ipairs(items) do\n local entries = redis.call('XRANGE', LOG, tostring(item[3]) .. '-0', tostring(item[3]) .. '-0')\n if #entries > 0 then\n result[#result + 1] = { item[1], item[2], tostring(item[3]), rows[k], entries[1] }\n end\nend\nreturn result\n`\n\n/**\n * KEYS: counter. Returns every listed cursor as [consumer, tag,\n * position, head or nil, columns]: lane and log cursors from the cursor\n * hash, read against the clear marker, then the event cursors with a head.\n */\nconst CURSORS_LUA = `${BASE_LUA}\nread_bounds()\nlocal listed = {}\nlocal fields = redis.call('HGETALL', CURSORS)\nlocal records, order = {}, {}\nfor i = 1, #fields, 2 do\n local at = string.find(fields[i], ':[^:]*$')\n local cid, suffix = string.sub(fields[i], 1, at - 1), string.sub(fields[i], at + 1)\n if not records[cid] then\n records[cid] = {}\n order[#order + 1] = cid\n end\n records[cid][suffix] = fields[i + 1]\nend\nfor _, cid in ipairs(order) do\n local record = records[cid]\n local consumer, tag = parse_cid(cid)\n local head = (record.h and record.h ~= '') and tonumber(record.h) or false\n local position = tonumber(record.p or '0')\n if head and head <= boundary then head = false end\n local kind = string.sub(tag, 1, 1)\n if kind == 'G' and position < boundary then position = boundary end\n if kind == 'G' or head or position > boundary then\n listed[#listed + 1] = { consumer, tag, position, head }\n end\nend\nlocal ready = redis.call('ZRANGE', READY, 0, -1, 'WITHSCORES')\nfor i = 1, #ready, 2 do\n local consumer, tag = parse_cid(ready[i])\n if string.sub(tag, 1, 1) == '#' then\n local head = math.abs(tonumber(ready[i + 1]))\n listed[#listed + 1] = { consumer, '#', head - 1, head }\n end\nend\nlocal refs, positions = {}, {}\nfor position, cursor in ipairs(listed) do\n if cursor[4] then\n refs[#refs + 1] = { cursor[4], row_of(cursor[1], cursor[2]) }\n positions[#refs] = position\n end\nend\nlocal rows = read_rows(refs, ${HEAD_COLUMNS})\nlocal columns = {}\nfor k, position in ipairs(positions) do columns[position] = rows[k] end\nlocal result = {}\nfor position, cursor in ipairs(listed) do\n result[#result + 1] = {\n cursor[1], cursor[2], tostring(cursor[3]),\n cursor[4] and tostring(cursor[4]) or false, columns[position] or {},\n }\nend\nreturn result\n`\n\n/**\n * KEYS: counter, head, history.\n * ARGV: write count, then per write: index, snapshot JSON, pin count, then\n * per pin: consumer, tag, index; finally the reducer name. Writes from\n * before the latest clear are dropped, and a head from before it is\n * replaced as if absent. A pin holds only on an unfinished, live head: a\n * claimed row says so itself, an unclaimed head by its cursor's ready score.\n */\nconst PUT_SNAPSHOTS_LUA = `${BASE_LUA}\nread_bounds()\nlocal HEAD, HISTORY = KEYS[2], KEYS[3]\nlocal entries, refs, pins = {}, {}, {}\nlocal offset = 2\nfor w = 1, tonumber(ARGV[1]) do\n local index = tonumber(ARGV[offset])\n local pin_count = tonumber(ARGV[offset + 2])\n local entry = { index = index, snapshot = ARGV[offset + 1], pins = {} }\n for p = 1, pin_count do\n local o = offset + 2 + (p - 1) * 3\n local pin = { consumer = ARGV[o + 1], tag = ARGV[o + 2], idx = tonumber(ARGV[o + 3]) }\n if index >= boundary and pin.idx > boundary and pin.idx <= total then\n pin.row = row_of(pin.consumer, pin.tag)\n refs[#refs + 1] = { pin.idx, pin.row }\n pins[#pins + 1] = pin\n entry.pins[#entry.pins + 1] = pin\n end\n end\n entries[w] = entry\n offset = offset + 3 + pin_count * 3\nend\nlocal function live(consumer, tag, idx)\n return tonumber(redis.call('ZSCORE', READY, cid_of(consumer, tag)) or '0') == idx\nend\nlocal rows = read_rows(refs, { 'pins', 'p', 'f', 'l' })\nlocal held, held_order = {}, {}\nfor k, pin in ipairs(pins) do\n local row = rows[k]\n local valid\n if row[2] or row[3] then\n valid = false\n elseif row[4] then\n valid = owns(row[4], pin.tag)\n elseif pin.tag == '#' then\n valid = live(pin.consumer, '#' .. tostring(pin.idx), pin.idx)\n else\n valid = live(pin.consumer, pin.tag, pin.idx) or\n live(pin.consumer, 'G' .. string.sub(pin.tag, 2), pin.idx)\n end\n if valid then\n local key = tostring(pin.idx) .. ':' .. pin.row\n if not held[key] then\n local list = (row[1] and row[1] ~= '') and cjson.decode(row[1]) or {}\n local set = {}\n for _, ref in ipairs(list) do set[ref] = true end\n held[key] = { list = list, set = set, idx = pin.idx, row = pin.row }\n held_order[#held_order + 1] = key\n end\n pin.held = held[key]\n end\nend\nlocal snapshots = {}\nfor _, entry in ipairs(entries) do\n local ref = cjson.encode({ HISTORY, tostring(entry.index) })\n for _, pin in ipairs(entry.pins) do\n if pin.held and not pin.held.set[ref] then\n pin.held.set[ref] = true\n pin.held.list[#pin.held.list + 1] = ref\n pin.held.dirty = true\n redis.call('HINCRBY', PIN_COUNTS, ref, 1)\n end\n end\n if entry.index >= boundary then\n snapshots[#snapshots + 1] = { index = entry.index, snapshot = entry.snapshot, ref = ref }\n end\nend\nfor _, key in ipairs(held_order) do\n local row = held[key]\n if row.dirty then put_row(row.idx, row.row, 'pins', cjson.encode(row.list)) end\nend\nflush()\nif #snapshots == 0 then return 1 end\n\nlocal head = redis.call('GET', HEAD)\nlocal head_index = nil\nlocal registered = false\nif head then\n local record = cjson.decode(head)\n head_index = tonumber(record['index'])\n registered = record['r'] == 1\nend\nif head_index and head_index < boundary then\n head, head_index, registered = nil, nil, false\n redis.call('ZREMRANGEBYSCORE', HISTORY, '-inf', '(' .. boundary)\nend\n-- Deletion finds each reducer's keys through this set instead of a scan.\n-- Heads written since carry the mark, so a reducer registers once.\nif not registered then\n redis.call('SADD', SESSION .. 'snap-reducers', ARGV[#ARGV])\nend\nlocal latest = snapshots[#snapshots]\nlocal writes_head = not head_index or latest.index >= head_index\nlocal advances = not head_index or latest.index > head_index\nlocal final_head_index = advances and latest.index or head_index\n\nif advances and head and tonumber(redis.call('HGET', PIN_COUNTS, cjson.encode({ HISTORY, tostring(head_index) })) or '0') > 0 then\n redis.call('ZREMRANGEBYSCORE', HISTORY, head_index, head_index)\n redis.call('ZADD', HISTORY, head_index, head)\nend\n\nfor _, write in ipairs(snapshots) do\n if write.index < final_head_index then\n redis.call('ZREMRANGEBYSCORE', HISTORY, write.index, write.index)\n if tonumber(redis.call('HGET', PIN_COUNTS, write.ref) or '0') > 0 then\n redis.call('ZADD', HISTORY, write.index, write.snapshot)\n end\n end\nend\n\nif writes_head then\n redis.call('SET', HEAD, latest.snapshot)\n redis.call('ZREMRANGEBYSCORE', HISTORY, latest.index, latest.index)\nend\nreturn 1\n`\n\n/**\n * Presence entries are `{\"a\":at,\"e\":expiresAt,\"s\":seen,\"v\":value}`, stamps\n * first and both integers, so scripts read them with a pattern instead of\n * decoding the value.\n */\nconst PRESENCE_STAMPS_LUA = `\nlocal function stamps(entry)\n local a, e = string.match(entry, '^{\"a\":(%-?%d+),\"e\":(%-?%d+),')\n return tonumber(a), tonumber(e)\nend\n`\n\n/**\n * KEYS: presence hash. ARGV: atMs, ttlMs, nowMs, n, then per entry:\n * hashField, entryJson (a null value is a tombstone). Field-wise LWW by the\n * sender's `at`: an unexpired stored entry with a strictly newer stamp\n * survives; ties go to the incoming write (memory-tier parity). Returns the applied 1-based entry\n * positions. The whole-key PEXPIRE is the coarse bound, sound because core\n * sends one uniform ttlMs; the per-entry `e` stamp is the fine bound (the\n * injected storage clock at the applied set plus ttlMs — never the sender\n * stamp), filtered on read against the same clock.\n */\nconst PRESENCE_SET_LUA = `${SLICED_LUA}${PRESENCE_STAMPS_LUA}\nlocal at, now, n = tonumber(ARGV[1]), tonumber(ARGV[3]), tonumber(ARGV[4])\nlocal fields = {}\nfor i = 1, n do fields[i] = ARGV[4 + (i - 1) * 2 + 1] end\nlocal stored = sliced('HMGET', KEYS[1], fields)\nlocal applied, sets = {}, {}\nfor i = 1, n do\n local a, e = false, false\n if stored[i] then a, e = stamps(stored[i]) end\n if not stored[i] or a <= at or e <= now then\n sets[#sets + 1] = fields[i]\n sets[#sets + 1] = ARGV[4 + (i - 1) * 2 + 2]\n applied[#applied + 1] = tostring(i)\n end\nend\nsliced('HSET', KEYS[1], sets)\nif #applied > 0 then redis.call('PEXPIRE', KEYS[1], ARGV[2]) end\nreturn applied\n`\n\n/**\n * KEYS: presence hash. ARGV: nowMs, then the fields a read saw expired.\n * Deletes those still at or past their expiry, re-checked in this atomic\n * step so a stale reader never sweeps a concurrently refreshed field.\n */\nconst PRESENCE_PRUNE_LUA = `${SLICED_LUA}${PRESENCE_STAMPS_LUA}\nlocal now = tonumber(ARGV[1])\nlocal fields = {}\nfor i = 2, #ARGV do fields[#fields + 1] = ARGV[i] end\nlocal stored = sliced('HMGET', KEYS[1], fields)\nlocal stale = {}\nfor i = 1, #fields do\n if stored[i] then\n local _, e = stamps(stored[i])\n if e <= now then stale[#stale + 1] = fields[i] end\n end\nend\nsliced('HDEL', KEYS[1], stale)\nreturn 1\n`\n\n/**\n * KEYS: counter, head, history. ARGV: snapshot frontier or ''.\n * Returns one atomic multiversion-cache-plus-log-frontier read, with the\n * clear boundary; checkpoints from before it are ignored.\n */\nconst READ_STATE_LUA = `\nlocal session = string.sub(KEYS[1], 1, -6)\nlocal counts = redis.call('MGET', KEYS[1], session .. 'clear')\nlocal boundary = tonumber(counts[2] or '0')\nlocal head = redis.call('GET', KEYS[2])\nlocal head_index = head and cjson.decode(head)['index'] or nil\nif head and (type(head_index) ~= 'number' or head_index < 0 or head_index > 9007199254740991 or head_index ~= math.floor(head_index)) then\n return redis.error_reply('invalid snapshot head index')\nend\nif head_index and head_index < boundary then\n head = nil\n head_index = nil\nend\nlocal cap = ARGV[1]\nlocal snapshot = nil\nif head_index and (cap == '' or head_index <= tonumber(cap)) then\n snapshot = head\nelse\n local upper = cap == '' and '+inf' or cap\n local historical = redis.call('ZREVRANGEBYSCORE', KEYS[3], upper, boundary, 'LIMIT', 0, 1)\n if #historical > 0 then snapshot = historical[1] end\nend\nreturn { head_index or '', snapshot or '', counts[1] or counts[2] or '0', tostring(boundary) }\n`\n\n/**\n * What reclaim removes once no entry is left below the latest clear: every\n * registered reducer's checkpoints below the latest clear and the pin counts\n * naming them (snapshot writes register each reducer in\n * {prefix}:{session}:snap-reducers, so no key scan is needed), every head\n * row below it (whole chunks, then the boundary chunk's older rows), the\n * type indexes' older entries, and the lane cursors it left idle.\n *\n * A lifetime that ended empty then collapses to the clear marker, the log\n * holding it (readers resuming before the marker still read it), and the log\n * cursors the sweep reset to the marker. The counter reads from the marker\n * when absent, type indexes rebuild lazily, and the marker's own id key goes\n * too.\n */\nconst SWEEP_LUA = `\nlocal function sweep(boundary)\n for _, reducer in ipairs(redis.call('SMEMBERS', SESSION .. 'snap-reducers')) do\n local head_key = SESSION .. 'snap:' .. reducer\n local head = redis.call('GET', head_key)\n if head and tonumber(cjson.decode(head)['index']) < boundary then\n redis.call('DEL', head_key)\n end\n redis.call('ZREMRANGEBYSCORE', SESSION .. 'snap-history:' .. reducer, '-inf', '(' .. boundary)\n end\n local counts = redis.call('HGETALL', PIN_COUNTS)\n local stale = {}\n for i = 1, #counts, 2 do\n if tonumber(cjson.decode(counts[i])[2]) < boundary then stale[#stale + 1] = counts[i] end\n end\n sliced('HDEL', PIN_COUNTS, stale)\n local raw = redis.call('GET', TYPED)\n if raw then\n for name in pairs(cjson.decode(raw)) do\n redis.call('ZREMRANGEBYSCORE', type_key(name), '-inf', '(' .. boundary)\n end\n end\n local last = math.floor((boundary - 1) / ${META_CHUNK_SIZE})\n local chunks = {}\n for chunk = 0, last - 1 do chunks[#chunks + 1] = META .. ':' .. tostring(chunk) end\n sliced('UNLINK', nil, chunks)\n local older = {}\n for _, field in ipairs(redis.call('HKEYS', META .. ':' .. tostring(last))) do\n local idx = tonumber(string.match(field, '^(%d+):'))\n if idx and idx < boundary then older[#older + 1] = field end\n end\n sliced('HDEL', META .. ':' .. tostring(last), older)\n local fields = redis.call('HGETALL', CURSORS)\n local records, order = {}, {}\n for i = 1, #fields, 2 do\n local at = string.find(fields[i], ':[^:]*$')\n local cid, suffix = string.sub(fields[i], 1, at - 1), string.sub(fields[i], at + 1)\n if not records[cid] then\n records[cid] = {}\n order[#order + 1] = cid\n end\n records[cid][suffix] = fields[i + 1]\n end\n local idle, moved = {}, {}\n for _, cid in ipairs(order) do\n local record = records[cid]\n local _, tag = parse_cid(cid)\n local head = (record.h and record.h ~= '') and tonumber(record.h) or false\n local position = tonumber(record.p or '0')\n if head and head <= boundary then head = false end\n if string.sub(tag, 1, 1) == 'G' then\n if position < boundary then position = boundary end\n moved[#moved + 1] = cid .. ':h'\n moved[#moved + 1] = head and tostring(head) or ''\n moved[#moved + 1] = cid .. ':p'\n moved[#moved + 1] = tostring(position)\n elseif not head and position <= boundary then\n for suffix in pairs(record) do idle[#idle + 1] = cid .. ':' .. suffix end\n elseif not head and record.h ~= '' then\n moved[#moved + 1] = cid .. ':h'\n moved[#moved + 1] = ''\n end\n end\n sliced('HDEL', CURSORS, idle)\n sliced('HSET', CURSORS, moved)\n local count = redis.call('GET', COUNTER)\n if not count or tonumber(count) ~= boundary or redis.call('ZCARD', READY) > 0 then\n return\n end\n local stale = { COUNTER, TYPED, PIN_COUNTS, chunk_key(boundary) }\n local reducers = SESSION .. 'snap-reducers'\n for _, reducer in ipairs(redis.call('SMEMBERS', reducers)) do\n stale[#stale + 1] = SESSION .. 'snap:' .. reducer\n stale[#stale + 1] = SESSION .. 'snap-history:' .. reducer\n end\n stale[#stale + 1] = reducers\n local types = redis.call('GET', TYPED)\n if types then\n for name in pairs(cjson.decode(types)) do stale[#stale + 1] = type_key(name) end\n end\n local marker = redis.call('XRANGE', LOG, boundary .. '-0', boundary .. '-0')[1]\n if marker then\n for i = 1, #marker[2], 2 do\n if marker[2][i] == 'id' then stale[#stale + 1] = id_key(marker[2][i + 1]) end\n end\n end\n sliced('UNLINK', nil, stale)\nend\n`\n\n/**\n * KEYS: counter.\n * ARGV: sessionId, nowMs, ifLastIndex or '', cause consumer or '', cause tag,\n * cause index, cause attempt, marker id.\n * Returns ['ok', cleared, head, boundary, appended?] | ['superseded'] |\n * ['foreign']; `appended` is '1' when this call appended the marker.\n * The marker joins the same log: the old lifetime's entries stay below it\n * until reclaim deletes them, and every read of the log starts at the\n * marker. (A stream can't be renamed aside: Upstash refuses RENAME on one.)\n * The ready set is unlinked, so claims and pending checks see only the new\n * lifetime. Type indexes keep their older entries until reclaim too:\n * lookups start above the marker.\n */\nconst CLEAR_LUA = `${BASE_LUA}\nlocal marker_owner = owners_of(read_bounds({ ARGV[8] }))[1]\nlocal head = total\nif ARGV[4] ~= '' then\n local idx = tonumber(ARGV[6])\n if idx <= boundary then return { 'superseded' } end\n local row = read_rows({ { idx, row_of(ARGV[4], ARGV[5]) } }, { 'd', 'f', 'p', 'l' })[1]\n if tonumber(row[1] or '0') ~= tonumber(ARGV[7]) or row[2] or row[3] or not owns(row[4], ARGV[5]) then\n return { 'superseded' }\n end\nend\nlocal expected = ARGV[3]\nif expected ~= '' and boundary == tonumber(expected) + 1 then\n return { 'ok', '1', tostring(head), tostring(boundary) }\nend\nif expected ~= '' and tonumber(expected) ~= head then\n return { 'ok', '0', tostring(head), tostring(boundary) }\nend\nif head == 0 or boundary == head then\n return { 'ok', '1', tostring(head), tostring(boundary) }\nend\nif marker_owner then return { 'foreign' } end\nlocal field = tostring(head + 1)\nredis.call('UNLINK', READY)\nredis.call('XADD', LOG, field .. '-0',\n 'id', ARGV[8], 'type', CLEAR_TYPE, 'payload', 'null',\n 'created_at', ARGV[2], 'cause', '')\nredis.call('MSET', id_key(ARGV[8]), field, COUNTER, field, CLEAR, field)\nreturn { 'ok', '1', field, field, '1' }\n`\n\n/**\n * KEYS: counter. ARGV: page size.\n * The next deletion batch: up to a page of [index, id] pairs of the log's\n * entries below the latest clear. With none left it sweeps what the old\n * lifetimes left and returns nothing, so a retry finishes an interrupted\n * deletion.\n */\nconst RECLAIM_BATCH_LUA = `${BASE_LUA}${SWEEP_LUA}\nlocal boundary = tonumber(redis.call('GET', CLEAR) or '0')\nif boundary == 0 then return {} end\nlocal entries = redis.call('XRANGE', LOG, '-', tostring(boundary - 1) .. '-0', 'COUNT', ARGV[1])\nif #entries == 0 then\n sweep(boundary)\n return {}\nend\nlocal batch = {}\nfor _, entry in ipairs(entries) do\n local fields = entry[2]\n for i = 1, #fields, 2 do\n if fields[i] == 'id' then\n batch[#batch + 1] = string.match(entry[1], '^(%d+)')\n batch[#batch + 1] = fields[i + 1]\n break\n end\n end\nend\nreturn batch\n`\n\n/**\n * KEYS: counter.\n * ARGV: sessionId, n, then per entry: index, id.\n * Deletes the listed entries below the latest clear and their id records.\n * An id record survives when it names another event, so an id reused in the\n * new lifetime keeps its owner. Returns [deleted, done].\n */\nconst RECLAIM_LUA = `${BASE_LUA}${SWEEP_LUA}\nlocal boundary = tonumber(redis.call('GET', CLEAR) or '0')\nlocal ids, indexes, stream_ids = {}, {}, {}\n-- A concurrent reclaim may list the same batch; only XDEL says who removed it.\nlocal removed = 0\nfor i = 1, tonumber(ARGV[2]) do\n local field = ARGV[2 + (i - 1) * 2 + 1]\n if tonumber(field) < boundary then\n ids[#ids + 1] = ARGV[2 + (i - 1) * 2 + 2]\n indexes[#ids] = tonumber(field)\n stream_ids[#ids] = field .. '-0'\n end\nend\nif #ids > 0 then\n local keys = {}\n for i = 1, #ids do keys[i] = id_key(ids[i]) end\n local owners, stale = sliced('MGET', nil, keys), {}\n for i = 1, #ids do\n if owners[i] and tonumber(owners[i]) == indexes[i] then\n stale[#stale + 1] = keys[i]\n end\n end\n sliced('DEL', nil, stale)\n removed = redis.call('XDEL', LOG, unpack(stream_ids))\nend\nlocal done = 0\nif #redis.call('XRANGE', LOG, '-', tostring(boundary - 1) .. '-0', 'COUNT', 1) == 0 then\n sweep(boundary)\n done = 1\nend\nreturn { removed, done }\n`\n\n// ── wire helpers ─────────────────────────────────────────────────────\n\nconst WAKE_PAYLOAD_LIMIT = 32_768\nconst encoder = new TextEncoder()\n\n/**\n * A wake-up announcing events through `frontier`. It carries them, so a\n * caught-up reader applies them without reading the stream:\n * `{\"f\":frontier,\"t\":createdAt,\"e\":[[index,id,type,payload],...]}`, with\n * payloads inlined as their stored JSON text. One over WAKE_PAYLOAD_LIMIT\n * bytes, or one without entries, announces only `{\"f\":frontier}`, and\n * readers catch up with XRANGE.\n */\nconst wakeMessage = (\n frontier: number,\n createdAt: number,\n entries: ReadonlyArray<\n readonly [index: number, id: string, type: string, payloadJson: string]\n >,\n): string => {\n const bare = `{\"f\":${frontier}}`\n if (entries.length === 0) return bare\n const carried = `{\"f\":${frontier},\"t\":${createdAt},\"e\":[${entries\n .map(\n ([index, id, type, payload]) =>\n `[${index},${JSON.stringify(id)},${JSON.stringify(type)},${payload}]`,\n )\n .join(',')}]}`\n return encoder.encode(carried).byteLength > WAKE_PAYLOAD_LIMIT\n ? bare\n : carried\n}\n\nexport type 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 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\n/**\n * A stream entry's causal edge. Releases before consumers (0.17 to 0.20)\n * recorded only the parent's index and attempt: their edge reads as the\n * handlers table's, `a2.handlers`, as Postgres's upgrade names it, with no\n * lane, since nothing reads an earlier release's lane.\n */\nconst decodeStreamCause = (raw: string): EventCause | null => {\n const parsed = JSON.parse(raw) as { consumer?: unknown }\n return decodeCause(\n parsed.consumer === undefined\n ? { consumer: 'a2.handlers', lane: null, ...parsed }\n : parsed,\n )\n}\n\n/** A stream entry with its causal edge. */\nconst toStoredEvent = (sessionId: string, entry: StreamEntry): StoredEvent => {\n const cause = fieldMap(entry[1])['cause']\n return Object.assign(toEvent(sessionId, entry), {\n cause:\n cause === undefined || cause === '' ? null : decodeStreamCause(cause),\n })\n}\n\ntype ByteStreamEntry = [Uint8Array, Uint8Array[]]\n\nconst fieldBytes = (name: string): Uint8Array => new TextEncoder().encode(name)\nconst ID_FIELD = fieldBytes('id')\nconst TYPE_FIELD = fieldBytes('type')\nconst PAYLOAD_FIELD = fieldBytes('payload')\nconst CREATED_AT_FIELD = fieldBytes('created_at')\n// A leading U+FEFF is part of a stored id or type, never a byte-order mark.\nconst utf8 = new TextDecoder('utf-8', { ignoreBOM: true })\n\nconst isField = (name: Uint8Array, field: Uint8Array): boolean => {\n if (name.length !== field.length) return false\n for (let at = 0; at < field.length; at++)\n if (name[at] !== field[at]) return false\n return true\n}\n\nconst bytesToSerializedEvent = (\n sessionId: string,\n entry: ByteStreamEntry,\n): SerializedEvent => {\n const values = entry[1]\n let id: string | undefined\n let type: string | undefined\n let payloadJson: string | undefined\n let createdAt: string | undefined\n for (let at = 0; at + 1 < values.length; at += 2) {\n const name = values[at]!\n if (isField(name, ID_FIELD)) id = utf8.decode(values[at + 1])\n else if (isField(name, TYPE_FIELD)) type = utf8.decode(values[at + 1])\n else if (isField(name, PAYLOAD_FIELD))\n payloadJson = utf8.decode(values[at + 1])\n else if (isField(name, CREATED_AT_FIELD))\n createdAt = utf8.decode(values[at + 1])\n }\n return {\n id: id!,\n type: type!,\n payloadJson: payloadJson!,\n index: entryIndex(utf8.decode(entry[0])),\n sessionId,\n createdAtMs: Number(createdAt),\n }\n}\n\nconst toSerializedEvent = (\n sessionId: string,\n entry: StreamEntry,\n): SerializedEvent => {\n const fields = fieldMap(entry[1])\n return {\n id: fields['id']!,\n type: fields['type']!,\n payloadJson: fields['payload']!,\n index: entryIndex(entry[0]),\n sessionId,\n createdAtMs: Number(fields['created_at']),\n }\n}\n\nconst json = (value: unknown): string => JSON.stringify(value) ?? 'null'\n\n/** How scripts name a cursor within its consumer: `#`, or `L` and its lane. */\nconst tagOf = (lane: string | null): string =>\n lane === null ? '#' : `L${lane}`\n\n/** The lane a cursor tag names: null for an event cursor. */\nconst laneOf = (tag: string): string | null =>\n tag.startsWith('#') ? null : tag.slice(1)\n\nconst targetsArg = (targets: readonly EventTarget[] | undefined): string =>\n targets === undefined || targets.length === 0\n ? ''\n : JSON.stringify(\n targets.map((target) => [target.consumer, tagOf(target.lane)]),\n )\n\n/** A log consumer's types, as scripts read them; '*' takes every type. */\nconst logTypesArg = (types: readonly string[] | null): string =>\n types === null ? '*' : JSON.stringify(types)\n\n/** The served consumers, as scripts read them; '' serves every consumer. */\nconst servedArg = (consumers: readonly StoreConsumer[] | undefined): string =>\n consumers === undefined\n ? ''\n : JSON.stringify(\n consumers.map((consumer) =>\n consumer.log\n ? [\n consumer.name,\n consumer.log.lane,\n logTypesArg(consumer.log.types),\n consumer.log.backfill ? 1 : 0,\n ]\n : [consumer.name],\n ),\n )\n\ntype Column = string | number | null | undefined\n\n/** A head's dispatch state from a row's HEAD_COLUMNS. */\nconst dispatchOf = (\n consumer: string,\n lane: string | null,\n columns: readonly Column[] | null | undefined,\n): HeadDispatch => {\n const column = (at: number): string | null => {\n const value = columns?.[at]\n return value === null || value === undefined ? null : String(value)\n }\n const date = (at: number): Date | null => {\n const value = column(at)\n return value === null ? null : new Date(Number(value))\n }\n const count = (at: number): number | null => {\n const value = column(at)\n return value === null ? null : Number(value)\n }\n return {\n consumer,\n lane,\n firstClaimedAt: date(1),\n lastClaimedAt: date(2),\n attemptCount: count(0) ?? 0,\n claimHolder: column(3),\n claimExpiresAt: date(4),\n failureCount: count(5) ?? 0,\n lastFailedAt: date(6),\n lastFailedAttempt: count(7),\n lastError: column(8),\n failedAt: date(9),\n }\n}\n\ntype HeadTuple = [\n consumer: string,\n tag: string,\n index: string,\n columns: Column[],\n entry: StreamEntry,\n]\n\nconst toHeadEvent = (sessionId: string, head: HeadTuple): HeadEvent => ({\n ...toStoredEvent(sessionId, head[4]),\n ...dispatchOf(head[0], laneOf(head[1]), head[3]),\n})\n\nconst compareText = (a: string, b: string): number =>\n a < b ? -1 : a > b ? 1 : 0\n\nconst byHead = (a: HeadRef, b: HeadRef): number =>\n a.index - b.index || compareText(a.consumer, b.consumer)\n\n/** Consumer, then lane, with concurrent cursors last by head. */\nconst byCursor = (a: StoredCursor, b: StoredCursor): number =>\n compareText(a.consumer, b.consumer) ||\n (a.lane === null\n ? b.lane === null\n ? (a.head ?? 0) - (b.head ?? 0)\n : 1\n : b.lane === null\n ? -1\n : compareText(a.lane, b.lane))\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 = (reply.slice(2, 2 + count) as HeadTuple[])\n .map((head) => toHeadEvent(sessionId, head))\n .toSorted(byHead)\n const clearIndex = Number(reply[2 + count] ?? 0)\n if (clearIndex > 0)\n for (const event of events) event.clearIndex = clearIndex\n return { outcome: 'claimed', events, head: Number(reply[3 + count]) }\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 toIndex = ({\n raw,\n sessionId,\n name,\n}: {\n raw: unknown\n sessionId: string\n name: string\n}): number => {\n const index = Number(raw ?? 0)\n if (!Number.isSafeInteger(index) || index < 0) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis ${name} for session '${sessionId}' is invalid`,\n )\n }\n return index\n}\n\n/**\n * A range read that met a gap. Reclaim deletes the entries below a clear, so\n * a read whose bounds predate the clear finds nothing where it expected\n * events; anywhere else a gap is corruption.\n */\nconst rangeGap = (sessionId: string, message: string): A2Error =>\n Object.assign(\n new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' ${message}`,\n ),\n { rangeGap: true },\n )\n\nconst isRangeGap = (error: unknown): boolean =>\n error instanceof A2Error &&\n (error as { rangeGap?: boolean }).rangeGap === true\n\n/**\n * Run a whole read, bounds included. A gap means a clear raced it, so the\n * read runs once more from fresh bounds. A second gap, from another clear or\n * from corruption, surfaces as the retryable STORE_UNAVAILABLE.\n */\nconst retryAfterGap = async <T>(read: () => Promise<T>): Promise<T> => {\n try {\n return await read()\n } catch (error) {\n if (!isRangeGap(error)) throw error\n return read()\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\n/**\n * A session's key. Every key of a session shares one hash tag, so a script\n * locks only its session on Upstash and stays in one slot on a cluster. The\n * tag is the session id as JSON with its braces escaped: it can't end early,\n * and no session's tag spells another's.\n */\nexport const redisSessionKey = ({\n prefix,\n sessionId,\n suffix,\n}: {\n prefix: string\n sessionId: string\n suffix: string\n}): string =>\n `${prefix}:{${JSON.stringify(sessionId).replaceAll('{', '\\\\u007b').replaceAll('}', '\\\\u007d')}}:${suffix}`\n\nexport function createRedisStoreCore(\n options: RedisStoreCoreOptions,\n): RedisStoreCore {\n const { call, callBytes, clock } = options\n const generateId = options.ids\n const prefix = options.keyPrefix\n if (prefix.includes('{') || prefix.includes('}')) {\n throw new TypeError(\n 'a redis keyPrefix cannot contain braces: they would set the hash tag of every key',\n )\n }\n\n const sessionKey = (sessionId: string, suffix: string): string =>\n redisSessionKey({ prefix, sessionId, suffix })\n const storeKey = (s: string): string => sessionKey(s, 'log')\n const countKey = (s: string): string => sessionKey(s, 'count')\n // Scripts derive the session's other keys from the counter; keep them in step.\n const clearKey = (s: string): string => sessionKey(s, 'clear')\n const schemaKey = `${prefix}:schema`\n const snapKey = (s: string, r: string): string => sessionKey(s, `snap:${r}`)\n const snapHistoryKey = (s: string, r: string): string =>\n sessionKey(s, `snap-history:${r}`)\n const presenceKey = (s: string): string => sessionKey(s, 'presence')\n // Channels aren't keys; the notify store subscribes by these names.\n const notifyChannel = (s: string): string => `${prefix}:${s}:notify`\n const presenceChannel = (s: string): string => `${prefix}:${s}:presence`\n\n const layoutError = (): A2Error =>\n new A2Error(\n 'STORE_NOT_CONFIGURED',\n `the redis keys under prefix '${prefix}' hold an A2 layout other than '${REDIS_LAYOUT}', from another release; point the store at a fresh keyPrefix, or give the contract a new name so it starts new sessions`,\n )\n\n /**\n * Once per store, before its first script: claim or check the prefix's\n * layout, and ask the server whether scripts may lock only their keys.\n * Only the server's own rejection of the flag means no; a transport error\n * fails this attempt, and the next operation asks again.\n */\n const prepared = retryableLazy(async () => {\n const [, keyLocking] = await Promise.all([\n (async () => {\n await call('SET', schemaKey, REDIS_LAYOUT, 'NX')\n if ((await call('GET', schemaKey)) !== REDIS_LAYOUT) throw layoutError()\n })(),\n call('EVAL', `${KEY_LOCKING}\\nreturn 1`, 1, schemaKey).then(\n () => true,\n (error: unknown) => {\n if (SHEBANG_REJECTED.test(String((error as Error)?.message)))\n return false\n throw error\n },\n ),\n ])\n return { keyLocking }\n })\n\n /** Runs a script; a `readOnly` one takes Upstash's shared lock. */\n const runScript = async ({\n script,\n keys,\n args,\n readOnly,\n }: {\n script: string\n keys: string[]\n args: Array<string | number>\n readOnly: boolean\n }): Promise<unknown> => {\n const { keyLocking } = await prepared.get()\n let source = script\n if (keyLocking)\n source = `${readOnly ? READ_ONLY_KEY_LOCKING : KEY_LOCKING}\\n${script}`\n return call('EVAL', source, keys.length, ...keys, ...args)\n }\n\n /** Runs a writing script over a session: KEYS start with its counter. */\n const evalScript = (\n script: string,\n sessionId: string,\n keys: string[],\n args: Array<string | number>,\n ): Promise<unknown> =>\n runScript({\n script,\n keys: [countKey(sessionId), ...keys],\n args,\n readOnly: false,\n })\n\n /**\n * Publishes a write's wake-up after its script, never inside it: a script\n * holds its lock while PUBLISH waits on subscribers. Wake-ups are hints (a\n * lost one costs subscribers a catch-up read, never an event), so a failed\n * publish is dropped and the write waits for it at most\n * NOTIFY_TIMINGS.publishMs.\n */\n const publish = async (channel: string, message: string): Promise<void> => {\n let timer: ReturnType<typeof setTimeout> | undefined\n await Promise.race([\n call('PUBLISH', channel, message).catch(() => undefined),\n new Promise<void>((resolve) => {\n timer = setTimeout(resolve, NOTIFY_TIMINGS.publishMs)\n ;(timer as { unref?: () => void }).unref?.()\n }),\n ])\n clearTimeout(timer)\n }\n\n /**\n * Runs a write that may need a type's index to reach further back first.\n * Such a write returns ['index', type, after] having written nothing, and\n * runs again once INDEX_TYPE_LUA has indexed that history, a page per call.\n */\n const indexing = async (\n sessionId: string,\n run: () => Promise<unknown>,\n ): Promise<unknown[]> => {\n for (;;) {\n // oxlint-disable-next-line no-await-in-loop -- each run follows the indexing it asked for\n const reply = (await run()) as unknown[]\n if (reply[0] !== 'index') return reply\n for (;;) {\n // oxlint-disable-next-line no-await-in-loop -- each page lowers the index's floor\n const [done] = (await evalScript(\n INDEX_TYPE_LUA,\n sessionId,\n [],\n [String(reply[1]), Number(reply[2])],\n )) as [string]\n if (done === '1') break\n }\n }\n }\n\n /** The frontier and clear marker, in one MGET. */\n const readBounds = async (\n sessionId: string,\n ): Promise<{ frontier: number; clearIndex: number }> => {\n await prepared.get()\n const [count, cleared] = (await call(\n 'MGET',\n countKey(sessionId),\n clearKey(sessionId),\n )) as [unknown, unknown]\n return {\n // A collapsed session keeps only its clear marker.\n frontier: toIndex({\n raw: count ?? cleared,\n sessionId,\n name: 'event count',\n }),\n clearIndex: toIndex({ raw: cleared, sessionId, name: 'clear boundary' }),\n }\n }\n\n const readClearIndex = async (sessionId: string): Promise<number> => {\n await prepared.get()\n return toIndex({\n raw: await call('GET', clearKey(sessionId)),\n sessionId,\n name: 'clear boundary',\n })\n }\n\n const readEventPages = 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 rangeGap(sessionId, `ended before index ${throughIndex}`)\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 rangeGap(sessionId, 'is not contiguous')\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 const readEventRange = async (\n sessionId: string,\n afterIndex: number,\n throughIndex: number,\n ): Promise<Event[]> => {\n const events: Event[] = []\n for (\n let start = afterIndex;\n start < throughIndex;\n start += READ_PAGE_SIZE * READ_CONCURRENCY\n ) {\n const ranges = Array.from(\n {\n length: Math.min(\n READ_CONCURRENCY,\n Math.ceil((throughIndex - start) / READ_PAGE_SIZE),\n ),\n },\n (_, position) => {\n const after = start + position * READ_PAGE_SIZE\n return readEventPages(\n sessionId,\n after,\n Math.min(after + READ_PAGE_SIZE, throughIndex),\n )\n },\n )\n // oxlint-disable-next-line no-await-in-loop -- bound in-flight reads and settle every page before returning\n const results = await Promise.allSettled(ranges)\n for (const result of results) {\n if (result.status === 'rejected') throw result.reason\n events.push(...result.value)\n }\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 (await readBoundedRange(sessionId, afterIndex, throughIndex)).rows\n\n /** `readRange`, with the clear boundary its rows start at. */\n const readBoundedRange = async (\n sessionId: string,\n afterIndex: number,\n throughIndex?: number,\n ): Promise<{ rows: StoredEvent[]; clearIndex: number }> => {\n if (throughIndex !== undefined && throughIndex <= afterIndex)\n return { rows: [], clearIndex: 0 }\n const { frontier: currentFrontier, clearIndex } =\n await readBounds(sessionId)\n const frontier = Math.min(throughIndex ?? currentFrontier, currentFrontier)\n // Reads start at the latest clear marker.\n const after = Math.max(afterIndex, clearIndex - 1)\n if (frontier <= after) return { rows: [], clearIndex }\n\n const rows: StoredEvent[] = []\n let nextIndex = after + 1\n while (nextIndex <= frontier) {\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 `${frontier}-0`,\n 'COUNT',\n READ_PAGE_SIZE,\n )) as StreamEntry[]\n if (entries.length === 0) {\n throw rangeGap(sessionId, `ended before index ${frontier}`)\n }\n const page = entries.map((entry) => toStoredEvent(sessionId, entry))\n for (let position = 0; position < page.length; position += 1) {\n if (page[position]!.index !== nextIndex + position) {\n throw rangeGap(sessionId, 'is not contiguous')\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, clearIndex }\n }\n\n const readStream = options.readStream\n return {\n ...(readStream\n ? {\n streamEvents: async function* ({\n sessionId,\n afterIndex,\n signal,\n }: {\n sessionId: string\n afterIndex: number\n signal: AbortSignal\n }): AsyncGenerator<SerializedEvent[], void> {\n try {\n for await (const entries of readStream({\n key: storeKey(sessionId),\n from: `${afterIndex + 1}-0`,\n signal,\n })) {\n signal.throwIfAborted()\n yield entries.map((entry) =>\n toSerializedEvent(sessionId, entry),\n )\n }\n } catch (cause) {\n if (cause instanceof A2Error) throw cause\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'redis stream read failed',\n { cause },\n )\n }\n },\n }\n : {}),\n async append(sessionId, events, appendOptions) {\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 targets: e.targets,\n }))\n const payloads = withIds.map((e) => json(e.payload))\n const reply = (await indexing(sessionId, () =>\n evalScript(\n APPEND_LUA,\n sessionId,\n [],\n [\n sessionId,\n now,\n withIds.length,\n servedArg(appendOptions?.consumers),\n appendOptions?.originIndex ?? '',\n ...withIds.flatMap((e, i) => [\n e.id,\n e.type,\n payloads[i]!,\n e.cause ? json(e.cause) : '',\n targetsArg(e.targets),\n ]),\n ],\n ),\n )) as string[]\n\n const [verdict] = reply\n if (verdict === 'empty') {\n return { events: [], hasPending: reply[1] === '1' }\n }\n if (verdict === 'cleared') {\n throw sessionClearedError(\n `scheduled work from event ${reply[1]} was cleared in session '${sessionId}'`,\n )\n }\n if (verdict === 'superseded') {\n throw supersededAttemptError(\n sessionId,\n withIds[Number(reply[1]) - 1]!.cause!,\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 const hasPending = reply[1] === '1'\n const firstIndex = Math.min(...reply.slice(2).map(Number))\n // A valid retry occupies exactly events.length consecutive indexes.\n const { rows: existing, clearIndex } = await retryAfterGap(() =>\n readBoundedRange(\n sessionId,\n firstIndex - 1,\n firstIndex + events.length - 1,\n ),\n )\n if (firstIndex < clearIndex) {\n // Cleared after the script matched the retry: the ids are free.\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `session '${sessionId}' was cleared while an append retry replayed; retry it`,\n )\n }\n const replayed = idempotentReplay(events, existing)\n // The first attempt may have committed without publishing.\n await publish(\n notifyChannel(sessionId),\n wakeMessage(firstIndex + events.length - 1, now, []),\n )\n return { events: replayed, hasPending }\n }\n const base = Number(reply[1]) - 1\n await publish(\n notifyChannel(sessionId),\n wakeMessage(\n base + withIds.length,\n now,\n withIds.map((e, i) => [base + 1 + i, e.id, e.type, payloads[i]!]),\n ),\n )\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 })),\n hasPending: reply[2] === '1',\n }\n })\n },\n\n async cursors(sessionId) {\n return wrap(async () => {\n const reply = (await runScript({\n script: CURSORS_LUA,\n keys: [countKey(sessionId)],\n args: [],\n readOnly: true,\n })) as Array<\n [\n consumer: string,\n tag: string,\n position: string,\n head: string | null,\n columns: Column[],\n ]\n >\n return reply\n .map(([consumer, tag, position, head, columns]): StoredCursor =>\n Object.assign(dispatchOf(consumer, laneOf(tag), columns), {\n position: Number(position),\n head: head === null ? null : Number(head),\n }),\n )\n .toSorted(byCursor)\n })\n },\n\n async read(sessionId, opts) {\n return wrap(() =>\n retryAfterGap(() =>\n readRange(sessionId, opts?.afterIndex ?? 0, opts?.throughIndex),\n ),\n )\n },\n\n async readHistory(sessionId, opts) {\n const after = opts?.afterIndex ?? 0\n if (opts?.throughIndex !== undefined && opts.throughIndex <= after)\n return []\n return wrap(() =>\n retryAfterGap(async () => {\n const { frontier, clearIndex } = await readBounds(sessionId)\n // Reads start at the latest clear marker.\n return readEventRange(\n sessionId,\n Math.max(after, clearIndex - 1),\n Math.min(opts?.throughIndex ?? frontier, frontier),\n )\n }),\n )\n },\n\n async claimAvailable({\n sessionId,\n holder,\n ttlMs,\n expiresAtMs,\n exclude,\n consumers,\n maxFailures,\n through,\n }) {\n return wrap(async () => {\n const now = clock.now().getTime()\n const expiresAt = expiresAtMs ?? now + ttlMs\n const excluded = exclude ?? []\n const reply = (await evalScript(\n CLAIM_AVAILABLE_LUA,\n sessionId,\n [],\n [\n holder,\n now,\n expiresAt,\n maxFailures ?? '',\n servedArg(consumers),\n through ?? '',\n excluded.length,\n ...excluded.flatMap((ref) => [\n ref.consumer,\n ref.lane === null ? `#${ref.index}` : `L${ref.lane}`,\n ]),\n ],\n )) as unknown[]\n return toClaimAvailableResult(sessionId, reply)\n })\n },\n\n async renewClaims({ sessionId, holder, claims, ttlMs, expiresAtMs }) {\n if (claims.length === 0) return { renewed: [], superseded: [] }\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 sessionId,\n [],\n [\n holder,\n now,\n expiresAt,\n claims.length,\n ...claims.flatMap((claim) => [\n claim.consumer,\n tagOf(claim.lane),\n claim.index,\n claim.attempt,\n ]),\n ],\n )) as string[]\n const renewedCount = Number(reply[0])\n const refs = reply.slice(1).map((position): HeadRef => {\n const { consumer, lane, index } = claims[Number(position) - 1]!\n return { consumer, lane, index }\n })\n return {\n renewed: refs.slice(0, renewedCount).toSorted(byHead),\n superseded: refs.slice(renewedCount).toSorted(byHead),\n }\n })\n },\n\n async completeAttempt({\n sessionId,\n consumer,\n lane,\n index,\n attempt,\n events,\n consumers,\n }) {\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 cause: EventCause = { consumer, lane, index, attempt }\n const payloads = events.map((event) => json(event.payload))\n const reply = (await indexing(sessionId, () =>\n evalScript(\n COMPLETE_ATTEMPT_LUA,\n sessionId,\n [],\n [\n sessionId,\n now,\n consumer,\n tagOf(lane),\n index,\n attempt,\n json(cause),\n servedArg(consumers),\n json(ids),\n events.length,\n ...events.flatMap((event, i) => [\n event.id,\n event.type,\n payloads[i]!,\n targetsArg(event.targets),\n ]),\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 if (outcome === 'ok') {\n const base = Number(reply[1]) - 1\n if (events.length > 0) {\n await publish(\n notifyChannel(sessionId),\n wakeMessage(\n base + events.length,\n now,\n events.map((event, i) => [\n base + 1 + i,\n event.id,\n event.type,\n payloads[i]!,\n ]),\n ),\n )\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: { ...cause },\n })),\n } satisfies CompleteAttemptResult\n }\n if (outcome === 'duplicate') {\n if (events.length === 0) {\n return { outcome: 'completed', events: [] }\n }\n const firstIndex = Math.min(...reply.slice(1).map(Number))\n const { rows: existing, clearIndex } = await retryAfterGap(() =>\n readBoundedRange(\n sessionId,\n firstIndex - 1,\n firstIndex + events.length - 1,\n ),\n )\n // Cleared after the script matched the retry.\n if (index <= clearIndex) return { outcome: 'superseded' }\n if (\n existing.some(\n (row) =>\n row.cause?.consumer !== consumer ||\n row.cause.lane !== lane ||\n row.cause.index !== index ||\n row.cause.attempt !== attempt,\n )\n ) {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n 'returned event retry does not match the committed batch',\n )\n }\n const replayed = idempotentReplay(events, existing)\n // The first attempt may have committed without publishing.\n await publish(\n notifyChannel(sessionId),\n wakeMessage(firstIndex + events.length - 1, now, []),\n )\n return { outcome: 'completed', events: replayed }\n }\n throw new TypeError(`unexpected completion outcome '${outcome}'`)\n })\n },\n\n async failAttempt({\n sessionId,\n consumer,\n lane,\n index,\n attempt,\n error,\n maxFailures,\n retryAfterMs,\n }) {\n return wrap(async () => {\n const now = clock.now().getTime()\n const reply = (await evalScript(\n FAIL_ATTEMPT_LUA,\n sessionId,\n [],\n [\n consumer,\n tagOf(lane),\n index,\n attempt,\n error,\n maxFailures,\n now,\n retryAfterMs === undefined ? '' : now + retryAfterMs,\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 return {\n outcome: outcome as 'failed' | 'dead_lettered' | 'superseded',\n failureCount: Number(reply[1]),\n }\n })\n },\n\n async deadLetters(sessionId) {\n return wrap(async () => {\n const reply = (await runScript({\n script: DEAD_LETTERS_LUA,\n keys: [countKey(sessionId)],\n args: [],\n readOnly: true,\n })) as HeadTuple[]\n return reply\n .map((head) => toHeadEvent(sessionId, head))\n .toSorted(byHead)\n })\n },\n\n async redrive({ sessionId, consumer, index, action, consumers }) {\n return wrap(async () => {\n const [redriven, hasPending] = (await indexing(sessionId, () =>\n evalScript(\n REDRIVE_LUA,\n sessionId,\n [],\n [\n consumer,\n index,\n action,\n clock.now().getTime(),\n servedArg(consumers),\n ],\n ),\n )) as [string, string]\n return { redriven: redriven === '1', hasPending: hasPending === '1' }\n })\n },\n\n async wakeLog({ sessionId, consumer }) {\n return wrap(async () => {\n const { name, log } = consumer\n const [, position, head, columns, sessionHead] = (await indexing(\n sessionId,\n () =>\n evalScript(\n WAKE_LOG_LUA,\n sessionId,\n [],\n [name, log.lane, logTypesArg(log.types), log.backfill ? 1 : 0],\n ),\n )) as [\n ok: string,\n position: string,\n head: string | null,\n columns: Column[],\n sessionHead: string,\n ]\n return {\n cursor: Object.assign(dispatchOf(name, log.lane, columns), {\n position: Number(position),\n head: head === null ? null : Number(head),\n }),\n head: Number(sessionHead),\n }\n })\n },\n\n async readState(sessionId, reducerName, stateOptions) {\n return wrap(() =>\n retryAfterGap(async () => {\n const reply = (await runScript({\n script: READ_STATE_LUA,\n keys: [\n countKey(sessionId),\n snapKey(sessionId, reducerName),\n snapHistoryKey(sessionId, reducerName),\n ],\n args: [\n stateOptions?.snapshotThroughIndex ??\n stateOptions?.throughIndex ??\n '',\n ],\n readOnly: true,\n })) as [number | string, string, string, string]\n const rawHead = reply[0]\n const raw = reply[1]\n const headIndex = rawHead === '' ? null : Number(rawHead)\n const parsed =\n raw === ''\n ? null\n : (JSON.parse(raw) as { index: number; state: unknown })\n const currentFrontier = Number(reply[2])\n if (\n !Number.isSafeInteger(currentFrontier) ||\n currentFrontier < 0 ||\n (headIndex !== null &&\n (!Number.isSafeInteger(headIndex) || headIndex < 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 frontier = Math.min(\n currentFrontier,\n stateOptions?.throughIndex ?? currentFrontier,\n )\n const snapshot = parsed\n const clearIndex = Number(reply[3] ?? 0)\n // The fold starts at the latest clear marker when no checkpoint is newer.\n const events = await readEventRange(\n sessionId,\n Math.max(snapshot?.index ?? 0, clearIndex - 1),\n frontier,\n )\n return {\n ...(clearIndex > 0 ? { clearIndex } : {}),\n headIndex,\n snapshot: snapshot\n ? { index: snapshot.index, state: snapshot.state }\n : null,\n events,\n }\n }),\n )\n },\n\n async putSnapshots(sessionId, reducerName, writes) {\n const byIndex = new Map<\n number,\n { index: number; state: unknown; pins: Map<string, HeadRef> }\n >()\n for (const write of writes) {\n const existing = byIndex.get(write.index)\n const pins = existing?.pins ?? new Map<string, HeadRef>()\n for (const pin of write.pins ?? []) {\n pins.set(JSON.stringify([pin.consumer, pin.lane, pin.index]), pin)\n }\n if (existing) existing.state = write.state\n else\n byIndex.set(write.index, {\n index: write.index,\n state: write.state,\n pins,\n })\n }\n const normalized = [...byIndex.values()].toSorted(\n (a, b) => a.index - b.index,\n )\n if (normalized.length === 0) return\n await wrap(async () => {\n const args: Array<string | number> = [normalized.length]\n for (const write of normalized) {\n args.push(\n write.index,\n json({\n index: write.index,\n state: write.state,\n updatedAt: clock.now().getTime(),\n // This reducer is in the session's registry (PUT_SNAPSHOTS_LUA).\n r: 1,\n }),\n write.pins.size,\n ...[...write.pins.values()].flatMap((pin) => [\n pin.consumer,\n tagOf(pin.lane),\n pin.index,\n ]),\n )\n }\n await evalScript(\n PUT_SNAPSHOTS_LUA,\n sessionId,\n [\n snapKey(sessionId, reducerName),\n snapHistoryKey(sessionId, reducerName),\n ],\n [...args, reducerName],\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 nowMs = clock.now().getTime()\n const expiresAtMs = nowMs + 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 runScript({\n script: PRESENCE_SET_LUA,\n keys: [presenceKey(ns)],\n args: [\n atMs,\n meta.ttlMs,\n nowMs,\n entries.length,\n // A null is stored as a tombstone: it keeps its stamp, so an\n // older set that arrives after it still loses.\n ...entries.flatMap(([field, value]) => [\n JSON.stringify([participant, field]),\n json({ a: atMs, e: expiresAtMs, s: meta.seen, v: value }),\n ]),\n ],\n readOnly: false,\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 const published: string[] = []\n for (const position of applied) {\n const [field, value] = entries[Number(position) - 1]!\n appliedValues[field] = structuredClone(value)\n published.push(`${json(field)}:${json(value)}`)\n }\n return {\n patch: {\n participant,\n values: appliedValues,\n seen: meta.seen,\n at: new Date(atMs),\n },\n published: publish(\n presenceChannel(ns),\n `${json({ src: options.presenceSource, participant, seen: meta.seen, at: atMs }).slice(0, -1)},\"values\":{${published.join(',')}}}`,\n ),\n }\n })\n },\n\n async read(ns) {\n return wrap(async () => {\n const key = presenceKey(ns)\n const reply = (await call('HGETALL', key)) as string[]\n const nowMs = clock.now().getTime()\n const rows: PresenceRow[] = []\n const expired: string[] = []\n for (let i = 0; i + 1 < reply.length; i += 2) {\n const entry = JSON.parse(reply[i + 1]!) as {\n v: unknown\n s: number\n a: number\n e: number\n }\n if (entry.e <= nowMs) {\n expired.push(reply[i]!)\n continue\n }\n if (entry.v === null) continue\n const [participant, field] = JSON.parse(reply[i]!) as [\n string,\n string,\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 // The rows are already filtered; a failed prune only defers the\n // deletion to the next read.\n if (expired.length > 0) {\n await runScript({\n script: PRESENCE_PRUNE_LUA,\n keys: [key],\n args: [nowMs, ...expired],\n readOnly: false,\n }).catch(() => undefined)\n }\n return rows\n })\n },\n },\n\n async readRange(sessionId, afterIndex, throughIndex) {\n return wrap(() => readRange(sessionId, afterIndex, throughIndex))\n },\n\n async readTail(sessionId, fromIndex, throughIndex) {\n return wrap(async () => {\n // Entries below the latest clear are an old lifetime's, awaiting\n // reclaim: a tail that reaches them is the boundary-aware reads' job.\n const [newest, clearIndex] = await Promise.all([\n call(\n 'XREVRANGE',\n storeKey(sessionId),\n throughIndex === undefined ? '+' : `${throughIndex}-0`,\n `${fromIndex}-0`,\n 'COUNT',\n READ_PAGE_SIZE,\n ) as Promise<StreamEntry[]>,\n readClearIndex(sessionId),\n ])\n if (clearIndex > fromIndex) return null\n const page = newest\n .toReversed()\n .map((entry) => toEvent(sessionId, entry))\n for (let position = 1; position < page.length; position += 1) {\n if (page[position]!.index !== page[position - 1]!.index + 1) {\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n `redis event range for session '${sessionId}' is not contiguous`,\n )\n }\n }\n const oldest = page[0]?.index\n return oldest === undefined || oldest <= fromIndex ? page : null\n })\n },\n\n async clear({ sessionId, ifLastIndex, cause }) {\n return wrap(async () => {\n const markerId = generateId()\n const now = clock.now().getTime()\n const reply = (await evalScript(\n CLEAR_LUA,\n sessionId,\n [],\n [\n sessionId,\n now,\n ifLastIndex ?? '',\n cause?.consumer ?? '',\n cause ? tagOf(cause.lane) : '',\n cause?.index ?? '',\n cause?.attempt ?? '',\n markerId,\n ],\n )) as string[]\n if (reply[0] === 'superseded') {\n throw supersededAttemptError(sessionId, cause!)\n }\n if (reply[0] === 'foreign') {\n throw new A2Error(\n 'PARTIAL_DUPLICATE_BATCH',\n `the clear marker id '${markerId}' already exists`,\n )\n }\n const head = Number(reply[2])\n if (reply[4] === '1') {\n await publish(\n notifyChannel(sessionId),\n wakeMessage(head, now, [\n [head, markerId, CLEAR_EVENT_TYPE, 'null'],\n ]),\n )\n } else if (reply[1] === '1' && head > 0) {\n // An earlier attempt may have appended the marker without publishing.\n await publish(notifyChannel(sessionId), wakeMessage(head, now, []))\n }\n return {\n cleared: reply[1] === '1',\n index: Number(reply[2]),\n clearIndex: Number(reply[3]),\n }\n })\n },\n\n async reclaim(sessionId) {\n return wrap(async () => {\n const pairs = (await evalScript(\n RECLAIM_BATCH_LUA,\n sessionId,\n [],\n [READ_PAGE_SIZE],\n )) as string[]\n if (pairs.length === 0) return { deleted: 0, done: true }\n const [deleted, done] = (await evalScript(\n RECLAIM_LUA,\n sessionId,\n [],\n [sessionId, pairs.length / 2, ...pairs],\n )) as [number, number]\n return { deleted: Number(deleted), done: Number(done) === 1 }\n })\n },\n\n clearIndex: (sessionId) => wrap(() => readClearIndex(sessionId)),\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 'COUNT',\n READ_PAGE_SIZE,\n )) as StreamEntry[]\n return entries.map((entry) => toEvent(sessionId, entry))\n })\n },\n async readFeedPage(sessionId, afterIndex) {\n return wrap(async () => {\n const range = [\n storeKey(sessionId),\n `${afterIndex + 1}-0`,\n '+',\n 'COUNT',\n FEED_PAGE_SIZE,\n ] as const\n if (callBytes) {\n const entries = (await callBytes(\n 'XRANGE',\n ...range,\n )) as ByteStreamEntry[]\n return entries.map((entry) =>\n bytesToSerializedEvent(sessionId, entry),\n )\n }\n const entries = (await call('XRANGE', ...range)) as StreamEntry[]\n return entries.map((entry) => toSerializedEvent(sessionId, entry))\n })\n },\n }\n}\n","import type { PresencePatch } from './contract.ts'\nimport { A2Error } from './errors.ts'\nimport { NOTIFY_TIMINGS, stateTailReaders } from './internal.ts'\nimport { defaultSleep, pollingStream, type Sleeper } from './store-polling.ts'\nimport { pollPresence } from './store-presence-polling.ts'\nimport { presenceRecovery } from './presence-recovery.ts'\nimport { createStreamActivity, streamActivities } from './stream-activity.ts'\nimport {\n bufferedEvents,\n isSerializedEvent,\n parseSerializedEvent,\n type HeldEvent,\n type SerializedEvent,\n} from './stream-buffer.ts'\nimport {\n createRedisStoreCore,\n FEED_PAGE_SIZE,\n type RedisCall,\n type RedisStreamRead,\n} from './store-redis-core.ts'\nimport type { A2Store, Clock, Event, IdSource } from './store.ts'\n\nexport type RedisConnection = {\n call(command: string, ...args: Array<string | number>): Promise<unknown>\n duplicate(): RedisConnection\n on(\n event: 'message',\n listener: (channel: string, message: string) => void,\n ): unknown\n disconnect(): void\n}\n\ntype PresenceWireMessage = {\n src: string\n participant: string\n values: Record<string, unknown>\n seen: number\n at: number\n}\n\ntype PresenceSubscription = {\n listeners: Set<(patch: PresencePatch) => void>\n poll?: ReturnType<typeof pollPresence> | undefined\n recovery?: ReturnType<typeof presenceRecovery> | undefined\n}\n\n/**\n * One live event feed's view of its channel: wake-up messages received\n * since it subscribed, whether it must catch up with XRANGE before trusting\n * them, and the resolver of its park, if parked.\n */\ntype Feed = {\n inbox: string[]\n bytes: number\n dirty: boolean\n /** Wake-ups past a gap, waiting up to NOTIFY_TIMINGS.reorderMs for it. */\n held: Array<{ wake: Wake; bytes: number }>\n gapSince: number | undefined\n wake: (() => void) | null\n stopRead: () => Promise<void>\n}\n\n/** A feed that falls this far behind drops its inbox and catches up. */\nconst INBOX_BYTES = 512 * 1024\n\ntype WakeEntry = [index: number, id: string, type: string, payload: unknown]\ntype Wake = { frontier: number; createdAt: number; entries: WakeEntry[] }\n\n/**\n * Parses a wake-up message: `{\"f\":frontier}` or a bare frontier number\n * announces events without carrying them; `{\"f\",\"t\",\"e\"}` carries them.\n * Returns null for anything else, which callers treat as a hint to catch up.\n */\nconst parseWake = (message: string): Wake | null => {\n let parsed: unknown\n try {\n parsed = JSON.parse(message)\n } catch {\n return null\n }\n if (typeof parsed === 'number') {\n return Number.isSafeInteger(parsed)\n ? { frontier: parsed, createdAt: 0, entries: [] }\n : null\n }\n if (typeof parsed !== 'object' || parsed === null) return null\n const { f, t, e } = parsed as { f?: unknown; t?: unknown; e?: unknown }\n if (typeof f !== 'number' || !Number.isSafeInteger(f)) return null\n if (e === undefined) return { frontier: f, createdAt: 0, entries: [] }\n if (!Array.isArray(e) || typeof t !== 'number' || !Number.isFinite(t))\n return null\n for (const entry of e) {\n if (\n !Array.isArray(entry) ||\n entry.length !== 4 ||\n !Number.isSafeInteger(entry[0]) ||\n typeof entry[1] !== 'string' ||\n typeof entry[2] !== 'string'\n )\n return null\n }\n return { frontier: f, createdAt: t, entries: e as WakeEntry[] }\n}\n\nexport function createRedisNotifyStore(options: {\n client: () => Promise<RedisConnection>\n callBytes?: RedisCall\n closeClient: () => Promise<void>\n clock: Clock\n ids: IdSource\n keyPrefix: string\n onReconnect?: (listener: (channel: string) => void) => void\n polling?: () => boolean\n readStream?: RedisStreamRead\n}): A2Store & { close(): Promise<void> } {\n const { client, clock, ids, keyPrefix: prefix } = options\n const notifyChannel = (sessionId: string): string =>\n `${prefix}:${sessionId}:notify`\n const presenceChannel = (ns: string): string => `${prefix}:${ns}:presence`\n\n const instanceId = crypto.randomUUID()\n const core = createRedisStoreCore({\n call: async (command, ...args) => {\n const c = await client()\n return c.call(command, ...args)\n },\n clock,\n ids,\n keyPrefix: prefix,\n presenceSource: instanceId,\n ...(options.callBytes ? { callBytes: options.callBytes } : {}),\n ...(options.readStream ? { readStream: options.readStream } : {}),\n })\n\n let severed = false\n let subscriber: Promise<RedisConnection> | null = null\n /** channel → live event feeds, registered before they subscribe. */\n const feeds = new Map<string, Set<Feed>>()\n /** channel → refcounted subscription held for each live iterator. */\n const subscriptions = new Map<\n string,\n { refs: number; ready: Promise<unknown> }\n >()\n const presenceSubscriptions = new Map<string, PresenceSubscription>()\n const pollingStreams = new Set<AsyncIterator<Event>>()\n\n options.onReconnect?.((channel) => {\n for (const feed of feeds.get(channel) ?? []) {\n feed.dirty = true\n feed.wake?.()\n }\n void presenceSubscriptions\n .get(channel)\n ?.recovery?.reconnect()\n .catch(() => {})\n })\n\n const deliverRemotePresence = ({\n subscription,\n message,\n }: {\n subscription: PresenceSubscription\n message: string\n }): void => {\n let parsed: PresenceWireMessage\n try {\n parsed = JSON.parse(message) as PresenceWireMessage\n } catch {\n return\n }\n if (parsed.src === instanceId) return\n const patch: PresencePatch = {\n participant: parsed.participant,\n values: parsed.values,\n seen: parsed.seen,\n at: new Date(parsed.at),\n }\n subscription.recovery?.update(patch)\n for (const listener of subscription.listeners) listener(patch)\n }\n\n const getSubscriber = (): Promise<RedisConnection> => {\n subscriber ??= (async () => {\n const conn = (await client()).duplicate()\n conn.on('message', (channel, message) => {\n for (const feed of feeds.get(channel) ?? []) {\n if (feed.bytes + message.length <= INBOX_BYTES) {\n feed.inbox.push(message)\n feed.bytes += message.length\n } else {\n feed.inbox.length = 0\n feed.held.length = 0\n feed.gapSince = undefined\n feed.bytes = 0\n feed.dirty = true\n }\n feed.wake?.()\n }\n const subscription = presenceSubscriptions.get(channel)\n if (subscription) deliverRemotePresence({ subscription, message })\n })\n return conn\n })()\n return subscriber\n }\n\n /**\n * The subscriber, gated on connection readiness — for channel\n * commands only (`close` uses the ungated promise, so it can never\n * hang on an unreachable server). SUBSCRIBE carries redis's\n * ok-loading flag, so ioredis writes it mid-handshake, ahead of the\n * ready check's INFO — which then finds the connection already in\n * subscriber mode and fails the check (a spurious error event plus\n * a reconnect). ECHO has no such flag: it resolves only once the\n * connection is ready, so every channel command chained behind it\n * lands after the check.\n */\n let subscriberGate: Promise<RedisConnection> | null = null\n const gatedSubscriber = (): Promise<RedisConnection> => {\n if (subscriberGate) return subscriberGate\n const gate = getSubscriber().then(async (conn) => {\n await conn.call('echo', 'ready')\n return conn\n })\n subscriberGate = gate\n // A failed check is retried by the next channel command.\n void gate.catch(() => {\n if (subscriberGate === gate) subscriberGate = null\n })\n return gate\n }\n\n const acquireChannel = async (channel: string): Promise<void> => {\n const existing = subscriptions.get(channel)\n if (existing) {\n existing.refs += 1\n try {\n await existing.ready\n } catch (err) {\n existing.refs -= 1\n throw err\n }\n return\n }\n const lease: { refs: number; ready: Promise<void> } = {\n refs: 1,\n // Lowercase on purpose: ioredis keys its subscriber-mode\n // bookkeeping (and reconnect resubscription) on the exact\n // command name.\n ready: gatedSubscriber().then(async (conn) => {\n await conn.call('subscribe', channel)\n if (subscriptions.get(channel) !== lease || severed) return\n const presence = presenceSubscriptions.get(channel)\n if (options.polling?.() && presence) {\n presence.recovery = undefined\n const ns = channel.slice(prefix.length + 1, -':presence'.length)\n presence.poll = pollPresence({\n read: () => core.presence.read(ns),\n deliver: (patch) => {\n for (const listener of presence.listeners) listener(patch)\n },\n })\n await presence.poll.ready\n }\n return undefined\n }),\n }\n subscriptions.set(channel, lease)\n try {\n await lease.ready\n } catch (err) {\n if (subscriptions.get(channel) === lease) {\n subscriptions.delete(channel)\n const presence = presenceSubscriptions.get(channel)\n presence?.poll?.stop()\n if (presence) presence.poll = undefined\n }\n throw err\n }\n }\n\n const releaseChannel = (channel: string): void => {\n const lease = subscriptions.get(channel)\n if (!lease) return\n lease.refs -= 1\n if (lease.refs > 0) return\n subscriptions.delete(channel)\n void gatedSubscriber()\n .then((conn) => conn.call('unsubscribe', channel))\n .catch(() => {})\n }\n\n const store: A2Store & { close(): Promise<void> } = {\n append: core.append,\n cursors: core.cursors,\n read: core.read,\n readHistory: core.readHistory!,\n claimAvailable: core.claimAvailable,\n renewClaims: core.renewClaims,\n completeAttempt: core.completeAttempt,\n failAttempt: core.failAttempt,\n readState: core.readState,\n putSnapshots: core.putSnapshots,\n clear: core.clear!,\n reclaim: core.reclaim!,\n deadLetters: core.deadLetters!,\n redrive: core.redrive!,\n wakeLog: core.wakeLog,\n\n presence: {\n async set(ns, participant, values, meta) {\n const applied = await core.presence.set(ns, participant, values, meta)\n if (!applied) return\n const { patch } = applied\n const channel = presenceChannel(ns)\n const subscription = presenceSubscriptions.get(channel)\n if (subscription) {\n subscription.poll?.update(patch)\n subscription.recovery?.update(patch)\n for (const listener of subscription.listeners) listener(patch)\n }\n await applied.published\n },\n\n async read(ns) {\n const channel = presenceChannel(ns)\n await subscriptions.get(channel)?.ready\n const subscription = presenceSubscriptions.get(channel)\n return (\n subscription?.recovery?.read() ??\n subscription?.poll?.snapshot(() => core.presence.read(ns)) ??\n core.presence.read(ns)\n )\n },\n\n subscribe(ns, onPatch) {\n const channel = presenceChannel(ns)\n let subscription = presenceSubscriptions.get(channel)\n if (!subscription) {\n subscription = { listeners: new Set() }\n presenceSubscriptions.set(channel, subscription)\n if (options.onReconnect && !options.polling?.()) {\n const current = subscription\n subscription.recovery = presenceRecovery({\n read: () => core.presence.read(ns),\n deliver: (patch) => {\n for (const listener of current.listeners) listener(patch)\n },\n isActive: () =>\n !severed && presenceSubscriptions.get(channel) === current,\n })\n }\n }\n subscription.listeners.add(onPatch)\n // Fire-and-forget on the shared refcounted subscription: it\n // only carries remote patches, so settling late (or failing)\n // degrades cross-instance latency, never same-process\n // delivery.\n void acquireChannel(channel).catch(() => {})\n let stopped = false\n return () => {\n if (stopped) return\n stopped = true\n subscription.listeners.delete(onPatch)\n if (subscription.listeners.size === 0) {\n subscription.poll?.stop()\n presenceSubscriptions.delete(channel)\n }\n releaseChannel(channel)\n }\n },\n },\n\n stream(sessionId, opts) {\n const startAfter = opts?.startAfter ?? 0\n const channel = notifyChannel(sessionId)\n\n return {\n [Symbol.asyncIterator](): AsyncIterator<Event> {\n let last = startAfter\n let buffer: HeldEvent[] = []\n let closed = false\n let subscribed = false\n let pending: Sleeper | null = null\n let poller: AsyncIterator<Event> | undefined\n let stopActivity: (() => void) | undefined\n let catchup:\n | {\n controller: AbortController\n iterator: AsyncGenerator<SerializedEvent[], void>\n delivered: boolean\n }\n | undefined\n let stoppingRead: Promise<void> | undefined\n const stopRead = (): Promise<void> => {\n const reading = catchup\n catchup = undefined\n if (reading) {\n reading.controller.abort()\n stoppingRead = reading.iterator\n .return(undefined)\n .then(() => undefined)\n .catch(() => {})\n }\n return stoppingRead ?? Promise.resolve()\n }\n const nextRead = async (\n after: number,\n ): Promise<IteratorResult<SerializedEvent[], void>> => {\n if (!catchup) {\n const controller = new AbortController()\n catchup = {\n controller,\n delivered: false,\n iterator: core.streamEvents!({\n sessionId,\n afterIndex: after,\n signal: controller.signal,\n })[Symbol.asyncIterator](),\n }\n }\n const reading = catchup\n const result = await reading.iterator.next()\n if (result.done && catchup === reading) {\n catchup = undefined\n if (reading.delivered) feed.dirty = true\n } else if (!result.done) reading.delivered = true\n return result\n }\n const feed: Feed = {\n inbox: [],\n bytes: 0,\n dirty: true,\n held: [],\n gapSince: undefined,\n wake: null,\n stopRead,\n }\n const activity = createStreamActivity(() => {\n if (poller) streamActivities.get(poller)?.activate()\n })\n\n const release = (): void => {\n void stopRead()\n stopActivity?.()\n if (poller) {\n void poller.return?.()\n pollingStreams.delete(poller)\n }\n if (!subscribed) return\n subscribed = false\n const set = feeds.get(channel)\n set?.delete(feed)\n if (set?.size === 0) feeds.delete(channel)\n releaseChannel(channel)\n }\n\n /**\n * Moves carried events that continue this feed's sequence into\n * the buffer. Writers publish after their scripts, so concurrent\n * writers' wake-ups can arrive out of order: they apply in index\n * order, and one past a gap is held until the gap fills or\n * NOTIFY_TIMINGS.reorderMs passes. A message without events past\n * the buffer, or an unreadable one, marks the feed dirty so it\n * catches up with XRANGE.\n */\n const applyInbox = (): void => {\n const carried = feed.held.splice(0)\n let announced = 0\n for (const message of feed.inbox.splice(0)) {\n const wake = parseWake(message)\n if (!wake) feed.dirty = true\n else if (wake.entries.length === 0)\n announced = Math.max(announced, wake.frontier)\n else carried.push({ wake, bytes: message.length })\n }\n carried.sort(\n (a, b) => a.wake.entries[0]![0] - b.wake.entries[0]![0],\n )\n feed.bytes = 0\n for (const held of carried) {\n if (\n held.wake.entries[0]![0] >\n (buffer.at(-1)?.index ?? last) + 1\n ) {\n feed.held.push(held)\n feed.bytes += held.bytes\n continue\n }\n for (const [index, id, type, payload] of held.wake.entries) {\n if (index <= (buffer.at(-1)?.index ?? last)) continue\n buffer.push({\n id,\n type,\n payload,\n index,\n sessionId,\n createdAt: new Date(held.wake.createdAt),\n })\n }\n }\n if (announced > (buffer.at(-1)?.index ?? last)) feed.dirty = true\n if (feed.held.length === 0) feed.gapSince = undefined\n else feed.gapSince ??= Date.now()\n }\n\n const iterator: AsyncIterator<Event> = {\n async next(): Promise<IteratorResult<Event>> {\n for (;;) {\n if (closed) {\n release()\n return { value: undefined, done: true }\n }\n const row = buffer.shift()\n if (row) {\n let event: Event\n try {\n event = isSerializedEvent(row)\n ? parseSerializedEvent(row)\n : row\n } catch (cause) {\n // A retry re-reads from the last delivered event.\n buffer = []\n feed.dirty = true\n release()\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'redis stream read failed',\n { cause },\n )\n }\n last = row.index\n return { value: event, done: false }\n }\n if (severed) {\n release()\n throw new A2Error('STORE_UNAVAILABLE', 'the store was closed')\n }\n try {\n if (!subscribed) {\n // Register and subscribe before the first read:\n // anything appended after it arrives in the inbox,\n // so nothing slips between catch-up and park.\n subscribed = true\n let set = feeds.get(channel)\n if (!set) {\n set = new Set()\n feeds.set(channel, set)\n }\n set.add(feed)\n // A feed starts at the latest clear at the earliest:\n // entries below it await reclaim.\n // oxlint-disable-next-line no-await-in-loop -- one-time setup\n const [, clearIndex] = await Promise.all([\n acquireChannel(channel),\n core.clearIndex(sessionId),\n ])\n if (clearIndex - 1 > last) last = clearIndex - 1\n if (closed || severed) continue\n }\n if (options.polling?.()) {\n if (!poller) {\n poller = pollingStream(\n (after) => core.readEvents(sessionId, after),\n { startAfter: last },\n )[Symbol.asyncIterator]()\n pollingStreams.add(poller)\n stopActivity = streamActivities\n .get(poller)\n ?.subscribe(activity.setIdle)\n }\n // oxlint-disable-next-line no-await-in-loop -- delegate the fallback to the shared poller\n const result = await poller.next()\n if (!result.done)\n presenceSubscriptions\n .get(presenceChannel(sessionId))\n ?.poll?.wake()\n if (severed)\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'the store was closed',\n )\n return result\n }\n if (catchup) {\n // oxlint-disable-next-line no-await-in-loop -- decode the next catch-up batch on demand\n const result = await nextRead(last)\n if (closed || severed) continue\n if (!result.done) buffer = result.value\n continue\n }\n applyInbox()\n if (buffer.length > 0) continue\n if (\n feed.gapSince !== undefined &&\n Date.now() - feed.gapSince >= NOTIFY_TIMINGS.reorderMs\n )\n feed.dirty = true\n if (feed.dirty) {\n feed.dirty = false\n // A read covers whatever the held wake-ups carry.\n feed.held.length = 0\n feed.gapSince = undefined\n if (core.streamEvents) {\n // oxlint-disable-next-line no-await-in-loop -- begin an incremental catch-up\n const result = await nextRead(last)\n if (closed || severed) continue\n if (!result.done) buffer = result.value\n continue\n }\n // oxlint-disable-next-line no-await-in-loop -- live-feed loop\n buffer = await core.readFeedPage(sessionId, last)\n if (buffer.length >= FEED_PAGE_SIZE) feed.dirty = true\n continue\n }\n // Caught up: park until a message, a reconnect, the end of\n // a held gap, or the safety read that heals a lost message.\n const woken = new Promise<'wake'>((resolve) => {\n feed.wake = () => resolve('wake')\n })\n const holding = feed.gapSince !== undefined\n pending = defaultSleep(\n holding\n ? Math.max(\n 0,\n NOTIFY_TIMINGS.reorderMs -\n (Date.now() - feed.gapSince!),\n )\n : NOTIFY_TIMINGS.safetyReadMs,\n )\n // oxlint-disable-next-line no-await-in-loop -- live-feed loop\n const outcome = await Promise.race([\n woken,\n pending.promise.then(() => 'timer' as const),\n ])\n pending.cancel()\n pending = null\n if (outcome === 'timer' && !holding) feed.dirty = true\n } catch (err) {\n release()\n if (closed) return { value: undefined, done: true }\n if (err instanceof A2Error) throw err\n throw new A2Error(\n 'STORE_UNAVAILABLE',\n 'redis store operation failed',\n { cause: err },\n )\n } finally {\n feed.wake = null\n }\n }\n },\n async return(): Promise<IteratorResult<Event>> {\n closed = true\n pending?.cancel()\n feed.wake?.() // wakes an in-flight next() immediately\n release()\n await stopRead()\n return { value: undefined, done: true }\n },\n }\n streamActivities.set(iterator, activity)\n bufferedEvents.set(iterator, () => {\n if (closed) return undefined\n const row = buffer.shift()\n if (row) last = row.index\n return row\n })\n return iterator\n },\n }\n },\n\n async close() {\n severed = true\n for (const subscription of presenceSubscriptions.values())\n subscription.poll?.stop()\n presenceSubscriptions.clear()\n for (const poller of pollingStreams) void poller.return?.()\n pollingStreams.clear()\n const reads: Promise<void>[] = []\n for (const set of feeds.values()) {\n for (const feed of set) {\n feed.wake?.()\n reads.push(feed.stopRead())\n }\n }\n await Promise.allSettled(reads)\n if (subscriber) {\n const sub = await subscriber.catch(() => null)\n sub?.disconnect()\n }\n await options.closeClient()\n },\n }\n stateTailReaders.set(store, core.readTail)\n return store\n}\n"],"mappings":";;;;;;;;AAKA,MAAM,OAAO,EACX,aACA,YAEA,KAAK,UAAU,CAAC,aAAa,KAAK,CAAC;AAErC,SAAgB,iBAAiB,EAC/B,MACA,SACA,YASA;CACA,MAAM,yBAAS,IAAI,IAAmB;CACtC,MAAM,UAAU,mBAAmB;CACnC,MAAM,0BAAU,IAAI,IAAiB;CACrC,IAAI,aAAa;CACjB,IAAI;CAEJ,MAAM,YAAY,EAAE,aAAa,OAAO,MAAM,SAAsB;EAClE,MAAM,KAAK,IAAI;GAAE;GAAa;EAAM,CAAC;EACrC,MAAM,WAAW,OAAO,IAAI,EAAE;EAC9B,IACE,CAAC,YACD,SAAS,GAAG,QAAQ,IAAI,GAAG,QAAQ,KAClC,SAAS,GAAG,QAAQ,MAAM,GAAG,QAAQ,KAAK,SAAS,SAAS,MAE7D,OAAO,IAAI,IAAI;GAAE;GAAa;GAAO;GAAM;EAAG,CAAC;CACnD;CACA,MAAM,WAAW,OAAO,EACtB,gBAG4B;EAC5B,MAAM,MAAM;EACZ,MAAM,SAAS,IAAI,IAAI,MAAM;EAC7B,MAAM,0BAAU,IAAI,IAAY;EAChC,QAAQ,IAAI,OAAO;EACnB,IAAI;GACF,MAAM,OAAO,MAAM,KAAK;GACxB,IAAI,CAAC,SAAS,GAAG,OAAO;GACxB,IAAI,QAAQ,YAAY,OAAO;GAE/B,MAAM,UAAU,KAAK,QAAQ,QAAQ,CAAC,QAAQ,SAAS,GAAG,CAAC;GAC3D,MAAM,uBAAO,IAAI,IAAY;GAC7B,KAAK,MAAM,OAAO,SAAS;IACzB,MAAM,KAAK,IAAI,GAAG;IAClB,KAAK,IAAI,EAAE;IACX,IAAI,QAAQ,IAAI,EAAE,GAAG;IACrB,QAAQ,KAAK,GAAG;IAChB,SAAS,GAAG;IACZ,IAAI,WACF,QAAQ;KACN,aAAa,IAAI;KACjB,QAAQ,GAAG,IAAI,QAAQ,IAAI,MAAM;KACjC,MAAM,IAAI;KACV,IAAI,IAAI;IACV,CAAC;GACL;GACA,IAAI,WACF,KAAK,MAAM,CAAC,IAAI,UAAU,QAAQ;IAChC,IAAI,KAAK,IAAI,EAAE,KAAK,QAAQ,IAAI,EAAE,KAAK,OAAO,IAAI,EAAE,MAAM,OACxD;IACF,OAAO,OAAO,EAAE;IAChB,QAAQ,KAAK,KAAK;IAClB,QAAQ;KACN,aAAa,MAAM;KACnB,QAAQ,GAAG,MAAM,QAAQ,KAAK;KAC9B,MAAM,MAAM;KACZ,IAAI,MAAM;IACZ,CAAC;GACH;GAEF,OAAO;EACT,UAAU;GACR,QAAQ,OAAO,OAAO;GACtB,IAAI,QAAQ,SAAS,GAAG,SAAS,KAAA;EACnC;CACF;CAEA,OAAO;EACL,MAAM,OAAO;GACX,IAAI,UAAU;GACd,OAAO,SAAS;IAEd,MAAM;IACN,IAAI,WAAW,SAAS;IACxB,UAAU;GACZ;GACA,OAAO,SAAS,EAAE,WAAW,MAAM,CAAC;EACtC;EACA,MAAM,YAAY;GAChB,cAAc;GACd,SAAS,SAAS,EAAE,WAAW,KAAK,CAAC;GACrC,MAAM;EACR;EACA,OAAO,OAAO;GACZ,KAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,MAAM,MAAM,GAAG;IACzD,IAAI,UAAU,KAAA,GAAW;IACzB,MAAM,QAAQ;KAAE,aAAa,MAAM;KAAa;KAAO,IAAI,MAAM;IAAG;IACpE,MAAM,KAAK,IAAI,KAAK;IACpB,MAAM,WAAW,OAAO,IAAI,EAAE;IAC9B,IAAI,YAAY,SAAS,GAAG,QAAQ,IAAI,MAAM,GAAG,QAAQ,GAAG;IAC5D,IAAI,QAAQ,SAAS,KAAK,GAAG;IAC7B,KAAK,MAAM,WAAW,SAAS,QAAQ,IAAI,EAAE;IAC7C,IAAI,UAAU,MAAM;KAClB,OAAO,OAAO,EAAE;KAChB,QAAQ,KAAK,KAAK;IACpB,OAAO;KACL,QAAQ,KAAK,KAAK;KAClB,SAAS;MAAE,GAAG;MAAO,MAAM,MAAM;KAAK,CAAC;IACzC;GACF;EACF;CACF;AACF;ACsCA,MAAa,iBAAiB;AAC9B,MAAM,mBAAmB;;AAGzB,MAAa,eAAe;;;;;;AAO5B,MAAM,cAAc;AACpB,MAAM,wBAAwB;AAC9B,MAAM,mBAAmB;AAEzB,MAAM,kBAAkB;;AAGxB,MAAM,eAAe;;;;;;AASrB,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BnB,MAAM,WAAW,GAAG,WAAW;;;;;;sBAMT,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;0DAmDmB,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkI1E,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmTnB,MAAM,aAAa,GAAG,WAAW,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkF5C,MAAM,iBAAiB,GAAG,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDnC,MAAM,sBAAsB,GAAG,WAAW,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsGrD,MAAM,mBAAmB,GAAG,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CrC,MAAM,uBAAuB,GAAG,WAAW,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+EtD,MAAM,mBAAmB,GAAG,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CrC,MAAM,cAAc,GAAG,WAAW,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4C7C,MAAM,eAAe,GAAG,WAAW,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wEAmC0B,aAAa;;;;;;;;;;;;;;AAerF,MAAM,mBAAmB,GAAG,SAAS;;;;;;;;;;;;;+BAaN,aAAa;;;;;;;;;;;;;;;AAgB5C,MAAM,cAAc,GAAG,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+BAyCD,aAAa;;;;;;;;;;;;;;;;;;;;AAqB5C,MAAM,oBAAoB,GAAG,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0HtC,MAAM,sBAAsB;;;;;;;;;;;;;;;;AAiB5B,MAAM,mBAAmB,GAAG,aAAa,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;AAyB7D,MAAM,qBAAqB,GAAG,aAAa,oBAAoB;;;;;;;;;;;;;;;;;;;;AAqB/D,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCvB,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;6CAsB2B,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiF7D,MAAM,YAAY,GAAG,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsC9B,MAAM,oBAAoB,GAAG,WAAW,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BlD,MAAM,cAAc,GAAG,WAAW,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmC5C,MAAM,qBAAqB;AAC3B,MAAM,UAAU,IAAI,YAAY;;;;;;;;;AAUhC,MAAM,eACJ,UACA,WACA,YAGW;CACX,MAAM,OAAO,QAAQ,SAAS;CAC9B,IAAI,QAAQ,WAAW,GAAG,OAAO;CACjC,MAAM,UAAU,QAAQ,SAAS,OAAO,UAAU,QAAQ,QACvD,KACE,CAAC,OAAO,IAAI,MAAM,aACjB,IAAI,MAAM,GAAG,KAAK,UAAU,EAAE,EAAE,GAAG,KAAK,UAAU,IAAI,EAAE,GAAG,QAAQ,EACvE,CAAC,CACA,KAAK,GAAG,EAAE;CACb,OAAO,QAAQ,OAAO,OAAO,CAAC,CAAC,aAAa,qBACxC,OACA;AACN;AAIA,MAAM,YAAY,WAA6C;CAC7D,MAAM,MAA8B,CAAC;CACrC,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,OAAO,QAAQ,KAAK,GAC1C,IAAI,OAAO,MAAO,OAAO,IAAI;CAE/B,OAAO;AACT;AAEA,MAAM,cAAc,YAClB,OAAO,QAAQ,MAAM,GAAG,QAAQ,QAAQ,GAAG,CAAC,CAAC;AAE/C,MAAM,WAAW,WAAmB,UAA8B;CAChE,MAAM,SAAS,SAAS,MAAM,EAAE;CAChC,OAAO;EACL,IAAI,OAAO;EACX,MAAM,OAAO;EACb,SAAS,KAAK,MAAM,OAAO,UAAW;EACtC,OAAO,WAAW,MAAM,EAAE;EAC1B;EACA,WAAW,IAAI,KAAK,OAAO,OAAO,aAAa,CAAC;CAClD;AACF;;;;;;;AAQA,MAAM,qBAAqB,QAAmC;CAC5D,MAAM,SAAS,KAAK,MAAM,GAAG;CAC7B,OAAO,YACL,OAAO,aAAa,KAAA,IAChB;EAAE,UAAU;EAAe,MAAM;EAAM,GAAG;CAAO,IACjD,MACN;AACF;;AAGA,MAAM,iBAAiB,WAAmB,UAAoC;CAC5E,MAAM,QAAQ,SAAS,MAAM,EAAE,CAAC,CAAC;CACjC,OAAO,OAAO,OAAO,QAAQ,WAAW,KAAK,GAAG,EAC9C,OACE,UAAU,KAAA,KAAa,UAAU,KAAK,OAAO,kBAAkB,KAAK,EACxE,CAAC;AACH;AAIA,MAAM,cAAc,SAA6B,IAAI,YAAY,CAAC,CAAC,OAAO,IAAI;AAC9E,MAAM,WAAW,WAAW,IAAI;AAChC,MAAM,aAAa,WAAW,MAAM;AACpC,MAAM,gBAAgB,WAAW,SAAS;AAC1C,MAAM,mBAAmB,WAAW,YAAY;AAEhD,MAAM,OAAO,IAAI,YAAY,SAAS,EAAE,WAAW,KAAK,CAAC;AAEzD,MAAM,WAAW,MAAkB,UAA+B;CAChE,IAAI,KAAK,WAAW,MAAM,QAAQ,OAAO;CACzC,KAAK,IAAI,KAAK,GAAG,KAAK,MAAM,QAAQ,MAClC,IAAI,KAAK,QAAQ,MAAM,KAAK,OAAO;CACrC,OAAO;AACT;AAEA,MAAM,0BACJ,WACA,UACoB;CACpB,MAAM,SAAS,MAAM;CACrB,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,KAAK,IAAI,KAAK,GAAG,KAAK,IAAI,OAAO,QAAQ,MAAM,GAAG;EAChD,MAAM,OAAO,OAAO;EACpB,IAAI,QAAQ,MAAM,QAAQ,GAAG,KAAK,KAAK,OAAO,OAAO,KAAK,EAAE;OACvD,IAAI,QAAQ,MAAM,UAAU,GAAG,OAAO,KAAK,OAAO,OAAO,KAAK,EAAE;OAChE,IAAI,QAAQ,MAAM,aAAa,GAClC,cAAc,KAAK,OAAO,OAAO,KAAK,EAAE;OACrC,IAAI,QAAQ,MAAM,gBAAgB,GACrC,YAAY,KAAK,OAAO,OAAO,KAAK,EAAE;CAC1C;CACA,OAAO;EACD;EACE;EACO;EACb,OAAO,WAAW,KAAK,OAAO,MAAM,EAAE,CAAC;EACvC;EACA,aAAa,OAAO,SAAS;CAC/B;AACF;AAEA,MAAM,qBACJ,WACA,UACoB;CACpB,MAAM,SAAS,SAAS,MAAM,EAAE;CAChC,OAAO;EACL,IAAI,OAAO;EACX,MAAM,OAAO;EACb,aAAa,OAAO;EACpB,OAAO,WAAW,MAAM,EAAE;EAC1B;EACA,aAAa,OAAO,OAAO,aAAa;CAC1C;AACF;AAEA,MAAM,QAAQ,UAA2B,KAAK,UAAU,KAAK,KAAK;;AAGlE,MAAM,SAAS,SACb,SAAS,OAAO,MAAM,IAAI;;AAG5B,MAAM,UAAU,QACd,IAAI,WAAW,GAAG,IAAI,OAAO,IAAI,MAAM,CAAC;AAE1C,MAAM,cAAc,YAClB,YAAY,KAAA,KAAa,QAAQ,WAAW,IACxC,KACA,KAAK,UACH,QAAQ,KAAK,WAAW,CAAC,OAAO,UAAU,MAAM,OAAO,IAAI,CAAC,CAAC,CAC/D;;AAGN,MAAM,eAAe,UACnB,UAAU,OAAO,MAAM,KAAK,UAAU,KAAK;;AAG7C,MAAM,aAAa,cACjB,cAAc,KAAA,IACV,KACA,KAAK,UACH,UAAU,KAAK,aACb,SAAS,MACL;CACE,SAAS;CACT,SAAS,IAAI;CACb,YAAY,SAAS,IAAI,KAAK;CAC9B,SAAS,IAAI,WAAW,IAAI;AAC9B,IACA,CAAC,SAAS,IAAI,CACpB,CACF;;AAKN,MAAM,cACJ,UACA,MACA,YACiB;CACjB,MAAM,UAAU,OAA8B;EAC5C,MAAM,QAAQ,UAAU;EACxB,OAAO,UAAU,QAAQ,UAAU,KAAA,IAAY,OAAO,OAAO,KAAK;CACpE;CACA,MAAM,QAAQ,OAA4B;EACxC,MAAM,QAAQ,OAAO,EAAE;EACvB,OAAO,UAAU,OAAO,OAAO,IAAI,KAAK,OAAO,KAAK,CAAC;CACvD;CACA,MAAM,SAAS,OAA8B;EAC3C,MAAM,QAAQ,OAAO,EAAE;EACvB,OAAO,UAAU,OAAO,OAAO,OAAO,KAAK;CAC7C;CACA,OAAO;EACL;EACA;EACA,gBAAgB,KAAK,CAAC;EACtB,eAAe,KAAK,CAAC;EACrB,cAAc,MAAM,CAAC,KAAK;EAC1B,aAAa,OAAO,CAAC;EACrB,gBAAgB,KAAK,CAAC;EACtB,cAAc,MAAM,CAAC,KAAK;EAC1B,cAAc,KAAK,CAAC;EACpB,mBAAmB,MAAM,CAAC;EAC1B,WAAW,OAAO,CAAC;EACnB,UAAU,KAAK,CAAC;CAClB;AACF;AAUA,MAAM,eAAe,WAAmB,UAAgC;CACtE,GAAG,cAAc,WAAW,KAAK,EAAE;CACnC,GAAG,WAAW,KAAK,IAAI,OAAO,KAAK,EAAE,GAAG,KAAK,EAAE;AACjD;AAEA,MAAM,eAAe,GAAW,MAC9B,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI;AAE3B,MAAM,UAAU,GAAY,MAC1B,EAAE,QAAQ,EAAE,SAAS,YAAY,EAAE,UAAU,EAAE,QAAQ;;AAGzD,MAAM,YAAY,GAAiB,MACjC,YAAY,EAAE,UAAU,EAAE,QAAQ,MACjC,EAAE,SAAS,OACR,EAAE,SAAS,QACR,EAAE,QAAQ,MAAM,EAAE,QAAQ,KAC3B,IACF,EAAE,SAAS,OACT,KACA,YAAY,EAAE,MAAM,EAAE,IAAI;AAElC,MAAM,0BACJ,WACA,UAC8B;CAC9B,MAAM,UAAU,OAAO,MAAM,EAAE;CAC/B,IAAI,YAAY,WAAW;EACzB,MAAM,QAAQ,OAAO,MAAM,EAAE;EAC7B,MAAM,SAAU,MAAM,MAAM,GAAG,IAAI,KAAK,CAAC,CACtC,KAAK,SAAS,YAAY,WAAW,IAAI,CAAC,CAAC,CAC3C,SAAS,MAAM;EAClB,MAAM,aAAa,OAAO,MAAM,IAAI,UAAU,CAAC;EAC/C,IAAI,aAAa,GACf,KAAK,MAAM,SAAS,QAAQ,MAAM,aAAa;EACjD,OAAO;GAAE,SAAS;GAAW;GAAQ,MAAM,OAAO,MAAM,IAAI,MAAM;EAAE;CACtE;CACA,IAAI,YAAY,QACd,OAAO;EAAE;EAAS,SAAS,IAAI,KAAK,OAAO,MAAM,EAAE,CAAC;CAAE;CAExD,IAAI,YAAY,WAAW,OAAO,EAAE,QAAQ;CAC5C,MAAM,IAAI,UACR,gDAAgD,UAAU,EAC5D;AACF;AAEA,MAAM,WAAW,EACf,KACA,WACA,WAKY;CACZ,MAAM,QAAQ,OAAO,OAAO,CAAC;CAC7B,IAAI,CAAC,OAAO,cAAc,KAAK,KAAK,QAAQ,GAC1C,MAAM,IAAI,QACR,qBACA,SAAS,KAAK,gBAAgB,UAAU,aAC1C;CAEF,OAAO;AACT;;;;;;AAOA,MAAM,YAAY,WAAmB,YACnC,OAAO,OACL,IAAI,QACF,qBACA,kCAAkC,UAAU,IAAI,SAClD,GACA,EAAE,UAAU,KAAK,CACnB;AAEF,MAAM,cAAc,UAClB,iBAAiB,WAChB,MAAiC,aAAa;;;;;;AAOjD,MAAM,gBAAgB,OAAU,SAAuC;CACrE,IAAI;EACF,OAAO,MAAM,KAAK;CACpB,SAAS,OAAO;EACd,IAAI,CAAC,WAAW,KAAK,GAAG,MAAM;EAC9B,OAAO,KAAK;CACd;AACF;AAEA,MAAM,OAAO,OAAU,OAAqC;CAC1D,IAAI;EACF,OAAO,MAAM,GAAG;CAClB,SAAS,KAAK;EACZ,IAAI,eAAe,WAAW,eAAe,WAAW,MAAM;EAC9D,MAAM,IAAI,QAAQ,qBAAqB,gCAAgC,EACrE,OAAO,IACT,CAAC;CACH;AACF;;;;;;;AAQA,MAAa,mBAAmB,EAC9B,QACA,WACA,aAMA,GAAG,OAAO,IAAI,KAAK,UAAU,SAAS,CAAC,CAAC,WAAW,KAAK,SAAS,CAAC,CAAC,WAAW,KAAK,SAAS,EAAE,IAAI;AAEpG,SAAgB,qBACd,SACgB;CAChB,MAAM,EAAE,MAAM,WAAW,UAAU;CACnC,MAAM,aAAa,QAAQ;CAC3B,MAAM,SAAS,QAAQ;CACvB,IAAI,OAAO,SAAS,GAAG,KAAK,OAAO,SAAS,GAAG,GAC7C,MAAM,IAAI,UACR,mFACF;CAGF,MAAM,cAAc,WAAmB,WACrC,gBAAgB;EAAE;EAAQ;EAAW;CAAO,CAAC;CAC/C,MAAM,YAAY,MAAsB,WAAW,GAAG,KAAK;CAC3D,MAAM,YAAY,MAAsB,WAAW,GAAG,OAAO;CAE7D,MAAM,YAAY,MAAsB,WAAW,GAAG,OAAO;CAC7D,MAAM,YAAY,GAAG,OAAO;CAC5B,MAAM,WAAW,GAAW,MAAsB,WAAW,GAAG,QAAQ,GAAG;CAC3E,MAAM,kBAAkB,GAAW,MACjC,WAAW,GAAG,gBAAgB,GAAG;CACnC,MAAM,eAAe,MAAsB,WAAW,GAAG,UAAU;CAEnE,MAAM,iBAAiB,MAAsB,GAAG,OAAO,GAAG,EAAE;CAC5D,MAAM,mBAAmB,MAAsB,GAAG,OAAO,GAAG,EAAE;CAE9D,MAAM,oBACJ,IAAI,QACF,wBACA,gCAAgC,OAAO,kCAAkC,aAAa,yHACxF;;;;;;;CAQF,MAAM,WAAW,cAAc,YAAY;EACzC,MAAM,GAAG,cAAc,MAAM,QAAQ,IAAI,EACtC,YAAY;GACX,MAAM,KAAK,OAAO,WAAW,cAAc,IAAI;GAC/C,IAAK,MAAM,KAAK,OAAO,SAAS,MAAA,YAAqB,MAAM,YAAY;EACzE,EAAA,CAAG,GACH,KAAK,QAAQ,GAAG,YAAY,aAAa,GAAG,SAAS,CAAC,CAAC,WAC/C,OACL,UAAmB;GAClB,IAAI,iBAAiB,KAAK,OAAQ,OAAiB,OAAO,CAAC,GACzD,OAAO;GACT,MAAM;EACR,CACF,CACF,CAAC;EACD,OAAO,EAAE,WAAW;CACtB,CAAC;;CAGD,MAAM,YAAY,OAAO,EACvB,QACA,MACA,MACA,eAMsB;EACtB,MAAM,EAAE,eAAe,MAAM,SAAS,IAAI;EAC1C,IAAI,SAAS;EACb,IAAI,YACF,SAAS,GAAG,WAAW,wBAAwB,YAAY,IAAI;EACjE,OAAO,KAAK,QAAQ,QAAQ,KAAK,QAAQ,GAAG,MAAM,GAAG,IAAI;CAC3D;;CAGA,MAAM,cACJ,QACA,WACA,MACA,SAEA,UAAU;EACR;EACA,MAAM,CAAC,SAAS,SAAS,GAAG,GAAG,IAAI;EACnC;EACA,UAAU;CACZ,CAAC;;;;;;;;CASH,MAAM,UAAU,OAAO,SAAiB,YAAmC;EACzE,IAAI;EACJ,MAAM,QAAQ,KAAK,CACjB,KAAK,WAAW,SAAS,OAAO,CAAC,CAAC,YAAY,KAAA,CAAS,GACvD,IAAI,SAAe,YAAY;GAC7B,QAAQ,WAAW,SAAS,eAAe,SAAS;GACnD,MAAkC,QAAQ;EAC7C,CAAC,CACH,CAAC;EACD,aAAa,KAAK;CACpB;;;;;;CAOA,MAAM,WAAW,OACf,WACA,QACuB;EACvB,SAAS;GAEP,MAAM,QAAS,MAAM,IAAI;GACzB,IAAI,MAAM,OAAO,SAAS,OAAO;GACjC,SAAS;IAEP,MAAM,CAAC,QAAS,MAAM,WACpB,gBACA,WACA,CAAC,GACD,CAAC,OAAO,MAAM,EAAE,GAAG,OAAO,MAAM,EAAE,CAAC,CACrC;IACA,IAAI,SAAS,KAAK;GACpB;EACF;CACF;;CAGA,MAAM,aAAa,OACjB,cACsD;EACtD,MAAM,SAAS,IAAI;EACnB,MAAM,CAAC,OAAO,WAAY,MAAM,KAC9B,QACA,SAAS,SAAS,GAClB,SAAS,SAAS,CACpB;EACA,OAAO;GAEL,UAAU,QAAQ;IAChB,KAAK,SAAS;IACd;IACA,MAAM;GACR,CAAC;GACD,YAAY,QAAQ;IAAE,KAAK;IAAS;IAAW,MAAM;GAAiB,CAAC;EACzE;CACF;CAEA,MAAM,iBAAiB,OAAO,cAAuC;EACnE,MAAM,SAAS,IAAI;EACnB,OAAO,QAAQ;GACb,KAAK,MAAM,KAAK,OAAO,SAAS,SAAS,CAAC;GAC1C;GACA,MAAM;EACR,CAAC;CACH;CAEA,MAAM,iBAAiB,OACrB,WACA,YACA,iBACqB;EACrB,IAAI,gBAAgB,YAAY,OAAO,CAAC;EACxC,MAAM,SAAkB,CAAC;EACzB,IAAI,YAAY,aAAa;EAC7B,OAAO,aAAa,cAAc;GAEhC,MAAM,UAAW,MAAM,KACrB,UACA,SAAS,SAAS,GAClB,GAAG,UAAU,KACb,GAAG,aAAa,KAChB,SAAA,GAEF;GACA,IAAI,QAAQ,WAAW,GACrB,MAAM,SAAS,WAAW,sBAAsB,cAAc;GAEhE,KAAK,IAAI,WAAW,GAAG,WAAW,QAAQ,QAAQ,YAAY,GAAG;IAC/D,MAAM,QAAQ,WAAW,QAAQ,SAAS,CAAE,EAAE;IAC9C,IAAI,UAAU,YAAY,YAAY,QAAQ,cAC5C,MAAM,SAAS,WAAW,mBAAmB;GAEjD;GACA,OAAO,KAAK,GAAG,QAAQ,KAAK,UAAU,QAAQ,WAAW,KAAK,CAAC,CAAC;GAChE,YAAY,OAAO,GAAG,EAAE,CAAC,CAAE,QAAQ;EACrC;EACA,OAAO;CACT;CAEA,MAAM,iBAAiB,OACrB,WACA,YACA,iBACqB;EACrB,MAAM,SAAkB,CAAC;EACzB,KACE,IAAI,QAAQ,YACZ,QAAQ,cACR,SAAA,MAA0B,kBAC1B;GACA,MAAM,SAAS,MAAM,KACnB,EACE,QAAQ,KAAK,IACX,kBACA,KAAK,MAAM,eAAe,SAAA,GAAuB,CACnD,EACF,IACC,GAAG,aAAa;IACf,MAAM,QAAQ,QAAQ,WAAA;IACtB,OAAO,eACL,WACA,OACA,KAAK,IAAI,QAAA,KAAwB,YAAY,CAC/C;GACF,CACF;GAEA,MAAM,UAAU,MAAM,QAAQ,WAAW,MAAM;GAC/C,KAAK,MAAM,UAAU,SAAS;IAC5B,IAAI,OAAO,WAAW,YAAY,MAAM,OAAO;IAC/C,OAAO,KAAK,GAAG,OAAO,KAAK;GAC7B;EACF;EACA,OAAO;CACT;;CAGA,MAAM,YAAY,OAChB,WACA,YACA,kBAEC,MAAM,iBAAiB,WAAW,YAAY,YAAY,EAAA,CAAG;;CAGhE,MAAM,mBAAmB,OACvB,WACA,YACA,iBACyD;EACzD,IAAI,iBAAiB,KAAA,KAAa,gBAAgB,YAChD,OAAO;GAAE,MAAM,CAAC;GAAG,YAAY;EAAE;EACnC,MAAM,EAAE,UAAU,iBAAiB,eACjC,MAAM,WAAW,SAAS;EAC5B,MAAM,WAAW,KAAK,IAAI,gBAAgB,iBAAiB,eAAe;EAE1E,MAAM,QAAQ,KAAK,IAAI,YAAY,aAAa,CAAC;EACjD,IAAI,YAAY,OAAO,OAAO;GAAE,MAAM,CAAC;GAAG;EAAW;EAErD,MAAM,OAAsB,CAAC;EAC7B,IAAI,YAAY,QAAQ;EACxB,OAAO,aAAa,UAAU;GAE5B,MAAM,UAAW,MAAM,KACrB,UACA,SAAS,SAAS,GAClB,GAAG,UAAU,KACb,GAAG,SAAS,KACZ,SAAA,GAEF;GACA,IAAI,QAAQ,WAAW,GACrB,MAAM,SAAS,WAAW,sBAAsB,UAAU;GAE5D,MAAM,OAAO,QAAQ,KAAK,UAAU,cAAc,WAAW,KAAK,CAAC;GACnE,KAAK,IAAI,WAAW,GAAG,WAAW,KAAK,QAAQ,YAAY,GACzD,IAAI,KAAK,SAAS,CAAE,UAAU,YAAY,UACxC,MAAM,SAAS,WAAW,mBAAmB;GAGjD,MAAM,YAAY,KAAK,GAAG,EAAE,CAAC,CAAE;GAC/B,IAAI,YAAY,aAAa,YAAY,UACvC,MAAM,IAAI,QACR,qBACA,kCAAkC,UAAU,kBAC9C;GAEF,KAAK,KAAK,GAAG,IAAI;GACjB,YAAY,YAAY;EAC1B;EACA,OAAO;GAAE;GAAM;EAAW;CAC5B;CAEA,MAAM,aAAa,QAAQ;CAC3B,OAAO;EACL,GAAI,aACA,EACE,cAAc,iBAAiB,EAC7B,WACA,YACA,UAK0C;GAC1C,IAAI;IACF,WAAW,MAAM,WAAW,WAAW;KACrC,KAAK,SAAS,SAAS;KACvB,MAAM,GAAG,aAAa,EAAE;KACxB;IACF,CAAC,GAAG;KACF,OAAO,eAAe;KACtB,MAAM,QAAQ,KAAK,UACjB,kBAAkB,WAAW,KAAK,CACpC;IACF;GACF,SAAS,OAAO;IACd,IAAI,iBAAiB,SAAS,MAAM;IACpC,MAAM,IAAI,QACR,qBACA,4BACA,EAAE,MAAM,CACV;GACF;EACF,EACF,IACA,CAAC;EACL,MAAM,OAAO,WAAW,QAAQ,eAAe;GAC7C,OAAO,KAAK,YAAY;IAEtB,MAAM,cADW,OAAO,QAAQ,MAAM,EAAE,OAAO,KAAA,CACpB,CAAC,CAAC,KAAK,MAAM,EAAE,EAAY;IACtD,IAAI,IAAI,IAAI,WAAW,CAAC,CAAC,SAAS,YAAY,QAC5C,MAAM,IAAI,QACR,2BACA,iDACF;IAEF,MAAM,MAAM,MAAM,IAAI,CAAC,CAAC,QAAQ;IAChC,MAAM,UAAU,OAAO,KAAK,OAAO;KACjC,IAAI,EAAE,MAAM,WAAW;KACvB,MAAM,EAAE;KACR,SAAS,EAAE;KACX,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,IAAI;KAClC,SAAS,EAAE;IACb,EAAE;IACF,MAAM,WAAW,QAAQ,KAAK,MAAM,KAAK,EAAE,OAAO,CAAC;IACnD,MAAM,QAAS,MAAM,SAAS,iBAC5B,WACE,YACA,WACA,CAAC,GACD;KACE;KACA;KACA,QAAQ;KACR,UAAU,eAAe,SAAS;KAClC,eAAe,eAAe;KAC9B,GAAG,QAAQ,SAAS,GAAG,MAAM;MAC3B,EAAE;MACF,EAAE;MACF,SAAS;MACT,EAAE,QAAQ,KAAK,EAAE,KAAK,IAAI;MAC1B,WAAW,EAAE,OAAO;KACtB,CAAC;IACH,CACF,CACF;IAEA,MAAM,CAAC,WAAW;IAClB,IAAI,YAAY,SACd,OAAO;KAAE,QAAQ,CAAC;KAAG,YAAY,MAAM,OAAO;IAAI;IAEpD,IAAI,YAAY,WACd,MAAM,oBACJ,6BAA6B,MAAM,GAAG,2BAA2B,UAAU,EAC7E;IAEF,IAAI,YAAY,cACd,MAAM,uBACJ,WACA,QAAQ,OAAO,MAAM,EAAE,IAAI,EAAE,CAAE,KACjC;IAEF,IAAI,YAAY,WACd,MAAM,IAAI,QACR,2BACA,eAAe,MAAM,GAAG,wBAAwB,OAAO,SAAS,OAAO,MAAM,EAAE,EAAE,cACnF;IAEF,IAAI,YAAY,OAAO;KACrB,MAAM,aAAa,MAAM,OAAO;KAChC,MAAM,aAAa,KAAK,IAAI,GAAG,MAAM,MAAM,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC;KAEzD,MAAM,EAAE,MAAM,UAAU,eAAe,MAAM,oBAC3C,iBACE,WACA,aAAa,GACb,aAAa,OAAO,SAAS,CAC/B,CACF;KACA,IAAI,aAAa,YAEf,MAAM,IAAI,QACR,qBACA,YAAY,UAAU,uDACxB;KAEF,MAAM,WAAW,iBAAiB,QAAQ,QAAQ;KAElD,MAAM,QACJ,cAAc,SAAS,GACvB,YAAY,aAAa,OAAO,SAAS,GAAG,KAAK,CAAC,CAAC,CACrD;KACA,OAAO;MAAE,QAAQ;MAAU;KAAW;IACxC;IACA,MAAM,OAAO,OAAO,MAAM,EAAE,IAAI;IAChC,MAAM,QACJ,cAAc,SAAS,GACvB,YACE,OAAO,QAAQ,QACf,KACA,QAAQ,KAAK,GAAG,MAAM;KAAC,OAAO,IAAI;KAAG,EAAE;KAAI,EAAE;KAAM,SAAS;IAAG,CAAC,CAClE,CACF;IACA,OAAO;KACL,QAAQ,QAAQ,KAAK,GAAG,OAAO;MAC7B,IAAI,EAAE;MACN,MAAM,EAAE;MACR,SAAS,gBAAgB,EAAE,OAAO;MAClC,OAAO,OAAO,IAAI;MAClB;MACA,WAAW,IAAI,KAAK,GAAG;MACvB,OAAO,EAAE;KACX,EAAE;KACF,YAAY,MAAM,OAAO;IAC3B;GACF,CAAC;EACH;EAEA,MAAM,QAAQ,WAAW;GACvB,OAAO,KAAK,YAAY;IAetB,QAAO,MAdc,UAAU;KAC7B,QAAQ;KACR,MAAM,CAAC,SAAS,SAAS,CAAC;KAC1B,MAAM,CAAC;KACP,UAAU;IACZ,CAAC,EAAA,CAUE,KAAK,CAAC,UAAU,KAAK,UAAU,MAAM,aACpC,OAAO,OAAO,WAAW,UAAU,OAAO,GAAG,GAAG,OAAO,GAAG;KACxD,UAAU,OAAO,QAAQ;KACzB,MAAM,SAAS,OAAO,OAAO,OAAO,IAAI;IAC1C,CAAC,CACH,CAAC,CACA,SAAS,QAAQ;GACtB,CAAC;EACH;EAEA,MAAM,KAAK,WAAW,MAAM;GAC1B,OAAO,WACL,oBACE,UAAU,WAAW,MAAM,cAAc,GAAG,MAAM,YAAY,CAChE,CACF;EACF;EAEA,MAAM,YAAY,WAAW,MAAM;GACjC,MAAM,QAAQ,MAAM,cAAc;GAClC,IAAI,MAAM,iBAAiB,KAAA,KAAa,KAAK,gBAAgB,OAC3D,OAAO,CAAC;GACV,OAAO,WACL,cAAc,YAAY;IACxB,MAAM,EAAE,UAAU,eAAe,MAAM,WAAW,SAAS;IAE3D,OAAO,eACL,WACA,KAAK,IAAI,OAAO,aAAa,CAAC,GAC9B,KAAK,IAAI,MAAM,gBAAgB,UAAU,QAAQ,CACnD;GACF,CAAC,CACH;EACF;EAEA,MAAM,eAAe,EACnB,WACA,QACA,OACA,aACA,SACA,WACA,aACA,WACC;GACD,OAAO,KAAK,YAAY;IACtB,MAAM,MAAM,MAAM,IAAI,CAAC,CAAC,QAAQ;IAChC,MAAM,YAAY,eAAe,MAAM;IACvC,MAAM,WAAW,WAAW,CAAC;IAC7B,MAAM,QAAS,MAAM,WACnB,qBACA,WACA,CAAC,GACD;KACE;KACA;KACA;KACA,eAAe;KACf,UAAU,SAAS;KACnB,WAAW;KACX,SAAS;KACT,GAAG,SAAS,SAAS,QAAQ,CAC3B,IAAI,UACJ,IAAI,SAAS,OAAO,IAAI,IAAI,UAAU,IAAI,IAAI,MAChD,CAAC;IACH,CACF;IACA,OAAO,uBAAuB,WAAW,KAAK;GAChD,CAAC;EACH;EAEA,MAAM,YAAY,EAAE,WAAW,QAAQ,QAAQ,OAAO,eAAe;GACnE,IAAI,OAAO,WAAW,GAAG,OAAO;IAAE,SAAS,CAAC;IAAG,YAAY,CAAC;GAAE;GAC9D,OAAO,KAAK,YAAY;IACtB,MAAM,MAAM,MAAM,IAAI,CAAC,CAAC,QAAQ;IAChC,MAAM,YAAY,eAAe,MAAM;IACvC,MAAM,QAAS,MAAM,WACnB,kBACA,WACA,CAAC,GACD;KACE;KACA;KACA;KACA,OAAO;KACP,GAAG,OAAO,SAAS,UAAU;MAC3B,MAAM;MACN,MAAM,MAAM,IAAI;MAChB,MAAM;MACN,MAAM;KACR,CAAC;IACH,CACF;IACA,MAAM,eAAe,OAAO,MAAM,EAAE;IACpC,MAAM,OAAO,MAAM,MAAM,CAAC,CAAC,CAAC,KAAK,aAAsB;KACrD,MAAM,EAAE,UAAU,MAAM,UAAU,OAAO,OAAO,QAAQ,IAAI;KAC5D,OAAO;MAAE;MAAU;MAAM;KAAM;IACjC,CAAC;IACD,OAAO;KACL,SAAS,KAAK,MAAM,GAAG,YAAY,CAAC,CAAC,SAAS,MAAM;KACpD,YAAY,KAAK,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM;IACtD;GACF,CAAC;EACH;EAEA,MAAM,gBAAgB,EACpB,WACA,UACA,MACA,OACA,SACA,QACA,aACC;GACD,OAAO,KAAK,YAAY;IACtB,MAAM,MAAM,OAAO,KAAK,UAAU,MAAM,EAAE;IAC1C,IAAI,IAAI,IAAI,GAAG,CAAC,CAAC,SAAS,IAAI,QAC5B,MAAM,IAAI,QACR,2BACA,iDACF;IAEF,MAAM,MAAM,MAAM,IAAI,CAAC,CAAC,QAAQ;IAChC,MAAM,QAAoB;KAAE;KAAU;KAAM;KAAO;IAAQ;IAC3D,MAAM,WAAW,OAAO,KAAK,UAAU,KAAK,MAAM,OAAO,CAAC;IAC1D,MAAM,QAAS,MAAM,SAAS,iBAC5B,WACE,sBACA,WACA,CAAC,GACD;KACE;KACA;KACA;KACA,MAAM,IAAI;KACV;KACA;KACA,KAAK,KAAK;KACV,UAAU,SAAS;KACnB,KAAK,GAAG;KACR,OAAO;KACP,GAAG,OAAO,SAAS,OAAO,MAAM;MAC9B,MAAM;MACN,MAAM;MACN,SAAS;MACT,WAAW,MAAM,OAAO;KAC1B,CAAC;IACH,CACF,CACF;IACA,MAAM,UAAU,MAAM;IACtB,IAAI,YAAY,WACd,MAAM,IAAI,UACR,qBAAqB,MAAM,eAAe,UAAU,EACtD;IAEF,IAAI,YAAY,WACd,MAAM,IAAI,QACR,2BACA,uDACF;IAEF,IAAI,YAAY,cAAc,OAAO,EAAE,QAAQ;IAE/C,IAAI,YAAY,MAAM;KACpB,MAAM,OAAO,OAAO,MAAM,EAAE,IAAI;KAChC,IAAI,OAAO,SAAS,GAClB,MAAM,QACJ,cAAc,SAAS,GACvB,YACE,OAAO,OAAO,QACd,KACA,OAAO,KAAK,OAAO,MAAM;MACvB,OAAO,IAAI;MACX,MAAM;MACN,MAAM;MACN,SAAS;KACX,CAAC,CACH,CACF;KAEF,OAAO;MACL,SAAS;MACT,QAAQ,OAAO,KAAK,OAAO,OAAO;OAChC,IAAI,MAAM;OACV,MAAM,MAAM;OACZ,SAAS,gBAAgB,MAAM,OAAO;OACtC,OAAO,OAAO,IAAI;OAClB;OACA,WAAW,IAAI,KAAK,GAAG;OACvB,OAAO,EAAE,GAAG,MAAM;MACpB,EAAE;KACJ;IACF;IACA,IAAI,YAAY,aAAa;KAC3B,IAAI,OAAO,WAAW,GACpB,OAAO;MAAE,SAAS;MAAa,QAAQ,CAAC;KAAE;KAE5C,MAAM,aAAa,KAAK,IAAI,GAAG,MAAM,MAAM,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC;KACzD,MAAM,EAAE,MAAM,UAAU,eAAe,MAAM,oBAC3C,iBACE,WACA,aAAa,GACb,aAAa,OAAO,SAAS,CAC/B,CACF;KAEA,IAAI,SAAS,YAAY,OAAO,EAAE,SAAS,aAAa;KACxD,IACE,SAAS,MACN,QACC,IAAI,OAAO,aAAa,YACxB,IAAI,MAAM,SAAS,QACnB,IAAI,MAAM,UAAU,SACpB,IAAI,MAAM,YAAY,OAC1B,GAEA,MAAM,IAAI,QACR,2BACA,yDACF;KAEF,MAAM,WAAW,iBAAiB,QAAQ,QAAQ;KAElD,MAAM,QACJ,cAAc,SAAS,GACvB,YAAY,aAAa,OAAO,SAAS,GAAG,KAAK,CAAC,CAAC,CACrD;KACA,OAAO;MAAE,SAAS;MAAa,QAAQ;KAAS;IAClD;IACA,MAAM,IAAI,UAAU,kCAAkC,QAAQ,EAAE;GAClE,CAAC;EACH;EAEA,MAAM,YAAY,EAChB,WACA,UACA,MACA,OACA,SACA,OACA,aACA,gBACC;GACD,OAAO,KAAK,YAAY;IACtB,MAAM,MAAM,MAAM,IAAI,CAAC,CAAC,QAAQ;IAChC,MAAM,QAAS,MAAM,WACnB,kBACA,WACA,CAAC,GACD;KACE;KACA,MAAM,IAAI;KACV;KACA;KACA;KACA;KACA;KACA,iBAAiB,KAAA,IAAY,KAAK,MAAM;IAC1C,CACF;IACA,MAAM,UAAU,MAAM;IACtB,IAAI,YAAY,WACd,MAAM,IAAI,UACR,qBAAqB,MAAM,eAAe,UAAU,EACtD;IAEF,OAAO;KACI;KACT,cAAc,OAAO,MAAM,EAAE;IAC/B;GACF,CAAC;EACH;EAEA,MAAM,YAAY,WAAW;GAC3B,OAAO,KAAK,YAAY;IAOtB,QAAO,MANc,UAAU;KAC7B,QAAQ;KACR,MAAM,CAAC,SAAS,SAAS,CAAC;KAC1B,MAAM,CAAC;KACP,UAAU;IACZ,CAAC,EAAA,CAEE,KAAK,SAAS,YAAY,WAAW,IAAI,CAAC,CAAC,CAC3C,SAAS,MAAM;GACpB,CAAC;EACH;EAEA,MAAM,QAAQ,EAAE,WAAW,UAAU,OAAO,QAAQ,aAAa;GAC/D,OAAO,KAAK,YAAY;IACtB,MAAM,CAAC,UAAU,cAAe,MAAM,SAAS,iBAC7C,WACE,aACA,WACA,CAAC,GACD;KACE;KACA;KACA;KACA,MAAM,IAAI,CAAC,CAAC,QAAQ;KACpB,UAAU,SAAS;IACrB,CACF,CACF;IACA,OAAO;KAAE,UAAU,aAAa;KAAK,YAAY,eAAe;IAAI;GACtE,CAAC;EACH;EAEA,MAAM,QAAQ,EAAE,WAAW,YAAY;GACrC,OAAO,KAAK,YAAY;IACtB,MAAM,EAAE,MAAM,QAAQ;IACtB,MAAM,GAAG,UAAU,MAAM,SAAS,eAAgB,MAAM,SACtD,iBAEE,WACE,cACA,WACA,CAAC,GACD;KAAC;KAAM,IAAI;KAAM,YAAY,IAAI,KAAK;KAAG,IAAI,WAAW,IAAI;IAAC,CAC/D,CACJ;IAOA,OAAO;KACL,QAAQ,OAAO,OAAO,WAAW,MAAM,IAAI,MAAM,OAAO,GAAG;MACzD,UAAU,OAAO,QAAQ;MACzB,MAAM,SAAS,OAAO,OAAO,OAAO,IAAI;KAC1C,CAAC;KACD,MAAM,OAAO,WAAW;IAC1B;GACF,CAAC;EACH;EAEA,MAAM,UAAU,WAAW,aAAa,cAAc;GACpD,OAAO,WACL,cAAc,YAAY;IACxB,MAAM,QAAS,MAAM,UAAU;KAC7B,QAAQ;KACR,MAAM;MACJ,SAAS,SAAS;MAClB,QAAQ,WAAW,WAAW;MAC9B,eAAe,WAAW,WAAW;KACvC;KACA,MAAM,CACJ,cAAc,wBACZ,cAAc,gBACd,EACJ;KACA,UAAU;IACZ,CAAC;IACD,MAAM,UAAU,MAAM;IACtB,MAAM,MAAM,MAAM;IAClB,MAAM,YAAY,YAAY,KAAK,OAAO,OAAO,OAAO;IACxD,MAAM,SACJ,QAAQ,KACJ,OACC,KAAK,MAAM,GAAG;IACrB,MAAM,kBAAkB,OAAO,MAAM,EAAE;IACvC,IACE,CAAC,OAAO,cAAc,eAAe,KACrC,kBAAkB,KACjB,cAAc,SACZ,CAAC,OAAO,cAAc,SAAS,KAAK,YAAY,MAClD,WAAW,SACT,CAAC,OAAO,cAAc,OAAO,KAAK,KAAK,OAAO,QAAQ,IAEzD,MAAM,IAAI,QACR,qBACA,qCAAqC,UAAU,aACjD;IAEF,MAAM,WAAW,KAAK,IACpB,iBACA,cAAc,gBAAgB,eAChC;IACA,MAAM,WAAW;IACjB,MAAM,aAAa,OAAO,MAAM,MAAM,CAAC;IAEvC,MAAM,SAAS,MAAM,eACnB,WACA,KAAK,IAAI,UAAU,SAAS,GAAG,aAAa,CAAC,GAC7C,QACF;IACA,OAAO;KACL,GAAI,aAAa,IAAI,EAAE,WAAW,IAAI,CAAC;KACvC;KACA,UAAU,WACN;MAAE,OAAO,SAAS;MAAO,OAAO,SAAS;KAAM,IAC/C;KACJ;IACF;GACF,CAAC,CACH;EACF;EAEA,MAAM,aAAa,WAAW,aAAa,QAAQ;GACjD,MAAM,0BAAU,IAAI,IAGlB;GACF,KAAK,MAAM,SAAS,QAAQ;IAC1B,MAAM,WAAW,QAAQ,IAAI,MAAM,KAAK;IACxC,MAAM,OAAO,UAAU,wBAAQ,IAAI,IAAqB;IACxD,KAAK,MAAM,OAAO,MAAM,QAAQ,CAAC,GAC/B,KAAK,IAAI,KAAK,UAAU;KAAC,IAAI;KAAU,IAAI;KAAM,IAAI;IAAK,CAAC,GAAG,GAAG;IAEnE,IAAI,UAAU,SAAS,QAAQ,MAAM;SAEnC,QAAQ,IAAI,MAAM,OAAO;KACvB,OAAO,MAAM;KACb,OAAO,MAAM;KACb;IACF,CAAC;GACL;GACA,MAAM,aAAa,CAAC,GAAG,QAAQ,OAAO,CAAC,CAAC,CAAC,UACtC,GAAG,MAAM,EAAE,QAAQ,EAAE,KACxB;GACA,IAAI,WAAW,WAAW,GAAG;GAC7B,MAAM,KAAK,YAAY;IACrB,MAAM,OAA+B,CAAC,WAAW,MAAM;IACvD,KAAK,MAAM,SAAS,YAClB,KAAK,KACH,MAAM,OACN,KAAK;KACH,OAAO,MAAM;KACb,OAAO,MAAM;KACb,WAAW,MAAM,IAAI,CAAC,CAAC,QAAQ;KAE/B,GAAG;IACL,CAAC,GACD,MAAM,KAAK,MACX,GAAG,CAAC,GAAG,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,SAAS,QAAQ;KAC3C,IAAI;KACJ,MAAM,IAAI,IAAI;KACd,IAAI;IACN,CAAC,CACH;IAEF,MAAM,WACJ,mBACA,WACA,CACE,QAAQ,WAAW,WAAW,GAC9B,eAAe,WAAW,WAAW,CACvC,GACA,CAAC,GAAG,MAAM,WAAW,CACvB;GACF,CAAC;EACH;EAEA,UAAU;GACR,MAAM,IAAI,IAAI,aAAa,QAAQ,MAAM;IACvC,OAAO,KAAK,YAAY;KACtB,MAAM,UAAU,OAAO,QAAQ,MAAM;KACrC,IAAI,QAAQ,WAAW,GAAG,OAAO;KACjC,MAAM,OAAO,KAAK,GAAG,QAAQ;KAI7B,MAAM,QAAQ,MAAM,IAAI,CAAC,CAAC,QAAQ;KAClC,MAAM,cAAc,QAAQ,KAAK;KAIjC,MAAM,UAAW,MAAM,UAAU;MAC/B,QAAQ;MACR,MAAM,CAAC,YAAY,EAAE,CAAC;MACtB,MAAM;OACJ;OACA,KAAK;OACL;OACA,QAAQ;OAGR,GAAG,QAAQ,SAAS,CAAC,OAAO,WAAW,CACrC,KAAK,UAAU,CAAC,aAAa,KAAK,CAAC,GACnC,KAAK;QAAE,GAAG;QAAM,GAAG;QAAa,GAAG,KAAK;QAAM,GAAG;OAAM,CAAC,CAC1D,CAAC;MACH;MACA,UAAU;KACZ,CAAC;KACD,IAAI,QAAQ,WAAW,GAAG,OAAO;KAGjC,MAAM,gBAAyC,gBAAgB;KAC/D,MAAM,YAAsB,CAAC;KAC7B,KAAK,MAAM,YAAY,SAAS;MAC9B,MAAM,CAAC,OAAO,SAAS,QAAQ,OAAO,QAAQ,IAAI;MAClD,cAAc,SAAS,gBAAgB,KAAK;MAC5C,UAAU,KAAK,GAAG,KAAK,KAAK,EAAE,GAAG,KAAK,KAAK,GAAG;KAChD;KACA,OAAO;MACL,OAAO;OACL;OACA,QAAQ;OACR,MAAM,KAAK;OACX,IAAI,IAAI,KAAK,IAAI;MACnB;MACA,WAAW,QACT,gBAAgB,EAAE,GAClB,GAAG,KAAK;OAAE,KAAK,QAAQ;OAAgB;OAAa,MAAM,KAAK;OAAM,IAAI;MAAK,CAAC,CAAC,CAAC,MAAM,GAAG,EAAE,EAAE,aAAa,UAAU,KAAK,GAAG,EAAE,GACjI;KACF;IACF,CAAC;GACH;GAEA,MAAM,KAAK,IAAI;IACb,OAAO,KAAK,YAAY;KACtB,MAAM,MAAM,YAAY,EAAE;KAC1B,MAAM,QAAS,MAAM,KAAK,WAAW,GAAG;KACxC,MAAM,QAAQ,MAAM,IAAI,CAAC,CAAC,QAAQ;KAClC,MAAM,OAAsB,CAAC;KAC7B,MAAM,UAAoB,CAAC;KAC3B,KAAK,IAAI,IAAI,GAAG,IAAI,IAAI,MAAM,QAAQ,KAAK,GAAG;MAC5C,MAAM,QAAQ,KAAK,MAAM,MAAM,IAAI,EAAG;MAMtC,IAAI,MAAM,KAAK,OAAO;OACpB,QAAQ,KAAK,MAAM,EAAG;OACtB;MACF;MACA,IAAI,MAAM,MAAM,MAAM;MACtB,MAAM,CAAC,aAAa,SAAS,KAAK,MAAM,MAAM,EAAG;MAIjD,KAAK,KAAK;OACR;OACA;OACA,OAAO,MAAM;OACb,MAAM,MAAM;OACZ,IAAI,IAAI,KAAK,MAAM,CAAC;OACpB,WAAW,IAAI,KAAK,MAAM,CAAC;MAC7B,CAAC;KACH;KAGA,IAAI,QAAQ,SAAS,GACnB,MAAM,UAAU;MACd,QAAQ;MACR,MAAM,CAAC,GAAG;MACV,MAAM,CAAC,OAAO,GAAG,OAAO;MACxB,UAAU;KACZ,CAAC,CAAC,CAAC,YAAY,KAAA,CAAS;KAE1B,OAAO;IACT,CAAC;GACH;EACF;EAEA,MAAM,UAAU,WAAW,YAAY,cAAc;GACnD,OAAO,WAAW,UAAU,WAAW,YAAY,YAAY,CAAC;EAClE;EAEA,MAAM,SAAS,WAAW,WAAW,cAAc;GACjD,OAAO,KAAK,YAAY;IAGtB,MAAM,CAAC,QAAQ,cAAc,MAAM,QAAQ,IAAI,CAC7C,KACE,aACA,SAAS,SAAS,GAClB,iBAAiB,KAAA,IAAY,MAAM,GAAG,aAAa,KACnD,GAAG,UAAU,KACb,SAAA,GAEF,GACA,eAAe,SAAS,CAC1B,CAAC;IACD,IAAI,aAAa,WAAW,OAAO;IACnC,MAAM,OAAO,OACV,WAAW,CAAC,CACZ,KAAK,UAAU,QAAQ,WAAW,KAAK,CAAC;IAC3C,KAAK,IAAI,WAAW,GAAG,WAAW,KAAK,QAAQ,YAAY,GACzD,IAAI,KAAK,SAAS,CAAE,UAAU,KAAK,WAAW,EAAE,CAAE,QAAQ,GACxD,MAAM,IAAI,QACR,qBACA,kCAAkC,UAAU,oBAC9C;IAGJ,MAAM,SAAS,KAAK,EAAE,EAAE;IACxB,OAAO,WAAW,KAAA,KAAa,UAAU,YAAY,OAAO;GAC9D,CAAC;EACH;EAEA,MAAM,MAAM,EAAE,WAAW,aAAa,SAAS;GAC7C,OAAO,KAAK,YAAY;IACtB,MAAM,WAAW,WAAW;IAC5B,MAAM,MAAM,MAAM,IAAI,CAAC,CAAC,QAAQ;IAChC,MAAM,QAAS,MAAM,WACnB,WACA,WACA,CAAC,GACD;KACE;KACA;KACA,eAAe;KACf,OAAO,YAAY;KACnB,QAAQ,MAAM,MAAM,IAAI,IAAI;KAC5B,OAAO,SAAS;KAChB,OAAO,WAAW;KAClB;IACF,CACF;IACA,IAAI,MAAM,OAAO,cACf,MAAM,uBAAuB,WAAW,KAAM;IAEhD,IAAI,MAAM,OAAO,WACf,MAAM,IAAI,QACR,2BACA,wBAAwB,SAAS,iBACnC;IAEF,MAAM,OAAO,OAAO,MAAM,EAAE;IAC5B,IAAI,MAAM,OAAO,KACf,MAAM,QACJ,cAAc,SAAS,GACvB,YAAY,MAAM,KAAK,CACrB;KAAC;KAAM;KAAU;KAAkB;IAAM,CAC3C,CAAC,CACH;SACK,IAAI,MAAM,OAAO,OAAO,OAAO,GAEpC,MAAM,QAAQ,cAAc,SAAS,GAAG,YAAY,MAAM,KAAK,CAAC,CAAC,CAAC;IAEpE,OAAO;KACL,SAAS,MAAM,OAAO;KACtB,OAAO,OAAO,MAAM,EAAE;KACtB,YAAY,OAAO,MAAM,EAAE;IAC7B;GACF,CAAC;EACH;EAEA,MAAM,QAAQ,WAAW;GACvB,OAAO,KAAK,YAAY;IACtB,MAAM,QAAS,MAAM,WACnB,mBACA,WACA,CAAC,GACD,CAAA,GAAe,CACjB;IACA,IAAI,MAAM,WAAW,GAAG,OAAO;KAAE,SAAS;KAAG,MAAM;IAAK;IACxD,MAAM,CAAC,SAAS,QAAS,MAAM,WAC7B,aACA,WACA,CAAC,GACD;KAAC;KAAW,MAAM,SAAS;KAAG,GAAG;IAAK,CACxC;IACA,OAAO;KAAE,SAAS,OAAO,OAAO;KAAG,MAAM,OAAO,IAAI,MAAM;IAAE;GAC9D,CAAC;EACH;EAEA,aAAa,cAAc,WAAW,eAAe,SAAS,CAAC;EAE/D,MAAM,WAAW,WAAW,YAAY;GACtC,OAAO,KAAK,YAAY;IAStB,QAAO,MARgB,KACrB,UACA,SAAS,SAAS,GAClB,GAAG,aAAa,EAAE,KAClB,KACA,SAAA,GAEF,EAAA,CACe,KAAK,UAAU,QAAQ,WAAW,KAAK,CAAC;GACzD,CAAC;EACH;EACA,MAAM,aAAa,WAAW,YAAY;GACxC,OAAO,KAAK,YAAY;IACtB,MAAM,QAAQ;KACZ,SAAS,SAAS;KAClB,GAAG,aAAa,EAAE;KAClB;KACA;KACA;IACF;IACA,IAAI,WAKF,QAAO,MAJgB,UACrB,UACA,GAAG,KACL,EAAA,CACe,KAAK,UAClB,uBAAuB,WAAW,KAAK,CACzC;IAGF,QAAO,MADgB,KAAK,UAAU,GAAG,KAAK,EAAA,CAC/B,KAAK,UAAU,kBAAkB,WAAW,KAAK,CAAC;GACnE,CAAC;EACH;CACF;AACF;;;;AC3lGA,MAAM,cAAc;;;;;;AAUpB,MAAM,aAAa,YAAiC;CAClD,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,OAAO;CAC7B,QAAQ;EACN,OAAO;CACT;CACA,IAAI,OAAO,WAAW,UACpB,OAAO,OAAO,cAAc,MAAM,IAC9B;EAAE,UAAU;EAAQ,WAAW;EAAG,SAAS,CAAC;CAAE,IAC9C;CAEN,IAAI,OAAO,WAAW,YAAY,WAAW,MAAM,OAAO;CAC1D,MAAM,EAAE,GAAG,GAAG,MAAM;CACpB,IAAI,OAAO,MAAM,YAAY,CAAC,OAAO,cAAc,CAAC,GAAG,OAAO;CAC9D,IAAI,MAAM,KAAA,GAAW,OAAO;EAAE,UAAU;EAAG,WAAW;EAAG,SAAS,CAAC;CAAE;CACrE,IAAI,CAAC,MAAM,QAAQ,CAAC,KAAK,OAAO,MAAM,YAAY,CAAC,OAAO,SAAS,CAAC,GAClE,OAAO;CACT,KAAK,MAAM,SAAS,GAClB,IACE,CAAC,MAAM,QAAQ,KAAK,KACpB,MAAM,WAAW,KACjB,CAAC,OAAO,cAAc,MAAM,EAAE,KAC9B,OAAO,MAAM,OAAO,YACpB,OAAO,MAAM,OAAO,UAEpB,OAAO;CAEX,OAAO;EAAE,UAAU;EAAG,WAAW;EAAG,SAAS;CAAiB;AAChE;AAEA,SAAgB,uBAAuB,SAUE;CACvC,MAAM,EAAE,QAAQ,OAAO,KAAK,WAAW,WAAW;CAClD,MAAM,iBAAiB,cACrB,GAAG,OAAO,GAAG,UAAU;CACzB,MAAM,mBAAmB,OAAuB,GAAG,OAAO,GAAG,GAAG;CAEhE,MAAM,aAAa,OAAO,WAAW;CACrC,MAAM,OAAO,qBAAqB;EAChC,MAAM,OAAO,SAAS,GAAG,SAAS;GAEhC,QAAO,MADS,OAAO,EAAA,CACd,KAAK,SAAS,GAAG,IAAI;EAChC;EACA;EACA;EACA,WAAW;EACX,gBAAgB;EAChB,GAAI,QAAQ,YAAY,EAAE,WAAW,QAAQ,UAAU,IAAI,CAAC;EAC5D,GAAI,QAAQ,aAAa,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;CACjE,CAAC;CAED,IAAI,UAAU;CACd,IAAI,aAA8C;;CAElD,MAAM,wBAAQ,IAAI,IAAuB;;CAEzC,MAAM,gCAAgB,IAAI,IAGxB;CACF,MAAM,wCAAwB,IAAI,IAAkC;CACpE,MAAM,iCAAiB,IAAI,IAA0B;CAErD,QAAQ,eAAe,YAAY;EACjC,KAAK,MAAM,QAAQ,MAAM,IAAI,OAAO,KAAK,CAAC,GAAG;GAC3C,KAAK,QAAQ;GACb,KAAK,OAAO;EACd;EACA,sBACG,IAAI,OAAO,CAAC,EACX,UAAU,UAAU,CAAC,CACtB,YAAY,CAAC,CAAC;CACnB,CAAC;CAED,MAAM,yBAAyB,EAC7B,cACA,cAIU;EACV,IAAI;EACJ,IAAI;GACF,SAAS,KAAK,MAAM,OAAO;EAC7B,QAAQ;GACN;EACF;EACA,IAAI,OAAO,QAAQ,YAAY;EAC/B,MAAM,QAAuB;GAC3B,aAAa,OAAO;GACpB,QAAQ,OAAO;GACf,MAAM,OAAO;GACb,IAAI,IAAI,KAAK,OAAO,EAAE;EACxB;EACA,aAAa,UAAU,OAAO,KAAK;EACnC,KAAK,MAAM,YAAY,aAAa,WAAW,SAAS,KAAK;CAC/D;CAEA,MAAM,sBAAgD;EACpD,gBAAgB,YAAY;GAC1B,MAAM,QAAQ,MAAM,OAAO,EAAA,CAAG,UAAU;GACxC,KAAK,GAAG,YAAY,SAAS,YAAY;IACvC,KAAK,MAAM,QAAQ,MAAM,IAAI,OAAO,KAAK,CAAC,GAAG;KAC3C,IAAI,KAAK,QAAQ,QAAQ,UAAU,aAAa;MAC9C,KAAK,MAAM,KAAK,OAAO;MACvB,KAAK,SAAS,QAAQ;KACxB,OAAO;MACL,KAAK,MAAM,SAAS;MACpB,KAAK,KAAK,SAAS;MACnB,KAAK,WAAW,KAAA;MAChB,KAAK,QAAQ;MACb,KAAK,QAAQ;KACf;KACA,KAAK,OAAO;IACd;IACA,MAAM,eAAe,sBAAsB,IAAI,OAAO;IACtD,IAAI,cAAc,sBAAsB;KAAE;KAAc;IAAQ,CAAC;GACnE,CAAC;GACD,OAAO;EACT,EAAA,CAAG;EACH,OAAO;CACT;;;;;;;;;;;;CAaA,IAAI,iBAAkD;CACtD,MAAM,wBAAkD;EACtD,IAAI,gBAAgB,OAAO;EAC3B,MAAM,OAAO,cAAc,CAAC,CAAC,KAAK,OAAO,SAAS;GAChD,MAAM,KAAK,KAAK,QAAQ,OAAO;GAC/B,OAAO;EACT,CAAC;EACD,iBAAiB;EAEjB,KAAU,YAAY;GACpB,IAAI,mBAAmB,MAAM,iBAAiB;EAChD,CAAC;EACD,OAAO;CACT;CAEA,MAAM,iBAAiB,OAAO,YAAmC;EAC/D,MAAM,WAAW,cAAc,IAAI,OAAO;EAC1C,IAAI,UAAU;GACZ,SAAS,QAAQ;GACjB,IAAI;IACF,MAAM,SAAS;GACjB,SAAS,KAAK;IACZ,SAAS,QAAQ;IACjB,MAAM;GACR;GACA;EACF;EACA,MAAM,QAAgD;GACpD,MAAM;GAIN,OAAO,gBAAgB,CAAC,CAAC,KAAK,OAAO,SAAS;IAC5C,MAAM,KAAK,KAAK,aAAa,OAAO;IACpC,IAAI,cAAc,IAAI,OAAO,MAAM,SAAS,SAAS;IACrD,MAAM,WAAW,sBAAsB,IAAI,OAAO;IAClD,IAAI,QAAQ,UAAU,KAAK,UAAU;KACnC,SAAS,WAAW,KAAA;KACpB,MAAM,KAAK,QAAQ,MAAM,OAAO,SAAS,GAAG,EAAmB;KAC/D,SAAS,OAAO,aAAa;MAC3B,YAAY,KAAK,SAAS,KAAK,EAAE;MACjC,UAAU,UAAU;OAClB,KAAK,MAAM,YAAY,SAAS,WAAW,SAAS,KAAK;MAC3D;KACF,CAAC;KACD,MAAM,SAAS,KAAK;IACtB;GAEF,CAAC;EACH;EACA,cAAc,IAAI,SAAS,KAAK;EAChC,IAAI;GACF,MAAM,MAAM;EACd,SAAS,KAAK;GACZ,IAAI,cAAc,IAAI,OAAO,MAAM,OAAO;IACxC,cAAc,OAAO,OAAO;IAC5B,MAAM,WAAW,sBAAsB,IAAI,OAAO;IAClD,UAAU,MAAM,KAAK;IACrB,IAAI,UAAU,SAAS,OAAO,KAAA;GAChC;GACA,MAAM;EACR;CACF;CAEA,MAAM,kBAAkB,YAA0B;EAChD,MAAM,QAAQ,cAAc,IAAI,OAAO;EACvC,IAAI,CAAC,OAAO;EACZ,MAAM,QAAQ;EACd,IAAI,MAAM,OAAO,GAAG;EACpB,cAAc,OAAO,OAAO;EAC5B,gBAAqB,CAAC,CACnB,MAAM,SAAS,KAAK,KAAK,eAAe,OAAO,CAAC,CAAC,CACjD,YAAY,CAAC,CAAC;CACnB;CAEA,MAAM,QAA8C;EAClD,QAAQ,KAAK;EACb,SAAS,KAAK;EACd,MAAM,KAAK;EACX,aAAa,KAAK;EAClB,gBAAgB,KAAK;EACrB,aAAa,KAAK;EAClB,iBAAiB,KAAK;EACtB,aAAa,KAAK;EAClB,WAAW,KAAK;EAChB,cAAc,KAAK;EACnB,OAAO,KAAK;EACZ,SAAS,KAAK;EACd,aAAa,KAAK;EAClB,SAAS,KAAK;EACd,SAAS,KAAK;EAEd,UAAU;GACR,MAAM,IAAI,IAAI,aAAa,QAAQ,MAAM;IACvC,MAAM,UAAU,MAAM,KAAK,SAAS,IAAI,IAAI,aAAa,QAAQ,IAAI;IACrE,IAAI,CAAC,SAAS;IACd,MAAM,EAAE,UAAU;IAClB,MAAM,UAAU,gBAAgB,EAAE;IAClC,MAAM,eAAe,sBAAsB,IAAI,OAAO;IACtD,IAAI,cAAc;KAChB,aAAa,MAAM,OAAO,KAAK;KAC/B,aAAa,UAAU,OAAO,KAAK;KACnC,KAAK,MAAM,YAAY,aAAa,WAAW,SAAS,KAAK;IAC/D;IACA,MAAM,QAAQ;GAChB;GAEA,MAAM,KAAK,IAAI;IACb,MAAM,UAAU,gBAAgB,EAAE;IAClC,MAAM,cAAc,IAAI,OAAO,CAAC,EAAE;IAClC,MAAM,eAAe,sBAAsB,IAAI,OAAO;IACtD,OACE,cAAc,UAAU,KAAK,KAC7B,cAAc,MAAM,eAAe,KAAK,SAAS,KAAK,EAAE,CAAC,KACzD,KAAK,SAAS,KAAK,EAAE;GAEzB;GAEA,UAAU,IAAI,SAAS;IACrB,MAAM,UAAU,gBAAgB,EAAE;IAClC,IAAI,eAAe,sBAAsB,IAAI,OAAO;IACpD,IAAI,CAAC,cAAc;KACjB,eAAe,EAAE,2BAAW,IAAI,IAAI,EAAE;KACtC,sBAAsB,IAAI,SAAS,YAAY;KAC/C,IAAI,QAAQ,eAAe,CAAC,QAAQ,UAAU,GAAG;MAC/C,MAAM,UAAU;MAChB,aAAa,WAAW,iBAAiB;OACvC,YAAY,KAAK,SAAS,KAAK,EAAE;OACjC,UAAU,UAAU;QAClB,KAAK,MAAM,YAAY,QAAQ,WAAW,SAAS,KAAK;OAC1D;OACA,gBACE,CAAC,WAAW,sBAAsB,IAAI,OAAO,MAAM;MACvD,CAAC;KACH;IACF;IACA,aAAa,UAAU,IAAI,OAAO;IAKlC,eAAoB,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC;IAC3C,IAAI,UAAU;IACd,aAAa;KACX,IAAI,SAAS;KACb,UAAU;KACV,aAAa,UAAU,OAAO,OAAO;KACrC,IAAI,aAAa,UAAU,SAAS,GAAG;MACrC,aAAa,MAAM,KAAK;MACxB,sBAAsB,OAAO,OAAO;KACtC;KACA,eAAe,OAAO;IACxB;GACF;EACF;EAEA,OAAO,WAAW,MAAM;GACtB,MAAM,aAAa,MAAM,cAAc;GACvC,MAAM,UAAU,cAAc,SAAS;GAEvC,OAAO,EACL,CAAC,OAAO,iBAAuC;IAC7C,IAAI,OAAO;IACX,IAAI,SAAsB,CAAC;IAC3B,IAAI,SAAS;IACb,IAAI,aAAa;IACjB,IAAI,UAA0B;IAC9B,IAAI;IACJ,IAAI;IACJ,IAAI;IAOJ,IAAI;IACJ,MAAM,iBAAgC;KACpC,MAAM,UAAU;KAChB,UAAU,KAAA;KACV,IAAI,SAAS;MACX,QAAQ,WAAW,MAAM;MACzB,eAAe,QAAQ,SACpB,OAAO,KAAA,CAAS,CAAC,CACjB,WAAW,KAAA,CAAS,CAAC,CACrB,YAAY,CAAC,CAAC;KACnB;KACA,OAAO,gBAAgB,QAAQ,QAAQ;IACzC;IACA,MAAM,WAAW,OACf,UACqD;KACrD,IAAI,CAAC,SAAS;MACZ,MAAM,aAAa,IAAI,gBAAgB;MACvC,UAAU;OACR;OACA,WAAW;OACX,UAAU,KAAK,aAAc;QAC3B;QACA,YAAY;QACZ,QAAQ,WAAW;OACrB,CAAC,CAAC,CAAC,OAAO,cAAc,CAAC;MAC3B;KACF;KACA,MAAM,UAAU;KAChB,MAAM,SAAS,MAAM,QAAQ,SAAS,KAAK;KAC3C,IAAI,OAAO,QAAQ,YAAY,SAAS;MACtC,UAAU,KAAA;MACV,IAAI,QAAQ,WAAW,KAAK,QAAQ;KACtC,OAAO,IAAI,CAAC,OAAO,MAAM,QAAQ,YAAY;KAC7C,OAAO;IACT;IACA,MAAM,OAAa;KACjB,OAAO,CAAC;KACR,OAAO;KACP,OAAO;KACP,MAAM,CAAC;KACP,UAAU,KAAA;KACV,MAAM;KACN;IACF;IACA,MAAM,WAAW,2BAA2B;KAC1C,IAAI,QAAQ,iBAAiB,IAAI,MAAM,CAAC,EAAE,SAAS;IACrD,CAAC;IAED,MAAM,gBAAsB;KAC1B,SAAc;KACd,eAAe;KACf,IAAI,QAAQ;MACV,OAAY,SAAS;MACrB,eAAe,OAAO,MAAM;KAC9B;KACA,IAAI,CAAC,YAAY;KACjB,aAAa;KACb,MAAM,MAAM,MAAM,IAAI,OAAO;KAC7B,KAAK,OAAO,IAAI;KAChB,IAAI,KAAK,SAAS,GAAG,MAAM,OAAO,OAAO;KACzC,eAAe,OAAO;IACxB;;;;;;;;;;IAWA,MAAM,mBAAyB;KAC7B,MAAM,UAAU,KAAK,KAAK,OAAO,CAAC;KAClC,IAAI,YAAY;KAChB,KAAK,MAAM,WAAW,KAAK,MAAM,OAAO,CAAC,GAAG;MAC1C,MAAM,OAAO,UAAU,OAAO;MAC9B,IAAI,CAAC,MAAM,KAAK,QAAQ;WACnB,IAAI,KAAK,QAAQ,WAAW,GAC/B,YAAY,KAAK,IAAI,WAAW,KAAK,QAAQ;WAC1C,QAAQ,KAAK;OAAE;OAAM,OAAO,QAAQ;MAAO,CAAC;KACnD;KACA,QAAQ,MACL,GAAG,MAAM,EAAE,KAAK,QAAQ,EAAE,CAAE,KAAK,EAAE,KAAK,QAAQ,EAAE,CAAE,EACvD;KACA,KAAK,QAAQ;KACb,KAAK,MAAM,QAAQ,SAAS;MAC1B,IACE,KAAK,KAAK,QAAQ,EAAE,CAAE,MACrB,OAAO,GAAG,EAAE,CAAC,EAAE,SAAS,QAAQ,GACjC;OACA,KAAK,KAAK,KAAK,IAAI;OACnB,KAAK,SAAS,KAAK;OACnB;MACF;MACA,KAAK,MAAM,CAAC,OAAO,IAAI,MAAM,YAAY,KAAK,KAAK,SAAS;OAC1D,IAAI,UAAU,OAAO,GAAG,EAAE,CAAC,EAAE,SAAS,OAAO;OAC7C,OAAO,KAAK;QACV;QACA;QACA;QACA;QACA;QACA,WAAW,IAAI,KAAK,KAAK,KAAK,SAAS;OACzC,CAAC;MACH;KACF;KACA,IAAI,aAAa,OAAO,GAAG,EAAE,CAAC,EAAE,SAAS,OAAO,KAAK,QAAQ;KAC7D,IAAI,KAAK,KAAK,WAAW,GAAG,KAAK,WAAW,KAAA;UACvC,KAAK,aAAa,KAAK,IAAI;IAClC;IAEA,MAAM,WAAiC;KACrC,MAAM,OAAuC;MAC3C,SAAS;OACP,IAAI,QAAQ;QACV,QAAQ;QACR,OAAO;SAAE,OAAO,KAAA;SAAW,MAAM;QAAK;OACxC;OACA,MAAM,MAAM,OAAO,MAAM;OACzB,IAAI,KAAK;QACP,IAAI;QACJ,IAAI;SACF,QAAQ,kBAAkB,GAAG,IACzB,qBAAqB,GAAG,IACxB;QACN,SAAS,OAAO;SAEd,SAAS,CAAC;SACV,KAAK,QAAQ;SACb,QAAQ;SACR,MAAM,IAAI,QACR,qBACA,4BACA,EAAE,MAAM,CACV;QACF;QACA,OAAO,IAAI;QACX,OAAO;SAAE,OAAO;SAAO,MAAM;QAAM;OACrC;OACA,IAAI,SAAS;QACX,QAAQ;QACR,MAAM,IAAI,QAAQ,qBAAqB,sBAAsB;OAC/D;OACA,IAAI;QACF,IAAI,CAAC,YAAY;SAIf,aAAa;SACb,IAAI,MAAM,MAAM,IAAI,OAAO;SAC3B,IAAI,CAAC,KAAK;UACR,sBAAM,IAAI,IAAI;UACd,MAAM,IAAI,SAAS,GAAG;SACxB;SACA,IAAI,IAAI,IAAI;SAIZ,MAAM,GAAG,cAAc,MAAM,QAAQ,IAAI,CACvC,eAAe,OAAO,GACtB,KAAK,WAAW,SAAS,CAC3B,CAAC;SACD,IAAI,aAAa,IAAI,MAAM,OAAO,aAAa;SAC/C,IAAI,UAAU,SAAS;QACzB;QACA,IAAI,QAAQ,UAAU,GAAG;SACvB,IAAI,CAAC,QAAQ;UACX,SAAS,eACN,UAAU,KAAK,WAAW,WAAW,KAAK,GAC3C,EAAE,YAAY,KAAK,CACrB,CAAC,CAAC,OAAO,cAAc,CAAC;UACxB,eAAe,IAAI,MAAM;UACzB,eAAe,iBACZ,IAAI,MAAM,CAAC,EACV,UAAU,SAAS,OAAO;SAChC;SAEA,MAAM,SAAS,MAAM,OAAO,KAAK;SACjC,IAAI,CAAC,OAAO,MACV,sBACG,IAAI,gBAAgB,SAAS,CAAC,CAAC,EAC9B,MAAM,KAAK;SACjB,IAAI,SACF,MAAM,IAAI,QACR,qBACA,sBACF;SACF,OAAO;QACT;QACA,IAAI,SAAS;SAEX,MAAM,SAAS,MAAM,SAAS,IAAI;SAClC,IAAI,UAAU,SAAS;SACvB,IAAI,CAAC,OAAO,MAAM,SAAS,OAAO;SAClC;QACF;QACA,WAAW;QACX,IAAI,OAAO,SAAS,GAAG;QACvB,IACE,KAAK,aAAa,KAAA,KAClB,KAAK,IAAI,IAAI,KAAK,YAAY,eAAe,WAE7C,KAAK,QAAQ;QACf,IAAI,KAAK,OAAO;SACd,KAAK,QAAQ;SAEb,KAAK,KAAK,SAAS;SACnB,KAAK,WAAW,KAAA;SAChB,IAAI,KAAK,cAAc;UAErB,MAAM,SAAS,MAAM,SAAS,IAAI;UAClC,IAAI,UAAU,SAAS;UACvB,IAAI,CAAC,OAAO,MAAM,SAAS,OAAO;UAClC;SACF;SAEA,SAAS,MAAM,KAAK,aAAa,WAAW,IAAI;SAChD,IAAI,OAAO,UAAA,KAA0B,KAAK,QAAQ;SAClD;QACF;QAGA,MAAM,QAAQ,IAAI,SAAiB,YAAY;SAC7C,KAAK,aAAa,QAAQ,MAAM;QAClC,CAAC;QACD,MAAM,UAAU,KAAK,aAAa,KAAA;QAClC,UAAU,aACR,UACI,KAAK,IACH,GACA,eAAe,aACZ,KAAK,IAAI,IAAI,KAAK,SACvB,IACA,eAAe,YACrB;QAEA,MAAM,UAAU,MAAM,QAAQ,KAAK,CACjC,OACA,QAAQ,QAAQ,WAAW,OAAgB,CAC7C,CAAC;QACD,QAAQ,OAAO;QACf,UAAU;QACV,IAAI,YAAY,WAAW,CAAC,SAAS,KAAK,QAAQ;OACpD,SAAS,KAAK;QACZ,QAAQ;QACR,IAAI,QAAQ,OAAO;SAAE,OAAO,KAAA;SAAW,MAAM;QAAK;QAClD,IAAI,eAAe,SAAS,MAAM;QAClC,MAAM,IAAI,QACR,qBACA,gCACA,EAAE,OAAO,IAAI,CACf;OACF,UAAU;QACR,KAAK,OAAO;OACd;MACF;KACF;KACA,MAAM,SAAyC;MAC7C,SAAS;MACT,SAAS,OAAO;MAChB,KAAK,OAAO;MACZ,QAAQ;MACR,MAAM,SAAS;MACf,OAAO;OAAE,OAAO,KAAA;OAAW,MAAM;MAAK;KACxC;IACF;IACA,iBAAiB,IAAI,UAAU,QAAQ;IACvC,eAAe,IAAI,gBAAgB;KACjC,IAAI,QAAQ,OAAO,KAAA;KACnB,MAAM,MAAM,OAAO,MAAM;KACzB,IAAI,KAAK,OAAO,IAAI;KACpB,OAAO;IACT,CAAC;IACD,OAAO;GACT,EACF;EACF;EAEA,MAAM,QAAQ;GACZ,UAAU;GACV,KAAK,MAAM,gBAAgB,sBAAsB,OAAO,GACtD,aAAa,MAAM,KAAK;GAC1B,sBAAsB,MAAM;GAC5B,KAAK,MAAM,UAAU,gBAAgB,OAAY,SAAS;GAC1D,eAAe,MAAM;GACrB,MAAM,QAAyB,CAAC;GAChC,KAAK,MAAM,OAAO,MAAM,OAAO,GAC7B,KAAK,MAAM,QAAQ,KAAK;IACtB,KAAK,OAAO;IACZ,MAAM,KAAK,KAAK,SAAS,CAAC;GAC5B;GAEF,MAAM,QAAQ,WAAW,KAAK;GAC9B,IAAI,YAEF,CAAA,MADkB,WAAW,YAAY,IAAI,EAAA,EACxC,WAAW;GAElB,MAAM,QAAQ,YAAY;EAC5B;CACF;CACA,iBAAiB,IAAI,OAAO,KAAK,QAAQ;CACzC,OAAO;AACT"}