@warlock.js/scheduler 5.22.1 → 5.23.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/CHANGELOG.md +113 -107
- package/cjs/index.cjs +83 -5
- package/cjs/index.cjs.map +1 -1
- package/esm/index.d.mts +2 -2
- package/esm/job.d.mts +19 -1
- package/esm/job.d.mts.map +1 -1
- package/esm/job.mjs +59 -4
- package/esm/job.mjs.map +1 -1
- package/esm/scheduler.d.mts +20 -1
- package/esm/scheduler.d.mts.map +1 -1
- package/esm/scheduler.mjs +24 -1
- package/esm/scheduler.mjs.map +1 -1
- package/esm/types.d.mts +2 -1
- package/esm/types.d.mts.map +1 -1
- package/package.json +2 -2
- package/skills/scheduler-basics/SKILL.md +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,107 +1,113 @@
|
|
|
1
|
-
# Changelog — @warlock.js/scheduler
|
|
2
|
-
|
|
3
|
-
All notable changes to `@warlock.js/scheduler` are documented in this file.
|
|
4
|
-
|
|
5
|
-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
|
-
|
|
7
|
-
## 5.
|
|
8
|
-
|
|
9
|
-
###
|
|
10
|
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
## 5.22.
|
|
14
|
-
|
|
15
|
-
###
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
## 5.
|
|
20
|
-
|
|
21
|
-
###
|
|
22
|
-
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
## 5.
|
|
31
|
-
|
|
32
|
-
###
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
## 5.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
## 5.
|
|
55
|
-
|
|
56
|
-
### Fixed
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
## 5.2.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
## 5.0
|
|
77
|
-
|
|
78
|
-
No changes to `@warlock.js/scheduler`. Released in lockstep with the `@warlock.js/web`
|
|
79
|
-
fix
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
## 5.0.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
##
|
|
94
|
-
|
|
95
|
-
### Changed
|
|
96
|
-
|
|
97
|
-
-
|
|
98
|
-
|
|
99
|
-
## 4.
|
|
100
|
-
|
|
101
|
-
###
|
|
102
|
-
|
|
103
|
-
-
|
|
104
|
-
|
|
105
|
-
## 4.
|
|
106
|
-
|
|
107
|
-
|
|
1
|
+
# Changelog — @warlock.js/scheduler
|
|
2
|
+
|
|
3
|
+
All notable changes to `@warlock.js/scheduler` are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
|
+
|
|
7
|
+
## 5.23.0 - 2026-09-25
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `scheduler.around((job, run) => …)` wraps every job callback execution, each retry attempt included, so jobs can run inside a context such as `AsyncLocalStorage`. Multiple hooks compose with the first registered outermost; the call returns an unsubscribe function. A hook that never calls `run` skips the execution and emits `job:skip` (`JobResult.skipped` / `skipReason`); a hook that throws follows the normal retry/error path.
|
|
12
|
+
|
|
13
|
+
## 5.22.1 - 2026-09-25
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Lockstep patch release; package APIs are unchanged.
|
|
18
|
+
|
|
19
|
+
## 5.22.0 - 2026-09-25
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Cron and time parsing handle absent fields explicitly under strict TypeScript checks.
|
|
24
|
+
|
|
25
|
+
## 5.21.0 - 2026-09-25
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Lockstep release maintenance and dependency refresh.
|
|
30
|
+
## 5.20.0 - 2026-09-24
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- `job.onOneServer({ lockTtl?, key? })` — run each tick on exactly one server. Every server races for a create-only, TTL-bounded cache claim keyed `scheduler.<key ?? name>.<scheduledTickEpochMs>`. The claim is never released, so exactly one server runs each tick even if another server's timer fires late. Losers skip the tick and emit `job:skip`. Requires a shared cache driver (redis/pg) and a job name or `key`. `@warlock.js/cache` is an optional peer dependency. Default claim TTL is `min(interval, 1h)` with a 60s floor (1h for cron jobs).
|
|
35
|
+
|
|
36
|
+
## 5.19.0 - 2026-09-23
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- Refined package skill-discovery descriptions and regenerated the llms projections.
|
|
41
|
+
|
|
42
|
+
## 5.11.0 - 2026-09-14
|
|
43
|
+
|
|
44
|
+
_Released in lockstep with the `@warlock.js/*` family; no package-specific changes in 5.11.0._
|
|
45
|
+
|
|
46
|
+
## 5.10.0 - 2026-09-14
|
|
47
|
+
|
|
48
|
+
_Released in lockstep with the `@warlock.js/*` family; no package-specific changes in 5.10.0._
|
|
49
|
+
|
|
50
|
+
## 5.9.0 - 2026-09-13
|
|
51
|
+
|
|
52
|
+
_Released in lockstep with the `@warlock.js/*` family; no package-specific changes in 5.9.0._
|
|
53
|
+
|
|
54
|
+
## 5.5.0 - 2026-09-07
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- Documentation shipped in this package's `skills/` told users to run `pnpm`-specific commands. `pnpm <binary>` has no npm equivalent, so those instructions failed outright for anyone not using pnpm. Commands are now package-manager neutral.
|
|
59
|
+
|
|
60
|
+
## 5.2.3 - 2026-09-02
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- Released in exact lockstep with Core's Web generator repairs so every family dependency remains installable at 5.2.3.
|
|
65
|
+
|
|
66
|
+
## 5.2.2
|
|
67
|
+
|
|
68
|
+
- Documented: the README, CHANGELOG, and `overview` skill now describe the
|
|
69
|
+
already-shipped `package.json` `"warlock": { "environment": "server" }`
|
|
70
|
+
marker — `@warlock.js/scheduler`'s entire runtime surface is server-only.
|
|
71
|
+
The marker is build-boundary metadata read by `@warlock.js/web`'s Gate A
|
|
72
|
+
(import resolution) and Gate C (emitted-bundle verification); app client
|
|
73
|
+
code must not value-import this package, type-only imports are allowed, and
|
|
74
|
+
server loaders/controllers/modules may import it freely. No source change.
|
|
75
|
+
|
|
76
|
+
## 5.1.0
|
|
77
|
+
|
|
78
|
+
No changes to `@warlock.js/scheduler`. Released in lockstep with the `@warlock.js/web`
|
|
79
|
+
React-execution fix and the `@warlock.js/core` CLI additions — see those packages'
|
|
80
|
+
changelogs.
|
|
81
|
+
|
|
82
|
+
## 5.0.2 - 2026-08-25
|
|
83
|
+
|
|
84
|
+
No changes to `@warlock.js/scheduler`. Released in lockstep with the `@warlock.js/web` SSR
|
|
85
|
+
fix (`ssr.noExternal`) — see that package's changelog.
|
|
86
|
+
|
|
87
|
+
## 5.0.1 - 2026-08-25
|
|
88
|
+
|
|
89
|
+
No changes to `@warlock.js/scheduler`. Released in lockstep with the `create-warlock` vite
|
|
90
|
+
resolution pin and the `@warlock.js/web` peer narrowing — see those packages'
|
|
91
|
+
changelogs.
|
|
92
|
+
|
|
93
|
+
## 5.0.0 - 2026-08-25
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
|
|
97
|
+
- This package is unchanged in 5.0.0; its version moved only because the Warlock family releases in lockstep.
|
|
98
|
+
|
|
99
|
+
## 4.12.0
|
|
100
|
+
|
|
101
|
+
### Changed
|
|
102
|
+
|
|
103
|
+
- Declares its own test runner and pins it to an exact version (`vitest@4.1.10`). The package is its own repository, so a runner resolved from a workspace root it may not be cloned with is a runner it cannot rely on. The pin is exact rather than a range because the version moved underneath the suite mid-development on an unrelated install — a suite whose runner can change without anyone choosing it proves less than it appears to
|
|
104
|
+
|
|
105
|
+
## 4.6.0
|
|
106
|
+
|
|
107
|
+
### Fixed
|
|
108
|
+
|
|
109
|
+
- Warn in development when jobs are registered but `start()` is never called — a one-shot deferred check logs `N job(s) registered but scheduler.start() was never called`, is suppressed once `start()` runs or in production (`NODE_ENV=production`), and is unref'd so it never holds the process open.
|
|
110
|
+
|
|
111
|
+
## 4.1.15
|
|
112
|
+
|
|
113
|
+
- Baseline — per-package changelog tracking starts at this version.
|
package/cjs/index.cjs
CHANGED
|
@@ -267,6 +267,11 @@ const UNIT_MS = {
|
|
|
267
267
|
year: 31536e6
|
|
268
268
|
};
|
|
269
269
|
/**
|
|
270
|
+
* Hook wrapping every callback execution (each retry attempt included).
|
|
271
|
+
* Not calling `run` skips the execution; throwing counts as a callback error.
|
|
272
|
+
*/
|
|
273
|
+
const HOOK_SKIP_REASON = "An around hook did not run the job";
|
|
274
|
+
/**
|
|
270
275
|
* Days of week mapping (lowercase for consistency with Day type)
|
|
271
276
|
*/
|
|
272
277
|
const DAYS_OF_WEEK = [
|
|
@@ -322,6 +327,7 @@ var Job = class {
|
|
|
322
327
|
this._intervals = {};
|
|
323
328
|
this._lastRun = null;
|
|
324
329
|
this._isRunning = false;
|
|
330
|
+
this._aroundHooks = () => [];
|
|
325
331
|
this._skipIfRunning = false;
|
|
326
332
|
this._retryConfig = null;
|
|
327
333
|
this._timezone = "UTC";
|
|
@@ -758,6 +764,7 @@ var Job = class {
|
|
|
758
764
|
let executedRetries = 0;
|
|
759
765
|
try {
|
|
760
766
|
let skipped = false;
|
|
767
|
+
let skipReason;
|
|
761
768
|
if (this._oneServer) {
|
|
762
769
|
const lockKey = this._lockKey(scheduledAt);
|
|
763
770
|
const { cache } = await loadCache();
|
|
@@ -765,13 +772,28 @@ var Job = class {
|
|
|
765
772
|
onConflict: "create",
|
|
766
773
|
ttl: this._lockTtl()
|
|
767
774
|
}))?.wasSet !== true;
|
|
768
|
-
if (!skipped)
|
|
769
|
-
|
|
775
|
+
if (!skipped) {
|
|
776
|
+
const inner = await this._executeWithRetry();
|
|
777
|
+
executedRetries = inner.retries;
|
|
778
|
+
if (inner.skipped) {
|
|
779
|
+
skipped = true;
|
|
780
|
+
skipReason = HOOK_SKIP_REASON;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
} else {
|
|
784
|
+
const inner = await this._executeWithRetry();
|
|
785
|
+
executedRetries = inner.retries;
|
|
786
|
+
if (inner.skipped) {
|
|
787
|
+
skipped = true;
|
|
788
|
+
skipReason = HOOK_SKIP_REASON;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
770
791
|
result = {
|
|
771
792
|
success: true,
|
|
772
793
|
duration: Date.now() - startTime,
|
|
773
794
|
retries: executedRetries,
|
|
774
|
-
...skipped ? { skipped: true } : {}
|
|
795
|
+
...skipped ? { skipped: true } : {},
|
|
796
|
+
...skipReason ? { skipReason } : {}
|
|
775
797
|
};
|
|
776
798
|
} catch (error) {
|
|
777
799
|
result = {
|
|
@@ -790,6 +812,16 @@ var Job = class {
|
|
|
790
812
|
return result;
|
|
791
813
|
}
|
|
792
814
|
/**
|
|
815
|
+
* Bind a provider of around hooks (called by the Scheduler on registration).
|
|
816
|
+
* A provider, not a snapshot, so later `around()` / unsubscribe calls apply.
|
|
817
|
+
*
|
|
818
|
+
* @internal
|
|
819
|
+
*/
|
|
820
|
+
bindAroundHooks(provider) {
|
|
821
|
+
this._aroundHooks = provider;
|
|
822
|
+
return this;
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
793
825
|
* Wait for the currently executing run to complete.
|
|
794
826
|
*
|
|
795
827
|
* Multiple concurrent waiters are all resolved when the run finishes.
|
|
@@ -833,7 +865,10 @@ var Job = class {
|
|
|
833
865
|
let attempts = 0;
|
|
834
866
|
const maxAttempts = (this._retryConfig?.maxRetries ?? 0) + 1;
|
|
835
867
|
while (attempts < maxAttempts) try {
|
|
836
|
-
await this.
|
|
868
|
+
if (!await this._invokeCallback()) return {
|
|
869
|
+
retries: attempts,
|
|
870
|
+
skipped: true
|
|
871
|
+
};
|
|
837
872
|
return { retries: attempts };
|
|
838
873
|
} catch (error) {
|
|
839
874
|
lastError = error;
|
|
@@ -846,6 +881,26 @@ var Job = class {
|
|
|
846
881
|
throw lastError;
|
|
847
882
|
}
|
|
848
883
|
/**
|
|
884
|
+
* Run the callback inside the around hooks (first registered = outermost).
|
|
885
|
+
*
|
|
886
|
+
* @returns false when a hook never called `run` (the callback did not execute)
|
|
887
|
+
*/
|
|
888
|
+
async _invokeCallback() {
|
|
889
|
+
let reached = false;
|
|
890
|
+
let chain = async () => {
|
|
891
|
+
reached = true;
|
|
892
|
+
await this._callback(this);
|
|
893
|
+
};
|
|
894
|
+
const hooks = [...this._aroundHooks()];
|
|
895
|
+
for (let index = hooks.length - 1; index >= 0; index--) {
|
|
896
|
+
const hook = hooks[index];
|
|
897
|
+
const next = chain;
|
|
898
|
+
chain = () => hook(this, next);
|
|
899
|
+
}
|
|
900
|
+
await chain();
|
|
901
|
+
return reached;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
849
904
|
* Calculate retry delay with optional exponential backoff
|
|
850
905
|
*/
|
|
851
906
|
_calculateRetryDelay(attempt) {
|
|
@@ -971,6 +1026,7 @@ var Scheduler = class extends node_events.EventEmitter {
|
|
|
971
1026
|
constructor(..._args) {
|
|
972
1027
|
super(..._args);
|
|
973
1028
|
this._jobs = [];
|
|
1029
|
+
this._aroundHooks = [];
|
|
974
1030
|
this._timeoutId = null;
|
|
975
1031
|
this._tickInterval = 1e3;
|
|
976
1032
|
this._runInParallel = false;
|
|
@@ -998,12 +1054,33 @@ var Scheduler = class extends node_events.EventEmitter {
|
|
|
998
1054
|
* @returns this for chaining
|
|
999
1055
|
*/
|
|
1000
1056
|
addJob(job) {
|
|
1057
|
+
job.bindAroundHooks(() => this._aroundHooks);
|
|
1001
1058
|
this._jobs.push(job);
|
|
1002
1059
|
if (this.isRunning) job.prepare();
|
|
1003
1060
|
this._armStartWarning();
|
|
1004
1061
|
return this;
|
|
1005
1062
|
}
|
|
1006
1063
|
/**
|
|
1064
|
+
* Wrap every job callback execution (each retry attempt included) in a hook.
|
|
1065
|
+
*
|
|
1066
|
+
* The first registered hook is the outermost. A hook that never calls `run`
|
|
1067
|
+
* skips the execution (`job:skip` is emitted); a hook that throws is treated
|
|
1068
|
+
* like a callback error.
|
|
1069
|
+
*
|
|
1070
|
+
* @returns Unsubscribe function
|
|
1071
|
+
*
|
|
1072
|
+
* @example
|
|
1073
|
+
* ```typescript
|
|
1074
|
+
* scheduler.around((job, run) => storage.run({ job: job.name }, run));
|
|
1075
|
+
* ```
|
|
1076
|
+
*/
|
|
1077
|
+
around(hook) {
|
|
1078
|
+
this._aroundHooks = [...this._aroundHooks, hook];
|
|
1079
|
+
return () => {
|
|
1080
|
+
this._aroundHooks = this._aroundHooks.filter((item) => item !== hook);
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
/**
|
|
1007
1084
|
* Alias to create a new job directly and store it
|
|
1008
1085
|
*/
|
|
1009
1086
|
newJob(name, jobCallback) {
|
|
@@ -1021,6 +1098,7 @@ var Scheduler = class extends node_events.EventEmitter {
|
|
|
1021
1098
|
* @returns this for chaining
|
|
1022
1099
|
*/
|
|
1023
1100
|
addJobs(jobs) {
|
|
1101
|
+
for (const job of jobs) job.bindAroundHooks(() => this._aroundHooks);
|
|
1024
1102
|
this._jobs.push(...jobs);
|
|
1025
1103
|
if (this.isRunning) for (const job of jobs) job.prepare();
|
|
1026
1104
|
this._armStartWarning();
|
|
@@ -1210,7 +1288,7 @@ var Scheduler = class extends node_events.EventEmitter {
|
|
|
1210
1288
|
async _runJob(job) {
|
|
1211
1289
|
this.emit("job:start", job.name);
|
|
1212
1290
|
const result = await job.run();
|
|
1213
|
-
if (result.skipped) this.emit("job:skip", job.name, "Another server holds the lock");
|
|
1291
|
+
if (result.skipped) this.emit("job:skip", job.name, result.skipReason ?? "Another server holds the lock");
|
|
1214
1292
|
else if (result.success) this.emit("job:complete", job.name, result);
|
|
1215
1293
|
else this.emit("job:error", job.name, result.error);
|
|
1216
1294
|
return result;
|