@ultimabase/jobs 0.0.0-stage → 0.4.0-canary.0
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/README.md +4 -2
- package/dist/define-job.d.ts +94 -0
- package/dist/define-job.d.ts.map +1 -0
- package/dist/define-job.js +128 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/model.d.ts +37 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +27 -0
- package/dist/plugin.d.ts +20 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +77 -0
- package/dist/runner.d.ts +21 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +168 -0
- package/dist/state.d.ts +12 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +11 -0
- package/package.json +45 -3
- package/src/define-job.ts +252 -0
- package/src/index.ts +11 -0
- package/src/model.ts +65 -0
- package/src/plugin.ts +103 -0
- package/src/runner.ts +200 -0
- package/src/state.ts +25 -0
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ultimabase/jobs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Jobs for UltimaBase. A run enqueued from a hook is written in the write's transaction, so it exists if and only if the write committed; it can wait a delay, or run on an `every` or `cron` schedule, and is retried until it succeeds.
|
|
4
|
+
|
|
5
|
+
Documentation: [Jobs](https://ultimabase.dev/docs/jobs).
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standard Schema (standardschema.dev), the part a job reads. Its
|
|
3
|
+
* specification asks for the interface to be copied, not imported.
|
|
4
|
+
*/
|
|
5
|
+
export interface StandardSchema<I = unknown, O = I> {
|
|
6
|
+
readonly '~standard': {
|
|
7
|
+
readonly version: 1;
|
|
8
|
+
readonly vendor: string;
|
|
9
|
+
readonly validate: (value: unknown) => unknown;
|
|
10
|
+
readonly types?: {
|
|
11
|
+
readonly input: I;
|
|
12
|
+
readonly output: O;
|
|
13
|
+
} | undefined;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
type Types<S> = S extends StandardSchema<infer I, infer O> ? {
|
|
17
|
+
readonly input: I;
|
|
18
|
+
readonly output: O;
|
|
19
|
+
} : never;
|
|
20
|
+
/** What a job's `run` returns, which nothing reads. */
|
|
21
|
+
type Done = unknown;
|
|
22
|
+
/** The id of the run's row, the key that makes its effect idempotent. */
|
|
23
|
+
type Facts = {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
};
|
|
26
|
+
type Schedule = {
|
|
27
|
+
readonly input?: never;
|
|
28
|
+
readonly every?: never;
|
|
29
|
+
readonly cron?: never;
|
|
30
|
+
readonly timeZone?: never;
|
|
31
|
+
};
|
|
32
|
+
type InputDefinition<N, S> = Omit<Schedule, 'input'> & {
|
|
33
|
+
readonly name: N;
|
|
34
|
+
/** Validated when the run is enqueued, and stored as JSON. */
|
|
35
|
+
readonly input: S;
|
|
36
|
+
readonly run: (facts: Facts & {
|
|
37
|
+
readonly input: Types<S>['output'];
|
|
38
|
+
}) => Done;
|
|
39
|
+
};
|
|
40
|
+
type PlainDefinition<N> = Schedule & {
|
|
41
|
+
readonly name: N;
|
|
42
|
+
readonly run: (facts: Facts) => Done;
|
|
43
|
+
};
|
|
44
|
+
type EveryDefinition<N> = Omit<Schedule, 'every'> & {
|
|
45
|
+
readonly name: N;
|
|
46
|
+
/** In milliseconds. */
|
|
47
|
+
readonly every: number;
|
|
48
|
+
readonly run: (facts: Facts) => Done;
|
|
49
|
+
};
|
|
50
|
+
type CronDefinition<N> = Omit<Schedule, 'cron' | 'timeZone'> & {
|
|
51
|
+
readonly name: N;
|
|
52
|
+
/** Five fields: minute, hour, day of the month, month, day of the week. */
|
|
53
|
+
readonly cron: string;
|
|
54
|
+
/** An IANA time zone, such as `'Europe/Paris'`: `'UTC'` by default. */
|
|
55
|
+
readonly timeZone?: string;
|
|
56
|
+
readonly run: (facts: Facts) => Done;
|
|
57
|
+
};
|
|
58
|
+
/** `enqueue`'s options. */
|
|
59
|
+
export interface EnqueueOptions {
|
|
60
|
+
/** In milliseconds, how long the run waits. */
|
|
61
|
+
readonly after?: number;
|
|
62
|
+
}
|
|
63
|
+
/** A job enqueued from server code. */
|
|
64
|
+
export interface Job<N extends string, I> {
|
|
65
|
+
readonly name: N;
|
|
66
|
+
/**
|
|
67
|
+
* Writes a run in the current transaction, or alone outside one: it
|
|
68
|
+
* exists if and only if the transaction commits.
|
|
69
|
+
*/
|
|
70
|
+
readonly enqueue: (...args: [I] extends [never] ? [options?: EnqueueOptions] : [input: I, options?: EnqueueOptions]) => Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
/** A job that runs on its schedule, never enqueued. */
|
|
73
|
+
export interface RecurringJob<N extends string> {
|
|
74
|
+
readonly name: N;
|
|
75
|
+
}
|
|
76
|
+
/** What the plugin reads of a job. */
|
|
77
|
+
export interface JobRecord {
|
|
78
|
+
readonly name: string;
|
|
79
|
+
readonly input: StandardSchema | undefined;
|
|
80
|
+
/** `every <ms>` or `cron <pattern> <zone>`, for a recurring job. */
|
|
81
|
+
readonly schedule: string | undefined;
|
|
82
|
+
/** The first due time after `from`, for a recurring job. */
|
|
83
|
+
readonly next: (from: number) => number;
|
|
84
|
+
readonly run: (facts: Facts & {
|
|
85
|
+
readonly input?: unknown;
|
|
86
|
+
}) => Done;
|
|
87
|
+
}
|
|
88
|
+
/** A `defineJob` value's record. */
|
|
89
|
+
export declare function recordOf(job: unknown): JobRecord | undefined;
|
|
90
|
+
export declare function defineJob<const N extends string, const S extends StandardSchema>(definition: InputDefinition<N, S>): Job<N, Types<S>['input']>;
|
|
91
|
+
export declare function defineJob<const N extends string>(definition: PlainDefinition<N>): Job<N, never>;
|
|
92
|
+
export declare function defineJob<const N extends string>(definition: EveryDefinition<N> | CronDefinition<N>): RecurringJob<N>;
|
|
93
|
+
export {};
|
|
94
|
+
//# sourceMappingURL=define-job.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-job.d.ts","sourceRoot":"","sources":["../src/define-job.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// `defineJob` (D-221): a job takes an input, or runs `every` milliseconds,
|
|
2
|
+
// or on a `cron` pattern. A job with no schedule is enqueued, from its
|
|
3
|
+
// definition, in the current transaction (D-36).
|
|
4
|
+
import { Cron } from 'croner';
|
|
5
|
+
import { PublicError, Refused } from 'ultimabase';
|
|
6
|
+
import { checked, standardOf } from 'ultimabase/internal';
|
|
7
|
+
import { JobsRun, M } from './model.js';
|
|
8
|
+
import { runningOf } from './state.js';
|
|
9
|
+
const records = new WeakMap();
|
|
10
|
+
/** A `defineJob` value's record. */
|
|
11
|
+
export function recordOf(job) {
|
|
12
|
+
return typeof job === 'object' && job !== null ? records.get(job) : undefined;
|
|
13
|
+
}
|
|
14
|
+
/** A positive number of milliseconds. */
|
|
15
|
+
function isDuration(value) {
|
|
16
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0;
|
|
17
|
+
}
|
|
18
|
+
/** The next due time of a pattern croner accepts; refuses one it does not. */
|
|
19
|
+
function cronNext(name, pattern, zone) {
|
|
20
|
+
if (typeof pattern !== 'string') {
|
|
21
|
+
throw new Refused(`${name}: cron is a pattern of five fields.`);
|
|
22
|
+
}
|
|
23
|
+
if (zone !== undefined && typeof zone !== 'string') {
|
|
24
|
+
throw new Refused(`${name}: timeZone is an IANA time zone.`);
|
|
25
|
+
}
|
|
26
|
+
let cron;
|
|
27
|
+
try {
|
|
28
|
+
cron = new Cron(pattern, { timezone: zone ?? 'UTC', mode: '5-part' });
|
|
29
|
+
cron.nextRun();
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw new Refused(`${name}: ${String(error)}`, { cause: error });
|
|
33
|
+
}
|
|
34
|
+
return (from) => {
|
|
35
|
+
const next = cron.nextRun(new Date(from));
|
|
36
|
+
if (next === null)
|
|
37
|
+
throw new Error(`${name}: cron has no next time.`);
|
|
38
|
+
return next.getTime();
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** The record of a job's definition, checked at import. */
|
|
42
|
+
function recordFor(definition) {
|
|
43
|
+
const read = (key) => Reflect.get(definition, key);
|
|
44
|
+
const [name, input, every, cron, timeZone, run] = [
|
|
45
|
+
'name',
|
|
46
|
+
'input',
|
|
47
|
+
'every',
|
|
48
|
+
'cron',
|
|
49
|
+
'timeZone',
|
|
50
|
+
'run',
|
|
51
|
+
].map(read);
|
|
52
|
+
if (typeof name !== 'string' || name === '') {
|
|
53
|
+
throw new Refused('defineJob: name is a non-empty string.');
|
|
54
|
+
}
|
|
55
|
+
if (typeof run !== 'function')
|
|
56
|
+
throw new Refused(`${name}: run is a function.`);
|
|
57
|
+
const forms = [input, every, cron].filter((form) => form !== undefined);
|
|
58
|
+
if (forms.length > 1) {
|
|
59
|
+
throw new Refused(`${name}: a job takes one of input, every and cron.`);
|
|
60
|
+
}
|
|
61
|
+
if (timeZone !== undefined && cron === undefined) {
|
|
62
|
+
throw new Refused(`${name}: timeZone goes with cron.`);
|
|
63
|
+
}
|
|
64
|
+
if (input !== undefined && standardOf(input) === undefined) {
|
|
65
|
+
throw new Refused(`${name}: input is a Standard Schema.`);
|
|
66
|
+
}
|
|
67
|
+
if (every !== undefined && !isDuration(every)) {
|
|
68
|
+
throw new Refused(`${name}: every is a positive number of milliseconds.`);
|
|
69
|
+
}
|
|
70
|
+
const next = cron !== undefined
|
|
71
|
+
? cronNext(name, cron, timeZone)
|
|
72
|
+
: (from) => from + Number(every);
|
|
73
|
+
// cronNext refused a pattern or a zone that is no string.
|
|
74
|
+
const schedule = cron !== undefined
|
|
75
|
+
? `cron ${JSON.stringify(cron)} ${JSON.stringify(timeZone ?? 'UTC')}`
|
|
76
|
+
: every === undefined
|
|
77
|
+
? undefined
|
|
78
|
+
: `every ${every}`;
|
|
79
|
+
const runs = (facts) => Reflect.apply(run, undefined, [facts]);
|
|
80
|
+
const schema = standardOf(input)?.schema;
|
|
81
|
+
return { name, input: schema, schedule, next, run: runs };
|
|
82
|
+
}
|
|
83
|
+
/** The input's validated value; refuses it as `VALIDATION_FAILED`. */
|
|
84
|
+
async function validated(name, schema, input) {
|
|
85
|
+
const standard = standardOf(schema);
|
|
86
|
+
if (standard === undefined)
|
|
87
|
+
return input;
|
|
88
|
+
const result = await checked(standard, input, [], name);
|
|
89
|
+
if ('issues' in result) {
|
|
90
|
+
throw new PublicError('VALIDATION_FAILED', 'Invalid input.', {
|
|
91
|
+
details: { issues: result.issues },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return result.value;
|
|
95
|
+
}
|
|
96
|
+
async function enqueue(record, args) {
|
|
97
|
+
const { name, input: schema } = record;
|
|
98
|
+
const [input, options] = schema === undefined ? [undefined, args[0]] : [args[0], args[1]];
|
|
99
|
+
const after = typeof options === 'object' && options !== null
|
|
100
|
+
? Reflect.get(options, 'after')
|
|
101
|
+
: undefined;
|
|
102
|
+
if (after !== undefined && !isDuration(after)) {
|
|
103
|
+
throw new TypeError(`${name}: after is a positive number of milliseconds.`);
|
|
104
|
+
}
|
|
105
|
+
if (runningOf(`${name}.enqueue`).jobs.get(name) !== record) {
|
|
106
|
+
throw new Error(`${name} is not in jobsPlugin's jobs.`);
|
|
107
|
+
}
|
|
108
|
+
const value = schema === undefined ? null : await validated(name, schema, input);
|
|
109
|
+
const data = {
|
|
110
|
+
job: name,
|
|
111
|
+
input: value ?? null,
|
|
112
|
+
state: 'pending',
|
|
113
|
+
dueAt: new Date(Date.now() + (after ?? 0)),
|
|
114
|
+
attempts: 0,
|
|
115
|
+
};
|
|
116
|
+
await JobsRun.create({ data }, M);
|
|
117
|
+
}
|
|
118
|
+
export function defineJob(definition) {
|
|
119
|
+
const record = recordFor(definition);
|
|
120
|
+
const job = Object.freeze(record.schedule === undefined
|
|
121
|
+
? {
|
|
122
|
+
name: record.name,
|
|
123
|
+
enqueue: async (...args) => enqueue(record, args),
|
|
124
|
+
}
|
|
125
|
+
: { name: record.name });
|
|
126
|
+
records.set(job, record);
|
|
127
|
+
return job;
|
|
128
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { EnqueueOptions, Job, RecurringJob, StandardSchema, } from './define-job.ts';
|
|
2
|
+
export { defineJob } from './define-job.ts';
|
|
3
|
+
export type { Jobs, JobsApi, JobsOptions } from './plugin.ts';
|
|
4
|
+
export { jobsPlugin } from './plugin.ts';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,YAAY,EACX,cAAc,EACd,GAAG,EACH,YAAY,EACZ,cAAc,GACd,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC3C,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/index.js
ADDED
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Field, ModelOf, ScalarOptions } from 'ultimabase';
|
|
2
|
+
import { master } from 'ultimabase';
|
|
3
|
+
export declare const M: {
|
|
4
|
+
readonly as: typeof master;
|
|
5
|
+
};
|
|
6
|
+
/** A run's state: waiting for its due time, or out of attempts. */
|
|
7
|
+
export type RunState = 'pending' | 'failed';
|
|
8
|
+
type Optional<V> = Field<V | null, {
|
|
9
|
+
readonly optional: true;
|
|
10
|
+
}>;
|
|
11
|
+
type RunFields = {
|
|
12
|
+
/** The job's name. */
|
|
13
|
+
readonly job: Field<string, ScalarOptions<string>>;
|
|
14
|
+
/** A recurring job's name, on its one row. */
|
|
15
|
+
readonly recurring: Field<string | null, {
|
|
16
|
+
readonly optional: true;
|
|
17
|
+
readonly unique: true;
|
|
18
|
+
}>;
|
|
19
|
+
/** A recurring job's `every` or `cron`, which tells when it changed. */
|
|
20
|
+
readonly schedule: Optional<string>;
|
|
21
|
+
readonly input: Field<unknown, {
|
|
22
|
+
readonly optional: true;
|
|
23
|
+
}, never, never, unknown, true>;
|
|
24
|
+
readonly state: Field<string, ScalarOptions<string>>;
|
|
25
|
+
readonly dueAt: Field<Date, ScalarOptions<Date>>;
|
|
26
|
+
/** The runs that failed. */
|
|
27
|
+
readonly attempts: Field<number, {
|
|
28
|
+
readonly integer: true;
|
|
29
|
+
}>;
|
|
30
|
+
/** Until when an instance holds the row. */
|
|
31
|
+
readonly lockedUntil: Optional<Date>;
|
|
32
|
+
/** The last failure's message. */
|
|
33
|
+
readonly error: Optional<string>;
|
|
34
|
+
};
|
|
35
|
+
export declare const JobsRun: ModelOf<'JobsRun', RunFields, []>;
|
|
36
|
+
export {};
|
|
37
|
+
//# sourceMappingURL=model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC/D,OAAO,EAAe,MAAM,EAAE,MAAM,YAAY,CAAA;AAGhD,eAAO,MAAM,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,EAAE,OAAO,MAAM,CAAA;CAAmB,CAAA;AAE/D,mEAAmE;AACnE,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAA;AAE3C,KAAK,QAAQ,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,GAAG,IAAI,EAAE;IAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;CAAE,CAAC,CAAA;AAE/D,KAAK,SAAS,GAAG;IAChB,sBAAsB;IACtB,QAAQ,CAAC,GAAG,EAAE,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;IAClD,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,KAAK,CACxB,MAAM,GAAG,IAAI,EACb;QAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAA;KAAE,CAClD,CAAA;IACD,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;IACnC,QAAQ,CAAC,KAAK,EAAE,KAAK,CACpB,OAAO,EACP;QAAE,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAA;KAAE,EAC3B,KAAK,EACL,KAAK,EACL,OAAO,EACP,IAAI,CACJ,CAAA;IACD,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAA;IACpD,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC,CAAA;IAChD,4BAA4B;IAC5B,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAA;KAAE,CAAC,CAAA;IAC5D,4CAA4C;IAC5C,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAA;IACpC,kCAAkC;IAClC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;CAChC,CAAA;AAED,eAAO,MAAM,OAAO,EAAE,OAAO,CAAC,SAAS,EAAE,SAAS,EAAE,EAAE,CAuBpD,CAAA"}
|
package/dist/model.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineModel, master } from 'ultimabase';
|
|
2
|
+
import { runningOf } from './state.js';
|
|
3
|
+
export const M = { as: master };
|
|
4
|
+
export const JobsRun = defineModel({
|
|
5
|
+
name: 'JobsRun',
|
|
6
|
+
fields: (f) => ({
|
|
7
|
+
job: f.string(),
|
|
8
|
+
recurring: f.string({ optional: true, unique: true }),
|
|
9
|
+
schedule: f.string({ optional: true }),
|
|
10
|
+
input: f.json({ optional: true }),
|
|
11
|
+
state: f.string(),
|
|
12
|
+
dueAt: f.dateTime(),
|
|
13
|
+
attempts: f.number({ integer: true }),
|
|
14
|
+
lockedUntil: f.dateTime({ optional: true }),
|
|
15
|
+
error: f.string({ optional: true }),
|
|
16
|
+
}),
|
|
17
|
+
indexes: [{ fields: ['state', 'dueAt'] }],
|
|
18
|
+
}).with({
|
|
19
|
+
hooks: {
|
|
20
|
+
// A run due now starts once its write committed, in this instance.
|
|
21
|
+
afterCreateCommit: ({ record }) => {
|
|
22
|
+
if (record.recurring === null && record.dueAt.getTime() <= Date.now()) {
|
|
23
|
+
runningOf('A run').kick(record.id);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PluginOf, PluginTypes } from 'ultimabase';
|
|
2
|
+
import type { Job, RecurringJob } from './define-job.ts';
|
|
3
|
+
/** What `jobsPlugin` takes. */
|
|
4
|
+
export interface JobsOptions {
|
|
5
|
+
readonly jobs: readonly (Job<string, never> | RecurringJob<string>)[];
|
|
6
|
+
}
|
|
7
|
+
/** What the jobs plugin offers other plugins, and `runJobs()`. */
|
|
8
|
+
export interface JobsApi {
|
|
9
|
+
/** Runs the due rows, and those their runs enqueue, until none is due. */
|
|
10
|
+
readonly drain: () => Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/** The jobs plugin's types (D-57). */
|
|
13
|
+
export interface Jobs extends PluginTypes {
|
|
14
|
+
readonly name: 'jobs';
|
|
15
|
+
readonly appOptions: JobsOptions;
|
|
16
|
+
readonly api: JobsApi;
|
|
17
|
+
}
|
|
18
|
+
/** Jobs: runs enqueued with a write, delayed or on a schedule (D-221). */
|
|
19
|
+
export declare const jobsPlugin: PluginOf<Jobs>;
|
|
20
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAGvD,OAAO,KAAK,EAAa,GAAG,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAOnE,+BAA+B;AAC/B,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC,EAAE,CAAA;CACrE;AAED,kEAAkE;AAClE,MAAM,WAAW,OAAO;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACnC;AAED,sCAAsC;AACtC,MAAM,WAAW,IAAK,SAAQ,WAAW;IACxC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAA;IAChC,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAA;CACrB;AAwED,0EAA0E;AAC1E,eAAO,MAAM,UAAU,EAAE,QAAQ,CAAC,IAAI,CAAqB,CAAA"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { definePlugin, Refused } from 'ultimabase';
|
|
2
|
+
import manifest from '../package.json' with { type: 'json' };
|
|
3
|
+
import { recordOf } from './define-job.js';
|
|
4
|
+
import { JobsRun } from './model.js';
|
|
5
|
+
import { drain, pass, schedule, settled, start } from './runner.js';
|
|
6
|
+
import { runningOf, useRunning } from './state.js';
|
|
7
|
+
/** Five seconds, between two looks for due rows. */
|
|
8
|
+
const pollEvery = 5000;
|
|
9
|
+
/** The jobs by name; refuses a value `defineJob` did not return and a name twice. */
|
|
10
|
+
function jobsOf(jobs) {
|
|
11
|
+
if (!Array.isArray(jobs)) {
|
|
12
|
+
throw new Refused('jobsPlugin’s jobs is an array of defineJob values.');
|
|
13
|
+
}
|
|
14
|
+
const byName = new Map();
|
|
15
|
+
for (const job of jobs) {
|
|
16
|
+
const record = recordOf(job);
|
|
17
|
+
if (record === undefined) {
|
|
18
|
+
throw new Refused('jobsPlugin’s jobs is an array of defineJob values.');
|
|
19
|
+
}
|
|
20
|
+
if (byName.has(record.name)) {
|
|
21
|
+
throw new Refused(`jobsPlugin’s jobs name ${record.name} twice.`);
|
|
22
|
+
}
|
|
23
|
+
byName.set(record.name, record);
|
|
24
|
+
}
|
|
25
|
+
return byName;
|
|
26
|
+
}
|
|
27
|
+
// The poll's timer, while the application runs.
|
|
28
|
+
let timer;
|
|
29
|
+
let polling = false;
|
|
30
|
+
/** One look for due rows, unless the last one still runs. */
|
|
31
|
+
async function poll(app) {
|
|
32
|
+
if (polling)
|
|
33
|
+
return;
|
|
34
|
+
polling = true;
|
|
35
|
+
try {
|
|
36
|
+
while ((await pass(app)) > 0)
|
|
37
|
+
;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
app.logger.warn({ err: error }, 'The jobs could not be read.');
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
polling = false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const definition = definePlugin({
|
|
47
|
+
name: 'jobs',
|
|
48
|
+
manifest,
|
|
49
|
+
models: [JobsRun],
|
|
50
|
+
api: () => ({ drain: () => drain(runningOf('runJobs()')) }),
|
|
51
|
+
start: async ({ options, logger }) => {
|
|
52
|
+
const app = {
|
|
53
|
+
jobs: jobsOf(options.jobs),
|
|
54
|
+
logger,
|
|
55
|
+
kick: (id) => void start(app, id),
|
|
56
|
+
};
|
|
57
|
+
useRunning(app);
|
|
58
|
+
await schedule(app);
|
|
59
|
+
},
|
|
60
|
+
// What was due while no instance ran runs once now.
|
|
61
|
+
started: () => {
|
|
62
|
+
const app = runningOf('jobsPlugin');
|
|
63
|
+
void poll(app);
|
|
64
|
+
timer = setInterval(() => void poll(app), pollEvery);
|
|
65
|
+
timer.unref();
|
|
66
|
+
},
|
|
67
|
+
stopping: async () => {
|
|
68
|
+
clearInterval(timer);
|
|
69
|
+
timer = undefined;
|
|
70
|
+
await settled();
|
|
71
|
+
},
|
|
72
|
+
stop: () => {
|
|
73
|
+
useRunning(undefined);
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
/** Jobs: runs enqueued with a write, delayed or on a schedule (D-221). */
|
|
77
|
+
export const jobsPlugin = definition.plugin;
|
package/dist/runner.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Running } from './state.ts';
|
|
2
|
+
/** Five minutes, an instance's hold on a row. */
|
|
3
|
+
export declare const lease: number;
|
|
4
|
+
/** The attempts of a run before it turns `failed`. */
|
|
5
|
+
export declare const attempts = 10;
|
|
6
|
+
/** Ten seconds, the wait after a first failure. */
|
|
7
|
+
export declare const backoff: number;
|
|
8
|
+
/** Starts the row's run; a failure outside the job is logged. */
|
|
9
|
+
export declare function start(app: Running, id: string): Promise<boolean>;
|
|
10
|
+
/** Runs the rows due now, a batch at a time; how many it ran. */
|
|
11
|
+
export declare function pass(app: Running): Promise<number>;
|
|
12
|
+
/** Waits for the runs this instance started. */
|
|
13
|
+
export declare function settled(): Promise<void>;
|
|
14
|
+
/**
|
|
15
|
+
* Runs the due rows, and those their runs enqueue, until none is due:
|
|
16
|
+
* what `runJobs()` does.
|
|
17
|
+
*/
|
|
18
|
+
export declare function drain(app: Running): Promise<void>;
|
|
19
|
+
/** Creates the rows of new recurring jobs, and deletes those of old ones. */
|
|
20
|
+
export declare function schedule(app: Running): Promise<void>;
|
|
21
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAEzC,iDAAiD;AACjD,eAAO,MAAM,KAAK,EAAE,MAAsB,CAAA;AAC1C,sDAAsD;AACtD,eAAO,MAAM,QAAQ,KAAK,CAAA;AAC1B,mDAAmD;AACnD,eAAO,MAAM,OAAO,EAAE,MAAkB,CAAA;AA4GxC,iEAAiE;AACjE,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQhE;AAED,iEAAiE;AACjE,wBAAsB,IAAI,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAYxD;AAED,gDAAgD;AAChD,wBAAsB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAE7C;AAED;;;GAGG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAQvD;AAED,6EAA6E;AAC7E,wBAAsB,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAgC1D"}
|
package/dist/runner.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { JobsRun, M } from './model.js';
|
|
2
|
+
/** Five minutes, an instance's hold on a row. */
|
|
3
|
+
export const lease = 5 * 60 * 1000;
|
|
4
|
+
/** The attempts of a run before it turns `failed`. */
|
|
5
|
+
export const attempts = 10;
|
|
6
|
+
/** Ten seconds, the wait after a first failure. */
|
|
7
|
+
export const backoff = 10 * 1000;
|
|
8
|
+
/** The rows a pass runs at once. */
|
|
9
|
+
const batch = 20;
|
|
10
|
+
/** The rows due at `now` that no instance holds. */
|
|
11
|
+
function due(now) {
|
|
12
|
+
return {
|
|
13
|
+
state: 'pending',
|
|
14
|
+
dueAt: { lte: now },
|
|
15
|
+
OR: [{ lockedUntil: null }, { lockedUntil: { lt: now } }],
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Whether this instance now holds the row: one update counts it. */
|
|
19
|
+
async function claim(id, now) {
|
|
20
|
+
const { count } = await JobsRun.updateMany({
|
|
21
|
+
where: { AND: [{ id }, due(new Date(now))] },
|
|
22
|
+
data: { lockedUntil: new Date(now + lease) },
|
|
23
|
+
}, M);
|
|
24
|
+
return count === 1;
|
|
25
|
+
}
|
|
26
|
+
/** The first due time of a recurring job after `now`, on its cadence. */
|
|
27
|
+
function following(job, row, now) {
|
|
28
|
+
let next = job.next(row.dueAt.getTime());
|
|
29
|
+
// Missed occurrences run once: the next is the first after now.
|
|
30
|
+
if (next <= now)
|
|
31
|
+
next = job.next(now);
|
|
32
|
+
return new Date(next);
|
|
33
|
+
}
|
|
34
|
+
function messageOf(error) {
|
|
35
|
+
return error instanceof Error ? error.message : String(error);
|
|
36
|
+
}
|
|
37
|
+
async function failed(app, row, job, error, now) {
|
|
38
|
+
const message = messageOf(error);
|
|
39
|
+
const facts = { err: error, job: row.job, run: row.id };
|
|
40
|
+
if (job?.schedule !== undefined && row.recurring !== null) {
|
|
41
|
+
app.logger.error(facts, `The job ${row.job} failed.`);
|
|
42
|
+
const dueAt = following(job, row, now);
|
|
43
|
+
const data = { dueAt, lockedUntil: null, error: message };
|
|
44
|
+
await JobsRun.update({ where: { id: row.id }, data }, M);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const tried = row.attempts + 1;
|
|
48
|
+
const last = job === undefined || tried >= attempts;
|
|
49
|
+
if (last) {
|
|
50
|
+
app.logger.error(facts, `The job ${row.job} failed: its run is kept.`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
app.logger.warn(facts, `The job ${row.job} failed: it runs again.`);
|
|
54
|
+
}
|
|
55
|
+
const data = {
|
|
56
|
+
attempts: tried,
|
|
57
|
+
lockedUntil: null,
|
|
58
|
+
error: message,
|
|
59
|
+
...(last
|
|
60
|
+
? { state: 'failed' }
|
|
61
|
+
: { dueAt: new Date(now + backoff * 2 ** (tried - 1)) }),
|
|
62
|
+
};
|
|
63
|
+
await JobsRun.update({ where: { id: row.id }, data }, M);
|
|
64
|
+
}
|
|
65
|
+
/** Runs the row once this instance holds it; whether it did. */
|
|
66
|
+
async function runRow(app, id) {
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
if (!(await claim(id, now)))
|
|
69
|
+
return false;
|
|
70
|
+
const row = await JobsRun.findUnique({ where: { id } }, M);
|
|
71
|
+
if (row === null)
|
|
72
|
+
return false;
|
|
73
|
+
const job = app.jobs.get(row.job);
|
|
74
|
+
if (job === undefined) {
|
|
75
|
+
const error = new Error(`The job ${row.job} is not in jobsPlugin's jobs.`);
|
|
76
|
+
await failed(app, row, job, error, now);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
await job.run(job.input === undefined ? { id } : { id, input: row.input });
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
await failed(app, row, job, error, now);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
if (row.recurring === null) {
|
|
87
|
+
await JobsRun.delete({ where: { id } }, M);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const data = {
|
|
91
|
+
dueAt: following(job, row, now),
|
|
92
|
+
attempts: 0,
|
|
93
|
+
lockedUntil: null,
|
|
94
|
+
error: null,
|
|
95
|
+
};
|
|
96
|
+
await JobsRun.update({ where: { id }, data }, M);
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
/** The runs this instance has started and not finished. */
|
|
101
|
+
const running = new Set();
|
|
102
|
+
/** Starts the row's run; a failure outside the job is logged. */
|
|
103
|
+
export function start(app, id) {
|
|
104
|
+
const run = runRow(app, id).catch((error) => {
|
|
105
|
+
app.logger.error({ err: error, run: id }, 'A job could not run.');
|
|
106
|
+
return false;
|
|
107
|
+
});
|
|
108
|
+
running.add(run);
|
|
109
|
+
void run.finally(() => running.delete(run));
|
|
110
|
+
return run;
|
|
111
|
+
}
|
|
112
|
+
/** Runs the rows due now, a batch at a time; how many it ran. */
|
|
113
|
+
export async function pass(app) {
|
|
114
|
+
const rows = await JobsRun.findMany({
|
|
115
|
+
where: due(new Date()),
|
|
116
|
+
orderBy: [{ dueAt: 'asc' }],
|
|
117
|
+
take: batch,
|
|
118
|
+
select: { id: true },
|
|
119
|
+
}, M);
|
|
120
|
+
const ran = await Promise.all(rows.map((row) => start(app, row.id)));
|
|
121
|
+
return ran.filter(Boolean).length;
|
|
122
|
+
}
|
|
123
|
+
/** Waits for the runs this instance started. */
|
|
124
|
+
export async function settled() {
|
|
125
|
+
await Promise.all(running);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Runs the due rows, and those their runs enqueue, until none is due:
|
|
129
|
+
* what `runJobs()` does.
|
|
130
|
+
*/
|
|
131
|
+
export async function drain(app) {
|
|
132
|
+
for (let passes = 0; passes < 100; passes++) {
|
|
133
|
+
await settled();
|
|
134
|
+
if ((await pass(app)) === 0 && running.size === 0)
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
throw new Error('runJobs stopped after 100 passes: a job enqueues a run due at once each time it runs.');
|
|
138
|
+
}
|
|
139
|
+
/** Creates the rows of new recurring jobs, and deletes those of old ones. */
|
|
140
|
+
export async function schedule(app) {
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
const rows = await JobsRun.findMany({ where: { recurring: { not: null } } }, M);
|
|
143
|
+
for (const row of rows) {
|
|
144
|
+
const job = app.jobs.get(row.recurring ?? '');
|
|
145
|
+
if (job?.schedule === undefined) {
|
|
146
|
+
await JobsRun.delete({ where: { id: row.id } }, M);
|
|
147
|
+
}
|
|
148
|
+
else if (job.schedule !== row.schedule) {
|
|
149
|
+
const data = { schedule: job.schedule, dueAt: new Date(job.next(now)) };
|
|
150
|
+
await JobsRun.update({ where: { id: row.id }, data }, M);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const known = new Set(rows.map((row) => row.recurring));
|
|
154
|
+
for (const job of app.jobs.values()) {
|
|
155
|
+
if (job.schedule === undefined || known.has(job.name))
|
|
156
|
+
continue;
|
|
157
|
+
const create = {
|
|
158
|
+
job: job.name,
|
|
159
|
+
recurring: job.name,
|
|
160
|
+
schedule: job.schedule,
|
|
161
|
+
state: 'pending',
|
|
162
|
+
dueAt: new Date(job.next(now)),
|
|
163
|
+
attempts: 0,
|
|
164
|
+
};
|
|
165
|
+
// Another instance starting at once may have created it.
|
|
166
|
+
await JobsRun.upsert({ where: { recurring: job.name }, create, update: {} }, M);
|
|
167
|
+
}
|
|
168
|
+
}
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Logger } from 'ultimabase';
|
|
2
|
+
import type { JobRecord } from './define-job.ts';
|
|
3
|
+
export interface Running {
|
|
4
|
+
readonly jobs: ReadonlyMap<string, JobRecord>;
|
|
5
|
+
readonly logger: Logger;
|
|
6
|
+
/** Runs the row `id` now, in this instance. */
|
|
7
|
+
readonly kick: (id: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function useRunning(value: Running | undefined): void;
|
|
10
|
+
/** The running plugin's state; `what` names the call that needs it. */
|
|
11
|
+
export declare function runningOf(what: string): Running;
|
|
12
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AACxC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAEhD,MAAM,WAAW,OAAO;IACvB,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;IAC7C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;CACnC;AAID,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAE3D;AAED,uEAAuE;AACvE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAK/C"}
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
let running;
|
|
2
|
+
export function useRunning(value) {
|
|
3
|
+
running = value;
|
|
4
|
+
}
|
|
5
|
+
/** The running plugin's state; `what` names the call that needs it. */
|
|
6
|
+
export function runningOf(what) {
|
|
7
|
+
if (running === undefined) {
|
|
8
|
+
throw new Error(`${what} needs an application with jobsPlugin() started.`);
|
|
9
|
+
}
|
|
10
|
+
return running;
|
|
11
|
+
}
|