@stonyx/cron 0.2.1-alpha.2 → 0.2.1-alpha.21
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 -0
- package/dist/cron-parser.d.ts +30 -0
- package/dist/cron-parser.js +200 -0
- package/dist/job.d.ts +72 -0
- package/dist/job.js +172 -0
- package/dist/locked.d.ts +13 -0
- package/dist/locked.js +27 -0
- package/dist/main.d.ts +21 -0
- package/dist/main.js +107 -0
- package/dist/min-heap.d.ts +13 -0
- package/dist/min-heap.js +67 -0
- package/dist/normalize.d.ts +49 -0
- package/dist/normalize.js +148 -0
- package/dist/run-log.d.ts +44 -0
- package/dist/run-log.js +60 -0
- package/dist/schedule.d.ts +23 -0
- package/dist/schedule.js +65 -0
- package/dist/service.d.ts +125 -0
- package/dist/service.js +446 -0
- package/package.json +53 -16
- package/.claude/architecture.md +0 -215
- package/.claude/extension-guide.md +0 -291
- package/.claude/improvements.md +0 -53
- package/.claude/project-structure.md +0 -139
- package/.claude/testing.md +0 -85
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/publish.yml +0 -51
- package/.gitignore +0 -16
- package/.npmignore +0 -7
- package/logs/error.log +0 -4
- package/pnpm-lock.yaml +0 -370
- package/src/cron-parser.js +0 -246
- package/src/job.js +0 -200
- package/src/locked.js +0 -34
- package/src/main.js +0 -112
- package/src/min-heap.js +0 -73
- package/src/normalize.js +0 -163
- package/src/run-log.js +0 -79
- package/src/schedule.js +0 -81
- package/src/service.js +0 -303
- package/stonyx-bootstrap.cjs +0 -9
package/dist/service.js
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CronService - the main API for advanced job scheduling.
|
|
3
|
+
*
|
|
4
|
+
* Manages jobs in memory with a min-heap for efficient next-job lookup.
|
|
5
|
+
* All state mutations are serialized via async locking.
|
|
6
|
+
*/
|
|
7
|
+
import config from 'stonyx/config';
|
|
8
|
+
import log from 'stonyx/log';
|
|
9
|
+
import MinHeap from './min-heap.js';
|
|
10
|
+
import { createJob, updateJob, markRunning, applyResult, isDue } from './job.js';
|
|
11
|
+
import { locked } from './locked.js';
|
|
12
|
+
import { normalizeJobInput, recoverFlatParams } from './normalize.js';
|
|
13
|
+
import RunLog from './run-log.js';
|
|
14
|
+
const MAX_TIMER_DELAY_MS = 60_000;
|
|
15
|
+
/**
|
|
16
|
+
* Describe a thrown value without ever throwing.
|
|
17
|
+
*
|
|
18
|
+
* `String(err)` is not total: a null-prototype object, or any object whose
|
|
19
|
+
* `toString`/`Symbol.toPrimitive` throws, raises "Cannot convert object to
|
|
20
|
+
* primitive value". Consumer callbacks throw arbitrary values, so the error
|
|
21
|
+
* handler itself must not be a second failure source.
|
|
22
|
+
*/
|
|
23
|
+
function describeError(err) {
|
|
24
|
+
if (err instanceof Error)
|
|
25
|
+
return err.message;
|
|
26
|
+
try {
|
|
27
|
+
return String(err);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return 'unknown error';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export default class CronService {
|
|
34
|
+
jobs;
|
|
35
|
+
heap;
|
|
36
|
+
timer;
|
|
37
|
+
running;
|
|
38
|
+
runLog;
|
|
39
|
+
started;
|
|
40
|
+
// Pluggable callbacks for consumers
|
|
41
|
+
onJobDue;
|
|
42
|
+
constructor() {
|
|
43
|
+
this.jobs = new Map();
|
|
44
|
+
this.heap = new MinHeap();
|
|
45
|
+
this.timer = null;
|
|
46
|
+
this.running = false;
|
|
47
|
+
this.runLog = new RunLog();
|
|
48
|
+
this.started = false;
|
|
49
|
+
this.onJobDue = null;
|
|
50
|
+
}
|
|
51
|
+
// -- Lifecycle -------------------------------------------------------
|
|
52
|
+
/**
|
|
53
|
+
* Start the service. Loads jobs from store (if any), arms timer.
|
|
54
|
+
*/
|
|
55
|
+
async start(initialJobs) {
|
|
56
|
+
if (this.started)
|
|
57
|
+
return;
|
|
58
|
+
this.started = true;
|
|
59
|
+
if (initialJobs) {
|
|
60
|
+
for (const job of initialJobs) {
|
|
61
|
+
this.jobs.set(job.id, job);
|
|
62
|
+
if (job.enabled && job.state.nextRunAtMs) {
|
|
63
|
+
this.heap.push({ key: job.id, nextTrigger: job.state.nextRunAtMs });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
this.armTimer();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Stop the service. Clears timer.
|
|
71
|
+
*/
|
|
72
|
+
stop() {
|
|
73
|
+
this.started = false;
|
|
74
|
+
if (this.timer)
|
|
75
|
+
clearTimeout(this.timer);
|
|
76
|
+
this.timer = null;
|
|
77
|
+
}
|
|
78
|
+
// -- CRUD ------------------------------------------------------------
|
|
79
|
+
/**
|
|
80
|
+
* Get service status.
|
|
81
|
+
*/
|
|
82
|
+
status() {
|
|
83
|
+
const peek = this.heap.peek();
|
|
84
|
+
return {
|
|
85
|
+
started: this.started,
|
|
86
|
+
jobCount: this.jobs.size,
|
|
87
|
+
nextWakeAtMs: peek ? peek.nextTrigger : undefined,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* List jobs, optionally including disabled ones.
|
|
92
|
+
*/
|
|
93
|
+
list(opts) {
|
|
94
|
+
const includeDisabled = opts?.includeDisabled ?? false;
|
|
95
|
+
const jobs = [...this.jobs.values()];
|
|
96
|
+
const filtered = includeDisabled ? jobs : jobs.filter(j => j.enabled);
|
|
97
|
+
return filtered.sort((a, b) => (a.state.nextRunAtMs ?? Infinity) - (b.state.nextRunAtMs ?? Infinity));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get a single job by ID.
|
|
101
|
+
*/
|
|
102
|
+
get(id) {
|
|
103
|
+
return this.jobs.get(id) || null;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Add a new job. Input is normalized for AI compatibility.
|
|
107
|
+
*/
|
|
108
|
+
async add(rawInput) {
|
|
109
|
+
return locked(() => {
|
|
110
|
+
const input = normalizeJobInput(recoverFlatParams(rawInput));
|
|
111
|
+
const job = createJob(input);
|
|
112
|
+
this.jobs.set(job.id, job);
|
|
113
|
+
if (job.enabled && job.state.nextRunAtMs) {
|
|
114
|
+
this.heap.push({ key: job.id, nextTrigger: job.state.nextRunAtMs });
|
|
115
|
+
this.armTimer();
|
|
116
|
+
}
|
|
117
|
+
return job;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Update an existing job.
|
|
122
|
+
*/
|
|
123
|
+
async update(id, patch) {
|
|
124
|
+
return locked(() => {
|
|
125
|
+
const job = this.jobs.get(id);
|
|
126
|
+
if (!job)
|
|
127
|
+
throw new Error(`Job not found: ${id}`);
|
|
128
|
+
const oldNextRun = job.state.nextRunAtMs;
|
|
129
|
+
updateJob(job, patch);
|
|
130
|
+
// Update heap entry
|
|
131
|
+
this.removeFromHeap(id);
|
|
132
|
+
if (job.enabled && job.state.nextRunAtMs) {
|
|
133
|
+
this.heap.push({ key: id, nextTrigger: job.state.nextRunAtMs });
|
|
134
|
+
}
|
|
135
|
+
if (job.state.nextRunAtMs !== oldNextRun) {
|
|
136
|
+
this.armTimer();
|
|
137
|
+
}
|
|
138
|
+
return job;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Remove a job.
|
|
143
|
+
*/
|
|
144
|
+
async remove(id) {
|
|
145
|
+
return locked(() => {
|
|
146
|
+
const job = this.jobs.get(id);
|
|
147
|
+
if (!job)
|
|
148
|
+
throw new Error(`Job not found: ${id}`);
|
|
149
|
+
this.jobs.delete(id);
|
|
150
|
+
this.removeFromHeap(id);
|
|
151
|
+
this.runLog.removeJob(id);
|
|
152
|
+
this.armTimer();
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Manually trigger a job.
|
|
157
|
+
*/
|
|
158
|
+
async run(id, mode = 'force') {
|
|
159
|
+
const job = this.jobs.get(id);
|
|
160
|
+
if (!job)
|
|
161
|
+
throw new Error(`Job not found: ${id}`);
|
|
162
|
+
if (mode === 'due' && !isDue(job, Date.now())) {
|
|
163
|
+
return { status: 'skipped', reason: 'not due' };
|
|
164
|
+
}
|
|
165
|
+
// Deliberately NOT wrapped in locked(): executeJob takes the lock itself
|
|
166
|
+
// for its claim and settle phases only. Wrapping here would re-create the
|
|
167
|
+
// wedge through a second door, since the callback would again be awaited
|
|
168
|
+
// while a lock is held.
|
|
169
|
+
return this.executeJob(job);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get run history for a job.
|
|
173
|
+
*/
|
|
174
|
+
runs(id, limit) {
|
|
175
|
+
return this.runLog.get(id, limit);
|
|
176
|
+
}
|
|
177
|
+
// -- Timer Engine ----------------------------------------------------
|
|
178
|
+
armTimer() {
|
|
179
|
+
if (this.timer)
|
|
180
|
+
clearTimeout(this.timer);
|
|
181
|
+
if (!this.started)
|
|
182
|
+
return;
|
|
183
|
+
const peek = this.heap.peek();
|
|
184
|
+
if (!peek)
|
|
185
|
+
return;
|
|
186
|
+
const delay = Math.min(Math.max(peek.nextTrigger - Date.now(), 0), MAX_TIMER_DELAY_MS);
|
|
187
|
+
this.timer = setTimeout(() => this.onTimer(), delay);
|
|
188
|
+
}
|
|
189
|
+
async onTimer() {
|
|
190
|
+
if (this.running) {
|
|
191
|
+
// Already processing - re-arm at max delay to prevent scheduler death
|
|
192
|
+
this.timer = setTimeout(() => this.onTimer(), MAX_TIMER_DELAY_MS);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
this.running = true;
|
|
196
|
+
try {
|
|
197
|
+
// Phase 1 - claim (locked). Collecting due jobs pops them off the heap
|
|
198
|
+
// and marking them running makes them un-collectable by anyone else, so
|
|
199
|
+
// both must happen under the same lock.
|
|
200
|
+
const dueJobs = await locked(() => {
|
|
201
|
+
const nowMs = Date.now();
|
|
202
|
+
const due = this.findDueJobs(nowMs);
|
|
203
|
+
for (const job of due) {
|
|
204
|
+
markRunning(job);
|
|
205
|
+
}
|
|
206
|
+
return due;
|
|
207
|
+
});
|
|
208
|
+
// Phases 2 and 3 run outside the claim lock. The consumer callback is
|
|
209
|
+
// awaited here holding no lock at all, so a callback that never settles
|
|
210
|
+
// cannot poison the lock chain.
|
|
211
|
+
for (const job of dueJobs) {
|
|
212
|
+
try {
|
|
213
|
+
await this.#executeClaimed(job);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
// One job's unexpected throw must not abort the batch. Every job in
|
|
217
|
+
// `dueJobs` is already claimed - marked running and detached from the
|
|
218
|
+
// heap - and only its own settle releases it, so aborting here would
|
|
219
|
+
// strand every sibling permanently un-due.
|
|
220
|
+
//
|
|
221
|
+
// This is the outermost handler on the timer path, so it is the one
|
|
222
|
+
// that must not be able to throw. `log()` is public, overridable and
|
|
223
|
+
// can reach a file transport, so its own failure is swallowed here
|
|
224
|
+
// rather than being allowed to take the batch down.
|
|
225
|
+
try {
|
|
226
|
+
this.log(`Job "${job.name}" (${job.id}) execution failed unexpectedly: ${describeError(err)}`);
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// Nothing left to report to.
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
this.running = false;
|
|
236
|
+
this.armTimer();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
findDueJobs(nowMs) {
|
|
240
|
+
const due = [];
|
|
241
|
+
while (!this.heap.isEmpty()) {
|
|
242
|
+
const peek = this.heap.peek();
|
|
243
|
+
if (!peek || peek.nextTrigger > nowMs)
|
|
244
|
+
break;
|
|
245
|
+
this.heap.pop();
|
|
246
|
+
const job = this.jobs.get(peek.key);
|
|
247
|
+
if (job && isDue(job, nowMs)) {
|
|
248
|
+
due.push(job);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return due;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Execute a job in three phases:
|
|
255
|
+
*
|
|
256
|
+
* 1. claim (locked) - take ownership of the job, detach it from the heap
|
|
257
|
+
* 2. invoke (UNLOCKED) - await the consumer callback
|
|
258
|
+
* 3. settle (locked) - apply the result, log it, re-insert into the heap
|
|
259
|
+
*
|
|
260
|
+
* The critical section deliberately excludes phase 2. `onJobDue` is
|
|
261
|
+
* arbitrary, unbounded consumer code; awaiting it under the module-global
|
|
262
|
+
* lock is what wedged every subsequent `locked()` call (add/update/remove)
|
|
263
|
+
* when a callback never settled.
|
|
264
|
+
*
|
|
265
|
+
* `onTimer` performs the batch claim (findDueJobs + markRunning) for all due
|
|
266
|
+
* jobs under a single lock, then enters at phase 2 via `#executeClaimed`.
|
|
267
|
+
* That entry point is a `#private` method rather than a parameter on this
|
|
268
|
+
* one: as a published `alreadyClaimed` boolean it was a supported way for a
|
|
269
|
+
* consumer to skip phase 1 entirely, which defeats the claim guard AC4 asks
|
|
270
|
+
* for and allows concurrent `onJobDue` invocations for the same job.
|
|
271
|
+
*/
|
|
272
|
+
async executeJob(job) {
|
|
273
|
+
// -- Phase 1: claim (locked) --
|
|
274
|
+
const refusal = await locked(() => this.claimJob(job));
|
|
275
|
+
if (refusal)
|
|
276
|
+
return { status: 'skipped', reason: refusal };
|
|
277
|
+
return this.#executeClaimed(job);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Phases 2 and 3 for a job that has already been claimed - either by
|
|
281
|
+
* `executeJob` above or by `onTimer`'s batch claim.
|
|
282
|
+
*
|
|
283
|
+
* Private: reaching this without a claim would run the consumer callback for
|
|
284
|
+
* a job nobody owns.
|
|
285
|
+
*/
|
|
286
|
+
async #executeClaimed(job) {
|
|
287
|
+
// Membership re-check. The claim and the invoke are no longer in the same
|
|
288
|
+
// critical section, and sibling callbacks run unlocked, so a `remove()` can
|
|
289
|
+
// land in between AND RESOLVE - it used to deadlock. A resolved `remove()`
|
|
290
|
+
// must keep meaning "this callback will not fire": settleJob's identity
|
|
291
|
+
// guard only cleans up afterwards, by which point the side effect has
|
|
292
|
+
// already happened. Identity, not id, so a removed-then-replaced key is
|
|
293
|
+
// caught too. Deliberately synchronous with the `onJobDue` call below -
|
|
294
|
+
// nothing can interleave between this check and the invocation.
|
|
295
|
+
if (this.jobs.get(job.id) !== job)
|
|
296
|
+
return { status: 'skipped', reason: 'removed' };
|
|
297
|
+
const startMs = Date.now();
|
|
298
|
+
let status = 'ok';
|
|
299
|
+
let error;
|
|
300
|
+
let summary;
|
|
301
|
+
let settled;
|
|
302
|
+
// The claim above marked the job running and detached it from the heap.
|
|
303
|
+
// Phase 3 is the ONLY thing that undoes either, so it must survive every
|
|
304
|
+
// non-local exit from phase 2 - including a throw from the catch handler
|
|
305
|
+
// itself. A claim with no matching settle is not a degraded state, it is a
|
|
306
|
+
// permanently dead job: `runningAtMs` set, no heap entry, `isDue` false
|
|
307
|
+
// forever and `run()` refused forever.
|
|
308
|
+
try {
|
|
309
|
+
// -- Phase 2: invoke (NOT locked) --
|
|
310
|
+
try {
|
|
311
|
+
if (this.onJobDue) {
|
|
312
|
+
const result = await this.onJobDue(job);
|
|
313
|
+
if (result) {
|
|
314
|
+
status = result.status || 'ok';
|
|
315
|
+
error = result.error;
|
|
316
|
+
summary = result.summary;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
status = 'error';
|
|
322
|
+
error = describeError(err);
|
|
323
|
+
this.log(`Job "${job.name}" (${job.id}) failed: ${error}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
// -- Phase 3: settle (locked) --
|
|
328
|
+
settled = await locked(() => this.settleJob(job, status, error, summary, startMs, Date.now() - startMs));
|
|
329
|
+
}
|
|
330
|
+
return settled;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Phase 1 - claim. Must be called while holding the lock.
|
|
334
|
+
*
|
|
335
|
+
* Returns `null` on a successful claim, or the reason the claim was refused.
|
|
336
|
+
* "already running" is what makes a second `run()` report a skip instead of
|
|
337
|
+
* launching a concurrent invocation. "removed" covers the job being deleted
|
|
338
|
+
* between `run()`'s unlocked lookup and this lock turn - claiming then would
|
|
339
|
+
* `markRunning` an orphan and, worse, `removeFromHeap` an id that may now
|
|
340
|
+
* belong to a replacement.
|
|
341
|
+
*
|
|
342
|
+
* Detaching from the heap here (rather than relying on phase 3 to push a
|
|
343
|
+
* fresh entry) is what keeps manual runs from permanently duplicating heap
|
|
344
|
+
* entries.
|
|
345
|
+
*/
|
|
346
|
+
claimJob(job) {
|
|
347
|
+
if (this.jobs.get(job.id) !== job)
|
|
348
|
+
return 'removed';
|
|
349
|
+
if (job.state.runningAtMs)
|
|
350
|
+
return 'already running';
|
|
351
|
+
markRunning(job);
|
|
352
|
+
this.removeFromHeap(job.id);
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Phase 3 - settle. Must be called while holding the lock.
|
|
357
|
+
*/
|
|
358
|
+
settleJob(job, status, error, summary, startMs, durationMs) {
|
|
359
|
+
try {
|
|
360
|
+
const validStatus = (status === 'ok' || status === 'error' || status === 'skipped') ? status : 'error';
|
|
361
|
+
applyResult(job, validStatus, error, durationMs);
|
|
362
|
+
// The callback ran unlocked, so this job may have been removed - or
|
|
363
|
+
// removed and re-registered under the same id (the shape
|
|
364
|
+
// `start(initialJobs)` uses) - while it was in flight. Identity, not id.
|
|
365
|
+
//
|
|
366
|
+
// Deliberately touch NOTHING here. The claim phase already detached this
|
|
367
|
+
// job's own heap entry and nothing re-added it, so there is nothing to
|
|
368
|
+
// clean up; any entry now filed under this id belongs to the
|
|
369
|
+
// replacement, and removing it by id would silently unschedule a live
|
|
370
|
+
// job. Do not resurrect a removed job's heap entry or run log either.
|
|
371
|
+
if (this.jobs.get(job.id) !== job) {
|
|
372
|
+
return { status, error, summary, durationMs };
|
|
373
|
+
}
|
|
374
|
+
// Log the run
|
|
375
|
+
this.runLog.record({
|
|
376
|
+
jobId: job.id,
|
|
377
|
+
status,
|
|
378
|
+
error,
|
|
379
|
+
summary,
|
|
380
|
+
runAtMs: startMs,
|
|
381
|
+
durationMs,
|
|
382
|
+
nextRunAtMs: job.state.nextRunAtMs,
|
|
383
|
+
});
|
|
384
|
+
// Handle one-shot auto-delete. The callback ran unlocked and may have
|
|
385
|
+
// pushed a heap entry for this job via update(), so drop it - the job is
|
|
386
|
+
// about to stop existing.
|
|
387
|
+
if (job.deleteAfterRun && status === 'ok' && !job.enabled) {
|
|
388
|
+
this.jobs.delete(job.id);
|
|
389
|
+
this.removeFromHeap(job.id);
|
|
390
|
+
this.runLog.removeJob(job.id);
|
|
391
|
+
return { status, summary, deleted: true };
|
|
392
|
+
}
|
|
393
|
+
// Re-insert into heap if still active. The callback ran unlocked, so it
|
|
394
|
+
// may itself have added a heap entry for this job (via add/update); drop
|
|
395
|
+
// any such entry first to preserve one-entry-per-key.
|
|
396
|
+
this.removeFromHeap(job.id);
|
|
397
|
+
if (job.enabled && job.state.nextRunAtMs) {
|
|
398
|
+
this.heap.push({ key: job.id, nextTrigger: job.state.nextRunAtMs });
|
|
399
|
+
}
|
|
400
|
+
return { status, error, summary, durationMs };
|
|
401
|
+
}
|
|
402
|
+
finally {
|
|
403
|
+
// One re-arm covering every exit, rather than one per branch. The claim
|
|
404
|
+
// phase detached this job from the heap, so a timer that fired during the
|
|
405
|
+
// unlocked invoke would have found an empty heap and armed nothing -
|
|
406
|
+
// and `run()` has no `finally { armTimer() }` of its own the way
|
|
407
|
+
// `onTimer` does. Without this a manual run() can leave the scheduler
|
|
408
|
+
// with no pending wake at all.
|
|
409
|
+
this.armTimer();
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// -- Helpers ---------------------------------------------------------
|
|
413
|
+
removeFromHeap(id) {
|
|
414
|
+
// MinHeap doesn't support remove-by-key efficiently,
|
|
415
|
+
// so we rebuild. Fine for typical job counts (< 1000).
|
|
416
|
+
const remaining = [];
|
|
417
|
+
while (!this.heap.isEmpty()) {
|
|
418
|
+
const item = this.heap.pop();
|
|
419
|
+
if (!item)
|
|
420
|
+
break;
|
|
421
|
+
if (item.key !== id)
|
|
422
|
+
remaining.push(item);
|
|
423
|
+
}
|
|
424
|
+
for (const item of remaining) {
|
|
425
|
+
this.heap.push(item);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
log(message) {
|
|
429
|
+
if (!config.cron?.log)
|
|
430
|
+
return;
|
|
431
|
+
// `log.cron` is created by `log.defineType`, which runs in `Cron.init()`
|
|
432
|
+
// (src/main.ts) - a DIFFERENT class. A consumer wiring CronService directly
|
|
433
|
+
// never runs it, while `config/environment.js` defaults `cron.log` to true,
|
|
434
|
+
// so an unguarded call throws `log.cron is not a function`. That throw
|
|
435
|
+
// escapes executeJob's catch, and the error-reporting path must never be
|
|
436
|
+
// the thing that kills the scheduler. `src/types/stonyx.d.ts:19` declares
|
|
437
|
+
// `cron()` unconditionally, so the type system will not catch this.
|
|
438
|
+
const { logColor = '#888', logMethod = 'cron' } = config.cron ?? {};
|
|
439
|
+
if (typeof log[logMethod] !== 'function')
|
|
440
|
+
log.defineType(logMethod, logColor);
|
|
441
|
+
const method = log[logMethod];
|
|
442
|
+
if (typeof method !== 'function')
|
|
443
|
+
return;
|
|
444
|
+
method.call(log, `Cron — ${message}`);
|
|
445
|
+
}
|
|
446
|
+
}
|
package/package.json
CHANGED
|
@@ -3,23 +3,53 @@
|
|
|
3
3
|
"keywords": [
|
|
4
4
|
"stonyx-module"
|
|
5
5
|
],
|
|
6
|
-
"version": "0.2.1-alpha.
|
|
6
|
+
"version": "0.2.1-alpha.21",
|
|
7
7
|
"description": "Cron/job scheduler for Stonyx framework",
|
|
8
|
-
"main": "
|
|
8
|
+
"main": "dist/main.js",
|
|
9
|
+
"types": "dist/main.d.ts",
|
|
9
10
|
"type": "module",
|
|
10
11
|
"files": [
|
|
11
|
-
"
|
|
12
|
+
"dist",
|
|
13
|
+
"config",
|
|
14
|
+
"README.md"
|
|
12
15
|
],
|
|
13
16
|
"exports": {
|
|
14
|
-
".":
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"./
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"./
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/main.d.ts",
|
|
19
|
+
"default": "./dist/main.js"
|
|
20
|
+
},
|
|
21
|
+
"./service": {
|
|
22
|
+
"types": "./dist/service.d.ts",
|
|
23
|
+
"default": "./dist/service.js"
|
|
24
|
+
},
|
|
25
|
+
"./cron-parser": {
|
|
26
|
+
"types": "./dist/cron-parser.d.ts",
|
|
27
|
+
"default": "./dist/cron-parser.js"
|
|
28
|
+
},
|
|
29
|
+
"./schedule": {
|
|
30
|
+
"types": "./dist/schedule.d.ts",
|
|
31
|
+
"default": "./dist/schedule.js"
|
|
32
|
+
},
|
|
33
|
+
"./job": {
|
|
34
|
+
"types": "./dist/job.d.ts",
|
|
35
|
+
"default": "./dist/job.js"
|
|
36
|
+
},
|
|
37
|
+
"./normalize": {
|
|
38
|
+
"types": "./dist/normalize.d.ts",
|
|
39
|
+
"default": "./dist/normalize.js"
|
|
40
|
+
},
|
|
41
|
+
"./locked": {
|
|
42
|
+
"types": "./dist/locked.d.ts",
|
|
43
|
+
"default": "./dist/locked.js"
|
|
44
|
+
},
|
|
45
|
+
"./run-log": {
|
|
46
|
+
"types": "./dist/run-log.d.ts",
|
|
47
|
+
"default": "./dist/run-log.js"
|
|
48
|
+
},
|
|
49
|
+
"./min-heap": {
|
|
50
|
+
"types": "./dist/min-heap.d.ts",
|
|
51
|
+
"default": "./dist/min-heap.js"
|
|
52
|
+
}
|
|
23
53
|
},
|
|
24
54
|
"publishConfig": {
|
|
25
55
|
"access": "public",
|
|
@@ -39,14 +69,21 @@
|
|
|
39
69
|
},
|
|
40
70
|
"homepage": "https://github.com/abofs/stonyx-cron#readme",
|
|
41
71
|
"devDependencies": {
|
|
42
|
-
"@stonyx/utils": "0.2.3-beta.
|
|
72
|
+
"@stonyx/utils": "0.2.3-beta.26",
|
|
73
|
+
"@types/node": "^25.5.2",
|
|
74
|
+
"@types/qunit": "^2.19.13",
|
|
75
|
+
"@types/sinon": "^21.0.1",
|
|
43
76
|
"qunit": "^2.24.1",
|
|
44
|
-
"sinon": "^21.0.0"
|
|
77
|
+
"sinon": "^21.0.0",
|
|
78
|
+
"tsx": "^4.21.0",
|
|
79
|
+
"typescript": "^5.8.3"
|
|
45
80
|
},
|
|
46
81
|
"dependencies": {
|
|
47
|
-
"stonyx": "0.2.3-beta.
|
|
82
|
+
"stonyx": "0.2.3-beta.76"
|
|
48
83
|
},
|
|
49
84
|
"scripts": {
|
|
50
|
-
"
|
|
85
|
+
"build": "tsc",
|
|
86
|
+
"build:test": "tsc -p tsconfig.test.json",
|
|
87
|
+
"test": "pnpm build && NODE_ENV=test node --import tsx/esm --import ./test/setup.ts node_modules/qunit/bin/qunit.js 'test/**/*-test.ts'"
|
|
51
88
|
}
|
|
52
89
|
}
|