@gobing-ai/ts-infra 0.4.49 → 0.4.51

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 CHANGED
@@ -331,10 +331,24 @@ export default {
331
331
  };
332
332
  ```
333
333
 
334
- **Cron support is deliberately minimal (task 0060 F7):** a positive millisecond number
335
- (`'60000'`), `'* * * * *'`, or `'*/N * * * *'` (N > 0). Any other expression — including a
336
- full 5-field cron like `'0 3 * * *'` makes `register()` throw a `RangeError`; it never
337
- silently falls back to a 60s interval.
334
+ **Cron support (task 0734).** Three legacy cadences are preserved as interval forms measured from
335
+ adapter start: a positive millisecond number (`'60000'`), `'* * * * *'` (60s), and `'*/N * * * *'`
336
+ (N > 0). Every other valid five-field expression is real cron, evaluated in local wall-clock time
337
+ and self-rescheduled with `setTimeout` so ticks never overlap and missed occurrences are skipped.
338
+ Supported fields: minute `0-59`, hour `0-23`, day-of-month `1-31`, month `1-12`, day-of-week `0-7`
339
+ (`0` and `7` are Sunday); each accepts `*`, a step form, a comma-separated list of numbers or
340
+ inclusive non-wrapping ranges, or a single number. When both day fields are restricted, standard
341
+ cron OR semantics apply. Wrong field counts, out-of-range values, descending ranges, zero steps,
342
+ empty list members, names/macros, and unsupported operators throw a `RangeError` at registration —
343
+ nothing silently falls back.
344
+
345
+ **Declarative jobs.** `runNodeApplication` (see `./application-node`) validates
346
+ `bootstrap.scheduler.jobs`, an array of `SchedulerJobConfig` items — each a non-empty `name` and
347
+ `command` plus exactly one of an integer `intervalMinutes` (`1..35791`) or a cron string using the
348
+ grammar above. Jobs are validated and normalized whether or not the scheduler is enabled; a disabled
349
+ scheduler retains validated definitions but creates no adapter and runs nothing. ts-infra treats
350
+ `command` as declarative data and never executes it — the consuming application binds it to its own
351
+ handler.
338
352
 
339
353
  ### API Client — typed HTTP with tracing
340
354
 
@@ -55,6 +55,7 @@ import type { ApplicationBootstrapOptions, ApplicationRuntime } from './types';
55
55
  export declare function runApplication<TAppConfig = unknown, TEvents extends EventMap = InfraEvents>(options: ApplicationBootstrapOptions<TAppConfig, TEvents>): Promise<ApplicationRuntime<TAppConfig, TEvents>>;
56
56
  export type { BusLifecycleEvents, EventMap } from '../event-bus/types';
57
57
  export type { InfraEvents } from '../events';
58
+ export type { SchedulerJobConfig } from '../scheduler/types';
58
59
  export type { PluginHost } from './plugins/host';
59
60
  export type { Plugin, PluginSummary } from './plugins/types';
60
61
  export type { ApplicationBootstrapConfig, ApplicationBootstrapOptions, ApplicationConfigLoader, ApplicationConfigValidator, ApplicationRuntime, ApplicationServices, ApplicationStopReason, ConfigValidationResult, DbAdapterLike, EventsOptions, LoggingOptions, SchedulerOptions, TelemetryOptions, } from './types';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/application/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,KAAK,EAAsB,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAM7C,OAAO,KAAK,EAER,2BAA2B,EAC3B,kBAAkB,EAGrB,MAAM,SAAS,CAAC;AAiCjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAsB,cAAc,CAAC,UAAU,GAAG,OAAO,EAAE,OAAO,SAAS,QAAQ,GAAG,WAAW,EAC7F,OAAO,EAAE,2BAA2B,CAAC,UAAU,EAAE,OAAO,CAAC,GAC1D,OAAO,CAAC,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CA0JlD;AAED,YAAY,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACvE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,YAAY,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAE7D,YAAY,EACR,0BAA0B,EAC1B,2BAA2B,EAC3B,uBAAuB,EACvB,0BAA0B,EAC1B,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,sBAAsB,EACtB,aAAa,EACb,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,gBAAgB,GACnB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/application/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,OAAO,KAAK,EAAsB,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAM7C,OAAO,KAAK,EAER,2BAA2B,EAC3B,kBAAkB,EAGrB,MAAM,SAAS,CAAC;AAiCjB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,wBAAsB,cAAc,CAAC,UAAU,GAAG,OAAO,EAAE,OAAO,SAAS,QAAQ,GAAG,WAAW,EAC7F,OAAO,EAAE,2BAA2B,CAAC,UAAU,EAAE,OAAO,CAAC,GAC1D,OAAO,CAAC,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CA2JlD;AAED,YAAY,EAAE,kBAAkB,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACvE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,YAAY,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjD,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAE7D,YAAY,EACR,0BAA0B,EAC1B,2BAA2B,EAC3B,uBAAuB,EACvB,0BAA0B,EAC1B,kBAAkB,EAClB,mBAAmB,EACnB,qBAAqB,EACrB,sBAAsB,EACtB,aAAa,EACb,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,gBAAgB,GACnB,MAAM,SAAS,CAAC"}
@@ -94,6 +94,7 @@ export async function runApplication(options) {
94
94
  const schedulerConfig = {
95
95
  enabled: schedOpts?.enabled ?? false,
96
96
  autoStart: schedOpts?.autoStart ?? true,
97
+ jobs: schedOpts?.jobs ?? [],
97
98
  };
98
99
  const eventsEnabled = options.config?.events?.enabled ?? true;
99
100
  const eventsLifecycle = options.config?.events?.lifecycle ?? true;
@@ -12,7 +12,7 @@ import type { FileObserverWriter } from '../event-bus/file-observer';
12
12
  import type { BusLifecycleEvents, EventMap } from '../event-bus/types';
13
13
  import type { InfraEvents } from '../events';
14
14
  import type { Logger, LogLevel } from '../logger';
15
- import type { SchedulerAdapter } from '../scheduler/types';
15
+ import type { SchedulerAdapter, SchedulerJobConfig } from '../scheduler/types';
16
16
  import type { PluginHost } from './plugins/host';
17
17
  import type { Plugin } from './plugins/types';
18
18
  /** Logging feature flags. */
@@ -77,6 +77,13 @@ export interface SchedulerOptions {
77
77
  entries?: Array<[string, () => Promise<void>]>;
78
78
  /** Start scheduler immediately after registration. Default `true` when enabled. */
79
79
  autoStart?: boolean;
80
+ /**
81
+ * Declarative scheduler jobs (task 0734). Validated and normalized by the
82
+ * Node subpath (`runNodeApplication`) before the user `start` callback;
83
+ * forwarded as data into the resolved config. ts-infra never executes the
84
+ * job `command` itself.
85
+ */
86
+ jobs?: readonly SchedulerJobConfig[];
80
87
  }
81
88
  /**
82
89
  * Fully-resolved bootstrap config (all optionals filled with defaults).
@@ -104,6 +111,7 @@ export interface ApplicationBootstrapConfig {
104
111
  readonly scheduler: {
105
112
  enabled: boolean;
106
113
  autoStart: boolean;
114
+ jobs: readonly SchedulerJobConfig[];
107
115
  };
108
116
  }
109
117
  /** Services that may be pre-injected instead of created by the bootstrap. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/application/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/application/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"application-node.d.ts","sourceRoot":"","sources":["../src/application-node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAWH,OAAO,KAAK,EACR,2BAA2B,EAC3B,uBAAuB,EAEvB,kBAAkB,EAClB,qBAAqB,EAGrB,QAAQ,EACR,aAAa,EACb,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EACnB,MAAM,qBAAqB,CAAC;AAM7B,kGAAkG;AAClG,qBAAa,qBAAsB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI9B;AAgHD;;;;GAIG;AACH,MAAM,WAAW,6BAA8B,SAAQ,gBAAgB;IACnE,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED,uEAAuE;AACvE,MAAM,WAAW,sBAAsB,CAAC,UAAU,GAAG,OAAO,EAAE,OAAO,SAAS,QAAQ,GAAG,WAAW;IAChG,gEAAgE;IAChE,QAAQ,CAAC,YAAY,CAAC,EAAE,uBAAuB,CAAC,UAAU,CAAC,CAAC;IAC5D,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,CAAC,EAAE;QACd,OAAO,CAAC,EAAE,cAAc,CAAC;QACzB,MAAM,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;QAChC,SAAS,CAAC,EAAE,6BAA6B,CAAC;QAC1C,SAAS,CAAC,EAAE,gBAAgB,CAAC;KAChC,CAAC;IACF,oCAAoC;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,2BAA2B,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC;IACjF,wCAAwC;IACxC,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvF,wDAAwD;IACxD,QAAQ,CAAC,IAAI,CAAC,EAAE,CACZ,GAAG,EAAE,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,EAC5C,MAAM,EAAE,qBAAqB,KAC5B,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC7B;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,kBAAkB,CAAC,UAAU,GAAG,OAAO,EAAE,OAAO,SAAS,QAAQ,GAAG,WAAW,EACjG,OAAO,EAAE,sBAAsB,CAAC,UAAU,EAAE,OAAO,CAAC,GACrD,OAAO,CAAC,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAuIlD"}
1
+ {"version":3,"file":"application-node.d.ts","sourceRoot":"","sources":["../src/application-node.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAWH,OAAO,KAAK,EACR,2BAA2B,EAC3B,uBAAuB,EAEvB,kBAAkB,EAClB,qBAAqB,EAGrB,QAAQ,EACR,aAAa,EACb,WAAW,EACX,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EACnB,MAAM,qBAAqB,CAAC;AAQ7B,kGAAkG;AAClG,qBAAa,qBAAsB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI9B;AAyMD;;;;GAIG;AACH,MAAM,WAAW,6BAA8B,SAAQ,gBAAgB;IACnE,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED,uEAAuE;AACvE,MAAM,WAAW,sBAAsB,CAAC,UAAU,GAAG,OAAO,EAAE,OAAO,SAAS,QAAQ,GAAG,WAAW;IAChG,gEAAgE;IAChE,QAAQ,CAAC,YAAY,CAAC,EAAE,uBAAuB,CAAC,UAAU,CAAC,CAAC;IAC5D,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,CAAC,EAAE;QACd,OAAO,CAAC,EAAE,cAAc,CAAC;QACzB,MAAM,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;QAChC,SAAS,CAAC,EAAE,6BAA6B,CAAC;QAC1C,SAAS,CAAC,EAAE,gBAAgB,CAAC;KAChC,CAAC;IACF,oCAAoC;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,2BAA2B,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC;IACjF,wCAAwC;IACxC,QAAQ,CAAC,KAAK,EAAE,CAAC,GAAG,EAAE,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACvF,wDAAwD;IACxD,QAAQ,CAAC,IAAI,CAAC,EAAE,CACZ,GAAG,EAAE,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,EAC5C,MAAM,EAAE,qBAAqB,KAC5B,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAC7B;AAID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAsB,kBAAkB,CAAC,UAAU,GAAG,OAAO,EAAE,OAAO,SAAS,QAAQ,GAAG,WAAW,EACjG,OAAO,EAAE,sBAAsB,CAAC,UAAU,EAAE,OAAO,CAAC,GACrD,OAAO,CAAC,kBAAkB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CA6IlD"}
@@ -20,6 +20,7 @@ import { createDbAdapter } from '@gobing-ai/ts-db';
20
20
  import { createNodeFileSystem, interpolateTree, parseYamlObject } from '@gobing-ai/ts-runtime';
21
21
  import { runApplication } from './application/index.js';
22
22
  import { dbPlugin } from './application/plugins/builtins.js';
23
+ import { parseCronExpression } from './scheduler/cron.js';
23
24
  import { NodeSchedulerAdapter } from './scheduler-node.js';
24
25
  import { initNodeTelemetry, shutdownNodeTelemetry } from './telemetry/otel-node.js';
25
26
  // ── Errors ────────────────────────────────────────────────────────────────
@@ -30,6 +31,8 @@ export class ConfigValidationError extends Error {
30
31
  this.name = 'ConfigValidationError';
31
32
  }
32
33
  }
34
+ /** Platform timer maximum for `setTimeout` (2^31 - 1 ms). */
35
+ const MAX_TIMEOUT_MS = 2_147_483_647;
33
36
  // ── Config validation helper ──────────────────────────────────────────────
34
37
  /**
35
38
  * Validate a raw config section using the caller-provided validator.
@@ -57,6 +60,78 @@ function validateAppConfig(validator, raw, section, filePath) {
57
60
  }
58
61
  throw new ConfigValidationError(`Unsupported validator shape for section "${section}"`);
59
62
  }
63
+ // ── Scheduler job validation (task 0734) ────────────────────────────────────
64
+ /** Max `intervalMinutes` so that `intervalMinutes * 60_000` fits in the platform timer maximum. */
65
+ const MAX_INTERVAL_MINUTES = Math.floor(MAX_TIMEOUT_MS / 60_000);
66
+ /**
67
+ * Validate and normalize a raw `bootstrap.scheduler.jobs` array.
68
+ *
69
+ * Accepts only object entries, trims `name`/`command`/`cron`, enforces the
70
+ * schedule XOR and `intervalMinutes` bounds, validates cron through the shared
71
+ * internal parser, and rejects duplicate post-trim names case-sensitively.
72
+ * Runs whether the scheduler is enabled or disabled; a disabled scheduler may
73
+ * retain validated job definitions but creates no adapter (R4).
74
+ *
75
+ * Throws `ConfigValidationError` with a `bootstrap.scheduler.jobs.<index>.<field>`
76
+ * path so a bad job aborts startup before the user `start` callback.
77
+ */
78
+ function normalizeSchedulerJobs(raw) {
79
+ if (raw === undefined || raw === null)
80
+ return [];
81
+ if (!Array.isArray(raw)) {
82
+ throw new ConfigValidationError('bootstrap.scheduler.jobs must be an array');
83
+ }
84
+ const seen = new Set();
85
+ const jobs = [];
86
+ raw.forEach((item, index) => {
87
+ if (typeof item !== 'object' || item === null || Array.isArray(item)) {
88
+ throw new ConfigValidationError(`bootstrap.scheduler.jobs.${index} must be an object`);
89
+ }
90
+ const entry = item;
91
+ const name = typeof entry.name === 'string' ? entry.name.trim() : '';
92
+ const command = typeof entry.command === 'string' ? entry.command.trim() : '';
93
+ const cron = typeof entry.cron === 'string' ? entry.cron.trim() : '';
94
+ const interval = entry.intervalMinutes;
95
+ if (name === '') {
96
+ throw new ConfigValidationError(`bootstrap.scheduler.jobs.${index}.name must be a non-empty string`);
97
+ }
98
+ if (command === '') {
99
+ throw new ConfigValidationError(`bootstrap.scheduler.jobs.${index}.command must be a non-empty string`);
100
+ }
101
+ const hasCron = entry.cron !== undefined && entry.cron !== null;
102
+ const hasInterval = interval !== undefined && interval !== null;
103
+ if (hasCron === hasInterval) {
104
+ throw new ConfigValidationError(`bootstrap.scheduler.jobs.${index} must have exactly one of intervalMinutes or cron`);
105
+ }
106
+ if (seen.has(name)) {
107
+ throw new ConfigValidationError(`bootstrap.scheduler.jobs.${index}.name duplicates the name of an earlier job: "${name}"`);
108
+ }
109
+ seen.add(name);
110
+ if (hasInterval) {
111
+ if (typeof interval !== 'number' ||
112
+ !Number.isInteger(interval) ||
113
+ interval < 1 ||
114
+ interval > MAX_INTERVAL_MINUTES) {
115
+ throw new ConfigValidationError(`bootstrap.scheduler.jobs.${index}.intervalMinutes must be an integer in 1..${MAX_INTERVAL_MINUTES}`);
116
+ }
117
+ jobs.push({ name, command, intervalMinutes: interval });
118
+ }
119
+ else {
120
+ if (cron === '') {
121
+ throw new ConfigValidationError(`bootstrap.scheduler.jobs.${index}.cron must be a non-empty string`);
122
+ }
123
+ try {
124
+ parseCronExpression(cron);
125
+ }
126
+ catch (error) {
127
+ const detail = error instanceof Error ? error.message : String(error);
128
+ throw new ConfigValidationError(`bootstrap.scheduler.jobs.${index}.cron: ${detail}`);
129
+ }
130
+ jobs.push({ name, command, cron });
131
+ }
132
+ });
133
+ return jobs;
134
+ }
60
135
  // ── File sink helper ──────────────────────────────────────────────────────
61
136
  function createFileSink(filePath) {
62
137
  return (line) => {
@@ -168,16 +243,22 @@ export async function runNodeApplication(options) {
168
243
  const eventsOpts = { ...yamlEvents, ...options.config?.events };
169
244
  const logFilePath = yamlBootstrap.logging?.filePath;
170
245
  const loggingConfig = typeof logFilePath === 'string' ? { ...loggingOpts, fileSink: createFileSink(logFilePath) } : loggingOpts;
171
- // ── Scheduler adapter ───────────────────────────────────────────────
246
+ // ── Scheduler adapter + jobs ────────────────────────────────────────
172
247
  // Honour a caller-supplied `SchedulerOptions.adapter` (documented injection
173
248
  // point, application/types.ts:82) instead of unconditionally overwriting
174
249
  // it. Only default-construct a NodeSchedulerAdapter when none was provided.
175
250
  // `drainTimeoutMs` (ADR-024) is reachable by passing a pre-built adapter;
176
251
  // the auto-wired default keeps the 30000 ms bound (CHANGELOG.md:16).
177
- const schedulerConfig = {};
178
- if (schedulerOpts.enabled === true) {
179
- schedulerConfig.enabled = true;
180
- schedulerConfig.autoStart = schedulerOpts.autoStart;
252
+ //
253
+ // Declarative jobs (task 0734) are validated and normalized whether or not
254
+ // the scheduler is enabled a disabled scheduler may retain validated job
255
+ // definitions but creates no adapter and runs nothing (R4).
256
+ const schedulerConfig = {
257
+ enabled: schedulerOpts.enabled === true,
258
+ autoStart: schedulerOpts.autoStart,
259
+ jobs: normalizeSchedulerJobs(schedulerOpts.jobs),
260
+ };
261
+ if (schedulerConfig.enabled) {
181
262
  schedulerConfig.adapter = schedulerOpts.adapter ?? new NodeSchedulerAdapter();
182
263
  if (schedulerOpts.entries) {
183
264
  schedulerConfig.entries = schedulerOpts.entries;
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Internal cron expression grammar + matching (task 0734).
3
+ *
4
+ * This module is deliberately NOT exported from package.json — it is a shared
5
+ * internal seam between `application-node.ts` (SchedulerJobConfig validation)
6
+ * and `scheduler/node.ts` (adapter matching), so both consume the same grammar.
7
+ * The public surface remains `NodeSchedulerAdapter` and `SchedulerJobConfig`.
8
+ *
9
+ * Supported grammar (five fields, whitespace-separated):
10
+ * minute 0-59
11
+ * hour 0-23
12
+ * day-of-month 1-31
13
+ * month 1-12
14
+ * day-of-week 0-7 (0 and 7 are both Sunday)
15
+ *
16
+ * Each field accepts a wildcard, a step-N wildcard form, a comma-separated list
17
+ * of numbers or inclusive non-wrapping ranges, or a single number. Names/macros
18
+ * (`MON`, `JAN`, `@daily`) and operators (`?`, `L`, `W`, `#`) are rejected.
19
+ *
20
+ * Matching uses local wall-clock fields (ADR / task 0734 R2): the fall-back hour
21
+ * can fire twice at two distinct instants; a nonexistent spring-forward minute
22
+ * never fires. No per-job timezone.
23
+ */
24
+ /** A single cron field: either a wildcard (whole range) or an explicit value set. */
25
+ export interface CronFieldSpec {
26
+ /** True when the whole field is `*` (matches every value in its range). */
27
+ readonly wildcard: boolean;
28
+ /** Allowed values when restricted. Day-of-week 7 is normalized into 0. */
29
+ readonly values: ReadonlySet<number>;
30
+ }
31
+ /** A parsed five-field cron expression. */
32
+ export interface CronExpression {
33
+ readonly minute: CronFieldSpec;
34
+ readonly hour: CronFieldSpec;
35
+ readonly dayOfMonth: CronFieldSpec;
36
+ readonly month: CronFieldSpec;
37
+ readonly dayOfWeek: CronFieldSpec;
38
+ /** The original expression, for error messages and metrics. */
39
+ readonly source: string;
40
+ }
41
+ /**
42
+ * Parse a five-field cron expression into its spec.
43
+ *
44
+ * Throws `RangeError` on a wrong field count, any unsupported field grammar, or
45
+ * out-of-range values. Day-of-week `7` normalizes to `0` (Sunday).
46
+ */
47
+ export declare function parseCronExpression(source: string): CronExpression;
48
+ /**
49
+ * Whether the instant matches the expression (local wall-clock fields).
50
+ *
51
+ * Day semantics follow standard cron: when both day fields are wildcards both
52
+ * pass; when one is a wildcard the restricted field must match; when both are
53
+ * restricted, either may match (OR).
54
+ */
55
+ export declare function matchesCron(date: Date, expr: CronExpression): boolean;
56
+ /**
57
+ * Compute the next matching minute strictly after `nowMs`, in local time.
58
+ *
59
+ * Starts at the next epoch-minute boundary and scans forward in one-minute
60
+ * steps (fixed epoch increments, local wall-clock fields read per instant), so
61
+ * DST fall-back selects both distinct instants and spring-forward skips
62
+ * nonexistent minutes. Bounded to eight calendar years so an unsatisfiable
63
+ * expression fails here (at registration) rather than silently never firing.
64
+ *
65
+ * # ponytail: O(minutes-to-next) forward scan, fine for small job counts; a
66
+ * field-jumping search would matter only if startup cost with hundreds of jobs
67
+ * ever shows up in measurements.
68
+ */
69
+ export declare function nextCronTime(expr: CronExpression, nowMs: number): Date;
70
+ //# sourceMappingURL=cron.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["../../src/scheduler/cron.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,qFAAqF;AACrF,MAAM,WAAW,aAAa;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACxC;AAED,2CAA2C;AAC3C,MAAM,WAAW,cAAc;IAC3B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAClC,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CAC3B;AA+ED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc,CAwBlE;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAgBrE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAYtE"}
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Internal cron expression grammar + matching (task 0734).
3
+ *
4
+ * This module is deliberately NOT exported from package.json — it is a shared
5
+ * internal seam between `application-node.ts` (SchedulerJobConfig validation)
6
+ * and `scheduler/node.ts` (adapter matching), so both consume the same grammar.
7
+ * The public surface remains `NodeSchedulerAdapter` and `SchedulerJobConfig`.
8
+ *
9
+ * Supported grammar (five fields, whitespace-separated):
10
+ * minute 0-59
11
+ * hour 0-23
12
+ * day-of-month 1-31
13
+ * month 1-12
14
+ * day-of-week 0-7 (0 and 7 are both Sunday)
15
+ *
16
+ * Each field accepts a wildcard, a step-N wildcard form, a comma-separated list
17
+ * of numbers or inclusive non-wrapping ranges, or a single number. Names/macros
18
+ * (`MON`, `JAN`, `@daily`) and operators (`?`, `L`, `W`, `#`) are rejected.
19
+ *
20
+ * Matching uses local wall-clock fields (ADR / task 0734 R2): the fall-back hour
21
+ * can fire twice at two distinct instants; a nonexistent spring-forward minute
22
+ * never fires. No per-job timezone.
23
+ */
24
+ /** Upper bound on the forward minute scan (8 calendar years, upper-capped at 366 days). */
25
+ const MAX_SCAN_MINUTES = 8 * 366 * 24 * 60;
26
+ const DAYS_OF_WEEK = 7; // 0-7 accepted on input, 7 aliases to 0
27
+ /**
28
+ * Parse a single cron field into its wildcard/values spec.
29
+ *
30
+ * Throws `RangeError` for anything outside the supported grammar: names/macros,
31
+ * operators, zero steps, empty list members, descending ranges, non-integer
32
+ * values, or out-of-range values. Nothing silently falls back (task 0060 F7).
33
+ */
34
+ function parseField(raw, min, max) {
35
+ const trimmed = raw.trim();
36
+ if (trimmed === '*') {
37
+ return { wildcard: true, values: new Set() };
38
+ }
39
+ // Names ('MON', 'JAN'), macros ('@daily'), and '?' are unsupported operators.
40
+ if (/^[A-Za-z@?]/.test(trimmed)) {
41
+ throw new RangeError(`unsupported cron field "${raw}": names/macros are not supported`);
42
+ }
43
+ // Whole-field step form: */N
44
+ const stepMatch = trimmed.match(/^\*\/(\d+)$/);
45
+ if (stepMatch) {
46
+ const step = Number(stepMatch[1]);
47
+ if (step <= 0) {
48
+ throw new RangeError(`unsupported cron field "${raw}": step must be a positive integer`);
49
+ }
50
+ const values = new Set();
51
+ for (let v = min; v <= max; v += step) {
52
+ values.add(v);
53
+ }
54
+ return { wildcard: false, values };
55
+ }
56
+ // Comma-separated list of numbers or inclusive non-wrapping ranges.
57
+ const members = trimmed.split(',');
58
+ if (members.some((m) => m.trim() === '')) {
59
+ throw new RangeError(`unsupported cron field "${raw}": empty list member`);
60
+ }
61
+ const values = new Set();
62
+ for (const rawMember of members) {
63
+ const member = rawMember.trim();
64
+ if (member.includes('-')) {
65
+ const rangeParts = member.split('-');
66
+ if (rangeParts.length !== 2 || rangeParts.some((p) => p === '' || !/^\d+$/.test(p))) {
67
+ throw new RangeError(`unsupported cron field "${raw}": invalid range "${rawMember}"`);
68
+ }
69
+ const lo = Number(rangeParts[0]);
70
+ const hi = Number(rangeParts[1]);
71
+ if (lo > hi) {
72
+ throw new RangeError(`unsupported cron field "${raw}": descending range "${rawMember}"`);
73
+ }
74
+ if (lo < min || hi > max) {
75
+ throw new RangeError(`cron field "${raw}" value out of range [${min}-${max}] in range "${rawMember}"`);
76
+ }
77
+ for (let v = lo; v <= hi; v++) {
78
+ values.add(v);
79
+ }
80
+ }
81
+ else {
82
+ if (!/^\d+$/.test(member)) {
83
+ throw new RangeError(`unsupported cron field "${raw}": invalid value "${rawMember}"`);
84
+ }
85
+ const v = Number(member);
86
+ if (v < min || v > max) {
87
+ throw new RangeError(`cron field "${raw}" value out of range [${min}-${max}]: ${rawMember}`);
88
+ }
89
+ values.add(v);
90
+ }
91
+ }
92
+ return { wildcard: false, values };
93
+ }
94
+ /**
95
+ * Parse a five-field cron expression into its spec.
96
+ *
97
+ * Throws `RangeError` on a wrong field count, any unsupported field grammar, or
98
+ * out-of-range values. Day-of-week `7` normalizes to `0` (Sunday).
99
+ */
100
+ export function parseCronExpression(source) {
101
+ const trimmed = source.trim();
102
+ const parts = trimmed.split(/\s+/);
103
+ if (parts.length !== 5) {
104
+ throw new RangeError(`unsupported cron expression "${source}": expected exactly 5 fields, got ${parts.length}`);
105
+ }
106
+ const [rawMinute, rawHour, rawDom, rawMonth, rawDow] = parts;
107
+ const minute = parseField(rawMinute, 0, 59);
108
+ const hour = parseField(rawHour, 0, 23);
109
+ const dayOfMonth = parseField(rawDom, 1, 31);
110
+ const month = parseField(rawMonth, 1, 12);
111
+ let dayOfWeek = parseField(rawDow, 0, DAYS_OF_WEEK);
112
+ // Standard cron: 7 is an alias for 0 (Sunday).
113
+ if (!dayOfWeek.wildcard && dayOfWeek.values.has(DAYS_OF_WEEK)) {
114
+ const values = new Set(dayOfWeek.values);
115
+ values.delete(DAYS_OF_WEEK);
116
+ values.add(0);
117
+ dayOfWeek = { wildcard: false, values };
118
+ }
119
+ return { minute, hour, dayOfMonth, month, dayOfWeek, source: trimmed };
120
+ }
121
+ /**
122
+ * Whether the instant matches the expression (local wall-clock fields).
123
+ *
124
+ * Day semantics follow standard cron: when both day fields are wildcards both
125
+ * pass; when one is a wildcard the restricted field must match; when both are
126
+ * restricted, either may match (OR).
127
+ */
128
+ export function matchesCron(date, expr) {
129
+ const minute = date.getMinutes();
130
+ if (!expr.minute.wildcard && !expr.minute.values.has(minute))
131
+ return false;
132
+ const hour = date.getHours();
133
+ if (!expr.hour.wildcard && !expr.hour.values.has(hour))
134
+ return false;
135
+ const month = date.getMonth() + 1;
136
+ if (!expr.month.wildcard && !expr.month.values.has(month))
137
+ return false;
138
+ const domWild = expr.dayOfMonth.wildcard;
139
+ const dowWild = expr.dayOfWeek.wildcard;
140
+ if (domWild && dowWild)
141
+ return true;
142
+ const domMatch = domWild || expr.dayOfMonth.values.has(date.getDate());
143
+ const dowMatch = dowWild || expr.dayOfWeek.values.has(date.getDay());
144
+ if (domWild)
145
+ return dowMatch;
146
+ if (dowWild)
147
+ return domMatch;
148
+ return domMatch || dowMatch;
149
+ }
150
+ /**
151
+ * Compute the next matching minute strictly after `nowMs`, in local time.
152
+ *
153
+ * Starts at the next epoch-minute boundary and scans forward in one-minute
154
+ * steps (fixed epoch increments, local wall-clock fields read per instant), so
155
+ * DST fall-back selects both distinct instants and spring-forward skips
156
+ * nonexistent minutes. Bounded to eight calendar years so an unsatisfiable
157
+ * expression fails here (at registration) rather than silently never firing.
158
+ *
159
+ * # ponytail: O(minutes-to-next) forward scan, fine for small job counts; a
160
+ * field-jumping search would matter only if startup cost with hundreds of jobs
161
+ * ever shows up in measurements.
162
+ */
163
+ export function nextCronTime(expr, nowMs) {
164
+ // Next epoch-minute boundary strictly after now (never the current minute).
165
+ let candidate = Math.floor(nowMs / 60_000) * 60_000 + 60_000;
166
+ const limit = candidate + MAX_SCAN_MINUTES * 60_000;
167
+ while (candidate < limit) {
168
+ const instant = new Date(candidate);
169
+ if (matchesCron(instant, expr)) {
170
+ return instant;
171
+ }
172
+ candidate += 60_000;
173
+ }
174
+ throw new RangeError(`unsupported cron expression "${expr.source}": no matching time within 8 years`);
175
+ }
@@ -1,6 +1,6 @@
1
1
  export { ActionRegistry, type CreateDefaultRegistryOptions, createDefaultRegistry, HealthPingAction, type HealthPingWriter, LogAction, QueueStatsAction, type QueueStatsDaoProvider, type SchedulerAction, toScheduledAction, } from './action';
2
2
  export { initScheduler } from './factory';
3
3
  export { NoopSchedulerAdapter } from './noop';
4
- export type { ScheduledAction, SchedulerAdapter } from './types';
4
+ export type { ScheduledAction, SchedulerAdapter, SchedulerJobConfig } from './types';
5
5
  export { wrapScheduledHandler } from './wrap-handler';
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scheduler/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,cAAc,EACd,KAAK,4BAA4B,EACjC,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,SAAS,EACT,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,iBAAiB,GACpB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,QAAQ,CAAC;AAC9C,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scheduler/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,cAAc,EACd,KAAK,4BAA4B,EACjC,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,gBAAgB,EACrB,SAAS,EACT,gBAAgB,EAChB,KAAK,qBAAqB,EAC1B,KAAK,eAAe,EACpB,iBAAiB,GACpB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,QAAQ,CAAC;AAC9C,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AACrF,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC"}
@@ -1,6 +1,9 @@
1
1
  /**
2
- * Node.js scheduler adapter using a simple setInterval-based approach.
3
- * No external cron library dependency cron expressions are parsed minimally.
2
+ * Node.js scheduler adapter interval-based for the three legacy cadence
3
+ * forms, self-rescheduling setTimeout for real five-field cron (task 0734).
4
+ *
5
+ * No external cron library dependency — cron expressions are parsed by the
6
+ * internal `scheduler/cron.ts` grammar shared with `application-node.ts`.
4
7
  */
5
8
  import type { ScheduledAction, SchedulerAdapter } from './types';
6
9
  /** Constructor options for {@link NodeSchedulerAdapter}. */
@@ -11,10 +14,16 @@ export interface NodeSchedulerAdapterConfig {
11
14
  * Defaults to 30000 (matching `DBQueueConsumer`).
12
15
  */
13
16
  readonly drainTimeoutMs?: number;
17
+ /**
18
+ * Deterministic clock seam (task 0734 R2). Returns epoch milliseconds;
19
+ * defaults to `Date.now`. Used for cron target computation and re-checks.
20
+ */
21
+ readonly now?: () => number;
14
22
  }
15
23
  /**
16
- * Scheduler adapter for Node.js using a setInterval-based approach.
17
- * No external cron library dependency cron expressions are parsed minimally.
24
+ * Scheduler adapter for Node.js. Interval entries use `setInterval`; real cron
25
+ * entries self-reschedule with `setTimeout` so ticks never overlap and
26
+ * occurrences missed while a tick runs are skipped (task 0734 R2).
18
27
  *
19
28
  * `stop()` drains in-flight ticks, bounded by `drainTimeoutMs` (ADR-024): an
20
29
  * action already running when `stop()` is called is awaited rather than torn
@@ -23,13 +32,28 @@ export interface NodeSchedulerAdapterConfig {
23
32
  export declare class NodeSchedulerAdapter implements SchedulerAdapter {
24
33
  private readonly entries;
25
34
  private readonly drainTimeoutMs;
35
+ private readonly now;
26
36
  private running;
27
37
  private readonly inflight;
28
38
  constructor(config?: NodeSchedulerAdapterConfig);
29
39
  register(cron: string, action: ScheduledAction): void;
40
+ private parseEntry;
30
41
  start(): Promise<void>;
31
42
  stop(): Promise<void>;
32
43
  private startEntry;
44
+ /**
45
+ * Arm the cron entry's next `setTimeout`, chunking any delay beyond the
46
+ * platform timer maximum. On a chunk wake the wall clock is re-checked
47
+ * before firing, so an injected/real clock shift never fires early.
48
+ */
49
+ private armCron;
50
+ /**
51
+ * Fire a cron tick: re-check the wall clock against the target, run the
52
+ * action once, then recompute the next occurrence strictly from `now()` and
53
+ * re-arm only while still running. Ticks never overlap and occurrences
54
+ * missed while a tick runs are skipped (task 0734 R2).
55
+ */
56
+ private fireCron;
33
57
  private _onScheduledTick;
34
58
  }
35
59
  //# sourceMappingURL=node.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/scheduler/node.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AA2CjE,4DAA4D;AAC5D,MAAM,WAAW,0BAA0B;IACvC;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,qBAAa,oBAAqB,YAAW,gBAAgB;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;gBAEzC,MAAM,GAAE,0BAA+B;IAUnD,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI;IAa/C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAStB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB3B,OAAO,CAAC,UAAU;YAiBJ,gBAAgB;CAYjC"}
1
+ {"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../../src/scheduler/node.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AASH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AA0DjE,4DAA4D;AAC5D,MAAM,WAAW,0BAA0B;IACvC;;;;OAIG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC/B;AAED;;;;;;;;GAQG;AACH,qBAAa,oBAAqB,YAAW,gBAAgB;IACzD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4B;gBAEzC,MAAM,GAAE,0BAA+B;IAWnD,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI;IAWrD,OAAO,CAAC,UAAU;IAYZ,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAStB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAwB3B,OAAO,CAAC,UAAU;IAiBlB;;;;OAIG;IACH,OAAO,CAAC,OAAO;IAwBf;;;;;OAKG;YACW,QAAQ;YAsBR,gBAAgB;CAYjC"}