@agent-foundry/replay-server 1.0.0 → 1.0.2
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/.cursor/dev.mdc +941 -0
- package/.cursor/project.mdc +17 -2
- package/.env +30 -0
- package/Dockerfile +6 -0
- package/README.md +297 -27
- package/dist/cli/render.js +14 -4
- package/dist/cli/render.js.map +1 -1
- package/dist/renderer/PuppeteerRenderer.d.ts +28 -2
- package/dist/renderer/PuppeteerRenderer.d.ts.map +1 -1
- package/dist/renderer/PuppeteerRenderer.js +134 -36
- package/dist/renderer/PuppeteerRenderer.js.map +1 -1
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +200 -46
- package/dist/server/index.js.map +1 -1
- package/dist/services/BundleManager.d.ts +99 -0
- package/dist/services/BundleManager.d.ts.map +1 -0
- package/dist/services/BundleManager.js +410 -0
- package/dist/services/BundleManager.js.map +1 -0
- package/dist/services/OSSClient.d.ts +51 -0
- package/dist/services/OSSClient.d.ts.map +1 -0
- package/dist/services/OSSClient.js +207 -0
- package/dist/services/OSSClient.js.map +1 -0
- package/dist/services/index.d.ts +7 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +7 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/types.d.ts +73 -0
- package/dist/services/types.d.ts.map +1 -0
- package/dist/services/types.js +5 -0
- package/dist/services/types.js.map +1 -0
- package/docker-compose.local.yml +10 -0
- package/env.example +30 -0
- package/package.json +7 -3
- package/restart.sh +5 -0
- package/samples/jump_arena_5_ja-mksi5fku-qgk5iq.json +1952 -0
- package/scripts/render-pipeline.sh +657 -0
- package/scripts/test-bundle-preload.sh +20 -0
- package/scripts/test-service-sts.sh +176 -0
- package/src/cli/render.ts +18 -7
- package/src/renderer/PuppeteerRenderer.ts +192 -39
- package/src/server/index.ts +249 -68
- package/src/services/BundleManager.ts +503 -0
- package/src/services/OSSClient.ts +286 -0
- package/src/services/index.ts +7 -0
- package/src/services/types.ts +78 -0
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages bundle lifecycle: download, extraction, caching, and cleanup.
|
|
5
|
+
* Uses LRU eviction to manage cache size.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { createGunzip } from 'zlib';
|
|
11
|
+
import { pipeline } from 'stream/promises';
|
|
12
|
+
import { extract } from 'tar-stream';
|
|
13
|
+
import type { OSSClient } from './OSSClient.js';
|
|
14
|
+
import type { BundleInfo, BundleManagerConfig, CacheStats, DownloadStats } from './types.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Error thrown by bundle manager operations
|
|
18
|
+
*/
|
|
19
|
+
export class BundleManagerError extends Error {
|
|
20
|
+
constructor(
|
|
21
|
+
message: string,
|
|
22
|
+
public readonly code: string,
|
|
23
|
+
public readonly bundleId?: string
|
|
24
|
+
) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = 'BundleManagerError';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Bundle Manager for downloading, caching, and serving game bundles
|
|
32
|
+
*/
|
|
33
|
+
export class BundleManager {
|
|
34
|
+
private readonly bundlesDir: string;
|
|
35
|
+
private readonly maxCacheSize: number;
|
|
36
|
+
private readonly ossClient: OSSClient;
|
|
37
|
+
|
|
38
|
+
private bundles = new Map<string, BundleInfo>();
|
|
39
|
+
private downloadPromises = new Map<string, Promise<string>>();
|
|
40
|
+
private bundlesInUse = new Set<string>();
|
|
41
|
+
|
|
42
|
+
private downloadStats: DownloadStats = {
|
|
43
|
+
active: 0,
|
|
44
|
+
completed: 0,
|
|
45
|
+
failed: 0,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
constructor(config: BundleManagerConfig & { ossClient: OSSClient }) {
|
|
49
|
+
this.bundlesDir = config.bundlesDir;
|
|
50
|
+
this.maxCacheSize = config.maxCacheSize;
|
|
51
|
+
this.ossClient = config.ossClient;
|
|
52
|
+
|
|
53
|
+
// Ensure bundles directory exists
|
|
54
|
+
fs.mkdirSync(this.bundlesDir, { recursive: true });
|
|
55
|
+
|
|
56
|
+
// Load existing bundles from disk
|
|
57
|
+
this.loadCachedBundles();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Ensure a bundle is available (download if needed)
|
|
62
|
+
* Returns the path to the bundle directory
|
|
63
|
+
*/
|
|
64
|
+
async ensureBundle(bundleId: string, bundleUrl?: string): Promise<string> {
|
|
65
|
+
const bundlePath = path.join(this.bundlesDir, bundleId);
|
|
66
|
+
|
|
67
|
+
// Check if already cached
|
|
68
|
+
if (this.isCached(bundleId)) {
|
|
69
|
+
// Update last accessed time
|
|
70
|
+
const info = this.bundles.get(bundleId);
|
|
71
|
+
if (info) {
|
|
72
|
+
info.lastAccessedAt = new Date();
|
|
73
|
+
}
|
|
74
|
+
return bundlePath;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check if already downloading
|
|
78
|
+
if (this.downloadPromises.has(bundleId)) {
|
|
79
|
+
console.log(`[BundleManager] Waiting for existing download: ${bundleId}`);
|
|
80
|
+
return this.downloadPromises.get(bundleId)!;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Need to download
|
|
84
|
+
if (!bundleUrl) {
|
|
85
|
+
throw new BundleManagerError(
|
|
86
|
+
`Bundle ${bundleId} not cached and no bundleUrl provided`,
|
|
87
|
+
'BUNDLE_URL_REQUIRED',
|
|
88
|
+
bundleId
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Start download
|
|
93
|
+
const downloadPromise = this.downloadBundle(bundleId, bundleUrl);
|
|
94
|
+
this.downloadPromises.set(bundleId, downloadPromise);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const result = await downloadPromise;
|
|
98
|
+
return result;
|
|
99
|
+
} finally {
|
|
100
|
+
this.downloadPromises.delete(bundleId);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if a bundle is cached and ready
|
|
106
|
+
*/
|
|
107
|
+
isCached(bundleId: string): boolean {
|
|
108
|
+
const bundlePath = path.join(this.bundlesDir, bundleId);
|
|
109
|
+
const indexPath = path.join(bundlePath, 'index.html');
|
|
110
|
+
return fs.existsSync(bundlePath) && fs.existsSync(indexPath);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Mark bundle as in use (prevents eviction during rendering)
|
|
115
|
+
*/
|
|
116
|
+
markInUse(bundleId: string): void {
|
|
117
|
+
this.bundlesInUse.add(bundleId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Mark bundle as no longer in use
|
|
122
|
+
*/
|
|
123
|
+
markNotInUse(bundleId: string): void {
|
|
124
|
+
this.bundlesInUse.delete(bundleId);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Download and extract a bundle
|
|
129
|
+
*/
|
|
130
|
+
private async downloadBundle(bundleId: string, bundleUrl: string): Promise<string> {
|
|
131
|
+
const bundlePath = path.join(this.bundlesDir, bundleId);
|
|
132
|
+
const tempPath = `${bundlePath}.downloading`;
|
|
133
|
+
const archivePath = `${bundlePath}.tar.gz`;
|
|
134
|
+
|
|
135
|
+
// Initialize bundle info
|
|
136
|
+
this.bundles.set(bundleId, {
|
|
137
|
+
bundleId,
|
|
138
|
+
bundleUrl,
|
|
139
|
+
status: 'downloading',
|
|
140
|
+
progress: 0,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
this.downloadStats.active++;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
console.log(`[BundleManager] Downloading bundle ${bundleId} from ${bundleUrl}`);
|
|
147
|
+
|
|
148
|
+
// Ensure cache has space
|
|
149
|
+
await this.ensureCacheSpace(100 * 1024 * 1024); // Reserve 100MB
|
|
150
|
+
|
|
151
|
+
// Download archive
|
|
152
|
+
await this.ossClient.downloadFile(bundleUrl, archivePath, (percent) => {
|
|
153
|
+
const info = this.bundles.get(bundleId);
|
|
154
|
+
if (info) {
|
|
155
|
+
info.progress = percent;
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
console.log(`[BundleManager] Extracting bundle ${bundleId}...`);
|
|
160
|
+
|
|
161
|
+
// Create temp directory for extraction
|
|
162
|
+
fs.mkdirSync(tempPath, { recursive: true });
|
|
163
|
+
|
|
164
|
+
// Extract TAR.GZ
|
|
165
|
+
await this.extractTarGz(archivePath, tempPath);
|
|
166
|
+
|
|
167
|
+
// Clean up archive
|
|
168
|
+
fs.unlinkSync(archivePath);
|
|
169
|
+
|
|
170
|
+
// Move temp to final location
|
|
171
|
+
if (fs.existsSync(bundlePath)) {
|
|
172
|
+
fs.rmSync(bundlePath, { recursive: true });
|
|
173
|
+
}
|
|
174
|
+
fs.renameSync(tempPath, bundlePath);
|
|
175
|
+
|
|
176
|
+
// Verify extraction
|
|
177
|
+
const indexPath = path.join(bundlePath, 'index.html');
|
|
178
|
+
if (!fs.existsSync(indexPath)) {
|
|
179
|
+
throw new BundleManagerError(
|
|
180
|
+
`Bundle extraction failed: index.html not found`,
|
|
181
|
+
'EXTRACTION_INVALID',
|
|
182
|
+
bundleId
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Calculate bundle size
|
|
187
|
+
const size = this.getDirectorySize(bundlePath);
|
|
188
|
+
|
|
189
|
+
// Update bundle info
|
|
190
|
+
this.bundles.set(bundleId, {
|
|
191
|
+
bundleId,
|
|
192
|
+
bundleUrl,
|
|
193
|
+
status: 'ready',
|
|
194
|
+
size,
|
|
195
|
+
cachedAt: new Date(),
|
|
196
|
+
lastAccessedAt: new Date(),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
this.downloadStats.active--;
|
|
200
|
+
this.downloadStats.completed++;
|
|
201
|
+
|
|
202
|
+
console.log(`[BundleManager] Bundle ${bundleId} ready (${(size / 1024 / 1024).toFixed(2)} MB)`);
|
|
203
|
+
|
|
204
|
+
return bundlePath;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
// Cleanup on error
|
|
207
|
+
for (const p of [tempPath, archivePath, bundlePath]) {
|
|
208
|
+
if (fs.existsSync(p)) {
|
|
209
|
+
try {
|
|
210
|
+
fs.rmSync(p, { recursive: true });
|
|
211
|
+
} catch {
|
|
212
|
+
// Ignore cleanup errors
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
218
|
+
|
|
219
|
+
this.bundles.set(bundleId, {
|
|
220
|
+
bundleId,
|
|
221
|
+
bundleUrl,
|
|
222
|
+
status: 'error',
|
|
223
|
+
error: errorMessage,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
this.downloadStats.active--;
|
|
227
|
+
this.downloadStats.failed++;
|
|
228
|
+
|
|
229
|
+
throw new BundleManagerError(
|
|
230
|
+
`Failed to download bundle ${bundleId}: ${errorMessage}`,
|
|
231
|
+
'DOWNLOAD_FAILED',
|
|
232
|
+
bundleId
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Extract TAR.GZ archive to destination directory
|
|
239
|
+
*/
|
|
240
|
+
private async extractTarGz(archivePath: string, destPath: string): Promise<void> {
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
const extractStream = extract();
|
|
243
|
+
const entries: string[] = [];
|
|
244
|
+
|
|
245
|
+
extractStream.on('entry', (header, stream, next) => {
|
|
246
|
+
let filePath = path.join(destPath, header.name);
|
|
247
|
+
|
|
248
|
+
// Handle case where archive has a single root directory
|
|
249
|
+
// (common with GitHub releases: bundle-v1.0.0/index.html)
|
|
250
|
+
entries.push(header.name);
|
|
251
|
+
|
|
252
|
+
if (header.type === 'directory') {
|
|
253
|
+
fs.mkdirSync(filePath, { recursive: true });
|
|
254
|
+
stream.resume();
|
|
255
|
+
next();
|
|
256
|
+
} else if (header.type === 'file') {
|
|
257
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
258
|
+
const writeStream = fs.createWriteStream(filePath);
|
|
259
|
+
stream.pipe(writeStream);
|
|
260
|
+
stream.on('end', () => {
|
|
261
|
+
writeStream.end();
|
|
262
|
+
next();
|
|
263
|
+
});
|
|
264
|
+
stream.on('error', reject);
|
|
265
|
+
writeStream.on('error', reject);
|
|
266
|
+
} else {
|
|
267
|
+
// Skip other types (symlinks, etc.)
|
|
268
|
+
stream.resume();
|
|
269
|
+
next();
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
extractStream.on('finish', () => {
|
|
274
|
+
// Check if all files are in a subdirectory
|
|
275
|
+
// If so, move them up one level
|
|
276
|
+
const subdirs = fs.readdirSync(destPath);
|
|
277
|
+
if (subdirs.length === 1) {
|
|
278
|
+
const subdir = path.join(destPath, subdirs[0]);
|
|
279
|
+
if (fs.statSync(subdir).isDirectory()) {
|
|
280
|
+
// Check if index.html is in subdirectory
|
|
281
|
+
if (fs.existsSync(path.join(subdir, 'index.html'))) {
|
|
282
|
+
// Move all files up
|
|
283
|
+
const files = fs.readdirSync(subdir);
|
|
284
|
+
for (const file of files) {
|
|
285
|
+
fs.renameSync(
|
|
286
|
+
path.join(subdir, file),
|
|
287
|
+
path.join(destPath, file)
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
fs.rmdirSync(subdir);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
resolve();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
extractStream.on('error', reject);
|
|
298
|
+
|
|
299
|
+
const gunzip = createGunzip();
|
|
300
|
+
gunzip.on('error', reject);
|
|
301
|
+
|
|
302
|
+
const readStream = fs.createReadStream(archivePath);
|
|
303
|
+
readStream.on('error', reject);
|
|
304
|
+
|
|
305
|
+
readStream.pipe(gunzip).pipe(extractStream);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Ensure cache has enough space by evicting old bundles
|
|
311
|
+
*/
|
|
312
|
+
private async ensureCacheSpace(requiredBytes: number): Promise<void> {
|
|
313
|
+
const stats = this.getCacheStats();
|
|
314
|
+
const availableBytes = this.maxCacheSize - stats.used;
|
|
315
|
+
|
|
316
|
+
if (availableBytes >= requiredBytes) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const bytesToFree = requiredBytes - availableBytes;
|
|
321
|
+
let freedBytes = 0;
|
|
322
|
+
|
|
323
|
+
// Get bundles sorted by last accessed time (oldest first)
|
|
324
|
+
const evictionCandidates = Array.from(this.bundles.entries())
|
|
325
|
+
.filter(([id, info]) => {
|
|
326
|
+
return (
|
|
327
|
+
info.status === 'ready' &&
|
|
328
|
+
!this.bundlesInUse.has(id) &&
|
|
329
|
+
!this.downloadPromises.has(id)
|
|
330
|
+
);
|
|
331
|
+
})
|
|
332
|
+
.sort(([, a], [, b]) => {
|
|
333
|
+
const aTime = a.lastAccessedAt?.getTime() || 0;
|
|
334
|
+
const bTime = b.lastAccessedAt?.getTime() || 0;
|
|
335
|
+
return aTime - bTime;
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
for (const [bundleId, info] of evictionCandidates) {
|
|
339
|
+
if (freedBytes >= bytesToFree) {
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
console.log(`[BundleManager] Evicting bundle ${bundleId} to free space`);
|
|
344
|
+
this.removeBundle(bundleId);
|
|
345
|
+
freedBytes += info.size || 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (freedBytes < bytesToFree) {
|
|
349
|
+
throw new BundleManagerError(
|
|
350
|
+
`Unable to free enough cache space (needed: ${bytesToFree}, freed: ${freedBytes})`,
|
|
351
|
+
'CACHE_FULL'
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Load existing bundles from disk
|
|
358
|
+
*/
|
|
359
|
+
private loadCachedBundles(): void {
|
|
360
|
+
if (!fs.existsSync(this.bundlesDir)) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const entries = fs.readdirSync(this.bundlesDir, { withFileTypes: true });
|
|
365
|
+
|
|
366
|
+
for (const entry of entries) {
|
|
367
|
+
if (!entry.isDirectory()) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Skip temp directories
|
|
372
|
+
if (entry.name.endsWith('.downloading') || entry.name.endsWith('.tar.gz')) {
|
|
373
|
+
// Clean up orphaned temp files
|
|
374
|
+
try {
|
|
375
|
+
fs.rmSync(path.join(this.bundlesDir, entry.name), { recursive: true });
|
|
376
|
+
} catch {
|
|
377
|
+
// Ignore
|
|
378
|
+
}
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const bundlePath = path.join(this.bundlesDir, entry.name);
|
|
383
|
+
const indexPath = path.join(bundlePath, 'index.html');
|
|
384
|
+
|
|
385
|
+
if (fs.existsSync(indexPath)) {
|
|
386
|
+
const stats = fs.statSync(bundlePath);
|
|
387
|
+
const size = this.getDirectorySize(bundlePath);
|
|
388
|
+
|
|
389
|
+
this.bundles.set(entry.name, {
|
|
390
|
+
bundleId: entry.name,
|
|
391
|
+
status: 'ready',
|
|
392
|
+
size,
|
|
393
|
+
cachedAt: stats.mtime,
|
|
394
|
+
lastAccessedAt: stats.mtime,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
console.log(`[BundleManager] Loaded ${this.bundles.size} cached bundles`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get directory size recursively
|
|
404
|
+
*/
|
|
405
|
+
private getDirectorySize(dirPath: string): number {
|
|
406
|
+
let size = 0;
|
|
407
|
+
|
|
408
|
+
try {
|
|
409
|
+
const files = fs.readdirSync(dirPath);
|
|
410
|
+
|
|
411
|
+
for (const file of files) {
|
|
412
|
+
const filePath = path.join(dirPath, file);
|
|
413
|
+
const stats = fs.statSync(filePath);
|
|
414
|
+
|
|
415
|
+
if (stats.isDirectory()) {
|
|
416
|
+
size += this.getDirectorySize(filePath);
|
|
417
|
+
} else {
|
|
418
|
+
size += stats.size;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
} catch {
|
|
422
|
+
// Ignore errors
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return size;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* List all bundles
|
|
430
|
+
*/
|
|
431
|
+
listBundles(): BundleInfo[] {
|
|
432
|
+
return Array.from(this.bundles.values());
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Get bundle info
|
|
437
|
+
*/
|
|
438
|
+
getBundleInfo(bundleId: string): BundleInfo | undefined {
|
|
439
|
+
return this.bundles.get(bundleId);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Remove bundle from cache
|
|
444
|
+
*/
|
|
445
|
+
removeBundle(bundleId: string): void {
|
|
446
|
+
const bundlePath = path.join(this.bundlesDir, bundleId);
|
|
447
|
+
|
|
448
|
+
if (fs.existsSync(bundlePath)) {
|
|
449
|
+
try {
|
|
450
|
+
fs.rmSync(bundlePath, { recursive: true });
|
|
451
|
+
} catch (error) {
|
|
452
|
+
console.error(`[BundleManager] Failed to remove bundle ${bundleId}:`, error);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
this.bundles.delete(bundleId);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Get cache statistics
|
|
461
|
+
*/
|
|
462
|
+
getCacheStats(): CacheStats {
|
|
463
|
+
let used = 0;
|
|
464
|
+
let count = 0;
|
|
465
|
+
|
|
466
|
+
for (const info of this.bundles.values()) {
|
|
467
|
+
if (info.status === 'ready' && info.size) {
|
|
468
|
+
used += info.size;
|
|
469
|
+
count++;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
used,
|
|
475
|
+
max: this.maxCacheSize,
|
|
476
|
+
usedPercent: Math.round((used / this.maxCacheSize) * 100),
|
|
477
|
+
bundleCount: count,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Get download statistics
|
|
483
|
+
*/
|
|
484
|
+
getDownloadStats(): DownloadStats {
|
|
485
|
+
return { ...this.downloadStats };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Get bundles directory path
|
|
490
|
+
*/
|
|
491
|
+
getBundlesDir(): string {
|
|
492
|
+
return this.bundlesDir;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Create a bundle manager instance
|
|
498
|
+
*/
|
|
499
|
+
export function createBundleManager(
|
|
500
|
+
config: BundleManagerConfig & { ossClient: OSSClient }
|
|
501
|
+
): BundleManager {
|
|
502
|
+
return new BundleManager(config);
|
|
503
|
+
}
|