@apisurf/ngn-core 0.2.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/LICENSE +661 -0
- package/README.md +13 -0
- package/dist/core.d.ts +292 -0
- package/dist/core.js +1217 -0
- package/package.json +54 -0
package/dist/core.js
ADDED
|
@@ -0,0 +1,1217 @@
|
|
|
1
|
+
// src/source/compiler.ts
|
|
2
|
+
import invariant from "tiny-invariant";
|
|
3
|
+
import * as esbuild from "esbuild";
|
|
4
|
+
var Compiler = class {
|
|
5
|
+
options;
|
|
6
|
+
externalizePlugin;
|
|
7
|
+
constructor(options = {}) {
|
|
8
|
+
this.options = {
|
|
9
|
+
minify: options.minify ?? false,
|
|
10
|
+
sourcemap: options.sourcemap ?? false,
|
|
11
|
+
silent: options.silent ?? true,
|
|
12
|
+
// bundle all ngn packages by default
|
|
13
|
+
noExternal: options.noExternal ?? [/^ngn-(.*)/, /^@apisurf\/(.*)/]
|
|
14
|
+
};
|
|
15
|
+
this.externalizePlugin = this.createExternalizePlugin();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Creates an esbuild plugin that externalizes all bare package imports
|
|
19
|
+
* except for those matching the noExternal patterns.
|
|
20
|
+
* This approach automatically handles all dependencies (including transitive ones)
|
|
21
|
+
* without needing to maintain a list or read package.json.
|
|
22
|
+
*
|
|
23
|
+
* NOTE: There are esbuild config options that can be used to externalize dependencies
|
|
24
|
+
* but they are not as flexible as this plugin. This plugin is more flexible and
|
|
25
|
+
* can be used to externalize dependencies that are not in the package.json(alternative approach).
|
|
26
|
+
* Previous implementations used tsup which did this by default or used a list of
|
|
27
|
+
* dependencies to externalize. This approach externalizes everything dynamically.
|
|
28
|
+
* Problematic dependencies were: playwright, playwright-core, chromium-bidi, etc.
|
|
29
|
+
*/
|
|
30
|
+
createExternalizePlugin() {
|
|
31
|
+
const noExternalPatterns = this.options.noExternal || [];
|
|
32
|
+
return {
|
|
33
|
+
name: "externalize-deps",
|
|
34
|
+
setup(build2) {
|
|
35
|
+
build2.onResolve({ filter: /^[^./]/ }, (args) => {
|
|
36
|
+
const parts = args.path.split("/");
|
|
37
|
+
const pkgName = args.path.startsWith("@") ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
38
|
+
const shouldBundle = noExternalPatterns.some((pattern) => {
|
|
39
|
+
if (typeof pattern === "string") {
|
|
40
|
+
return pkgName === pattern || args.path === pattern;
|
|
41
|
+
}
|
|
42
|
+
return pattern.test(pkgName) || pattern.test(args.path);
|
|
43
|
+
});
|
|
44
|
+
if (shouldBundle) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return { path: args.path, external: true };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Compile entry files to in-memory strings (no disk I/O)
|
|
54
|
+
* @param entry - Array of input paths
|
|
55
|
+
* @returns Map of input file path to compiled code string
|
|
56
|
+
*/
|
|
57
|
+
async compile(entry) {
|
|
58
|
+
invariant(entry, "Entry file(s) not provided");
|
|
59
|
+
const compiled = /* @__PURE__ */ new Map();
|
|
60
|
+
for (const entryFile of entry) {
|
|
61
|
+
const result = await esbuild.build({
|
|
62
|
+
entryPoints: [entryFile],
|
|
63
|
+
bundle: true,
|
|
64
|
+
write: false,
|
|
65
|
+
// Output as IIFE (Immediately Invoked Function Expression) so it can be
|
|
66
|
+
// executed with vm.Script in a sandboxed context. The globalName specifies
|
|
67
|
+
// which variable the exports are assigned to: var __exports = (() => { ... })();
|
|
68
|
+
format: "iife",
|
|
69
|
+
globalName: "__exports",
|
|
70
|
+
platform: "node",
|
|
71
|
+
target: "node20",
|
|
72
|
+
minify: this.options.minify,
|
|
73
|
+
sourcemap: this.options.sourcemap ? "inline" : false,
|
|
74
|
+
plugins: [this.externalizePlugin],
|
|
75
|
+
logLevel: this.options.silent ? "silent" : "info"
|
|
76
|
+
});
|
|
77
|
+
compiled.set(entryFile, result.outputFiles[0].text);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
compiled
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Compile source code from memory (no disk I/O required)
|
|
85
|
+
* @param sourceCode - The source code string to compile
|
|
86
|
+
* @param virtualPath - A virtual file path for the source (used for resolve directory and loader detection)
|
|
87
|
+
* @returns The compiled code string
|
|
88
|
+
*/
|
|
89
|
+
async compileFromSource(sourceCode, virtualPath) {
|
|
90
|
+
invariant(sourceCode, "Source code not provided");
|
|
91
|
+
invariant(virtualPath, "Virtual path not provided");
|
|
92
|
+
const ext = virtualPath.split(".").pop()?.toLowerCase();
|
|
93
|
+
const loader = ext === "ts" ? "ts" : ext === "tsx" ? "tsx" : ext === "jsx" ? "jsx" : "js";
|
|
94
|
+
const result = await esbuild.build({
|
|
95
|
+
stdin: {
|
|
96
|
+
contents: sourceCode,
|
|
97
|
+
resolveDir: process.cwd(),
|
|
98
|
+
sourcefile: virtualPath,
|
|
99
|
+
loader
|
|
100
|
+
},
|
|
101
|
+
bundle: true,
|
|
102
|
+
write: false,
|
|
103
|
+
// Output as IIFE (Immediately Invoked Function Expression) so it can be
|
|
104
|
+
// executed with vm.Script in a sandboxed context. The globalName specifies
|
|
105
|
+
// which variable the exports are assigned to: var __exports = (() => { ... })();
|
|
106
|
+
format: "iife",
|
|
107
|
+
globalName: "__exports",
|
|
108
|
+
platform: "node",
|
|
109
|
+
target: "node20",
|
|
110
|
+
minify: this.options.minify,
|
|
111
|
+
sourcemap: this.options.sourcemap ? "inline" : false,
|
|
112
|
+
plugins: [this.externalizePlugin],
|
|
113
|
+
logLevel: this.options.silent ? "silent" : "info"
|
|
114
|
+
});
|
|
115
|
+
return result.outputFiles[0].text;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// src/runtime/vm.ts
|
|
120
|
+
import { Script, createContext } from "node:vm";
|
|
121
|
+
import { createRequire } from "node:module";
|
|
122
|
+
var buildGlobals = () => Object.fromEntries(
|
|
123
|
+
Object.getOwnPropertyNames(globalThis).map((key) => [
|
|
124
|
+
key,
|
|
125
|
+
globalThis[key]
|
|
126
|
+
])
|
|
127
|
+
);
|
|
128
|
+
async function executeEsm(code) {
|
|
129
|
+
const requireResolutionRoot = `${process.cwd()}/`;
|
|
130
|
+
const script = new Script(`${code}
|
|
131
|
+
__exports;`);
|
|
132
|
+
const sandbox = createContext({
|
|
133
|
+
...buildGlobals(),
|
|
134
|
+
require: createRequire(requireResolutionRoot),
|
|
135
|
+
// Provide CommonJS-style globals for bundled dependencies that expect them
|
|
136
|
+
// (e.g., node-cron uses __dirname for its daemon path)
|
|
137
|
+
__dirname: process.cwd(),
|
|
138
|
+
__filename: `${process.cwd()}/vm-script.js`
|
|
139
|
+
});
|
|
140
|
+
return script.runInContext(sandbox);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/runtime/task.ts
|
|
144
|
+
import invariant5 from "tiny-invariant";
|
|
145
|
+
|
|
146
|
+
// src/source/entry.ts
|
|
147
|
+
import invariant4 from "tiny-invariant";
|
|
148
|
+
|
|
149
|
+
// src/source/entryLiveSource.ts
|
|
150
|
+
import { createHash } from "node:crypto";
|
|
151
|
+
import invariant2 from "tiny-invariant";
|
|
152
|
+
var EntryLiveSource = class {
|
|
153
|
+
_code;
|
|
154
|
+
codeMd5Hash = null;
|
|
155
|
+
onTaskCodeLoaded;
|
|
156
|
+
paths;
|
|
157
|
+
_fileDescriptor;
|
|
158
|
+
_tasksRootDir;
|
|
159
|
+
constructor(options, config) {
|
|
160
|
+
invariant2(options.livePath, "Live task path not provided");
|
|
161
|
+
invariant2(options.code, "Code not provided");
|
|
162
|
+
this._code = options.code;
|
|
163
|
+
this.paths = {
|
|
164
|
+
compiled: options.livePath,
|
|
165
|
+
source: options.livePath,
|
|
166
|
+
relativeEntry: "",
|
|
167
|
+
relativeParent: ""
|
|
168
|
+
};
|
|
169
|
+
this._fileDescriptor = {
|
|
170
|
+
type: "file",
|
|
171
|
+
path: {
|
|
172
|
+
absolute: options.livePath,
|
|
173
|
+
relative: options.livePath,
|
|
174
|
+
parent: {
|
|
175
|
+
absolute: "",
|
|
176
|
+
relative: ""
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
ext: ".ts",
|
|
180
|
+
filename: options.livePath,
|
|
181
|
+
filenameNoExt: options.livePath.replace(/\.[^.]+$/, "")
|
|
182
|
+
};
|
|
183
|
+
this._tasksRootDir = "";
|
|
184
|
+
this.onTaskCodeLoaded = config.onTaskCodeLoaded;
|
|
185
|
+
}
|
|
186
|
+
get path() {
|
|
187
|
+
return this.paths;
|
|
188
|
+
}
|
|
189
|
+
get fileDescriptor() {
|
|
190
|
+
return this._fileDescriptor;
|
|
191
|
+
}
|
|
192
|
+
get tasksRootDir() {
|
|
193
|
+
return this._tasksRootDir;
|
|
194
|
+
}
|
|
195
|
+
get codeHash() {
|
|
196
|
+
return this.codeMd5Hash;
|
|
197
|
+
}
|
|
198
|
+
get code() {
|
|
199
|
+
return this._code;
|
|
200
|
+
}
|
|
201
|
+
async load() {
|
|
202
|
+
if (this.isLoaded()) {
|
|
203
|
+
return this.codeMd5Hash;
|
|
204
|
+
}
|
|
205
|
+
this.codeMd5Hash = createHash("md5").update(this.code).digest("hex");
|
|
206
|
+
console.log(`Loading live task from: ${this.paths.compiled}, hash: ${this.codeMd5Hash}`);
|
|
207
|
+
console.log("Code hash:", this.codeMd5Hash);
|
|
208
|
+
this.onTaskCodeLoaded?.(this.codeMd5Hash);
|
|
209
|
+
return this.codeMd5Hash;
|
|
210
|
+
}
|
|
211
|
+
isLoaded() {
|
|
212
|
+
return Boolean(this.code && this.codeMd5Hash);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/source/entryDbSource.ts
|
|
217
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
218
|
+
import invariant3 from "tiny-invariant";
|
|
219
|
+
var EntryDbSource = class {
|
|
220
|
+
_code;
|
|
221
|
+
codeMd5Hash = null;
|
|
222
|
+
onTaskCodeLoaded;
|
|
223
|
+
paths;
|
|
224
|
+
_fileDescriptor;
|
|
225
|
+
_tasksRootDir;
|
|
226
|
+
constructor(options, config) {
|
|
227
|
+
invariant3(options.compiledCode, "Compiled code not provided");
|
|
228
|
+
invariant3(options.descriptor, "File descriptor not provided");
|
|
229
|
+
invariant3(options.tasksRootDir, "Tasks root directory not provided");
|
|
230
|
+
this._code = options.compiledCode;
|
|
231
|
+
this._fileDescriptor = options.descriptor;
|
|
232
|
+
this._tasksRootDir = options.tasksRootDir;
|
|
233
|
+
this.paths = {
|
|
234
|
+
// Source path is the original TypeScript file
|
|
235
|
+
source: options.descriptor.path.absolute,
|
|
236
|
+
// Compiled path is no longer a file path - we use the relative entry as identifier
|
|
237
|
+
compiled: options.descriptor.path.relative,
|
|
238
|
+
relativeEntry: options.descriptor.path.relative,
|
|
239
|
+
relativeParent: options.descriptor.path.parent.relative
|
|
240
|
+
};
|
|
241
|
+
this.onTaskCodeLoaded = config.onTaskCodeLoaded;
|
|
242
|
+
}
|
|
243
|
+
get path() {
|
|
244
|
+
return this.paths;
|
|
245
|
+
}
|
|
246
|
+
get fileDescriptor() {
|
|
247
|
+
return this._fileDescriptor;
|
|
248
|
+
}
|
|
249
|
+
get tasksRootDir() {
|
|
250
|
+
return this._tasksRootDir;
|
|
251
|
+
}
|
|
252
|
+
get codeHash() {
|
|
253
|
+
return this.codeMd5Hash;
|
|
254
|
+
}
|
|
255
|
+
get code() {
|
|
256
|
+
return this._code;
|
|
257
|
+
}
|
|
258
|
+
async load() {
|
|
259
|
+
if (this.isLoaded()) {
|
|
260
|
+
return this.codeMd5Hash;
|
|
261
|
+
}
|
|
262
|
+
this.codeMd5Hash = createHash2("md5").update(this._code).digest("hex");
|
|
263
|
+
this.onTaskCodeLoaded?.(this.codeMd5Hash);
|
|
264
|
+
return this.codeMd5Hash;
|
|
265
|
+
}
|
|
266
|
+
isLoaded() {
|
|
267
|
+
return Boolean(this._code && this.codeMd5Hash);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// src/source/util.ts
|
|
272
|
+
function isLiveSourceParams(params) {
|
|
273
|
+
return "livePath" in params && "code" in params;
|
|
274
|
+
}
|
|
275
|
+
function isDbSourceParams(params) {
|
|
276
|
+
return "compiledCode" in params && "descriptor" in params;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/source/entry.ts
|
|
280
|
+
var Entry = class {
|
|
281
|
+
entry;
|
|
282
|
+
callbacks;
|
|
283
|
+
constructor(options, config) {
|
|
284
|
+
this.callbacks = {
|
|
285
|
+
onTaskCodeLoaded: config.onTaskCodeLoaded ?? void 0
|
|
286
|
+
};
|
|
287
|
+
if (isLiveSourceParams(options)) {
|
|
288
|
+
this.entry = new EntryLiveSource(options, config);
|
|
289
|
+
} else if (isDbSourceParams(options)) {
|
|
290
|
+
this.entry = new EntryDbSource(options, config);
|
|
291
|
+
} else {
|
|
292
|
+
throw new TypeError("Invalid source parameters");
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async load() {
|
|
296
|
+
return this.entry.load();
|
|
297
|
+
}
|
|
298
|
+
isLoaded() {
|
|
299
|
+
return this.entry.isLoaded();
|
|
300
|
+
}
|
|
301
|
+
async getExports() {
|
|
302
|
+
invariant4(this.entry.code, "Entry code not loaded. Load entry first.");
|
|
303
|
+
try {
|
|
304
|
+
const taskExports = await executeEsm(this.entry.code);
|
|
305
|
+
this.callbacks.onTaskCodeLoaded?.(this.entry.codeHash);
|
|
306
|
+
let taskFunction;
|
|
307
|
+
if (typeof taskExports.default === "function") {
|
|
308
|
+
taskFunction = taskExports.default;
|
|
309
|
+
} else if (taskExports.default && typeof taskExports.default === "object" && "task" in taskExports.default) {
|
|
310
|
+
taskFunction = taskExports.default.task;
|
|
311
|
+
} else {
|
|
312
|
+
taskFunction = taskExports.task;
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
task: taskFunction,
|
|
316
|
+
timing: taskExports.timing,
|
|
317
|
+
shouldSkip: taskExports.shouldSkip,
|
|
318
|
+
shouldRetry: taskExports.shouldRetry,
|
|
319
|
+
onSuccess: taskExports.onSuccess,
|
|
320
|
+
onError: taskExports.onError,
|
|
321
|
+
onComplete: taskExports.onComplete
|
|
322
|
+
};
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error("Error getting task exports:", error);
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
get paths() {
|
|
329
|
+
return this.entry.path;
|
|
330
|
+
}
|
|
331
|
+
get fileDescriptor() {
|
|
332
|
+
return this.entry.fileDescriptor;
|
|
333
|
+
}
|
|
334
|
+
get tasksRootDir() {
|
|
335
|
+
return this.entry.tasksRootDir;
|
|
336
|
+
}
|
|
337
|
+
get codeHash() {
|
|
338
|
+
return this.entry.codeHash;
|
|
339
|
+
}
|
|
340
|
+
get code() {
|
|
341
|
+
return this.entry.code;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
// src/runtime/task.ts
|
|
346
|
+
var Task = class {
|
|
347
|
+
taskCallbacks;
|
|
348
|
+
runtimeCallbacks;
|
|
349
|
+
env;
|
|
350
|
+
kv;
|
|
351
|
+
log;
|
|
352
|
+
timing;
|
|
353
|
+
sqlite;
|
|
354
|
+
meta;
|
|
355
|
+
entry = null;
|
|
356
|
+
entryExports = null;
|
|
357
|
+
constructor({
|
|
358
|
+
entryParams,
|
|
359
|
+
buildConfigFn
|
|
360
|
+
}) {
|
|
361
|
+
invariant5(entryParams, "Entry params not provided! Cannot create task.");
|
|
362
|
+
this.entry = new Entry(entryParams, {
|
|
363
|
+
onTaskCodeLoaded: this.taskCallbacks?.onTaskCodeLoaded
|
|
364
|
+
});
|
|
365
|
+
const { taskCallbacks, runtimeCallbacks, env, kv, log, timing, sqlite } = buildConfigFn({
|
|
366
|
+
sourcePath: this.entry.paths.source,
|
|
367
|
+
compiledPath: this.entry.paths.compiled
|
|
368
|
+
});
|
|
369
|
+
this.taskCallbacks = taskCallbacks;
|
|
370
|
+
this.runtimeCallbacks = runtimeCallbacks;
|
|
371
|
+
this.sqlite = sqlite;
|
|
372
|
+
this.meta = {
|
|
373
|
+
fileTaskId: -1,
|
|
374
|
+
fileTaskVersionId: -1,
|
|
375
|
+
file: this.entry.fileDescriptor,
|
|
376
|
+
tasksRootDir: this.entry.tasksRootDir
|
|
377
|
+
};
|
|
378
|
+
this.env = env;
|
|
379
|
+
this.kv = kv;
|
|
380
|
+
this.log = log;
|
|
381
|
+
this.timing = timing;
|
|
382
|
+
}
|
|
383
|
+
get sourcePath() {
|
|
384
|
+
invariant5(this.entry, "Entry not initialized.");
|
|
385
|
+
return this.entry.paths.source;
|
|
386
|
+
}
|
|
387
|
+
get compiledPath() {
|
|
388
|
+
invariant5(this.entry, "Entry not initialized.");
|
|
389
|
+
return this.entry.paths.compiled;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Unique identifier for the task - the relative entry path
|
|
393
|
+
*/
|
|
394
|
+
get taskPath() {
|
|
395
|
+
invariant5(this.entry, "Entry not initialized.");
|
|
396
|
+
return this.entry.paths.relativeEntry;
|
|
397
|
+
}
|
|
398
|
+
get codeHash() {
|
|
399
|
+
invariant5(this.entry, "Entry not initialized.");
|
|
400
|
+
return this.entry.codeHash;
|
|
401
|
+
}
|
|
402
|
+
get timingPattern() {
|
|
403
|
+
invariant5(this.entryExports, "Entry exports not initialized.");
|
|
404
|
+
return this.entryExports.timing;
|
|
405
|
+
}
|
|
406
|
+
hasTaskExport() {
|
|
407
|
+
return this.entryExports?.task !== void 0;
|
|
408
|
+
}
|
|
409
|
+
hasTimingExport() {
|
|
410
|
+
return this.entryExports?.timing !== void 0;
|
|
411
|
+
}
|
|
412
|
+
async loadEntry() {
|
|
413
|
+
invariant5(this.entry, "Entry not initialized.");
|
|
414
|
+
await this.entry.load();
|
|
415
|
+
this.entryExports = await this.entry.getExports();
|
|
416
|
+
}
|
|
417
|
+
async execute() {
|
|
418
|
+
invariant5(this.entry, "Entry not initialized.");
|
|
419
|
+
try {
|
|
420
|
+
await this.loadEntry();
|
|
421
|
+
invariant5(this.entry.isLoaded(), "Entry failed to load.");
|
|
422
|
+
invariant5(this.entryExports, "Entry failed to get exports.");
|
|
423
|
+
this.taskCallbacks?.onTaskExportsLoaded?.();
|
|
424
|
+
const loadResult = await this.taskCallbacks?.onTaskLoadComplete?.({
|
|
425
|
+
paths: this.entry.paths,
|
|
426
|
+
md5Hash: this.entry.codeHash,
|
|
427
|
+
compiledCode: this.entry.code
|
|
428
|
+
});
|
|
429
|
+
this.meta.fileTaskId = loadResult?.fileTaskId ?? -1;
|
|
430
|
+
this.meta.fileTaskVersionId = loadResult?.fileTaskVersionId ?? -1;
|
|
431
|
+
} catch (error) {
|
|
432
|
+
if (this.entry.paths.relativeEntry) {
|
|
433
|
+
await this.runtimeCallbacks?.onFailure?.({
|
|
434
|
+
path: this.entry.paths.relativeEntry
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
throw error;
|
|
438
|
+
}
|
|
439
|
+
await this.taskCallbacks?.onTaskExecutionStart?.();
|
|
440
|
+
const {
|
|
441
|
+
shouldSkip,
|
|
442
|
+
// shouldRetry,
|
|
443
|
+
onSuccess,
|
|
444
|
+
onError,
|
|
445
|
+
onComplete,
|
|
446
|
+
task
|
|
447
|
+
} = this.entryExports;
|
|
448
|
+
if (!task) {
|
|
449
|
+
await this.runtimeCallbacks?.onNotFound?.({
|
|
450
|
+
path: this.entry.paths.relativeEntry
|
|
451
|
+
});
|
|
452
|
+
throw new Error("Task function not found");
|
|
453
|
+
}
|
|
454
|
+
const taskContext = {
|
|
455
|
+
env: this.env,
|
|
456
|
+
meta: this.meta,
|
|
457
|
+
kv: this.kv,
|
|
458
|
+
log: this.log,
|
|
459
|
+
timing: this.timing,
|
|
460
|
+
sqlite: this.sqlite
|
|
461
|
+
};
|
|
462
|
+
if (await shouldSkip?.(taskContext)) {
|
|
463
|
+
await this.runtimeCallbacks?.onSkip?.({
|
|
464
|
+
path: this.entry.paths.relativeEntry
|
|
465
|
+
});
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
await this.runtimeCallbacks?.onStart?.({
|
|
470
|
+
path: this.entry.paths.relativeEntry
|
|
471
|
+
});
|
|
472
|
+
const taskReturnValue = await task(taskContext);
|
|
473
|
+
await this.runtimeCallbacks?.onSuccess?.({
|
|
474
|
+
path: this.entry.paths.relativeEntry
|
|
475
|
+
});
|
|
476
|
+
await onSuccess?.(taskContext);
|
|
477
|
+
return taskReturnValue;
|
|
478
|
+
} catch (err) {
|
|
479
|
+
await this.runtimeCallbacks?.onFailure?.({
|
|
480
|
+
path: this.entry.paths.relativeEntry
|
|
481
|
+
});
|
|
482
|
+
await onError?.(err, taskContext);
|
|
483
|
+
throw err;
|
|
484
|
+
} finally {
|
|
485
|
+
await onComplete?.(taskContext);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// src/runtime/taskLibrary.ts
|
|
491
|
+
import invariant6 from "tiny-invariant";
|
|
492
|
+
var TaskLibrary = class {
|
|
493
|
+
tasks;
|
|
494
|
+
constructor() {
|
|
495
|
+
this.tasks = /* @__PURE__ */ new Map();
|
|
496
|
+
}
|
|
497
|
+
add(taskOrTasks) {
|
|
498
|
+
if (Array.isArray(taskOrTasks)) {
|
|
499
|
+
taskOrTasks.forEach((task) => this.tasks.set(task.taskPath, task));
|
|
500
|
+
} else {
|
|
501
|
+
this.tasks.set(taskOrTasks.taskPath, taskOrTasks);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
remove(taskPath) {
|
|
505
|
+
if (Array.isArray(taskPath)) {
|
|
506
|
+
taskPath.forEach((task) => this.tasks.delete(task.taskPath));
|
|
507
|
+
} else {
|
|
508
|
+
this.tasks.delete(taskPath);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Add tasks from in-memory compiled code (stored in DB)
|
|
513
|
+
*/
|
|
514
|
+
addFromCompiledCode({
|
|
515
|
+
compiledCode,
|
|
516
|
+
descriptors,
|
|
517
|
+
tasksRootDir,
|
|
518
|
+
buildConfigFn
|
|
519
|
+
}) {
|
|
520
|
+
compiledCode.forEach((code, sourcePath) => {
|
|
521
|
+
const descriptor = descriptors[sourcePath];
|
|
522
|
+
if (!descriptor) {
|
|
523
|
+
console.warn(`No descriptor found for source path: ${sourcePath}`);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const task = new Task({
|
|
527
|
+
buildConfigFn,
|
|
528
|
+
entryParams: {
|
|
529
|
+
compiledCode: code,
|
|
530
|
+
descriptor,
|
|
531
|
+
tasksRootDir
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
this.tasks.set(task.taskPath, task);
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
async execute(taskPath) {
|
|
538
|
+
const task = this.tasks.get(taskPath);
|
|
539
|
+
invariant6(task, `Task not found for path: ${taskPath}`);
|
|
540
|
+
return task.execute();
|
|
541
|
+
}
|
|
542
|
+
getTasks() {
|
|
543
|
+
return Array.from(this.tasks.values());
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
// src/runtime/taskLibraryScheduler.ts
|
|
548
|
+
import { validate, schedule } from "node-cron";
|
|
549
|
+
import invariant7 from "tiny-invariant";
|
|
550
|
+
import { logTaskLibraryScheduler, logTaskLibrarySchedulerError } from "@apisurf/ngn-os";
|
|
551
|
+
var TaskLibraryScheduler = class {
|
|
552
|
+
taskLibrary;
|
|
553
|
+
scheduledTasks = /* @__PURE__ */ new Map();
|
|
554
|
+
constructor(options) {
|
|
555
|
+
this.taskLibrary = options.taskLibrary;
|
|
556
|
+
}
|
|
557
|
+
async start() {
|
|
558
|
+
logTaskLibraryScheduler("Starting scheduler");
|
|
559
|
+
const scheduledTasks = this.taskLibrary.getTasks().map(async (task) => {
|
|
560
|
+
try {
|
|
561
|
+
const res = await this.scheduleTask(task);
|
|
562
|
+
return res;
|
|
563
|
+
} catch (error) {
|
|
564
|
+
logTaskLibrarySchedulerError(`Error scheduling task: ${task.taskPath}`);
|
|
565
|
+
logTaskLibrarySchedulerError(error);
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
await Promise.all(scheduledTasks);
|
|
569
|
+
logTaskLibraryScheduler("Scheduler started");
|
|
570
|
+
}
|
|
571
|
+
async stop() {
|
|
572
|
+
logTaskLibraryScheduler("Stopping scheduler");
|
|
573
|
+
for (const [path, task] of this.scheduledTasks) {
|
|
574
|
+
logTaskLibraryScheduler(`Stopping scheduled task: ${path}`);
|
|
575
|
+
task.stop();
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
async scheduleTask(task) {
|
|
579
|
+
logTaskLibraryScheduler(`Scheduling task: ${task.taskPath}`);
|
|
580
|
+
await task.loadEntry();
|
|
581
|
+
logTaskLibraryScheduler(`Scheduling: ${task.taskPath} with pattern: ${task.timingPattern}`);
|
|
582
|
+
invariant7(task.hasTimingExport(), `timing pattern is missing for ${task.taskPath}`);
|
|
583
|
+
invariant7(task.hasTaskExport(), `task function is missing for ${task.taskPath}`);
|
|
584
|
+
invariant7(
|
|
585
|
+
task.timingPattern && this.isCronPatternValid(task.timingPattern),
|
|
586
|
+
`Invalid cron pattern: ${task.timingPattern} for ${task.taskPath}`
|
|
587
|
+
);
|
|
588
|
+
const wrappedUserTask = async () => task.execute();
|
|
589
|
+
const scheduledTask = schedule(task.timingPattern, wrappedUserTask);
|
|
590
|
+
this.scheduledTasks.set(task.taskPath, scheduledTask);
|
|
591
|
+
logTaskLibraryScheduler(`Scheduled: ${task.taskPath}`);
|
|
592
|
+
}
|
|
593
|
+
unscheduleTask(task) {
|
|
594
|
+
const existingTask = this.scheduledTasks.get(task.taskPath);
|
|
595
|
+
if (!existingTask) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
logTaskLibraryScheduler(`Unscheduling: ${task.taskPath}`);
|
|
599
|
+
existingTask.stop();
|
|
600
|
+
this.scheduledTasks.delete(task.taskPath);
|
|
601
|
+
logTaskLibraryScheduler(`Unscheduled: ${task.taskPath}`);
|
|
602
|
+
}
|
|
603
|
+
isCronPatternValid(cronPattern) {
|
|
604
|
+
return validate(cronPattern);
|
|
605
|
+
}
|
|
606
|
+
// startScheduledTask(path: string) {
|
|
607
|
+
// const task = this.scheduledTasks.get(path);
|
|
608
|
+
// if (!task) {
|
|
609
|
+
// return;
|
|
610
|
+
// }
|
|
611
|
+
// task.start();
|
|
612
|
+
// }
|
|
613
|
+
// stopScheduledTask(path: string) {
|
|
614
|
+
// const task = this.scheduledTasks.get(path);
|
|
615
|
+
// if (!task) {
|
|
616
|
+
// return;
|
|
617
|
+
// }
|
|
618
|
+
// task.stop();
|
|
619
|
+
// }
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
// src/runtime/taskOnceRunner.ts
|
|
623
|
+
import invariant8 from "tiny-invariant";
|
|
624
|
+
import { logTaskOnceRunner, logTaskOnceRunnerError } from "@apisurf/ngn-os";
|
|
625
|
+
var TaskOnceRunner = class {
|
|
626
|
+
task;
|
|
627
|
+
constructor(options) {
|
|
628
|
+
this.task = options.task;
|
|
629
|
+
}
|
|
630
|
+
async run() {
|
|
631
|
+
logTaskOnceRunner(`Preparing to run task: ${this.task.taskPath}`);
|
|
632
|
+
await this.task.loadEntry();
|
|
633
|
+
logTaskOnceRunner(`Loaded task code: ${this.task.taskPath}`);
|
|
634
|
+
invariant8(this.task.hasTaskExport(), `task function is missing for ${this.task.taskPath}`);
|
|
635
|
+
logTaskOnceRunner(`Running task: ${this.task.taskPath}`);
|
|
636
|
+
try {
|
|
637
|
+
await this.task.execute();
|
|
638
|
+
logTaskOnceRunner(`Task completed: ${this.task.taskPath}`);
|
|
639
|
+
} catch (error) {
|
|
640
|
+
logTaskOnceRunnerError(`Error running task: ${this.task.taskPath}`);
|
|
641
|
+
logTaskOnceRunnerError(error);
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
// src/db/db.ts
|
|
648
|
+
import {
|
|
649
|
+
safeInitDbClient,
|
|
650
|
+
initDbFileIfNotExists,
|
|
651
|
+
isDbPath
|
|
652
|
+
} from "@apisurf/ngn-persistence";
|
|
653
|
+
import invariant9 from "tiny-invariant";
|
|
654
|
+
var db;
|
|
655
|
+
async function setupDbClient(dbPath) {
|
|
656
|
+
invariant9(isDbPath(dbPath), "Invalid db path");
|
|
657
|
+
if (db) {
|
|
658
|
+
return db;
|
|
659
|
+
}
|
|
660
|
+
db = await safeInitDbClient(dbPath);
|
|
661
|
+
return db;
|
|
662
|
+
}
|
|
663
|
+
function getDbClient() {
|
|
664
|
+
invariant9(db, "DB client not initialized");
|
|
665
|
+
return db;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// src/runtime/generateControls.ts
|
|
669
|
+
import {
|
|
670
|
+
logTaskCallback,
|
|
671
|
+
logTaskConsoleError,
|
|
672
|
+
logTaskConsoleInfo,
|
|
673
|
+
logTaskConsoleWarn
|
|
674
|
+
} from "@apisurf/ngn-os";
|
|
675
|
+
import invariant10 from "tiny-invariant";
|
|
676
|
+
|
|
677
|
+
// src/runtime/taskSqlite.ts
|
|
678
|
+
import { mkdirSync } from "node:fs";
|
|
679
|
+
import { unlink } from "node:fs/promises";
|
|
680
|
+
import { basename, dirname, extname, isAbsolute, join, resolve, sep } from "node:path";
|
|
681
|
+
import {
|
|
682
|
+
openDbClient
|
|
683
|
+
} from "@apisurf/ngn-persistence";
|
|
684
|
+
var openClients = /* @__PURE__ */ new Map();
|
|
685
|
+
async function runMigrations(client, migrations) {
|
|
686
|
+
if (!migrations.length) return;
|
|
687
|
+
await client.execute(`
|
|
688
|
+
CREATE TABLE IF NOT EXISTS _ngn_migrations (
|
|
689
|
+
id TEXT PRIMARY KEY,
|
|
690
|
+
applied_at TEXT NOT NULL
|
|
691
|
+
)
|
|
692
|
+
`);
|
|
693
|
+
const applied = await client.execute("SELECT id FROM _ngn_migrations");
|
|
694
|
+
const appliedIds = new Set(applied.rows.map((r) => r.id));
|
|
695
|
+
for (const migration of migrations) {
|
|
696
|
+
if (appliedIds.has(migration.id)) continue;
|
|
697
|
+
await client.execute(migration.up);
|
|
698
|
+
await client.execute({
|
|
699
|
+
sql: "INSERT INTO _ngn_migrations (id, applied_at) VALUES (?, ?)",
|
|
700
|
+
args: [migration.id, (/* @__PURE__ */ new Date()).toISOString()]
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function resolveTaskDbPath(taskDir, file) {
|
|
705
|
+
if (typeof file !== "string" || !file.trim()) {
|
|
706
|
+
throw new Error("A database file name is required");
|
|
707
|
+
}
|
|
708
|
+
const relativePath = file.startsWith("file:") ? file.slice(5) : file;
|
|
709
|
+
if (!relativePath) {
|
|
710
|
+
throw new Error("A database file name is required");
|
|
711
|
+
}
|
|
712
|
+
if (isAbsolute(relativePath)) {
|
|
713
|
+
throw new Error(
|
|
714
|
+
`Database file must be relative to the task folder, got an absolute path: "${file}"`
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
const resolved = resolve(taskDir, relativePath);
|
|
718
|
+
const taskDirRoot = taskDir.endsWith(sep) ? taskDir : `${taskDir}${sep}`;
|
|
719
|
+
if (!resolved.startsWith(taskDirRoot)) {
|
|
720
|
+
throw new Error(
|
|
721
|
+
`Database file "${file}" resolves outside the task folder (${taskDir}). A task can only manage databases in its own folder.`
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
return resolved;
|
|
725
|
+
}
|
|
726
|
+
function getClient(absolutePath) {
|
|
727
|
+
const existing = openClients.get(absolutePath);
|
|
728
|
+
if (existing) {
|
|
729
|
+
return existing;
|
|
730
|
+
}
|
|
731
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
732
|
+
const client = openDbClient(`file:${absolutePath}`);
|
|
733
|
+
openClients.set(absolutePath, client);
|
|
734
|
+
return client;
|
|
735
|
+
}
|
|
736
|
+
function closeClient(absolutePath) {
|
|
737
|
+
const client = openClients.get(absolutePath);
|
|
738
|
+
if (client) {
|
|
739
|
+
client.close();
|
|
740
|
+
openClients.delete(absolutePath);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
function createTaskSqlite(taskSourcePath) {
|
|
744
|
+
const taskDir = dirname(taskSourcePath);
|
|
745
|
+
const defaultDbPath = join(taskDir, `${basename(taskSourcePath, extname(taskSourcePath))}.db`);
|
|
746
|
+
const defaultClient = () => getClient(defaultDbPath);
|
|
747
|
+
return {
|
|
748
|
+
get client() {
|
|
749
|
+
return defaultClient();
|
|
750
|
+
},
|
|
751
|
+
get path() {
|
|
752
|
+
return defaultDbPath;
|
|
753
|
+
},
|
|
754
|
+
execute: (sql, args) => defaultClient().execute(args ? { sql, args } : sql),
|
|
755
|
+
batch: (statements) => defaultClient().batch(statements),
|
|
756
|
+
async initDB(options) {
|
|
757
|
+
const dbPath = resolveTaskDbPath(taskDir, options.file);
|
|
758
|
+
const client = getClient(dbPath);
|
|
759
|
+
if (options.migrations?.length) {
|
|
760
|
+
await runMigrations(client, options.migrations);
|
|
761
|
+
}
|
|
762
|
+
return {
|
|
763
|
+
client,
|
|
764
|
+
path: dbPath,
|
|
765
|
+
execute: (sql, args) => client.execute(args ? { sql, args } : sql),
|
|
766
|
+
batch: (statements) => client.batch(statements),
|
|
767
|
+
close: () => closeClient(dbPath)
|
|
768
|
+
};
|
|
769
|
+
},
|
|
770
|
+
async destroyDB(file) {
|
|
771
|
+
const dbPath = resolveTaskDbPath(taskDir, file);
|
|
772
|
+
closeClient(dbPath);
|
|
773
|
+
await unlink(dbPath);
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function closeTaskDatabases() {
|
|
778
|
+
for (const client of openClients.values()) {
|
|
779
|
+
client.close();
|
|
780
|
+
}
|
|
781
|
+
openClients.clear();
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/db/services/fileTask.ts
|
|
785
|
+
import { $fileTaskInsert } from "@apisurf/ngn-schema";
|
|
786
|
+
var FileTaskService = class {
|
|
787
|
+
constructor(db2) {
|
|
788
|
+
this.db = db2;
|
|
789
|
+
}
|
|
790
|
+
async add(task) {
|
|
791
|
+
const { path, status } = $fileTaskInsert.parse(task);
|
|
792
|
+
const sql = `
|
|
793
|
+
INSERT INTO file_tasks (path, parent_path, status)
|
|
794
|
+
VALUES (:path, :parentPath, :status)
|
|
795
|
+
`;
|
|
796
|
+
return this.db.execute({
|
|
797
|
+
sql,
|
|
798
|
+
args: { path, parentPath: task.parent_path, status }
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
async del(id) {
|
|
802
|
+
return this.db.execute({
|
|
803
|
+
sql: "DELETE FROM file_tasks WHERE id = :id",
|
|
804
|
+
args: { id }
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
async getByPath(path) {
|
|
808
|
+
const { rows } = await this.db.execute({
|
|
809
|
+
sql: "SELECT * FROM file_tasks WHERE path = :path LIMIT 1",
|
|
810
|
+
args: { path }
|
|
811
|
+
});
|
|
812
|
+
return rows[0];
|
|
813
|
+
}
|
|
814
|
+
async list() {
|
|
815
|
+
const { rows } = await this.db.execute("SELECT * FROM file_tasks LIMIT 1000");
|
|
816
|
+
return rows;
|
|
817
|
+
}
|
|
818
|
+
async bumpUpdatedAt(id) {
|
|
819
|
+
return this.db.execute({
|
|
820
|
+
sql: "UPDATE file_tasks SET updated_at = :updatedAt WHERE id = :id",
|
|
821
|
+
args: { updatedAt: (/* @__PURE__ */ new Date()).valueOf(), id }
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
async updateStatus(id, status) {
|
|
825
|
+
return this.db.execute({
|
|
826
|
+
sql: "UPDATE file_tasks SET status = :status WHERE id = :id",
|
|
827
|
+
args: { status, id }
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
// src/db/services/fileTaskVersion.ts
|
|
833
|
+
import { $fileTaskVersion } from "@apisurf/ngn-schema";
|
|
834
|
+
var $fileTaskVersionInsert = $fileTaskVersion.omit({
|
|
835
|
+
id: true,
|
|
836
|
+
created_at: true
|
|
837
|
+
});
|
|
838
|
+
var FileTaskVersionService = class {
|
|
839
|
+
constructor(db2) {
|
|
840
|
+
this.db = db2;
|
|
841
|
+
}
|
|
842
|
+
async add(taskVersion) {
|
|
843
|
+
const { file_task_id, md5_hash, version, compiled_code } = $fileTaskVersionInsert.parse(taskVersion);
|
|
844
|
+
const sql = `
|
|
845
|
+
INSERT INTO file_task_versions (file_task_id, md5_hash, version, compiled_code)
|
|
846
|
+
VALUES (:file_task_id, :md5_hash, :version, :compiled_code)
|
|
847
|
+
`;
|
|
848
|
+
return this.db.execute({
|
|
849
|
+
sql,
|
|
850
|
+
args: { file_task_id, md5_hash, version, compiled_code }
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
async getByHash(hash, file_task_id) {
|
|
854
|
+
const { rows } = await this.db.execute({
|
|
855
|
+
sql: "SELECT * FROM file_task_versions WHERE md5_hash = :md5_hash AND file_task_id = :file_task_id ORDER BY version DESC LIMIT 1",
|
|
856
|
+
args: { md5_hash: hash, file_task_id }
|
|
857
|
+
});
|
|
858
|
+
return rows[0];
|
|
859
|
+
}
|
|
860
|
+
async getByVersion(version, file_task_id) {
|
|
861
|
+
const { rows } = await this.db.execute({
|
|
862
|
+
sql: "SELECT * FROM file_task_versions WHERE version = :version AND file_task_id = :file_task_id LIMIT 1",
|
|
863
|
+
args: { version, file_task_id }
|
|
864
|
+
});
|
|
865
|
+
return rows[0];
|
|
866
|
+
}
|
|
867
|
+
async getLastVersion(file_task_id) {
|
|
868
|
+
const { rows } = await this.db.execute({
|
|
869
|
+
sql: "SELECT MAX(version) as version FROM file_task_versions WHERE file_task_id = :file_task_id",
|
|
870
|
+
args: { file_task_id }
|
|
871
|
+
});
|
|
872
|
+
return rows[0]?.version ?? 0;
|
|
873
|
+
}
|
|
874
|
+
async getById(id) {
|
|
875
|
+
const { rows } = await this.db.execute({
|
|
876
|
+
sql: "SELECT * FROM file_task_versions WHERE id = :id LIMIT 1",
|
|
877
|
+
args: { id }
|
|
878
|
+
});
|
|
879
|
+
return rows[0];
|
|
880
|
+
}
|
|
881
|
+
async getCompiledCode(id) {
|
|
882
|
+
const { rows } = await this.db.execute({
|
|
883
|
+
sql: "SELECT compiled_code FROM file_task_versions WHERE id = :id LIMIT 1",
|
|
884
|
+
args: { id }
|
|
885
|
+
});
|
|
886
|
+
return rows[0]?.compiled_code;
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
// src/db/services/registerTask.ts
|
|
891
|
+
async function registerFileTask(dbClient, md5Hash, compiledCode, paths) {
|
|
892
|
+
const fileTaskVersionService = new FileTaskVersionService(dbClient);
|
|
893
|
+
const fileTaskService = new FileTaskService(dbClient);
|
|
894
|
+
const previousFileTask = await fileTaskService.getByPath(paths.relativeEntry);
|
|
895
|
+
if (!previousFileTask) {
|
|
896
|
+
await fileTaskService.add({
|
|
897
|
+
path: paths.relativeEntry,
|
|
898
|
+
parent_path: paths.relativeParent,
|
|
899
|
+
status: "active"
|
|
900
|
+
});
|
|
901
|
+
} else {
|
|
902
|
+
await fileTaskService.updateStatus(previousFileTask.id, "active");
|
|
903
|
+
}
|
|
904
|
+
const fileTask = await fileTaskService.getByPath(paths.relativeEntry);
|
|
905
|
+
if (!fileTask) {
|
|
906
|
+
throw new Error("Could not create file task in DB.");
|
|
907
|
+
}
|
|
908
|
+
const lastVersionNumber = await fileTaskVersionService.getLastVersion(fileTask.id);
|
|
909
|
+
if (!lastVersionNumber) {
|
|
910
|
+
await fileTaskVersionService.add({
|
|
911
|
+
file_task_id: fileTask.id,
|
|
912
|
+
md5_hash: md5Hash,
|
|
913
|
+
compiled_code: compiledCode,
|
|
914
|
+
version: 1
|
|
915
|
+
});
|
|
916
|
+
await fileTaskService.bumpUpdatedAt(fileTask.id);
|
|
917
|
+
} else {
|
|
918
|
+
const lastVersion = await fileTaskVersionService.getByVersion(lastVersionNumber, fileTask.id);
|
|
919
|
+
if (lastVersion.md5_hash !== md5Hash) {
|
|
920
|
+
const nextVersionNumber = lastVersionNumber + 1;
|
|
921
|
+
await fileTaskVersionService.add({
|
|
922
|
+
version: nextVersionNumber,
|
|
923
|
+
file_task_id: fileTask.id,
|
|
924
|
+
md5_hash: md5Hash,
|
|
925
|
+
compiled_code: compiledCode
|
|
926
|
+
});
|
|
927
|
+
await fileTaskService.bumpUpdatedAt(fileTask.id);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
const version = await fileTaskVersionService.getByHash(md5Hash, fileTask.id);
|
|
931
|
+
if (!version) {
|
|
932
|
+
throw new Error("Could not create file task version in DB.");
|
|
933
|
+
}
|
|
934
|
+
return {
|
|
935
|
+
fileTaskId: fileTask.id,
|
|
936
|
+
fileVersionId: version.id,
|
|
937
|
+
compiledCode: version.compiled_code
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/db/services/taskRun.ts
|
|
942
|
+
var TaskRunService = class {
|
|
943
|
+
constructor(db2) {
|
|
944
|
+
this.db = db2;
|
|
945
|
+
}
|
|
946
|
+
async initTaskRun(data) {
|
|
947
|
+
const { rows } = await this.db.execute({
|
|
948
|
+
sql: "INSERT INTO task_runs (file_task_id, file_task_version_id, status) VALUES (:file_task_id, :file_task_version_id, 'pending') RETURNING id",
|
|
949
|
+
args: {
|
|
950
|
+
file_task_id: data.file_task_id,
|
|
951
|
+
file_task_version_id: data.file_task_version_id
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
return rows[0].id;
|
|
955
|
+
}
|
|
956
|
+
async skipTaskRun(taskRunId) {
|
|
957
|
+
await this.db.execute({
|
|
958
|
+
sql: "UPDATE task_runs SET status = 'skipped', ended_at = :ended_at WHERE id = :id",
|
|
959
|
+
args: { id: taskRunId, ended_at: Date.now() }
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
async startTaskRun(taskRunId) {
|
|
963
|
+
await this.db.execute({
|
|
964
|
+
sql: "UPDATE task_runs SET started_at = :started_at, status = 'running' WHERE id = :id",
|
|
965
|
+
args: {
|
|
966
|
+
started_at: Date.now(),
|
|
967
|
+
id: taskRunId
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
async endTaskRun(taskRunId, status) {
|
|
972
|
+
await this.db.execute({
|
|
973
|
+
sql: "UPDATE task_runs SET ended_at = :ended_at, status = :status WHERE id = :id",
|
|
974
|
+
args: {
|
|
975
|
+
ended_at: Date.now(),
|
|
976
|
+
status,
|
|
977
|
+
id: taskRunId
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
async getById(taskId) {
|
|
982
|
+
const { rows } = await this.db.execute({
|
|
983
|
+
sql: "SELECT * FROM task_runs WHERE id = :taskId",
|
|
984
|
+
args: { taskId }
|
|
985
|
+
});
|
|
986
|
+
return rows[0];
|
|
987
|
+
}
|
|
988
|
+
async listByTaskVersion(fileTaskId, fileTaskVersionId) {
|
|
989
|
+
const { rows } = await this.db.execute({
|
|
990
|
+
sql: "SELECT * FROM task_runs WHERE file_task_id = :fileTaskId AND file_task_version_id = :fileTaskVersionId ORDER BY started_at DESC LIMIT 100",
|
|
991
|
+
args: { fileTaskId, fileTaskVersionId }
|
|
992
|
+
});
|
|
993
|
+
return rows;
|
|
994
|
+
}
|
|
995
|
+
};
|
|
996
|
+
|
|
997
|
+
// src/db/services/log.ts
|
|
998
|
+
var LogService = class {
|
|
999
|
+
constructor(db2) {
|
|
1000
|
+
this.db = db2;
|
|
1001
|
+
}
|
|
1002
|
+
async logInfo(fileTaskId, taskRunId, value) {
|
|
1003
|
+
return this.add(fileTaskId, taskRunId, "info", value);
|
|
1004
|
+
}
|
|
1005
|
+
async logWarning(fileTaskId, taskRunId, value) {
|
|
1006
|
+
return this.add(fileTaskId, taskRunId, "warning", value);
|
|
1007
|
+
}
|
|
1008
|
+
async logError(fileTaskId, taskRunId, value) {
|
|
1009
|
+
return this.add(fileTaskId, taskRunId, "error", value);
|
|
1010
|
+
}
|
|
1011
|
+
async add(fileTaskId, taskRunId, status, value) {
|
|
1012
|
+
const { rows } = await this.db.execute({
|
|
1013
|
+
sql: "INSERT INTO logs (file_task_id, task_run_id, status, value) VALUES (:fileTaskId, :taskRunId, :status, :value) RETURNING id",
|
|
1014
|
+
args: { fileTaskId, taskRunId, status, value }
|
|
1015
|
+
});
|
|
1016
|
+
return rows[0].id;
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
// src/db/services/kv.ts
|
|
1021
|
+
function normalizeValue(value) {
|
|
1022
|
+
if (typeof value !== "string") {
|
|
1023
|
+
try {
|
|
1024
|
+
return JSON.stringify(value);
|
|
1025
|
+
} catch {
|
|
1026
|
+
try {
|
|
1027
|
+
return value?.toString() ?? null;
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
console.error("Error setting key-value pair in db", error);
|
|
1030
|
+
return null;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
} else {
|
|
1034
|
+
return value;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
var KVService = class {
|
|
1038
|
+
constructor(db2) {
|
|
1039
|
+
this.db = db2;
|
|
1040
|
+
}
|
|
1041
|
+
async getValue(fileTaskId, key) {
|
|
1042
|
+
const { rows } = await this.db.execute({
|
|
1043
|
+
sql: "SELECT * FROM kvs WHERE file_task_id = :fileTaskId AND key = :key LIMIT 1",
|
|
1044
|
+
args: { fileTaskId, key }
|
|
1045
|
+
});
|
|
1046
|
+
return rows[0]?.value;
|
|
1047
|
+
}
|
|
1048
|
+
async setValue(fileTaskId, key, value) {
|
|
1049
|
+
const existingValue = await this.getValue(fileTaskId, key);
|
|
1050
|
+
const normalizedValue = normalizeValue(value);
|
|
1051
|
+
if (existingValue) {
|
|
1052
|
+
return this.db.execute({
|
|
1053
|
+
sql: "UPDATE kvs SET value = :value WHERE file_task_id = :fileTaskId AND key = :key",
|
|
1054
|
+
args: { fileTaskId, key, value: normalizedValue }
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
return this.db.execute({
|
|
1058
|
+
sql: "INSERT INTO kvs (file_task_id, key, value) VALUES (:fileTaskId, :key, :value)",
|
|
1059
|
+
args: { fileTaskId, key, value: normalizedValue }
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
async deleteValue(fileTaskId, key) {
|
|
1063
|
+
return this.db.execute({
|
|
1064
|
+
sql: "DELETE FROM kvs WHERE file_task_id = :fileTaskId AND key = :key",
|
|
1065
|
+
args: { fileTaskId, key }
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
|
|
1070
|
+
// src/db/services/timing.ts
|
|
1071
|
+
var TimingService = class {
|
|
1072
|
+
constructor(db2) {
|
|
1073
|
+
this.db = db2;
|
|
1074
|
+
}
|
|
1075
|
+
start(file_task_id, task_run_id, label) {
|
|
1076
|
+
const startTime = Date.now();
|
|
1077
|
+
const stopAndRecordCb = async () => {
|
|
1078
|
+
const endTime = Date.now();
|
|
1079
|
+
const duration = endTime - startTime;
|
|
1080
|
+
await this.db.execute({
|
|
1081
|
+
sql: "INSERT INTO timings (label, value, file_task_id, task_run_id) VALUES (:label, :value, :file_task_id, :task_run_id) RETURNING id",
|
|
1082
|
+
args: { label, value: duration, file_task_id, task_run_id }
|
|
1083
|
+
});
|
|
1084
|
+
return;
|
|
1085
|
+
};
|
|
1086
|
+
return stopAndRecordCb;
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
// src/runtime/generateControls.ts
|
|
1091
|
+
function createControlsGenerator({
|
|
1092
|
+
dbClient,
|
|
1093
|
+
env
|
|
1094
|
+
}) {
|
|
1095
|
+
const taskRunService = new TaskRunService(dbClient);
|
|
1096
|
+
const logService = new LogService(dbClient);
|
|
1097
|
+
const kvService = new KVService(dbClient);
|
|
1098
|
+
const timingService = new TimingService(dbClient);
|
|
1099
|
+
const generateControls = ({ sourcePath, compiledPath }) => {
|
|
1100
|
+
let fileTaskId;
|
|
1101
|
+
let fileTaskVersionId;
|
|
1102
|
+
let fileTaskRunId;
|
|
1103
|
+
const assertMandatoryIds = () => {
|
|
1104
|
+
invariant10(!!fileTaskId, "Task ID not found");
|
|
1105
|
+
invariant10(!!fileTaskVersionId, "Task version ID not found");
|
|
1106
|
+
};
|
|
1107
|
+
const taskCallbacks = {
|
|
1108
|
+
async onTaskLoadComplete({ paths, md5Hash, compiledCode }) {
|
|
1109
|
+
const result = await registerFileTask(dbClient, md5Hash, compiledCode, {
|
|
1110
|
+
relativeEntry: paths.relativeEntry,
|
|
1111
|
+
relativeParent: paths.relativeParent
|
|
1112
|
+
});
|
|
1113
|
+
fileTaskId = result.fileTaskId;
|
|
1114
|
+
fileTaskVersionId = result.fileVersionId;
|
|
1115
|
+
return {
|
|
1116
|
+
fileTaskId,
|
|
1117
|
+
fileTaskVersionId
|
|
1118
|
+
};
|
|
1119
|
+
},
|
|
1120
|
+
async onTaskExecutionStart() {
|
|
1121
|
+
assertMandatoryIds();
|
|
1122
|
+
fileTaskRunId = await taskRunService.initTaskRun({
|
|
1123
|
+
file_task_id: fileTaskId,
|
|
1124
|
+
file_task_version_id: fileTaskVersionId
|
|
1125
|
+
});
|
|
1126
|
+
},
|
|
1127
|
+
async onTaskCodeLoaded() {
|
|
1128
|
+
},
|
|
1129
|
+
async onTaskExportsLoaded() {
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
const runtimeCallbacks = {
|
|
1133
|
+
async onNotFound() {
|
|
1134
|
+
assertMandatoryIds();
|
|
1135
|
+
await logService.logError(fileTaskId, fileTaskRunId, "Task not found");
|
|
1136
|
+
logTaskCallback(`Task not found: ${fileTaskId}, run ID: ${fileTaskRunId}`);
|
|
1137
|
+
},
|
|
1138
|
+
async onSkip() {
|
|
1139
|
+
assertMandatoryIds();
|
|
1140
|
+
await taskRunService.skipTaskRun(fileTaskRunId);
|
|
1141
|
+
await logService.logInfo(fileTaskId, fileTaskRunId, "Task skipped");
|
|
1142
|
+
logTaskCallback(`Task skipped: ${fileTaskId}, run ID: ${fileTaskRunId}`);
|
|
1143
|
+
},
|
|
1144
|
+
async onStart() {
|
|
1145
|
+
assertMandatoryIds();
|
|
1146
|
+
await taskRunService.startTaskRun(fileTaskRunId);
|
|
1147
|
+
await logService.logInfo(fileTaskId, fileTaskRunId, "Task started");
|
|
1148
|
+
logTaskCallback(`Task started: ${fileTaskId}, run ID: ${fileTaskRunId}`);
|
|
1149
|
+
},
|
|
1150
|
+
async onFailure() {
|
|
1151
|
+
assertMandatoryIds();
|
|
1152
|
+
await taskRunService.endTaskRun(fileTaskRunId, "failure");
|
|
1153
|
+
await logService.logError(fileTaskId, fileTaskRunId, "Task failed");
|
|
1154
|
+
logTaskCallback(`Task failed: ${fileTaskId}, run ID: ${fileTaskRunId}`);
|
|
1155
|
+
},
|
|
1156
|
+
async onSuccess() {
|
|
1157
|
+
assertMandatoryIds();
|
|
1158
|
+
await taskRunService.endTaskRun(fileTaskRunId, "success");
|
|
1159
|
+
await logService.logInfo(fileTaskId, fileTaskRunId, "Task succeeded");
|
|
1160
|
+
logTaskCallback(`Task succeeded: ${fileTaskId}, run ID: ${fileTaskRunId}`);
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
return {
|
|
1164
|
+
compiledPath,
|
|
1165
|
+
sourcePath,
|
|
1166
|
+
taskCallbacks,
|
|
1167
|
+
runtimeCallbacks,
|
|
1168
|
+
env,
|
|
1169
|
+
// Scoped to this task's own folder — see createTaskSqlite
|
|
1170
|
+
sqlite: createTaskSqlite(sourcePath),
|
|
1171
|
+
kv: {
|
|
1172
|
+
async set(key, value) {
|
|
1173
|
+
await kvService.setValue(fileTaskId, key, value);
|
|
1174
|
+
},
|
|
1175
|
+
async get(key) {
|
|
1176
|
+
return kvService.getValue(fileTaskId, key);
|
|
1177
|
+
},
|
|
1178
|
+
async delete(key) {
|
|
1179
|
+
await kvService.deleteValue(fileTaskId, key);
|
|
1180
|
+
}
|
|
1181
|
+
},
|
|
1182
|
+
log: {
|
|
1183
|
+
async info(value) {
|
|
1184
|
+
logTaskConsoleInfo(value);
|
|
1185
|
+
await logService.logInfo(fileTaskId, fileTaskRunId, value);
|
|
1186
|
+
},
|
|
1187
|
+
async error(value) {
|
|
1188
|
+
logTaskConsoleError(value);
|
|
1189
|
+
await logService.logError(fileTaskId, fileTaskRunId, value);
|
|
1190
|
+
},
|
|
1191
|
+
async warning(value) {
|
|
1192
|
+
logTaskConsoleWarn(value);
|
|
1193
|
+
await logService.logWarning(fileTaskId, fileTaskRunId, value);
|
|
1194
|
+
}
|
|
1195
|
+
},
|
|
1196
|
+
timing: {
|
|
1197
|
+
start(label) {
|
|
1198
|
+
return timingService.start(fileTaskId, fileTaskRunId, label);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
};
|
|
1203
|
+
return generateControls;
|
|
1204
|
+
}
|
|
1205
|
+
export {
|
|
1206
|
+
Compiler,
|
|
1207
|
+
Task,
|
|
1208
|
+
TaskLibrary,
|
|
1209
|
+
TaskLibraryScheduler,
|
|
1210
|
+
TaskOnceRunner,
|
|
1211
|
+
closeTaskDatabases,
|
|
1212
|
+
createControlsGenerator,
|
|
1213
|
+
executeEsm,
|
|
1214
|
+
getDbClient,
|
|
1215
|
+
initDbFileIfNotExists,
|
|
1216
|
+
setupDbClient
|
|
1217
|
+
};
|