@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.
Files changed (196) hide show
  1. package/CHANGELOG.md +384 -0
  2. package/LICENSE +21 -0
  3. package/README.md +794 -0
  4. package/dist/internal/asset-file-name.cjs +13 -0
  5. package/dist/internal/asset-file-name.cjs.map +1 -0
  6. package/dist/internal/asset-file-name.d.cts +6 -0
  7. package/dist/internal/asset-file-name.d.cts.map +1 -0
  8. package/dist/internal/asset-file-name.d.ts +6 -0
  9. package/dist/internal/asset-file-name.d.ts.map +1 -0
  10. package/dist/internal/asset-file-name.js +12 -0
  11. package/dist/internal/asset-file-name.js.map +1 -0
  12. package/dist/internal/asset-key.cjs +30 -0
  13. package/dist/internal/asset-key.cjs.map +1 -0
  14. package/dist/internal/asset-key.d.cts +19 -0
  15. package/dist/internal/asset-key.d.cts.map +1 -0
  16. package/dist/internal/asset-key.d.ts +19 -0
  17. package/dist/internal/asset-key.d.ts.map +1 -0
  18. package/dist/internal/asset-key.js +27 -0
  19. package/dist/internal/asset-key.js.map +1 -0
  20. package/dist/internal/log-format.cjs +98 -0
  21. package/dist/internal/log-format.cjs.map +1 -0
  22. package/dist/internal/log-format.d.cts +10 -0
  23. package/dist/internal/log-format.d.cts.map +1 -0
  24. package/dist/internal/log-format.d.ts +10 -0
  25. package/dist/internal/log-format.d.ts.map +1 -0
  26. package/dist/internal/log-format.js +97 -0
  27. package/dist/internal/log-format.js.map +1 -0
  28. package/dist/internal/media-kind.cjs +46 -0
  29. package/dist/internal/media-kind.cjs.map +1 -0
  30. package/dist/internal/media-kind.d.cts +20 -0
  31. package/dist/internal/media-kind.d.cts.map +1 -0
  32. package/dist/internal/media-kind.d.ts +20 -0
  33. package/dist/internal/media-kind.d.ts.map +1 -0
  34. package/dist/internal/media-kind.js +45 -0
  35. package/dist/internal/media-kind.js.map +1 -0
  36. package/dist/internal/url-warn.cjs +14 -0
  37. package/dist/internal/url-warn.cjs.map +1 -0
  38. package/dist/internal/url-warn.d.cts +10 -0
  39. package/dist/internal/url-warn.d.cts.map +1 -0
  40. package/dist/internal/url-warn.d.ts +10 -0
  41. package/dist/internal/url-warn.d.ts.map +1 -0
  42. package/dist/internal/url-warn.js +13 -0
  43. package/dist/internal/url-warn.js.map +1 -0
  44. package/dist/internal/validation.cjs +222 -0
  45. package/dist/internal/validation.cjs.map +1 -0
  46. package/dist/internal/validation.d.cts +78 -0
  47. package/dist/internal/validation.d.cts.map +1 -0
  48. package/dist/internal/validation.d.ts +78 -0
  49. package/dist/internal/validation.d.ts.map +1 -0
  50. package/dist/internal/validation.js +196 -0
  51. package/dist/internal/validation.js.map +1 -0
  52. package/dist/main/asset-download.cjs +265 -0
  53. package/dist/main/asset-download.cjs.map +1 -0
  54. package/dist/main/asset-download.d.cts +12 -0
  55. package/dist/main/asset-download.d.cts.map +1 -0
  56. package/dist/main/asset-download.d.ts +12 -0
  57. package/dist/main/asset-download.d.ts.map +1 -0
  58. package/dist/main/asset-download.js +263 -0
  59. package/dist/main/asset-download.js.map +1 -0
  60. package/dist/main/database.cjs +473 -0
  61. package/dist/main/database.cjs.map +1 -0
  62. package/dist/main/database.d.cts +81 -0
  63. package/dist/main/database.d.cts.map +1 -0
  64. package/dist/main/database.d.ts +81 -0
  65. package/dist/main/database.d.ts.map +1 -0
  66. package/dist/main/database.js +472 -0
  67. package/dist/main/database.js.map +1 -0
  68. package/dist/main/index.cjs +22 -0
  69. package/dist/main/index.d.cts +7 -0
  70. package/dist/main/index.d.ts +7 -0
  71. package/dist/main/index.js +7 -0
  72. package/dist/main/media-cache.cjs +862 -0
  73. package/dist/main/media-cache.cjs.map +1 -0
  74. package/dist/main/media-cache.d.cts +134 -0
  75. package/dist/main/media-cache.d.cts.map +1 -0
  76. package/dist/main/media-cache.d.ts +134 -0
  77. package/dist/main/media-cache.d.ts.map +1 -0
  78. package/dist/main/media-cache.js +854 -0
  79. package/dist/main/media-cache.js.map +1 -0
  80. package/dist/main/storage-root-lock.cjs +124 -0
  81. package/dist/main/storage-root-lock.cjs.map +1 -0
  82. package/dist/main/storage-root-lock.d.cts +11 -0
  83. package/dist/main/storage-root-lock.d.cts.map +1 -0
  84. package/dist/main/storage-root-lock.d.ts +11 -0
  85. package/dist/main/storage-root-lock.d.ts.map +1 -0
  86. package/dist/main/storage-root-lock.js +120 -0
  87. package/dist/main/storage-root-lock.js.map +1 -0
  88. package/dist/main/store.cjs +197 -0
  89. package/dist/main/store.cjs.map +1 -0
  90. package/dist/main/store.d.cts +83 -0
  91. package/dist/main/store.d.cts.map +1 -0
  92. package/dist/main/store.d.ts +83 -0
  93. package/dist/main/store.d.ts.map +1 -0
  94. package/dist/main/store.js +195 -0
  95. package/dist/main/store.js.map +1 -0
  96. package/dist/preload/index.cjs +36 -0
  97. package/dist/preload/index.cjs.map +1 -0
  98. package/dist/preload/index.d.cts +14 -0
  99. package/dist/preload/index.d.cts.map +1 -0
  100. package/dist/preload/index.d.ts +14 -0
  101. package/dist/preload/index.d.ts.map +1 -0
  102. package/dist/preload/index.js +34 -0
  103. package/dist/preload/index.js.map +1 -0
  104. package/dist/react/index.cjs +199 -0
  105. package/dist/react/index.cjs.map +1 -0
  106. package/dist/react/index.d.cts +50 -0
  107. package/dist/react/index.d.cts.map +1 -0
  108. package/dist/react/index.d.ts +50 -0
  109. package/dist/react/index.d.ts.map +1 -0
  110. package/dist/react/index.js +191 -0
  111. package/dist/react/index.js.map +1 -0
  112. package/dist/renderer/helpers.cjs +36 -0
  113. package/dist/renderer/helpers.cjs.map +1 -0
  114. package/dist/renderer/helpers.d.cts +11 -0
  115. package/dist/renderer/helpers.d.cts.map +1 -0
  116. package/dist/renderer/helpers.d.ts +11 -0
  117. package/dist/renderer/helpers.d.ts.map +1 -0
  118. package/dist/renderer/helpers.js +35 -0
  119. package/dist/renderer/helpers.js.map +1 -0
  120. package/dist/renderer/index.cjs +20 -0
  121. package/dist/renderer/index.cjs.map +1 -0
  122. package/dist/renderer/index.d.cts +14 -0
  123. package/dist/renderer/index.d.cts.map +1 -0
  124. package/dist/renderer/index.d.ts +14 -0
  125. package/dist/renderer/index.d.ts.map +1 -0
  126. package/dist/renderer/index.js +14 -0
  127. package/dist/renderer/index.js.map +1 -0
  128. package/dist/renderer/runtime.cjs +278 -0
  129. package/dist/renderer/runtime.cjs.map +1 -0
  130. package/dist/renderer/runtime.d.cts +35 -0
  131. package/dist/renderer/runtime.d.cts.map +1 -0
  132. package/dist/renderer/runtime.d.ts +35 -0
  133. package/dist/renderer/runtime.d.ts.map +1 -0
  134. package/dist/renderer/runtime.js +273 -0
  135. package/dist/renderer/runtime.js.map +1 -0
  136. package/dist/renderer/window-globals.d.cts +9 -0
  137. package/dist/renderer/window-globals.d.cts.map +1 -0
  138. package/dist/renderer/window-globals.d.ts +9 -0
  139. package/dist/renderer/window-globals.d.ts.map +1 -0
  140. package/dist/shared/errors.cjs +102 -0
  141. package/dist/shared/errors.cjs.map +1 -0
  142. package/dist/shared/errors.d.cts +45 -0
  143. package/dist/shared/errors.d.cts.map +1 -0
  144. package/dist/shared/errors.d.ts +45 -0
  145. package/dist/shared/errors.d.ts.map +1 -0
  146. package/dist/shared/errors.js +93 -0
  147. package/dist/shared/errors.js.map +1 -0
  148. package/dist/shared/ipc.cjs +14 -0
  149. package/dist/shared/ipc.cjs.map +1 -0
  150. package/dist/shared/ipc.d.cts +12 -0
  151. package/dist/shared/ipc.d.cts.map +1 -0
  152. package/dist/shared/ipc.d.ts +12 -0
  153. package/dist/shared/ipc.d.ts.map +1 -0
  154. package/dist/shared/ipc.js +13 -0
  155. package/dist/shared/ipc.js.map +1 -0
  156. package/dist/shared/normalize.cjs +19 -0
  157. package/dist/shared/normalize.cjs.map +1 -0
  158. package/dist/shared/normalize.d.cts +11 -0
  159. package/dist/shared/normalize.d.cts.map +1 -0
  160. package/dist/shared/normalize.d.ts +11 -0
  161. package/dist/shared/normalize.d.ts.map +1 -0
  162. package/dist/shared/normalize.js +18 -0
  163. package/dist/shared/normalize.js.map +1 -0
  164. package/dist/shared/pagination.cjs +32 -0
  165. package/dist/shared/pagination.cjs.map +1 -0
  166. package/dist/shared/pagination.d.cts +14 -0
  167. package/dist/shared/pagination.d.cts.map +1 -0
  168. package/dist/shared/pagination.d.ts +14 -0
  169. package/dist/shared/pagination.d.ts.map +1 -0
  170. package/dist/shared/pagination.js +28 -0
  171. package/dist/shared/pagination.js.map +1 -0
  172. package/dist/shared/stem.cjs +16 -0
  173. package/dist/shared/stem.cjs.map +1 -0
  174. package/dist/shared/stem.d.cts +6 -0
  175. package/dist/shared/stem.d.cts.map +1 -0
  176. package/dist/shared/stem.d.ts +6 -0
  177. package/dist/shared/stem.d.ts.map +1 -0
  178. package/dist/shared/stem.js +14 -0
  179. package/dist/shared/stem.js.map +1 -0
  180. package/dist/shared/types.cjs +15 -0
  181. package/dist/shared/types.cjs.map +1 -0
  182. package/dist/shared/types.d.cts +234 -0
  183. package/dist/shared/types.d.cts.map +1 -0
  184. package/dist/shared/types.d.ts +234 -0
  185. package/dist/shared/types.d.ts.map +1 -0
  186. package/dist/shared/types.js +14 -0
  187. package/dist/shared/types.js.map +1 -0
  188. package/package.json +120 -0
  189. package/skills/authenticated-downloads/SKILL.md +203 -0
  190. package/skills/cache-configuration/SKILL.md +357 -0
  191. package/skills/cache-configuration/references/options.md +356 -0
  192. package/skills/getting-started/SKILL.md +407 -0
  193. package/skills/production-checklist/SKILL.md +397 -0
  194. package/skills/react-rendering/SKILL.md +424 -0
  195. package/skills/react-rendering/references/hooks.md +443 -0
  196. 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