@j13b/scheduler 0.1.0 → 0.1.1

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
@@ -63,6 +63,7 @@ See [`src/index.ts`](./src/index.ts) for the full export list. Notable types:
63
63
 
64
64
  - `RecurrenceRule` — discriminated union over the nine rule kinds.
65
65
  - `ScheduleTemplate` — `{ id, rule, timezone, disambiguation?, payload }`. `timezone` is required and validated at write time (use `"UTC"` for zone-free scheduling; it's ignored by the absolute `instant`/`anchored` rules).
66
+ - `validateRule(rule)` — checks a rule's whole shape (every required field present, typed and in range; no fields its kind lacks) and throws an `Error` naming the field and the form it expects. `Scheduler.add`/`update` run it, so a rule from JSON or an LLM fails with a clear message instead of crashing a generator. `validateTimeSpan` and `RULE_KINDS` are exported too.
66
67
  - `ScheduledEvent` — `{ templateId, occurrenceKey, startTimestamp, endTimestamp, payload, alterationId? }`.
67
68
  - `Disambiguation` — DST-edge resolution policy: `"compatible" | "earlier" | "later" | "reject" | "skip"`.
68
69
  - `SchedulerPort` — async storage interface; an `InMemorySchedulerPort` ships with the package.
@@ -4,3 +4,4 @@ export type { SchedulerPort } from "./port.js";
4
4
  export { Dispatcher, type DispatcherConfig } from "./dispatcher.js";
5
5
  export { type DispatcherStore, InMemoryDispatcherStore } from "./dispatcher_store.js";
6
6
  export { Day, Month, type RecurrenceRule, type InstantRule, type LocalDateTimeRule, type AnchoredRule, type DaysOfWeekRule, type WeeksOfMonthRule, type MonthsOfYearRule, type YearlyRule, type DaysOfMonthRule, type DaysOfYearRule, type ScheduleTemplate, type Alteration, type ScheduledEvent, type TimeSpan, type Disambiguation, } from "./types.js";
7
+ export { RULE_KINDS, validateRule, validateTimeSpan } from "./validate_rule.js";
package/dist/cjs/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Month = exports.Day = exports.InMemoryDispatcherStore = exports.Dispatcher = exports.InMemorySchedulerPort = exports.Scheduler = void 0;
3
+ exports.validateTimeSpan = exports.validateRule = exports.RULE_KINDS = exports.Month = exports.Day = exports.InMemoryDispatcherStore = exports.Dispatcher = exports.InMemorySchedulerPort = exports.Scheduler = void 0;
4
4
  var scheduler_js_1 = require("./scheduler.js");
5
5
  Object.defineProperty(exports, "Scheduler", { enumerable: true, get: function () { return scheduler_js_1.Scheduler; } });
6
6
  var in_memory_port_js_1 = require("./in_memory_port.js");
@@ -12,3 +12,7 @@ Object.defineProperty(exports, "InMemoryDispatcherStore", { enumerable: true, ge
12
12
  var types_js_1 = require("./types.js");
13
13
  Object.defineProperty(exports, "Day", { enumerable: true, get: function () { return types_js_1.Day; } });
14
14
  Object.defineProperty(exports, "Month", { enumerable: true, get: function () { return types_js_1.Month; } });
15
+ var validate_rule_js_1 = require("./validate_rule.js");
16
+ Object.defineProperty(exports, "RULE_KINDS", { enumerable: true, get: function () { return validate_rule_js_1.RULE_KINDS; } });
17
+ Object.defineProperty(exports, "validateRule", { enumerable: true, get: function () { return validate_rule_js_1.validateRule; } });
18
+ Object.defineProperty(exports, "validateTimeSpan", { enumerable: true, get: function () { return validate_rule_js_1.validateTimeSpan; } });
@@ -1,6 +1,6 @@
1
1
  import { type IEventBroadcast } from "@j13b/state";
2
2
  import type { SchedulerPort } from "./port.js";
3
- import type { RecurrenceRule, ScheduleTemplate, ScheduledEvent, TimeSpan } from "./types.js";
3
+ import type { ScheduleTemplate, ScheduledEvent, TimeSpan } from "./types.js";
4
4
  export declare class Scheduler<TPayload extends {
5
5
  type: string;
6
6
  }> {
@@ -21,4 +21,3 @@ export declare class Scheduler<TPayload extends {
21
21
  private _generateKeyed;
22
22
  private _removeExistingAlteration;
23
23
  }
24
- export declare function validateRule(rule: RecurrenceRule): void;
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Scheduler = void 0;
4
- exports.validateRule = validateRule;
5
4
  const state_1 = require("@j13b/state");
6
5
  const alterations_js_1 = require("./alterations.js");
7
6
  const anchored_js_1 = require("./generators/anchored.js");
@@ -15,6 +14,7 @@ const weeks_of_month_js_1 = require("./generators/weeks_of_month.js");
15
14
  const yearly_js_1 = require("./generators/yearly.js");
16
15
  const occurrence_key_js_1 = require("./occurrence_key.js");
17
16
  const tz_js_1 = require("./tz.js");
17
+ const validate_rule_js_1 = require("./validate_rule.js");
18
18
  const DAY_MS = 24 * 60 * 60 * 1000;
19
19
  // Search horizon for next() / nextDue() before giving up. Five years covers
20
20
  // every realistic use; if you need longer, call list() with an explicit
@@ -100,7 +100,7 @@ class Scheduler {
100
100
  }
101
101
  // ─── Writes ─────────────────────────────────────────────────
102
102
  async add(template) {
103
- validateRule(template.rule);
103
+ (0, validate_rule_js_1.validateRule)(template.rule);
104
104
  _validateTimezone(template.timezone);
105
105
  _validateDisambiguation(template.disambiguation);
106
106
  const normalized = {
@@ -122,7 +122,7 @@ class Scheduler {
122
122
  ...delta,
123
123
  id: delta.id,
124
124
  };
125
- validateRule(merged.rule);
125
+ (0, validate_rule_js_1.validateRule)(merged.rule);
126
126
  _validateTimezone(merged.timezone);
127
127
  _validateDisambiguation(merged.disambiguation);
128
128
  const updated = {
@@ -148,7 +148,7 @@ class Scheduler {
148
148
  // in time but keeps the same length).
149
149
  async reschedule(templateId, occurrenceKey, newStart, newTimeSpan, payloadOverride) {
150
150
  if (newTimeSpan != null)
151
- _validateTimeSpan(newTimeSpan);
151
+ (0, validate_rule_js_1.validateTimeSpan)(newTimeSpan);
152
152
  const id = `alt-${templateId}-${occurrenceKey}-${Date.now()}`;
153
153
  await this._removeExistingAlteration(templateId, occurrenceKey);
154
154
  const alt = {
@@ -232,153 +232,6 @@ class Scheduler {
232
232
  }
233
233
  exports.Scheduler = Scheduler;
234
234
  // ─── Validation ───────────────────────────────────────────────
235
- function validateRule(rule) {
236
- switch (rule.kind) {
237
- case "instant":
238
- _validateTimeSpan(rule.timeSpan);
239
- return;
240
- case "localDateTime":
241
- if (!Number.isInteger(rule.year)) {
242
- throw new Error("LocalDateTimeRule.year must be an integer");
243
- }
244
- if (rule.month < 0 || rule.month > 11) {
245
- throw new Error("LocalDateTimeRule.month must be in 0..11");
246
- }
247
- if (rule.day < 1 || rule.day > 31)
248
- throw new Error("LocalDateTimeRule.day must be in 1..31");
249
- _validateTimeOfDay(rule.timeOfDay);
250
- _validateTimeSpan(rule.timeSpan);
251
- return;
252
- case "anchored":
253
- if (rule.intervalMs <= 0)
254
- throw new Error("AnchoredRule.intervalMs must be > 0");
255
- _validateTimeSpan(rule.timeSpan);
256
- _validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);
257
- return;
258
- case "daysOfWeek":
259
- if (rule.weekInterval <= 0)
260
- throw new Error("DaysOfWeekRule.weekInterval must be > 0");
261
- if (rule.days.length === 0)
262
- throw new Error("DaysOfWeekRule.days must be non-empty");
263
- _validateTimeOfDay(rule.timeOfDay);
264
- _validateTimeSpan(rule.timeSpan);
265
- _validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);
266
- return;
267
- case "weeksOfMonth":
268
- if (rule.monthInterval <= 0)
269
- throw new Error("WeeksOfMonthRule.monthInterval must be > 0");
270
- if (rule.weeks.length === 0)
271
- throw new Error("WeeksOfMonthRule.weeks must be non-empty");
272
- for (const w of rule.weeks) {
273
- if (w === 0 || w < -5 || w > 5) {
274
- throw new Error("WeeksOfMonthRule.weeks entries must be in [-5..-1] or [1..5]");
275
- }
276
- }
277
- if (rule.days.length === 0)
278
- throw new Error("WeeksOfMonthRule.days must be non-empty");
279
- _validateTimeOfDay(rule.timeOfDay);
280
- _validateTimeSpan(rule.timeSpan);
281
- _validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);
282
- return;
283
- case "monthsOfYear":
284
- if (rule.yearInterval <= 0)
285
- throw new Error("MonthsOfYearRule.yearInterval must be > 0");
286
- if (rule.months.length === 0)
287
- throw new Error("MonthsOfYearRule.months must be non-empty");
288
- for (const m of rule.months) {
289
- if (m < 0 || m > 11) {
290
- throw new Error("MonthsOfYearRule.months entries must be in 0..11");
291
- }
292
- }
293
- if (rule.weeks.length === 0)
294
- throw new Error("MonthsOfYearRule.weeks must be non-empty");
295
- for (const w of rule.weeks) {
296
- if (w === 0 || w < -5 || w > 5) {
297
- throw new Error("MonthsOfYearRule.weeks entries must be in [-5..-1] or [1..5]");
298
- }
299
- }
300
- if (rule.days.length === 0)
301
- throw new Error("MonthsOfYearRule.days must be non-empty");
302
- _validateTimeOfDay(rule.timeOfDay);
303
- _validateTimeSpan(rule.timeSpan);
304
- _validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);
305
- return;
306
- case "yearly":
307
- if (rule.yearInterval <= 0)
308
- throw new Error("YearlyRule.yearInterval must be > 0");
309
- if (rule.month < 0 || rule.month > 11) {
310
- throw new Error("YearlyRule.month must be in 0..11");
311
- }
312
- if (rule.day < 1 || rule.day > 31)
313
- throw new Error("YearlyRule.day must be in 1..31");
314
- _validateTimeOfDay(rule.timeOfDay);
315
- _validateTimeSpan(rule.timeSpan);
316
- _validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);
317
- return;
318
- case "daysOfMonth":
319
- if (rule.yearInterval <= 0)
320
- throw new Error("DaysOfMonthRule.yearInterval must be > 0");
321
- if (rule.months.length === 0)
322
- throw new Error("DaysOfMonthRule.months must be non-empty");
323
- for (const m of rule.months) {
324
- if (m < 0 || m > 11) {
325
- throw new Error("DaysOfMonthRule.months entries must be in 0..11");
326
- }
327
- }
328
- if (rule.daysOfMonth.length === 0) {
329
- throw new Error("DaysOfMonthRule.daysOfMonth must be non-empty");
330
- }
331
- for (const d of rule.daysOfMonth) {
332
- if (d < 1 || d > 31) {
333
- throw new Error("DaysOfMonthRule.daysOfMonth entries must be in 1..31");
334
- }
335
- }
336
- _validateTimeOfDay(rule.timeOfDay);
337
- _validateTimeSpan(rule.timeSpan);
338
- _validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);
339
- return;
340
- case "daysOfYear":
341
- if (rule.yearInterval <= 0)
342
- throw new Error("DaysOfYearRule.yearInterval must be > 0");
343
- if (rule.daysOfYear.length === 0) {
344
- throw new Error("DaysOfYearRule.daysOfYear must be non-empty");
345
- }
346
- for (const d of rule.daysOfYear) {
347
- if (d < 1 || d > 366) {
348
- throw new Error("DaysOfYearRule.daysOfYear entries must be in 1..366");
349
- }
350
- }
351
- _validateTimeOfDay(rule.timeOfDay);
352
- _validateTimeSpan(rule.timeSpan);
353
- _validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);
354
- return;
355
- default: {
356
- const _exhaustive = rule;
357
- throw new Error(`Unknown rule kind: ${JSON.stringify(_exhaustive)}`);
358
- }
359
- }
360
- }
361
- // Validates the TimeSpan shape. Throws on { type: "duration", ms < 1 } or
362
- // { type: "hourSpan", hours <= 0 }. Undefined is valid (defaults to 1ms).
363
- function _validateTimeSpan(span) {
364
- if (span == null)
365
- return;
366
- if (span.type === "duration") {
367
- if (span.ms < 1) {
368
- throw new Error(`timeSpan.ms must be >= 1 if supplied (got ${span.ms})`);
369
- }
370
- return;
371
- }
372
- if (span.type === "hourSpan") {
373
- if (!Number.isFinite(span.hours) || span.hours <= 0) {
374
- throw new Error(`timeSpan.hours must be > 0 (got ${span.hours})`);
375
- }
376
- return;
377
- }
378
- // Exhaustiveness — TS will flag a missing case if TimeSpan grows.
379
- const _exhaustive = span;
380
- throw new Error(`Unknown TimeSpan type: ${JSON.stringify(_exhaustive)}`);
381
- }
382
235
  // Returns a copy of `rule` with timeSpan defaulted when omitted, so
383
236
  // generators don't have to handle undefined repeatedly.
384
237
  function _normalizeRule(rule) {
@@ -408,17 +261,6 @@ function _validateDisambiguation(d) {
408
261
  throw new Error(`Unknown disambiguation '${d}' (expected one of ${_DISAMBIGUATIONS.join(", ")})`);
409
262
  }
410
263
  }
411
- function _validateTimeOfDay(ms) {
412
- if (ms < 0 || ms >= 24 * 60 * 60 * 1000) {
413
- throw new Error(`timeOfDay must be in [0, 86_400_000) (got ${ms})`);
414
- }
415
- }
416
- function _validateSeries(start, end, count) {
417
- if (end != null && end <= start)
418
- throw new Error("seriesEnd must be > seriesStart");
419
- if (count != null && count < 0)
420
- throw new Error("count must be >= 0");
421
- }
422
264
  function _rulesEqual(a, b) {
423
265
  return JSON.stringify(_canonicalize(a)) === JSON.stringify(_canonicalize(b));
424
266
  }
@@ -0,0 +1,4 @@
1
+ import type { RecurrenceRule, TimeSpan } from "./types.js";
2
+ export declare const RULE_KINDS: RecurrenceRule["kind"][];
3
+ export declare function validateRule(rule: RecurrenceRule): void;
4
+ export declare function validateTimeSpan(span: unknown): asserts span is TimeSpan | undefined;
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RULE_KINDS = void 0;
4
+ exports.validateRule = validateRule;
5
+ exports.validateTimeSpan = validateTimeSpan;
6
+ // ─── Rule validation ──────────────────────────────────────────
7
+ //
8
+ // A rule often comes from outside the type system: JSON in a database, a
9
+ // request body, or an LLM tool call. So validation checks the WHOLE shape —
10
+ // every required field present, of the right type, in range, and no fields
11
+ // the kind does not have — and throws an Error that names the field and the
12
+ // form it expects. A bad rule never reaches a generator, where it would crash
13
+ // with "Cannot read properties of undefined" or "Invalid time value".
14
+ const DAY_MS = 24 * 60 * 60 * 1000;
15
+ const DAYS_HINT = "Day numbers 0..6 (0=Sunday, 1=Monday … 6=Saturday)";
16
+ const MONTHS_HINT = "Month numbers 0..11 (0=January … 11=December)";
17
+ const WEEKS_HINT = "week-of-month numbers 1..5 (1=first) or -1..-5 (-1=last)";
18
+ const TIME_OF_DAY_HINT = "ms after local midnight, 0..86_399_999 (7:00 = 25_200_000)";
19
+ const TIMESTAMP_HINT = "a UTC timestamp in ms";
20
+ // The common series fields every repeating kind has.
21
+ const SERIES = {
22
+ timeSpan: { required: false, check: (v) => validateTimeSpan(v) },
23
+ seriesStart: { required: true, check: timestamp },
24
+ seriesEnd: { required: true, check: nullable(timestamp, "null (no end)") },
25
+ count: {
26
+ required: false,
27
+ check: nullable(int(0, Number.MAX_SAFE_INTEGER, "a whole number >= 0"), "null"),
28
+ },
29
+ };
30
+ const TIME_OF_DAY = { required: true, check: int(0, DAY_MS - 1, TIME_OF_DAY_HINT) };
31
+ const KINDS = {
32
+ instant: {
33
+ name: "InstantRule",
34
+ fields: {
35
+ startOn: { required: true, check: timestamp },
36
+ timeSpan: { required: false, check: (v) => validateTimeSpan(v) },
37
+ },
38
+ },
39
+ localDateTime: {
40
+ name: "LocalDateTimeRule",
41
+ fields: {
42
+ year: { required: true, check: int(1, 9999, "a year such as 2026") },
43
+ month: { required: true, check: int(0, 11, MONTHS_HINT) },
44
+ day: { required: true, check: int(1, 31, "a day of the month 1..31") },
45
+ timeOfDay: TIME_OF_DAY,
46
+ timeSpan: { required: false, check: (v) => validateTimeSpan(v) },
47
+ },
48
+ },
49
+ anchored: {
50
+ name: "AnchoredRule",
51
+ fields: {
52
+ anchorDate: { required: true, check: timestamp },
53
+ intervalMs: { required: true, check: positive("an interval in ms > 0") },
54
+ ...SERIES,
55
+ },
56
+ },
57
+ daysOfWeek: {
58
+ name: "DaysOfWeekRule",
59
+ fields: {
60
+ days: { required: true, check: intList(0, 6, DAYS_HINT) },
61
+ weekInterval: {
62
+ required: true,
63
+ check: int(1, Number.MAX_SAFE_INTEGER, "a whole number >= 1 (1 = every week)"),
64
+ },
65
+ timeOfDay: TIME_OF_DAY,
66
+ ...SERIES,
67
+ },
68
+ },
69
+ weeksOfMonth: {
70
+ name: "WeeksOfMonthRule",
71
+ fields: {
72
+ days: { required: true, check: intList(0, 6, DAYS_HINT) },
73
+ weeks: { required: true, check: weekList },
74
+ monthInterval: {
75
+ required: true,
76
+ check: int(1, Number.MAX_SAFE_INTEGER, "a whole number >= 1 (1 = every month)"),
77
+ },
78
+ timeOfDay: TIME_OF_DAY,
79
+ ...SERIES,
80
+ },
81
+ },
82
+ monthsOfYear: {
83
+ name: "MonthsOfYearRule",
84
+ fields: {
85
+ months: { required: true, check: intList(0, 11, MONTHS_HINT) },
86
+ weeks: { required: true, check: weekList },
87
+ days: { required: true, check: intList(0, 6, DAYS_HINT) },
88
+ yearInterval: {
89
+ required: true,
90
+ check: int(1, Number.MAX_SAFE_INTEGER, "a whole number >= 1 (1 = every year)"),
91
+ },
92
+ timeOfDay: TIME_OF_DAY,
93
+ ...SERIES,
94
+ },
95
+ },
96
+ yearly: {
97
+ name: "YearlyRule",
98
+ fields: {
99
+ month: { required: true, check: int(0, 11, MONTHS_HINT) },
100
+ day: { required: true, check: int(1, 31, "a day of the month 1..31") },
101
+ yearInterval: {
102
+ required: true,
103
+ check: int(1, Number.MAX_SAFE_INTEGER, "a whole number >= 1 (1 = every year)"),
104
+ },
105
+ timeOfDay: TIME_OF_DAY,
106
+ ...SERIES,
107
+ },
108
+ },
109
+ daysOfMonth: {
110
+ name: "DaysOfMonthRule",
111
+ fields: {
112
+ months: { required: true, check: intList(0, 11, MONTHS_HINT) },
113
+ daysOfMonth: { required: true, check: intList(1, 31, "days of the month 1..31") },
114
+ yearInterval: {
115
+ required: true,
116
+ check: int(1, Number.MAX_SAFE_INTEGER, "a whole number >= 1 (1 = every year)"),
117
+ },
118
+ timeOfDay: TIME_OF_DAY,
119
+ ...SERIES,
120
+ },
121
+ },
122
+ daysOfYear: {
123
+ name: "DaysOfYearRule",
124
+ fields: {
125
+ daysOfYear: { required: true, check: intList(1, 366, "days of the year 1..366") },
126
+ yearInterval: {
127
+ required: true,
128
+ check: int(1, Number.MAX_SAFE_INTEGER, "a whole number >= 1 (1 = every year)"),
129
+ },
130
+ timeOfDay: TIME_OF_DAY,
131
+ ...SERIES,
132
+ },
133
+ },
134
+ };
135
+ // The rule kinds, in the order the types declare them.
136
+ exports.RULE_KINDS = Object.keys(KINDS);
137
+ // Throws when `rule` is not a valid RecurrenceRule. The message names the
138
+ // first wrong field and the form it expects.
139
+ function validateRule(rule) {
140
+ const raw = rule;
141
+ if (typeof raw !== "object" || raw == null || Array.isArray(raw)) {
142
+ throw new Error(`A rule must be an object with a "kind" (one of ${exports.RULE_KINDS.join(", ")})`);
143
+ }
144
+ const obj = raw;
145
+ const kind = KINDS[obj.kind];
146
+ if (kind == null) {
147
+ throw new Error(`Unknown rule kind ${JSON.stringify(obj.kind)} (expected one of ${exports.RULE_KINDS.join(", ")})`);
148
+ }
149
+ const known = Object.keys(kind.fields);
150
+ for (const key of Object.keys(obj)) {
151
+ if (key !== "kind" && !known.includes(key)) {
152
+ throw new Error(`${kind.name} has no field "${key}" (its fields are: ${known.join(", ")})`);
153
+ }
154
+ }
155
+ for (const [field, spec] of Object.entries(kind.fields)) {
156
+ const label = `${kind.name}.${field}`;
157
+ if (obj[field] === undefined) {
158
+ if (spec.required)
159
+ throw new Error(`${label} is required: ${describe(spec, label)}`);
160
+ continue;
161
+ }
162
+ spec.check(obj[field], label);
163
+ }
164
+ const seriesStart = obj.seriesStart;
165
+ const seriesEnd = obj.seriesEnd;
166
+ if (seriesStart != null && seriesEnd != null && seriesEnd <= seriesStart) {
167
+ throw new Error(`${kind.name}.seriesEnd must be > seriesStart`);
168
+ }
169
+ }
170
+ // Throws when `span` is not a TimeSpan. Undefined is valid (the rule's
171
+ // default span applies).
172
+ function validateTimeSpan(span) {
173
+ if (span === undefined)
174
+ return;
175
+ const form = '{ "type": "duration", "ms": <ms >= 1> } or { "type": "hourSpan", "hours": <hours > 0> }';
176
+ if (typeof span !== "object" || span == null) {
177
+ throw new Error(`timeSpan must be ${form}`);
178
+ }
179
+ const s = span;
180
+ if (s.type === "duration") {
181
+ if (typeof s.ms !== "number" || !Number.isFinite(s.ms) || s.ms < 1) {
182
+ throw new Error(`timeSpan.ms must be >= 1 if supplied (got ${String(s.ms)})`);
183
+ }
184
+ return;
185
+ }
186
+ if (s.type === "hourSpan") {
187
+ if (typeof s.hours !== "number" || !Number.isFinite(s.hours) || s.hours <= 0) {
188
+ throw new Error(`timeSpan.hours must be > 0 (got ${String(s.hours)})`);
189
+ }
190
+ return;
191
+ }
192
+ throw new Error(`timeSpan must be ${form} (got type ${JSON.stringify(s.type)})`);
193
+ }
194
+ // ─── Field checks ─────────────────────────────────────────────
195
+ // What the field should hold, for "is required" messages: each check is
196
+ // called on undefined and its message reused.
197
+ function describe(spec, label) {
198
+ try {
199
+ spec.check(undefined, label);
200
+ }
201
+ catch (err) {
202
+ const msg = err instanceof Error ? err.message : String(err);
203
+ const at = msg.indexOf(" must be ");
204
+ if (at >= 0)
205
+ return msg.slice(at + " must be ".length).replace(/ \(got [^)]*\)$/, "");
206
+ }
207
+ return "see the rule's type";
208
+ }
209
+ function show(v) {
210
+ return v === undefined ? "nothing" : JSON.stringify(v);
211
+ }
212
+ function isInt(v) {
213
+ return typeof v === "number" && Number.isInteger(v);
214
+ }
215
+ function int(min, max, hint) {
216
+ return (v, field) => {
217
+ if (!isInt(v) || v < min || v > max)
218
+ throw new Error(`${field} must be ${hint} (got ${show(v)})`);
219
+ };
220
+ }
221
+ function positive(hint) {
222
+ return (v, field) => {
223
+ if (typeof v !== "number" || !Number.isFinite(v) || v <= 0) {
224
+ throw new Error(`${field} must be ${hint} (got ${show(v)})`);
225
+ }
226
+ };
227
+ }
228
+ function timestamp(v, field) {
229
+ if (typeof v !== "number" || !Number.isFinite(v)) {
230
+ throw new Error(`${field} must be ${TIMESTAMP_HINT} (got ${show(v)})`);
231
+ }
232
+ }
233
+ function nullable(check, nullHint) {
234
+ return (v, field) => {
235
+ if (v === null)
236
+ return;
237
+ try {
238
+ check(v, field);
239
+ }
240
+ catch (err) {
241
+ const msg = err instanceof Error ? err.message : String(err);
242
+ throw new Error(msg.replace(/ \(got /, ` or ${nullHint} (got `));
243
+ }
244
+ };
245
+ }
246
+ function intList(min, max, hint) {
247
+ return (v, field) => {
248
+ const form = `a non-empty array of ${hint}`;
249
+ if (!Array.isArray(v) || v.length === 0)
250
+ throw new Error(`${field} must be ${form} (got ${show(v)})`);
251
+ for (const x of v) {
252
+ if (!isInt(x) || x < min || x > max) {
253
+ throw new Error(`${field} must be ${form} (got ${show(v)}; ${show(x)} is out of range)`);
254
+ }
255
+ }
256
+ };
257
+ }
258
+ function weekList(v, field) {
259
+ const form = `a non-empty array of ${WEEKS_HINT}`;
260
+ if (!Array.isArray(v) || v.length === 0)
261
+ throw new Error(`${field} must be ${form} (got ${show(v)})`);
262
+ for (const w of v) {
263
+ if (!isInt(w) || w === 0 || w < -5 || w > 5) {
264
+ throw new Error(`${field} must be ${form} (got ${show(v)}; ${show(w)} is out of range)`);
265
+ }
266
+ }
267
+ }
package/dist/index.d.ts CHANGED
@@ -4,4 +4,5 @@ export type { SchedulerPort } from './port.js';
4
4
  export { Dispatcher, type DispatcherConfig } from './dispatcher.js';
5
5
  export { type DispatcherStore, InMemoryDispatcherStore } from './dispatcher_store.js';
6
6
  export { Day, Month, type RecurrenceRule, type InstantRule, type LocalDateTimeRule, type AnchoredRule, type DaysOfWeekRule, type WeeksOfMonthRule, type MonthsOfYearRule, type YearlyRule, type DaysOfMonthRule, type DaysOfYearRule, type ScheduleTemplate, type Alteration, type ScheduledEvent, type TimeSpan, type Disambiguation, } from './types.js';
7
+ export { RULE_KINDS, validateRule, validateTimeSpan } from './validate_rule.js';
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,EAAE,KAAK,eAAe,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EACL,GAAG,EACH,KAAK,EACL,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,cAAc,GACpB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACpE,OAAO,EAAE,KAAK,eAAe,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AACtF,OAAO,EACL,GAAG,EACH,KAAK,EACL,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,QAAQ,EACb,KAAK,cAAc,GACpB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/index.js CHANGED
@@ -3,4 +3,5 @@ import { InMemorySchedulerPort } from "./in_memory_port.js";
3
3
  import { Dispatcher } from "./dispatcher.js";
4
4
  import { InMemoryDispatcherStore } from "./dispatcher_store.js";
5
5
  import { Day, Month } from "./types.js";
6
- export { Day, Dispatcher, InMemoryDispatcherStore, InMemorySchedulerPort, Month, Scheduler };
6
+ import { RULE_KINDS, validateRule, validateTimeSpan } from "./validate_rule.js";
7
+ export { Day, Dispatcher, InMemoryDispatcherStore, InMemorySchedulerPort, Month, RULE_KINDS, Scheduler, validateRule, validateTimeSpan };
@@ -1,6 +1,6 @@
1
1
  import { IEventBroadcast } from '@j13b/state';
2
2
  import { SchedulerPort } from './port.js';
3
- import { RecurrenceRule, ScheduleTemplate, ScheduledEvent, TimeSpan } from './types.js';
3
+ import { ScheduleTemplate, ScheduledEvent, TimeSpan } from './types.js';
4
4
  export declare class Scheduler<TPayload extends {
5
5
  type: string;
6
6
  }> {
@@ -21,5 +21,4 @@ export declare class Scheduler<TPayload extends {
21
21
  private _generateKeyed;
22
22
  private _removeExistingAlteration;
23
23
  }
24
- export declare function validateRule(rule: RecurrenceRule): void;
25
24
  //# sourceMappingURL=scheduler.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAa1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,KAAK,EAGV,cAAc,EACd,gBAAgB,EAChB,cAAc,EACd,QAAQ,EACT,MAAM,YAAY,CAAC;AASpB,qBAAa,SAAS,CAAC,QAAQ,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE;IACtD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0B;IAIhD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;gBAElC,IAAI,EAAE,aAAa,CAAC,QAAQ,CAAC;IAIzC,IAAI,OAAO,IAAI,eAAe,CAAC,IAAI,CAAC,CAEnC;IAMK,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC;IA0BjF,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC;IA+BjF,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC;IAuBvE,GAAG,CAAC,QAAQ,EAAE,gBAAgB,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAexD,MAAM,CACV,KAAK,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,GACtE,OAAO,CAAC,IAAI,CAAC;IAuBV,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWjC,UAAU,CACd,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,QAAQ,EACtB,eAAe,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,GAClC,OAAO,CAAC,MAAM,CAAC;IAkBZ,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAgBxE,OAAO,CAAC,cAAc;YA4CR,yBAAyB;CAWxC;AAID,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CA6GvD"}
1
+ {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAa1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,KAAK,EAIV,gBAAgB,EAChB,cAAc,EACd,QAAQ,EACT,MAAM,YAAY,CAAC;AAUpB,qBAAa,SAAS,CAAC,QAAQ,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE;IACtD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0B;IAIhD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;gBAElC,IAAI,EAAE,aAAa,CAAC,QAAQ,CAAC;IAIzC,IAAI,OAAO,IAAI,eAAe,CAAC,IAAI,CAAC,CAEnC;IAMK,IAAI,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC;IA0BjF,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC;IA+BjF,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,cAAc,CAAC,QAAQ,CAAC,EAAE,CAAC;IAuBvE,GAAG,CAAC,QAAQ,EAAE,gBAAgB,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAexD,MAAM,CACV,KAAK,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,GACtE,OAAO,CAAC,IAAI,CAAC;IAuBV,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWjC,UAAU,CACd,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,EACrB,QAAQ,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,QAAQ,EACtB,eAAe,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,GAClC,OAAO,CAAC,MAAM,CAAC;IAkBZ,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAgBxE,OAAO,CAAC,cAAc;YA4CR,yBAAyB;CAWxC"}