@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
package/src/server/index.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Replay Render Server
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* HTTP API for rendering replays to video
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* Endpoints:
|
|
7
7
|
* - POST /render - Start a render job
|
|
8
8
|
* - GET /jobs - List all jobs
|
|
9
9
|
* - GET /status/:jobId - Get job status
|
|
10
10
|
* - GET /download/:jobId - Download completed video
|
|
11
11
|
* - GET /bundles - List available game bundles
|
|
12
|
+
* - POST /bundles/preload - Preload a bundle
|
|
13
|
+
* - GET /bundles/stats - Get cache statistics
|
|
14
|
+
* - GET /bundles/:bundleId - Get specific bundle info
|
|
15
|
+
* - DELETE /bundles/:bundleId - Remove bundle from cache
|
|
12
16
|
*/
|
|
13
17
|
|
|
14
18
|
import express, { type Express } from 'express';
|
|
@@ -20,6 +24,8 @@ import { fileURLToPath } from 'url';
|
|
|
20
24
|
import { dirname } from 'path';
|
|
21
25
|
import { execSync } from 'child_process';
|
|
22
26
|
import { PuppeteerRenderer, RenderProgress } from '../renderer/PuppeteerRenderer.js';
|
|
27
|
+
import { OSSClient, createOSSClient } from '../services/OSSClient.js';
|
|
28
|
+
import { BundleManager, createBundleManager, BundleManagerError } from '../services/BundleManager.js';
|
|
23
29
|
import type { ReplayManifestV1 } from '@agent-foundry/replay';
|
|
24
30
|
|
|
25
31
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -33,50 +39,184 @@ const PORT = process.env.PORT || 3001;
|
|
|
33
39
|
const GAME_URL = process.env.GAME_URL || 'http://localhost:5173';
|
|
34
40
|
const OUTPUT_DIR = process.env.OUTPUT_DIR || path.join(process.cwd(), 'output');
|
|
35
41
|
const BUNDLES_DIR = process.env.BUNDLES_DIR || path.join(__dirname, '../../bundles');
|
|
42
|
+
const BFF_BASE_URL = process.env.BFF_BASE_URL || 'http://localhost:11001';
|
|
43
|
+
const BFF_SERVICE_TOKEN = process.env.BFF_SERVICE_TOKEN;
|
|
44
|
+
const MAX_CACHE_SIZE = parseInt(process.env.MAX_CACHE_SIZE || '10737418240'); // 10GB default
|
|
45
|
+
|
|
46
|
+
// Initialize OSS Client (for downloading bundles)
|
|
47
|
+
const ossClient = createOSSClient({
|
|
48
|
+
bffBaseUrl: BFF_BASE_URL,
|
|
49
|
+
serviceToken: BFF_SERVICE_TOKEN,
|
|
50
|
+
timeout: 30000,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Initialize Bundle Manager
|
|
54
|
+
const bundleManager = createBundleManager({
|
|
55
|
+
bundlesDir: BUNDLES_DIR,
|
|
56
|
+
maxCacheSize: MAX_CACHE_SIZE,
|
|
57
|
+
ossClient,
|
|
58
|
+
});
|
|
36
59
|
|
|
37
60
|
/**
|
|
38
|
-
* GET /bundles
|
|
61
|
+
* GET /api/bundles
|
|
39
62
|
* List available game bundles
|
|
40
|
-
* This route must be registered BEFORE the static middleware to handle /bundles requests
|
|
41
63
|
*/
|
|
42
|
-
app.get('/bundles', (req, res) => {
|
|
43
|
-
console.log('[GET /bundles] Request received');
|
|
64
|
+
app.get('/api/bundles', (req, res) => {
|
|
65
|
+
console.log('[GET /api/bundles] Request received');
|
|
44
66
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
67
|
+
const bundles = bundleManager.listBundles().map(info => ({
|
|
68
|
+
bundleId: info.bundleId,
|
|
69
|
+
status: info.status,
|
|
70
|
+
size: info.size,
|
|
71
|
+
cachedAt: info.cachedAt,
|
|
72
|
+
lastAccessedAt: info.lastAccessedAt,
|
|
73
|
+
url: info.status === 'ready'
|
|
74
|
+
? `http://localhost:${PORT}/bundles/${info.bundleId}/`
|
|
75
|
+
: undefined,
|
|
76
|
+
ready: info.status === 'ready',
|
|
77
|
+
progress: info.progress,
|
|
78
|
+
error: info.error,
|
|
79
|
+
}));
|
|
49
80
|
|
|
50
|
-
|
|
51
|
-
const entries = fs.readdirSync(BUNDLES_DIR, { withFileTypes: true });
|
|
52
|
-
const bundles = entries
|
|
53
|
-
.filter(entry => entry.isDirectory())
|
|
54
|
-
.map(entry => {
|
|
55
|
-
const bundlePath = path.join(BUNDLES_DIR, entry.name);
|
|
56
|
-
const indexPath = path.join(bundlePath, 'index.html');
|
|
57
|
-
const hasIndex = fs.existsSync(indexPath);
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
bundleId: entry.name,
|
|
61
|
-
path: bundlePath,
|
|
62
|
-
url: `http://localhost:${PORT}/bundles/${entry.name}/`,
|
|
63
|
-
ready: hasIndex,
|
|
64
|
-
};
|
|
65
|
-
});
|
|
81
|
+
const cacheStats = bundleManager.getCacheStats();
|
|
66
82
|
|
|
67
|
-
console.log('[GET /bundles] Found bundles:', bundles.length);
|
|
83
|
+
console.log('[GET /api/bundles] Found bundles:', bundles.length);
|
|
68
84
|
res.json({
|
|
69
85
|
bundles,
|
|
70
86
|
count: bundles.length,
|
|
71
|
-
|
|
87
|
+
cache: cacheStats,
|
|
72
88
|
});
|
|
73
89
|
} catch (error) {
|
|
74
|
-
console.error('[GET /bundles] Error listing bundles:', error);
|
|
90
|
+
console.error('[GET /api/bundles] Error listing bundles:', error);
|
|
75
91
|
res.status(500).json({ error: 'Failed to list bundles' });
|
|
76
92
|
}
|
|
77
93
|
});
|
|
78
94
|
|
|
79
|
-
|
|
95
|
+
/**
|
|
96
|
+
* GET /api/bundles/stats
|
|
97
|
+
* Get cache and download statistics
|
|
98
|
+
*/
|
|
99
|
+
app.get('/api/bundles/stats', (req, res) => {
|
|
100
|
+
console.log('[GET /api/bundles/stats] Request received');
|
|
101
|
+
try {
|
|
102
|
+
const cacheStats = bundleManager.getCacheStats();
|
|
103
|
+
const downloadStats = bundleManager.getDownloadStats();
|
|
104
|
+
|
|
105
|
+
res.json({
|
|
106
|
+
cache: cacheStats,
|
|
107
|
+
downloads: downloadStats,
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error('[GET /api/bundles/stats] Error getting stats:', error);
|
|
111
|
+
res.status(500).json({ error: 'Failed to get stats' });
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* POST /api/bundles/preload
|
|
117
|
+
* Preload a bundle (async download)
|
|
118
|
+
*/
|
|
119
|
+
app.post('/api/bundles/preload', async (req, res) => {
|
|
120
|
+
console.log('[POST /api/bundles/preload] Request received');
|
|
121
|
+
try {
|
|
122
|
+
const { bundleId, bundleUrl } = req.body;
|
|
123
|
+
|
|
124
|
+
if (!bundleId) {
|
|
125
|
+
return res.status(400).json({ error: 'bundleId is required' });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!bundleUrl) {
|
|
129
|
+
return res.status(400).json({ error: 'bundleUrl is required' });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check if already cached
|
|
133
|
+
if (bundleManager.isCached(bundleId)) {
|
|
134
|
+
return res.json({
|
|
135
|
+
bundleId,
|
|
136
|
+
status: 'ready',
|
|
137
|
+
message: 'Bundle already cached',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Start download in background (non-blocking)
|
|
142
|
+
bundleManager.ensureBundle(bundleId, bundleUrl).catch(error => {
|
|
143
|
+
console.error(`[POST /api/bundles/preload] Failed to download ${bundleId}:`, error);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
res.json({
|
|
147
|
+
bundleId,
|
|
148
|
+
status: 'downloading',
|
|
149
|
+
message: 'Bundle download started',
|
|
150
|
+
});
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error('[POST /api/bundles/preload] Error:', error);
|
|
153
|
+
res.status(500).json({ error: 'Failed to start bundle download' });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* GET /api/bundles/:bundleId
|
|
159
|
+
* Get specific bundle info
|
|
160
|
+
*/
|
|
161
|
+
app.get('/api/bundles/:bundleId', (req, res) => {
|
|
162
|
+
const { bundleId } = req.params;
|
|
163
|
+
console.log(`[GET /api/bundles/${bundleId}] Request received`);
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const info = bundleManager.getBundleInfo(bundleId);
|
|
167
|
+
|
|
168
|
+
if (!info) {
|
|
169
|
+
return res.status(404).json({ error: 'Bundle not found' });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
res.json({
|
|
173
|
+
bundleId: info.bundleId,
|
|
174
|
+
status: info.status,
|
|
175
|
+
size: info.size,
|
|
176
|
+
cachedAt: info.cachedAt,
|
|
177
|
+
lastAccessedAt: info.lastAccessedAt,
|
|
178
|
+
url: info.status === 'ready'
|
|
179
|
+
? `http://localhost:${PORT}/bundles/${info.bundleId}/`
|
|
180
|
+
: undefined,
|
|
181
|
+
ready: info.status === 'ready',
|
|
182
|
+
progress: info.progress,
|
|
183
|
+
error: info.error,
|
|
184
|
+
});
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error(`[GET /api/bundles/${bundleId}] Error:`, error);
|
|
187
|
+
res.status(500).json({ error: 'Failed to get bundle info' });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* DELETE /api/bundles/:bundleId
|
|
193
|
+
* Remove bundle from cache
|
|
194
|
+
*/
|
|
195
|
+
app.delete('/api/bundles/:bundleId', (req, res) => {
|
|
196
|
+
const { bundleId } = req.params;
|
|
197
|
+
console.log(`[DELETE /api/bundles/${bundleId}] Request received`);
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const info = bundleManager.getBundleInfo(bundleId);
|
|
201
|
+
|
|
202
|
+
if (!info) {
|
|
203
|
+
return res.status(404).json({ error: 'Bundle not found' });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
bundleManager.removeBundle(bundleId);
|
|
207
|
+
|
|
208
|
+
res.json({
|
|
209
|
+
bundleId,
|
|
210
|
+
message: 'Bundle removed from cache',
|
|
211
|
+
});
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error(`[DELETE /api/bundles/${bundleId}] Error:`, error);
|
|
214
|
+
res.status(500).json({ error: 'Failed to remove bundle' });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Serve static game bundles at /bundles/<bundleId>/
|
|
219
|
+
// API routes are at /api/bundles/* to avoid conflicts
|
|
80
220
|
app.use('/bundles', express.static(BUNDLES_DIR, {
|
|
81
221
|
index: 'index.html',
|
|
82
222
|
extensions: ['html']
|
|
@@ -91,37 +231,37 @@ app.get('/debug/chrome', async (req, res) => {
|
|
|
91
231
|
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium',
|
|
92
232
|
tests: {}
|
|
93
233
|
};
|
|
94
|
-
|
|
234
|
+
|
|
95
235
|
try {
|
|
96
236
|
// Test 1: Check if chromium exists
|
|
97
|
-
const version = execSync(`${results.executablePath} --version`, {
|
|
237
|
+
const version = execSync(`${results.executablePath} --version`, {
|
|
98
238
|
encoding: 'utf8',
|
|
99
|
-
timeout: 5000
|
|
239
|
+
timeout: 5000
|
|
100
240
|
});
|
|
101
241
|
results.tests.version = { ok: true, output: version.trim() };
|
|
102
242
|
} catch (e: any) {
|
|
103
|
-
results.tests.version = {
|
|
104
|
-
ok: false,
|
|
243
|
+
results.tests.version = {
|
|
244
|
+
ok: false,
|
|
105
245
|
error: e.message,
|
|
106
|
-
stderr: e.stderr?.toString()
|
|
246
|
+
stderr: e.stderr?.toString()
|
|
107
247
|
};
|
|
108
248
|
}
|
|
109
|
-
|
|
249
|
+
|
|
110
250
|
try {
|
|
111
251
|
// Test 2: Check library dependencies
|
|
112
|
-
const ldd = execSync(`ldd ${results.executablePath}`, {
|
|
252
|
+
const ldd = execSync(`ldd ${results.executablePath}`, {
|
|
113
253
|
encoding: 'utf8',
|
|
114
|
-
timeout: 5000
|
|
254
|
+
timeout: 5000
|
|
115
255
|
});
|
|
116
256
|
results.tests.dependencies = { ok: true, summary: 'All libraries found' };
|
|
117
257
|
} catch (e: any) {
|
|
118
|
-
results.tests.dependencies = {
|
|
119
|
-
ok: false,
|
|
258
|
+
results.tests.dependencies = {
|
|
259
|
+
ok: false,
|
|
120
260
|
error: e.message,
|
|
121
|
-
stderr: e.stderr?.toString()
|
|
261
|
+
stderr: e.stderr?.toString()
|
|
122
262
|
};
|
|
123
263
|
}
|
|
124
|
-
|
|
264
|
+
|
|
125
265
|
try {
|
|
126
266
|
// Test 3: Check if Chinese fonts are available
|
|
127
267
|
const fonts = execSync(`fc-list :lang=zh-cn`, {
|
|
@@ -140,28 +280,45 @@ app.get('/debug/chrome', async (req, res) => {
|
|
|
140
280
|
error: 'fontconfig not available or no Chinese fonts found'
|
|
141
281
|
};
|
|
142
282
|
}
|
|
143
|
-
|
|
283
|
+
|
|
144
284
|
res.json(results);
|
|
145
285
|
});
|
|
146
286
|
|
|
147
287
|
/**
|
|
148
288
|
* Resolve game URL from manifest bundleId or config
|
|
289
|
+
* Now supports dynamic bundle downloading
|
|
149
290
|
*/
|
|
150
|
-
function resolveGameUrl(
|
|
291
|
+
async function resolveGameUrl(
|
|
292
|
+
manifest: { bundleId?: string; bundleUrl?: string; [key: string]: unknown },
|
|
293
|
+
config?: { gameUrl?: string; bundleUrl?: string }
|
|
294
|
+
): Promise<string> {
|
|
151
295
|
// 1. Explicit gameUrl in config takes precedence
|
|
152
|
-
if (
|
|
153
|
-
return
|
|
296
|
+
if (config?.gameUrl) {
|
|
297
|
+
return config.gameUrl;
|
|
154
298
|
}
|
|
155
|
-
|
|
299
|
+
|
|
156
300
|
// 2. Use bundleId from manifest to serve from local bundles
|
|
157
301
|
if (manifest.bundleId) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
302
|
+
// Get bundleUrl from manifest or config
|
|
303
|
+
const bundleUrl = manifest.bundleUrl || config?.bundleUrl;
|
|
304
|
+
|
|
305
|
+
// Ensure bundle is available (download if needed)
|
|
306
|
+
try {
|
|
307
|
+
await bundleManager.ensureBundle(manifest.bundleId, bundleUrl);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (error instanceof BundleManagerError && error.code === 'BUNDLE_URL_REQUIRED') {
|
|
310
|
+
// Bundle not cached and no URL provided - check if it exists locally
|
|
311
|
+
if (!bundleManager.isCached(manifest.bundleId)) {
|
|
312
|
+
throw new Error(`Bundle ${manifest.bundleId} not found and no bundleUrl provided`);
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
161
317
|
}
|
|
318
|
+
|
|
162
319
|
return `http://localhost:${PORT}/bundles/${manifest.bundleId}/`;
|
|
163
320
|
}
|
|
164
|
-
|
|
321
|
+
|
|
165
322
|
// 3. Fall back to default GAME_URL
|
|
166
323
|
return GAME_URL;
|
|
167
324
|
}
|
|
@@ -175,6 +332,7 @@ interface RenderJob {
|
|
|
175
332
|
error: string | null;
|
|
176
333
|
createdAt: Date;
|
|
177
334
|
completedAt: Date | null;
|
|
335
|
+
bundleId?: string;
|
|
178
336
|
}
|
|
179
337
|
|
|
180
338
|
const jobs = new Map<string, RenderJob>();
|
|
@@ -192,7 +350,13 @@ app.post('/render', async (req, res) => {
|
|
|
192
350
|
const { manifest, manifestUrl, config } = req.body;
|
|
193
351
|
|
|
194
352
|
// Get manifest from body or URL
|
|
195
|
-
let replayManifest:
|
|
353
|
+
let replayManifest: {
|
|
354
|
+
schema: string;
|
|
355
|
+
bundleId?: string;
|
|
356
|
+
bundleUrl?: string;
|
|
357
|
+
gameId: string;
|
|
358
|
+
[key: string]: unknown;
|
|
359
|
+
};
|
|
196
360
|
|
|
197
361
|
if (manifest) {
|
|
198
362
|
replayManifest = manifest;
|
|
@@ -208,10 +372,17 @@ app.post('/render', async (req, res) => {
|
|
|
208
372
|
return res.status(400).json({ error: 'Either manifest or manifestUrl is required' });
|
|
209
373
|
}
|
|
210
374
|
|
|
211
|
-
// Validate manifest
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
375
|
+
// Validate manifest - basic structure only (schema-agnostic)
|
|
376
|
+
console.log('[POST /render] Manifest schema:', replayManifest.schema);
|
|
377
|
+
|
|
378
|
+
if (!replayManifest.schema || typeof replayManifest.schema !== 'string') {
|
|
379
|
+
console.error('[POST /render] Missing or invalid schema field');
|
|
380
|
+
return res.status(400).json({ error: 'Manifest must have a schema field' });
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!replayManifest.gameId || typeof replayManifest.gameId !== 'string') {
|
|
384
|
+
console.error('[POST /render] Missing or invalid gameId field');
|
|
385
|
+
return res.status(400).json({ error: 'Manifest must have a gameId field' });
|
|
215
386
|
}
|
|
216
387
|
|
|
217
388
|
// Create job
|
|
@@ -226,20 +397,27 @@ app.post('/render', async (req, res) => {
|
|
|
226
397
|
error: null,
|
|
227
398
|
createdAt: new Date(),
|
|
228
399
|
completedAt: null,
|
|
400
|
+
bundleId: replayManifest.bundleId,
|
|
229
401
|
};
|
|
230
402
|
|
|
231
403
|
jobs.set(jobId, job);
|
|
232
404
|
|
|
233
|
-
// Resolve game URL from bundleId or config
|
|
405
|
+
// Resolve game URL from bundleId or config (async - may download bundle)
|
|
234
406
|
let gameUrl: string;
|
|
235
407
|
try {
|
|
236
|
-
gameUrl = resolveGameUrl(replayManifest, config
|
|
408
|
+
gameUrl = await resolveGameUrl(replayManifest, config);
|
|
237
409
|
} catch (error) {
|
|
238
410
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
239
411
|
console.error('[POST /render] Failed to resolve game URL:', errorMessage);
|
|
412
|
+
jobs.delete(jobId);
|
|
240
413
|
return res.status(400).json({ error: errorMessage });
|
|
241
414
|
}
|
|
242
415
|
|
|
416
|
+
// Mark bundle as in use during rendering
|
|
417
|
+
if (replayManifest.bundleId) {
|
|
418
|
+
bundleManager.markInUse(replayManifest.bundleId);
|
|
419
|
+
}
|
|
420
|
+
|
|
243
421
|
// Start rendering in background
|
|
244
422
|
processJob(job, replayManifest, {
|
|
245
423
|
gameUrl,
|
|
@@ -340,18 +518,15 @@ app.get('/download/:jobId', (req, res) => {
|
|
|
340
518
|
* Health check endpoint
|
|
341
519
|
*/
|
|
342
520
|
app.get('/health', (req, res) => {
|
|
343
|
-
|
|
344
|
-
let bundleCount = 0;
|
|
345
|
-
if (fs.existsSync(BUNDLES_DIR)) {
|
|
346
|
-
const entries = fs.readdirSync(BUNDLES_DIR, { withFileTypes: true });
|
|
347
|
-
bundleCount = entries.filter(e => e.isDirectory()).length;
|
|
348
|
-
}
|
|
521
|
+
const cacheStats = bundleManager.getCacheStats();
|
|
349
522
|
|
|
350
523
|
res.json({
|
|
351
524
|
status: 'ok',
|
|
352
525
|
gameUrl: GAME_URL,
|
|
353
526
|
bundlesDir: BUNDLES_DIR,
|
|
354
|
-
bundleCount,
|
|
527
|
+
bundleCount: cacheStats.bundleCount,
|
|
528
|
+
cache: cacheStats,
|
|
529
|
+
bffBaseUrl: BFF_BASE_URL,
|
|
355
530
|
});
|
|
356
531
|
});
|
|
357
532
|
|
|
@@ -360,7 +535,7 @@ app.get('/health', (req, res) => {
|
|
|
360
535
|
*/
|
|
361
536
|
async function processJob(
|
|
362
537
|
job: RenderJob,
|
|
363
|
-
manifest:
|
|
538
|
+
manifest: { schema: string; gameId: string; [key: string]: unknown },
|
|
364
539
|
config: {
|
|
365
540
|
gameUrl: string;
|
|
366
541
|
outputPath: string;
|
|
@@ -395,6 +570,11 @@ async function processJob(
|
|
|
395
570
|
}
|
|
396
571
|
|
|
397
572
|
job.completedAt = new Date();
|
|
573
|
+
|
|
574
|
+
// Mark bundle as no longer in use
|
|
575
|
+
if (job.bundleId) {
|
|
576
|
+
bundleManager.markNotInUse(job.bundleId);
|
|
577
|
+
}
|
|
398
578
|
}
|
|
399
579
|
|
|
400
580
|
// Start server
|
|
@@ -403,5 +583,6 @@ app.listen(PORT, () => {
|
|
|
403
583
|
console.log(`📺 Default Game URL: ${GAME_URL}`);
|
|
404
584
|
console.log(`📦 Bundles directory: ${BUNDLES_DIR}`);
|
|
405
585
|
console.log(`📁 Output directory: ${OUTPUT_DIR}`);
|
|
586
|
+
console.log(`🔗 BFF Base URL: ${BFF_BASE_URL}`);
|
|
587
|
+
console.log(`💾 Max cache size: ${(MAX_CACHE_SIZE / 1024 / 1024 / 1024).toFixed(2)} GB`);
|
|
406
588
|
});
|
|
407
|
-
|