@procedures/schedule 0.4.0 → 0.5.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +55 -5
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/cron.d.ts +36 -6
  5. package/dist/cron.d.ts.map +1 -1
  6. package/dist/cron.js +265 -41
  7. package/dist/cron.js.map +1 -1
  8. package/dist/errors.d.ts +4 -0
  9. package/dist/errors.d.ts.map +1 -1
  10. package/dist/errors.js.map +1 -1
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/module.d.ts +5 -0
  16. package/dist/module.d.ts.map +1 -1
  17. package/dist/module.js +8 -2
  18. package/dist/module.js.map +1 -1
  19. package/dist/procedures/schedule/add.d.ts +5 -2
  20. package/dist/procedures/schedule/add.d.ts.map +1 -1
  21. package/dist/procedures/schedule/add.js +15 -10
  22. package/dist/procedures/schedule/add.js.map +1 -1
  23. package/dist/procedures/schedule/list.js +1 -1
  24. package/dist/procedures/schedule/list.js.map +1 -1
  25. package/dist/procedures/schedule/next.d.ts +1 -1
  26. package/dist/procedures/schedule/next.d.ts.map +1 -1
  27. package/dist/procedures/schedule/next.js +2 -2
  28. package/dist/procedures/schedule/next.js.map +1 -1
  29. package/dist/programs.d.ts +3 -1
  30. package/dist/programs.d.ts.map +1 -1
  31. package/dist/programs.js.map +1 -1
  32. package/dist/scheduler.d.ts +9 -0
  33. package/dist/scheduler.d.ts.map +1 -1
  34. package/dist/scheduler.js +11 -6
  35. package/dist/scheduler.js.map +1 -1
  36. package/dist/store.d.ts +50 -5
  37. package/dist/store.d.ts.map +1 -1
  38. package/dist/store.js +161 -26
  39. package/dist/store.js.map +1 -1
  40. package/dist/types.d.ts +8 -4
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +4 -4
  43. package/src/cron.ts +277 -48
  44. package/src/errors.ts +4 -0
  45. package/src/index.ts +10 -2
  46. package/src/module.ts +13 -2
  47. package/src/procedures/schedule/add.ts +15 -10
  48. package/src/procedures/schedule/list.ts +1 -1
  49. package/src/procedures/schedule/next.ts +3 -3
  50. package/src/programs.ts +3 -1
  51. package/src/scheduler.ts +16 -6
  52. package/src/store.ts +166 -24
  53. package/src/types.ts +8 -4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # @procedures/schedule
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6a98b23: IANA time zones, and a lock file for `FileScheduleStore` across processes:
8
+
9
+ - `schedule.add`, `schedule.next` and the stored schedules take a time zone `utc`, `local` or an IANA name, for example `Europe/Berlin`. `Intl` examines the name and gives the offsets: the package has no time zone database. An unknown name gives the new error `BAD_TIME_ZONE`.
10
+ - `createScheduleModule({ timeZone })` and `new Scheduler({ timeZone })` give the IANA zone of `local`. The default is the time zone of the process.
11
+ - The changes of daylight saving time follow Vixie cron. A fixed time (no `*` at the start of the minute and hour fields) in the hour that does not exist runs once, at the first minute after the change. A fixed time in the hour that occurs twice runs once, at its first occurrence. An expression with `*` in the minute or hour field follows the clock.
12
+ - The zone `local` uses these rules too. Before, a time in the skipped hour did not run, and a time in the repeated hour could run two times.
13
+ - `FileScheduleStore` holds a lock file (`<path>.lock`, made with the flag `wx`) for each read-change-write. Two processes on one file thus lose no write. A lock older than `staleMs` (default 30 seconds) is stale, and the next store takes it over. A store that waits for twice `staleMs` gives the new error `LOCKED`.
14
+ - The new exports: `checkTimeZone`, `hostTimeZone`, `zoneFor`, `DEFAULT_LOCK_STALE_MS` and `FileScheduleStoreOptions`. `CronExpression` has the new field `fixedTime`.
15
+
16
+ ### Patch Changes
17
+
18
+ - 0f75644: The dependencies go to `@mark1russell7/algebra` `^0.7.0` and `@mark1russell7/service` `^0.4.1`. A host that uses algebra 0.7.0 gets one copy of it. Algebra 0.7.0 only adds to 0.6.0.
19
+ - Updated dependencies [0f75644]
20
+ - Updated dependencies [09e5566]
21
+ - Updated dependencies [ab77f75]
22
+ - Updated dependencies [a47bee6]
23
+ - Updated dependencies [89481b0]
24
+ - Updated dependencies [bc74091]
25
+ - Updated dependencies [bc74091]
26
+ - @procedures/client@0.5.0
27
+ - @procedures/store@0.5.0
28
+
3
29
  ## 0.4.0
4
30
 
5
31
  ### Patch Changes
package/README.md CHANGED
@@ -22,8 +22,10 @@ A cron for stored programs. The procedures add, list and remove schedules. A `Sc
22
22
  - `schedule.next` reads no store. Its output comes from its input and the time.
23
23
  - A store can be a file, so the procedures that read or write the schedules declare `fs` too.
24
24
 
25
+ - The time zone of `schedule.add` and `schedule.next` is `local` by default. It can be `utc` or an IANA name, for example `Europe/Berlin` (refer to [Time zones](#time-zones)).
26
+
25
27
  ```ts
26
- await client.call(["schedule", "add"], { cron: "0 9 * * mon-fri", program: "reports.daily", input: { team: "core" } });
28
+ await client.call(["schedule", "add"], { cron: "0 9 * * mon-fri", program: "reports.daily", input: { team: "core" }, timeZone: "Europe/Berlin" });
27
29
  await client.call(["schedule", "next"], { cron: "*/15 * * * *", count: 3 });
28
30
  ```
29
31
 
@@ -60,20 +62,40 @@ await scheduler.start();
60
62
  | `policy` | The policy of the host, from `hostPolicy()`. |
61
63
  | `clock` | The clock of the timers and of the durations. The default is `realClock`. |
62
64
  | `now` | The wall time in milliseconds. The default is `Date.now`. |
65
+ | `timeZone` | The IANA time zone of the schedules with the time zone `local`. The default is the time zone of the process. |
63
66
  | `runTimeoutMs` | The time limit of a run. 0 sets no limit. |
64
67
  | `onRun`, `onError` | Functions that get each run and each error of the scheduler. |
65
68
 
66
69
  A test gives `fakeClock` of `@mark1russell7/service/testing`, and a wall time from it: `now: () => start + clock.now()`. It gives the same `now` to `createScheduleModule()`.
67
70
 
71
+ A host gives the same `timeZone` to `createScheduleModule()` and to its `Scheduler`. Then `nextRunAt` of the procedures and the runs of the scheduler agree.
72
+
68
73
  ## The stores
69
74
 
70
75
  - `MemoryScheduleStore` keeps the schedules while the process runs. The default module uses `defaultScheduleStore`, one store in memory for the process.
71
- - `FileScheduleStore` keeps them in a JSON file. A write goes through a temporary file and a rename. The store is for one process.
76
+ - `FileScheduleStore` keeps them in a JSON file. A write goes through a temporary file and a rename, so a reader does not see a part of a write.
72
77
  - `update()` changes one schedule in one step. The record of a run thus does not lose a change of `schedule.add`, and it does not touch a schedule that replaced the old one.
73
78
 
79
+ ### Two processes on one file
80
+
81
+ Two processes can use the same `FileScheduleStore` file, and they lose no write. Each change holds a lock file next to the store file: `schedules.json.lock`.
82
+
83
+ 1. The store makes the lock file with the flag `wx`. Only one process can make it. The other processes wait and try again.
84
+ 2. Under the lock, the store reads the file again, makes its change, writes the file and removes the lock. An error also removes the lock.
85
+ 3. A lock file older than `staleMs` is stale: its process stopped. The next store takes it over. The default of `staleMs` is 30 seconds.
86
+ 4. A store that waits for twice `staleMs` gives up with the error `LOCKED`.
87
+
88
+ ```ts
89
+ const store = new FileScheduleStore("schedules.json", { staleMs: 30_000 });
90
+ ```
91
+
92
+ - To take over a stale lock, a store makes a second lock file (`.lock.break`) with `wx`. It examines the age of the lock again under it, and then it removes the stale lock. Two stores thus cannot both remove the lock.
93
+ - The lock file holds a token of its store. A store removes only its own lock, so a slow store does not remove the lock of the store that took it over.
94
+ - The lock uses only `node:fs`, with no dependency. A store on a network file system gets the guarantees of the flag `wx` on that system.
95
+
74
96
  ## Cron expressions
75
97
 
76
- This package parses cron itself. The format is small, and a cron library brings a time zone database or another format that this package does not need.
98
+ This package parses cron itself. The format is small, and `Intl` gives the offsets of the time zones. A cron library brings a time zone database or another format that this package does not need.
77
99
 
78
100
  | Field | Values |
79
101
  |---|---|
@@ -86,10 +108,35 @@ This package parses cron itself. The format is small, and a cron library brings
86
108
  - Each field takes `*`, a number, a range `a-b`, a step after `/` (`*/15`, `a-b/n`, `a/n`), and lists of these with commas.
87
109
  - The names `@yearly`, `@annually`, `@monthly`, `@weekly`, `@daily`, `@midnight` and `@hourly` replace the 5 fields.
88
110
  - When the two day fields have a limit, a day matches when one of the two matches, as in Vixie cron. A field that starts with `*` has no limit.
89
- - The time zone is `local` (the time of the host) by default, or `utc`.
90
- - In local time, a time that the change to summer time skips gives no run. A time that the change back repeats can give two runs.
91
111
  - An expression without a time in the next 9 years is an error of `schedule.add`.
92
112
 
113
+ ## Time zones
114
+
115
+ The time zone of a schedule is `local` by default. It can also be `utc` or an IANA name, for example `Europe/Berlin`, `Australia/Adelaide` or `Asia/Kolkata`.
116
+
117
+ - `local` is the time zone of the process. The option `timeZone` of `createScheduleModule()` and of `Scheduler` gives another zone to `local`.
118
+ - `Intl.DateTimeFormat` examines a name and gives the offsets of the zone. The package has no time zone database.
119
+ - A name that `Intl` does not know gives the error `BAD_TIME_ZONE`. An offset, for example `+05:30`, is not an IANA name.
120
+ - A name that differs from the name of `Intl` only in the case gets the case of `Intl`: `europe/berlin` becomes `Europe/Berlin`.
121
+ - `nextRunAt` and the times of `schedule.next` have the offset of the zone at that time: `2026-10-17T09:00:00+02:00`. An offset of 0 gives `Z`.
122
+
123
+ ### The changes of daylight saving time
124
+
125
+ The changes of daylight saving time follow Vixie cron. The rules depend on the minute field and the hour field of the expression.
126
+
127
+ A fixed time has no `*` at the start of its minute field and of its hour field, for example `30 2 * * *`:
128
+
129
+ - A time in the hour that does not exist runs once, at the first minute after the change. In `Europe/Berlin` on 2026-03-29, `30 2 * * *` runs at 03:00 CEST.
130
+ - Two times of one expression in that hour give one run.
131
+ - A time in the hour that occurs twice runs once, at its first occurrence. In `Europe/Berlin` on 2026-10-25, `30 2 * * *` runs at 02:30 CEST, and not at 02:30 CET.
132
+
133
+ An expression with `*` at the start of its minute field or of its hour field follows the clock, for example `0 * * * *` or `*/15 * * * *`:
134
+
135
+ - A time in the hour that does not exist has no run. The clock does not show it.
136
+ - A time in the hour that occurs twice runs at the two occurrences. So `*/15 * * * *` runs each 15 minutes of real time across the two changes.
137
+
138
+ The same rules apply to the zones with a change of 30 minutes, for example `Australia/Lord_Howe`.
139
+
93
140
  ## MCP
94
141
 
95
142
  `schedule.next` and `schedule.list` change nothing, and they are good tools. `schedule.add` makes the host start a program later, at each time, so a host gives it to Claude only on purpose. `schedule.remove` stops the runs that the owner wants.
@@ -97,3 +144,6 @@ This package parses cron itself. The format is small, and a cron library brings
97
144
  ## Tests
98
145
 
99
146
  `pnpm --filter @procedures/schedule test` examines the procedures, the stores and the cron expressions. The tests of the scheduler use `fakeClock`, so each time is exact and no test waits.
147
+
148
+ - The tests of the time zones use fixed dates across the two changes of `Europe/Berlin` and `Australia/Adelaide`, a zone with a half-hour offset.
149
+ - The tests of the lock start two child processes. Each one writes 50 schedules to one file, and the file then holds all 100. The Windows job of the nightly workflow runs them too.