@elytracms/studio 0.0.78 → 0.0.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/app-views/default-modules.js +5 -1
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/assets/asset-thumb.js +12 -11
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/builder/blocks-panel.d.ts +11 -5
- package/dist/components/builder/blocks-panel.js +18 -24
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +22 -0
- package/dist/components/builder/canvas-render-context.js +65 -9
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +25 -0
- package/dist/components/builder/reuse.js +11 -2
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/asset-field.js +52 -19
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker-core.d.ts +40 -0
- package/dist/components/cms/asset-picker-core.js +21 -0
- package/dist/components/cms/asset-picker-core.js.map +1 -1
- package/dist/components/cms/asset-picker.d.ts +11 -2
- package/dist/components/cms/asset-picker.js +34 -13
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/cell.js +2 -1
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +8 -1
- package/dist/components/cms/cms-store.js +8 -7
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +7 -1
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/date-input-core.d.ts +85 -0
- package/dist/components/cms/date-input-core.js +98 -0
- package/dist/components/cms/date-input-core.js.map +1 -0
- package/dist/components/cms/date-input.d.ts +51 -0
- package/dist/components/cms/date-input.js +109 -0
- package/dist/components/cms/date-input.js.map +1 -0
- package/dist/components/cms/document-diff-view.js +2 -1
- package/dist/components/cms/document-diff-view.js.map +1 -1
- package/dist/components/cms/document-staging.d.ts +15 -4
- package/dist/components/cms/document-staging.js +26 -15
- package/dist/components/cms/document-staging.js.map +1 -1
- package/dist/components/cms/editable-cell.js +40 -5
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/field-control.js +39 -7
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/scalar-value-editor.js +5 -1
- package/dist/components/cms/scalar-value-editor.js.map +1 -1
- package/dist/components/cms/slug-auto-derive.d.ts +41 -0
- package/dist/components/cms/slug-auto-derive.js +14 -0
- package/dist/components/cms/slug-auto-derive.js.map +1 -0
- package/dist/components/components/transfer-dialog.js +3 -1
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/project-session.d.ts +44 -22
- package/dist/components/project-session.js +72 -36
- package/dist/components/project-session.js.map +1 -1
- package/dist/lib/assets/asset-url.d.ts +20 -0
- package/dist/lib/assets/asset-url.js +25 -0
- package/dist/lib/assets/asset-url.js.map +1 -0
- package/dist/lib/assets/browser-blob-storage.d.ts +80 -0
- package/dist/lib/assets/browser-blob-storage.js +86 -0
- package/dist/lib/assets/browser-blob-storage.js.map +1 -0
- package/dist/lib/assets/index.d.ts +2 -0
- package/dist/lib/assets/index.js +2 -0
- package/dist/lib/assets/index.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +20 -1
- package/dist/lib/data/collection-content.js +51 -1
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/data/content-session-refresh.d.ts +74 -0
- package/dist/lib/data/content-session-refresh.js +138 -0
- package/dist/lib/data/content-session-refresh.js.map +1 -0
- package/dist/lib/data/index.d.ts +2 -0
- package/dist/lib/data/index.js +2 -0
- package/dist/lib/data/index.js.map +1 -1
- package/dist/lib/data/playground-unload-guard.d.ts +22 -0
- package/dist/lib/data/playground-unload-guard.js +73 -0
- package/dist/lib/data/playground-unload-guard.js.map +1 -0
- package/dist/lib/i18n/messages.de.js +3 -1
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +2 -0
- package/dist/lib/i18n/messages.en.js +5 -1
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/persistence/index.js +6 -1
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/ui/input.d.ts +8 -3
- package/dist/ui/input.js +6 -1
- package/dist/ui/input.js.map +1 -1
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useCallback, useEffect } from 'react';
|
|
3
3
|
import { getStudioOperations, loadHydratedCms, loadProjectSession } from '@elytracms/studio/lib/operations';
|
|
4
4
|
import { isPlaygroundMode } from '@elytracms/studio/lib/persistence';
|
|
5
|
-
import { configureContentSession } from '@elytracms/studio/lib/data';
|
|
5
|
+
import { configureContentSession, isContentSessionWrite, markContentSessionStale, notePlaygroundWriteForUnloadGuard, registerContentSessionRefresher, } from '@elytracms/studio/lib/data';
|
|
6
6
|
import { resolveDeploymentLocaleConfig, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
|
|
7
7
|
import { createRequiredContext } from '@elytracms/studio/lib/required-context';
|
|
8
8
|
import { HydrationBoundary, useHydrationPhase } from './use-hydration-phase.js';
|
|
@@ -31,23 +31,30 @@ export function contentSessionOf(cms, routes = resolveDeploymentRoutes(), locale
|
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
33
|
* Load the CURRENT persisted CMS state and point the preview content engine at
|
|
34
|
-
* it — the framework-free core of `loadCms` (both modes run it; EC-361).
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
34
|
+
* it — the framework-free core of `loadCms` (both modes run it; EC-361).
|
|
35
|
+
*
|
|
36
|
+
* EC-461 — the gate value is the REAL hydration result in BOTH modes now. The
|
|
37
|
+
* old EC-320 playground contract (`{ ok: true, value: null }` — CMS surfaces
|
|
38
|
+
* keep their static config/fixture baseline forever) was the root of the
|
|
39
|
+
* playground loss family: every cross-section navigation remounted
|
|
40
|
+
* `CmsProvider` from the static seed, so same-session edits vanished from the
|
|
41
|
+
* CMS surfaces even though the in-memory adapter still held them — and any
|
|
42
|
+
* write issued from that re-seeded session (e.g. an autosave) then clobbered
|
|
43
|
+
* the adapter's newer record with the stale baseline. The seeded in-memory
|
|
44
|
+
* adapter IS a persisted backend behind the operations seam (AD-17), so the
|
|
45
|
+
* playground hydrates from it per CMS mount exactly like connected mode
|
|
46
|
+
* hydrates from Convex; a fresh boot returns the deterministic seed, so
|
|
47
|
+
* fixture reproducibility (EC-138) is preserved in substance. A failure is
|
|
48
|
+
* loud twice over: logged here (for the fire-and-forget refresh callers) and
|
|
49
|
+
* rendered as the gate's retryable error state.
|
|
40
50
|
*/
|
|
41
|
-
export async function loadCmsIntoSession(operations, projectId
|
|
51
|
+
export async function loadCmsIntoSession(operations, projectId) {
|
|
42
52
|
const result = await loadHydratedCms(operations, projectId);
|
|
43
53
|
// Keep the preview engine in sync with the freshest persisted content.
|
|
44
54
|
if (result.ok && result.value !== null)
|
|
45
55
|
configureContentSession(contentSessionOf(result.value));
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
console.error('[elytra] playground content-session load failed', result.error);
|
|
49
|
-
return { ok: true, value: null };
|
|
50
|
-
}
|
|
56
|
+
if (!result.ok)
|
|
57
|
+
console.error('[elytra] content-session load failed', result.error);
|
|
51
58
|
return result;
|
|
52
59
|
}
|
|
53
60
|
export function ProjectSessionProvider({ projectId, children, }) {
|
|
@@ -71,15 +78,45 @@ export function ProjectSessionProvider({ projectId, children, }) {
|
|
|
71
78
|
// Leaving the project resets the preview engine to fixtures; the next
|
|
72
79
|
// project's gate reconfigures it before rendering its own children.
|
|
73
80
|
useEffect(() => () => configureContentSession(null), []);
|
|
74
|
-
// EC-361:
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
// pure core (`loadCmsIntoSession`, above) carries + pins that contract.
|
|
81
|
+
// EC-361/EC-461: both modes run the SAME load — the content-session side
|
|
82
|
+
// effect plus the REAL hydration value for the CMS gates. (EC-320's
|
|
83
|
+
// playground null-mask is retired: it was the loss family's root — see
|
|
84
|
+
// `loadCmsIntoSession`.)
|
|
79
85
|
const loadCms = useCallback(async () => {
|
|
80
86
|
const { operations } = await getStudioOperations();
|
|
81
|
-
return loadCmsIntoSession(operations, projectId
|
|
82
|
-
}, [
|
|
87
|
+
return loadCmsIntoSession(operations, projectId);
|
|
88
|
+
}, [projectId]);
|
|
89
|
+
// EC-460: keep the preview content session in sync with WRITES. Every
|
|
90
|
+
// succeeded content-bearing command (documents/schema/assets) on the shared
|
|
91
|
+
// operations event bus marks the session stale; the registered refresher —
|
|
92
|
+
// this provider's own `loadCms`, i.e. the identical hydration path both
|
|
93
|
+
// modes already run — re-points it (debounced, and only while a preview
|
|
94
|
+
// consumer is mounted; see `content-session-refresh.ts`). Before this, the
|
|
95
|
+
// session refreshed only at boot + CMS-surface mounts, so a canvas opened
|
|
96
|
+
// after authoring/publishing resolved listings and relation picks against a
|
|
97
|
+
// stale snapshot: the EC-276 empty-listing / 1-of-3-relations family.
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const unregister = registerContentSessionRefresher(() => loadCms());
|
|
100
|
+
let unsubscribe;
|
|
101
|
+
let cancelled = false;
|
|
102
|
+
void getStudioOperations().then(({ operations }) => {
|
|
103
|
+
if (cancelled)
|
|
104
|
+
return;
|
|
105
|
+
unsubscribe = operations.events.subscribe((event) => {
|
|
106
|
+
if (isContentSessionWrite(event))
|
|
107
|
+
markContentSessionStale();
|
|
108
|
+
// EC-461: the first authored playground write arms the beforeunload
|
|
109
|
+
// guard for the rest of the page's life (module-level — surviving this
|
|
110
|
+
// provider's unmount, because the in-memory world does too).
|
|
111
|
+
notePlaygroundWriteForUnloadGuard(event);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
return () => {
|
|
115
|
+
cancelled = true;
|
|
116
|
+
unregister();
|
|
117
|
+
unsubscribe?.();
|
|
118
|
+
};
|
|
119
|
+
}, [loadCms]);
|
|
83
120
|
// EC-361: playground boot hydration for the content preview engine — the same
|
|
84
121
|
// loader connected mode gates on, fired WITHOUT gating so the synchronous
|
|
85
122
|
// playground first render (EC-138) is preserved. `loadCms` owns the
|
|
@@ -94,28 +131,27 @@ export function ProjectSessionProvider({ projectId, children, }) {
|
|
|
94
131
|
}
|
|
95
132
|
export { useProjectSession };
|
|
96
133
|
/**
|
|
97
|
-
* Gate one CMS surface mount on a FRESH persisted CMS load
|
|
134
|
+
* Gate one CMS surface mount on a FRESH persisted CMS load — BOTH modes.
|
|
98
135
|
* Render-prop: hands the hydrated content (or `null` → fixture fallback,
|
|
99
|
-
* first-run) to the caller once it resolved.
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* `onResolved`/`enabled` is passed here.
|
|
136
|
+
* first-run / un-baselined kit) to the caller once it resolved. Unlike
|
|
137
|
+
* `ProjectSessionProvider`, this gate owns no `configureContentSession` side
|
|
138
|
+
* effect of its own — `loadCms` (from `useProjectSession`) already calls it
|
|
139
|
+
* internally when it resolves — so no `onResolved`/`enabled` is passed here.
|
|
104
140
|
*
|
|
105
|
-
* EC-
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
141
|
+
* EC-461: the playground gates on the SAME async hydration as connected mode
|
|
142
|
+
* (over the seeded in-memory adapter, resolved in microtasks). The old
|
|
143
|
+
* synchronous `null` fast-path (EC-320/EC-361) made every remounted CMS
|
|
144
|
+
* session re-seed the STATIC baseline, which both hid and then destroyed
|
|
145
|
+
* same-session edits — the playground loss family. A fresh playground boot
|
|
146
|
+
* hydrates the deterministic seed, so fixture reproducibility is unchanged in
|
|
147
|
+
* substance; only a microtask-long loading frame replaced the synchronous
|
|
148
|
+
* first render HERE (`ProjectSessionProvider`'s EC-138 playground fast-path is
|
|
149
|
+
* untouched).
|
|
109
150
|
*/
|
|
110
151
|
export function CmsSessionGate({ children, }) {
|
|
111
152
|
const { loadCms } = useProjectSession();
|
|
112
|
-
const playground = isPlaygroundMode();
|
|
113
|
-
useEffect(() => {
|
|
114
|
-
if (playground)
|
|
115
|
-
void loadCms();
|
|
116
|
-
}, [playground, loadCms]);
|
|
117
153
|
const { phase, retry } = useHydrationPhase(loadCms, {
|
|
118
|
-
playground,
|
|
154
|
+
playground: false,
|
|
119
155
|
playgroundValue: null,
|
|
120
156
|
});
|
|
121
157
|
return (_jsx(HydrationBoundary, { phase: phase, retry: retry, children: children }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"project-session.js","sourceRoot":"","sources":["../../src/components/project-session.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport type { LocaleConfig, RouteRecord } from '@elytracms/core/cms-core'\nimport { getStudioOperations, loadHydratedCms, loadProjectSession } from '@elytracms/studio/lib/operations'\nimport type {\n HydratedCms,\n HydratedProjectSession,\n OperationResult,\n OperationsClient,\n} from '@elytracms/studio/lib/operations'\nimport { isPlaygroundMode } from '@elytracms/studio/lib/persistence'\nimport { configureContentSession } from '@elytracms/studio/lib/data'\nimport type { ContentSessionState } from '@elytracms/studio/lib/data'\nimport { resolveDeploymentLocaleConfig, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\nimport { HydrationBoundary, useHydrationPhase } from './use-hydration-phase'\n\n/**\n * Project-session hydration gate (Milestone B sign-off fix, EC-177 gap 1).\n *\n * In Convex mode every project session must BOOT from persistence: the\n * builder from the latest persisted graph revision, the CMS from the saved\n * schema + documents, publishing from the persisted environment states.\n * This provider runs the `@elytracms/core/operations` hydration queries\n * (`lib/operations/session-hydration`) and — like `DeploymentSetupGate` — gates its\n * children on a deterministic loading state, so fixture content never\n * flashes (and is never editable) before the persisted state arrives.\n *\n * Playground mode renders children synchronously with `session: null`\n * (fixtures), byte-identical to the pre-hydration behavior — the EC-138\n * determinism contract.\n *\n * Side effect on hydration: the content preview engine\n * (`lib/data/collection-content`) is pointed at the hydrated documents\n * BEFORE children render, so \"Preview with\" lists and section previews show\n * live entries, not fixture titles.\n *\n * EC-361 — the playground gets the SAME side effect, without the render gate:\n * the seeded in-memory adapter IS a persisted backend behind the operations\n * seam (AD-17), so the content session hydrates from it through the identical\n * `loadHydratedCms` loader connected mode uses — fire-and-forget after the\n * synchronous first render (EC-138 stays intact), refreshed per CMS surface\n * mount exactly like connected's `CmsSessionGate` → `loadCms` rhythm. Canvas\n * listing/relation population therefore resolves from in-memory session\n * documents with connected-mode semantics (published-ness = the version\n * pointer; the Draft view resolves live rows) instead of the blanket-empty\n * session the playground used to run with. The React `session`/`loadCms`\n * VALUES stay `null` in playground — the CMS surfaces keep their fixture\n * baseline (EC-320's static-baseline rule); only the preview content engine\n * reads persisted state.\n *\n * EC-308/AD-17 note — this gate deliberately does NOT read through the\n * `useOperationQuery` cache seam. It is boot/session orchestration, not a\n * cacheable server read: (1) the synchronous playground first-render is the\n * EC-138 determinism contract (a pull-query is async even when disabled);\n * (2) `configureContentSession` must run synchronously BEFORE children mount,\n * which a post-render query effect cannot guarantee; (3) `CmsSessionGate` is\n * intentionally fresh-per-visit (it remounts to show edits persisted since\n * project open) — the opposite of a cached read. It still reads through the\n * operations client (no transport bypass), and uses a user-driven retry epoch,\n * not freshness machinery — so it is outside the patchwork AD-17 retires.\n */\n\nexport interface ProjectSessionStore {\n /** Hydrated persisted state (Convex mode); `null` in playground (fixtures). */\n session: HydratedProjectSession | null\n /**\n * Load the CURRENT persisted CMS state (fresh query — the CMS section\n * remounts per visit and must show edits persisted since project open).\n * Playground resolves `null` immediately (fixtures).\n */\n loadCms(): Promise<OperationResult<HydratedCms | null>>\n}\n\nconst { Context: ProjectSessionContext, useRequired: useProjectSession } =\n createRequiredContext<ProjectSessionStore>('ProjectSession')\n\n/**\n * Compose the preview content-session state from a hydrated CMS load plus the\n * config-owned deployment seams — the ONE place hydrated content meets config:\n *\n * - `routes` (EC-291): the config-sourced routes, so the canvas `resolveLink`\n * derives a rich-text link's URL exactly as delivery does.\n * - `localeConfig` (EC-385): the active config project's declared locales\n * (AD-11 — locales are structure, structure is code), so canvas content\n * clients evaluate locale-shaped values (EC-018 fallback, localized\n * filter/sort) over the PROJECT's locale set, never the sample `en/de`\n * fixture. Without a config project (env-only wiring) the key stays absent\n * and the preview engine keeps its sample-config default, mirroring the CMS\n * surface ladder (`resolveCmsInitialLocaleConfig`).\n *\n * The parameters exist purely as a unit-test seam (house pattern, same as\n * `resolveCmsInitialCollections`); app code always uses the defaults.\n */\nexport function contentSessionOf(\n cms: HydratedCms | null,\n routes: readonly RouteRecord[] = resolveDeploymentRoutes(),\n localeConfig: LocaleConfig | undefined = resolveDeploymentLocaleConfig(),\n): ContentSessionState | null {\n if (!cms) return null\n return { ...cms, routes, ...(localeConfig ? { localeConfig } : {}) }\n}\n\n/**\n * Load the CURRENT persisted CMS state and point the preview content engine at\n * it — the framework-free core of `loadCms` (both modes run it; EC-361). The\n * playground return contract (EC-320) is pinned here: the GATE value is always\n * `{ ok: true, value: null }` in playground — CMS surfaces keep their\n * config/fixture baseline — while the content session above still hydrates\n * from the in-memory adapter (a load failure is loud, never a silent empty\n * preview).\n */\nexport async function loadCmsIntoSession(\n operations: OperationsClient,\n projectId: string,\n playground: boolean,\n): Promise<OperationResult<HydratedCms | null>> {\n const result = await loadHydratedCms(operations, projectId)\n // Keep the preview engine in sync with the freshest persisted content.\n if (result.ok && result.value !== null) configureContentSession(contentSessionOf(result.value))\n if (playground) {\n if (!result.ok) console.error('[elytra] playground content-session load failed', result.error)\n return { ok: true, value: null }\n }\n return result\n}\n\nexport function ProjectSessionProvider({\n projectId,\n children,\n}: {\n projectId: string\n children: ReactNode\n}) {\n const playground = isPlaygroundMode()\n\n const loader = useCallback(\n () => getStudioOperations().then(({ operations }) => loadProjectSession(operations, projectId)),\n [projectId],\n )\n // Point the preview content engine at the hydrated documents BEFORE any\n // child renders (panels read it module-level, synchronously), composed with\n // the config-owned routes (EC-291) + locale config (EC-385) via\n // `contentSessionOf`. `enabled: true` below runs this in the playground\n // fast-path too (with `session: null`) — the one divergence from\n // `CmsSessionGate`, which owns no such side effect.\n const onResolved = useCallback((session: HydratedProjectSession | null) => {\n configureContentSession(contentSessionOf(session?.cms ?? null))\n }, [])\n const { phase, retry } = useHydrationPhase<HydratedProjectSession | null>(loader, {\n playground,\n playgroundValue: null,\n enabled: true,\n onResolved,\n })\n\n // Leaving the project resets the preview engine to fixtures; the next\n // project's gate reconfigures it before rendering its own children.\n useEffect(() => () => configureContentSession(null), [])\n\n // EC-361: the playground runs the SAME load (over the seeded in-memory\n // adapter) for the content-session side effect, but the GATE value stays\n // `null` — CMS surfaces keep their config/fixture baseline (EC-320's\n // static-baseline rule), matching the pre-EC-361 playground contract. The\n // pure core (`loadCmsIntoSession`, above) carries + pins that contract.\n const loadCms = useCallback(async (): Promise<OperationResult<HydratedCms | null>> => {\n const { operations } = await getStudioOperations()\n return loadCmsIntoSession(operations, projectId, playground)\n }, [playground, projectId])\n\n // EC-361: playground boot hydration for the content preview engine — the same\n // loader connected mode gates on, fired WITHOUT gating so the synchronous\n // playground first render (EC-138) is preserved. `loadCms` owns the\n // `configureContentSession` side effect; the resolved value is discarded here.\n // Runs after `useHydrationPhase`'s fast-path effect (registered above) has\n // reset the session, so the hydrated state always lands last.\n useEffect(() => {\n if (playground) void loadCms()\n }, [playground, loadCms])\n\n return (\n <HydrationBoundary phase={phase} retry={retry}>\n {(session) => (\n <ProjectSessionContext.Provider value={{ session, loadCms }}>\n {children}\n </ProjectSessionContext.Provider>\n )}\n </HydrationBoundary>\n )\n}\n\nexport { useProjectSession }\n\n/**\n * Gate one CMS surface mount on a FRESH persisted CMS load (Convex mode).\n * Render-prop: hands the hydrated content (or `null` → fixture fallback,\n * first-run) to the caller once it resolved. Playground renders synchronously\n * with `null`. Unlike `ProjectSessionProvider`, this gate owns no\n * `configureContentSession` side effect of its own — `loadCms` (from\n * `useProjectSession`) already calls it internally when it resolves — so no\n * `onResolved`/`enabled` is passed here.\n *\n * EC-361: in playground the gate stays synchronous (fixture baseline, EC-138),\n * but `loadCms` is still fired per mount for its content-session side effect —\n * the preview engine refreshes from the in-memory adapter on the same\n * per-CMS-mount rhythm connected mode gets from the gated load.\n */\nexport function CmsSessionGate({\n children,\n}: {\n children: (cms: HydratedCms | null) => ReactNode\n}) {\n const { loadCms } = useProjectSession()\n const playground = isPlaygroundMode()\n useEffect(() => {\n if (playground) void loadCms()\n }, [playground, loadCms])\n const { phase, retry } = useHydrationPhase<HydratedCms | null>(loadCms, {\n playground,\n playgroundValue: null,\n })\n return (\n <HydrationBoundary phase={phase} retry={retry}>\n {children}\n </HydrationBoundary>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"project-session.js","sourceRoot":"","sources":["../../src/components/project-session.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport type { LocaleConfig, RouteRecord } from '@elytracms/core/cms-core'\nimport { getStudioOperations, loadHydratedCms, loadProjectSession } from '@elytracms/studio/lib/operations'\nimport type {\n HydratedCms,\n HydratedProjectSession,\n OperationResult,\n OperationsClient,\n} from '@elytracms/studio/lib/operations'\nimport { isPlaygroundMode } from '@elytracms/studio/lib/persistence'\nimport {\n configureContentSession,\n isContentSessionWrite,\n markContentSessionStale,\n notePlaygroundWriteForUnloadGuard,\n registerContentSessionRefresher,\n} from '@elytracms/studio/lib/data'\nimport type { ContentSessionState } from '@elytracms/studio/lib/data'\nimport { resolveDeploymentLocaleConfig, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\nimport { HydrationBoundary, useHydrationPhase } from './use-hydration-phase'\n\n/**\n * Project-session hydration gate (Milestone B sign-off fix, EC-177 gap 1).\n *\n * In Convex mode every project session must BOOT from persistence: the\n * builder from the latest persisted graph revision, the CMS from the saved\n * schema + documents, publishing from the persisted environment states.\n * This provider runs the `@elytracms/core/operations` hydration queries\n * (`lib/operations/session-hydration`) and — like `DeploymentSetupGate` — gates its\n * children on a deterministic loading state, so fixture content never\n * flashes (and is never editable) before the persisted state arrives.\n *\n * Playground mode renders children synchronously with `session: null`\n * (fixtures), byte-identical to the pre-hydration behavior — the EC-138\n * determinism contract.\n *\n * Side effect on hydration: the content preview engine\n * (`lib/data/collection-content`) is pointed at the hydrated documents\n * BEFORE children render, so \"Preview with\" lists and section previews show\n * live entries, not fixture titles.\n *\n * EC-361 — the playground gets the SAME side effect, without the render gate:\n * the seeded in-memory adapter IS a persisted backend behind the operations\n * seam (AD-17), so the content session hydrates from it through the identical\n * `loadHydratedCms` loader connected mode uses — fire-and-forget after the\n * synchronous first render (EC-138 stays intact), refreshed per CMS surface\n * mount exactly like connected's `CmsSessionGate` → `loadCms` rhythm. Canvas\n * listing/relation population therefore resolves from in-memory session\n * documents with connected-mode semantics (published-ness = the version\n * pointer; the Draft view resolves live rows) instead of the blanket-empty\n * session the playground used to run with.\n *\n * EC-461 — `CmsSessionGate`/`loadCms` hydrate the CMS surfaces from the\n * adapter in the playground too now. The old EC-320 \"static-baseline rule\"\n * (gate value pinned `null`; CMS sessions re-seeded the fixture baseline on\n * every mount) is retired: it made every cross-section navigation silently\n * revert the visible world to the seed, and a subsequent autosave from that\n * re-seeded session destroyed the adapter's newer record — the EC-276/EC-461\n * playground loss family. Only this provider's OWN `session` value keeps the\n * playground `null` fast-path (boot determinism, EC-138 below).\n *\n * EC-308/AD-17 note — this gate deliberately does NOT read through the\n * `useOperationQuery` cache seam. It is boot/session orchestration, not a\n * cacheable server read: (1) the synchronous playground first-render is the\n * EC-138 determinism contract (a pull-query is async even when disabled);\n * (2) `configureContentSession` must run synchronously BEFORE children mount,\n * which a post-render query effect cannot guarantee; (3) `CmsSessionGate` is\n * intentionally fresh-per-visit (it remounts to show edits persisted since\n * project open) — the opposite of a cached read. It still reads through the\n * operations client (no transport bypass), and uses a user-driven retry epoch,\n * not freshness machinery — so it is outside the patchwork AD-17 retires.\n */\n\nexport interface ProjectSessionStore {\n /** Hydrated persisted state (Convex mode); `null` in playground (fixtures). */\n session: HydratedProjectSession | null\n /**\n * Load the CURRENT persisted CMS state (fresh query — the CMS section\n * remounts per visit and must show edits persisted since project open).\n * EC-461: the playground resolves the seeded in-memory adapter's state the\n * same way (`null` only while nothing is persisted — e.g. an un-baselined\n * kit project), so remounted CMS sessions carry same-session edits.\n */\n loadCms(): Promise<OperationResult<HydratedCms | null>>\n}\n\nconst { Context: ProjectSessionContext, useRequired: useProjectSession } =\n createRequiredContext<ProjectSessionStore>('ProjectSession')\n\n/**\n * Compose the preview content-session state from a hydrated CMS load plus the\n * config-owned deployment seams — the ONE place hydrated content meets config:\n *\n * - `routes` (EC-291): the config-sourced routes, so the canvas `resolveLink`\n * derives a rich-text link's URL exactly as delivery does.\n * - `localeConfig` (EC-385): the active config project's declared locales\n * (AD-11 — locales are structure, structure is code), so canvas content\n * clients evaluate locale-shaped values (EC-018 fallback, localized\n * filter/sort) over the PROJECT's locale set, never the sample `en/de`\n * fixture. Without a config project (env-only wiring) the key stays absent\n * and the preview engine keeps its sample-config default, mirroring the CMS\n * surface ladder (`resolveCmsInitialLocaleConfig`).\n *\n * The parameters exist purely as a unit-test seam (house pattern, same as\n * `resolveCmsInitialCollections`); app code always uses the defaults.\n */\nexport function contentSessionOf(\n cms: HydratedCms | null,\n routes: readonly RouteRecord[] = resolveDeploymentRoutes(),\n localeConfig: LocaleConfig | undefined = resolveDeploymentLocaleConfig(),\n): ContentSessionState | null {\n if (!cms) return null\n return { ...cms, routes, ...(localeConfig ? { localeConfig } : {}) }\n}\n\n/**\n * Load the CURRENT persisted CMS state and point the preview content engine at\n * it — the framework-free core of `loadCms` (both modes run it; EC-361).\n *\n * EC-461 — the gate value is the REAL hydration result in BOTH modes now. The\n * old EC-320 playground contract (`{ ok: true, value: null }` — CMS surfaces\n * keep their static config/fixture baseline forever) was the root of the\n * playground loss family: every cross-section navigation remounted\n * `CmsProvider` from the static seed, so same-session edits vanished from the\n * CMS surfaces even though the in-memory adapter still held them — and any\n * write issued from that re-seeded session (e.g. an autosave) then clobbered\n * the adapter's newer record with the stale baseline. The seeded in-memory\n * adapter IS a persisted backend behind the operations seam (AD-17), so the\n * playground hydrates from it per CMS mount exactly like connected mode\n * hydrates from Convex; a fresh boot returns the deterministic seed, so\n * fixture reproducibility (EC-138) is preserved in substance. A failure is\n * loud twice over: logged here (for the fire-and-forget refresh callers) and\n * rendered as the gate's retryable error state.\n */\nexport async function loadCmsIntoSession(\n operations: OperationsClient,\n projectId: string,\n): Promise<OperationResult<HydratedCms | null>> {\n const result = await loadHydratedCms(operations, projectId)\n // Keep the preview engine in sync with the freshest persisted content.\n if (result.ok && result.value !== null) configureContentSession(contentSessionOf(result.value))\n if (!result.ok) console.error('[elytra] content-session load failed', result.error)\n return result\n}\n\nexport function ProjectSessionProvider({\n projectId,\n children,\n}: {\n projectId: string\n children: ReactNode\n}) {\n const playground = isPlaygroundMode()\n\n const loader = useCallback(\n () => getStudioOperations().then(({ operations }) => loadProjectSession(operations, projectId)),\n [projectId],\n )\n // Point the preview content engine at the hydrated documents BEFORE any\n // child renders (panels read it module-level, synchronously), composed with\n // the config-owned routes (EC-291) + locale config (EC-385) via\n // `contentSessionOf`. `enabled: true` below runs this in the playground\n // fast-path too (with `session: null`) — the one divergence from\n // `CmsSessionGate`, which owns no such side effect.\n const onResolved = useCallback((session: HydratedProjectSession | null) => {\n configureContentSession(contentSessionOf(session?.cms ?? null))\n }, [])\n const { phase, retry } = useHydrationPhase<HydratedProjectSession | null>(loader, {\n playground,\n playgroundValue: null,\n enabled: true,\n onResolved,\n })\n\n // Leaving the project resets the preview engine to fixtures; the next\n // project's gate reconfigures it before rendering its own children.\n useEffect(() => () => configureContentSession(null), [])\n\n // EC-361/EC-461: both modes run the SAME load — the content-session side\n // effect plus the REAL hydration value for the CMS gates. (EC-320's\n // playground null-mask is retired: it was the loss family's root — see\n // `loadCmsIntoSession`.)\n const loadCms = useCallback(async (): Promise<OperationResult<HydratedCms | null>> => {\n const { operations } = await getStudioOperations()\n return loadCmsIntoSession(operations, projectId)\n }, [projectId])\n\n // EC-460: keep the preview content session in sync with WRITES. Every\n // succeeded content-bearing command (documents/schema/assets) on the shared\n // operations event bus marks the session stale; the registered refresher —\n // this provider's own `loadCms`, i.e. the identical hydration path both\n // modes already run — re-points it (debounced, and only while a preview\n // consumer is mounted; see `content-session-refresh.ts`). Before this, the\n // session refreshed only at boot + CMS-surface mounts, so a canvas opened\n // after authoring/publishing resolved listings and relation picks against a\n // stale snapshot: the EC-276 empty-listing / 1-of-3-relations family.\n useEffect(() => {\n const unregister = registerContentSessionRefresher(() => loadCms())\n let unsubscribe: (() => void) | undefined\n let cancelled = false\n void getStudioOperations().then(({ operations }) => {\n if (cancelled) return\n unsubscribe = operations.events.subscribe((event) => {\n if (isContentSessionWrite(event)) markContentSessionStale()\n // EC-461: the first authored playground write arms the beforeunload\n // guard for the rest of the page's life (module-level — surviving this\n // provider's unmount, because the in-memory world does too).\n notePlaygroundWriteForUnloadGuard(event)\n })\n })\n return () => {\n cancelled = true\n unregister()\n unsubscribe?.()\n }\n }, [loadCms])\n\n // EC-361: playground boot hydration for the content preview engine — the same\n // loader connected mode gates on, fired WITHOUT gating so the synchronous\n // playground first render (EC-138) is preserved. `loadCms` owns the\n // `configureContentSession` side effect; the resolved value is discarded here.\n // Runs after `useHydrationPhase`'s fast-path effect (registered above) has\n // reset the session, so the hydrated state always lands last.\n useEffect(() => {\n if (playground) void loadCms()\n }, [playground, loadCms])\n\n return (\n <HydrationBoundary phase={phase} retry={retry}>\n {(session) => (\n <ProjectSessionContext.Provider value={{ session, loadCms }}>\n {children}\n </ProjectSessionContext.Provider>\n )}\n </HydrationBoundary>\n )\n}\n\nexport { useProjectSession }\n\n/**\n * Gate one CMS surface mount on a FRESH persisted CMS load — BOTH modes.\n * Render-prop: hands the hydrated content (or `null` → fixture fallback,\n * first-run / un-baselined kit) to the caller once it resolved. Unlike\n * `ProjectSessionProvider`, this gate owns no `configureContentSession` side\n * effect of its own — `loadCms` (from `useProjectSession`) already calls it\n * internally when it resolves — so no `onResolved`/`enabled` is passed here.\n *\n * EC-461: the playground gates on the SAME async hydration as connected mode\n * (over the seeded in-memory adapter, resolved in microtasks). The old\n * synchronous `null` fast-path (EC-320/EC-361) made every remounted CMS\n * session re-seed the STATIC baseline, which both hid and then destroyed\n * same-session edits — the playground loss family. A fresh playground boot\n * hydrates the deterministic seed, so fixture reproducibility is unchanged in\n * substance; only a microtask-long loading frame replaced the synchronous\n * first render HERE (`ProjectSessionProvider`'s EC-138 playground fast-path is\n * untouched).\n */\nexport function CmsSessionGate({\n children,\n}: {\n children: (cms: HydratedCms | null) => ReactNode\n}) {\n const { loadCms } = useProjectSession()\n const { phase, retry } = useHydrationPhase<HydratedCms | null>(loadCms, {\n playground: false,\n playgroundValue: null,\n })\n return (\n <HydrationBoundary phase={phase} retry={retry}>\n {children}\n </HydrationBoundary>\n )\n}\n"]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset URL scheme classification (EC-459). `InMemoryBlobStorage` (playground's
|
|
3
|
+
* default blob backend, `@elytracms/core/persistence`) mints an opaque
|
|
4
|
+
* `memory://blobs/<key>` locator for stored bytes — correct for Node tests and
|
|
5
|
+
* inspection, but not a resource a browser can ever fetch. Every studio surface
|
|
6
|
+
* that puts an asset's `url` in an `<img src>` must gate on "is this actually
|
|
7
|
+
* displayable", not "is this specifically `http(s)`" — the fix wiring
|
|
8
|
+
* (`browser-blob-storage.ts`) replaces `memory://` with a `blob:` object URL,
|
|
9
|
+
* which the OLD `url.startsWith('http')` gates here would have kept rejecting.
|
|
10
|
+
*
|
|
11
|
+
* Pure and dependency-free so it is trivially Node-testable; every surface that
|
|
12
|
+
* decides "show the `<img>` or the placeholder" imports this one predicate so
|
|
13
|
+
* the decision can never drift between call sites again.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* True when `url` is a scheme a browser can fetch/render (`http(s):`, `blob:`,
|
|
17
|
+
* `data:`) — false for `undefined`, empty, and opaque backend-internal locators
|
|
18
|
+
* like `memory://blobs/…` that must never reach an `<img>`/`<a href>`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isDisplayableAssetUrl(url: string | undefined | null): url is string;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset URL scheme classification (EC-459). `InMemoryBlobStorage` (playground's
|
|
3
|
+
* default blob backend, `@elytracms/core/persistence`) mints an opaque
|
|
4
|
+
* `memory://blobs/<key>` locator for stored bytes — correct for Node tests and
|
|
5
|
+
* inspection, but not a resource a browser can ever fetch. Every studio surface
|
|
6
|
+
* that puts an asset's `url` in an `<img src>` must gate on "is this actually
|
|
7
|
+
* displayable", not "is this specifically `http(s)`" — the fix wiring
|
|
8
|
+
* (`browser-blob-storage.ts`) replaces `memory://` with a `blob:` object URL,
|
|
9
|
+
* which the OLD `url.startsWith('http')` gates here would have kept rejecting.
|
|
10
|
+
*
|
|
11
|
+
* Pure and dependency-free so it is trivially Node-testable; every surface that
|
|
12
|
+
* decides "show the `<img>` or the placeholder" imports this one predicate so
|
|
13
|
+
* the decision can never drift between call sites again.
|
|
14
|
+
*/
|
|
15
|
+
/** Schemes a browser can actually load pixels from in an `<img src>`. */
|
|
16
|
+
const DISPLAYABLE_URL_PATTERN = /^(https?:|blob:|data:)/i;
|
|
17
|
+
/**
|
|
18
|
+
* True when `url` is a scheme a browser can fetch/render (`http(s):`, `blob:`,
|
|
19
|
+
* `data:`) — false for `undefined`, empty, and opaque backend-internal locators
|
|
20
|
+
* like `memory://blobs/…` that must never reach an `<img>`/`<a href>`.
|
|
21
|
+
*/
|
|
22
|
+
export function isDisplayableAssetUrl(url) {
|
|
23
|
+
return typeof url === 'string' && DISPLAYABLE_URL_PATTERN.test(url);
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=asset-url.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-url.js","sourceRoot":"","sources":["../../../src/lib/assets/asset-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,yEAAyE;AACzE,MAAM,uBAAuB,GAAG,yBAAyB,CAAA;AAEzD;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAA8B;IAClE,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,uBAAuB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACrE,CAAC","sourcesContent":["/**\n * Asset URL scheme classification (EC-459). `InMemoryBlobStorage` (playground's\n * default blob backend, `@elytracms/core/persistence`) mints an opaque\n * `memory://blobs/<key>` locator for stored bytes — correct for Node tests and\n * inspection, but not a resource a browser can ever fetch. Every studio surface\n * that puts an asset's `url` in an `<img src>` must gate on \"is this actually\n * displayable\", not \"is this specifically `http(s)`\" — the fix wiring\n * (`browser-blob-storage.ts`) replaces `memory://` with a `blob:` object URL,\n * which the OLD `url.startsWith('http')` gates here would have kept rejecting.\n *\n * Pure and dependency-free so it is trivially Node-testable; every surface that\n * decides \"show the `<img>` or the placeholder\" imports this one predicate so\n * the decision can never drift between call sites again.\n */\n\n/** Schemes a browser can actually load pixels from in an `<img src>`. */\nconst DISPLAYABLE_URL_PATTERN = /^(https?:|blob:|data:)/i\n\n/**\n * True when `url` is a scheme a browser can fetch/render (`http(s):`, `blob:`,\n * `data:`) — false for `undefined`, empty, and opaque backend-internal locators\n * like `memory://blobs/…` that must never reach an `<img>`/`<a href>`.\n */\nexport function isDisplayableAssetUrl(url: string | undefined | null): url is string {\n return typeof url === 'string' && DISPLAYABLE_URL_PATTERN.test(url)\n}\n"]}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { BlobStorageAdapter } from '@elytracms/core/persistence';
|
|
2
|
+
/**
|
|
3
|
+
* Studio-side (playground-only) blob storage wrapper (EC-459).
|
|
4
|
+
*
|
|
5
|
+
* Root cause this fixes: `InMemoryBlobStorage` (`@elytracms/core/persistence`)
|
|
6
|
+
* mints `url(key)` as an opaque `memory://blobs/<key>` locator. That is correct
|
|
7
|
+
* for its job — Node tests and programmatic inspection never need a real
|
|
8
|
+
* browser resource — but the studio's upload flow (`upload.ts`) bakes
|
|
9
|
+
* `blobs.resolveUrl?.(storageKey) ?? blobs.url(storageKey)` straight into the
|
|
10
|
+
* persisted `AssetRecord.url` (and the EC-400 thumb variant's `url`). Every
|
|
11
|
+
* surface that then puts that value in an `<img src>` — the asset library grid,
|
|
12
|
+
* the compact field preview, the focal-point editor, and (worst of all, no
|
|
13
|
+
* gating at all) the canvas iframe's resolved-asset renderer — hands the
|
|
14
|
+
* browser a URL it can never fetch. Pixels never render; the canvas iframe
|
|
15
|
+
* shows an actual broken-image icon (`naturalWidth === 0`).
|
|
16
|
+
*
|
|
17
|
+
* The fix: wrap the byte store, not replace it. `put`/`store` still delegate
|
|
18
|
+
* unchanged to `inner` (an `InMemoryBlobStorage` by default, or any adapter a
|
|
19
|
+
* test wants), so bytes live exactly where they always did. The moment bytes
|
|
20
|
+
* are known, THIS wrapper additionally mints a real browser-displayable URL
|
|
21
|
+
* (an object URL by default) and serves THAT from `url()`/`resolveUrl()` —
|
|
22
|
+
* so the value the upload flow bakes into the record is something an `<img>`
|
|
23
|
+
* can actually load. `@elytracms/core`'s `InMemoryBlobStorage` is untouched and
|
|
24
|
+
* stays platform-neutral (it still runs fine under Node tests); the one
|
|
25
|
+
* DOM-touching function (`objectUrlMinter`, using the real `Blob`/`URL` globals)
|
|
26
|
+
* is injected, so this whole orchestrator — caching, delegation, revoke-on-
|
|
27
|
+
* delete/overwrite — is unit-testable in Node with a fake minter.
|
|
28
|
+
*
|
|
29
|
+
* Playground-only by construction: nothing calls this in the Convex/connected
|
|
30
|
+
* path, and the `BlobStorageAdapter` interface is unchanged, so the delivery
|
|
31
|
+
* wire and the connected-mode adapter are untouched.
|
|
32
|
+
*/
|
|
33
|
+
/** Mints a browser-displayable URL from raw bytes. Injected for Node testability. */
|
|
34
|
+
export type UrlMinter = (data: Uint8Array, contentType: string) => string;
|
|
35
|
+
/** Releases a previously-minted URL's resources (e.g. `URL.revokeObjectURL`). */
|
|
36
|
+
export type UrlRevoker = (url: string) => void;
|
|
37
|
+
/**
|
|
38
|
+
* The real browser minter: an object URL backed by a same-process `Blob`.
|
|
39
|
+
* Deliberately thin and untested in Node (mirrors `canvasVariantEncoder` in
|
|
40
|
+
* `thumbnail.ts` — a DOM shell around a pure/injectable orchestrator).
|
|
41
|
+
*/
|
|
42
|
+
export declare const objectUrlMinter: UrlMinter;
|
|
43
|
+
/** The real browser revoker: releases an object URL's underlying resources. */
|
|
44
|
+
export declare const objectUrlRevoker: UrlRevoker;
|
|
45
|
+
export interface BrowserBlobStorageOptions {
|
|
46
|
+
/** The wrapped adapter that actually holds the bytes. Defaults to a fresh `InMemoryBlobStorage`. */
|
|
47
|
+
inner?: BlobStorageAdapter;
|
|
48
|
+
/** Defaults to {@link objectUrlMinter}; tests inject a deterministic fake. */
|
|
49
|
+
mint?: UrlMinter;
|
|
50
|
+
/** Defaults to {@link objectUrlRevoker}; tests inject a fake to assert revocation. */
|
|
51
|
+
revoke?: UrlRevoker;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Wraps a `BlobStorageAdapter` so `url()`/`resolveUrl()` return a browser-
|
|
55
|
+
* displayable URL instead of the inner adapter's opaque locator (EC-459). See
|
|
56
|
+
* the module doc above for the full rationale.
|
|
57
|
+
*/
|
|
58
|
+
export declare class BrowserBlobStorage implements BlobStorageAdapter {
|
|
59
|
+
private readonly inner;
|
|
60
|
+
private readonly mint;
|
|
61
|
+
private readonly revoke;
|
|
62
|
+
/** storageKey → minted display URL, populated at the moment bytes are known. */
|
|
63
|
+
private readonly displayUrls;
|
|
64
|
+
constructor(options?: BrowserBlobStorageOptions);
|
|
65
|
+
/**
|
|
66
|
+
* Mint (or re-mint) the display URL for `key`. A re-put under the same key
|
|
67
|
+
* (re-upload / overwrite) revokes the stale URL first, so no object URL leaks
|
|
68
|
+
* and no surface can ever show stale bytes under a reused key.
|
|
69
|
+
*/
|
|
70
|
+
private remember;
|
|
71
|
+
put(key: string, data: Uint8Array, contentType?: string): Promise<void>;
|
|
72
|
+
get(key: string): Promise<Uint8Array | undefined>;
|
|
73
|
+
url(key: string): string;
|
|
74
|
+
has(key: string): Promise<boolean>;
|
|
75
|
+
delete(key: string): Promise<void>;
|
|
76
|
+
store(data: Uint8Array, contentType?: string): Promise<string>;
|
|
77
|
+
resolveUrl(key: string): Promise<string | undefined>;
|
|
78
|
+
}
|
|
79
|
+
/** Convenience factory — `createBrowserBlobStorage()` wraps a fresh `InMemoryBlobStorage`. */
|
|
80
|
+
export declare function createBrowserBlobStorage(options?: BrowserBlobStorageOptions): BlobStorageAdapter;
|
|
@@ -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"]}
|
package/dist/lib/assets/index.js
CHANGED
|
@@ -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
|