@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 +1 -0
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +5 -1
- package/dist/cjs/scheduler.d.ts +1 -2
- package/dist/cjs/scheduler.js +4 -162
- package/dist/cjs/validate_rule.d.ts +4 -0
- package/dist/cjs/validate_rule.js +267 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/scheduler.d.ts +1 -2
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +3 -94
- package/dist/scheduler.js.map +1 -1
- package/dist/validate_rule.d.ts +5 -0
- package/dist/validate_rule.d.ts.map +1 -0
- package/dist/validate_rule.js +280 -0
- package/dist/validate_rule.js.map +1 -0
- package/package.json +8 -2
- package/src/__tests__/validate_rule.test.ts +190 -0
- package/src/index.ts +1 -0
- package/src/scheduler.ts +2 -148
- package/src/validate_rule.ts +311 -0
package/dist/scheduler.js
CHANGED
|
@@ -10,6 +10,7 @@ import { generateWeeksOfMonth } from "./generators/weeks_of_month.js";
|
|
|
10
10
|
import { generateYearly } from "./generators/yearly.js";
|
|
11
11
|
import { assignOccurrenceKeys } from "./occurrence_key.js";
|
|
12
12
|
import { isValidTimeZone } from "./tz.js";
|
|
13
|
+
import { validateRule, validateTimeSpan } from "./validate_rule.js";
|
|
13
14
|
import { Event } from "@j13b/state";
|
|
14
15
|
// src/scheduler.ts
|
|
15
16
|
var DAY_MS = 864e5;
|
|
@@ -107,7 +108,7 @@ var Scheduler = class {
|
|
|
107
108
|
this._changed.notify();
|
|
108
109
|
}
|
|
109
110
|
async reschedule(templateId, occurrenceKey, newStart, newTimeSpan, payloadOverride) {
|
|
110
|
-
if (newTimeSpan != null)
|
|
111
|
+
if (newTimeSpan != null) validateTimeSpan(newTimeSpan);
|
|
111
112
|
const id = `alt-${templateId}-${occurrenceKey}-${Date.now()}`;
|
|
112
113
|
await this._removeExistingAlteration(templateId, occurrenceKey);
|
|
113
114
|
const alt = {
|
|
@@ -180,91 +181,6 @@ var Scheduler = class {
|
|
|
180
181
|
for (const alt of existing) if (alt.occurrenceKey === occurrenceKey) await this._port.removeAlteration(alt.id);
|
|
181
182
|
}
|
|
182
183
|
};
|
|
183
|
-
function validateRule(rule) {
|
|
184
|
-
switch (rule.kind) {
|
|
185
|
-
case "instant":
|
|
186
|
-
_validateTimeSpan(rule.timeSpan);
|
|
187
|
-
return;
|
|
188
|
-
case "localDateTime":
|
|
189
|
-
if (!Number.isInteger(rule.year)) throw new Error("LocalDateTimeRule.year must be an integer");
|
|
190
|
-
if (rule.month < 0 || rule.month > 11) throw new Error("LocalDateTimeRule.month must be in 0..11");
|
|
191
|
-
if (rule.day < 1 || rule.day > 31) throw new Error("LocalDateTimeRule.day must be in 1..31");
|
|
192
|
-
_validateTimeOfDay(rule.timeOfDay);
|
|
193
|
-
_validateTimeSpan(rule.timeSpan);
|
|
194
|
-
return;
|
|
195
|
-
case "anchored":
|
|
196
|
-
if (rule.intervalMs <= 0) throw new Error("AnchoredRule.intervalMs must be > 0");
|
|
197
|
-
_validateTimeSpan(rule.timeSpan);
|
|
198
|
-
_validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);
|
|
199
|
-
return;
|
|
200
|
-
case "daysOfWeek":
|
|
201
|
-
if (rule.weekInterval <= 0) throw new Error("DaysOfWeekRule.weekInterval must be > 0");
|
|
202
|
-
if (rule.days.length === 0) throw new Error("DaysOfWeekRule.days must be non-empty");
|
|
203
|
-
_validateTimeOfDay(rule.timeOfDay);
|
|
204
|
-
_validateTimeSpan(rule.timeSpan);
|
|
205
|
-
_validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);
|
|
206
|
-
return;
|
|
207
|
-
case "weeksOfMonth":
|
|
208
|
-
if (rule.monthInterval <= 0) throw new Error("WeeksOfMonthRule.monthInterval must be > 0");
|
|
209
|
-
if (rule.weeks.length === 0) throw new Error("WeeksOfMonthRule.weeks must be non-empty");
|
|
210
|
-
for (const w of rule.weeks) if (w === 0 || w < -5 || w > 5) throw new Error("WeeksOfMonthRule.weeks entries must be in [-5..-1] or [1..5]");
|
|
211
|
-
if (rule.days.length === 0) throw new Error("WeeksOfMonthRule.days must be non-empty");
|
|
212
|
-
_validateTimeOfDay(rule.timeOfDay);
|
|
213
|
-
_validateTimeSpan(rule.timeSpan);
|
|
214
|
-
_validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);
|
|
215
|
-
return;
|
|
216
|
-
case "monthsOfYear":
|
|
217
|
-
if (rule.yearInterval <= 0) throw new Error("MonthsOfYearRule.yearInterval must be > 0");
|
|
218
|
-
if (rule.months.length === 0) throw new Error("MonthsOfYearRule.months must be non-empty");
|
|
219
|
-
for (const m of rule.months) if (m < 0 || m > 11) throw new Error("MonthsOfYearRule.months entries must be in 0..11");
|
|
220
|
-
if (rule.weeks.length === 0) throw new Error("MonthsOfYearRule.weeks must be non-empty");
|
|
221
|
-
for (const w of rule.weeks) if (w === 0 || w < -5 || w > 5) throw new Error("MonthsOfYearRule.weeks entries must be in [-5..-1] or [1..5]");
|
|
222
|
-
if (rule.days.length === 0) throw new Error("MonthsOfYearRule.days must be non-empty");
|
|
223
|
-
_validateTimeOfDay(rule.timeOfDay);
|
|
224
|
-
_validateTimeSpan(rule.timeSpan);
|
|
225
|
-
_validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);
|
|
226
|
-
return;
|
|
227
|
-
case "yearly":
|
|
228
|
-
if (rule.yearInterval <= 0) throw new Error("YearlyRule.yearInterval must be > 0");
|
|
229
|
-
if (rule.month < 0 || rule.month > 11) throw new Error("YearlyRule.month must be in 0..11");
|
|
230
|
-
if (rule.day < 1 || rule.day > 31) throw new Error("YearlyRule.day must be in 1..31");
|
|
231
|
-
_validateTimeOfDay(rule.timeOfDay);
|
|
232
|
-
_validateTimeSpan(rule.timeSpan);
|
|
233
|
-
_validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);
|
|
234
|
-
return;
|
|
235
|
-
case "daysOfMonth":
|
|
236
|
-
if (rule.yearInterval <= 0) throw new Error("DaysOfMonthRule.yearInterval must be > 0");
|
|
237
|
-
if (rule.months.length === 0) throw new Error("DaysOfMonthRule.months must be non-empty");
|
|
238
|
-
for (const m of rule.months) if (m < 0 || m > 11) throw new Error("DaysOfMonthRule.months entries must be in 0..11");
|
|
239
|
-
if (rule.daysOfMonth.length === 0) throw new Error("DaysOfMonthRule.daysOfMonth must be non-empty");
|
|
240
|
-
for (const d of rule.daysOfMonth) if (d < 1 || d > 31) throw new Error("DaysOfMonthRule.daysOfMonth entries must be in 1..31");
|
|
241
|
-
_validateTimeOfDay(rule.timeOfDay);
|
|
242
|
-
_validateTimeSpan(rule.timeSpan);
|
|
243
|
-
_validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);
|
|
244
|
-
return;
|
|
245
|
-
case "daysOfYear":
|
|
246
|
-
if (rule.yearInterval <= 0) throw new Error("DaysOfYearRule.yearInterval must be > 0");
|
|
247
|
-
if (rule.daysOfYear.length === 0) throw new Error("DaysOfYearRule.daysOfYear must be non-empty");
|
|
248
|
-
for (const d of rule.daysOfYear) if (d < 1 || d > 366) throw new Error("DaysOfYearRule.daysOfYear entries must be in 1..366");
|
|
249
|
-
_validateTimeOfDay(rule.timeOfDay);
|
|
250
|
-
_validateTimeSpan(rule.timeSpan);
|
|
251
|
-
_validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);
|
|
252
|
-
return;
|
|
253
|
-
default: throw new Error(`Unknown rule kind: ${JSON.stringify(rule)}`);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
function _validateTimeSpan(span) {
|
|
257
|
-
if (span == null) return;
|
|
258
|
-
if (span.type === "duration") {
|
|
259
|
-
if (span.ms < 1) throw new Error(`timeSpan.ms must be >= 1 if supplied (got ${span.ms})`);
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
if (span.type === "hourSpan") {
|
|
263
|
-
if (!Number.isFinite(span.hours) || span.hours <= 0) throw new Error(`timeSpan.hours must be > 0 (got ${span.hours})`);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
throw new Error(`Unknown TimeSpan type: ${JSON.stringify(span)}`);
|
|
267
|
-
}
|
|
268
184
|
function _normalizeRule(rule) {
|
|
269
185
|
if (rule.timeSpan != null) return rule;
|
|
270
186
|
return {
|
|
@@ -290,13 +206,6 @@ function _validateDisambiguation(d) {
|
|
|
290
206
|
if (d == null) return;
|
|
291
207
|
if (!_DISAMBIGUATIONS.includes(d)) throw new Error(`Unknown disambiguation '${d}' (expected one of ${_DISAMBIGUATIONS.join(", ")})`);
|
|
292
208
|
}
|
|
293
|
-
function _validateTimeOfDay(ms) {
|
|
294
|
-
if (ms < 0 || ms >= 864e5) throw new Error(`timeOfDay must be in [0, 86_400_000) (got ${ms})`);
|
|
295
|
-
}
|
|
296
|
-
function _validateSeries(start, end, count) {
|
|
297
|
-
if (end != null && end <= start) throw new Error("seriesEnd must be > seriesStart");
|
|
298
|
-
if (count != null && count < 0) throw new Error("count must be >= 0");
|
|
299
|
-
}
|
|
300
209
|
function _rulesEqual(a, b) {
|
|
301
210
|
return JSON.stringify(_canonicalize(a)) === JSON.stringify(_canonicalize(b));
|
|
302
211
|
}
|
|
@@ -327,6 +236,6 @@ function _canonicalize(rule) {
|
|
|
327
236
|
};
|
|
328
237
|
return rule;
|
|
329
238
|
}
|
|
330
|
-
export { Scheduler
|
|
239
|
+
export { Scheduler };
|
|
331
240
|
|
|
332
241
|
//# sourceMappingURL=scheduler.js.map
|
package/dist/scheduler.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scheduler.js","names":["_exhaustive"],"sources":["../src/scheduler.ts"],"sourcesContent":["import { Event, type IEventBroadcast } from \"@j13b/state\";\nimport { applyAlterations } from \"./alterations.js\";\nimport { generateAnchored } from \"./generators/anchored.js\";\nimport { generateDaysOfMonth } from \"./generators/days_of_month.js\";\nimport { generateDaysOfWeek } from \"./generators/days_of_week.js\";\nimport { generateDaysOfYear } from \"./generators/days_of_year.js\";\nimport { generateInstant } from \"./generators/instant.js\";\nimport { generateLocalDateTime } from \"./generators/local_date_time.js\";\nimport { generateMonthsOfYear } from \"./generators/months_of_year.js\";\nimport type { Occurrence } from \"./generators/types.js\";\nimport { generateWeeksOfMonth } from \"./generators/weeks_of_month.js\";\nimport { generateYearly } from \"./generators/yearly.js\";\nimport { assignOccurrenceKeys } from \"./occurrence_key.js\";\nimport type { SchedulerPort } from \"./port.js\";\nimport type {\n Alteration,\n Disambiguation,\n RecurrenceRule,\n ScheduleTemplate,\n ScheduledEvent,\n TimeSpan,\n} from \"./types.js\";\nimport { isValidTimeZone } from \"./tz.js\";\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n// Search horizon for next() / nextDue() before giving up. Five years covers\n// every realistic use; if you need longer, call list() with an explicit\n// window.\nconst NEXT_SEARCH_HORIZON_MS = 5 * 365 * DAY_MS;\n\nexport class Scheduler<TPayload extends { type: string }> {\n private readonly _port: SchedulerPort<TPayload>;\n // Fires after every successful write (add/update/remove/reschedule/cancel).\n // The Dispatcher subscribes via .changed so a new event added during a\n // sleep can interrupt the sleep and re-query nextDue().\n private readonly _changed = new Event<void>();\n\n constructor(port: SchedulerPort<TPayload>) {\n this._port = port;\n }\n\n get changed(): IEventBroadcast<void> {\n return this._changed.broadcast;\n }\n\n // ─── Reads ──────────────────────────────────────────────────\n\n // All occurrences whose [start, end) intersects [windowStart, windowEnd),\n // sorted ascending by startTimestamp. Honors alterations.\n async list(windowStart: number, windowEnd: number): Promise<ScheduledEvent<TPayload>[]> {\n if (windowEnd <= windowStart) return [];\n const [templates, alterations] = await Promise.all([\n this._port.listTemplates(windowStart, windowEnd),\n this._port.listAlterations(windowStart, windowEnd),\n ]);\n const out: ScheduledEvent<TPayload>[] = [];\n for (const tpl of templates) {\n const keyed = this._generateKeyed(tpl, windowStart, windowEnd);\n const events = applyAlterations(\n tpl.id,\n tpl.payload,\n keyed,\n alterations,\n tpl.timezone,\n windowStart,\n windowEnd\n );\n out.push(...events);\n }\n out.sort((a, b) => a.startTimestamp - b.startTimestamp);\n return out;\n }\n\n // Earliest occurrence of one template strictly after `after`. Returns null\n // if no occurrence exists within NEXT_SEARCH_HORIZON_MS.\n async next(templateId: string, after: number): Promise<ScheduledEvent<TPayload> | null> {\n const tpl = await this._port.getTemplate(templateId);\n if (tpl == null) return null;\n\n // Expanding-window search. Doubles each iteration until horizon.\n const horizonEnd = after + NEXT_SEARCH_HORIZON_MS;\n let windowStart = after;\n let windowSize = DAY_MS;\n while (windowStart < horizonEnd) {\n const windowEnd = Math.min(windowStart + windowSize, horizonEnd);\n const alterations = await this._port.listAlterationsByTemplate(templateId);\n const keyed = this._generateKeyed(tpl, windowStart, windowEnd);\n const events = applyAlterations(\n tpl.id,\n tpl.payload,\n keyed,\n alterations,\n tpl.timezone,\n windowStart,\n windowEnd\n );\n const firstAfter = events.find((e) => e.startTimestamp > after);\n if (firstAfter != null) return firstAfter;\n windowStart = windowEnd;\n windowSize = Math.min(windowSize * 2, 365 * DAY_MS);\n }\n return null;\n }\n\n // The next `limit` occurrences globally after `after`, sorted ascending.\n // Defaults to 10. Searches in expanding windows like next().\n async nextDue(after: number, limit = 10): Promise<ScheduledEvent<TPayload>[]> {\n if (limit <= 0) return [];\n const horizonEnd = after + NEXT_SEARCH_HORIZON_MS;\n const out: ScheduledEvent<TPayload>[] = [];\n let windowStart = after;\n let windowSize = DAY_MS;\n while (windowStart < horizonEnd && out.length < limit) {\n const windowEnd = Math.min(windowStart + windowSize, horizonEnd);\n const events = await this.list(windowStart, windowEnd);\n for (const e of events) {\n if (e.startTimestamp > after) {\n out.push(e);\n if (out.length >= limit) break;\n }\n }\n windowStart = windowEnd;\n windowSize = Math.min(windowSize * 2, 365 * DAY_MS);\n }\n return out;\n }\n\n // ─── Writes ─────────────────────────────────────────────────\n\n async add(template: ScheduleTemplate<TPayload>): Promise<void> {\n validateRule(template.rule);\n _validateTimezone(template.timezone);\n _validateDisambiguation(template.disambiguation);\n const normalized: ScheduleTemplate<TPayload> = {\n ...template,\n rule: _normalizeRule(template.rule),\n };\n await this._port.addTemplate(normalized);\n this._changed.notify();\n }\n\n // Partial update. Caller supplies any subset of the template fields plus\n // the id. If the rule changes (kind or any field), all alterations for\n // this template are wiped — same policy as activity-board, made explicit.\n async update(\n delta: { id: string } & Partial<Omit<ScheduleTemplate<TPayload>, \"id\">>\n ): Promise<void> {\n const existing = await this._port.getTemplate(delta.id);\n if (existing == null) throw new Error(`No template with id ${delta.id}`);\n const merged: ScheduleTemplate<TPayload> = {\n ...existing,\n ...delta,\n id: delta.id,\n };\n validateRule(merged.rule);\n _validateTimezone(merged.timezone);\n _validateDisambiguation(merged.disambiguation);\n const updated: ScheduleTemplate<TPayload> = {\n ...merged,\n rule: _normalizeRule(merged.rule),\n };\n const ruleChanged = !_rulesEqual(existing.rule, updated.rule);\n await this._port.updateTemplate(updated);\n if (ruleChanged) {\n await this._port.removeAlterationsForTemplate(updated.id);\n }\n this._changed.notify();\n }\n\n async remove(id: string): Promise<void> {\n await this._port.removeAlterationsForTemplate(id);\n await this._port.removeTemplate(id);\n this._changed.notify();\n }\n\n // Adds or replaces a reschedule alteration for the given occurrence.\n // Caller is responsible for providing a valid occurrenceKey (typically\n // taken from a prior list() result). If `newTimeSpan` is omitted, the\n // rescheduled occurrence inherits the original's UTC duration (i.e. moves\n // in time but keeps the same length).\n async reschedule(\n templateId: string,\n occurrenceKey: string,\n newStart: number,\n newTimeSpan?: TimeSpan,\n payloadOverride?: Partial<TPayload>\n ): Promise<string> {\n if (newTimeSpan != null) _validateTimeSpan(newTimeSpan);\n const id = `alt-${templateId}-${occurrenceKey}-${Date.now()}`;\n await this._removeExistingAlteration(templateId, occurrenceKey);\n const alt: Alteration<TPayload> = {\n id,\n templateId,\n occurrenceKey,\n kind: \"reschedule\",\n startTimestamp: newStart,\n timeSpan: newTimeSpan,\n payloadOverride,\n };\n await this._port.addAlteration(alt);\n this._changed.notify();\n return id;\n }\n\n async cancel(templateId: string, occurrenceKey: string): Promise<string> {\n const id = `alt-${templateId}-${occurrenceKey}-${Date.now()}`;\n await this._removeExistingAlteration(templateId, occurrenceKey);\n const alt: Alteration<TPayload> = {\n id,\n templateId,\n occurrenceKey,\n kind: \"cancel\",\n };\n await this._port.addAlteration(alt);\n this._changed.notify();\n return id;\n }\n\n // ─── Internals ──────────────────────────────────────────────\n\n private _generateKeyed(tpl: ScheduleTemplate<TPayload>, windowStart: number, windowEnd: number) {\n const tz = tpl.timezone;\n const dis = tpl.disambiguation ?? \"compatible\";\n let occurrences: Occurrence[];\n switch (tpl.rule.kind) {\n case \"instant\":\n // Absolute start — no wall-clock ambiguity, disambiguation N/A.\n occurrences = generateInstant(tpl.rule, tz, windowStart, windowEnd);\n break;\n case \"localDateTime\":\n occurrences = generateLocalDateTime(tpl.rule, tz, windowStart, windowEnd, dis);\n break;\n case \"anchored\":\n // Absolute stepping — no wall-clock ambiguity, disambiguation N/A.\n occurrences = generateAnchored(tpl.rule, tz, windowStart, windowEnd);\n break;\n case \"daysOfWeek\":\n occurrences = generateDaysOfWeek(tpl.rule, tz, windowStart, windowEnd, dis);\n break;\n case \"weeksOfMonth\":\n occurrences = generateWeeksOfMonth(tpl.rule, tz, windowStart, windowEnd, dis);\n break;\n case \"monthsOfYear\":\n occurrences = generateMonthsOfYear(tpl.rule, tz, windowStart, windowEnd, dis);\n break;\n case \"yearly\":\n occurrences = generateYearly(tpl.rule, tz, windowStart, windowEnd, dis);\n break;\n case \"daysOfMonth\":\n occurrences = generateDaysOfMonth(tpl.rule, tz, windowStart, windowEnd, dis);\n break;\n case \"daysOfYear\":\n occurrences = generateDaysOfYear(tpl.rule, tz, windowStart, windowEnd, dis);\n break;\n default: {\n // Exhaustiveness guard — adding a new kind without handling it here\n // is a compile error.\n const _exhaustive: never = tpl.rule;\n throw new Error(`Unknown rule kind: ${JSON.stringify(_exhaustive)}`);\n }\n }\n return assignOccurrenceKeys(occurrences, tz);\n }\n\n private async _removeExistingAlteration(\n templateId: string,\n occurrenceKey: string\n ): Promise<void> {\n const existing = await this._port.listAlterationsByTemplate(templateId);\n for (const alt of existing) {\n if (alt.occurrenceKey === occurrenceKey) {\n await this._port.removeAlteration(alt.id);\n }\n }\n }\n}\n\n// ─── Validation ───────────────────────────────────────────────\n\nexport function validateRule(rule: RecurrenceRule): void {\n switch (rule.kind) {\n case \"instant\":\n _validateTimeSpan(rule.timeSpan);\n return;\n case \"localDateTime\":\n if (!Number.isInteger(rule.year)) {\n throw new Error(\"LocalDateTimeRule.year must be an integer\");\n }\n if (rule.month < 0 || rule.month > 11) {\n throw new Error(\"LocalDateTimeRule.month must be in 0..11\");\n }\n if (rule.day < 1 || rule.day > 31) throw new Error(\"LocalDateTimeRule.day must be in 1..31\");\n _validateTimeOfDay(rule.timeOfDay);\n _validateTimeSpan(rule.timeSpan);\n return;\n case \"anchored\":\n if (rule.intervalMs <= 0) throw new Error(\"AnchoredRule.intervalMs must be > 0\");\n _validateTimeSpan(rule.timeSpan);\n _validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);\n return;\n case \"daysOfWeek\":\n if (rule.weekInterval <= 0) throw new Error(\"DaysOfWeekRule.weekInterval must be > 0\");\n if (rule.days.length === 0) throw new Error(\"DaysOfWeekRule.days must be non-empty\");\n _validateTimeOfDay(rule.timeOfDay);\n _validateTimeSpan(rule.timeSpan);\n _validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);\n return;\n case \"weeksOfMonth\":\n if (rule.monthInterval <= 0) throw new Error(\"WeeksOfMonthRule.monthInterval must be > 0\");\n if (rule.weeks.length === 0) throw new Error(\"WeeksOfMonthRule.weeks must be non-empty\");\n for (const w of rule.weeks) {\n if (w === 0 || w < -5 || w > 5) {\n throw new Error(\"WeeksOfMonthRule.weeks entries must be in [-5..-1] or [1..5]\");\n }\n }\n if (rule.days.length === 0) throw new Error(\"WeeksOfMonthRule.days must be non-empty\");\n _validateTimeOfDay(rule.timeOfDay);\n _validateTimeSpan(rule.timeSpan);\n _validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);\n return;\n case \"monthsOfYear\":\n if (rule.yearInterval <= 0) throw new Error(\"MonthsOfYearRule.yearInterval must be > 0\");\n if (rule.months.length === 0) throw new Error(\"MonthsOfYearRule.months must be non-empty\");\n for (const m of rule.months) {\n if (m < 0 || m > 11) {\n throw new Error(\"MonthsOfYearRule.months entries must be in 0..11\");\n }\n }\n if (rule.weeks.length === 0) throw new Error(\"MonthsOfYearRule.weeks must be non-empty\");\n for (const w of rule.weeks) {\n if (w === 0 || w < -5 || w > 5) {\n throw new Error(\"MonthsOfYearRule.weeks entries must be in [-5..-1] or [1..5]\");\n }\n }\n if (rule.days.length === 0) throw new Error(\"MonthsOfYearRule.days must be non-empty\");\n _validateTimeOfDay(rule.timeOfDay);\n _validateTimeSpan(rule.timeSpan);\n _validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);\n return;\n case \"yearly\":\n if (rule.yearInterval <= 0) throw new Error(\"YearlyRule.yearInterval must be > 0\");\n if (rule.month < 0 || rule.month > 11) {\n throw new Error(\"YearlyRule.month must be in 0..11\");\n }\n if (rule.day < 1 || rule.day > 31) throw new Error(\"YearlyRule.day must be in 1..31\");\n _validateTimeOfDay(rule.timeOfDay);\n _validateTimeSpan(rule.timeSpan);\n _validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);\n return;\n case \"daysOfMonth\":\n if (rule.yearInterval <= 0) throw new Error(\"DaysOfMonthRule.yearInterval must be > 0\");\n if (rule.months.length === 0) throw new Error(\"DaysOfMonthRule.months must be non-empty\");\n for (const m of rule.months) {\n if (m < 0 || m > 11) {\n throw new Error(\"DaysOfMonthRule.months entries must be in 0..11\");\n }\n }\n if (rule.daysOfMonth.length === 0) {\n throw new Error(\"DaysOfMonthRule.daysOfMonth must be non-empty\");\n }\n for (const d of rule.daysOfMonth) {\n if (d < 1 || d > 31) {\n throw new Error(\"DaysOfMonthRule.daysOfMonth entries must be in 1..31\");\n }\n }\n _validateTimeOfDay(rule.timeOfDay);\n _validateTimeSpan(rule.timeSpan);\n _validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);\n return;\n case \"daysOfYear\":\n if (rule.yearInterval <= 0) throw new Error(\"DaysOfYearRule.yearInterval must be > 0\");\n if (rule.daysOfYear.length === 0) {\n throw new Error(\"DaysOfYearRule.daysOfYear must be non-empty\");\n }\n for (const d of rule.daysOfYear) {\n if (d < 1 || d > 366) {\n throw new Error(\"DaysOfYearRule.daysOfYear entries must be in 1..366\");\n }\n }\n _validateTimeOfDay(rule.timeOfDay);\n _validateTimeSpan(rule.timeSpan);\n _validateSeries(rule.seriesStart, rule.seriesEnd, rule.count);\n return;\n default: {\n const _exhaustive: never = rule;\n throw new Error(`Unknown rule kind: ${JSON.stringify(_exhaustive)}`);\n }\n }\n}\n\n// Validates the TimeSpan shape. Throws on { type: \"duration\", ms < 1 } or\n// { type: \"hourSpan\", hours <= 0 }. Undefined is valid (defaults to 1ms).\nfunction _validateTimeSpan(span: TimeSpan | undefined): void {\n if (span == null) return;\n if (span.type === \"duration\") {\n if (span.ms < 1) {\n throw new Error(`timeSpan.ms must be >= 1 if supplied (got ${span.ms})`);\n }\n return;\n }\n if (span.type === \"hourSpan\") {\n if (!Number.isFinite(span.hours) || span.hours <= 0) {\n throw new Error(`timeSpan.hours must be > 0 (got ${span.hours})`);\n }\n return;\n }\n // Exhaustiveness — TS will flag a missing case if TimeSpan grows.\n const _exhaustive: never = span;\n throw new Error(`Unknown TimeSpan type: ${JSON.stringify(_exhaustive)}`);\n}\n\n// Returns a copy of `rule` with timeSpan defaulted when omitted, so\n// generators don't have to handle undefined repeatedly.\nfunction _normalizeRule(rule: RecurrenceRule): RecurrenceRule {\n if (rule.timeSpan != null) return rule;\n return { ...rule, timeSpan: { type: \"duration\", ms: 1 } };\n}\n\nfunction _validateTimezone(tz: string): void {\n if (typeof tz !== \"string\" || tz.length === 0) {\n throw new Error(\"ScheduleTemplate.timezone is required (use 'UTC' for zone-free scheduling)\");\n }\n if (!isValidTimeZone(tz)) {\n throw new Error(`Unknown timezone '${tz}' (expected an IANA zone name or 'UTC')`);\n }\n}\n\nconst _DISAMBIGUATIONS: readonly Disambiguation[] = [\n \"compatible\",\n \"earlier\",\n \"later\",\n \"reject\",\n \"skip\",\n];\n\nfunction _validateDisambiguation(d: Disambiguation | undefined): void {\n if (d == null) return;\n if (!_DISAMBIGUATIONS.includes(d)) {\n throw new Error(\n `Unknown disambiguation '${d}' (expected one of ${_DISAMBIGUATIONS.join(\", \")})`\n );\n }\n}\n\nfunction _validateTimeOfDay(ms: number): void {\n if (ms < 0 || ms >= 24 * 60 * 60 * 1000) {\n throw new Error(`timeOfDay must be in [0, 86_400_000) (got ${ms})`);\n }\n}\n\nfunction _validateSeries(\n start: number,\n end: number | null,\n count: number | null | undefined\n): void {\n if (end != null && end <= start) throw new Error(\"seriesEnd must be > seriesStart\");\n if (count != null && count < 0) throw new Error(\"count must be >= 0\");\n}\n\nfunction _rulesEqual(a: RecurrenceRule, b: RecurrenceRule): boolean {\n return JSON.stringify(_canonicalize(a)) === JSON.stringify(_canonicalize(b));\n}\n\nfunction _canonicalize(rule: RecurrenceRule): unknown {\n // Sort + dedup any array selectors so reorderings or duplicates don't\n // false-flag a rule change (which would wipe alterations).\n if (rule.kind === \"daysOfWeek\") {\n return { ...rule, days: [...new Set(rule.days)].sort((a, b) => a - b) };\n }\n if (rule.kind === \"weeksOfMonth\") {\n return {\n ...rule,\n days: [...new Set(rule.days)].sort((a, b) => a - b),\n weeks: [...new Set(rule.weeks)].sort((a, b) => a - b),\n };\n }\n if (rule.kind === \"monthsOfYear\") {\n return {\n ...rule,\n months: [...new Set(rule.months)].sort((a, b) => a - b),\n weeks: [...new Set(rule.weeks)].sort((a, b) => a - b),\n days: [...new Set(rule.days)].sort((a, b) => a - b),\n };\n }\n if (rule.kind === \"daysOfMonth\") {\n return {\n ...rule,\n months: [...new Set(rule.months)].sort((a, b) => a - b),\n daysOfMonth: [...new Set(rule.daysOfMonth)].sort((a, b) => a - b),\n };\n }\n if (rule.kind === \"daysOfYear\") {\n return {\n ...rule,\n daysOfYear: [...new Set(rule.daysOfYear)].sort((a, b) => a - b),\n };\n }\n return rule;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"scheduler.js","names":[],"sources":["../src/scheduler.ts"],"sourcesContent":["import { Event, type IEventBroadcast } from \"@j13b/state\";\nimport { applyAlterations } from \"./alterations.js\";\nimport { generateAnchored } from \"./generators/anchored.js\";\nimport { generateDaysOfMonth } from \"./generators/days_of_month.js\";\nimport { generateDaysOfWeek } from \"./generators/days_of_week.js\";\nimport { generateDaysOfYear } from \"./generators/days_of_year.js\";\nimport { generateInstant } from \"./generators/instant.js\";\nimport { generateLocalDateTime } from \"./generators/local_date_time.js\";\nimport { generateMonthsOfYear } from \"./generators/months_of_year.js\";\nimport type { Occurrence } from \"./generators/types.js\";\nimport { generateWeeksOfMonth } from \"./generators/weeks_of_month.js\";\nimport { generateYearly } from \"./generators/yearly.js\";\nimport { assignOccurrenceKeys } from \"./occurrence_key.js\";\nimport type { SchedulerPort } from \"./port.js\";\nimport type {\n Alteration,\n Disambiguation,\n RecurrenceRule,\n ScheduleTemplate,\n ScheduledEvent,\n TimeSpan,\n} from \"./types.js\";\nimport { isValidTimeZone } from \"./tz.js\";\nimport { validateRule, validateTimeSpan } from \"./validate_rule.js\";\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n// Search horizon for next() / nextDue() before giving up. Five years covers\n// every realistic use; if you need longer, call list() with an explicit\n// window.\nconst NEXT_SEARCH_HORIZON_MS = 5 * 365 * DAY_MS;\n\nexport class Scheduler<TPayload extends { type: string }> {\n private readonly _port: SchedulerPort<TPayload>;\n // Fires after every successful write (add/update/remove/reschedule/cancel).\n // The Dispatcher subscribes via .changed so a new event added during a\n // sleep can interrupt the sleep and re-query nextDue().\n private readonly _changed = new Event<void>();\n\n constructor(port: SchedulerPort<TPayload>) {\n this._port = port;\n }\n\n get changed(): IEventBroadcast<void> {\n return this._changed.broadcast;\n }\n\n // ─── Reads ──────────────────────────────────────────────────\n\n // All occurrences whose [start, end) intersects [windowStart, windowEnd),\n // sorted ascending by startTimestamp. Honors alterations.\n async list(windowStart: number, windowEnd: number): Promise<ScheduledEvent<TPayload>[]> {\n if (windowEnd <= windowStart) return [];\n const [templates, alterations] = await Promise.all([\n this._port.listTemplates(windowStart, windowEnd),\n this._port.listAlterations(windowStart, windowEnd),\n ]);\n const out: ScheduledEvent<TPayload>[] = [];\n for (const tpl of templates) {\n const keyed = this._generateKeyed(tpl, windowStart, windowEnd);\n const events = applyAlterations(\n tpl.id,\n tpl.payload,\n keyed,\n alterations,\n tpl.timezone,\n windowStart,\n windowEnd\n );\n out.push(...events);\n }\n out.sort((a, b) => a.startTimestamp - b.startTimestamp);\n return out;\n }\n\n // Earliest occurrence of one template strictly after `after`. Returns null\n // if no occurrence exists within NEXT_SEARCH_HORIZON_MS.\n async next(templateId: string, after: number): Promise<ScheduledEvent<TPayload> | null> {\n const tpl = await this._port.getTemplate(templateId);\n if (tpl == null) return null;\n\n // Expanding-window search. Doubles each iteration until horizon.\n const horizonEnd = after + NEXT_SEARCH_HORIZON_MS;\n let windowStart = after;\n let windowSize = DAY_MS;\n while (windowStart < horizonEnd) {\n const windowEnd = Math.min(windowStart + windowSize, horizonEnd);\n const alterations = await this._port.listAlterationsByTemplate(templateId);\n const keyed = this._generateKeyed(tpl, windowStart, windowEnd);\n const events = applyAlterations(\n tpl.id,\n tpl.payload,\n keyed,\n alterations,\n tpl.timezone,\n windowStart,\n windowEnd\n );\n const firstAfter = events.find((e) => e.startTimestamp > after);\n if (firstAfter != null) return firstAfter;\n windowStart = windowEnd;\n windowSize = Math.min(windowSize * 2, 365 * DAY_MS);\n }\n return null;\n }\n\n // The next `limit` occurrences globally after `after`, sorted ascending.\n // Defaults to 10. Searches in expanding windows like next().\n async nextDue(after: number, limit = 10): Promise<ScheduledEvent<TPayload>[]> {\n if (limit <= 0) return [];\n const horizonEnd = after + NEXT_SEARCH_HORIZON_MS;\n const out: ScheduledEvent<TPayload>[] = [];\n let windowStart = after;\n let windowSize = DAY_MS;\n while (windowStart < horizonEnd && out.length < limit) {\n const windowEnd = Math.min(windowStart + windowSize, horizonEnd);\n const events = await this.list(windowStart, windowEnd);\n for (const e of events) {\n if (e.startTimestamp > after) {\n out.push(e);\n if (out.length >= limit) break;\n }\n }\n windowStart = windowEnd;\n windowSize = Math.min(windowSize * 2, 365 * DAY_MS);\n }\n return out;\n }\n\n // ─── Writes ─────────────────────────────────────────────────\n\n async add(template: ScheduleTemplate<TPayload>): Promise<void> {\n validateRule(template.rule);\n _validateTimezone(template.timezone);\n _validateDisambiguation(template.disambiguation);\n const normalized: ScheduleTemplate<TPayload> = {\n ...template,\n rule: _normalizeRule(template.rule),\n };\n await this._port.addTemplate(normalized);\n this._changed.notify();\n }\n\n // Partial update. Caller supplies any subset of the template fields plus\n // the id. If the rule changes (kind or any field), all alterations for\n // this template are wiped — same policy as activity-board, made explicit.\n async update(\n delta: { id: string } & Partial<Omit<ScheduleTemplate<TPayload>, \"id\">>\n ): Promise<void> {\n const existing = await this._port.getTemplate(delta.id);\n if (existing == null) throw new Error(`No template with id ${delta.id}`);\n const merged: ScheduleTemplate<TPayload> = {\n ...existing,\n ...delta,\n id: delta.id,\n };\n validateRule(merged.rule);\n _validateTimezone(merged.timezone);\n _validateDisambiguation(merged.disambiguation);\n const updated: ScheduleTemplate<TPayload> = {\n ...merged,\n rule: _normalizeRule(merged.rule),\n };\n const ruleChanged = !_rulesEqual(existing.rule, updated.rule);\n await this._port.updateTemplate(updated);\n if (ruleChanged) {\n await this._port.removeAlterationsForTemplate(updated.id);\n }\n this._changed.notify();\n }\n\n async remove(id: string): Promise<void> {\n await this._port.removeAlterationsForTemplate(id);\n await this._port.removeTemplate(id);\n this._changed.notify();\n }\n\n // Adds or replaces a reschedule alteration for the given occurrence.\n // Caller is responsible for providing a valid occurrenceKey (typically\n // taken from a prior list() result). If `newTimeSpan` is omitted, the\n // rescheduled occurrence inherits the original's UTC duration (i.e. moves\n // in time but keeps the same length).\n async reschedule(\n templateId: string,\n occurrenceKey: string,\n newStart: number,\n newTimeSpan?: TimeSpan,\n payloadOverride?: Partial<TPayload>\n ): Promise<string> {\n if (newTimeSpan != null) validateTimeSpan(newTimeSpan);\n const id = `alt-${templateId}-${occurrenceKey}-${Date.now()}`;\n await this._removeExistingAlteration(templateId, occurrenceKey);\n const alt: Alteration<TPayload> = {\n id,\n templateId,\n occurrenceKey,\n kind: \"reschedule\",\n startTimestamp: newStart,\n timeSpan: newTimeSpan,\n payloadOverride,\n };\n await this._port.addAlteration(alt);\n this._changed.notify();\n return id;\n }\n\n async cancel(templateId: string, occurrenceKey: string): Promise<string> {\n const id = `alt-${templateId}-${occurrenceKey}-${Date.now()}`;\n await this._removeExistingAlteration(templateId, occurrenceKey);\n const alt: Alteration<TPayload> = {\n id,\n templateId,\n occurrenceKey,\n kind: \"cancel\",\n };\n await this._port.addAlteration(alt);\n this._changed.notify();\n return id;\n }\n\n // ─── Internals ──────────────────────────────────────────────\n\n private _generateKeyed(tpl: ScheduleTemplate<TPayload>, windowStart: number, windowEnd: number) {\n const tz = tpl.timezone;\n const dis = tpl.disambiguation ?? \"compatible\";\n let occurrences: Occurrence[];\n switch (tpl.rule.kind) {\n case \"instant\":\n // Absolute start — no wall-clock ambiguity, disambiguation N/A.\n occurrences = generateInstant(tpl.rule, tz, windowStart, windowEnd);\n break;\n case \"localDateTime\":\n occurrences = generateLocalDateTime(tpl.rule, tz, windowStart, windowEnd, dis);\n break;\n case \"anchored\":\n // Absolute stepping — no wall-clock ambiguity, disambiguation N/A.\n occurrences = generateAnchored(tpl.rule, tz, windowStart, windowEnd);\n break;\n case \"daysOfWeek\":\n occurrences = generateDaysOfWeek(tpl.rule, tz, windowStart, windowEnd, dis);\n break;\n case \"weeksOfMonth\":\n occurrences = generateWeeksOfMonth(tpl.rule, tz, windowStart, windowEnd, dis);\n break;\n case \"monthsOfYear\":\n occurrences = generateMonthsOfYear(tpl.rule, tz, windowStart, windowEnd, dis);\n break;\n case \"yearly\":\n occurrences = generateYearly(tpl.rule, tz, windowStart, windowEnd, dis);\n break;\n case \"daysOfMonth\":\n occurrences = generateDaysOfMonth(tpl.rule, tz, windowStart, windowEnd, dis);\n break;\n case \"daysOfYear\":\n occurrences = generateDaysOfYear(tpl.rule, tz, windowStart, windowEnd, dis);\n break;\n default: {\n // Exhaustiveness guard — adding a new kind without handling it here\n // is a compile error.\n const _exhaustive: never = tpl.rule;\n throw new Error(`Unknown rule kind: ${JSON.stringify(_exhaustive)}`);\n }\n }\n return assignOccurrenceKeys(occurrences, tz);\n }\n\n private async _removeExistingAlteration(\n templateId: string,\n occurrenceKey: string\n ): Promise<void> {\n const existing = await this._port.listAlterationsByTemplate(templateId);\n for (const alt of existing) {\n if (alt.occurrenceKey === occurrenceKey) {\n await this._port.removeAlteration(alt.id);\n }\n }\n }\n}\n\n// ─── Validation ───────────────────────────────────────────────\n\n// Returns a copy of `rule` with timeSpan defaulted when omitted, so\n// generators don't have to handle undefined repeatedly.\nfunction _normalizeRule(rule: RecurrenceRule): RecurrenceRule {\n if (rule.timeSpan != null) return rule;\n return { ...rule, timeSpan: { type: \"duration\", ms: 1 } };\n}\n\nfunction _validateTimezone(tz: string): void {\n if (typeof tz !== \"string\" || tz.length === 0) {\n throw new Error(\"ScheduleTemplate.timezone is required (use 'UTC' for zone-free scheduling)\");\n }\n if (!isValidTimeZone(tz)) {\n throw new Error(`Unknown timezone '${tz}' (expected an IANA zone name or 'UTC')`);\n }\n}\n\nconst _DISAMBIGUATIONS: readonly Disambiguation[] = [\n \"compatible\",\n \"earlier\",\n \"later\",\n \"reject\",\n \"skip\",\n];\n\nfunction _validateDisambiguation(d: Disambiguation | undefined): void {\n if (d == null) return;\n if (!_DISAMBIGUATIONS.includes(d)) {\n throw new Error(\n `Unknown disambiguation '${d}' (expected one of ${_DISAMBIGUATIONS.join(\", \")})`\n );\n }\n}\n\nfunction _rulesEqual(a: RecurrenceRule, b: RecurrenceRule): boolean {\n return JSON.stringify(_canonicalize(a)) === JSON.stringify(_canonicalize(b));\n}\n\nfunction _canonicalize(rule: RecurrenceRule): unknown {\n // Sort + dedup any array selectors so reorderings or duplicates don't\n // false-flag a rule change (which would wipe alterations).\n if (rule.kind === \"daysOfWeek\") {\n return { ...rule, days: [...new Set(rule.days)].sort((a, b) => a - b) };\n }\n if (rule.kind === \"weeksOfMonth\") {\n return {\n ...rule,\n days: [...new Set(rule.days)].sort((a, b) => a - b),\n weeks: [...new Set(rule.weeks)].sort((a, b) => a - b),\n };\n }\n if (rule.kind === \"monthsOfYear\") {\n return {\n ...rule,\n months: [...new Set(rule.months)].sort((a, b) => a - b),\n weeks: [...new Set(rule.weeks)].sort((a, b) => a - b),\n days: [...new Set(rule.days)].sort((a, b) => a - b),\n };\n }\n if (rule.kind === \"daysOfMonth\") {\n return {\n ...rule,\n months: [...new Set(rule.months)].sort((a, b) => a - b),\n daysOfMonth: [...new Set(rule.daysOfMonth)].sort((a, b) => a - b),\n };\n }\n if (rule.kind === \"daysOfYear\") {\n return {\n ...rule,\n daysOfYear: [...new Set(rule.daysOfYear)].sort((a, b) => a - b),\n };\n }\n return rule;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { 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;
|
|
5
|
+
//# sourceMappingURL=validate_rule.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate_rule.d.ts","sourceRoot":"","sources":["../src/validate_rule.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAgJ3D,eAAO,MAAM,UAAU,EAAyB,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC;AAIzE,wBAAgB,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,IAAI,CAyCvD;AAID,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,IAAI,QAAQ,GAAG,SAAS,CA6BpF"}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// src/validate_rule.ts
|
|
2
|
+
var DAYS_HINT = "Day numbers 0..6 (0=Sunday, 1=Monday … 6=Saturday)";
|
|
3
|
+
var MONTHS_HINT = "Month numbers 0..11 (0=January … 11=December)";
|
|
4
|
+
var WEEKS_HINT = "week-of-month numbers 1..5 (1=first) or -1..-5 (-1=last)";
|
|
5
|
+
var TIME_OF_DAY_HINT = "ms after local midnight, 0..86_399_999 (7:00 = 25_200_000)";
|
|
6
|
+
var TIMESTAMP_HINT = "a UTC timestamp in ms";
|
|
7
|
+
var SERIES = {
|
|
8
|
+
timeSpan: {
|
|
9
|
+
required: false,
|
|
10
|
+
check: (v) => validateTimeSpan(v)
|
|
11
|
+
},
|
|
12
|
+
seriesStart: {
|
|
13
|
+
required: true,
|
|
14
|
+
check: timestamp
|
|
15
|
+
},
|
|
16
|
+
seriesEnd: {
|
|
17
|
+
required: true,
|
|
18
|
+
check: nullable(timestamp, "null (no end)")
|
|
19
|
+
},
|
|
20
|
+
count: {
|
|
21
|
+
required: false,
|
|
22
|
+
check: nullable(int(0, Number.MAX_SAFE_INTEGER, "a whole number >= 0"), "null")
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var TIME_OF_DAY = {
|
|
26
|
+
required: true,
|
|
27
|
+
check: int(0, 86399999, TIME_OF_DAY_HINT)
|
|
28
|
+
};
|
|
29
|
+
var KINDS = {
|
|
30
|
+
instant: {
|
|
31
|
+
name: "InstantRule",
|
|
32
|
+
fields: {
|
|
33
|
+
startOn: {
|
|
34
|
+
required: true,
|
|
35
|
+
check: timestamp
|
|
36
|
+
},
|
|
37
|
+
timeSpan: {
|
|
38
|
+
required: false,
|
|
39
|
+
check: (v) => validateTimeSpan(v)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
localDateTime: {
|
|
44
|
+
name: "LocalDateTimeRule",
|
|
45
|
+
fields: {
|
|
46
|
+
year: {
|
|
47
|
+
required: true,
|
|
48
|
+
check: int(1, 9999, "a year such as 2026")
|
|
49
|
+
},
|
|
50
|
+
month: {
|
|
51
|
+
required: true,
|
|
52
|
+
check: int(0, 11, MONTHS_HINT)
|
|
53
|
+
},
|
|
54
|
+
day: {
|
|
55
|
+
required: true,
|
|
56
|
+
check: int(1, 31, "a day of the month 1..31")
|
|
57
|
+
},
|
|
58
|
+
timeOfDay: TIME_OF_DAY,
|
|
59
|
+
timeSpan: {
|
|
60
|
+
required: false,
|
|
61
|
+
check: (v) => validateTimeSpan(v)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
anchored: {
|
|
66
|
+
name: "AnchoredRule",
|
|
67
|
+
fields: {
|
|
68
|
+
anchorDate: {
|
|
69
|
+
required: true,
|
|
70
|
+
check: timestamp
|
|
71
|
+
},
|
|
72
|
+
intervalMs: {
|
|
73
|
+
required: true,
|
|
74
|
+
check: positive("an interval in ms > 0")
|
|
75
|
+
},
|
|
76
|
+
...SERIES
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
daysOfWeek: {
|
|
80
|
+
name: "DaysOfWeekRule",
|
|
81
|
+
fields: {
|
|
82
|
+
days: {
|
|
83
|
+
required: true,
|
|
84
|
+
check: intList(0, 6, DAYS_HINT)
|
|
85
|
+
},
|
|
86
|
+
weekInterval: {
|
|
87
|
+
required: true,
|
|
88
|
+
check: int(1, Number.MAX_SAFE_INTEGER, "a whole number >= 1 (1 = every week)")
|
|
89
|
+
},
|
|
90
|
+
timeOfDay: TIME_OF_DAY,
|
|
91
|
+
...SERIES
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
weeksOfMonth: {
|
|
95
|
+
name: "WeeksOfMonthRule",
|
|
96
|
+
fields: {
|
|
97
|
+
days: {
|
|
98
|
+
required: true,
|
|
99
|
+
check: intList(0, 6, DAYS_HINT)
|
|
100
|
+
},
|
|
101
|
+
weeks: {
|
|
102
|
+
required: true,
|
|
103
|
+
check: weekList
|
|
104
|
+
},
|
|
105
|
+
monthInterval: {
|
|
106
|
+
required: true,
|
|
107
|
+
check: int(1, Number.MAX_SAFE_INTEGER, "a whole number >= 1 (1 = every month)")
|
|
108
|
+
},
|
|
109
|
+
timeOfDay: TIME_OF_DAY,
|
|
110
|
+
...SERIES
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
monthsOfYear: {
|
|
114
|
+
name: "MonthsOfYearRule",
|
|
115
|
+
fields: {
|
|
116
|
+
months: {
|
|
117
|
+
required: true,
|
|
118
|
+
check: intList(0, 11, MONTHS_HINT)
|
|
119
|
+
},
|
|
120
|
+
weeks: {
|
|
121
|
+
required: true,
|
|
122
|
+
check: weekList
|
|
123
|
+
},
|
|
124
|
+
days: {
|
|
125
|
+
required: true,
|
|
126
|
+
check: intList(0, 6, DAYS_HINT)
|
|
127
|
+
},
|
|
128
|
+
yearInterval: {
|
|
129
|
+
required: true,
|
|
130
|
+
check: int(1, Number.MAX_SAFE_INTEGER, "a whole number >= 1 (1 = every year)")
|
|
131
|
+
},
|
|
132
|
+
timeOfDay: TIME_OF_DAY,
|
|
133
|
+
...SERIES
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
yearly: {
|
|
137
|
+
name: "YearlyRule",
|
|
138
|
+
fields: {
|
|
139
|
+
month: {
|
|
140
|
+
required: true,
|
|
141
|
+
check: int(0, 11, MONTHS_HINT)
|
|
142
|
+
},
|
|
143
|
+
day: {
|
|
144
|
+
required: true,
|
|
145
|
+
check: int(1, 31, "a day of the month 1..31")
|
|
146
|
+
},
|
|
147
|
+
yearInterval: {
|
|
148
|
+
required: true,
|
|
149
|
+
check: int(1, Number.MAX_SAFE_INTEGER, "a whole number >= 1 (1 = every year)")
|
|
150
|
+
},
|
|
151
|
+
timeOfDay: TIME_OF_DAY,
|
|
152
|
+
...SERIES
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
daysOfMonth: {
|
|
156
|
+
name: "DaysOfMonthRule",
|
|
157
|
+
fields: {
|
|
158
|
+
months: {
|
|
159
|
+
required: true,
|
|
160
|
+
check: intList(0, 11, MONTHS_HINT)
|
|
161
|
+
},
|
|
162
|
+
daysOfMonth: {
|
|
163
|
+
required: true,
|
|
164
|
+
check: intList(1, 31, "days of the month 1..31")
|
|
165
|
+
},
|
|
166
|
+
yearInterval: {
|
|
167
|
+
required: true,
|
|
168
|
+
check: int(1, Number.MAX_SAFE_INTEGER, "a whole number >= 1 (1 = every year)")
|
|
169
|
+
},
|
|
170
|
+
timeOfDay: TIME_OF_DAY,
|
|
171
|
+
...SERIES
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
daysOfYear: {
|
|
175
|
+
name: "DaysOfYearRule",
|
|
176
|
+
fields: {
|
|
177
|
+
daysOfYear: {
|
|
178
|
+
required: true,
|
|
179
|
+
check: intList(1, 366, "days of the year 1..366")
|
|
180
|
+
},
|
|
181
|
+
yearInterval: {
|
|
182
|
+
required: true,
|
|
183
|
+
check: int(1, Number.MAX_SAFE_INTEGER, "a whole number >= 1 (1 = every year)")
|
|
184
|
+
},
|
|
185
|
+
timeOfDay: TIME_OF_DAY,
|
|
186
|
+
...SERIES
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
var RULE_KINDS = Object.keys(KINDS);
|
|
191
|
+
function validateRule(rule) {
|
|
192
|
+
const raw = rule;
|
|
193
|
+
if (typeof raw !== "object" || raw == null || Array.isArray(raw)) throw new Error(`A rule must be an object with a "kind" (one of ${RULE_KINDS.join(", ")})`);
|
|
194
|
+
const obj = raw;
|
|
195
|
+
const kind = KINDS[obj.kind];
|
|
196
|
+
if (kind == null) throw new Error(`Unknown rule kind ${JSON.stringify(obj.kind)} (expected one of ${RULE_KINDS.join(", ")})`);
|
|
197
|
+
const known = Object.keys(kind.fields);
|
|
198
|
+
for (const key of Object.keys(obj)) if (key !== "kind" && !known.includes(key)) throw new Error(`${kind.name} has no field "${key}" (its fields are: ${known.join(", ")})`);
|
|
199
|
+
for (const [field, spec] of Object.entries(kind.fields)) {
|
|
200
|
+
const label = `${kind.name}.${field}`;
|
|
201
|
+
if (obj[field] === void 0) {
|
|
202
|
+
if (spec.required) throw new Error(`${label} is required: ${describe(spec, label)}`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
spec.check(obj[field], label);
|
|
206
|
+
}
|
|
207
|
+
const seriesStart = obj.seriesStart;
|
|
208
|
+
const seriesEnd = obj.seriesEnd;
|
|
209
|
+
if (seriesStart != null && seriesEnd != null && seriesEnd <= seriesStart) throw new Error(`${kind.name}.seriesEnd must be > seriesStart`);
|
|
210
|
+
}
|
|
211
|
+
function validateTimeSpan(span) {
|
|
212
|
+
if (span === void 0) return;
|
|
213
|
+
const form = "{ \"type\": \"duration\", \"ms\": <ms >= 1> } or { \"type\": \"hourSpan\", \"hours\": <hours > 0> }";
|
|
214
|
+
if (typeof span !== "object" || span == null) throw new Error(`timeSpan must be ${form}`);
|
|
215
|
+
const s = span;
|
|
216
|
+
if (s.type === "duration") {
|
|
217
|
+
if (typeof s.ms !== "number" || !Number.isFinite(s.ms) || s.ms < 1) throw new Error(`timeSpan.ms must be >= 1 if supplied (got ${String(s.ms)})`);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (s.type === "hourSpan") {
|
|
221
|
+
if (typeof s.hours !== "number" || !Number.isFinite(s.hours) || s.hours <= 0) throw new Error(`timeSpan.hours must be > 0 (got ${String(s.hours)})`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
throw new Error(`timeSpan must be ${form} (got type ${JSON.stringify(s.type)})`);
|
|
225
|
+
}
|
|
226
|
+
function describe(spec, label) {
|
|
227
|
+
try {
|
|
228
|
+
spec.check(void 0, label);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
231
|
+
const at = msg.indexOf(" must be ");
|
|
232
|
+
if (at >= 0) return msg.slice(at + 9).replace(/ \(got [^)]*\)$/, "");
|
|
233
|
+
}
|
|
234
|
+
return "see the rule's type";
|
|
235
|
+
}
|
|
236
|
+
function show(v) {
|
|
237
|
+
return v === void 0 ? "nothing" : JSON.stringify(v);
|
|
238
|
+
}
|
|
239
|
+
function isInt(v) {
|
|
240
|
+
return typeof v === "number" && Number.isInteger(v);
|
|
241
|
+
}
|
|
242
|
+
function int(min, max, hint) {
|
|
243
|
+
return (v, field) => {
|
|
244
|
+
if (!isInt(v) || v < min || v > max) throw new Error(`${field} must be ${hint} (got ${show(v)})`);
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function positive(hint) {
|
|
248
|
+
return (v, field) => {
|
|
249
|
+
if (typeof v !== "number" || !Number.isFinite(v) || v <= 0) throw new Error(`${field} must be ${hint} (got ${show(v)})`);
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function timestamp(v, field) {
|
|
253
|
+
if (typeof v !== "number" || !Number.isFinite(v)) throw new Error(`${field} must be ${TIMESTAMP_HINT} (got ${show(v)})`);
|
|
254
|
+
}
|
|
255
|
+
function nullable(check, nullHint) {
|
|
256
|
+
return (v, field) => {
|
|
257
|
+
if (v === null) return;
|
|
258
|
+
try {
|
|
259
|
+
check(v, field);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
262
|
+
throw new Error(msg.replace(/ \(got /, ` or ${nullHint} (got `));
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function intList(min, max, hint) {
|
|
267
|
+
return (v, field) => {
|
|
268
|
+
const form = `a non-empty array of ${hint}`;
|
|
269
|
+
if (!Array.isArray(v) || v.length === 0) throw new Error(`${field} must be ${form} (got ${show(v)})`);
|
|
270
|
+
for (const x of v) if (!isInt(x) || x < min || x > max) throw new Error(`${field} must be ${form} (got ${show(v)}; ${show(x)} is out of range)`);
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function weekList(v, field) {
|
|
274
|
+
const form = `a non-empty array of ${WEEKS_HINT}`;
|
|
275
|
+
if (!Array.isArray(v) || v.length === 0) throw new Error(`${field} must be ${form} (got ${show(v)})`);
|
|
276
|
+
for (const w of v) if (!isInt(w) || w === 0 || w < -5 || w > 5) throw new Error(`${field} must be ${form} (got ${show(v)}; ${show(w)} is out of range)`);
|
|
277
|
+
}
|
|
278
|
+
export { RULE_KINDS, validateRule, validateTimeSpan };
|
|
279
|
+
|
|
280
|
+
//# sourceMappingURL=validate_rule.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate_rule.js","names":[],"sources":["../src/validate_rule.ts"],"sourcesContent":["import type { RecurrenceRule, TimeSpan } from \"./types.js\";\n\n// ─── Rule validation ──────────────────────────────────────────\n//\n// A rule often comes from outside the type system: JSON in a database, a\n// request body, or an LLM tool call. So validation checks the WHOLE shape —\n// every required field present, of the right type, in range, and no fields\n// the kind does not have — and throws an Error that names the field and the\n// form it expects. A bad rule never reaches a generator, where it would crash\n// with \"Cannot read properties of undefined\" or \"Invalid time value\".\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\nconst DAYS_HINT = \"Day numbers 0..6 (0=Sunday, 1=Monday … 6=Saturday)\";\nconst MONTHS_HINT = \"Month numbers 0..11 (0=January … 11=December)\";\nconst WEEKS_HINT = \"week-of-month numbers 1..5 (1=first) or -1..-5 (-1=last)\";\nconst TIME_OF_DAY_HINT = \"ms after local midnight, 0..86_399_999 (7:00 = 25_200_000)\";\nconst TIMESTAMP_HINT = \"a UTC timestamp in ms\";\n\n// One field of a rule kind: whether it must be there, and how to check it.\ninterface FieldSpec {\n required: boolean;\n check: (value: unknown, field: string) => void;\n}\n\n// The common series fields every repeating kind has.\nconst SERIES: Record<string, FieldSpec> = {\n timeSpan: { required: false, check: (v) => validateTimeSpan(v) },\n seriesStart: { required: true, check: timestamp },\n seriesEnd: { required: true, check: nullable(timestamp, \"null (no end)\") },\n count: {\n required: false,\n check: nullable(int(0, Number.MAX_SAFE_INTEGER, \"a whole number >= 0\"), \"null\"),\n },\n};\n\nconst TIME_OF_DAY: FieldSpec = { required: true, check: int(0, DAY_MS - 1, TIME_OF_DAY_HINT) };\n\nconst KINDS: Record<RecurrenceRule[\"kind\"], { name: string; fields: Record<string, FieldSpec> }> = {\n instant: {\n name: \"InstantRule\",\n fields: {\n startOn: { required: true, check: timestamp },\n timeSpan: { required: false, check: (v) => validateTimeSpan(v) },\n },\n },\n localDateTime: {\n name: \"LocalDateTimeRule\",\n fields: {\n year: { required: true, check: int(1, 9999, \"a year such as 2026\") },\n month: { required: true, check: int(0, 11, MONTHS_HINT) },\n day: { required: true, check: int(1, 31, \"a day of the month 1..31\") },\n timeOfDay: TIME_OF_DAY,\n timeSpan: { required: false, check: (v) => validateTimeSpan(v) },\n },\n },\n anchored: {\n name: \"AnchoredRule\",\n fields: {\n anchorDate: { required: true, check: timestamp },\n intervalMs: { required: true, check: positive(\"an interval in ms > 0\") },\n ...SERIES,\n },\n },\n daysOfWeek: {\n name: \"DaysOfWeekRule\",\n fields: {\n days: { required: true, check: intList(0, 6, DAYS_HINT) },\n weekInterval: {\n required: true,\n check: int(1, Number.MAX_SAFE_INTEGER, \"a whole number >= 1 (1 = every week)\"),\n },\n timeOfDay: TIME_OF_DAY,\n ...SERIES,\n },\n },\n weeksOfMonth: {\n name: \"WeeksOfMonthRule\",\n fields: {\n days: { required: true, check: intList(0, 6, DAYS_HINT) },\n weeks: { required: true, check: weekList },\n monthInterval: {\n required: true,\n check: int(1, Number.MAX_SAFE_INTEGER, \"a whole number >= 1 (1 = every month)\"),\n },\n timeOfDay: TIME_OF_DAY,\n ...SERIES,\n },\n },\n monthsOfYear: {\n name: \"MonthsOfYearRule\",\n fields: {\n months: { required: true, check: intList(0, 11, MONTHS_HINT) },\n weeks: { required: true, check: weekList },\n days: { required: true, check: intList(0, 6, DAYS_HINT) },\n yearInterval: {\n required: true,\n check: int(1, Number.MAX_SAFE_INTEGER, \"a whole number >= 1 (1 = every year)\"),\n },\n timeOfDay: TIME_OF_DAY,\n ...SERIES,\n },\n },\n yearly: {\n name: \"YearlyRule\",\n fields: {\n month: { required: true, check: int(0, 11, MONTHS_HINT) },\n day: { required: true, check: int(1, 31, \"a day of the month 1..31\") },\n yearInterval: {\n required: true,\n check: int(1, Number.MAX_SAFE_INTEGER, \"a whole number >= 1 (1 = every year)\"),\n },\n timeOfDay: TIME_OF_DAY,\n ...SERIES,\n },\n },\n daysOfMonth: {\n name: \"DaysOfMonthRule\",\n fields: {\n months: { required: true, check: intList(0, 11, MONTHS_HINT) },\n daysOfMonth: { required: true, check: intList(1, 31, \"days of the month 1..31\") },\n yearInterval: {\n required: true,\n check: int(1, Number.MAX_SAFE_INTEGER, \"a whole number >= 1 (1 = every year)\"),\n },\n timeOfDay: TIME_OF_DAY,\n ...SERIES,\n },\n },\n daysOfYear: {\n name: \"DaysOfYearRule\",\n fields: {\n daysOfYear: { required: true, check: intList(1, 366, \"days of the year 1..366\") },\n yearInterval: {\n required: true,\n check: int(1, Number.MAX_SAFE_INTEGER, \"a whole number >= 1 (1 = every year)\"),\n },\n timeOfDay: TIME_OF_DAY,\n ...SERIES,\n },\n },\n};\n\n// The rule kinds, in the order the types declare them.\nexport const RULE_KINDS = Object.keys(KINDS) as RecurrenceRule[\"kind\"][];\n\n// Throws when `rule` is not a valid RecurrenceRule. The message names the\n// first wrong field and the form it expects.\nexport function validateRule(rule: RecurrenceRule): void {\n const raw: unknown = rule;\n\n if (typeof raw !== \"object\" || raw == null || Array.isArray(raw)) {\n throw new Error(`A rule must be an object with a \"kind\" (one of ${RULE_KINDS.join(\", \")})`);\n }\n\n const obj = raw as Record<string, unknown>;\n const kind = KINDS[obj.kind as RecurrenceRule[\"kind\"]];\n\n if (kind == null) {\n throw new Error(\n `Unknown rule kind ${JSON.stringify(obj.kind)} (expected one of ${RULE_KINDS.join(\", \")})`\n );\n }\n\n const known = Object.keys(kind.fields);\n\n for (const key of Object.keys(obj)) {\n if (key !== \"kind\" && !known.includes(key)) {\n throw new Error(`${kind.name} has no field \"${key}\" (its fields are: ${known.join(\", \")})`);\n }\n }\n\n for (const [field, spec] of Object.entries(kind.fields)) {\n const label = `${kind.name}.${field}`;\n\n if (obj[field] === undefined) {\n if (spec.required) throw new Error(`${label} is required: ${describe(spec, label)}`);\n continue;\n }\n\n spec.check(obj[field], label);\n }\n\n const seriesStart = obj.seriesStart as number | undefined;\n const seriesEnd = obj.seriesEnd as number | null | undefined;\n\n if (seriesStart != null && seriesEnd != null && seriesEnd <= seriesStart) {\n throw new Error(`${kind.name}.seriesEnd must be > seriesStart`);\n }\n}\n\n// Throws when `span` is not a TimeSpan. Undefined is valid (the rule's\n// default span applies).\nexport function validateTimeSpan(span: unknown): asserts span is TimeSpan | undefined {\n if (span === undefined) return;\n\n const form =\n '{ \"type\": \"duration\", \"ms\": <ms >= 1> } or { \"type\": \"hourSpan\", \"hours\": <hours > 0> }';\n\n if (typeof span !== \"object\" || span == null) {\n throw new Error(`timeSpan must be ${form}`);\n }\n\n const s = span as Record<string, unknown>;\n\n if (s.type === \"duration\") {\n if (typeof s.ms !== \"number\" || !Number.isFinite(s.ms) || s.ms < 1) {\n throw new Error(`timeSpan.ms must be >= 1 if supplied (got ${String(s.ms)})`);\n }\n\n return;\n }\n\n if (s.type === \"hourSpan\") {\n if (typeof s.hours !== \"number\" || !Number.isFinite(s.hours) || s.hours <= 0) {\n throw new Error(`timeSpan.hours must be > 0 (got ${String(s.hours)})`);\n }\n\n return;\n }\n\n throw new Error(`timeSpan must be ${form} (got type ${JSON.stringify(s.type)})`);\n}\n\n// ─── Field checks ─────────────────────────────────────────────\n\n// What the field should hold, for \"is required\" messages: each check is\n// called on undefined and its message reused.\nfunction describe(spec: FieldSpec, label: string): string {\n try {\n spec.check(undefined, label);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n const at = msg.indexOf(\" must be \");\n\n if (at >= 0) return msg.slice(at + \" must be \".length).replace(/ \\(got [^)]*\\)$/, \"\");\n }\n\n return \"see the rule's type\";\n}\n\nfunction show(v: unknown): string {\n return v === undefined ? \"nothing\" : JSON.stringify(v);\n}\n\nfunction isInt(v: unknown): v is number {\n return typeof v === \"number\" && Number.isInteger(v);\n}\n\nfunction int(min: number, max: number, hint: string) {\n return (v: unknown, field: string): void => {\n if (!isInt(v) || v < min || v > max)\n throw new Error(`${field} must be ${hint} (got ${show(v)})`);\n };\n}\n\nfunction positive(hint: string) {\n return (v: unknown, field: string): void => {\n if (typeof v !== \"number\" || !Number.isFinite(v) || v <= 0) {\n throw new Error(`${field} must be ${hint} (got ${show(v)})`);\n }\n };\n}\n\nfunction timestamp(v: unknown, field: string): void {\n if (typeof v !== \"number\" || !Number.isFinite(v)) {\n throw new Error(`${field} must be ${TIMESTAMP_HINT} (got ${show(v)})`);\n }\n}\n\nfunction nullable(check: (v: unknown, field: string) => void, nullHint: string) {\n return (v: unknown, field: string): void => {\n if (v === null) return;\n\n try {\n check(v, field);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n\n throw new Error(msg.replace(/ \\(got /, ` or ${nullHint} (got `));\n }\n };\n}\n\nfunction intList(min: number, max: number, hint: string) {\n return (v: unknown, field: string): void => {\n const form = `a non-empty array of ${hint}`;\n\n if (!Array.isArray(v) || v.length === 0)\n throw new Error(`${field} must be ${form} (got ${show(v)})`);\n\n for (const x of v) {\n if (!isInt(x) || x < min || x > max) {\n throw new Error(`${field} must be ${form} (got ${show(v)}; ${show(x)} is out of range)`);\n }\n }\n };\n}\n\nfunction weekList(v: unknown, field: string): void {\n const form = `a non-empty array of ${WEEKS_HINT}`;\n\n if (!Array.isArray(v) || v.length === 0)\n throw new Error(`${field} must be ${form} (got ${show(v)})`);\n\n for (const w of v) {\n if (!isInt(w) || w === 0 || w < -5 || w > 5) {\n throw new Error(`${field} must be ${form} (got ${show(v)}; ${show(w)} is out of range)`);\n }\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@j13b/scheduler",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "A timezone-aware recurrence and dispatch engine: a pure-data rule DSL, IANA timezones, DST-aware generators and a cron-style dispatcher.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Jared J Barnes",
|
|
@@ -21,7 +21,13 @@
|
|
|
21
21
|
},
|
|
22
22
|
"./package.json": "./package.json"
|
|
23
23
|
},
|
|
24
|
-
"files": [
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src",
|
|
27
|
+
"tsconfig.json",
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
25
31
|
"scripts": {
|
|
26
32
|
"build": "vite build && node scripts/build-cjs.mjs",
|
|
27
33
|
"check:types": "tsc --noEmit",
|