@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,862 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_shared_ipc = require("../shared/ipc.cjs");
|
|
3
|
+
const require_shared_errors = require("../shared/errors.cjs");
|
|
4
|
+
const require_shared_normalize = require("../shared/normalize.cjs");
|
|
5
|
+
const require_shared_stem = require("../shared/stem.cjs");
|
|
6
|
+
const require_internal_log_format = require("../internal/log-format.cjs");
|
|
7
|
+
const require_internal_validation = require("../internal/validation.cjs");
|
|
8
|
+
const require_main_database = require("./database.cjs");
|
|
9
|
+
const require_internal_url_warn = require("../internal/url-warn.cjs");
|
|
10
|
+
const require_internal_asset_key = require("../internal/asset-key.cjs");
|
|
11
|
+
const require_main_storage_root_lock = require("./storage-root-lock.cjs");
|
|
12
|
+
const require_asset_download = require("./asset-download.cjs");
|
|
13
|
+
let node_events = require("node:events");
|
|
14
|
+
let node_fs = require("node:fs");
|
|
15
|
+
let node_fs_promises = require("node:fs/promises");
|
|
16
|
+
let node_stream = require("node:stream");
|
|
17
|
+
let node_timers_promises = require("node:timers/promises");
|
|
18
|
+
let node_module = require("node:module");
|
|
19
|
+
let node_path = require("node:path");
|
|
20
|
+
//#region src/main/media-cache.ts
|
|
21
|
+
const requireElectron = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
22
|
+
const DEFAULT_STALE_DELETE_MS = 10080 * 60 * 1e3;
|
|
23
|
+
const DEFAULT_SYNC_HISTORY_LIMIT = 50;
|
|
24
|
+
const LOG_LEVEL_WEIGHT = {
|
|
25
|
+
debug: 10,
|
|
26
|
+
info: 20,
|
|
27
|
+
warn: 30,
|
|
28
|
+
error: 40
|
|
29
|
+
};
|
|
30
|
+
let mediaCacheProtocolSchemesPrivileged = false;
|
|
31
|
+
function isModuleNotFoundError(error) {
|
|
32
|
+
return error instanceof Error && "code" in error && error.code === "MODULE_NOT_FOUND";
|
|
33
|
+
}
|
|
34
|
+
/** True when not a production build; Electron `forge start` often leaves `NODE_ENV` unset. */
|
|
35
|
+
function isNonProductionNodeEnv() {
|
|
36
|
+
return process.env.NODE_ENV !== "production";
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Registers the privileged `media:` scheme once per process. Call happens when constructing
|
|
40
|
+
* {@link MediaCache} in offline mode so consumers do not need a separate bootstrap step.
|
|
41
|
+
*
|
|
42
|
+
* No-ops when `electron` cannot be loaded (e.g. unit tests outside Electron). When Electron is
|
|
43
|
+
* available, failures from `protocol.registerSchemesAsPrivileged` propagate (including calling
|
|
44
|
+
* too late after `app.ready`).
|
|
45
|
+
*/
|
|
46
|
+
function ensureMediaCacheProtocolSchemesPrivileged() {
|
|
47
|
+
if (mediaCacheProtocolSchemesPrivileged) return;
|
|
48
|
+
let protocol;
|
|
49
|
+
try {
|
|
50
|
+
({protocol} = requireElectron("electron"));
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (isModuleNotFoundError(error)) return;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
if (protocol == null || typeof protocol.registerSchemesAsPrivileged !== "function") return;
|
|
56
|
+
protocol.registerSchemesAsPrivileged([{
|
|
57
|
+
scheme: "media",
|
|
58
|
+
privileges: {
|
|
59
|
+
standard: true,
|
|
60
|
+
secure: true,
|
|
61
|
+
supportFetchAPI: true,
|
|
62
|
+
stream: true
|
|
63
|
+
}
|
|
64
|
+
}]);
|
|
65
|
+
mediaCacheProtocolSchemesPrivileged = true;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Clears the internal `media:` scheme registration flag so subsequent {@link MediaCache}
|
|
69
|
+
* construction runs registration again. **Unit tests only**; do not use in application code.
|
|
70
|
+
* @internal
|
|
71
|
+
*/
|
|
72
|
+
function resetMediaCacheProtocolRegistrationStateForTests() {
|
|
73
|
+
mediaCacheProtocolSchemesPrivileged = false;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Clears any held storage-root locks so tests can run multiple scenarios in one process.
|
|
77
|
+
* **Unit tests only**; do not use in application code.
|
|
78
|
+
* @internal
|
|
79
|
+
*/
|
|
80
|
+
const resetMediaCacheStorageRootLocksForTests = require_main_storage_root_lock.resetStorageRootLocksForTests;
|
|
81
|
+
/**
|
|
82
|
+
* Disables storage-root exclusivity checks so tests can exercise multiple cache instances freely.
|
|
83
|
+
* **Unit tests only**; do not use in application code.
|
|
84
|
+
* @internal
|
|
85
|
+
*/
|
|
86
|
+
const disableMediaCacheStorageRootLockForTests = require_main_storage_root_lock.disableStorageRootLockForTests;
|
|
87
|
+
/**
|
|
88
|
+
* Re-enables storage-root exclusivity checks after test-only overrides.
|
|
89
|
+
* **Unit tests only**; do not use in application code.
|
|
90
|
+
* @internal
|
|
91
|
+
*/
|
|
92
|
+
const enableMediaCacheStorageRootLockForTests = require_main_storage_root_lock.enableStorageRootLockForTests;
|
|
93
|
+
/** Constructs a {@link MediaCacheMain} instance with the given options (does not start sync until `start` or `syncNow`). */
|
|
94
|
+
function createMediaCache(options) {
|
|
95
|
+
return new MediaCache(options);
|
|
96
|
+
}
|
|
97
|
+
var MediaCache = class {
|
|
98
|
+
options;
|
|
99
|
+
events = new node_events.EventEmitter();
|
|
100
|
+
deps;
|
|
101
|
+
/** When no custom handler is configured, log to the main-process console in development. */
|
|
102
|
+
defaultDevelopmentConsole;
|
|
103
|
+
/** Custom structured log sink when configured via `options.logging.onLog`. */
|
|
104
|
+
logHandler;
|
|
105
|
+
/** Built-in console line shape when {@link defaultDevelopmentConsole} is active. */
|
|
106
|
+
logFormat;
|
|
107
|
+
/** Minimum severity emitted for the currently active log sink. */
|
|
108
|
+
effectiveLogLevel;
|
|
109
|
+
devPassthrough;
|
|
110
|
+
assetBaseUrlOrigin;
|
|
111
|
+
storageRootLock = null;
|
|
112
|
+
db = null;
|
|
113
|
+
storageRoot = null;
|
|
114
|
+
status;
|
|
115
|
+
syncPromise = null;
|
|
116
|
+
ipcAttached = false;
|
|
117
|
+
protocolRegistered = false;
|
|
118
|
+
constructor(options, deps) {
|
|
119
|
+
this.options = options;
|
|
120
|
+
this.deps = {
|
|
121
|
+
fetchImpl: deps?.fetchImpl ?? globalThis.fetch.bind(globalThis),
|
|
122
|
+
now: deps?.now ?? Date.now,
|
|
123
|
+
sleep: deps?.sleep ?? node_timers_promises.setTimeout,
|
|
124
|
+
resolveAppPath: deps?.resolveAppPath ?? resolveElectronAppPath,
|
|
125
|
+
statfs: deps?.statfs ?? node_fs_promises.statfs
|
|
126
|
+
};
|
|
127
|
+
const logging = normalizeLoggingOptions(options.logging);
|
|
128
|
+
this.logHandler = logging.onLog;
|
|
129
|
+
this.defaultDevelopmentConsole = this.logHandler == null && isNonProductionNodeEnv() && process.env.VITEST !== "true";
|
|
130
|
+
this.logFormat = logging.format;
|
|
131
|
+
this.effectiveLogLevel = logging.level ?? (this.logHandler == null && this.defaultDevelopmentConsole ? "debug" : "info");
|
|
132
|
+
this.devPassthrough = options.devPassthrough ?? process.env.NODE_ENV === "development";
|
|
133
|
+
if (this.devPassthrough) this.assetBaseUrlOrigin = normalizeAssetBaseUrl(options.assetBaseUrl);
|
|
134
|
+
else {
|
|
135
|
+
if (options.assetBaseUrl) throw new Error("assetBaseUrl has no effect when devPassthrough is false. Set devPassthrough: true or remove assetBaseUrl.");
|
|
136
|
+
this.assetBaseUrlOrigin = null;
|
|
137
|
+
}
|
|
138
|
+
if (this.devPassthrough) this.emitLog("info", "dev_passthrough_active", {
|
|
139
|
+
source: options.devPassthrough === true ? "option" : "node_env",
|
|
140
|
+
node_env: process.env.NODE_ENV ?? null
|
|
141
|
+
});
|
|
142
|
+
if (this.devPassthrough && this.options.onSyncFailure && this.options.onSyncFailure !== "throw") this.emitLog("warn", "dev_passthrough_ignores_sync_failure_mode", { configured_mode: this.options.onSyncFailure });
|
|
143
|
+
this.status = {
|
|
144
|
+
phase: "idle",
|
|
145
|
+
storageRoot: null,
|
|
146
|
+
activeGenerationId: null,
|
|
147
|
+
progress: null,
|
|
148
|
+
lastRun: null,
|
|
149
|
+
error: null,
|
|
150
|
+
updatedAt: this.deps.now()
|
|
151
|
+
};
|
|
152
|
+
if (!this.devPassthrough) ensureMediaCacheProtocolSchemesPrivileged();
|
|
153
|
+
}
|
|
154
|
+
async start() {
|
|
155
|
+
await this.registerProtocol();
|
|
156
|
+
await this.attachIpc();
|
|
157
|
+
await this.syncNow();
|
|
158
|
+
}
|
|
159
|
+
async syncNow() {
|
|
160
|
+
await this.ensureInitialized();
|
|
161
|
+
if (this.syncPromise) {
|
|
162
|
+
this.emitLog("debug", "sync_reused", {
|
|
163
|
+
phase: this.status.phase,
|
|
164
|
+
active_generation_id: this.status.activeGenerationId
|
|
165
|
+
});
|
|
166
|
+
return this.syncPromise;
|
|
167
|
+
}
|
|
168
|
+
this.syncPromise = this.runSync().finally(() => {
|
|
169
|
+
this.syncPromise = null;
|
|
170
|
+
});
|
|
171
|
+
return this.syncPromise;
|
|
172
|
+
}
|
|
173
|
+
async getStatus() {
|
|
174
|
+
await this.ensureInitialized();
|
|
175
|
+
return this.status;
|
|
176
|
+
}
|
|
177
|
+
async getAsset(key) {
|
|
178
|
+
const hashedKey = require_internal_asset_key.hashKey(key);
|
|
179
|
+
await this.ensureInitialized();
|
|
180
|
+
return this.db.getAsset(hashedKey);
|
|
181
|
+
}
|
|
182
|
+
async listByIndex(indexName, value, pagination) {
|
|
183
|
+
const validatedIndexName = require_internal_validation.parseWithSchema(require_internal_validation.stringInputSchema, indexName, "index name");
|
|
184
|
+
const validatedValue = require_internal_validation.parseWithSchema(require_internal_validation.stringInputSchema, value, "index value");
|
|
185
|
+
const validatedPagination = require_internal_validation.parseWithSchema(require_internal_validation.optionalPaginationInputSchema, pagination, "index pagination input");
|
|
186
|
+
await this.ensureInitialized();
|
|
187
|
+
return this.db.listByIndex(validatedIndexName, validatedValue, validatedPagination);
|
|
188
|
+
}
|
|
189
|
+
async findByFileStem(stem, pagination) {
|
|
190
|
+
const validatedStem = require_internal_validation.parseWithSchema(require_internal_validation.stringInputSchema, stem, "file stem");
|
|
191
|
+
const validatedPagination = require_internal_validation.parseWithSchema(require_internal_validation.optionalPaginationInputSchema, pagination, "file stem pagination input");
|
|
192
|
+
await this.ensureInitialized();
|
|
193
|
+
return this.db.findByFileStem(require_shared_stem.normalizeStem(validatedStem), validatedPagination);
|
|
194
|
+
}
|
|
195
|
+
async registerProtocol(options) {
|
|
196
|
+
await this.ensureInitialized();
|
|
197
|
+
if (this.devPassthrough) {
|
|
198
|
+
this.protocolRegistered = true;
|
|
199
|
+
this.emitLog("debug", "protocol_registration_skipped", { reason: "dev_passthrough" });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (this.protocolRegistered) {
|
|
203
|
+
this.emitLog("debug", "protocol_registration_skipped", { reason: "already_registered" });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const electron = options?.session ? null : await import("electron");
|
|
207
|
+
const session = options?.session ?? electron?.session?.defaultSession;
|
|
208
|
+
if (!session || typeof session.protocol?.handle !== "function") {
|
|
209
|
+
this.emitLog("debug", "protocol_registration_skipped", { reason: "session_unavailable" });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const fetchFile = options?.fetchFile ?? (async (request, filePath) => createFileResponse(filePath, request));
|
|
213
|
+
session.protocol.handle("media", async (request) => {
|
|
214
|
+
const parsed = new URL(request.url);
|
|
215
|
+
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
216
|
+
if (parsed.hostname !== "asset" || parts.length !== 1) return new Response("Not found", { status: 404 });
|
|
217
|
+
let assetKey;
|
|
218
|
+
try {
|
|
219
|
+
assetKey = decodeURIComponent(parts[0]);
|
|
220
|
+
} catch {
|
|
221
|
+
return new Response("Not found", { status: 404 });
|
|
222
|
+
}
|
|
223
|
+
const target = this.db.getProtocolAssetTarget(assetKey);
|
|
224
|
+
if (!target) {
|
|
225
|
+
this.emitLog("debug", "protocol_request_not_found", {
|
|
226
|
+
asset_key: assetKey,
|
|
227
|
+
method: request.method
|
|
228
|
+
});
|
|
229
|
+
return new Response("Not found", { status: 404 });
|
|
230
|
+
}
|
|
231
|
+
if (!target.absolutePath || !(0, node_fs.existsSync)(target.absolutePath)) {
|
|
232
|
+
this.emitLog("debug", "protocol_request_file_missing", {
|
|
233
|
+
asset_key: assetKey,
|
|
234
|
+
method: request.method
|
|
235
|
+
});
|
|
236
|
+
return new Response("Not found", { status: 404 });
|
|
237
|
+
}
|
|
238
|
+
this.emitLog("debug", "protocol_request_local_resolved", {
|
|
239
|
+
asset_key: assetKey,
|
|
240
|
+
method: request.method,
|
|
241
|
+
range: request.headers.get("range")
|
|
242
|
+
});
|
|
243
|
+
return fetchFile(request, target.absolutePath);
|
|
244
|
+
});
|
|
245
|
+
this.protocolRegistered = true;
|
|
246
|
+
this.emitLog("info", "protocol_registered", {});
|
|
247
|
+
}
|
|
248
|
+
async attachIpc(options) {
|
|
249
|
+
await this.ensureInitialized();
|
|
250
|
+
if (this.ipcAttached) {
|
|
251
|
+
this.emitLog("debug", "ipc_attach_skipped", { reason: "already_attached" });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const electron = options?.ipcMain ? null : await import("electron");
|
|
255
|
+
const ipcMain = options?.ipcMain ?? electron?.ipcMain;
|
|
256
|
+
if (!ipcMain || typeof ipcMain.handle !== "function") {
|
|
257
|
+
this.emitLog("debug", "ipc_attach_skipped", { reason: "ipc_main_unavailable" });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
ipcMain.handle(require_shared_ipc.MEDIA_CACHE_IPC.getStatus, async () => this.getStatus());
|
|
261
|
+
ipcMain.handle(require_shared_ipc.MEDIA_CACHE_IPC.syncNow, async () => this.syncNow());
|
|
262
|
+
ipcMain.handle(require_shared_ipc.MEDIA_CACHE_IPC.getAsset, async (_event, key) => this.getAsset(key));
|
|
263
|
+
ipcMain.handle(require_shared_ipc.MEDIA_CACHE_IPC.listByIndex, async (_event, indexName, value, pagination) => this.listByIndex(indexName, value, pagination));
|
|
264
|
+
ipcMain.handle(require_shared_ipc.MEDIA_CACHE_IPC.findByFileStem, async (_event, stem, pagination) => this.findByFileStem(stem, pagination));
|
|
265
|
+
if (electron) this.events.on(require_shared_ipc.MEDIA_CACHE_IPC.statusChanged, (status) => {
|
|
266
|
+
for (const window of electron.BrowserWindow.getAllWindows()) window.webContents.send(require_shared_ipc.MEDIA_CACHE_IPC.statusChanged, status);
|
|
267
|
+
});
|
|
268
|
+
this.ipcAttached = true;
|
|
269
|
+
this.emitLog("info", "ipc_attached", {});
|
|
270
|
+
}
|
|
271
|
+
async ensureInitialized() {
|
|
272
|
+
if (this.db) return;
|
|
273
|
+
this.storageRoot = await resolveStorageRoot(this.options.storagePath, this.deps.resolveAppPath);
|
|
274
|
+
(0, node_fs.mkdirSync)(this.storageRoot, { recursive: true });
|
|
275
|
+
this.storageRootLock ??= require_main_storage_root_lock.acquireStorageRootLock(this.storageRoot, this);
|
|
276
|
+
(0, node_fs.mkdirSync)((0, node_path.join)(this.storageRoot, "temp"), { recursive: true });
|
|
277
|
+
(0, node_fs.mkdirSync)((0, node_path.join)(this.storageRoot, "blobs"), { recursive: true });
|
|
278
|
+
if (!this.devPassthrough) this.emitLog("info", "cache_storage_location", { storage_root: this.storageRoot });
|
|
279
|
+
this.db = new require_main_database.MediaCacheDatabase(this.storageRoot, {
|
|
280
|
+
devPassthrough: this.devPassthrough,
|
|
281
|
+
assetBaseUrlOrigin: this.assetBaseUrlOrigin,
|
|
282
|
+
onWarn: (contextLabel, err) => {
|
|
283
|
+
if (this.logHandler != null || this.defaultDevelopmentConsole) this.emitLog("warn", "resolve_asset_base_url_fallback", {
|
|
284
|
+
context_label: contextLabel,
|
|
285
|
+
error: err != null ? String(err) : void 0
|
|
286
|
+
});
|
|
287
|
+
else require_internal_url_warn.consoleWarnResolveAssetBaseUrlFallback(contextLabel, err);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
if (this.devPassthrough) this.prepareDevRuntimeState();
|
|
291
|
+
let storedStatus = null;
|
|
292
|
+
let activeGenerationId = null;
|
|
293
|
+
if (!this.devPassthrough) {
|
|
294
|
+
activeGenerationId = this.reconcileOrphanedStagedGenerations();
|
|
295
|
+
try {
|
|
296
|
+
storedStatus = this.db.loadStatus();
|
|
297
|
+
} catch (error) {
|
|
298
|
+
if (!(error instanceof require_shared_errors.DataValidationError)) throw error;
|
|
299
|
+
this.emitLog("warn", "status_snapshot_invalid", {
|
|
300
|
+
error_code: error.code,
|
|
301
|
+
error_message: error.message
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
if (storedStatus) this.status = storedStatus;
|
|
305
|
+
else if (activeGenerationId !== null) this.status = {
|
|
306
|
+
...this.status,
|
|
307
|
+
phase: "ready",
|
|
308
|
+
activeGenerationId,
|
|
309
|
+
progress: null,
|
|
310
|
+
error: null
|
|
311
|
+
};
|
|
312
|
+
this.status.activeGenerationId = activeGenerationId;
|
|
313
|
+
}
|
|
314
|
+
this.status = {
|
|
315
|
+
...this.status,
|
|
316
|
+
storageRoot: this.storageRoot
|
|
317
|
+
};
|
|
318
|
+
this.emitLog("info", "cache_initialized", {
|
|
319
|
+
storage_root: this.storageRoot,
|
|
320
|
+
active_generation_id: this.status.activeGenerationId,
|
|
321
|
+
dev_passthrough_enabled: this.devPassthrough
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
reconcileOrphanedStagedGenerations() {
|
|
325
|
+
const activeGenerationId = this.db.getActiveGenerationId();
|
|
326
|
+
const stagedGenerationIds = this.db.listStagedGenerationIds().filter((generationId) => generationId !== activeGenerationId);
|
|
327
|
+
if (stagedGenerationIds.length === 0) return activeGenerationId;
|
|
328
|
+
for (const stagedGenerationId of stagedGenerationIds) {
|
|
329
|
+
this.cleanupStagedGenerationFiles(stagedGenerationId, activeGenerationId);
|
|
330
|
+
this.db.deleteGeneration(stagedGenerationId);
|
|
331
|
+
}
|
|
332
|
+
this.emitLog("warn", "orphaned_staged_generations_removed", {
|
|
333
|
+
active_generation_id: activeGenerationId,
|
|
334
|
+
removed_generation_ids: stagedGenerationIds,
|
|
335
|
+
removed_generation_count: stagedGenerationIds.length
|
|
336
|
+
});
|
|
337
|
+
return activeGenerationId;
|
|
338
|
+
}
|
|
339
|
+
prepareDevRuntimeState() {
|
|
340
|
+
this.emitLog("warn", "dev_passthrough_clearing_state", {
|
|
341
|
+
storage_root: this.storageRoot,
|
|
342
|
+
reason: "devPassthrough=true clears all local state on startup"
|
|
343
|
+
});
|
|
344
|
+
this.db.clearAllState();
|
|
345
|
+
(0, node_fs.rmSync)((0, node_path.join)(this.storageRoot, "blobs"), {
|
|
346
|
+
recursive: true,
|
|
347
|
+
force: true
|
|
348
|
+
});
|
|
349
|
+
(0, node_fs.rmSync)((0, node_path.join)(this.storageRoot, "temp"), {
|
|
350
|
+
recursive: true,
|
|
351
|
+
force: true
|
|
352
|
+
});
|
|
353
|
+
(0, node_fs.mkdirSync)((0, node_path.join)(this.storageRoot, "blobs"), { recursive: true });
|
|
354
|
+
(0, node_fs.mkdirSync)((0, node_path.join)(this.storageRoot, "temp"), { recursive: true });
|
|
355
|
+
this.status = {
|
|
356
|
+
...this.status,
|
|
357
|
+
phase: "idle",
|
|
358
|
+
activeGenerationId: null,
|
|
359
|
+
progress: null,
|
|
360
|
+
lastRun: null,
|
|
361
|
+
error: null,
|
|
362
|
+
updatedAt: this.deps.now()
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
async runSync() {
|
|
366
|
+
const now = this.deps.now();
|
|
367
|
+
const runId = this.db.createSyncRun(now);
|
|
368
|
+
const stats = {
|
|
369
|
+
totalAssets: 0,
|
|
370
|
+
downloadedAssets: 0,
|
|
371
|
+
skippedAssets: 0,
|
|
372
|
+
bytesDownloaded: 0
|
|
373
|
+
};
|
|
374
|
+
this.updateStatus({
|
|
375
|
+
phase: "syncing",
|
|
376
|
+
error: null,
|
|
377
|
+
progress: {
|
|
378
|
+
runId,
|
|
379
|
+
phase: "resolving-store",
|
|
380
|
+
totalAssets: 0,
|
|
381
|
+
completedAssets: 0,
|
|
382
|
+
downloadedAssets: 0,
|
|
383
|
+
skippedAssets: 0,
|
|
384
|
+
bytesDownloaded: 0
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
this.emitLog("info", "sync_started", {
|
|
388
|
+
run_id: runId,
|
|
389
|
+
active_generation_id: this.status.activeGenerationId
|
|
390
|
+
});
|
|
391
|
+
let stagedGenerationId = null;
|
|
392
|
+
try {
|
|
393
|
+
const manifest = require_shared_normalize.validateFlatManifest((await this.options.resolveStore())._serialize());
|
|
394
|
+
this.assertStoreNotExpired(manifest, runId);
|
|
395
|
+
stagedGenerationId = this.db.createStagedGeneration(manifest, now);
|
|
396
|
+
this.emitLog("info", "store_resolved", {
|
|
397
|
+
run_id: runId,
|
|
398
|
+
staged_generation_id: stagedGenerationId,
|
|
399
|
+
index_count: manifest.indexDefinitions.length,
|
|
400
|
+
asset_count: manifest.assets.length
|
|
401
|
+
});
|
|
402
|
+
const currentGenerationId = this.db.getActiveGenerationId();
|
|
403
|
+
const currentAssets = currentGenerationId ? this.db.getGenerationAssets(currentGenerationId) : [];
|
|
404
|
+
const stagedAssets = this.db.getGenerationAssets(stagedGenerationId);
|
|
405
|
+
stats.totalAssets = stagedAssets.length;
|
|
406
|
+
this.updateProgress((progress) => ({
|
|
407
|
+
...progress,
|
|
408
|
+
phase: "diffing",
|
|
409
|
+
totalAssets: stagedAssets.length
|
|
410
|
+
}));
|
|
411
|
+
const manifestAssetMap = new Map(manifest.assets.map((asset) => [asset.key, asset]));
|
|
412
|
+
const currentMap = new Map(currentAssets.map((row) => [row.assetKey, row]));
|
|
413
|
+
const downloads = [];
|
|
414
|
+
for (const row of stagedAssets) {
|
|
415
|
+
const manifestAsset = manifestAssetMap.get(row.assetKey);
|
|
416
|
+
if (!manifestAsset) throw new require_shared_errors.StoreValidationError(`Asset "${row.assetKey}" not found in serialized store.`);
|
|
417
|
+
const activeRow = currentMap.get(row.assetKey);
|
|
418
|
+
const activeRelativePath = activeRow?.relativePath ?? null;
|
|
419
|
+
const normalizedActiveRelativePath = activeRelativePath === null ? null : normalizeStoredRelativePath(activeRelativePath);
|
|
420
|
+
const nextVersion = manifestAsset.version;
|
|
421
|
+
const canReuseActiveBlob = normalizedActiveRelativePath !== null && (0, node_fs.existsSync)((0, node_path.join)(this.storageRoot, normalizedActiveRelativePath));
|
|
422
|
+
if (this.devPassthrough) {
|
|
423
|
+
stats.skippedAssets += 1;
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (canReuseActiveBlob) {
|
|
427
|
+
if (getResolvedVersionFromPath(normalizedActiveRelativePath) === nextVersion) {
|
|
428
|
+
this.db.setAssetDownloadState(stagedGenerationId, row.assetKey, normalizedActiveRelativePath, activeRow?.mimeType ?? null);
|
|
429
|
+
stats.skippedAssets += 1;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
downloads.push({
|
|
434
|
+
assetKey: row.assetKey,
|
|
435
|
+
version: manifestAsset.version,
|
|
436
|
+
fileName: manifestAsset.fileName,
|
|
437
|
+
byteLength: manifestAsset.byteLength
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
this.emitLog("info", "sync_diffed", {
|
|
441
|
+
run_id: runId,
|
|
442
|
+
total_assets: stagedAssets.length,
|
|
443
|
+
download_count: downloads.length,
|
|
444
|
+
skipped_assets: stats.skippedAssets,
|
|
445
|
+
dev_passthrough_enabled: this.devPassthrough
|
|
446
|
+
});
|
|
447
|
+
await this.pruneExpiredDeletions();
|
|
448
|
+
this.cleanupObsoletePartialDownloads(downloads);
|
|
449
|
+
if (!this.devPassthrough) {
|
|
450
|
+
await this.enforceStorageLimits(downloads);
|
|
451
|
+
this.updateProgress((progress) => ({
|
|
452
|
+
...progress,
|
|
453
|
+
phase: "downloading",
|
|
454
|
+
totalAssets: stagedAssets.length,
|
|
455
|
+
completedAssets: stats.skippedAssets,
|
|
456
|
+
skippedAssets: stats.skippedAssets
|
|
457
|
+
}));
|
|
458
|
+
for (const queuedDownload of downloads) {
|
|
459
|
+
this.assertStoreNotExpired(manifest, runId, queuedDownload);
|
|
460
|
+
const download = {
|
|
461
|
+
...queuedDownload,
|
|
462
|
+
request: { url: manifestAssetMap.get(queuedDownload.assetKey).url }
|
|
463
|
+
};
|
|
464
|
+
this.emitLog("debug", "asset_download_started", {
|
|
465
|
+
run_id: runId,
|
|
466
|
+
asset_key: download.assetKey,
|
|
467
|
+
version: download.version,
|
|
468
|
+
url: download.request.url
|
|
469
|
+
});
|
|
470
|
+
const { relativePath, fallbackMimeType } = await this.createAssetDownloader().downloadAsset(download, (chunkBytes) => {
|
|
471
|
+
stats.bytesDownloaded += chunkBytes;
|
|
472
|
+
this.updateProgress((progress) => ({
|
|
473
|
+
...progress,
|
|
474
|
+
bytesDownloaded: stats.bytesDownloaded
|
|
475
|
+
}));
|
|
476
|
+
});
|
|
477
|
+
this.db.setAssetDownloadState(stagedGenerationId, download.assetKey, relativePath, fallbackMimeType);
|
|
478
|
+
stats.downloadedAssets += 1;
|
|
479
|
+
this.emitLog("debug", "asset_download_completed", {
|
|
480
|
+
run_id: runId,
|
|
481
|
+
asset_key: download.assetKey,
|
|
482
|
+
relative_path: relativePath
|
|
483
|
+
});
|
|
484
|
+
this.updateProgress((progress) => ({
|
|
485
|
+
...progress,
|
|
486
|
+
completedAssets: stats.downloadedAssets + stats.skippedAssets,
|
|
487
|
+
downloadedAssets: stats.downloadedAssets,
|
|
488
|
+
skippedAssets: stats.skippedAssets,
|
|
489
|
+
bytesDownloaded: stats.bytesDownloaded
|
|
490
|
+
}));
|
|
491
|
+
}
|
|
492
|
+
} else this.updateProgress((progress) => ({
|
|
493
|
+
...progress,
|
|
494
|
+
completedAssets: stagedAssets.length,
|
|
495
|
+
skippedAssets: stagedAssets.length
|
|
496
|
+
}));
|
|
497
|
+
this.updateProgress((progress) => ({
|
|
498
|
+
...progress,
|
|
499
|
+
phase: "committing"
|
|
500
|
+
}));
|
|
501
|
+
const previousGenerationId = this.db.activateGeneration(stagedGenerationId, this.deps.now());
|
|
502
|
+
this.db.clearPendingDeletionsForGeneration(stagedGenerationId);
|
|
503
|
+
if (previousGenerationId) this.markRemovedAssetsForDeletion(previousGenerationId, stagedGenerationId);
|
|
504
|
+
this.emitLog("info", "generation_committed", {
|
|
505
|
+
run_id: runId,
|
|
506
|
+
previous_generation_id: previousGenerationId,
|
|
507
|
+
active_generation_id: stagedGenerationId
|
|
508
|
+
});
|
|
509
|
+
this.updateProgress((progress) => ({
|
|
510
|
+
...progress,
|
|
511
|
+
phase: "pruning"
|
|
512
|
+
}));
|
|
513
|
+
await this.pruneExpiredDeletions();
|
|
514
|
+
const summary = this.db.completeSyncRun(runId, "success", this.deps.now(), stats);
|
|
515
|
+
this.db.pruneSyncHistory(this.options.syncHistoryLimit ?? DEFAULT_SYNC_HISTORY_LIMIT);
|
|
516
|
+
this.emitLog("info", "sync_completed", {
|
|
517
|
+
run_id: runId,
|
|
518
|
+
active_generation_id: stagedGenerationId,
|
|
519
|
+
total_assets: summary.stats.totalAssets,
|
|
520
|
+
downloaded_assets: summary.stats.downloadedAssets,
|
|
521
|
+
skipped_assets: summary.stats.skippedAssets,
|
|
522
|
+
bytes_downloaded: summary.stats.bytesDownloaded
|
|
523
|
+
});
|
|
524
|
+
this.updateStatus({
|
|
525
|
+
phase: "ready",
|
|
526
|
+
activeGenerationId: stagedGenerationId,
|
|
527
|
+
progress: null,
|
|
528
|
+
lastRun: summary,
|
|
529
|
+
error: null
|
|
530
|
+
});
|
|
531
|
+
} catch (error) {
|
|
532
|
+
if (stagedGenerationId !== null) {
|
|
533
|
+
this.cleanupStagedGenerationFiles(stagedGenerationId, this.db.getActiveGenerationId());
|
|
534
|
+
this.db.deleteGeneration(stagedGenerationId);
|
|
535
|
+
}
|
|
536
|
+
const serialized = require_shared_errors.toSerializedError(error);
|
|
537
|
+
const summary = this.db.completeSyncRun(runId, "error", this.deps.now(), stats, serialized.code, serialized.message);
|
|
538
|
+
this.emitLog("error", "sync_failed", {
|
|
539
|
+
run_id: runId,
|
|
540
|
+
active_generation_id: this.db.getActiveGenerationId(),
|
|
541
|
+
error_code: serialized.code,
|
|
542
|
+
error_message: serialized.message,
|
|
543
|
+
total_assets: summary.stats.totalAssets,
|
|
544
|
+
downloaded_assets: summary.stats.downloadedAssets,
|
|
545
|
+
skipped_assets: summary.stats.skippedAssets,
|
|
546
|
+
bytes_downloaded: summary.stats.bytesDownloaded
|
|
547
|
+
});
|
|
548
|
+
this.updateStatus({
|
|
549
|
+
phase: this.devPassthrough || this.options.onSyncFailure === "throw" ? "error" : this.db.getActiveGenerationId() ? "ready" : "error",
|
|
550
|
+
activeGenerationId: this.db.getActiveGenerationId(),
|
|
551
|
+
progress: null,
|
|
552
|
+
lastRun: summary,
|
|
553
|
+
error: serialized
|
|
554
|
+
});
|
|
555
|
+
if (this.devPassthrough || this.options.onSyncFailure === "throw") throw error;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
assertStoreNotExpired(manifest, runId, download) {
|
|
559
|
+
if (!manifest.expiresAt) return;
|
|
560
|
+
const expiresAtMs = Date.parse(manifest.expiresAt);
|
|
561
|
+
const now = this.deps.now();
|
|
562
|
+
if (Number.isNaN(expiresAtMs) || now < expiresAtMs) return;
|
|
563
|
+
this.emitLog("warn", "store_expired", {
|
|
564
|
+
run_id: runId,
|
|
565
|
+
expires_at: manifest.expiresAt,
|
|
566
|
+
now_ms: now,
|
|
567
|
+
asset_key: download?.assetKey
|
|
568
|
+
});
|
|
569
|
+
const assetLabel = download ? ` before downloading ${download.assetKey}` : "";
|
|
570
|
+
throw new require_shared_errors.StoreExpiredError(`Store URLs expired at ${manifest.expiresAt}${assetLabel}. Refresh the store and retry sync.`);
|
|
571
|
+
}
|
|
572
|
+
async enforceStorageLimits(downloads) {
|
|
573
|
+
const estimatedBlobBytes = downloads.reduce((sum, download) => sum + (download.byteLength ?? 0), 0);
|
|
574
|
+
const estimatedRemainingDownloadBytes = downloads.reduce((sum, download) => sum + this.createAssetDownloader().remainingDownloadBytes(download), 0);
|
|
575
|
+
if (this.options.maxCacheBytes !== void 0) {
|
|
576
|
+
const currentBytes = this.currentBytesOnDisk((0, node_path.join)(this.storageRoot, "blobs"));
|
|
577
|
+
if (currentBytes + estimatedBlobBytes > this.options.maxCacheBytes) {
|
|
578
|
+
this.emitLog("warn", "storage_limit_exceeded", {
|
|
579
|
+
current_bytes: currentBytes,
|
|
580
|
+
estimated_download_bytes: estimatedBlobBytes,
|
|
581
|
+
max_cache_bytes: this.options.maxCacheBytes
|
|
582
|
+
});
|
|
583
|
+
throw new require_shared_errors.StorageLimitError(`Estimated cache size ${currentBytes + estimatedBlobBytes} exceeds maxCacheBytes ${this.options.maxCacheBytes}.`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const stats = await this.deps.statfs(this.storageRoot);
|
|
587
|
+
const availableBytes = Number(stats.bavail) * Number(stats.bsize);
|
|
588
|
+
const reserve = require_asset_download.effectiveReserveFreeBytes(this.options.reserveFreeBytes);
|
|
589
|
+
if (availableBytes - estimatedRemainingDownloadBytes < reserve) {
|
|
590
|
+
this.emitLog("warn", "storage_reserve_violation", {
|
|
591
|
+
available_bytes: availableBytes,
|
|
592
|
+
estimated_download_bytes: estimatedRemainingDownloadBytes,
|
|
593
|
+
reserve_free_bytes: reserve
|
|
594
|
+
});
|
|
595
|
+
throw new require_shared_errors.StorageLimitError(`Estimated download size ${estimatedRemainingDownloadBytes} leaves less than reserveFreeBytes ${reserve}.`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
async ensureFileSpaceCommit() {
|
|
599
|
+
await this.createAssetDownloader().ensureFileSpaceCommit();
|
|
600
|
+
}
|
|
601
|
+
cleanupObsoletePartialDownloads(downloads) {
|
|
602
|
+
this.createAssetDownloader().cleanupObsoletePartialDownloads(downloads);
|
|
603
|
+
}
|
|
604
|
+
createAssetDownloader() {
|
|
605
|
+
return new require_asset_download.AssetDownloader(this.storageRoot, this.deps, {
|
|
606
|
+
reserveFreeBytes: this.options.reserveFreeBytes,
|
|
607
|
+
emitLog: (level, event, fields = {}) => this.emitLog(level, event, fields)
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
cleanupStagedGenerationFiles(stagedGenerationId, activeGenerationId) {
|
|
611
|
+
const activePaths = new Set(activeGenerationId ? this.db.getGenerationAssets(activeGenerationId).flatMap((row) => row.relativePath ? [normalizeStoredRelativePath(row.relativePath)] : []) : []);
|
|
612
|
+
for (const row of this.db.getGenerationAssets(stagedGenerationId)) {
|
|
613
|
+
if (!row.relativePath) continue;
|
|
614
|
+
const normalizedRelativePath = normalizeStoredRelativePath(row.relativePath);
|
|
615
|
+
if (activePaths.has(normalizedRelativePath)) continue;
|
|
616
|
+
const absolutePath = (0, node_path.join)(this.storageRoot, normalizedRelativePath);
|
|
617
|
+
(0, node_fs.rmSync)(absolutePath, { force: true });
|
|
618
|
+
pruneEmptyParents(absolutePath, this.storageRoot);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
markRemovedAssetsForDeletion(previousGenerationId, stagedGenerationId) {
|
|
622
|
+
const previousAssets = this.db.getGenerationAssets(previousGenerationId);
|
|
623
|
+
const nextAssets = new Map(this.db.getGenerationAssets(stagedGenerationId).map((row) => [row.assetKey, row.relativePath]));
|
|
624
|
+
const deleteAfterMs = this.deps.now() + (this.options.staleDeleteAfterMs ?? DEFAULT_STALE_DELETE_MS);
|
|
625
|
+
let markedCount = 0;
|
|
626
|
+
for (const row of previousAssets) {
|
|
627
|
+
const nextRelativePath = nextAssets.get(row.assetKey);
|
|
628
|
+
if (row.relativePath && nextRelativePath !== row.relativePath) {
|
|
629
|
+
this.db.markPendingDeletion(row.assetKey, row.relativePath, previousGenerationId, createPendingDeletionKey(row.assetKey, row.relativePath), deleteAfterMs);
|
|
630
|
+
markedCount += 1;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
this.emitLog("debug", "assets_marked_for_deletion", {
|
|
634
|
+
previous_generation_id: previousGenerationId,
|
|
635
|
+
active_generation_id: stagedGenerationId,
|
|
636
|
+
marked_count: markedCount,
|
|
637
|
+
delete_after_ms: deleteAfterMs
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
async pruneExpiredDeletions() {
|
|
641
|
+
const expired = this.db.getExpiredPendingDeletions(this.deps.now());
|
|
642
|
+
if (expired.length === 0) {
|
|
643
|
+
this.emitLog("debug", "deletion_prune_skipped", { expired_count: 0 });
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
for (const deletion of expired) {
|
|
647
|
+
const absolutePath = (0, node_path.join)(this.storageRoot, normalizeStoredRelativePath(deletion.relativePath));
|
|
648
|
+
(0, node_fs.rmSync)(absolutePath, { force: true });
|
|
649
|
+
pruneEmptyParents(absolutePath, this.storageRoot);
|
|
650
|
+
}
|
|
651
|
+
this.db.deletePendingDeletions(expired.map((item) => item.deletionKey));
|
|
652
|
+
this.emitLog("debug", "assets_pruned", { pruned_count: expired.length });
|
|
653
|
+
}
|
|
654
|
+
currentBytesOnDisk(directory) {
|
|
655
|
+
if (!(0, node_fs.existsSync)(directory)) return 0;
|
|
656
|
+
const stats = (0, node_fs.statSync)(directory);
|
|
657
|
+
if (stats.isFile()) return stats.size;
|
|
658
|
+
return (0, node_fs.readdirSync)(directory).reduce((sum, entry) => sum + this.currentBytesOnDisk((0, node_path.join)(directory, entry)), 0);
|
|
659
|
+
}
|
|
660
|
+
updateProgress(transform) {
|
|
661
|
+
if (!this.status.progress) return;
|
|
662
|
+
this.updateStatus({ progress: transform(this.status.progress) });
|
|
663
|
+
}
|
|
664
|
+
updateStatus(partial) {
|
|
665
|
+
this.status = {
|
|
666
|
+
...this.status,
|
|
667
|
+
...partial,
|
|
668
|
+
updatedAt: this.deps.now()
|
|
669
|
+
};
|
|
670
|
+
this.db?.saveStatus(this.status, this.status.updatedAt);
|
|
671
|
+
this.events.emit(require_shared_ipc.MEDIA_CACHE_IPC.statusChanged, this.status);
|
|
672
|
+
}
|
|
673
|
+
emitLog(level, event, fields = {}) {
|
|
674
|
+
if (this.logHandler == null && !this.defaultDevelopmentConsole) return;
|
|
675
|
+
const threshold = LOG_LEVEL_WEIGHT[this.effectiveLogLevel];
|
|
676
|
+
if (LOG_LEVEL_WEIGHT[level] < threshold) return;
|
|
677
|
+
const entry = {
|
|
678
|
+
timestamp: new Date(this.deps.now()).toISOString(),
|
|
679
|
+
level,
|
|
680
|
+
event,
|
|
681
|
+
service: "rockhall-electron-offline-content",
|
|
682
|
+
component: "media-cache",
|
|
683
|
+
...fields
|
|
684
|
+
};
|
|
685
|
+
if (this.logHandler != null) {
|
|
686
|
+
try {
|
|
687
|
+
this.logHandler(entry);
|
|
688
|
+
} catch {}
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
writeDefaultDevelopmentConsoleLog(level, entry, this.logFormat);
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
function normalizeLoggingOptions(logging) {
|
|
695
|
+
if (logging?.onLog != null && logging.format !== void 0) throw new Error("MediaCacheOptions.logging.format cannot be set when logging.onLog is provided.");
|
|
696
|
+
const format = logging?.format;
|
|
697
|
+
if (format !== void 0 && format !== "english" && format !== "json") throw new Error(`Invalid MediaCacheOptions.logging.format: expected "english" | "json", received ${JSON.stringify(format)}`);
|
|
698
|
+
return {
|
|
699
|
+
onLog: logging?.onLog ?? null,
|
|
700
|
+
level: logging?.level,
|
|
701
|
+
format: format ?? "english"
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
function writeDefaultDevelopmentConsoleLog(level, entry, format) {
|
|
705
|
+
const line = require_internal_log_format.formatMediaCacheConsoleLine(entry, format);
|
|
706
|
+
switch (level) {
|
|
707
|
+
case "debug":
|
|
708
|
+
console.debug(line);
|
|
709
|
+
break;
|
|
710
|
+
case "info":
|
|
711
|
+
console.log(line);
|
|
712
|
+
break;
|
|
713
|
+
case "warn":
|
|
714
|
+
console.warn(line);
|
|
715
|
+
break;
|
|
716
|
+
case "error":
|
|
717
|
+
console.error(line);
|
|
718
|
+
break;
|
|
719
|
+
default: console.log(line);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
function createPendingDeletionKey(assetKey, relativePath) {
|
|
723
|
+
return JSON.stringify([assetKey, relativePath]);
|
|
724
|
+
}
|
|
725
|
+
function normalizeAssetBaseUrl(assetBaseUrl) {
|
|
726
|
+
if (!assetBaseUrl) return null;
|
|
727
|
+
let parsed;
|
|
728
|
+
try {
|
|
729
|
+
parsed = new URL(assetBaseUrl);
|
|
730
|
+
} catch {
|
|
731
|
+
throw new Error(`assetBaseUrl is not a valid URL: "${assetBaseUrl}"`);
|
|
732
|
+
}
|
|
733
|
+
if (parsed.username || parsed.password) throw new Error("assetBaseUrl must not include credentials.");
|
|
734
|
+
if (parsed.search || parsed.hash) throw new Error("assetBaseUrl must not include a query string or hash fragment.");
|
|
735
|
+
if (parsed.pathname !== "/" && parsed.pathname !== "") throw new Error("assetBaseUrl must be an origin without a path.");
|
|
736
|
+
return parsed.origin;
|
|
737
|
+
}
|
|
738
|
+
function pruneEmptyParents(pathToFile, storageRoot) {
|
|
739
|
+
let current = (0, node_path.dirname)(pathToFile);
|
|
740
|
+
while (current.startsWith(storageRoot) && current !== storageRoot) {
|
|
741
|
+
if ((0, node_fs.existsSync)(current) && (0, node_fs.readdirSync)(current).length === 0) {
|
|
742
|
+
(0, node_fs.rmSync)(current, {
|
|
743
|
+
recursive: true,
|
|
744
|
+
force: true
|
|
745
|
+
});
|
|
746
|
+
current = (0, node_path.dirname)(current);
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
function getResolvedVersionFromPath(relativePath) {
|
|
753
|
+
const parts = relativePath.split(/[\\/]/);
|
|
754
|
+
return parts.length >= 4 ? decodeURIComponent(parts.at(-2)) : null;
|
|
755
|
+
}
|
|
756
|
+
function normalizeStoredRelativePath(relativePath) {
|
|
757
|
+
return relativePath.split(/[\\/]/).join("/");
|
|
758
|
+
}
|
|
759
|
+
async function resolveElectronAppPath(name) {
|
|
760
|
+
return (await import("electron")).app.getPath(name);
|
|
761
|
+
}
|
|
762
|
+
async function resolveStorageRoot(input, resolveAppPath) {
|
|
763
|
+
const storagePath = require_internal_validation.parseWithSchema(require_internal_validation.mediaCacheStoragePathSchema, input, "storage path");
|
|
764
|
+
return (0, node_path.join)(await resolveAppPath(storagePath.appPath), ...storagePath.segments ?? []);
|
|
765
|
+
}
|
|
766
|
+
function createFileResponse(filePath, request) {
|
|
767
|
+
const size = (0, node_fs.statSync)(filePath).size;
|
|
768
|
+
const rangeHeader = request.headers.get("range");
|
|
769
|
+
const mimeType = inferMimeType(filePath);
|
|
770
|
+
const baseHeaders = new Headers({
|
|
771
|
+
"accept-ranges": "bytes",
|
|
772
|
+
"content-type": mimeType
|
|
773
|
+
});
|
|
774
|
+
if (request.method === "HEAD") {
|
|
775
|
+
baseHeaders.set("content-length", String(size));
|
|
776
|
+
return new Response(null, {
|
|
777
|
+
status: 200,
|
|
778
|
+
headers: baseHeaders
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
if (!rangeHeader) {
|
|
782
|
+
baseHeaders.set("content-length", String(size));
|
|
783
|
+
return new Response(node_stream.Readable.toWeb((0, node_fs.createReadStream)(filePath)), {
|
|
784
|
+
status: 200,
|
|
785
|
+
headers: baseHeaders
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
const parsedRange = parseByteRange(rangeHeader, size);
|
|
789
|
+
if (!parsedRange) {
|
|
790
|
+
baseHeaders.set("content-range", `bytes */${size}`);
|
|
791
|
+
return new Response(null, {
|
|
792
|
+
status: 416,
|
|
793
|
+
headers: baseHeaders
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
const { start, end } = parsedRange;
|
|
797
|
+
const chunkLength = end - start + 1;
|
|
798
|
+
baseHeaders.set("content-length", String(chunkLength));
|
|
799
|
+
baseHeaders.set("content-range", `bytes ${start}-${end}/${size}`);
|
|
800
|
+
return new Response(node_stream.Readable.toWeb((0, node_fs.createReadStream)(filePath, {
|
|
801
|
+
start,
|
|
802
|
+
end
|
|
803
|
+
})), {
|
|
804
|
+
status: 206,
|
|
805
|
+
headers: baseHeaders
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
function parseByteRange(rangeHeader, size) {
|
|
809
|
+
if (!rangeHeader.startsWith("bytes=")) return null;
|
|
810
|
+
const value = rangeHeader.slice(6).trim();
|
|
811
|
+
if (value.length === 0 || value.includes(",")) return null;
|
|
812
|
+
const [startText, endText] = value.split("-", 2);
|
|
813
|
+
if (startText === void 0 || endText === void 0) return null;
|
|
814
|
+
if (startText === "") {
|
|
815
|
+
const suffixLength = Number.parseInt(endText, 10);
|
|
816
|
+
if (!Number.isFinite(suffixLength) || suffixLength <= 0) return null;
|
|
817
|
+
const start = Math.max(size - suffixLength, 0);
|
|
818
|
+
const end = size - 1;
|
|
819
|
+
return start <= end ? {
|
|
820
|
+
start,
|
|
821
|
+
end
|
|
822
|
+
} : null;
|
|
823
|
+
}
|
|
824
|
+
const start = Number.parseInt(startText, 10);
|
|
825
|
+
const end = endText === "" ? size - 1 : Number.parseInt(endText, 10);
|
|
826
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
|
|
827
|
+
if (start < 0 || end < start || start >= size) return null;
|
|
828
|
+
return {
|
|
829
|
+
start,
|
|
830
|
+
end: Math.min(end, size - 1)
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
function inferMimeType(filePath) {
|
|
834
|
+
const lower = filePath.toLowerCase();
|
|
835
|
+
if (lower.endsWith(".mp4")) return "video/mp4";
|
|
836
|
+
if (lower.endsWith(".webm")) return "video/webm";
|
|
837
|
+
if (lower.endsWith(".mov")) return "video/quicktime";
|
|
838
|
+
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
839
|
+
if (lower.endsWith(".png")) return "image/png";
|
|
840
|
+
if (lower.endsWith(".gif")) return "image/gif";
|
|
841
|
+
if (lower.endsWith(".webp")) return "image/webp";
|
|
842
|
+
if (lower.endsWith(".vtt")) return "text/vtt";
|
|
843
|
+
if (lower.endsWith(".srt")) return "application/x-subrip";
|
|
844
|
+
if (lower.endsWith(".mp3")) return "audio/mpeg";
|
|
845
|
+
if (lower.endsWith(".wav")) return "audio/wav";
|
|
846
|
+
if (lower.endsWith(".html")) return "text/html; charset=utf-8";
|
|
847
|
+
if (lower.endsWith(".txt")) return "text/plain; charset=utf-8";
|
|
848
|
+
if (lower.endsWith(".json")) return "application/json; charset=utf-8";
|
|
849
|
+
if (lower.endsWith(".pdf")) return "application/pdf";
|
|
850
|
+
return "application/octet-stream";
|
|
851
|
+
}
|
|
852
|
+
//#endregion
|
|
853
|
+
exports.DEFAULT_RESERVE_FREE_BYTES = require_asset_download.DEFAULT_RESERVE_FREE_BYTES;
|
|
854
|
+
exports.MediaCache = MediaCache;
|
|
855
|
+
exports.createMediaCache = createMediaCache;
|
|
856
|
+
exports.disableMediaCacheStorageRootLockForTests = disableMediaCacheStorageRootLockForTests;
|
|
857
|
+
exports.effectiveReserveFreeBytes = require_asset_download.effectiveReserveFreeBytes;
|
|
858
|
+
exports.enableMediaCacheStorageRootLockForTests = enableMediaCacheStorageRootLockForTests;
|
|
859
|
+
exports.resetMediaCacheProtocolRegistrationStateForTests = resetMediaCacheProtocolRegistrationStateForTests;
|
|
860
|
+
exports.resetMediaCacheStorageRootLocksForTests = resetMediaCacheStorageRootLocksForTests;
|
|
861
|
+
|
|
862
|
+
//# sourceMappingURL=media-cache.cjs.map
|