@softure-ai/jobs 0.1.1 → 0.1.2

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 CHANGED
@@ -4,6 +4,22 @@ Newest first. Each version lists what changed for an app that uses `@softure-ai/
4
4
  production, the version gets a line `verified in: <app>@<commit>` ([docs/05](../../docs/05-adoption-playbook.md),
5
5
  "Definition of done").
6
6
 
7
+ ## 0.1.2
8
+
9
+ - Schedules at local times of day (#554): `schedules: { "<queue>": { at: ["09:00", "21:00"], timeZone: "Europe/Warsaw" } }`
10
+ enqueues one run per listed local time, with the zone's offset on each date, so the run keeps its local time across
11
+ DST changes (a skipped time runs just after the change, a repeated one once). The same "once per slot however many
12
+ workers run" guarantee as periods, no migration. A schedule takes exactly one of `every` and `at`; `timeZone` is
13
+ required with `at` and refused with `every`. Period schedules are unchanged. `getQueueStats` reports an `at`
14
+ schedule's `everySeconds` as the longest gap between its times. New types `PeriodScheduleOptions` and
15
+ `LocalTimeScheduleOptions`; `JobScheduleOptions` is their union, so code that read `schedule.every` checks
16
+ `"every" in schedule` first.
17
+ - Migration for an app that runs a job at a local time from a host cron: add the schedule to `jobs({ schedules })`
18
+ (to move billing's reminder mail: `"billing.access-reminders": { at: ["09:00", "21:00"], timeZone: "<zone>" }`
19
+ after `...billingJobSchedules`, which replaces its daily UTC period), make sure the worker runs the queue's handler
20
+ (`...billingJobHandlers` or `createBillingJobHandlers({ reminders })`), then remove the host cron entry (the deploy
21
+ `maintain` hook) so the job does not run twice.
22
+
7
23
  ## 0.1.1
8
24
 
9
25
  - Recurring schedules (#491): `jobs({ schedules: { "<queue>": { every: "1h", payload } } })`. Workers enqueue each
package/README.md CHANGED
@@ -47,7 +47,9 @@ export default defineSoftureConfig({
47
47
  | `queues.<name>.backoff` | `{ baseSeconds: 10, maxSeconds: 3600 }` | delay after attempt n is `min(max, base * 2^(n-1))`, shortened by up to 20 % at random |
48
48
  | `queues.<name>.leaseSeconds` | `300` | how long a claimed job is reserved; the worker extends it while the handler runs, so it bounds how long a crashed worker's job waits, not how long a handler may take |
49
49
  | `queues.<name>.concurrency` | `1` | jobs of the queue one worker process runs at once |
50
- | `schedules.<queue>.every` | (required) | how often the schedule runs: `<n>m`, `<n>h` or `<n>d`, from `1m` to `7d` (§3, "Schedules") |
50
+ | `schedules.<queue>.every` | (one of `every`, `at`) | how often the schedule runs: `<n>m`, `<n>h` or `<n>d`, from `1m` to `7d`, aligned to UTC (§3, "Schedules") |
51
+ | `schedules.<queue>.at` | (one of `every`, `at`) | local times of day the schedule runs at, `HH:MM` (`00:00` to `23:59`), each once, up to 48 |
52
+ | `schedules.<queue>.timeZone` | (required with `at`) | the IANA time zone of `at`, such as `Europe/Warsaw` or `UTC` |
51
53
  | `schedules.<queue>.payload` | `{}` | the payload of every scheduled run |
52
54
  | `pollIntervalMs` | `1000` | how often an idle worker looks for due jobs |
53
55
  | `shutdownGraceMs` | `20000` | how long a stopping worker waits for running handlers before releasing their jobs |
@@ -154,7 +156,8 @@ a process of its own.
154
156
 
155
157
  ### Schedules
156
158
 
157
- A schedule runs its queue's handler once per period, instead of an app cron:
159
+ A schedule runs its queue's handler once per period (`every`) or at local times of day (`at` in `timeZone`), instead
160
+ of an app cron:
158
161
 
159
162
  ```ts
160
163
  import { authJobSchedules } from "@softure-ai/auth/jobs";
@@ -165,6 +168,8 @@ jobs({
165
168
  ...authJobSchedules, // auth.prune-sessions hourly, auth.prune-password-resets daily
166
169
  ...securityJobSchedules, // security.prune-rate-limits hourly
167
170
  "app.digest": { every: "1d", payload: { kind: "weekly" } },
171
+ // The trial-ending reminder at 09:00 and 21:00 in the users' time zone, in place of billing's daily UTC period.
172
+ "billing.access-reminders": { at: ["09:00", "21:00"], timeZone: "Europe/Warsaw" },
168
173
  },
169
174
  });
170
175
  ```
@@ -179,10 +184,15 @@ export const handlers: JobHandlers = { ...authJobHandlers, ...securityJobHandler
179
184
 
180
185
  - Periods start on whole multiples of `every` counted from 1970-01-01 00:00 UTC: `1h` on the full hour, `1d` at
181
186
  midnight UTC, `15m` at :00, :15, :30, :45. A run is due at the start of its period.
187
+ - An `at` schedule's slots are its listed times on each local date in `timeZone`, with the zone's offset on that date:
188
+ `09:00` in `Europe/Warsaw` is 07:00 UTC in summer and 08:00 UTC in winter. A time a DST change skips runs as far
189
+ after the change as it would have been (`02:30` on the spring-forward day runs at 03:30); a time a DST change
190
+ repeats runs once, at its first occurrence. A worker that starts after a listed time enqueues that slot's run at
191
+ once, the same way a period schedule enqueues the period it starts in.
182
192
  - Once per period, however many workers run: a worker takes a period with a conditional upsert of
183
193
  `jobs.schedules` (the period must be later than the stored one) and enqueues the run in the same transaction, so a
184
194
  missed or doubled tick cannot happen. A worker enqueues only the schedules of the queues it runs.
185
- - Missed periods (no worker ran) collapse into one run for the current period.
195
+ - Missed periods or times (no worker ran) collapse into one run for the current slot.
186
196
  - Every scheduled run has the dedup key `schedule`: while a run is still pending or running (retrying a failure), the
187
197
  next period joins it instead of queueing a second run behind it.
188
198
  - Scheduled runs are ordinary jobs: retries, `maxAttempts`, `getJob`, `retryJob` and pruning apply.
@@ -190,8 +200,8 @@ export const handlers: JobHandlers = { ...authJobHandlers, ...securityJobHandler
190
200
  `@softure-ai/waitlist/jobs`, `@softure-ai/security/jobs`, `@softure-ai/analytics/jobs`), with `@softure-ai/jobs` as an
191
201
  optional peer.
192
202
 
193
- `getQueueStats` reports each scheduled queue's `schedule`: `everySeconds`, `lastEnqueuedAt` (the start of the latest
194
- period a run was enqueued for), `lastSucceededAt` (any succeeded job of the queue, a run enqueued by hand too) and
203
+ `getQueueStats` reports each scheduled queue's `schedule`: `everySeconds` (for an `at` schedule the longest gap
204
+ between two of its times, 24h for one time), `lastEnqueuedAt` (the start of the latest slot a run was enqueued for), `lastSucceededAt` (any succeeded job of the queue, a run enqueued by hand too) and
195
205
  `isLate`: no success within two periods, counted from when a worker first saw the schedule, or no worker has seen it
196
206
  at all. The module's health check does not fail on a late schedule (a late cleanup must not take the app out of
197
207
  rotation); an app that wants an alarm adds a check of its own:
@@ -283,7 +293,8 @@ logger gets payload paths, never values, for a payload that does not match its s
283
293
  ## 12. Limitations
284
294
 
285
295
  - Polling, no `LISTEN/NOTIFY`: a job waits up to `pollIntervalMs` when the worker is idle.
286
- - Schedules take a fixed period (`every`), aligned to UTC; no cron expressions, no time zones, no "at 03:00".
296
+ - Schedules take a fixed period aligned to UTC (`every`) or times of day in a time zone (`at`); no cron expressions,
297
+ no days of the week.
287
298
  - No priorities within a queue and no rate limits per queue beyond `concurrency`; split work into queues instead.
288
299
  - At-least-once delivery (§3, "Handlers run at least once").
289
300
  - PGlite has one connection, so tests run claims one after another; concurrent claiming is Postgres behaviour
package/dist/index.d.ts CHANGED
@@ -23,10 +23,17 @@ export declare const jobs: import("@softure-ai/core").ModuleFactory<{}, {
23
23
  leaseSeconds: import("zod").ZodDefault<import("zod").ZodNumber>;
24
24
  concurrency: import("zod").ZodDefault<import("zod").ZodNumber>;
25
25
  }, import("zod/v4/core").$strict>>>;
26
- schedules: import("zod").ZodDefault<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodObject<{
27
- every: import("zod").ZodString;
26
+ schedules: import("zod").ZodDefault<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodPipe<import("zod").ZodObject<{
27
+ every: import("zod").ZodOptional<import("zod").ZodString>;
28
+ at: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
29
+ timeZone: import("zod").ZodOptional<import("zod").ZodString>;
28
30
  payload: import("zod").ZodDefault<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodUnknown>>;
29
- }, import("zod/v4/core").$strict>>>;
31
+ }, import("zod/v4/core").$strict>, import("zod").ZodTransform<import("./options.js").PeriodScheduleOptions | import("./options.js").LocalTimeScheduleOptions, {
32
+ payload: Record<string, unknown>;
33
+ every?: string | undefined;
34
+ at?: string[] | undefined;
35
+ timeZone?: string | undefined;
36
+ }>>>>;
30
37
  pollIntervalMs: import("zod").ZodDefault<import("zod").ZodNumber>;
31
38
  shutdownGraceMs: import("zod").ZodDefault<import("zod").ZodNumber>;
32
39
  retention: import("zod").ZodDefault<import("zod").ZodObject<{
@@ -46,10 +53,7 @@ export declare const jobs: import("@softure-ai/core").ModuleFactory<{}, {
46
53
  concurrency: number;
47
54
  };
48
55
  };
49
- schedules: Record<string, {
50
- every: string;
51
- payload: Record<string, unknown>;
52
- }>;
56
+ schedules: Record<string, import("./options.js").PeriodScheduleOptions | import("./options.js").LocalTimeScheduleOptions>;
53
57
  pollIntervalMs: number;
54
58
  shutdownGraceMs: number;
55
59
  retention: {
@@ -67,10 +71,7 @@ export declare const jobs: import("@softure-ai/core").ModuleFactory<{}, {
67
71
  leaseSeconds: number;
68
72
  concurrency: number;
69
73
  }>;
70
- schedules: Record<string, {
71
- every: string;
72
- payload: Record<string, unknown>;
73
- }>;
74
+ schedules: Record<string, import("./options.js").PeriodScheduleOptions | import("./options.js").LocalTimeScheduleOptions>;
74
75
  pollIntervalMs: number;
75
76
  shutdownGraceMs: number;
76
77
  retention: {
@@ -81,7 +82,7 @@ export declare const jobs: import("@softure-ai/core").ModuleFactory<{}, {
81
82
  }>>>;
82
83
  export { JOB_STATUSES, type Job, type JobFailure, type JobOutcome, type JobsErrorCode, type JobStatus, } from "./contract.js";
83
84
  export { jobsMessages, type JobsMessages } from "./messages/index.js";
84
- export { jobsOptionsSchema, type JobQueueOptions, type JobScheduleOptions, type JobSchedulesInput, type JobsOptions, type JobsOptionsInput, } from "./options.js";
85
+ export { jobsOptionsSchema, type JobQueueOptions, type JobScheduleOptions, type JobSchedulesInput, type JobsOptions, type JobsOptionsInput, type LocalTimeScheduleOptions, type PeriodScheduleOptions, } from "./options.js";
85
86
  export { defineJobHandler, failJob, retryJobLater, type JobHandler, type JobHandlerContext, type JobHandlers } from "./handlers.js";
86
87
  export { jobs as jobsTable, schedules as jobSchedulesTable } from "./schema.js";
87
88
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,SAAS,SAAS,CAAC;AAEhC;;;GAGG;AACH,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAiBf,CAAC;AAEH,OAAO,EACL,YAAY,EACZ,KAAK,GAAG,EACR,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,SAAS,GACf,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,EACL,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,gBAAgB,GACtB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,UAAU,EAAE,KAAK,iBAAiB,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AACpI,OAAO,EAAE,IAAI,IAAI,SAAS,EAAE,SAAS,IAAI,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,SAAS,SAAS,CAAC;AAEhC;;;GAGG;AACH,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAiBf,CAAC;AAEH,OAAO,EACL,YAAY,EACZ,KAAK,GAAG,EACR,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,SAAS,GACf,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,EACL,iBAAiB,EACjB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,GAC3B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,aAAa,EAAE,KAAK,UAAU,EAAE,KAAK,iBAAiB,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AACpI,OAAO,EAAE,IAAI,IAAI,SAAS,EAAE,SAAS,IAAI,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ export const MODULE_ID = "jobs";
13
13
  export const jobs = defineModule({
14
14
  manifest: {
15
15
  id: MODULE_ID,
16
- version: "0.1.1",
16
+ version: "0.1.2",
17
17
  dependsOn: {},
18
18
  dbSchema: "jobs",
19
19
  tables: ["jobs", "schedules"],
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+GAA+G;AAC/G,oHAAoH;AACpH,uCAAuC;AACvC,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC;AAEhC;;;GAGG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,YAAY,CAAC;IAC/B,QAAQ,EAAE;QACR,EAAE,EAAE,SAAS;QACb,OAAO,EAAE,OAAO;QAChB,SAAS,EAAE,EAAE;QACb,QAAQ,EAAE,MAAM;QAChB,MAAM,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC;QAC7B,GAAG,EAAE,EAAE;QACP,QAAQ,EAAE,EAAE;QACZ,MAAM,EAAE,EAAE;QACV,KAAK,EAAE,EAAE;QACT,OAAO,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE;KAC5C;IACD,QAAQ,EAAE,YAAY;IACtB,OAAO,EAAE,iBAAiB;IAC1B,UAAU,EAAE,EAAE,GAAG,EAAE,oBAAoB,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,EAAE;IAC5E,MAAM,EAAE,cAAc;CACvB,CAAC,CAAC;AAEH,OAAO,EACL,YAAY,GAMb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAqB,MAAM,qBAAqB,CAAC;AACtE,OAAO,EACL,iBAAiB,GAMlB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,aAAa,EAA6D,MAAM,eAAe,CAAC;AACpI,OAAO,EAAE,IAAI,IAAI,SAAS,EAAE,SAAS,IAAI,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+GAA+G;AAC/G,oHAAoH;AACpH,uCAAuC;AACvC,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEpD,MAAM,CAAC,MAAM,SAAS,GAAG,MAAM,CAAC;AAEhC;;;GAGG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,YAAY,CAAC;IAC/B,QAAQ,EAAE;QACR,EAAE,EAAE,SAAS;QACb,OAAO,EAAE,OAAO;QAChB,SAAS,EAAE,EAAE;QACb,QAAQ,EAAE,MAAM;QAChB,MAAM,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC;QAC7B,GAAG,EAAE,EAAE;QACP,QAAQ,EAAE,EAAE;QACZ,MAAM,EAAE,EAAE;QACV,KAAK,EAAE,EAAE;QACT,OAAO,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE;KAC5C;IACD,QAAQ,EAAE,YAAY;IACtB,OAAO,EAAE,iBAAiB;IAC1B,UAAU,EAAE,EAAE,GAAG,EAAE,oBAAoB,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,EAAE;IAC5E,MAAM,EAAE,cAAc;CACvB,CAAC,CAAC;AAEH,OAAO,EACL,YAAY,GAMb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAqB,MAAM,qBAAqB,CAAC;AACtE,OAAO,EACL,iBAAiB,GAQlB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,gBAAgB,EAAE,OAAO,EAAE,aAAa,EAA6D,MAAM,eAAe,CAAC;AACpI,OAAO,EAAE,IAAI,IAAI,SAAS,EAAE,SAAS,IAAI,iBAAiB,EAAE,MAAM,aAAa,CAAC"}
package/dist/options.d.ts CHANGED
@@ -1,6 +1,17 @@
1
1
  import { z } from "zod";
2
2
  /** Seconds of an `every` text such as `15m`, `1h` or `1d`; null when it is not one or is outside 1m to 7d. */
3
3
  export declare function parseEverySeconds(every: string): number | null;
4
+ /** A schedule that runs once per period, aligned to UTC. */
5
+ export interface PeriodScheduleOptions {
6
+ readonly every: string;
7
+ readonly payload: Record<string, unknown>;
8
+ }
9
+ /** A schedule that runs at local times of day in a time zone; `at` is sorted. */
10
+ export interface LocalTimeScheduleOptions {
11
+ readonly at: readonly string[];
12
+ readonly timeZone: string;
13
+ readonly payload: Record<string, unknown>;
14
+ }
4
15
  export declare const jobsOptionsSchema: z.ZodPipe<z.ZodObject<{
5
16
  queues: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
6
17
  maxAttempts: z.ZodDefault<z.ZodNumber>;
@@ -11,10 +22,17 @@ export declare const jobsOptionsSchema: z.ZodPipe<z.ZodObject<{
11
22
  leaseSeconds: z.ZodDefault<z.ZodNumber>;
12
23
  concurrency: z.ZodDefault<z.ZodNumber>;
13
24
  }, z.core.$strict>>>;
14
- schedules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
15
- every: z.ZodString;
25
+ schedules: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodPipe<z.ZodObject<{
26
+ every: z.ZodOptional<z.ZodString>;
27
+ at: z.ZodOptional<z.ZodArray<z.ZodString>>;
28
+ timeZone: z.ZodOptional<z.ZodString>;
16
29
  payload: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
17
- }, z.core.$strict>>>;
30
+ }, z.core.$strict>, z.ZodTransform<PeriodScheduleOptions | LocalTimeScheduleOptions, {
31
+ payload: Record<string, unknown>;
32
+ every?: string | undefined;
33
+ at?: string[] | undefined;
34
+ timeZone?: string | undefined;
35
+ }>>>>;
18
36
  pollIntervalMs: z.ZodDefault<z.ZodNumber>;
19
37
  shutdownGraceMs: z.ZodDefault<z.ZodNumber>;
20
38
  retention: z.ZodDefault<z.ZodObject<{
@@ -34,10 +52,7 @@ export declare const jobsOptionsSchema: z.ZodPipe<z.ZodObject<{
34
52
  concurrency: number;
35
53
  };
36
54
  };
37
- schedules: Record<string, {
38
- every: string;
39
- payload: Record<string, unknown>;
40
- }>;
55
+ schedules: Record<string, PeriodScheduleOptions | LocalTimeScheduleOptions>;
41
56
  pollIntervalMs: number;
42
57
  shutdownGraceMs: number;
43
58
  retention: {
@@ -55,10 +70,7 @@ export declare const jobsOptionsSchema: z.ZodPipe<z.ZodObject<{
55
70
  leaseSeconds: number;
56
71
  concurrency: number;
57
72
  }>;
58
- schedules: Record<string, {
59
- every: string;
60
- payload: Record<string, unknown>;
61
- }>;
73
+ schedules: Record<string, PeriodScheduleOptions | LocalTimeScheduleOptions>;
62
74
  pollIntervalMs: number;
63
75
  shutdownGraceMs: number;
64
76
  retention: {
@@ -1 +1 @@
1
- {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAuBtB,8GAA8G;AAC9G,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK9D;AAgCH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwB1B,CAAC;AAQL,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AACjE,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC7D,MAAM,MAAM,eAAe,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC;AAC5D,MAAM,MAAM,kBAAkB,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAClE,iGAAiG;AACjG,MAAM,MAAM,iBAAiB,GAAG,WAAW,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"options.d.ts","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAwBtB,8GAA8G;AAC9G,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK9D;AAyCH,4DAA4D;AAC5D,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAED,iFAAiF;AACjF,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC3C;AAyBD,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyB1B,CAAC;AAQL,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AACjE,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC7D,MAAM,MAAM,eAAe,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC;AAC5D,MAAM,MAAM,kBAAkB,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAClE,iGAAiG;AACjG,MAAM,MAAM,iBAAiB,GAAG,WAAW,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC,CAAC"}
package/dist/options.js CHANGED
@@ -1,5 +1,6 @@
1
1
  // The options an app passes to `jobs({ ... })` in softure.config.ts, parsed at startup.
2
2
  import { z } from "zod";
3
+ import { isTimeZone } from "./zoned-time.js";
3
4
  const QUEUE_NAME = /^[a-z][a-z0-9_.-]{0,62}$/;
4
5
  const QUEUE_NAME_HINT = "is not a queue name: lowercase letters, digits, _ . and -, starting with a letter";
5
6
  const DAY_SECONDS = 24 * 60 * 60;
@@ -26,11 +27,42 @@ export function parseEverySeconds(every) {
26
27
  const seconds = Number(match[1]) * UNIT_SECONDS[match[2]];
27
28
  return seconds <= MAX_EVERY_SECONDS ? seconds : null;
28
29
  }
29
- const scheduleSchema = z.strictObject({
30
+ const AT_TIME = /^([01][0-9]|2[0-3]):[0-5][0-9]$/;
31
+ const AT_HINT = "must be times of day as HH:MM (00:00 to 23:59), each once";
32
+ const MAX_AT_TIMES = 48;
33
+ const scheduleSchema = z
34
+ .strictObject({
30
35
  /** How often the schedule runs: `15m`, `1h`, `1d`. Runs start on whole periods counted from 1970-01-01 00:00 UTC. */
31
- every: z.string().refine((every) => parseEverySeconds(every) !== null, { message: EVERY_HINT }),
36
+ every: z.string().refine((every) => parseEverySeconds(every) !== null, { message: EVERY_HINT }).optional(),
37
+ /** Instead of `every`: the local times of day the schedule runs at, `HH:MM`, in `timeZone`. */
38
+ at: z
39
+ .array(z.string().regex(AT_TIME, { message: AT_HINT }))
40
+ .min(1)
41
+ .max(MAX_AT_TIMES)
42
+ .refine((times) => new Set(times).size === times.length, { message: AT_HINT })
43
+ .optional(),
44
+ /** The IANA time zone of `at`, such as `Europe/Warsaw`; required with `at`. */
45
+ timeZone: z.string().refine(isTimeZone, { message: "is not a time zone the runtime knows (an IANA name such as Europe/Warsaw)" }).optional(),
32
46
  /** The payload of every run; default `{}`. */
33
47
  payload: z.record(z.string(), z.unknown()).default({}),
48
+ })
49
+ .superRefine((schedule, context) => {
50
+ if ((schedule.every === undefined) === (schedule.at === undefined)) {
51
+ context.addIssue({ code: "custom", path: ["every"], message: "set either every or at, not both" });
52
+ }
53
+ if (schedule.at !== undefined && schedule.timeZone === undefined) {
54
+ context.addIssue({ code: "custom", path: ["timeZone"], message: "is required with at" });
55
+ }
56
+ if (schedule.every !== undefined && schedule.timeZone !== undefined) {
57
+ context.addIssue({ code: "custom", path: ["timeZone"], message: "applies to at only; every runs in UTC" });
58
+ }
59
+ })
60
+ .transform((schedule) => {
61
+ if (schedule.at !== undefined && schedule.timeZone !== undefined) {
62
+ return { at: [...schedule.at].sort(), timeZone: schedule.timeZone, payload: schedule.payload };
63
+ }
64
+ // The refinement above leaves every set whenever at is not.
65
+ return { every: schedule.every ?? "", payload: schedule.payload };
34
66
  });
35
67
  const queueSchema = z.strictObject({
36
68
  /** Attempts before a job is `failed` for good; the first run counts. */
@@ -58,8 +90,9 @@ export const jobsOptionsSchema = z
58
90
  /** The queues, by name. A job is enqueued to one of them and runs with its settings. */
59
91
  queues: z.record(z.string(), queueSchema).default({}).superRefine(checkNames),
60
92
  /**
61
- * Recurring runs, by queue name: the worker enqueues one job per period (`every`) however many workers run. A
62
- * schedule's queue takes the default queue settings unless `queues` names it.
93
+ * Recurring runs, by queue name: the worker enqueues one job per period (`every`) or per local time of day (`at`
94
+ * in `timeZone`) however many workers run. A schedule's queue takes the default queue settings unless `queues`
95
+ * names it.
63
96
  */
64
97
  schedules: z.record(z.string(), scheduleSchema).default({}).superRefine(checkNames),
65
98
  /** How long an idle worker waits before it looks for due jobs again. */
@@ -1 +1 @@
1
- {"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,UAAU,GAAG,0BAA0B,CAAC;AAC9C,MAAM,eAAe,GAAG,mFAAmF,CAAC;AAC5G,MAAM,WAAW,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC;AAEjC,MAAM,aAAa,GAAG,CAAC;KACpB,YAAY,CAAC;IACV,kEAAkE;IAClE,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACjE,8CAA8C;IAC9C,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;CACvE,CAAC;KACD,MAAM,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,WAAW,EAAE;IAC9D,OAAO,EAAE,yCAAyC;IAClD,IAAI,EAAE,CAAC,YAAY,CAAC;CACrB,CAAC,CAAC;AAEL,MAAM,KAAK,GAAG,4BAA4B,CAAC;AAC3C,MAAM,YAAY,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,EAAE,CAAC,EAAE,WAAW,EAAW,CAAC;AACpE,MAAM,iBAAiB,GAAG,CAAC,GAAG,WAAW,CAAC;AAC1C,MAAM,UAAU,GAAG,oFAAoF,CAAC;AAExG,8GAA8G;AAC9G,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChC,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAA8B,CAAC,CAAC;IACvF,OAAO,OAAO,IAAI,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AACvD,CAAC;AAED,MAAM,cAAc,GAAG,CAAC,CAAC,YAAY,CAAC;IACpC,qHAAqH;IACrH,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC,KAAK,CAAC,KAAK,IAAI,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC;IAC/F,8CAA8C;IAC9C,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;CACvD,CAAC,CAAC;AAEL,MAAM,WAAW,GAAG,CAAC,CAAC,YAAY,CAAC;IACjC,wEAAwE;IACxE,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IACzD,0CAA0C;IAC1C,OAAO,EAAE,aAAa,CAAC,OAAO,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;IACrE;;;OAGG;IACH,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC;IACnE,mEAAmE;IACnE,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;CACzD,CAAC,CAAC;AAEH,MAAM,eAAe,GAAG,CAAC,CAAC,YAAY,CAAC;IACrC,8DAA8D;IAC9D,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;IAC3D,gEAAgE;IAChE,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACzD,oCAAoC;IACpC,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;CAC5D,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC;KAC/B,YAAY,CAAC;IACZ,wFAAwF;IACxF,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,WAAW,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,WAAW,CAAC,UAAU,CAAC;IAC7E;;;OAGG;IACH,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,cAAc,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,WAAW,CAAC,UAAU,CAAC;IACnF,wEAAwE;IACxE,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC;IAClE,2FAA2F;IAC3F,eAAe,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IACzE,uEAAuE;IACvE,SAAS,EAAE,eAAe,CAAC,OAAO,CAAC,EAAE,aAAa,EAAE,CAAC,EAAE,UAAU,EAAE,EAAE,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC;CAC3F,CAAC;KACD,WAAW,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE;IAChC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC5F,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,4CAA4C,EAAE,CAAC,CAAC;IAChH,CAAC;AACH,CAAC,CAAC;KACD,SAAS,CAAC,CAAC,OAAO,EAAE,EAAE;IACrB,MAAM,aAAa,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;IAC5G,OAAO,EAAE,GAAG,OAAO,EAAE,MAAM,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,GAAG,MAAM,CAAC,WAAW,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;AAC1I,CAAC,CAAC,CAAC;AAEL,SAAS,UAAU,CAAC,MAA+B,EAAE,OAAwB;IAC3E,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC;QAC1F,OAAO,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;IAC/E,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"options.js","sourceRoot":"","sources":["../src/options.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,7 @@ import type { JobsContext } from "./context.js";
7
7
  export declare const SCHEDULE_DEDUP_KEY = "schedule";
8
8
  /** A schedule's health as `getQueueStats` reports it. */
9
9
  export interface ScheduleState {
10
+ /** The period; for a local-time schedule the longest gap between two of its times of day (24h for one time). */
10
11
  readonly everySeconds: number;
11
12
  /** The start of the latest period a run was enqueued for; null before any worker saw the schedule. */
12
13
  readonly lastEnqueuedAt: Date | null;
@@ -22,7 +23,10 @@ export interface EnqueueDueSchedulesInput {
22
23
  /** Only the schedules of these queues; default every schedule. */
23
24
  readonly queues?: readonly string[];
24
25
  }
25
- /** The start of the period `now` falls in. */
26
+ /**
27
+ * The slot `now` falls in: the start of its period, or the latest of the schedule's local times at or before `now`.
28
+ * Slots only move forward as `now` does, across DST changes too.
29
+ */
26
30
  export declare function getScheduleSlot(schedule: JobScheduleOptions, now: Date): Date;
27
31
  /**
28
32
  * Enqueues a run for every schedule whose current period has none yet, and returns their queue names. Missed periods
@@ -1 +1 @@
1
- {"version":3,"file":"schedules.d.ts","sourceRoot":"","sources":["../../src/server/schedules.ts"],"names":[],"mappings":"AAIA,OAAO,EAAqB,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAAE,MAAM,eAAe,CAAC;AAE7F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD;;;GAGG;AACH,eAAO,MAAM,kBAAkB,aAAa,CAAC;AAE7C,yDAAyD;AACzD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,sGAAsG;IACtG,QAAQ,CAAC,cAAc,EAAE,IAAI,GAAG,IAAI,CAAC;IACrC,iFAAiF;IACjF,QAAQ,CAAC,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;IACtC;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,wBAAwB;IACvC,kEAAkE;IAClE,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,8CAA8C;AAC9C,wBAAgB,eAAe,CAAC,QAAQ,EAAE,kBAAkB,EAAE,GAAG,EAAE,IAAI,GAAG,IAAI,CAG7E;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,GAAE,wBAA6B,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAQnH;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAiBtG;AAED,8FAA8F;AAC9F,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI1F;AAED,6DAA6D;AAC7D,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAe7F;AAED,0EAA0E;AAC1E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,EAAE,CAEtG;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,kBAAkB,CAIlF"}
1
+ {"version":3,"file":"schedules.d.ts","sourceRoot":"","sources":["../../src/server/schedules.ts"],"names":[],"mappings":"AAIA,OAAO,EAAqB,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAAiC,MAAM,eAAe,CAAC;AAG5H,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD;;;GAGG;AACH,eAAO,MAAM,kBAAkB,aAAa,CAAC;AAE7C,yDAAyD;AACzD,MAAM,WAAW,aAAa;IAC5B,gHAAgH;IAChH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,sGAAsG;IACtG,QAAQ,CAAC,cAAc,EAAE,IAAI,GAAG,IAAI,CAAC;IACrC,iFAAiF;IACjF,QAAQ,CAAC,eAAe,EAAE,IAAI,GAAG,IAAI,CAAC;IACtC;;;OAGG;IACH,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,wBAAwB;IACvC,kEAAkE;IAClE,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,kBAAkB,EAAE,GAAG,EAAE,IAAI,GAAG,IAAI,CAI7E;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,GAAE,wBAA6B,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAQnH;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAiBtG;AAED,8FAA8F;AAC9F,wBAAsB,qBAAqB,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAI1F;AAED,6DAA6D;AAC7D,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAe7F;AAED,0EAA0E;AAC1E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,EAAE,CAEtG;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,kBAAkB,CAIlF"}
@@ -1,9 +1,10 @@
1
- // Recurring schedules: each schedule's runs start on whole periods counted from the Unix epoch, so every worker
2
- // computes the same period. A worker takes a period with a conditional upsert of `jobs.schedules` and enqueues its run
1
+ // Recurring schedules: a period schedule's runs start on whole periods counted from the Unix epoch, a local-time
2
+ // schedule's at its listed times of day in its time zone, so every worker computes the same slot. A worker takes a period with a conditional upsert of `jobs.schedules` and enqueues its run
3
3
  // in the same transaction, so each period is enqueued once however many workers race for it.
4
4
  import { eq, inArray, lt } from "drizzle-orm";
5
5
  import { parseEverySeconds } from "../options.js";
6
6
  import { schedules } from "../schema.js";
7
+ import { getWallClock, getZonedInstant } from "../zoned-time.js";
7
8
  import { enqueueJob } from "./enqueue.js";
8
9
  import { getJobsOptions } from "./options.js";
9
10
  /**
@@ -11,8 +12,13 @@ import { getJobsOptions } from "./options.js";
11
12
  * the next period joins it instead of piling up a second run behind it.
12
13
  */
13
14
  export const SCHEDULE_DEDUP_KEY = "schedule";
14
- /** The start of the period `now` falls in. */
15
+ /**
16
+ * The slot `now` falls in: the start of its period, or the latest of the schedule's local times at or before `now`.
17
+ * Slots only move forward as `now` does, across DST changes too.
18
+ */
15
19
  export function getScheduleSlot(schedule, now) {
20
+ if ("at" in schedule)
21
+ return new Date(getLocalTimeSlot(schedule, now.getTime()));
16
22
  const periodMs = getEverySeconds(schedule) * 1000;
17
23
  return new Date(Math.floor(now.getTime() / periodMs) * periodMs);
18
24
  }
@@ -88,10 +94,40 @@ export function getSchedule(options, name) {
88
94
  return schedule;
89
95
  }
90
96
  function getEverySeconds(schedule) {
97
+ if ("at" in schedule)
98
+ return getLongestGapSeconds(schedule.at);
91
99
  const seconds = parseEverySeconds(schedule.every);
92
100
  // The options schema refused any other value at startup.
93
101
  if (seconds === null)
94
102
  throw new Error(`@softure-ai/jobs: invalid schedule period "${schedule.every}"`);
95
103
  return seconds;
96
104
  }
105
+ function getLocalTimeSlot(schedule, now) {
106
+ const today = getWallClock(now, schedule.timeZone);
107
+ // The calendar day before today's local date; Date.UTC rolls day 0 over to the month before.
108
+ const yesterday = new Date(Date.UTC(today.year, today.month - 1, today.day - 1));
109
+ const days = [
110
+ { year: yesterday.getUTCFullYear(), month: yesterday.getUTCMonth() + 1, day: yesterday.getUTCDate() },
111
+ { year: today.year, month: today.month, day: today.day },
112
+ ];
113
+ let slot = Number.NEGATIVE_INFINITY;
114
+ for (const day of days) {
115
+ for (const time of schedule.at) {
116
+ const [hour, minute] = time.split(":").map(Number);
117
+ const instant = getZonedInstant({ ...day, hour: hour ?? 0, minute: minute ?? 0 }, schedule.timeZone);
118
+ if (instant <= now && instant > slot)
119
+ slot = instant;
120
+ }
121
+ }
122
+ return slot;
123
+ }
124
+ /** The longest gap between two consecutive times of day, wrapping past midnight; `times` is sorted. */
125
+ function getLongestGapSeconds(times) {
126
+ const minutes = times.map((time) => {
127
+ const [hour, minute] = time.split(":").map(Number);
128
+ return (hour ?? 0) * 60 + (minute ?? 0);
129
+ });
130
+ const gaps = minutes.map((minute, index) => (minutes[(index + 1) % minutes.length] ?? minute) - minute + (index === minutes.length - 1 ? 24 * 60 : 0));
131
+ return Math.max(...gaps) * 60;
132
+ }
97
133
  //# sourceMappingURL=schedules.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schedules.js","sourceRoot":"","sources":["../../src/server/schedules.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schedules.js","sourceRoot":"","sources":["../../src/server/schedules.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /** A date and a time of day as a clock in some time zone shows them; `month` counts from 1. */
2
+ export interface WallClock {
3
+ readonly year: number;
4
+ readonly month: number;
5
+ readonly day: number;
6
+ readonly hour: number;
7
+ readonly minute: number;
8
+ }
9
+ /** True when `timeZone` is an IANA zone the runtime knows, such as `Europe/Warsaw` or `UTC`. */
10
+ export declare function isTimeZone(timeZone: string): boolean;
11
+ /** The wall clock of `instant` in `timeZone`, to the minute. */
12
+ export declare function getWallClock(instant: number, timeZone: string): WallClock;
13
+ /**
14
+ * The instant a clock in `timeZone` shows `wallClock`. A time that occurs twice (the hour a DST change repeats) gives
15
+ * its first occurrence; a time that does not occur (the hour a DST change skips) gives the instant the same distance
16
+ * after the change, so 02:30 on a day that jumps from 02:00 to 03:00 gives 03:30.
17
+ */
18
+ export declare function getZonedInstant(wallClock: WallClock, timeZone: string): number;
19
+ //# sourceMappingURL=zoned-time.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zoned-time.d.ts","sourceRoot":"","sources":["../src/zoned-time.ts"],"names":[],"mappings":"AAKA,+FAA+F;AAC/F,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,gGAAgG;AAChG,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAOpD;AAED,gEAAgE;AAChE,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,CASzE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAO9E"}
@@ -0,0 +1,61 @@
1
+ // Wall-clock time in an IANA time zone, with the platform's Intl data and no date library.
2
+ const HOUR_MS = 60 * 60 * 1000;
3
+ const formatters = new Map();
4
+ /** True when `timeZone` is an IANA zone the runtime knows, such as `Europe/Warsaw` or `UTC`. */
5
+ export function isTimeZone(timeZone) {
6
+ try {
7
+ getFormatter(timeZone);
8
+ return true;
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ /** The wall clock of `instant` in `timeZone`, to the minute. */
15
+ export function getWallClock(instant, timeZone) {
16
+ const parts = Object.fromEntries(getFormatter(timeZone).formatToParts(new Date(instant)).map((part) => [part.type, part.value]));
17
+ return {
18
+ year: Number(parts.year),
19
+ month: Number(parts.month),
20
+ day: Number(parts.day),
21
+ hour: Number(parts.hour),
22
+ minute: Number(parts.minute),
23
+ };
24
+ }
25
+ /**
26
+ * The instant a clock in `timeZone` shows `wallClock`. A time that occurs twice (the hour a DST change repeats) gives
27
+ * its first occurrence; a time that does not occur (the hour a DST change skips) gives the instant the same distance
28
+ * after the change, so 02:30 on a day that jumps from 02:00 to 03:00 gives 03:30.
29
+ */
30
+ export function getZonedInstant(wallClock, timeZone) {
31
+ const asUtc = toUtcMs(wallClock);
32
+ // One DST change a day at most: the offsets half a day before and after cover both sides of it.
33
+ const offsetBefore = getOffsetMs(asUtc - 12 * HOUR_MS, timeZone);
34
+ const offsetAfter = getOffsetMs(asUtc + 12 * HOUR_MS, timeZone);
35
+ const matching = [asUtc - offsetBefore, asUtc - offsetAfter].filter((instant) => toUtcMs(getWallClock(instant, timeZone)) === asUtc);
36
+ return matching.length > 0 ? Math.min(...matching) : asUtc - offsetBefore;
37
+ }
38
+ function getOffsetMs(instant, timeZone) {
39
+ const minute = Math.floor(instant / 60_000) * 60_000;
40
+ return toUtcMs(getWallClock(minute, timeZone)) - minute;
41
+ }
42
+ function toUtcMs(wallClock) {
43
+ return Date.UTC(wallClock.year, wallClock.month - 1, wallClock.day, wallClock.hour, wallClock.minute);
44
+ }
45
+ function getFormatter(timeZone) {
46
+ let formatter = formatters.get(timeZone);
47
+ if (formatter === undefined) {
48
+ formatter = new Intl.DateTimeFormat("en-US", {
49
+ timeZone,
50
+ hourCycle: "h23",
51
+ year: "numeric",
52
+ month: "numeric",
53
+ day: "numeric",
54
+ hour: "numeric",
55
+ minute: "numeric",
56
+ });
57
+ formatters.set(timeZone, formatter);
58
+ }
59
+ return formatter;
60
+ }
61
+ //# sourceMappingURL=zoned-time.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zoned-time.js","sourceRoot":"","sources":["../src/zoned-time.ts"],"names":[],"mappings":"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"}
package/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "jobs",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "dependsOn": {},
5
5
  "dbSchema": "jobs",
6
6
  "tables": ["jobs", "schedules"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softure-ai/jobs",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "SOFTURE AI jobs module: background jobs in Postgres. Enqueue inside the caller's transaction with an optional dedup key, claim with FOR UPDATE SKIP LOCKED under a lease, fenced completion, retry with exponential backoff and a terminal failed state, a worker process with graceful shutdown, and an in-process runner for tests.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/index.ts CHANGED
@@ -15,7 +15,7 @@ export const MODULE_ID = "jobs";
15
15
  export const jobs = defineModule({
16
16
  manifest: {
17
17
  id: MODULE_ID,
18
- version: "0.1.1",
18
+ version: "0.1.2",
19
19
  dependsOn: {},
20
20
  dbSchema: "jobs",
21
21
  tables: ["jobs", "schedules"],
@@ -47,6 +47,8 @@ export {
47
47
  type JobSchedulesInput,
48
48
  type JobsOptions,
49
49
  type JobsOptionsInput,
50
+ type LocalTimeScheduleOptions,
51
+ type PeriodScheduleOptions,
50
52
  } from "./options.js";
51
53
  export { defineJobHandler, failJob, retryJobLater, type JobHandler, type JobHandlerContext, type JobHandlers } from "./handlers.js";
52
54
  export { jobs as jobsTable, schedules as jobSchedulesTable } from "./schema.js";
package/src/options.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // The options an app passes to `jobs({ ... })` in softure.config.ts, parsed at startup.
2
2
  import { z } from "zod";
3
+ import { isTimeZone } from "./zoned-time.js";
3
4
 
4
5
  const QUEUE_NAME = /^[a-z][a-z0-9_.-]{0,62}$/;
5
6
  const QUEUE_NAME_HINT = "is not a queue name: lowercase letters, digits, _ . and -, starting with a letter";
@@ -30,12 +31,57 @@ const backoffSchema = z
30
31
  return seconds <= MAX_EVERY_SECONDS ? seconds : null;
31
32
  }
32
33
 
33
- const scheduleSchema = z.strictObject({
34
- /** How often the schedule runs: `15m`, `1h`, `1d`. Runs start on whole periods counted from 1970-01-01 00:00 UTC. */
35
- every: z.string().refine((every) => parseEverySeconds(every) !== null, { message: EVERY_HINT }),
36
- /** The payload of every run; default `{}`. */
37
- payload: z.record(z.string(), z.unknown()).default({}),
38
- });
34
+ const AT_TIME = /^([01][0-9]|2[0-3]):[0-5][0-9]$/;
35
+ const AT_HINT = "must be times of day as HH:MM (00:00 to 23:59), each once";
36
+ const MAX_AT_TIMES = 48;
37
+
38
+ const scheduleSchema = z
39
+ .strictObject({
40
+ /** How often the schedule runs: `15m`, `1h`, `1d`. Runs start on whole periods counted from 1970-01-01 00:00 UTC. */
41
+ every: z.string().refine((every) => parseEverySeconds(every) !== null, { message: EVERY_HINT }).optional(),
42
+ /** Instead of `every`: the local times of day the schedule runs at, `HH:MM`, in `timeZone`. */
43
+ at: z
44
+ .array(z.string().regex(AT_TIME, { message: AT_HINT }))
45
+ .min(1)
46
+ .max(MAX_AT_TIMES)
47
+ .refine((times) => new Set(times).size === times.length, { message: AT_HINT })
48
+ .optional(),
49
+ /** The IANA time zone of `at`, such as `Europe/Warsaw`; required with `at`. */
50
+ timeZone: z.string().refine(isTimeZone, { message: "is not a time zone the runtime knows (an IANA name such as Europe/Warsaw)" }).optional(),
51
+ /** The payload of every run; default `{}`. */
52
+ payload: z.record(z.string(), z.unknown()).default({}),
53
+ })
54
+ .superRefine((schedule, context) => {
55
+ if ((schedule.every === undefined) === (schedule.at === undefined)) {
56
+ context.addIssue({ code: "custom", path: ["every"], message: "set either every or at, not both" });
57
+ }
58
+ if (schedule.at !== undefined && schedule.timeZone === undefined) {
59
+ context.addIssue({ code: "custom", path: ["timeZone"], message: "is required with at" });
60
+ }
61
+ if (schedule.every !== undefined && schedule.timeZone !== undefined) {
62
+ context.addIssue({ code: "custom", path: ["timeZone"], message: "applies to at only; every runs in UTC" });
63
+ }
64
+ })
65
+ .transform((schedule): PeriodScheduleOptions | LocalTimeScheduleOptions => {
66
+ if (schedule.at !== undefined && schedule.timeZone !== undefined) {
67
+ return { at: [...schedule.at].sort(), timeZone: schedule.timeZone, payload: schedule.payload };
68
+ }
69
+ // The refinement above leaves every set whenever at is not.
70
+ return { every: schedule.every ?? "", payload: schedule.payload };
71
+ });
72
+
73
+ /** A schedule that runs once per period, aligned to UTC. */
74
+ export interface PeriodScheduleOptions {
75
+ readonly every: string;
76
+ readonly payload: Record<string, unknown>;
77
+ }
78
+
79
+ /** A schedule that runs at local times of day in a time zone; `at` is sorted. */
80
+ export interface LocalTimeScheduleOptions {
81
+ readonly at: readonly string[];
82
+ readonly timeZone: string;
83
+ readonly payload: Record<string, unknown>;
84
+ }
39
85
 
40
86
  const queueSchema = z.strictObject({
41
87
  /** Attempts before a job is `failed` for good; the first run counts. */
@@ -65,8 +111,9 @@ export const jobsOptionsSchema = z
65
111
  /** The queues, by name. A job is enqueued to one of them and runs with its settings. */
66
112
  queues: z.record(z.string(), queueSchema).default({}).superRefine(checkNames),
67
113
  /**
68
- * Recurring runs, by queue name: the worker enqueues one job per period (`every`) however many workers run. A
69
- * schedule's queue takes the default queue settings unless `queues` names it.
114
+ * Recurring runs, by queue name: the worker enqueues one job per period (`every`) or per local time of day (`at`
115
+ * in `timeZone`) however many workers run. A schedule's queue takes the default queue settings unless `queues`
116
+ * names it.
70
117
  */
71
118
  schedules: z.record(z.string(), scheduleSchema).default({}).superRefine(checkNames),
72
119
  /** How long an idle worker waits before it looks for due jobs again. */
@@ -1,9 +1,10 @@
1
- // Recurring schedules: each schedule's runs start on whole periods counted from the Unix epoch, so every worker
2
- // computes the same period. A worker takes a period with a conditional upsert of `jobs.schedules` and enqueues its run
1
+ // Recurring schedules: a period schedule's runs start on whole periods counted from the Unix epoch, a local-time
2
+ // schedule's at its listed times of day in its time zone, so every worker computes the same slot. A worker takes a period with a conditional upsert of `jobs.schedules` and enqueues its run
3
3
  // in the same transaction, so each period is enqueued once however many workers race for it.
4
4
  import { eq, inArray, lt } from "drizzle-orm";
5
- import { parseEverySeconds, type JobScheduleOptions, type JobsOptions } from "../options.js";
5
+ import { parseEverySeconds, type JobScheduleOptions, type JobsOptions, type LocalTimeScheduleOptions } from "../options.js";
6
6
  import { schedules } from "../schema.js";
7
+ import { getWallClock, getZonedInstant } from "../zoned-time.js";
7
8
  import type { JobsContext } from "./context.js";
8
9
  import { enqueueJob } from "./enqueue.js";
9
10
  import { getJobsOptions } from "./options.js";
@@ -16,6 +17,7 @@ export const SCHEDULE_DEDUP_KEY = "schedule";
16
17
 
17
18
  /** A schedule's health as `getQueueStats` reports it. */
18
19
  export interface ScheduleState {
20
+ /** The period; for a local-time schedule the longest gap between two of its times of day (24h for one time). */
19
21
  readonly everySeconds: number;
20
22
  /** The start of the latest period a run was enqueued for; null before any worker saw the schedule. */
21
23
  readonly lastEnqueuedAt: Date | null;
@@ -33,8 +35,12 @@ export interface EnqueueDueSchedulesInput {
33
35
  readonly queues?: readonly string[];
34
36
  }
35
37
 
36
- /** The start of the period `now` falls in. */
38
+ /**
39
+ * The slot `now` falls in: the start of its period, or the latest of the schedule's local times at or before `now`.
40
+ * Slots only move forward as `now` does, across DST changes too.
41
+ */
37
42
  export function getScheduleSlot(schedule: JobScheduleOptions, now: Date): Date {
43
+ if ("at" in schedule) return new Date(getLocalTimeSlot(schedule, now.getTime()));
38
44
  const periodMs = getEverySeconds(schedule) * 1000;
39
45
  return new Date(Math.floor(now.getTime() / periodMs) * periodMs);
40
46
  }
@@ -114,8 +120,38 @@ export function getSchedule(options: JobsOptions, name: string): JobScheduleOpti
114
120
  }
115
121
 
116
122
  function getEverySeconds(schedule: JobScheduleOptions): number {
123
+ if ("at" in schedule) return getLongestGapSeconds(schedule.at);
117
124
  const seconds = parseEverySeconds(schedule.every);
118
125
  // The options schema refused any other value at startup.
119
126
  if (seconds === null) throw new Error(`@softure-ai/jobs: invalid schedule period "${schedule.every}"`);
120
127
  return seconds;
121
128
  }
129
+
130
+ function getLocalTimeSlot(schedule: LocalTimeScheduleOptions, now: number): number {
131
+ const today = getWallClock(now, schedule.timeZone);
132
+ // The calendar day before today's local date; Date.UTC rolls day 0 over to the month before.
133
+ const yesterday = new Date(Date.UTC(today.year, today.month - 1, today.day - 1));
134
+ const days = [
135
+ { year: yesterday.getUTCFullYear(), month: yesterday.getUTCMonth() + 1, day: yesterday.getUTCDate() },
136
+ { year: today.year, month: today.month, day: today.day },
137
+ ];
138
+ let slot = Number.NEGATIVE_INFINITY;
139
+ for (const day of days) {
140
+ for (const time of schedule.at) {
141
+ const [hour, minute] = time.split(":").map(Number);
142
+ const instant = getZonedInstant({ ...day, hour: hour ?? 0, minute: minute ?? 0 }, schedule.timeZone);
143
+ if (instant <= now && instant > slot) slot = instant;
144
+ }
145
+ }
146
+ return slot;
147
+ }
148
+
149
+ /** The longest gap between two consecutive times of day, wrapping past midnight; `times` is sorted. */
150
+ function getLongestGapSeconds(times: readonly string[]): number {
151
+ const minutes = times.map((time) => {
152
+ const [hour, minute] = time.split(":").map(Number);
153
+ return (hour ?? 0) * 60 + (minute ?? 0);
154
+ });
155
+ const gaps = minutes.map((minute, index) => (minutes[(index + 1) % minutes.length] ?? minute) - minute + (index === minutes.length - 1 ? 24 * 60 : 0));
156
+ return Math.max(...gaps) * 60;
157
+ }
@@ -0,0 +1,75 @@
1
+ // Wall-clock time in an IANA time zone, with the platform's Intl data and no date library.
2
+
3
+ const HOUR_MS = 60 * 60 * 1000;
4
+ const formatters = new Map<string, Intl.DateTimeFormat>();
5
+
6
+ /** A date and a time of day as a clock in some time zone shows them; `month` counts from 1. */
7
+ export interface WallClock {
8
+ readonly year: number;
9
+ readonly month: number;
10
+ readonly day: number;
11
+ readonly hour: number;
12
+ readonly minute: number;
13
+ }
14
+
15
+ /** True when `timeZone` is an IANA zone the runtime knows, such as `Europe/Warsaw` or `UTC`. */
16
+ export function isTimeZone(timeZone: string): boolean {
17
+ try {
18
+ getFormatter(timeZone);
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ /** The wall clock of `instant` in `timeZone`, to the minute. */
26
+ export function getWallClock(instant: number, timeZone: string): WallClock {
27
+ const parts = Object.fromEntries(getFormatter(timeZone).formatToParts(new Date(instant)).map((part) => [part.type, part.value]));
28
+ return {
29
+ year: Number(parts.year),
30
+ month: Number(parts.month),
31
+ day: Number(parts.day),
32
+ hour: Number(parts.hour),
33
+ minute: Number(parts.minute),
34
+ };
35
+ }
36
+
37
+ /**
38
+ * The instant a clock in `timeZone` shows `wallClock`. A time that occurs twice (the hour a DST change repeats) gives
39
+ * its first occurrence; a time that does not occur (the hour a DST change skips) gives the instant the same distance
40
+ * after the change, so 02:30 on a day that jumps from 02:00 to 03:00 gives 03:30.
41
+ */
42
+ export function getZonedInstant(wallClock: WallClock, timeZone: string): number {
43
+ const asUtc = toUtcMs(wallClock);
44
+ // One DST change a day at most: the offsets half a day before and after cover both sides of it.
45
+ const offsetBefore = getOffsetMs(asUtc - 12 * HOUR_MS, timeZone);
46
+ const offsetAfter = getOffsetMs(asUtc + 12 * HOUR_MS, timeZone);
47
+ const matching = [asUtc - offsetBefore, asUtc - offsetAfter].filter((instant) => toUtcMs(getWallClock(instant, timeZone)) === asUtc);
48
+ return matching.length > 0 ? Math.min(...matching) : asUtc - offsetBefore;
49
+ }
50
+
51
+ function getOffsetMs(instant: number, timeZone: string): number {
52
+ const minute = Math.floor(instant / 60_000) * 60_000;
53
+ return toUtcMs(getWallClock(minute, timeZone)) - minute;
54
+ }
55
+
56
+ function toUtcMs(wallClock: WallClock): number {
57
+ return Date.UTC(wallClock.year, wallClock.month - 1, wallClock.day, wallClock.hour, wallClock.minute);
58
+ }
59
+
60
+ function getFormatter(timeZone: string): Intl.DateTimeFormat {
61
+ let formatter = formatters.get(timeZone);
62
+ if (formatter === undefined) {
63
+ formatter = new Intl.DateTimeFormat("en-US", {
64
+ timeZone,
65
+ hourCycle: "h23",
66
+ year: "numeric",
67
+ month: "numeric",
68
+ day: "numeric",
69
+ hour: "numeric",
70
+ minute: "numeric",
71
+ });
72
+ formatters.set(timeZone, formatter);
73
+ }
74
+ return formatter;
75
+ }