@rockhall/electron-offline-content 0.4.0
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/CHANGELOG.md +384 -0
- package/LICENSE +21 -0
- package/README.md +794 -0
- package/dist/internal/asset-file-name.cjs +13 -0
- package/dist/internal/asset-file-name.cjs.map +1 -0
- package/dist/internal/asset-file-name.d.cts +6 -0
- package/dist/internal/asset-file-name.d.cts.map +1 -0
- package/dist/internal/asset-file-name.d.ts +6 -0
- package/dist/internal/asset-file-name.d.ts.map +1 -0
- package/dist/internal/asset-file-name.js +12 -0
- package/dist/internal/asset-file-name.js.map +1 -0
- package/dist/internal/asset-key.cjs +30 -0
- package/dist/internal/asset-key.cjs.map +1 -0
- package/dist/internal/asset-key.d.cts +19 -0
- package/dist/internal/asset-key.d.cts.map +1 -0
- package/dist/internal/asset-key.d.ts +19 -0
- package/dist/internal/asset-key.d.ts.map +1 -0
- package/dist/internal/asset-key.js +27 -0
- package/dist/internal/asset-key.js.map +1 -0
- package/dist/internal/log-format.cjs +98 -0
- package/dist/internal/log-format.cjs.map +1 -0
- package/dist/internal/log-format.d.cts +10 -0
- package/dist/internal/log-format.d.cts.map +1 -0
- package/dist/internal/log-format.d.ts +10 -0
- package/dist/internal/log-format.d.ts.map +1 -0
- package/dist/internal/log-format.js +97 -0
- package/dist/internal/log-format.js.map +1 -0
- package/dist/internal/media-kind.cjs +46 -0
- package/dist/internal/media-kind.cjs.map +1 -0
- package/dist/internal/media-kind.d.cts +20 -0
- package/dist/internal/media-kind.d.cts.map +1 -0
- package/dist/internal/media-kind.d.ts +20 -0
- package/dist/internal/media-kind.d.ts.map +1 -0
- package/dist/internal/media-kind.js +45 -0
- package/dist/internal/media-kind.js.map +1 -0
- package/dist/internal/url-warn.cjs +14 -0
- package/dist/internal/url-warn.cjs.map +1 -0
- package/dist/internal/url-warn.d.cts +10 -0
- package/dist/internal/url-warn.d.cts.map +1 -0
- package/dist/internal/url-warn.d.ts +10 -0
- package/dist/internal/url-warn.d.ts.map +1 -0
- package/dist/internal/url-warn.js +13 -0
- package/dist/internal/url-warn.js.map +1 -0
- package/dist/internal/validation.cjs +222 -0
- package/dist/internal/validation.cjs.map +1 -0
- package/dist/internal/validation.d.cts +78 -0
- package/dist/internal/validation.d.cts.map +1 -0
- package/dist/internal/validation.d.ts +78 -0
- package/dist/internal/validation.d.ts.map +1 -0
- package/dist/internal/validation.js +196 -0
- package/dist/internal/validation.js.map +1 -0
- package/dist/main/asset-download.cjs +265 -0
- package/dist/main/asset-download.cjs.map +1 -0
- package/dist/main/asset-download.d.cts +12 -0
- package/dist/main/asset-download.d.cts.map +1 -0
- package/dist/main/asset-download.d.ts +12 -0
- package/dist/main/asset-download.d.ts.map +1 -0
- package/dist/main/asset-download.js +263 -0
- package/dist/main/asset-download.js.map +1 -0
- package/dist/main/database.cjs +473 -0
- package/dist/main/database.cjs.map +1 -0
- package/dist/main/database.d.cts +81 -0
- package/dist/main/database.d.cts.map +1 -0
- package/dist/main/database.d.ts +81 -0
- package/dist/main/database.d.ts.map +1 -0
- package/dist/main/database.js +472 -0
- package/dist/main/database.js.map +1 -0
- package/dist/main/index.cjs +22 -0
- package/dist/main/index.d.cts +7 -0
- package/dist/main/index.d.ts +7 -0
- package/dist/main/index.js +7 -0
- package/dist/main/media-cache.cjs +862 -0
- package/dist/main/media-cache.cjs.map +1 -0
- package/dist/main/media-cache.d.cts +134 -0
- package/dist/main/media-cache.d.cts.map +1 -0
- package/dist/main/media-cache.d.ts +134 -0
- package/dist/main/media-cache.d.ts.map +1 -0
- package/dist/main/media-cache.js +854 -0
- package/dist/main/media-cache.js.map +1 -0
- package/dist/main/storage-root-lock.cjs +124 -0
- package/dist/main/storage-root-lock.cjs.map +1 -0
- package/dist/main/storage-root-lock.d.cts +11 -0
- package/dist/main/storage-root-lock.d.cts.map +1 -0
- package/dist/main/storage-root-lock.d.ts +11 -0
- package/dist/main/storage-root-lock.d.ts.map +1 -0
- package/dist/main/storage-root-lock.js +120 -0
- package/dist/main/storage-root-lock.js.map +1 -0
- package/dist/main/store.cjs +197 -0
- package/dist/main/store.cjs.map +1 -0
- package/dist/main/store.d.cts +83 -0
- package/dist/main/store.d.cts.map +1 -0
- package/dist/main/store.d.ts +83 -0
- package/dist/main/store.d.ts.map +1 -0
- package/dist/main/store.js +195 -0
- package/dist/main/store.js.map +1 -0
- package/dist/preload/index.cjs +36 -0
- package/dist/preload/index.cjs.map +1 -0
- package/dist/preload/index.d.cts +14 -0
- package/dist/preload/index.d.cts.map +1 -0
- package/dist/preload/index.d.ts +14 -0
- package/dist/preload/index.d.ts.map +1 -0
- package/dist/preload/index.js +34 -0
- package/dist/preload/index.js.map +1 -0
- package/dist/react/index.cjs +199 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +50 -0
- package/dist/react/index.d.cts.map +1 -0
- package/dist/react/index.d.ts +50 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +191 -0
- package/dist/react/index.js.map +1 -0
- package/dist/renderer/helpers.cjs +36 -0
- package/dist/renderer/helpers.cjs.map +1 -0
- package/dist/renderer/helpers.d.cts +11 -0
- package/dist/renderer/helpers.d.cts.map +1 -0
- package/dist/renderer/helpers.d.ts +11 -0
- package/dist/renderer/helpers.d.ts.map +1 -0
- package/dist/renderer/helpers.js +35 -0
- package/dist/renderer/helpers.js.map +1 -0
- package/dist/renderer/index.cjs +20 -0
- package/dist/renderer/index.cjs.map +1 -0
- package/dist/renderer/index.d.cts +14 -0
- package/dist/renderer/index.d.cts.map +1 -0
- package/dist/renderer/index.d.ts +14 -0
- package/dist/renderer/index.d.ts.map +1 -0
- package/dist/renderer/index.js +14 -0
- package/dist/renderer/index.js.map +1 -0
- package/dist/renderer/runtime.cjs +278 -0
- package/dist/renderer/runtime.cjs.map +1 -0
- package/dist/renderer/runtime.d.cts +35 -0
- package/dist/renderer/runtime.d.cts.map +1 -0
- package/dist/renderer/runtime.d.ts +35 -0
- package/dist/renderer/runtime.d.ts.map +1 -0
- package/dist/renderer/runtime.js +273 -0
- package/dist/renderer/runtime.js.map +1 -0
- package/dist/renderer/window-globals.d.cts +9 -0
- package/dist/renderer/window-globals.d.cts.map +1 -0
- package/dist/renderer/window-globals.d.ts +9 -0
- package/dist/renderer/window-globals.d.ts.map +1 -0
- package/dist/shared/errors.cjs +102 -0
- package/dist/shared/errors.cjs.map +1 -0
- package/dist/shared/errors.d.cts +45 -0
- package/dist/shared/errors.d.cts.map +1 -0
- package/dist/shared/errors.d.ts +45 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/errors.js +93 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/ipc.cjs +14 -0
- package/dist/shared/ipc.cjs.map +1 -0
- package/dist/shared/ipc.d.cts +12 -0
- package/dist/shared/ipc.d.cts.map +1 -0
- package/dist/shared/ipc.d.ts +12 -0
- package/dist/shared/ipc.d.ts.map +1 -0
- package/dist/shared/ipc.js +13 -0
- package/dist/shared/ipc.js.map +1 -0
- package/dist/shared/normalize.cjs +19 -0
- package/dist/shared/normalize.cjs.map +1 -0
- package/dist/shared/normalize.d.cts +11 -0
- package/dist/shared/normalize.d.cts.map +1 -0
- package/dist/shared/normalize.d.ts +11 -0
- package/dist/shared/normalize.d.ts.map +1 -0
- package/dist/shared/normalize.js +18 -0
- package/dist/shared/normalize.js.map +1 -0
- package/dist/shared/pagination.cjs +32 -0
- package/dist/shared/pagination.cjs.map +1 -0
- package/dist/shared/pagination.d.cts +14 -0
- package/dist/shared/pagination.d.cts.map +1 -0
- package/dist/shared/pagination.d.ts +14 -0
- package/dist/shared/pagination.d.ts.map +1 -0
- package/dist/shared/pagination.js +28 -0
- package/dist/shared/pagination.js.map +1 -0
- package/dist/shared/stem.cjs +16 -0
- package/dist/shared/stem.cjs.map +1 -0
- package/dist/shared/stem.d.cts +6 -0
- package/dist/shared/stem.d.cts.map +1 -0
- package/dist/shared/stem.d.ts +6 -0
- package/dist/shared/stem.d.ts.map +1 -0
- package/dist/shared/stem.js +14 -0
- package/dist/shared/stem.js.map +1 -0
- package/dist/shared/types.cjs +15 -0
- package/dist/shared/types.cjs.map +1 -0
- package/dist/shared/types.d.cts +234 -0
- package/dist/shared/types.d.cts.map +1 -0
- package/dist/shared/types.d.ts +234 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +14 -0
- package/dist/shared/types.js.map +1 -0
- package/package.json +120 -0
- package/skills/authenticated-downloads/SKILL.md +203 -0
- package/skills/cache-configuration/SKILL.md +357 -0
- package/skills/cache-configuration/references/options.md +356 -0
- package/skills/getting-started/SKILL.md +407 -0
- package/skills/production-checklist/SKILL.md +397 -0
- package/skills/react-rendering/SKILL.md +424 -0
- package/skills/react-rendering/references/hooks.md +443 -0
- package/skills/store-authoring/SKILL.md +369 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_shared_errors = require("../shared/errors.cjs");
|
|
3
|
+
const require_internal_asset_key = require("../internal/asset-key.cjs");
|
|
4
|
+
const require_internal_asset_file_name = require("../internal/asset-file-name.cjs");
|
|
5
|
+
const require_internal_media_kind = require("../internal/media-kind.cjs");
|
|
6
|
+
const require_shared_types = require("../shared/types.cjs");
|
|
7
|
+
//#region src/main/store.ts
|
|
8
|
+
const MIME_PATTERN = /^\S+\/\S+$/;
|
|
9
|
+
const BUILTIN_INDEX_NAMES = new Set(["mimeType", "mediaKind"]);
|
|
10
|
+
function fileStem(fileName) {
|
|
11
|
+
const dotIndex = fileName.lastIndexOf(".");
|
|
12
|
+
return dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A flat key-value asset store with user-defined secondary indexes.
|
|
16
|
+
*
|
|
17
|
+
* Build a store imperatively inside your `resolveStore` callback:
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* const store = createMediaStore();
|
|
21
|
+
* const gallery = store.defineIndex("gallery");
|
|
22
|
+
* store.add(["forest", "video"], {
|
|
23
|
+
* version: "v1",
|
|
24
|
+
* mimeType: "video/mp4",
|
|
25
|
+
* url: "https://cdn.example.com/forest.mp4",
|
|
26
|
+
* indexes: [gallery("nature")],
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
var MediaStore = class {
|
|
31
|
+
options;
|
|
32
|
+
indexes = /* @__PURE__ */ new Map();
|
|
33
|
+
assets = /* @__PURE__ */ new Map();
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this.options = options ?? {};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Register a secondary index that assets can be tagged with and queried by.
|
|
39
|
+
* Must be called before any {@link add} call that references this index.
|
|
40
|
+
*
|
|
41
|
+
* @param name - Unique index name. Must not collide with built-in indexes (`mimeType`, `mediaKind`).
|
|
42
|
+
* @param options - Cardinality (`"single"` or `"multi"`) and whether the index is required on every asset.
|
|
43
|
+
* @returns A callable {@link MediaIndex} handle. Call it with a value to produce an {@link IndexTag}.
|
|
44
|
+
*/
|
|
45
|
+
defineIndex(name, options) {
|
|
46
|
+
if (!name) throw new require_shared_errors.StoreValidationError("Index name must be a non-empty string.");
|
|
47
|
+
if (BUILTIN_INDEX_NAMES.has(name)) throw new require_shared_errors.StoreValidationError(`Index name "${name}" is reserved as a built-in index.`);
|
|
48
|
+
if (this.indexes.has(name)) throw new require_shared_errors.StoreValidationError(`Duplicate index name "${name}".`);
|
|
49
|
+
const cardinality = options?.cardinality ?? "single";
|
|
50
|
+
const required = options?.required ?? false;
|
|
51
|
+
if (cardinality !== "single" && cardinality !== "multi") throw new require_shared_errors.StoreValidationError(`Index "${name}": cardinality must be "single" or "multi" (got "${String(cardinality)}").`);
|
|
52
|
+
if (typeof required !== "boolean") throw new require_shared_errors.StoreValidationError(`Index "${name}": required must be a boolean (got ${typeof required}).`);
|
|
53
|
+
this.indexes.set(name, {
|
|
54
|
+
name,
|
|
55
|
+
cardinality,
|
|
56
|
+
required,
|
|
57
|
+
builtin: false
|
|
58
|
+
});
|
|
59
|
+
const fn = (value) => new require_shared_types.IndexTag(name, value);
|
|
60
|
+
Object.defineProperties(fn, {
|
|
61
|
+
indexName: {
|
|
62
|
+
value: name,
|
|
63
|
+
enumerable: true
|
|
64
|
+
},
|
|
65
|
+
cardinality: {
|
|
66
|
+
value: cardinality,
|
|
67
|
+
enumerable: true
|
|
68
|
+
},
|
|
69
|
+
required: {
|
|
70
|
+
value: required,
|
|
71
|
+
enumerable: true
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return fn;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Add an asset to the store.
|
|
78
|
+
*
|
|
79
|
+
* @param key - Unique asset key. A string or array of string segments (e.g. `["videos", "hubble", "main"]`).
|
|
80
|
+
* @param input - Asset data: version, mimeType, source, and optional indexes/metadata.
|
|
81
|
+
*/
|
|
82
|
+
add(key, input) {
|
|
83
|
+
if (!require_internal_asset_key.isValidKeyInput(key)) throw new require_shared_errors.StoreValidationError("Asset key must be a non-empty string or non-empty string array.");
|
|
84
|
+
const hashedKey = require_internal_asset_key.hashKey(key);
|
|
85
|
+
const display = require_internal_asset_key.displayKey(key);
|
|
86
|
+
if (this.assets.has(hashedKey)) throw new require_shared_errors.StoreValidationError(`Duplicate asset key "${display}" (hash collision with existing key).`);
|
|
87
|
+
if (!input.version) throw new require_shared_errors.StoreValidationError(`Asset "${display}": version is required.`);
|
|
88
|
+
if (!input.mimeType || !MIME_PATTERN.test(input.mimeType)) throw new require_shared_errors.StoreValidationError(`Asset "${display}": mimeType must be a valid type/subtype string (got "${input.mimeType ?? ""}").`);
|
|
89
|
+
if (!input.url) throw new require_shared_errors.StoreValidationError(`Asset "${display}": url is required.`);
|
|
90
|
+
try {
|
|
91
|
+
const parsed = new URL(input.url);
|
|
92
|
+
if (!/^https?:$/i.test(parsed.protocol)) throw new require_shared_errors.StoreValidationError(`Asset "${display}": URL must use http or https (got "${parsed.protocol}").`);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
if (err instanceof require_shared_errors.StoreValidationError) throw err;
|
|
95
|
+
throw new require_shared_errors.StoreValidationError(`Asset "${display}": URL is not valid: "${input.url}".`);
|
|
96
|
+
}
|
|
97
|
+
if (input.byteLength !== void 0 && (typeof input.byteLength !== "number" || !Number.isFinite(input.byteLength) || input.byteLength < 0)) throw new require_shared_errors.StoreValidationError(`Asset "${display}": byteLength must be a non-negative finite number (got ${String(input.byteLength)}).`);
|
|
98
|
+
const fileName = input.fileName ?? require_internal_asset_file_name.deriveAssetFileName(input.url);
|
|
99
|
+
const assetIndexes = Object.create(null);
|
|
100
|
+
if (input.indexes !== void 0 && !Array.isArray(input.indexes)) throw new require_shared_errors.StoreValidationError(`Asset "${display}": indexes must be an array of IndexTag entries produced by calling a defineIndex handle.`);
|
|
101
|
+
if (input.indexes) for (const tag of input.indexes) {
|
|
102
|
+
if (!(tag instanceof require_shared_types.IndexTag)) throw new require_shared_errors.StoreValidationError(`Asset "${display}": indexes must be an array of IndexTag entries produced by calling a defineIndex handle.`);
|
|
103
|
+
const indexName = tag.name;
|
|
104
|
+
const value = tag.value;
|
|
105
|
+
const def = this.indexes.get(indexName);
|
|
106
|
+
if (!def) throw new require_shared_errors.StoreValidationError(`Asset "${display}": index "${indexName}" has not been defined. Call store.defineIndex("${indexName}") first.`);
|
|
107
|
+
if (Object.hasOwn(assetIndexes, indexName)) throw new require_shared_errors.StoreValidationError(`Asset "${display}": duplicate index "${indexName}" in indexes array.`);
|
|
108
|
+
if (def.cardinality === "single") {
|
|
109
|
+
if (Array.isArray(value)) throw new require_shared_errors.StoreValidationError(`Asset "${display}": index "${indexName}" has single cardinality but received an array. Use { cardinality: "multi" } when defining the index to allow arrays.`);
|
|
110
|
+
if (typeof value !== "string" || !value) throw new require_shared_errors.StoreValidationError(`Asset "${display}": index "${indexName}" value must be a non-empty string.`);
|
|
111
|
+
assetIndexes[indexName] = value;
|
|
112
|
+
} else {
|
|
113
|
+
const values = Array.isArray(value) ? value : [value];
|
|
114
|
+
if (values.length === 0) throw new require_shared_errors.StoreValidationError(`Asset "${display}": index "${indexName}" value array must not be empty.`);
|
|
115
|
+
for (const v of values) if (typeof v !== "string" || !v) throw new require_shared_errors.StoreValidationError(`Asset "${display}": index "${indexName}" values must be non-empty strings.`);
|
|
116
|
+
assetIndexes[indexName] = [...new Set(values)];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const metadataSnapshot = input.metadata === void 0 ? {} : structuredClone(input.metadata);
|
|
120
|
+
this.assets.set(hashedKey, {
|
|
121
|
+
key: hashedKey,
|
|
122
|
+
displayKey: display,
|
|
123
|
+
version: input.version,
|
|
124
|
+
mimeType: input.mimeType,
|
|
125
|
+
url: input.url,
|
|
126
|
+
fileName,
|
|
127
|
+
byteLength: input.byteLength,
|
|
128
|
+
metadata: metadataSnapshot,
|
|
129
|
+
indexes: assetIndexes
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Serialize the store for the sync engine. Validates required indexes and produces
|
|
134
|
+
* the flat manifest consumed internally. Not part of the public consumer API.
|
|
135
|
+
* @internal
|
|
136
|
+
*/
|
|
137
|
+
_serialize() {
|
|
138
|
+
for (const [, def] of this.indexes) {
|
|
139
|
+
if (!def.required) continue;
|
|
140
|
+
for (const [, asset] of this.assets) if (!(def.name in asset.indexes)) throw new require_shared_errors.StoreValidationError(`Asset "${asset.displayKey}": required index "${def.name}" is missing.`);
|
|
141
|
+
}
|
|
142
|
+
const indexDefinitions = [
|
|
143
|
+
...Array.from(this.indexes.values()),
|
|
144
|
+
{
|
|
145
|
+
name: "mimeType",
|
|
146
|
+
cardinality: "single",
|
|
147
|
+
required: false,
|
|
148
|
+
builtin: true
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "mediaKind",
|
|
152
|
+
cardinality: "single",
|
|
153
|
+
required: false,
|
|
154
|
+
builtin: true
|
|
155
|
+
}
|
|
156
|
+
];
|
|
157
|
+
const assets = [];
|
|
158
|
+
for (const [, stored] of this.assets) {
|
|
159
|
+
const mediaKind = require_internal_media_kind.mediaKindFromMime(stored.mimeType);
|
|
160
|
+
const stem = fileStem(stored.fileName);
|
|
161
|
+
const allIndexes = {
|
|
162
|
+
...stored.indexes,
|
|
163
|
+
mimeType: stored.mimeType,
|
|
164
|
+
mediaKind
|
|
165
|
+
};
|
|
166
|
+
assets.push({
|
|
167
|
+
key: stored.key,
|
|
168
|
+
displayKey: stored.displayKey,
|
|
169
|
+
version: stored.version,
|
|
170
|
+
mimeType: stored.mimeType,
|
|
171
|
+
mediaKind,
|
|
172
|
+
url: stored.url,
|
|
173
|
+
fileName: stored.fileName,
|
|
174
|
+
fileStem: stem,
|
|
175
|
+
byteLength: stored.byteLength,
|
|
176
|
+
metadata: stored.metadata,
|
|
177
|
+
indexes: allIndexes
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
snapshotId: this.options.snapshotId,
|
|
182
|
+
retrievedAt: this.options.retrievedAt,
|
|
183
|
+
expiresAt: this.options.expiresAt,
|
|
184
|
+
indexDefinitions,
|
|
185
|
+
assets
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
/** Creates a new {@link MediaStore} for populating in a `resolveStore` callback. */
|
|
190
|
+
function createMediaStore(options) {
|
|
191
|
+
return new MediaStore(options);
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
exports.MediaStore = MediaStore;
|
|
195
|
+
exports.createMediaStore = createMediaStore;
|
|
196
|
+
|
|
197
|
+
//# sourceMappingURL=store.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.cjs","names":["StoreValidationError","IndexTag","isValidKeyInput","hashKey","displayKey","deriveAssetFileName","mediaKindFromMime"],"sources":["../../src/main/store.ts"],"sourcesContent":["import { StoreValidationError } from \"../shared/errors.js\";\nimport { deriveAssetFileName } from \"../internal/asset-file-name.js\";\nimport { mediaKindFromMime } from \"../internal/media-kind.js\";\nimport { hashKey, displayKey, isValidKeyInput, type AssetKeyInput } from \"../internal/asset-key.js\";\nimport {\n IndexTag,\n type FlatManifest,\n type FlatManifestAsset,\n type IndexDefinition,\n type JsonValue,\n type MediaAssetInput,\n} from \"../shared/types.js\";\n\nconst MIME_PATTERN = /^\\S+\\/\\S+$/;\n\nconst BUILTIN_INDEX_NAMES = new Set([\"mimeType\", \"mediaKind\"]);\n\nfunction fileStem(fileName: string): string {\n const dotIndex = fileName.lastIndexOf(\".\");\n return dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName;\n}\n\n/**\n * Callable handle returned by {@link MediaStore.defineIndex}. Call it with a value\n * to produce an {@link IndexTag} for use in the `indexes` array of {@link MediaStore.add}:\n *\n * ```ts\n * const gallery = store.defineIndex(\"gallery\");\n * store.add(\"photo-1\", { ..., indexes: [gallery(\"nature\")] });\n * ```\n */\nexport interface MediaIndex {\n (value: string | string[]): IndexTag;\n readonly indexName: string;\n readonly cardinality: \"single\" | \"multi\";\n readonly required: boolean;\n}\n\n/** Options for {@link createMediaStore}. */\nexport interface MediaStoreOptions {\n /** Optional opaque id for correlation, debugging, or multi-source merges. */\n snapshotId?: string;\n /** ISO 8601 timestamp describing when the store payload was built. */\n retrievedAt?: string;\n /**\n * ISO 8601 timestamp after which source URLs must be treated as expired.\n * Sync will fail assets whose download starts after this time.\n */\n expiresAt?: string;\n}\n\ninterface StoredAsset {\n key: string;\n displayKey: string;\n version: string;\n mimeType: string;\n url: string;\n fileName: string;\n byteLength?: number;\n metadata: Record<string, JsonValue>;\n indexes: Record<string, string | string[]>;\n}\n\n/**\n * A flat key-value asset store with user-defined secondary indexes.\n *\n * Build a store imperatively inside your `resolveStore` callback:\n *\n * ```ts\n * const store = createMediaStore();\n * const gallery = store.defineIndex(\"gallery\");\n * store.add([\"forest\", \"video\"], {\n * version: \"v1\",\n * mimeType: \"video/mp4\",\n * url: \"https://cdn.example.com/forest.mp4\",\n * indexes: [gallery(\"nature\")],\n * });\n * ```\n */\nexport class MediaStore {\n private readonly options: MediaStoreOptions;\n private readonly indexes = new Map<string, IndexDefinition>();\n private readonly assets = new Map<string, StoredAsset>();\n\n constructor(options?: MediaStoreOptions) {\n this.options = options ?? {};\n }\n\n /**\n * Register a secondary index that assets can be tagged with and queried by.\n * Must be called before any {@link add} call that references this index.\n *\n * @param name - Unique index name. Must not collide with built-in indexes (`mimeType`, `mediaKind`).\n * @param options - Cardinality (`\"single\"` or `\"multi\"`) and whether the index is required on every asset.\n * @returns A callable {@link MediaIndex} handle. Call it with a value to produce an {@link IndexTag}.\n */\n defineIndex(\n name: string,\n options?: { cardinality?: \"single\" | \"multi\"; required?: boolean },\n ): MediaIndex {\n if (!name) {\n throw new StoreValidationError(\"Index name must be a non-empty string.\");\n }\n if (BUILTIN_INDEX_NAMES.has(name)) {\n throw new StoreValidationError(`Index name \"${name}\" is reserved as a built-in index.`);\n }\n if (this.indexes.has(name)) {\n throw new StoreValidationError(`Duplicate index name \"${name}\".`);\n }\n\n const cardinality = options?.cardinality ?? \"single\";\n const required = options?.required ?? false;\n\n if (cardinality !== \"single\" && cardinality !== \"multi\") {\n throw new StoreValidationError(\n `Index \"${name}\": cardinality must be \"single\" or \"multi\" (got \"${String(cardinality)}\").`,\n );\n }\n if (typeof required !== \"boolean\") {\n throw new StoreValidationError(\n `Index \"${name}\": required must be a boolean (got ${typeof required}).`,\n );\n }\n\n this.indexes.set(name, { name, cardinality, required, builtin: false });\n\n const fn = (value: string | string[]) => new IndexTag(name, value);\n Object.defineProperties(fn, {\n indexName: { value: name, enumerable: true },\n cardinality: { value: cardinality, enumerable: true },\n required: { value: required, enumerable: true },\n });\n return fn as MediaIndex;\n }\n\n /**\n * Add an asset to the store.\n *\n * @param key - Unique asset key. A string or array of string segments (e.g. `[\"videos\", \"hubble\", \"main\"]`).\n * @param input - Asset data: version, mimeType, source, and optional indexes/metadata.\n */\n add(key: AssetKeyInput, input: MediaAssetInput): void {\n if (!isValidKeyInput(key)) {\n throw new StoreValidationError(\n \"Asset key must be a non-empty string or non-empty string array.\",\n );\n }\n const hashedKey = hashKey(key);\n const display = displayKey(key);\n if (this.assets.has(hashedKey)) {\n throw new StoreValidationError(\n `Duplicate asset key \"${display}\" (hash collision with existing key).`,\n );\n }\n\n if (!input.version) {\n throw new StoreValidationError(`Asset \"${display}\": version is required.`);\n }\n if (!input.mimeType || !MIME_PATTERN.test(input.mimeType)) {\n throw new StoreValidationError(\n `Asset \"${display}\": mimeType must be a valid type/subtype string (got \"${input.mimeType ?? \"\"}\").`,\n );\n }\n if (!input.url) {\n throw new StoreValidationError(`Asset \"${display}\": url is required.`);\n }\n try {\n const parsed = new URL(input.url);\n if (!/^https?:$/i.test(parsed.protocol)) {\n throw new StoreValidationError(\n `Asset \"${display}\": URL must use http or https (got \"${parsed.protocol}\").`,\n );\n }\n } catch (err) {\n if (err instanceof StoreValidationError) throw err;\n throw new StoreValidationError(`Asset \"${display}\": URL is not valid: \"${input.url}\".`);\n }\n\n // byteLength: any non-negative finite number is accepted, including non-integer fractions\n // (see MediaAssetInput.byteLength JSDoc).\n if (\n input.byteLength !== undefined &&\n (typeof input.byteLength !== \"number\" ||\n !Number.isFinite(input.byteLength) ||\n input.byteLength < 0)\n ) {\n throw new StoreValidationError(\n `Asset \"${display}\": byteLength must be a non-negative finite number (got ${String(input.byteLength)}).`,\n );\n }\n\n const fileName = input.fileName ?? deriveAssetFileName(input.url);\n\n const assetIndexes: Record<string, string | string[]> = Object.create(null);\n if (input.indexes !== undefined && !Array.isArray(input.indexes)) {\n throw new StoreValidationError(\n `Asset \"${display}\": indexes must be an array of IndexTag entries produced by calling a defineIndex handle.`,\n );\n }\n if (input.indexes) {\n for (const tag of input.indexes) {\n if (!(tag instanceof IndexTag)) {\n throw new StoreValidationError(\n `Asset \"${display}\": indexes must be an array of IndexTag entries produced by calling a defineIndex handle.`,\n );\n }\n const indexName = tag.name;\n const value = tag.value;\n const def = this.indexes.get(indexName);\n if (!def) {\n throw new StoreValidationError(\n `Asset \"${display}\": index \"${indexName}\" has not been defined. Call store.defineIndex(\"${indexName}\") first.`,\n );\n }\n if (Object.hasOwn(assetIndexes, indexName)) {\n throw new StoreValidationError(\n `Asset \"${display}\": duplicate index \"${indexName}\" in indexes array.`,\n );\n }\n\n if (def.cardinality === \"single\") {\n if (Array.isArray(value)) {\n throw new StoreValidationError(\n `Asset \"${display}\": index \"${indexName}\" has single cardinality but received an array. ` +\n `Use { cardinality: \"multi\" } when defining the index to allow arrays.`,\n );\n }\n if (typeof value !== \"string\" || !value) {\n throw new StoreValidationError(\n `Asset \"${display}\": index \"${indexName}\" value must be a non-empty string.`,\n );\n }\n assetIndexes[indexName] = value;\n } else {\n const values = Array.isArray(value) ? value : [value];\n if (values.length === 0) {\n throw new StoreValidationError(\n `Asset \"${display}\": index \"${indexName}\" value array must not be empty.`,\n );\n }\n for (const v of values) {\n if (typeof v !== \"string\" || !v) {\n throw new StoreValidationError(\n `Asset \"${display}\": index \"${indexName}\" values must be non-empty strings.`,\n );\n }\n }\n assetIndexes[indexName] = [...new Set(values)];\n }\n }\n }\n\n const metadataSnapshot = input.metadata === undefined ? {} : structuredClone(input.metadata);\n\n this.assets.set(hashedKey, {\n key: hashedKey,\n displayKey: display,\n version: input.version,\n mimeType: input.mimeType,\n url: input.url,\n fileName,\n byteLength: input.byteLength,\n metadata: metadataSnapshot,\n indexes: assetIndexes,\n });\n }\n\n /**\n * Serialize the store for the sync engine. Validates required indexes and produces\n * the flat manifest consumed internally. Not part of the public consumer API.\n * @internal\n */\n _serialize(): FlatManifest {\n for (const [, def] of this.indexes) {\n if (!def.required) continue;\n for (const [, asset] of this.assets) {\n if (!(def.name in asset.indexes)) {\n throw new StoreValidationError(\n `Asset \"${asset.displayKey}\": required index \"${def.name}\" is missing.`,\n );\n }\n }\n }\n\n const indexDefinitions: IndexDefinition[] = [\n ...Array.from(this.indexes.values()),\n { name: \"mimeType\", cardinality: \"single\", required: false, builtin: true },\n { name: \"mediaKind\", cardinality: \"single\", required: false, builtin: true },\n ];\n\n const assets: FlatManifestAsset[] = [];\n for (const [, stored] of this.assets) {\n const mediaKind = mediaKindFromMime(stored.mimeType);\n const stem = fileStem(stored.fileName);\n\n const allIndexes: Record<string, string | string[]> = {\n ...stored.indexes,\n mimeType: stored.mimeType,\n mediaKind,\n };\n\n assets.push({\n key: stored.key,\n displayKey: stored.displayKey,\n version: stored.version,\n mimeType: stored.mimeType,\n mediaKind,\n url: stored.url,\n fileName: stored.fileName,\n fileStem: stem,\n byteLength: stored.byteLength,\n metadata: stored.metadata,\n indexes: allIndexes,\n });\n }\n\n return {\n snapshotId: this.options.snapshotId,\n retrievedAt: this.options.retrievedAt,\n expiresAt: this.options.expiresAt,\n indexDefinitions,\n assets,\n };\n }\n}\n\n/** Creates a new {@link MediaStore} for populating in a `resolveStore` callback. */\nexport function createMediaStore(options?: MediaStoreOptions): MediaStore {\n return new MediaStore(options);\n}\n"],"mappings":";;;;;;;AAaA,MAAM,eAAe;AAErB,MAAM,sBAAsB,IAAI,IAAI,CAAC,YAAY,YAAY,CAAC;AAE9D,SAAS,SAAS,UAA0B;CAC1C,MAAM,WAAW,SAAS,YAAY,IAAI;CAC1C,OAAO,WAAW,IAAI,SAAS,MAAM,GAAG,SAAS,GAAG;;;;;;;;;;;;;;;;;;AA4DtD,IAAa,aAAb,MAAwB;CACtB;CACA,0BAA2B,IAAI,KAA8B;CAC7D,yBAA0B,IAAI,KAA0B;CAExD,YAAY,SAA6B;EACvC,KAAK,UAAU,WAAW,EAAE;;;;;;;;;;CAW9B,YACE,MACA,SACY;EACZ,IAAI,CAAC,MACH,MAAM,IAAIA,sBAAAA,qBAAqB,yCAAyC;EAE1E,IAAI,oBAAoB,IAAI,KAAK,EAC/B,MAAM,IAAIA,sBAAAA,qBAAqB,eAAe,KAAK,oCAAoC;EAEzF,IAAI,KAAK,QAAQ,IAAI,KAAK,EACxB,MAAM,IAAIA,sBAAAA,qBAAqB,yBAAyB,KAAK,IAAI;EAGnE,MAAM,cAAc,SAAS,eAAe;EAC5C,MAAM,WAAW,SAAS,YAAY;EAEtC,IAAI,gBAAgB,YAAY,gBAAgB,SAC9C,MAAM,IAAIA,sBAAAA,qBACR,UAAU,KAAK,mDAAmD,OAAO,YAAY,CAAC,KACvF;EAEH,IAAI,OAAO,aAAa,WACtB,MAAM,IAAIA,sBAAAA,qBACR,UAAU,KAAK,qCAAqC,OAAO,SAAS,IACrE;EAGH,KAAK,QAAQ,IAAI,MAAM;GAAE;GAAM;GAAa;GAAU,SAAS;GAAO,CAAC;EAEvE,MAAM,MAAM,UAA6B,IAAIC,qBAAAA,SAAS,MAAM,MAAM;EAClE,OAAO,iBAAiB,IAAI;GAC1B,WAAW;IAAE,OAAO;IAAM,YAAY;IAAM;GAC5C,aAAa;IAAE,OAAO;IAAa,YAAY;IAAM;GACrD,UAAU;IAAE,OAAO;IAAU,YAAY;IAAM;GAChD,CAAC;EACF,OAAO;;;;;;;;CAST,IAAI,KAAoB,OAA8B;EACpD,IAAI,CAACC,2BAAAA,gBAAgB,IAAI,EACvB,MAAM,IAAIF,sBAAAA,qBACR,kEACD;EAEH,MAAM,YAAYG,2BAAAA,QAAQ,IAAI;EAC9B,MAAM,UAAUC,2BAAAA,WAAW,IAAI;EAC/B,IAAI,KAAK,OAAO,IAAI,UAAU,EAC5B,MAAM,IAAIJ,sBAAAA,qBACR,wBAAwB,QAAQ,uCACjC;EAGH,IAAI,CAAC,MAAM,SACT,MAAM,IAAIA,sBAAAA,qBAAqB,UAAU,QAAQ,yBAAyB;EAE5E,IAAI,CAAC,MAAM,YAAY,CAAC,aAAa,KAAK,MAAM,SAAS,EACvD,MAAM,IAAIA,sBAAAA,qBACR,UAAU,QAAQ,wDAAwD,MAAM,YAAY,GAAG,KAChG;EAEH,IAAI,CAAC,MAAM,KACT,MAAM,IAAIA,sBAAAA,qBAAqB,UAAU,QAAQ,qBAAqB;EAExE,IAAI;GACF,MAAM,SAAS,IAAI,IAAI,MAAM,IAAI;GACjC,IAAI,CAAC,aAAa,KAAK,OAAO,SAAS,EACrC,MAAM,IAAIA,sBAAAA,qBACR,UAAU,QAAQ,sCAAsC,OAAO,SAAS,KACzE;WAEI,KAAK;GACZ,IAAI,eAAeA,sBAAAA,sBAAsB,MAAM;GAC/C,MAAM,IAAIA,sBAAAA,qBAAqB,UAAU,QAAQ,wBAAwB,MAAM,IAAI,IAAI;;EAKzF,IACE,MAAM,eAAe,KAAA,MACpB,OAAO,MAAM,eAAe,YAC3B,CAAC,OAAO,SAAS,MAAM,WAAW,IAClC,MAAM,aAAa,IAErB,MAAM,IAAIA,sBAAAA,qBACR,UAAU,QAAQ,0DAA0D,OAAO,MAAM,WAAW,CAAC,IACtG;EAGH,MAAM,WAAW,MAAM,YAAYK,iCAAAA,oBAAoB,MAAM,IAAI;EAEjE,MAAM,eAAkD,OAAO,OAAO,KAAK;EAC3E,IAAI,MAAM,YAAY,KAAA,KAAa,CAAC,MAAM,QAAQ,MAAM,QAAQ,EAC9D,MAAM,IAAIL,sBAAAA,qBACR,UAAU,QAAQ,2FACnB;EAEH,IAAI,MAAM,SACR,KAAK,MAAM,OAAO,MAAM,SAAS;GAC/B,IAAI,EAAE,eAAeC,qBAAAA,WACnB,MAAM,IAAID,sBAAAA,qBACR,UAAU,QAAQ,2FACnB;GAEH,MAAM,YAAY,IAAI;GACtB,MAAM,QAAQ,IAAI;GAClB,MAAM,MAAM,KAAK,QAAQ,IAAI,UAAU;GACvC,IAAI,CAAC,KACH,MAAM,IAAIA,sBAAAA,qBACR,UAAU,QAAQ,YAAY,UAAU,kDAAkD,UAAU,WACrG;GAEH,IAAI,OAAO,OAAO,cAAc,UAAU,EACxC,MAAM,IAAIA,sBAAAA,qBACR,UAAU,QAAQ,sBAAsB,UAAU,qBACnD;GAGH,IAAI,IAAI,gBAAgB,UAAU;IAChC,IAAI,MAAM,QAAQ,MAAM,EACtB,MAAM,IAAIA,sBAAAA,qBACR,UAAU,QAAQ,YAAY,UAAU,uHAEzC;IAEH,IAAI,OAAO,UAAU,YAAY,CAAC,OAChC,MAAM,IAAIA,sBAAAA,qBACR,UAAU,QAAQ,YAAY,UAAU,qCACzC;IAEH,aAAa,aAAa;UACrB;IACL,MAAM,SAAS,MAAM,QAAQ,MAAM,GAAG,QAAQ,CAAC,MAAM;IACrD,IAAI,OAAO,WAAW,GACpB,MAAM,IAAIA,sBAAAA,qBACR,UAAU,QAAQ,YAAY,UAAU,kCACzC;IAEH,KAAK,MAAM,KAAK,QACd,IAAI,OAAO,MAAM,YAAY,CAAC,GAC5B,MAAM,IAAIA,sBAAAA,qBACR,UAAU,QAAQ,YAAY,UAAU,qCACzC;IAGL,aAAa,aAAa,CAAC,GAAG,IAAI,IAAI,OAAO,CAAC;;;EAKpD,MAAM,mBAAmB,MAAM,aAAa,KAAA,IAAY,EAAE,GAAG,gBAAgB,MAAM,SAAS;EAE5F,KAAK,OAAO,IAAI,WAAW;GACzB,KAAK;GACL,YAAY;GACZ,SAAS,MAAM;GACf,UAAU,MAAM;GAChB,KAAK,MAAM;GACX;GACA,YAAY,MAAM;GAClB,UAAU;GACV,SAAS;GACV,CAAC;;;;;;;CAQJ,aAA2B;EACzB,KAAK,MAAM,GAAG,QAAQ,KAAK,SAAS;GAClC,IAAI,CAAC,IAAI,UAAU;GACnB,KAAK,MAAM,GAAG,UAAU,KAAK,QAC3B,IAAI,EAAE,IAAI,QAAQ,MAAM,UACtB,MAAM,IAAIA,sBAAAA,qBACR,UAAU,MAAM,WAAW,qBAAqB,IAAI,KAAK,eAC1D;;EAKP,MAAM,mBAAsC;GAC1C,GAAG,MAAM,KAAK,KAAK,QAAQ,QAAQ,CAAC;GACpC;IAAE,MAAM;IAAY,aAAa;IAAU,UAAU;IAAO,SAAS;IAAM;GAC3E;IAAE,MAAM;IAAa,aAAa;IAAU,UAAU;IAAO,SAAS;IAAM;GAC7E;EAED,MAAM,SAA8B,EAAE;EACtC,KAAK,MAAM,GAAG,WAAW,KAAK,QAAQ;GACpC,MAAM,YAAYM,4BAAAA,kBAAkB,OAAO,SAAS;GACpD,MAAM,OAAO,SAAS,OAAO,SAAS;GAEtC,MAAM,aAAgD;IACpD,GAAG,OAAO;IACV,UAAU,OAAO;IACjB;IACD;GAED,OAAO,KAAK;IACV,KAAK,OAAO;IACZ,YAAY,OAAO;IACnB,SAAS,OAAO;IAChB,UAAU,OAAO;IACjB;IACA,KAAK,OAAO;IACZ,UAAU,OAAO;IACjB,UAAU;IACV,YAAY,OAAO;IACnB,UAAU,OAAO;IACjB,SAAS;IACV,CAAC;;EAGJ,OAAO;GACL,YAAY,KAAK,QAAQ;GACzB,aAAa,KAAK,QAAQ;GAC1B,WAAW,KAAK,QAAQ;GACxB;GACA;GACD;;;;AAKL,SAAgB,iBAAiB,SAAyC;CACxE,OAAO,IAAI,WAAW,QAAQ"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { AssetKeyInput } from "../internal/asset-key.cjs";
|
|
2
|
+
import { FlatManifest, IndexTag, MediaAssetInput } from "../shared/types.cjs";
|
|
3
|
+
|
|
4
|
+
//#region src/main/store.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Callable handle returned by {@link MediaStore.defineIndex}. Call it with a value
|
|
7
|
+
* to produce an {@link IndexTag} for use in the `indexes` array of {@link MediaStore.add}:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* const gallery = store.defineIndex("gallery");
|
|
11
|
+
* store.add("photo-1", { ..., indexes: [gallery("nature")] });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
interface MediaIndex {
|
|
15
|
+
(value: string | string[]): IndexTag;
|
|
16
|
+
readonly indexName: string;
|
|
17
|
+
readonly cardinality: "single" | "multi";
|
|
18
|
+
readonly required: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** Options for {@link createMediaStore}. */
|
|
21
|
+
interface MediaStoreOptions {
|
|
22
|
+
/** Optional opaque id for correlation, debugging, or multi-source merges. */
|
|
23
|
+
snapshotId?: string;
|
|
24
|
+
/** ISO 8601 timestamp describing when the store payload was built. */
|
|
25
|
+
retrievedAt?: string;
|
|
26
|
+
/**
|
|
27
|
+
* ISO 8601 timestamp after which source URLs must be treated as expired.
|
|
28
|
+
* Sync will fail assets whose download starts after this time.
|
|
29
|
+
*/
|
|
30
|
+
expiresAt?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A flat key-value asset store with user-defined secondary indexes.
|
|
34
|
+
*
|
|
35
|
+
* Build a store imperatively inside your `resolveStore` callback:
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* const store = createMediaStore();
|
|
39
|
+
* const gallery = store.defineIndex("gallery");
|
|
40
|
+
* store.add(["forest", "video"], {
|
|
41
|
+
* version: "v1",
|
|
42
|
+
* mimeType: "video/mp4",
|
|
43
|
+
* url: "https://cdn.example.com/forest.mp4",
|
|
44
|
+
* indexes: [gallery("nature")],
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
declare class MediaStore {
|
|
49
|
+
private readonly options;
|
|
50
|
+
private readonly indexes;
|
|
51
|
+
private readonly assets;
|
|
52
|
+
constructor(options?: MediaStoreOptions);
|
|
53
|
+
/**
|
|
54
|
+
* Register a secondary index that assets can be tagged with and queried by.
|
|
55
|
+
* Must be called before any {@link add} call that references this index.
|
|
56
|
+
*
|
|
57
|
+
* @param name - Unique index name. Must not collide with built-in indexes (`mimeType`, `mediaKind`).
|
|
58
|
+
* @param options - Cardinality (`"single"` or `"multi"`) and whether the index is required on every asset.
|
|
59
|
+
* @returns A callable {@link MediaIndex} handle. Call it with a value to produce an {@link IndexTag}.
|
|
60
|
+
*/
|
|
61
|
+
defineIndex(name: string, options?: {
|
|
62
|
+
cardinality?: "single" | "multi";
|
|
63
|
+
required?: boolean;
|
|
64
|
+
}): MediaIndex;
|
|
65
|
+
/**
|
|
66
|
+
* Add an asset to the store.
|
|
67
|
+
*
|
|
68
|
+
* @param key - Unique asset key. A string or array of string segments (e.g. `["videos", "hubble", "main"]`).
|
|
69
|
+
* @param input - Asset data: version, mimeType, source, and optional indexes/metadata.
|
|
70
|
+
*/
|
|
71
|
+
add(key: AssetKeyInput, input: MediaAssetInput): void;
|
|
72
|
+
/**
|
|
73
|
+
* Serialize the store for the sync engine. Validates required indexes and produces
|
|
74
|
+
* the flat manifest consumed internally. Not part of the public consumer API.
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
_serialize(): FlatManifest;
|
|
78
|
+
}
|
|
79
|
+
/** Creates a new {@link MediaStore} for populating in a `resolveStore` callback. */
|
|
80
|
+
declare function createMediaStore(options?: MediaStoreOptions): MediaStore;
|
|
81
|
+
//#endregion
|
|
82
|
+
export { MediaIndex, MediaStore, MediaStoreOptions, createMediaStore };
|
|
83
|
+
//# sourceMappingURL=store.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.cts","names":[],"sources":["../../src/main/store.ts"],"mappings":";;;;;;AA+BA;;;;;;;UAAiB,UAAA;EAAA,CACd,KAAA,sBAA2B,QAAA;EAAA,SACnB,SAAA;EAAA,SACA,WAAA;EAAA,SACA,QAAA;AAAA;;UAIM,iBAAA;EAEf;EAAA,UAAA;EAOA;EALA,WAAA;EAKS;AA+BX;;;EA/BE,SAAA;AAAA;;;;;;;;;;;;;;;;;cA+BW,UAAA;EAAA,iBACM,OAAA;EAAA,iBACA,OAAA;EAAA,iBACA,MAAA;cAEL,OAAA,GAAU,iBAAA;EAyDE;;;;;AA0L1B;;;EAvOE,WAAA,CACE,IAAA,UACA,OAAA;IAAY,WAAA;IAAkC,QAAA;EAAA,IAC7C,UAAA;EAoOoE;;;;;;EA1LvE,GAAA,CAAI,GAAA,EAAK,aAAA,EAAe,KAAA,EAAO,eAAA;;;;;;EAmI/B,UAAA,CAAA,GAAc,YAAA;AAAA;;iBAuDA,gBAAA,CAAiB,OAAA,GAAU,iBAAA,GAAoB,UAAA"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { AssetKeyInput } from "../internal/asset-key.js";
|
|
2
|
+
import { FlatManifest, IndexTag, MediaAssetInput } from "../shared/types.js";
|
|
3
|
+
|
|
4
|
+
//#region src/main/store.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Callable handle returned by {@link MediaStore.defineIndex}. Call it with a value
|
|
7
|
+
* to produce an {@link IndexTag} for use in the `indexes` array of {@link MediaStore.add}:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* const gallery = store.defineIndex("gallery");
|
|
11
|
+
* store.add("photo-1", { ..., indexes: [gallery("nature")] });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
interface MediaIndex {
|
|
15
|
+
(value: string | string[]): IndexTag;
|
|
16
|
+
readonly indexName: string;
|
|
17
|
+
readonly cardinality: "single" | "multi";
|
|
18
|
+
readonly required: boolean;
|
|
19
|
+
}
|
|
20
|
+
/** Options for {@link createMediaStore}. */
|
|
21
|
+
interface MediaStoreOptions {
|
|
22
|
+
/** Optional opaque id for correlation, debugging, or multi-source merges. */
|
|
23
|
+
snapshotId?: string;
|
|
24
|
+
/** ISO 8601 timestamp describing when the store payload was built. */
|
|
25
|
+
retrievedAt?: string;
|
|
26
|
+
/**
|
|
27
|
+
* ISO 8601 timestamp after which source URLs must be treated as expired.
|
|
28
|
+
* Sync will fail assets whose download starts after this time.
|
|
29
|
+
*/
|
|
30
|
+
expiresAt?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A flat key-value asset store with user-defined secondary indexes.
|
|
34
|
+
*
|
|
35
|
+
* Build a store imperatively inside your `resolveStore` callback:
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* const store = createMediaStore();
|
|
39
|
+
* const gallery = store.defineIndex("gallery");
|
|
40
|
+
* store.add(["forest", "video"], {
|
|
41
|
+
* version: "v1",
|
|
42
|
+
* mimeType: "video/mp4",
|
|
43
|
+
* url: "https://cdn.example.com/forest.mp4",
|
|
44
|
+
* indexes: [gallery("nature")],
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
declare class MediaStore {
|
|
49
|
+
private readonly options;
|
|
50
|
+
private readonly indexes;
|
|
51
|
+
private readonly assets;
|
|
52
|
+
constructor(options?: MediaStoreOptions);
|
|
53
|
+
/**
|
|
54
|
+
* Register a secondary index that assets can be tagged with and queried by.
|
|
55
|
+
* Must be called before any {@link add} call that references this index.
|
|
56
|
+
*
|
|
57
|
+
* @param name - Unique index name. Must not collide with built-in indexes (`mimeType`, `mediaKind`).
|
|
58
|
+
* @param options - Cardinality (`"single"` or `"multi"`) and whether the index is required on every asset.
|
|
59
|
+
* @returns A callable {@link MediaIndex} handle. Call it with a value to produce an {@link IndexTag}.
|
|
60
|
+
*/
|
|
61
|
+
defineIndex(name: string, options?: {
|
|
62
|
+
cardinality?: "single" | "multi";
|
|
63
|
+
required?: boolean;
|
|
64
|
+
}): MediaIndex;
|
|
65
|
+
/**
|
|
66
|
+
* Add an asset to the store.
|
|
67
|
+
*
|
|
68
|
+
* @param key - Unique asset key. A string or array of string segments (e.g. `["videos", "hubble", "main"]`).
|
|
69
|
+
* @param input - Asset data: version, mimeType, source, and optional indexes/metadata.
|
|
70
|
+
*/
|
|
71
|
+
add(key: AssetKeyInput, input: MediaAssetInput): void;
|
|
72
|
+
/**
|
|
73
|
+
* Serialize the store for the sync engine. Validates required indexes and produces
|
|
74
|
+
* the flat manifest consumed internally. Not part of the public consumer API.
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
_serialize(): FlatManifest;
|
|
78
|
+
}
|
|
79
|
+
/** Creates a new {@link MediaStore} for populating in a `resolveStore` callback. */
|
|
80
|
+
declare function createMediaStore(options?: MediaStoreOptions): MediaStore;
|
|
81
|
+
//#endregion
|
|
82
|
+
export { MediaIndex, MediaStore, MediaStoreOptions, createMediaStore };
|
|
83
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","names":[],"sources":["../../src/main/store.ts"],"mappings":";;;;;;AA+BA;;;;;;;UAAiB,UAAA;EAAA,CACd,KAAA,sBAA2B,QAAA;EAAA,SACnB,SAAA;EAAA,SACA,WAAA;EAAA,SACA,QAAA;AAAA;;UAIM,iBAAA;EAEf;EAAA,UAAA;EAOA;EALA,WAAA;EAKS;AA+BX;;;EA/BE,SAAA;AAAA;;;;;;;;;;;;;;;;;cA+BW,UAAA;EAAA,iBACM,OAAA;EAAA,iBACA,OAAA;EAAA,iBACA,MAAA;cAEL,OAAA,GAAU,iBAAA;EAyDE;;;;;AA0L1B;;;EAvOE,WAAA,CACE,IAAA,UACA,OAAA;IAAY,WAAA;IAAkC,QAAA;EAAA,IAC7C,UAAA;EAoOoE;;;;;;EA1LvE,GAAA,CAAI,GAAA,EAAK,aAAA,EAAe,KAAA,EAAO,eAAA;;;;;;EAmI/B,UAAA,CAAA,GAAc,YAAA;AAAA;;iBAuDA,gBAAA,CAAiB,OAAA,GAAU,iBAAA,GAAoB,UAAA"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { StoreValidationError } from "../shared/errors.js";
|
|
2
|
+
import { displayKey, hashKey, isValidKeyInput } from "../internal/asset-key.js";
|
|
3
|
+
import { deriveAssetFileName } from "../internal/asset-file-name.js";
|
|
4
|
+
import { mediaKindFromMime } from "../internal/media-kind.js";
|
|
5
|
+
import { IndexTag } from "../shared/types.js";
|
|
6
|
+
//#region src/main/store.ts
|
|
7
|
+
const MIME_PATTERN = /^\S+\/\S+$/;
|
|
8
|
+
const BUILTIN_INDEX_NAMES = new Set(["mimeType", "mediaKind"]);
|
|
9
|
+
function fileStem(fileName) {
|
|
10
|
+
const dotIndex = fileName.lastIndexOf(".");
|
|
11
|
+
return dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A flat key-value asset store with user-defined secondary indexes.
|
|
15
|
+
*
|
|
16
|
+
* Build a store imperatively inside your `resolveStore` callback:
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* const store = createMediaStore();
|
|
20
|
+
* const gallery = store.defineIndex("gallery");
|
|
21
|
+
* store.add(["forest", "video"], {
|
|
22
|
+
* version: "v1",
|
|
23
|
+
* mimeType: "video/mp4",
|
|
24
|
+
* url: "https://cdn.example.com/forest.mp4",
|
|
25
|
+
* indexes: [gallery("nature")],
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
var MediaStore = class {
|
|
30
|
+
options;
|
|
31
|
+
indexes = /* @__PURE__ */ new Map();
|
|
32
|
+
assets = /* @__PURE__ */ new Map();
|
|
33
|
+
constructor(options) {
|
|
34
|
+
this.options = options ?? {};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Register a secondary index that assets can be tagged with and queried by.
|
|
38
|
+
* Must be called before any {@link add} call that references this index.
|
|
39
|
+
*
|
|
40
|
+
* @param name - Unique index name. Must not collide with built-in indexes (`mimeType`, `mediaKind`).
|
|
41
|
+
* @param options - Cardinality (`"single"` or `"multi"`) and whether the index is required on every asset.
|
|
42
|
+
* @returns A callable {@link MediaIndex} handle. Call it with a value to produce an {@link IndexTag}.
|
|
43
|
+
*/
|
|
44
|
+
defineIndex(name, options) {
|
|
45
|
+
if (!name) throw new StoreValidationError("Index name must be a non-empty string.");
|
|
46
|
+
if (BUILTIN_INDEX_NAMES.has(name)) throw new StoreValidationError(`Index name "${name}" is reserved as a built-in index.`);
|
|
47
|
+
if (this.indexes.has(name)) throw new StoreValidationError(`Duplicate index name "${name}".`);
|
|
48
|
+
const cardinality = options?.cardinality ?? "single";
|
|
49
|
+
const required = options?.required ?? false;
|
|
50
|
+
if (cardinality !== "single" && cardinality !== "multi") throw new StoreValidationError(`Index "${name}": cardinality must be "single" or "multi" (got "${String(cardinality)}").`);
|
|
51
|
+
if (typeof required !== "boolean") throw new StoreValidationError(`Index "${name}": required must be a boolean (got ${typeof required}).`);
|
|
52
|
+
this.indexes.set(name, {
|
|
53
|
+
name,
|
|
54
|
+
cardinality,
|
|
55
|
+
required,
|
|
56
|
+
builtin: false
|
|
57
|
+
});
|
|
58
|
+
const fn = (value) => new IndexTag(name, value);
|
|
59
|
+
Object.defineProperties(fn, {
|
|
60
|
+
indexName: {
|
|
61
|
+
value: name,
|
|
62
|
+
enumerable: true
|
|
63
|
+
},
|
|
64
|
+
cardinality: {
|
|
65
|
+
value: cardinality,
|
|
66
|
+
enumerable: true
|
|
67
|
+
},
|
|
68
|
+
required: {
|
|
69
|
+
value: required,
|
|
70
|
+
enumerable: true
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return fn;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Add an asset to the store.
|
|
77
|
+
*
|
|
78
|
+
* @param key - Unique asset key. A string or array of string segments (e.g. `["videos", "hubble", "main"]`).
|
|
79
|
+
* @param input - Asset data: version, mimeType, source, and optional indexes/metadata.
|
|
80
|
+
*/
|
|
81
|
+
add(key, input) {
|
|
82
|
+
if (!isValidKeyInput(key)) throw new StoreValidationError("Asset key must be a non-empty string or non-empty string array.");
|
|
83
|
+
const hashedKey = hashKey(key);
|
|
84
|
+
const display = displayKey(key);
|
|
85
|
+
if (this.assets.has(hashedKey)) throw new StoreValidationError(`Duplicate asset key "${display}" (hash collision with existing key).`);
|
|
86
|
+
if (!input.version) throw new StoreValidationError(`Asset "${display}": version is required.`);
|
|
87
|
+
if (!input.mimeType || !MIME_PATTERN.test(input.mimeType)) throw new StoreValidationError(`Asset "${display}": mimeType must be a valid type/subtype string (got "${input.mimeType ?? ""}").`);
|
|
88
|
+
if (!input.url) throw new StoreValidationError(`Asset "${display}": url is required.`);
|
|
89
|
+
try {
|
|
90
|
+
const parsed = new URL(input.url);
|
|
91
|
+
if (!/^https?:$/i.test(parsed.protocol)) throw new StoreValidationError(`Asset "${display}": URL must use http or https (got "${parsed.protocol}").`);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
if (err instanceof StoreValidationError) throw err;
|
|
94
|
+
throw new StoreValidationError(`Asset "${display}": URL is not valid: "${input.url}".`);
|
|
95
|
+
}
|
|
96
|
+
if (input.byteLength !== void 0 && (typeof input.byteLength !== "number" || !Number.isFinite(input.byteLength) || input.byteLength < 0)) throw new StoreValidationError(`Asset "${display}": byteLength must be a non-negative finite number (got ${String(input.byteLength)}).`);
|
|
97
|
+
const fileName = input.fileName ?? deriveAssetFileName(input.url);
|
|
98
|
+
const assetIndexes = Object.create(null);
|
|
99
|
+
if (input.indexes !== void 0 && !Array.isArray(input.indexes)) throw new StoreValidationError(`Asset "${display}": indexes must be an array of IndexTag entries produced by calling a defineIndex handle.`);
|
|
100
|
+
if (input.indexes) for (const tag of input.indexes) {
|
|
101
|
+
if (!(tag instanceof IndexTag)) throw new StoreValidationError(`Asset "${display}": indexes must be an array of IndexTag entries produced by calling a defineIndex handle.`);
|
|
102
|
+
const indexName = tag.name;
|
|
103
|
+
const value = tag.value;
|
|
104
|
+
const def = this.indexes.get(indexName);
|
|
105
|
+
if (!def) throw new StoreValidationError(`Asset "${display}": index "${indexName}" has not been defined. Call store.defineIndex("${indexName}") first.`);
|
|
106
|
+
if (Object.hasOwn(assetIndexes, indexName)) throw new StoreValidationError(`Asset "${display}": duplicate index "${indexName}" in indexes array.`);
|
|
107
|
+
if (def.cardinality === "single") {
|
|
108
|
+
if (Array.isArray(value)) throw new StoreValidationError(`Asset "${display}": index "${indexName}" has single cardinality but received an array. Use { cardinality: "multi" } when defining the index to allow arrays.`);
|
|
109
|
+
if (typeof value !== "string" || !value) throw new StoreValidationError(`Asset "${display}": index "${indexName}" value must be a non-empty string.`);
|
|
110
|
+
assetIndexes[indexName] = value;
|
|
111
|
+
} else {
|
|
112
|
+
const values = Array.isArray(value) ? value : [value];
|
|
113
|
+
if (values.length === 0) throw new StoreValidationError(`Asset "${display}": index "${indexName}" value array must not be empty.`);
|
|
114
|
+
for (const v of values) if (typeof v !== "string" || !v) throw new StoreValidationError(`Asset "${display}": index "${indexName}" values must be non-empty strings.`);
|
|
115
|
+
assetIndexes[indexName] = [...new Set(values)];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const metadataSnapshot = input.metadata === void 0 ? {} : structuredClone(input.metadata);
|
|
119
|
+
this.assets.set(hashedKey, {
|
|
120
|
+
key: hashedKey,
|
|
121
|
+
displayKey: display,
|
|
122
|
+
version: input.version,
|
|
123
|
+
mimeType: input.mimeType,
|
|
124
|
+
url: input.url,
|
|
125
|
+
fileName,
|
|
126
|
+
byteLength: input.byteLength,
|
|
127
|
+
metadata: metadataSnapshot,
|
|
128
|
+
indexes: assetIndexes
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Serialize the store for the sync engine. Validates required indexes and produces
|
|
133
|
+
* the flat manifest consumed internally. Not part of the public consumer API.
|
|
134
|
+
* @internal
|
|
135
|
+
*/
|
|
136
|
+
_serialize() {
|
|
137
|
+
for (const [, def] of this.indexes) {
|
|
138
|
+
if (!def.required) continue;
|
|
139
|
+
for (const [, asset] of this.assets) if (!(def.name in asset.indexes)) throw new StoreValidationError(`Asset "${asset.displayKey}": required index "${def.name}" is missing.`);
|
|
140
|
+
}
|
|
141
|
+
const indexDefinitions = [
|
|
142
|
+
...Array.from(this.indexes.values()),
|
|
143
|
+
{
|
|
144
|
+
name: "mimeType",
|
|
145
|
+
cardinality: "single",
|
|
146
|
+
required: false,
|
|
147
|
+
builtin: true
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "mediaKind",
|
|
151
|
+
cardinality: "single",
|
|
152
|
+
required: false,
|
|
153
|
+
builtin: true
|
|
154
|
+
}
|
|
155
|
+
];
|
|
156
|
+
const assets = [];
|
|
157
|
+
for (const [, stored] of this.assets) {
|
|
158
|
+
const mediaKind = mediaKindFromMime(stored.mimeType);
|
|
159
|
+
const stem = fileStem(stored.fileName);
|
|
160
|
+
const allIndexes = {
|
|
161
|
+
...stored.indexes,
|
|
162
|
+
mimeType: stored.mimeType,
|
|
163
|
+
mediaKind
|
|
164
|
+
};
|
|
165
|
+
assets.push({
|
|
166
|
+
key: stored.key,
|
|
167
|
+
displayKey: stored.displayKey,
|
|
168
|
+
version: stored.version,
|
|
169
|
+
mimeType: stored.mimeType,
|
|
170
|
+
mediaKind,
|
|
171
|
+
url: stored.url,
|
|
172
|
+
fileName: stored.fileName,
|
|
173
|
+
fileStem: stem,
|
|
174
|
+
byteLength: stored.byteLength,
|
|
175
|
+
metadata: stored.metadata,
|
|
176
|
+
indexes: allIndexes
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
snapshotId: this.options.snapshotId,
|
|
181
|
+
retrievedAt: this.options.retrievedAt,
|
|
182
|
+
expiresAt: this.options.expiresAt,
|
|
183
|
+
indexDefinitions,
|
|
184
|
+
assets
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
/** Creates a new {@link MediaStore} for populating in a `resolveStore` callback. */
|
|
189
|
+
function createMediaStore(options) {
|
|
190
|
+
return new MediaStore(options);
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
export { MediaStore, createMediaStore };
|
|
194
|
+
|
|
195
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","names":[],"sources":["../../src/main/store.ts"],"sourcesContent":["import { StoreValidationError } from \"../shared/errors.js\";\nimport { deriveAssetFileName } from \"../internal/asset-file-name.js\";\nimport { mediaKindFromMime } from \"../internal/media-kind.js\";\nimport { hashKey, displayKey, isValidKeyInput, type AssetKeyInput } from \"../internal/asset-key.js\";\nimport {\n IndexTag,\n type FlatManifest,\n type FlatManifestAsset,\n type IndexDefinition,\n type JsonValue,\n type MediaAssetInput,\n} from \"../shared/types.js\";\n\nconst MIME_PATTERN = /^\\S+\\/\\S+$/;\n\nconst BUILTIN_INDEX_NAMES = new Set([\"mimeType\", \"mediaKind\"]);\n\nfunction fileStem(fileName: string): string {\n const dotIndex = fileName.lastIndexOf(\".\");\n return dotIndex > 0 ? fileName.slice(0, dotIndex) : fileName;\n}\n\n/**\n * Callable handle returned by {@link MediaStore.defineIndex}. Call it with a value\n * to produce an {@link IndexTag} for use in the `indexes` array of {@link MediaStore.add}:\n *\n * ```ts\n * const gallery = store.defineIndex(\"gallery\");\n * store.add(\"photo-1\", { ..., indexes: [gallery(\"nature\")] });\n * ```\n */\nexport interface MediaIndex {\n (value: string | string[]): IndexTag;\n readonly indexName: string;\n readonly cardinality: \"single\" | \"multi\";\n readonly required: boolean;\n}\n\n/** Options for {@link createMediaStore}. */\nexport interface MediaStoreOptions {\n /** Optional opaque id for correlation, debugging, or multi-source merges. */\n snapshotId?: string;\n /** ISO 8601 timestamp describing when the store payload was built. */\n retrievedAt?: string;\n /**\n * ISO 8601 timestamp after which source URLs must be treated as expired.\n * Sync will fail assets whose download starts after this time.\n */\n expiresAt?: string;\n}\n\ninterface StoredAsset {\n key: string;\n displayKey: string;\n version: string;\n mimeType: string;\n url: string;\n fileName: string;\n byteLength?: number;\n metadata: Record<string, JsonValue>;\n indexes: Record<string, string | string[]>;\n}\n\n/**\n * A flat key-value asset store with user-defined secondary indexes.\n *\n * Build a store imperatively inside your `resolveStore` callback:\n *\n * ```ts\n * const store = createMediaStore();\n * const gallery = store.defineIndex(\"gallery\");\n * store.add([\"forest\", \"video\"], {\n * version: \"v1\",\n * mimeType: \"video/mp4\",\n * url: \"https://cdn.example.com/forest.mp4\",\n * indexes: [gallery(\"nature\")],\n * });\n * ```\n */\nexport class MediaStore {\n private readonly options: MediaStoreOptions;\n private readonly indexes = new Map<string, IndexDefinition>();\n private readonly assets = new Map<string, StoredAsset>();\n\n constructor(options?: MediaStoreOptions) {\n this.options = options ?? {};\n }\n\n /**\n * Register a secondary index that assets can be tagged with and queried by.\n * Must be called before any {@link add} call that references this index.\n *\n * @param name - Unique index name. Must not collide with built-in indexes (`mimeType`, `mediaKind`).\n * @param options - Cardinality (`\"single\"` or `\"multi\"`) and whether the index is required on every asset.\n * @returns A callable {@link MediaIndex} handle. Call it with a value to produce an {@link IndexTag}.\n */\n defineIndex(\n name: string,\n options?: { cardinality?: \"single\" | \"multi\"; required?: boolean },\n ): MediaIndex {\n if (!name) {\n throw new StoreValidationError(\"Index name must be a non-empty string.\");\n }\n if (BUILTIN_INDEX_NAMES.has(name)) {\n throw new StoreValidationError(`Index name \"${name}\" is reserved as a built-in index.`);\n }\n if (this.indexes.has(name)) {\n throw new StoreValidationError(`Duplicate index name \"${name}\".`);\n }\n\n const cardinality = options?.cardinality ?? \"single\";\n const required = options?.required ?? false;\n\n if (cardinality !== \"single\" && cardinality !== \"multi\") {\n throw new StoreValidationError(\n `Index \"${name}\": cardinality must be \"single\" or \"multi\" (got \"${String(cardinality)}\").`,\n );\n }\n if (typeof required !== \"boolean\") {\n throw new StoreValidationError(\n `Index \"${name}\": required must be a boolean (got ${typeof required}).`,\n );\n }\n\n this.indexes.set(name, { name, cardinality, required, builtin: false });\n\n const fn = (value: string | string[]) => new IndexTag(name, value);\n Object.defineProperties(fn, {\n indexName: { value: name, enumerable: true },\n cardinality: { value: cardinality, enumerable: true },\n required: { value: required, enumerable: true },\n });\n return fn as MediaIndex;\n }\n\n /**\n * Add an asset to the store.\n *\n * @param key - Unique asset key. A string or array of string segments (e.g. `[\"videos\", \"hubble\", \"main\"]`).\n * @param input - Asset data: version, mimeType, source, and optional indexes/metadata.\n */\n add(key: AssetKeyInput, input: MediaAssetInput): void {\n if (!isValidKeyInput(key)) {\n throw new StoreValidationError(\n \"Asset key must be a non-empty string or non-empty string array.\",\n );\n }\n const hashedKey = hashKey(key);\n const display = displayKey(key);\n if (this.assets.has(hashedKey)) {\n throw new StoreValidationError(\n `Duplicate asset key \"${display}\" (hash collision with existing key).`,\n );\n }\n\n if (!input.version) {\n throw new StoreValidationError(`Asset \"${display}\": version is required.`);\n }\n if (!input.mimeType || !MIME_PATTERN.test(input.mimeType)) {\n throw new StoreValidationError(\n `Asset \"${display}\": mimeType must be a valid type/subtype string (got \"${input.mimeType ?? \"\"}\").`,\n );\n }\n if (!input.url) {\n throw new StoreValidationError(`Asset \"${display}\": url is required.`);\n }\n try {\n const parsed = new URL(input.url);\n if (!/^https?:$/i.test(parsed.protocol)) {\n throw new StoreValidationError(\n `Asset \"${display}\": URL must use http or https (got \"${parsed.protocol}\").`,\n );\n }\n } catch (err) {\n if (err instanceof StoreValidationError) throw err;\n throw new StoreValidationError(`Asset \"${display}\": URL is not valid: \"${input.url}\".`);\n }\n\n // byteLength: any non-negative finite number is accepted, including non-integer fractions\n // (see MediaAssetInput.byteLength JSDoc).\n if (\n input.byteLength !== undefined &&\n (typeof input.byteLength !== \"number\" ||\n !Number.isFinite(input.byteLength) ||\n input.byteLength < 0)\n ) {\n throw new StoreValidationError(\n `Asset \"${display}\": byteLength must be a non-negative finite number (got ${String(input.byteLength)}).`,\n );\n }\n\n const fileName = input.fileName ?? deriveAssetFileName(input.url);\n\n const assetIndexes: Record<string, string | string[]> = Object.create(null);\n if (input.indexes !== undefined && !Array.isArray(input.indexes)) {\n throw new StoreValidationError(\n `Asset \"${display}\": indexes must be an array of IndexTag entries produced by calling a defineIndex handle.`,\n );\n }\n if (input.indexes) {\n for (const tag of input.indexes) {\n if (!(tag instanceof IndexTag)) {\n throw new StoreValidationError(\n `Asset \"${display}\": indexes must be an array of IndexTag entries produced by calling a defineIndex handle.`,\n );\n }\n const indexName = tag.name;\n const value = tag.value;\n const def = this.indexes.get(indexName);\n if (!def) {\n throw new StoreValidationError(\n `Asset \"${display}\": index \"${indexName}\" has not been defined. Call store.defineIndex(\"${indexName}\") first.`,\n );\n }\n if (Object.hasOwn(assetIndexes, indexName)) {\n throw new StoreValidationError(\n `Asset \"${display}\": duplicate index \"${indexName}\" in indexes array.`,\n );\n }\n\n if (def.cardinality === \"single\") {\n if (Array.isArray(value)) {\n throw new StoreValidationError(\n `Asset \"${display}\": index \"${indexName}\" has single cardinality but received an array. ` +\n `Use { cardinality: \"multi\" } when defining the index to allow arrays.`,\n );\n }\n if (typeof value !== \"string\" || !value) {\n throw new StoreValidationError(\n `Asset \"${display}\": index \"${indexName}\" value must be a non-empty string.`,\n );\n }\n assetIndexes[indexName] = value;\n } else {\n const values = Array.isArray(value) ? value : [value];\n if (values.length === 0) {\n throw new StoreValidationError(\n `Asset \"${display}\": index \"${indexName}\" value array must not be empty.`,\n );\n }\n for (const v of values) {\n if (typeof v !== \"string\" || !v) {\n throw new StoreValidationError(\n `Asset \"${display}\": index \"${indexName}\" values must be non-empty strings.`,\n );\n }\n }\n assetIndexes[indexName] = [...new Set(values)];\n }\n }\n }\n\n const metadataSnapshot = input.metadata === undefined ? {} : structuredClone(input.metadata);\n\n this.assets.set(hashedKey, {\n key: hashedKey,\n displayKey: display,\n version: input.version,\n mimeType: input.mimeType,\n url: input.url,\n fileName,\n byteLength: input.byteLength,\n metadata: metadataSnapshot,\n indexes: assetIndexes,\n });\n }\n\n /**\n * Serialize the store for the sync engine. Validates required indexes and produces\n * the flat manifest consumed internally. Not part of the public consumer API.\n * @internal\n */\n _serialize(): FlatManifest {\n for (const [, def] of this.indexes) {\n if (!def.required) continue;\n for (const [, asset] of this.assets) {\n if (!(def.name in asset.indexes)) {\n throw new StoreValidationError(\n `Asset \"${asset.displayKey}\": required index \"${def.name}\" is missing.`,\n );\n }\n }\n }\n\n const indexDefinitions: IndexDefinition[] = [\n ...Array.from(this.indexes.values()),\n { name: \"mimeType\", cardinality: \"single\", required: false, builtin: true },\n { name: \"mediaKind\", cardinality: \"single\", required: false, builtin: true },\n ];\n\n const assets: FlatManifestAsset[] = [];\n for (const [, stored] of this.assets) {\n const mediaKind = mediaKindFromMime(stored.mimeType);\n const stem = fileStem(stored.fileName);\n\n const allIndexes: Record<string, string | string[]> = {\n ...stored.indexes,\n mimeType: stored.mimeType,\n mediaKind,\n };\n\n assets.push({\n key: stored.key,\n displayKey: stored.displayKey,\n version: stored.version,\n mimeType: stored.mimeType,\n mediaKind,\n url: stored.url,\n fileName: stored.fileName,\n fileStem: stem,\n byteLength: stored.byteLength,\n metadata: stored.metadata,\n indexes: allIndexes,\n });\n }\n\n return {\n snapshotId: this.options.snapshotId,\n retrievedAt: this.options.retrievedAt,\n expiresAt: this.options.expiresAt,\n indexDefinitions,\n assets,\n };\n }\n}\n\n/** Creates a new {@link MediaStore} for populating in a `resolveStore` callback. */\nexport function createMediaStore(options?: MediaStoreOptions): MediaStore {\n return new MediaStore(options);\n}\n"],"mappings":"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"}
|