@ai-matrx/records 0.99.29 → 0.99.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +12 -1
  2. package/dist/entity/index.cjs +1 -1
  3. package/dist/entity/index.cjs.map +1 -1
  4. package/dist/entity/index.d.cts +3 -3
  5. package/dist/entity/index.d.ts +3 -3
  6. package/dist/entity/index.js +1 -1
  7. package/dist/entity/index.js.map +1 -1
  8. package/dist/entity/react.cjs +1 -1
  9. package/dist/entity/react.cjs.map +1 -1
  10. package/dist/entity/react.js +1 -1
  11. package/dist/entity/react.js.map +1 -1
  12. package/dist/{port-DWHLj_Sa.d.ts → port-Bk7TG0aE.d.ts} +3 -3
  13. package/dist/{port-BbWB9Pgq.d.cts → port-rqQEGhew.d.cts} +3 -3
  14. package/dist/react/index.cjs +4 -116
  15. package/dist/react/index.cjs.map +1 -1
  16. package/dist/react/index.d.cts +1 -2
  17. package/dist/react/index.d.ts +1 -2
  18. package/dist/react/index.js +3 -115
  19. package/dist/react/index.js.map +1 -1
  20. package/dist/realtime/index.cjs +1 -1
  21. package/dist/realtime/index.cjs.map +1 -1
  22. package/dist/realtime/index.d.cts +4 -4
  23. package/dist/realtime/index.d.ts +4 -4
  24. package/dist/realtime/index.js +1 -1
  25. package/dist/realtime/index.js.map +1 -1
  26. package/dist/setup/index.cjs +3 -3
  27. package/dist/setup/index.cjs.map +1 -1
  28. package/dist/setup/index.d.cts +2 -2
  29. package/dist/setup/index.d.ts +2 -2
  30. package/dist/setup/index.js +3 -3
  31. package/dist/setup/index.js.map +1 -1
  32. package/dist/typed-table/index.cjs +4 -116
  33. package/dist/typed-table/index.cjs.map +1 -1
  34. package/dist/typed-table/index.d.cts +1 -2
  35. package/dist/typed-table/index.d.ts +1 -2
  36. package/dist/typed-table/index.js +3 -115
  37. package/dist/typed-table/index.js.map +1 -1
  38. package/package.json +7 -5
@@ -28,7 +28,7 @@ __export(realtime_exports, {
28
28
  storeTableChannelSpec: () => storeTableChannelSpec
29
29
  });
30
30
  module.exports = __toCommonJS(realtime_exports);
31
- var import_realtime = require("@ai-matrx/realtime");
31
+ var import_realtime = require("@ai-matrx/data/realtime");
32
32
 
33
33
  // src/ops.ts
34
34
  var import_global_slot = require("@ai-matrx/kit/global-slot");
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/realtime/index.ts","../../src/ops.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/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/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/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 * THE CLIENT OPERATION LEDGER — how a browser recognises 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 recognisable 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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/realtime/index.ts","../../src/ops.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 * THE CLIENT OPERATION LEDGER — how a browser recognises 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 recognisable 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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2BA,sBAKO;;;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;;;AD1EtH,IAAM,wBAAoB,wCAAuB;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,4CAA2B,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":[]}
@@ -1,5 +1,5 @@
1
- import * as _ai_matrx_realtime from '@ai-matrx/realtime';
2
- import { RealtimeManager, ChannelSpec } from '@ai-matrx/realtime';
1
+ import * as _ai_matrx_data_realtime from '@ai-matrx/data/realtime';
2
+ import { RealtimeManager, ChannelSpec } from '@ai-matrx/data/realtime';
3
3
  import { O as OpLedger, R as RecordsRealtimePort } from '../ports-iATV-A2v.cjs';
4
4
  import '../field-ppX29ddi.cjs';
5
5
  import '../record-C8q21jjB.cjs';
@@ -8,7 +8,7 @@ import '../record-C8q21jjB.cjs';
8
8
  * The topic is a CONTRACT WITH THE DATABASE (a Postgres trigger writes it, an RLS policy parses it),
9
9
  * so it is built in one place through the realtime package's foreign-topic door.
10
10
  */
11
- declare const storeTableChannel: _ai_matrx_realtime.ChannelNamespace;
11
+ declare const storeTableChannel: _ai_matrx_data_realtime.ChannelNamespace;
12
12
  /** The broadcast event the store sends on a table's topic. */
13
13
  declare const STORE_NOTICE_EVENT = "records.changed";
14
14
  /** Coalesce a burst into one re-read. A statement is already one notice; this catches two. */
@@ -28,7 +28,7 @@ interface StoreNotice {
28
28
  declare function noticeKey(notice: StoreNotice): string | undefined;
29
29
  interface CreateRecordsRealtimePortOptions {
30
30
  /**
31
- * The host's realtime manager (`@ai-matrx/realtime`). Left out, the AMBIENT manager is used — the one
31
+ * The host's realtime manager (`@ai-matrx/data/realtime`). Left out, the AMBIENT manager is used — the one
32
32
  * the app mounted (`setAmbientRealtimeManager` / `<RealtimeProvider>`); the port joins when one is
33
33
  * mounted and follows it if it is replaced. Never a second socket of the port's own.
34
34
  */
@@ -1,5 +1,5 @@
1
- import * as _ai_matrx_realtime from '@ai-matrx/realtime';
2
- import { RealtimeManager, ChannelSpec } from '@ai-matrx/realtime';
1
+ import * as _ai_matrx_data_realtime from '@ai-matrx/data/realtime';
2
+ import { RealtimeManager, ChannelSpec } from '@ai-matrx/data/realtime';
3
3
  import { O as OpLedger, R as RecordsRealtimePort } from '../ports-BCNNghmf.js';
4
4
  import '../field-BHlhq24W.js';
5
5
  import '../record-C8q21jjB.js';
@@ -8,7 +8,7 @@ import '../record-C8q21jjB.js';
8
8
  * The topic is a CONTRACT WITH THE DATABASE (a Postgres trigger writes it, an RLS policy parses it),
9
9
  * so it is built in one place through the realtime package's foreign-topic door.
10
10
  */
11
- declare const storeTableChannel: _ai_matrx_realtime.ChannelNamespace;
11
+ declare const storeTableChannel: _ai_matrx_data_realtime.ChannelNamespace;
12
12
  /** The broadcast event the store sends on a table's topic. */
13
13
  declare const STORE_NOTICE_EVENT = "records.changed";
14
14
  /** Coalesce a burst into one re-read. A statement is already one notice; this catches two. */
@@ -28,7 +28,7 @@ interface StoreNotice {
28
28
  declare function noticeKey(notice: StoreNotice): string | undefined;
29
29
  interface CreateRecordsRealtimePortOptions {
30
30
  /**
31
- * The host's realtime manager (`@ai-matrx/realtime`). Left out, the AMBIENT manager is used — the one
31
+ * The host's realtime manager (`@ai-matrx/data/realtime`). Left out, the AMBIENT manager is used — the one
32
32
  * the app mounted (`setAmbientRealtimeManager` / `<RealtimeProvider>`); the port joins when one is
33
33
  * mounted and follows it if it is replaced. Never a second socket of the port's own.
34
34
  */
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  defineChannelNamespace,
4
4
  subscribeToRealtimeManager
5
- } from "@ai-matrx/realtime";
5
+ } from "@ai-matrx/data/realtime";
6
6
 
7
7
  // src/ops.ts
8
8
  import { globalSlot } from "@ai-matrx/kit/global-slot";
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/realtime/index.ts","../../src/ops.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/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/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/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 * THE CLIENT OPERATION LEDGER — how a browser recognises 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 recognisable 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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/realtime/index.ts","../../src/ops.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 * THE CLIENT OPERATION LEDGER — how a browser recognises 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 recognisable 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"],"mappings":"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","names":[]}
@@ -34,7 +34,7 @@ __export(setup_exports, {
34
34
  sessionAuth: () => sessionAuth
35
35
  });
36
36
  module.exports = __toCommonJS(setup_exports);
37
- var import_realtime4 = require("@ai-matrx/realtime");
37
+ var import_realtime4 = require("@ai-matrx/data/realtime");
38
38
 
39
39
  // src/core/primedBundle.ts
40
40
  var PRIMED_TTL_MS = 15e3;
@@ -7219,7 +7219,7 @@ function opHeader(opId) {
7219
7219
  var import_uuid4 = require("@ai-matrx/kit/uuid");
7220
7220
 
7221
7221
  // src/entity/core/realtime.ts
7222
- var import_realtime = require("@ai-matrx/realtime");
7222
+ var import_realtime = require("@ai-matrx/data/realtime");
7223
7223
  var entityOrgChannel = (0, import_realtime.defineChannelNamespace)({
7224
7224
  namespace: "entity-org",
7225
7225
  foreignTopic: "mx:entity-org",
@@ -7669,7 +7669,7 @@ function whereIdOpensPort(dataSource) {
7669
7669
  }
7670
7670
 
7671
7671
  // src/realtime/index.ts
7672
- var import_realtime3 = require("@ai-matrx/realtime");
7672
+ var import_realtime3 = require("@ai-matrx/data/realtime");
7673
7673
  var storeTableChannel = (0, import_realtime3.defineChannelNamespace)({
7674
7674
  namespace: "records-store-table",
7675
7675
  foreignTopic: "custom:table",