@columnus/shared 0.1.56 → 0.1.59
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/apiProfile.d.ts +2 -0
- package/dist/apiProfile.js +2 -1
- package/dist/apiProfile.js.map +1 -1
- package/dist/assetGraph.d.ts +25 -3
- package/dist/assetGraph.js +30 -3
- package/dist/assetGraph.js.map +1 -1
- package/dist/assetJobs.d.ts +7 -3
- package/dist/assetJobs.js +2 -2
- package/dist/assetJobs.js.map +1 -1
- package/dist/assetSettings.d.ts +0 -6
- package/dist/assetSettings.js.map +1 -1
- package/dist/index.js +42 -64
- package/dist/index.js.map +1 -1
- package/dist/schemaGenerator.d.ts +0 -19
- package/dist/schemaGenerator.js +10 -60
- package/dist/schemaGenerator.js.map +1 -1
- package/dist/share.js.map +1 -1
- package/dist/shareApiBridge.js.map +1 -1
- package/dist/syncChannel.js.map +1 -1
- package/package.json +1 -1
package/dist/assetGraph.d.ts
CHANGED
|
@@ -10,10 +10,10 @@ export type AssetRenditionNode = {
|
|
|
10
10
|
quality?: number;
|
|
11
11
|
};
|
|
12
12
|
/** R11: fixed in v1, not tenant-tunable. Applies to decode *and* output. */
|
|
13
|
-
export declare const MAX_RENDER_MEGAPIXELS =
|
|
13
|
+
export declare const MAX_RENDER_MEGAPIXELS = 500;
|
|
14
14
|
export declare const MAX_RENDER_PIXELS: number;
|
|
15
|
-
/** R11: per-job wall clock for a rendition. */
|
|
16
|
-
export declare const RENDITION_TIMEOUT_MS =
|
|
15
|
+
/** R11: per-job wall clock for a rendition (10 minutes). */
|
|
16
|
+
export declare const RENDITION_TIMEOUT_MS = 600000;
|
|
17
17
|
/**
|
|
18
18
|
* The effective graph for one asset: the family floor plus every extra node
|
|
19
19
|
* contributed by its profiles (G6 — profiles are additive, never subtractive).
|
|
@@ -153,6 +153,14 @@ export declare function resolveAssetSlotDisplay(input: {
|
|
|
153
153
|
kind: AssetDisplayKind;
|
|
154
154
|
storageId?: string;
|
|
155
155
|
};
|
|
156
|
+
/** Normalized focal on the asset (G9); default center when absent. */
|
|
157
|
+
export type AssetFocalPoint = {
|
|
158
|
+
x: number;
|
|
159
|
+
y: number;
|
|
160
|
+
};
|
|
161
|
+
export declare function normalizeDisplayFocal(focal?: AssetFocalPoint | null): AssetFocalPoint;
|
|
162
|
+
/** CSS `object-position` for dense cover chrome (062 consumer paint). */
|
|
163
|
+
export declare function focalObjectPosition(focal?: AssetFocalPoint | null): string;
|
|
156
164
|
/** One row of `resolveAssetDisplay`, as every client consumes it. */
|
|
157
165
|
export type AssetDisplayRow = {
|
|
158
166
|
assetId: string;
|
|
@@ -168,6 +176,9 @@ export type AssetDisplayRow = {
|
|
|
168
176
|
processingStatus?: AssetProcessingStatus;
|
|
169
177
|
contentType?: string;
|
|
170
178
|
size?: number;
|
|
179
|
+
family?: AssetFamily;
|
|
180
|
+
/** Absent → treat as center (compat with pre-focal kernels). */
|
|
181
|
+
focalPoint?: AssetFocalPoint;
|
|
171
182
|
};
|
|
172
183
|
/** What a client paints for one slot: a url, or why there is not one yet. */
|
|
173
184
|
export type AssetSlotView = {
|
|
@@ -175,6 +186,17 @@ export type AssetSlotView = {
|
|
|
175
186
|
pending: boolean;
|
|
176
187
|
failed: boolean;
|
|
177
188
|
};
|
|
189
|
+
/** Client maps derived from a `resolveAssetDisplay` batch (Studio + CMS). */
|
|
190
|
+
export type AssetDisplayMaps = {
|
|
191
|
+
urls: Map<string, string | null>;
|
|
192
|
+
slots: Map<string, AssetSlotView>;
|
|
193
|
+
focals: Map<string, AssetFocalPoint>;
|
|
194
|
+
metadata: Map<string, {
|
|
195
|
+
contentType?: string;
|
|
196
|
+
size: number;
|
|
197
|
+
}>;
|
|
198
|
+
};
|
|
199
|
+
export declare function mapsFromAssetDisplayRows(rows: readonly AssetDisplayRow[], slot?: AssetSlot): AssetDisplayMaps;
|
|
178
200
|
/**
|
|
179
201
|
* Client half of G13/G36, shared so Studio and CMS cannot diverge on it — the
|
|
180
202
|
* two consumers that both put asset stamps in a dense grid.
|
package/dist/assetGraph.js
CHANGED
|
@@ -24,9 +24,9 @@ function normalizeContentType(contentType) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
// packages/@columnus/shared/src/assetGraph.ts
|
|
27
|
-
var MAX_RENDER_MEGAPIXELS =
|
|
27
|
+
var MAX_RENDER_MEGAPIXELS = 500;
|
|
28
28
|
var MAX_RENDER_PIXELS = MAX_RENDER_MEGAPIXELS * 1e6;
|
|
29
|
-
var RENDITION_TIMEOUT_MS =
|
|
29
|
+
var RENDITION_TIMEOUT_MS = 6e5;
|
|
30
30
|
function resolveAssetGraph(familyGraph, profileExtraNodes = []) {
|
|
31
31
|
const byId = /* @__PURE__ */ new Map();
|
|
32
32
|
for (const nodes of profileExtraNodes) {
|
|
@@ -188,6 +188,33 @@ function resolveAssetSlotDisplay(input) {
|
|
|
188
188
|
}
|
|
189
189
|
return { kind: "pending" };
|
|
190
190
|
}
|
|
191
|
+
function normalizeDisplayFocal(focal) {
|
|
192
|
+
return { x: focal?.x ?? 0.5, y: focal?.y ?? 0.5 };
|
|
193
|
+
}
|
|
194
|
+
function focalObjectPosition(focal) {
|
|
195
|
+
const { x, y } = normalizeDisplayFocal(focal);
|
|
196
|
+
return `${x * 100}% ${y * 100}%`;
|
|
197
|
+
}
|
|
198
|
+
function mapsFromAssetDisplayRows(rows, slot = "compact") {
|
|
199
|
+
const urls = /* @__PURE__ */ new Map();
|
|
200
|
+
const slots = /* @__PURE__ */ new Map();
|
|
201
|
+
const focals = /* @__PURE__ */ new Map();
|
|
202
|
+
const metadata = /* @__PURE__ */ new Map();
|
|
203
|
+
for (const row of rows) {
|
|
204
|
+
const id = String(row.assetId);
|
|
205
|
+
const view = assetSlotView(row, slot);
|
|
206
|
+
urls.set(id, view.url);
|
|
207
|
+
slots.set(id, view);
|
|
208
|
+
focals.set(id, normalizeDisplayFocal(row.focalPoint));
|
|
209
|
+
if (row.size !== void 0) {
|
|
210
|
+
metadata.set(id, {
|
|
211
|
+
contentType: row.contentType,
|
|
212
|
+
size: row.size
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return { urls, slots, focals, metadata };
|
|
217
|
+
}
|
|
191
218
|
function assetSlotView(row, slot) {
|
|
192
219
|
const decision = row[slot];
|
|
193
220
|
if (!decision || decision.kind === "original") {
|
|
@@ -209,6 +236,6 @@ function renderPixelCapError(label, size) {
|
|
|
209
236
|
return `${label} is ${(pixels / 1e6).toFixed(1)}MP, above the ${MAX_RENDER_MEGAPIXELS}MP render limit`;
|
|
210
237
|
}
|
|
211
238
|
|
|
212
|
-
export { ASSET_PROCESSING_STATUSES, MAX_RENDER_MEGAPIXELS, MAX_RENDER_PIXELS, RENDITION_TIMEOUT_MS, assertAssetSlotsResolve, assetBuildsRenditions, assetSlotView, childRenditionNodes, pendingRebuildsForSave, planGraphDelta, publicRenditionUrlKeys, renderPixelCapError, renditionNodeFingerprint, renditionPlan, resolveAssetGraph, resolveAssetSlotDisplay, rollupProcessingStatus, rootRenditionNodes, validateRenditionNodes };
|
|
239
|
+
export { ASSET_PROCESSING_STATUSES, MAX_RENDER_MEGAPIXELS, MAX_RENDER_PIXELS, RENDITION_TIMEOUT_MS, assertAssetSlotsResolve, assetBuildsRenditions, assetSlotView, childRenditionNodes, focalObjectPosition, mapsFromAssetDisplayRows, normalizeDisplayFocal, pendingRebuildsForSave, planGraphDelta, publicRenditionUrlKeys, renderPixelCapError, renditionNodeFingerprint, renditionPlan, resolveAssetGraph, resolveAssetSlotDisplay, rollupProcessingStatus, rootRenditionNodes, validateRenditionNodes };
|
|
213
240
|
//# sourceMappingURL=assetGraph.js.map
|
|
214
241
|
//# sourceMappingURL=assetGraph.js.map
|
package/dist/assetGraph.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/assetFamily.ts","../src/assetGraph.ts"],"names":[],"mappings":"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","file":"assetGraph.js","sourcesContent":["// Media family classification shared by the host, the component, Studio, and\n// the generated clients. `vector` was added by 059 S0 (061 grill 2026-08-23):\n// SVG/EPS/Illustrator are their own family so the raster rendition DAG (G38)\n// and the spinner-for-derived rule (G36) never apply to them.\n\nexport const ASSET_FAMILIES = [\"image\", \"document\", \"audio\", \"video\", \"vector\", \"other\"] as const;\nexport type AssetFamily = (typeof ASSET_FAMILIES)[number];\n\nconst ASSET_FAMILY_SET: ReadonlySet<string> = new Set(ASSET_FAMILIES);\n\n/**\n * Narrow an untyped `family` read off a row. Convex hands asset rows back\n * untyped — the table is generated per tenant — so a reader that needs the\n * union has to check it rather than assert it.\n */\nexport function isAssetFamily(value: unknown): value is AssetFamily {\n return typeof value === \"string\" && ASSET_FAMILY_SET.has(value);\n}\n\n/**\n * Vector MIME table (G20/G38). `application/pdf` deliberately stays\n * `document`, which also covers PDF-based `.ai` files.\n */\nconst VECTOR_CONTENT_TYPES = new Set([\n \"image/svg+xml\",\n \"application/postscript\",\n \"application/eps\",\n \"application/x-eps\",\n \"image/eps\",\n \"image/x-eps\",\n \"application/illustrator\",\n \"application/vnd.adobe.illustrator\",\n]);\n\n/** Vector types a browser can render in an `<img>`; the rest fall back to an icon. */\nexport const DISPLAYABLE_VECTOR_CONTENT_TYPES = [\"image/svg+xml\"] as const;\nconst DISPLAYABLE_VECTOR_CONTENT_TYPE_SET: ReadonlySet<string> = new Set(\n DISPLAYABLE_VECTOR_CONTENT_TYPES,\n);\n\n/**\n * G38 raster allowlist: only these get a rendition DAG (and therefore the G36\n * spinner). Animated GIF/WebP are included and rendered **first frame**.\n * Everything else that is family `image` — BMP, ICO, camera RAW — keeps its\n * original and never enters the render pipeline.\n */\nconst RASTER_RENDITION_CONTENT_TYPES: ReadonlySet<string> = new Set([\n \"image/jpeg\",\n \"image/pjpeg\",\n \"image/png\",\n \"image/webp\",\n \"image/avif\",\n \"image/tiff\",\n \"image/heic\",\n \"image/heif\",\n \"image/gif\",\n]);\n\n/** True when this MIME may be decoded into the G21 rendition graph. */\nexport function isRasterRenditionSource(contentType: string | undefined): boolean {\n return RASTER_RENDITION_CONTENT_TYPES.has(normalizeContentType(contentType));\n}\n\n/** `accept` value for a file input restricted to family `vector`. */\nexport const VECTOR_FILE_ACCEPT =\n \".svg,.eps,.ai,image/svg+xml,application/postscript,application/illustrator\";\n\nfunction normalizeContentType(contentType: string | undefined): string {\n return contentType?.split(\";\")[0]?.trim().toLowerCase() ?? \"\";\n}\n\n/** Only `.svg` is inferred from the filename, and only when the MIME is missing. */\nfunction hasVectorExtension(filename: string | undefined): boolean {\n return filename?.trim().toLowerCase().endsWith(\".svg\") ?? false;\n}\n\nexport function isVectorContentType(contentType: string | undefined, filename?: string): boolean {\n const value = normalizeContentType(contentType);\n if (!value) return hasVectorExtension(filename);\n return VECTOR_CONTENT_TYPES.has(value);\n}\n\n/**\n * True when the asset can be shown directly in an `<img>` even though it is\n * not family `image`: SVG only, so EPS keeps the file icon.\n */\nexport function isDisplayableVectorContentType(\n contentType: string | undefined,\n filename?: string,\n): boolean {\n const value = normalizeContentType(contentType);\n if (!value) return hasVectorExtension(filename);\n return DISPLAYABLE_VECTOR_CONTENT_TYPE_SET.has(value);\n}\n\n/** Derive the stable family used by profiles, graphs, and library filtering. */\nexport function assetFamilyForContentType(\n contentType: string | undefined,\n filename?: string,\n): AssetFamily {\n const value = normalizeContentType(contentType);\n if (isVectorContentType(contentType, filename)) return \"vector\";\n if (value.startsWith(\"image/\")) return \"image\";\n if (value.startsWith(\"audio/\")) return \"audio\";\n if (value.startsWith(\"video/\")) return \"video\";\n if (\n value === \"application/pdf\" ||\n value.startsWith(\"text/\") ||\n value.startsWith(\"application/msword\") ||\n value.startsWith(\"application/vnd.openxmlformats-officedocument\") ||\n value.startsWith(\"application/vnd.oasis.opendocument\")\n ) {\n return \"document\";\n }\n return \"other\";\n}\n\n/**\n * G20: ExifTool extract runs for `image` and `vector` only. Rendition graphs\n * are a stricter gate (`assetBuildsRenditions`: raster `image`).\n */\nexport function isExtractableFamily(family: AssetFamily | undefined): boolean {\n return family === \"image\" || family === \"vector\";\n}\n\n/**\n * Library eligibility for an `image` field picker: family `image` plus vector\n * assets a browser can display. EPS stays reachable from `file` fields only.\n */\nexport function isImageFieldPickable(asset: {\n family?: string;\n contentType?: string;\n filename?: string;\n}): boolean {\n if (asset.family === \"image\") return true;\n return isDisplayableVectorContentType(asset.contentType, asset.filename);\n}\n","// Rendition graph resolution (061 G21/G22/R8/R11). Pure, so the kernel plans\n// jobs and the render worker sizes output from the same rules.\n\nimport { isRasterRenditionSource, type AssetFamily } from \"./assetFamily.ts\";\n\nexport type AssetRenditionNode = {\n id: string;\n /** Absent = built from the original; otherwise built from that node's output. */\n parentId?: string;\n op: \"contain\" | \"cover\";\n maxWidth: number;\n maxHeight: number;\n format: string;\n quality?: number;\n};\n\n/** R11: fixed in v1, not tenant-tunable. Applies to decode *and* output. */\nexport const MAX_RENDER_MEGAPIXELS = 50;\nexport const MAX_RENDER_PIXELS = MAX_RENDER_MEGAPIXELS * 1_000_000;\n\n/** R11: per-job wall clock for a rendition. */\nexport const RENDITION_TIMEOUT_MS = 60_000;\n\n/**\n * The effective graph for one asset: the family floor plus every extra node\n * contributed by its profiles (G6 — profiles are additive, never subtractive).\n * A profile re-declaring a family node id loses; the family graph is the floor.\n */\nexport function resolveAssetGraph(\n familyGraph: readonly AssetRenditionNode[] | undefined,\n profileExtraNodes: readonly (readonly AssetRenditionNode[])[] = [],\n): AssetRenditionNode[] {\n const byId = new Map<string, AssetRenditionNode>();\n for (const nodes of profileExtraNodes) {\n for (const node of nodes) byId.set(node.id, node);\n }\n for (const node of familyGraph ?? []) byId.set(node.id, node);\n return [...byId.values()];\n}\n\n/** Nodes built straight from the original — the first wave of jobs (R8). */\nexport function rootRenditionNodes(graph: readonly AssetRenditionNode[]): AssetRenditionNode[] {\n const ids = new Set(graph.map((node) => node.id));\n // A parent that is not in the graph cannot ever be built, so such a node is\n // treated as a root rather than being stranded forever.\n return graph.filter((node) => node.parentId === undefined || !ids.has(node.parentId));\n}\n\n/** Nodes whose source is `nodeId`, enqueued once that node's bytes exist (R8). */\nexport function childRenditionNodes(\n graph: readonly AssetRenditionNode[],\n nodeId: string,\n): AssetRenditionNode[] {\n return graph.filter((node) => node.parentId === nodeId);\n}\n\nconst MAX_RENDITION_NODES = 100;\n\n/**\n * Whether this asset gets a rendition DAG at all (G20/G38/R13): raster `image`\n * only. `vector`, documents, audio, video, and non-allowlisted `image/*` keep\n * the original. Extract is a separate gate (`isExtractableFamily`: image + vector).\n */\nexport function assetBuildsRenditions(\n family: AssetFamily | undefined,\n contentType: string | undefined,\n): boolean {\n return family === \"image\" && isRasterRenditionSource(contentType);\n}\n\n/** Unique ids, known parents, no cycles, positive dims, a format, quality 1–100. */\nexport function validateRenditionNodes(nodes: readonly AssetRenditionNode[], label: string): void {\n if (nodes.length > MAX_RENDITION_NODES) {\n throw new Error(`${label} may have at most ${MAX_RENDITION_NODES} nodes`);\n }\n const byId = new Map<string, AssetRenditionNode>();\n for (const node of nodes) {\n const id = node.id.trim();\n if (!id) throw new Error(`${label} nodes need an id`);\n if (byId.has(id)) throw new Error(`${label} nodes must have unique ids`);\n if (!Number.isSafeInteger(node.maxWidth) || node.maxWidth < 1) {\n throw new Error(`${label} node widths must be positive whole numbers`);\n }\n if (!Number.isSafeInteger(node.maxHeight) || node.maxHeight < 1) {\n throw new Error(`${label} node heights must be positive whole numbers`);\n }\n if (!node.format.trim()) throw new Error(`${label} nodes need an output format`);\n const quality = node.quality;\n if (quality !== undefined && (!Number.isSafeInteger(quality) || quality < 1 || quality > 100)) {\n throw new Error(`${label} node quality must be between 1 and 100`);\n }\n byId.set(id, node);\n }\n const visiting = new Set<string>();\n const visited = new Set<string>();\n const visit = (id: string): void => {\n if (visited.has(id)) return;\n if (visiting.has(id)) throw new Error(`${label} nodes cannot contain a cycle`);\n visiting.add(id);\n const parentId = byId.get(id)?.parentId;\n if (parentId) {\n if (!byId.has(parentId)) throw new Error(`${label} node “${id}” has an unknown parent`);\n visit(parentId);\n }\n visiting.delete(id);\n visited.add(id);\n };\n for (const id of byId.keys()) visit(id);\n}\n\n/**\n * The library chip's states (R6). One tuple so the Convex validator, the\n * generated tenant schema, and the generated client are all written from the\n * same list rather than re-spelling it.\n */\nexport const ASSET_PROCESSING_STATUSES = [\"ready\", \"pending\", \"failed\", \"dead_letter\"] as const;\nexport type AssetProcessingStatus = (typeof ASSET_PROCESSING_STATUSES)[number];\n\n/**\n * R6: the library chip reflects the **whole** current graph, so it is pending\n * until every node exists and failed as soon as any job gives up. Extract is\n * deliberately not part of this.\n */\nexport function rollupProcessingStatus(\n graph: readonly AssetRenditionNode[],\n builtNodeIds: Iterable<string>,\n failure?: \"failed\" | \"dead_letter\",\n): AssetProcessingStatus {\n if (failure) return failure;\n const built = new Set(builtNodeIds);\n return graph.every((node) => built.has(node.id)) ? \"ready\" : \"pending\";\n}\n\nexport type RenditionPlan = {\n /** Resize the source to this first. */\n resize: { width: number; height: number };\n /** `cover` only: the window to take out of the resized image. */\n crop?: { width: number; height: number; left: number; top: number };\n /** Final output dimensions. */\n output: { width: number; height: number };\n};\n\n/**\n * How to turn a source into one node's output.\n *\n * `contain` scales to fit inside the box. `cover` scales to fill it and then\n * crops, with the window centred on the asset's focal point (G9/G26) rather\n * than on the middle of the frame. Neither ever upscales — enlarging a small\n * original wastes bytes and looks worse than letting the consumer scale it.\n */\nexport function renditionPlan(\n node: Pick<AssetRenditionNode, \"op\" | \"maxWidth\" | \"maxHeight\">,\n source: { width: number; height: number },\n focal: { x: number; y: number } = { x: 0.5, y: 0.5 },\n): RenditionPlan {\n const empty = { width: 0, height: 0 };\n if (source.width <= 0 || source.height <= 0) return { resize: empty, output: empty };\n\n const scaleX = node.maxWidth / source.width;\n const scaleY = node.maxHeight / source.height;\n const scale = Math.min(\n node.op === \"cover\" ? Math.max(scaleX, scaleY) : Math.min(scaleX, scaleY),\n 1,\n );\n const resize = {\n width: Math.max(1, Math.round(source.width * scale)),\n height: Math.max(1, Math.round(source.height * scale)),\n };\n if (node.op !== \"cover\") return { resize, output: resize };\n\n // Never crop to more than we have: a source smaller than the box yields the\n // whole (downscaled) image rather than an upscaled slice.\n const output = {\n width: Math.min(node.maxWidth, resize.width),\n height: Math.min(node.maxHeight, resize.height),\n };\n const clamp = (value: number, max: number) => Math.max(0, Math.min(Math.round(value), max));\n const crop = {\n ...output,\n left: clamp(resize.width * focal.x - output.width / 2, resize.width - output.width),\n top: clamp(resize.height * focal.y - output.height / 2, resize.height - output.height),\n };\n return { resize, crop, output };\n}\n\n/**\n * Identity of a node's *render-affecting* parameters (K3).\n *\n * The node id alone is not enough to key a job: widening `thumb` from 400 to\n * 600 keeps the id, so an enqueue that dedupes on id alone is swallowed while\n * the old job is still in flight, and the stale result is then recorded as\n * current. Including this in the job key makes the changed node a genuinely\n * different unit of work, and lets a late-finishing stale job be discarded.\n *\n * `parentId` is in here because changing a node's source changes its output.\n */\nexport function renditionNodeFingerprint(\n node: Pick<\n AssetRenditionNode,\n \"parentId\" | \"op\" | \"maxWidth\" | \"maxHeight\" | \"format\" | \"quality\"\n >,\n): string {\n return [\n node.parentId ?? \"\",\n node.op,\n node.maxWidth,\n node.maxHeight,\n node.format,\n node.quality ?? \"\",\n ].join(\"|\");\n}\n\nexport type GraphDelta = {\n /** Nodes to (re)build: added, changed, or downstream of a change. */\n rebuild: string[];\n /** Nodes the graph no longer has; their bytes become obsolete (G34). */\n removed: string[];\n};\n\n/**\n * G22/R10: what a graph edit actually costs. A changed node drags its\n * descendants with it, because they are rendered *from* it. Nodes that are\n * untouched and whose ancestors are untouched keep their existing bytes — the\n * whole point of a delta rather than a full rebuild.\n *\n * A save that changes nothing render-affecting (reordering, or a caps-only or\n * slot-mapping-only save) yields empty lists, which is what lets the UI skip\n * the confirm entirely.\n */\nexport function planGraphDelta(\n previous: readonly AssetRenditionNode[],\n next: readonly AssetRenditionNode[],\n): GraphDelta {\n const before = new Map(previous.map((node) => [node.id, node]));\n const stillPresent = new Set(next.map((node) => node.id));\n\n const dirty = new Set<string>();\n for (const node of next) {\n const old = before.get(node.id);\n if (!old || renditionNodeFingerprint(old) !== renditionNodeFingerprint(node)) {\n dirty.add(node.id);\n }\n }\n\n // Walk downstream until nothing new is marked: a child of a dirty node is\n // itself dirty, however deep the chain.\n let changed = true;\n while (changed) {\n changed = false;\n for (const node of next) {\n if (dirty.has(node.id)) continue;\n if (node.parentId !== undefined && dirty.has(node.parentId)) {\n dirty.add(node.id);\n changed = true;\n }\n }\n }\n\n return {\n rebuild: next.filter((node) => dirty.has(node.id)).map((node) => node.id),\n removed: previous.filter((node) => !stillPresent.has(node.id)).map((node) => node.id),\n };\n}\n\ntype FamilyGraphs = Partial<Record<AssetFamily, readonly AssetRenditionNode[]>>;\ntype FamilySlots = Partial<Record<AssetFamily, { compact?: string; preview?: string }>>;\n\n/**\n * Per-family deltas for one settings save. Families whose graph did not change\n * contribute nothing, so the drain only walks the library when there is real\n * work — and a node id shared across two families cannot cross-trigger.\n */\nexport function pendingRebuildsForSave(\n previous: FamilyGraphs | undefined,\n next: FamilyGraphs,\n): { family: AssetFamily; rebuild: string[]; removed: string[] }[] {\n const families = new Set<string>([...Object.keys(previous ?? {}), ...Object.keys(next ?? {})]);\n const pending: { family: AssetFamily; rebuild: string[]; removed: string[] }[] = [];\n for (const family of families) {\n const delta = planGraphDelta(\n previous?.[family as AssetFamily] ?? [],\n next?.[family as AssetFamily] ?? [],\n );\n if (delta.rebuild.length === 0 && delta.removed.length === 0) continue;\n pending.push({ family: family as AssetFamily, ...delta });\n }\n return pending;\n}\n\n/**\n * R3: Studio's `compact` / `preview` slots point at graph node ids. Validated\n * on save so a mis-typed slot fails loudly instead of silently falling back to\n * the original at read time.\n */\nexport function assertAssetSlotsResolve(\n familyGraphs: FamilyGraphs,\n slots: FamilySlots | undefined,\n): void {\n for (const [family, mapping] of Object.entries(slots ?? {})) {\n const ids = new Set((familyGraphs?.[family as AssetFamily] ?? []).map((node) => node.id));\n for (const [slot, nodeId] of Object.entries(mapping ?? {})) {\n if (nodeId === undefined || nodeId === \"\") continue;\n if (!ids.has(nodeId)) {\n throw new Error(\n `Media ${family} ${slot} slot points at “${nodeId}”, which is not a node in that graph`,\n );\n }\n }\n }\n}\n\nexport type AssetSlot = \"compact\" | \"preview\";\n\n/**\n * What Studio should paint for one asset in one slot (G13/G36).\n *\n * The rule that matters is the third case. For a raster image whose graph is\n * still building, we show a spinner rather than the original: the original can\n * be a 40MP master, and quietly serving it in a grid cell is how a \"fast\" list\n * becomes hundreds of megabytes. Anything that never gets a rendition — vector,\n * documents, BMP/ICO/RAW — keeps its original with no spinner, because waiting\n * for a derivative that is never coming would spin forever (R13).\n */\nexport type AssetDisplayKind = \"rendition\" | \"original\" | \"pending\" | \"failed\";\n\nexport function resolveAssetSlotDisplay(input: {\n family: AssetFamily | undefined;\n contentType: string | undefined;\n processingStatus: AssetProcessingStatus;\n /** Node id this slot maps to, from settings. */\n slotNodeId: string | undefined;\n /** Built nodes for the current version. */\n renditions: Record<string, string> | undefined;\n}): { kind: AssetDisplayKind; storageId?: string } {\n // Non-DAG assets never acquire a rendition, so they are always their original.\n if (!assetBuildsRenditions(input.family, input.contentType)) return { kind: \"original\" };\n // An unmapped slot means the tenant opted out of a derivative here (R3).\n if (!input.slotNodeId) return { kind: \"original\" };\n\n const storageId = input.renditions?.[input.slotNodeId];\n if (storageId) return { kind: \"rendition\", storageId };\n if (input.processingStatus === \"failed\" || input.processingStatus === \"dead_letter\") {\n return { kind: \"failed\" };\n }\n return { kind: \"pending\" };\n}\n\n/** One row of `resolveAssetDisplay`, as every client consumes it. */\nexport type AssetDisplayRow = {\n assetId: string;\n originalUrl: string | null;\n compact?: { kind: AssetDisplayKind; url: string | null };\n preview?: { kind: AssetDisplayKind; url: string | null };\n processingStatus?: AssetProcessingStatus;\n contentType?: string;\n size?: number;\n};\n\n/** What a client paints for one slot: a url, or why there is not one yet. */\nexport type AssetSlotView = {\n url: string | null;\n pending: boolean;\n failed: boolean;\n};\n\n/**\n * Client half of G13/G36, shared so Studio and CMS cannot diverge on it — the\n * two consumers that both put asset stamps in a dense grid.\n *\n * `pending` and `failed` deliberately return a `null` url and never\n * `originalUrl`: falling back there is exactly the 40MP-master-in-a-grid-cell\n * failure `resolveAssetSlotDisplay` exists to prevent. A missing decision means\n * a pre-cutover server, so that degrades to the original rather than blanking.\n */\nexport function assetSlotView(row: AssetDisplayRow, slot: AssetSlot): AssetSlotView {\n const decision = row[slot];\n if (!decision || decision.kind === \"original\") {\n return { url: row.originalUrl, pending: false, failed: false };\n }\n return {\n url: decision.url,\n pending: decision.kind === \"pending\",\n failed: decision.kind === \"failed\",\n };\n}\n\n/**\n * R4: the public `urls` map is keyed by **node id**, and keys for nodes that\n * are missing, still building, or already retired are omitted rather than sent\n * as null — a consumer that sees a key can rely on it.\n */\nexport function publicRenditionUrlKeys(\n graph: readonly AssetRenditionNode[],\n renditions: Record<string, string> | undefined,\n): string[] {\n const built = renditions ?? {};\n return graph.filter((node) => built[node.id] !== undefined).map((node) => node.id);\n}\n\n/** R11: refuse rather than silently downscaling past the cap. */\nexport function renderPixelCapError(\n label: string,\n size: { width: number; height: number },\n): string | null {\n const pixels = size.width * size.height;\n if (pixels <= MAX_RENDER_PIXELS) return null;\n return `${label} is ${(pixels / 1_000_000).toFixed(1)}MP, above the ${MAX_RENDER_MEGAPIXELS}MP render limit`;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/assetFamily.ts","../src/assetGraph.ts"],"names":[],"mappings":"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","file":"assetGraph.js","sourcesContent":["// Media family classification shared by the host, the component, Studio, and\n// the generated clients. `vector` was added by 059 S0 (061 grill 2026-08-23):\n// SVG/EPS/Illustrator are their own family so the raster rendition DAG (G38)\n// and the spinner-for-derived rule (G36) never apply to them.\n\nexport const ASSET_FAMILIES = [\"image\", \"document\", \"audio\", \"video\", \"vector\", \"other\"] as const;\nexport type AssetFamily = (typeof ASSET_FAMILIES)[number];\n\nconst ASSET_FAMILY_SET: ReadonlySet<string> = new Set(ASSET_FAMILIES);\n\n/**\n * Narrow an untyped `family` read off a row. Convex hands asset rows back\n * untyped — the table is generated per tenant — so a reader that needs the\n * union has to check it rather than assert it.\n */\nexport function isAssetFamily(value: unknown): value is AssetFamily {\n return typeof value === \"string\" && ASSET_FAMILY_SET.has(value);\n}\n\n/**\n * Vector MIME table (G20/G38). `application/pdf` deliberately stays\n * `document`, which also covers PDF-based `.ai` files.\n */\nconst VECTOR_CONTENT_TYPES = new Set([\n \"image/svg+xml\",\n \"application/postscript\",\n \"application/eps\",\n \"application/x-eps\",\n \"image/eps\",\n \"image/x-eps\",\n \"application/illustrator\",\n \"application/vnd.adobe.illustrator\",\n]);\n\n/** Vector types a browser can render in an `<img>`; the rest fall back to an icon. */\nexport const DISPLAYABLE_VECTOR_CONTENT_TYPES = [\"image/svg+xml\"] as const;\nconst DISPLAYABLE_VECTOR_CONTENT_TYPE_SET: ReadonlySet<string> = new Set(\n DISPLAYABLE_VECTOR_CONTENT_TYPES,\n);\n\n/**\n * G38 raster allowlist: only these get a rendition DAG (and therefore the G36\n * spinner). Animated GIF/WebP are included and rendered **first frame**.\n * Everything else that is family `image` — BMP, ICO, camera RAW — keeps its\n * original and never enters the render pipeline.\n */\nconst RASTER_RENDITION_CONTENT_TYPES: ReadonlySet<string> = new Set([\n \"image/jpeg\",\n \"image/pjpeg\",\n \"image/png\",\n \"image/webp\",\n \"image/avif\",\n \"image/tiff\",\n \"image/heic\",\n \"image/heif\",\n \"image/gif\",\n]);\n\n/** True when this MIME may be decoded into the G21 rendition graph. */\nexport function isRasterRenditionSource(contentType: string | undefined): boolean {\n return RASTER_RENDITION_CONTENT_TYPES.has(normalizeContentType(contentType));\n}\n\n/** `accept` value for a file input restricted to family `vector`. */\nexport const VECTOR_FILE_ACCEPT =\n \".svg,.eps,.ai,image/svg+xml,application/postscript,application/illustrator\";\n\nfunction normalizeContentType(contentType: string | undefined): string {\n return contentType?.split(\";\")[0]?.trim().toLowerCase() ?? \"\";\n}\n\n/** Only `.svg` is inferred from the filename, and only when the MIME is missing. */\nfunction hasVectorExtension(filename: string | undefined): boolean {\n return filename?.trim().toLowerCase().endsWith(\".svg\") ?? false;\n}\n\nexport function isVectorContentType(contentType: string | undefined, filename?: string): boolean {\n const value = normalizeContentType(contentType);\n if (!value) return hasVectorExtension(filename);\n return VECTOR_CONTENT_TYPES.has(value);\n}\n\n/**\n * True when the asset can be shown directly in an `<img>` even though it is\n * not family `image`: SVG only, so EPS keeps the file icon.\n */\nexport function isDisplayableVectorContentType(\n contentType: string | undefined,\n filename?: string,\n): boolean {\n const value = normalizeContentType(contentType);\n if (!value) return hasVectorExtension(filename);\n return DISPLAYABLE_VECTOR_CONTENT_TYPE_SET.has(value);\n}\n\n/** Derive the stable family used by profiles, graphs, and library filtering. */\nexport function assetFamilyForContentType(\n contentType: string | undefined,\n filename?: string,\n): AssetFamily {\n const value = normalizeContentType(contentType);\n if (isVectorContentType(contentType, filename)) return \"vector\";\n if (value.startsWith(\"image/\")) return \"image\";\n if (value.startsWith(\"audio/\")) return \"audio\";\n if (value.startsWith(\"video/\")) return \"video\";\n if (\n value === \"application/pdf\" ||\n value.startsWith(\"text/\") ||\n value.startsWith(\"application/msword\") ||\n value.startsWith(\"application/vnd.openxmlformats-officedocument\") ||\n value.startsWith(\"application/vnd.oasis.opendocument\")\n ) {\n return \"document\";\n }\n return \"other\";\n}\n\n/**\n * G20: ExifTool extract runs for `image` and `vector` only. Rendition graphs\n * are a stricter gate (`assetBuildsRenditions`: raster `image`).\n */\nexport function isExtractableFamily(family: AssetFamily | undefined): boolean {\n return family === \"image\" || family === \"vector\";\n}\n\n/**\n * Library eligibility for an `image` field picker: family `image` plus vector\n * assets a browser can display. EPS stays reachable from `file` fields only.\n */\nexport function isImageFieldPickable(asset: {\n family?: string;\n contentType?: string;\n filename?: string;\n}): boolean {\n if (asset.family === \"image\") return true;\n return isDisplayableVectorContentType(asset.contentType, asset.filename);\n}\n","// Rendition graph resolution (061 G21/G22/R8/R11). Pure, so the kernel plans\n// jobs and the render worker sizes output from the same rules.\n\nimport { isRasterRenditionSource, type AssetFamily } from \"./assetFamily.ts\";\n\nexport type AssetRenditionNode = {\n id: string;\n /** Absent = built from the original; otherwise built from that node's output. */\n parentId?: string;\n op: \"contain\" | \"cover\";\n maxWidth: number;\n maxHeight: number;\n format: string;\n quality?: number;\n};\n\n/** R11: fixed in v1, not tenant-tunable. Applies to decode *and* output. */\nexport const MAX_RENDER_MEGAPIXELS = 500;\nexport const MAX_RENDER_PIXELS = MAX_RENDER_MEGAPIXELS * 1_000_000;\n\n/** R11: per-job wall clock for a rendition (10 minutes). */\nexport const RENDITION_TIMEOUT_MS = 600_000;\n\n/**\n * The effective graph for one asset: the family floor plus every extra node\n * contributed by its profiles (G6 — profiles are additive, never subtractive).\n * A profile re-declaring a family node id loses; the family graph is the floor.\n */\nexport function resolveAssetGraph(\n familyGraph: readonly AssetRenditionNode[] | undefined,\n profileExtraNodes: readonly (readonly AssetRenditionNode[])[] = [],\n): AssetRenditionNode[] {\n const byId = new Map<string, AssetRenditionNode>();\n for (const nodes of profileExtraNodes) {\n for (const node of nodes) byId.set(node.id, node);\n }\n for (const node of familyGraph ?? []) byId.set(node.id, node);\n return [...byId.values()];\n}\n\n/** Nodes built straight from the original — the first wave of jobs (R8). */\nexport function rootRenditionNodes(graph: readonly AssetRenditionNode[]): AssetRenditionNode[] {\n const ids = new Set(graph.map((node) => node.id));\n // A parent that is not in the graph cannot ever be built, so such a node is\n // treated as a root rather than being stranded forever.\n return graph.filter((node) => node.parentId === undefined || !ids.has(node.parentId));\n}\n\n/** Nodes whose source is `nodeId`, enqueued once that node's bytes exist (R8). */\nexport function childRenditionNodes(\n graph: readonly AssetRenditionNode[],\n nodeId: string,\n): AssetRenditionNode[] {\n return graph.filter((node) => node.parentId === nodeId);\n}\n\nconst MAX_RENDITION_NODES = 100;\n\n/**\n * Whether this asset gets a rendition DAG at all (G20/G38/R13): raster `image`\n * only. `vector`, documents, audio, video, and non-allowlisted `image/*` keep\n * the original. Extract is a separate gate (`isExtractableFamily`: image + vector).\n */\nexport function assetBuildsRenditions(\n family: AssetFamily | undefined,\n contentType: string | undefined,\n): boolean {\n return family === \"image\" && isRasterRenditionSource(contentType);\n}\n\n/** Unique ids, known parents, no cycles, positive dims, a format, quality 1–100. */\nexport function validateRenditionNodes(nodes: readonly AssetRenditionNode[], label: string): void {\n if (nodes.length > MAX_RENDITION_NODES) {\n throw new Error(`${label} may have at most ${MAX_RENDITION_NODES} nodes`);\n }\n const byId = new Map<string, AssetRenditionNode>();\n for (const node of nodes) {\n const id = node.id.trim();\n if (!id) throw new Error(`${label} nodes need an id`);\n if (byId.has(id)) throw new Error(`${label} nodes must have unique ids`);\n if (!Number.isSafeInteger(node.maxWidth) || node.maxWidth < 1) {\n throw new Error(`${label} node widths must be positive whole numbers`);\n }\n if (!Number.isSafeInteger(node.maxHeight) || node.maxHeight < 1) {\n throw new Error(`${label} node heights must be positive whole numbers`);\n }\n if (!node.format.trim()) throw new Error(`${label} nodes need an output format`);\n const quality = node.quality;\n if (quality !== undefined && (!Number.isSafeInteger(quality) || quality < 1 || quality > 100)) {\n throw new Error(`${label} node quality must be between 1 and 100`);\n }\n byId.set(id, node);\n }\n const visiting = new Set<string>();\n const visited = new Set<string>();\n const visit = (id: string): void => {\n if (visited.has(id)) return;\n if (visiting.has(id)) throw new Error(`${label} nodes cannot contain a cycle`);\n visiting.add(id);\n const parentId = byId.get(id)?.parentId;\n if (parentId) {\n if (!byId.has(parentId)) throw new Error(`${label} node “${id}” has an unknown parent`);\n visit(parentId);\n }\n visiting.delete(id);\n visited.add(id);\n };\n for (const id of byId.keys()) visit(id);\n}\n\n/**\n * The library chip's states (R6). One tuple so the Convex validator, the\n * generated tenant schema, and the generated client are all written from the\n * same list rather than re-spelling it.\n */\nexport const ASSET_PROCESSING_STATUSES = [\"ready\", \"pending\", \"failed\", \"dead_letter\"] as const;\nexport type AssetProcessingStatus = (typeof ASSET_PROCESSING_STATUSES)[number];\n\n/**\n * R6: the library chip reflects the **whole** current graph, so it is pending\n * until every node exists and failed as soon as any job gives up. Extract is\n * deliberately not part of this.\n */\nexport function rollupProcessingStatus(\n graph: readonly AssetRenditionNode[],\n builtNodeIds: Iterable<string>,\n failure?: \"failed\" | \"dead_letter\",\n): AssetProcessingStatus {\n if (failure) return failure;\n const built = new Set(builtNodeIds);\n return graph.every((node) => built.has(node.id)) ? \"ready\" : \"pending\";\n}\n\nexport type RenditionPlan = {\n /** Resize the source to this first. */\n resize: { width: number; height: number };\n /** `cover` only: the window to take out of the resized image. */\n crop?: { width: number; height: number; left: number; top: number };\n /** Final output dimensions. */\n output: { width: number; height: number };\n};\n\n/**\n * How to turn a source into one node's output.\n *\n * `contain` scales to fit inside the box. `cover` scales to fill it and then\n * crops, with the window centred on the asset's focal point (G9/G26) rather\n * than on the middle of the frame. Neither ever upscales — enlarging a small\n * original wastes bytes and looks worse than letting the consumer scale it.\n */\nexport function renditionPlan(\n node: Pick<AssetRenditionNode, \"op\" | \"maxWidth\" | \"maxHeight\">,\n source: { width: number; height: number },\n focal: { x: number; y: number } = { x: 0.5, y: 0.5 },\n): RenditionPlan {\n const empty = { width: 0, height: 0 };\n if (source.width <= 0 || source.height <= 0) return { resize: empty, output: empty };\n\n const scaleX = node.maxWidth / source.width;\n const scaleY = node.maxHeight / source.height;\n const scale = Math.min(\n node.op === \"cover\" ? Math.max(scaleX, scaleY) : Math.min(scaleX, scaleY),\n 1,\n );\n const resize = {\n width: Math.max(1, Math.round(source.width * scale)),\n height: Math.max(1, Math.round(source.height * scale)),\n };\n if (node.op !== \"cover\") return { resize, output: resize };\n\n // Never crop to more than we have: a source smaller than the box yields the\n // whole (downscaled) image rather than an upscaled slice.\n const output = {\n width: Math.min(node.maxWidth, resize.width),\n height: Math.min(node.maxHeight, resize.height),\n };\n const clamp = (value: number, max: number) => Math.max(0, Math.min(Math.round(value), max));\n const crop = {\n ...output,\n left: clamp(resize.width * focal.x - output.width / 2, resize.width - output.width),\n top: clamp(resize.height * focal.y - output.height / 2, resize.height - output.height),\n };\n return { resize, crop, output };\n}\n\n/**\n * Identity of a node's *render-affecting* parameters (K3).\n *\n * The node id alone is not enough to key a job: widening `thumb` from 400 to\n * 600 keeps the id, so an enqueue that dedupes on id alone is swallowed while\n * the old job is still in flight, and the stale result is then recorded as\n * current. Including this in the job key makes the changed node a genuinely\n * different unit of work, and lets a late-finishing stale job be discarded.\n *\n * `parentId` is in here because changing a node's source changes its output.\n */\nexport function renditionNodeFingerprint(\n node: Pick<\n AssetRenditionNode,\n \"parentId\" | \"op\" | \"maxWidth\" | \"maxHeight\" | \"format\" | \"quality\"\n >,\n): string {\n return [\n node.parentId ?? \"\",\n node.op,\n node.maxWidth,\n node.maxHeight,\n node.format,\n node.quality ?? \"\",\n ].join(\"|\");\n}\n\nexport type GraphDelta = {\n /** Nodes to (re)build: added, changed, or downstream of a change. */\n rebuild: string[];\n /** Nodes the graph no longer has; their bytes become obsolete (G34). */\n removed: string[];\n};\n\n/**\n * G22/R10: what a graph edit actually costs. A changed node drags its\n * descendants with it, because they are rendered *from* it. Nodes that are\n * untouched and whose ancestors are untouched keep their existing bytes — the\n * whole point of a delta rather than a full rebuild.\n *\n * A save that changes nothing render-affecting (reordering, or a caps-only or\n * slot-mapping-only save) yields empty lists, which is what lets the UI skip\n * the confirm entirely.\n */\nexport function planGraphDelta(\n previous: readonly AssetRenditionNode[],\n next: readonly AssetRenditionNode[],\n): GraphDelta {\n const before = new Map(previous.map((node) => [node.id, node]));\n const stillPresent = new Set(next.map((node) => node.id));\n\n const dirty = new Set<string>();\n for (const node of next) {\n const old = before.get(node.id);\n if (!old || renditionNodeFingerprint(old) !== renditionNodeFingerprint(node)) {\n dirty.add(node.id);\n }\n }\n\n // Walk downstream until nothing new is marked: a child of a dirty node is\n // itself dirty, however deep the chain.\n let changed = true;\n while (changed) {\n changed = false;\n for (const node of next) {\n if (dirty.has(node.id)) continue;\n if (node.parentId !== undefined && dirty.has(node.parentId)) {\n dirty.add(node.id);\n changed = true;\n }\n }\n }\n\n return {\n rebuild: next.filter((node) => dirty.has(node.id)).map((node) => node.id),\n removed: previous.filter((node) => !stillPresent.has(node.id)).map((node) => node.id),\n };\n}\n\ntype FamilyGraphs = Partial<Record<AssetFamily, readonly AssetRenditionNode[]>>;\ntype FamilySlots = Partial<Record<AssetFamily, { compact?: string; preview?: string }>>;\n\n/**\n * Per-family deltas for one settings save. Families whose graph did not change\n * contribute nothing, so the drain only walks the library when there is real\n * work — and a node id shared across two families cannot cross-trigger.\n */\nexport function pendingRebuildsForSave(\n previous: FamilyGraphs | undefined,\n next: FamilyGraphs,\n): { family: AssetFamily; rebuild: string[]; removed: string[] }[] {\n const families = new Set<string>([...Object.keys(previous ?? {}), ...Object.keys(next ?? {})]);\n const pending: { family: AssetFamily; rebuild: string[]; removed: string[] }[] = [];\n for (const family of families) {\n const delta = planGraphDelta(\n previous?.[family as AssetFamily] ?? [],\n next?.[family as AssetFamily] ?? [],\n );\n if (delta.rebuild.length === 0 && delta.removed.length === 0) continue;\n pending.push({ family: family as AssetFamily, ...delta });\n }\n return pending;\n}\n\n/**\n * R3: Studio's `compact` / `preview` slots point at graph node ids. Validated\n * on save so a mis-typed slot fails loudly instead of silently falling back to\n * the original at read time.\n */\nexport function assertAssetSlotsResolve(\n familyGraphs: FamilyGraphs,\n slots: FamilySlots | undefined,\n): void {\n for (const [family, mapping] of Object.entries(slots ?? {})) {\n const ids = new Set((familyGraphs?.[family as AssetFamily] ?? []).map((node) => node.id));\n for (const [slot, nodeId] of Object.entries(mapping ?? {})) {\n if (nodeId === undefined || nodeId === \"\") continue;\n if (!ids.has(nodeId)) {\n throw new Error(\n `Media ${family} ${slot} slot points at “${nodeId}”, which is not a node in that graph`,\n );\n }\n }\n }\n}\n\nexport type AssetSlot = \"compact\" | \"preview\";\n\n/**\n * What Studio should paint for one asset in one slot (G13/G36).\n *\n * The rule that matters is the third case. For a raster image whose graph is\n * still building, we show a spinner rather than the original: the original can\n * be a 40MP master, and quietly serving it in a grid cell is how a \"fast\" list\n * becomes hundreds of megabytes. Anything that never gets a rendition — vector,\n * documents, BMP/ICO/RAW — keeps its original with no spinner, because waiting\n * for a derivative that is never coming would spin forever (R13).\n */\nexport type AssetDisplayKind = \"rendition\" | \"original\" | \"pending\" | \"failed\";\n\nexport function resolveAssetSlotDisplay(input: {\n family: AssetFamily | undefined;\n contentType: string | undefined;\n processingStatus: AssetProcessingStatus;\n /** Node id this slot maps to, from settings. */\n slotNodeId: string | undefined;\n /** Built nodes for the current version. */\n renditions: Record<string, string> | undefined;\n}): { kind: AssetDisplayKind; storageId?: string } {\n // Non-DAG assets never acquire a rendition, so they are always their original.\n if (!assetBuildsRenditions(input.family, input.contentType)) return { kind: \"original\" };\n // An unmapped slot means the tenant opted out of a derivative here (R3).\n if (!input.slotNodeId) return { kind: \"original\" };\n\n const storageId = input.renditions?.[input.slotNodeId];\n if (storageId) return { kind: \"rendition\", storageId };\n if (input.processingStatus === \"failed\" || input.processingStatus === \"dead_letter\") {\n return { kind: \"failed\" };\n }\n return { kind: \"pending\" };\n}\n\n/** Normalized focal on the asset (G9); default center when absent. */\nexport type AssetFocalPoint = { x: number; y: number };\n\nexport function normalizeDisplayFocal(focal?: AssetFocalPoint | null): AssetFocalPoint {\n return { x: focal?.x ?? 0.5, y: focal?.y ?? 0.5 };\n}\n\n/** CSS `object-position` for dense cover chrome (062 consumer paint). */\nexport function focalObjectPosition(focal?: AssetFocalPoint | null): string {\n const { x, y } = normalizeDisplayFocal(focal);\n return `${x * 100}% ${y * 100}%`;\n}\n\n/** One row of `resolveAssetDisplay`, as every client consumes it. */\nexport type AssetDisplayRow = {\n assetId: string;\n originalUrl: string | null;\n compact?: { kind: AssetDisplayKind; url: string | null };\n preview?: { kind: AssetDisplayKind; url: string | null };\n processingStatus?: AssetProcessingStatus;\n contentType?: string;\n size?: number;\n family?: AssetFamily;\n /** Absent → treat as center (compat with pre-focal kernels). */\n focalPoint?: AssetFocalPoint;\n};\n\n/** What a client paints for one slot: a url, or why there is not one yet. */\nexport type AssetSlotView = {\n url: string | null;\n pending: boolean;\n failed: boolean;\n};\n\n/** Client maps derived from a `resolveAssetDisplay` batch (Studio + CMS). */\nexport type AssetDisplayMaps = {\n urls: Map<string, string | null>;\n slots: Map<string, AssetSlotView>;\n focals: Map<string, AssetFocalPoint>;\n metadata: Map<string, { contentType?: string; size: number }>;\n};\n\nexport function mapsFromAssetDisplayRows(\n rows: readonly AssetDisplayRow[],\n slot: AssetSlot = \"compact\",\n): AssetDisplayMaps {\n const urls = new Map<string, string | null>();\n const slots = new Map<string, AssetSlotView>();\n const focals = new Map<string, AssetFocalPoint>();\n const metadata = new Map<string, { contentType?: string; size: number }>();\n for (const row of rows) {\n const id = String(row.assetId);\n const view = assetSlotView(row, slot);\n urls.set(id, view.url);\n slots.set(id, view);\n focals.set(id, normalizeDisplayFocal(row.focalPoint));\n if (row.size !== undefined) {\n metadata.set(id, {\n contentType: row.contentType,\n size: row.size,\n });\n }\n }\n return { urls, slots, focals, metadata };\n}\n\n/**\n * Client half of G13/G36, shared so Studio and CMS cannot diverge on it — the\n * two consumers that both put asset stamps in a dense grid.\n *\n * `pending` and `failed` deliberately return a `null` url and never\n * `originalUrl`: falling back there is exactly the 40MP-master-in-a-grid-cell\n * failure `resolveAssetSlotDisplay` exists to prevent. A missing decision means\n * a pre-cutover server, so that degrades to the original rather than blanking.\n */\nexport function assetSlotView(row: AssetDisplayRow, slot: AssetSlot): AssetSlotView {\n const decision = row[slot];\n if (!decision || decision.kind === \"original\") {\n return { url: row.originalUrl, pending: false, failed: false };\n }\n return {\n url: decision.url,\n pending: decision.kind === \"pending\",\n failed: decision.kind === \"failed\",\n };\n}\n\n/**\n * R4: the public `urls` map is keyed by **node id**, and keys for nodes that\n * are missing, still building, or already retired are omitted rather than sent\n * as null — a consumer that sees a key can rely on it.\n */\nexport function publicRenditionUrlKeys(\n graph: readonly AssetRenditionNode[],\n renditions: Record<string, string> | undefined,\n): string[] {\n const built = renditions ?? {};\n return graph.filter((node) => built[node.id] !== undefined).map((node) => node.id);\n}\n\n/** R11: refuse rather than silently downscaling past the cap. */\nexport function renderPixelCapError(\n label: string,\n size: { width: number; height: number },\n): string | null {\n const pixels = size.width * size.height;\n if (pixels <= MAX_RENDER_PIXELS) return null;\n return `${label} is ${(pixels / 1_000_000).toFixed(1)}MP, above the ${MAX_RENDER_MEGAPIXELS}MP render limit`;\n}\n"]}
|
package/dist/assetJobs.d.ts
CHANGED
|
@@ -5,10 +5,10 @@ export type AssetJobStatus = (typeof ASSET_JOB_STATUSES)[number];
|
|
|
5
5
|
/** R12: five attempts, then the job is parked in the dead-letter queue. */
|
|
6
6
|
export declare const ASSET_JOB_MAX_ATTEMPTS = 5;
|
|
7
7
|
/** R11: per-job wall clock. The lease must outlast it (see below). */
|
|
8
|
-
export declare const ASSET_JOB_TIMEOUT_MS =
|
|
8
|
+
export declare const ASSET_JOB_TIMEOUT_MS = 600000;
|
|
9
9
|
/**
|
|
10
|
-
* Derived lock:
|
|
11
|
-
* but healthy worker is never reclaimed out from under itself.
|
|
10
|
+
* Derived lock: 15 minutes, comfortably above the 10 minute job timeout so a
|
|
11
|
+
* slow but healthy worker is never reclaimed out from under itself.
|
|
12
12
|
*/
|
|
13
13
|
export declare const ASSET_JOB_LEASE_MS: number;
|
|
14
14
|
/**
|
|
@@ -67,6 +67,10 @@ export type AssetJob = {
|
|
|
67
67
|
leaseUntil?: number;
|
|
68
68
|
lastError?: string;
|
|
69
69
|
createdAt: number;
|
|
70
|
+
/** Set on claim; kept after complete / dead-letter. */
|
|
71
|
+
claimedAt?: number;
|
|
72
|
+
/** Set on complete and dead-letter. Absent while the job may retry. */
|
|
73
|
+
completedAt?: number;
|
|
70
74
|
};
|
|
71
75
|
/**
|
|
72
76
|
* When the worker should next wake for a job that is pending but still inside
|
package/dist/assetJobs.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
var ASSET_JOB_KINDS = ["extract", "rendition"];
|
|
3
3
|
var ASSET_JOB_STATUSES = ["pending", "claimed", "done", "failed", "dead_letter"];
|
|
4
4
|
var ASSET_JOB_MAX_ATTEMPTS = 5;
|
|
5
|
-
var ASSET_JOB_TIMEOUT_MS =
|
|
6
|
-
var ASSET_JOB_LEASE_MS =
|
|
5
|
+
var ASSET_JOB_TIMEOUT_MS = 6e5;
|
|
6
|
+
var ASSET_JOB_LEASE_MS = 15 * 6e4;
|
|
7
7
|
var ASSET_JOB_MAX_RECLAIMS = 5;
|
|
8
8
|
var ASSET_JOB_BACKOFF_MS = [15e3, 6e4, 5 * 6e4, 60 * 6e4];
|
|
9
9
|
function assetJobRetryDelayMs(attempts) {
|
package/dist/assetJobs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/assetJobs.ts"],"names":[],"mappings":";AAIO,IAAM,eAAA,GAAkB,CAAC,SAAA,EAAW,WAAW;AAG/C,IAAM,qBAAqB,CAAC,SAAA,EAAW,SAAA,EAAW,MAAA,EAAQ,UAAU,aAAa;AAIjF,IAAM,sBAAA,GAAyB;AAG/B,IAAM,oBAAA,GAAuB;AAM7B,IAAM,qBAAqB,
|
|
1
|
+
{"version":3,"sources":["../src/assetJobs.ts"],"names":[],"mappings":";AAIO,IAAM,eAAA,GAAkB,CAAC,SAAA,EAAW,WAAW;AAG/C,IAAM,qBAAqB,CAAC,SAAA,EAAW,SAAA,EAAW,MAAA,EAAQ,UAAU,aAAa;AAIjF,IAAM,sBAAA,GAAyB;AAG/B,IAAM,oBAAA,GAAuB;AAM7B,IAAM,qBAAqB,EAAA,GAAK;AAQhC,IAAM,sBAAA,GAAyB;AAGtC,IAAM,uBAAuB,CAAC,IAAA,EAAQ,KAAQ,CAAA,GAAI,GAAA,EAAQ,KAAK,GAAM,CAAA;AAO9D,SAAS,qBAAqB,QAAA,EAAiC;AACpE,EAAA,IAAI,QAAA,IAAY,wBAAwB,OAAO,IAAA;AAC/C,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,GAAA,CAAI,CAAA,EAAG,IAAA,CAAK,GAAA,CAAI,QAAA,GAAW,CAAA,EAAG,oBAAA,CAAqB,MAAA,GAAS,CAAC,CAAC,CAAA;AACjF,EAAA,OAAO,oBAAA,CAAqB,KAAK,CAAA,IAAK,IAAA;AACxC;AAGO,SAAS,sBAAA,CACd,UACA,GAAA,EACqE;AACrE,EAAA,MAAM,KAAA,GAAQ,qBAAqB,QAAQ,CAAA;AAC3C,EAAA,IAAI,KAAA,KAAU,IAAA,EAAM,OAAO,EAAE,QAAQ,aAAA,EAAc;AACnD,EAAA,OAAO,EAAE,MAAA,EAAQ,SAAA,EAAW,QAAA,EAAU,MAAM,KAAA,EAAM;AACpD;AAOO,SAAS,uBACd,QAAA,EACmD;AACnD,EAAA,OAAO,QAAA,GAAW,KAAK,sBAAA,GAAyB,EAAE,QAAQ,aAAA,EAAc,GAAI,EAAE,MAAA,EAAQ,SAAA,EAAU;AAClG;AAMO,SAAS,oBAAA,CAAqB,YAAgC,GAAA,EAAsB;AACzF,EAAA,OAAO,UAAA,KAAe,UAAa,UAAA,IAAc,GAAA;AACnD;AAsCO,SAAS,kBAAA,CACd,MACA,GAAA,EACe;AACf,EAAA,IAAI,QAAA,GAA0B,IAAA;AAC9B,EAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,IAAA,IAAI,GAAA,CAAI,MAAA,KAAW,SAAA,IAAa,GAAA,CAAI,YAAY,GAAA,EAAK;AACrD,IAAA,IAAI,aAAa,IAAA,IAAQ,GAAA,CAAI,QAAA,GAAW,QAAA,aAAqB,GAAA,CAAI,QAAA;AAAA,EACnE;AACA,EAAA,OAAO,QAAA;AACT;AAMO,SAAS,YAAY,KAAA,EAUjB;AACT,EAAA,IAAI,MAAM,IAAA,KAAS,WAAA,EAAa,OAAO,CAAA,EAAG,MAAM,SAAS,CAAA,QAAA,CAAA;AACzD,EAAA,MAAM,IAAA,GAAO,MAAM,MAAA,IAAU,EAAA;AAC7B,EAAA,OAAO,KAAA,CAAM,WAAA,GACT,CAAA,EAAG,KAAA,CAAM,SAAS,CAAA,WAAA,EAAc,IAAI,CAAA,CAAA,EAAI,KAAA,CAAM,WAAW,CAAA,CAAA,GACzD,CAAA,EAAG,KAAA,CAAM,SAAS,cAAc,IAAI,CAAA,CAAA;AAC1C","file":"assetJobs.js","sourcesContent":["// Media render job policy (061 R8/R11/R12 + derived leases). Kept pure and\n// outside Convex so the tenant kernel, the render worker, and unit tests all\n// agree on attempt counts, backoff, and lease windows.\n\nexport const ASSET_JOB_KINDS = [\"extract\", \"rendition\"] as const;\nexport type AssetJobKind = (typeof ASSET_JOB_KINDS)[number];\n\nexport const ASSET_JOB_STATUSES = [\"pending\", \"claimed\", \"done\", \"failed\", \"dead_letter\"] as const;\nexport type AssetJobStatus = (typeof ASSET_JOB_STATUSES)[number];\n\n/** R12: five attempts, then the job is parked in the dead-letter queue. */\nexport const ASSET_JOB_MAX_ATTEMPTS = 5;\n\n/** R11: per-job wall clock. The lease must outlast it (see below). */\nexport const ASSET_JOB_TIMEOUT_MS = 600_000;\n\n/**\n * Derived lock: 15 minutes, comfortably above the 10 minute job timeout so a\n * slow but healthy worker is never reclaimed out from under itself.\n */\nexport const ASSET_JOB_LEASE_MS = 15 * 60_000;\n\n/**\n * K1 ceiling. A reclaim hands the attempt back (R12) because a dead machine\n * never got its try — but a job that *kills* whichever worker claims it would\n * then cycle forever. After this many reclaims the job is dead-lettered so it\n * surfaces in Retry instead of silently holding a worker slot.\n */\nexport const ASSET_JOB_MAX_RECLAIMS = 5;\n\n/** R12 backoff ladder, ~15s → 1h, applied after attempts 1..4. */\nconst ASSET_JOB_BACKOFF_MS = [15_000, 60_000, 5 * 60_000, 60 * 60_000] as const;\n\n/**\n * Delay before a failed job becomes claimable again. `attempts` is the count\n * *including* the attempt that just failed, so the first failure waits 15s.\n * Returns null once the job has exhausted its attempts.\n */\nexport function assetJobRetryDelayMs(attempts: number): number | null {\n if (attempts >= ASSET_JOB_MAX_ATTEMPTS) return null;\n const index = Math.max(0, Math.min(attempts - 1, ASSET_JOB_BACKOFF_MS.length - 1));\n return ASSET_JOB_BACKOFF_MS[index] ?? null;\n}\n\n/** R12: a failure either schedules a retry or dead-letters the job. */\nexport function assetJobFailureOutcome(\n attempts: number,\n now: number,\n): { status: \"pending\"; runAfter: number } | { status: \"dead_letter\" } {\n const delay = assetJobRetryDelayMs(attempts);\n if (delay === null) return { status: \"dead_letter\" };\n return { status: \"pending\", runAfter: now + delay };\n}\n\n/**\n * What to do with a claim whose lease elapsed. The attempt is refunded (R12 —\n * the worker died, the job never got its try), but reclaims are counted so a\n * job that reliably kills its worker still reaches the dead-letter queue (K1).\n */\nexport function assetJobReclaimOutcome(\n reclaims: number,\n): { status: \"pending\" } | { status: \"dead_letter\" } {\n return reclaims + 1 >= ASSET_JOB_MAX_RECLAIMS ? { status: \"dead_letter\" } : { status: \"pending\" };\n}\n\n/**\n * A claim is stale once its lease has elapsed. Reclaiming does **not** burn an\n * attempt (R12) — the worker died, the job never got its try.\n */\nexport function assetJobLeaseExpired(leaseUntil: number | undefined, now: number): boolean {\n return leaseUntil === undefined || leaseUntil <= now;\n}\n\n/**\n * One durable job row, as the kernel stores it and the worker sees it. Every\n * layer derives its row/snapshot shape from this instead of re-listing the\n * union members.\n */\nexport type AssetJob = {\n jobId: string;\n assetId: string;\n versionId: string;\n kind: AssetJobKind;\n nodeId?: string;\n /** K3: the node parameters this job was queued for. */\n fingerprint?: string;\n status: AssetJobStatus;\n attempts: number;\n /** Lease reclaims, capped separately from attempts (K1). */\n reclaims?: number;\n runAfter: number;\n leaseUntil?: number;\n lastError?: string;\n createdAt: number;\n /** Set on claim; kept after complete / dead-letter. */\n claimedAt?: number;\n /** Set on complete and dead-letter. Absent while the job may retry. */\n completedAt?: number;\n};\n\n/**\n * When the worker should next wake for a job that is pending but still inside\n * its retry backoff. `null` means nothing is waiting on the clock.\n *\n * The subscribed query cannot do this filtering itself: Convex invalidates a\n * subscription when its read set changes, never when time passes, so a query\n * that filtered on `Date.now()` would leave a matured retry unnoticed until an\n * unrelated write happened to touch the table.\n */\nexport function nextAssetJobWakeAt(\n jobs: readonly Pick<AssetJob, \"status\" | \"runAfter\">[],\n now: number,\n): number | null {\n let earliest: number | null = null;\n for (const job of jobs) {\n if (job.status !== \"pending\" || job.runAfter <= now) continue;\n if (earliest === null || job.runAfter < earliest) earliest = job.runAfter;\n }\n return earliest;\n}\n\n/**\n * Stable identity for one unit of work (R8): one `extract` per version, one\n * `rendition` per node per version. Used to keep enqueue idempotent.\n */\nexport function assetJobKey(input: {\n versionId: string;\n kind: AssetJobKind;\n nodeId?: string;\n /**\n * K3: a rendition's render-affecting parameters. Two jobs for the same node\n * with different parameters are different units of work, so a graph edit is\n * never swallowed by an outstanding job for the old parameters.\n */\n fingerprint?: string;\n}): string {\n if (input.kind !== \"rendition\") return `${input.versionId}:extract`;\n const node = input.nodeId ?? \"\";\n return input.fingerprint\n ? `${input.versionId}:rendition:${node}:${input.fingerprint}`\n : `${input.versionId}:rendition:${node}`;\n}\n"]}
|
package/dist/assetSettings.d.ts
CHANGED
|
@@ -41,11 +41,6 @@ export type AssetRebuildStatus = {
|
|
|
41
41
|
assetsExamined: number;
|
|
42
42
|
enqueued: number;
|
|
43
43
|
};
|
|
44
|
-
export type AssetMigrationStatus = {
|
|
45
|
-
state: "idle" | "running" | "failed" | "done";
|
|
46
|
-
cursor?: string;
|
|
47
|
-
error?: string;
|
|
48
|
-
};
|
|
49
44
|
export type AssetSettings = MediaUploadCaps & {
|
|
50
45
|
familyGraphs: Partial<Record<AssetFamily, AssetRenditionNode[]>>;
|
|
51
46
|
familySchemas: Partial<Record<AssetFamily, AssetSchemaBinding[]>>;
|
|
@@ -53,7 +48,6 @@ export type AssetSettings = MediaUploadCaps & {
|
|
|
53
48
|
slots?: Partial<Record<AssetFamily, AssetSlotMapping>>;
|
|
54
49
|
/** R10: rebuild work a graph save signed up for, drained by cron. */
|
|
55
50
|
rebuildStatus?: AssetRebuildStatus;
|
|
56
|
-
migrateStatus?: AssetMigrationStatus;
|
|
57
51
|
updatedAt: number;
|
|
58
52
|
};
|
|
59
53
|
export type AssetProfile = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/assetSettings.ts"],"names":[],"mappings":"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","file":"assetSettings.js","sourcesContent":["import type { AssetFamily } from \"./assetFamily.ts\";\nimport type { AssetRenditionNode } from \"./assetGraph.ts\";\n\nexport const DEFAULT_MEDIA_WARN_BYTES = 10 * 1024 * 1024 * 1024;\nexport const DEFAULT_MEDIA_REJECT_BYTES = 100 * 1024 * 1024 * 1024;\n\nexport type MediaUploadCaps = {\n warnBytes: number;\n rejectBytes: number;\n};\n\nexport const DEFAULT_MEDIA_UPLOAD_CAPS: MediaUploadCaps = {\n warnBytes: DEFAULT_MEDIA_WARN_BYTES,\n rejectBytes: DEFAULT_MEDIA_REJECT_BYTES,\n};\n\nexport type MediaUploadLimit =\n | { level: \"warn\"; message: string }\n | { level: \"reject\"; message: string };\n\nexport function formatMediaBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;\n}\n\n/** Returns a stable validation message for settings forms and server-side writes. */\nexport function mediaUploadCapsError(caps: MediaUploadCaps): string | null {\n if (!Number.isSafeInteger(caps.warnBytes) || caps.warnBytes < 0) {\n return \"Media warning limit must be a non-negative whole number of bytes\";\n }\n if (!Number.isSafeInteger(caps.rejectBytes) || caps.rejectBytes <= 0) {\n return \"Media reject limit must be a positive whole number of bytes\";\n }\n if (caps.warnBytes > caps.rejectBytes) {\n return \"Media warning limit cannot exceed the reject limit\";\n }\n return null;\n}\n\n/** Determine whether a client should ask for confirmation or reject immediately. */\nexport function mediaUploadLimit(size: number, caps: MediaUploadCaps): MediaUploadLimit | null {\n if (!Number.isSafeInteger(size) || size < 0) {\n return { level: \"reject\", message: \"Media upload size is invalid\" };\n }\n if (size > caps.rejectBytes) {\n return {\n level: \"reject\",\n message: `This file is ${formatMediaBytes(size)}. Media uploads are limited to ${formatMediaBytes(caps.rejectBytes)}.`,\n };\n }\n if (size > caps.warnBytes) {\n return {\n level: \"warn\",\n message: `This file is ${formatMediaBytes(size)}, above the ${formatMediaBytes(caps.warnBytes)} Media warning limit. Continue uploading?`,\n };\n }\n return null;\n}\n\n/** A schema table bound to a family or profile, with its required fields. */\nexport type AssetSchemaBinding = {\n schemaTable: string;\n requiredFields?: string[];\n defaultValues?: unknown;\n};\n\n/** R3: which graph node each Studio delivery slot resolves to. */\nexport type AssetSlotMapping = { compact?: string; preview?: string };\n\nexport type AssetRebuildStatus = {\n pending: { family: AssetFamily; rebuild: string[]; removed: string[] }[];\n cursor?: string | null;\n startedAt: number;\n assetsExamined: number;\n enqueued: number;\n};\n\nexport type AssetMigrationStatus = {\n state: \"idle\" | \"running\" | \"failed\" | \"done\";\n cursor?: string;\n error?: string;\n};\n\nexport type AssetSettings = MediaUploadCaps & {\n familyGraphs: Partial<Record<AssetFamily, AssetRenditionNode[]>>;\n familySchemas: Partial<Record<AssetFamily, AssetSchemaBinding[]>>;\n /** R3: Studio delivery slots → graph node id, per family. */\n slots?: Partial<Record<AssetFamily, AssetSlotMapping>>;\n /** R10: rebuild work a graph save signed up for, drained by cron. */\n rebuildStatus?: AssetRebuildStatus;\n migrateStatus?: AssetMigrationStatus;\n updatedAt: number;\n};\n\nexport type AssetProfile = {\n slug: string;\n name: string;\n families?: AssetFamily[];\n schemas: AssetSchemaBinding[];\n extraNodes: AssetRenditionNode[];\n createdAt: number;\n updatedAt: number;\n};\n\n/** G21 default image graph: preview 2000 contain WebP q80, siblings thumb 400 and thumbSm 200. */\nexport const DEFAULT_IMAGE_GRAPH: AssetRenditionNode[] = [\n { id: \"preview\", op: \"contain\", maxWidth: 2000, maxHeight: 2000, format: \"webp\", quality: 80 },\n {\n id: \"thumb\",\n parentId: \"preview\",\n op: \"contain\",\n maxWidth: 400,\n maxHeight: 400,\n format: \"webp\",\n quality: 80,\n },\n {\n id: \"thumbSm\",\n parentId: \"preview\",\n op: \"contain\",\n maxWidth: 200,\n maxHeight: 200,\n format: \"webp\",\n quality: 80,\n },\n];\n\n/** R3 seed: grid/CMS stamp/library take `thumbSm`, Media detail takes `preview`. */\nexport const DEFAULT_ASSET_SLOTS: Partial<Record<AssetFamily, AssetSlotMapping>> = {\n image: { compact: \"thumbSm\", preview: \"preview\" },\n};\n\n/**\n * The settings a tenant starts with. The component seeds a row from this and\n * the Studio form falls back to it, so a fresh tenant and an unsaved form agree\n * on what the defaults are.\n */\nexport function defaultAssetSettings(): Omit<AssetSettings, \"updatedAt\"> {\n return {\n ...DEFAULT_MEDIA_UPLOAD_CAPS,\n familyGraphs: { image: DEFAULT_IMAGE_GRAPH },\n familySchemas: {},\n slots: DEFAULT_ASSET_SLOTS,\n };\n}\n\n/**\n * What every reader should see when a tenant has never saved Media settings.\n *\n * The component seeds a row from `defaultAssetSettings`, but until something\n * writes one `getSettings` answers `null` — and a caller reading that as \"no\n * graphs, no caps\" concludes the tenant builds nothing, which is the opposite\n * of what a fresh tenant is supposed to do. `updatedAt: 0` because there is no\n * save to report.\n */\nexport function unsavedAssetSettings(): AssetSettings {\n return { ...defaultAssetSettings(), updatedAt: 0 };\n}\n\n/**\n * Slot mapping Studio/CMS should apply for a family.\n *\n * Matches the graph builder: a never-saved tenant (or a settings row with no\n * slots for that family) gets the seed defaults. An explicit empty mapping\n * `{ image: {} }` is an opt-out and stays empty so unset still means original.\n */\nexport function resolveFamilySlotMapping(\n settings: AssetSettings | null | undefined,\n family: AssetFamily | undefined,\n): AssetSlotMapping | undefined {\n if (family === undefined) return undefined;\n const resolved = settings ?? unsavedAssetSettings();\n return resolved.slots?.[family] ?? DEFAULT_ASSET_SLOTS[family];\n}\n\n/**\n * One immutable version of an asset's original, as the component stores it and\n * every reader projects it.\n *\n * The host's component-API declaration used to name only `versionId` and\n * `originalStorageId` while Media detail declared its own copy with `size` and\n * `createdAt` — two descriptions of one payload that already disagreed.\n */\nexport type AssetVersionSnapshot = {\n versionId: string;\n assetId: string;\n originalStorageId: string;\n contentType?: string;\n filename?: string;\n size: number;\n createdAt: number;\n};\n\n/** G27: one indexed placement of an asset in a tenant row. */\nexport type AssetRefRow = {\n fromTable: string;\n fromId: string;\n fieldName: string;\n /** `null` for a scalar field; the position for an array placement. */\n arrayIndex: number | null;\n};\n\n/** One attached metadata table, projected for the Media detail editor. */\nexport type AssetMetadataRow = {\n table: string;\n fields: Record<string, unknown>;\n hasStoredRow: boolean;\n requiredFields: string[];\n /** Two active bindings disagree on a default; an editor must choose (G33). */\n conflictingDefaultFields: string[];\n};\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/assetSettings.ts"],"names":[],"mappings":"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","file":"assetSettings.js","sourcesContent":["import type { AssetFamily } from \"./assetFamily.ts\";\nimport type { AssetRenditionNode } from \"./assetGraph.ts\";\n\nexport const DEFAULT_MEDIA_WARN_BYTES = 10 * 1024 * 1024 * 1024;\nexport const DEFAULT_MEDIA_REJECT_BYTES = 100 * 1024 * 1024 * 1024;\n\nexport type MediaUploadCaps = {\n warnBytes: number;\n rejectBytes: number;\n};\n\nexport const DEFAULT_MEDIA_UPLOAD_CAPS: MediaUploadCaps = {\n warnBytes: DEFAULT_MEDIA_WARN_BYTES,\n rejectBytes: DEFAULT_MEDIA_REJECT_BYTES,\n};\n\nexport type MediaUploadLimit =\n | { level: \"warn\"; message: string }\n | { level: \"reject\"; message: string };\n\nexport function formatMediaBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;\n}\n\n/** Returns a stable validation message for settings forms and server-side writes. */\nexport function mediaUploadCapsError(caps: MediaUploadCaps): string | null {\n if (!Number.isSafeInteger(caps.warnBytes) || caps.warnBytes < 0) {\n return \"Media warning limit must be a non-negative whole number of bytes\";\n }\n if (!Number.isSafeInteger(caps.rejectBytes) || caps.rejectBytes <= 0) {\n return \"Media reject limit must be a positive whole number of bytes\";\n }\n if (caps.warnBytes > caps.rejectBytes) {\n return \"Media warning limit cannot exceed the reject limit\";\n }\n return null;\n}\n\n/** Determine whether a client should ask for confirmation or reject immediately. */\nexport function mediaUploadLimit(size: number, caps: MediaUploadCaps): MediaUploadLimit | null {\n if (!Number.isSafeInteger(size) || size < 0) {\n return { level: \"reject\", message: \"Media upload size is invalid\" };\n }\n if (size > caps.rejectBytes) {\n return {\n level: \"reject\",\n message: `This file is ${formatMediaBytes(size)}. Media uploads are limited to ${formatMediaBytes(caps.rejectBytes)}.`,\n };\n }\n if (size > caps.warnBytes) {\n return {\n level: \"warn\",\n message: `This file is ${formatMediaBytes(size)}, above the ${formatMediaBytes(caps.warnBytes)} Media warning limit. Continue uploading?`,\n };\n }\n return null;\n}\n\n/** A schema table bound to a family or profile, with its required fields. */\nexport type AssetSchemaBinding = {\n schemaTable: string;\n requiredFields?: string[];\n defaultValues?: unknown;\n};\n\n/** R3: which graph node each Studio delivery slot resolves to. */\nexport type AssetSlotMapping = { compact?: string; preview?: string };\n\nexport type AssetRebuildStatus = {\n pending: { family: AssetFamily; rebuild: string[]; removed: string[] }[];\n cursor?: string | null;\n startedAt: number;\n assetsExamined: number;\n enqueued: number;\n};\n\nexport type AssetSettings = MediaUploadCaps & {\n familyGraphs: Partial<Record<AssetFamily, AssetRenditionNode[]>>;\n familySchemas: Partial<Record<AssetFamily, AssetSchemaBinding[]>>;\n /** R3: Studio delivery slots → graph node id, per family. */\n slots?: Partial<Record<AssetFamily, AssetSlotMapping>>;\n /** R10: rebuild work a graph save signed up for, drained by cron. */\n rebuildStatus?: AssetRebuildStatus;\n updatedAt: number;\n};\n\nexport type AssetProfile = {\n slug: string;\n name: string;\n families?: AssetFamily[];\n schemas: AssetSchemaBinding[];\n extraNodes: AssetRenditionNode[];\n createdAt: number;\n updatedAt: number;\n};\n\n/** G21 default image graph: preview 2000 contain WebP q80, siblings thumb 400 and thumbSm 200. */\nexport const DEFAULT_IMAGE_GRAPH: AssetRenditionNode[] = [\n { id: \"preview\", op: \"contain\", maxWidth: 2000, maxHeight: 2000, format: \"webp\", quality: 80 },\n {\n id: \"thumb\",\n parentId: \"preview\",\n op: \"contain\",\n maxWidth: 400,\n maxHeight: 400,\n format: \"webp\",\n quality: 80,\n },\n {\n id: \"thumbSm\",\n parentId: \"preview\",\n op: \"contain\",\n maxWidth: 200,\n maxHeight: 200,\n format: \"webp\",\n quality: 80,\n },\n];\n\n/** R3 seed: grid/CMS stamp/library take `thumbSm`, Media detail takes `preview`. */\nexport const DEFAULT_ASSET_SLOTS: Partial<Record<AssetFamily, AssetSlotMapping>> = {\n image: { compact: \"thumbSm\", preview: \"preview\" },\n};\n\n/**\n * The settings a tenant starts with. The component seeds a row from this and\n * the Studio form falls back to it, so a fresh tenant and an unsaved form agree\n * on what the defaults are.\n */\nexport function defaultAssetSettings(): Omit<AssetSettings, \"updatedAt\"> {\n return {\n ...DEFAULT_MEDIA_UPLOAD_CAPS,\n familyGraphs: { image: DEFAULT_IMAGE_GRAPH },\n familySchemas: {},\n slots: DEFAULT_ASSET_SLOTS,\n };\n}\n\n/**\n * What every reader should see when a tenant has never saved Media settings.\n *\n * The component seeds a row from `defaultAssetSettings`, but until something\n * writes one `getSettings` answers `null` — and a caller reading that as \"no\n * graphs, no caps\" concludes the tenant builds nothing, which is the opposite\n * of what a fresh tenant is supposed to do. `updatedAt: 0` because there is no\n * save to report.\n */\nexport function unsavedAssetSettings(): AssetSettings {\n return { ...defaultAssetSettings(), updatedAt: 0 };\n}\n\n/**\n * Slot mapping Studio/CMS should apply for a family.\n *\n * Matches the graph builder: a never-saved tenant (or a settings row with no\n * slots for that family) gets the seed defaults. An explicit empty mapping\n * `{ image: {} }` is an opt-out and stays empty so unset still means original.\n */\nexport function resolveFamilySlotMapping(\n settings: AssetSettings | null | undefined,\n family: AssetFamily | undefined,\n): AssetSlotMapping | undefined {\n if (family === undefined) return undefined;\n const resolved = settings ?? unsavedAssetSettings();\n return resolved.slots?.[family] ?? DEFAULT_ASSET_SLOTS[family];\n}\n\n/**\n * One immutable version of an asset's original, as the component stores it and\n * every reader projects it.\n *\n * The host's component-API declaration used to name only `versionId` and\n * `originalStorageId` while Media detail declared its own copy with `size` and\n * `createdAt` — two descriptions of one payload that already disagreed.\n */\nexport type AssetVersionSnapshot = {\n versionId: string;\n assetId: string;\n originalStorageId: string;\n contentType?: string;\n filename?: string;\n size: number;\n createdAt: number;\n};\n\n/** G27: one indexed placement of an asset in a tenant row. */\nexport type AssetRefRow = {\n fromTable: string;\n fromId: string;\n fieldName: string;\n /** `null` for a scalar field; the position for an array placement. */\n arrayIndex: number | null;\n};\n\n/** One attached metadata table, projected for the Media detail editor. */\nexport type AssetMetadataRow = {\n table: string;\n fields: Record<string, unknown>;\n hasStoredRow: boolean;\n requiredFields: string[];\n /** Two active bindings disagree on a default; an editor must choose (G33). */\n conflictingDefaultFields: string[];\n};\n"]}
|