@ultimat3/jobs 1.0.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/LICENSE +21 -0
- package/README.md +202 -0
- package/package.json +38 -0
- package/src/clock.d.ts +7 -0
- package/src/clock.d.ts.map +1 -0
- package/src/clock.js +21 -0
- package/src/clock.js.map +1 -0
- package/src/clock.ts +23 -0
- package/src/describe.ts +61 -0
- package/src/driver-memory.d.ts +9 -0
- package/src/driver-memory.d.ts.map +1 -0
- package/src/driver-memory.js +189 -0
- package/src/driver-memory.js.map +1 -0
- package/src/driver-memory.ts +216 -0
- package/src/driver-nats.d.ts +7 -0
- package/src/driver-nats.d.ts.map +1 -0
- package/src/driver-nats.js +51 -0
- package/src/driver-nats.js.map +1 -0
- package/src/driver-nats.ts +73 -0
- package/src/driver-pg-sql.d.ts +19 -0
- package/src/driver-pg-sql.d.ts.map +1 -0
- package/src/driver-pg-sql.js +160 -0
- package/src/driver-pg-sql.js.map +1 -0
- package/src/driver-pg-sql.ts +170 -0
- package/src/driver-pg.d.ts +17 -0
- package/src/driver-pg.d.ts.map +1 -0
- package/src/driver-pg.js +246 -0
- package/src/driver-pg.js.map +1 -0
- package/src/driver-pg.ts +356 -0
- package/src/driver-redis.d.ts +7 -0
- package/src/driver-redis.d.ts.map +1 -0
- package/src/driver-redis.js +54 -0
- package/src/driver-redis.js.map +1 -0
- package/src/driver-redis.ts +76 -0
- package/src/driver.d.ts +114 -0
- package/src/driver.d.ts.map +1 -0
- package/src/driver.js +14 -0
- package/src/driver.js.map +1 -0
- package/src/driver.ts +143 -0
- package/src/errors.d.ts +63 -0
- package/src/errors.d.ts.map +1 -0
- package/src/errors.js +105 -0
- package/src/errors.js.map +1 -0
- package/src/errors.ts +165 -0
- package/src/events.d.ts +35 -0
- package/src/events.d.ts.map +1 -0
- package/src/events.js +92 -0
- package/src/events.js.map +1 -0
- package/src/events.ts +134 -0
- package/src/index.d.ts +32 -0
- package/src/index.d.ts.map +1 -0
- package/src/index.js +18 -0
- package/src/index.js.map +1 -0
- package/src/index.ts +172 -0
- package/src/inspect.d.ts +82 -0
- package/src/inspect.d.ts.map +1 -0
- package/src/inspect.js +113 -0
- package/src/inspect.js.map +1 -0
- package/src/inspect.ts +213 -0
- package/src/job.d.ts +71 -0
- package/src/job.d.ts.map +1 -0
- package/src/job.js +99 -0
- package/src/job.js.map +1 -0
- package/src/job.ts +261 -0
- package/src/limits.d.ts +47 -0
- package/src/limits.d.ts.map +1 -0
- package/src/limits.js +0 -0
- package/src/limits.js.map +1 -0
- package/src/limits.ts +0 -0
- package/src/outbox.d.ts +81 -0
- package/src/outbox.d.ts.map +1 -0
- package/src/outbox.js +202 -0
- package/src/outbox.js.map +1 -0
- package/src/outbox.ts +336 -0
- package/src/register.ts +40 -0
- package/src/retry.d.ts +40 -0
- package/src/retry.d.ts.map +1 -0
- package/src/retry.js +59 -0
- package/src/retry.js.map +1 -0
- package/src/retry.ts +90 -0
- package/src/scheduler.d.ts +79 -0
- package/src/scheduler.d.ts.map +1 -0
- package/src/scheduler.js +183 -0
- package/src/scheduler.js.map +1 -0
- package/src/scheduler.ts +417 -0
- package/src/steps.d.ts +86 -0
- package/src/steps.d.ts.map +1 -0
- package/src/steps.js +227 -0
- package/src/steps.js.map +1 -0
- package/src/steps.ts +339 -0
- package/src/worker.d.ts +68 -0
- package/src/worker.d.ts.map +1 -0
- package/src/worker.js +273 -0
- package/src/worker.js.map +1 -0
- package/src/worker.ts +356 -0
package/src/outbox.ts
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// The transactional outbox — ON BY DEFAULT, because the alternative is a bug you cannot see
|
|
2
|
+
// in review. `ctx.jobs.enqueue()` inside a request writes the job row in the SAME `tx` as the
|
|
3
|
+
// business rows and a relay publishes it after commit. Without it, every enqueue is a
|
|
4
|
+
// distributed-transaction coin flip:
|
|
5
|
+
//
|
|
6
|
+
// enqueue then rollback -> the job runs against rows that never existed
|
|
7
|
+
// commit then enqueue -> the process dies in between and the job is lost forever
|
|
8
|
+
//
|
|
9
|
+
// Both are load-dependent, both pass every test, and both are the top source of "the email
|
|
10
|
+
// went out but the order isn't in the database" tickets. Joining the transaction removes the
|
|
11
|
+
// window entirely; the relay's at-least-once delivery is deduped by the job's idempotencyKey.
|
|
12
|
+
|
|
13
|
+
import type { Clock } from '@ultimat3/core';
|
|
14
|
+
import { logger, uuid } from '@ultimat3/core';
|
|
15
|
+
import type { Tx } from '@ultimat3/entity';
|
|
16
|
+
import { nowMs } from './clock';
|
|
17
|
+
import type { EnqueueResult, JobDriver } from './driver';
|
|
18
|
+
import { DEFAULT_QUEUE, jobDriver } from './driver';
|
|
19
|
+
import { DriverUnavailableError, OutboxNoTxError } from './errors';
|
|
20
|
+
import type { JobHandle } from './job';
|
|
21
|
+
|
|
22
|
+
export interface OutboxRecord {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
readonly job: string;
|
|
25
|
+
readonly queue: string;
|
|
26
|
+
readonly input: unknown;
|
|
27
|
+
readonly idempotencyKey: string;
|
|
28
|
+
readonly maxAttempts: number;
|
|
29
|
+
readonly runAt: number;
|
|
30
|
+
readonly stagedAt: number;
|
|
31
|
+
readonly tenantId?: string;
|
|
32
|
+
readonly publishedAt?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface OutboxStore {
|
|
36
|
+
/** Write inside `tx`. Nothing is visible to the relay until that tx commits. */
|
|
37
|
+
stage(tx: Tx, record: OutboxRecord): Promise<void>;
|
|
38
|
+
/** Called by the tx runner after COMMIT. */
|
|
39
|
+
commit(tx: Tx): Promise<readonly OutboxRecord[]>;
|
|
40
|
+
/** Called by the tx runner after ROLLBACK. Staged rows vanish with the transaction. */
|
|
41
|
+
rollback(tx: Tx): Promise<void>;
|
|
42
|
+
/** Unpublished, committed rows — the relay's work queue. */
|
|
43
|
+
claim(limit: number): Promise<readonly OutboxRecord[]>;
|
|
44
|
+
markPublished(id: string, at: number): Promise<void>;
|
|
45
|
+
pendingCount(): Promise<number>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Default store. Staged rows hang off the `Tx` object itself in a WeakMap, so the "same
|
|
50
|
+
* transaction" guarantee needs no cooperation from the DB layer and rollback is a delete.
|
|
51
|
+
* The pg store swaps this for a real `x_outbox` table written by the same connection.
|
|
52
|
+
*/
|
|
53
|
+
export function createMemoryOutboxStore(): OutboxStore {
|
|
54
|
+
const staged = new WeakMap<object, OutboxRecord[]>();
|
|
55
|
+
const committed = new Map<string, OutboxRecord>();
|
|
56
|
+
|
|
57
|
+
const key = (tx: Tx): object => tx as unknown as object;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
stage(tx, record) {
|
|
61
|
+
const bucket = staged.get(key(tx)) ?? [];
|
|
62
|
+
bucket.push(record);
|
|
63
|
+
staged.set(key(tx), bucket);
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
},
|
|
66
|
+
commit(tx) {
|
|
67
|
+
const bucket = staged.get(key(tx)) ?? [];
|
|
68
|
+
staged.delete(key(tx));
|
|
69
|
+
for (const record of bucket) committed.set(record.id, record);
|
|
70
|
+
return Promise.resolve(bucket);
|
|
71
|
+
},
|
|
72
|
+
rollback(tx) {
|
|
73
|
+
staged.delete(key(tx));
|
|
74
|
+
return Promise.resolve();
|
|
75
|
+
},
|
|
76
|
+
claim(limit) {
|
|
77
|
+
const ready = [...committed.values()]
|
|
78
|
+
.filter((record) => record.publishedAt === undefined)
|
|
79
|
+
.sort((a, b) => a.stagedAt - b.stagedAt)
|
|
80
|
+
.slice(0, limit);
|
|
81
|
+
return Promise.resolve(ready);
|
|
82
|
+
},
|
|
83
|
+
markPublished(id, at) {
|
|
84
|
+
const record = committed.get(id);
|
|
85
|
+
if (record !== undefined) committed.set(id, { ...record, publishedAt: at });
|
|
86
|
+
return Promise.resolve();
|
|
87
|
+
},
|
|
88
|
+
pendingCount() {
|
|
89
|
+
let count = 0;
|
|
90
|
+
for (const record of committed.values()) {
|
|
91
|
+
if (record.publishedAt === undefined) count += 1;
|
|
92
|
+
}
|
|
93
|
+
return Promise.resolve(count);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface EnqueueOptions {
|
|
99
|
+
/** Epoch ms, or a delay via `runAt: nowMs() + toMs('5m')`. */
|
|
100
|
+
readonly runAt?: number;
|
|
101
|
+
readonly tenantId?: string;
|
|
102
|
+
readonly queue?: string;
|
|
103
|
+
/** Escape hatch for enqueues that must fire regardless of the caller's transaction. */
|
|
104
|
+
readonly outbox?: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface OutboxDeps {
|
|
108
|
+
readonly store: OutboxStore;
|
|
109
|
+
readonly driver: JobDriver;
|
|
110
|
+
readonly clock?: Clock;
|
|
111
|
+
/** `'required'` fails an out-of-transaction enqueue instead of publishing it directly. */
|
|
112
|
+
readonly mode?: 'default' | 'required';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Stage a job inside `tx`. Returns the row that the relay will publish after commit. */
|
|
116
|
+
export function enqueueInTx<I>(
|
|
117
|
+
deps: OutboxDeps,
|
|
118
|
+
tx: Tx,
|
|
119
|
+
handle: JobHandle<I>,
|
|
120
|
+
input: I,
|
|
121
|
+
options: EnqueueOptions = {},
|
|
122
|
+
): Promise<OutboxRecord> {
|
|
123
|
+
const at = nowMs(deps.clock);
|
|
124
|
+
const record: OutboxRecord = {
|
|
125
|
+
id: uuid(),
|
|
126
|
+
job: handle.name,
|
|
127
|
+
queue: options.queue ?? handle.queue ?? DEFAULT_QUEUE,
|
|
128
|
+
input,
|
|
129
|
+
idempotencyKey: handle.idempotencyKeyFor(input),
|
|
130
|
+
maxAttempts: handle.retry.attempts,
|
|
131
|
+
runAt: options.runAt ?? at,
|
|
132
|
+
stagedAt: at,
|
|
133
|
+
...(options.tenantId === undefined ? {} : { tenantId: options.tenantId }),
|
|
134
|
+
};
|
|
135
|
+
return deps.store.stage(tx, record).then(() => record);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface JobsFacade {
|
|
139
|
+
/**
|
|
140
|
+
* Joins the ambient transaction when there is one. Same call site in a request handler,
|
|
141
|
+
* a job, or a script — the transactional behaviour is the framework's problem, not yours.
|
|
142
|
+
*/
|
|
143
|
+
enqueue<I>(handle: JobHandle<I>, input: I, options?: EnqueueOptions): Promise<EnqueueResult>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const STAGED_RESULT: EnqueueResult = { id: '', runId: '', deduped: false };
|
|
147
|
+
|
|
148
|
+
export function createJobsFacade(deps: OutboxDeps, currentTx: () => Tx | undefined): JobsFacade {
|
|
149
|
+
return {
|
|
150
|
+
async enqueue<I>(
|
|
151
|
+
handle: JobHandle<I>,
|
|
152
|
+
input: I,
|
|
153
|
+
options: EnqueueOptions = {},
|
|
154
|
+
): Promise<EnqueueResult> {
|
|
155
|
+
const tx = options.outbox === false ? undefined : currentTx();
|
|
156
|
+
|
|
157
|
+
if (tx === undefined) {
|
|
158
|
+
if (deps.mode === 'required' && options.outbox !== false) {
|
|
159
|
+
throw new OutboxNoTxError({ job: handle.name });
|
|
160
|
+
}
|
|
161
|
+
return deps.driver.enqueue({
|
|
162
|
+
name: handle.name,
|
|
163
|
+
queue: options.queue ?? handle.queue,
|
|
164
|
+
input,
|
|
165
|
+
idempotencyKey: handle.idempotencyKeyFor(input),
|
|
166
|
+
maxAttempts: handle.retry.attempts,
|
|
167
|
+
runAt: options.runAt ?? nowMs(deps.clock),
|
|
168
|
+
...(options.tenantId === undefined ? {} : { tenantId: options.tenantId }),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const record = await enqueueInTx(deps, tx, handle, input, options);
|
|
173
|
+
// No queue id yet by design: the row does not exist until COMMIT.
|
|
174
|
+
return { ...STAGED_RESULT, id: record.id };
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
let ambient: JobsFacade | undefined;
|
|
180
|
+
let fallback: JobsFacade | undefined;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Installed once at boot, next to `setJobDriver`, with the app's outbox store and its
|
|
184
|
+
* transaction accessor. `handle.enqueue()` then joins the caller's transaction wherever it is
|
|
185
|
+
* called from — which is the only reason the outbox protects anything.
|
|
186
|
+
*/
|
|
187
|
+
export function setJobsFacade(facade: JobsFacade | null): void {
|
|
188
|
+
ambient = facade ?? undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The one enqueue path. With no facade installed the fallback publishes straight to the
|
|
193
|
+
* ambient driver, so a script, a test or `x dev` enqueues without wiring anything — an app
|
|
194
|
+
* that installed the outbox gets the outbox, at the same call site.
|
|
195
|
+
*/
|
|
196
|
+
export function jobsFacade(): JobsFacade {
|
|
197
|
+
if (ambient !== undefined) return ambient;
|
|
198
|
+
fallback ??= createJobsFacade(
|
|
199
|
+
{
|
|
200
|
+
// A getter, not a snapshot: `setJobDriver()` after the first enqueue is honoured, and a
|
|
201
|
+
// missing driver is an error at the call rather than at import time.
|
|
202
|
+
get driver(): JobDriver {
|
|
203
|
+
const installed = jobDriver();
|
|
204
|
+
if (installed === undefined) {
|
|
205
|
+
throw new DriverUnavailableError({
|
|
206
|
+
driver: 'none',
|
|
207
|
+
cause: 'no queue driver is installed in this process',
|
|
208
|
+
fix: 'call setJobDriver(createMemoryDriver()) before enqueuing — or set jobs.driver in app.config.ts and run `x dev`',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return installed;
|
|
212
|
+
},
|
|
213
|
+
// Unreachable while `currentTx` is `() => undefined`; present because `OutboxDeps`
|
|
214
|
+
// requires a store, and a real one is cheaper than an assertion that cannot fire.
|
|
215
|
+
store: createMemoryOutboxStore(),
|
|
216
|
+
},
|
|
217
|
+
() => undefined,
|
|
218
|
+
);
|
|
219
|
+
return fallback;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Test/CLI seam, the counterpart to `resetJobDriver()`: forget the installed facade. */
|
|
223
|
+
export function resetJobsFacade(): void {
|
|
224
|
+
ambient = undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface RelayOptions extends OutboxDeps {
|
|
228
|
+
readonly batchSize?: number;
|
|
229
|
+
readonly intervalMs?: number;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface OutboxRelay {
|
|
233
|
+
/** One pass. Returns how many rows were published. Call it directly in tests. */
|
|
234
|
+
tick(): Promise<number>;
|
|
235
|
+
start(): void;
|
|
236
|
+
stop(): void;
|
|
237
|
+
pending(): Promise<number>;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* At-least-once by construction: publish, THEN mark published. A crash between the two
|
|
242
|
+
* re-publishes, which the idempotency key collapses — the opposite order would lose jobs.
|
|
243
|
+
*/
|
|
244
|
+
export function createOutboxRelay(options: RelayOptions): OutboxRelay {
|
|
245
|
+
const batchSize = options.batchSize ?? 100;
|
|
246
|
+
const intervalMs = options.intervalMs ?? 200;
|
|
247
|
+
let timer: ReturnType<typeof setInterval> | undefined;
|
|
248
|
+
let running = false;
|
|
249
|
+
|
|
250
|
+
const tick = async (): Promise<number> => {
|
|
251
|
+
const batch = await options.store.claim(batchSize);
|
|
252
|
+
let published = 0;
|
|
253
|
+
for (const record of batch) {
|
|
254
|
+
try {
|
|
255
|
+
await options.driver.enqueue({
|
|
256
|
+
name: record.job,
|
|
257
|
+
queue: record.queue,
|
|
258
|
+
input: record.input,
|
|
259
|
+
idempotencyKey: record.idempotencyKey,
|
|
260
|
+
maxAttempts: record.maxAttempts,
|
|
261
|
+
runAt: record.runAt,
|
|
262
|
+
...(record.tenantId === undefined ? {} : { tenantId: record.tenantId }),
|
|
263
|
+
});
|
|
264
|
+
await options.store.markPublished(record.id, nowMs(options.clock));
|
|
265
|
+
published += 1;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
// Leave the row unpublished; the next tick retries it. Order is preserved per queue.
|
|
268
|
+
logger.warn('jobs.outbox.publish-failed', {
|
|
269
|
+
job: record.job,
|
|
270
|
+
id: record.id,
|
|
271
|
+
error: error instanceof Error ? error.message : String(error),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return published;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
tick,
|
|
280
|
+
start() {
|
|
281
|
+
if (timer !== undefined) return;
|
|
282
|
+
timer = setInterval(() => {
|
|
283
|
+
if (running) return;
|
|
284
|
+
running = true;
|
|
285
|
+
void tick().finally(() => {
|
|
286
|
+
running = false;
|
|
287
|
+
});
|
|
288
|
+
}, intervalMs);
|
|
289
|
+
},
|
|
290
|
+
stop() {
|
|
291
|
+
if (timer !== undefined) clearInterval(timer);
|
|
292
|
+
timer = undefined;
|
|
293
|
+
},
|
|
294
|
+
pending: () => options.store.pendingCount(),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** SQL for the pg-backed outbox. The relay publishes rows this INSERT created. */
|
|
299
|
+
export const SQL_OUTBOX_TABLE = `
|
|
300
|
+
create table if not exists x_outbox (
|
|
301
|
+
id uuid primary key,
|
|
302
|
+
job text not null,
|
|
303
|
+
queue text not null default 'default',
|
|
304
|
+
input jsonb not null,
|
|
305
|
+
idempotency_key text not null,
|
|
306
|
+
max_attempts int not null default 3,
|
|
307
|
+
run_at timestamptz not null default now(),
|
|
308
|
+
staged_at timestamptz not null default now(),
|
|
309
|
+
tenant_id text,
|
|
310
|
+
published_at timestamptz
|
|
311
|
+
);
|
|
312
|
+
create index if not exists x_outbox_unpublished_idx
|
|
313
|
+
on x_outbox (staged_at) where published_at is null;
|
|
314
|
+
`.trim();
|
|
315
|
+
|
|
316
|
+
export const SQL_OUTBOX_STAGE = `
|
|
317
|
+
insert into x_outbox
|
|
318
|
+
(id, job, queue, input, idempotency_key, max_attempts, run_at, staged_at, tenant_id)
|
|
319
|
+
values ($1, $2, $3, $4::jsonb, $5, $6, to_timestamp($7 / 1000.0), to_timestamp($8 / 1000.0), $9)
|
|
320
|
+
`.trim();
|
|
321
|
+
|
|
322
|
+
export const SQL_OUTBOX_CLAIM = `
|
|
323
|
+
select id, job, queue, input, idempotency_key, max_attempts,
|
|
324
|
+
(extract(epoch from run_at) * 1000)::bigint as run_at,
|
|
325
|
+
(extract(epoch from staged_at) * 1000)::bigint as staged_at,
|
|
326
|
+
tenant_id
|
|
327
|
+
from x_outbox
|
|
328
|
+
where published_at is null
|
|
329
|
+
order by staged_at
|
|
330
|
+
limit $1
|
|
331
|
+
for update skip locked
|
|
332
|
+
`.trim();
|
|
333
|
+
|
|
334
|
+
export const SQL_OUTBOX_MARK_PUBLISHED = `
|
|
335
|
+
update x_outbox set published_at = to_timestamp($2 / 1000.0) where id = $1
|
|
336
|
+
`.trim();
|
package/src/register.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export names become job and task names. `registerJobs(await import('./jobs'))` is how a module
|
|
3
|
+
* namespace becomes registered handles the queue, the scheduler and the manifest all address by
|
|
4
|
+
* the identifier the source file uses — never a positional `anonymous-job-2`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { type RegisteredPrimitive, registerPrimitiveRegistrar } from '@ultimat3/core';
|
|
8
|
+
import { isJobHandle, registerJob } from './job';
|
|
9
|
+
import { isTaskHandle, registerTask } from './scheduler';
|
|
10
|
+
|
|
11
|
+
/** `registerJobs(await import('./jobs'))` — export names become job names. */
|
|
12
|
+
export function registerJobs(
|
|
13
|
+
module: Readonly<Record<string, unknown>>,
|
|
14
|
+
): readonly RegisteredPrimitive[] {
|
|
15
|
+
const registered: RegisteredPrimitive[] = [];
|
|
16
|
+
for (const name of Object.keys(module).sort()) {
|
|
17
|
+
const value = module[name];
|
|
18
|
+
if (isJobHandle(value)) registered.push(registerJob(name, value));
|
|
19
|
+
}
|
|
20
|
+
return registered;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** `registerTasks(await import('./tasks'))` — export names become task names. */
|
|
24
|
+
export function registerTasks(
|
|
25
|
+
module: Readonly<Record<string, unknown>>,
|
|
26
|
+
): readonly RegisteredPrimitive[] {
|
|
27
|
+
const registered: RegisteredPrimitive[] = [];
|
|
28
|
+
for (const name of Object.keys(module).sort()) {
|
|
29
|
+
const value = module[name];
|
|
30
|
+
if (isTaskHandle(value)) registered.push(registerTask(name, value));
|
|
31
|
+
}
|
|
32
|
+
return registered;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// `defineApi` lives in `@ultimat3/action`, which sits on this tier and so cannot import this
|
|
36
|
+
// file. Announcing the registrars in core's table is what lets one `defineApi({ jobs, tasks })`
|
|
37
|
+
// call name durable work without a sideways import — importing the module you pass is what
|
|
38
|
+
// loads this.
|
|
39
|
+
registerPrimitiveRegistrar('job', registerJobs);
|
|
40
|
+
registerPrimitiveRegistrar('task', registerTasks);
|
package/src/retry.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { DurationInput } from './clock';
|
|
2
|
+
export type BackoffStrategy = 'exponential' | 'linear' | 'fixed';
|
|
3
|
+
export interface RetryPolicy {
|
|
4
|
+
/** Total attempts including the first. `attempts: 1` means no retry. */
|
|
5
|
+
readonly attempts: number;
|
|
6
|
+
readonly backoff?: BackoffStrategy;
|
|
7
|
+
/** Base delay. Default 1s. */
|
|
8
|
+
readonly delay?: DurationInput;
|
|
9
|
+
/** Ceiling for any single delay. Default 1h. */
|
|
10
|
+
readonly maxDelay?: DurationInput;
|
|
11
|
+
/**
|
|
12
|
+
* Equal jitter (half fixed, half random) by default. Without it, a burst of failures
|
|
13
|
+
* retries in lockstep and re-creates the thundering herd that killed the dependency.
|
|
14
|
+
*/
|
|
15
|
+
readonly jitter?: boolean;
|
|
16
|
+
/** Default true: an exhausted job is parked, never dropped. */
|
|
17
|
+
readonly deadLetter?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare const DEFAULT_RETRY: {
|
|
20
|
+
attempts: number;
|
|
21
|
+
backoff: "exponential";
|
|
22
|
+
delay: number;
|
|
23
|
+
maxDelay: number;
|
|
24
|
+
jitter: true;
|
|
25
|
+
deadLetter: true;
|
|
26
|
+
};
|
|
27
|
+
export type Random = () => number;
|
|
28
|
+
/** Delay before `attempt` (1-based: the delay after attempt 1 failed is `attempt: 1`). */
|
|
29
|
+
export declare function backoffDelayMs(policy: RetryPolicy, attempt: number, random?: Random): number;
|
|
30
|
+
export interface RetryDecision {
|
|
31
|
+
readonly retry: boolean;
|
|
32
|
+
readonly delayMs: number;
|
|
33
|
+
readonly deadLetter: boolean;
|
|
34
|
+
readonly nextAttempt: number;
|
|
35
|
+
}
|
|
36
|
+
/** The one place that decides retry vs dead-letter. Drivers never re-derive this. */
|
|
37
|
+
export declare function nextRetry(policy: RetryPolicy, attempt: number, random?: Random): RetryDecision;
|
|
38
|
+
/** Every delay in the policy, jitter off, for docs and `--json` output. */
|
|
39
|
+
export declare function retrySchedule(policy: RetryPolicy): readonly number[];
|
|
40
|
+
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["retry.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAG7C,MAAM,MAAM,eAAe,GAAG,aAAa,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEjE,MAAM,WAAW,WAAW;IAC1B,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,CAAC;IACnC,8BAA8B;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;IAC/B,gDAAgD;IAChD,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC;IAClC;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,eAAO,MAAM,aAAa;;;;;;;CAOH,CAAC;AAExB,MAAM,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC;AAElC,0FAA0F;AAC1F,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAe5F;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,qFAAqF;AACrF,wBAAgB,SAAS,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAgB9F;AAED,2EAA2E;AAC3E,wBAAgB,aAAa,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,MAAM,EAAE,CAOpE"}
|
package/src/retry.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Retry arithmetic, kept pure so the schedule is testable and printable. `x jobs schedule`
|
|
2
|
+
// renders `retrySchedule()` verbatim — an agent should be able to see when attempt 5 lands
|
|
3
|
+
// without running the queue.
|
|
4
|
+
import { toMs } from './clock';
|
|
5
|
+
export const DEFAULT_RETRY = {
|
|
6
|
+
attempts: 3,
|
|
7
|
+
backoff: 'exponential',
|
|
8
|
+
delay: 1_000,
|
|
9
|
+
maxDelay: 3_600_000,
|
|
10
|
+
jitter: true,
|
|
11
|
+
deadLetter: true,
|
|
12
|
+
};
|
|
13
|
+
/** Delay before `attempt` (1-based: the delay after attempt 1 failed is `attempt: 1`). */
|
|
14
|
+
export function backoffDelayMs(policy, attempt, random) {
|
|
15
|
+
const base = toMs(policy.delay ?? DEFAULT_RETRY.delay ?? 1_000);
|
|
16
|
+
const cap = toMs(policy.maxDelay ?? DEFAULT_RETRY.maxDelay ?? 3_600_000);
|
|
17
|
+
const strategy = policy.backoff ?? 'exponential';
|
|
18
|
+
const step = Math.max(1, attempt);
|
|
19
|
+
let raw;
|
|
20
|
+
if (strategy === 'fixed')
|
|
21
|
+
raw = base;
|
|
22
|
+
else if (strategy === 'linear')
|
|
23
|
+
raw = base * step;
|
|
24
|
+
else
|
|
25
|
+
raw = base * 2 ** (step - 1);
|
|
26
|
+
const capped = Math.min(raw, cap);
|
|
27
|
+
if (policy.jitter !== true)
|
|
28
|
+
return Math.round(capped);
|
|
29
|
+
const roll = (random ?? Math.random)();
|
|
30
|
+
return Math.round(capped / 2 + (capped / 2) * roll);
|
|
31
|
+
}
|
|
32
|
+
/** The one place that decides retry vs dead-letter. Drivers never re-derive this. */
|
|
33
|
+
export function nextRetry(policy, attempt, random) {
|
|
34
|
+
const exhausted = attempt >= policy.attempts;
|
|
35
|
+
if (exhausted) {
|
|
36
|
+
return {
|
|
37
|
+
retry: false,
|
|
38
|
+
delayMs: 0,
|
|
39
|
+
deadLetter: policy.deadLetter ?? true,
|
|
40
|
+
nextAttempt: attempt,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
retry: true,
|
|
45
|
+
delayMs: backoffDelayMs(policy, attempt, random),
|
|
46
|
+
deadLetter: false,
|
|
47
|
+
nextAttempt: attempt + 1,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Every delay in the policy, jitter off, for docs and `--json` output. */
|
|
51
|
+
export function retrySchedule(policy) {
|
|
52
|
+
const deterministic = { ...policy, jitter: false };
|
|
53
|
+
const out = [];
|
|
54
|
+
for (let attempt = 1; attempt < policy.attempts; attempt += 1) {
|
|
55
|
+
out.push(backoffDelayMs(deterministic, attempt));
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=retry.js.map
|
package/src/retry.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.js","sourceRoot":"","sources":["retry.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,2FAA2F;AAC3F,6BAA6B;AAG7B,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAqB/B,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,QAAQ,EAAE,CAAC;IACX,OAAO,EAAE,aAAa;IACtB,KAAK,EAAE,KAAK;IACZ,QAAQ,EAAE,SAAS;IACnB,MAAM,EAAE,IAAI;IACZ,UAAU,EAAE,IAAI;CACK,CAAC;AAIxB,0FAA0F;AAC1F,MAAM,UAAU,cAAc,CAAC,MAAmB,EAAE,OAAe,EAAE,MAAe;IAClF,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,aAAa,CAAC,KAAK,IAAI,KAAK,CAAC,CAAC;IAChE,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,aAAa,CAAC,QAAQ,IAAI,SAAS,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,IAAI,aAAa,CAAC;IACjD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IAElC,IAAI,GAAW,CAAC;IAChB,IAAI,QAAQ,KAAK,OAAO;QAAE,GAAG,GAAG,IAAI,CAAC;SAChC,IAAI,QAAQ,KAAK,QAAQ;QAAE,GAAG,GAAG,IAAI,GAAG,IAAI,CAAC;;QAC7C,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC;IAElC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAClC,IAAI,MAAM,CAAC,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;IACvC,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;AACtD,CAAC;AASD,qFAAqF;AACrF,MAAM,UAAU,SAAS,CAAC,MAAmB,EAAE,OAAe,EAAE,MAAe;IAC7E,MAAM,SAAS,GAAG,OAAO,IAAI,MAAM,CAAC,QAAQ,CAAC;IAC7C,IAAI,SAAS,EAAE,CAAC;QACd,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,OAAO,EAAE,CAAC;YACV,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,IAAI;YACrC,WAAW,EAAE,OAAO;SACrB,CAAC;IACJ,CAAC;IACD,OAAO;QACL,KAAK,EAAE,IAAI;QACX,OAAO,EAAE,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC;QAChD,UAAU,EAAE,KAAK;QACjB,WAAW,EAAE,OAAO,GAAG,CAAC;KACzB,CAAC;AACJ,CAAC;AAED,2EAA2E;AAC3E,MAAM,UAAU,aAAa,CAAC,MAAmB;IAC/C,MAAM,aAAa,GAAgB,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAChE,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,QAAQ,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QAC9D,GAAG,CAAC,IAAI,CAAC,cAAc,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/src/retry.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Retry arithmetic, kept pure so the schedule is testable and printable. `x jobs schedule`
|
|
2
|
+
// renders `retrySchedule()` verbatim — an agent should be able to see when attempt 5 lands
|
|
3
|
+
// without running the queue.
|
|
4
|
+
|
|
5
|
+
import type { DurationInput } from './clock';
|
|
6
|
+
import { toMs } from './clock';
|
|
7
|
+
|
|
8
|
+
export type BackoffStrategy = 'exponential' | 'linear' | 'fixed';
|
|
9
|
+
|
|
10
|
+
export interface RetryPolicy {
|
|
11
|
+
/** Total attempts including the first. `attempts: 1` means no retry. */
|
|
12
|
+
readonly attempts: number;
|
|
13
|
+
readonly backoff?: BackoffStrategy;
|
|
14
|
+
/** Base delay. Default 1s. */
|
|
15
|
+
readonly delay?: DurationInput;
|
|
16
|
+
/** Ceiling for any single delay. Default 1h. */
|
|
17
|
+
readonly maxDelay?: DurationInput;
|
|
18
|
+
/**
|
|
19
|
+
* Equal jitter (half fixed, half random) by default. Without it, a burst of failures
|
|
20
|
+
* retries in lockstep and re-creates the thundering herd that killed the dependency.
|
|
21
|
+
*/
|
|
22
|
+
readonly jitter?: boolean;
|
|
23
|
+
/** Default true: an exhausted job is parked, never dropped. */
|
|
24
|
+
readonly deadLetter?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_RETRY = {
|
|
28
|
+
attempts: 3,
|
|
29
|
+
backoff: 'exponential',
|
|
30
|
+
delay: 1_000,
|
|
31
|
+
maxDelay: 3_600_000,
|
|
32
|
+
jitter: true,
|
|
33
|
+
deadLetter: true,
|
|
34
|
+
} satisfies RetryPolicy;
|
|
35
|
+
|
|
36
|
+
export type Random = () => number;
|
|
37
|
+
|
|
38
|
+
/** Delay before `attempt` (1-based: the delay after attempt 1 failed is `attempt: 1`). */
|
|
39
|
+
export function backoffDelayMs(policy: RetryPolicy, attempt: number, random?: Random): number {
|
|
40
|
+
const base = toMs(policy.delay ?? DEFAULT_RETRY.delay ?? 1_000);
|
|
41
|
+
const cap = toMs(policy.maxDelay ?? DEFAULT_RETRY.maxDelay ?? 3_600_000);
|
|
42
|
+
const strategy = policy.backoff ?? 'exponential';
|
|
43
|
+
const step = Math.max(1, attempt);
|
|
44
|
+
|
|
45
|
+
let raw: number;
|
|
46
|
+
if (strategy === 'fixed') raw = base;
|
|
47
|
+
else if (strategy === 'linear') raw = base * step;
|
|
48
|
+
else raw = base * 2 ** (step - 1);
|
|
49
|
+
|
|
50
|
+
const capped = Math.min(raw, cap);
|
|
51
|
+
if (policy.jitter !== true) return Math.round(capped);
|
|
52
|
+
const roll = (random ?? Math.random)();
|
|
53
|
+
return Math.round(capped / 2 + (capped / 2) * roll);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface RetryDecision {
|
|
57
|
+
readonly retry: boolean;
|
|
58
|
+
readonly delayMs: number;
|
|
59
|
+
readonly deadLetter: boolean;
|
|
60
|
+
readonly nextAttempt: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The one place that decides retry vs dead-letter. Drivers never re-derive this. */
|
|
64
|
+
export function nextRetry(policy: RetryPolicy, attempt: number, random?: Random): RetryDecision {
|
|
65
|
+
const exhausted = attempt >= policy.attempts;
|
|
66
|
+
if (exhausted) {
|
|
67
|
+
return {
|
|
68
|
+
retry: false,
|
|
69
|
+
delayMs: 0,
|
|
70
|
+
deadLetter: policy.deadLetter ?? true,
|
|
71
|
+
nextAttempt: attempt,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
retry: true,
|
|
76
|
+
delayMs: backoffDelayMs(policy, attempt, random),
|
|
77
|
+
deadLetter: false,
|
|
78
|
+
nextAttempt: attempt + 1,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Every delay in the policy, jitter off, for docs and `--json` output. */
|
|
83
|
+
export function retrySchedule(policy: RetryPolicy): readonly number[] {
|
|
84
|
+
const deterministic: RetryPolicy = { ...policy, jitter: false };
|
|
85
|
+
const out: number[] = [];
|
|
86
|
+
for (let attempt = 1; attempt < policy.attempts; attempt += 1) {
|
|
87
|
+
out.push(backoffDelayMs(deterministic, attempt));
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Clock } from '@ultimat3/core';
|
|
2
|
+
import type { EnqueueResult, JobDriver } from './driver';
|
|
3
|
+
import type { AnyJobHandle } from './job';
|
|
4
|
+
/** `[[sendDigest, {}]]` — a job handle plus its input. */
|
|
5
|
+
export type TaskEnqueueEntry = readonly [AnyJobHandle, unknown];
|
|
6
|
+
/**
|
|
7
|
+
* What to do when the scheduler was down across one or more occurrences.
|
|
8
|
+
* `skip` (default) waits for the next one; `run-once` fires a single catch-up; `run-all`
|
|
9
|
+
* fires one per missed occurrence, bounded by `maxCatchUp`.
|
|
10
|
+
*/
|
|
11
|
+
export type CatchUpPolicy = 'skip' | 'run-once' | 'run-all';
|
|
12
|
+
export interface TaskDefinition {
|
|
13
|
+
readonly name?: string;
|
|
14
|
+
readonly cron: string;
|
|
15
|
+
/** REQUIRED IANA zone, e.g. `'UTC'`, `'America/New_York'`. */
|
|
16
|
+
readonly tz: string;
|
|
17
|
+
enqueue: () => readonly TaskEnqueueEntry[];
|
|
18
|
+
readonly catchUp?: CatchUpPolicy;
|
|
19
|
+
readonly maxCatchUp?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface TaskHandle {
|
|
22
|
+
readonly kind: 'task';
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly cron: string;
|
|
25
|
+
readonly tz: string;
|
|
26
|
+
readonly catchUp: CatchUpPolicy;
|
|
27
|
+
readonly maxCatchUp: number;
|
|
28
|
+
entries(): readonly TaskEnqueueEntry[];
|
|
29
|
+
}
|
|
30
|
+
/** Resolves the next fire time. Injected so scheduling logic is testable without a cron impl. */
|
|
31
|
+
export type CronResolver = (cron: string, options: {
|
|
32
|
+
tz: string;
|
|
33
|
+
from: Date;
|
|
34
|
+
}) => Date;
|
|
35
|
+
export declare function task(definition: TaskDefinition): TaskHandle;
|
|
36
|
+
export declare function nameTasks(record: Readonly<Record<string, TaskHandle>>): void;
|
|
37
|
+
export declare function registeredTasks(): readonly TaskHandle[];
|
|
38
|
+
export declare function getTask(name: string): TaskHandle | undefined;
|
|
39
|
+
export declare function resetTasks(): void;
|
|
40
|
+
export interface LeaderElection {
|
|
41
|
+
acquire(): Promise<boolean>;
|
|
42
|
+
release(): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
/** Single-node default: always the leader. Multi-node uses `createPgLeader()`. */
|
|
45
|
+
export declare function soleLeader(): LeaderElection;
|
|
46
|
+
export interface SchedulerState {
|
|
47
|
+
/** Epoch ms of the last occurrence this task was dispatched for. */
|
|
48
|
+
lastFiredAt(taskName: string): Promise<number | undefined>;
|
|
49
|
+
markFired(taskName: string, occurrenceMs: number): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
export declare function createMemorySchedulerState(): SchedulerState;
|
|
52
|
+
export interface SchedulerOptions {
|
|
53
|
+
readonly driver: JobDriver;
|
|
54
|
+
readonly clock?: Clock;
|
|
55
|
+
readonly leader?: LeaderElection;
|
|
56
|
+
readonly state?: SchedulerState;
|
|
57
|
+
readonly cron?: CronResolver;
|
|
58
|
+
readonly tickIntervalMs?: number;
|
|
59
|
+
/** Defaults to every registered task. */
|
|
60
|
+
readonly tasks?: readonly TaskHandle[];
|
|
61
|
+
}
|
|
62
|
+
export interface DispatchedOccurrence {
|
|
63
|
+
readonly task: string;
|
|
64
|
+
readonly occurrenceMs: number;
|
|
65
|
+
readonly jobs: readonly {
|
|
66
|
+
readonly job: string;
|
|
67
|
+
readonly result: EnqueueResult;
|
|
68
|
+
}[];
|
|
69
|
+
readonly catchUp: boolean;
|
|
70
|
+
}
|
|
71
|
+
export interface Scheduler {
|
|
72
|
+
start(): void;
|
|
73
|
+
stop(): Promise<void>;
|
|
74
|
+
/** One dispatch round. Returns what it enqueued — tests call this, not the timer. */
|
|
75
|
+
tick(): Promise<readonly DispatchedOccurrence[]>;
|
|
76
|
+
nextRunFor(handle: TaskHandle, from?: Date): Date;
|
|
77
|
+
}
|
|
78
|
+
export declare function createScheduler(options: SchedulerOptions): Scheduler;
|
|
79
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["scheduler.ts"],"names":[],"mappings":"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"}
|