ugly-app 0.1.240 → 0.1.241
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/cli/build.d.ts.map +1 -1
- package/dist/cli/build.js +19 -0
- package/dist/cli/build.js.map +1 -1
- package/dist/cli/version.d.ts +1 -1
- package/dist/cli/version.js +1 -1
- package/dist/worker/TaskContext.d.ts +59 -0
- package/dist/worker/TaskContext.d.ts.map +1 -0
- package/dist/worker/TaskContext.js +2 -0
- package/dist/worker/TaskContext.js.map +1 -0
- package/dist/worker/TaskHost.d.ts +74 -0
- package/dist/worker/TaskHost.d.ts.map +1 -0
- package/dist/worker/TaskHost.js +187 -0
- package/dist/worker/TaskHost.js.map +1 -0
- package/dist/worker/index.d.ts +3 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +2 -0
- package/dist/worker/index.js.map +1 -0
- package/package.json +2 -1
- package/src/cli/build.ts +23 -0
- package/src/cli/version.ts +1 -1
- package/src/worker/TaskContext.ts +74 -0
- package/src/worker/TaskHost.ts +261 -0
- package/src/worker/index.ts +13 -0
package/dist/cli/build.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"AAIA,wBAAgB,QAAQ,IAAI,IAAI,
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"AAIA,wBAAgB,QAAQ,IAAI,IAAI,CAsE/B"}
|
package/dist/cli/build.js
CHANGED
|
@@ -33,6 +33,25 @@ export const buildId = '${buildId}';
|
|
|
33
33
|
fs.mkdirSync(serverOutDir, { recursive: true });
|
|
34
34
|
execSync(`npx esbuild server/index.ts --bundle --platform=node --format=esm --outfile=dist/server/index.mjs --external:ugly-app --external:ugly-app/*`, { stdio: 'inherit', cwd: process.cwd() });
|
|
35
35
|
}
|
|
36
|
+
// Bundle task.ts if it exists (for cron/worker/GPU tasks)
|
|
37
|
+
const taskEntry = path.join(process.cwd(), 'server', 'task.ts');
|
|
38
|
+
if (fs.existsSync(taskEntry)) {
|
|
39
|
+
console.log('[Build] Bundling server/task.ts...');
|
|
40
|
+
const tasksOutDir = path.join(distDir, 'tasks');
|
|
41
|
+
fs.mkdirSync(tasksOutDir, { recursive: true });
|
|
42
|
+
// Externalize allowlisted packages — they'll be installed on the task host
|
|
43
|
+
const externals = [
|
|
44
|
+
'ugly-app', 'ugly-app/*',
|
|
45
|
+
'three', '@types/three', 'jsdom', 'gl',
|
|
46
|
+
'@ffmpeg-installer/ffmpeg', 'fluent-ffmpeg', 'sharp',
|
|
47
|
+
'@anthropic-ai/sdk', 'openai',
|
|
48
|
+
'zod', 'date-fns', 'lodash',
|
|
49
|
+
'mediasoup', 'mediasoup-client',
|
|
50
|
+
];
|
|
51
|
+
const externalFlags = externals.map(e => `--external:${e}`).join(' ');
|
|
52
|
+
execSync(`npx esbuild server/task.ts --bundle --platform=node --format=esm --outfile=dist/tasks/task.js ${externalFlags}`, { stdio: 'inherit', cwd: process.cwd() });
|
|
53
|
+
console.log('[Build] Task bundle: dist/tasks/task.js');
|
|
54
|
+
}
|
|
36
55
|
// Run tsc for typecheck
|
|
37
56
|
console.log('[Build] Running tsc...');
|
|
38
57
|
execSync('npx tsc --noEmit', { stdio: 'inherit', cwd: process.cwd() });
|
package/dist/cli/build.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,MAAM,UAAU,QAAQ;IACtB,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAEnC,wBAAwB;IACxB,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;IACrE,MAAM,gBAAgB,GAAG;0BACD,OAAO;CAChC,CAAC;IAEA,4BAA4B;IAC5B,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAC9C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,EAAE,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/C,CAAC;IAED,EAAE,CAAC,aAAa,CAAC,aAAa,EAAE,gBAAgB,EAAE,OAAO,CAAC,CAAC;IAC3D,OAAO,CAAC,GAAG,CAAC,oBAAoB,OAAO,EAAE,CAAC,CAAC;IAE3C,8BAA8B;IAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,CAAC;IACjD,IAAI,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QAC3B,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;IACvC,CAAC;IAED,iBAAiB;IACjB,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC,CAAC;IAC7C,QAAQ,CAAC,gBAAgB,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAErE,oFAAoF;IACpF,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;IACnE,MAAM,oBAAoB,GAAG,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,uBAAuB,CAAC,CAAC,CAAC;IAC9F,IAAI,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,IAAI,CAAC,oBAAoB,EAAE,CAAC;QACxD,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;QAC1C,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QAClD,EAAE,CAAC,SAAS,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,QAAQ,CACN,6IAA6I,EAC7I,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,CACzC,CAAC;IACJ,CAAC;IAED,wBAAwB;IACxB,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;IACtC,QAAQ,CAAC,kBAAkB,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAEvE,OAAO,CAAC,GAAG,CAAC,2BAA2B,OAAO,GAAG,CAAC,CAAC;AACrD,CAAC"}
|
|
1
|
+
{"version":3,"file":"build.js","sourceRoot":"","sources":["../../src/cli/build.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "0.1.
|
|
1
|
+
export declare const CLI_VERSION = "0.1.241";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/cli/version.js
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskContext — the execution environment for project task bundles.
|
|
3
|
+
*
|
|
4
|
+
* Every project's `task.js` exports an `execute(ctx, data)` function.
|
|
5
|
+
* The platform creates a TaskContext scoped to the project's own DB and R2,
|
|
6
|
+
* then calls execute(). The project never needs DB credentials or R2 keys
|
|
7
|
+
* — the platform provides them through this interface.
|
|
8
|
+
*
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // server/task.ts (project source)
|
|
11
|
+
* import type { TaskContext } from 'ugly-app/worker';
|
|
12
|
+
*
|
|
13
|
+
* export async function execute(ctx: TaskContext, data: { taskName: string }) {
|
|
14
|
+
* switch (data.taskName) {
|
|
15
|
+
* case 'dailyCleanup':
|
|
16
|
+
* const old = await ctx.db.find('todo', { done: true, updated: { $lt: daysAgo(30) } });
|
|
17
|
+
* for (const doc of old) await ctx.db.deleteDoc('todo', doc._id);
|
|
18
|
+
* break;
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export interface TaskContext {
|
|
24
|
+
db: TaskDB;
|
|
25
|
+
r2: TaskStorage;
|
|
26
|
+
tempR2: TaskTempStorage;
|
|
27
|
+
taskId: string;
|
|
28
|
+
taskName: string;
|
|
29
|
+
projectId: string;
|
|
30
|
+
working(): void;
|
|
31
|
+
log(message: string, data?: Record<string, unknown>): void;
|
|
32
|
+
}
|
|
33
|
+
export interface TaskDB {
|
|
34
|
+
find(collection: string, query: Record<string, unknown>): Promise<Record<string, unknown>[]>;
|
|
35
|
+
getDoc(collection: string, id: string): Promise<Record<string, unknown> | null>;
|
|
36
|
+
setDoc(collection: string, doc: Record<string, unknown>): Promise<void>;
|
|
37
|
+
setDocFields(collection: string, id: string, fields: Record<string, unknown>): Promise<void>;
|
|
38
|
+
deleteDoc(collection: string, id: string): Promise<void>;
|
|
39
|
+
deleteMany(collection: string, ids: string[]): Promise<void>;
|
|
40
|
+
count(collection: string, query: Record<string, unknown>): Promise<number>;
|
|
41
|
+
}
|
|
42
|
+
export interface TaskStorage {
|
|
43
|
+
get(key: string): Promise<Buffer | null>;
|
|
44
|
+
getJSON<T = unknown>(key: string): Promise<T | null>;
|
|
45
|
+
put(key: string, data: Buffer | string, opts?: {
|
|
46
|
+
contentType?: string;
|
|
47
|
+
}): Promise<string>;
|
|
48
|
+
delete(key: string): Promise<void>;
|
|
49
|
+
list(prefix: string): Promise<string[]>;
|
|
50
|
+
}
|
|
51
|
+
export interface TaskTempStorage {
|
|
52
|
+
get(key: string): Promise<Buffer | null>;
|
|
53
|
+
put(key: string, data: Buffer): Promise<string>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The function signature that every project's task.js must export.
|
|
57
|
+
*/
|
|
58
|
+
export type TaskExecuteFn = (ctx: TaskContext, data: Record<string, unknown>) => Promise<unknown>;
|
|
59
|
+
//# sourceMappingURL=TaskContext.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TaskContext.d.ts","sourceRoot":"","sources":["../../src/worker/TaskContext.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,WAAW;IAE1B,EAAE,EAAE,MAAM,CAAC;IAGX,EAAE,EAAE,WAAW,CAAC;IAGhB,MAAM,EAAE,eAAe,CAAC;IAGxB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAGlB,OAAO,IAAI,IAAI,CAAC;IAGhB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;CAC5D;AAED,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;IAC7F,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IAChF,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7F,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7D,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC5E;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACrD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,IAAI,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1F,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACnC,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACzC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACjD;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,CAC1B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC1B,OAAO,CAAC,OAAO,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TaskContext.js","sourceRoot":"","sources":["../../src/worker/TaskContext.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { TaskDB, TaskStorage, TaskTempStorage } from './TaskContext.js';
|
|
2
|
+
/**
|
|
3
|
+
* NPM packages that task bundles are allowed to install.
|
|
4
|
+
* Any package not on this list is rejected.
|
|
5
|
+
*/
|
|
6
|
+
export declare const ALLOWED_PACKAGES: readonly ["three", "@types/three", "jsdom", "gl", "@ffmpeg-installer/ffmpeg", "fluent-ffmpeg", "sharp", "@anthropic-ai/sdk", "openai", "zod", "date-fns", "lodash", "mediasoup", "mediasoup-client", "ugly-app"];
|
|
7
|
+
/**
|
|
8
|
+
* Configuration for the task host.
|
|
9
|
+
*/
|
|
10
|
+
export interface TaskHostConfig {
|
|
11
|
+
/**
|
|
12
|
+
* Factory to create a scoped DB proxy for a project.
|
|
13
|
+
* The proxy restricts access to the project's own PostgreSQL user/database.
|
|
14
|
+
*/
|
|
15
|
+
createDbProxy(projectId: string): TaskDB;
|
|
16
|
+
/**
|
|
17
|
+
* Factory to create a scoped R2 proxy for a project.
|
|
18
|
+
* Restricts access to the project's own R2 bucket.
|
|
19
|
+
*/
|
|
20
|
+
createR2Proxy(projectId: string): TaskStorage;
|
|
21
|
+
/**
|
|
22
|
+
* Factory to create a temp R2 proxy for a project.
|
|
23
|
+
*/
|
|
24
|
+
createTempR2Proxy(projectId: string): TaskTempStorage;
|
|
25
|
+
/**
|
|
26
|
+
* Fetch a task bundle from R2 by URL.
|
|
27
|
+
* Returns the JavaScript source code as a string.
|
|
28
|
+
*/
|
|
29
|
+
fetchBundle(codeUrl: string): Promise<string>;
|
|
30
|
+
/** Working directory for task bundles. Default: os.tmpdir()/ugly-tasks/ */
|
|
31
|
+
workDir?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* TaskHost — loads project task.js bundles from R2, caches warm processes,
|
|
35
|
+
* and executes tasks with scoped DB/R2 access.
|
|
36
|
+
*/
|
|
37
|
+
export declare class TaskHost {
|
|
38
|
+
private config;
|
|
39
|
+
private cleanupTimer;
|
|
40
|
+
private baseWorkDir;
|
|
41
|
+
constructor(config: TaskHostConfig);
|
|
42
|
+
/**
|
|
43
|
+
* Execute a task using a cached or freshly loaded task bundle.
|
|
44
|
+
*/
|
|
45
|
+
execute(opts: {
|
|
46
|
+
codeUrl: string;
|
|
47
|
+
projectId: string;
|
|
48
|
+
taskId: string;
|
|
49
|
+
taskName: string;
|
|
50
|
+
data?: Record<string, unknown>;
|
|
51
|
+
timeoutMs?: number;
|
|
52
|
+
}): Promise<unknown>;
|
|
53
|
+
/**
|
|
54
|
+
* Get a cached process for this code URL, or create one.
|
|
55
|
+
*/
|
|
56
|
+
private getOrCreateProcess;
|
|
57
|
+
/**
|
|
58
|
+
* Install only allowlisted packages from a package.json.
|
|
59
|
+
*/
|
|
60
|
+
private installAllowedPackages;
|
|
61
|
+
/**
|
|
62
|
+
* Evict idle processes (no invocation for IDLE_TIMEOUT_MS).
|
|
63
|
+
*/
|
|
64
|
+
private cleanupIdle;
|
|
65
|
+
/**
|
|
66
|
+
* Invalidate a cached process (e.g., when buildId changes).
|
|
67
|
+
*/
|
|
68
|
+
invalidate(codeUrl: string): void;
|
|
69
|
+
/**
|
|
70
|
+
* Shutdown — clean up all processes and timers.
|
|
71
|
+
*/
|
|
72
|
+
shutdown(): Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=TaskHost.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TaskHost.d.ts","sourceRoot":"","sources":["../../src/worker/TaskHost.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAe,MAAM,EAAE,WAAW,EAAE,eAAe,EAAiB,MAAM,kBAAkB,CAAC;AAIzG;;;GAGG;AACH,eAAO,MAAM,gBAAgB,kNAanB,CAAC;AAgBX;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAEzC;;;OAGG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,CAAC;IAE9C;;OAEG;IACH,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,eAAe,CAAC;IAEtD;;;OAGG;IACH,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE9C,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,YAAY,CAA+C;IACnE,OAAO,CAAC,WAAW,CAAS;gBAEhB,MAAM,EAAE,cAAc;IASlC;;OAEG;IACG,OAAO,CAAC,IAAI,EAAE;QAClB,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,OAAO,CAAC;IAiCpB;;OAEG;YACW,kBAAkB;IA4ChC;;OAEG;YACW,sBAAsB;IA+BpC;;OAEG;IACH,OAAO,CAAC,WAAW;IAcnB;;OAEG;IACH,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAWjC;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAYhC"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
5
|
+
/**
|
|
6
|
+
* NPM packages that task bundles are allowed to install.
|
|
7
|
+
* Any package not on this list is rejected.
|
|
8
|
+
*/
|
|
9
|
+
export const ALLOWED_PACKAGES = [
|
|
10
|
+
// Rendering
|
|
11
|
+
'three', '@types/three', 'jsdom', 'gl',
|
|
12
|
+
// Video/Audio
|
|
13
|
+
'@ffmpeg-installer/ffmpeg', 'fluent-ffmpeg', 'sharp',
|
|
14
|
+
// AI
|
|
15
|
+
'@anthropic-ai/sdk', 'openai',
|
|
16
|
+
// Data
|
|
17
|
+
'zod', 'date-fns', 'lodash',
|
|
18
|
+
// WebRTC
|
|
19
|
+
'mediasoup', 'mediasoup-client',
|
|
20
|
+
// Framework
|
|
21
|
+
'ugly-app',
|
|
22
|
+
];
|
|
23
|
+
const ALLOWED_SET = new Set(ALLOWED_PACKAGES);
|
|
24
|
+
const processCache = new Map();
|
|
25
|
+
const IDLE_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
|
|
26
|
+
/**
|
|
27
|
+
* TaskHost — loads project task.js bundles from R2, caches warm processes,
|
|
28
|
+
* and executes tasks with scoped DB/R2 access.
|
|
29
|
+
*/
|
|
30
|
+
export class TaskHost {
|
|
31
|
+
config;
|
|
32
|
+
cleanupTimer = null;
|
|
33
|
+
baseWorkDir;
|
|
34
|
+
constructor(config) {
|
|
35
|
+
this.config = config;
|
|
36
|
+
this.baseWorkDir = config.workDir ?? path.join(os.tmpdir(), 'ugly-tasks');
|
|
37
|
+
fs.mkdirSync(this.baseWorkDir, { recursive: true });
|
|
38
|
+
// Periodic cleanup of idle processes
|
|
39
|
+
this.cleanupTimer = setInterval(() => this.cleanupIdle(), 60_000);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Execute a task using a cached or freshly loaded task bundle.
|
|
43
|
+
*/
|
|
44
|
+
async execute(opts) {
|
|
45
|
+
const { codeUrl, projectId, taskId, taskName, data = {} } = opts;
|
|
46
|
+
// Get or create warm process
|
|
47
|
+
const proc = await this.getOrCreateProcess(codeUrl);
|
|
48
|
+
proc.lastUsed = Date.now();
|
|
49
|
+
// Create scoped TaskContext
|
|
50
|
+
const ctx = {
|
|
51
|
+
db: this.config.createDbProxy(projectId),
|
|
52
|
+
r2: this.config.createR2Proxy(projectId),
|
|
53
|
+
tempR2: this.config.createTempR2Proxy(projectId),
|
|
54
|
+
taskId,
|
|
55
|
+
taskName,
|
|
56
|
+
projectId,
|
|
57
|
+
working: () => { },
|
|
58
|
+
log: (message, logData) => {
|
|
59
|
+
console.log(`[Task:${projectId}/${taskName}] ${message}`, logData ?? '');
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
// Execute with timeout
|
|
63
|
+
const timeoutMs = opts.timeoutMs ?? 300_000; // 5 min default
|
|
64
|
+
const result = await Promise.race([
|
|
65
|
+
proc.executeFn(ctx, { taskName, ...data }),
|
|
66
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Task ${taskName} timed out after ${timeoutMs}ms`)), timeoutMs)),
|
|
67
|
+
]);
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Get a cached process for this code URL, or create one.
|
|
72
|
+
*/
|
|
73
|
+
async getOrCreateProcess(codeUrl) {
|
|
74
|
+
const cached = processCache.get(codeUrl);
|
|
75
|
+
if (cached)
|
|
76
|
+
return cached;
|
|
77
|
+
// Fetch bundle
|
|
78
|
+
const source = await this.config.fetchBundle(codeUrl);
|
|
79
|
+
// Create working directory for this bundle
|
|
80
|
+
const hash = codeUrl.replace(/[^a-zA-Z0-9]/g, '_').slice(-64);
|
|
81
|
+
const workDir = path.join(this.baseWorkDir, hash);
|
|
82
|
+
fs.mkdirSync(workDir, { recursive: true });
|
|
83
|
+
// Write bundle to disk
|
|
84
|
+
const bundlePath = path.join(workDir, 'task.mjs');
|
|
85
|
+
fs.writeFileSync(bundlePath, source, 'utf-8');
|
|
86
|
+
// Install allowlisted packages if package.json exists alongside bundle
|
|
87
|
+
const pkgJsonUrl = codeUrl.replace(/task\.js$/, 'package.json');
|
|
88
|
+
try {
|
|
89
|
+
const pkgSource = await this.config.fetchBundle(pkgJsonUrl);
|
|
90
|
+
const pkg = JSON.parse(pkgSource);
|
|
91
|
+
await this.installAllowedPackages(workDir, pkg);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// No package.json — bundle is self-contained
|
|
95
|
+
}
|
|
96
|
+
// Dynamic import the module
|
|
97
|
+
const mod = await import(bundlePath);
|
|
98
|
+
if (typeof mod.execute !== 'function') {
|
|
99
|
+
throw new Error(`Task bundle at ${codeUrl} does not export an execute() function`);
|
|
100
|
+
}
|
|
101
|
+
const proc = {
|
|
102
|
+
codeUrl,
|
|
103
|
+
executeFn: mod.execute,
|
|
104
|
+
lastUsed: Date.now(),
|
|
105
|
+
workDir,
|
|
106
|
+
};
|
|
107
|
+
processCache.set(codeUrl, proc);
|
|
108
|
+
console.log(`[TaskHost] Loaded bundle: ${codeUrl}`);
|
|
109
|
+
return proc;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Install only allowlisted packages from a package.json.
|
|
113
|
+
*/
|
|
114
|
+
async installAllowedPackages(workDir, pkg) {
|
|
115
|
+
if (!pkg.dependencies)
|
|
116
|
+
return;
|
|
117
|
+
const filtered = {};
|
|
118
|
+
for (const [name, version] of Object.entries(pkg.dependencies)) {
|
|
119
|
+
if (ALLOWED_SET.has(name)) {
|
|
120
|
+
filtered[name] = version;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
console.warn(`[TaskHost] Package "${name}" not in allowlist — skipped`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (Object.keys(filtered).length === 0)
|
|
127
|
+
return;
|
|
128
|
+
// Write filtered package.json
|
|
129
|
+
fs.writeFileSync(path.join(workDir, 'package.json'), JSON.stringify({ dependencies: filtered }, null, 2));
|
|
130
|
+
// Install
|
|
131
|
+
const { execSync } = await import('child_process');
|
|
132
|
+
execSync('npm install --production --ignore-scripts', {
|
|
133
|
+
cwd: workDir,
|
|
134
|
+
stdio: 'pipe',
|
|
135
|
+
timeout: 120_000,
|
|
136
|
+
});
|
|
137
|
+
console.log(`[TaskHost] Installed ${Object.keys(filtered).length} allowlisted packages`);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Evict idle processes (no invocation for IDLE_TIMEOUT_MS).
|
|
141
|
+
*/
|
|
142
|
+
cleanupIdle() {
|
|
143
|
+
const now = Date.now();
|
|
144
|
+
for (const [url, proc] of processCache) {
|
|
145
|
+
if (now - proc.lastUsed > IDLE_TIMEOUT_MS) {
|
|
146
|
+
processCache.delete(url);
|
|
147
|
+
// Clean up work directory
|
|
148
|
+
try {
|
|
149
|
+
fs.rmSync(proc.workDir, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
catch { /* ignore */ }
|
|
152
|
+
console.log(`[TaskHost] Evicted idle process: ${url}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Invalidate a cached process (e.g., when buildId changes).
|
|
158
|
+
*/
|
|
159
|
+
invalidate(codeUrl) {
|
|
160
|
+
const proc = processCache.get(codeUrl);
|
|
161
|
+
if (proc) {
|
|
162
|
+
processCache.delete(codeUrl);
|
|
163
|
+
try {
|
|
164
|
+
fs.rmSync(proc.workDir, { recursive: true, force: true });
|
|
165
|
+
}
|
|
166
|
+
catch { /* ignore */ }
|
|
167
|
+
console.log(`[TaskHost] Invalidated: ${codeUrl}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Shutdown — clean up all processes and timers.
|
|
172
|
+
*/
|
|
173
|
+
async shutdown() {
|
|
174
|
+
if (this.cleanupTimer) {
|
|
175
|
+
clearInterval(this.cleanupTimer);
|
|
176
|
+
this.cleanupTimer = null;
|
|
177
|
+
}
|
|
178
|
+
for (const [url, proc] of processCache) {
|
|
179
|
+
processCache.delete(url);
|
|
180
|
+
try {
|
|
181
|
+
fs.rmSync(proc.workDir, { recursive: true, force: true });
|
|
182
|
+
}
|
|
183
|
+
catch { /* ignore */ }
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=TaskHost.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TaskHost.js","sourceRoot":"","sources":["../../src/worker/TaskHost.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/worker/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,WAAW,EACX,MAAM,EACN,WAAW,EACX,eAAe,EACf,aAAa,GACd,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,QAAQ,EACR,gBAAgB,EAChB,KAAK,cAAc,GACpB,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/worker/index.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,QAAQ,EACR,gBAAgB,GAEjB,MAAM,eAAe,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ugly-app",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.241",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/server/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"default": "./dist/webrtc/index.js"
|
|
17
17
|
},
|
|
18
18
|
"./webrtc/server": "./dist/webrtc/server/index.js",
|
|
19
|
+
"./worker": "./dist/worker/index.js",
|
|
19
20
|
"./vite": "./dist/vite/index.js",
|
|
20
21
|
"./eslint": "./dist/eslint/index.js"
|
|
21
22
|
},
|
package/src/cli/build.ts
CHANGED
|
@@ -44,6 +44,29 @@ export const buildId = '${buildId}';
|
|
|
44
44
|
);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
// Bundle task.ts if it exists (for cron/worker/GPU tasks)
|
|
48
|
+
const taskEntry = path.join(process.cwd(), 'server', 'task.ts');
|
|
49
|
+
if (fs.existsSync(taskEntry)) {
|
|
50
|
+
console.log('[Build] Bundling server/task.ts...');
|
|
51
|
+
const tasksOutDir = path.join(distDir, 'tasks');
|
|
52
|
+
fs.mkdirSync(tasksOutDir, { recursive: true });
|
|
53
|
+
// Externalize allowlisted packages — they'll be installed on the task host
|
|
54
|
+
const externals = [
|
|
55
|
+
'ugly-app', 'ugly-app/*',
|
|
56
|
+
'three', '@types/three', 'jsdom', 'gl',
|
|
57
|
+
'@ffmpeg-installer/ffmpeg', 'fluent-ffmpeg', 'sharp',
|
|
58
|
+
'@anthropic-ai/sdk', 'openai',
|
|
59
|
+
'zod', 'date-fns', 'lodash',
|
|
60
|
+
'mediasoup', 'mediasoup-client',
|
|
61
|
+
];
|
|
62
|
+
const externalFlags = externals.map(e => `--external:${e}`).join(' ');
|
|
63
|
+
execSync(
|
|
64
|
+
`npx esbuild server/task.ts --bundle --platform=node --format=esm --outfile=dist/tasks/task.js ${externalFlags}`,
|
|
65
|
+
{ stdio: 'inherit', cwd: process.cwd() },
|
|
66
|
+
);
|
|
67
|
+
console.log('[Build] Task bundle: dist/tasks/task.js');
|
|
68
|
+
}
|
|
69
|
+
|
|
47
70
|
// Run tsc for typecheck
|
|
48
71
|
console.log('[Build] Running tsc...');
|
|
49
72
|
execSync('npx tsc --noEmit', { stdio: 'inherit', cwd: process.cwd() });
|
package/src/cli/version.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by prebuild — do not edit manually
|
|
2
|
-
export const CLI_VERSION = "0.1.
|
|
2
|
+
export const CLI_VERSION = "0.1.241";
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskContext — the execution environment for project task bundles.
|
|
3
|
+
*
|
|
4
|
+
* Every project's `task.js` exports an `execute(ctx, data)` function.
|
|
5
|
+
* The platform creates a TaskContext scoped to the project's own DB and R2,
|
|
6
|
+
* then calls execute(). The project never needs DB credentials or R2 keys
|
|
7
|
+
* — the platform provides them through this interface.
|
|
8
|
+
*
|
|
9
|
+
* ```typescript
|
|
10
|
+
* // server/task.ts (project source)
|
|
11
|
+
* import type { TaskContext } from 'ugly-app/worker';
|
|
12
|
+
*
|
|
13
|
+
* export async function execute(ctx: TaskContext, data: { taskName: string }) {
|
|
14
|
+
* switch (data.taskName) {
|
|
15
|
+
* case 'dailyCleanup':
|
|
16
|
+
* const old = await ctx.db.find('todo', { done: true, updated: { $lt: daysAgo(30) } });
|
|
17
|
+
* for (const doc of old) await ctx.db.deleteDoc('todo', doc._id);
|
|
18
|
+
* break;
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export interface TaskContext {
|
|
24
|
+
// ─── Database — project's isolated PostgreSQL ────────────────────────
|
|
25
|
+
db: TaskDB;
|
|
26
|
+
|
|
27
|
+
// ─── R2 storage — project's own bucket (ugly-{projectId}) ───────────
|
|
28
|
+
r2: TaskStorage;
|
|
29
|
+
|
|
30
|
+
// ─── Temp R2 — for large intermediate blobs (auto-cleaned) ──────────
|
|
31
|
+
tempR2: TaskTempStorage;
|
|
32
|
+
|
|
33
|
+
// ─── Task metadata ──────────────────────────────────────────────────
|
|
34
|
+
taskId: string;
|
|
35
|
+
taskName: string;
|
|
36
|
+
projectId: string;
|
|
37
|
+
|
|
38
|
+
// ─── For long-running tasks: extend ack deadline ────────────────────
|
|
39
|
+
working(): void;
|
|
40
|
+
|
|
41
|
+
// ─── Logging ────────────────────────────────────────────────────────
|
|
42
|
+
log(message: string, data?: Record<string, unknown>): void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TaskDB {
|
|
46
|
+
find(collection: string, query: Record<string, unknown>): Promise<Record<string, unknown>[]>;
|
|
47
|
+
getDoc(collection: string, id: string): Promise<Record<string, unknown> | null>;
|
|
48
|
+
setDoc(collection: string, doc: Record<string, unknown>): Promise<void>;
|
|
49
|
+
setDocFields(collection: string, id: string, fields: Record<string, unknown>): Promise<void>;
|
|
50
|
+
deleteDoc(collection: string, id: string): Promise<void>;
|
|
51
|
+
deleteMany(collection: string, ids: string[]): Promise<void>;
|
|
52
|
+
count(collection: string, query: Record<string, unknown>): Promise<number>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface TaskStorage {
|
|
56
|
+
get(key: string): Promise<Buffer | null>;
|
|
57
|
+
getJSON<T = unknown>(key: string): Promise<T | null>;
|
|
58
|
+
put(key: string, data: Buffer | string, opts?: { contentType?: string }): Promise<string>;
|
|
59
|
+
delete(key: string): Promise<void>;
|
|
60
|
+
list(prefix: string): Promise<string[]>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface TaskTempStorage {
|
|
64
|
+
get(key: string): Promise<Buffer | null>;
|
|
65
|
+
put(key: string, data: Buffer): Promise<string>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The function signature that every project's task.js must export.
|
|
70
|
+
*/
|
|
71
|
+
export type TaskExecuteFn = (
|
|
72
|
+
ctx: TaskContext,
|
|
73
|
+
data: Record<string, unknown>,
|
|
74
|
+
) => Promise<unknown>;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import type { TaskContext, TaskDB, TaskStorage, TaskTempStorage, TaskExecuteFn } from './TaskContext.js';
|
|
6
|
+
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* NPM packages that task bundles are allowed to install.
|
|
11
|
+
* Any package not on this list is rejected.
|
|
12
|
+
*/
|
|
13
|
+
export const ALLOWED_PACKAGES = [
|
|
14
|
+
// Rendering
|
|
15
|
+
'three', '@types/three', 'jsdom', 'gl',
|
|
16
|
+
// Video/Audio
|
|
17
|
+
'@ffmpeg-installer/ffmpeg', 'fluent-ffmpeg', 'sharp',
|
|
18
|
+
// AI
|
|
19
|
+
'@anthropic-ai/sdk', 'openai',
|
|
20
|
+
// Data
|
|
21
|
+
'zod', 'date-fns', 'lodash',
|
|
22
|
+
// WebRTC
|
|
23
|
+
'mediasoup', 'mediasoup-client',
|
|
24
|
+
// Framework
|
|
25
|
+
'ugly-app',
|
|
26
|
+
] as const;
|
|
27
|
+
|
|
28
|
+
const ALLOWED_SET = new Set<string>(ALLOWED_PACKAGES);
|
|
29
|
+
|
|
30
|
+
// ─── Warm process cache ────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
interface CachedProcess {
|
|
33
|
+
codeUrl: string;
|
|
34
|
+
executeFn: TaskExecuteFn;
|
|
35
|
+
lastUsed: number;
|
|
36
|
+
workDir: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const processCache = new Map<string, CachedProcess>();
|
|
40
|
+
const IDLE_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Configuration for the task host.
|
|
44
|
+
*/
|
|
45
|
+
export interface TaskHostConfig {
|
|
46
|
+
/**
|
|
47
|
+
* Factory to create a scoped DB proxy for a project.
|
|
48
|
+
* The proxy restricts access to the project's own PostgreSQL user/database.
|
|
49
|
+
*/
|
|
50
|
+
createDbProxy(projectId: string): TaskDB;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Factory to create a scoped R2 proxy for a project.
|
|
54
|
+
* Restricts access to the project's own R2 bucket.
|
|
55
|
+
*/
|
|
56
|
+
createR2Proxy(projectId: string): TaskStorage;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Factory to create a temp R2 proxy for a project.
|
|
60
|
+
*/
|
|
61
|
+
createTempR2Proxy(projectId: string): TaskTempStorage;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Fetch a task bundle from R2 by URL.
|
|
65
|
+
* Returns the JavaScript source code as a string.
|
|
66
|
+
*/
|
|
67
|
+
fetchBundle(codeUrl: string): Promise<string>;
|
|
68
|
+
|
|
69
|
+
/** Working directory for task bundles. Default: os.tmpdir()/ugly-tasks/ */
|
|
70
|
+
workDir?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* TaskHost — loads project task.js bundles from R2, caches warm processes,
|
|
75
|
+
* and executes tasks with scoped DB/R2 access.
|
|
76
|
+
*/
|
|
77
|
+
export class TaskHost {
|
|
78
|
+
private config: TaskHostConfig;
|
|
79
|
+
private cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
|
80
|
+
private baseWorkDir: string;
|
|
81
|
+
|
|
82
|
+
constructor(config: TaskHostConfig) {
|
|
83
|
+
this.config = config;
|
|
84
|
+
this.baseWorkDir = config.workDir ?? path.join(os.tmpdir(), 'ugly-tasks');
|
|
85
|
+
fs.mkdirSync(this.baseWorkDir, { recursive: true });
|
|
86
|
+
|
|
87
|
+
// Periodic cleanup of idle processes
|
|
88
|
+
this.cleanupTimer = setInterval(() => this.cleanupIdle(), 60_000);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Execute a task using a cached or freshly loaded task bundle.
|
|
93
|
+
*/
|
|
94
|
+
async execute(opts: {
|
|
95
|
+
codeUrl: string;
|
|
96
|
+
projectId: string;
|
|
97
|
+
taskId: string;
|
|
98
|
+
taskName: string;
|
|
99
|
+
data?: Record<string, unknown>;
|
|
100
|
+
timeoutMs?: number;
|
|
101
|
+
}): Promise<unknown> {
|
|
102
|
+
const { codeUrl, projectId, taskId, taskName, data = {} } = opts;
|
|
103
|
+
|
|
104
|
+
// Get or create warm process
|
|
105
|
+
const proc = await this.getOrCreateProcess(codeUrl);
|
|
106
|
+
proc.lastUsed = Date.now();
|
|
107
|
+
|
|
108
|
+
// Create scoped TaskContext
|
|
109
|
+
const ctx: TaskContext = {
|
|
110
|
+
db: this.config.createDbProxy(projectId),
|
|
111
|
+
r2: this.config.createR2Proxy(projectId),
|
|
112
|
+
tempR2: this.config.createTempR2Proxy(projectId),
|
|
113
|
+
taskId,
|
|
114
|
+
taskName,
|
|
115
|
+
projectId,
|
|
116
|
+
working: () => { /* extend deadline — platform manages this */ },
|
|
117
|
+
log: (message, logData) => {
|
|
118
|
+
console.log(`[Task:${projectId}/${taskName}] ${message}`, logData ?? '');
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Execute with timeout
|
|
123
|
+
const timeoutMs = opts.timeoutMs ?? 300_000; // 5 min default
|
|
124
|
+
const result = await Promise.race([
|
|
125
|
+
proc.executeFn(ctx, { taskName, ...data }),
|
|
126
|
+
new Promise((_, reject) =>
|
|
127
|
+
setTimeout(() => reject(new Error(`Task ${taskName} timed out after ${timeoutMs}ms`)), timeoutMs),
|
|
128
|
+
),
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get a cached process for this code URL, or create one.
|
|
136
|
+
*/
|
|
137
|
+
private async getOrCreateProcess(codeUrl: string): Promise<CachedProcess> {
|
|
138
|
+
const cached = processCache.get(codeUrl);
|
|
139
|
+
if (cached) return cached;
|
|
140
|
+
|
|
141
|
+
// Fetch bundle
|
|
142
|
+
const source = await this.config.fetchBundle(codeUrl);
|
|
143
|
+
|
|
144
|
+
// Create working directory for this bundle
|
|
145
|
+
const hash = codeUrl.replace(/[^a-zA-Z0-9]/g, '_').slice(-64);
|
|
146
|
+
const workDir = path.join(this.baseWorkDir, hash);
|
|
147
|
+
fs.mkdirSync(workDir, { recursive: true });
|
|
148
|
+
|
|
149
|
+
// Write bundle to disk
|
|
150
|
+
const bundlePath = path.join(workDir, 'task.mjs');
|
|
151
|
+
fs.writeFileSync(bundlePath, source, 'utf-8');
|
|
152
|
+
|
|
153
|
+
// Install allowlisted packages if package.json exists alongside bundle
|
|
154
|
+
const pkgJsonUrl = codeUrl.replace(/task\.js$/, 'package.json');
|
|
155
|
+
try {
|
|
156
|
+
const pkgSource = await this.config.fetchBundle(pkgJsonUrl);
|
|
157
|
+
const pkg = JSON.parse(pkgSource);
|
|
158
|
+
await this.installAllowedPackages(workDir, pkg);
|
|
159
|
+
} catch {
|
|
160
|
+
// No package.json — bundle is self-contained
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Dynamic import the module
|
|
164
|
+
const mod = await import(bundlePath) as { execute?: TaskExecuteFn };
|
|
165
|
+
if (typeof mod.execute !== 'function') {
|
|
166
|
+
throw new Error(`Task bundle at ${codeUrl} does not export an execute() function`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const proc: CachedProcess = {
|
|
170
|
+
codeUrl,
|
|
171
|
+
executeFn: mod.execute,
|
|
172
|
+
lastUsed: Date.now(),
|
|
173
|
+
workDir,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
processCache.set(codeUrl, proc);
|
|
177
|
+
console.log(`[TaskHost] Loaded bundle: ${codeUrl}`);
|
|
178
|
+
return proc;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Install only allowlisted packages from a package.json.
|
|
183
|
+
*/
|
|
184
|
+
private async installAllowedPackages(workDir: string, pkg: { dependencies?: Record<string, string> }): Promise<void> {
|
|
185
|
+
if (!pkg.dependencies) return;
|
|
186
|
+
|
|
187
|
+
const filtered: Record<string, string> = {};
|
|
188
|
+
for (const [name, version] of Object.entries(pkg.dependencies)) {
|
|
189
|
+
if (ALLOWED_SET.has(name)) {
|
|
190
|
+
filtered[name] = version;
|
|
191
|
+
} else {
|
|
192
|
+
console.warn(`[TaskHost] Package "${name}" not in allowlist — skipped`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (Object.keys(filtered).length === 0) return;
|
|
197
|
+
|
|
198
|
+
// Write filtered package.json
|
|
199
|
+
fs.writeFileSync(
|
|
200
|
+
path.join(workDir, 'package.json'),
|
|
201
|
+
JSON.stringify({ dependencies: filtered }, null, 2),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
// Install
|
|
205
|
+
const { execSync } = await import('child_process');
|
|
206
|
+
execSync('npm install --production --ignore-scripts', {
|
|
207
|
+
cwd: workDir,
|
|
208
|
+
stdio: 'pipe',
|
|
209
|
+
timeout: 120_000,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
console.log(`[TaskHost] Installed ${Object.keys(filtered).length} allowlisted packages`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Evict idle processes (no invocation for IDLE_TIMEOUT_MS).
|
|
217
|
+
*/
|
|
218
|
+
private cleanupIdle(): void {
|
|
219
|
+
const now = Date.now();
|
|
220
|
+
for (const [url, proc] of processCache) {
|
|
221
|
+
if (now - proc.lastUsed > IDLE_TIMEOUT_MS) {
|
|
222
|
+
processCache.delete(url);
|
|
223
|
+
// Clean up work directory
|
|
224
|
+
try {
|
|
225
|
+
fs.rmSync(proc.workDir, { recursive: true, force: true });
|
|
226
|
+
} catch { /* ignore */ }
|
|
227
|
+
console.log(`[TaskHost] Evicted idle process: ${url}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Invalidate a cached process (e.g., when buildId changes).
|
|
234
|
+
*/
|
|
235
|
+
invalidate(codeUrl: string): void {
|
|
236
|
+
const proc = processCache.get(codeUrl);
|
|
237
|
+
if (proc) {
|
|
238
|
+
processCache.delete(codeUrl);
|
|
239
|
+
try {
|
|
240
|
+
fs.rmSync(proc.workDir, { recursive: true, force: true });
|
|
241
|
+
} catch { /* ignore */ }
|
|
242
|
+
console.log(`[TaskHost] Invalidated: ${codeUrl}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Shutdown — clean up all processes and timers.
|
|
248
|
+
*/
|
|
249
|
+
async shutdown(): Promise<void> {
|
|
250
|
+
if (this.cleanupTimer) {
|
|
251
|
+
clearInterval(this.cleanupTimer);
|
|
252
|
+
this.cleanupTimer = null;
|
|
253
|
+
}
|
|
254
|
+
for (const [url, proc] of processCache) {
|
|
255
|
+
processCache.delete(url);
|
|
256
|
+
try {
|
|
257
|
+
fs.rmSync(proc.workDir, { recursive: true, force: true });
|
|
258
|
+
} catch { /* ignore */ }
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|