{"version":3,"sources":["../../src/realtime/index.ts","../../src/ops.ts","../../src/realtime/presence.ts"],"sourcesContent":["// src/realtime/index.ts — @ai-matrx/records/realtime\n//\n// THE STORE'S LIVE-UPDATES PORT, ONCE, FOR EVERY HOST (Applets AP-3, 2026-10-06).\n//\n// Until now `RecordsRealtimePort` was a contract with no implementation in the package: matrx-frontend\n// wrote its own (features/unified-data/realtime/recordsRealtimePort.ts) and every other host — the\n// Applets platform host first — would have had to write a second, or go without and never fire a\n// `rows:<alias>` event. This is that implementation, moved here so every host binds the same one.\n//\n// HOW THE STORE GOES LIVE. Schema `custom` is doors-only (zero table grants) and `custom.record` is\n// hash-partitioned, so `postgres_changes` cannot serve it. The database BROADCASTS a notice instead,\n// on a PRIVATE topic per Table — `custom:table:<table_id>`, event `records.changed`, written by\n// `custom.io_outbox_broadcast_stmt` and admitted by the one RLS policy on `realtime.messages`, which\n// asks the store's own ladder (`custom.assert_may_know_table`). This file joins that topic through\n// `@ai-matrx/data/realtime` — the ONE realtime layer (never a hand-written `.channel(`) — so the socket,\n// `setAuth` before a private join, backoff, dedupe and wake/reconnect backfill are the manager's.\n//\n// WHAT COMES DOWN THE WIRE IS NOT DATA: `{table_id, kind, op, op_id, record_ids, fields_changed, at}`\n// — ids, never values. The hooks re-read the named ids through the read door, which applies the ladder.\n//\n//   · ECHO SUPPRESSION BY OP ID — every write through this package mints `_op_id`; the store returns it\n//     as the notice's `op_id`; `isOwnOp` drops our own write's echo before anything else.\n//   · ONE CHANNEL PER TABLE, HOWEVER MANY READERS — opened by the first listener, closed by the last;\n//     each listener coalesces its own bursts.\n//   · A COLUMN IS NOT A ROW — a `field` / `table` notice calls `sink.shape()`.\n//   · NO REPLAY ON A WAKE OR RECONNECT — the only correct answer is `sink.records(null)` (read again).\n\nimport {\n  defineChannelNamespace,\n  subscribeToRealtimeManager,\n  type ChannelSpec,\n  type RealtimeManager,\n} from \"@ai-matrx/data/realtime\";\n\nimport { defaultOpLedger, type OpLedger } from \"../ops\";\nimport type { RecordsChangeSink, RecordsRealtimePort } from \"../ports\";\nimport type { Uuid } from \"../record\";\n\n/**\n * The topic is a CONTRACT WITH THE DATABASE (a Postgres trigger writes it, an RLS policy parses it),\n * so it is built in one place through the realtime package's foreign-topic door.\n */\nexport const storeTableChannel = defineChannelNamespace({\n  namespace: \"records-store-table\",\n  foreignTopic: \"custom:table\",\n  parts: [\"tableId\"],\n  description:\n    \"One Table in the record store. Carries NOTICES written by custom.io_outbox_broadcast_stmt — which record ids moved and whether the table's shape moved — never any value. Authorized by the one RLS policy on realtime.messages, which asks the store's own ladder.\",\n});\n\n/** The broadcast event the store sends on a table's topic. */\nexport const STORE_NOTICE_EVENT = \"records.changed\";\n\n/** Coalesce a burst into one re-read. A statement is already one notice; this catches two. */\nexport const NUDGE_DEBOUNCE_MS = 150;\n\nexport interface StoreNotice {\n  table_id?: string;\n  kind?: \"record\" | \"field\" | \"table\";\n  op?: \"created\" | \"updated\" | \"deleted\";\n  /** The client operation that caused it (`_op_id`), or null for a server, agent or foreign write. */\n  op_id?: string | null;\n  /** `null` MEANS \"re-read the page\" — above the cap the database stops listing ids. */\n  record_ids?: string[] | null;\n  fields_changed?: boolean;\n  at?: string;\n}\n\n/** The notice's own identity: a redelivered notice is the same statement at the same instant. */\nexport function noticeKey(notice: StoreNotice): string | undefined {\n  if (!notice.at) return undefined;\n  const ids = notice.record_ids == null ? \"*\" : [...notice.record_ids].sort().join(\",\");\n  return `${notice.table_id ?? \"\"}:${notice.kind ?? \"\"}:${notice.op ?? \"\"}:${notice.op_id ?? \"\"}:${notice.at}:${ids}`;\n}\n\ninterface TableListener {\n  notice(notice: StoreNotice): void;\n  backfill(): void;\n}\n\ninterface TableChannel {\n  listeners: Set<TableListener>;\n  stop: (() => void) | null;\n}\n\nexport interface CreateRecordsRealtimePortOptions {\n  /**\n   * The host's realtime manager (`@ai-matrx/data/realtime`). Left out, the AMBIENT manager is used — the one\n   * the app mounted (`setAmbientRealtimeManager` / `<RealtimeProvider>`); the port joins when one is\n   * mounted and follows it if it is replaced. Never a second socket of the port's own.\n   */\n  manager?: RealtimeManager;\n  /**\n   * The own-write memory of the client this port listens for (`createOpLedger()`, the same one handed\n   * to `createRecordsClient({ opLedger })`). Absent ⇒ the realm's default ledger.\n   */\n  ledger?: OpLedger;\n}\n\n/** The channel spec for one table: private, echo-guarded, deduped, backfilled. */\nexport function storeTableChannelSpec(\n  tableId: string,\n  onNotice: (notice: StoreNotice) => void,\n  onBackfill: () => void,\n  ledger: OpLedger = defaultOpLedger,\n): ChannelSpec {\n  return {\n    topic: storeTableChannel.topic({ tableId }),\n    // Database Broadcast is authorized by RLS on realtime.messages — that is what `private` means here;\n    // the manager awaits `setAuth()` before subscribing (its absence is a channel that looks healthy and\n    // delivers nothing forever).\n    private: true,\n    // The sender is Postgres: no Matrx envelope. OUR OWN WRITE is known by the op id the write door carried.\n    wire: { mode: \"raw\", isOwnMessage: (payload) => ledger.isOwn((payload as StoreNotice | null)?.op_id) },\n    eventKey: (_source, payload) => noticeKey((payload ?? {}) as StoreNotice),\n    broadcast: [\n      {\n        event: STORE_NOTICE_EVENT,\n        onMessage: (message) => {\n          const notice = (message.data ?? {}) as StoreNotice;\n          if (ledger.isOwn(notice.op_id)) return; // belt and braces: the manager's test ran first\n          onNotice(notice);\n        },\n      },\n    ],\n    // A CHANNEL WITH NO RECONCILIATION IS A SCREEN THAT WILL EVENTUALLY LIE: realtime has no replay.\n    onBackfill: () => onBackfill(),\n  };\n}\n\n/**\n * The store's live-updates port, bound to a realtime manager. Hand it to `RecordsConfig.realtime`; the\n * `@ai-matrx/records/data` port turns its sink calls into `rows:<alias>` events.\n */\nexport function createRecordsRealtimePort(options: CreateRecordsRealtimePortOptions = {}): RecordsRealtimePort {\n  const channels = new Map<string, TableChannel>();\n\n  const open = (tableId: string): TableChannel => {\n    const channel: TableChannel = { listeners: new Set(), stop: null };\n    const spec = storeTableChannelSpec(\n      tableId,\n      (notice) => {\n        for (const listener of channel.listeners) listener.notice(notice);\n      },\n      () => {\n        for (const listener of channel.listeners) listener.backfill();\n      },\n      options.ledger,\n    );\n    if (options.manager) {\n      const handle = options.manager.open(spec);\n      channel.stop = () => handle.close();\n    } else {\n      channel.stop = subscribeToRealtimeManager(() => spec);\n    }\n    return channel;\n  };\n\n  return {\n    subscribeRecords({ table_id }, sink: RecordsChangeSink) {\n      let timer: ReturnType<typeof setTimeout> | null = null;\n      let pending = new Set<Uuid>();\n      let wholePage = false;\n\n      const flush = () => {\n        timer = null;\n        const ids = [...pending];\n        const all = wholePage;\n        pending = new Set();\n        wholePage = false;\n        // THE IDS ARE USED AS IDS. `null` is reserved for \"the store could not name them\" or \"the socket\n        // was away\" and costs a whole read; a nudge naming three records re-reads three records.\n        if (all) sink.records(null);\n        else if (ids.length > 0) sink.records(ids);\n      };\n      const nudge = () => {\n        if (timer === null) timer = setTimeout(flush, NUDGE_DEBOUNCE_MS);\n      };\n\n      const listener: TableListener = {\n        notice(notice) {\n          if (notice.kind === \"field\" || notice.kind === \"table\") sink.shape();\n          if (notice.kind === \"field\") return;\n          if (notice.record_ids == null) wholePage = true;\n          else for (const id of notice.record_ids) pending.add(id);\n          nudge();\n        },\n        backfill() {\n          wholePage = true;\n          flush();\n        },\n      };\n\n      const channel = channels.get(table_id) ?? open(table_id);\n      channels.set(table_id, channel);\n      channel.listeners.add(listener);\n\n      return () => {\n        if (timer !== null) clearTimeout(timer);\n        channel.listeners.delete(listener);\n        if (channel.listeners.size > 0) return;\n        // The last reader of this table left: the table's one subscription goes with it.\n        channel.stop?.();\n        channels.delete(table_id);\n      };\n    },\n  };\n}\n\n// WHO ELSE IS LOOKING AT THIS TABLE (row 183): the presence port, on the same realtime layer.\nexport {\n  createRecordsPresencePort,\n  peopleOf,\n  presenceColor,\n  storeTablePresenceChannel,\n  type CreateRecordsPresencePortOptions,\n  type PresenceCell,\n  type PresenceSelf,\n  type RecordsPresencePort,\n  type TablePresencePerson,\n  type TablePresenceSession,\n} from \"./presence\";\n","/**\n * THE CLIENT OPERATION LEDGER — how a browser recognizes the echo of its own write.\n *\n * Every private-channel notice the record store broadcasts reaches EVERY browser watching that\n * Table, including the one that caused it. Until now the writer had no way to tell its own\n * change from a colleague's: the notice carried ids and nothing about who moved them, so\n * `recordsRealtimePort.ts` declared `acceptEchoFromSelf: true` and the writer re-read a page it\n * had already updated. It was named honestly rather than faked, and this is the other half.\n *\n * THE SHAPE. Every write through `RecordsClient` mints a uuid, sends it to the store as the\n * platform envelope key `_op_id`, and remembers it here. The store lifts that key out before\n * storage — it is never written onto the record — carries it on the change-feed row, and puts\n * it in the notice as `op_id`. A notice whose `op_id` is in this ledger is this browser's own\n * echo and is dropped.\n *\n * WHY A TIME WINDOW RATHER THAN A DELETE-ON-MATCH. One write can produce MORE THAN ONE notice:\n * a record write that also moves a column produces a `record` notice and a `field` notice, and\n * a statement touching two Tables produces one per Table. Forgetting an op the first time it\n * came back would make the second notice look like somebody else's and cost the re-read this\n * exists to avoid. So an op is remembered for a window and expires on its own.\n *\n * 🚨 THE WINDOW IS A CORRECTNESS BOUNDARY, NOT A TUNING KNOB, AND IT FAILS IN THE SAFE\n * DIRECTION BY CONSTRUCTION. Too SHORT and a late echo is treated as a stranger's change: one\n * extra read of a page that is already correct — invisible. Too LONG and a stranger's change\n * that happened to reuse our uuid would be dropped — which cannot happen, because it is a v4\n * uuid nobody else has ever seen. The only real cost of a long window is memory, so it is\n * bounded by COUNT as well as by time.\n *\n * WHAT THIS IS NOT. It is not a replacement for the store's optimistic concurrency, and it\n * never decides what is true — a dropped echo means \"I already know about this\", and every\n * path that could be wrong about that (a reconnect, a tab wake, a notice with no `op_id` at\n * all) re-reads through the door as it always did.\n */\nimport { globalSlot } from \"@ai-matrx/kit/global-slot\";\n\nimport type { Uuid } from \"./core/index\";\n\n/** How long an operation stays recognizable as ours. See the header: it fails safe both ways. */\nconst REMEMBER_MS = 30_000;\n\n/** A ceiling so a long-lived tab that writes constantly cannot grow this without bound. */\nconst REMEMBER_MAX = 500;\n\n/**\n * One client's memory of its own operations. `mint()` names a write and remembers it; `isOwn(opId)`\n * answers whether a notice is that write's echo.\n *\n * OWN-WRITE DETECTION IS PER CLIENT INSTANCE. Two setups in ONE process (a server holding two\n * identities, a worker beside a UI) must hear each other: a realm-wide ledger made A's write \"ours\" to\n * B as well, and B dropped it as an echo (2026-10-08, the README proof). `createRecords` gives every\n * setup its own ledger and hands it to its client, its entity port and its live-changes port.\n */\nexport interface OpLedger {\n  mint(): Uuid;\n  isOwn(opId: string | null | undefined): boolean;\n  /** Test seam: forget everything. */\n  clear(): void;\n  /** Test seam: how many operations are currently remembered. */\n  readonly size: number;\n}\n\nfunction ledgerOver(ours: Map<string, number>): OpLedger {\n  function trim(now: number): void {\n    // A Map iterates in insertion order and ids are minted in time order, so the first entry\n    // that is still inside the window means every entry after it is too.\n    for (const [id, at] of ours) {\n      if (now - at <= REMEMBER_MS) break;\n      ours.delete(id);\n    }\n    // Then the ceiling, oldest first, for a tab that writes faster than the window expires.\n    while (ours.size > REMEMBER_MAX) {\n      const oldest = ours.keys().next();\n      if (oldest.done === true) break;\n      ours.delete(oldest.value);\n    }\n  }\n  return {\n    mint() {\n      const id = crypto.randomUUID();\n      const now = Date.now();\n      ours.set(id, now);\n      trim(now);\n      return id as Uuid;\n    },\n    isOwn(opId) {\n      if (!opId) return false;\n      const at = ours.get(opId);\n      if (at === undefined) return false;\n      if (Date.now() - at > REMEMBER_MS) {\n        ours.delete(opId);\n        return false;\n      }\n      return true;\n    },\n    clear() {\n      ours.clear();\n    },\n    get size() {\n      return ours.size;\n    },\n  };\n}\n\n/** A ledger of one client's own: hand the SAME one to the client that writes and the port that listens. */\nexport function createOpLedger(): OpLedger {\n  return ledgerOver(new Map<string, number>());\n}\n\n/**\n * THE REALM'S DEFAULT LEDGER, IN A GLOBAL SLOT — for hosts that mount one client per realm (a browser\n * tab) and never name a ledger. Every section of the built package (root, core, react, realtime, data,\n * typed-table) carries its own copy of this module, and the ESM and CJS builds are two more graphs; a\n * module-level Map split it so `records/core` minted in one and `records/realtime` checked another — own\n * writes came back as strangers' changes (2026-10-08). Guarded on the packed tarball by\n * scripts/check-shared-state.mjs.\n */\nexport const defaultOpLedger: OpLedger = ledgerOver(globalSlot(\"ai-matrx.records.op-ledger\", () => new Map<string, number>()));\n\n/**\n * Mint the id for one client operation and remember it as ours (the realm's default ledger).\n *\n * ONE CALL IS ONE OPERATION. A fifty-row paste through `recordWriteMany` is ONE op id, because\n * the store announces one statement once — and it REFUSES a batch carrying two different ones\n * by name, for exactly that reason.\n */\nexport function mintOpId(): Uuid {\n  return defaultOpLedger.mint();\n}\n\n/**\n * Is this notice the echo of a write this realm's default ledger made?\n *\n * `undefined`/`null` is NOT ours and never has been: a notice with no `op_id` came from a\n * server-side write, an agent tool, another client, or a store older than this mechanism —\n * all of which are changes this browser has not seen and must read.\n */\nexport function isOwnOp(opId: string | null | undefined): boolean {\n  return defaultOpLedger.isOwn(opId);\n}\n\n/** Test seam: forget everything. Never called by product code. */\nexport function forgetAllOps(): void {\n  defaultOpLedger.clear();\n}\n\n/** Test seam: how many operations are currently remembered. */\nexport function rememberedOpCount(): number {\n  return defaultOpLedger.size;\n}\n","// src/realtime/presence.ts — @ai-matrx/records/realtime\n//\n// WHO ELSE IS LOOKING AT THIS TABLE (AIRTABLE-PARITY row 183), on the platform's one realtime layer.\n//\n// A PRIVATE presence topic per Table — `custom:table-presence:<table_id>` — joined through\n// `@ai-matrx/data/realtime` (the manager, `presence` on a ChannelSpec: heartbeat, ghost expiry, debounce\n// and reconnect are the package's). It is NOT the records topic `custom:table:<id>`: that one carries\n// database-written notices and has no INSERT policy on purpose. This one is admitted by\n// `custom.realtime_presence_topic_admits` (migration table_presence_who_is_looking) — exactly the people who may\n// know the table, the same ladder — and the one presence-only INSERT policy lets a browser announce itself.\n//\n// WHAT IS PUBLISHED about a person: their user id, a color derived from it, and the cell they have selected\n// (`{recordId, fieldKey}`) - never a value of the table, and NEVER a name or an avatar. A browser that supplies its\n// own name could appear as anyone (ADV-1 F10); the reader resolves the id to a name and picture through the host's\n// members roster (records-ui `TablePresence`), so the words on screen are the platform's, not the sender's.\n\nimport {\n  defineChannelNamespace,\n  onRealtimeManagerChange,\n  type ChannelHandle,\n  type RealtimeManager,\n} from \"@ai-matrx/data/realtime\";\n\nexport const storeTablePresenceChannel = defineChannelNamespace({\n  namespace: \"records-store-table-presence\",\n  foreignTopic: \"custom:table-presence\",\n  parts: [\"tableId\"],\n  description:\n    \"Who is looking at one Table in the record store right now: name, avatar, color and the selected cell. Authorized by the one admission registry (custom.realtime_presence_topic_admits), never a value of the table.\",\n});\n\n/** The cell a person has selected. */\nexport interface PresenceCell {\n  recordId: string;\n  fieldKey: string;\n}\n\n/** One other person looking at the table. */\nexport interface TablePresencePerson {\n  /** Stable per person (their user id, else the session key). */\n  key: string;\n  /** The published user id (null for an anonymous session). The reader names the person from it. */\n  userId: string | null;\n  /** \"Someone\" here: the reader resolves the id to a name through the members roster. Never read from the sender. */\n  name: string;\n  /** Always null here: the reader resolves the id through the members roster. Never read from the sender. */\n  avatarUrl: string | null;\n  /** An outline / ring color, the same on every screen for the same person. */\n  color: string;\n  cell: PresenceCell | null;\n}\n\nexport interface PresenceSelf {\n  userId: string | null;\n  /** Ignored: a name is never published (the reader resolves the id). Accepted so older hosts still type-check. */\n  name?: string;\n  /** Ignored, as `name`. */\n  avatarUrl?: string | null;\n}\n\nexport interface TablePresenceSession {\n  /** Everyone else looking right now (never this person, even from a second tab). */\n  getSnapshot(): readonly TablePresencePerson[];\n  subscribe(listener: () => void): () => void;\n  /** Tell the others which cell is selected (null = none). */\n  selectCell(cell: PresenceCell | null): void;\n  close(): void;\n}\n\nexport interface RecordsPresencePort {\n  join(args: { tableId: string }): TablePresenceSession;\n}\n\nexport interface CreateRecordsPresencePortOptions {\n  /** Who this person is, read at join time (the host's signed-in profile). Null = not known yet: watch, and join once it is. */\n  self: () => PresenceSelf | null;\n  /**\n   * Tell the port when `self()` may have changed (the signed-in user lands after the page mounted). A viewer\n   * whose user was not loaded at join otherwise never announced itself and never saw anybody (ADV-1 F10).\n   * Returns the unsubscribe. Left out, `self()` is read at join only.\n   */\n  onSelfChange?: (listener: () => void) => () => void;\n  /** The host's realtime manager. Left out, the AMBIENT manager is used (joins when one is mounted). */\n  manager?: RealtimeManager;\n}\n\ntype RosterState = Record<string, unknown> & {\n  userId: string | null;\n  color: string;\n  cell: PresenceCell | null;\n};\n\n/** The same color for the same person on every screen (a hue from their key; fixed saturation and lightness). */\nexport function presenceColor(key: string): string {\n  let h = 0;\n  for (let i = 0; i < key.length; i += 1) h = (h * 31 + key.charCodeAt(i)) >>> 0;\n  return `hsl(${h % 360} 70% 45%)`;\n}\n\nconst EMPTY: readonly TablePresencePerson[] = [];\n\nfunction readCell(value: unknown): PresenceCell | null {\n  if (!value || typeof value !== \"object\") return null;\n  const { recordId, fieldKey } = value as Record<string, unknown>;\n  return typeof recordId === \"string\" && typeof fieldKey === \"string\" ? { recordId, fieldKey } : null;\n}\n\n/**\n * The roster as the screen wants it: one entry per PERSON (two tabs of one person are one avatar), never\n * this person, ordered by name so the avatars do not shuffle on every heartbeat.\n */\nexport function peopleOf(\n  members: ReadonlyArray<{ key: string; isSelf: boolean; state: Partial<RosterState> }>,\n  selfUserId: string | null,\n): TablePresencePerson[] {\n  const byPerson = new Map<string, TablePresencePerson>();\n  for (const m of members) {\n    if (m.isSelf) continue;\n    const userId = typeof m.state.userId === \"string\" ? m.state.userId : null;\n    if (userId && userId === selfUserId) continue;\n    const key = userId ?? m.key;\n    const cell = readCell(m.state.cell);\n    const prev = byPerson.get(key);\n    // Two tabs of one person: show the one with a selected cell.\n    if (prev && (prev.cell || !cell)) continue;\n    // The sender's own `name` / `avatarUrl` are NEVER read: anyone admitted to the topic could put another\n    // member's name there. The reader names the person from `userId` (ADV-1 F10).\n    byPerson.set(key, {\n      key,\n      userId,\n      name: \"Someone\",\n      avatarUrl: null,\n      color: presenceColor(key),\n      cell,\n    });\n  }\n  return [...byPerson.values()].sort((a, b) => a.name.localeCompare(b.name) || a.key.localeCompare(b.key));\n}\n\nexport function createRecordsPresencePort(options: CreateRecordsPresencePortOptions): RecordsPresencePort {\n  return {\n    join({ tableId }) {\n      let self = options.self();\n      const listeners = new Set<() => void>();\n      let snapshot: readonly TablePresencePerson[] = EMPTY;\n      let handle: ChannelHandle<RosterState> | null = null;\n      let unsub: (() => void) | null = null;\n      let cell: PresenceCell | null = null;\n      let closed = false;\n      let current: RealtimeManager | null = null;\n      let stopSelf: (() => void) | null = null;\n\n      const publish = (next: readonly TablePresencePerson[]) => {\n        const same =\n          next.length === snapshot.length &&\n          next.every((p, i) => {\n            const q = snapshot[i]!;\n            return p.key === q.key && p.name === q.name && p.avatarUrl === q.avatarUrl && p.cell?.recordId === q.cell?.recordId && p.cell?.fieldKey === q.cell?.fieldKey;\n          });\n        if (same) return;\n        snapshot = next;\n        for (const l of [...listeners]) l();\n      };\n\n      const attach = (manager: RealtimeManager | null) => {\n        unsub?.();\n        unsub = null;\n        handle?.close();\n        handle = null;\n        publish(EMPTY);\n        current = manager;\n        self = self ?? options.self();\n        if (!manager || closed || !self) return;\n        const who = self;\n        const state: RosterState = {\n          userId: who.userId,\n          color: presenceColor(who.userId ?? who.name ?? \"someone\"),\n          cell,\n        };\n        handle = manager.open<RosterState>({\n          topic: storeTablePresenceChannel.topic({ tableId }),\n          private: true,\n          presence: { state },\n        });\n        const room = handle.presence;\n        if (!room) return;\n        const read = () => publish(peopleOf(room.members() as never, who.userId));\n        unsub = room.subscribe(read);\n        read();\n      };\n\n      const stopWatching = options.manager\n        ? (attach(options.manager), () => undefined)\n        : onRealtimeManagerChange((manager) => attach(manager));\n\n      // The user was not known at join: attach as soon as the host says they are (once; then stop listening).\n      if (!self && options.onSelfChange) {\n        stopSelf = options.onSelfChange(() => {\n          if (closed || self || !current) return;\n          if (!options.self()) return;\n          stopSelf?.();\n          stopSelf = null;\n          attach(current);\n        });\n      }\n\n      return {\n        getSnapshot: () => snapshot,\n        subscribe(listener) {\n          listeners.add(listener);\n          return () => {\n            listeners.delete(listener);\n          };\n        },\n        selectCell(next) {\n          if (cell?.recordId === next?.recordId && cell?.fieldKey === next?.fieldKey) return;\n          cell = next;\n          handle?.presence?.update({ cell: next });\n        },\n        close() {\n          if (closed) return;\n          closed = true;\n          stopSelf?.();\n          stopSelf = null;\n          stopWatching();\n          unsub?.();\n          handle?.close();\n          handle = null;\n          listeners.clear();\n        },\n      };\n    },\n  };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2BA,IAAAA,mBAKO;;;ACCP,yBAA2B;AAK3B,IAAM,cAAc;AAGpB,IAAM,eAAe;AAoBrB,SAAS,WAAW,MAAqC;AACvD,WAAS,KAAK,KAAmB;AAG/B,eAAW,CAAC,IAAI,EAAE,KAAK,MAAM;AAC3B,UAAI,MAAM,MAAM,YAAa;AAC7B,WAAK,OAAO,EAAE;AAAA,IAChB;AAEA,WAAO,KAAK,OAAO,cAAc;AAC/B,YAAM,SAAS,KAAK,KAAK,EAAE,KAAK;AAChC,UAAI,OAAO,SAAS,KAAM;AAC1B,WAAK,OAAO,OAAO,KAAK;AAAA,IAC1B;AAAA,EACF;AACA,SAAO;AAAA,IACL,OAAO;AACL,YAAM,KAAK,OAAO,WAAW;AAC7B,YAAM,MAAM,KAAK,IAAI;AACrB,WAAK,IAAI,IAAI,GAAG;AAChB,WAAK,GAAG;AACR,aAAO;AAAA,IACT;AAAA,IACA,MAAM,MAAM;AACV,UAAI,CAAC,KAAM,QAAO;AAClB,YAAM,KAAK,KAAK,IAAI,IAAI;AACxB,UAAI,OAAO,OAAW,QAAO;AAC7B,UAAI,KAAK,IAAI,IAAI,KAAK,aAAa;AACjC,aAAK,OAAO,IAAI;AAChB,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT;AAAA,IACA,QAAQ;AACN,WAAK,MAAM;AAAA,IACb;AAAA,IACA,IAAI,OAAO;AACT,aAAO,KAAK;AAAA,IACd;AAAA,EACF;AACF;AAeO,IAAM,kBAA4B,eAAW,+BAAW,8BAA8B,MAAM,oBAAI,IAAoB,CAAC,CAAC;;;ACpG7H,sBAKO;AAEA,IAAM,gCAA4B,wCAAuB;AAAA,EAC9D,WAAW;AAAA,EACX,cAAc;AAAA,EACd,OAAO,CAAC,SAAS;AAAA,EACjB,aACE;AACJ,CAAC;AAgEM,SAAS,cAAc,KAAqB;AACjD,MAAI,IAAI;AACR,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK,EAAG,KAAK,IAAI,KAAK,IAAI,WAAW,CAAC,MAAO;AAC7E,SAAO,OAAO,IAAI,GAAG;AACvB;AAEA,IAAM,QAAwC,CAAC;AAE/C,SAAS,SAAS,OAAqC;AACrD,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,EAAE,UAAU,SAAS,IAAI;AAC/B,SAAO,OAAO,aAAa,YAAY,OAAO,aAAa,WAAW,EAAE,UAAU,SAAS,IAAI;AACjG;AAMO,SAAS,SACd,SACA,YACuB;AACvB,QAAM,WAAW,oBAAI,IAAiC;AACtD,aAAW,KAAK,SAAS;AACvB,QAAI,EAAE,OAAQ;AACd,UAAM,SAAS,OAAO,EAAE,MAAM,WAAW,WAAW,EAAE,MAAM,SAAS;AACrE,QAAI,UAAU,WAAW,WAAY;AACrC,UAAM,MAAM,UAAU,EAAE;AACxB,UAAM,OAAO,SAAS,EAAE,MAAM,IAAI;AAClC,UAAM,OAAO,SAAS,IAAI,GAAG;AAE7B,QAAI,SAAS,KAAK,QAAQ,CAAC,MAAO;AAGlC,aAAS,IAAI,KAAK;AAAA,MAChB;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN,WAAW;AAAA,MACX,OAAO,cAAc,GAAG;AAAA,MACxB;AAAA,IACF,CAAC;AAAA,EACH;AACA,SAAO,CAAC,GAAG,SAAS,OAAO,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,KAAK,EAAE,IAAI,cAAc,EAAE,GAAG,CAAC;AACzG;AAEO,SAAS,0BAA0B,SAAgE;AACxG,SAAO;AAAA,IACL,KAAK,EAAE,QAAQ,GAAG;AAChB,UAAI,OAAO,QAAQ,KAAK;AACxB,YAAM,YAAY,oBAAI,IAAgB;AACtC,UAAI,WAA2C;AAC/C,UAAI,SAA4C;AAChD,UAAI,QAA6B;AACjC,UAAI,OAA4B;AAChC,UAAI,SAAS;AACb,UAAI,UAAkC;AACtC,UAAI,WAAgC;AAEpC,YAAM,UAAU,CAAC,SAAyC;AACxD,cAAM,OACJ,KAAK,WAAW,SAAS,UACzB,KAAK,MAAM,CAAC,GAAG,MAAM;AACnB,gBAAM,IAAI,SAAS,CAAC;AACpB,iBAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,aAAa,EAAE,MAAM,YAAY,EAAE,MAAM,aAAa,EAAE,MAAM;AAAA,QACtJ,CAAC;AACH,YAAI,KAAM;AACV,mBAAW;AACX,mBAAW,KAAK,CAAC,GAAG,SAAS,EAAG,GAAE;AAAA,MACpC;AAEA,YAAM,SAAS,CAAC,YAAoC;AAClD,gBAAQ;AACR,gBAAQ;AACR,gBAAQ,MAAM;AACd,iBAAS;AACT,gBAAQ,KAAK;AACb,kBAAU;AACV,eAAO,QAAQ,QAAQ,KAAK;AAC5B,YAAI,CAAC,WAAW,UAAU,CAAC,KAAM;AACjC,cAAM,MAAM;AACZ,cAAM,QAAqB;AAAA,UACzB,QAAQ,IAAI;AAAA,UACZ,OAAO,cAAc,IAAI,UAAU,IAAI,QAAQ,SAAS;AAAA,UACxD;AAAA,QACF;AACA,iBAAS,QAAQ,KAAkB;AAAA,UACjC,OAAO,0BAA0B,MAAM,EAAE,QAAQ,CAAC;AAAA,UAClD,SAAS;AAAA,UACT,UAAU,EAAE,MAAM;AAAA,QACpB,CAAC;AACD,cAAM,OAAO,OAAO;AACpB,YAAI,CAAC,KAAM;AACX,cAAM,OAAO,MAAM,QAAQ,SAAS,KAAK,QAAQ,GAAY,IAAI,MAAM,CAAC;AACxE,gBAAQ,KAAK,UAAU,IAAI;AAC3B,aAAK;AAAA,MACP;AAEA,YAAM,eAAe,QAAQ,WACxB,OAAO,QAAQ,OAAO,GAAG,MAAM,cAChC,yCAAwB,CAAC,YAAY,OAAO,OAAO,CAAC;AAGxD,UAAI,CAAC,QAAQ,QAAQ,cAAc;AACjC,mBAAW,QAAQ,aAAa,MAAM;AACpC,cAAI,UAAU,QAAQ,CAAC,QAAS;AAChC,cAAI,CAAC,QAAQ,KAAK,EAAG;AACrB,qBAAW;AACX,qBAAW;AACX,iBAAO,OAAO;AAAA,QAChB,CAAC;AAAA,MACH;AAEA,aAAO;AAAA,QACL,aAAa,MAAM;AAAA,QACnB,UAAU,UAAU;AAClB,oBAAU,IAAI,QAAQ;AACtB,iBAAO,MAAM;AACX,sBAAU,OAAO,QAAQ;AAAA,UAC3B;AAAA,QACF;AAAA,QACA,WAAW,MAAM;AACf,cAAI,MAAM,aAAa,MAAM,YAAY,MAAM,aAAa,MAAM,SAAU;AAC5E,iBAAO;AACP,kBAAQ,UAAU,OAAO,EAAE,MAAM,KAAK,CAAC;AAAA,QACzC;AAAA,QACA,QAAQ;AACN,cAAI,OAAQ;AACZ,mBAAS;AACT,qBAAW;AACX,qBAAW;AACX,uBAAa;AACb,kBAAQ;AACR,kBAAQ,MAAM;AACd,mBAAS;AACT,oBAAU,MAAM;AAAA,QAClB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;AF/LO,IAAM,wBAAoB,yCAAuB;AAAA,EACtD,WAAW;AAAA,EACX,cAAc;AAAA,EACd,OAAO,CAAC,SAAS;AAAA,EACjB,aACE;AACJ,CAAC;AAGM,IAAM,qBAAqB;AAG3B,IAAM,oBAAoB;AAe1B,SAAS,UAAU,QAAyC;AACjE,MAAI,CAAC,OAAO,GAAI,QAAO;AACvB,QAAM,MAAM,OAAO,cAAc,OAAO,MAAM,CAAC,GAAG,OAAO,UAAU,EAAE,KAAK,EAAE,KAAK,GAAG;AACpF,SAAO,GAAG,OAAO,YAAY,EAAE,IAAI,OAAO,QAAQ,EAAE,IAAI,OAAO,MAAM,EAAE,IAAI,OAAO,SAAS,EAAE,IAAI,OAAO,EAAE,IAAI,GAAG;AACnH;AA2BO,SAAS,sBACd,SACA,UACA,YACA,SAAmB,iBACN;AACb,SAAO;AAAA,IACL,OAAO,kBAAkB,MAAM,EAAE,QAAQ,CAAC;AAAA;AAAA;AAAA;AAAA,IAI1C,SAAS;AAAA;AAAA,IAET,MAAM,EAAE,MAAM,OAAO,cAAc,CAAC,YAAY,OAAO,MAAO,SAAgC,KAAK,EAAE;AAAA,IACrG,UAAU,CAAC,SAAS,YAAY,UAAW,WAAW,CAAC,CAAiB;AAAA,IACxE,WAAW;AAAA,MACT;AAAA,QACE,OAAO;AAAA,QACP,WAAW,CAAC,YAAY;AACtB,gBAAM,SAAU,QAAQ,QAAQ,CAAC;AACjC,cAAI,OAAO,MAAM,OAAO,KAAK,EAAG;AAChC,mBAAS,MAAM;AAAA,QACjB;AAAA,MACF;AAAA,IACF;AAAA;AAAA,IAEA,YAAY,MAAM,WAAW;AAAA,EAC/B;AACF;AAMO,SAAS,0BAA0B,UAA4C,CAAC,GAAwB;AAC7G,QAAM,WAAW,oBAAI,IAA0B;AAE/C,QAAM,OAAO,CAAC,YAAkC;AAC9C,UAAM,UAAwB,EAAE,WAAW,oBAAI,IAAI,GAAG,MAAM,KAAK;AACjE,UAAM,OAAO;AAAA,MACX;AAAA,MACA,CAAC,WAAW;AACV,mBAAW,YAAY,QAAQ,UAAW,UAAS,OAAO,MAAM;AAAA,MAClE;AAAA,MACA,MAAM;AACJ,mBAAW,YAAY,QAAQ,UAAW,UAAS,SAAS;AAAA,MAC9D;AAAA,MACA,QAAQ;AAAA,IACV;AACA,QAAI,QAAQ,SAAS;AACnB,YAAM,SAAS,QAAQ,QAAQ,KAAK,IAAI;AACxC,cAAQ,OAAO,MAAM,OAAO,MAAM;AAAA,IACpC,OAAO;AACL,cAAQ,WAAO,6CAA2B,MAAM,IAAI;AAAA,IACtD;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,iBAAiB,EAAE,SAAS,GAAG,MAAyB;AACtD,UAAI,QAA8C;AAClD,UAAI,UAAU,oBAAI,IAAU;AAC5B,UAAI,YAAY;AAEhB,YAAM,QAAQ,MAAM;AAClB,gBAAQ;AACR,cAAM,MAAM,CAAC,GAAG,OAAO;AACvB,cAAM,MAAM;AACZ,kBAAU,oBAAI,IAAI;AAClB,oBAAY;AAGZ,YAAI,IAAK,MAAK,QAAQ,IAAI;AAAA,iBACjB,IAAI,SAAS,EAAG,MAAK,QAAQ,GAAG;AAAA,MAC3C;AACA,YAAM,QAAQ,MAAM;AAClB,YAAI,UAAU,KAAM,SAAQ,WAAW,OAAO,iBAAiB;AAAA,MACjE;AAEA,YAAM,WAA0B;AAAA,QAC9B,OAAO,QAAQ;AACb,cAAI,OAAO,SAAS,WAAW,OAAO,SAAS,QAAS,MAAK,MAAM;AACnE,cAAI,OAAO,SAAS,QAAS;AAC7B,cAAI,OAAO,cAAc,KAAM,aAAY;AAAA,cACtC,YAAW,MAAM,OAAO,WAAY,SAAQ,IAAI,EAAE;AACvD,gBAAM;AAAA,QACR;AAAA,QACA,WAAW;AACT,sBAAY;AACZ,gBAAM;AAAA,QACR;AAAA,MACF;AAEA,YAAM,UAAU,SAAS,IAAI,QAAQ,KAAK,KAAK,QAAQ;AACvD,eAAS,IAAI,UAAU,OAAO;AAC9B,cAAQ,UAAU,IAAI,QAAQ;AAE9B,aAAO,MAAM;AACX,YAAI,UAAU,KAAM,cAAa,KAAK;AACtC,gBAAQ,UAAU,OAAO,QAAQ;AACjC,YAAI,QAAQ,UAAU,OAAO,EAAG;AAEhC,gBAAQ,OAAO;AACf,iBAAS,OAAO,QAAQ;AAAA,MAC1B;AAAA,IACF;AAAA,EACF;AACF;","names":["import_realtime"]}