@robodev-ai/runtime 0.4.0 → 0.4.1
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/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/local-jobs.test.ts +74 -0
- package/src/local-jobs.ts +44 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@robodev-ai/runtime",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Robodev project runtime — deploy-file classification, esbuild compile, module loading, schema push, route matching, OpenAPI, handler invocation, Robodev Auth, the socket engine, and local file storage. Shared by hosted Starbase and `robodev dev`.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/index.ts
CHANGED
package/src/local-jobs.test.ts
CHANGED
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
ensureJobsSchema,
|
|
11
11
|
type JobsQueryable,
|
|
12
12
|
type LocalCronRow,
|
|
13
|
+
type LocalJobFinishedEvent,
|
|
13
14
|
type LocalJobRow,
|
|
15
|
+
type LocalJobsEngineOptions,
|
|
14
16
|
type LocalJobsGeneration,
|
|
15
17
|
} from "./local-jobs.js";
|
|
16
18
|
|
|
@@ -208,6 +210,7 @@ function engineFor(
|
|
|
208
210
|
store: ReturnType<typeof memoryJobs>,
|
|
209
211
|
jobs: LocalJobsGeneration["jobs"],
|
|
210
212
|
logs: string[] = [],
|
|
213
|
+
onJobFinished?: LocalJobsEngineOptions["onJobFinished"],
|
|
211
214
|
) {
|
|
212
215
|
const generation: LocalJobsGeneration = { db: {} as RobodevDb, jobs };
|
|
213
216
|
return createLocalJobsEngine({
|
|
@@ -215,6 +218,7 @@ function engineFor(
|
|
|
215
218
|
getGeneration: () => generation,
|
|
216
219
|
log: (line) => logs.push(line),
|
|
217
220
|
clients: mockClients,
|
|
221
|
+
onJobFinished,
|
|
218
222
|
});
|
|
219
223
|
}
|
|
220
224
|
|
|
@@ -465,3 +469,73 @@ test("start resets running rows; stop clears timers", async () => {
|
|
|
465
469
|
assert.equal(engine.running, false);
|
|
466
470
|
await engine.stop();
|
|
467
471
|
});
|
|
472
|
+
|
|
473
|
+
test("onJobFinished fires once per finishJob for succeeded, failed-retry, and dead", async () => {
|
|
474
|
+
const succeeded: LocalJobFinishedEvent[] = [];
|
|
475
|
+
const okStore = memoryJobs();
|
|
476
|
+
const ok = engineFor(okStore, [{ name: "digest", def: jobDef(() => undefined) }], [], (event) =>
|
|
477
|
+
succeeded.push(event),
|
|
478
|
+
);
|
|
479
|
+
await ok.enqueue({ name: "digest", payload: { n: 1 } });
|
|
480
|
+
await ok.tickJobs();
|
|
481
|
+
await waitUntil(() => succeeded.length === 1);
|
|
482
|
+
assert.equal(succeeded.length, 1);
|
|
483
|
+
assert.equal(succeeded[0]?.outcome, "succeeded");
|
|
484
|
+
assert.equal(succeeded[0]?.engineStatus, "succeeded");
|
|
485
|
+
assert.equal(succeeded[0]?.error, null);
|
|
486
|
+
assert.equal(succeeded[0]?.name, "digest");
|
|
487
|
+
assert.deepEqual(succeeded[0]?.payload, { n: 1 });
|
|
488
|
+
|
|
489
|
+
const retried: LocalJobFinishedEvent[] = [];
|
|
490
|
+
const retryStore = memoryJobs();
|
|
491
|
+
const retry = engineFor(
|
|
492
|
+
retryStore,
|
|
493
|
+
[
|
|
494
|
+
{
|
|
495
|
+
name: "digest",
|
|
496
|
+
def: jobDef(() => {
|
|
497
|
+
throw new Error("boom");
|
|
498
|
+
}),
|
|
499
|
+
},
|
|
500
|
+
],
|
|
501
|
+
[],
|
|
502
|
+
(event) => retried.push(event),
|
|
503
|
+
);
|
|
504
|
+
await retry.enqueue({ name: "digest", payload: { n: 2 } });
|
|
505
|
+
await retry.tickJobs();
|
|
506
|
+
await waitUntil(() => retried.length === 1);
|
|
507
|
+
assert.equal(retried.length, 1);
|
|
508
|
+
assert.equal(retried[0]?.outcome, "failed");
|
|
509
|
+
assert.equal(retried[0]?.engineStatus, "queued");
|
|
510
|
+
assert.equal(retried[0]?.error, "boom");
|
|
511
|
+
assert.equal(retried[0]?.attempts, 1);
|
|
512
|
+
assert.deepEqual(retried[0]?.payload, { n: 2 });
|
|
513
|
+
|
|
514
|
+
const finished: LocalJobFinishedEvent[] = [];
|
|
515
|
+
const deadStore = memoryJobs();
|
|
516
|
+
const dying = engineFor(
|
|
517
|
+
deadStore,
|
|
518
|
+
[
|
|
519
|
+
{
|
|
520
|
+
name: "digest",
|
|
521
|
+
def: jobDef(() => {
|
|
522
|
+
throw new Error("boom");
|
|
523
|
+
}),
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
[],
|
|
527
|
+
(event) => finished.push(event),
|
|
528
|
+
);
|
|
529
|
+
await dying.enqueue({ name: "digest" });
|
|
530
|
+
for (let attempt = 1; attempt <= JOB_MAX_ATTEMPTS; attempt++) {
|
|
531
|
+
deadStore.jobs[0]!.run_at = new Date(0);
|
|
532
|
+
deadStore.jobs[0]!.status = "queued";
|
|
533
|
+
await dying.tickJobs();
|
|
534
|
+
await waitUntil(() => finished.length === attempt);
|
|
535
|
+
}
|
|
536
|
+
assert.equal(finished.length, JOB_MAX_ATTEMPTS);
|
|
537
|
+
assert.equal(finished.at(-1)?.outcome, "failed");
|
|
538
|
+
assert.equal(finished.at(-1)?.engineStatus, "dead");
|
|
539
|
+
assert.equal(finished.at(-1)?.attempts, JOB_MAX_ATTEMPTS);
|
|
540
|
+
assert.equal(finished.at(-1)?.error, "boom");
|
|
541
|
+
});
|
package/src/local-jobs.ts
CHANGED
|
@@ -49,11 +49,23 @@ export type LocalJobsGeneration = {
|
|
|
49
49
|
jobs: readonly { name: string; file?: string; def: JobDefinition }[];
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
export type LocalJobFinishedEvent = {
|
|
53
|
+
id: string;
|
|
54
|
+
name: string;
|
|
55
|
+
payload: unknown;
|
|
56
|
+
outcome: "succeeded" | "failed";
|
|
57
|
+
engineStatus: "succeeded" | "queued" | "dead";
|
|
58
|
+
attempts: number;
|
|
59
|
+
error: string | null;
|
|
60
|
+
};
|
|
61
|
+
|
|
52
62
|
export type LocalJobsEngineOptions = {
|
|
53
63
|
query: JobsQueryable["query"];
|
|
54
64
|
getGeneration: () => LocalJobsGeneration | null;
|
|
55
65
|
log?: (line: string) => void;
|
|
56
66
|
clients: () => RouteClients;
|
|
67
|
+
/** Invoked after the job row UPDATE. Failed-retry (`queued`) and `dead` both report `failed`. */
|
|
68
|
+
onJobFinished?: (event: LocalJobFinishedEvent) => void;
|
|
57
69
|
};
|
|
58
70
|
|
|
59
71
|
export type LocalJobsEngine = {
|
|
@@ -221,6 +233,8 @@ export function createLocalJobsEngine(options: LocalJobsEngineOptions): LocalJob
|
|
|
221
233
|
}
|
|
222
234
|
|
|
223
235
|
async function finishJob(row: LocalJobRow, error: string | null): Promise<void> {
|
|
236
|
+
let engineStatus: LocalJobFinishedEvent["engineStatus"];
|
|
237
|
+
let attempts = row.attempts;
|
|
224
238
|
if (!error) {
|
|
225
239
|
await query(
|
|
226
240
|
`UPDATE robodev_jobs.jobs
|
|
@@ -229,21 +243,38 @@ export function createLocalJobsEngine(options: LocalJobsEngineOptions): LocalJob
|
|
|
229
243
|
[row.id],
|
|
230
244
|
);
|
|
231
245
|
log(` ${row.name} succeeded`);
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
const attempts = row.attempts + 1;
|
|
235
|
-
const next = nextJobFailureState(attempts);
|
|
236
|
-
await query(
|
|
237
|
-
`UPDATE robodev_jobs.jobs
|
|
238
|
-
SET status = $2, attempts = $3, last_error = $4, run_at = COALESCE($5, run_at), updated_at = now()
|
|
239
|
-
WHERE id = $1`,
|
|
240
|
-
[row.id, next.status, attempts, error, next.runAt],
|
|
241
|
-
);
|
|
242
|
-
if (next.status === "dead") {
|
|
243
|
-
log(` ${row.name} dead: ${shortError(error)}`);
|
|
246
|
+
engineStatus = "succeeded";
|
|
244
247
|
} else {
|
|
245
|
-
|
|
248
|
+
attempts = row.attempts + 1;
|
|
249
|
+
const next = nextJobFailureState(attempts);
|
|
250
|
+
await query(
|
|
251
|
+
`UPDATE robodev_jobs.jobs
|
|
252
|
+
SET status = $2, attempts = $3, last_error = $4, run_at = COALESCE($5, run_at), updated_at = now()
|
|
253
|
+
WHERE id = $1`,
|
|
254
|
+
[row.id, next.status, attempts, error, next.runAt],
|
|
255
|
+
);
|
|
256
|
+
engineStatus = next.status;
|
|
257
|
+
if (next.status === "dead") {
|
|
258
|
+
log(` ${row.name} dead: ${shortError(error)}`);
|
|
259
|
+
} else {
|
|
260
|
+
log(` ${row.name} failed attempt ${attempts}/${JOB_MAX_ATTEMPTS}: ${shortError(error)}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
let payload: unknown = null;
|
|
264
|
+
try {
|
|
265
|
+
payload = JSON.parse(row.payload_json) as unknown;
|
|
266
|
+
} catch {
|
|
267
|
+
payload = null;
|
|
246
268
|
}
|
|
269
|
+
options.onJobFinished?.({
|
|
270
|
+
id: row.id,
|
|
271
|
+
name: row.name,
|
|
272
|
+
payload,
|
|
273
|
+
outcome: error ? "failed" : "succeeded",
|
|
274
|
+
engineStatus,
|
|
275
|
+
attempts,
|
|
276
|
+
error,
|
|
277
|
+
});
|
|
247
278
|
}
|
|
248
279
|
|
|
249
280
|
async function runClaimedJob(
|