@meframe/core 0.0.22 → 0.0.24
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/Meframe.d.ts +17 -0
- package/dist/Meframe.d.ts.map +1 -1
- package/dist/Meframe.js +20 -0
- package/dist/Meframe.js.map +1 -1
- package/dist/cache/CacheManager.d.ts +18 -1
- package/dist/cache/CacheManager.d.ts.map +1 -1
- package/dist/cache/CacheManager.js +19 -1
- package/dist/cache/CacheManager.js.map +1 -1
- package/dist/cache/index.d.ts +3 -3
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/l2/IndexedDBStore.d.ts +74 -0
- package/dist/cache/l2/IndexedDBStore.d.ts.map +1 -0
- package/dist/cache/l2/IndexedDBStore.js +180 -0
- package/dist/cache/l2/IndexedDBStore.js.map +1 -0
- package/dist/cache/{L2Cache.d.ts → l2/L2Cache.d.ts} +24 -14
- package/dist/cache/l2/L2Cache.d.ts.map +1 -0
- package/dist/cache/l2/L2Cache.js +329 -0
- package/dist/cache/l2/L2Cache.js.map +1 -0
- package/dist/cache/l2/OPFSStore.d.ts +46 -0
- package/dist/cache/l2/OPFSStore.d.ts.map +1 -0
- package/dist/cache/l2/OPFSStore.js +131 -0
- package/dist/cache/l2/OPFSStore.js.map +1 -0
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +1 -0
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/types.d.ts +3 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/model/validation.js +2 -2
- package/dist/model/validation.js.map +1 -1
- package/dist/orchestrator/Orchestrator.js +1 -1
- package/dist/orchestrator/Orchestrator.js.map +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts +1 -1
- package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
- package/dist/orchestrator/VideoClipSession.js +25 -36
- package/dist/orchestrator/VideoClipSession.js.map +1 -1
- package/dist/orchestrator/types.d.ts +1 -0
- package/dist/orchestrator/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/cache/BatchWriter.d.ts +0 -25
- package/dist/cache/BatchWriter.d.ts.map +0 -1
- package/dist/cache/CacheStatsDecorator.d.ts +0 -27
- package/dist/cache/CacheStatsDecorator.d.ts.map +0 -1
- package/dist/cache/L2Cache.d.ts.map +0 -1
- package/dist/cache/L2Cache.js +0 -488
- package/dist/cache/L2Cache.js.map +0 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
class IndexedDBStore {
|
|
2
|
+
db = null;
|
|
3
|
+
initPromise = null;
|
|
4
|
+
async init() {
|
|
5
|
+
if (this.initPromise) return this.initPromise;
|
|
6
|
+
this.initPromise = this.initStorage();
|
|
7
|
+
return this.initPromise;
|
|
8
|
+
}
|
|
9
|
+
async initStorage() {
|
|
10
|
+
const request = indexedDB.open("meframe_cache", 2);
|
|
11
|
+
request.onupgradeneeded = (event) => {
|
|
12
|
+
const db = event.target.result;
|
|
13
|
+
const oldVersion = event.oldVersion;
|
|
14
|
+
if (oldVersion < 2) {
|
|
15
|
+
if (db.objectStoreNames.contains("chunks")) {
|
|
16
|
+
db.deleteObjectStore("chunks");
|
|
17
|
+
}
|
|
18
|
+
const store = db.createObjectStore("chunks", {
|
|
19
|
+
keyPath: ["projectId", "clipId", "track"]
|
|
20
|
+
});
|
|
21
|
+
store.createIndex("lastAccess", "lastAccess");
|
|
22
|
+
store.createIndex("projectId", "projectId");
|
|
23
|
+
}
|
|
24
|
+
if (!db.objectStoreNames.contains("meta")) {
|
|
25
|
+
db.createObjectStore("meta", { keyPath: "projectId" });
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
this.db = await new Promise((resolve, reject) => {
|
|
29
|
+
request.onsuccess = () => resolve(request.result);
|
|
30
|
+
request.onerror = () => reject(request.error);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get a chunk record
|
|
35
|
+
*/
|
|
36
|
+
async getRecord(projectId, clipId, track) {
|
|
37
|
+
if (!this.db) return null;
|
|
38
|
+
const tx = this.db.transaction("chunks", "readonly");
|
|
39
|
+
const store = tx.objectStore("chunks");
|
|
40
|
+
const record = await this.promisifyRequest(store.get([projectId, clipId, track]));
|
|
41
|
+
return record || null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Put/update a chunk record
|
|
45
|
+
*/
|
|
46
|
+
async putRecord(record) {
|
|
47
|
+
if (!this.db) return;
|
|
48
|
+
const tx = this.db.transaction("chunks", "readwrite");
|
|
49
|
+
const store = tx.objectStore("chunks");
|
|
50
|
+
store.put(record);
|
|
51
|
+
await new Promise((resolve, reject) => {
|
|
52
|
+
tx.oncomplete = () => resolve();
|
|
53
|
+
tx.onerror = () => reject(tx.error);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Delete a chunk record
|
|
58
|
+
*/
|
|
59
|
+
async deleteRecord(projectId, clipId, track) {
|
|
60
|
+
if (!this.db) return;
|
|
61
|
+
const tx = this.db.transaction("chunks", "readwrite");
|
|
62
|
+
const store = tx.objectStore("chunks");
|
|
63
|
+
await this.promisifyRequest(store.delete([projectId, clipId, track]));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Update last access time for a record
|
|
67
|
+
*/
|
|
68
|
+
async updateLastAccess(projectId, clipId, track) {
|
|
69
|
+
if (!this.db) return;
|
|
70
|
+
const tx = this.db.transaction("chunks", "readwrite");
|
|
71
|
+
const store = tx.objectStore("chunks");
|
|
72
|
+
const record = await this.promisifyRequest(store.get([projectId, clipId, track]));
|
|
73
|
+
if (record) {
|
|
74
|
+
record.lastAccess = Date.now();
|
|
75
|
+
await this.promisifyRequest(store.put(record));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Collect all records for a specific clip
|
|
80
|
+
*/
|
|
81
|
+
async collectRecordsByClipId(projectId, clipId) {
|
|
82
|
+
if (!this.db) return [];
|
|
83
|
+
const tx = this.db.transaction("chunks", "readonly");
|
|
84
|
+
const store = tx.objectStore("chunks");
|
|
85
|
+
const records = [];
|
|
86
|
+
const cursor = store.openCursor();
|
|
87
|
+
await new Promise((resolve) => {
|
|
88
|
+
cursor.onsuccess = (event) => {
|
|
89
|
+
const cursor2 = event.target.result;
|
|
90
|
+
if (!cursor2) {
|
|
91
|
+
resolve();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const record = cursor2.value;
|
|
95
|
+
if (record.projectId === projectId && record.clipId === clipId) {
|
|
96
|
+
records.push(record);
|
|
97
|
+
}
|
|
98
|
+
cursor2.continue();
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
return records;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Get all records for a specific project
|
|
105
|
+
*/
|
|
106
|
+
async getRecordsByProjectId(projectId) {
|
|
107
|
+
if (!this.db) return [];
|
|
108
|
+
const tx = this.db.transaction("chunks", "readonly");
|
|
109
|
+
const store = tx.objectStore("chunks");
|
|
110
|
+
const index = store.index("projectId");
|
|
111
|
+
const records = await this.promisifyRequest(
|
|
112
|
+
index.getAll(IDBKeyRange.only(projectId))
|
|
113
|
+
);
|
|
114
|
+
return records;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get all records sorted by lastAccess (for quota enforcement)
|
|
118
|
+
*/
|
|
119
|
+
async getAllRecordsSortedByAccess() {
|
|
120
|
+
if (!this.db) return [];
|
|
121
|
+
const tx = this.db.transaction("chunks", "readonly");
|
|
122
|
+
const store = tx.objectStore("chunks");
|
|
123
|
+
const index = store.index("lastAccess");
|
|
124
|
+
const records = [];
|
|
125
|
+
const cursor = index.openCursor();
|
|
126
|
+
await new Promise((resolve) => {
|
|
127
|
+
cursor.onsuccess = (event) => {
|
|
128
|
+
const cursor2 = event.target.result;
|
|
129
|
+
if (!cursor2) {
|
|
130
|
+
resolve();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
records.push(cursor2.value);
|
|
134
|
+
cursor2.continue();
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
return records;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Get all records (for project statistics)
|
|
141
|
+
*/
|
|
142
|
+
async getAllRecords() {
|
|
143
|
+
if (!this.db) return [];
|
|
144
|
+
const tx = this.db.transaction("chunks", "readonly");
|
|
145
|
+
const store = tx.objectStore("chunks");
|
|
146
|
+
const records = [];
|
|
147
|
+
const cursor = store.openCursor();
|
|
148
|
+
await new Promise((resolve) => {
|
|
149
|
+
cursor.onsuccess = (event) => {
|
|
150
|
+
const cursor2 = event.target.result;
|
|
151
|
+
if (!cursor2) {
|
|
152
|
+
resolve();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
records.push(cursor2.value);
|
|
156
|
+
cursor2.continue();
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
return records;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Clear all data from chunks and meta stores
|
|
163
|
+
*/
|
|
164
|
+
async clear() {
|
|
165
|
+
if (!this.db) return;
|
|
166
|
+
const tx = this.db.transaction(["chunks", "meta"], "readwrite");
|
|
167
|
+
await this.promisifyRequest(tx.objectStore("chunks").clear());
|
|
168
|
+
await this.promisifyRequest(tx.objectStore("meta").clear());
|
|
169
|
+
}
|
|
170
|
+
promisifyRequest(request) {
|
|
171
|
+
return new Promise((resolve, reject) => {
|
|
172
|
+
request.onsuccess = () => resolve(request.result);
|
|
173
|
+
request.onerror = () => reject(request.error);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export {
|
|
178
|
+
IndexedDBStore
|
|
179
|
+
};
|
|
180
|
+
//# sourceMappingURL=IndexedDBStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"IndexedDBStore.js","sources":["../../../src/cache/l2/IndexedDBStore.ts"],"sourcesContent":["import type { ChunkBatch } from './OPFSStore';\n\nexport interface ChunkRecord {\n projectId: string;\n clipId: string;\n track: 'video' | 'audio';\n fileName: string;\n batches: ChunkBatch[];\n lastAccess: number;\n totalBytes: number;\n isComplete: boolean;\n expectedDurationUs?: number;\n metadata?: {\n codec?: string;\n description?: Uint8Array;\n codedWidth?: number;\n codedHeight?: number;\n displayAspectWidth?: number;\n displayAspectHeight?: number;\n colorSpace?: VideoColorSpaceInit;\n hardwareAcceleration?: HardwareAcceleration;\n optimizeForLatency?: boolean;\n sampleRate?: number;\n numberOfChannels?: number;\n };\n}\n\n/**\n * IndexedDB storage for L2 cache metadata\n * Handles all database operations for chunk records\n */\nexport class IndexedDBStore {\n private db: IDBDatabase | 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 // meframe_cache schema v1.1 (IndexedDB version 2)\n // - Added projectId support for multi-project isolation\n // - Composite key: [projectId, clipId, track]\n const request = indexedDB.open('meframe_cache', 2);\n\n request.onupgradeneeded = (event) => {\n const db = (event.target as IDBOpenDBRequest).result;\n const oldVersion = event.oldVersion;\n\n // Version 1→2: Add projectId support\n if (oldVersion < 2) {\n // Delete old chunks store if exists\n if (db.objectStoreNames.contains('chunks')) {\n db.deleteObjectStore('chunks');\n }\n\n // Create new chunks store with composite key [projectId, clipId, track]\n const store = db.createObjectStore('chunks', {\n keyPath: ['projectId', 'clipId', 'track'],\n });\n store.createIndex('lastAccess', 'lastAccess');\n store.createIndex('projectId', 'projectId'); // New: query by project\n }\n\n // meta store (unchanged)\n if (!db.objectStoreNames.contains('meta')) {\n db.createObjectStore('meta', { keyPath: 'projectId' });\n }\n };\n\n this.db = await new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n }\n\n /**\n * Get a chunk record\n */\n async getRecord(projectId: string, clipId: string, track: string): Promise<ChunkRecord | null> {\n if (!this.db) return null;\n\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n const record = await this.promisifyRequest<ChunkRecord>(store.get([projectId, clipId, track]));\n\n return record || null;\n }\n\n /**\n * Put/update a chunk record\n */\n async putRecord(record: ChunkRecord): Promise<void> {\n if (!this.db) return;\n\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n store.put(record);\n\n // Wait for transaction to complete\n await new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n });\n }\n\n /**\n * Delete a chunk record\n */\n async deleteRecord(projectId: string, clipId: string, track: string): Promise<void> {\n if (!this.db) return;\n\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n await this.promisifyRequest(store.delete([projectId, clipId, track]));\n }\n\n /**\n * Update last access time for a record\n */\n async updateLastAccess(projectId: string, clipId: string, track: string): Promise<void> {\n if (!this.db) return;\n\n const tx = this.db.transaction('chunks', 'readwrite');\n const store = tx.objectStore('chunks');\n const record = await this.promisifyRequest<ChunkRecord>(store.get([projectId, clipId, track]));\n\n if (record) {\n record.lastAccess = Date.now();\n await this.promisifyRequest(store.put(record));\n }\n }\n\n /**\n * Collect all records for a specific clip\n */\n async collectRecordsByClipId(projectId: string, clipId: string): Promise<ChunkRecord[]> {\n if (!this.db) return [];\n\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n const records: ChunkRecord[] = [];\n const cursor = store.openCursor();\n\n await new Promise<void>((resolve) => {\n cursor.onsuccess = (event) => {\n const cursor = (event.target as IDBRequest).result;\n if (!cursor) {\n resolve();\n return;\n }\n\n const record: ChunkRecord = cursor.value;\n if (record.projectId === projectId && record.clipId === clipId) {\n records.push(record);\n }\n\n cursor.continue();\n };\n });\n\n return records;\n }\n\n /**\n * Get all records for a specific project\n */\n async getRecordsByProjectId(projectId: string): Promise<ChunkRecord[]> {\n if (!this.db) return [];\n\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n const index = store.index('projectId');\n const records = await this.promisifyRequest<ChunkRecord[]>(\n index.getAll(IDBKeyRange.only(projectId))\n );\n\n return records;\n }\n\n /**\n * Get all records sorted by lastAccess (for quota enforcement)\n */\n async getAllRecordsSortedByAccess(): Promise<ChunkRecord[]> {\n if (!this.db) return [];\n\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n const index = store.index('lastAccess');\n const records: ChunkRecord[] = [];\n\n const cursor = index.openCursor();\n await new Promise<void>((resolve) => {\n cursor.onsuccess = (event) => {\n const cursor = (event.target as IDBRequest).result;\n if (!cursor) {\n resolve();\n return;\n }\n\n records.push(cursor.value);\n cursor.continue();\n };\n });\n\n return records;\n }\n\n /**\n * Get all records (for project statistics)\n */\n async getAllRecords(): Promise<ChunkRecord[]> {\n if (!this.db) return [];\n\n const tx = this.db.transaction('chunks', 'readonly');\n const store = tx.objectStore('chunks');\n const records: ChunkRecord[] = [];\n\n const cursor = store.openCursor();\n await new Promise<void>((resolve) => {\n cursor.onsuccess = (event) => {\n const cursor = (event.target as IDBRequest).result;\n if (!cursor) {\n resolve();\n return;\n }\n\n records.push(cursor.value);\n cursor.continue();\n };\n });\n\n return records;\n }\n\n /**\n * Clear all data from chunks and meta stores\n */\n async clear(): Promise<void> {\n if (!this.db) return;\n\n const tx = this.db.transaction(['chunks', 'meta'], 'readwrite');\n await this.promisifyRequest(tx.objectStore('chunks').clear());\n await this.promisifyRequest(tx.objectStore('meta').clear());\n }\n\n private promisifyRequest<T>(request: IDBRequest): Promise<T> {\n return new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result);\n request.onerror = () => reject(request.error);\n });\n }\n}\n"],"names":["cursor"],"mappings":"AA+BO,MAAM,eAAe;AAAA,EAClB,KAAyB;AAAA,EACzB,cAAoC;AAAA,EAE5C,MAAM,OAAsB;AAC1B,QAAI,KAAK,YAAa,QAAO,KAAK;AAElC,SAAK,cAAc,KAAK,YAAA;AACxB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,MAAc,cAA6B;AAIzC,UAAM,UAAU,UAAU,KAAK,iBAAiB,CAAC;AAEjD,YAAQ,kBAAkB,CAAC,UAAU;AACnC,YAAM,KAAM,MAAM,OAA4B;AAC9C,YAAM,aAAa,MAAM;AAGzB,UAAI,aAAa,GAAG;AAElB,YAAI,GAAG,iBAAiB,SAAS,QAAQ,GAAG;AAC1C,aAAG,kBAAkB,QAAQ;AAAA,QAC/B;AAGA,cAAM,QAAQ,GAAG,kBAAkB,UAAU;AAAA,UAC3C,SAAS,CAAC,aAAa,UAAU,OAAO;AAAA,QAAA,CACzC;AACD,cAAM,YAAY,cAAc,YAAY;AAC5C,cAAM,YAAY,aAAa,WAAW;AAAA,MAC5C;AAGA,UAAI,CAAC,GAAG,iBAAiB,SAAS,MAAM,GAAG;AACzC,WAAG,kBAAkB,QAAQ,EAAE,SAAS,aAAa;AAAA,MACvD;AAAA,IACF;AAEA,SAAK,KAAK,MAAM,IAAI,QAAQ,CAAC,SAAS,WAAW;AAC/C,cAAQ,YAAY,MAAM,QAAQ,QAAQ,MAAM;AAChD,cAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,IAC9C,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAU,WAAmB,QAAgB,OAA4C;AAC7F,QAAI,CAAC,KAAK,GAAI,QAAO;AAErB,UAAM,KAAK,KAAK,GAAG,YAAY,UAAU,UAAU;AACnD,UAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,UAAM,SAAS,MAAM,KAAK,iBAA8B,MAAM,IAAI,CAAC,WAAW,QAAQ,KAAK,CAAC,CAAC;AAE7F,WAAO,UAAU;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAU,QAAoC;AAClD,QAAI,CAAC,KAAK,GAAI;AAEd,UAAM,KAAK,KAAK,GAAG,YAAY,UAAU,WAAW;AACpD,UAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,UAAM,IAAI,MAAM;AAGhB,UAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,SAAG,aAAa,MAAM,QAAA;AACtB,SAAG,UAAU,MAAM,OAAO,GAAG,KAAK;AAAA,IACpC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAAa,WAAmB,QAAgB,OAA8B;AAClF,QAAI,CAAC,KAAK,GAAI;AAEd,UAAM,KAAK,KAAK,GAAG,YAAY,UAAU,WAAW;AACpD,UAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,UAAM,KAAK,iBAAiB,MAAM,OAAO,CAAC,WAAW,QAAQ,KAAK,CAAC,CAAC;AAAA,EACtE;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,iBAAiB,WAAmB,QAAgB,OAA8B;AACtF,QAAI,CAAC,KAAK,GAAI;AAEd,UAAM,KAAK,KAAK,GAAG,YAAY,UAAU,WAAW;AACpD,UAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,UAAM,SAAS,MAAM,KAAK,iBAA8B,MAAM,IAAI,CAAC,WAAW,QAAQ,KAAK,CAAC,CAAC;AAE7F,QAAI,QAAQ;AACV,aAAO,aAAa,KAAK,IAAA;AACzB,YAAM,KAAK,iBAAiB,MAAM,IAAI,MAAM,CAAC;AAAA,IAC/C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,uBAAuB,WAAmB,QAAwC;AACtF,QAAI,CAAC,KAAK,GAAI,QAAO,CAAA;AAErB,UAAM,KAAK,KAAK,GAAG,YAAY,UAAU,UAAU;AACnD,UAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,UAAM,UAAyB,CAAA;AAC/B,UAAM,SAAS,MAAM,WAAA;AAErB,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,aAAO,YAAY,CAAC,UAAU;AAC5B,cAAMA,UAAU,MAAM,OAAsB;AAC5C,YAAI,CAACA,SAAQ;AACX,kBAAA;AACA;AAAA,QACF;AAEA,cAAM,SAAsBA,QAAO;AACnC,YAAI,OAAO,cAAc,aAAa,OAAO,WAAW,QAAQ;AAC9D,kBAAQ,KAAK,MAAM;AAAA,QACrB;AAEAA,gBAAO,SAAA;AAAA,MACT;AAAA,IACF,CAAC;AAED,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,sBAAsB,WAA2C;AACrE,QAAI,CAAC,KAAK,GAAI,QAAO,CAAA;AAErB,UAAM,KAAK,KAAK,GAAG,YAAY,UAAU,UAAU;AACnD,UAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,UAAM,QAAQ,MAAM,MAAM,WAAW;AACrC,UAAM,UAAU,MAAM,KAAK;AAAA,MACzB,MAAM,OAAO,YAAY,KAAK,SAAS,CAAC;AAAA,IAAA;AAG1C,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,8BAAsD;AAC1D,QAAI,CAAC,KAAK,GAAI,QAAO,CAAA;AAErB,UAAM,KAAK,KAAK,GAAG,YAAY,UAAU,UAAU;AACnD,UAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,UAAM,QAAQ,MAAM,MAAM,YAAY;AACtC,UAAM,UAAyB,CAAA;AAE/B,UAAM,SAAS,MAAM,WAAA;AACrB,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,aAAO,YAAY,CAAC,UAAU;AAC5B,cAAMA,UAAU,MAAM,OAAsB;AAC5C,YAAI,CAACA,SAAQ;AACX,kBAAA;AACA;AAAA,QACF;AAEA,gBAAQ,KAAKA,QAAO,KAAK;AACzBA,gBAAO,SAAA;AAAA,MACT;AAAA,IACF,CAAC;AAED,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,gBAAwC;AAC5C,QAAI,CAAC,KAAK,GAAI,QAAO,CAAA;AAErB,UAAM,KAAK,KAAK,GAAG,YAAY,UAAU,UAAU;AACnD,UAAM,QAAQ,GAAG,YAAY,QAAQ;AACrC,UAAM,UAAyB,CAAA;AAE/B,UAAM,SAAS,MAAM,WAAA;AACrB,UAAM,IAAI,QAAc,CAAC,YAAY;AACnC,aAAO,YAAY,CAAC,UAAU;AAC5B,cAAMA,UAAU,MAAM,OAAsB;AAC5C,YAAI,CAACA,SAAQ;AACX,kBAAA;AACA;AAAA,QACF;AAEA,gBAAQ,KAAKA,QAAO,KAAK;AACzBA,gBAAO,SAAA;AAAA,MACT;AAAA,IACF,CAAC;AAED,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAuB;AAC3B,QAAI,CAAC,KAAK,GAAI;AAEd,UAAM,KAAK,KAAK,GAAG,YAAY,CAAC,UAAU,MAAM,GAAG,WAAW;AAC9D,UAAM,KAAK,iBAAiB,GAAG,YAAY,QAAQ,EAAE,OAAO;AAC5D,UAAM,KAAK,iBAAiB,GAAG,YAAY,MAAM,EAAE,OAAO;AAAA,EAC5D;AAAA,EAEQ,iBAAoB,SAAiC;AAC3D,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,cAAQ,YAAY,MAAM,QAAQ,QAAQ,MAAM;AAChD,cAAQ,UAAU,MAAM,OAAO,QAAQ,KAAK;AAAA,IAC9C,CAAC;AAAA,EACH;AACF;"}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
import { TimeUs } from '
|
|
1
|
+
import { TimeUs } from '../../model/types';
|
|
2
2
|
|
|
3
3
|
interface L2Config {
|
|
4
4
|
maxSizeMB: number;
|
|
5
5
|
projectId: string;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* L2 Cache - High-level cache coordinator
|
|
9
|
+
* Uses OPFSStore for file storage and IndexedDBStore for metadata
|
|
10
|
+
*/
|
|
7
11
|
export declare class L2Cache {
|
|
8
|
-
private
|
|
9
|
-
private
|
|
12
|
+
private readonly opfsStore;
|
|
13
|
+
private readonly dbStore;
|
|
10
14
|
readonly maxSize: number;
|
|
11
15
|
readonly projectId: string;
|
|
12
16
|
private initPromise;
|
|
@@ -38,20 +42,9 @@ export declare class L2Cache {
|
|
|
38
42
|
*/
|
|
39
43
|
createReadStream(clipId: string, track: 'video' | 'audio'): Promise<ReadableStream<EncodedVideoChunk | EncodedAudioChunk> | null>;
|
|
40
44
|
clear(): Promise<void>;
|
|
41
|
-
private initStorage;
|
|
42
|
-
private readFromOPFS;
|
|
43
|
-
/**
|
|
44
|
-
* Append chunks to OPFS file (or create new file)
|
|
45
|
-
* Supports incremental writing for streaming scenarios
|
|
46
|
-
*/
|
|
47
|
-
private appendToOPFS;
|
|
48
|
-
private chunkToArrayBuffer;
|
|
49
45
|
private createChunk;
|
|
50
|
-
private updateLastAccess;
|
|
51
46
|
private deleteEntry;
|
|
52
47
|
private enforceQuota;
|
|
53
|
-
private collectRecords;
|
|
54
|
-
private promisifyRequest;
|
|
55
48
|
getMetadata(): {
|
|
56
49
|
maxSizeMB: number;
|
|
57
50
|
usedSizeMB: number;
|
|
@@ -63,6 +56,23 @@ export declare class L2Cache {
|
|
|
63
56
|
* Get chunk metadata (decoderConfig) for a specific clip
|
|
64
57
|
*/
|
|
65
58
|
getClipMetadata(clipId: string, track: 'video' | 'audio'): Promise<any | null>;
|
|
59
|
+
/**
|
|
60
|
+
* List all cached projects
|
|
61
|
+
*/
|
|
62
|
+
listProjects(): Promise<Array<{
|
|
63
|
+
projectId: string;
|
|
64
|
+
totalBytes: number;
|
|
65
|
+
clipCount: number;
|
|
66
|
+
lastAccess: number;
|
|
67
|
+
}>>;
|
|
68
|
+
/**
|
|
69
|
+
* Clear all cache data for a specific project
|
|
70
|
+
*/
|
|
71
|
+
clearProject(targetProjectId: string): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Get cache size for a specific project
|
|
74
|
+
*/
|
|
75
|
+
getProjectSize(projectId: string): Promise<number>;
|
|
66
76
|
}
|
|
67
77
|
export {};
|
|
68
78
|
//# sourceMappingURL=L2Cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"L2Cache.d.ts","sourceRoot":"","sources":["../../../src/cache/l2/L2Cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { binarySearchRange } from "../../utils/binary-search.js";
|
|
2
|
+
import { OPFSStore } from "./OPFSStore.js";
|
|
3
|
+
import { IndexedDBStore } from "./IndexedDBStore.js";
|
|
4
|
+
class L2Cache {
|
|
5
|
+
opfsStore;
|
|
6
|
+
dbStore;
|
|
7
|
+
maxSize;
|
|
8
|
+
projectId;
|
|
9
|
+
initPromise = null;
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.maxSize = config.maxSizeMB * 1024 * 1024;
|
|
12
|
+
this.projectId = config.projectId;
|
|
13
|
+
this.opfsStore = new OPFSStore();
|
|
14
|
+
this.dbStore = new IndexedDBStore();
|
|
15
|
+
}
|
|
16
|
+
async init() {
|
|
17
|
+
if (this.initPromise) return this.initPromise;
|
|
18
|
+
this.initPromise = (async () => {
|
|
19
|
+
await Promise.all([this.opfsStore.init(), this.dbStore.init()]);
|
|
20
|
+
})();
|
|
21
|
+
return this.initPromise;
|
|
22
|
+
}
|
|
23
|
+
async get(timeUs, clipId) {
|
|
24
|
+
await this.init();
|
|
25
|
+
const records = await this.dbStore.collectRecordsByClipId(this.projectId, clipId);
|
|
26
|
+
for (const record of records) {
|
|
27
|
+
const batch = binarySearchRange(record.batches, timeUs, (b) => ({
|
|
28
|
+
start: b.startUs,
|
|
29
|
+
end: b.startUs + b.durationUs
|
|
30
|
+
}));
|
|
31
|
+
if (!batch) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const chunkData = await this.opfsStore.read(record.fileName, batch, this.projectId);
|
|
35
|
+
if (!chunkData) continue;
|
|
36
|
+
await this.dbStore.updateLastAccess(this.projectId, record.clipId, record.track);
|
|
37
|
+
return this.createChunk(chunkData, timeUs, record.track, batch.type, batch.durationUs);
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
async put(clipId, chunks, track, options) {
|
|
42
|
+
await this.init();
|
|
43
|
+
if (chunks.length === 0) return;
|
|
44
|
+
const fileName = `clip-${clipId}-${track[0]}1.${track === "video" ? "webm" : "m4a"}`;
|
|
45
|
+
let existingRecord = await this.dbStore.getRecord(this.projectId, clipId, track);
|
|
46
|
+
if (existingRecord) {
|
|
47
|
+
const fileExists = await this.opfsStore.fileExists(existingRecord.fileName, this.projectId);
|
|
48
|
+
if (!fileExists) {
|
|
49
|
+
await this.deleteEntry(clipId, track);
|
|
50
|
+
existingRecord = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
let chunksToWrite = chunks;
|
|
54
|
+
if (existingRecord && existingRecord.batches.length > 0) {
|
|
55
|
+
const lastBatch = existingRecord.batches[existingRecord.batches.length - 1];
|
|
56
|
+
if (lastBatch) {
|
|
57
|
+
const lastTimestamp = lastBatch.startUs;
|
|
58
|
+
chunksToWrite = chunks.filter((chunk) => chunk.timestamp > lastTimestamp);
|
|
59
|
+
if (chunksToWrite.length === 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const newBatches = await this.opfsStore.append(
|
|
65
|
+
fileName,
|
|
66
|
+
chunksToWrite,
|
|
67
|
+
existingRecord?.batches,
|
|
68
|
+
this.projectId
|
|
69
|
+
);
|
|
70
|
+
const record = {
|
|
71
|
+
projectId: this.projectId,
|
|
72
|
+
clipId,
|
|
73
|
+
track,
|
|
74
|
+
fileName,
|
|
75
|
+
batches: existingRecord?.batches ? [...existingRecord.batches, ...newBatches] : newBatches,
|
|
76
|
+
lastAccess: Date.now(),
|
|
77
|
+
totalBytes: (existingRecord?.totalBytes || 0) + newBatches.reduce((sum, b) => sum + b.byteLength, 0),
|
|
78
|
+
isComplete: options?.isComplete ?? existingRecord?.isComplete ?? false,
|
|
79
|
+
expectedDurationUs: options?.expectedDurationUs ?? existingRecord?.expectedDurationUs,
|
|
80
|
+
metadata: options?.metadata ?? existingRecord?.metadata
|
|
81
|
+
};
|
|
82
|
+
await this.dbStore.putRecord(record);
|
|
83
|
+
await this.enforceQuota();
|
|
84
|
+
}
|
|
85
|
+
async invalidateRange(startUs, endUs, clipId) {
|
|
86
|
+
await this.init();
|
|
87
|
+
let records;
|
|
88
|
+
if (clipId) {
|
|
89
|
+
records = await this.dbStore.collectRecordsByClipId(this.projectId, clipId);
|
|
90
|
+
} else {
|
|
91
|
+
const allRecords = await this.dbStore.getAllRecords();
|
|
92
|
+
records = allRecords.filter((r) => r.projectId === this.projectId);
|
|
93
|
+
}
|
|
94
|
+
for (const record of records) {
|
|
95
|
+
const hasOverlap = record.batches.some((batch) => {
|
|
96
|
+
const batchEnd = batch.startUs + batch.durationUs;
|
|
97
|
+
return batch.startUs < endUs && batchEnd > startUs;
|
|
98
|
+
});
|
|
99
|
+
if (hasOverlap) {
|
|
100
|
+
await this.deleteEntry(record.clipId, record.track);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Check if clip has cached data in L2
|
|
106
|
+
*/
|
|
107
|
+
async hasClip(clipId, track) {
|
|
108
|
+
await this.init();
|
|
109
|
+
const record = await this.dbStore.getRecord(this.projectId, clipId, track);
|
|
110
|
+
return record !== null && record.batches && record.batches.length > 0;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Check if clip has complete cached data in L2
|
|
114
|
+
*/
|
|
115
|
+
async hasCompleteClip(clipId, track) {
|
|
116
|
+
await this.init();
|
|
117
|
+
const record = await this.dbStore.getRecord(this.projectId, clipId, track);
|
|
118
|
+
return record?.isComplete === true;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Mark clip as complete in L2 cache
|
|
122
|
+
*/
|
|
123
|
+
async markComplete(clipId, track) {
|
|
124
|
+
await this.init();
|
|
125
|
+
const record = await this.dbStore.getRecord(this.projectId, clipId, track);
|
|
126
|
+
if (record) {
|
|
127
|
+
record.isComplete = true;
|
|
128
|
+
record.lastAccess = Date.now();
|
|
129
|
+
await this.dbStore.putRecord(record);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async invalidateClip(clipId) {
|
|
133
|
+
await this.init();
|
|
134
|
+
const records = await this.dbStore.collectRecordsByClipId(this.projectId, clipId);
|
|
135
|
+
for (const record of records) {
|
|
136
|
+
await this.deleteEntry(record.clipId, record.track);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Create a readable stream of encoded chunks for export
|
|
141
|
+
* Reads chunks in timestamp order from OPFS
|
|
142
|
+
*/
|
|
143
|
+
async createReadStream(clipId, track) {
|
|
144
|
+
await this.init();
|
|
145
|
+
const record = await this.dbStore.getRecord(this.projectId, clipId, track);
|
|
146
|
+
if (!record || record.batches.length === 0) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const batches = [...record.batches];
|
|
150
|
+
let batchIndex = 0;
|
|
151
|
+
return new ReadableStream({
|
|
152
|
+
pull: async (controller) => {
|
|
153
|
+
if (batchIndex >= batches.length) {
|
|
154
|
+
controller.close();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const batch = batches[batchIndex];
|
|
158
|
+
if (!batch) {
|
|
159
|
+
controller.close();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const chunkData = await this.opfsStore.read(record.fileName, batch, this.projectId);
|
|
164
|
+
if (!chunkData) {
|
|
165
|
+
controller.close();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const chunk = this.createChunk(
|
|
169
|
+
chunkData,
|
|
170
|
+
batch.startUs,
|
|
171
|
+
track,
|
|
172
|
+
batch.type,
|
|
173
|
+
batch.durationUs
|
|
174
|
+
);
|
|
175
|
+
controller.enqueue(chunk);
|
|
176
|
+
batchIndex++;
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error instanceof DOMException && error.name === "NotFoundError") {
|
|
179
|
+
controller.close();
|
|
180
|
+
} else {
|
|
181
|
+
controller.error(error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
async clear() {
|
|
188
|
+
await this.init();
|
|
189
|
+
try {
|
|
190
|
+
await this.dbStore.clear();
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error("[L2Cache] Failed to clear IndexedDB:", error);
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
await this.opfsStore.clear(this.projectId);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (error?.name !== "NotFoundError") {
|
|
199
|
+
console.warn("[L2Cache] Failed to clear OPFS:", error);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
createChunk(data, timeUs, track, chunkType = "key", durationUs = 0) {
|
|
204
|
+
if (track === "video") {
|
|
205
|
+
return new EncodedVideoChunk({
|
|
206
|
+
type: chunkType,
|
|
207
|
+
timestamp: timeUs,
|
|
208
|
+
duration: durationUs,
|
|
209
|
+
data
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
212
|
+
return new EncodedAudioChunk({
|
|
213
|
+
type: chunkType,
|
|
214
|
+
timestamp: timeUs,
|
|
215
|
+
duration: durationUs,
|
|
216
|
+
data
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async deleteEntry(clipId, track, projectId) {
|
|
221
|
+
const targetProjectId = projectId ?? this.projectId;
|
|
222
|
+
const record = await this.dbStore.getRecord(targetProjectId, clipId, track);
|
|
223
|
+
if (record) {
|
|
224
|
+
await this.opfsStore.deleteFile(record.fileName, targetProjectId);
|
|
225
|
+
}
|
|
226
|
+
await this.dbStore.deleteRecord(targetProjectId, clipId, track);
|
|
227
|
+
}
|
|
228
|
+
async enforceQuota() {
|
|
229
|
+
const estimate = await navigator.storage.estimate();
|
|
230
|
+
const usage = estimate.usage || 0;
|
|
231
|
+
if (usage <= this.maxSize) return;
|
|
232
|
+
console.warn(
|
|
233
|
+
`[L2Cache] Quota exceeded! Deleting oldest entries: usage=${usage}, maxSize=${this.maxSize}`
|
|
234
|
+
);
|
|
235
|
+
const toDelete = usage - this.maxSize;
|
|
236
|
+
let bytesDeleted = 0;
|
|
237
|
+
const records = await this.dbStore.getAllRecordsSortedByAccess();
|
|
238
|
+
for (const record of records) {
|
|
239
|
+
if (bytesDeleted >= toDelete) break;
|
|
240
|
+
if (record.projectId !== this.projectId) {
|
|
241
|
+
await this.deleteEntry(record.clipId, record.track, record.projectId);
|
|
242
|
+
bytesDeleted += record.totalBytes;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (bytesDeleted < toDelete) {
|
|
246
|
+
for (const record of records) {
|
|
247
|
+
if (bytesDeleted >= toDelete) break;
|
|
248
|
+
if (record.projectId === this.projectId) {
|
|
249
|
+
await this.deleteEntry(record.clipId, record.track);
|
|
250
|
+
bytesDeleted += record.totalBytes;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
getMetadata() {
|
|
256
|
+
return {
|
|
257
|
+
maxSizeMB: this.maxSize / (1024 * 1024),
|
|
258
|
+
usedSizeMB: 0,
|
|
259
|
+
// Would need to track actual usage
|
|
260
|
+
entries: 0,
|
|
261
|
+
// Would need to track actual entries
|
|
262
|
+
hitRate: 0
|
|
263
|
+
// Would need to track hits and misses
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
async hasAvailableQuota(sizeMB) {
|
|
267
|
+
if (typeof navigator === "undefined" || !navigator.storage?.estimate) {
|
|
268
|
+
throw new Error("Storage API not available");
|
|
269
|
+
}
|
|
270
|
+
const estimate = await navigator.storage.estimate();
|
|
271
|
+
const availableMB = ((estimate.quota || 0) - (estimate.usage || 0)) / (1024 * 1024);
|
|
272
|
+
return availableMB >= sizeMB;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Get chunk metadata (decoderConfig) for a specific clip
|
|
276
|
+
*/
|
|
277
|
+
async getClipMetadata(clipId, track) {
|
|
278
|
+
await this.init();
|
|
279
|
+
const record = await this.dbStore.getRecord(this.projectId, clipId, track);
|
|
280
|
+
return record?.metadata || null;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* List all cached projects
|
|
284
|
+
*/
|
|
285
|
+
async listProjects() {
|
|
286
|
+
await this.init();
|
|
287
|
+
const records = await this.dbStore.getAllRecords();
|
|
288
|
+
const projects = /* @__PURE__ */ new Map();
|
|
289
|
+
for (const record of records) {
|
|
290
|
+
const existing = projects.get(record.projectId) || {
|
|
291
|
+
totalBytes: 0,
|
|
292
|
+
clipCount: 0,
|
|
293
|
+
lastAccess: 0
|
|
294
|
+
};
|
|
295
|
+
projects.set(record.projectId, {
|
|
296
|
+
totalBytes: existing.totalBytes + record.totalBytes,
|
|
297
|
+
clipCount: existing.clipCount + 1,
|
|
298
|
+
lastAccess: Math.max(existing.lastAccess, record.lastAccess)
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
return Array.from(projects.entries()).map(([projectId, stats]) => ({
|
|
302
|
+
projectId,
|
|
303
|
+
...stats
|
|
304
|
+
}));
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Clear all cache data for a specific project
|
|
308
|
+
*/
|
|
309
|
+
async clearProject(targetProjectId) {
|
|
310
|
+
await this.init();
|
|
311
|
+
const records = await this.dbStore.getRecordsByProjectId(targetProjectId);
|
|
312
|
+
for (const record of records) {
|
|
313
|
+
await this.deleteEntry(record.clipId, record.track, targetProjectId);
|
|
314
|
+
}
|
|
315
|
+
await this.opfsStore.deleteProjectDirectory(targetProjectId);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Get cache size for a specific project
|
|
319
|
+
*/
|
|
320
|
+
async getProjectSize(projectId) {
|
|
321
|
+
await this.init();
|
|
322
|
+
const records = await this.dbStore.getRecordsByProjectId(projectId);
|
|
323
|
+
return records.reduce((sum, r) => sum + r.totalBytes, 0);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
export {
|
|
327
|
+
L2Cache
|
|
328
|
+
};
|
|
329
|
+
//# sourceMappingURL=L2Cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"L2Cache.js","sources":["../../../src/cache/l2/L2Cache.ts"],"sourcesContent":["import type { TimeUs } from '../../model/types';\nimport { binarySearchRange } from '../../utils/binary-search';\nimport { OPFSStore } from './OPFSStore';\nimport { IndexedDBStore, type ChunkRecord } from './IndexedDBStore';\n\ninterface L2Config {\n maxSizeMB: number;\n projectId: string;\n}\n\n/**\n * L2 Cache - High-level cache coordinator\n * Uses OPFSStore for file storage and IndexedDBStore for metadata\n */\nexport class L2Cache {\n private readonly opfsStore: OPFSStore;\n private readonly dbStore: IndexedDBStore;\n readonly maxSize: number;\n readonly projectId: string;\n private initPromise: Promise<void> | null = null;\n\n constructor(config: L2Config) {\n this.maxSize = config.maxSizeMB * 1024 * 1024;\n this.projectId = config.projectId;\n this.opfsStore = new OPFSStore();\n this.dbStore = new IndexedDBStore();\n }\n\n async init(): Promise<void> {\n if (this.initPromise) return this.initPromise;\n\n this.initPromise = (async () => {\n await Promise.all([this.opfsStore.init(), this.dbStore.init()]);\n })();\n\n return this.initPromise;\n }\n\n async get(timeUs: TimeUs, clipId: string): Promise<EncodedVideoChunk | EncodedAudioChunk | null> {\n await this.init();\n\n // Query IndexedDB for chunk metadata\n const records = await this.dbStore.collectRecordsByClipId(this.projectId, clipId);\n\n for (const record of records) {\n const batch = binarySearchRange(record.batches, timeUs, (b) => ({\n start: b.startUs,\n end: b.startUs + b.durationUs,\n }));\n\n if (!batch) {\n continue;\n }\n\n const chunkData = await this.opfsStore.read(record.fileName, batch, this.projectId);\n if (!chunkData) continue;\n\n await this.dbStore.updateLastAccess(this.projectId, record.clipId, record.track);\n\n return this.createChunk(chunkData, timeUs, record.track, batch.type, batch.durationUs);\n }\n\n return null;\n }\n\n async put(\n clipId: string,\n chunks: Array<EncodedVideoChunk | EncodedAudioChunk>,\n track: 'video' | 'audio',\n options?: {\n isComplete?: boolean;\n expectedDurationUs?: number;\n metadata?: any;\n }\n ): Promise<void> {\n await this.init();\n\n if (chunks.length === 0) return;\n\n const fileName = `clip-${clipId}-${track[0]}1.${track === 'video' ? 'webm' : 'm4a'}`;\n\n // Step 1: Read existing record\n let existingRecord = await this.dbStore.getRecord(this.projectId, clipId, track);\n\n // Step 2: Validate consistency - if IndexedDB has record but OPFS file missing, delete the record\n if (existingRecord) {\n const fileExists = await this.opfsStore.fileExists(existingRecord.fileName, this.projectId);\n if (!fileExists) {\n await this.deleteEntry(clipId, track);\n existingRecord = null;\n }\n }\n\n // Step 3: Deduplicate based on timestamp\n let chunksToWrite = chunks;\n if (existingRecord && existingRecord.batches.length > 0) {\n const lastBatch = existingRecord.batches[existingRecord.batches.length - 1];\n if (lastBatch) {\n const lastTimestamp = lastBatch.startUs;\n // Filter out chunks with timestamp <= lastTimestamp\n chunksToWrite = chunks.filter((chunk) => chunk.timestamp > lastTimestamp);\n\n if (chunksToWrite.length === 0) {\n return;\n }\n }\n }\n\n // Step 4: Write to OPFS\n const newBatches = await this.opfsStore.append(\n fileName,\n chunksToWrite,\n existingRecord?.batches,\n this.projectId\n );\n\n // Step 5: Update IndexedDB\n const record: ChunkRecord = {\n projectId: this.projectId,\n clipId,\n track,\n fileName,\n batches: existingRecord?.batches ? [...existingRecord.batches, ...newBatches] : newBatches,\n lastAccess: Date.now(),\n totalBytes:\n (existingRecord?.totalBytes || 0) + newBatches.reduce((sum, b) => sum + b.byteLength, 0),\n isComplete: options?.isComplete ?? existingRecord?.isComplete ?? false,\n expectedDurationUs: options?.expectedDurationUs ?? existingRecord?.expectedDurationUs,\n metadata: options?.metadata ?? existingRecord?.metadata,\n };\n\n await this.dbStore.putRecord(record);\n\n // Check and enforce quota\n await this.enforceQuota();\n }\n\n async invalidateRange(startUs: TimeUs, endUs: TimeUs, clipId?: string): Promise<void> {\n await this.init();\n\n // Get records to check\n let records: ChunkRecord[];\n if (clipId) {\n records = await this.dbStore.collectRecordsByClipId(this.projectId, clipId);\n } else {\n // Need all records for current project\n const allRecords = await this.dbStore.getAllRecords();\n records = allRecords.filter((r) => r.projectId === this.projectId);\n }\n\n // Check overlaps and delete\n for (const record of records) {\n const hasOverlap = record.batches.some((batch) => {\n const batchEnd = batch.startUs + batch.durationUs;\n return batch.startUs < endUs && batchEnd > startUs;\n });\n\n if (hasOverlap) {\n await this.deleteEntry(record.clipId, record.track);\n }\n }\n }\n\n /**\n * Check if clip has cached data in L2\n */\n async hasClip(clipId: string, track: 'video' | 'audio'): Promise<boolean> {\n await this.init();\n\n const record = await this.dbStore.getRecord(this.projectId, clipId, track);\n return record !== null && record.batches && record.batches.length > 0;\n }\n\n /**\n * Check if clip has complete cached data in L2\n */\n async hasCompleteClip(clipId: string, track: 'video' | 'audio'): Promise<boolean> {\n await this.init();\n\n const record = await this.dbStore.getRecord(this.projectId, clipId, track);\n return record?.isComplete === true;\n }\n\n /**\n * Mark clip as complete in L2 cache\n */\n async markComplete(clipId: string, track: 'video' | 'audio'): Promise<void> {\n await this.init();\n\n const record = await this.dbStore.getRecord(this.projectId, clipId, track);\n if (record) {\n record.isComplete = true;\n record.lastAccess = Date.now();\n await this.dbStore.putRecord(record);\n }\n }\n\n async invalidateClip(clipId: string): Promise<void> {\n await this.init();\n\n // Collect records to delete\n const records = await this.dbStore.collectRecordsByClipId(this.projectId, clipId);\n\n // Delete each record\n for (const record of records) {\n await this.deleteEntry(record.clipId, record.track);\n }\n }\n\n /**\n * Create a readable stream of encoded chunks for export\n * Reads chunks in timestamp order from OPFS\n */\n async createReadStream(\n clipId: string,\n track: 'video' | 'audio'\n ): Promise<ReadableStream<EncodedVideoChunk | EncodedAudioChunk> | null> {\n await this.init();\n\n // Get chunk record\n const record = await this.dbStore.getRecord(this.projectId, clipId, track);\n\n if (!record || record.batches.length === 0) {\n return null;\n }\n\n // Clone batches array for stream iteration\n const batches = [...record.batches];\n let batchIndex = 0;\n\n return new ReadableStream<EncodedVideoChunk | EncodedAudioChunk>({\n pull: async (controller) => {\n if (batchIndex >= batches.length) {\n controller.close();\n return;\n }\n\n const batch = batches[batchIndex];\n if (!batch) {\n controller.close();\n return;\n }\n\n try {\n // Read chunk data from OPFS\n const chunkData = await this.opfsStore.read(record.fileName, batch, this.projectId);\n if (!chunkData) {\n controller.close();\n return;\n }\n\n // Create encoded chunk with correct type and duration\n const chunk = this.createChunk(\n chunkData,\n batch.startUs,\n track,\n batch.type,\n batch.durationUs\n );\n controller.enqueue(chunk);\n\n batchIndex++;\n } catch (error) {\n // File not found or read error - close stream gracefully\n if (error instanceof DOMException && error.name === 'NotFoundError') {\n controller.close();\n } else {\n controller.error(error);\n }\n }\n },\n });\n }\n\n async clear(): Promise<void> {\n await this.init();\n\n // Clear IndexedDB\n try {\n await this.dbStore.clear();\n } catch (error) {\n console.error('[L2Cache] Failed to clear IndexedDB:', error);\n throw error;\n }\n\n // Clear OPFS files\n try {\n await this.opfsStore.clear(this.projectId);\n } catch (error) {\n if ((error as any)?.name !== 'NotFoundError') {\n console.warn('[L2Cache] Failed to clear OPFS:', error);\n }\n }\n }\n\n private createChunk(\n data: ArrayBuffer,\n timeUs: TimeUs,\n track: 'video' | 'audio',\n chunkType: 'key' | 'delta' = 'key',\n durationUs: TimeUs = 0\n ): EncodedVideoChunk | EncodedAudioChunk {\n if (track === 'video') {\n return new EncodedVideoChunk({\n type: chunkType,\n timestamp: timeUs,\n duration: durationUs,\n data,\n });\n } else {\n return new EncodedAudioChunk({\n type: chunkType,\n timestamp: timeUs,\n duration: durationUs,\n data,\n });\n }\n }\n\n private async deleteEntry(clipId: string, track: string, projectId?: string): Promise<void> {\n const targetProjectId = projectId ?? this.projectId;\n\n // Step 1: Get record info\n const record = await this.dbStore.getRecord(targetProjectId, clipId, track);\n\n // Step 2: Delete OPFS file\n if (record) {\n await this.opfsStore.deleteFile(record.fileName, targetProjectId);\n }\n\n // Step 3: Delete IndexedDB record\n await this.dbStore.deleteRecord(targetProjectId, clipId, track);\n }\n\n private async enforceQuota(): Promise<void> {\n const estimate = await navigator.storage.estimate();\n const usage = estimate.usage || 0;\n\n if (usage <= this.maxSize) return;\n\n console.warn(\n `[L2Cache] Quota exceeded! Deleting oldest entries: usage=${usage}, maxSize=${this.maxSize}`\n );\n\n const toDelete = usage - this.maxSize;\n let bytesDeleted = 0;\n\n // Get all records sorted by lastAccess\n const records = await this.dbStore.getAllRecordsSortedByAccess();\n\n // Step 1: Delete oldest entries from OTHER projects (protect current project)\n for (const record of records) {\n if (bytesDeleted >= toDelete) break;\n\n if (record.projectId !== this.projectId) {\n await this.deleteEntry(record.clipId, record.track, record.projectId);\n bytesDeleted += record.totalBytes;\n }\n }\n\n // Step 2: If still over quota, delete oldest from current project\n if (bytesDeleted < toDelete) {\n for (const record of records) {\n if (bytesDeleted >= toDelete) break;\n\n if (record.projectId === this.projectId) {\n await this.deleteEntry(record.clipId, record.track);\n bytesDeleted += record.totalBytes;\n }\n }\n }\n }\n\n getMetadata(): {\n maxSizeMB: number;\n usedSizeMB: number;\n entries: number;\n hitRate: number;\n } {\n // This is a simplified implementation\n // In a real implementation, we would track actual usage\n return {\n maxSizeMB: this.maxSize / (1024 * 1024),\n usedSizeMB: 0, // Would need to track actual usage\n entries: 0, // Would need to track actual entries\n hitRate: 0, // Would need to track hits and misses\n };\n }\n\n async hasAvailableQuota(sizeMB: number): Promise<boolean> {\n if (typeof navigator === 'undefined' || !navigator.storage?.estimate) {\n // L2Cache requires storage API to function\n throw new Error('Storage API not available');\n }\n\n const estimate = await navigator.storage.estimate();\n const availableMB = ((estimate.quota || 0) - (estimate.usage || 0)) / (1024 * 1024);\n return availableMB >= sizeMB;\n }\n\n /**\n * Get chunk metadata (decoderConfig) for a specific clip\n */\n async getClipMetadata(clipId: string, track: 'video' | 'audio'): Promise<any | null> {\n await this.init();\n\n const record = await this.dbStore.getRecord(this.projectId, clipId, track);\n return record?.metadata || null;\n }\n\n /**\n * List all cached projects\n */\n async listProjects(): Promise<\n Array<{ projectId: string; totalBytes: number; clipCount: number; lastAccess: number }>\n > {\n await this.init();\n\n const records = await this.dbStore.getAllRecords();\n const projects = new Map<\n string,\n { totalBytes: number; clipCount: number; lastAccess: number }\n >();\n\n // Aggregate stats per project\n for (const record of records) {\n const existing = projects.get(record.projectId) || {\n totalBytes: 0,\n clipCount: 0,\n lastAccess: 0,\n };\n\n projects.set(record.projectId, {\n totalBytes: existing.totalBytes + record.totalBytes,\n clipCount: existing.clipCount + 1,\n lastAccess: Math.max(existing.lastAccess, record.lastAccess),\n });\n }\n\n return Array.from(projects.entries()).map(([projectId, stats]) => ({\n projectId,\n ...stats,\n }));\n }\n\n /**\n * Clear all cache data for a specific project\n */\n async clearProject(targetProjectId: string): Promise<void> {\n await this.init();\n\n // 1. Collect all records for this project\n const records = await this.dbStore.getRecordsByProjectId(targetProjectId);\n\n // 2. Delete OPFS files and IndexedDB records\n for (const record of records) {\n await this.deleteEntry(record.clipId, record.track, targetProjectId);\n }\n\n // 3. Delete project directory (recursive, even if not empty)\n await this.opfsStore.deleteProjectDirectory(targetProjectId);\n }\n\n /**\n * Get cache size for a specific project\n */\n async getProjectSize(projectId: string): Promise<number> {\n await this.init();\n\n const records = await this.dbStore.getRecordsByProjectId(projectId);\n return records.reduce((sum, r) => sum + r.totalBytes, 0);\n }\n}\n"],"names":[],"mappings":"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"}
|