@ai-matrx/summon 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,8 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.3
4
+ ### Unreleased
5
+
6
+ - **New: the render map can live on the host.** `render` is now optional on `defineOverlay`, and `<SummonHost renders={…}>` takes an id → `() => import("./<id>.render")` map (`OverlayRenderMap`, data type erased, no `any`) for definitions declared without one. A definition's own `render` still wins. Why: every file that imports a definition to open it also inherited an `import()` edge to that overlay's window subtree; with ~220 overlays and ~450 opener files in AI Matrx that multiplied the build graph (a sub-app build went from ~10 s to an out-of-memory kill). An opened overlay with no render source shows its error card (`OverlayRenderMissingError`, `MISSING_RENDER_ERROR`) and logs in development. Guard: `src/react/__tests__/hostRenders.test.tsx`. Consumer action: none for existing users.
7
+
8
+ ## 0.1.2
9
+
10
+ SOURCE CHANGED WITH NO RELEASE NOTE. This release shipped source changes that nobody described in an Unreleased entry,
11
+ so they may include breaking API changes. Consumer action is UNKNOWN: read the diff before upgrading.
12
+ Diff: `git diff b7bc6ffc86 19d4c1f55d -- apps/shared/summon/src`
13
+
14
+ Changed shipped source files (4):
15
+ - `apps/shared/summon/src/persistence/controller.ts`
16
+ - `apps/shared/summon/src/persistence/data/createDataPersistence.ts`
17
+ - `apps/shared/summon/src/sheet/OverlaySheets.tsx`
18
+ - `apps/shared/summon/src/window/useWindowPanel.ts`
19
+
3
20
  ## 0.1.1
4
21
  ### Unreleased
5
22
 
23
+ - Window auto-placement bookkeeping lives in a global slot instead of module state (one record across duplicated module graphs). `./sheet` reads the close binding through `@ai-matrx/summon/dialog`. New guard `fullStackReload.test.tsx`: a window opened with data survives a full teardown and remount through `OverlayPersistence` (red without persistence). Consumer action: none.
6
24
  - **Fixed: an open overlay re-rendered on every render of the host's parent** (runtime sweep, 2026-10-16: a `shareLinkDialog` opened without its file id logged its refusal 425 times in its first 2 s inside AI Matrx). SummonHost's per-definition slot, its per-instance slot and every definition's instance body are now `memo` components (SummonHost itself stays a plain function), so an open overlay re-renders only when ITS instance changes (data reference, hidden), never because the app shell above the host re-rendered. Guard: `src/react/__tests__/renderStorm.test.tsx` (null-render under host churn, `parse()` refusal closes once, close-on-mount renders once, Controller never reopens a self-closed overlay, keepAlive hidden not re-rendered) — red on 0.1.0 (21 renders vs 1). Consumer action: none.
7
25
  - **New: `useOptionalSummonStore()`** (`./react`) — the nearest provider's store or `null`, for a package that mounts its own `SummonProvider` only when the host has none (`@ai-matrx/chat` does). Consumer action: none.
8
26
  - Source passes stricter consumer compilers (`verbatimModuleSyntax`, `erasableSyntaxOnly`, `noUnusedLocals`): type-only import in the window-manager slice, no constructor parameter properties in `KeepAliveRegistry` / `LoaderCache`, four unused names removed. Consumer action: none.
@@ -19,3 +37,4 @@ First release (C38): the Redux-driven render system carved out of matrx-frontend
19
37
  - Links: `?panels=` manager, grammar v2 (percent-encoded, v1-compatible), one open-from-link door. Fixed: link vs restore race (link data wins, restored geometry wins).
20
38
  - Persistence: browser-local store (per tab or per person) and `@ai-matrx/data` adapter over `users.user_surface_state`; Next.js router port; testing helpers.
21
39
 
40
+
package/README.md CHANGED
@@ -25,7 +25,7 @@ pnpm add @ai-matrx/summon react react-dom react-redux @reduxjs/toolkit
25
25
  | `html-to-image` (optional) | tray thumbnails of minimized windows (without it the tile shows no preview) |
26
26
  | `@supabase/supabase-js` | only `./persistence/data`, through `@ai-matrx/data` |
27
27
 
28
- Entries: `.` (core, no React), `./handles` (no React, no Redux), `./react`, `./window`, `./dialog`, `./sheet`, `./lite`, `./url`, `./persistence`, `./persistence/data`, `./next`, `./testing`, `./styles.css`.
28
+ Entries: `.` (core, no React), `./handles` (no React, no Redux), `./react`, `./window`, `./window-host` (the root-mountable half of the window section: `WindowsProvider`, `WindowTraySync`, `registerTrayPreview` — never loads WindowPanel), `./dialog`, `./sheet`, `./lite`, `./url`, `./persistence`, `./persistence/data`, `./next`, `./testing`, `./styles.css`.
29
29
 
30
30
  ## Quick start
31
31
 
@@ -121,6 +121,7 @@ const Always = () => <notesWindow.Controller noteId="42" />;
121
121
  | `window` | `{ mobile, mobileSidebarAs, ephemeral, preservation, restoreDefaults, windowId, deprecated, unbiddenHome }` for window overlays |
122
122
  | `link` | `{ key, aliases?, hydrate }`, see Links |
123
123
  | `parse` | narrow raw data once; `null` refuses |
124
+ | `render` | `() => import("./x.render")`; optional when the host supplies it (see below), and wins over the host map when present |
124
125
 
125
126
  Minimizing a window always keeps its component mounted, whatever `keepAlive` says.
126
127
 
@@ -159,6 +160,26 @@ emit("uploaded", { url });
159
160
 
160
161
  Closing the instance disposes the channel. Emitting after that is a silent no-op.
161
162
 
163
+ ### Large apps: the render map lives on the host
164
+
165
+ Every file that imports a definition to open it also imports that definition's `render: () => import(…)`. To a bundler, each such file gains an edge to the overlay's whole window subtree. With a few overlays that is free. With hundreds of overlays and hundreds of opener files it multiplies the build graph: in AI Matrx, about 450 opener files each reached about 220 window subtrees, and a sub-app build went from about 10 s to an out-of-memory kill. This is the host-build law: never fan out `import()` edges.
166
+
167
+ The fix keeps "declare once" at two files plus one line. Leave `render` off the definitions, and give the host one id → import map, generated from the file names, that only the host imports:
168
+
169
+ ```ts
170
+ // definitions/notes.overlay.ts — no render: importing it costs nothing
171
+ export const notes = defineOverlay<{ noteId?: string }>()({ id: "notes", label: "Notes" });
172
+
173
+ // definitions/render-map.ts — generated, imported ONLY by the host (behind its own lazy boundary)
174
+ import type { OverlayRenderMap } from "@ai-matrx/summon/react";
175
+ export const RENDER_MAP: OverlayRenderMap = { notes: () => import("./notes.render") };
176
+
177
+ // the host
178
+ <SummonHost definitions={APP_OVERLAYS} renders={RENDER_MAP} />
179
+ ```
180
+
181
+ `OverlayRenderMap` erases each module's data type (`OverlayRenderProps<never>`), so differently typed render modules share one map without `any`. An overlay that is opened with no render source at all (no `render`, no map entry) shows its error card (`OverlayRenderMissingError`, exported as `MISSING_RENDER_ERROR`) and logs in development. It is never a silent blank.
182
+
162
183
  ## Mounting into an existing Redux store
163
184
 
164
185
  ```ts
@@ -331,7 +352,7 @@ Behaviour you can rely on: maximize, minimize, pop-out and dock never remount th
331
352
 
332
353
  **`WindowsProvider`** is optional; options: `escapeYieldSelector` (visible elements matching it own the Escape key; default `[data-overlay-escape-yield]`), `mobileTopOffset` (default `var(--header-height, 0px)`), `selectionSurface` (a component wrapping every window body, for a host's selection or right-click menu).
333
354
 
334
- Also exported from `./window`: `WindowTraySync`, `LayoutIcon`, `registerTrayPreview` (a semantic preview for a minimized tile), `usePopoutControl`, `MobileDrawerSurface`, `MobileCardSurface`, `useWindowPanel`.
355
+ Also exported from `./window`: `WindowTraySync` and `registerTrayPreview` (a semantic preview for a minimized tile; both also on the light `./window-host` entry — mount providers from there at the app root), `LayoutIcon`, `usePopoutControl`, `MobileDrawerSurface`, `MobileCardSurface`, `useWindowPanel`.
335
356
 
336
357
  ### Dialog, sheet, light window
337
358
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/persistence-data.ts","../src/persistence/data/createDataPersistence.ts","../src/persistence/errors.ts"],"sourcesContent":["// @ai-matrx/summon/persistence/data — the @ai-matrx/data adapter over users.user_surface_state\n// (feature 'summon'; surface_key 'workspace' and 'state:<instanceKey>'; optimistic versions).\nexport {\n createDataPersistence,\n userSurfaceStateTable,\n SUMMON_SURFACE_FEATURE,\n type DataPersistenceOptions,\n type DataOverlayPersistence,\n type UserSurfaceStateRow,\n type UserSurfaceStateInsert,\n type UserSurfaceStateUpdate,\n} from \"./persistence/data/createDataPersistence\";\nexport { PersistenceVersionConflictError, isVersionConflict } from \"./persistence/errors\";\n","/**\n * createDataPersistence — the OverlayPersistencePort over `@ai-matrx/data`, backed by the\n * canonical per-person working-data table `users.user_surface_state` (design R4):\n *\n * feature = 'overlays', surface_key = <key>, state = <value>, version = optimistic version.\n * Keys: `workspace` (the person's open windows/layout), `state:<overlayId>:<instanceId>`.\n *\n * Every call goes through the data package's doors (listAll / insert / update / remove) — the\n * descriptor's rules apply: the table is an `entity` with `org.rule = \"binding\"`, so an insert\n * takes the organization from `createDb({ organization })` (the host's active organization) and\n * the door REFUSES when there is none; this adapter adds no fallback of its own. Reads are scoped\n * to the signed-in person (`user_id`), never to an organization.\n *\n * The row is unique per (user_id, feature, surface_key) including soft-deleted rows, so `remove`\n * soft-deletes and a later `save` revives the same row instead of inserting a duplicate.\n */\nimport { DatabaseError, insert, listAll, remove as removeRow, update, type Db, type TableDescriptor } from \"@ai-matrx/data/db\";\nimport type { OverlayPersistencePort, StoredValue } from \"@ai-matrx/summon\";\nimport { PersistenceVersionConflictError } from \"../errors\";\n\nexport const SUMMON_SURFACE_FEATURE = \"summon\";\n\ntype Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[];\n\n/** The columns of users.user_surface_state this adapter reads and writes. */\nexport interface UserSurfaceStateRow {\n id: string;\n user_id: string;\n feature: string;\n surface_key: string;\n state: Json;\n organization_id: string;\n version: number;\n deleted_at: string | null;\n created_at: string;\n updated_at: string;\n created_by: string | null;\n updated_by: string | null;\n metadata: Json;\n custom_fields: Json;\n}\nexport interface UserSurfaceStateInsert {\n user_id: string;\n feature: string;\n surface_key?: string;\n state?: Json;\n organization_id?: string;\n deleted_at?: string | null;\n}\nexport type UserSurfaceStateUpdate = Partial<UserSurfaceStateInsert>;\n\n/**\n * The generated descriptor for users.user_surface_state (`matrx-data generate --schemas users`,\n * contract 1436b3e225ff). A host may pass its own freshly generated one instead.\n */\nexport const userSurfaceStateTable = {\n schema: \"users\",\n name: \"user_surface_state\",\n kind: \"table\",\n type: \"entity\",\n pk: [\"id\"],\n org: { rule: \"binding\" },\n versioned: true,\n softDelete: true,\n archive: null,\n readOnly: false,\n deletesRefused: false,\n readOnlyColumns: [],\n listScope: \"mine\",\n realtime: false,\n createdBy: true,\n} as const satisfies TableDescriptor<UserSurfaceStateRow, UserSurfaceStateInsert, UserSurfaceStateUpdate> as unknown as TableDescriptor<\n UserSurfaceStateRow,\n UserSurfaceStateInsert,\n UserSurfaceStateUpdate\n>;\n\nexport interface DataPersistenceOptions {\n readonly db: Db;\n /** The signed-in person's id. Default: the session on `db.client.auth`. */\n readonly userId?: () => string | null | undefined;\n /** Override the descriptor (a host's own generated `userSurfaceState`). */\n readonly table?: TableDescriptor<UserSurfaceStateRow, UserSurfaceStateInsert, UserSurfaceStateUpdate>;\n /** Default \"overlays\". */\n readonly feature?: string;\n}\n\nexport interface DataOverlayPersistence extends OverlayPersistencePort {\n readonly perPerson: true;\n list(prefix: string): Promise<readonly string[]>;\n}\n\nconst COLUMNS = [\"id\", \"surface_key\", \"state\", \"version\", \"deleted_at\"] as const;\ntype Found = Pick<UserSurfaceStateRow, (typeof COLUMNS)[number]>;\n\nconst likeEscape = (value: string) => value.replace(/[\\\\%_]/g, (ch) => `\\\\${ch}`);\nconst isUniqueViolation = (error: unknown) => error instanceof DatabaseError && error.cause.code === \"23505\";\n\nexport function createDataPersistence(options: DataPersistenceOptions): DataOverlayPersistence {\n const { db } = options;\n const table = options.table ?? userSurfaceStateTable;\n const feature = options.feature ?? SUMMON_SURFACE_FEATURE;\n\n const personId = async (): Promise<string> => {\n const explicit = options.userId?.();\n if (explicit) return explicit;\n const session = db.client.auth ? await db.client.auth.getSession() : null;\n const id = session?.data.session?.user?.id;\n if (!id) throw new Error(\"[overlays/persistence/data] no signed-in person: overlay state is saved per person\");\n return id;\n };\n\n const find = async (key: string, includeDeleted: boolean): Promise<Found | null> => {\n const userId = await personId();\n const rows = await listAll(db, table, (q) => q.eq(\"user_id\", userId).eq(\"feature\", feature).eq(\"surface_key\", key), {\n columns: COLUMNS,\n includeDeleted,\n });\n // Unique per (user_id, feature, surface_key); a live row beats a soft-deleted one.\n return rows.find((row) => row.deleted_at === null) ?? rows[0] ?? null;\n };\n\n const conflict = (key: string, expected: number, current: number) => new PersistenceVersionConflictError(key, expected, current);\n\n return {\n perPerson: true,\n\n async load<T = unknown>(key: string): Promise<StoredValue<T> | null> {\n const row = await find(key, false);\n return row ? { value: row.state as T, version: row.version } : null;\n },\n\n async save<T = unknown>(key: string, value: T, saveOptions?: { readonly expectedVersion?: number }) {\n const expected = saveOptions?.expectedVersion;\n const state = value as unknown as Json;\n const row = await find(key, true);\n if (row && row.deleted_at === null) {\n if (expected === 0) throw conflict(key, 0, row.version);\n const result = await update(db, table, row.id, { state }, expected === undefined ? { overwrite: true } : { expectedVersion: expected });\n if (result.status === \"saved\") return { version: result.row.version };\n if (result.status === \"conflict\") throw conflict(key, expected ?? row.version, result.currentVersion);\n throw conflict(key, expected ?? row.version, 0); // vanished between read and write\n }\n if (expected !== undefined && expected !== 0) throw conflict(key, expected, 0);\n if (row) {\n // Revive the soft-deleted row (the unique key includes deleted rows).\n const result = await update(db, table, row.id, { state, deleted_at: null }, { expectedVersion: row.version });\n if (result.status === \"saved\") return { version: result.row.version };\n throw conflict(key, 0, result.status === \"conflict\" ? result.currentVersion : 0);\n }\n try {\n const created = await insert(db, table, { user_id: await personId(), feature, surface_key: key, state });\n return { version: created.version };\n } catch (error) {\n // Another device created it first.\n if (isUniqueViolation(error)) throw conflict(key, 0, (await find(key, true))?.version ?? 1);\n throw error;\n }\n },\n\n async remove(key: string) {\n const row = await find(key, false);\n if (!row) return;\n await removeRow(db, table, row.id, { overwrite: true });\n },\n\n async list(prefix: string) {\n const userId = await personId();\n const rows = await listAll(\n db,\n table,\n (q) => q.eq(\"user_id\", userId).eq(\"feature\", feature).like(\"surface_key\", `${likeEscape(prefix)}%`),\n { columns: [\"surface_key\"] },\n );\n return rows.map((row) => row.surface_key).filter((key) => key.startsWith(prefix)).sort();\n },\n };\n}\n","/**\n * Errors every persistence store throws in the same shape. Matched by `name`, never `instanceof`:\n * each section entry may carry its own copy of this module in a built consumer.\n */\nimport type { StoredValue } from \"@ai-matrx/summon\";\n\nexport const VERSION_CONFLICT_ERROR = \"OverlayPersistenceVersionConflict\";\nexport const LOAD_TIMEOUT_ERROR = \"OverlayPersistenceLoadTimeout\";\n\n/** A save whose `expectedVersion` no longer matches the stored version. */\nexport class PersistenceVersionConflictError extends Error {\n readonly key: string;\n readonly expectedVersion: number;\n readonly currentVersion: number;\n constructor(key: string, expectedVersion: number, currentVersion: number) {\n super(`[overlays/persistence] \"${key}\" is at version ${currentVersion}, not the expected ${expectedVersion}`);\n this.name = VERSION_CONFLICT_ERROR;\n this.key = key;\n this.expectedVersion = expectedVersion;\n this.currentVersion = currentVersion;\n }\n}\n\nexport function isVersionConflict(error: unknown): error is PersistenceVersionConflictError {\n return error instanceof Error && error.name === VERSION_CONFLICT_ERROR;\n}\n\n/**\n * The fast tier missed and the slow tier did not answer within the read budget. `pending` still\n * resolves the authoritative answer: a caller keeps every automatic writer gated until it does,\n * so an unknown stored value is never overwritten.\n */\nexport class PersistenceLoadTimeoutError extends Error {\n readonly key: string;\n readonly pending: Promise<StoredValue | null>;\n constructor(key: string, budgetMs: number, pending: Promise<StoredValue | null>) {\n super(`[overlays/persistence] \"${key}\" did not load within ${budgetMs}ms`);\n this.name = LOAD_TIMEOUT_ERROR;\n this.key = key;\n this.pending = pending;\n }\n}\n\nexport function isLoadTimeout(error: unknown): error is PersistenceLoadTimeoutError {\n return error instanceof Error && error.name === LOAD_TIMEOUT_ERROR;\n}\n"],"mappings":"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","names":["removeRow"]}
1
+ {"version":3,"sources":["../src/persistence-data.ts","../src/persistence/data/createDataPersistence.ts","../src/persistence/errors.ts"],"sourcesContent":["// @ai-matrx/summon/persistence/data — the @ai-matrx/data adapter over users.user_surface_state\n// (feature 'summon'; surface_key 'workspace' and 'state:<instanceKey>'; optimistic versions).\nexport {\n createDataPersistence,\n userSurfaceStateTable,\n SUMMON_SURFACE_FEATURE,\n type DataPersistenceOptions,\n type DataOverlayPersistence,\n type UserSurfaceStateRow,\n type UserSurfaceStateInsert,\n type UserSurfaceStateUpdate,\n} from \"./persistence/data/createDataPersistence\";\nexport { PersistenceVersionConflictError, isVersionConflict } from \"./persistence/errors\";\n","/**\n * createDataPersistence — the OverlayPersistencePort over `@ai-matrx/data`, backed by the\n * canonical per-person working-data table `users.user_surface_state` (design R4):\n *\n * feature = 'summon', surface_key = <key>, state = <value>, version = optimistic version.\n * Keys: `workspace` (the person's open windows/layout), `state:<overlayId>:<instanceId>`.\n *\n * Every call goes through the data package's doors (listAll / insert / update / remove) — the\n * descriptor's rules apply: the table is an `entity` with `org.rule = \"binding\"`, so an insert\n * takes the organization from `createDb({ organization })` (the host's active organization) and\n * the door REFUSES when there is none; this adapter adds no fallback of its own. Reads are scoped\n * to the signed-in person (`user_id`), never to an organization.\n *\n * The row is unique per (user_id, feature, surface_key) including soft-deleted rows, so `remove`\n * soft-deletes and a later `save` revives the same row instead of inserting a duplicate.\n */\nimport { DatabaseError, insert, listAll, remove as removeRow, update, type Db, type TableDescriptor } from \"@ai-matrx/data/db\";\nimport type { OverlayPersistencePort, StoredValue } from \"@ai-matrx/summon\";\nimport { PersistenceVersionConflictError } from \"../errors\";\n\nexport const SUMMON_SURFACE_FEATURE = \"summon\";\n\ntype Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[];\n\n/** The columns of users.user_surface_state this adapter reads and writes. */\nexport interface UserSurfaceStateRow {\n id: string;\n user_id: string;\n feature: string;\n surface_key: string;\n state: Json;\n organization_id: string;\n version: number;\n deleted_at: string | null;\n created_at: string;\n updated_at: string;\n created_by: string | null;\n updated_by: string | null;\n metadata: Json;\n custom_fields: Json;\n}\nexport interface UserSurfaceStateInsert {\n user_id: string;\n feature: string;\n surface_key?: string;\n state?: Json;\n organization_id?: string;\n deleted_at?: string | null;\n}\nexport type UserSurfaceStateUpdate = Partial<UserSurfaceStateInsert>;\n\n/**\n * The generated descriptor for users.user_surface_state (`matrx-data generate --schemas users`,\n * contract 1436b3e225ff). A host may pass its own freshly generated one instead.\n */\nexport const userSurfaceStateTable = {\n schema: \"users\",\n name: \"user_surface_state\",\n kind: \"table\",\n type: \"entity\",\n pk: [\"id\"],\n org: { rule: \"binding\" },\n versioned: true,\n softDelete: true,\n archive: null,\n readOnly: false,\n deletesRefused: false,\n readOnlyColumns: [],\n listScope: \"mine\",\n realtime: false,\n createdBy: true,\n} as const satisfies TableDescriptor<UserSurfaceStateRow, UserSurfaceStateInsert, UserSurfaceStateUpdate> as unknown as TableDescriptor<\n UserSurfaceStateRow,\n UserSurfaceStateInsert,\n UserSurfaceStateUpdate\n>;\n\nexport interface DataPersistenceOptions {\n readonly db: Db;\n /** The signed-in person's id. Default: the session on `db.client.auth`. */\n readonly userId?: () => string | null | undefined;\n /** Override the descriptor (a host's own generated `userSurfaceState`). */\n readonly table?: TableDescriptor<UserSurfaceStateRow, UserSurfaceStateInsert, UserSurfaceStateUpdate>;\n /** Default \"summon\". */\n readonly feature?: string;\n}\n\nexport interface DataOverlayPersistence extends OverlayPersistencePort {\n readonly perPerson: true;\n list(prefix: string): Promise<readonly string[]>;\n}\n\nconst COLUMNS = [\"id\", \"surface_key\", \"state\", \"version\", \"deleted_at\"] as const;\ntype Found = Pick<UserSurfaceStateRow, (typeof COLUMNS)[number]>;\n\nconst likeEscape = (value: string) => value.replace(/[\\\\%_]/g, (ch) => `\\\\${ch}`);\nconst isUniqueViolation = (error: unknown) => error instanceof DatabaseError && error.cause.code === \"23505\";\n\nexport function createDataPersistence(options: DataPersistenceOptions): DataOverlayPersistence {\n const { db } = options;\n const table = options.table ?? userSurfaceStateTable;\n const feature = options.feature ?? SUMMON_SURFACE_FEATURE;\n\n const personId = async (): Promise<string> => {\n const explicit = options.userId?.();\n if (explicit) return explicit;\n const session = db.client.auth ? await db.client.auth.getSession() : null;\n const id = session?.data.session?.user?.id;\n if (!id) throw new Error(\"[overlays/persistence/data] no signed-in person: overlay state is saved per person\");\n return id;\n };\n\n const find = async (key: string, includeDeleted: boolean): Promise<Found | null> => {\n const userId = await personId();\n const rows = await listAll(db, table, (q) => q.eq(\"user_id\", userId).eq(\"feature\", feature).eq(\"surface_key\", key), {\n columns: COLUMNS,\n includeDeleted,\n });\n // Unique per (user_id, feature, surface_key); a live row beats a soft-deleted one.\n return rows.find((row) => row.deleted_at === null) ?? rows[0] ?? null;\n };\n\n const conflict = (key: string, expected: number, current: number) => new PersistenceVersionConflictError(key, expected, current);\n\n return {\n perPerson: true,\n\n async load<T = unknown>(key: string): Promise<StoredValue<T> | null> {\n const row = await find(key, false);\n return row ? { value: row.state as T, version: row.version } : null;\n },\n\n async save<T = unknown>(key: string, value: T, saveOptions?: { readonly expectedVersion?: number }) {\n const expected = saveOptions?.expectedVersion;\n const state = value as unknown as Json;\n const row = await find(key, true);\n if (row && row.deleted_at === null) {\n if (expected === 0) throw conflict(key, 0, row.version);\n const result = await update(db, table, row.id, { state }, expected === undefined ? { overwrite: true } : { expectedVersion: expected });\n if (result.status === \"saved\") return { version: result.row.version };\n if (result.status === \"conflict\") throw conflict(key, expected ?? row.version, result.currentVersion);\n throw conflict(key, expected ?? row.version, 0); // vanished between read and write\n }\n if (expected !== undefined && expected !== 0) throw conflict(key, expected, 0);\n if (row) {\n // Revive the soft-deleted row (the unique key includes deleted rows).\n const result = await update(db, table, row.id, { state, deleted_at: null }, { expectedVersion: row.version });\n if (result.status === \"saved\") return { version: result.row.version };\n throw conflict(key, 0, result.status === \"conflict\" ? result.currentVersion : 0);\n }\n try {\n const created = await insert(db, table, { user_id: await personId(), feature, surface_key: key, state });\n return { version: created.version };\n } catch (error) {\n // Another device created it first.\n if (isUniqueViolation(error)) throw conflict(key, 0, (await find(key, true))?.version ?? 1);\n throw error;\n }\n },\n\n async remove(key: string) {\n const row = await find(key, false);\n if (!row) return;\n await removeRow(db, table, row.id, { overwrite: true });\n },\n\n async list(prefix: string) {\n const userId = await personId();\n const rows = await listAll(\n db,\n table,\n (q) => q.eq(\"user_id\", userId).eq(\"feature\", feature).like(\"surface_key\", `${likeEscape(prefix)}%`),\n { columns: [\"surface_key\"] },\n );\n return rows.map((row) => row.surface_key).filter((key) => key.startsWith(prefix)).sort();\n },\n };\n}\n","/**\n * Errors every persistence store throws in the same shape. Matched by `name`, never `instanceof`:\n * each section entry may carry its own copy of this module in a built consumer.\n */\nimport type { StoredValue } from \"@ai-matrx/summon\";\n\nexport const VERSION_CONFLICT_ERROR = \"OverlayPersistenceVersionConflict\";\nexport const LOAD_TIMEOUT_ERROR = \"OverlayPersistenceLoadTimeout\";\n\n/** A save whose `expectedVersion` no longer matches the stored version. */\nexport class PersistenceVersionConflictError extends Error {\n readonly key: string;\n readonly expectedVersion: number;\n readonly currentVersion: number;\n constructor(key: string, expectedVersion: number, currentVersion: number) {\n super(`[overlays/persistence] \"${key}\" is at version ${currentVersion}, not the expected ${expectedVersion}`);\n this.name = VERSION_CONFLICT_ERROR;\n this.key = key;\n this.expectedVersion = expectedVersion;\n this.currentVersion = currentVersion;\n }\n}\n\nexport function isVersionConflict(error: unknown): error is PersistenceVersionConflictError {\n return error instanceof Error && error.name === VERSION_CONFLICT_ERROR;\n}\n\n/**\n * The fast tier missed and the slow tier did not answer within the read budget. `pending` still\n * resolves the authoritative answer: a caller keeps every automatic writer gated until it does,\n * so an unknown stored value is never overwritten.\n */\nexport class PersistenceLoadTimeoutError extends Error {\n readonly key: string;\n readonly pending: Promise<StoredValue | null>;\n constructor(key: string, budgetMs: number, pending: Promise<StoredValue | null>) {\n super(`[overlays/persistence] \"${key}\" did not load within ${budgetMs}ms`);\n this.name = LOAD_TIMEOUT_ERROR;\n this.key = key;\n this.pending = pending;\n }\n}\n\nexport function isLoadTimeout(error: unknown): error is PersistenceLoadTimeoutError {\n return error instanceof Error && error.name === LOAD_TIMEOUT_ERROR;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACgBA,gBAA2G;;;ACVpG,IAAM,yBAAyB;AAI/B,IAAM,kCAAN,cAA8C,MAAM;AAAA,EAChD;AAAA,EACA;AAAA,EACA;AAAA,EACT,YAAY,KAAa,iBAAyB,gBAAwB;AACxE,UAAM,2BAA2B,GAAG,mBAAmB,cAAc,sBAAsB,eAAe,EAAE;AAC5G,SAAK,OAAO;AACZ,SAAK,MAAM;AACX,SAAK,kBAAkB;AACvB,SAAK,iBAAiB;AAAA,EACxB;AACF;AAEO,SAAS,kBAAkB,OAA0D;AAC1F,SAAO,iBAAiB,SAAS,MAAM,SAAS;AAClD;;;ADLO,IAAM,yBAAyB;AAmC/B,IAAM,wBAAwB;AAAA,EACnC,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,IAAI,CAAC,IAAI;AAAA,EACT,KAAK,EAAE,MAAM,UAAU;AAAA,EACvB,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,UAAU;AAAA,EACV,gBAAgB;AAAA,EAChB,iBAAiB,CAAC;AAAA,EAClB,WAAW;AAAA,EACX,UAAU;AAAA,EACV,WAAW;AACb;AAqBA,IAAM,UAAU,CAAC,MAAM,eAAe,SAAS,WAAW,YAAY;AAGtE,IAAM,aAAa,CAAC,UAAkB,MAAM,QAAQ,WAAW,CAAC,OAAO,KAAK,EAAE,EAAE;AAChF,IAAM,oBAAoB,CAAC,UAAmB,iBAAiB,2BAAiB,MAAM,MAAM,SAAS;AAE9F,SAAS,sBAAsB,SAAyD;AAC7F,QAAM,EAAE,GAAG,IAAI;AACf,QAAM,QAAQ,QAAQ,SAAS;AAC/B,QAAM,UAAU,QAAQ,WAAW;AAEnC,QAAM,WAAW,YAA6B;AAC5C,UAAM,WAAW,QAAQ,SAAS;AAClC,QAAI,SAAU,QAAO;AACrB,UAAM,UAAU,GAAG,OAAO,OAAO,MAAM,GAAG,OAAO,KAAK,WAAW,IAAI;AACrE,UAAM,KAAK,SAAS,KAAK,SAAS,MAAM;AACxC,QAAI,CAAC,GAAI,OAAM,IAAI,MAAM,oFAAoF;AAC7G,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,OAAO,KAAa,mBAAmD;AAClF,UAAM,SAAS,MAAM,SAAS;AAC9B,UAAM,OAAO,UAAM,mBAAQ,IAAI,OAAO,CAAC,MAAM,EAAE,GAAG,WAAW,MAAM,EAAE,GAAG,WAAW,OAAO,EAAE,GAAG,eAAe,GAAG,GAAG;AAAA,MAClH,SAAS;AAAA,MACT;AAAA,IACF,CAAC;AAED,WAAO,KAAK,KAAK,CAAC,QAAQ,IAAI,eAAe,IAAI,KAAK,KAAK,CAAC,KAAK;AAAA,EACnE;AAEA,QAAM,WAAW,CAAC,KAAa,UAAkB,YAAoB,IAAI,gCAAgC,KAAK,UAAU,OAAO;AAE/H,SAAO;AAAA,IACL,WAAW;AAAA,IAEX,MAAM,KAAkB,KAA6C;AACnE,YAAM,MAAM,MAAM,KAAK,KAAK,KAAK;AACjC,aAAO,MAAM,EAAE,OAAO,IAAI,OAAY,SAAS,IAAI,QAAQ,IAAI;AAAA,IACjE;AAAA,IAEA,MAAM,KAAkB,KAAa,OAAU,aAAqD;AAClG,YAAM,WAAW,aAAa;AAC9B,YAAM,QAAQ;AACd,YAAM,MAAM,MAAM,KAAK,KAAK,IAAI;AAChC,UAAI,OAAO,IAAI,eAAe,MAAM;AAClC,YAAI,aAAa,EAAG,OAAM,SAAS,KAAK,GAAG,IAAI,OAAO;AACtD,cAAM,SAAS,UAAM,kBAAO,IAAI,OAAO,IAAI,IAAI,EAAE,MAAM,GAAG,aAAa,SAAY,EAAE,WAAW,KAAK,IAAI,EAAE,iBAAiB,SAAS,CAAC;AACtI,YAAI,OAAO,WAAW,QAAS,QAAO,EAAE,SAAS,OAAO,IAAI,QAAQ;AACpE,YAAI,OAAO,WAAW,WAAY,OAAM,SAAS,KAAK,YAAY,IAAI,SAAS,OAAO,cAAc;AACpG,cAAM,SAAS,KAAK,YAAY,IAAI,SAAS,CAAC;AAAA,MAChD;AACA,UAAI,aAAa,UAAa,aAAa,EAAG,OAAM,SAAS,KAAK,UAAU,CAAC;AAC7E,UAAI,KAAK;AAEP,cAAM,SAAS,UAAM,kBAAO,IAAI,OAAO,IAAI,IAAI,EAAE,OAAO,YAAY,KAAK,GAAG,EAAE,iBAAiB,IAAI,QAAQ,CAAC;AAC5G,YAAI,OAAO,WAAW,QAAS,QAAO,EAAE,SAAS,OAAO,IAAI,QAAQ;AACpE,cAAM,SAAS,KAAK,GAAG,OAAO,WAAW,aAAa,OAAO,iBAAiB,CAAC;AAAA,MACjF;AACA,UAAI;AACF,cAAM,UAAU,UAAM,kBAAO,IAAI,OAAO,EAAE,SAAS,MAAM,SAAS,GAAG,SAAS,aAAa,KAAK,MAAM,CAAC;AACvG,eAAO,EAAE,SAAS,QAAQ,QAAQ;AAAA,MACpC,SAAS,OAAO;AAEd,YAAI,kBAAkB,KAAK,EAAG,OAAM,SAAS,KAAK,IAAI,MAAM,KAAK,KAAK,IAAI,IAAI,WAAW,CAAC;AAC1F,cAAM;AAAA,MACR;AAAA,IACF;AAAA,IAEA,MAAM,OAAO,KAAa;AACxB,YAAM,MAAM,MAAM,KAAK,KAAK,KAAK;AACjC,UAAI,CAAC,IAAK;AACV,gBAAM,UAAAA,QAAU,IAAI,OAAO,IAAI,IAAI,EAAE,WAAW,KAAK,CAAC;AAAA,IACxD;AAAA,IAEA,MAAM,KAAK,QAAgB;AACzB,YAAM,SAAS,MAAM,SAAS;AAC9B,YAAM,OAAO,UAAM;AAAA,QACjB;AAAA,QACA;AAAA,QACA,CAAC,MAAM,EAAE,GAAG,WAAW,MAAM,EAAE,GAAG,WAAW,OAAO,EAAE,KAAK,eAAe,GAAG,WAAW,MAAM,CAAC,GAAG;AAAA,QAClG,EAAE,SAAS,CAAC,aAAa,EAAE;AAAA,MAC7B;AACA,aAAO,KAAK,IAAI,CAAC,QAAQ,IAAI,WAAW,EAAE,OAAO,CAAC,QAAQ,IAAI,WAAW,MAAM,CAAC,EAAE,KAAK;AAAA,IACzF;AAAA,EACF;AACF;","names":["removeRow"]}
@@ -6,7 +6,7 @@ export { P as PersistenceVersionConflictError, i as isVersionConflict } from './
6
6
  * createDataPersistence — the OverlayPersistencePort over `@ai-matrx/data`, backed by the
7
7
  * canonical per-person working-data table `users.user_surface_state` (design R4):
8
8
  *
9
- * feature = 'overlays', surface_key = <key>, state = <value>, version = optimistic version.
9
+ * feature = 'summon', surface_key = <key>, state = <value>, version = optimistic version.
10
10
  * Keys: `workspace` (the person's open windows/layout), `state:<overlayId>:<instanceId>`.
11
11
  *
12
12
  * Every call goes through the data package's doors (listAll / insert / update / remove) — the
@@ -60,7 +60,7 @@ interface DataPersistenceOptions {
60
60
  readonly userId?: () => string | null | undefined;
61
61
  /** Override the descriptor (a host's own generated `userSurfaceState`). */
62
62
  readonly table?: TableDescriptor<UserSurfaceStateRow, UserSurfaceStateInsert, UserSurfaceStateUpdate>;
63
- /** Default "overlays". */
63
+ /** Default "summon". */
64
64
  readonly feature?: string;
65
65
  }
66
66
  interface DataOverlayPersistence extends OverlayPersistencePort {
@@ -6,7 +6,7 @@ export { P as PersistenceVersionConflictError, i as isVersionConflict } from './
6
6
  * createDataPersistence — the OverlayPersistencePort over `@ai-matrx/data`, backed by the
7
7
  * canonical per-person working-data table `users.user_surface_state` (design R4):
8
8
  *
9
- * feature = 'overlays', surface_key = <key>, state = <value>, version = optimistic version.
9
+ * feature = 'summon', surface_key = <key>, state = <value>, version = optimistic version.
10
10
  * Keys: `workspace` (the person's open windows/layout), `state:<overlayId>:<instanceId>`.
11
11
  *
12
12
  * Every call goes through the data package's doors (listAll / insert / update / remove) — the
@@ -60,7 +60,7 @@ interface DataPersistenceOptions {
60
60
  readonly userId?: () => string | null | undefined;
61
61
  /** Override the descriptor (a host's own generated `userSurfaceState`). */
62
62
  readonly table?: TableDescriptor<UserSurfaceStateRow, UserSurfaceStateInsert, UserSurfaceStateUpdate>;
63
- /** Default "overlays". */
63
+ /** Default "summon". */
64
64
  readonly feature?: string;
65
65
  }
66
66
  interface DataOverlayPersistence extends OverlayPersistencePort {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/persistence/data/createDataPersistence.ts","../src/persistence/errors.ts"],"sourcesContent":["/**\n * createDataPersistence — the OverlayPersistencePort over `@ai-matrx/data`, backed by the\n * canonical per-person working-data table `users.user_surface_state` (design R4):\n *\n * feature = 'overlays', surface_key = <key>, state = <value>, version = optimistic version.\n * Keys: `workspace` (the person's open windows/layout), `state:<overlayId>:<instanceId>`.\n *\n * Every call goes through the data package's doors (listAll / insert / update / remove) — the\n * descriptor's rules apply: the table is an `entity` with `org.rule = \"binding\"`, so an insert\n * takes the organization from `createDb({ organization })` (the host's active organization) and\n * the door REFUSES when there is none; this adapter adds no fallback of its own. Reads are scoped\n * to the signed-in person (`user_id`), never to an organization.\n *\n * The row is unique per (user_id, feature, surface_key) including soft-deleted rows, so `remove`\n * soft-deletes and a later `save` revives the same row instead of inserting a duplicate.\n */\nimport { DatabaseError, insert, listAll, remove as removeRow, update, type Db, type TableDescriptor } from \"@ai-matrx/data/db\";\nimport type { OverlayPersistencePort, StoredValue } from \"@ai-matrx/summon\";\nimport { PersistenceVersionConflictError } from \"../errors\";\n\nexport const SUMMON_SURFACE_FEATURE = \"summon\";\n\ntype Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[];\n\n/** The columns of users.user_surface_state this adapter reads and writes. */\nexport interface UserSurfaceStateRow {\n id: string;\n user_id: string;\n feature: string;\n surface_key: string;\n state: Json;\n organization_id: string;\n version: number;\n deleted_at: string | null;\n created_at: string;\n updated_at: string;\n created_by: string | null;\n updated_by: string | null;\n metadata: Json;\n custom_fields: Json;\n}\nexport interface UserSurfaceStateInsert {\n user_id: string;\n feature: string;\n surface_key?: string;\n state?: Json;\n organization_id?: string;\n deleted_at?: string | null;\n}\nexport type UserSurfaceStateUpdate = Partial<UserSurfaceStateInsert>;\n\n/**\n * The generated descriptor for users.user_surface_state (`matrx-data generate --schemas users`,\n * contract 1436b3e225ff). A host may pass its own freshly generated one instead.\n */\nexport const userSurfaceStateTable = {\n schema: \"users\",\n name: \"user_surface_state\",\n kind: \"table\",\n type: \"entity\",\n pk: [\"id\"],\n org: { rule: \"binding\" },\n versioned: true,\n softDelete: true,\n archive: null,\n readOnly: false,\n deletesRefused: false,\n readOnlyColumns: [],\n listScope: \"mine\",\n realtime: false,\n createdBy: true,\n} as const satisfies TableDescriptor<UserSurfaceStateRow, UserSurfaceStateInsert, UserSurfaceStateUpdate> as unknown as TableDescriptor<\n UserSurfaceStateRow,\n UserSurfaceStateInsert,\n UserSurfaceStateUpdate\n>;\n\nexport interface DataPersistenceOptions {\n readonly db: Db;\n /** The signed-in person's id. Default: the session on `db.client.auth`. */\n readonly userId?: () => string | null | undefined;\n /** Override the descriptor (a host's own generated `userSurfaceState`). */\n readonly table?: TableDescriptor<UserSurfaceStateRow, UserSurfaceStateInsert, UserSurfaceStateUpdate>;\n /** Default \"overlays\". */\n readonly feature?: string;\n}\n\nexport interface DataOverlayPersistence extends OverlayPersistencePort {\n readonly perPerson: true;\n list(prefix: string): Promise<readonly string[]>;\n}\n\nconst COLUMNS = [\"id\", \"surface_key\", \"state\", \"version\", \"deleted_at\"] as const;\ntype Found = Pick<UserSurfaceStateRow, (typeof COLUMNS)[number]>;\n\nconst likeEscape = (value: string) => value.replace(/[\\\\%_]/g, (ch) => `\\\\${ch}`);\nconst isUniqueViolation = (error: unknown) => error instanceof DatabaseError && error.cause.code === \"23505\";\n\nexport function createDataPersistence(options: DataPersistenceOptions): DataOverlayPersistence {\n const { db } = options;\n const table = options.table ?? userSurfaceStateTable;\n const feature = options.feature ?? SUMMON_SURFACE_FEATURE;\n\n const personId = async (): Promise<string> => {\n const explicit = options.userId?.();\n if (explicit) return explicit;\n const session = db.client.auth ? await db.client.auth.getSession() : null;\n const id = session?.data.session?.user?.id;\n if (!id) throw new Error(\"[overlays/persistence/data] no signed-in person: overlay state is saved per person\");\n return id;\n };\n\n const find = async (key: string, includeDeleted: boolean): Promise<Found | null> => {\n const userId = await personId();\n const rows = await listAll(db, table, (q) => q.eq(\"user_id\", userId).eq(\"feature\", feature).eq(\"surface_key\", key), {\n columns: COLUMNS,\n includeDeleted,\n });\n // Unique per (user_id, feature, surface_key); a live row beats a soft-deleted one.\n return rows.find((row) => row.deleted_at === null) ?? rows[0] ?? null;\n };\n\n const conflict = (key: string, expected: number, current: number) => new PersistenceVersionConflictError(key, expected, current);\n\n return {\n perPerson: true,\n\n async load<T = unknown>(key: string): Promise<StoredValue<T> | null> {\n const row = await find(key, false);\n return row ? { value: row.state as T, version: row.version } : null;\n },\n\n async save<T = unknown>(key: string, value: T, saveOptions?: { readonly expectedVersion?: number }) {\n const expected = saveOptions?.expectedVersion;\n const state = value as unknown as Json;\n const row = await find(key, true);\n if (row && row.deleted_at === null) {\n if (expected === 0) throw conflict(key, 0, row.version);\n const result = await update(db, table, row.id, { state }, expected === undefined ? { overwrite: true } : { expectedVersion: expected });\n if (result.status === \"saved\") return { version: result.row.version };\n if (result.status === \"conflict\") throw conflict(key, expected ?? row.version, result.currentVersion);\n throw conflict(key, expected ?? row.version, 0); // vanished between read and write\n }\n if (expected !== undefined && expected !== 0) throw conflict(key, expected, 0);\n if (row) {\n // Revive the soft-deleted row (the unique key includes deleted rows).\n const result = await update(db, table, row.id, { state, deleted_at: null }, { expectedVersion: row.version });\n if (result.status === \"saved\") return { version: result.row.version };\n throw conflict(key, 0, result.status === \"conflict\" ? result.currentVersion : 0);\n }\n try {\n const created = await insert(db, table, { user_id: await personId(), feature, surface_key: key, state });\n return { version: created.version };\n } catch (error) {\n // Another device created it first.\n if (isUniqueViolation(error)) throw conflict(key, 0, (await find(key, true))?.version ?? 1);\n throw error;\n }\n },\n\n async remove(key: string) {\n const row = await find(key, false);\n if (!row) return;\n await removeRow(db, table, row.id, { overwrite: true });\n },\n\n async list(prefix: string) {\n const userId = await personId();\n const rows = await listAll(\n db,\n table,\n (q) => q.eq(\"user_id\", userId).eq(\"feature\", feature).like(\"surface_key\", `${likeEscape(prefix)}%`),\n { columns: [\"surface_key\"] },\n );\n return rows.map((row) => row.surface_key).filter((key) => key.startsWith(prefix)).sort();\n },\n };\n}\n","/**\n * Errors every persistence store throws in the same shape. Matched by `name`, never `instanceof`:\n * each section entry may carry its own copy of this module in a built consumer.\n */\nimport type { StoredValue } from \"@ai-matrx/summon\";\n\nexport const VERSION_CONFLICT_ERROR = \"OverlayPersistenceVersionConflict\";\nexport const LOAD_TIMEOUT_ERROR = \"OverlayPersistenceLoadTimeout\";\n\n/** A save whose `expectedVersion` no longer matches the stored version. */\nexport class PersistenceVersionConflictError extends Error {\n readonly key: string;\n readonly expectedVersion: number;\n readonly currentVersion: number;\n constructor(key: string, expectedVersion: number, currentVersion: number) {\n super(`[overlays/persistence] \"${key}\" is at version ${currentVersion}, not the expected ${expectedVersion}`);\n this.name = VERSION_CONFLICT_ERROR;\n this.key = key;\n this.expectedVersion = expectedVersion;\n this.currentVersion = currentVersion;\n }\n}\n\nexport function isVersionConflict(error: unknown): error is PersistenceVersionConflictError {\n return error instanceof Error && error.name === VERSION_CONFLICT_ERROR;\n}\n\n/**\n * The fast tier missed and the slow tier did not answer within the read budget. `pending` still\n * resolves the authoritative answer: a caller keeps every automatic writer gated until it does,\n * so an unknown stored value is never overwritten.\n */\nexport class PersistenceLoadTimeoutError extends Error {\n readonly key: string;\n readonly pending: Promise<StoredValue | null>;\n constructor(key: string, budgetMs: number, pending: Promise<StoredValue | null>) {\n super(`[overlays/persistence] \"${key}\" did not load within ${budgetMs}ms`);\n this.name = LOAD_TIMEOUT_ERROR;\n this.key = key;\n this.pending = pending;\n }\n}\n\nexport function isLoadTimeout(error: unknown): error is PersistenceLoadTimeoutError {\n return error instanceof Error && error.name === LOAD_TIMEOUT_ERROR;\n}\n"],"mappings":";AAgBA,SAAS,eAAe,QAAQ,SAAS,UAAU,WAAW,cAA6C;;;ACVpG,IAAM,yBAAyB;AAI/B,IAAM,kCAAN,cAA8C,MAAM;AAAA,EAChD;AAAA,EACA;AAAA,EACA;AAAA,EACT,YAAY,KAAa,iBAAyB,gBAAwB;AACxE,UAAM,2BAA2B,GAAG,mBAAmB,cAAc,sBAAsB,eAAe,EAAE;AAC5G,SAAK,OAAO;AACZ,SAAK,MAAM;AACX,SAAK,kBAAkB;AACvB,SAAK,iBAAiB;AAAA,EACxB;AACF;AAEO,SAAS,kBAAkB,OAA0D;AAC1F,SAAO,iBAAiB,SAAS,MAAM,SAAS;AAClD;;;ADLO,IAAM,yBAAyB;AAmC/B,IAAM,wBAAwB;AAAA,EACnC,QAAQ;AAAA,EACR,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,IAAI,CAAC,IAAI;AAAA,EACT,KAAK,EAAE,MAAM,UAAU;AAAA,EACvB,WAAW;AAAA,EACX,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,UAAU;AAAA,EACV,gBAAgB;AAAA,EAChB,iBAAiB,CAAC;AAAA,EAClB,WAAW;AAAA,EACX,UAAU;AAAA,EACV,WAAW;AACb;AAqBA,IAAM,UAAU,CAAC,MAAM,eAAe,SAAS,WAAW,YAAY;AAGtE,IAAM,aAAa,CAAC,UAAkB,MAAM,QAAQ,WAAW,CAAC,OAAO,KAAK,EAAE,EAAE;AAChF,IAAM,oBAAoB,CAAC,UAAmB,iBAAiB,iBAAiB,MAAM,MAAM,SAAS;AAE9F,SAAS,sBAAsB,SAAyD;AAC7F,QAAM,EAAE,GAAG,IAAI;AACf,QAAM,QAAQ,QAAQ,SAAS;AAC/B,QAAM,UAAU,QAAQ,WAAW;AAEnC,QAAM,WAAW,YAA6B;AAC5C,UAAM,WAAW,QAAQ,SAAS;AAClC,QAAI,SAAU,QAAO;AACrB,UAAM,UAAU,GAAG,OAAO,OAAO,MAAM,GAAG,OAAO,KAAK,WAAW,IAAI;AACrE,UAAM,KAAK,SAAS,KAAK,SAAS,MAAM;AACxC,QAAI,CAAC,GAAI,OAAM,IAAI,MAAM,oFAAoF;AAC7G,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,OAAO,KAAa,mBAAmD;AAClF,UAAM,SAAS,MAAM,SAAS;AAC9B,UAAM,OAAO,MAAM,QAAQ,IAAI,OAAO,CAAC,MAAM,EAAE,GAAG,WAAW,MAAM,EAAE,GAAG,WAAW,OAAO,EAAE,GAAG,eAAe,GAAG,GAAG;AAAA,MAClH,SAAS;AAAA,MACT;AAAA,IACF,CAAC;AAED,WAAO,KAAK,KAAK,CAAC,QAAQ,IAAI,eAAe,IAAI,KAAK,KAAK,CAAC,KAAK;AAAA,EACnE;AAEA,QAAM,WAAW,CAAC,KAAa,UAAkB,YAAoB,IAAI,gCAAgC,KAAK,UAAU,OAAO;AAE/H,SAAO;AAAA,IACL,WAAW;AAAA,IAEX,MAAM,KAAkB,KAA6C;AACnE,YAAM,MAAM,MAAM,KAAK,KAAK,KAAK;AACjC,aAAO,MAAM,EAAE,OAAO,IAAI,OAAY,SAAS,IAAI,QAAQ,IAAI;AAAA,IACjE;AAAA,IAEA,MAAM,KAAkB,KAAa,OAAU,aAAqD;AAClG,YAAM,WAAW,aAAa;AAC9B,YAAM,QAAQ;AACd,YAAM,MAAM,MAAM,KAAK,KAAK,IAAI;AAChC,UAAI,OAAO,IAAI,eAAe,MAAM;AAClC,YAAI,aAAa,EAAG,OAAM,SAAS,KAAK,GAAG,IAAI,OAAO;AACtD,cAAM,SAAS,MAAM,OAAO,IAAI,OAAO,IAAI,IAAI,EAAE,MAAM,GAAG,aAAa,SAAY,EAAE,WAAW,KAAK,IAAI,EAAE,iBAAiB,SAAS,CAAC;AACtI,YAAI,OAAO,WAAW,QAAS,QAAO,EAAE,SAAS,OAAO,IAAI,QAAQ;AACpE,YAAI,OAAO,WAAW,WAAY,OAAM,SAAS,KAAK,YAAY,IAAI,SAAS,OAAO,cAAc;AACpG,cAAM,SAAS,KAAK,YAAY,IAAI,SAAS,CAAC;AAAA,MAChD;AACA,UAAI,aAAa,UAAa,aAAa,EAAG,OAAM,SAAS,KAAK,UAAU,CAAC;AAC7E,UAAI,KAAK;AAEP,cAAM,SAAS,MAAM,OAAO,IAAI,OAAO,IAAI,IAAI,EAAE,OAAO,YAAY,KAAK,GAAG,EAAE,iBAAiB,IAAI,QAAQ,CAAC;AAC5G,YAAI,OAAO,WAAW,QAAS,QAAO,EAAE,SAAS,OAAO,IAAI,QAAQ;AACpE,cAAM,SAAS,KAAK,GAAG,OAAO,WAAW,aAAa,OAAO,iBAAiB,CAAC;AAAA,MACjF;AACA,UAAI;AACF,cAAM,UAAU,MAAM,OAAO,IAAI,OAAO,EAAE,SAAS,MAAM,SAAS,GAAG,SAAS,aAAa,KAAK,MAAM,CAAC;AACvG,eAAO,EAAE,SAAS,QAAQ,QAAQ;AAAA,MACpC,SAAS,OAAO;AAEd,YAAI,kBAAkB,KAAK,EAAG,OAAM,SAAS,KAAK,IAAI,MAAM,KAAK,KAAK,IAAI,IAAI,WAAW,CAAC;AAC1F,cAAM;AAAA,MACR;AAAA,IACF;AAAA,IAEA,MAAM,OAAO,KAAa;AACxB,YAAM,MAAM,MAAM,KAAK,KAAK,KAAK;AACjC,UAAI,CAAC,IAAK;AACV,YAAM,UAAU,IAAI,OAAO,IAAI,IAAI,EAAE,WAAW,KAAK,CAAC;AAAA,IACxD;AAAA,IAEA,MAAM,KAAK,QAAgB;AACzB,YAAM,SAAS,MAAM,SAAS;AAC9B,YAAM,OAAO,MAAM;AAAA,QACjB;AAAA,QACA;AAAA,QACA,CAAC,MAAM,EAAE,GAAG,WAAW,MAAM,EAAE,GAAG,WAAW,OAAO,EAAE,KAAK,eAAe,GAAG,WAAW,MAAM,CAAC,GAAG;AAAA,QAClG,EAAE,SAAS,CAAC,aAAa,EAAE;AAAA,MAC7B;AACA,aAAO,KAAK,IAAI,CAAC,QAAQ,IAAI,WAAW,EAAE,OAAO,CAAC,QAAQ,IAAI,WAAW,MAAM,CAAC,EAAE,KAAK;AAAA,IACzF;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/persistence/data/createDataPersistence.ts","../src/persistence/errors.ts"],"sourcesContent":["/**\n * createDataPersistence — the OverlayPersistencePort over `@ai-matrx/data`, backed by the\n * canonical per-person working-data table `users.user_surface_state` (design R4):\n *\n * feature = 'summon', surface_key = <key>, state = <value>, version = optimistic version.\n * Keys: `workspace` (the person's open windows/layout), `state:<overlayId>:<instanceId>`.\n *\n * Every call goes through the data package's doors (listAll / insert / update / remove) — the\n * descriptor's rules apply: the table is an `entity` with `org.rule = \"binding\"`, so an insert\n * takes the organization from `createDb({ organization })` (the host's active organization) and\n * the door REFUSES when there is none; this adapter adds no fallback of its own. Reads are scoped\n * to the signed-in person (`user_id`), never to an organization.\n *\n * The row is unique per (user_id, feature, surface_key) including soft-deleted rows, so `remove`\n * soft-deletes and a later `save` revives the same row instead of inserting a duplicate.\n */\nimport { DatabaseError, insert, listAll, remove as removeRow, update, type Db, type TableDescriptor } from \"@ai-matrx/data/db\";\nimport type { OverlayPersistencePort, StoredValue } from \"@ai-matrx/summon\";\nimport { PersistenceVersionConflictError } from \"../errors\";\n\nexport const SUMMON_SURFACE_FEATURE = \"summon\";\n\ntype Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[];\n\n/** The columns of users.user_surface_state this adapter reads and writes. */\nexport interface UserSurfaceStateRow {\n id: string;\n user_id: string;\n feature: string;\n surface_key: string;\n state: Json;\n organization_id: string;\n version: number;\n deleted_at: string | null;\n created_at: string;\n updated_at: string;\n created_by: string | null;\n updated_by: string | null;\n metadata: Json;\n custom_fields: Json;\n}\nexport interface UserSurfaceStateInsert {\n user_id: string;\n feature: string;\n surface_key?: string;\n state?: Json;\n organization_id?: string;\n deleted_at?: string | null;\n}\nexport type UserSurfaceStateUpdate = Partial<UserSurfaceStateInsert>;\n\n/**\n * The generated descriptor for users.user_surface_state (`matrx-data generate --schemas users`,\n * contract 1436b3e225ff). A host may pass its own freshly generated one instead.\n */\nexport const userSurfaceStateTable = {\n schema: \"users\",\n name: \"user_surface_state\",\n kind: \"table\",\n type: \"entity\",\n pk: [\"id\"],\n org: { rule: \"binding\" },\n versioned: true,\n softDelete: true,\n archive: null,\n readOnly: false,\n deletesRefused: false,\n readOnlyColumns: [],\n listScope: \"mine\",\n realtime: false,\n createdBy: true,\n} as const satisfies TableDescriptor<UserSurfaceStateRow, UserSurfaceStateInsert, UserSurfaceStateUpdate> as unknown as TableDescriptor<\n UserSurfaceStateRow,\n UserSurfaceStateInsert,\n UserSurfaceStateUpdate\n>;\n\nexport interface DataPersistenceOptions {\n readonly db: Db;\n /** The signed-in person's id. Default: the session on `db.client.auth`. */\n readonly userId?: () => string | null | undefined;\n /** Override the descriptor (a host's own generated `userSurfaceState`). */\n readonly table?: TableDescriptor<UserSurfaceStateRow, UserSurfaceStateInsert, UserSurfaceStateUpdate>;\n /** Default \"summon\". */\n readonly feature?: string;\n}\n\nexport interface DataOverlayPersistence extends OverlayPersistencePort {\n readonly perPerson: true;\n list(prefix: string): Promise<readonly string[]>;\n}\n\nconst COLUMNS = [\"id\", \"surface_key\", \"state\", \"version\", \"deleted_at\"] as const;\ntype Found = Pick<UserSurfaceStateRow, (typeof COLUMNS)[number]>;\n\nconst likeEscape = (value: string) => value.replace(/[\\\\%_]/g, (ch) => `\\\\${ch}`);\nconst isUniqueViolation = (error: unknown) => error instanceof DatabaseError && error.cause.code === \"23505\";\n\nexport function createDataPersistence(options: DataPersistenceOptions): DataOverlayPersistence {\n const { db } = options;\n const table = options.table ?? userSurfaceStateTable;\n const feature = options.feature ?? SUMMON_SURFACE_FEATURE;\n\n const personId = async (): Promise<string> => {\n const explicit = options.userId?.();\n if (explicit) return explicit;\n const session = db.client.auth ? await db.client.auth.getSession() : null;\n const id = session?.data.session?.user?.id;\n if (!id) throw new Error(\"[overlays/persistence/data] no signed-in person: overlay state is saved per person\");\n return id;\n };\n\n const find = async (key: string, includeDeleted: boolean): Promise<Found | null> => {\n const userId = await personId();\n const rows = await listAll(db, table, (q) => q.eq(\"user_id\", userId).eq(\"feature\", feature).eq(\"surface_key\", key), {\n columns: COLUMNS,\n includeDeleted,\n });\n // Unique per (user_id, feature, surface_key); a live row beats a soft-deleted one.\n return rows.find((row) => row.deleted_at === null) ?? rows[0] ?? null;\n };\n\n const conflict = (key: string, expected: number, current: number) => new PersistenceVersionConflictError(key, expected, current);\n\n return {\n perPerson: true,\n\n async load<T = unknown>(key: string): Promise<StoredValue<T> | null> {\n const row = await find(key, false);\n return row ? { value: row.state as T, version: row.version } : null;\n },\n\n async save<T = unknown>(key: string, value: T, saveOptions?: { readonly expectedVersion?: number }) {\n const expected = saveOptions?.expectedVersion;\n const state = value as unknown as Json;\n const row = await find(key, true);\n if (row && row.deleted_at === null) {\n if (expected === 0) throw conflict(key, 0, row.version);\n const result = await update(db, table, row.id, { state }, expected === undefined ? { overwrite: true } : { expectedVersion: expected });\n if (result.status === \"saved\") return { version: result.row.version };\n if (result.status === \"conflict\") throw conflict(key, expected ?? row.version, result.currentVersion);\n throw conflict(key, expected ?? row.version, 0); // vanished between read and write\n }\n if (expected !== undefined && expected !== 0) throw conflict(key, expected, 0);\n if (row) {\n // Revive the soft-deleted row (the unique key includes deleted rows).\n const result = await update(db, table, row.id, { state, deleted_at: null }, { expectedVersion: row.version });\n if (result.status === \"saved\") return { version: result.row.version };\n throw conflict(key, 0, result.status === \"conflict\" ? result.currentVersion : 0);\n }\n try {\n const created = await insert(db, table, { user_id: await personId(), feature, surface_key: key, state });\n return { version: created.version };\n } catch (error) {\n // Another device created it first.\n if (isUniqueViolation(error)) throw conflict(key, 0, (await find(key, true))?.version ?? 1);\n throw error;\n }\n },\n\n async remove(key: string) {\n const row = await find(key, false);\n if (!row) return;\n await removeRow(db, table, row.id, { overwrite: true });\n },\n\n async list(prefix: string) {\n const userId = await personId();\n const rows = await listAll(\n db,\n table,\n (q) => q.eq(\"user_id\", userId).eq(\"feature\", feature).like(\"surface_key\", `${likeEscape(prefix)}%`),\n { columns: [\"surface_key\"] },\n );\n return rows.map((row) => row.surface_key).filter((key) => key.startsWith(prefix)).sort();\n },\n };\n}\n","/**\n * Errors every persistence store throws in the same shape. Matched by `name`, never `instanceof`:\n * each section entry may carry its own copy of this module in a built consumer.\n */\nimport type { StoredValue } from \"@ai-matrx/summon\";\n\nexport const VERSION_CONFLICT_ERROR = \"OverlayPersistenceVersionConflict\";\nexport const LOAD_TIMEOUT_ERROR = \"OverlayPersistenceLoadTimeout\";\n\n/** A save whose `expectedVersion` no longer matches the stored version. */\nexport class PersistenceVersionConflictError extends Error {\n readonly key: string;\n readonly expectedVersion: number;\n readonly currentVersion: number;\n constructor(key: string, expectedVersion: number, currentVersion: number) {\n super(`[overlays/persistence] \"${key}\" is at version ${currentVersion}, not the expected ${expectedVersion}`);\n this.name = VERSION_CONFLICT_ERROR;\n this.key = key;\n this.expectedVersion = expectedVersion;\n this.currentVersion = currentVersion;\n }\n}\n\nexport function isVersionConflict(error: unknown): error is PersistenceVersionConflictError {\n return error instanceof Error && error.name === VERSION_CONFLICT_ERROR;\n}\n\n/**\n * The fast tier missed and the slow tier did not answer within the read budget. `pending` still\n * resolves the authoritative answer: a caller keeps every automatic writer gated until it does,\n * so an unknown stored value is never overwritten.\n */\nexport class PersistenceLoadTimeoutError extends Error {\n readonly key: string;\n readonly pending: Promise<StoredValue | null>;\n constructor(key: string, budgetMs: number, pending: Promise<StoredValue | null>) {\n super(`[overlays/persistence] \"${key}\" did not load within ${budgetMs}ms`);\n this.name = LOAD_TIMEOUT_ERROR;\n this.key = key;\n this.pending = pending;\n }\n}\n\nexport function isLoadTimeout(error: unknown): error is PersistenceLoadTimeoutError {\n return error instanceof Error && error.name === LOAD_TIMEOUT_ERROR;\n}\n"],"mappings":"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","names":[]}
@@ -886,7 +886,9 @@ function createOverlayPersistenceController(store, ports, options = {}) {
886
886
  let stored = await loadValue(ownKey);
887
887
  let expectedWorkspaceId = scope === "tab" && workspaceId ? workspaceId : void 0;
888
888
  if (stored) versions.set(ownKey, stored.version);
889
+ let storedUnderOwnKey = stored !== null;
889
890
  if (!stored && scope === "tab" && seedFromPerson) {
891
+ storedUnderOwnKey = false;
890
892
  const personKey = scoped(PERSON_WORKSPACE_KEY);
891
893
  stored = await loadValue(personKey);
892
894
  if (stored) versions.set(personKey, stored.version);
@@ -943,7 +945,7 @@ function createOverlayPersistenceController(store, ports, options = {}) {
943
945
  if (before.overlays.overlays[session.overlayId]?.[session.instanceId]?.isOpen) continue;
944
946
  openRegisteredOverlay(store.dispatch, session.overlayId, session.instanceId, session.data);
945
947
  }
946
- lastWorkspaceFp = stored && expectedWorkspaceId !== void 0 && isRecord2(stored.value) && Array.isArray(stored.value.sessions) ? JSON.stringify(stored.value.sessions.map((s) => ({ ...s, savedAt: 0 }))) : null;
948
+ lastWorkspaceFp = stored && storedUnderOwnKey && isRecord2(stored.value) && Array.isArray(stored.value.sessions) ? JSON.stringify(stored.value.sessions.map((s) => ({ ...s, savedAt: 0 }))) : null;
947
949
  lastSavedAt = stored && isRecord2(stored.value) && typeof stored.value.savedAt === "number" ? stored.value.savedAt : 0;
948
950
  writeSafe = true;
949
951
  setHydrated(true);