@elytracms/studio 0.0.64 → 0.0.66

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 (182) hide show
  1. package/convex/assets.ts +11 -0
  2. package/convex/delivery.ts +5 -0
  3. package/convex/schema.ts +23 -0
  4. package/dist/components/account/self-account-view-source.js +9 -2
  5. package/dist/components/account/self-account-view-source.js.map +1 -1
  6. package/dist/components/app-views/default-modules.d.ts +28 -1
  7. package/dist/components/app-views/default-modules.js +47 -2
  8. package/dist/components/app-views/default-modules.js.map +1 -1
  9. package/dist/components/assets/asset-thumb.d.ts +4 -2
  10. package/dist/components/assets/asset-thumb.js +26 -4
  11. package/dist/components/assets/asset-thumb.js.map +1 -1
  12. package/dist/components/assets/assets-module.js +48 -2
  13. package/dist/components/assets/assets-module.js.map +1 -1
  14. package/dist/components/assets/assets-store.d.ts +11 -1
  15. package/dist/components/assets/assets-store.js +18 -1
  16. package/dist/components/assets/assets-store.js.map +1 -1
  17. package/dist/components/auth-context.js +32 -1
  18. package/dist/components/auth-context.js.map +1 -1
  19. package/dist/components/builder/blocks-model.d.ts +11 -0
  20. package/dist/components/builder/blocks-model.js +30 -10
  21. package/dist/components/builder/blocks-model.js.map +1 -1
  22. package/dist/components/builder/canvas-chrome-css.js +90 -8
  23. package/dist/components/builder/canvas-chrome-css.js.map +1 -1
  24. package/dist/components/builder/canvas-dnd.js +4 -2
  25. package/dist/components/builder/canvas-dnd.js.map +1 -1
  26. package/dist/components/builder/canvas-inline-editing.d.ts +7 -0
  27. package/dist/components/builder/canvas-inline-editing.js +3 -1
  28. package/dist/components/builder/canvas-inline-editing.js.map +1 -1
  29. package/dist/components/builder/canvas-overlay-geometry.d.ts +72 -0
  30. package/dist/components/builder/canvas-overlay-geometry.js +80 -0
  31. package/dist/components/builder/canvas-overlay-geometry.js.map +1 -0
  32. package/dist/components/builder/canvas-rect-tracking.d.ts +124 -0
  33. package/dist/components/builder/canvas-rect-tracking.js +198 -0
  34. package/dist/components/builder/canvas-rect-tracking.js.map +1 -0
  35. package/dist/components/builder/canvas.js +61 -12
  36. package/dist/components/builder/canvas.js.map +1 -1
  37. package/dist/components/builder/composition-field-editor.js +22 -3
  38. package/dist/components/builder/composition-field-editor.js.map +1 -1
  39. package/dist/components/builder/delete-key.d.ts +38 -0
  40. package/dist/components/builder/delete-key.js +46 -0
  41. package/dist/components/builder/delete-key.js.map +1 -0
  42. package/dist/components/builder/drag-session.d.ts +8 -0
  43. package/dist/components/builder/drag-session.js +15 -0
  44. package/dist/components/builder/drag-session.js.map +1 -1
  45. package/dist/components/builder/insert-affordance-context.d.ts +19 -0
  46. package/dist/components/builder/insert-affordance-context.js +7 -0
  47. package/dist/components/builder/insert-affordance-context.js.map +1 -0
  48. package/dist/components/builder/insert-context.d.ts +40 -0
  49. package/dist/components/builder/insert-context.js +97 -5
  50. package/dist/components/builder/insert-context.js.map +1 -1
  51. package/dist/components/builder/node-title.d.ts +60 -0
  52. package/dist/components/builder/node-title.js +135 -0
  53. package/dist/components/builder/node-title.js.map +1 -0
  54. package/dist/components/builder/page-tree.js +25 -5
  55. package/dist/components/builder/page-tree.js.map +1 -1
  56. package/dist/components/builder/prop-selection.d.ts +52 -0
  57. package/dist/components/builder/prop-selection.js +38 -0
  58. package/dist/components/builder/prop-selection.js.map +1 -0
  59. package/dist/components/builder/prop-sync-context.d.ts +49 -0
  60. package/dist/components/builder/prop-sync-context.js +23 -0
  61. package/dist/components/builder/prop-sync-context.js.map +1 -0
  62. package/dist/components/builder/property-inspector.js +24 -1
  63. package/dist/components/builder/property-inspector.js.map +1 -1
  64. package/dist/components/builder/reuse-context.js +1 -1
  65. package/dist/components/builder/reuse-context.js.map +1 -1
  66. package/dist/components/builder/selection-overlay.d.ts +79 -0
  67. package/dist/components/builder/selection-overlay.js +244 -0
  68. package/dist/components/builder/selection-overlay.js.map +1 -0
  69. package/dist/components/builder/spacing-handles.d.ts +57 -1
  70. package/dist/components/builder/spacing-handles.js +142 -26
  71. package/dist/components/builder/spacing-handles.js.map +1 -1
  72. package/dist/components/cms/asset-field.js +21 -5
  73. package/dist/components/cms/asset-field.js.map +1 -1
  74. package/dist/components/cms/asset-picker.js +11 -5
  75. package/dist/components/cms/asset-picker.js.map +1 -1
  76. package/dist/components/cms/bulk-actions.d.ts +22 -3
  77. package/dist/components/cms/bulk-actions.js +27 -3
  78. package/dist/components/cms/bulk-actions.js.map +1 -1
  79. package/dist/components/cms/cms-store.js +19 -0
  80. package/dist/components/cms/cms-store.js.map +1 -1
  81. package/dist/components/cms/detail-editor.js +18 -19
  82. package/dist/components/cms/detail-editor.js.map +1 -1
  83. package/dist/components/cms/prop-fields.js +23 -16
  84. package/dist/components/cms/prop-fields.js.map +1 -1
  85. package/dist/components/cms/publish-clean.d.ts +21 -0
  86. package/dist/components/cms/publish-clean.js +30 -0
  87. package/dist/components/cms/publish-clean.js.map +1 -0
  88. package/dist/components/cms/rich-text/block-style.d.ts +48 -0
  89. package/dist/components/cms/rich-text/block-style.js +30 -0
  90. package/dist/components/cms/rich-text/block-style.js.map +1 -0
  91. package/dist/components/cms/rich-text/embed-picker.js +4 -3
  92. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  93. package/dist/components/cms/rich-text/extensions.d.ts +2 -0
  94. package/dist/components/cms/rich-text/extensions.js +4 -0
  95. package/dist/components/cms/rich-text/extensions.js.map +1 -1
  96. package/dist/components/cms/rich-text/font-size-control.d.ts +31 -0
  97. package/dist/components/cms/rich-text/font-size-control.js +40 -0
  98. package/dist/components/cms/rich-text/font-size-control.js.map +1 -0
  99. package/dist/components/cms/rich-text/font-size.d.ts +16 -0
  100. package/dist/components/cms/rich-text/font-size.js +40 -0
  101. package/dist/components/cms/rich-text/font-size.js.map +1 -0
  102. package/dist/components/cms/rich-text/toolbar-state.d.ts +3 -0
  103. package/dist/components/cms/rich-text/toolbar-state.js +3 -0
  104. package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
  105. package/dist/components/cms/rich-text/toolbar.d.ts +13 -0
  106. package/dist/components/cms/rich-text/toolbar.js +17 -5
  107. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  108. package/dist/components/cms/use-live-url.d.ts +24 -1
  109. package/dist/components/cms/use-live-url.js +26 -4
  110. package/dist/components/cms/use-live-url.js.map +1 -1
  111. package/dist/components/cms/use-publish-controller.js +8 -6
  112. package/dist/components/cms/use-publish-controller.js.map +1 -1
  113. package/dist/components/collection-view/cms-collection-view-source.js +15 -7
  114. package/dist/components/collection-view/cms-collection-view-source.js.map +1 -1
  115. package/dist/components/components/component-detail.js +3 -3
  116. package/dist/components/components/component-detail.js.map +1 -1
  117. package/dist/components/components/component-preview.js +1 -1
  118. package/dist/components/components/component-preview.js.map +1 -1
  119. package/dist/components/components/registry-browser.js +2 -2
  120. package/dist/components/components/registry-browser.js.map +1 -1
  121. package/dist/components/components/taxonomy.js +3 -2
  122. package/dist/components/components/taxonomy.js.map +1 -1
  123. package/dist/components/components/transfer-dialog.js +1 -1
  124. package/dist/components/components/transfer-dialog.js.map +1 -1
  125. package/dist/lib/assets/backfill-thumbs.d.ts +67 -0
  126. package/dist/lib/assets/backfill-thumbs.js +120 -0
  127. package/dist/lib/assets/backfill-thumbs.js.map +1 -0
  128. package/dist/lib/assets/delete-asset.d.ts +2 -1
  129. package/dist/lib/assets/delete-asset.js +12 -0
  130. package/dist/lib/assets/delete-asset.js.map +1 -1
  131. package/dist/lib/assets/index.d.ts +2 -0
  132. package/dist/lib/assets/index.js +2 -0
  133. package/dist/lib/assets/index.js.map +1 -1
  134. package/dist/lib/assets/thumbnail.d.ts +145 -0
  135. package/dist/lib/assets/thumbnail.js +220 -0
  136. package/dist/lib/assets/thumbnail.js.map +1 -0
  137. package/dist/lib/assets/upload.d.ts +9 -0
  138. package/dist/lib/assets/upload.js +31 -0
  139. package/dist/lib/assets/upload.js.map +1 -1
  140. package/dist/lib/auth/auth-adapter.d.ts +22 -0
  141. package/dist/lib/auth/auth-adapter.js.map +1 -1
  142. package/dist/lib/auth/convex-auth-adapter.d.ts +42 -1
  143. package/dist/lib/auth/convex-auth-adapter.js +69 -0
  144. package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
  145. package/dist/lib/auth/dev-auto-login-adapter.d.ts +2 -1
  146. package/dist/lib/auth/dev-auto-login-adapter.js +6 -0
  147. package/dist/lib/auth/dev-auto-login-adapter.js.map +1 -1
  148. package/dist/lib/auth/index.d.ts +1 -1
  149. package/dist/lib/auth/index.js.map +1 -1
  150. package/dist/lib/auth/local-auth-adapter.d.ts +2 -1
  151. package/dist/lib/auth/local-auth-adapter.js +7 -0
  152. package/dist/lib/auth/local-auth-adapter.js.map +1 -1
  153. package/dist/lib/auth-errors.d.ts +45 -3
  154. package/dist/lib/auth-errors.js +46 -5
  155. package/dist/lib/auth-errors.js.map +1 -1
  156. package/dist/lib/clipboard/paste-target.js +5 -2
  157. package/dist/lib/clipboard/paste-target.js.map +1 -1
  158. package/dist/lib/dnd/drop-rules.d.ts +13 -0
  159. package/dist/lib/dnd/drop-rules.js +15 -0
  160. package/dist/lib/dnd/drop-rules.js.map +1 -1
  161. package/dist/lib/i18n/messages.de.js +19 -0
  162. package/dist/lib/i18n/messages.de.js.map +1 -1
  163. package/dist/lib/i18n/messages.en.d.ts +16 -0
  164. package/dist/lib/i18n/messages.en.js +24 -0
  165. package/dist/lib/i18n/messages.en.js.map +1 -1
  166. package/dist/lib/persistence/convex-adapter.js +4 -0
  167. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  168. package/dist/lib/persistence/index.js +6 -3
  169. package/dist/lib/persistence/index.js.map +1 -1
  170. package/dist/lib/persona/labels.d.ts +9 -5
  171. package/dist/lib/persona/labels.js +13 -8
  172. package/dist/lib/persona/labels.js.map +1 -1
  173. package/dist/lib/references/references-store.js +7 -1
  174. package/dist/lib/references/references-store.js.map +1 -1
  175. package/dist/lib/session-expiry.d.ts +19 -6
  176. package/dist/lib/session-expiry.js +13 -1
  177. package/dist/lib/session-expiry.js.map +1 -1
  178. package/dist/lib/session-revalidation.d.ts +81 -0
  179. package/dist/lib/session-revalidation.js +94 -0
  180. package/dist/lib/session-revalidation.js.map +1 -0
  181. package/package.json +2 -2
  182. package/src/styles/studio.css +13 -0
@@ -1 +1 @@
1
- {"version":3,"file":"references-store.js","sourceRoot":"","sources":["../../../src/lib/references/references-store.ts"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react'\nimport { documentKey } from '@elytracms/core/cms-core'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\nimport type { CmsDocument, DocumentRef } from '@elytracms/core/cms-core'\nimport type {\n ReferenceEntryRecord,\n ReferenceSource,\n ReferenceTarget,\n} from '@elytracms/core/persistence'\nimport { getStudioOperations } from '../operations'\nimport type { OperationEvent, OperationsClient, StudioOperations } from '../operations'\nimport { getStudioRegistry } from '../registry'\nimport { buildReferenceEntryViews } from './reference-model'\nimport type { ReferenceEntryView, ReferenceLabelContext } from './reference-model'\n\n/**\n * Data layer for the EC-172 references panels. All reads go through\n * `@elytracms/core/operations` queries (`references.outbound` / `references.inbound`\n * plus the labeling queries) — UI code never touches persistence repositories\n * or index tables. The hook keeps panels live: any succeeded command in a\n * content-bearing domain re-queries after the EC-155 maintained-on-write\n * attachment has flushed.\n */\n\nexport interface ReferencePanelQuery {\n projectId: string\n /** Enables the outbound (\"references\") section. */\n source?: ReferenceSource | undefined\n /** Enables the inbound (\"used by\") section. */\n target?: ReferenceTarget | undefined\n}\n\nexport interface ReferencePanelData {\n outbound: ReferenceEntryView[]\n inbound: ReferenceEntryView[]\n /**\n * Per-direction read error (EC-205): a direction whose query returns a\n * structured non-ok degrades to an empty list plus this message, surfaced as a\n * visible per-section error — instead of collapsing the whole panel into its\n * hard-error state. `null` when the direction loaded (or was not requested).\n */\n outboundError: string | null\n inboundError: string | null\n}\n\n// --- Index bootstrap -----------------------------------------------------------\n\n/**\n * The EC-155 index is maintained on write, but seeded/imported projects start\n * with an empty index. {@link ensureReferenceIndex} seeds it once per project per\n * operations client; failures (e.g. a viewer's command denial) degrade to\n * whatever the maintained-on-write index already holds.\n */\nconst backfillsByClient = new WeakMap<StudioOperations, Map<string, Promise<void>>>()\n\n/**\n * Seed the index once if it has never been backfilled, then never again — the\n * EC-269 mutation-free read path. The index is maintained on write, so the\n * (clear + rebuild) `backfill` is only needed to seed a never-built index. A\n * persisted marker (`references.buildMarker`, a read) records completion; when\n * present this returns after a single query with ZERO mutations — so opening the\n * References panel no longer rewrites the whole index every session (the normal\n * Convex case: the EC-233 import already backfilled). Only a genuinely empty/\n * never-built project still pays the one-time backfill (and a viewer who cannot\n * run it degrades to the maintained-on-write index, as before).\n */\nasync function seedReferenceIndex(studio: StudioOperations, projectId: string): Promise<void> {\n try {\n const marker = await studio.operations.references.buildMarker({ projectId })\n if (marker.ok && marker.value !== null) return\n } catch {\n // A marker-read failure falls through to the backfill (best-effort seed).\n }\n await studio.operations.references.backfill({ projectId }).then(\n () => undefined,\n () => undefined,\n )\n}\n\nexport function ensureReferenceIndex(studio: StudioOperations, projectId: string): Promise<void> {\n let byProject = backfillsByClient.get(studio)\n if (!byProject) {\n byProject = new Map()\n backfillsByClient.set(studio, byProject)\n }\n let pending = byProject.get(projectId)\n if (!pending) {\n pending = seedReferenceIndex(studio, projectId)\n byProject.set(projectId, pending)\n }\n return pending\n}\n\n// --- Label context loading -------------------------------------------------------\n\nasync function loadLabelContext(\n operations: OperationsClient,\n projectId: string,\n): Promise<ReferenceLabelContext> {\n const [schemaResult, documentsResult, assetsResult] = await Promise.all([\n operations.schema.get({ projectId }),\n operations.documents.list({ projectId }),\n operations.assets.list({ projectId }),\n ])\n\n const collections = schemaResult.ok ? schemaResult.value.collections : []\n const documents = new Map<string, CmsDocument>()\n if (documentsResult.ok) {\n for (const record of documentsResult.value) {\n documents.set(\n documentKey({ collection: record.document.collection, id: record.document.id }),\n record.document,\n )\n }\n }\n // EC-287: the project graph is retired; reference source labels resolve from\n // CMS documents only. `page`/`layout` graph sources no longer exist.\n const assets = new Map<string, { filename: string; title?: string | undefined }>()\n if (assetsResult.ok) {\n for (const asset of assetsResult.value) {\n assets.set(asset.id, { filename: asset.filename, title: asset.title })\n }\n }\n const registry = getStudioRegistry().registry\n return {\n collections,\n documents,\n assets,\n componentTitle: (componentId) => registry.getManifest(componentId)?.title,\n }\n}\n\n// --- Panel data loading -----------------------------------------------------------\n\n/** Load both directions + labels for one panel (operations queries only). */\nexport async function loadReferencePanelData(\n query: ReferencePanelQuery,\n): Promise<ReferencePanelData> {\n const studio = await getStudioOperations()\n await ensureReferenceIndex(studio, query.projectId)\n // Observe a settled index: maintained-on-write updates run async after events.\n await studio.references.flush()\n const operations = studio.operations\n\n // EC-205: load each direction INDEPENDENTLY. A structured non-ok from one\n // direction is a degradable operation result (it carries an OperationError —\n // the documented contract), not an exception: degrade that direction to empty\n // and surface its error per-section (references-panel) while the other still\n // renders, instead of throwing the whole panel into its hard-error state. A\n // genuinely broken backend still throws from getStudioOperations/flush above.\n const [outbound, inbound] = await Promise.all([\n query.source\n ? operations.references.outbound({ projectId: query.projectId, source: query.source })\n : null,\n query.target\n ? operations.references.inbound({ projectId: query.projectId, target: query.target })\n : null,\n ])\n\n type DirectionResult = Awaited<ReturnType<typeof operations.references.outbound>>\n const settle = (\n result: DirectionResult | null,\n ): { records: ReferenceEntryRecord[]; error: string | null } =>\n result === null\n ? { records: [], error: null }\n : result.ok\n ? { records: result.value, error: null }\n : { records: [], error: result.error.message }\n const out = settle(outbound)\n const inb = settle(inbound)\n\n // EC-302: nothing referenced in either direction → no sources/targets to label,\n // so skip the label-context load entirely (schema + ALL documents + ALL assets).\n // Common for leaf content; mirrors the asset short-circuit in\n // `loadInboundAssetReferenceViews`.\n if (out.records.length === 0 && inb.records.length === 0) {\n return { outbound: [], inbound: [], outboundError: out.error, inboundError: inb.error }\n }\n\n const context = await loadLabelContext(operations, query.projectId)\n return {\n outbound: buildReferenceEntryViews(out.records, context),\n inbound: buildReferenceEntryViews(inb.records, context),\n outboundError: out.error,\n inboundError: inb.error,\n }\n}\n\n// --- Impact loading (EC-173 safe unpublish/delete) -------------------------------\n\n/**\n * Load the labelled inbound entry views for a set of target documents in ONE\n * pass (one index flush, one label-context load) — the pre-flight data of the\n * EC-173 confirmation dialogs, including the bulk flows. Returns entries in\n * `targets` order; pair with `buildReferenceImpact` for break/block semantics.\n */\nexport async function loadInboundReferenceViews(\n projectId: string,\n targets: readonly DocumentRef[],\n): Promise<{ ref: DocumentRef; inbound: ReferenceEntryView[] }[]> {\n const studio = await getStudioOperations()\n await ensureReferenceIndex(studio, projectId)\n // Observe a settled index: maintained-on-write updates run async after events.\n await studio.references.flush()\n const operations = studio.operations\n\n // EC-308: one batched query for ALL targets (the operation loops the by_target\n // index server-side) instead of a per-target `inbound` fan-out. Returns one\n // record list per target, in `targets` order.\n const recordsPerTarget = await operations.references\n .inboundMany({\n projectId,\n targets: targets.map((ref) => ({ type: 'document', id: documentKey(ref) }) as const),\n })\n .then((result) => {\n if (!result.ok) throw new Error(result.error.message)\n return result.value\n })\n // EC-302: no inbound refs for ANY target → no sources to label, so skip the\n // label-context load (schema + ALL documents + ALL assets). The common case when\n // deleting/unpublishing unreferenced documents (incl. bulk); mirrors the EC-301\n // short-circuit already in `loadInboundAssetReferenceViews`.\n if (recordsPerTarget.every((records) => records.length === 0)) {\n return targets.map((ref) => ({ ref, inbound: [] }))\n }\n const context = await loadLabelContext(operations, projectId)\n return targets.map((ref, index) => ({\n ref,\n inbound: buildReferenceEntryViews(recordsPerTarget[index] ?? [], context),\n }))\n}\n\n/**\n * Inbound reference views for ONE asset (EC-202): the \"what uses this asset\"\n * pre-flight of the asset-delete dialog, index-accurate (the EC-155 reference\n * index, not the retired `find-asset-usages` string scan). Mirrors\n * {@link loadInboundReferenceViews} for a single `{ type: 'asset' }` target —\n * the source labels resolve the referencing documents/graph-nodes identically.\n */\nexport async function loadInboundAssetReferenceViews(\n projectId: string,\n assetId: string,\n): Promise<ReferenceEntryView[]> {\n const studio = await getStudioOperations()\n await ensureReferenceIndex(studio, projectId)\n // Observe a settled index: maintained-on-write updates run async after events.\n await studio.references.flush()\n const operations = studio.operations\n const result = await operations.references.inbound({\n projectId,\n target: { type: 'asset', id: assetId },\n })\n if (!result.ok) throw new Error(result.error.message)\n // No inbound references → no sources to label, so skip the label-context load\n // entirely (schema + ALL documents + ALL assets). This is the common case for a\n // deletable asset, making the delete dialog's pre-flight a single indexed query\n // instead of three full-table lists (EC-301 perf).\n if (result.value.length === 0) return []\n const context = await loadLabelContext(operations, projectId)\n return buildReferenceEntryViews(result.value, context)\n}\n\n// --- React hook ----------------------------------------------------------------\n\n/**\n * Domains whose successful commands can change the reference index or labels and\n * should refresh an open panel. `references` is deliberately ABSENT (EC-269):\n * reacting to reference-index command events (the `backfill`) would make the\n * panel re-trigger on its OWN index maintenance — a self-cascade that, compounded\n * with the per-keystroke `documents` events, produced hundreds of redundant\n * reloads per document. The index is maintained on write; the panel reacts to the\n * CONTENT domains that feed it, never to its own index writes.\n */\nconst REFERENCE_REFRESH_TRIGGERS = [\n 'documents',\n 'graph',\n 'publishing',\n 'assets',\n 'schema',\n] as const\n\nconst REFRESH_DOMAINS: ReadonlySet<string> = new Set(REFERENCE_REFRESH_TRIGGERS)\n\n/**\n * Whether one operation event should refresh an open reference panel. Exported\n * for tests — pins the EC-269 contract that reference-index commands (and any\n * non-succeeded / non-command event) never trigger a reload.\n */\nexport function isReferenceRefreshEvent(event: OperationEvent): boolean {\n return (\n event.kind === 'command' && event.status === 'succeeded' && REFRESH_DOMAINS.has(event.domain)\n )\n}\n\nexport type ReferencePanelStatus = 'loading' | 'ready' | 'error'\n\nexport interface ReferencePanelState {\n status: ReferencePanelStatus\n data: ReferencePanelData | null\n errorMessage: string | null\n refresh: () => void\n}\n\n/**\n * Live panel state for one entity (EC-308/AD-17): loads + caches through the one\n * `useOperationQuery` seam, and re-queries whenever a content-bearing command\n * succeeds anywhere in the studio (the index is maintained on write). Per-direction\n * read errors degrade INSIDE `loadReferencePanelData` (carried on the data); a hard\n * failure (broken backend) surfaces as the query's error → `status: 'error'`.\n */\nexport function useReferencePanel(query: ReferencePanelQuery): ReferencePanelState {\n const { projectId } = query\n const sourceType = query.source?.type\n const sourceId = query.source?.id\n const targetType = query.target?.type\n const targetId = query.target?.id\n\n const stableQuery = useMemo<ReferencePanelQuery>(\n () => ({\n projectId,\n ...(sourceType && sourceId ? { source: { type: sourceType, id: sourceId } } : {}),\n ...(targetType && targetId ? { target: { type: targetType, id: targetId } } : {}),\n }),\n [projectId, sourceType, sourceId, targetType, targetId],\n )\n\n const result = useOperationQuery<ReferencePanelData>({\n queryKey: [\n 'referencePanel',\n projectId,\n sourceType ?? '',\n sourceId ?? '',\n targetType ?? '',\n targetId ?? '',\n ],\n loader: () => loadReferencePanelData(stableQuery),\n invalidateOn: REFERENCE_REFRESH_TRIGGERS,\n })\n\n const status: ReferencePanelStatus = result.isPending\n ? 'loading'\n : result.isError\n ? 'error'\n : 'ready'\n return {\n status,\n data: result.data ?? null,\n errorMessage: result.error instanceof Error ? result.error.message : null,\n refresh: () => void result.refetch(),\n }\n}\n"]}
1
+ {"version":3,"file":"references-store.js","sourceRoot":"","sources":["../../../src/lib/references/references-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAC/B,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AAO/D,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA;AAEnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAA;AAiC5D,kFAAkF;AAElF;;;;;GAKG;AACH,MAAM,iBAAiB,GAAG,IAAI,OAAO,EAAgD,CAAA;AAErF;;;;;;;;;;GAUG;AACH,KAAK,UAAU,kBAAkB,CAAC,MAAwB,EAAE,SAAiB;IAC3E,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,SAAS,EAAE,CAAC,CAAA;QAC5E,IAAI,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI;YAAE,OAAM;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,0EAA0E;IAC5E,CAAC;IACD,MAAM,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,IAAI,CAC7D,GAAG,EAAE,CAAC,SAAS,EACf,GAAG,EAAE,CAAC,SAAS,CAChB,CAAA;AACH,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,MAAwB,EAAE,SAAiB;IAC9E,IAAI,SAAS,GAAG,iBAAiB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAC7C,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,SAAS,GAAG,IAAI,GAAG,EAAE,CAAA;QACrB,iBAAiB,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IAC1C,CAAC;IACD,IAAI,OAAO,GAAG,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IACtC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,OAAO,GAAG,kBAAkB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;QAC/C,SAAS,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IACnC,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,oFAAoF;AAEpF,KAAK,UAAU,gBAAgB,CAC7B,UAA4B,EAC5B,SAAiB;IAEjB,MAAM,CAAC,YAAY,EAAE,eAAe,EAAE,YAAY,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACtE,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,SAAS,EAAE,CAAC;QACpC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC;QACxC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC;KACtC,CAAC,CAAA;IAEF,MAAM,WAAW,GAAG,YAAY,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAA;IACzE,MAAM,SAAS,GAAG,IAAI,GAAG,EAAuB,CAAA;IAChD,IAAI,eAAe,CAAC,EAAE,EAAE,CAAC;QACvB,KAAK,MAAM,MAAM,IAAI,eAAe,CAAC,KAAK,EAAE,CAAC;YAC3C,SAAS,CAAC,GAAG,CACX,WAAW,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,EAAE,EAAE,MAAM,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC,EAC/E,MAAM,CAAC,QAAQ,CAChB,CAAA;QACH,CAAC;IACH,CAAC;IACD,6EAA6E;IAC7E,qEAAqE;IACrE,MAAM,MAAM,GAAG,IAAI,GAAG,EAA4D,CAAA;IAClF,IAAI,YAAY,CAAC,EAAE,EAAE,CAAC;QACpB,KAAK,MAAM,KAAK,IAAI,YAAY,CAAC,KAAK,EAAE,CAAC;YACvC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAA;QACxE,CAAC;IACH,CAAC;IACD,MAAM,QAAQ,GAAG,iBAAiB,EAAE,CAAC,QAAQ,CAAA;IAC7C,OAAO;QACL,WAAW;QACX,SAAS;QACT,MAAM;QACN,4EAA4E;QAC5E,gFAAgF;QAChF,cAAc,EAAE,CAAC,WAAW,EAAE,EAAE;YAC9B,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,CAAC,WAAW,CAAC,EAAE,KAAK,CAAA;YACtD,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,SAAS,CAAA;QAC7E,CAAC;KACF,CAAA;AACH,CAAC;AAED,qFAAqF;AAErF,6EAA6E;AAC7E,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,KAA0B;IAE1B,MAAM,MAAM,GAAG,MAAM,mBAAmB,EAAE,CAAA;IAC1C,MAAM,oBAAoB,CAAC,MAAM,EAAE,KAAK,CAAC,SAAS,CAAC,CAAA;IACnD,+EAA+E;IAC/E,MAAM,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,CAAA;IAC/B,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;IAEpC,0EAA0E;IAC1E,6EAA6E;IAC7E,8EAA8E;IAC9E,6EAA6E;IAC7E,4EAA4E;IAC5E,8EAA8E;IAC9E,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC5C,KAAK,CAAC,MAAM;YACV,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC;YACtF,CAAC,CAAC,IAAI;QACR,KAAK,CAAC,MAAM;YACV,CAAC,CAAC,UAAU,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC;YACrF,CAAC,CAAC,IAAI;KACT,CAAC,CAAA;IAGF,MAAM,MAAM,GAAG,CACb,MAA8B,EAC6B,EAAE,CAC7D,MAAM,KAAK,IAAI;QACb,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;QAC9B,CAAC,CAAC,MAAM,CAAC,EAAE;YACT,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE;YACxC,CAAC,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAA;IACpD,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC5B,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,CAAA;IAE3B,gFAAgF;IAChF,iFAAiF;IACjF,8DAA8D;IAC9D,oCAAoC;IACpC,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzD,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,aAAa,EAAE,GAAG,CAAC,KAAK,EAAE,YAAY,EAAE,GAAG,CAAC,KAAK,EAAE,CAAA;IACzF,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,CAAA;IACnE,OAAO;QACL,QAAQ,EAAE,wBAAwB,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC;QACxD,OAAO,EAAE,wBAAwB,CAAC,GAAG,CAAC,OAAO,EAAE,OAAO,CAAC;QACvD,aAAa,EAAE,GAAG,CAAC,KAAK;QACxB,YAAY,EAAE,GAAG,CAAC,KAAK;KACxB,CAAA;AACH,CAAC;AAED,oFAAoF;AAEpF;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,SAAiB,EACjB,OAA+B;IAE/B,MAAM,MAAM,GAAG,MAAM,mBAAmB,EAAE,CAAA;IAC1C,MAAM,oBAAoB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IAC7C,+EAA+E;IAC/E,MAAM,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,CAAA;IAC/B,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;IAEpC,+EAA+E;IAC/E,4EAA4E;IAC5E,8CAA8C;IAC9C,MAAM,gBAAgB,GAAG,MAAM,UAAU,CAAC,UAAU;SACjD,WAAW,CAAC;QACX,SAAS;QACT,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,WAAW,CAAC,GAAG,CAAC,EAAE,CAAU,CAAC;KACrF,CAAC;SACD,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;QACf,IAAI,CAAC,MAAM,CAAC,EAAE;YAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QACrD,OAAO,MAAM,CAAC,KAAK,CAAA;IACrB,CAAC,CAAC,CAAA;IACJ,4EAA4E;IAC5E,iFAAiF;IACjF,gFAAgF;IAChF,6DAA6D;IAC7D,IAAI,gBAAgB,CAAC,KAAK,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC;QAC9D,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,CAAA;IACrD,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,UAAU,EAAE,SAAS,CAAC,CAAA;IAC7D,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;QAClC,GAAG;QACH,OAAO,EAAE,wBAAwB,CAAC,gBAAgB,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,OAAO,CAAC;KAC1E,CAAC,CAAC,CAAA;AACL,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAClD,SAAiB,EACjB,OAAe;IAEf,MAAM,MAAM,GAAG,MAAM,mBAAmB,EAAE,CAAA;IAC1C,MAAM,oBAAoB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IAC7C,+EAA+E;IAC/E,MAAM,MAAM,CAAC,UAAU,CAAC,KAAK,EAAE,CAAA;IAC/B,MAAM,UAAU,GAAG,MAAM,CAAC,UAAU,CAAA;IACpC,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,UAAU,CAAC,OAAO,CAAC;QACjD,SAAS;QACT,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,OAAO,EAAE;KACvC,CAAC,CAAA;IACF,IAAI,CAAC,MAAM,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IACrD,8EAA8E;IAC9E,gFAAgF;IAChF,gFAAgF;IAChF,mDAAmD;IACnD,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACxC,MAAM,OAAO,GAAG,MAAM,gBAAgB,CAAC,UAAU,EAAE,SAAS,CAAC,CAAA;IAC7D,OAAO,wBAAwB,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;AACxD,CAAC;AAED,kFAAkF;AAElF;;;;;;;;GAQG;AACH,MAAM,0BAA0B,GAAG;IACjC,WAAW;IACX,OAAO;IACP,YAAY;IACZ,QAAQ;IACR,QAAQ;CACA,CAAA;AAEV,MAAM,eAAe,GAAwB,IAAI,GAAG,CAAC,0BAA0B,CAAC,CAAA;AAEhF;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,KAAqB;IAC3D,OAAO,CACL,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,WAAW,IAAI,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAC9F,CAAA;AACH,CAAC;AAWD;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAA0B;IAC1D,MAAM,EAAE,SAAS,EAAE,GAAG,KAAK,CAAA;IAC3B,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,EAAE,IAAI,CAAA;IACrC,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,EAAE,EAAE,CAAA;IACjC,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,EAAE,IAAI,CAAA;IACrC,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,EAAE,EAAE,CAAA;IAEjC,MAAM,WAAW,GAAG,OAAO,CACzB,GAAG,EAAE,CAAC,CAAC;QACL,SAAS;QACT,GAAG,CAAC,UAAU,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjF,GAAG,CAAC,UAAU,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClF,CAAC,EACF,CAAC,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,CAAC,CACxD,CAAA;IAED,MAAM,MAAM,GAAG,iBAAiB,CAAqB;QACnD,QAAQ,EAAE;YACR,gBAAgB;YAChB,SAAS;YACT,UAAU,IAAI,EAAE;YAChB,QAAQ,IAAI,EAAE;YACd,UAAU,IAAI,EAAE;YAChB,QAAQ,IAAI,EAAE;SACf;QACD,MAAM,EAAE,GAAG,EAAE,CAAC,sBAAsB,CAAC,WAAW,CAAC;QACjD,YAAY,EAAE,0BAA0B;KACzC,CAAC,CAAA;IAEF,MAAM,MAAM,GAAyB,MAAM,CAAC,SAAS;QACnD,CAAC,CAAC,SAAS;QACX,CAAC,CAAC,MAAM,CAAC,OAAO;YACd,CAAC,CAAC,OAAO;YACT,CAAC,CAAC,OAAO,CAAA;IACb,OAAO;QACL,MAAM;QACN,IAAI,EAAE,MAAM,CAAC,IAAI,IAAI,IAAI;QACzB,YAAY,EAAE,MAAM,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI;QACzE,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,MAAM,CAAC,OAAO,EAAE;KACrC,CAAA;AACH,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport { documentKey } from '@elytracms/core/cms-core'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\nimport type { CmsDocument, DocumentRef } from '@elytracms/core/cms-core'\nimport type {\n ReferenceEntryRecord,\n ReferenceSource,\n ReferenceTarget,\n} from '@elytracms/core/persistence'\nimport { localizedLabel } from '../i18n'\nimport { getStudioOperations } from '../operations'\nimport type { OperationEvent, OperationsClient, StudioOperations } from '../operations'\nimport { getStudioRegistry } from '../registry'\nimport { buildReferenceEntryViews } from './reference-model'\nimport type { ReferenceEntryView, ReferenceLabelContext } from './reference-model'\n\n/**\n * Data layer for the EC-172 references panels. All reads go through\n * `@elytracms/core/operations` queries (`references.outbound` / `references.inbound`\n * plus the labeling queries) — UI code never touches persistence repositories\n * or index tables. The hook keeps panels live: any succeeded command in a\n * content-bearing domain re-queries after the EC-155 maintained-on-write\n * attachment has flushed.\n */\n\nexport interface ReferencePanelQuery {\n projectId: string\n /** Enables the outbound (\"references\") section. */\n source?: ReferenceSource | undefined\n /** Enables the inbound (\"used by\") section. */\n target?: ReferenceTarget | undefined\n}\n\nexport interface ReferencePanelData {\n outbound: ReferenceEntryView[]\n inbound: ReferenceEntryView[]\n /**\n * Per-direction read error (EC-205): a direction whose query returns a\n * structured non-ok degrades to an empty list plus this message, surfaced as a\n * visible per-section error — instead of collapsing the whole panel into its\n * hard-error state. `null` when the direction loaded (or was not requested).\n */\n outboundError: string | null\n inboundError: string | null\n}\n\n// --- Index bootstrap -----------------------------------------------------------\n\n/**\n * The EC-155 index is maintained on write, but seeded/imported projects start\n * with an empty index. {@link ensureReferenceIndex} seeds it once per project per\n * operations client; failures (e.g. a viewer's command denial) degrade to\n * whatever the maintained-on-write index already holds.\n */\nconst backfillsByClient = new WeakMap<StudioOperations, Map<string, Promise<void>>>()\n\n/**\n * Seed the index once if it has never been backfilled, then never again — the\n * EC-269 mutation-free read path. The index is maintained on write, so the\n * (clear + rebuild) `backfill` is only needed to seed a never-built index. A\n * persisted marker (`references.buildMarker`, a read) records completion; when\n * present this returns after a single query with ZERO mutations — so opening the\n * References panel no longer rewrites the whole index every session (the normal\n * Convex case: the EC-233 import already backfilled). Only a genuinely empty/\n * never-built project still pays the one-time backfill (and a viewer who cannot\n * run it degrades to the maintained-on-write index, as before).\n */\nasync function seedReferenceIndex(studio: StudioOperations, projectId: string): Promise<void> {\n try {\n const marker = await studio.operations.references.buildMarker({ projectId })\n if (marker.ok && marker.value !== null) return\n } catch {\n // A marker-read failure falls through to the backfill (best-effort seed).\n }\n await studio.operations.references.backfill({ projectId }).then(\n () => undefined,\n () => undefined,\n )\n}\n\nexport function ensureReferenceIndex(studio: StudioOperations, projectId: string): Promise<void> {\n let byProject = backfillsByClient.get(studio)\n if (!byProject) {\n byProject = new Map()\n backfillsByClient.set(studio, byProject)\n }\n let pending = byProject.get(projectId)\n if (!pending) {\n pending = seedReferenceIndex(studio, projectId)\n byProject.set(projectId, pending)\n }\n return pending\n}\n\n// --- Label context loading -------------------------------------------------------\n\nasync function loadLabelContext(\n operations: OperationsClient,\n projectId: string,\n): Promise<ReferenceLabelContext> {\n const [schemaResult, documentsResult, assetsResult] = await Promise.all([\n operations.schema.get({ projectId }),\n operations.documents.list({ projectId }),\n operations.assets.list({ projectId }),\n ])\n\n const collections = schemaResult.ok ? schemaResult.value.collections : []\n const documents = new Map<string, CmsDocument>()\n if (documentsResult.ok) {\n for (const record of documentsResult.value) {\n documents.set(\n documentKey({ collection: record.document.collection, id: record.document.id }),\n record.document,\n )\n }\n }\n // EC-287: the project graph is retired; reference source labels resolve from\n // CMS documents only. `page`/`layout` graph sources no longer exist.\n const assets = new Map<string, { filename: string; title?: string | undefined }>()\n if (assetsResult.ok) {\n for (const asset of assetsResult.value) {\n assets.set(asset.id, { filename: asset.filename, title: asset.title })\n }\n }\n const registry = getStudioRegistry().registry\n return {\n collections,\n documents,\n assets,\n // EC-407: `title` is a LocalizedString — resolve it to the active UI locale\n // (a plain string is unchanged), never leak a `{ en, de }` object to the label.\n componentTitle: (componentId) => {\n const title = registry.getManifest(componentId)?.title\n return title === undefined ? undefined : localizedLabel(title) || undefined\n },\n }\n}\n\n// --- Panel data loading -----------------------------------------------------------\n\n/** Load both directions + labels for one panel (operations queries only). */\nexport async function loadReferencePanelData(\n query: ReferencePanelQuery,\n): Promise<ReferencePanelData> {\n const studio = await getStudioOperations()\n await ensureReferenceIndex(studio, query.projectId)\n // Observe a settled index: maintained-on-write updates run async after events.\n await studio.references.flush()\n const operations = studio.operations\n\n // EC-205: load each direction INDEPENDENTLY. A structured non-ok from one\n // direction is a degradable operation result (it carries an OperationError —\n // the documented contract), not an exception: degrade that direction to empty\n // and surface its error per-section (references-panel) while the other still\n // renders, instead of throwing the whole panel into its hard-error state. A\n // genuinely broken backend still throws from getStudioOperations/flush above.\n const [outbound, inbound] = await Promise.all([\n query.source\n ? operations.references.outbound({ projectId: query.projectId, source: query.source })\n : null,\n query.target\n ? operations.references.inbound({ projectId: query.projectId, target: query.target })\n : null,\n ])\n\n type DirectionResult = Awaited<ReturnType<typeof operations.references.outbound>>\n const settle = (\n result: DirectionResult | null,\n ): { records: ReferenceEntryRecord[]; error: string | null } =>\n result === null\n ? { records: [], error: null }\n : result.ok\n ? { records: result.value, error: null }\n : { records: [], error: result.error.message }\n const out = settle(outbound)\n const inb = settle(inbound)\n\n // EC-302: nothing referenced in either direction → no sources/targets to label,\n // so skip the label-context load entirely (schema + ALL documents + ALL assets).\n // Common for leaf content; mirrors the asset short-circuit in\n // `loadInboundAssetReferenceViews`.\n if (out.records.length === 0 && inb.records.length === 0) {\n return { outbound: [], inbound: [], outboundError: out.error, inboundError: inb.error }\n }\n\n const context = await loadLabelContext(operations, query.projectId)\n return {\n outbound: buildReferenceEntryViews(out.records, context),\n inbound: buildReferenceEntryViews(inb.records, context),\n outboundError: out.error,\n inboundError: inb.error,\n }\n}\n\n// --- Impact loading (EC-173 safe unpublish/delete) -------------------------------\n\n/**\n * Load the labelled inbound entry views for a set of target documents in ONE\n * pass (one index flush, one label-context load) — the pre-flight data of the\n * EC-173 confirmation dialogs, including the bulk flows. Returns entries in\n * `targets` order; pair with `buildReferenceImpact` for break/block semantics.\n */\nexport async function loadInboundReferenceViews(\n projectId: string,\n targets: readonly DocumentRef[],\n): Promise<{ ref: DocumentRef; inbound: ReferenceEntryView[] }[]> {\n const studio = await getStudioOperations()\n await ensureReferenceIndex(studio, projectId)\n // Observe a settled index: maintained-on-write updates run async after events.\n await studio.references.flush()\n const operations = studio.operations\n\n // EC-308: one batched query for ALL targets (the operation loops the by_target\n // index server-side) instead of a per-target `inbound` fan-out. Returns one\n // record list per target, in `targets` order.\n const recordsPerTarget = await operations.references\n .inboundMany({\n projectId,\n targets: targets.map((ref) => ({ type: 'document', id: documentKey(ref) }) as const),\n })\n .then((result) => {\n if (!result.ok) throw new Error(result.error.message)\n return result.value\n })\n // EC-302: no inbound refs for ANY target → no sources to label, so skip the\n // label-context load (schema + ALL documents + ALL assets). The common case when\n // deleting/unpublishing unreferenced documents (incl. bulk); mirrors the EC-301\n // short-circuit already in `loadInboundAssetReferenceViews`.\n if (recordsPerTarget.every((records) => records.length === 0)) {\n return targets.map((ref) => ({ ref, inbound: [] }))\n }\n const context = await loadLabelContext(operations, projectId)\n return targets.map((ref, index) => ({\n ref,\n inbound: buildReferenceEntryViews(recordsPerTarget[index] ?? [], context),\n }))\n}\n\n/**\n * Inbound reference views for ONE asset (EC-202): the \"what uses this asset\"\n * pre-flight of the asset-delete dialog, index-accurate (the EC-155 reference\n * index, not the retired `find-asset-usages` string scan). Mirrors\n * {@link loadInboundReferenceViews} for a single `{ type: 'asset' }` target —\n * the source labels resolve the referencing documents/graph-nodes identically.\n */\nexport async function loadInboundAssetReferenceViews(\n projectId: string,\n assetId: string,\n): Promise<ReferenceEntryView[]> {\n const studio = await getStudioOperations()\n await ensureReferenceIndex(studio, projectId)\n // Observe a settled index: maintained-on-write updates run async after events.\n await studio.references.flush()\n const operations = studio.operations\n const result = await operations.references.inbound({\n projectId,\n target: { type: 'asset', id: assetId },\n })\n if (!result.ok) throw new Error(result.error.message)\n // No inbound references → no sources to label, so skip the label-context load\n // entirely (schema + ALL documents + ALL assets). This is the common case for a\n // deletable asset, making the delete dialog's pre-flight a single indexed query\n // instead of three full-table lists (EC-301 perf).\n if (result.value.length === 0) return []\n const context = await loadLabelContext(operations, projectId)\n return buildReferenceEntryViews(result.value, context)\n}\n\n// --- React hook ----------------------------------------------------------------\n\n/**\n * Domains whose successful commands can change the reference index or labels and\n * should refresh an open panel. `references` is deliberately ABSENT (EC-269):\n * reacting to reference-index command events (the `backfill`) would make the\n * panel re-trigger on its OWN index maintenance — a self-cascade that, compounded\n * with the per-keystroke `documents` events, produced hundreds of redundant\n * reloads per document. The index is maintained on write; the panel reacts to the\n * CONTENT domains that feed it, never to its own index writes.\n */\nconst REFERENCE_REFRESH_TRIGGERS = [\n 'documents',\n 'graph',\n 'publishing',\n 'assets',\n 'schema',\n] as const\n\nconst REFRESH_DOMAINS: ReadonlySet<string> = new Set(REFERENCE_REFRESH_TRIGGERS)\n\n/**\n * Whether one operation event should refresh an open reference panel. Exported\n * for tests — pins the EC-269 contract that reference-index commands (and any\n * non-succeeded / non-command event) never trigger a reload.\n */\nexport function isReferenceRefreshEvent(event: OperationEvent): boolean {\n return (\n event.kind === 'command' && event.status === 'succeeded' && REFRESH_DOMAINS.has(event.domain)\n )\n}\n\nexport type ReferencePanelStatus = 'loading' | 'ready' | 'error'\n\nexport interface ReferencePanelState {\n status: ReferencePanelStatus\n data: ReferencePanelData | null\n errorMessage: string | null\n refresh: () => void\n}\n\n/**\n * Live panel state for one entity (EC-308/AD-17): loads + caches through the one\n * `useOperationQuery` seam, and re-queries whenever a content-bearing command\n * succeeds anywhere in the studio (the index is maintained on write). Per-direction\n * read errors degrade INSIDE `loadReferencePanelData` (carried on the data); a hard\n * failure (broken backend) surfaces as the query's error → `status: 'error'`.\n */\nexport function useReferencePanel(query: ReferencePanelQuery): ReferencePanelState {\n const { projectId } = query\n const sourceType = query.source?.type\n const sourceId = query.source?.id\n const targetType = query.target?.type\n const targetId = query.target?.id\n\n const stableQuery = useMemo<ReferencePanelQuery>(\n () => ({\n projectId,\n ...(sourceType && sourceId ? { source: { type: sourceType, id: sourceId } } : {}),\n ...(targetType && targetId ? { target: { type: targetType, id: targetId } } : {}),\n }),\n [projectId, sourceType, sourceId, targetType, targetId],\n )\n\n const result = useOperationQuery<ReferencePanelData>({\n queryKey: [\n 'referencePanel',\n projectId,\n sourceType ?? '',\n sourceId ?? '',\n targetType ?? '',\n targetId ?? '',\n ],\n loader: () => loadReferencePanelData(stableQuery),\n invalidateOn: REFERENCE_REFRESH_TRIGGERS,\n })\n\n const status: ReferencePanelStatus = result.isPending\n ? 'loading'\n : result.isError\n ? 'error'\n : 'ready'\n return {\n status,\n data: result.data ?? null,\n errorMessage: result.error instanceof Error ? result.error.message : null,\n refresh: () => void result.refetch(),\n }\n}\n"]}
@@ -19,12 +19,15 @@ import type { OperationError, OperationEvent, OperationEventBus } from './operat
19
19
  * state stay alive. This module is deliberately framework-free (a plain
20
20
  * listener registry + pure predicates) so the state machine is unit-testable.
21
21
  *
22
- * Proactive re-validation (window focus/interval) deliberately does NOT ship:
23
- * the auth adapters' `currentSession()` treats *any* failure — including a
24
- * transient network drop — as "signed out" and destroys the stored token, so
25
- * polling it would sign editors out on a network blip. Distinguishing
26
- * "token invalid" from "backend unreachable" needs an adapter change under
27
- * the no-touch `lib/auth/**` (reported as a finding on EC-403).
22
+ * Since EC-415 detection is ALSO proactive: the adapters carry a tri-state
23
+ * `probeSession()` (valid / invalid / unreachable), and the scheduler core in
24
+ * `lib/session-revalidation.ts` — wired to window focus + a modest visible-tab
25
+ * interval in `components/auth-context.tsx` — funnels a probe's `invalid`
26
+ * answer into this SAME chokepoint via {@link raiseSessionExpiry}. One
27
+ * listener registry, one auth-store guard, one overlay — no second raise
28
+ * path. `unreachable` stays silent by contract (the probe never clears the
29
+ * token; the operations layer already surfaces backend unavailability,
30
+ * EC-362/363).
28
31
  */
29
32
  type SessionExpiryListener = () => void;
30
33
  /**
@@ -39,6 +42,16 @@ export declare function onSessionExpiry(listener: SessionExpiryListener): () =>
39
42
  * errors are ignored — callers can funnel every failure through unconditionally.
40
43
  */
41
44
  export declare function reportSessionExpiredError(error: OperationError): boolean;
45
+ /**
46
+ * Raise session expiry directly (EC-415): the proactive probe path — the auth
47
+ * adapter's `probeSession()` answered `invalid`, so there is no failed
48
+ * operation whose error could carry the predicate through
49
+ * {@link reportSessionExpiredError}. Notifies the SAME listener registry, so
50
+ * the auth store's guarded subscription (`shouldBlockOnSessionExpiry`:
51
+ * signed-in only, idempotent while blocked) and the one blocking overlay
52
+ * (EC-403) serve both detection paths — never a second raise mechanism.
53
+ */
54
+ export declare function raiseSessionExpiry(): void;
42
55
  /**
43
56
  * Is this operation lifecycle event a session-expiry failure? Only `failed`
44
57
  * events can carry the backend's unauthenticated transport blob (`rejected` is
@@ -30,9 +30,21 @@ export function reportSessionExpiredError(error) {
30
30
  if (reportedErrors.has(error))
31
31
  return false;
32
32
  reportedErrors.add(error);
33
+ raiseSessionExpiry();
34
+ return true;
35
+ }
36
+ /**
37
+ * Raise session expiry directly (EC-415): the proactive probe path — the auth
38
+ * adapter's `probeSession()` answered `invalid`, so there is no failed
39
+ * operation whose error could carry the predicate through
40
+ * {@link reportSessionExpiredError}. Notifies the SAME listener registry, so
41
+ * the auth store's guarded subscription (`shouldBlockOnSessionExpiry`:
42
+ * signed-in only, idempotent while blocked) and the one blocking overlay
43
+ * (EC-403) serve both detection paths — never a second raise mechanism.
44
+ */
45
+ export function raiseSessionExpiry() {
33
46
  for (const listener of [...listeners])
34
47
  listener();
35
- return true;
36
48
  }
37
49
  /**
38
50
  * Is this operation lifecycle event a session-expiry failure? Only `failed`
@@ -1 +1 @@
1
- {"version":3,"file":"session-expiry.js","sourceRoot":"","sources":["../../src/lib/session-expiry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAiCrD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAyB,CAAA;AAElD;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,QAA+B;IAC7D,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACvB,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC5B,CAAC,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,IAAI,cAAc,GAAG,IAAI,OAAO,EAAkB,CAAA;AAElD;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAqB;IAC7D,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAC/C,IAAI,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAC3C,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IACzB,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,SAAS,CAAC;QAAE,QAAQ,EAAE,CAAA;IACjD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAqB;IACxD,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,IAAI,qBAAqB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;AACrG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B,CAAC,MAAyB;IACpE,OAAO,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;QAChC,IAAI,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC3D,yBAAyB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QACxC,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B,CAAC,KAG1C;IACC,OAAO,KAAK,CAAC,MAAM,KAAK,WAAW,IAAI,CAAC,KAAK,CAAC,cAAc,CAAA;AAC9D,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,0BAA0B;IACxC,cAAc,GAAG,IAAI,OAAO,EAAkB,CAAA;IAC9C,SAAS,CAAC,KAAK,EAAE,CAAA;AACnB,CAAC","sourcesContent":["import { isSessionExpiredError } from './auth-errors'\nimport type { AuthStatus } from './auth'\nimport type { OperationError, OperationEvent, OperationEventBus } from './operations'\n\n/**\n * App-level session-expiry chokepoint (EC-403).\n *\n * Detection is reactive: an expired Convex session surfaces as a failed\n * operation whose error matches {@link isSessionExpiredError}. Every studio\n * command/query already flows through the operations pipeline, and every\n * failure is emitted on the shared {@link OperationEventBus} — so ONE\n * subscriber on that bus ({@link attachSessionExpiryReporting}, attached in\n * `createStudioOperations`) sees every expiry, no matter which surface issued\n * the operation. Surfaces that hold a structured `OperationError` directly\n * (the shared error banner) report through the same funnel\n * ({@link reportSessionExpiredError}).\n *\n * The consumer is the auth store (`components/auth-context.tsx`): it raises\n * one app-wide `sessionExpired` state which mounts the blocking re-auth\n * overlay — in place, no navigation, the React tree and all unsaved editor\n * state stay alive. This module is deliberately framework-free (a plain\n * listener registry + pure predicates) so the state machine is unit-testable.\n *\n * Proactive re-validation (window focus/interval) deliberately does NOT ship:\n * the auth adapters' `currentSession()` treats *any* failure — including a\n * transient network drop — as \"signed out\" and destroys the stored token, so\n * polling it would sign editors out on a network blip. Distinguishing\n * \"token invalid\" from \"backend unreachable\" needs an adapter change under\n * the no-touch `lib/auth/**` (reported as a finding on EC-403).\n */\n\ntype SessionExpiryListener = () => void\n\nconst listeners = new Set<SessionExpiryListener>()\n\n/**\n * Subscribe to session-expiry reports (the auth store does, once). Returns the\n * unsubscribe function.\n */\nexport function onSessionExpiry(listener: SessionExpiryListener): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * Each expired-session error object is reported at most ONCE. The bus produces\n * a fresh error object per failure, so genuine new failures always report; but\n * a surface can keep rendering a *stale* error long after re-auth (a\n * `persistError` only clears on the next successful save), and remounting that\n * surface must not re-raise the overlay against a session that is valid again.\n */\nlet reportedErrors = new WeakSet<OperationError>()\n\n/**\n * Report a structured operation failure as a *possible* session expiry: runs\n * the {@link isSessionExpiredError} predicate, dedupes per error instance, and\n * notifies listeners. Returns whether listeners were notified. Non-expiry\n * errors are ignored — callers can funnel every failure through unconditionally.\n */\nexport function reportSessionExpiredError(error: OperationError): boolean {\n if (!isSessionExpiredError(error)) return false\n if (reportedErrors.has(error)) return false\n reportedErrors.add(error)\n for (const listener of [...listeners]) listener()\n return true\n}\n\n/**\n * Is this operation lifecycle event a session-expiry failure? Only `failed`\n * events can carry the backend's unauthenticated transport blob (`rejected` is\n * input validation, `denied` is the role hook — both structured, never expiry).\n */\nexport function isSessionExpiryEvent(event: OperationEvent): boolean {\n return event.status === 'failed' && event.error !== undefined && isSessionExpiredError(event.error)\n}\n\n/**\n * The one reactive detection seam (EC-403): subscribe to an operations event\n * bus and report every session-expiry failure. Attached per operations bundle\n * in `createStudioOperations`, so commands, queries, and changeset operations\n * are all covered. Returns the unsubscribe function.\n */\nexport function attachSessionExpiryReporting(events: OperationEventBus): () => void {\n return events.subscribe((event) => {\n if (event.status === 'failed' && event.error !== undefined) {\n reportSessionExpiredError(event.error)\n }\n })\n}\n\n/**\n * Pure raise-guard for the auth store's `sessionExpired` state: an expiry\n * report blocks the studio only when a live session exists (a failure surfacing\n * while signed out or still restoring must not overlay the sign-in screen),\n * and raising is idempotent while already blocked.\n */\nexport function shouldBlockOnSessionExpiry(input: {\n status: AuthStatus\n alreadyBlocked: boolean\n}): boolean {\n return input.status === 'signed-in' && !input.alreadyBlocked\n}\n\n/** Test seam: forget reported errors and listeners between test cases. */\nexport function resetSessionExpiryForTests(): void {\n reportedErrors = new WeakSet<OperationError>()\n listeners.clear()\n}\n"]}
1
+ {"version":3,"file":"session-expiry.js","sourceRoot":"","sources":["../../src/lib/session-expiry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAoCrD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAyB,CAAA;AAElD;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,QAA+B;IAC7D,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACvB,OAAO,GAAG,EAAE;QACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;IAC5B,CAAC,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,IAAI,cAAc,GAAG,IAAI,OAAO,EAAkB,CAAA;AAElD;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CAAC,KAAqB;IAC7D,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAC/C,IAAI,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IAC3C,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IACzB,kBAAkB,EAAE,CAAA;IACpB,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB;IAChC,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,SAAS,CAAC;QAAE,QAAQ,EAAE,CAAA;AACnD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAqB;IACxD,OAAO,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,IAAI,qBAAqB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;AACrG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B,CAAC,MAAyB;IACpE,OAAO,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;QAChC,IAAI,KAAK,CAAC,MAAM,KAAK,QAAQ,IAAI,KAAK,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC3D,yBAAyB,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QACxC,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,0BAA0B,CAAC,KAG1C;IACC,OAAO,KAAK,CAAC,MAAM,KAAK,WAAW,IAAI,CAAC,KAAK,CAAC,cAAc,CAAA;AAC9D,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,0BAA0B;IACxC,cAAc,GAAG,IAAI,OAAO,EAAkB,CAAA;IAC9C,SAAS,CAAC,KAAK,EAAE,CAAA;AACnB,CAAC","sourcesContent":["import { isSessionExpiredError } from './auth-errors'\nimport type { AuthStatus } from './auth'\nimport type { OperationError, OperationEvent, OperationEventBus } from './operations'\n\n/**\n * App-level session-expiry chokepoint (EC-403).\n *\n * Detection is reactive: an expired Convex session surfaces as a failed\n * operation whose error matches {@link isSessionExpiredError}. Every studio\n * command/query already flows through the operations pipeline, and every\n * failure is emitted on the shared {@link OperationEventBus} — so ONE\n * subscriber on that bus ({@link attachSessionExpiryReporting}, attached in\n * `createStudioOperations`) sees every expiry, no matter which surface issued\n * the operation. Surfaces that hold a structured `OperationError` directly\n * (the shared error banner) report through the same funnel\n * ({@link reportSessionExpiredError}).\n *\n * The consumer is the auth store (`components/auth-context.tsx`): it raises\n * one app-wide `sessionExpired` state which mounts the blocking re-auth\n * overlay — in place, no navigation, the React tree and all unsaved editor\n * state stay alive. This module is deliberately framework-free (a plain\n * listener registry + pure predicates) so the state machine is unit-testable.\n *\n * Since EC-415 detection is ALSO proactive: the adapters carry a tri-state\n * `probeSession()` (valid / invalid / unreachable), and the scheduler core in\n * `lib/session-revalidation.ts` — wired to window focus + a modest visible-tab\n * interval in `components/auth-context.tsx` — funnels a probe's `invalid`\n * answer into this SAME chokepoint via {@link raiseSessionExpiry}. One\n * listener registry, one auth-store guard, one overlay — no second raise\n * path. `unreachable` stays silent by contract (the probe never clears the\n * token; the operations layer already surfaces backend unavailability,\n * EC-362/363).\n */\n\ntype SessionExpiryListener = () => void\n\nconst listeners = new Set<SessionExpiryListener>()\n\n/**\n * Subscribe to session-expiry reports (the auth store does, once). Returns the\n * unsubscribe function.\n */\nexport function onSessionExpiry(listener: SessionExpiryListener): () => void {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n}\n\n/**\n * Each expired-session error object is reported at most ONCE. The bus produces\n * a fresh error object per failure, so genuine new failures always report; but\n * a surface can keep rendering a *stale* error long after re-auth (a\n * `persistError` only clears on the next successful save), and remounting that\n * surface must not re-raise the overlay against a session that is valid again.\n */\nlet reportedErrors = new WeakSet<OperationError>()\n\n/**\n * Report a structured operation failure as a *possible* session expiry: runs\n * the {@link isSessionExpiredError} predicate, dedupes per error instance, and\n * notifies listeners. Returns whether listeners were notified. Non-expiry\n * errors are ignored — callers can funnel every failure through unconditionally.\n */\nexport function reportSessionExpiredError(error: OperationError): boolean {\n if (!isSessionExpiredError(error)) return false\n if (reportedErrors.has(error)) return false\n reportedErrors.add(error)\n raiseSessionExpiry()\n return true\n}\n\n/**\n * Raise session expiry directly (EC-415): the proactive probe path — the auth\n * adapter's `probeSession()` answered `invalid`, so there is no failed\n * operation whose error could carry the predicate through\n * {@link reportSessionExpiredError}. Notifies the SAME listener registry, so\n * the auth store's guarded subscription (`shouldBlockOnSessionExpiry`:\n * signed-in only, idempotent while blocked) and the one blocking overlay\n * (EC-403) serve both detection paths — never a second raise mechanism.\n */\nexport function raiseSessionExpiry(): void {\n for (const listener of [...listeners]) listener()\n}\n\n/**\n * Is this operation lifecycle event a session-expiry failure? Only `failed`\n * events can carry the backend's unauthenticated transport blob (`rejected` is\n * input validation, `denied` is the role hook — both structured, never expiry).\n */\nexport function isSessionExpiryEvent(event: OperationEvent): boolean {\n return event.status === 'failed' && event.error !== undefined && isSessionExpiredError(event.error)\n}\n\n/**\n * The one reactive detection seam (EC-403): subscribe to an operations event\n * bus and report every session-expiry failure. Attached per operations bundle\n * in `createStudioOperations`, so commands, queries, and changeset operations\n * are all covered. Returns the unsubscribe function.\n */\nexport function attachSessionExpiryReporting(events: OperationEventBus): () => void {\n return events.subscribe((event) => {\n if (event.status === 'failed' && event.error !== undefined) {\n reportSessionExpiredError(event.error)\n }\n })\n}\n\n/**\n * Pure raise-guard for the auth store's `sessionExpired` state: an expiry\n * report blocks the studio only when a live session exists (a failure surfacing\n * while signed out or still restoring must not overlay the sign-in screen),\n * and raising is idempotent while already blocked.\n */\nexport function shouldBlockOnSessionExpiry(input: {\n status: AuthStatus\n alreadyBlocked: boolean\n}): boolean {\n return input.status === 'signed-in' && !input.alreadyBlocked\n}\n\n/** Test seam: forget reported errors and listeners between test cases. */\nexport function resetSessionExpiryForTests(): void {\n reportedErrors = new WeakSet<OperationError>()\n listeners.clear()\n}\n"]}
@@ -0,0 +1,81 @@
1
+ import type { SessionProbeResult } from './auth/index.js';
2
+ /**
3
+ * Proactive session re-validation scheduler (EC-415) — the pure, framework-free
4
+ * core behind the window-focus/interval probes wired up in
5
+ * `components/auth-context.tsx`.
6
+ *
7
+ * The adapters' tri-state `probeSession()` makes proactive checking SAFE
8
+ * (`unreachable` never clears the stored token — see `lib/auth/auth-adapter.ts`);
9
+ * this core makes it CHEAP and QUIET:
10
+ *
11
+ * - **Min gap:** at most one probe per {@link SESSION_PROBE_MIN_GAP_MS}
12
+ * (30 s), no matter how many focus/visibility/interval triggers fire — alt-
13
+ * tabbing back and forth never hammers the backend.
14
+ * - **In-flight dedupe:** never two concurrent probes.
15
+ * - **Gating:** probes run only over a live session (`signedIn`), never while
16
+ * the re-auth overlay is already up (`blocked` — nothing left to detect),
17
+ * and only while the tab is visible (`visible` — a background tab's session
18
+ * dying is caught by the focus probe the moment the editor returns).
19
+ * - **Routing:** `invalid` → {@link SessionRevalidatorOptions.onInvalid} (the
20
+ * EC-403 chokepoint, `raiseSessionExpiry`); `valid` and `unreachable` are
21
+ * silent. A probe that THROWS (contract violation) is treated as
22
+ * `unreachable` — a scheduler must never turn a transport surprise into a
23
+ * sign-out.
24
+ *
25
+ * Trigger cadence (the React wiring): window `focus` + document
26
+ * `visibilitychange` fire a trigger immediately (subject to the min gap), and
27
+ * a {@link SESSION_PROBE_INTERVAL_MS} (5 min) interval covers the
28
+ * tab-left-open-and-visible case. Proactive detection is a UX refinement on
29
+ * top of the reactive EC-403 chokepoint (any operation against a dead session
30
+ * still raises within one call), so the cadence is deliberately modest.
31
+ */
32
+ /** At most one probe per this many milliseconds, across ALL trigger sources. */
33
+ export declare const SESSION_PROBE_MIN_GAP_MS = 30000;
34
+ /** Background probe cadence while the tab stays visible (the React interval). */
35
+ export declare const SESSION_PROBE_INTERVAL_MS: number;
36
+ /** The live studio state a probe is gated on (read fresh per trigger). */
37
+ export interface SessionRevalidatorGate {
38
+ /** A session exists to re-validate (`status === 'signed-in'`). */
39
+ readonly signedIn: boolean;
40
+ /** The re-auth overlay is already up — nothing left to detect. */
41
+ readonly blocked: boolean;
42
+ /** The tab is visible; hidden tabs wait for the return-focus probe. */
43
+ readonly visible: boolean;
44
+ }
45
+ /**
46
+ * Pure per-trigger decision: should this trigger actually probe? Exported
47
+ * separately so the gating matrix is unit-pinnable without a scheduler.
48
+ */
49
+ export declare function shouldProbeSession(input: {
50
+ gate: SessionRevalidatorGate;
51
+ probeInFlight: boolean;
52
+ lastProbeAt: number | null;
53
+ now: number;
54
+ minGapMs: number;
55
+ }): boolean;
56
+ export interface SessionRevalidatorOptions {
57
+ /** The adapter probe (`auth.probeSession()`). */
58
+ probe: () => Promise<SessionProbeResult>;
59
+ /** Funnel for an `invalid` answer — the EC-403 raise (`raiseSessionExpiry`). */
60
+ onInvalid: () => void;
61
+ /** Read the CURRENT gate state (refs, not a snapshot) at trigger time. */
62
+ readGate: () => SessionRevalidatorGate;
63
+ /** Override {@link SESSION_PROBE_MIN_GAP_MS} (tests). */
64
+ minGapMs?: number;
65
+ /** Clock override (tests). */
66
+ now?: () => number;
67
+ }
68
+ export interface SessionRevalidator {
69
+ /**
70
+ * Fire from any trigger source (focus, visibilitychange, interval tick).
71
+ * Resolves once the trigger is handled — immediately when gated away,
72
+ * after the probe settles when one runs.
73
+ */
74
+ trigger(): Promise<void>;
75
+ }
76
+ /**
77
+ * Create the scheduler. One instance per auth adapter (the React effect's
78
+ * lifetime); all internal state — last probe time, in-flight flag — lives
79
+ * here, keeping the React wiring a thin listener layer.
80
+ */
81
+ export declare function createSessionRevalidator(options: SessionRevalidatorOptions): SessionRevalidator;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Proactive session re-validation scheduler (EC-415) — the pure, framework-free
3
+ * core behind the window-focus/interval probes wired up in
4
+ * `components/auth-context.tsx`.
5
+ *
6
+ * The adapters' tri-state `probeSession()` makes proactive checking SAFE
7
+ * (`unreachable` never clears the stored token — see `lib/auth/auth-adapter.ts`);
8
+ * this core makes it CHEAP and QUIET:
9
+ *
10
+ * - **Min gap:** at most one probe per {@link SESSION_PROBE_MIN_GAP_MS}
11
+ * (30 s), no matter how many focus/visibility/interval triggers fire — alt-
12
+ * tabbing back and forth never hammers the backend.
13
+ * - **In-flight dedupe:** never two concurrent probes.
14
+ * - **Gating:** probes run only over a live session (`signedIn`), never while
15
+ * the re-auth overlay is already up (`blocked` — nothing left to detect),
16
+ * and only while the tab is visible (`visible` — a background tab's session
17
+ * dying is caught by the focus probe the moment the editor returns).
18
+ * - **Routing:** `invalid` → {@link SessionRevalidatorOptions.onInvalid} (the
19
+ * EC-403 chokepoint, `raiseSessionExpiry`); `valid` and `unreachable` are
20
+ * silent. A probe that THROWS (contract violation) is treated as
21
+ * `unreachable` — a scheduler must never turn a transport surprise into a
22
+ * sign-out.
23
+ *
24
+ * Trigger cadence (the React wiring): window `focus` + document
25
+ * `visibilitychange` fire a trigger immediately (subject to the min gap), and
26
+ * a {@link SESSION_PROBE_INTERVAL_MS} (5 min) interval covers the
27
+ * tab-left-open-and-visible case. Proactive detection is a UX refinement on
28
+ * top of the reactive EC-403 chokepoint (any operation against a dead session
29
+ * still raises within one call), so the cadence is deliberately modest.
30
+ */
31
+ /** At most one probe per this many milliseconds, across ALL trigger sources. */
32
+ export const SESSION_PROBE_MIN_GAP_MS = 30_000;
33
+ /** Background probe cadence while the tab stays visible (the React interval). */
34
+ export const SESSION_PROBE_INTERVAL_MS = 5 * 60_000;
35
+ /**
36
+ * Pure per-trigger decision: should this trigger actually probe? Exported
37
+ * separately so the gating matrix is unit-pinnable without a scheduler.
38
+ */
39
+ export function shouldProbeSession(input) {
40
+ const { gate, probeInFlight, lastProbeAt, now, minGapMs } = input;
41
+ if (!gate.signedIn || gate.blocked || !gate.visible)
42
+ return false;
43
+ if (probeInFlight)
44
+ return false;
45
+ if (lastProbeAt !== null && now - lastProbeAt < minGapMs)
46
+ return false;
47
+ return true;
48
+ }
49
+ /**
50
+ * Create the scheduler. One instance per auth adapter (the React effect's
51
+ * lifetime); all internal state — last probe time, in-flight flag — lives
52
+ * here, keeping the React wiring a thin listener layer.
53
+ */
54
+ export function createSessionRevalidator(options) {
55
+ const minGapMs = options.minGapMs ?? SESSION_PROBE_MIN_GAP_MS;
56
+ const now = options.now ?? (() => Date.now());
57
+ let lastProbeAt = null;
58
+ let probeInFlight = false;
59
+ return {
60
+ async trigger() {
61
+ if (!shouldProbeSession({
62
+ gate: options.readGate(),
63
+ probeInFlight,
64
+ lastProbeAt,
65
+ now: now(),
66
+ minGapMs,
67
+ })) {
68
+ return;
69
+ }
70
+ // The gap counts from probe START so a slow probe doesn't schedule the
71
+ // next one early; the in-flight flag covers the window until it settles.
72
+ lastProbeAt = now();
73
+ probeInFlight = true;
74
+ let result;
75
+ try {
76
+ result = await options.probe();
77
+ }
78
+ catch {
79
+ // Contract violation (probeSession must not throw) — treat as
80
+ // unreachable: silence, never a sign-out.
81
+ result = 'unreachable';
82
+ }
83
+ finally {
84
+ probeInFlight = false;
85
+ }
86
+ if (result === 'invalid')
87
+ options.onInvalid();
88
+ // 'valid' → nothing to do; 'unreachable' → silent by ratified scope:
89
+ // the token survived (adapter contract) and the operations layer owns
90
+ // backend-unavailability surfacing (EC-362/363).
91
+ },
92
+ };
93
+ }
94
+ //# sourceMappingURL=session-revalidation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-revalidation.js","sourceRoot":"","sources":["../../src/lib/session-revalidation.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,gFAAgF;AAChF,MAAM,CAAC,MAAM,wBAAwB,GAAG,MAAM,CAAA;AAE9C,iFAAiF;AACjF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,GAAG,MAAM,CAAA;AAYnD;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAMlC;IACC,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,WAAW,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAA;IACjE,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAA;IACjE,IAAI,aAAa;QAAE,OAAO,KAAK,CAAA;IAC/B,IAAI,WAAW,KAAK,IAAI,IAAI,GAAG,GAAG,WAAW,GAAG,QAAQ;QAAE,OAAO,KAAK,CAAA;IACtE,OAAO,IAAI,CAAA;AACb,CAAC;AAwBD;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAkC;IACzE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,wBAAwB,CAAA;IAC7D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAA;IAC7C,IAAI,WAAW,GAAkB,IAAI,CAAA;IACrC,IAAI,aAAa,GAAG,KAAK,CAAA;IAEzB,OAAO;QACL,KAAK,CAAC,OAAO;YACX,IACE,CAAC,kBAAkB,CAAC;gBAClB,IAAI,EAAE,OAAO,CAAC,QAAQ,EAAE;gBACxB,aAAa;gBACb,WAAW;gBACX,GAAG,EAAE,GAAG,EAAE;gBACV,QAAQ;aACT,CAAC,EACF,CAAC;gBACD,OAAM;YACR,CAAC;YACD,uEAAuE;YACvE,yEAAyE;YACzE,WAAW,GAAG,GAAG,EAAE,CAAA;YACnB,aAAa,GAAG,IAAI,CAAA;YACpB,IAAI,MAA0B,CAAA;YAC9B,IAAI,CAAC;gBACH,MAAM,GAAG,MAAM,OAAO,CAAC,KAAK,EAAE,CAAA;YAChC,CAAC;YAAC,MAAM,CAAC;gBACP,8DAA8D;gBAC9D,0CAA0C;gBAC1C,MAAM,GAAG,aAAa,CAAA;YACxB,CAAC;oBAAS,CAAC;gBACT,aAAa,GAAG,KAAK,CAAA;YACvB,CAAC;YACD,IAAI,MAAM,KAAK,SAAS;gBAAE,OAAO,CAAC,SAAS,EAAE,CAAA;YAC7C,qEAAqE;YACrE,sEAAsE;YACtE,iDAAiD;QACnD,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["import type { SessionProbeResult } from './auth'\n\n/**\n * Proactive session re-validation scheduler (EC-415) — the pure, framework-free\n * core behind the window-focus/interval probes wired up in\n * `components/auth-context.tsx`.\n *\n * The adapters' tri-state `probeSession()` makes proactive checking SAFE\n * (`unreachable` never clears the stored token — see `lib/auth/auth-adapter.ts`);\n * this core makes it CHEAP and QUIET:\n *\n * - **Min gap:** at most one probe per {@link SESSION_PROBE_MIN_GAP_MS}\n * (30 s), no matter how many focus/visibility/interval triggers fire — alt-\n * tabbing back and forth never hammers the backend.\n * - **In-flight dedupe:** never two concurrent probes.\n * - **Gating:** probes run only over a live session (`signedIn`), never while\n * the re-auth overlay is already up (`blocked` — nothing left to detect),\n * and only while the tab is visible (`visible` — a background tab's session\n * dying is caught by the focus probe the moment the editor returns).\n * - **Routing:** `invalid` → {@link SessionRevalidatorOptions.onInvalid} (the\n * EC-403 chokepoint, `raiseSessionExpiry`); `valid` and `unreachable` are\n * silent. A probe that THROWS (contract violation) is treated as\n * `unreachable` — a scheduler must never turn a transport surprise into a\n * sign-out.\n *\n * Trigger cadence (the React wiring): window `focus` + document\n * `visibilitychange` fire a trigger immediately (subject to the min gap), and\n * a {@link SESSION_PROBE_INTERVAL_MS} (5 min) interval covers the\n * tab-left-open-and-visible case. Proactive detection is a UX refinement on\n * top of the reactive EC-403 chokepoint (any operation against a dead session\n * still raises within one call), so the cadence is deliberately modest.\n */\n\n/** At most one probe per this many milliseconds, across ALL trigger sources. */\nexport const SESSION_PROBE_MIN_GAP_MS = 30_000\n\n/** Background probe cadence while the tab stays visible (the React interval). */\nexport const SESSION_PROBE_INTERVAL_MS = 5 * 60_000\n\n/** The live studio state a probe is gated on (read fresh per trigger). */\nexport interface SessionRevalidatorGate {\n /** A session exists to re-validate (`status === 'signed-in'`). */\n readonly signedIn: boolean\n /** The re-auth overlay is already up — nothing left to detect. */\n readonly blocked: boolean\n /** The tab is visible; hidden tabs wait for the return-focus probe. */\n readonly visible: boolean\n}\n\n/**\n * Pure per-trigger decision: should this trigger actually probe? Exported\n * separately so the gating matrix is unit-pinnable without a scheduler.\n */\nexport function shouldProbeSession(input: {\n gate: SessionRevalidatorGate\n probeInFlight: boolean\n lastProbeAt: number | null\n now: number\n minGapMs: number\n}): boolean {\n const { gate, probeInFlight, lastProbeAt, now, minGapMs } = input\n if (!gate.signedIn || gate.blocked || !gate.visible) return false\n if (probeInFlight) return false\n if (lastProbeAt !== null && now - lastProbeAt < minGapMs) return false\n return true\n}\n\nexport interface SessionRevalidatorOptions {\n /** The adapter probe (`auth.probeSession()`). */\n probe: () => Promise<SessionProbeResult>\n /** Funnel for an `invalid` answer — the EC-403 raise (`raiseSessionExpiry`). */\n onInvalid: () => void\n /** Read the CURRENT gate state (refs, not a snapshot) at trigger time. */\n readGate: () => SessionRevalidatorGate\n /** Override {@link SESSION_PROBE_MIN_GAP_MS} (tests). */\n minGapMs?: number\n /** Clock override (tests). */\n now?: () => number\n}\n\nexport interface SessionRevalidator {\n /**\n * Fire from any trigger source (focus, visibilitychange, interval tick).\n * Resolves once the trigger is handled — immediately when gated away,\n * after the probe settles when one runs.\n */\n trigger(): Promise<void>\n}\n\n/**\n * Create the scheduler. One instance per auth adapter (the React effect's\n * lifetime); all internal state — last probe time, in-flight flag — lives\n * here, keeping the React wiring a thin listener layer.\n */\nexport function createSessionRevalidator(options: SessionRevalidatorOptions): SessionRevalidator {\n const minGapMs = options.minGapMs ?? SESSION_PROBE_MIN_GAP_MS\n const now = options.now ?? (() => Date.now())\n let lastProbeAt: number | null = null\n let probeInFlight = false\n\n return {\n async trigger(): Promise<void> {\n if (\n !shouldProbeSession({\n gate: options.readGate(),\n probeInFlight,\n lastProbeAt,\n now: now(),\n minGapMs,\n })\n ) {\n return\n }\n // The gap counts from probe START so a slow probe doesn't schedule the\n // next one early; the in-flight flag covers the window until it settles.\n lastProbeAt = now()\n probeInFlight = true\n let result: SessionProbeResult\n try {\n result = await options.probe()\n } catch {\n // Contract violation (probeSession must not throw) — treat as\n // unreachable: silence, never a sign-out.\n result = 'unreachable'\n } finally {\n probeInFlight = false\n }\n if (result === 'invalid') options.onInvalid()\n // 'valid' → nothing to do; 'unreachable' → silent by ratified scope:\n // the token survived (adapter contract) and the operations layer owns\n // backend-unavailability surfacing (EC-362/363).\n },\n }\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elytracms/studio",
3
3
  "author": "finaldream <hi@finaldream.de>",
4
- "version": "0.0.64",
4
+ "version": "0.0.66",
5
5
  "private": false,
6
6
  "description": "The Elytra studio engine (UI + lib + routes + data-only Convex schema) as an importable package. Extraction staged in EXTRACTION.md (EC-223); Stages 1-2 = UI primitives + router-free lib leaves.",
7
7
  "license": "MIT",
@@ -146,7 +146,7 @@
146
146
  "lucide-react": "^1.23.0",
147
147
  "tailwind-merge": "^3.6.0",
148
148
  "zod": "^4.4.3",
149
- "@elytracms/core": "^0.0.64"
149
+ "@elytracms/core": "^0.0.66"
150
150
  },
151
151
  "peerDependencies": {
152
152
  "@tanstack/react-router": "^1.170.0",
@@ -150,6 +150,19 @@
150
150
  text-decoration: underline;
151
151
  text-underline-offset: 2px;
152
152
  }
153
+ /* Semantic font-size steps (EC-411). Editing-surface styling for the `fontSize`
154
+ * mark's hook (`data-rich-text-size` / `.rich-text-size-*`) — em-relative so the
155
+ * steps scale off the surrounding text, never fixed pixels. Delivery owns its own
156
+ * step typography via the same documented class hook (the leash stays in code). */
157
+ .ec-rte-content .rich-text-size-sm {
158
+ font-size: 0.8em;
159
+ }
160
+ .ec-rte-content .rich-text-size-lg {
161
+ font-size: 1.35em;
162
+ }
163
+ .ec-rte-content .rich-text-size-xl {
164
+ font-size: 1.75em;
165
+ }
153
166
  .ec-rte-content ul,
154
167
  .ec-rte-content ol {
155
168
  padding-left: 1.4em;