@columnus/shared 0.1.55 → 0.1.58
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 +41 -0
- package/dist/apiProfile.js +20 -2
- package/dist/apiProfile.js.map +1 -1
- package/dist/assetFamily.d.ts +36 -0
- package/dist/assetFamily.js +73 -0
- package/dist/assetFamily.js.map +1 -0
- package/dist/assetFieldValues.d.ts +21 -0
- package/dist/{storageFieldValues.js → assetFieldValues.js} +159 -60
- package/dist/assetFieldValues.js.map +1 -0
- package/dist/assetGraph.d.ts +221 -0
- package/dist/assetGraph.js +241 -0
- package/dist/assetGraph.js.map +1 -0
- package/dist/assetJobs.d.ts +99 -0
- package/dist/assetJobs.js +41 -0
- package/dist/assetJobs.js.map +1 -0
- package/dist/assetSettings.d.ts +129 -0
- package/dist/assetSettings.js +87 -0
- package/dist/assetSettings.js.map +1 -0
- package/dist/assetValidators.d.ts +475 -0
- package/dist/assetValidators.js +83 -0
- package/dist/assetValidators.js.map +1 -0
- package/dist/bffProtocol.d.ts +29 -24
- package/dist/bffProtocol.js +5 -1
- package/dist/bffProtocol.js.map +1 -1
- package/dist/ciPreviewEnv.js +1 -1
- package/dist/ciPreviewEnv.js.map +1 -1
- package/dist/clipboardTransfer.js +112 -12
- package/dist/clipboardTransfer.js.map +1 -1
- package/dist/csv.js +118 -18
- package/dist/csv.js.map +1 -1
- package/dist/displayLabel.js +1 -1
- package/dist/displayLabel.js.map +1 -1
- package/dist/dotenv.js +5 -1
- package/dist/dotenv.js.map +1 -1
- package/dist/importMatch.js +118 -18
- package/dist/importMatch.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +750 -80
- package/dist/index.js.map +1 -1
- package/dist/postLoginPath.d.ts +21 -0
- package/dist/postLoginPath.js +24 -0
- package/dist/postLoginPath.js.map +1 -0
- package/dist/previewDeployName.js +2 -2
- package/dist/previewDeployName.js.map +1 -1
- package/dist/recordTableColumns.js +112 -12
- package/dist/recordTableColumns.js.map +1 -1
- package/dist/recordTablePreload.js.map +1 -1
- package/dist/refDisplayRequests.js +112 -12
- package/dist/refDisplayRequests.js.map +1 -1
- package/dist/richDoc.d.ts +2 -2
- package/dist/richDoc.js +5 -5
- package/dist/richDoc.js.map +1 -1
- package/dist/schemaGenerator.d.ts +25 -1
- package/dist/schemaGenerator.js +4569 -18
- package/dist/schemaGenerator.js.map +1 -1
- package/dist/schemaShrink.d.ts +3 -1
- package/dist/schemaShrink.js +4451 -1
- package/dist/schemaShrink.js.map +1 -1
- package/dist/schemaShrinkValidators.d.ts +2 -2
- package/dist/schemaTypes.d.ts +123 -0
- package/dist/schemaTypes.js +107 -13
- package/dist/schemaTypes.js.map +1 -1
- package/dist/share.js +6 -1
- package/dist/share.js.map +1 -1
- package/dist/shareApiBridge.js +6 -1
- package/dist/shareApiBridge.js.map +1 -1
- package/dist/shareProposal.js +112 -12
- package/dist/shareProposal.js.map +1 -1
- package/dist/spaceBundle.js +112 -12
- package/dist/spaceBundle.js.map +1 -1
- package/dist/syncChannel.d.ts +15 -0
- package/dist/syncChannel.js +4642 -0
- package/dist/syncChannel.js.map +1 -0
- package/package.json +1 -1
- package/dist/storageFieldValues.d.ts +0 -10
- package/dist/storageFieldValues.js.map +0 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// packages/@columnus/shared/src/assetJobs.ts
|
|
2
|
+
var ASSET_JOB_KINDS = ["extract", "rendition"];
|
|
3
|
+
var ASSET_JOB_STATUSES = ["pending", "claimed", "done", "failed", "dead_letter"];
|
|
4
|
+
var ASSET_JOB_MAX_ATTEMPTS = 5;
|
|
5
|
+
var ASSET_JOB_TIMEOUT_MS = 6e5;
|
|
6
|
+
var ASSET_JOB_LEASE_MS = 15 * 6e4;
|
|
7
|
+
var ASSET_JOB_MAX_RECLAIMS = 5;
|
|
8
|
+
var ASSET_JOB_BACKOFF_MS = [15e3, 6e4, 5 * 6e4, 60 * 6e4];
|
|
9
|
+
function assetJobRetryDelayMs(attempts) {
|
|
10
|
+
if (attempts >= ASSET_JOB_MAX_ATTEMPTS) return null;
|
|
11
|
+
const index = Math.max(0, Math.min(attempts - 1, ASSET_JOB_BACKOFF_MS.length - 1));
|
|
12
|
+
return ASSET_JOB_BACKOFF_MS[index] ?? null;
|
|
13
|
+
}
|
|
14
|
+
function assetJobFailureOutcome(attempts, now) {
|
|
15
|
+
const delay = assetJobRetryDelayMs(attempts);
|
|
16
|
+
if (delay === null) return { status: "dead_letter" };
|
|
17
|
+
return { status: "pending", runAfter: now + delay };
|
|
18
|
+
}
|
|
19
|
+
function assetJobReclaimOutcome(reclaims) {
|
|
20
|
+
return reclaims + 1 >= ASSET_JOB_MAX_RECLAIMS ? { status: "dead_letter" } : { status: "pending" };
|
|
21
|
+
}
|
|
22
|
+
function assetJobLeaseExpired(leaseUntil, now) {
|
|
23
|
+
return leaseUntil === void 0 || leaseUntil <= now;
|
|
24
|
+
}
|
|
25
|
+
function nextAssetJobWakeAt(jobs, now) {
|
|
26
|
+
let earliest = null;
|
|
27
|
+
for (const job of jobs) {
|
|
28
|
+
if (job.status !== "pending" || job.runAfter <= now) continue;
|
|
29
|
+
if (earliest === null || job.runAfter < earliest) earliest = job.runAfter;
|
|
30
|
+
}
|
|
31
|
+
return earliest;
|
|
32
|
+
}
|
|
33
|
+
function assetJobKey(input) {
|
|
34
|
+
if (input.kind !== "rendition") return `${input.versionId}:extract`;
|
|
35
|
+
const node = input.nodeId ?? "";
|
|
36
|
+
return input.fingerprint ? `${input.versionId}:rendition:${node}:${input.fingerprint}` : `${input.versionId}:rendition:${node}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { ASSET_JOB_KINDS, ASSET_JOB_LEASE_MS, ASSET_JOB_MAX_ATTEMPTS, ASSET_JOB_MAX_RECLAIMS, ASSET_JOB_STATUSES, ASSET_JOB_TIMEOUT_MS, assetJobFailureOutcome, assetJobKey, assetJobLeaseExpired, assetJobReclaimOutcome, assetJobRetryDelayMs, nextAssetJobWakeAt };
|
|
40
|
+
//# sourceMappingURL=assetJobs.js.map
|
|
41
|
+
//# sourceMappingURL=assetJobs.js.map
|
|
@@ -0,0 +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,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"]}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { AssetFamily } from "./assetFamily.ts";
|
|
2
|
+
import type { AssetRenditionNode } from "./assetGraph.ts";
|
|
3
|
+
export declare const DEFAULT_MEDIA_WARN_BYTES: number;
|
|
4
|
+
export declare const DEFAULT_MEDIA_REJECT_BYTES: number;
|
|
5
|
+
export type MediaUploadCaps = {
|
|
6
|
+
warnBytes: number;
|
|
7
|
+
rejectBytes: number;
|
|
8
|
+
};
|
|
9
|
+
export declare const DEFAULT_MEDIA_UPLOAD_CAPS: MediaUploadCaps;
|
|
10
|
+
export type MediaUploadLimit = {
|
|
11
|
+
level: "warn";
|
|
12
|
+
message: string;
|
|
13
|
+
} | {
|
|
14
|
+
level: "reject";
|
|
15
|
+
message: string;
|
|
16
|
+
};
|
|
17
|
+
export declare function formatMediaBytes(bytes: number): string;
|
|
18
|
+
/** Returns a stable validation message for settings forms and server-side writes. */
|
|
19
|
+
export declare function mediaUploadCapsError(caps: MediaUploadCaps): string | null;
|
|
20
|
+
/** Determine whether a client should ask for confirmation or reject immediately. */
|
|
21
|
+
export declare function mediaUploadLimit(size: number, caps: MediaUploadCaps): MediaUploadLimit | null;
|
|
22
|
+
/** A schema table bound to a family or profile, with its required fields. */
|
|
23
|
+
export type AssetSchemaBinding = {
|
|
24
|
+
schemaTable: string;
|
|
25
|
+
requiredFields?: string[];
|
|
26
|
+
defaultValues?: unknown;
|
|
27
|
+
};
|
|
28
|
+
/** R3: which graph node each Studio delivery slot resolves to. */
|
|
29
|
+
export type AssetSlotMapping = {
|
|
30
|
+
compact?: string;
|
|
31
|
+
preview?: string;
|
|
32
|
+
};
|
|
33
|
+
export type AssetRebuildStatus = {
|
|
34
|
+
pending: {
|
|
35
|
+
family: AssetFamily;
|
|
36
|
+
rebuild: string[];
|
|
37
|
+
removed: string[];
|
|
38
|
+
}[];
|
|
39
|
+
cursor?: string | null;
|
|
40
|
+
startedAt: number;
|
|
41
|
+
assetsExamined: number;
|
|
42
|
+
enqueued: number;
|
|
43
|
+
};
|
|
44
|
+
export type AssetMigrationStatus = {
|
|
45
|
+
state: "idle" | "running" | "failed" | "done";
|
|
46
|
+
cursor?: string;
|
|
47
|
+
error?: string;
|
|
48
|
+
};
|
|
49
|
+
export type AssetSettings = MediaUploadCaps & {
|
|
50
|
+
familyGraphs: Partial<Record<AssetFamily, AssetRenditionNode[]>>;
|
|
51
|
+
familySchemas: Partial<Record<AssetFamily, AssetSchemaBinding[]>>;
|
|
52
|
+
/** R3: Studio delivery slots → graph node id, per family. */
|
|
53
|
+
slots?: Partial<Record<AssetFamily, AssetSlotMapping>>;
|
|
54
|
+
/** R10: rebuild work a graph save signed up for, drained by cron. */
|
|
55
|
+
rebuildStatus?: AssetRebuildStatus;
|
|
56
|
+
migrateStatus?: AssetMigrationStatus;
|
|
57
|
+
updatedAt: number;
|
|
58
|
+
};
|
|
59
|
+
export type AssetProfile = {
|
|
60
|
+
slug: string;
|
|
61
|
+
name: string;
|
|
62
|
+
families?: AssetFamily[];
|
|
63
|
+
schemas: AssetSchemaBinding[];
|
|
64
|
+
extraNodes: AssetRenditionNode[];
|
|
65
|
+
createdAt: number;
|
|
66
|
+
updatedAt: number;
|
|
67
|
+
};
|
|
68
|
+
/** G21 default image graph: preview 2000 contain WebP q80, siblings thumb 400 and thumbSm 200. */
|
|
69
|
+
export declare const DEFAULT_IMAGE_GRAPH: AssetRenditionNode[];
|
|
70
|
+
/** R3 seed: grid/CMS stamp/library take `thumbSm`, Media detail takes `preview`. */
|
|
71
|
+
export declare const DEFAULT_ASSET_SLOTS: Partial<Record<AssetFamily, AssetSlotMapping>>;
|
|
72
|
+
/**
|
|
73
|
+
* The settings a tenant starts with. The component seeds a row from this and
|
|
74
|
+
* the Studio form falls back to it, so a fresh tenant and an unsaved form agree
|
|
75
|
+
* on what the defaults are.
|
|
76
|
+
*/
|
|
77
|
+
export declare function defaultAssetSettings(): Omit<AssetSettings, "updatedAt">;
|
|
78
|
+
/**
|
|
79
|
+
* What every reader should see when a tenant has never saved Media settings.
|
|
80
|
+
*
|
|
81
|
+
* The component seeds a row from `defaultAssetSettings`, but until something
|
|
82
|
+
* writes one `getSettings` answers `null` — and a caller reading that as "no
|
|
83
|
+
* graphs, no caps" concludes the tenant builds nothing, which is the opposite
|
|
84
|
+
* of what a fresh tenant is supposed to do. `updatedAt: 0` because there is no
|
|
85
|
+
* save to report.
|
|
86
|
+
*/
|
|
87
|
+
export declare function unsavedAssetSettings(): AssetSettings;
|
|
88
|
+
/**
|
|
89
|
+
* Slot mapping Studio/CMS should apply for a family.
|
|
90
|
+
*
|
|
91
|
+
* Matches the graph builder: a never-saved tenant (or a settings row with no
|
|
92
|
+
* slots for that family) gets the seed defaults. An explicit empty mapping
|
|
93
|
+
* `{ image: {} }` is an opt-out and stays empty so unset still means original.
|
|
94
|
+
*/
|
|
95
|
+
export declare function resolveFamilySlotMapping(settings: AssetSettings | null | undefined, family: AssetFamily | undefined): AssetSlotMapping | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* One immutable version of an asset's original, as the component stores it and
|
|
98
|
+
* every reader projects it.
|
|
99
|
+
*
|
|
100
|
+
* The host's component-API declaration used to name only `versionId` and
|
|
101
|
+
* `originalStorageId` while Media detail declared its own copy with `size` and
|
|
102
|
+
* `createdAt` — two descriptions of one payload that already disagreed.
|
|
103
|
+
*/
|
|
104
|
+
export type AssetVersionSnapshot = {
|
|
105
|
+
versionId: string;
|
|
106
|
+
assetId: string;
|
|
107
|
+
originalStorageId: string;
|
|
108
|
+
contentType?: string;
|
|
109
|
+
filename?: string;
|
|
110
|
+
size: number;
|
|
111
|
+
createdAt: number;
|
|
112
|
+
};
|
|
113
|
+
/** G27: one indexed placement of an asset in a tenant row. */
|
|
114
|
+
export type AssetRefRow = {
|
|
115
|
+
fromTable: string;
|
|
116
|
+
fromId: string;
|
|
117
|
+
fieldName: string;
|
|
118
|
+
/** `null` for a scalar field; the position for an array placement. */
|
|
119
|
+
arrayIndex: number | null;
|
|
120
|
+
};
|
|
121
|
+
/** One attached metadata table, projected for the Media detail editor. */
|
|
122
|
+
export type AssetMetadataRow = {
|
|
123
|
+
table: string;
|
|
124
|
+
fields: Record<string, unknown>;
|
|
125
|
+
hasStoredRow: boolean;
|
|
126
|
+
requiredFields: string[];
|
|
127
|
+
/** Two active bindings disagree on a default; an editor must choose (G33). */
|
|
128
|
+
conflictingDefaultFields: string[];
|
|
129
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// packages/@columnus/shared/src/assetSettings.ts
|
|
2
|
+
var DEFAULT_MEDIA_WARN_BYTES = 10 * 1024 * 1024 * 1024;
|
|
3
|
+
var DEFAULT_MEDIA_REJECT_BYTES = 100 * 1024 * 1024 * 1024;
|
|
4
|
+
var DEFAULT_MEDIA_UPLOAD_CAPS = {
|
|
5
|
+
warnBytes: DEFAULT_MEDIA_WARN_BYTES,
|
|
6
|
+
rejectBytes: DEFAULT_MEDIA_REJECT_BYTES
|
|
7
|
+
};
|
|
8
|
+
function formatMediaBytes(bytes) {
|
|
9
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
10
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
11
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
12
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
13
|
+
}
|
|
14
|
+
function mediaUploadCapsError(caps) {
|
|
15
|
+
if (!Number.isSafeInteger(caps.warnBytes) || caps.warnBytes < 0) {
|
|
16
|
+
return "Media warning limit must be a non-negative whole number of bytes";
|
|
17
|
+
}
|
|
18
|
+
if (!Number.isSafeInteger(caps.rejectBytes) || caps.rejectBytes <= 0) {
|
|
19
|
+
return "Media reject limit must be a positive whole number of bytes";
|
|
20
|
+
}
|
|
21
|
+
if (caps.warnBytes > caps.rejectBytes) {
|
|
22
|
+
return "Media warning limit cannot exceed the reject limit";
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function mediaUploadLimit(size, caps) {
|
|
27
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
28
|
+
return { level: "reject", message: "Media upload size is invalid" };
|
|
29
|
+
}
|
|
30
|
+
if (size > caps.rejectBytes) {
|
|
31
|
+
return {
|
|
32
|
+
level: "reject",
|
|
33
|
+
message: `This file is ${formatMediaBytes(size)}. Media uploads are limited to ${formatMediaBytes(caps.rejectBytes)}.`
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (size > caps.warnBytes) {
|
|
37
|
+
return {
|
|
38
|
+
level: "warn",
|
|
39
|
+
message: `This file is ${formatMediaBytes(size)}, above the ${formatMediaBytes(caps.warnBytes)} Media warning limit. Continue uploading?`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
var DEFAULT_IMAGE_GRAPH = [
|
|
45
|
+
{ id: "preview", op: "contain", maxWidth: 2e3, maxHeight: 2e3, format: "webp", quality: 80 },
|
|
46
|
+
{
|
|
47
|
+
id: "thumb",
|
|
48
|
+
parentId: "preview",
|
|
49
|
+
op: "contain",
|
|
50
|
+
maxWidth: 400,
|
|
51
|
+
maxHeight: 400,
|
|
52
|
+
format: "webp",
|
|
53
|
+
quality: 80
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "thumbSm",
|
|
57
|
+
parentId: "preview",
|
|
58
|
+
op: "contain",
|
|
59
|
+
maxWidth: 200,
|
|
60
|
+
maxHeight: 200,
|
|
61
|
+
format: "webp",
|
|
62
|
+
quality: 80
|
|
63
|
+
}
|
|
64
|
+
];
|
|
65
|
+
var DEFAULT_ASSET_SLOTS = {
|
|
66
|
+
image: { compact: "thumbSm", preview: "preview" }
|
|
67
|
+
};
|
|
68
|
+
function defaultAssetSettings() {
|
|
69
|
+
return {
|
|
70
|
+
...DEFAULT_MEDIA_UPLOAD_CAPS,
|
|
71
|
+
familyGraphs: { image: DEFAULT_IMAGE_GRAPH },
|
|
72
|
+
familySchemas: {},
|
|
73
|
+
slots: DEFAULT_ASSET_SLOTS
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function unsavedAssetSettings() {
|
|
77
|
+
return { ...defaultAssetSettings(), updatedAt: 0 };
|
|
78
|
+
}
|
|
79
|
+
function resolveFamilySlotMapping(settings, family) {
|
|
80
|
+
if (family === void 0) return void 0;
|
|
81
|
+
const resolved = settings ?? unsavedAssetSettings();
|
|
82
|
+
return resolved.slots?.[family] ?? DEFAULT_ASSET_SLOTS[family];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { DEFAULT_ASSET_SLOTS, DEFAULT_IMAGE_GRAPH, DEFAULT_MEDIA_REJECT_BYTES, DEFAULT_MEDIA_UPLOAD_CAPS, DEFAULT_MEDIA_WARN_BYTES, defaultAssetSettings, formatMediaBytes, mediaUploadCapsError, mediaUploadLimit, resolveFamilySlotMapping, unsavedAssetSettings };
|
|
86
|
+
//# sourceMappingURL=assetSettings.js.map
|
|
87
|
+
//# sourceMappingURL=assetSettings.js.map
|
|
@@ -0,0 +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"]}
|