@elytracms/studio 0.0.77 → 0.0.79

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 (90) hide show
  1. package/convex/contract-parity.test.ts +238 -0
  2. package/convex/tsconfig.json +3 -1
  3. package/dist/components/app-views/default-modules.js +5 -1
  4. package/dist/components/app-views/default-modules.js.map +1 -1
  5. package/dist/components/assets/asset-thumb.js +12 -11
  6. package/dist/components/assets/asset-thumb.js.map +1 -1
  7. package/dist/components/builder/blocks-panel.d.ts +11 -5
  8. package/dist/components/builder/blocks-panel.js +18 -24
  9. package/dist/components/builder/blocks-panel.js.map +1 -1
  10. package/dist/components/builder/canvas-render-context.d.ts +22 -0
  11. package/dist/components/builder/canvas-render-context.js +65 -9
  12. package/dist/components/builder/canvas-render-context.js.map +1 -1
  13. package/dist/components/builder/reuse.d.ts +25 -0
  14. package/dist/components/builder/reuse.js +11 -2
  15. package/dist/components/builder/reuse.js.map +1 -1
  16. package/dist/components/cms/asset-field.js +52 -19
  17. package/dist/components/cms/asset-field.js.map +1 -1
  18. package/dist/components/cms/asset-picker-core.d.ts +40 -0
  19. package/dist/components/cms/asset-picker-core.js +21 -0
  20. package/dist/components/cms/asset-picker-core.js.map +1 -1
  21. package/dist/components/cms/asset-picker.d.ts +11 -2
  22. package/dist/components/cms/asset-picker.js +34 -13
  23. package/dist/components/cms/asset-picker.js.map +1 -1
  24. package/dist/components/cms/cell.js +2 -1
  25. package/dist/components/cms/cell.js.map +1 -1
  26. package/dist/components/cms/cms-store.d.ts +8 -1
  27. package/dist/components/cms/cms-store.js +8 -7
  28. package/dist/components/cms/cms-store.js.map +1 -1
  29. package/dist/components/cms/collection-table.js +8 -2
  30. package/dist/components/cms/collection-table.js.map +1 -1
  31. package/dist/components/cms/date-input-core.d.ts +64 -0
  32. package/dist/components/cms/date-input-core.js +78 -0
  33. package/dist/components/cms/date-input-core.js.map +1 -0
  34. package/dist/components/cms/date-input.d.ts +43 -0
  35. package/dist/components/cms/date-input.js +109 -0
  36. package/dist/components/cms/date-input.js.map +1 -0
  37. package/dist/components/cms/detail-editor.js.map +1 -1
  38. package/dist/components/cms/document-diff-view.js +2 -1
  39. package/dist/components/cms/document-diff-view.js.map +1 -1
  40. package/dist/components/cms/document-staging.d.ts +15 -4
  41. package/dist/components/cms/document-staging.js +26 -15
  42. package/dist/components/cms/document-staging.js.map +1 -1
  43. package/dist/components/cms/editable-cell.js +39 -5
  44. package/dist/components/cms/editable-cell.js.map +1 -1
  45. package/dist/components/cms/field-control.js +38 -7
  46. package/dist/components/cms/field-control.js.map +1 -1
  47. package/dist/components/cms/scalar-value-editor.js +4 -1
  48. package/dist/components/cms/scalar-value-editor.js.map +1 -1
  49. package/dist/components/cms/slug-auto-derive.d.ts +41 -0
  50. package/dist/components/cms/slug-auto-derive.js +14 -0
  51. package/dist/components/cms/slug-auto-derive.js.map +1 -0
  52. package/dist/components/components/transfer-dialog.js +3 -1
  53. package/dist/components/components/transfer-dialog.js.map +1 -1
  54. package/dist/components/project-session.d.ts +44 -22
  55. package/dist/components/project-session.js +72 -36
  56. package/dist/components/project-session.js.map +1 -1
  57. package/dist/lib/assets/asset-url.d.ts +20 -0
  58. package/dist/lib/assets/asset-url.js +25 -0
  59. package/dist/lib/assets/asset-url.js.map +1 -0
  60. package/dist/lib/assets/browser-blob-storage.d.ts +80 -0
  61. package/dist/lib/assets/browser-blob-storage.js +86 -0
  62. package/dist/lib/assets/browser-blob-storage.js.map +1 -0
  63. package/dist/lib/assets/index.d.ts +2 -0
  64. package/dist/lib/assets/index.js +2 -0
  65. package/dist/lib/assets/index.js.map +1 -1
  66. package/dist/lib/data/collection-content.d.ts +20 -1
  67. package/dist/lib/data/collection-content.js +51 -1
  68. package/dist/lib/data/collection-content.js.map +1 -1
  69. package/dist/lib/data/content-session-refresh.d.ts +74 -0
  70. package/dist/lib/data/content-session-refresh.js +138 -0
  71. package/dist/lib/data/content-session-refresh.js.map +1 -0
  72. package/dist/lib/data/index.d.ts +2 -0
  73. package/dist/lib/data/index.js +2 -0
  74. package/dist/lib/data/index.js.map +1 -1
  75. package/dist/lib/data/playground-unload-guard.d.ts +22 -0
  76. package/dist/lib/data/playground-unload-guard.js +73 -0
  77. package/dist/lib/data/playground-unload-guard.js.map +1 -0
  78. package/dist/lib/i18n/messages.de.js +3 -1
  79. package/dist/lib/i18n/messages.de.js.map +1 -1
  80. package/dist/lib/i18n/messages.en.d.ts +2 -0
  81. package/dist/lib/i18n/messages.en.js +5 -1
  82. package/dist/lib/i18n/messages.en.js.map +1 -1
  83. package/dist/lib/persistence/convex-adapter.js +3 -3
  84. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  85. package/dist/lib/persistence/index.js +6 -1
  86. package/dist/lib/persistence/index.js.map +1 -1
  87. package/dist/ui/input.d.ts +8 -3
  88. package/dist/ui/input.js +6 -1
  89. package/dist/ui/input.js.map +1 -1
  90. package/package.json +3 -2
@@ -0,0 +1,86 @@
1
+ import { InMemoryBlobStorage } from '@elytracms/core/persistence';
2
+ /**
3
+ * The real browser minter: an object URL backed by a same-process `Blob`.
4
+ * Deliberately thin and untested in Node (mirrors `canvasVariantEncoder` in
5
+ * `thumbnail.ts` — a DOM shell around a pure/injectable orchestrator).
6
+ */
7
+ export const objectUrlMinter = (data, contentType) => URL.createObjectURL(new Blob([data.slice()], { type: contentType || 'application/octet-stream' }));
8
+ /** The real browser revoker: releases an object URL's underlying resources. */
9
+ export const objectUrlRevoker = (url) => {
10
+ URL.revokeObjectURL(url);
11
+ };
12
+ /**
13
+ * Wraps a `BlobStorageAdapter` so `url()`/`resolveUrl()` return a browser-
14
+ * displayable URL instead of the inner adapter's opaque locator (EC-459). See
15
+ * the module doc above for the full rationale.
16
+ */
17
+ export class BrowserBlobStorage {
18
+ inner;
19
+ mint;
20
+ revoke;
21
+ /** storageKey → minted display URL, populated at the moment bytes are known. */
22
+ displayUrls = new Map();
23
+ constructor(options = {}) {
24
+ this.inner = options.inner ?? new InMemoryBlobStorage();
25
+ this.mint = options.mint ?? objectUrlMinter;
26
+ this.revoke = options.revoke ?? objectUrlRevoker;
27
+ }
28
+ /**
29
+ * Mint (or re-mint) the display URL for `key`. A re-put under the same key
30
+ * (re-upload / overwrite) revokes the stale URL first, so no object URL leaks
31
+ * and no surface can ever show stale bytes under a reused key.
32
+ */
33
+ remember(key, data, contentType) {
34
+ const previous = this.displayUrls.get(key);
35
+ if (previous)
36
+ this.revoke(previous);
37
+ this.displayUrls.set(key, this.mint(data, contentType ?? 'application/octet-stream'));
38
+ }
39
+ async put(key, data, contentType) {
40
+ await this.inner.put(key, data, contentType);
41
+ this.remember(key, data, contentType);
42
+ }
43
+ async get(key) {
44
+ return this.inner.get(key);
45
+ }
46
+ url(key) {
47
+ // Fall back to the inner adapter's own locator only when this wrapper never
48
+ // saw the bytes (e.g. a seed record whose metadata carries no stored blob at
49
+ // all) — the same "no bytes, no pretending" behaviour the inner adapter has
50
+ // always had; nothing here silently invents a broken image.
51
+ return this.displayUrls.get(key) ?? this.inner.url(key);
52
+ }
53
+ async has(key) {
54
+ return this.inner.has(key);
55
+ }
56
+ async delete(key) {
57
+ await this.inner.delete(key);
58
+ const cached = this.displayUrls.get(key);
59
+ if (cached) {
60
+ this.revoke(cached);
61
+ this.displayUrls.delete(key);
62
+ }
63
+ }
64
+ async store(data, contentType) {
65
+ if (!this.inner.store) {
66
+ throw new Error('BrowserBlobStorage requires an inner adapter that implements store().');
67
+ }
68
+ const key = await this.inner.store(data, contentType);
69
+ this.remember(key, data, contentType);
70
+ return key;
71
+ }
72
+ async resolveUrl(key) {
73
+ const cached = this.displayUrls.get(key);
74
+ if (cached)
75
+ return cached;
76
+ // Not minted through THIS wrapper — fall back to whatever the inner adapter
77
+ // can resolve (still `undefined` when genuinely no bytes exist: scope §5,
78
+ // an explicit missing state, never a silently broken image).
79
+ return this.inner.resolveUrl ? this.inner.resolveUrl(key) : undefined;
80
+ }
81
+ }
82
+ /** Convenience factory — `createBrowserBlobStorage()` wraps a fresh `InMemoryBlobStorage`. */
83
+ export function createBrowserBlobStorage(options) {
84
+ return new BrowserBlobStorage(options);
85
+ }
86
+ //# sourceMappingURL=browser-blob-storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-blob-storage.js","sourceRoot":"","sources":["../../../src/lib/assets/browser-blob-storage.ts"],"names":[],"mappings":"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","sourcesContent":["import { InMemoryBlobStorage } from '@elytracms/core/persistence'\nimport type { BlobStorageAdapter } from '@elytracms/core/persistence'\n\n/**\n * Studio-side (playground-only) blob storage wrapper (EC-459).\n *\n * Root cause this fixes: `InMemoryBlobStorage` (`@elytracms/core/persistence`)\n * mints `url(key)` as an opaque `memory://blobs/<key>` locator. That is correct\n * for its job — Node tests and programmatic inspection never need a real\n * browser resource — but the studio's upload flow (`upload.ts`) bakes\n * `blobs.resolveUrl?.(storageKey) ?? blobs.url(storageKey)` straight into the\n * persisted `AssetRecord.url` (and the EC-400 thumb variant's `url`). Every\n * surface that then puts that value in an `<img src>` — the asset library grid,\n * the compact field preview, the focal-point editor, and (worst of all, no\n * gating at all) the canvas iframe's resolved-asset renderer — hands the\n * browser a URL it can never fetch. Pixels never render; the canvas iframe\n * shows an actual broken-image icon (`naturalWidth === 0`).\n *\n * The fix: wrap the byte store, not replace it. `put`/`store` still delegate\n * unchanged to `inner` (an `InMemoryBlobStorage` by default, or any adapter a\n * test wants), so bytes live exactly where they always did. The moment bytes\n * are known, THIS wrapper additionally mints a real browser-displayable URL\n * (an object URL by default) and serves THAT from `url()`/`resolveUrl()` —\n * so the value the upload flow bakes into the record is something an `<img>`\n * can actually load. `@elytracms/core`'s `InMemoryBlobStorage` is untouched and\n * stays platform-neutral (it still runs fine under Node tests); the one\n * DOM-touching function (`objectUrlMinter`, using the real `Blob`/`URL` globals)\n * is injected, so this whole orchestrator — caching, delegation, revoke-on-\n * delete/overwrite — is unit-testable in Node with a fake minter.\n *\n * Playground-only by construction: nothing calls this in the Convex/connected\n * path, and the `BlobStorageAdapter` interface is unchanged, so the delivery\n * wire and the connected-mode adapter are untouched.\n */\n\n/** Mints a browser-displayable URL from raw bytes. Injected for Node testability. */\nexport type UrlMinter = (data: Uint8Array, contentType: string) => string\n\n/** Releases a previously-minted URL's resources (e.g. `URL.revokeObjectURL`). */\nexport type UrlRevoker = (url: string) => void\n\n/**\n * The real browser minter: an object URL backed by a same-process `Blob`.\n * Deliberately thin and untested in Node (mirrors `canvasVariantEncoder` in\n * `thumbnail.ts` — a DOM shell around a pure/injectable orchestrator).\n */\nexport const objectUrlMinter: UrlMinter = (data, contentType) =>\n URL.createObjectURL(new Blob([data.slice() as BlobPart], { type: contentType || 'application/octet-stream' }))\n\n/** The real browser revoker: releases an object URL's underlying resources. */\nexport const objectUrlRevoker: UrlRevoker = (url) => {\n URL.revokeObjectURL(url)\n}\n\nexport interface BrowserBlobStorageOptions {\n /** The wrapped adapter that actually holds the bytes. Defaults to a fresh `InMemoryBlobStorage`. */\n inner?: BlobStorageAdapter\n /** Defaults to {@link objectUrlMinter}; tests inject a deterministic fake. */\n mint?: UrlMinter\n /** Defaults to {@link objectUrlRevoker}; tests inject a fake to assert revocation. */\n revoke?: UrlRevoker\n}\n\n/**\n * Wraps a `BlobStorageAdapter` so `url()`/`resolveUrl()` return a browser-\n * displayable URL instead of the inner adapter's opaque locator (EC-459). See\n * the module doc above for the full rationale.\n */\nexport class BrowserBlobStorage implements BlobStorageAdapter {\n private readonly inner: BlobStorageAdapter\n private readonly mint: UrlMinter\n private readonly revoke: UrlRevoker\n /** storageKey → minted display URL, populated at the moment bytes are known. */\n private readonly displayUrls = new Map<string, string>()\n\n constructor(options: BrowserBlobStorageOptions = {}) {\n this.inner = options.inner ?? new InMemoryBlobStorage()\n this.mint = options.mint ?? objectUrlMinter\n this.revoke = options.revoke ?? objectUrlRevoker\n }\n\n /**\n * Mint (or re-mint) the display URL for `key`. A re-put under the same key\n * (re-upload / overwrite) revokes the stale URL first, so no object URL leaks\n * and no surface can ever show stale bytes under a reused key.\n */\n private remember(key: string, data: Uint8Array, contentType: string | undefined): void {\n const previous = this.displayUrls.get(key)\n if (previous) this.revoke(previous)\n this.displayUrls.set(key, this.mint(data, contentType ?? 'application/octet-stream'))\n }\n\n async put(key: string, data: Uint8Array, contentType?: string): Promise<void> {\n await this.inner.put(key, data, contentType)\n this.remember(key, data, contentType)\n }\n\n async get(key: string): Promise<Uint8Array | undefined> {\n return this.inner.get(key)\n }\n\n url(key: string): string {\n // Fall back to the inner adapter's own locator only when this wrapper never\n // saw the bytes (e.g. a seed record whose metadata carries no stored blob at\n // all) — the same \"no bytes, no pretending\" behaviour the inner adapter has\n // always had; nothing here silently invents a broken image.\n return this.displayUrls.get(key) ?? this.inner.url(key)\n }\n\n async has(key: string): Promise<boolean> {\n return this.inner.has(key)\n }\n\n async delete(key: string): Promise<void> {\n await this.inner.delete(key)\n const cached = this.displayUrls.get(key)\n if (cached) {\n this.revoke(cached)\n this.displayUrls.delete(key)\n }\n }\n\n async store(data: Uint8Array, contentType?: string): Promise<string> {\n if (!this.inner.store) {\n throw new Error('BrowserBlobStorage requires an inner adapter that implements store().')\n }\n const key = await this.inner.store(data, contentType)\n this.remember(key, data, contentType)\n return key\n }\n\n async resolveUrl(key: string): Promise<string | undefined> {\n const cached = this.displayUrls.get(key)\n if (cached) return cached\n // Not minted through THIS wrapper — fall back to whatever the inner adapter\n // can resolve (still `undefined` when genuinely no bytes exist: scope §5,\n // an explicit missing state, never a silently broken image).\n return this.inner.resolveUrl ? this.inner.resolveUrl(key) : undefined\n }\n}\n\n/** Convenience factory — `createBrowserBlobStorage()` wraps a fresh `InMemoryBlobStorage`. */\nexport function createBrowserBlobStorage(options?: BrowserBlobStorageOptions): BlobStorageAdapter {\n return new BrowserBlobStorage(options)\n}\n"]}
@@ -9,3 +9,5 @@ export * from './thumbnail.js';
9
9
  export * from './backfill-thumbs.js';
10
10
  export * from './upload.js';
11
11
  export * from './delete-asset.js';
12
+ export * from './asset-url.js';
13
+ export * from './browser-blob-storage.js';
@@ -9,4 +9,6 @@ export * from './thumbnail.js';
9
9
  export * from './backfill-thumbs.js';
10
10
  export * from './upload.js';
11
11
  export * from './delete-asset.js';
12
+ export * from './asset-url.js';
13
+ export * from './browser-blob-storage.js';
12
14
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/assets/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA","sourcesContent":["/**\n * Asset upload, deletion, and usage logic (EC-151). Pure/injectable modules:\n * the studio UI consumes these; storage is reached only through the\n * `BlobStorageAdapter` boundary.\n */\nexport * from './upload-state'\nexport * from './image-dimensions'\nexport * from './thumbnail'\nexport * from './backfill-thumbs'\nexport * from './upload'\nexport * from './delete-asset'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/assets/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,mBAAmB,CAAA;AACjC,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA;AAC9B,cAAc,aAAa,CAAA;AAC3B,cAAc,wBAAwB,CAAA","sourcesContent":["/**\n * Asset upload, deletion, and usage logic (EC-151). Pure/injectable modules:\n * the studio UI consumes these; storage is reached only through the\n * `BlobStorageAdapter` boundary.\n */\nexport * from './upload-state'\nexport * from './image-dimensions'\nexport * from './thumbnail'\nexport * from './backfill-thumbs'\nexport * from './upload'\nexport * from './delete-asset'\nexport * from './asset-url'\nexport * from './browser-blob-storage'\n"]}
@@ -1,4 +1,4 @@
1
- import type { GetDocumentResult, ListDocumentsQuery, ListDocumentsResult, ResolvedDocument } from '@elytracms/core/content';
1
+ import type { ContentQueryError, GetDocumentResult, ListDocumentsQuery, ListDocumentsResult, ResolvedDocument } from '@elytracms/core/content';
2
2
  import type { CmsDocument, CollectionDef, DocumentRef, Locale, LocaleConfig, RouteRecord } from '@elytracms/core/cms-core';
3
3
  import type { AssetRecord } from '@elytracms/core/persistence';
4
4
  import type { ListingQuery } from '@elytracms/core/runtime-renderer';
@@ -72,6 +72,14 @@ export declare const playgroundCollections: readonly CollectionDef[];
72
72
  * published-only — this does not weaken `live-is-published-only`.
73
73
  */
74
74
  export type CanvasPerspective = 'draft' | 'published';
75
+ /** The current content-session epoch — bumps on every `configureContentSession`. */
76
+ export declare function contentSessionEpoch(): number;
77
+ /**
78
+ * Subscribe to content-session swaps (EC-460). The listener fires after every
79
+ * `configureContentSession`; returns the unsubscribe. Shaped for
80
+ * `useSyncExternalStore(subscribeContentSession, contentSessionEpoch)`.
81
+ */
82
+ export declare function subscribeContentSession(listener: () => void): () => void;
75
83
  /**
76
84
  * Point the preview engine at a project's hydrated content — Convex hydration
77
85
  * or the playground's in-memory persisted state (EC-361), both through the same
@@ -118,6 +126,17 @@ export declare function resolveCanvasRelation(ref: DocumentRef, locale?: Locale,
118
126
  * state. Reuses the same `listDocuments` path as `runCollectionListQuery`.
119
127
  */
120
128
  export declare function resolveCanvasListing(query: ListingQuery, locale?: Locale, perspective?: CanvasPerspective): ResolvedDocument[];
129
+ /**
130
+ * Statically validate a listing block's query against the ACTIVE session schema
131
+ * (EC-460, the never-silent rule): the exact `validateWhere`/`validateSort`/
132
+ * `validateLimit` checks `listDocuments` runs, WITHOUT executing the query — so
133
+ * the canvas can surface a config-invalid listing (a filter/sort field the
134
+ * collection doesn't declare `filterable`, an unknown collection) as a visible
135
+ * notice instead of the block's indistinguishable empty state. With no content
136
+ * session configured (boot instants, fixtures) there is no schema to judge
137
+ * against, so it reports nothing rather than false-flagging every listing.
138
+ */
139
+ export declare function validateCanvasListingQuery(query: ListingQuery): ContentQueryError[];
121
140
  /**
122
141
  * Resolve a rich-text internal link's target document to its URL path for the canvas
123
142
  * (EC-291) — the studio analog of the host's baked links map. Derives the URL through
@@ -1,4 +1,4 @@
1
- import { createContentClient, createContentContext, } from '@elytracms/core/content';
1
+ import { createContentClient, createContentContext, queryError, validateLimit, validateSort, validateWhere, } from '@elytracms/core/content';
2
2
  import { documentKey, resolveDocumentTitle, sampleLocaleConfig } from '@elytracms/core/cms-core';
3
3
  import { getStudioLocale } from '../locale/locale-store.js';
4
4
  /**
@@ -14,6 +14,26 @@ let contentSession = null;
14
14
  let lookup = createLookup(playgroundCollections, [], []);
15
15
  // Keyed by `${locale}:${perspective}` (EC-300) — one deterministic client per pair.
16
16
  const clients = new Map();
17
+ // EC-460: a monotonically increasing session epoch + listener set, so a REACT
18
+ // surface (the canvas render context) can re-render when the session is
19
+ // re-pointed at fresher content. The session itself stays a module global (the
20
+ // EC-361 design); this only makes "it changed" observable — `configureContentSession`
21
+ // remains the single setter.
22
+ let sessionEpoch = 0;
23
+ const sessionListeners = new Set();
24
+ /** The current content-session epoch — bumps on every `configureContentSession`. */
25
+ export function contentSessionEpoch() {
26
+ return sessionEpoch;
27
+ }
28
+ /**
29
+ * Subscribe to content-session swaps (EC-460). The listener fires after every
30
+ * `configureContentSession`; returns the unsubscribe. Shaped for
31
+ * `useSyncExternalStore(subscribeContentSession, contentSessionEpoch)`.
32
+ */
33
+ export function subscribeContentSession(listener) {
34
+ sessionListeners.add(listener);
35
+ return () => sessionListeners.delete(listener);
36
+ }
17
37
  /**
18
38
  * Point the preview engine at a project's hydrated content — Convex hydration
19
39
  * or the playground's in-memory persisted state (EC-361), both through the same
@@ -25,6 +45,9 @@ export function configureContentSession(next) {
25
45
  ? createLookup(next.collections, next.documents, next.publishedKeys, next.assets)
26
46
  : createLookup(playgroundCollections, [], []);
27
47
  clients.clear();
48
+ sessionEpoch += 1;
49
+ for (const listener of [...sessionListeners])
50
+ listener();
28
51
  }
29
52
  /** Whether a hydrated content session is configured (Convex mode). */
30
53
  export function hasContentSession() {
@@ -191,6 +214,33 @@ export function resolveCanvasListing(query, locale = getStudioLocale(), perspect
191
214
  });
192
215
  return result.ok ? result.documents : [];
193
216
  }
217
+ /**
218
+ * Statically validate a listing block's query against the ACTIVE session schema
219
+ * (EC-460, the never-silent rule): the exact `validateWhere`/`validateSort`/
220
+ * `validateLimit` checks `listDocuments` runs, WITHOUT executing the query — so
221
+ * the canvas can surface a config-invalid listing (a filter/sort field the
222
+ * collection doesn't declare `filterable`, an unknown collection) as a visible
223
+ * notice instead of the block's indistinguishable empty state. With no content
224
+ * session configured (boot instants, fixtures) there is no schema to judge
225
+ * against, so it reports nothing rather than false-flagging every listing.
226
+ */
227
+ export function validateCanvasListingQuery(query) {
228
+ if (contentSession === null)
229
+ return [];
230
+ const collection = playgroundCollection(query.collection);
231
+ if (!collection) {
232
+ return [
233
+ queryError('unknown-collection', `Collection "${query.collection}" is not defined.`, [
234
+ 'collection',
235
+ ]),
236
+ ];
237
+ }
238
+ return [
239
+ ...validateWhere(collection, query.where),
240
+ ...(query.sort ? validateSort(collection, query.sort) : []),
241
+ ...(query.limit !== undefined ? validateLimit(query.limit) : []),
242
+ ];
243
+ }
194
244
  /**
195
245
  * Resolve a rich-text internal link's target document to its URL path for the canvas
196
246
  * (EC-291) — the studio analog of the host's baked links map. Derives the URL through
@@ -1 +1 @@
1
- {"version":3,"file":"collection-content.js","sourceRoot":"","sources":["../../../src/lib/data/collection-content.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createContentClient,\n createContentContext,\n} from '@elytracms/core/content'\nimport type {\n ContentClient,\n ContentDocumentSource,\n ContentLookup,\n GetDocumentResult,\n ListDocumentsQuery,\n ListDocumentsResult,\n ResolvedDocument,\n} from '@elytracms/core/content'\nimport { documentKey, resolveDocumentTitle, sampleLocaleConfig } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CollectionDef,\n DocumentRef,\n Locale,\n LocaleConfig,\n RouteRecord,\n} from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport type { ListingQuery } from '@elytracms/core/runtime-renderer'\nimport { getStudioLocale } from '../locale/locale-store'\n\n/**\n * The studio's content delivery engine for canvas previews (EC-166).\n *\n * Repeating sections and template pages preview *delivery-shaped* documents —\n * the exact shape `@elytracms/core/content` serves in production — so the canvas\n * shows what publishing will show. This module wires the canonical content\n * client (closed AD-3 query vocabulary, published perspective, EC-018 locale\n * fallback, depth-1 population, resolved assets) over a **content session**:\n *\n * - Convex mode: the project-session gate (`components/studio/project-session`)\n * configures the session with the project's HYDRATED schema + documents, so\n * \"Preview with\" lists and section previews show the live content — never\n * fixture titles (EC-177 gap 1).\n * - Playground mode (EC-361): the SAME hydration loader runs over the seeded\n * in-memory adapter (through operations — AD-17), so canvas listing/relation\n * population resolves from the in-memory session documents with identical\n * semantics. With no session configured (tests, boot instants, no persisted\n * content) the engine serves nothing.\n *\n * Nothing here invents query semantics; both modes go through the same\n * canonical client.\n */\n\n/** The schema + documents the preview engine serves (one project session). */\nexport interface ContentSessionState {\n collections: readonly CollectionDef[]\n documents: readonly CmsDocument[]\n /**\n * Published document keys via the version POINTER (EC-224). When present, the\n * preview's published perspective resolves by the pin — never the flat\n * `state` enum — so it matches what delivery serves. Absent (playground\n * fixtures / tests) → the rows' own `state` gates the perspective, unchanged.\n */\n publishedKeys?: readonly string[]\n /** Locale config of the session content; defaults to the sample config. */\n localeConfig?: LocaleConfig\n /**\n * The project's config-sourced routes (EC-207). Wired into the content client so\n * the canvas preview's `resolveLink` (EC-291) derives a rich-text link's URL from\n * the SAME routes delivery serves — the studio-live half of the host link bake.\n * Absent (playground/tests) → no routes, so `resolveLink` returns null and a\n * reference link degrades to the visible `unresolved-ref` issue.\n */\n routes?: readonly RouteRecord[]\n /**\n * The project's asset records (assets table). Wired into the lookup so the\n * content client resolves `asset`-typed FIELDS on listing/relation results\n * (e.g. a ProductGrid product's `gallery`) to delivery shape — the same assets\n * the renderer's `resolveAsset` reads for static props (EC-262). Absent\n * (playground/tests) → assets come only from `asset`-collection fixture docs.\n */\n assets?: readonly AssetRecord[]\n}\n\n/**\n * Default (no-session) collection schema: EMPTY. The studio ships no demo content\n * (Oli, 2026-06-19) — with no backend/content session configured, there is nothing\n * to preview, and the studio says so rather than serving fixtures. A real project's\n * collections + documents arrive via {@link configureContentSession} in BOTH modes\n * (EC-361): Convex hydration, or the playground's seeded in-memory adapter through\n * the same loader.\n */\nexport const playgroundCollections: readonly CollectionDef[] = []\n\n/**\n * The perspective a canvas preview resolves references against (EC-300/AD-16). The\n * canvas is draft-coherent: the editable Draft view resolves references to their\n * `'draft'` targets (so a just-made reusable shows immediately), the read-only\n * Published view resolves `'published'` (an honest preview of what delivery serves;\n * an unpublished hard ref shows the in-canvas placeholder). Delivery stays\n * published-only — this does not weaken `live-is-published-only`.\n */\nexport type CanvasPerspective = 'draft' | 'published'\n\nlet contentSession: ContentSessionState | null = null\nlet lookup: ContentLookup = createLookup(playgroundCollections, [], [])\n// Keyed by `${locale}:${perspective}` (EC-300) — one deterministic client per pair.\nconst clients = new Map<string, ContentClient>()\n\n/**\n * Point the preview engine at a project's hydrated content — Convex hydration\n * or the playground's in-memory persisted state (EC-361), both through the same\n * `loadHydratedCms` loader — or back at the empty no-session default (`null`).\n */\nexport function configureContentSession(next: ContentSessionState | null): void {\n contentSession = next\n lookup = next\n ? createLookup(next.collections, next.documents, next.publishedKeys, next.assets)\n : createLookup(playgroundCollections, [], [])\n clients.clear()\n}\n\n/** Whether a hydrated content session is configured (Convex mode). */\nexport function hasContentSession(): boolean {\n return contentSession !== null\n}\n\n/** The collections the preview engine currently serves (session or fixtures). */\nfunction activeCollections(): readonly CollectionDef[] {\n return contentSession?.collections ?? playgroundCollections\n}\n\nfunction activeLocaleConfig(): LocaleConfig {\n return contentSession?.localeConfig ?? sampleLocaleConfig\n}\n\n/** Resolve a collection in the active content session (fixtures in playground). */\nexport function playgroundCollection(id: string): CollectionDef | undefined {\n return activeCollections().find((collection) => collection.id === id)\n}\n\n/**\n * Overlay the active session's canonical `filterable` declarations onto a\n * schema that doesn't carry any of its own. The inspector hands the Content\n * tab fixture collection defs; the filter/sort pickers must offer exactly the\n * EC-143 declaration set the delivery engine enforces, so both ends agree. A\n * collection that declares its own filterable fields is left untouched.\n */\nexport function withPlaygroundFilterable(\n collections: readonly CollectionDef[],\n): readonly CollectionDef[] {\n return collections.map((collection) => {\n if (collection.fields.some((field) => field.filterable === true)) return collection\n const counterpart = playgroundCollection(collection.id)\n if (counterpart === undefined) return collection\n const flagged = new Set(\n counterpart.fields.filter((field) => field.filterable === true).map((field) => field.name),\n )\n if (flagged.size === 0) return collection\n return {\n ...collection,\n fields: collection.fields.map((field) =>\n flagged.has(field.name) ? { ...field, filterable: true } : field,\n ),\n }\n })\n}\n\n/** Synthesize the persistence-shaped asset record the asset resolver reads. */\nfunction assetRecordOf(doc: CmsDocument): AssetRecord {\n const values = doc.values\n const alt = doc.localized?.en?.alt\n return {\n id: doc.id,\n projectId: 'playground',\n filename: typeof values.filename === 'string' ? values.filename : doc.id,\n contentType: 'image/png',\n byteSize: 0,\n alt: typeof alt === 'string' ? alt : undefined,\n storageKey: typeof values.url === 'string' ? values.url : doc.id,\n createdAt: '2026-01-01T00:00:00.000Z',\n updatedAt: '2026-01-01T00:00:00.000Z',\n }\n}\n\nfunction createLookup(\n collectionDefs: readonly CollectionDef[],\n documents: readonly CmsDocument[],\n publishedKeys?: readonly string[],\n assetRecords?: readonly AssetRecord[],\n): ContentLookup {\n const collections = new Map(collectionDefs.map((c) => [c.id, c]))\n // EC-224: published-ness is the version POINTER, not the flat `state` enum.\n // A hydrated session hands `publishedKeys` (the pinned set) → wrap each row as\n // a `DocumentHistory` so the published perspective resolves by the pin (the\n // live row is always a draft now) with ZERO dependence on `state`. Without a\n // pointer set (playground fixtures / tests) we keep bare rows, whose own\n // `state` gates the published perspective — byte-identical to before.\n const published = publishedKeys ? new Set(publishedKeys) : null\n const toSource = (doc: CmsDocument): ContentDocumentSource => {\n if (!published) return doc\n return published.has(documentKey({ collection: doc.collection, id: doc.id }))\n ? { draft: doc, published: doc, versions: [] }\n : { draft: doc, published: undefined, versions: [] }\n }\n const byKey = new Map(\n documents.map((doc) => [`${doc.collection}:${doc.id}`, toSource(doc)] as const),\n )\n // Asset resolution source for `asset`-typed FIELDS on resolved documents\n // (listing/relation results, e.g. a product's `gallery`). Two sources, real\n // records last so they win: (1) `asset`-collection fixture documents — how\n // playground/tests model assets; (2) the project's real asset-table records\n // (EC-262) — a Convex session has its assets here, NOT as `asset` documents,\n // so without these the content client resolves no nested image.\n const assets = new Map<string, AssetRecord>()\n for (const doc of documents) {\n if (doc.collection === 'asset') assets.set(doc.id, assetRecordOf(doc))\n }\n for (const record of assetRecords ?? []) assets.set(record.id, record)\n return {\n collection: (id) => collections.get(id),\n document: (ref) => byKey.get(`${ref.collection}:${ref.id}`),\n documents: (collectionId) =>\n documents.filter((doc) => doc.collection === collectionId).map(toSource),\n asset: (id) => assets.get(id),\n // Prefer the record's resolved servable `url` — the one the renderer's\n // resolveAsset reads (EC-262). Real asset-table records carry a Convex blob\n // locator in `storageKey` and the servable URL in `url`; fixture/`asset`-document\n // records (assetRecordOf) leave `url` unset and store the servable url in\n // `storageKey`. Falling back keeps both correct.\n assetUrl: (asset) => asset.url ?? asset.storageKey,\n }\n}\n\n/**\n * One deterministic client per (locale, perspective): EC-018 locale fallback,\n * draft-or-published perspective (EC-300/AD-16, default `'published'`). The studio's\n * ambient active locale (EC-176, `lib/locale`) picks the locale at query time, so the\n * canvas previews follow the locale switcher exactly like delivery would; the canvas's\n * Draft/Published view picks the perspective. The cache resets whenever the content\n * session changes.\n */\nfunction clientFor(locale: Locale, perspective: CanvasPerspective = 'published'): ContentClient {\n const key = `${locale}:${perspective}`\n const existing = clients.get(key)\n if (existing) return existing\n const client = createContentClient({\n lookup,\n context: createContentContext({\n locale,\n perspective,\n locales: activeLocaleConfig(),\n }),\n // EC-291: the routes a rich-text link resolves against (config-sourced) — the\n // studio-live half of the host link bake.\n routes: contentSession?.routes ?? [],\n })\n clients.set(key, client)\n return client\n}\n\n/**\n * Run a closed-vocabulary list query (AD-3: eq/in/lt/lte/gt/gte, AND-only,\n * filterable fields only) and return the delivery-shaped result. Invalid\n * queries come back as structured errors — never thrown, never silently\n * widened. Resolves in the studio's active locale unless one is passed.\n */\nexport function runCollectionListQuery(\n collectionId: string,\n query: ListDocumentsQuery = {},\n locale: Locale = getStudioLocale(),\n): ListDocumentsResult {\n return clientFor(locale).listDocuments(collectionId, query)\n}\n\n/** Resolve one document (by id or slug) into delivery shape. */\nexport function getCollectionDocument(\n collectionId: string,\n idOrSlug: string,\n locale: Locale = getStudioLocale(),\n): GetDocumentResult {\n return clientFor(locale).getDocument(collectionId, idOrSlug)\n}\n\n/**\n * Populate a composition-node `relation` prop to delivery shape for the canvas\n * (EC-254) — the studio analog of the host's baked relations map. Resolves the\n * ref through the active session's client (published perspective, active locale),\n * so the canvas previews a block's picked documents (a ProductGrid's products, a\n * Reusable's element) exactly as delivery renders them. `null` when the target is\n * missing or not visible → the block degrades to its own empty state.\n */\nexport function resolveCanvasRelation(\n ref: DocumentRef,\n locale: Locale = getStudioLocale(),\n perspective: CanvasPerspective = 'published',\n): ResolvedDocument | null {\n return clientFor(locale, perspective).resolveRelation(ref)\n}\n\n/**\n * Resolve a listing block's query to its documents for the canvas (EC-255) — the\n * studio analog of the host's baked listings map. Runs the query (collection +\n * AND-only filter, optional sort/limit) through the active session's client\n * (published perspective, active locale), so the canvas previews a dynamic grid (a\n * ProductGrid keyed to a category) exactly as delivery renders it. An invalid query\n * (structured, never thrown) or a non-list lookup yields `[]` → the block's empty\n * state. Reuses the same `listDocuments` path as `runCollectionListQuery`.\n */\nexport function resolveCanvasListing(\n query: ListingQuery,\n locale: Locale = getStudioLocale(),\n perspective: CanvasPerspective = 'published',\n): ResolvedDocument[] {\n const result = clientFor(locale, perspective).listDocuments(query.collection, {\n where: query.where,\n ...(query.sort ? { sort: query.sort } : {}),\n ...(query.limit !== undefined ? { limit: query.limit } : {}),\n })\n return result.ok ? result.documents : []\n}\n\n/**\n * Resolve a rich-text internal link's target document to its URL path for the canvas\n * (EC-291) — the studio analog of the host's baked links map. Derives the URL through\n * the active session's client (published perspective, active locale, config-sourced\n * routes), so the canvas previews a `{type:'reference'}` link's `href` exactly as\n * delivery serves it. `null` when the target is unpublished, missing, or addressed by\n * no route → the rich-text renderer shows the EC-290 `unresolved-ref` issue. Reuses\n * the same `resolveLink` path as the host bake, so both agree on the derived URL.\n */\nexport function resolveCanvasLink(\n ref: DocumentRef,\n locale: Locale = getStudioLocale(),\n perspective: CanvasPerspective = 'published',\n): string | null {\n return clientFor(locale, perspective).resolveLink(ref)\n}\n\n/** A pickable entry of a collection (the template-page preview switcher). */\nexport interface CollectionEntrySummary {\n id: string\n label: string\n}\n\n/** Published entries of a collection, labeled by the schema's title field. */\nexport function listCollectionEntries(collectionId: string): CollectionEntrySummary[] {\n const collection = playgroundCollection(collectionId)\n const result = runCollectionListQuery(collectionId)\n if (!result.ok) return []\n // EC-242: label via the shared identity resolver (string `titleField` or a\n // function pattern), over the delivery-shape values; falls back to the id.\n return result.documents.map((doc) => ({\n id: doc.id,\n label: collection ? resolveDocumentTitle(collection, doc, doc.id) : doc.id,\n }))\n}\n"]}
1
+ {"version":3,"file":"collection-content.js","sourceRoot":"","sources":["../../../src/lib/data/collection-content.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createContentClient,\n createContentContext,\n queryError,\n validateLimit,\n validateSort,\n validateWhere,\n} from '@elytracms/core/content'\nimport type {\n ContentClient,\n ContentDocumentSource,\n ContentLookup,\n ContentQueryError,\n GetDocumentResult,\n ListDocumentsQuery,\n ListDocumentsResult,\n ResolvedDocument,\n} from '@elytracms/core/content'\nimport { documentKey, resolveDocumentTitle, sampleLocaleConfig } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CollectionDef,\n DocumentRef,\n Locale,\n LocaleConfig,\n RouteRecord,\n} from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport type { ListingQuery } from '@elytracms/core/runtime-renderer'\nimport { getStudioLocale } from '../locale/locale-store'\n\n/**\n * The studio's content delivery engine for canvas previews (EC-166).\n *\n * Repeating sections and template pages preview *delivery-shaped* documents —\n * the exact shape `@elytracms/core/content` serves in production — so the canvas\n * shows what publishing will show. This module wires the canonical content\n * client (closed AD-3 query vocabulary, published perspective, EC-018 locale\n * fallback, depth-1 population, resolved assets) over a **content session**:\n *\n * - Convex mode: the project-session gate (`components/studio/project-session`)\n * configures the session with the project's HYDRATED schema + documents, so\n * \"Preview with\" lists and section previews show the live content — never\n * fixture titles (EC-177 gap 1).\n * - Playground mode (EC-361): the SAME hydration loader runs over the seeded\n * in-memory adapter (through operations — AD-17), so canvas listing/relation\n * population resolves from the in-memory session documents with identical\n * semantics. With no session configured (tests, boot instants, no persisted\n * content) the engine serves nothing.\n *\n * Nothing here invents query semantics; both modes go through the same\n * canonical client.\n */\n\n/** The schema + documents the preview engine serves (one project session). */\nexport interface ContentSessionState {\n collections: readonly CollectionDef[]\n documents: readonly CmsDocument[]\n /**\n * Published document keys via the version POINTER (EC-224). When present, the\n * preview's published perspective resolves by the pin — never the flat\n * `state` enum — so it matches what delivery serves. Absent (playground\n * fixtures / tests) → the rows' own `state` gates the perspective, unchanged.\n */\n publishedKeys?: readonly string[]\n /** Locale config of the session content; defaults to the sample config. */\n localeConfig?: LocaleConfig\n /**\n * The project's config-sourced routes (EC-207). Wired into the content client so\n * the canvas preview's `resolveLink` (EC-291) derives a rich-text link's URL from\n * the SAME routes delivery serves — the studio-live half of the host link bake.\n * Absent (playground/tests) → no routes, so `resolveLink` returns null and a\n * reference link degrades to the visible `unresolved-ref` issue.\n */\n routes?: readonly RouteRecord[]\n /**\n * The project's asset records (assets table). Wired into the lookup so the\n * content client resolves `asset`-typed FIELDS on listing/relation results\n * (e.g. a ProductGrid product's `gallery`) to delivery shape — the same assets\n * the renderer's `resolveAsset` reads for static props (EC-262). Absent\n * (playground/tests) → assets come only from `asset`-collection fixture docs.\n */\n assets?: readonly AssetRecord[]\n}\n\n/**\n * Default (no-session) collection schema: EMPTY. The studio ships no demo content\n * (Oli, 2026-06-19) — with no backend/content session configured, there is nothing\n * to preview, and the studio says so rather than serving fixtures. A real project's\n * collections + documents arrive via {@link configureContentSession} in BOTH modes\n * (EC-361): Convex hydration, or the playground's seeded in-memory adapter through\n * the same loader.\n */\nexport const playgroundCollections: readonly CollectionDef[] = []\n\n/**\n * The perspective a canvas preview resolves references against (EC-300/AD-16). The\n * canvas is draft-coherent: the editable Draft view resolves references to their\n * `'draft'` targets (so a just-made reusable shows immediately), the read-only\n * Published view resolves `'published'` (an honest preview of what delivery serves;\n * an unpublished hard ref shows the in-canvas placeholder). Delivery stays\n * published-only — this does not weaken `live-is-published-only`.\n */\nexport type CanvasPerspective = 'draft' | 'published'\n\nlet contentSession: ContentSessionState | null = null\nlet lookup: ContentLookup = createLookup(playgroundCollections, [], [])\n// Keyed by `${locale}:${perspective}` (EC-300) — one deterministic client per pair.\nconst clients = new Map<string, ContentClient>()\n\n// EC-460: a monotonically increasing session epoch + listener set, so a REACT\n// surface (the canvas render context) can re-render when the session is\n// re-pointed at fresher content. The session itself stays a module global (the\n// EC-361 design); this only makes \"it changed\" observable — `configureContentSession`\n// remains the single setter.\nlet sessionEpoch = 0\nconst sessionListeners = new Set<() => void>()\n\n/** The current content-session epoch — bumps on every `configureContentSession`. */\nexport function contentSessionEpoch(): number {\n return sessionEpoch\n}\n\n/**\n * Subscribe to content-session swaps (EC-460). The listener fires after every\n * `configureContentSession`; returns the unsubscribe. Shaped for\n * `useSyncExternalStore(subscribeContentSession, contentSessionEpoch)`.\n */\nexport function subscribeContentSession(listener: () => void): () => void {\n sessionListeners.add(listener)\n return () => sessionListeners.delete(listener)\n}\n\n/**\n * Point the preview engine at a project's hydrated content — Convex hydration\n * or the playground's in-memory persisted state (EC-361), both through the same\n * `loadHydratedCms` loader — or back at the empty no-session default (`null`).\n */\nexport function configureContentSession(next: ContentSessionState | null): void {\n contentSession = next\n lookup = next\n ? createLookup(next.collections, next.documents, next.publishedKeys, next.assets)\n : createLookup(playgroundCollections, [], [])\n clients.clear()\n sessionEpoch += 1\n for (const listener of [...sessionListeners]) listener()\n}\n\n/** Whether a hydrated content session is configured (Convex mode). */\nexport function hasContentSession(): boolean {\n return contentSession !== null\n}\n\n/** The collections the preview engine currently serves (session or fixtures). */\nfunction activeCollections(): readonly CollectionDef[] {\n return contentSession?.collections ?? playgroundCollections\n}\n\nfunction activeLocaleConfig(): LocaleConfig {\n return contentSession?.localeConfig ?? sampleLocaleConfig\n}\n\n/** Resolve a collection in the active content session (fixtures in playground). */\nexport function playgroundCollection(id: string): CollectionDef | undefined {\n return activeCollections().find((collection) => collection.id === id)\n}\n\n/**\n * Overlay the active session's canonical `filterable` declarations onto a\n * schema that doesn't carry any of its own. The inspector hands the Content\n * tab fixture collection defs; the filter/sort pickers must offer exactly the\n * EC-143 declaration set the delivery engine enforces, so both ends agree. A\n * collection that declares its own filterable fields is left untouched.\n */\nexport function withPlaygroundFilterable(\n collections: readonly CollectionDef[],\n): readonly CollectionDef[] {\n return collections.map((collection) => {\n if (collection.fields.some((field) => field.filterable === true)) return collection\n const counterpart = playgroundCollection(collection.id)\n if (counterpart === undefined) return collection\n const flagged = new Set(\n counterpart.fields.filter((field) => field.filterable === true).map((field) => field.name),\n )\n if (flagged.size === 0) return collection\n return {\n ...collection,\n fields: collection.fields.map((field) =>\n flagged.has(field.name) ? { ...field, filterable: true } : field,\n ),\n }\n })\n}\n\n/** Synthesize the persistence-shaped asset record the asset resolver reads. */\nfunction assetRecordOf(doc: CmsDocument): AssetRecord {\n const values = doc.values\n const alt = doc.localized?.en?.alt\n return {\n id: doc.id,\n projectId: 'playground',\n filename: typeof values.filename === 'string' ? values.filename : doc.id,\n contentType: 'image/png',\n byteSize: 0,\n alt: typeof alt === 'string' ? alt : undefined,\n storageKey: typeof values.url === 'string' ? values.url : doc.id,\n createdAt: '2026-01-01T00:00:00.000Z',\n updatedAt: '2026-01-01T00:00:00.000Z',\n }\n}\n\nfunction createLookup(\n collectionDefs: readonly CollectionDef[],\n documents: readonly CmsDocument[],\n publishedKeys?: readonly string[],\n assetRecords?: readonly AssetRecord[],\n): ContentLookup {\n const collections = new Map(collectionDefs.map((c) => [c.id, c]))\n // EC-224: published-ness is the version POINTER, not the flat `state` enum.\n // A hydrated session hands `publishedKeys` (the pinned set) → wrap each row as\n // a `DocumentHistory` so the published perspective resolves by the pin (the\n // live row is always a draft now) with ZERO dependence on `state`. Without a\n // pointer set (playground fixtures / tests) we keep bare rows, whose own\n // `state` gates the published perspective — byte-identical to before.\n const published = publishedKeys ? new Set(publishedKeys) : null\n const toSource = (doc: CmsDocument): ContentDocumentSource => {\n if (!published) return doc\n return published.has(documentKey({ collection: doc.collection, id: doc.id }))\n ? { draft: doc, published: doc, versions: [] }\n : { draft: doc, published: undefined, versions: [] }\n }\n const byKey = new Map(\n documents.map((doc) => [`${doc.collection}:${doc.id}`, toSource(doc)] as const),\n )\n // Asset resolution source for `asset`-typed FIELDS on resolved documents\n // (listing/relation results, e.g. a product's `gallery`). Two sources, real\n // records last so they win: (1) `asset`-collection fixture documents — how\n // playground/tests model assets; (2) the project's real asset-table records\n // (EC-262) — a Convex session has its assets here, NOT as `asset` documents,\n // so without these the content client resolves no nested image.\n const assets = new Map<string, AssetRecord>()\n for (const doc of documents) {\n if (doc.collection === 'asset') assets.set(doc.id, assetRecordOf(doc))\n }\n for (const record of assetRecords ?? []) assets.set(record.id, record)\n return {\n collection: (id) => collections.get(id),\n document: (ref) => byKey.get(`${ref.collection}:${ref.id}`),\n documents: (collectionId) =>\n documents.filter((doc) => doc.collection === collectionId).map(toSource),\n asset: (id) => assets.get(id),\n // Prefer the record's resolved servable `url` — the one the renderer's\n // resolveAsset reads (EC-262). Real asset-table records carry a Convex blob\n // locator in `storageKey` and the servable URL in `url`; fixture/`asset`-document\n // records (assetRecordOf) leave `url` unset and store the servable url in\n // `storageKey`. Falling back keeps both correct.\n assetUrl: (asset) => asset.url ?? asset.storageKey,\n }\n}\n\n/**\n * One deterministic client per (locale, perspective): EC-018 locale fallback,\n * draft-or-published perspective (EC-300/AD-16, default `'published'`). The studio's\n * ambient active locale (EC-176, `lib/locale`) picks the locale at query time, so the\n * canvas previews follow the locale switcher exactly like delivery would; the canvas's\n * Draft/Published view picks the perspective. The cache resets whenever the content\n * session changes.\n */\nfunction clientFor(locale: Locale, perspective: CanvasPerspective = 'published'): ContentClient {\n const key = `${locale}:${perspective}`\n const existing = clients.get(key)\n if (existing) return existing\n const client = createContentClient({\n lookup,\n context: createContentContext({\n locale,\n perspective,\n locales: activeLocaleConfig(),\n }),\n // EC-291: the routes a rich-text link resolves against (config-sourced) — the\n // studio-live half of the host link bake.\n routes: contentSession?.routes ?? [],\n })\n clients.set(key, client)\n return client\n}\n\n/**\n * Run a closed-vocabulary list query (AD-3: eq/in/lt/lte/gt/gte, AND-only,\n * filterable fields only) and return the delivery-shaped result. Invalid\n * queries come back as structured errors — never thrown, never silently\n * widened. Resolves in the studio's active locale unless one is passed.\n */\nexport function runCollectionListQuery(\n collectionId: string,\n query: ListDocumentsQuery = {},\n locale: Locale = getStudioLocale(),\n): ListDocumentsResult {\n return clientFor(locale).listDocuments(collectionId, query)\n}\n\n/** Resolve one document (by id or slug) into delivery shape. */\nexport function getCollectionDocument(\n collectionId: string,\n idOrSlug: string,\n locale: Locale = getStudioLocale(),\n): GetDocumentResult {\n return clientFor(locale).getDocument(collectionId, idOrSlug)\n}\n\n/**\n * Populate a composition-node `relation` prop to delivery shape for the canvas\n * (EC-254) — the studio analog of the host's baked relations map. Resolves the\n * ref through the active session's client (published perspective, active locale),\n * so the canvas previews a block's picked documents (a ProductGrid's products, a\n * Reusable's element) exactly as delivery renders them. `null` when the target is\n * missing or not visible → the block degrades to its own empty state.\n */\nexport function resolveCanvasRelation(\n ref: DocumentRef,\n locale: Locale = getStudioLocale(),\n perspective: CanvasPerspective = 'published',\n): ResolvedDocument | null {\n return clientFor(locale, perspective).resolveRelation(ref)\n}\n\n/**\n * Resolve a listing block's query to its documents for the canvas (EC-255) — the\n * studio analog of the host's baked listings map. Runs the query (collection +\n * AND-only filter, optional sort/limit) through the active session's client\n * (published perspective, active locale), so the canvas previews a dynamic grid (a\n * ProductGrid keyed to a category) exactly as delivery renders it. An invalid query\n * (structured, never thrown) or a non-list lookup yields `[]` → the block's empty\n * state. Reuses the same `listDocuments` path as `runCollectionListQuery`.\n */\nexport function resolveCanvasListing(\n query: ListingQuery,\n locale: Locale = getStudioLocale(),\n perspective: CanvasPerspective = 'published',\n): ResolvedDocument[] {\n const result = clientFor(locale, perspective).listDocuments(query.collection, {\n where: query.where,\n ...(query.sort ? { sort: query.sort } : {}),\n ...(query.limit !== undefined ? { limit: query.limit } : {}),\n })\n return result.ok ? result.documents : []\n}\n\n/**\n * Statically validate a listing block's query against the ACTIVE session schema\n * (EC-460, the never-silent rule): the exact `validateWhere`/`validateSort`/\n * `validateLimit` checks `listDocuments` runs, WITHOUT executing the query — so\n * the canvas can surface a config-invalid listing (a filter/sort field the\n * collection doesn't declare `filterable`, an unknown collection) as a visible\n * notice instead of the block's indistinguishable empty state. With no content\n * session configured (boot instants, fixtures) there is no schema to judge\n * against, so it reports nothing rather than false-flagging every listing.\n */\nexport function validateCanvasListingQuery(query: ListingQuery): ContentQueryError[] {\n if (contentSession === null) return []\n const collection = playgroundCollection(query.collection)\n if (!collection) {\n return [\n queryError('unknown-collection', `Collection \"${query.collection}\" is not defined.`, [\n 'collection',\n ]),\n ]\n }\n return [\n ...validateWhere(collection, query.where),\n ...(query.sort ? validateSort(collection, query.sort) : []),\n ...(query.limit !== undefined ? validateLimit(query.limit) : []),\n ]\n}\n\n/**\n * Resolve a rich-text internal link's target document to its URL path for the canvas\n * (EC-291) — the studio analog of the host's baked links map. Derives the URL through\n * the active session's client (published perspective, active locale, config-sourced\n * routes), so the canvas previews a `{type:'reference'}` link's `href` exactly as\n * delivery serves it. `null` when the target is unpublished, missing, or addressed by\n * no route → the rich-text renderer shows the EC-290 `unresolved-ref` issue. Reuses\n * the same `resolveLink` path as the host bake, so both agree on the derived URL.\n */\nexport function resolveCanvasLink(\n ref: DocumentRef,\n locale: Locale = getStudioLocale(),\n perspective: CanvasPerspective = 'published',\n): string | null {\n return clientFor(locale, perspective).resolveLink(ref)\n}\n\n/** A pickable entry of a collection (the template-page preview switcher). */\nexport interface CollectionEntrySummary {\n id: string\n label: string\n}\n\n/** Published entries of a collection, labeled by the schema's title field. */\nexport function listCollectionEntries(collectionId: string): CollectionEntrySummary[] {\n const collection = playgroundCollection(collectionId)\n const result = runCollectionListQuery(collectionId)\n if (!result.ok) return []\n // EC-242: label via the shared identity resolver (string `titleField` or a\n // function pattern), over the delivery-shape values; falls back to the id.\n return result.documents.map((doc) => ({\n id: doc.id,\n label: collection ? resolveDocumentTitle(collection, doc, doc.id) : doc.id,\n }))\n}\n"]}
@@ -0,0 +1,74 @@
1
+ import type { OperationEvent } from '@elytracms/studio/lib/operations';
2
+ /**
3
+ * Content-session write-sync (EC-460) — the root-cause fix for the playground
4
+ * population family.
5
+ *
6
+ * The canvas preview engine (`collection-content.ts`) resolves listings,
7
+ * relation picks, and rich-text links against a module-global content session
8
+ * that used to refresh ONLY at project boot and per CMS-surface mount
9
+ * (`project-session.tsx`). A document authored or published after the last of
10
+ * those hydrations was invisible to every canvas resolver while the picker UIs
11
+ * (which read the live CMS store) showed it — the listing rendered its empty
12
+ * state against real published same-session content, and a `multiple` relation
13
+ * resolved 1-of-3 / 0-of-3 picked chips, all silently.
14
+ *
15
+ * This module closes the loop at the ONE write chokepoint the studio already
16
+ * has: the operations lifecycle event bus ("validate → authorize → mutate →
17
+ * emit"; its documented consumers include cache invalidation, vision AD-7 —
18
+ * the reference index and revalidation webhooks attach the same way). Every
19
+ * SUCCEEDED command in a content-bearing domain marks the session stale; a
20
+ * registered refresher (the project session's own `loadCms`, so both modes run
21
+ * the identical `loadHydratedCms` → `configureContentSession` path — no new
22
+ * loader, no transport bypass, no second cache) re-hydrates it, debounced so a
23
+ * persister burst coalesces into one load.
24
+ *
25
+ * Refreshes only run while a CONSUMER (a mounted canvas) is registered — a
26
+ * session with no preview surface just keeps the stale flag and re-hydrates
27
+ * when the next consumer arrives, so connected mode never pays a per-autosave
28
+ * query while nobody is previewing. The pure orchestrator
29
+ * ({@link createContentSessionSync}) takes its timer + refresh injected, so the
30
+ * coalescing/latch behaviour is unit-tested in node (house pattern); the
31
+ * module-level singleton below is the app wiring.
32
+ */
33
+ /** Domains whose succeeded commands change what the preview engine serves. */
34
+ export declare const CONTENT_SESSION_WRITE_DOMAINS: ReadonlySet<string>;
35
+ /** Whether an operation event invalidates the preview content session. */
36
+ export declare function isContentSessionWrite(event: OperationEvent): boolean;
37
+ export interface ContentSessionSyncDeps {
38
+ /** Re-hydrate the session (the project session's `loadCms`). Never throws upward. */
39
+ refresh: () => Promise<unknown>;
40
+ /** Trailing debounce for write bursts (ms). */
41
+ delayMs?: number;
42
+ /** Injectable timer pair (tests drive time manually). */
43
+ setTimer?: (fn: () => void, ms: number) => unknown;
44
+ clearTimer?: (handle: unknown) => void;
45
+ }
46
+ export interface ContentSessionSync {
47
+ /** A content-bearing write succeeded — refresh (debounced) if anyone previews. */
48
+ markStale(): void;
49
+ /** A preview surface mounted; returns its release. A stale session refreshes now. */
50
+ addConsumer(): () => void;
51
+ /** Whether a refresh is currently owed (test/introspection seam). */
52
+ isStale(): boolean;
53
+ }
54
+ /**
55
+ * The pure orchestrator: stale flag + consumer count + one coalesced refresh.
56
+ *
57
+ * - `markStale` with no consumers only flags; the next `addConsumer` refreshes.
58
+ * - `markStale` with consumers schedules a trailing debounce (bursts coalesce).
59
+ * - A `markStale` DURING an in-flight refresh re-runs once after it settles —
60
+ * the refresh that was already reading may have missed that write.
61
+ */
62
+ export declare function createContentSessionSync(deps: ContentSessionSyncDeps): ContentSessionSync;
63
+ type Refresher = () => Promise<unknown>;
64
+ /**
65
+ * Register the active project's session refresher (`ProjectSessionProvider`
66
+ * mounts it with its `loadCms`; leaving the project unregisters). Only one is
67
+ * active at a time — the studio shows one project per session.
68
+ */
69
+ export declare function registerContentSessionRefresher(refresher: Refresher): () => void;
70
+ /** Mark the shared session stale (the event-bus listener calls this). */
71
+ export declare function markContentSessionStale(): void;
72
+ /** Register a mounted preview consumer (the canvas); returns the release. */
73
+ export declare function acquireContentSessionConsumer(): () => void;
74
+ export {};
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Content-session write-sync (EC-460) — the root-cause fix for the playground
3
+ * population family.
4
+ *
5
+ * The canvas preview engine (`collection-content.ts`) resolves listings,
6
+ * relation picks, and rich-text links against a module-global content session
7
+ * that used to refresh ONLY at project boot and per CMS-surface mount
8
+ * (`project-session.tsx`). A document authored or published after the last of
9
+ * those hydrations was invisible to every canvas resolver while the picker UIs
10
+ * (which read the live CMS store) showed it — the listing rendered its empty
11
+ * state against real published same-session content, and a `multiple` relation
12
+ * resolved 1-of-3 / 0-of-3 picked chips, all silently.
13
+ *
14
+ * This module closes the loop at the ONE write chokepoint the studio already
15
+ * has: the operations lifecycle event bus ("validate → authorize → mutate →
16
+ * emit"; its documented consumers include cache invalidation, vision AD-7 —
17
+ * the reference index and revalidation webhooks attach the same way). Every
18
+ * SUCCEEDED command in a content-bearing domain marks the session stale; a
19
+ * registered refresher (the project session's own `loadCms`, so both modes run
20
+ * the identical `loadHydratedCms` → `configureContentSession` path — no new
21
+ * loader, no transport bypass, no second cache) re-hydrates it, debounced so a
22
+ * persister burst coalesces into one load.
23
+ *
24
+ * Refreshes only run while a CONSUMER (a mounted canvas) is registered — a
25
+ * session with no preview surface just keeps the stale flag and re-hydrates
26
+ * when the next consumer arrives, so connected mode never pays a per-autosave
27
+ * query while nobody is previewing. The pure orchestrator
28
+ * ({@link createContentSessionSync}) takes its timer + refresh injected, so the
29
+ * coalescing/latch behaviour is unit-tested in node (house pattern); the
30
+ * module-level singleton below is the app wiring.
31
+ */
32
+ /** Domains whose succeeded commands change what the preview engine serves. */
33
+ export const CONTENT_SESSION_WRITE_DOMAINS = new Set([
34
+ 'documents',
35
+ 'schema',
36
+ 'assets',
37
+ ]);
38
+ /** Whether an operation event invalidates the preview content session. */
39
+ export function isContentSessionWrite(event) {
40
+ return (event.kind === 'command' &&
41
+ event.status === 'succeeded' &&
42
+ CONTENT_SESSION_WRITE_DOMAINS.has(event.domain));
43
+ }
44
+ /**
45
+ * The pure orchestrator: stale flag + consumer count + one coalesced refresh.
46
+ *
47
+ * - `markStale` with no consumers only flags; the next `addConsumer` refreshes.
48
+ * - `markStale` with consumers schedules a trailing debounce (bursts coalesce).
49
+ * - A `markStale` DURING an in-flight refresh re-runs once after it settles —
50
+ * the refresh that was already reading may have missed that write.
51
+ */
52
+ export function createContentSessionSync(deps) {
53
+ const delayMs = deps.delayMs ?? 150;
54
+ const setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
55
+ const clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
56
+ let stale = false;
57
+ let consumers = 0;
58
+ let timer = null;
59
+ let running = false;
60
+ let rerun = false;
61
+ function run() {
62
+ if (running) {
63
+ rerun = true;
64
+ return;
65
+ }
66
+ running = true;
67
+ stale = false;
68
+ void deps
69
+ .refresh()
70
+ .catch(() => {
71
+ // A failed hydration keeps the session stale so the next trigger retries;
72
+ // the loader itself already reports the failure loudly (loadCmsIntoSession).
73
+ stale = true;
74
+ })
75
+ .finally(() => {
76
+ running = false;
77
+ if (rerun || (stale && consumers > 0)) {
78
+ rerun = false;
79
+ schedule();
80
+ }
81
+ });
82
+ }
83
+ function schedule() {
84
+ if (timer !== null)
85
+ clearTimer(timer);
86
+ timer = setTimer(() => {
87
+ timer = null;
88
+ run();
89
+ }, delayMs);
90
+ }
91
+ return {
92
+ markStale() {
93
+ stale = true;
94
+ if (consumers > 0)
95
+ schedule();
96
+ },
97
+ addConsumer() {
98
+ consumers += 1;
99
+ if (stale && timer === null && !running)
100
+ schedule();
101
+ let released = false;
102
+ return () => {
103
+ if (released)
104
+ return;
105
+ released = true;
106
+ consumers -= 1;
107
+ };
108
+ },
109
+ isStale() {
110
+ return stale;
111
+ },
112
+ };
113
+ }
114
+ let currentRefresher = null;
115
+ const sync = createContentSessionSync({
116
+ refresh: () => currentRefresher?.() ?? Promise.resolve(),
117
+ });
118
+ /**
119
+ * Register the active project's session refresher (`ProjectSessionProvider`
120
+ * mounts it with its `loadCms`; leaving the project unregisters). Only one is
121
+ * active at a time — the studio shows one project per session.
122
+ */
123
+ export function registerContentSessionRefresher(refresher) {
124
+ currentRefresher = refresher;
125
+ return () => {
126
+ if (currentRefresher === refresher)
127
+ currentRefresher = null;
128
+ };
129
+ }
130
+ /** Mark the shared session stale (the event-bus listener calls this). */
131
+ export function markContentSessionStale() {
132
+ sync.markStale();
133
+ }
134
+ /** Register a mounted preview consumer (the canvas); returns the release. */
135
+ export function acquireContentSessionConsumer() {
136
+ return sync.addConsumer();
137
+ }
138
+ //# sourceMappingURL=content-session-refresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-session-refresh.js","sourceRoot":"","sources":["../../../src/lib/data/content-session-refresh.ts"],"names":[],"mappings":"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","sourcesContent":["import type { OperationEvent } from '@elytracms/studio/lib/operations'\n\n/**\n * Content-session write-sync (EC-460) — the root-cause fix for the playground\n * population family.\n *\n * The canvas preview engine (`collection-content.ts`) resolves listings,\n * relation picks, and rich-text links against a module-global content session\n * that used to refresh ONLY at project boot and per CMS-surface mount\n * (`project-session.tsx`). A document authored or published after the last of\n * those hydrations was invisible to every canvas resolver while the picker UIs\n * (which read the live CMS store) showed it — the listing rendered its empty\n * state against real published same-session content, and a `multiple` relation\n * resolved 1-of-3 / 0-of-3 picked chips, all silently.\n *\n * This module closes the loop at the ONE write chokepoint the studio already\n * has: the operations lifecycle event bus (\"validate → authorize → mutate →\n * emit\"; its documented consumers include cache invalidation, vision AD-7 —\n * the reference index and revalidation webhooks attach the same way). Every\n * SUCCEEDED command in a content-bearing domain marks the session stale; a\n * registered refresher (the project session's own `loadCms`, so both modes run\n * the identical `loadHydratedCms` → `configureContentSession` path — no new\n * loader, no transport bypass, no second cache) re-hydrates it, debounced so a\n * persister burst coalesces into one load.\n *\n * Refreshes only run while a CONSUMER (a mounted canvas) is registered — a\n * session with no preview surface just keeps the stale flag and re-hydrates\n * when the next consumer arrives, so connected mode never pays a per-autosave\n * query while nobody is previewing. The pure orchestrator\n * ({@link createContentSessionSync}) takes its timer + refresh injected, so the\n * coalescing/latch behaviour is unit-tested in node (house pattern); the\n * module-level singleton below is the app wiring.\n */\n\n/** Domains whose succeeded commands change what the preview engine serves. */\nexport const CONTENT_SESSION_WRITE_DOMAINS: ReadonlySet<string> = new Set([\n 'documents',\n 'schema',\n 'assets',\n])\n\n/** Whether an operation event invalidates the preview content session. */\nexport function isContentSessionWrite(event: OperationEvent): boolean {\n return (\n event.kind === 'command' &&\n event.status === 'succeeded' &&\n CONTENT_SESSION_WRITE_DOMAINS.has(event.domain)\n )\n}\n\nexport interface ContentSessionSyncDeps {\n /** Re-hydrate the session (the project session's `loadCms`). Never throws upward. */\n refresh: () => Promise<unknown>\n /** Trailing debounce for write bursts (ms). */\n delayMs?: number\n /** Injectable timer pair (tests drive time manually). */\n setTimer?: (fn: () => void, ms: number) => unknown\n clearTimer?: (handle: unknown) => void\n}\n\nexport interface ContentSessionSync {\n /** A content-bearing write succeeded — refresh (debounced) if anyone previews. */\n markStale(): void\n /** A preview surface mounted; returns its release. A stale session refreshes now. */\n addConsumer(): () => void\n /** Whether a refresh is currently owed (test/introspection seam). */\n isStale(): boolean\n}\n\n/**\n * The pure orchestrator: stale flag + consumer count + one coalesced refresh.\n *\n * - `markStale` with no consumers only flags; the next `addConsumer` refreshes.\n * - `markStale` with consumers schedules a trailing debounce (bursts coalesce).\n * - A `markStale` DURING an in-flight refresh re-runs once after it settles —\n * the refresh that was already reading may have missed that write.\n */\nexport function createContentSessionSync(deps: ContentSessionSyncDeps): ContentSessionSync {\n const delayMs = deps.delayMs ?? 150\n const setTimer = deps.setTimer ?? ((fn: () => void, ms: number) => setTimeout(fn, ms))\n const clearTimer = deps.clearTimer ?? ((handle: unknown) => clearTimeout(handle as number))\n\n let stale = false\n let consumers = 0\n let timer: unknown = null\n let running = false\n let rerun = false\n\n function run(): void {\n if (running) {\n rerun = true\n return\n }\n running = true\n stale = false\n void deps\n .refresh()\n .catch(() => {\n // A failed hydration keeps the session stale so the next trigger retries;\n // the loader itself already reports the failure loudly (loadCmsIntoSession).\n stale = true\n })\n .finally(() => {\n running = false\n if (rerun || (stale && consumers > 0)) {\n rerun = false\n schedule()\n }\n })\n }\n\n function schedule(): void {\n if (timer !== null) clearTimer(timer)\n timer = setTimer(() => {\n timer = null\n run()\n }, delayMs)\n }\n\n return {\n markStale() {\n stale = true\n if (consumers > 0) schedule()\n },\n addConsumer() {\n consumers += 1\n if (stale && timer === null && !running) schedule()\n let released = false\n return () => {\n if (released) return\n released = true\n consumers -= 1\n }\n },\n isStale() {\n return stale\n },\n }\n}\n\n// --- App wiring (module singleton) -------------------------------------------------\n\ntype Refresher = () => Promise<unknown>\n\nlet currentRefresher: Refresher | null = null\n\nconst sync = createContentSessionSync({\n refresh: () => currentRefresher?.() ?? Promise.resolve(),\n})\n\n/**\n * Register the active project's session refresher (`ProjectSessionProvider`\n * mounts it with its `loadCms`; leaving the project unregisters). Only one is\n * active at a time — the studio shows one project per session.\n */\nexport function registerContentSessionRefresher(refresher: Refresher): () => void {\n currentRefresher = refresher\n return () => {\n if (currentRefresher === refresher) currentRefresher = null\n }\n}\n\n/** Mark the shared session stale (the event-bus listener calls this). */\nexport function markContentSessionStale(): void {\n sync.markStale()\n}\n\n/** Register a mounted preview consumer (the canvas); returns the release. */\nexport function acquireContentSessionConsumer(): () => void {\n return sync.addConsumer()\n}\n"]}
@@ -2,3 +2,5 @@ export * from './data-store.js';
2
2
  export * from './sample-capture.js';
3
3
  export * from './sources-store.js';
4
4
  export * from './collection-content.js';
5
+ export * from './content-session-refresh.js';
6
+ export * from './playground-unload-guard.js';
@@ -2,6 +2,8 @@ export * from './data-store.js';
2
2
  export * from './sample-capture.js';
3
3
  export * from './sources-store.js';
4
4
  export * from './collection-content.js';
5
+ export * from './content-session-refresh.js';
6
+ export * from './playground-unload-guard.js';
5
7
  // Data providers (EC-167) are imported from './providers' directly: the module
6
8
  // exports a `DataProvider` domain type that would collide with the store's
7
9
  // `<DataProvider>` React provider in a wildcard barrel.
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/data/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,+EAA+E;AAC/E,2EAA2E;AAC3E,wDAAwD","sourcesContent":["export * from './data-store'\nexport * from './sample-capture'\nexport * from './sources-store'\nexport * from './collection-content'\n// Data providers (EC-167) are imported from './providers' directly: the module\n// exports a `DataProvider` domain type that would collide with the store's\n// `<DataProvider>` React provider in a wildcard barrel.\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/data/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,2BAA2B,CAAA;AACzC,cAAc,2BAA2B,CAAA;AACzC,+EAA+E;AAC/E,2EAA2E;AAC3E,wDAAwD","sourcesContent":["export * from './data-store'\nexport * from './sample-capture'\nexport * from './sources-store'\nexport * from './collection-content'\nexport * from './content-session-refresh'\nexport * from './playground-unload-guard'\n// Data providers (EC-167) are imported from './providers' directly: the module\n// exports a `DataProvider` domain type that would collide with the store's\n// `<DataProvider>` React provider in a wildcard barrel.\n"]}