@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 @@
1
+ {"version":3,"file":"media-cache.js","names":["sleep"],"sources":["../../src/main/media-cache.ts"],"sourcesContent":["import { EventEmitter } from \"node:events\";\nimport { createReadStream, existsSync, mkdirSync, readdirSync, rmSync, statSync } from \"node:fs\";\nimport { statfs } from \"node:fs/promises\";\nimport { Readable } from \"node:stream\";\nimport { setTimeout as sleep } from \"node:timers/promises\";\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\nimport type { IpcMain, Session } from \"electron\";\nimport { MEDIA_CACHE_IPC } from \"../shared/ipc.js\";\nimport { validateFlatManifest } from \"../shared/normalize.js\";\nimport { normalizeStem } from \"../shared/stem.js\";\nimport {\n DataValidationError,\n StoreExpiredError,\n StoreValidationError,\n StorageLimitError,\n toSerializedError,\n} from \"../shared/errors.js\";\nimport type {\n AssetKeyInput,\n FlatManifest,\n FileStemMatch,\n JsonValue,\n MediaCacheAppPath,\n MediaCacheLogEvent,\n MediaCacheLogFormat,\n MediaCacheLogHandler,\n MediaCacheLogLevel,\n MediaCacheOptions,\n MediaCacheStatus,\n PaginationInput,\n PaginationResult,\n ResolvedMediaAsset,\n SyncProgress,\n SyncRunStats,\n} from \"../shared/types.js\";\nimport { formatMediaCacheConsoleLine } from \"../internal/log-format.js\";\nimport {\n mediaCacheStoragePathSchema,\n optionalPaginationInputSchema,\n parseWithSchema,\n stringInputSchema,\n} from \"../internal/validation.js\";\nimport { MediaCacheDatabase } from \"./database.js\";\nimport { consoleWarnResolveAssetBaseUrlFallback } from \"../internal/url-warn.js\";\nimport { hashKey } from \"../internal/asset-key.js\";\nimport type { StorageRootLockHandle } from \"./storage-root-lock.js\";\nimport {\n acquireStorageRootLock,\n disableStorageRootLockForTests,\n enableStorageRootLockForTests,\n resetStorageRootLocksForTests,\n} from \"./storage-root-lock.js\";\nimport {\n AssetDownloader,\n effectiveReserveFreeBytes,\n type AssetDownloadTarget,\n type QueuedAssetDownloadTarget,\n} from \"./asset-download.js\";\n\nexport { DEFAULT_RESERVE_FREE_BYTES, effectiveReserveFreeBytes } from \"./asset-download.js\";\n\nconst requireElectron = createRequire(import.meta.url);\n\nconst DEFAULT_STALE_DELETE_MS = 7 * 24 * 60 * 60 * 1000;\nconst DEFAULT_SYNC_HISTORY_LIMIT = 50;\ntype StatFsResult = Awaited<ReturnType<typeof statfs>>;\nconst LOG_LEVEL_WEIGHT: Record<MediaCacheLogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n};\n\nlet mediaCacheProtocolSchemesPrivileged = false;\n\nfunction isModuleNotFoundError(error: unknown): boolean {\n return (\n error instanceof Error &&\n \"code\" in error &&\n (error as NodeJS.ErrnoException).code === \"MODULE_NOT_FOUND\"\n );\n}\n\n/** True when not a production build; Electron `forge start` often leaves `NODE_ENV` unset. */\nfunction isNonProductionNodeEnv(): boolean {\n return process.env.NODE_ENV !== \"production\";\n}\n\n/**\n * Registers the privileged `media:` scheme once per process. Call happens when constructing\n * {@link MediaCache} in offline mode so consumers do not need a separate bootstrap step.\n *\n * No-ops when `electron` cannot be loaded (e.g. unit tests outside Electron). When Electron is\n * available, failures from `protocol.registerSchemesAsPrivileged` propagate (including calling\n * too late after `app.ready`).\n */\nfunction ensureMediaCacheProtocolSchemesPrivileged(): void {\n if (mediaCacheProtocolSchemesPrivileged) {\n return;\n }\n\n let protocol: import(\"electron\").Protocol;\n try {\n ({ protocol } = requireElectron(\"electron\") as typeof import(\"electron\"));\n } catch (error) {\n if (isModuleNotFoundError(error)) {\n return;\n }\n throw error;\n }\n\n if (protocol == null || typeof protocol.registerSchemesAsPrivileged !== \"function\") {\n return;\n }\n\n protocol.registerSchemesAsPrivileged([\n {\n scheme: \"media\",\n privileges: {\n standard: true,\n secure: true,\n supportFetchAPI: true,\n stream: true,\n },\n },\n ]);\n mediaCacheProtocolSchemesPrivileged = true;\n}\n\n/**\n * Clears the internal `media:` scheme registration flag so subsequent {@link MediaCache}\n * construction runs registration again. **Unit tests only**; do not use in application code.\n * @internal\n */\nexport function resetMediaCacheProtocolRegistrationStateForTests(): void {\n mediaCacheProtocolSchemesPrivileged = false;\n}\n\n/**\n * Clears any held storage-root locks so tests can run multiple scenarios in one process.\n * **Unit tests only**; do not use in application code.\n * @internal\n */\nexport const resetMediaCacheStorageRootLocksForTests = resetStorageRootLocksForTests;\n\n/**\n * Disables storage-root exclusivity checks so tests can exercise multiple cache instances freely.\n * **Unit tests only**; do not use in application code.\n * @internal\n */\nexport const disableMediaCacheStorageRootLockForTests = disableStorageRootLockForTests;\n\n/**\n * Re-enables storage-root exclusivity checks after test-only overrides.\n * **Unit tests only**; do not use in application code.\n * @internal\n */\nexport const enableMediaCacheStorageRootLockForTests = enableStorageRootLockForTests;\n\ninterface RuntimeDependencies {\n fetchImpl: typeof globalThis.fetch;\n now: () => number;\n sleep: (delayMs: number) => Promise<void>;\n resolveAppPath: (name: MediaCacheAppPath) => Promise<string>;\n statfs: (path: string) => Promise<StatFsResult>;\n}\n\n/** Options for {@link MediaCacheMain.registerProtocol}. */\ninterface RegisterProtocolOptions {\n /** Electron session to register the `media:` handler on (defaults to `defaultSession`). */\n session?: Session;\n /** Custom file-serving handler; receives the protocol request and resolved local path. */\n fetchFile?: (request: Request, filePath: string) => Promise<Response>;\n}\n\n/** Options for {@link MediaCacheMain.attachIpc}. */\ninterface AttachIpcOptions {\n /** Custom `ipcMain` instance (defaults to `electron.ipcMain`). */\n ipcMain?: IpcMain;\n}\n\n/**\n * Main-process controller: syncs the store, stores blobs, serves `media:` URLs, and can expose\n * the same operations to renderers via IPC. In offline mode (default), construct the cache before\n * `app.whenReady()` so the privileged `media:` scheme can be registered, then after ready call\n * {@link MediaCacheMain.start} for the one-call happy path.\n *\n * `MediaCache` requires exclusive ownership of its resolved `storageRoot`. The first process that\n * acquires that root remains the owner for the process lifetime. A second process (or cache\n * instance) targeting the same root throws {@link import(\"../shared/errors.js\").StorageOwnershipError}.\n */\nexport interface MediaCacheMain {\n /**\n * One-call setup: register protocol, attach IPC, initialize storage, then run initial sync.\n * Cache-root ownership is enforced during initialization, before SQLite or blob writes begin.\n */\n start(): Promise<void>;\n /** Runs or joins the current sync; concurrent callers share one run. */\n syncNow(): Promise<void>;\n /** Latest status snapshot (phase, progress, last run, error). */\n getStatus(): Promise<MediaCacheStatus>;\n /** Single asset by key, or null if missing. */\n getAsset(key: string): Promise<ResolvedMediaAsset | null>;\n /** Assets matching a secondary index value, paginated. */\n listByIndex(\n indexName: string,\n value: string,\n pagination?: PaginationInput,\n ): Promise<PaginationResult<ResolvedMediaAsset>>;\n /** Search by normalized file name stem. */\n findByFileStem(\n stem: string,\n pagination?: PaginationInput,\n ): Promise<PaginationResult<FileStemMatch>>;\n /** Register the `media:` protocol handler for the given session (default: `defaultSession`). */\n registerProtocol(options?: RegisterProtocolOptions): Promise<void>;\n /** Wire `ipcMain` handlers and broadcast status to all browser windows. */\n attachIpc(options?: AttachIpcOptions): Promise<void>;\n}\n\n/** Constructs a {@link MediaCacheMain} instance with the given options (does not start sync until `start` or `syncNow`). */\nexport function createMediaCache(options: MediaCacheOptions): MediaCacheMain {\n return new MediaCache(options);\n}\n\nexport class MediaCache implements MediaCacheMain {\n private readonly events = new EventEmitter();\n private readonly deps: RuntimeDependencies;\n /** When no custom handler is configured, log to the main-process console in development. */\n private readonly defaultDevelopmentConsole: boolean;\n /** Custom structured log sink when configured via `options.logging.onLog`. */\n private readonly logHandler: MediaCacheLogHandler | null;\n /** Built-in console line shape when {@link defaultDevelopmentConsole} is active. */\n private readonly logFormat: MediaCacheLogFormat;\n /** Minimum severity emitted for the currently active log sink. */\n private readonly effectiveLogLevel: MediaCacheLogLevel;\n private readonly devPassthrough: boolean;\n private readonly assetBaseUrlOrigin: string | null;\n private storageRootLock: StorageRootLockHandle | null = null;\n private db: MediaCacheDatabase | null = null;\n private storageRoot: string | null = null;\n private status: MediaCacheStatus;\n private syncPromise: Promise<void> | null = null;\n private ipcAttached = false;\n private protocolRegistered = false;\n\n constructor(\n private readonly options: MediaCacheOptions,\n deps?: Partial<RuntimeDependencies>,\n ) {\n this.deps = {\n fetchImpl: deps?.fetchImpl ?? globalThis.fetch.bind(globalThis),\n now: deps?.now ?? Date.now,\n sleep: deps?.sleep ?? sleep,\n resolveAppPath: deps?.resolveAppPath ?? resolveElectronAppPath,\n statfs: deps?.statfs ?? statfs,\n };\n const logging = normalizeLoggingOptions(options.logging);\n this.logHandler = logging.onLog;\n this.defaultDevelopmentConsole =\n this.logHandler == null && isNonProductionNodeEnv() && process.env.VITEST !== \"true\";\n this.logFormat = logging.format;\n this.effectiveLogLevel =\n logging.level ??\n (this.logHandler == null && this.defaultDevelopmentConsole ? \"debug\" : \"info\");\n this.devPassthrough = options.devPassthrough ?? process.env.NODE_ENV === \"development\";\n if (this.devPassthrough) {\n this.assetBaseUrlOrigin = normalizeAssetBaseUrl(options.assetBaseUrl);\n } else {\n if (options.assetBaseUrl) {\n throw new Error(\n \"assetBaseUrl has no effect when devPassthrough is false. \" +\n \"Set devPassthrough: true or remove assetBaseUrl.\",\n );\n }\n this.assetBaseUrlOrigin = null;\n }\n if (this.devPassthrough) {\n this.emitLog(\"info\", \"dev_passthrough_active\", {\n source: options.devPassthrough === true ? \"option\" : \"node_env\",\n node_env: process.env.NODE_ENV ?? null,\n });\n }\n if (\n this.devPassthrough &&\n this.options.onSyncFailure &&\n this.options.onSyncFailure !== \"throw\"\n ) {\n this.emitLog(\"warn\", \"dev_passthrough_ignores_sync_failure_mode\", {\n configured_mode: this.options.onSyncFailure,\n });\n }\n this.status = {\n phase: \"idle\",\n storageRoot: null,\n activeGenerationId: null,\n progress: null,\n lastRun: null,\n error: null,\n updatedAt: this.deps.now(),\n };\n\n if (!this.devPassthrough) {\n ensureMediaCacheProtocolSchemesPrivileged();\n }\n }\n\n async start(): Promise<void> {\n await this.registerProtocol();\n await this.attachIpc();\n await this.syncNow();\n }\n\n async syncNow(): Promise<void> {\n await this.ensureInitialized();\n if (this.syncPromise) {\n this.emitLog(\"debug\", \"sync_reused\", {\n phase: this.status.phase,\n active_generation_id: this.status.activeGenerationId,\n });\n return this.syncPromise;\n }\n\n this.syncPromise = this.runSync().finally(() => {\n this.syncPromise = null;\n });\n return this.syncPromise;\n }\n\n async getStatus(): Promise<MediaCacheStatus> {\n await this.ensureInitialized();\n return this.status;\n }\n\n async getAsset(key: AssetKeyInput): Promise<ResolvedMediaAsset | null> {\n const hashedKey = hashKey(key);\n await this.ensureInitialized();\n return this.db!.getAsset(hashedKey);\n }\n\n async listByIndex(\n indexName: string,\n value: string,\n pagination?: PaginationInput,\n ): Promise<PaginationResult<ResolvedMediaAsset>> {\n const validatedIndexName = parseWithSchema(stringInputSchema, indexName, \"index name\");\n const validatedValue = parseWithSchema(stringInputSchema, value, \"index value\");\n const validatedPagination = parseWithSchema(\n optionalPaginationInputSchema,\n pagination,\n \"index pagination input\",\n );\n await this.ensureInitialized();\n return this.db!.listByIndex(validatedIndexName, validatedValue, validatedPagination);\n }\n\n async findByFileStem(\n stem: string,\n pagination?: PaginationInput,\n ): Promise<PaginationResult<FileStemMatch>> {\n const validatedStem = parseWithSchema(stringInputSchema, stem, \"file stem\");\n const validatedPagination = parseWithSchema(\n optionalPaginationInputSchema,\n pagination,\n \"file stem pagination input\",\n );\n await this.ensureInitialized();\n return this.db!.findByFileStem(normalizeStem(validatedStem), validatedPagination);\n }\n\n async registerProtocol(options?: RegisterProtocolOptions): Promise<void> {\n await this.ensureInitialized();\n if (this.devPassthrough) {\n this.protocolRegistered = true;\n this.emitLog(\"debug\", \"protocol_registration_skipped\", { reason: \"dev_passthrough\" });\n return;\n }\n if (this.protocolRegistered) {\n this.emitLog(\"debug\", \"protocol_registration_skipped\", { reason: \"already_registered\" });\n return;\n }\n\n const electron = options?.session ? null : await import(\"electron\");\n const session = options?.session ?? electron?.session?.defaultSession;\n if (!session || typeof session.protocol?.handle !== \"function\") {\n this.emitLog(\"debug\", \"protocol_registration_skipped\", {\n reason: \"session_unavailable\",\n });\n return;\n }\n\n const fetchFile =\n options?.fetchFile ??\n (async (request: Request, filePath: string) => createFileResponse(filePath, request));\n\n session.protocol.handle(\"media\", async (request) => {\n const parsed = new URL(request.url);\n const parts = parsed.pathname.split(\"/\").filter(Boolean);\n\n if (parsed.hostname !== \"asset\" || parts.length !== 1) {\n return new Response(\"Not found\", { status: 404 });\n }\n\n let assetKey: string;\n try {\n assetKey = decodeURIComponent(parts[0]);\n } catch {\n return new Response(\"Not found\", { status: 404 });\n }\n\n const target = this.db!.getProtocolAssetTarget(assetKey);\n\n if (!target) {\n this.emitLog(\"debug\", \"protocol_request_not_found\", {\n asset_key: assetKey,\n method: request.method,\n });\n return new Response(\"Not found\", { status: 404 });\n }\n\n if (!target.absolutePath || !existsSync(target.absolutePath)) {\n this.emitLog(\"debug\", \"protocol_request_file_missing\", {\n asset_key: assetKey,\n method: request.method,\n });\n return new Response(\"Not found\", { status: 404 });\n }\n\n this.emitLog(\"debug\", \"protocol_request_local_resolved\", {\n asset_key: assetKey,\n method: request.method,\n range: request.headers.get(\"range\"),\n });\n return fetchFile(request, target.absolutePath);\n });\n\n this.protocolRegistered = true;\n this.emitLog(\"info\", \"protocol_registered\", {});\n }\n\n async attachIpc(options?: AttachIpcOptions): Promise<void> {\n await this.ensureInitialized();\n if (this.ipcAttached) {\n this.emitLog(\"debug\", \"ipc_attach_skipped\", { reason: \"already_attached\" });\n return;\n }\n\n const electron = options?.ipcMain ? null : await import(\"electron\");\n const ipcMain = options?.ipcMain ?? electron?.ipcMain;\n if (!ipcMain || typeof ipcMain.handle !== \"function\") {\n this.emitLog(\"debug\", \"ipc_attach_skipped\", {\n reason: \"ipc_main_unavailable\",\n });\n return;\n }\n\n ipcMain.handle(MEDIA_CACHE_IPC.getStatus, async () => this.getStatus());\n ipcMain.handle(MEDIA_CACHE_IPC.syncNow, async () => this.syncNow());\n ipcMain.handle(MEDIA_CACHE_IPC.getAsset, async (_event, key: AssetKeyInput) =>\n this.getAsset(key),\n );\n ipcMain.handle(\n MEDIA_CACHE_IPC.listByIndex,\n async (_event, indexName: string, value: string, pagination?: PaginationInput) =>\n this.listByIndex(indexName, value, pagination),\n );\n ipcMain.handle(\n MEDIA_CACHE_IPC.findByFileStem,\n async (_event, stem: string, pagination?: PaginationInput) =>\n this.findByFileStem(stem, pagination),\n );\n\n if (electron) {\n this.events.on(MEDIA_CACHE_IPC.statusChanged, (status: MediaCacheStatus) => {\n for (const window of electron.BrowserWindow.getAllWindows()) {\n window.webContents.send(MEDIA_CACHE_IPC.statusChanged, status);\n }\n });\n }\n\n this.ipcAttached = true;\n this.emitLog(\"info\", \"ipc_attached\", {});\n }\n\n private async ensureInitialized(): Promise<void> {\n if (this.db) {\n return;\n }\n\n this.storageRoot = await resolveStorageRoot(this.options.storagePath, this.deps.resolveAppPath);\n mkdirSync(this.storageRoot, { recursive: true });\n this.storageRootLock ??= acquireStorageRootLock(this.storageRoot, this);\n mkdirSync(join(this.storageRoot, \"temp\"), { recursive: true });\n mkdirSync(join(this.storageRoot, \"blobs\"), { recursive: true });\n\n if (!this.devPassthrough) {\n this.emitLog(\"info\", \"cache_storage_location\", {\n storage_root: this.storageRoot,\n });\n }\n\n this.db = new MediaCacheDatabase(this.storageRoot, {\n devPassthrough: this.devPassthrough,\n assetBaseUrlOrigin: this.assetBaseUrlOrigin,\n onWarn: (contextLabel, err) => {\n if (this.logHandler != null || this.defaultDevelopmentConsole) {\n this.emitLog(\"warn\", \"resolve_asset_base_url_fallback\", {\n context_label: contextLabel,\n error: err != null ? String(err) : undefined,\n });\n } else {\n consoleWarnResolveAssetBaseUrlFallback(contextLabel, err);\n }\n },\n });\n if (this.devPassthrough) {\n this.prepareDevRuntimeState();\n }\n let storedStatus: MediaCacheStatus | null = null;\n let activeGenerationId: number | null = null;\n if (!this.devPassthrough) {\n activeGenerationId = this.reconcileOrphanedStagedGenerations();\n try {\n storedStatus = this.db.loadStatus();\n } catch (error) {\n if (!(error instanceof DataValidationError)) {\n throw error;\n }\n\n this.emitLog(\"warn\", \"status_snapshot_invalid\", {\n error_code: error.code,\n error_message: error.message,\n });\n }\n if (storedStatus) {\n this.status = storedStatus;\n } else if (activeGenerationId !== null) {\n this.status = {\n ...this.status,\n phase: \"ready\",\n activeGenerationId,\n progress: null,\n error: null,\n };\n }\n this.status.activeGenerationId = activeGenerationId;\n }\n this.status = {\n ...this.status,\n storageRoot: this.storageRoot,\n };\n this.emitLog(\"info\", \"cache_initialized\", {\n storage_root: this.storageRoot,\n active_generation_id: this.status.activeGenerationId,\n dev_passthrough_enabled: this.devPassthrough,\n });\n }\n\n private reconcileOrphanedStagedGenerations(): number | null {\n const activeGenerationId = this.db!.getActiveGenerationId();\n const stagedGenerationIds = this.db!.listStagedGenerationIds().filter(\n (generationId) => generationId !== activeGenerationId,\n );\n if (stagedGenerationIds.length === 0) {\n return activeGenerationId;\n }\n\n for (const stagedGenerationId of stagedGenerationIds) {\n this.cleanupStagedGenerationFiles(stagedGenerationId, activeGenerationId);\n this.db!.deleteGeneration(stagedGenerationId);\n }\n\n this.emitLog(\"warn\", \"orphaned_staged_generations_removed\", {\n active_generation_id: activeGenerationId,\n removed_generation_ids: stagedGenerationIds,\n removed_generation_count: stagedGenerationIds.length,\n });\n return activeGenerationId;\n }\n\n private prepareDevRuntimeState(): void {\n // Wipe happens before resolveStore; if store resolution later throws, blobs are\n // already gone. Deferring the wipe until after staging would require a broader restructure.\n this.emitLog(\"warn\", \"dev_passthrough_clearing_state\", {\n storage_root: this.storageRoot,\n reason: \"devPassthrough=true clears all local state on startup\",\n });\n this.db!.clearAllState();\n rmSync(join(this.storageRoot!, \"blobs\"), { recursive: true, force: true });\n rmSync(join(this.storageRoot!, \"temp\"), { recursive: true, force: true });\n mkdirSync(join(this.storageRoot!, \"blobs\"), { recursive: true });\n mkdirSync(join(this.storageRoot!, \"temp\"), { recursive: true });\n this.status = {\n ...this.status,\n phase: \"idle\",\n activeGenerationId: null,\n progress: null,\n lastRun: null,\n error: null,\n updatedAt: this.deps.now(),\n };\n }\n\n private async runSync(): Promise<void> {\n const now = this.deps.now();\n const runId = this.db!.createSyncRun(now);\n const stats: SyncRunStats = {\n totalAssets: 0,\n downloadedAssets: 0,\n skippedAssets: 0,\n bytesDownloaded: 0,\n };\n\n this.updateStatus({\n phase: \"syncing\",\n error: null,\n progress: {\n runId,\n phase: \"resolving-store\",\n totalAssets: 0,\n completedAssets: 0,\n downloadedAssets: 0,\n skippedAssets: 0,\n bytesDownloaded: 0,\n },\n });\n this.emitLog(\"info\", \"sync_started\", {\n run_id: runId,\n active_generation_id: this.status.activeGenerationId,\n });\n\n let stagedGenerationId: number | null = null;\n\n try {\n const store = await this.options.resolveStore();\n const manifest = validateFlatManifest(store._serialize());\n this.assertStoreNotExpired(manifest, runId);\n stagedGenerationId = this.db!.createStagedGeneration(manifest, now);\n this.emitLog(\"info\", \"store_resolved\", {\n run_id: runId,\n staged_generation_id: stagedGenerationId,\n index_count: manifest.indexDefinitions.length,\n asset_count: manifest.assets.length,\n });\n\n const currentGenerationId = this.db!.getActiveGenerationId();\n const currentAssets = currentGenerationId\n ? this.db!.getGenerationAssets(currentGenerationId)\n : [];\n const stagedAssets = this.db!.getGenerationAssets(stagedGenerationId);\n stats.totalAssets = stagedAssets.length;\n\n this.updateProgress((progress) => ({\n ...progress,\n phase: \"diffing\",\n totalAssets: stagedAssets.length,\n }));\n\n const manifestAssetMap = new Map(manifest.assets.map((asset) => [asset.key, asset]));\n const currentMap = new Map(currentAssets.map((row) => [row.assetKey, row]));\n const downloads: QueuedAssetDownloadTarget[] = [];\n\n for (const row of stagedAssets) {\n const manifestAsset = manifestAssetMap.get(row.assetKey);\n if (!manifestAsset) {\n throw new StoreValidationError(`Asset \"${row.assetKey}\" not found in serialized store.`);\n }\n\n const activeRow = currentMap.get(row.assetKey);\n const activeRelativePath = activeRow?.relativePath ?? null;\n const normalizedActiveRelativePath =\n activeRelativePath === null ? null : normalizeStoredRelativePath(activeRelativePath);\n const nextVersion = manifestAsset.version;\n const canReuseActiveBlob =\n normalizedActiveRelativePath !== null &&\n existsSync(join(this.storageRoot!, normalizedActiveRelativePath));\n\n if (this.devPassthrough) {\n stats.skippedAssets += 1;\n continue;\n }\n\n if (canReuseActiveBlob) {\n const currentVersion = getResolvedVersionFromPath(normalizedActiveRelativePath);\n if (currentVersion === nextVersion) {\n this.db!.setAssetDownloadState(\n stagedGenerationId,\n row.assetKey,\n normalizedActiveRelativePath,\n activeRow?.mimeType ?? null,\n );\n stats.skippedAssets += 1;\n continue;\n }\n }\n\n downloads.push({\n assetKey: row.assetKey,\n version: manifestAsset.version,\n fileName: manifestAsset.fileName,\n byteLength: manifestAsset.byteLength,\n });\n }\n\n this.emitLog(\"info\", \"sync_diffed\", {\n run_id: runId,\n total_assets: stagedAssets.length,\n download_count: downloads.length,\n skipped_assets: stats.skippedAssets,\n dev_passthrough_enabled: this.devPassthrough,\n });\n\n await this.pruneExpiredDeletions();\n this.cleanupObsoletePartialDownloads(downloads);\n if (!this.devPassthrough) {\n await this.enforceStorageLimits(downloads);\n\n this.updateProgress((progress) => ({\n ...progress,\n phase: \"downloading\",\n totalAssets: stagedAssets.length,\n completedAssets: stats.skippedAssets,\n skippedAssets: stats.skippedAssets,\n }));\n\n for (const queuedDownload of downloads) {\n this.assertStoreNotExpired(manifest, runId, queuedDownload);\n const download: AssetDownloadTarget = {\n ...queuedDownload,\n request: { url: manifestAssetMap.get(queuedDownload.assetKey)!.url },\n };\n this.emitLog(\"debug\", \"asset_download_started\", {\n run_id: runId,\n asset_key: download.assetKey,\n version: download.version,\n url: download.request.url,\n });\n const { relativePath, fallbackMimeType } =\n await this.createAssetDownloader().downloadAsset(download, (chunkBytes) => {\n stats.bytesDownloaded += chunkBytes;\n this.updateProgress((progress) => ({\n ...progress,\n bytesDownloaded: stats.bytesDownloaded,\n }));\n });\n this.db!.setAssetDownloadState(\n stagedGenerationId,\n download.assetKey,\n relativePath,\n fallbackMimeType,\n );\n stats.downloadedAssets += 1;\n this.emitLog(\"debug\", \"asset_download_completed\", {\n run_id: runId,\n asset_key: download.assetKey,\n relative_path: relativePath,\n });\n this.updateProgress((progress) => ({\n ...progress,\n completedAssets: stats.downloadedAssets + stats.skippedAssets,\n downloadedAssets: stats.downloadedAssets,\n skippedAssets: stats.skippedAssets,\n bytesDownloaded: stats.bytesDownloaded,\n }));\n }\n } else {\n this.updateProgress((progress) => ({\n ...progress,\n completedAssets: stagedAssets.length,\n skippedAssets: stagedAssets.length,\n }));\n }\n\n this.updateProgress((progress) => ({\n ...progress,\n phase: \"committing\",\n }));\n\n const previousGenerationId = this.db!.activateGeneration(stagedGenerationId, this.deps.now());\n this.db!.clearPendingDeletionsForGeneration(stagedGenerationId);\n if (previousGenerationId) {\n this.markRemovedAssetsForDeletion(previousGenerationId, stagedGenerationId);\n }\n this.emitLog(\"info\", \"generation_committed\", {\n run_id: runId,\n previous_generation_id: previousGenerationId,\n active_generation_id: stagedGenerationId,\n });\n\n this.updateProgress((progress) => ({\n ...progress,\n phase: \"pruning\",\n }));\n await this.pruneExpiredDeletions();\n\n const summary = this.db!.completeSyncRun(runId, \"success\", this.deps.now(), stats);\n this.db!.pruneSyncHistory(this.options.syncHistoryLimit ?? DEFAULT_SYNC_HISTORY_LIMIT);\n this.emitLog(\"info\", \"sync_completed\", {\n run_id: runId,\n active_generation_id: stagedGenerationId,\n total_assets: summary.stats.totalAssets,\n downloaded_assets: summary.stats.downloadedAssets,\n skipped_assets: summary.stats.skippedAssets,\n bytes_downloaded: summary.stats.bytesDownloaded,\n });\n this.updateStatus({\n phase: \"ready\",\n activeGenerationId: stagedGenerationId,\n progress: null,\n lastRun: summary,\n error: null,\n });\n } catch (error) {\n if (stagedGenerationId !== null) {\n this.cleanupStagedGenerationFiles(stagedGenerationId, this.db!.getActiveGenerationId());\n this.db!.deleteGeneration(stagedGenerationId);\n }\n\n const serialized = toSerializedError(error);\n const summary = this.db!.completeSyncRun(\n runId,\n \"error\",\n this.deps.now(),\n stats,\n serialized.code,\n serialized.message,\n );\n this.emitLog(\"error\", \"sync_failed\", {\n run_id: runId,\n active_generation_id: this.db!.getActiveGenerationId(),\n error_code: serialized.code,\n error_message: serialized.message,\n total_assets: summary.stats.totalAssets,\n downloaded_assets: summary.stats.downloadedAssets,\n skipped_assets: summary.stats.skippedAssets,\n bytes_downloaded: summary.stats.bytesDownloaded,\n });\n\n this.updateStatus({\n phase:\n this.devPassthrough || this.options.onSyncFailure === \"throw\"\n ? \"error\"\n : this.db!.getActiveGenerationId()\n ? \"ready\"\n : \"error\",\n activeGenerationId: this.db!.getActiveGenerationId(),\n progress: null,\n lastRun: summary,\n error: serialized,\n });\n\n if (this.devPassthrough || this.options.onSyncFailure === \"throw\") {\n throw error;\n }\n }\n }\n\n private assertStoreNotExpired(\n manifest: FlatManifest,\n runId: number,\n download?: Pick<QueuedAssetDownloadTarget, \"assetKey\">,\n ): void {\n if (!manifest.expiresAt) {\n return;\n }\n\n const expiresAtMs = Date.parse(manifest.expiresAt);\n const now = this.deps.now();\n if (Number.isNaN(expiresAtMs) || now < expiresAtMs) {\n return;\n }\n\n this.emitLog(\"warn\", \"store_expired\", {\n run_id: runId,\n expires_at: manifest.expiresAt,\n now_ms: now,\n asset_key: download?.assetKey,\n });\n\n const assetLabel = download ? ` before downloading ${download.assetKey}` : \"\";\n throw new StoreExpiredError(\n `Store URLs expired at ${manifest.expiresAt}${assetLabel}. Refresh the store and retry sync.`,\n );\n }\n\n private async enforceStorageLimits(downloads: QueuedAssetDownloadTarget[]): Promise<void> {\n const estimatedBlobBytes = downloads.reduce(\n (sum, download) => sum + (download.byteLength ?? 0),\n 0,\n );\n const estimatedRemainingDownloadBytes = downloads.reduce(\n (sum, download) => sum + this.createAssetDownloader().remainingDownloadBytes(download),\n 0,\n );\n\n if (this.options.maxCacheBytes !== undefined) {\n const currentBytes = this.currentBytesOnDisk(join(this.storageRoot!, \"blobs\"));\n if (currentBytes + estimatedBlobBytes > this.options.maxCacheBytes) {\n this.emitLog(\"warn\", \"storage_limit_exceeded\", {\n current_bytes: currentBytes,\n estimated_download_bytes: estimatedBlobBytes,\n max_cache_bytes: this.options.maxCacheBytes,\n });\n throw new StorageLimitError(\n `Estimated cache size ${currentBytes + estimatedBlobBytes} exceeds maxCacheBytes ${this.options.maxCacheBytes}.`,\n );\n }\n }\n\n const stats = await this.deps.statfs(this.storageRoot!);\n const availableBytes = Number(stats.bavail) * Number(stats.bsize);\n const reserve = effectiveReserveFreeBytes(this.options.reserveFreeBytes);\n if (availableBytes - estimatedRemainingDownloadBytes < reserve) {\n this.emitLog(\"warn\", \"storage_reserve_violation\", {\n available_bytes: availableBytes,\n estimated_download_bytes: estimatedRemainingDownloadBytes,\n reserve_free_bytes: reserve,\n });\n throw new StorageLimitError(\n `Estimated download size ${estimatedRemainingDownloadBytes} leaves less than reserveFreeBytes ${reserve}.`,\n );\n }\n }\n\n private async ensureFileSpaceCommit(): Promise<void> {\n await this.createAssetDownloader().ensureFileSpaceCommit();\n }\n\n private cleanupObsoletePartialDownloads(downloads: QueuedAssetDownloadTarget[]): void {\n this.createAssetDownloader().cleanupObsoletePartialDownloads(downloads);\n }\n\n private createAssetDownloader(): AssetDownloader {\n return new AssetDownloader(this.storageRoot!, this.deps, {\n reserveFreeBytes: this.options.reserveFreeBytes,\n emitLog: (level, event, fields = {}) => this.emitLog(level, event, fields),\n });\n }\n\n private cleanupStagedGenerationFiles(\n stagedGenerationId: number,\n activeGenerationId: number | null,\n ): void {\n const activePaths = new Set(\n activeGenerationId\n ? this.db!.getGenerationAssets(activeGenerationId).flatMap((row) =>\n row.relativePath ? [normalizeStoredRelativePath(row.relativePath)] : [],\n )\n : [],\n );\n\n for (const row of this.db!.getGenerationAssets(stagedGenerationId)) {\n if (!row.relativePath) {\n continue;\n }\n\n const normalizedRelativePath = normalizeStoredRelativePath(row.relativePath);\n if (activePaths.has(normalizedRelativePath)) {\n continue;\n }\n\n const absolutePath = join(this.storageRoot!, normalizedRelativePath);\n rmSync(absolutePath, { force: true });\n pruneEmptyParents(absolutePath, this.storageRoot!);\n }\n }\n\n private markRemovedAssetsForDeletion(\n previousGenerationId: number,\n stagedGenerationId: number,\n ): void {\n const previousAssets = this.db!.getGenerationAssets(previousGenerationId);\n const nextAssets = new Map(\n this.db!.getGenerationAssets(stagedGenerationId).map((row) => [\n row.assetKey,\n row.relativePath,\n ]),\n );\n const deleteAfterMs =\n this.deps.now() + (this.options.staleDeleteAfterMs ?? DEFAULT_STALE_DELETE_MS);\n\n let markedCount = 0;\n for (const row of previousAssets) {\n const nextRelativePath = nextAssets.get(row.assetKey);\n if (row.relativePath && nextRelativePath !== row.relativePath) {\n this.db!.markPendingDeletion(\n row.assetKey,\n row.relativePath,\n previousGenerationId,\n createPendingDeletionKey(row.assetKey, row.relativePath),\n deleteAfterMs,\n );\n markedCount += 1;\n }\n }\n this.emitLog(\"debug\", \"assets_marked_for_deletion\", {\n previous_generation_id: previousGenerationId,\n active_generation_id: stagedGenerationId,\n marked_count: markedCount,\n delete_after_ms: deleteAfterMs,\n });\n }\n\n private async pruneExpiredDeletions(): Promise<void> {\n const expired = this.db!.getExpiredPendingDeletions(this.deps.now());\n if (expired.length === 0) {\n this.emitLog(\"debug\", \"deletion_prune_skipped\", { expired_count: 0 });\n return;\n }\n\n for (const deletion of expired) {\n const absolutePath = join(\n this.storageRoot!,\n normalizeStoredRelativePath(deletion.relativePath),\n );\n rmSync(absolutePath, { force: true });\n pruneEmptyParents(absolutePath, this.storageRoot!);\n }\n\n this.db!.deletePendingDeletions(expired.map((item) => item.deletionKey));\n this.emitLog(\"debug\", \"assets_pruned\", { pruned_count: expired.length });\n }\n\n private currentBytesOnDisk(directory: string): number {\n if (!existsSync(directory)) {\n return 0;\n }\n\n const stats = statSync(directory);\n if (stats.isFile()) {\n return stats.size;\n }\n\n return readdirSync(directory).reduce(\n (sum, entry) => sum + this.currentBytesOnDisk(join(directory, entry)),\n 0,\n );\n }\n\n private updateProgress(transform: (progress: SyncProgress) => SyncProgress): void {\n if (!this.status.progress) {\n return;\n }\n this.updateStatus({\n progress: transform(this.status.progress),\n });\n }\n\n private updateStatus(partial: Partial<MediaCacheStatus>): void {\n this.status = {\n ...this.status,\n ...partial,\n updatedAt: this.deps.now(),\n };\n this.db?.saveStatus(this.status, this.status.updatedAt);\n this.events.emit(MEDIA_CACHE_IPC.statusChanged, this.status);\n }\n\n private emitLog(\n level: MediaCacheLogLevel,\n event: string,\n fields: Record<string, ReturnType<typeof normalizeLogValue>> = {},\n ): void {\n if (this.logHandler == null && !this.defaultDevelopmentConsole) {\n return;\n }\n\n const threshold = LOG_LEVEL_WEIGHT[this.effectiveLogLevel];\n if (LOG_LEVEL_WEIGHT[level] < threshold) {\n return;\n }\n\n const entry: MediaCacheLogEvent = {\n timestamp: new Date(this.deps.now()).toISOString(),\n level,\n event,\n service: \"rockhall-electron-offline-content\",\n component: \"media-cache\",\n ...fields,\n };\n\n if (this.logHandler != null) {\n try {\n this.logHandler(entry);\n } catch {\n // Consumer loggers must not break cache behavior.\n }\n return;\n }\n\n writeDefaultDevelopmentConsoleLog(level, entry, this.logFormat);\n }\n}\n\nfunction normalizeLoggingOptions(logging: MediaCacheOptions[\"logging\"]): {\n onLog: MediaCacheLogHandler | null;\n level: MediaCacheLogLevel | undefined;\n format: MediaCacheLogFormat;\n} {\n if (logging?.onLog != null && logging.format !== undefined) {\n throw new Error(\n \"MediaCacheOptions.logging.format cannot be set when logging.onLog is provided.\",\n );\n }\n\n const format = logging?.format;\n if (format !== undefined && format !== \"english\" && format !== \"json\") {\n throw new Error(\n `Invalid MediaCacheOptions.logging.format: expected \"english\" | \"json\", received ${JSON.stringify(format)}`,\n );\n }\n\n return {\n onLog: logging?.onLog ?? null,\n level: logging?.level,\n format: format ?? \"english\",\n };\n}\n\nfunction writeDefaultDevelopmentConsoleLog(\n level: MediaCacheLogLevel,\n entry: MediaCacheLogEvent,\n format: MediaCacheLogFormat,\n): void {\n const line = formatMediaCacheConsoleLine(entry, format);\n switch (level) {\n case \"debug\":\n console.debug(line);\n break;\n case \"info\":\n console.log(line);\n break;\n case \"warn\":\n console.warn(line);\n break;\n case \"error\":\n console.error(line);\n break;\n default:\n console.log(line);\n }\n}\n\nfunction createPendingDeletionKey(assetKey: string, relativePath: string): string {\n return JSON.stringify([assetKey, relativePath]);\n}\n\nfunction normalizeAssetBaseUrl(assetBaseUrl: string | null | undefined): string | null {\n if (!assetBaseUrl) {\n return null;\n }\n\n let parsed: URL;\n try {\n parsed = new URL(assetBaseUrl);\n } catch {\n throw new Error(`assetBaseUrl is not a valid URL: \"${assetBaseUrl}\"`);\n }\n if (parsed.username || parsed.password) {\n throw new Error(\"assetBaseUrl must not include credentials.\");\n }\n if (parsed.search || parsed.hash) {\n throw new Error(\"assetBaseUrl must not include a query string or hash fragment.\");\n }\n if (parsed.pathname !== \"/\" && parsed.pathname !== \"\") {\n throw new Error(\"assetBaseUrl must be an origin without a path.\");\n }\n\n return parsed.origin;\n}\n\nfunction pruneEmptyParents(pathToFile: string, storageRoot: string): void {\n let current = dirname(pathToFile);\n while (current.startsWith(storageRoot) && current !== storageRoot) {\n if (existsSync(current) && readdirSync(current).length === 0) {\n rmSync(current, { recursive: true, force: true });\n current = dirname(current);\n continue;\n }\n break;\n }\n}\n\nfunction getResolvedVersionFromPath(relativePath: string): string | null {\n const parts = relativePath.split(/[\\\\/]/);\n return parts.length >= 4 ? decodeURIComponent(parts.at(-2)!) : null;\n}\n\nfunction normalizeStoredRelativePath(relativePath: string): string {\n return relativePath.split(/[\\\\/]/).join(\"/\");\n}\n\nasync function resolveElectronAppPath(name: MediaCacheAppPath): Promise<string> {\n const electron = await import(\"electron\");\n return electron.app.getPath(name);\n}\n\nasync function resolveStorageRoot(\n input: MediaCacheOptions[\"storagePath\"],\n resolveAppPath: RuntimeDependencies[\"resolveAppPath\"],\n): Promise<string> {\n const storagePath = parseWithSchema(mediaCacheStoragePathSchema, input, \"storage path\");\n const root = await resolveAppPath(storagePath.appPath);\n return join(root, ...(storagePath.segments ?? []));\n}\n\nfunction createFileResponse(filePath: string, request: Request): Response {\n const stats = statSync(filePath);\n const size = stats.size;\n const rangeHeader = request.headers.get(\"range\");\n const mimeType = inferMimeType(filePath);\n const baseHeaders = new Headers({\n \"accept-ranges\": \"bytes\",\n \"content-type\": mimeType,\n });\n\n if (request.method === \"HEAD\") {\n baseHeaders.set(\"content-length\", String(size));\n return new Response(null, {\n status: 200,\n headers: baseHeaders,\n });\n }\n\n if (!rangeHeader) {\n baseHeaders.set(\"content-length\", String(size));\n return new Response(Readable.toWeb(createReadStream(filePath)) as BodyInit, {\n status: 200,\n headers: baseHeaders,\n });\n }\n\n const parsedRange = parseByteRange(rangeHeader, size);\n if (!parsedRange) {\n baseHeaders.set(\"content-range\", `bytes */${size}`);\n return new Response(null, {\n status: 416,\n headers: baseHeaders,\n });\n }\n\n const { start, end } = parsedRange;\n const chunkLength = end - start + 1;\n baseHeaders.set(\"content-length\", String(chunkLength));\n baseHeaders.set(\"content-range\", `bytes ${start}-${end}/${size}`);\n return new Response(Readable.toWeb(createReadStream(filePath, { start, end })) as BodyInit, {\n status: 206,\n headers: baseHeaders,\n });\n}\n\nfunction parseByteRange(rangeHeader: string, size: number): { start: number; end: number } | null {\n if (!rangeHeader.startsWith(\"bytes=\")) {\n return null;\n }\n\n const value = rangeHeader.slice(\"bytes=\".length).trim();\n if (value.length === 0 || value.includes(\",\")) {\n return null;\n }\n\n const [startText, endText] = value.split(\"-\", 2);\n if (startText === undefined || endText === undefined) {\n return null;\n }\n\n if (startText === \"\") {\n const suffixLength = Number.parseInt(endText, 10);\n if (!Number.isFinite(suffixLength) || suffixLength <= 0) {\n return null;\n }\n const start = Math.max(size - suffixLength, 0);\n const end = size - 1;\n return start <= end ? { start, end } : null;\n }\n\n const start = Number.parseInt(startText, 10);\n const end = endText === \"\" ? size - 1 : Number.parseInt(endText, 10);\n if (!Number.isFinite(start) || !Number.isFinite(end)) {\n return null;\n }\n\n if (start < 0 || end < start || start >= size) {\n return null;\n }\n\n return {\n start,\n end: Math.min(end, size - 1),\n };\n}\n\nfunction inferMimeType(filePath: string): string {\n const lower = filePath.toLowerCase();\n if (lower.endsWith(\".mp4\")) {\n return \"video/mp4\";\n }\n if (lower.endsWith(\".webm\")) {\n return \"video/webm\";\n }\n if (lower.endsWith(\".mov\")) {\n return \"video/quicktime\";\n }\n if (lower.endsWith(\".jpg\") || lower.endsWith(\".jpeg\")) {\n return \"image/jpeg\";\n }\n if (lower.endsWith(\".png\")) {\n return \"image/png\";\n }\n if (lower.endsWith(\".gif\")) {\n return \"image/gif\";\n }\n if (lower.endsWith(\".webp\")) {\n return \"image/webp\";\n }\n if (lower.endsWith(\".vtt\")) {\n return \"text/vtt\";\n }\n if (lower.endsWith(\".srt\")) {\n return \"application/x-subrip\";\n }\n if (lower.endsWith(\".mp3\")) {\n return \"audio/mpeg\";\n }\n if (lower.endsWith(\".wav\")) {\n return \"audio/wav\";\n }\n if (lower.endsWith(\".html\")) {\n return \"text/html; charset=utf-8\";\n }\n if (lower.endsWith(\".txt\")) {\n return \"text/plain; charset=utf-8\";\n }\n if (lower.endsWith(\".json\")) {\n return \"application/json; charset=utf-8\";\n }\n if (lower.endsWith(\".pdf\")) {\n return \"application/pdf\";\n }\n return \"application/octet-stream\";\n}\n\nfunction normalizeLogValue(value: unknown): JsonValue | undefined {\n if (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n return value;\n }\n\n if (value === undefined) {\n return undefined;\n }\n\n if (Array.isArray(value)) {\n return value\n .map((entry) => normalizeLogValue(entry))\n .filter((entry): entry is JsonValue => entry !== undefined);\n }\n\n if (value instanceof Error) {\n return {\n name: value.name,\n message: value.message,\n };\n }\n\n if (typeof value === \"object\") {\n return Object.fromEntries(\n Object.entries(value)\n .map(([key, entry]) => [key, normalizeLogValue(entry)])\n .filter(([, entry]) => entry !== undefined),\n ) as JsonValue;\n }\n\n return String(value);\n}\n"],"mappings":"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"}
@@ -0,0 +1,124 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_shared_errors = require("../shared/errors.cjs");
3
+ let node_fs = require("node:fs");
4
+ let node_path = require("node:path");
5
+ let node_crypto = require("node:crypto");
6
+ let node_os = require("node:os");
7
+ //#region src/main/storage-root-lock.ts
8
+ const STORAGE_ROOT_LOCK_FILE_NAME = ".media-cache.lock";
9
+ const LOCAL_HOSTNAME = (0, node_os.hostname)();
10
+ const activeLocks = /* @__PURE__ */ new Map();
11
+ let storageRootLockEnabled = true;
12
+ let cleanupHookInstalled = false;
13
+ function acquireStorageRootLock(storageRoot, owner) {
14
+ if (!storageRootLockEnabled) return null;
15
+ const activeLock = activeLocks.get(storageRoot);
16
+ if (activeLock) {
17
+ if (activeLock.owner === owner) return activeLock.handle;
18
+ throw createOwnershipError(storageRoot, (0, node_path.join)(storageRoot, STORAGE_ROOT_LOCK_FILE_NAME), null);
19
+ }
20
+ installCleanupHook();
21
+ return tryAcquireStorageRootLock(storageRoot, owner, false);
22
+ }
23
+ function resetStorageRootLocksForTests() {
24
+ cleanupActiveLocks();
25
+ }
26
+ function disableStorageRootLockForTests() {
27
+ storageRootLockEnabled = false;
28
+ cleanupActiveLocks();
29
+ }
30
+ function enableStorageRootLockForTests() {
31
+ storageRootLockEnabled = true;
32
+ cleanupActiveLocks();
33
+ }
34
+ function tryAcquireStorageRootLock(storageRoot, owner, hasReclaimedStaleLock) {
35
+ const lockFilePath = (0, node_path.join)(storageRoot, STORAGE_ROOT_LOCK_FILE_NAME);
36
+ const metadata = {
37
+ lockId: (0, node_crypto.randomUUID)(),
38
+ pid: process.pid,
39
+ hostname: LOCAL_HOSTNAME,
40
+ storageRoot,
41
+ acquiredAt: (/* @__PURE__ */ new Date()).toISOString()
42
+ };
43
+ try {
44
+ (0, node_fs.writeFileSync)(lockFilePath, `${JSON.stringify(metadata, null, 2)}\n`, { flag: "wx" });
45
+ } catch (error) {
46
+ if (!isAlreadyExistsError(error)) throw error;
47
+ const existingMetadata = readLockMetadata(lockFilePath);
48
+ if (!hasReclaimedStaleLock && existingMetadata?.hostname === LOCAL_HOSTNAME && !isProcessAlive(existingMetadata.pid)) {
49
+ (0, node_fs.rmSync)(lockFilePath, { force: true });
50
+ return tryAcquireStorageRootLock(storageRoot, owner, true);
51
+ }
52
+ throw createOwnershipError(storageRoot, lockFilePath, existingMetadata);
53
+ }
54
+ let released = false;
55
+ const handle = { release() {
56
+ if (released) return;
57
+ released = true;
58
+ if (activeLocks.get(storageRoot)?.handle === handle) activeLocks.delete(storageRoot);
59
+ removeOwnedLockFile(lockFilePath, metadata.lockId);
60
+ } };
61
+ activeLocks.set(storageRoot, {
62
+ owner,
63
+ handle
64
+ });
65
+ return handle;
66
+ }
67
+ function installCleanupHook() {
68
+ if (cleanupHookInstalled) return;
69
+ cleanupHookInstalled = true;
70
+ process.once("exit", cleanupActiveLocks);
71
+ }
72
+ function cleanupActiveLocks() {
73
+ const handles = [...activeLocks.values()].map((entry) => entry.handle);
74
+ for (const handle of handles) handle.release();
75
+ }
76
+ function removeOwnedLockFile(lockFilePath, expectedLockId) {
77
+ if (readLockMetadata(lockFilePath)?.lockId !== expectedLockId) return;
78
+ (0, node_fs.rmSync)(lockFilePath, { force: true });
79
+ }
80
+ function readLockMetadata(lockFilePath) {
81
+ try {
82
+ return parseLockMetadata((0, node_fs.readFileSync)(lockFilePath, "utf8"));
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+ function parseLockMetadata(raw) {
88
+ try {
89
+ const parsed = JSON.parse(raw);
90
+ if (typeof parsed.lockId !== "string" || typeof parsed.pid !== "number" || typeof parsed.hostname !== "string" || typeof parsed.storageRoot !== "string" || typeof parsed.acquiredAt !== "string") return null;
91
+ return {
92
+ lockId: parsed.lockId,
93
+ pid: parsed.pid,
94
+ hostname: parsed.hostname,
95
+ storageRoot: parsed.storageRoot,
96
+ acquiredAt: parsed.acquiredAt
97
+ };
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+ function createOwnershipError(storageRoot, lockFilePath, metadata) {
103
+ if (metadata) return new require_shared_errors.StorageOwnershipError(`Storage root "${storageRoot}" is already in use by process ${metadata.pid} on host ${metadata.hostname}. Lock file: ${lockFilePath}. If the recorded process is gone but belonged to another OS user, delete the lock file manually.`);
104
+ return new require_shared_errors.StorageOwnershipError(`Storage root "${storageRoot}" is already in use. Lock file: ${lockFilePath}`);
105
+ }
106
+ function isAlreadyExistsError(error) {
107
+ return error instanceof Error && "code" in error && error.code === "EEXIST";
108
+ }
109
+ function isProcessAlive(pid) {
110
+ if (!Number.isInteger(pid) || pid <= 0) return false;
111
+ try {
112
+ process.kill(pid, 0);
113
+ return true;
114
+ } catch (error) {
115
+ return !(error instanceof Error && "code" in error && error.code === "ESRCH");
116
+ }
117
+ }
118
+ //#endregion
119
+ exports.acquireStorageRootLock = acquireStorageRootLock;
120
+ exports.disableStorageRootLockForTests = disableStorageRootLockForTests;
121
+ exports.enableStorageRootLockForTests = enableStorageRootLockForTests;
122
+ exports.resetStorageRootLocksForTests = resetStorageRootLocksForTests;
123
+
124
+ //# sourceMappingURL=storage-root-lock.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-root-lock.cjs","names":["StorageOwnershipError"],"sources":["../../src/main/storage-root-lock.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { hostname } from \"node:os\";\nimport { join } from \"node:path\";\nimport { StorageOwnershipError } from \"../shared/errors.js\";\n\nconst STORAGE_ROOT_LOCK_FILE_NAME = \".media-cache.lock\";\nconst LOCAL_HOSTNAME = hostname();\n\ninterface StorageRootLockMetadata {\n lockId: string;\n pid: number;\n hostname: string;\n storageRoot: string;\n acquiredAt: string;\n}\n\nexport interface StorageRootLockHandle {\n release(): void;\n}\n\nconst activeLocks = new Map<\n string,\n {\n owner: object;\n handle: StorageRootLockHandle;\n }\n>();\n\nlet storageRootLockEnabled = true;\nlet cleanupHookInstalled = false;\n\nexport function acquireStorageRootLock(\n storageRoot: string,\n owner: object,\n): StorageRootLockHandle | null {\n if (!storageRootLockEnabled) {\n return null;\n }\n\n const activeLock = activeLocks.get(storageRoot);\n if (activeLock) {\n if (activeLock.owner === owner) {\n return activeLock.handle;\n }\n // Same-process collision: the active lock may not have written a file yet, so avoid a\n // misleading PID/hostname message that points back to the current process.\n throw createOwnershipError(storageRoot, join(storageRoot, STORAGE_ROOT_LOCK_FILE_NAME), null);\n }\n\n installCleanupHook();\n return tryAcquireStorageRootLock(storageRoot, owner, false);\n}\n\nexport function resetStorageRootLocksForTests(): void {\n cleanupActiveLocks();\n}\n\nexport function disableStorageRootLockForTests(): void {\n storageRootLockEnabled = false;\n cleanupActiveLocks();\n}\n\nexport function enableStorageRootLockForTests(): void {\n storageRootLockEnabled = true;\n cleanupActiveLocks();\n}\n\nfunction tryAcquireStorageRootLock(\n storageRoot: string,\n owner: object,\n hasReclaimedStaleLock: boolean,\n): StorageRootLockHandle {\n const lockFilePath = join(storageRoot, STORAGE_ROOT_LOCK_FILE_NAME);\n const metadata: StorageRootLockMetadata = {\n lockId: randomUUID(),\n pid: process.pid,\n hostname: LOCAL_HOSTNAME,\n storageRoot,\n acquiredAt: new Date().toISOString(),\n };\n\n try {\n writeFileSync(lockFilePath, `${JSON.stringify(metadata, null, 2)}\\n`, { flag: \"wx\" });\n } catch (error) {\n if (!isAlreadyExistsError(error)) {\n throw error;\n }\n\n const existingMetadata = readLockMetadata(lockFilePath);\n if (\n !hasReclaimedStaleLock &&\n existingMetadata?.hostname === LOCAL_HOSTNAME &&\n !isProcessAlive(existingMetadata.pid)\n ) {\n rmSync(lockFilePath, { force: true });\n return tryAcquireStorageRootLock(storageRoot, owner, true);\n }\n\n throw createOwnershipError(storageRoot, lockFilePath, existingMetadata);\n }\n\n let released = false;\n const handle: StorageRootLockHandle = {\n release() {\n if (released) {\n return;\n }\n released = true;\n\n const activeLock = activeLocks.get(storageRoot);\n if (activeLock?.handle === handle) {\n activeLocks.delete(storageRoot);\n }\n\n removeOwnedLockFile(lockFilePath, metadata.lockId);\n },\n };\n\n activeLocks.set(storageRoot, {\n owner,\n handle,\n });\n return handle;\n}\n\nfunction installCleanupHook(): void {\n if (cleanupHookInstalled) {\n return;\n }\n cleanupHookInstalled = true;\n process.once(\"exit\", cleanupActiveLocks);\n}\n\nfunction cleanupActiveLocks(): void {\n const handles = [...activeLocks.values()].map((entry) => entry.handle);\n for (const handle of handles) {\n handle.release();\n }\n}\n\nfunction removeOwnedLockFile(lockFilePath: string, expectedLockId: string): void {\n const metadata = readLockMetadata(lockFilePath);\n if (metadata?.lockId !== expectedLockId) {\n return;\n }\n rmSync(lockFilePath, { force: true });\n}\n\nfunction readLockMetadata(lockFilePath: string): StorageRootLockMetadata | null {\n try {\n return parseLockMetadata(readFileSync(lockFilePath, \"utf8\"));\n } catch {\n return null;\n }\n}\n\nfunction parseLockMetadata(raw: string): StorageRootLockMetadata | null {\n try {\n const parsed = JSON.parse(raw) as Partial<StorageRootLockMetadata>;\n if (\n typeof parsed.lockId !== \"string\" ||\n typeof parsed.pid !== \"number\" ||\n typeof parsed.hostname !== \"string\" ||\n typeof parsed.storageRoot !== \"string\" ||\n typeof parsed.acquiredAt !== \"string\"\n ) {\n return null;\n }\n return {\n lockId: parsed.lockId,\n pid: parsed.pid,\n hostname: parsed.hostname,\n storageRoot: parsed.storageRoot,\n acquiredAt: parsed.acquiredAt,\n };\n } catch {\n return null;\n }\n}\n\nfunction createOwnershipError(\n storageRoot: string,\n lockFilePath: string,\n metadata: Pick<StorageRootLockMetadata, \"hostname\" | \"pid\" | \"storageRoot\"> | null,\n): StorageOwnershipError {\n if (metadata) {\n return new StorageOwnershipError(\n `Storage root \"${storageRoot}\" is already in use by process ${metadata.pid} on host ${metadata.hostname}. Lock file: ${lockFilePath}. ` +\n \"If the recorded process is gone but belonged to another OS user, delete the lock file manually.\",\n );\n }\n\n return new StorageOwnershipError(\n `Storage root \"${storageRoot}\" is already in use. Lock file: ${lockFilePath}`,\n );\n}\n\nfunction isAlreadyExistsError(error: unknown): boolean {\n return (\n error instanceof Error && \"code\" in error && (error as NodeJS.ErrnoException).code === \"EEXIST\"\n );\n}\n\nfunction isProcessAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) {\n return false;\n }\n\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n // `EPERM` means another OS user may own the PID. Treat that as alive so we never steal a\n // lock from a running process; cross-user stale locks must be removed manually.\n return !(\n error instanceof Error &&\n \"code\" in error &&\n (error as NodeJS.ErrnoException).code === \"ESRCH\"\n );\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ //#region src/main/storage-root-lock.d.ts
2
+ interface StorageRootLockHandle {
3
+ release(): void;
4
+ }
5
+ declare function acquireStorageRootLock(storageRoot: string, owner: object): StorageRootLockHandle | null;
6
+ declare function resetStorageRootLocksForTests(): void;
7
+ declare function disableStorageRootLockForTests(): void;
8
+ declare function enableStorageRootLockForTests(): void;
9
+ //#endregion
10
+ export { StorageRootLockHandle, acquireStorageRootLock, disableStorageRootLockForTests, enableStorageRootLockForTests, resetStorageRootLocksForTests };
11
+ //# sourceMappingURL=storage-root-lock.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-root-lock.d.cts","names":[],"sources":["../../src/main/storage-root-lock.ts"],"mappings":";UAiBiB,qBAAA;EACf,OAAA;AAAA;AAAA,iBAcc,sBAAA,CACd,WAAA,UACA,KAAA,WACC,qBAAA;AAAA,iBAmBa,6BAAA,CAAA;AAAA,iBAIA,8BAAA,CAAA;AAAA,iBAKA,6BAAA,CAAA"}
@@ -0,0 +1,11 @@
1
+ //#region src/main/storage-root-lock.d.ts
2
+ interface StorageRootLockHandle {
3
+ release(): void;
4
+ }
5
+ declare function acquireStorageRootLock(storageRoot: string, owner: object): StorageRootLockHandle | null;
6
+ declare function resetStorageRootLocksForTests(): void;
7
+ declare function disableStorageRootLockForTests(): void;
8
+ declare function enableStorageRootLockForTests(): void;
9
+ //#endregion
10
+ export { StorageRootLockHandle, acquireStorageRootLock, disableStorageRootLockForTests, enableStorageRootLockForTests, resetStorageRootLocksForTests };
11
+ //# sourceMappingURL=storage-root-lock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-root-lock.d.ts","names":[],"sources":["../../src/main/storage-root-lock.ts"],"mappings":";UAiBiB,qBAAA;EACf,OAAA;AAAA;AAAA,iBAcc,sBAAA,CACd,WAAA,UACA,KAAA,WACC,qBAAA;AAAA,iBAmBa,6BAAA,CAAA;AAAA,iBAIA,8BAAA,CAAA;AAAA,iBAKA,6BAAA,CAAA"}
@@ -0,0 +1,120 @@
1
+ import { StorageOwnershipError } from "../shared/errors.js";
2
+ import { readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { randomUUID } from "node:crypto";
5
+ import { hostname } from "node:os";
6
+ //#region src/main/storage-root-lock.ts
7
+ const STORAGE_ROOT_LOCK_FILE_NAME = ".media-cache.lock";
8
+ const LOCAL_HOSTNAME = hostname();
9
+ const activeLocks = /* @__PURE__ */ new Map();
10
+ let storageRootLockEnabled = true;
11
+ let cleanupHookInstalled = false;
12
+ function acquireStorageRootLock(storageRoot, owner) {
13
+ if (!storageRootLockEnabled) return null;
14
+ const activeLock = activeLocks.get(storageRoot);
15
+ if (activeLock) {
16
+ if (activeLock.owner === owner) return activeLock.handle;
17
+ throw createOwnershipError(storageRoot, join(storageRoot, STORAGE_ROOT_LOCK_FILE_NAME), null);
18
+ }
19
+ installCleanupHook();
20
+ return tryAcquireStorageRootLock(storageRoot, owner, false);
21
+ }
22
+ function resetStorageRootLocksForTests() {
23
+ cleanupActiveLocks();
24
+ }
25
+ function disableStorageRootLockForTests() {
26
+ storageRootLockEnabled = false;
27
+ cleanupActiveLocks();
28
+ }
29
+ function enableStorageRootLockForTests() {
30
+ storageRootLockEnabled = true;
31
+ cleanupActiveLocks();
32
+ }
33
+ function tryAcquireStorageRootLock(storageRoot, owner, hasReclaimedStaleLock) {
34
+ const lockFilePath = join(storageRoot, STORAGE_ROOT_LOCK_FILE_NAME);
35
+ const metadata = {
36
+ lockId: randomUUID(),
37
+ pid: process.pid,
38
+ hostname: LOCAL_HOSTNAME,
39
+ storageRoot,
40
+ acquiredAt: (/* @__PURE__ */ new Date()).toISOString()
41
+ };
42
+ try {
43
+ writeFileSync(lockFilePath, `${JSON.stringify(metadata, null, 2)}\n`, { flag: "wx" });
44
+ } catch (error) {
45
+ if (!isAlreadyExistsError(error)) throw error;
46
+ const existingMetadata = readLockMetadata(lockFilePath);
47
+ if (!hasReclaimedStaleLock && existingMetadata?.hostname === LOCAL_HOSTNAME && !isProcessAlive(existingMetadata.pid)) {
48
+ rmSync(lockFilePath, { force: true });
49
+ return tryAcquireStorageRootLock(storageRoot, owner, true);
50
+ }
51
+ throw createOwnershipError(storageRoot, lockFilePath, existingMetadata);
52
+ }
53
+ let released = false;
54
+ const handle = { release() {
55
+ if (released) return;
56
+ released = true;
57
+ if (activeLocks.get(storageRoot)?.handle === handle) activeLocks.delete(storageRoot);
58
+ removeOwnedLockFile(lockFilePath, metadata.lockId);
59
+ } };
60
+ activeLocks.set(storageRoot, {
61
+ owner,
62
+ handle
63
+ });
64
+ return handle;
65
+ }
66
+ function installCleanupHook() {
67
+ if (cleanupHookInstalled) return;
68
+ cleanupHookInstalled = true;
69
+ process.once("exit", cleanupActiveLocks);
70
+ }
71
+ function cleanupActiveLocks() {
72
+ const handles = [...activeLocks.values()].map((entry) => entry.handle);
73
+ for (const handle of handles) handle.release();
74
+ }
75
+ function removeOwnedLockFile(lockFilePath, expectedLockId) {
76
+ if (readLockMetadata(lockFilePath)?.lockId !== expectedLockId) return;
77
+ rmSync(lockFilePath, { force: true });
78
+ }
79
+ function readLockMetadata(lockFilePath) {
80
+ try {
81
+ return parseLockMetadata(readFileSync(lockFilePath, "utf8"));
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+ function parseLockMetadata(raw) {
87
+ try {
88
+ const parsed = JSON.parse(raw);
89
+ if (typeof parsed.lockId !== "string" || typeof parsed.pid !== "number" || typeof parsed.hostname !== "string" || typeof parsed.storageRoot !== "string" || typeof parsed.acquiredAt !== "string") return null;
90
+ return {
91
+ lockId: parsed.lockId,
92
+ pid: parsed.pid,
93
+ hostname: parsed.hostname,
94
+ storageRoot: parsed.storageRoot,
95
+ acquiredAt: parsed.acquiredAt
96
+ };
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+ function createOwnershipError(storageRoot, lockFilePath, metadata) {
102
+ if (metadata) return new StorageOwnershipError(`Storage root "${storageRoot}" is already in use by process ${metadata.pid} on host ${metadata.hostname}. Lock file: ${lockFilePath}. If the recorded process is gone but belonged to another OS user, delete the lock file manually.`);
103
+ return new StorageOwnershipError(`Storage root "${storageRoot}" is already in use. Lock file: ${lockFilePath}`);
104
+ }
105
+ function isAlreadyExistsError(error) {
106
+ return error instanceof Error && "code" in error && error.code === "EEXIST";
107
+ }
108
+ function isProcessAlive(pid) {
109
+ if (!Number.isInteger(pid) || pid <= 0) return false;
110
+ try {
111
+ process.kill(pid, 0);
112
+ return true;
113
+ } catch (error) {
114
+ return !(error instanceof Error && "code" in error && error.code === "ESRCH");
115
+ }
116
+ }
117
+ //#endregion
118
+ export { acquireStorageRootLock, disableStorageRootLockForTests, enableStorageRootLockForTests, resetStorageRootLocksForTests };
119
+
120
+ //# sourceMappingURL=storage-root-lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-root-lock.js","names":[],"sources":["../../src/main/storage-root-lock.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { hostname } from \"node:os\";\nimport { join } from \"node:path\";\nimport { StorageOwnershipError } from \"../shared/errors.js\";\n\nconst STORAGE_ROOT_LOCK_FILE_NAME = \".media-cache.lock\";\nconst LOCAL_HOSTNAME = hostname();\n\ninterface StorageRootLockMetadata {\n lockId: string;\n pid: number;\n hostname: string;\n storageRoot: string;\n acquiredAt: string;\n}\n\nexport interface StorageRootLockHandle {\n release(): void;\n}\n\nconst activeLocks = new Map<\n string,\n {\n owner: object;\n handle: StorageRootLockHandle;\n }\n>();\n\nlet storageRootLockEnabled = true;\nlet cleanupHookInstalled = false;\n\nexport function acquireStorageRootLock(\n storageRoot: string,\n owner: object,\n): StorageRootLockHandle | null {\n if (!storageRootLockEnabled) {\n return null;\n }\n\n const activeLock = activeLocks.get(storageRoot);\n if (activeLock) {\n if (activeLock.owner === owner) {\n return activeLock.handle;\n }\n // Same-process collision: the active lock may not have written a file yet, so avoid a\n // misleading PID/hostname message that points back to the current process.\n throw createOwnershipError(storageRoot, join(storageRoot, STORAGE_ROOT_LOCK_FILE_NAME), null);\n }\n\n installCleanupHook();\n return tryAcquireStorageRootLock(storageRoot, owner, false);\n}\n\nexport function resetStorageRootLocksForTests(): void {\n cleanupActiveLocks();\n}\n\nexport function disableStorageRootLockForTests(): void {\n storageRootLockEnabled = false;\n cleanupActiveLocks();\n}\n\nexport function enableStorageRootLockForTests(): void {\n storageRootLockEnabled = true;\n cleanupActiveLocks();\n}\n\nfunction tryAcquireStorageRootLock(\n storageRoot: string,\n owner: object,\n hasReclaimedStaleLock: boolean,\n): StorageRootLockHandle {\n const lockFilePath = join(storageRoot, STORAGE_ROOT_LOCK_FILE_NAME);\n const metadata: StorageRootLockMetadata = {\n lockId: randomUUID(),\n pid: process.pid,\n hostname: LOCAL_HOSTNAME,\n storageRoot,\n acquiredAt: new Date().toISOString(),\n };\n\n try {\n writeFileSync(lockFilePath, `${JSON.stringify(metadata, null, 2)}\\n`, { flag: \"wx\" });\n } catch (error) {\n if (!isAlreadyExistsError(error)) {\n throw error;\n }\n\n const existingMetadata = readLockMetadata(lockFilePath);\n if (\n !hasReclaimedStaleLock &&\n existingMetadata?.hostname === LOCAL_HOSTNAME &&\n !isProcessAlive(existingMetadata.pid)\n ) {\n rmSync(lockFilePath, { force: true });\n return tryAcquireStorageRootLock(storageRoot, owner, true);\n }\n\n throw createOwnershipError(storageRoot, lockFilePath, existingMetadata);\n }\n\n let released = false;\n const handle: StorageRootLockHandle = {\n release() {\n if (released) {\n return;\n }\n released = true;\n\n const activeLock = activeLocks.get(storageRoot);\n if (activeLock?.handle === handle) {\n activeLocks.delete(storageRoot);\n }\n\n removeOwnedLockFile(lockFilePath, metadata.lockId);\n },\n };\n\n activeLocks.set(storageRoot, {\n owner,\n handle,\n });\n return handle;\n}\n\nfunction installCleanupHook(): void {\n if (cleanupHookInstalled) {\n return;\n }\n cleanupHookInstalled = true;\n process.once(\"exit\", cleanupActiveLocks);\n}\n\nfunction cleanupActiveLocks(): void {\n const handles = [...activeLocks.values()].map((entry) => entry.handle);\n for (const handle of handles) {\n handle.release();\n }\n}\n\nfunction removeOwnedLockFile(lockFilePath: string, expectedLockId: string): void {\n const metadata = readLockMetadata(lockFilePath);\n if (metadata?.lockId !== expectedLockId) {\n return;\n }\n rmSync(lockFilePath, { force: true });\n}\n\nfunction readLockMetadata(lockFilePath: string): StorageRootLockMetadata | null {\n try {\n return parseLockMetadata(readFileSync(lockFilePath, \"utf8\"));\n } catch {\n return null;\n }\n}\n\nfunction parseLockMetadata(raw: string): StorageRootLockMetadata | null {\n try {\n const parsed = JSON.parse(raw) as Partial<StorageRootLockMetadata>;\n if (\n typeof parsed.lockId !== \"string\" ||\n typeof parsed.pid !== \"number\" ||\n typeof parsed.hostname !== \"string\" ||\n typeof parsed.storageRoot !== \"string\" ||\n typeof parsed.acquiredAt !== \"string\"\n ) {\n return null;\n }\n return {\n lockId: parsed.lockId,\n pid: parsed.pid,\n hostname: parsed.hostname,\n storageRoot: parsed.storageRoot,\n acquiredAt: parsed.acquiredAt,\n };\n } catch {\n return null;\n }\n}\n\nfunction createOwnershipError(\n storageRoot: string,\n lockFilePath: string,\n metadata: Pick<StorageRootLockMetadata, \"hostname\" | \"pid\" | \"storageRoot\"> | null,\n): StorageOwnershipError {\n if (metadata) {\n return new StorageOwnershipError(\n `Storage root \"${storageRoot}\" is already in use by process ${metadata.pid} on host ${metadata.hostname}. Lock file: ${lockFilePath}. ` +\n \"If the recorded process is gone but belonged to another OS user, delete the lock file manually.\",\n );\n }\n\n return new StorageOwnershipError(\n `Storage root \"${storageRoot}\" is already in use. Lock file: ${lockFilePath}`,\n );\n}\n\nfunction isAlreadyExistsError(error: unknown): boolean {\n return (\n error instanceof Error && \"code\" in error && (error as NodeJS.ErrnoException).code === \"EEXIST\"\n );\n}\n\nfunction isProcessAlive(pid: number): boolean {\n if (!Number.isInteger(pid) || pid <= 0) {\n return false;\n }\n\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n // `EPERM` means another OS user may own the PID. Treat that as alive so we never steal a\n // lock from a running process; cross-user stale locks must be removed manually.\n return !(\n error instanceof Error &&\n \"code\" in error &&\n (error as NodeJS.ErrnoException).code === \"ESRCH\"\n );\n }\n}\n"],"mappings":"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"}