@meframe/core 0.0.40 → 0.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cache/CacheManager.d.ts +1 -0
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +2 -1
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/cache/resource/MP4IndexCache.d.ts +4 -0
- package/dist/cache/resource/MP4IndexCache.d.ts.map +1 -1
- package/dist/cache/resource/MP4IndexCache.js +6 -0
- package/dist/cache/resource/MP4IndexCache.js.map +1 -1
- package/dist/cache/resource/ResourceCache.d.ts +3 -8
- package/dist/cache/resource/ResourceCache.d.ts.map +1 -1
- package/dist/cache/resource/ResourceCache.js +26 -11
- package/dist/cache/resource/ResourceCache.js.map +1 -1
- package/dist/cache/storage/opfs/OPFSManager.d.ts +28 -4
- package/dist/cache/storage/opfs/OPFSManager.d.ts.map +1 -1
- package/dist/cache/storage/opfs/OPFSManager.js +110 -4
- package/dist/cache/storage/opfs/OPFSManager.js.map +1 -1
- package/dist/cache/storage/opfs/types.d.ts +5 -0
- package/dist/cache/storage/opfs/types.d.ts.map +1 -1
- package/dist/config/defaults.js +1 -1
- package/dist/config/defaults.js.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.d.ts +5 -0
- package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
- package/dist/orchestrator/GlobalAudioSession.js +45 -57
- package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
- package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
- package/dist/orchestrator/Orchestrator.js +2 -1
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.d.ts.map +1 -1
- package/dist/stages/compose/OfflineAudioMixer.js +3 -1
- package/dist/stages/compose/OfflineAudioMixer.js.map +1 -1
- package/dist/stages/demux/MP4IndexParser.d.ts +1 -0
- package/dist/stages/demux/MP4IndexParser.d.ts.map +1 -1
- package/dist/stages/demux/MP4IndexParser.js +20 -10
- package/dist/stages/demux/MP4IndexParser.js.map +1 -1
- package/dist/stages/load/ResourceLoader.d.ts +3 -1
- package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
- package/dist/stages/load/ResourceLoader.js +45 -4
- package/dist/stages/load/ResourceLoader.js.map +1 -1
- package/dist/stages/load/TaskManager.d.ts +7 -1
- package/dist/stages/load/TaskManager.d.ts.map +1 -1
- package/dist/stages/load/TaskManager.js +40 -2
- package/dist/stages/load/TaskManager.js.map +1 -1
- package/dist/utils/errors.d.ts +27 -0
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +32 -0
- package/dist/utils/errors.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CacheManager.d.ts","sourceRoot":"","sources":["../../src/cache/CacheManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAIvD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEtD,UAAU,kBAAkB;IAC1B,EAAE,EAAE;QACF,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,aAAa,CAAC,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,QAAQ,EAAE;QACR,SAAS,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"CacheManager.d.ts","sourceRoot":"","sources":["../../src/cache/CacheManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -17,7 +17,8 @@ class CacheManager {
|
|
|
17
17
|
constructor(config, eventBus) {
|
|
18
18
|
this.videoL1Cache = new VideoL1Cache(config.l1);
|
|
19
19
|
this.audioL1Cache = new AudioL1Cache();
|
|
20
|
-
const
|
|
20
|
+
const maxSizeMB = config.resource.maxSizeMB ?? 5120;
|
|
21
|
+
const opfsManager = new OPFSManager(maxSizeMB);
|
|
21
22
|
this.resourceCache = new ResourceCache(opfsManager, config.resource.projectId);
|
|
22
23
|
this.mp4IndexCache = new MP4IndexCache();
|
|
23
24
|
this.audioSampleCache = new AudioSampleCache();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CacheManager.js","sources":["../../src/cache/CacheManager.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\nimport { RcFrame } from '../model';\nimport { VideoL1Cache } from './l1/VideoL1Cache';\nimport { MeframeEvent } from '../event/events';\nimport type { EventBus } from '../event/EventBus';\nimport type { EventPayloadMap } from '../event/events';\nimport { AudioL1Cache } from './l1/AudioL1Cache';\nimport { WaiterReplacedError } from '../utils/errors';\nimport { OPFSManager } from './storage/opfs/OPFSManager';\nimport { ResourceCache } from './resource/ResourceCache';\nimport { MP4IndexCache } from './resource/MP4IndexCache';\nimport { AudioSampleCache } from './resource/AudioSampleCache';\nimport type { MP4Index } from '../stages/demux/types';\n\ninterface CacheManagerConfig {\n l1: {\n maxMemoryMB: number;\n maxGOPs?: number;\n gopIntervalUs?: number;\n };\n resource: {\n projectId: string;\n };\n}\n\ninterface ClipReadyWaiter {\n clipId: string;\n minFrameCount: number;\n startTimeUs: TimeUs;\n currentCount: number;\n resolve: (ready: boolean) => void;\n reject: (reason?: unknown) => void;\n timeoutId?: ReturnType<typeof setTimeout>;\n}\n\n/**\n * CacheManager for Window Cache Architecture\n *\n * Core features:\n * - L1 (VRAM) for composed VideoFrames (window cache ±3s)\n * - ResourceCache (OPFS) for original MP4 files\n * - MP4IndexCache (RAM) for GOP/Sample indexes\n * - AudioSampleCache (RAM) for extracted audio chunks\n */\nexport class CacheManager {\n readonly videoL1Cache: VideoL1Cache;\n private readonly audioL1Cache: AudioL1Cache;\n readonly resourceCache: ResourceCache;\n readonly mp4IndexCache: MP4IndexCache;\n readonly audioSampleCache: AudioSampleCache;\n private clipReadyWaiters = new Map<string, ClipReadyWaiter>();\n private eventBus?: EventBus<EventPayloadMap>;\n\n constructor(config: CacheManagerConfig, eventBus?: EventBus<EventPayloadMap>) {\n this.videoL1Cache = new VideoL1Cache(config.l1);\n this.audioL1Cache = new AudioL1Cache();\n\n // Initialize resource cache\n const opfsManager = new OPFSManager();\n this.resourceCache = new ResourceCache(opfsManager, config.resource.projectId);\n this.mp4IndexCache = new MP4IndexCache();\n this.audioSampleCache = new AudioSampleCache();\n this.eventBus = eventBus;\n }\n\n async init(): Promise<void> {\n await this.resourceCache.init();\n }\n\n /**\n * Set window center for L1 cache management (unified for video and audio)\n * L1 cache uses center ±3.5s window for video, ±5s for audio\n */\n setWindow(centerTimeUs: TimeUs): void {\n this.videoL1Cache.setWindow(centerTimeUs);\n this.audioL1Cache.setWindow(centerTimeUs);\n }\n\n /**\n * Check if resource exists in OPFS cache\n */\n async hasResourceInCache(resourceId: string): Promise<boolean> {\n return await this.resourceCache.hasResource(resourceId);\n }\n\n /**\n * Read byte range from resource (for GOP-level access)\n */\n async readResourceRange(resourceId: string, start: number, end: number): Promise<ArrayBuffer> {\n return await this.resourceCache.readRange(resourceId, start, end);\n }\n\n /**\n * Get MP4 index for a resource\n */\n getMP4Index(resourceId: string): MP4Index | null {\n return this.mp4IndexCache.get(resourceId);\n }\n\n putClipAudioData(\n clipId: string,\n audioData: AudioData,\n clipDurationUs: TimeUs,\n globalTimeUs?: TimeUs\n ): void {\n this.audioL1Cache.putClipAudioData(clipId, audioData, clipDurationUs, globalTimeUs);\n }\n\n getClipPCMWithMetadata(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs\n ): { planes: Float32Array[]; sampleRate: number; numberOfChannels: number } | null {\n return this.audioL1Cache.getPCMWithMetadata(clipId, startUs, endUs);\n }\n\n hasClipPCM(clipId: string): boolean {\n return this.audioL1Cache.hasClipPCM(clipId);\n }\n\n /**\n * Check if sufficient PCM data exists for the requested time window\n * Returns true only if at least 80% of requested duration is available\n */\n hasWindowPCM(clipId: string, startUs: TimeUs, endUs: TimeUs): boolean {\n return this.audioL1Cache.hasWindowData(clipId, startUs, endUs);\n }\n\n clearAudioCache(): void {\n this.audioL1Cache.clear();\n }\n\n clearClipAudioData(clipId: string): void {\n this.audioL1Cache.clearClipPCM(clipId);\n }\n\n /**\n * Add a frame to L1 cache\n * Handles event notifications (CacheCover, ComposeFrameReady)\n * @param frame - VideoFrame to add\n * @param clipId - Clip identifier\n * @param frameDuration - Frame duration in microseconds\n * @param trackId - Track identifier\n * @param globalTimeUs - Global timestamp for event emission and window management\n */\n addFrame(\n frame: VideoFrame,\n clipId: string,\n frameDuration: TimeUs,\n trackId: string,\n globalTimeUs: TimeUs\n ): RcFrame {\n const rcFrame = this.videoL1Cache.addFrame(frame, clipId, frameDuration, trackId, globalTimeUs);\n\n const relativeTimeUs = frame.timestamp ?? 0;\n\n // Check and notify clip ready\n this.checkAndNotifyClipReady(clipId, relativeTimeUs);\n\n // Emit cover event for first frame (globalTimeUs = 0 in composition)\n if (globalTimeUs === 0) {\n this.eventBus?.emit(MeframeEvent.CacheCover, {\n timeUs: globalTimeUs,\n clipId,\n level: 'L1',\n size: rcFrame.sizeEstimate ?? 0,\n });\n }\n\n // Emit frame ready event\n this.eventBus?.emit(MeframeEvent.ComposeFrameReady, {\n timeUs: globalTimeUs,\n frameNumber: Math.floor(globalTimeUs / frameDuration),\n renderTimeMs: 0,\n trackId,\n clipId,\n });\n\n return rcFrame;\n }\n\n /**\n * Get frame from L1 cache\n * @param timeUs - Clip-relative timestamp (0-based)\n * @param clipId - Clip identifier\n */\n getFrame(timeUs: TimeUs, clipId: string): RcFrame | null {\n return this.videoL1Cache.get(timeUs, clipId);\n }\n\n async invalidateClip(clipId: string): Promise<void> {\n this.videoL1Cache.invalidateClip(clipId);\n }\n\n /**\n * Evict a clip from L1 cache\n */\n invalidateClipInL1(clipId: string): void {\n this.videoL1Cache.invalidateClip(clipId);\n }\n\n /**\n * Check if a clip is cached in L1\n */\n hasClipInL1(clipId: string): boolean {\n return this.videoL1Cache.hasClip(clipId);\n }\n\n /**\n * Wait for a clip to have minimum frames cached\n * Used by PlaybackController for buffering state\n * Only one waiter per clip - new waiter replaces old one\n */\n waitForClipReady(\n clipId: string,\n options: { minFrameCount?: number; timeoutMs?: number; startTimeUs?: TimeUs } = {}\n ): Promise<boolean> {\n const minFrameCount = options.minFrameCount ?? 30;\n const startTimeUs = options.startTimeUs ?? 0;\n\n // Check if already have enough frames\n const currentFrameCount = this.videoL1Cache.getClipFrameCount(clipId, startTimeUs);\n if (currentFrameCount >= minFrameCount) {\n return Promise.resolve(true);\n }\n\n // Cancel previous waiter if exists\n const oldWaiter = this.clipReadyWaiters.get(clipId);\n if (oldWaiter) {\n if (oldWaiter.timeoutId) {\n clearTimeout(oldWaiter.timeoutId);\n }\n oldWaiter.reject(new WaiterReplacedError(clipId));\n }\n\n return new Promise<boolean>((resolve, reject) => {\n const waiter: ClipReadyWaiter = {\n clipId,\n minFrameCount,\n startTimeUs,\n currentCount: currentFrameCount,\n resolve,\n reject,\n };\n\n this.clipReadyWaiters.set(clipId, waiter);\n\n if (options.timeoutMs && options.timeoutMs > 0) {\n waiter.timeoutId = setTimeout(() => {\n if (this.clipReadyWaiters.get(clipId) === waiter) {\n this.clipReadyWaiters.delete(clipId);\n }\n resolve(false);\n }, options.timeoutMs);\n }\n });\n }\n\n clearL1Cache(): void {\n this.videoL1Cache.clear();\n }\n\n async clear(): Promise<void> {\n this.clearL1Cache();\n this.clearAudioCache();\n }\n\n getMetadata() {\n return {\n l1: this.videoL1Cache.getMetadata(),\n };\n }\n\n /**\n * Check if incoming frame satisfies clip ready condition\n * O(1) complexity - only checks single waiter and increments counter\n */\n checkAndNotifyClipReady(clipId: string, frameTimestampUs: TimeUs): void {\n const waiter = this.clipReadyWaiters.get(clipId);\n if (!waiter) {\n return;\n }\n\n // Count all frames if startTimeUs is 0 (buffering scenario)\n // Otherwise only count frames at or after the target start time\n const shouldCount = waiter.startTimeUs === 0 || frameTimestampUs >= waiter.startTimeUs;\n\n if (shouldCount) {\n waiter.currentCount++;\n\n if (waiter.currentCount >= waiter.minFrameCount) {\n if (waiter.timeoutId) {\n clearTimeout(waiter.timeoutId);\n }\n waiter.resolve(true);\n this.clipReadyWaiters.delete(clipId);\n }\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CacheManager.js","sources":["../../src/cache/CacheManager.ts"],"sourcesContent":["import type { TimeUs } from '../model/types';\nimport { RcFrame } from '../model';\nimport { VideoL1Cache } from './l1/VideoL1Cache';\nimport { MeframeEvent } from '../event/events';\nimport type { EventBus } from '../event/EventBus';\nimport type { EventPayloadMap } from '../event/events';\nimport { AudioL1Cache } from './l1/AudioL1Cache';\nimport { WaiterReplacedError } from '../utils/errors';\nimport { OPFSManager } from './storage/opfs/OPFSManager';\nimport { ResourceCache } from './resource/ResourceCache';\nimport { MP4IndexCache } from './resource/MP4IndexCache';\nimport { AudioSampleCache } from './resource/AudioSampleCache';\nimport type { MP4Index } from '../stages/demux/types';\n\ninterface CacheManagerConfig {\n l1: {\n maxMemoryMB: number;\n maxGOPs?: number;\n gopIntervalUs?: number;\n };\n resource: {\n projectId: string;\n maxSizeMB?: number;\n };\n}\n\ninterface ClipReadyWaiter {\n clipId: string;\n minFrameCount: number;\n startTimeUs: TimeUs;\n currentCount: number;\n resolve: (ready: boolean) => void;\n reject: (reason?: unknown) => void;\n timeoutId?: ReturnType<typeof setTimeout>;\n}\n\n/**\n * CacheManager for Window Cache Architecture\n *\n * Core features:\n * - L1 (VRAM) for composed VideoFrames (window cache ±3s)\n * - ResourceCache (OPFS) for original MP4 files\n * - MP4IndexCache (RAM) for GOP/Sample indexes\n * - AudioSampleCache (RAM) for extracted audio chunks\n */\nexport class CacheManager {\n readonly videoL1Cache: VideoL1Cache;\n private readonly audioL1Cache: AudioL1Cache;\n readonly resourceCache: ResourceCache;\n readonly mp4IndexCache: MP4IndexCache;\n readonly audioSampleCache: AudioSampleCache;\n private clipReadyWaiters = new Map<string, ClipReadyWaiter>();\n private eventBus?: EventBus<EventPayloadMap>;\n\n constructor(config: CacheManagerConfig, eventBus?: EventBus<EventPayloadMap>) {\n this.videoL1Cache = new VideoL1Cache(config.l1);\n this.audioL1Cache = new AudioL1Cache();\n\n const maxSizeMB = config.resource.maxSizeMB ?? 5120;\n const opfsManager = new OPFSManager(maxSizeMB);\n this.resourceCache = new ResourceCache(opfsManager, config.resource.projectId);\n this.mp4IndexCache = new MP4IndexCache();\n this.audioSampleCache = new AudioSampleCache();\n this.eventBus = eventBus;\n }\n\n async init(): Promise<void> {\n await this.resourceCache.init();\n }\n\n /**\n * Set window center for L1 cache management (unified for video and audio)\n * L1 cache uses center ±3.5s window for video, ±5s for audio\n */\n setWindow(centerTimeUs: TimeUs): void {\n this.videoL1Cache.setWindow(centerTimeUs);\n this.audioL1Cache.setWindow(centerTimeUs);\n }\n\n /**\n * Check if resource exists in OPFS cache\n */\n async hasResourceInCache(resourceId: string): Promise<boolean> {\n return await this.resourceCache.hasResource(resourceId);\n }\n\n /**\n * Read byte range from resource (for GOP-level access)\n */\n async readResourceRange(resourceId: string, start: number, end: number): Promise<ArrayBuffer> {\n return await this.resourceCache.readRange(resourceId, start, end);\n }\n\n /**\n * Get MP4 index for a resource\n */\n getMP4Index(resourceId: string): MP4Index | null {\n return this.mp4IndexCache.get(resourceId);\n }\n\n putClipAudioData(\n clipId: string,\n audioData: AudioData,\n clipDurationUs: TimeUs,\n globalTimeUs?: TimeUs\n ): void {\n this.audioL1Cache.putClipAudioData(clipId, audioData, clipDurationUs, globalTimeUs);\n }\n\n getClipPCMWithMetadata(\n clipId: string,\n startUs: TimeUs,\n endUs: TimeUs\n ): { planes: Float32Array[]; sampleRate: number; numberOfChannels: number } | null {\n return this.audioL1Cache.getPCMWithMetadata(clipId, startUs, endUs);\n }\n\n hasClipPCM(clipId: string): boolean {\n return this.audioL1Cache.hasClipPCM(clipId);\n }\n\n /**\n * Check if sufficient PCM data exists for the requested time window\n * Returns true only if at least 80% of requested duration is available\n */\n hasWindowPCM(clipId: string, startUs: TimeUs, endUs: TimeUs): boolean {\n return this.audioL1Cache.hasWindowData(clipId, startUs, endUs);\n }\n\n clearAudioCache(): void {\n this.audioL1Cache.clear();\n }\n\n clearClipAudioData(clipId: string): void {\n this.audioL1Cache.clearClipPCM(clipId);\n }\n\n /**\n * Add a frame to L1 cache\n * Handles event notifications (CacheCover, ComposeFrameReady)\n * @param frame - VideoFrame to add\n * @param clipId - Clip identifier\n * @param frameDuration - Frame duration in microseconds\n * @param trackId - Track identifier\n * @param globalTimeUs - Global timestamp for event emission and window management\n */\n addFrame(\n frame: VideoFrame,\n clipId: string,\n frameDuration: TimeUs,\n trackId: string,\n globalTimeUs: TimeUs\n ): RcFrame {\n const rcFrame = this.videoL1Cache.addFrame(frame, clipId, frameDuration, trackId, globalTimeUs);\n\n const relativeTimeUs = frame.timestamp ?? 0;\n\n // Check and notify clip ready\n this.checkAndNotifyClipReady(clipId, relativeTimeUs);\n\n // Emit cover event for first frame (globalTimeUs = 0 in composition)\n if (globalTimeUs === 0) {\n this.eventBus?.emit(MeframeEvent.CacheCover, {\n timeUs: globalTimeUs,\n clipId,\n level: 'L1',\n size: rcFrame.sizeEstimate ?? 0,\n });\n }\n\n // Emit frame ready event\n this.eventBus?.emit(MeframeEvent.ComposeFrameReady, {\n timeUs: globalTimeUs,\n frameNumber: Math.floor(globalTimeUs / frameDuration),\n renderTimeMs: 0,\n trackId,\n clipId,\n });\n\n return rcFrame;\n }\n\n /**\n * Get frame from L1 cache\n * @param timeUs - Clip-relative timestamp (0-based)\n * @param clipId - Clip identifier\n */\n getFrame(timeUs: TimeUs, clipId: string): RcFrame | null {\n return this.videoL1Cache.get(timeUs, clipId);\n }\n\n async invalidateClip(clipId: string): Promise<void> {\n this.videoL1Cache.invalidateClip(clipId);\n }\n\n /**\n * Evict a clip from L1 cache\n */\n invalidateClipInL1(clipId: string): void {\n this.videoL1Cache.invalidateClip(clipId);\n }\n\n /**\n * Check if a clip is cached in L1\n */\n hasClipInL1(clipId: string): boolean {\n return this.videoL1Cache.hasClip(clipId);\n }\n\n /**\n * Wait for a clip to have minimum frames cached\n * Used by PlaybackController for buffering state\n * Only one waiter per clip - new waiter replaces old one\n */\n waitForClipReady(\n clipId: string,\n options: { minFrameCount?: number; timeoutMs?: number; startTimeUs?: TimeUs } = {}\n ): Promise<boolean> {\n const minFrameCount = options.minFrameCount ?? 30;\n const startTimeUs = options.startTimeUs ?? 0;\n\n // Check if already have enough frames\n const currentFrameCount = this.videoL1Cache.getClipFrameCount(clipId, startTimeUs);\n if (currentFrameCount >= minFrameCount) {\n return Promise.resolve(true);\n }\n\n // Cancel previous waiter if exists\n const oldWaiter = this.clipReadyWaiters.get(clipId);\n if (oldWaiter) {\n if (oldWaiter.timeoutId) {\n clearTimeout(oldWaiter.timeoutId);\n }\n oldWaiter.reject(new WaiterReplacedError(clipId));\n }\n\n return new Promise<boolean>((resolve, reject) => {\n const waiter: ClipReadyWaiter = {\n clipId,\n minFrameCount,\n startTimeUs,\n currentCount: currentFrameCount,\n resolve,\n reject,\n };\n\n this.clipReadyWaiters.set(clipId, waiter);\n\n if (options.timeoutMs && options.timeoutMs > 0) {\n waiter.timeoutId = setTimeout(() => {\n if (this.clipReadyWaiters.get(clipId) === waiter) {\n this.clipReadyWaiters.delete(clipId);\n }\n resolve(false);\n }, options.timeoutMs);\n }\n });\n }\n\n clearL1Cache(): void {\n this.videoL1Cache.clear();\n }\n\n async clear(): Promise<void> {\n this.clearL1Cache();\n this.clearAudioCache();\n }\n\n getMetadata() {\n return {\n l1: this.videoL1Cache.getMetadata(),\n };\n }\n\n /**\n * Check if incoming frame satisfies clip ready condition\n * O(1) complexity - only checks single waiter and increments counter\n */\n checkAndNotifyClipReady(clipId: string, frameTimestampUs: TimeUs): void {\n const waiter = this.clipReadyWaiters.get(clipId);\n if (!waiter) {\n return;\n }\n\n // Count all frames if startTimeUs is 0 (buffering scenario)\n // Otherwise only count frames at or after the target start time\n const shouldCount = waiter.startTimeUs === 0 || frameTimestampUs >= waiter.startTimeUs;\n\n if (shouldCount) {\n waiter.currentCount++;\n\n if (waiter.currentCount >= waiter.minFrameCount) {\n if (waiter.timeoutId) {\n clearTimeout(waiter.timeoutId);\n }\n waiter.resolve(true);\n this.clipReadyWaiters.delete(clipId);\n }\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -44,5 +44,9 @@ export declare class MP4IndexCache {
|
|
|
44
44
|
* Remove index for a specific resource
|
|
45
45
|
*/
|
|
46
46
|
remove(resourceId: string): void;
|
|
47
|
+
/**
|
|
48
|
+
* Delete index for a resource (alias for remove)
|
|
49
|
+
*/
|
|
50
|
+
delete(resourceId: string): void;
|
|
47
51
|
}
|
|
48
52
|
//# sourceMappingURL=MP4IndexCache.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MP4IndexCache.d.ts","sourceRoot":"","sources":["../../../src/cache/resource/MP4IndexCache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAC3E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAGhD;;;;;;;GAOG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAA+B;IAE9C;;OAEG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,IAAI;IAI9C;;OAEG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IAIxC;;OAEG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAIhC;;;OAGG;IACH,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IA0CvE;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAclE;;OAEG;IACH,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAwBrE;;OAEG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACH,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;CAGjC"}
|
|
1
|
+
{"version":3,"file":"MP4IndexCache.d.ts","sourceRoot":"","sources":["../../../src/cache/resource/MP4IndexCache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAC3E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAGhD;;;;;;;GAOG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,OAAO,CAA+B;IAE9C;;OAEG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,GAAG,IAAI;IAI9C;;OAEG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IAIxC;;OAEG;IACH,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAIhC;;;OAGG;IACH,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI;IA0CvE;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAclE;;OAEG;IACH,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAwBrE;;OAEG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACH,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAIhC;;OAEG;IACH,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;CAGjC"}
|
|
@@ -97,6 +97,12 @@ class MP4IndexCache {
|
|
|
97
97
|
remove(resourceId) {
|
|
98
98
|
this.indexes.delete(resourceId);
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Delete index for a resource (alias for remove)
|
|
102
|
+
*/
|
|
103
|
+
delete(resourceId) {
|
|
104
|
+
this.indexes.delete(resourceId);
|
|
105
|
+
}
|
|
100
106
|
}
|
|
101
107
|
export {
|
|
102
108
|
MP4IndexCache
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MP4IndexCache.js","sources":["../../../src/cache/resource/MP4IndexCache.ts"],"sourcesContent":["import type { MP4Index, Sample, GOPRange } from '../../stages/demux/types';\nimport type { TimeUs } from '../../model/types';\nimport { binarySearchRange, binarySearchFirst } from '../../utils/binary-search';\n\n/**\n * MP4IndexCache - In-memory cache for MP4 indexes\n *\n * Provides fast lookup for:\n * - Sample at specific timestamp\n * - GOP range for time range\n * - Nearest keyframe\n */\nexport class MP4IndexCache {\n private indexes = new Map<string, MP4Index>();\n\n /**\n * Set index for a resource\n */\n set(resourceId: string, index: MP4Index): void {\n this.indexes.set(resourceId, index);\n }\n\n /**\n * Get index for a resource\n */\n get(resourceId: string): MP4Index | null {\n return this.indexes.get(resourceId) || null;\n }\n\n /**\n * Check if index exists for a resource\n */\n has(resourceId: string): boolean {\n return this.indexes.has(resourceId);\n }\n\n /**\n * Get GOP range (byte range) for a specific time\n * Returns the GOP that contains the target time\n */\n getGOPRangeForTime(resourceId: string, timeUs: TimeUs): GOPRange | null {\n const index = this.indexes.get(resourceId);\n if (!index?.tracks.video) return null;\n\n const { samples, gopIndex } = index.tracks.video;\n\n // Find GOP that contains this timestamp using binary search\n const targetGOP = binarySearchRange(gopIndex, timeUs, (gop, idx) => {\n const nextGOP = gopIndex[idx + 1];\n return {\n start: gop.startTimeUs,\n end: nextGOP ? nextGOP.startTimeUs : Infinity,\n };\n });\n\n if (!targetGOP) {\n console.warn('[MP4IndexCache] No GOP found for timeUs:', timeUs);\n return null;\n }\n\n // Calculate byte range for this GOP\n const startSample = samples[targetGOP.keyframeSampleIndex];\n const endSampleIndex = targetGOP.keyframeSampleIndex + targetGOP.sampleCount - 1;\n const endSample = samples[endSampleIndex];\n\n if (!startSample || !endSample) {\n console.error('[MP4IndexCache] Missing samples for GOP:', {\n keyframeSampleIndex: targetGOP.keyframeSampleIndex,\n sampleCount: targetGOP.sampleCount,\n totalSamples: samples.length,\n });\n return null;\n }\n\n return {\n byteStart: startSample.byteOffset,\n byteEnd: endSample.byteOffset + endSample.byteLength,\n startTimeUs: startSample.timestamp,\n endTimeUs: endSample.timestamp + endSample.duration,\n };\n }\n\n /**\n * Get sample at specific timestamp\n */\n getSampleAtTime(resourceId: string, timeUs: TimeUs): Sample | null {\n const index = this.indexes.get(resourceId);\n if (!index?.tracks.video) return null;\n\n const { samples } = index.tracks.video;\n\n const result = binarySearchRange(samples, timeUs, (sample, _index) => ({\n start: sample.timestamp,\n end: sample.timestamp + sample.duration,\n }));\n\n return result || null;\n }\n\n /**\n * Get nearest keyframe before or at the target time\n */\n getNearestKeyframe(resourceId: string, timeUs: TimeUs): Sample | null {\n const index = this.indexes.get(resourceId);\n if (!index?.tracks.video) return null;\n\n const { samples, gopIndex } = index.tracks.video;\n\n // Find the first GOP that starts after timeUs using binary search\n const firstGOPAfterIndex = binarySearchFirst(gopIndex, (gop) => gop.startTimeUs > timeUs);\n\n // The nearest keyframe is in the GOP before firstGOPAfterIndex\n const targetGOPIndex = firstGOPAfterIndex - 1;\n\n if (targetGOPIndex < 0) {\n // timeUs is before the first GOP\n return null;\n }\n\n const targetGOP = gopIndex[targetGOPIndex];\n if (!targetGOP) return null;\n\n // Return the keyframe sample of this GOP\n return samples[targetGOP.keyframeSampleIndex] ?? null;\n }\n\n /**\n * Clear all cached indexes\n */\n clear(): void {\n this.indexes.clear();\n }\n\n /**\n * Remove index for a specific resource\n */\n remove(resourceId: string): void {\n this.indexes.delete(resourceId);\n }\n}\n"],"names":[],"mappings":";AAYO,MAAM,cAAc;AAAA,EACjB,8BAAc,IAAA;AAAA;AAAA;AAAA;AAAA,EAKtB,IAAI,YAAoB,OAAuB;AAC7C,SAAK,QAAQ,IAAI,YAAY,KAAK;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,YAAqC;AACvC,WAAO,KAAK,QAAQ,IAAI,UAAU,KAAK;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,YAA6B;AAC/B,WAAO,KAAK,QAAQ,IAAI,UAAU;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,mBAAmB,YAAoB,QAAiC;AACtE,UAAM,QAAQ,KAAK,QAAQ,IAAI,UAAU;AACzC,QAAI,CAAC,OAAO,OAAO,MAAO,QAAO;AAEjC,UAAM,EAAE,SAAS,SAAA,IAAa,MAAM,OAAO;AAG3C,UAAM,YAAY,kBAAkB,UAAU,QAAQ,CAAC,KAAK,QAAQ;AAClE,YAAM,UAAU,SAAS,MAAM,CAAC;AAChC,aAAO;AAAA,QACL,OAAO,IAAI;AAAA,QACX,KAAK,UAAU,QAAQ,cAAc;AAAA,MAAA;AAAA,IAEzC,CAAC;AAED,QAAI,CAAC,WAAW;AACd,cAAQ,KAAK,4CAA4C,MAAM;AAC/D,aAAO;AAAA,IACT;AAGA,UAAM,cAAc,QAAQ,UAAU,mBAAmB;AACzD,UAAM,iBAAiB,UAAU,sBAAsB,UAAU,cAAc;AAC/E,UAAM,YAAY,QAAQ,cAAc;AAExC,QAAI,CAAC,eAAe,CAAC,WAAW;AAC9B,cAAQ,MAAM,4CAA4C;AAAA,QACxD,qBAAqB,UAAU;AAAA,QAC/B,aAAa,UAAU;AAAA,QACvB,cAAc,QAAQ;AAAA,MAAA,CACvB;AACD,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,MACL,WAAW,YAAY;AAAA,MACvB,SAAS,UAAU,aAAa,UAAU;AAAA,MAC1C,aAAa,YAAY;AAAA,MACzB,WAAW,UAAU,YAAY,UAAU;AAAA,IAAA;AAAA,EAE/C;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,YAAoB,QAA+B;AACjE,UAAM,QAAQ,KAAK,QAAQ,IAAI,UAAU;AACzC,QAAI,CAAC,OAAO,OAAO,MAAO,QAAO;AAEjC,UAAM,EAAE,QAAA,IAAY,MAAM,OAAO;AAEjC,UAAM,SAAS,kBAAkB,SAAS,QAAQ,CAAC,QAAQ,YAAY;AAAA,MACrE,OAAO,OAAO;AAAA,MACd,KAAK,OAAO,YAAY,OAAO;AAAA,IAAA,EAC/B;AAEF,WAAO,UAAU;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAmB,YAAoB,QAA+B;AACpE,UAAM,QAAQ,KAAK,QAAQ,IAAI,UAAU;AACzC,QAAI,CAAC,OAAO,OAAO,MAAO,QAAO;AAEjC,UAAM,EAAE,SAAS,SAAA,IAAa,MAAM,OAAO;AAG3C,UAAM,qBAAqB,kBAAkB,UAAU,CAAC,QAAQ,IAAI,cAAc,MAAM;AAGxF,UAAM,iBAAiB,qBAAqB;AAE5C,QAAI,iBAAiB,GAAG;AAEtB,aAAO;AAAA,IACT;AAEA,UAAM,YAAY,SAAS,cAAc;AACzC,QAAI,CAAC,UAAW,QAAO;AAGvB,WAAO,QAAQ,UAAU,mBAAmB,KAAK;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACZ,SAAK,QAAQ,MAAA;AAAA,EACf;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,YAA0B;AAC/B,SAAK,QAAQ,OAAO,UAAU;AAAA,EAChC;AACF;"}
|
|
1
|
+
{"version":3,"file":"MP4IndexCache.js","sources":["../../../src/cache/resource/MP4IndexCache.ts"],"sourcesContent":["import type { MP4Index, Sample, GOPRange } from '../../stages/demux/types';\nimport type { TimeUs } from '../../model/types';\nimport { binarySearchRange, binarySearchFirst } from '../../utils/binary-search';\n\n/**\n * MP4IndexCache - In-memory cache for MP4 indexes\n *\n * Provides fast lookup for:\n * - Sample at specific timestamp\n * - GOP range for time range\n * - Nearest keyframe\n */\nexport class MP4IndexCache {\n private indexes = new Map<string, MP4Index>();\n\n /**\n * Set index for a resource\n */\n set(resourceId: string, index: MP4Index): void {\n this.indexes.set(resourceId, index);\n }\n\n /**\n * Get index for a resource\n */\n get(resourceId: string): MP4Index | null {\n return this.indexes.get(resourceId) || null;\n }\n\n /**\n * Check if index exists for a resource\n */\n has(resourceId: string): boolean {\n return this.indexes.has(resourceId);\n }\n\n /**\n * Get GOP range (byte range) for a specific time\n * Returns the GOP that contains the target time\n */\n getGOPRangeForTime(resourceId: string, timeUs: TimeUs): GOPRange | null {\n const index = this.indexes.get(resourceId);\n if (!index?.tracks.video) return null;\n\n const { samples, gopIndex } = index.tracks.video;\n\n // Find GOP that contains this timestamp using binary search\n const targetGOP = binarySearchRange(gopIndex, timeUs, (gop, idx) => {\n const nextGOP = gopIndex[idx + 1];\n return {\n start: gop.startTimeUs,\n end: nextGOP ? nextGOP.startTimeUs : Infinity,\n };\n });\n\n if (!targetGOP) {\n console.warn('[MP4IndexCache] No GOP found for timeUs:', timeUs);\n return null;\n }\n\n // Calculate byte range for this GOP\n const startSample = samples[targetGOP.keyframeSampleIndex];\n const endSampleIndex = targetGOP.keyframeSampleIndex + targetGOP.sampleCount - 1;\n const endSample = samples[endSampleIndex];\n\n if (!startSample || !endSample) {\n console.error('[MP4IndexCache] Missing samples for GOP:', {\n keyframeSampleIndex: targetGOP.keyframeSampleIndex,\n sampleCount: targetGOP.sampleCount,\n totalSamples: samples.length,\n });\n return null;\n }\n\n return {\n byteStart: startSample.byteOffset,\n byteEnd: endSample.byteOffset + endSample.byteLength,\n startTimeUs: startSample.timestamp,\n endTimeUs: endSample.timestamp + endSample.duration,\n };\n }\n\n /**\n * Get sample at specific timestamp\n */\n getSampleAtTime(resourceId: string, timeUs: TimeUs): Sample | null {\n const index = this.indexes.get(resourceId);\n if (!index?.tracks.video) return null;\n\n const { samples } = index.tracks.video;\n\n const result = binarySearchRange(samples, timeUs, (sample, _index) => ({\n start: sample.timestamp,\n end: sample.timestamp + sample.duration,\n }));\n\n return result || null;\n }\n\n /**\n * Get nearest keyframe before or at the target time\n */\n getNearestKeyframe(resourceId: string, timeUs: TimeUs): Sample | null {\n const index = this.indexes.get(resourceId);\n if (!index?.tracks.video) return null;\n\n const { samples, gopIndex } = index.tracks.video;\n\n // Find the first GOP that starts after timeUs using binary search\n const firstGOPAfterIndex = binarySearchFirst(gopIndex, (gop) => gop.startTimeUs > timeUs);\n\n // The nearest keyframe is in the GOP before firstGOPAfterIndex\n const targetGOPIndex = firstGOPAfterIndex - 1;\n\n if (targetGOPIndex < 0) {\n // timeUs is before the first GOP\n return null;\n }\n\n const targetGOP = gopIndex[targetGOPIndex];\n if (!targetGOP) return null;\n\n // Return the keyframe sample of this GOP\n return samples[targetGOP.keyframeSampleIndex] ?? null;\n }\n\n /**\n * Clear all cached indexes\n */\n clear(): void {\n this.indexes.clear();\n }\n\n /**\n * Remove index for a specific resource\n */\n remove(resourceId: string): void {\n this.indexes.delete(resourceId);\n }\n\n /**\n * Delete index for a resource (alias for remove)\n */\n delete(resourceId: string): void {\n this.indexes.delete(resourceId);\n }\n}\n"],"names":[],"mappings":";AAYO,MAAM,cAAc;AAAA,EACjB,8BAAc,IAAA;AAAA;AAAA;AAAA;AAAA,EAKtB,IAAI,YAAoB,OAAuB;AAC7C,SAAK,QAAQ,IAAI,YAAY,KAAK;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,YAAqC;AACvC,WAAO,KAAK,QAAQ,IAAI,UAAU,KAAK;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,YAA6B;AAC/B,WAAO,KAAK,QAAQ,IAAI,UAAU;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,mBAAmB,YAAoB,QAAiC;AACtE,UAAM,QAAQ,KAAK,QAAQ,IAAI,UAAU;AACzC,QAAI,CAAC,OAAO,OAAO,MAAO,QAAO;AAEjC,UAAM,EAAE,SAAS,SAAA,IAAa,MAAM,OAAO;AAG3C,UAAM,YAAY,kBAAkB,UAAU,QAAQ,CAAC,KAAK,QAAQ;AAClE,YAAM,UAAU,SAAS,MAAM,CAAC;AAChC,aAAO;AAAA,QACL,OAAO,IAAI;AAAA,QACX,KAAK,UAAU,QAAQ,cAAc;AAAA,MAAA;AAAA,IAEzC,CAAC;AAED,QAAI,CAAC,WAAW;AACd,cAAQ,KAAK,4CAA4C,MAAM;AAC/D,aAAO;AAAA,IACT;AAGA,UAAM,cAAc,QAAQ,UAAU,mBAAmB;AACzD,UAAM,iBAAiB,UAAU,sBAAsB,UAAU,cAAc;AAC/E,UAAM,YAAY,QAAQ,cAAc;AAExC,QAAI,CAAC,eAAe,CAAC,WAAW;AAC9B,cAAQ,MAAM,4CAA4C;AAAA,QACxD,qBAAqB,UAAU;AAAA,QAC/B,aAAa,UAAU;AAAA,QACvB,cAAc,QAAQ;AAAA,MAAA,CACvB;AACD,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,MACL,WAAW,YAAY;AAAA,MACvB,SAAS,UAAU,aAAa,UAAU;AAAA,MAC1C,aAAa,YAAY;AAAA,MACzB,WAAW,UAAU,YAAY,UAAU;AAAA,IAAA;AAAA,EAE/C;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAgB,YAAoB,QAA+B;AACjE,UAAM,QAAQ,KAAK,QAAQ,IAAI,UAAU;AACzC,QAAI,CAAC,OAAO,OAAO,MAAO,QAAO;AAEjC,UAAM,EAAE,QAAA,IAAY,MAAM,OAAO;AAEjC,UAAM,SAAS,kBAAkB,SAAS,QAAQ,CAAC,QAAQ,YAAY;AAAA,MACrE,OAAO,OAAO;AAAA,MACd,KAAK,OAAO,YAAY,OAAO;AAAA,IAAA,EAC/B;AAEF,WAAO,UAAU;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAKA,mBAAmB,YAAoB,QAA+B;AACpE,UAAM,QAAQ,KAAK,QAAQ,IAAI,UAAU;AACzC,QAAI,CAAC,OAAO,OAAO,MAAO,QAAO;AAEjC,UAAM,EAAE,SAAS,SAAA,IAAa,MAAM,OAAO;AAG3C,UAAM,qBAAqB,kBAAkB,UAAU,CAAC,QAAQ,IAAI,cAAc,MAAM;AAGxF,UAAM,iBAAiB,qBAAqB;AAE5C,QAAI,iBAAiB,GAAG;AAEtB,aAAO;AAAA,IACT;AAEA,UAAM,YAAY,SAAS,cAAc;AACzC,QAAI,CAAC,UAAW,QAAO;AAGvB,WAAO,QAAQ,UAAU,mBAAmB,KAAK;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACZ,SAAK,QAAQ,MAAA;AAAA,EACf;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,YAA0B;AAC/B,SAAK,QAAQ,OAAO,UAAU;AAAA,EAChC;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,YAA0B;AAC/B,SAAK,QAAQ,OAAO,UAAU;AAAA,EAChC;AACF;"}
|
|
@@ -4,7 +4,7 @@ import { OPFSManager } from '../storage/opfs/OPFSManager';
|
|
|
4
4
|
* ResourceCache - Original video resource OPFS cache
|
|
5
5
|
*
|
|
6
6
|
* Stores original MP4/video files for on-demand decoding
|
|
7
|
-
*
|
|
7
|
+
* Project-level quota management handled by OPFSManager
|
|
8
8
|
*/
|
|
9
9
|
export declare class ResourceCache {
|
|
10
10
|
readonly opfsManager: OPFSManager;
|
|
@@ -13,6 +13,7 @@ export declare class ResourceCache {
|
|
|
13
13
|
init(): Promise<void>;
|
|
14
14
|
/**
|
|
15
15
|
* Write resource to OPFS (streaming)
|
|
16
|
+
* OPFSManager handles quota management automatically
|
|
16
17
|
*/
|
|
17
18
|
writeResource(resourceId: string, stream: ReadableStream<Uint8Array>): Promise<void>;
|
|
18
19
|
/**
|
|
@@ -20,7 +21,7 @@ export declare class ResourceCache {
|
|
|
20
21
|
*/
|
|
21
22
|
readRange(resourceId: string, start: number, end: number): Promise<ArrayBuffer>;
|
|
22
23
|
/**
|
|
23
|
-
* Check if resource exists in OPFS
|
|
24
|
+
* Check if resource exists in OPFS and has content
|
|
24
25
|
*/
|
|
25
26
|
hasResource(resourceId: string): Promise<boolean>;
|
|
26
27
|
/**
|
|
@@ -35,12 +36,6 @@ export declare class ResourceCache {
|
|
|
35
36
|
* Clear all resources for current project
|
|
36
37
|
*/
|
|
37
38
|
clear(): Promise<void>;
|
|
38
|
-
/**
|
|
39
|
-
* LRU eviction to free up space
|
|
40
|
-
* Note: This is a simplified implementation
|
|
41
|
-
* Full implementation would track access times in IndexedDB
|
|
42
|
-
*/
|
|
43
|
-
evictLRU(_targetBytes: number): Promise<void>;
|
|
44
39
|
private getFileName;
|
|
45
40
|
}
|
|
46
41
|
//# sourceMappingURL=ResourceCache.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResourceCache.d.ts","sourceRoot":"","sources":["../../../src/cache/resource/ResourceCache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"ResourceCache.d.ts","sourceRoot":"","sources":["../../../src/cache/resource/ResourceCache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAI1D;;;;;GAKG;AACH,qBAAa,aAAa;IACxB,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM;IAKjD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B;;;OAGG;IACG,aAAa,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAyB1F;;OAEG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAWrF;;OAEG;IACG,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAqBvD;;OAEG;IACG,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAW1D;;OAEG;IACG,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWvD;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B,OAAO,CAAC,WAAW;CAGpB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { OPFSQuotaExceededError } from "../../utils/errors.js";
|
|
1
2
|
class ResourceCache {
|
|
2
3
|
opfsManager;
|
|
3
4
|
projectId;
|
|
@@ -10,6 +11,7 @@ class ResourceCache {
|
|
|
10
11
|
}
|
|
11
12
|
/**
|
|
12
13
|
* Write resource to OPFS (streaming)
|
|
14
|
+
* OPFSManager handles quota management automatically
|
|
13
15
|
*/
|
|
14
16
|
async writeResource(resourceId, stream) {
|
|
15
17
|
const fileName = this.getFileName(resourceId);
|
|
@@ -18,7 +20,21 @@ class ResourceCache {
|
|
|
18
20
|
prefix: "resource",
|
|
19
21
|
fileName
|
|
20
22
|
};
|
|
21
|
-
|
|
23
|
+
try {
|
|
24
|
+
await this.opfsManager.writeFile(path, stream);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (error instanceof OPFSQuotaExceededError) {
|
|
27
|
+
if (error.retryable) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Failed to write resource ${resourceId}: ${error.message} Please retry with a fresh stream.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Failed to write resource ${resourceId}: No space available and no old projects to evict.`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
22
38
|
}
|
|
23
39
|
/**
|
|
24
40
|
* Read byte range from resource (for GOP-level access)
|
|
@@ -33,7 +49,7 @@ class ResourceCache {
|
|
|
33
49
|
return await this.opfsManager.readRange(path, start, end);
|
|
34
50
|
}
|
|
35
51
|
/**
|
|
36
|
-
* Check if resource exists in OPFS
|
|
52
|
+
* Check if resource exists in OPFS and has content
|
|
37
53
|
*/
|
|
38
54
|
async hasResource(resourceId) {
|
|
39
55
|
const fileName = this.getFileName(resourceId);
|
|
@@ -42,7 +58,14 @@ class ResourceCache {
|
|
|
42
58
|
prefix: "resource",
|
|
43
59
|
fileName
|
|
44
60
|
};
|
|
45
|
-
|
|
61
|
+
const exists = await this.opfsManager.exists(path);
|
|
62
|
+
if (!exists) return false;
|
|
63
|
+
try {
|
|
64
|
+
const size = await this.opfsManager.getFileSize(path);
|
|
65
|
+
return size > 0;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
46
69
|
}
|
|
47
70
|
/**
|
|
48
71
|
* Get resource file size
|
|
@@ -74,14 +97,6 @@ class ResourceCache {
|
|
|
74
97
|
async clear() {
|
|
75
98
|
await this.opfsManager.deleteProjectDirectory(this.projectId, "resource");
|
|
76
99
|
}
|
|
77
|
-
/**
|
|
78
|
-
* LRU eviction to free up space
|
|
79
|
-
* Note: This is a simplified implementation
|
|
80
|
-
* Full implementation would track access times in IndexedDB
|
|
81
|
-
*/
|
|
82
|
-
async evictLRU(_targetBytes) {
|
|
83
|
-
console.warn("[ResourceCache] LRU eviction not yet implemented");
|
|
84
|
-
}
|
|
85
100
|
getFileName(resourceId) {
|
|
86
101
|
return `${resourceId}.mp4`;
|
|
87
102
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResourceCache.js","sources":["../../../src/cache/resource/ResourceCache.ts"],"sourcesContent":["import { OPFSManager } from '../storage/opfs/OPFSManager';\nimport type { OPFSPath } from '../storage/opfs/types';\n\n/**\n * ResourceCache - Original video resource OPFS cache\n *\n * Stores original MP4/video files for on-demand decoding\n *
|
|
1
|
+
{"version":3,"file":"ResourceCache.js","sources":["../../../src/cache/resource/ResourceCache.ts"],"sourcesContent":["import { OPFSManager } from '../storage/opfs/OPFSManager';\nimport type { OPFSPath } from '../storage/opfs/types';\nimport { OPFSQuotaExceededError } from '../../utils/errors';\n\n/**\n * ResourceCache - Original video resource OPFS cache\n *\n * Stores original MP4/video files for on-demand decoding\n * Project-level quota management handled by OPFSManager\n */\nexport class ResourceCache {\n readonly opfsManager: OPFSManager;\n readonly projectId: string;\n\n constructor(opfsManager: OPFSManager, projectId: string) {\n this.opfsManager = opfsManager;\n this.projectId = projectId;\n }\n\n async init(): Promise<void> {\n await this.opfsManager.init();\n }\n\n /**\n * Write resource to OPFS (streaming)\n * OPFSManager handles quota management automatically\n */\n async writeResource(resourceId: string, stream: ReadableStream<Uint8Array>): Promise<void> {\n const fileName = this.getFileName(resourceId);\n const path: OPFSPath = {\n projectId: this.projectId,\n prefix: 'resource',\n fileName,\n };\n\n try {\n await this.opfsManager.writeFile(path, stream);\n } catch (error) {\n if (error instanceof OPFSQuotaExceededError) {\n if (error.retryable) {\n throw new Error(\n `Failed to write resource ${resourceId}: ${error.message} Please retry with a fresh stream.`\n );\n }\n throw new Error(\n `Failed to write resource ${resourceId}: No space available and no old projects to evict.`\n );\n }\n throw error;\n }\n }\n\n /**\n * Read byte range from resource (for GOP-level access)\n */\n async readRange(resourceId: string, start: number, end: number): Promise<ArrayBuffer> {\n const fileName = this.getFileName(resourceId);\n const path: OPFSPath = {\n projectId: this.projectId,\n prefix: 'resource',\n fileName,\n };\n\n return await this.opfsManager.readRange(path, start, end);\n }\n\n /**\n * Check if resource exists in OPFS and has content\n */\n async hasResource(resourceId: string): Promise<boolean> {\n const fileName = this.getFileName(resourceId);\n const path: OPFSPath = {\n projectId: this.projectId,\n prefix: 'resource',\n fileName,\n };\n\n // Check if file exists\n const exists = await this.opfsManager.exists(path);\n if (!exists) return false;\n\n // Check file size to avoid empty files (race condition protection)\n try {\n const size = await this.opfsManager.getFileSize(path);\n return size > 0;\n } catch {\n return false;\n }\n }\n\n /**\n * Get resource file size\n */\n async getResourceSize(resourceId: string): Promise<number> {\n const fileName = this.getFileName(resourceId);\n const path: OPFSPath = {\n projectId: this.projectId,\n prefix: 'resource',\n fileName,\n };\n\n return await this.opfsManager.getFileSize(path);\n }\n\n /**\n * Delete a resource from OPFS\n */\n async deleteResource(resourceId: string): Promise<void> {\n const fileName = this.getFileName(resourceId);\n const path: OPFSPath = {\n projectId: this.projectId,\n prefix: 'resource',\n fileName,\n };\n\n await this.opfsManager.deleteFile(path);\n }\n\n /**\n * Clear all resources for current project\n */\n async clear(): Promise<void> {\n await this.opfsManager.deleteProjectDirectory(this.projectId, 'resource');\n }\n\n private getFileName(resourceId: string): string {\n return `${resourceId}.mp4`;\n }\n}\n"],"names":[],"mappings":";AAUO,MAAM,cAAc;AAAA,EAChB;AAAA,EACA;AAAA,EAET,YAAY,aAA0B,WAAmB;AACvD,SAAK,cAAc;AACnB,SAAK,YAAY;AAAA,EACnB;AAAA,EAEA,MAAM,OAAsB;AAC1B,UAAM,KAAK,YAAY,KAAA;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,cAAc,YAAoB,QAAmD;AACzF,UAAM,WAAW,KAAK,YAAY,UAAU;AAC5C,UAAM,OAAiB;AAAA,MACrB,WAAW,KAAK;AAAA,MAChB,QAAQ;AAAA,MACR;AAAA,IAAA;AAGF,QAAI;AACF,YAAM,KAAK,YAAY,UAAU,MAAM,MAAM;AAAA,IAC/C,SAAS,OAAO;AACd,UAAI,iBAAiB,wBAAwB;AAC3C,YAAI,MAAM,WAAW;AACnB,gBAAM,IAAI;AAAA,YACR,4BAA4B,UAAU,KAAK,MAAM,OAAO;AAAA,UAAA;AAAA,QAE5D;AACA,cAAM,IAAI;AAAA,UACR,4BAA4B,UAAU;AAAA,QAAA;AAAA,MAE1C;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAU,YAAoB,OAAe,KAAmC;AACpF,UAAM,WAAW,KAAK,YAAY,UAAU;AAC5C,UAAM,OAAiB;AAAA,MACrB,WAAW,KAAK;AAAA,MAChB,QAAQ;AAAA,MACR;AAAA,IAAA;AAGF,WAAO,MAAM,KAAK,YAAY,UAAU,MAAM,OAAO,GAAG;AAAA,EAC1D;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAY,YAAsC;AACtD,UAAM,WAAW,KAAK,YAAY,UAAU;AAC5C,UAAM,OAAiB;AAAA,MACrB,WAAW,KAAK;AAAA,MAChB,QAAQ;AAAA,MACR;AAAA,IAAA;AAIF,UAAM,SAAS,MAAM,KAAK,YAAY,OAAO,IAAI;AACjD,QAAI,CAAC,OAAQ,QAAO;AAGpB,QAAI;AACF,YAAM,OAAO,MAAM,KAAK,YAAY,YAAY,IAAI;AACpD,aAAO,OAAO;AAAA,IAChB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,gBAAgB,YAAqC;AACzD,UAAM,WAAW,KAAK,YAAY,UAAU;AAC5C,UAAM,OAAiB;AAAA,MACrB,WAAW,KAAK;AAAA,MAChB,QAAQ;AAAA,MACR;AAAA,IAAA;AAGF,WAAO,MAAM,KAAK,YAAY,YAAY,IAAI;AAAA,EAChD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,eAAe,YAAmC;AACtD,UAAM,WAAW,KAAK,YAAY,UAAU;AAC5C,UAAM,OAAiB;AAAA,MACrB,WAAW,KAAK;AAAA,MAChB,QAAQ;AAAA,MACR;AAAA,IAAA;AAGF,UAAM,KAAK,YAAY,WAAW,IAAI;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAuB;AAC3B,UAAM,KAAK,YAAY,uBAAuB,KAAK,WAAW,UAAU;AAAA,EAC1E;AAAA,EAEQ,YAAY,YAA4B;AAC9C,WAAO,GAAG,UAAU;AAAA,EACtB;AACF;"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OPFSPrefix, OPFSPath } from './types';
|
|
1
|
+
import { OPFSPrefix, OPFSPath, ProjectMetadata } from './types';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* OPFSManager - Unified OPFS management infrastructure
|
|
@@ -6,12 +6,15 @@ import { OPFSPrefix, OPFSPath } from './types';
|
|
|
6
6
|
* Supports:
|
|
7
7
|
* - Directory isolation by prefix (l2/resource)
|
|
8
8
|
* - Range reads for on-demand decoding
|
|
9
|
-
* - Streaming writes
|
|
10
|
-
* -
|
|
9
|
+
* - Streaming writes with automatic quota management
|
|
10
|
+
* - Project-level LRU eviction
|
|
11
11
|
*/
|
|
12
12
|
export declare class OPFSManager {
|
|
13
13
|
private opfsRoot;
|
|
14
14
|
private initPromise;
|
|
15
|
+
readonly maxSizeBytes: number;
|
|
16
|
+
readonly quotaThreshold: number;
|
|
17
|
+
constructor(maxSizeMB?: number, quotaThresholdPercent?: number);
|
|
15
18
|
init(): Promise<void>;
|
|
16
19
|
private initStorage;
|
|
17
20
|
/**
|
|
@@ -19,9 +22,20 @@ export declare class OPFSManager {
|
|
|
19
22
|
*/
|
|
20
23
|
getProjectDir(projectId: string, prefix: OPFSPrefix): Promise<FileSystemDirectoryHandle>;
|
|
21
24
|
/**
|
|
22
|
-
* Write file
|
|
25
|
+
* Write file with automatic quota management
|
|
26
|
+
* Proactively checks quota before write; evicts old projects if threshold exceeded
|
|
23
27
|
*/
|
|
24
28
|
writeFile(path: OPFSPath, data: ArrayBuffer | ReadableStream<Uint8Array>): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Ensure quota is within threshold before write
|
|
31
|
+
* Proactively evicts old projects if usage exceeds threshold
|
|
32
|
+
*/
|
|
33
|
+
private ensureQuota;
|
|
34
|
+
/**
|
|
35
|
+
* Get total size of all projects for a prefix
|
|
36
|
+
*/
|
|
37
|
+
getTotalSize(prefix: OPFSPrefix): Promise<number>;
|
|
38
|
+
private writeFileInternal;
|
|
25
39
|
/**
|
|
26
40
|
* Read entire file
|
|
27
41
|
*/
|
|
@@ -50,5 +64,15 @@ export declare class OPFSManager {
|
|
|
50
64
|
* Delete entire project directory
|
|
51
65
|
*/
|
|
52
66
|
deleteProjectDirectory(projectId: string, prefix: OPFSPrefix): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* List all projects with size and lastModified metadata
|
|
69
|
+
* Used for LRU eviction
|
|
70
|
+
*/
|
|
71
|
+
listProjectsWithMetadata(prefix: OPFSPrefix): Promise<ProjectMetadata[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Evict oldest projects (by lastModified) excluding current project
|
|
74
|
+
* Returns number of projects evicted
|
|
75
|
+
*/
|
|
76
|
+
evictOldestProjects(currentProjectId: string, prefix: OPFSPrefix, count: number): Promise<number>;
|
|
53
77
|
}
|
|
54
78
|
//# sourceMappingURL=OPFSManager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OPFSManager.d.ts","sourceRoot":"","sources":["../../../../src/cache/storage/opfs/OPFSManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"OPFSManager.d.ts","sourceRoot":"","sources":["../../../../src/cache/storage/opfs/OPFSManager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAOrE;;;;;;;;GAQG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAA0C;IAC1D,OAAO,CAAC,WAAW,CAA8B;IACjD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;gBAEpB,SAAS,GAAE,MAAa,EAAE,qBAAqB,GAAE,MAAa;IAKpE,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YAOb,WAAW;IAIzB;;OAEG;IACG,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,yBAAyB,CAAC;IAS9F;;;OAGG;IACG,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,GAAG,cAAc,CAAC,UAAU,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAwB9F;;;OAGG;YACW,WAAW;IAgBzB;;OAEG;IACG,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;YAKzC,iBAAiB;IAgC/B;;OAEG;IACG,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,WAAW,CAAC;IAOpD;;OAEG;IACG,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAQjF;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAW/C;;OAEG;IACG,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAa9C;;OAEG;IACG,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC;IAOlD;;OAEG;IACG,oBAAoB,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,4BAA4B,CAAC;IAMjF;;OAEG;IACG,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAalF;;;OAGG;IACG,wBAAwB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAkC9E;;;OAGG;IACG,mBAAmB,CACvB,gBAAgB,EAAE,MAAM,EACxB,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC;CA+BnB"}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
import { OPFSQuotaExceededError } from "../../../utils/errors.js";
|
|
2
|
+
function isDOMException(error, name) {
|
|
3
|
+
return error instanceof Error && "name" in error && error.name === name;
|
|
4
|
+
}
|
|
1
5
|
class OPFSManager {
|
|
2
6
|
opfsRoot = null;
|
|
3
7
|
initPromise = null;
|
|
8
|
+
maxSizeBytes;
|
|
9
|
+
quotaThreshold;
|
|
10
|
+
constructor(maxSizeMB = 5120, quotaThresholdPercent = 0.85) {
|
|
11
|
+
this.maxSizeBytes = maxSizeMB * 1024 * 1024;
|
|
12
|
+
this.quotaThreshold = quotaThresholdPercent;
|
|
13
|
+
}
|
|
4
14
|
async init() {
|
|
5
15
|
if (this.initPromise) return this.initPromise;
|
|
6
16
|
this.initPromise = this.initStorage();
|
|
@@ -20,9 +30,52 @@ class OPFSManager {
|
|
|
20
30
|
return this.opfsRoot.getDirectoryHandle(dirName, { create: true });
|
|
21
31
|
}
|
|
22
32
|
/**
|
|
23
|
-
* Write file
|
|
33
|
+
* Write file with automatic quota management
|
|
34
|
+
* Proactively checks quota before write; evicts old projects if threshold exceeded
|
|
24
35
|
*/
|
|
25
36
|
async writeFile(path, data) {
|
|
37
|
+
await this.ensureQuota(path.projectId, path.prefix);
|
|
38
|
+
try {
|
|
39
|
+
await this.writeFileInternal(path, data);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (isDOMException(error, "QuotaExceededError")) {
|
|
42
|
+
const evictedCount = await this.evictOldestProjects(path.projectId, path.prefix, 1);
|
|
43
|
+
if (evictedCount === 0) {
|
|
44
|
+
throw new OPFSQuotaExceededError(path.projectId, path.prefix, false);
|
|
45
|
+
}
|
|
46
|
+
if (data instanceof ReadableStream) {
|
|
47
|
+
throw new OPFSQuotaExceededError(path.projectId, path.prefix, true);
|
|
48
|
+
}
|
|
49
|
+
await this.writeFileInternal(path, data);
|
|
50
|
+
} else {
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Ensure quota is within threshold before write
|
|
57
|
+
* Proactively evicts old projects if usage exceeds threshold
|
|
58
|
+
*/
|
|
59
|
+
async ensureQuota(currentProjectId, prefix) {
|
|
60
|
+
const totalSize = await this.getTotalSize(prefix);
|
|
61
|
+
const usagePercent = totalSize / this.maxSizeBytes;
|
|
62
|
+
if (usagePercent > this.quotaThreshold) {
|
|
63
|
+
const needToFreeBytes = totalSize - this.maxSizeBytes * this.quotaThreshold;
|
|
64
|
+
const projectsToEvict = Math.ceil(needToFreeBytes / (totalSize / 10));
|
|
65
|
+
console.log(
|
|
66
|
+
`[OPFSManager] Quota usage ${(usagePercent * 100).toFixed(1)}% exceeds threshold ${(this.quotaThreshold * 100).toFixed(0)}%, evicting old projects`
|
|
67
|
+
);
|
|
68
|
+
await this.evictOldestProjects(currentProjectId, prefix, Math.max(1, projectsToEvict));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Get total size of all projects for a prefix
|
|
73
|
+
*/
|
|
74
|
+
async getTotalSize(prefix) {
|
|
75
|
+
const projects = await this.listProjectsWithMetadata(prefix);
|
|
76
|
+
return projects.reduce((sum, p) => sum + p.size, 0);
|
|
77
|
+
}
|
|
78
|
+
async writeFileInternal(path, data) {
|
|
26
79
|
const projectDir = await this.getProjectDir(path.projectId, path.prefix);
|
|
27
80
|
const fileHandle = await projectDir.getFileHandle(path.fileName, { create: true });
|
|
28
81
|
const writable = await fileHandle.createWritable();
|
|
@@ -75,7 +128,7 @@ class OPFSManager {
|
|
|
75
128
|
const projectDir = await this.getProjectDir(path.projectId, path.prefix);
|
|
76
129
|
await projectDir.removeEntry(path.fileName);
|
|
77
130
|
} catch (error) {
|
|
78
|
-
if (error
|
|
131
|
+
if (!isDOMException(error, "NotFoundError")) {
|
|
79
132
|
console.warn(`[OPFSManager] Failed to delete file ${path.fileName}:`, error);
|
|
80
133
|
}
|
|
81
134
|
}
|
|
@@ -89,7 +142,7 @@ class OPFSManager {
|
|
|
89
142
|
await projectDir.getFileHandle(path.fileName, { create: false });
|
|
90
143
|
return true;
|
|
91
144
|
} catch (error) {
|
|
92
|
-
if (error
|
|
145
|
+
if (isDOMException(error, "NotFoundError")) {
|
|
93
146
|
return false;
|
|
94
147
|
}
|
|
95
148
|
throw error;
|
|
@@ -121,11 +174,64 @@ class OPFSManager {
|
|
|
121
174
|
const dirName = `meframe-${prefix}-${projectId}`;
|
|
122
175
|
await this.opfsRoot.removeEntry(dirName, { recursive: true });
|
|
123
176
|
} catch (error) {
|
|
124
|
-
if (error
|
|
177
|
+
if (!isDOMException(error, "NotFoundError")) {
|
|
125
178
|
console.warn(`[OPFSManager] Failed to remove directory ${prefix}-${projectId}:`, error);
|
|
126
179
|
}
|
|
127
180
|
}
|
|
128
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* List all projects with size and lastModified metadata
|
|
184
|
+
* Used for LRU eviction
|
|
185
|
+
*/
|
|
186
|
+
async listProjectsWithMetadata(prefix) {
|
|
187
|
+
if (!this.opfsRoot) {
|
|
188
|
+
throw new Error("[OPFSManager] Not initialized");
|
|
189
|
+
}
|
|
190
|
+
const projects = [];
|
|
191
|
+
const searchPrefix = `meframe-${prefix}-`;
|
|
192
|
+
for await (const [name, handle] of this.opfsRoot.entries()) {
|
|
193
|
+
if (handle.kind === "directory" && name.startsWith(searchPrefix)) {
|
|
194
|
+
const projectId = name.slice(searchPrefix.length);
|
|
195
|
+
const projectDir = handle;
|
|
196
|
+
let totalSize = 0;
|
|
197
|
+
let maxLastModified = 0;
|
|
198
|
+
for await (const [_fileName, fileHandle] of projectDir.entries()) {
|
|
199
|
+
if (fileHandle.kind === "file") {
|
|
200
|
+
const file = await fileHandle.getFile();
|
|
201
|
+
totalSize += file.size;
|
|
202
|
+
maxLastModified = Math.max(maxLastModified, file.lastModified);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (totalSize > 0) {
|
|
206
|
+
projects.push({ projectId, size: totalSize, lastModified: maxLastModified });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return projects;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Evict oldest projects (by lastModified) excluding current project
|
|
214
|
+
* Returns number of projects evicted
|
|
215
|
+
*/
|
|
216
|
+
async evictOldestProjects(currentProjectId, prefix, count) {
|
|
217
|
+
const projects = await this.listProjectsWithMetadata(prefix);
|
|
218
|
+
const candidates = projects.filter((p) => p.projectId !== currentProjectId).sort((a, b) => a.lastModified - b.lastModified).slice(0, count);
|
|
219
|
+
if (candidates.length === 0) {
|
|
220
|
+
return 0;
|
|
221
|
+
}
|
|
222
|
+
let freedBytes = 0;
|
|
223
|
+
for (const project of candidates) {
|
|
224
|
+
await this.deleteProjectDirectory(project.projectId, prefix);
|
|
225
|
+
freedBytes += project.size;
|
|
226
|
+
console.log(
|
|
227
|
+
`[OPFSManager] Evicted ${prefix} project ${project.projectId} (${(project.size / 1024 / 1024).toFixed(2)}MB, last modified: ${new Date(project.lastModified).toLocaleString()})`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
console.log(
|
|
231
|
+
`[OPFSManager] Freed ${(freedBytes / 1024 / 1024).toFixed(2)}MB by evicting ${candidates.length} old project(s)`
|
|
232
|
+
);
|
|
233
|
+
return candidates.length;
|
|
234
|
+
}
|
|
129
235
|
}
|
|
130
236
|
export {
|
|
131
237
|
OPFSManager
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OPFSManager.js","sources":["../../../../src/cache/storage/opfs/OPFSManager.ts"],"sourcesContent":["import type { OPFSPrefix, OPFSPath } from './types';\n\n/**\n * OPFSManager - Unified OPFS management infrastructure\n *\n * Supports:\n * - Directory isolation by prefix (l2/resource)\n * - Range reads for on-demand decoding\n * - Streaming writes\n * - File operations (exists, delete, getFileSize)\n */\nexport class OPFSManager {\n private opfsRoot: FileSystemDirectoryHandle | null = null;\n private initPromise: Promise<void> | null = null;\n\n async init(): Promise<void> {\n if (this.initPromise) return this.initPromise;\n\n this.initPromise = this.initStorage();\n return this.initPromise;\n }\n\n private async initStorage(): Promise<void> {\n this.opfsRoot = await navigator.storage.getDirectory();\n }\n\n /**\n * Get project directory by prefix\n */\n async getProjectDir(projectId: string, prefix: OPFSPrefix): Promise<FileSystemDirectoryHandle> {\n if (!this.opfsRoot) {\n throw new Error('[OPFSManager] Not initialized');\n }\n\n const dirName = `meframe-${prefix}-${projectId}`;\n return this.opfsRoot.getDirectoryHandle(dirName, { create: true });\n }\n\n /**\n * Write file (ArrayBuffer or ReadableStream)\n */\n async writeFile(path: OPFSPath, data: ArrayBuffer | ReadableStream<Uint8Array>): Promise<void> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: true });\n const writable = await fileHandle.createWritable();\n\n if (data instanceof ArrayBuffer) {\n await writable.write(data);\n } else {\n // Stream mode\n const reader = data.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n // Convert Uint8Array to ArrayBuffer for FileSystemWritableFileStream\n const buffer = value.buffer.slice(\n value.byteOffset,\n value.byteOffset + value.byteLength\n );\n await writable.write(buffer as ArrayBuffer);\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n\n await writable.close();\n }\n\n /**\n * Read entire file\n */\n async readFile(path: OPFSPath): Promise<ArrayBuffer> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: false });\n const file = await fileHandle.getFile();\n return await file.arrayBuffer();\n }\n\n /**\n * Read byte range from file (for on-demand decoding)\n */\n async readRange(path: OPFSPath, start: number, end: number): Promise<ArrayBuffer> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: false });\n const file = await fileHandle.getFile();\n const slice = file.slice(start, end);\n return await slice.arrayBuffer();\n }\n\n /**\n * Delete file\n */\n async deleteFile(path: OPFSPath): Promise<void> {\n try {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n await projectDir.removeEntry(path.fileName);\n } catch (error) {\n if ((error as any)?.name !== 'NotFoundError') {\n console.warn(`[OPFSManager] Failed to delete file ${path.fileName}:`, error);\n }\n }\n }\n\n /**\n * Check if file exists\n */\n async exists(path: OPFSPath): Promise<boolean> {\n try {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n await projectDir.getFileHandle(path.fileName, { create: false });\n return true;\n } catch (error) {\n if ((error as any)?.name === 'NotFoundError') {\n return false;\n }\n throw error;\n }\n }\n\n /**\n * Get file size\n */\n async getFileSize(path: OPFSPath): Promise<number> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: false });\n const file = await fileHandle.getFile();\n return file.size;\n }\n\n /**\n * Create writable stream for streaming writes\n */\n async createWritableStream(path: OPFSPath): Promise<FileSystemWritableFileStream> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: true });\n return await fileHandle.createWritable();\n }\n\n /**\n * Delete entire project directory\n */\n async deleteProjectDirectory(projectId: string, prefix: OPFSPrefix): Promise<void> {\n if (!this.opfsRoot) return;\n\n try {\n const dirName = `meframe-${prefix}-${projectId}`;\n await this.opfsRoot.removeEntry(dirName, { recursive: true });\n } catch (error) {\n if ((error as any)?.name !== 'NotFoundError') {\n console.warn(`[OPFSManager] Failed to remove directory ${prefix}-${projectId}:`, error);\n }\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OPFSManager.js","sources":["../../../../src/cache/storage/opfs/OPFSManager.ts"],"sourcesContent":["import type { OPFSPrefix, OPFSPath, ProjectMetadata } from './types';\nimport { OPFSQuotaExceededError } from '../../../utils/errors';\n\nfunction isDOMException(error: unknown, name: string): boolean {\n return error instanceof Error && 'name' in error && error.name === name;\n}\n\n/**\n * OPFSManager - Unified OPFS management infrastructure\n *\n * Supports:\n * - Directory isolation by prefix (l2/resource)\n * - Range reads for on-demand decoding\n * - Streaming writes with automatic quota management\n * - Project-level LRU eviction\n */\nexport class OPFSManager {\n private opfsRoot: FileSystemDirectoryHandle | null = null;\n private initPromise: Promise<void> | null = null;\n readonly maxSizeBytes: number;\n readonly quotaThreshold: number;\n\n constructor(maxSizeMB: number = 5120, quotaThresholdPercent: number = 0.85) {\n this.maxSizeBytes = maxSizeMB * 1024 * 1024;\n this.quotaThreshold = quotaThresholdPercent;\n }\n\n async init(): Promise<void> {\n if (this.initPromise) return this.initPromise;\n\n this.initPromise = this.initStorage();\n return this.initPromise;\n }\n\n private async initStorage(): Promise<void> {\n this.opfsRoot = await navigator.storage.getDirectory();\n }\n\n /**\n * Get project directory by prefix\n */\n async getProjectDir(projectId: string, prefix: OPFSPrefix): Promise<FileSystemDirectoryHandle> {\n if (!this.opfsRoot) {\n throw new Error('[OPFSManager] Not initialized');\n }\n\n const dirName = `meframe-${prefix}-${projectId}`;\n return this.opfsRoot.getDirectoryHandle(dirName, { create: true });\n }\n\n /**\n * Write file with automatic quota management\n * Proactively checks quota before write; evicts old projects if threshold exceeded\n */\n async writeFile(path: OPFSPath, data: ArrayBuffer | ReadableStream<Uint8Array>): Promise<void> {\n await this.ensureQuota(path.projectId, path.prefix);\n\n try {\n await this.writeFileInternal(path, data);\n } catch (error) {\n if (isDOMException(error, 'QuotaExceededError')) {\n const evictedCount = await this.evictOldestProjects(path.projectId, path.prefix, 1);\n\n if (evictedCount === 0) {\n throw new OPFSQuotaExceededError(path.projectId, path.prefix, false);\n }\n\n if (data instanceof ReadableStream) {\n throw new OPFSQuotaExceededError(path.projectId, path.prefix, true);\n }\n\n await this.writeFileInternal(path, data);\n } else {\n throw error;\n }\n }\n }\n\n /**\n * Ensure quota is within threshold before write\n * Proactively evicts old projects if usage exceeds threshold\n */\n private async ensureQuota(currentProjectId: string, prefix: OPFSPrefix): Promise<void> {\n const totalSize = await this.getTotalSize(prefix);\n const usagePercent = totalSize / this.maxSizeBytes;\n\n if (usagePercent > this.quotaThreshold) {\n const needToFreeBytes = totalSize - this.maxSizeBytes * this.quotaThreshold;\n const projectsToEvict = Math.ceil(needToFreeBytes / (totalSize / 10));\n\n console.log(\n `[OPFSManager] Quota usage ${(usagePercent * 100).toFixed(1)}% exceeds threshold ${(this.quotaThreshold * 100).toFixed(0)}%, evicting old projects`\n );\n\n await this.evictOldestProjects(currentProjectId, prefix, Math.max(1, projectsToEvict));\n }\n }\n\n /**\n * Get total size of all projects for a prefix\n */\n async getTotalSize(prefix: OPFSPrefix): Promise<number> {\n const projects = await this.listProjectsWithMetadata(prefix);\n return projects.reduce((sum, p) => sum + p.size, 0);\n }\n\n private async writeFileInternal(\n path: OPFSPath,\n data: ArrayBuffer | ReadableStream<Uint8Array>\n ): Promise<void> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: true });\n const writable = await fileHandle.createWritable();\n\n if (data instanceof ArrayBuffer) {\n await writable.write(data);\n } else {\n const reader = data.getReader();\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n const buffer = value.buffer.slice(\n value.byteOffset,\n value.byteOffset + value.byteLength\n );\n await writable.write(buffer as ArrayBuffer);\n }\n }\n } finally {\n reader.releaseLock();\n }\n }\n\n await writable.close();\n }\n\n /**\n * Read entire file\n */\n async readFile(path: OPFSPath): Promise<ArrayBuffer> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: false });\n const file = await fileHandle.getFile();\n return await file.arrayBuffer();\n }\n\n /**\n * Read byte range from file (for on-demand decoding)\n */\n async readRange(path: OPFSPath, start: number, end: number): Promise<ArrayBuffer> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: false });\n const file = await fileHandle.getFile();\n const slice = file.slice(start, end);\n return await slice.arrayBuffer();\n }\n\n /**\n * Delete file\n */\n async deleteFile(path: OPFSPath): Promise<void> {\n try {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n await projectDir.removeEntry(path.fileName);\n } catch (error) {\n if (!isDOMException(error, 'NotFoundError')) {\n console.warn(`[OPFSManager] Failed to delete file ${path.fileName}:`, error);\n }\n }\n }\n\n /**\n * Check if file exists\n */\n async exists(path: OPFSPath): Promise<boolean> {\n try {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n await projectDir.getFileHandle(path.fileName, { create: false });\n return true;\n } catch (error) {\n if (isDOMException(error, 'NotFoundError')) {\n return false;\n }\n throw error;\n }\n }\n\n /**\n * Get file size\n */\n async getFileSize(path: OPFSPath): Promise<number> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: false });\n const file = await fileHandle.getFile();\n return file.size;\n }\n\n /**\n * Create writable stream for streaming writes\n */\n async createWritableStream(path: OPFSPath): Promise<FileSystemWritableFileStream> {\n const projectDir = await this.getProjectDir(path.projectId, path.prefix);\n const fileHandle = await projectDir.getFileHandle(path.fileName, { create: true });\n return await fileHandle.createWritable();\n }\n\n /**\n * Delete entire project directory\n */\n async deleteProjectDirectory(projectId: string, prefix: OPFSPrefix): Promise<void> {\n if (!this.opfsRoot) return;\n\n try {\n const dirName = `meframe-${prefix}-${projectId}`;\n await this.opfsRoot.removeEntry(dirName, { recursive: true });\n } catch (error) {\n if (!isDOMException(error, 'NotFoundError')) {\n console.warn(`[OPFSManager] Failed to remove directory ${prefix}-${projectId}:`, error);\n }\n }\n }\n\n /**\n * List all projects with size and lastModified metadata\n * Used for LRU eviction\n */\n async listProjectsWithMetadata(prefix: OPFSPrefix): Promise<ProjectMetadata[]> {\n if (!this.opfsRoot) {\n throw new Error('[OPFSManager] Not initialized');\n }\n\n const projects: ProjectMetadata[] = [];\n const searchPrefix = `meframe-${prefix}-`;\n\n // @ts-expect-error - AsyncIterator type not well-supported\n for await (const [name, handle] of this.opfsRoot.entries()) {\n if (handle.kind === 'directory' && name.startsWith(searchPrefix)) {\n const projectId = name.slice(searchPrefix.length);\n const projectDir = handle as FileSystemDirectoryHandle;\n let totalSize = 0;\n let maxLastModified = 0;\n\n // @ts-expect-error - AsyncIterator type not well-supported\n for await (const [_fileName, fileHandle] of projectDir.entries()) {\n if (fileHandle.kind === 'file') {\n const file = await (fileHandle as FileSystemFileHandle).getFile();\n totalSize += file.size;\n maxLastModified = Math.max(maxLastModified, file.lastModified);\n }\n }\n\n if (totalSize > 0) {\n projects.push({ projectId, size: totalSize, lastModified: maxLastModified });\n }\n }\n }\n\n return projects;\n }\n\n /**\n * Evict oldest projects (by lastModified) excluding current project\n * Returns number of projects evicted\n */\n async evictOldestProjects(\n currentProjectId: string,\n prefix: OPFSPrefix,\n count: number\n ): Promise<number> {\n const projects = await this.listProjectsWithMetadata(prefix);\n const candidates = projects\n .filter((p) => p.projectId !== currentProjectId)\n .sort((a, b) => a.lastModified - b.lastModified)\n .slice(0, count);\n\n if (candidates.length === 0) {\n return 0;\n }\n\n let freedBytes = 0;\n\n for (const project of candidates) {\n await this.deleteProjectDirectory(project.projectId, prefix);\n freedBytes += project.size;\n\n console.log(\n `[OPFSManager] Evicted ${prefix} project ${project.projectId} ` +\n `(${(project.size / 1024 / 1024).toFixed(2)}MB, ` +\n `last modified: ${new Date(project.lastModified).toLocaleString()})`\n );\n }\n\n console.log(\n `[OPFSManager] Freed ${(freedBytes / 1024 / 1024).toFixed(2)}MB ` +\n `by evicting ${candidates.length} old project(s)`\n );\n\n return candidates.length;\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/cache/storage/opfs/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAEnD,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,UAAU,CAAC;AAE3C,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,KAAK,GAAG,OAAO,CAAC;CACvB"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/cache/storage/opfs/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAEnD,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,UAAU,CAAC;AAE3C,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,KAAK,GAAG,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACtB"}
|