@j13b/scheduler 0.0.0-stage → 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.
Files changed (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -2
  3. package/dist/alterations.d.ts +6 -0
  4. package/dist/alterations.d.ts.map +1 -0
  5. package/dist/alterations.js +61 -0
  6. package/dist/alterations.js.map +1 -0
  7. package/dist/cjs/alterations.d.ts +5 -0
  8. package/dist/cjs/alterations.js +96 -0
  9. package/dist/cjs/dispatcher.d.ts +38 -0
  10. package/dist/cjs/dispatcher.js +165 -0
  11. package/dist/cjs/dispatcher_store.d.ts +9 -0
  12. package/dist/cjs/dispatcher_store.js +15 -0
  13. package/dist/cjs/generators/anchored.d.ts +3 -0
  14. package/dist/cjs/generators/anchored.js +57 -0
  15. package/dist/cjs/generators/days_of_month.d.ts +3 -0
  16. package/dist/cjs/generators/days_of_month.js +76 -0
  17. package/dist/cjs/generators/days_of_week.d.ts +5 -0
  18. package/dist/cjs/generators/days_of_week.js +65 -0
  19. package/dist/cjs/generators/days_of_year.d.ts +3 -0
  20. package/dist/cjs/generators/days_of_year.js +70 -0
  21. package/dist/cjs/generators/instant.d.ts +3 -0
  22. package/dist/cjs/generators/instant.js +15 -0
  23. package/dist/cjs/generators/local_date_time.d.ts +3 -0
  24. package/dist/cjs/generators/local_date_time.js +31 -0
  25. package/dist/cjs/generators/months_of_year.d.ts +3 -0
  26. package/dist/cjs/generators/months_of_year.js +109 -0
  27. package/dist/cjs/generators/types.d.ts +9 -0
  28. package/dist/cjs/generators/types.js +19 -0
  29. package/dist/cjs/generators/weeks_of_month.d.ts +3 -0
  30. package/dist/cjs/generators/weeks_of_month.js +99 -0
  31. package/dist/cjs/generators/yearly.d.ts +3 -0
  32. package/dist/cjs/generators/yearly.js +52 -0
  33. package/dist/cjs/in_memory_port.d.ts +18 -0
  34. package/dist/cjs/in_memory_port.js +59 -0
  35. package/dist/cjs/index.d.ts +7 -0
  36. package/dist/cjs/index.js +18 -0
  37. package/dist/cjs/occurrence_key.d.ts +5 -0
  38. package/dist/cjs/occurrence_key.js +21 -0
  39. package/dist/cjs/package.json +3 -0
  40. package/dist/cjs/port.d.ts +15 -0
  41. package/dist/cjs/port.js +2 -0
  42. package/dist/cjs/scheduler.d.ts +23 -0
  43. package/dist/cjs/scheduler.js +302 -0
  44. package/dist/cjs/types.d.ts +149 -0
  45. package/dist/cjs/types.js +58 -0
  46. package/dist/cjs/tz.d.ts +22 -0
  47. package/dist/cjs/tz.js +197 -0
  48. package/dist/cjs/validate_rule.d.ts +4 -0
  49. package/dist/cjs/validate_rule.js +267 -0
  50. package/dist/dispatcher.d.ts +39 -0
  51. package/dist/dispatcher.d.ts.map +1 -0
  52. package/dist/dispatcher.js +111 -0
  53. package/dist/dispatcher.js.map +1 -0
  54. package/dist/dispatcher_store.d.ts +10 -0
  55. package/dist/dispatcher_store.d.ts.map +1 -0
  56. package/dist/dispatcher_store.js +13 -0
  57. package/dist/dispatcher_store.js.map +1 -0
  58. package/dist/generators/anchored.d.ts +4 -0
  59. package/dist/generators/anchored.d.ts.map +1 -0
  60. package/dist/generators/anchored.js +42 -0
  61. package/dist/generators/anchored.js.map +1 -0
  62. package/dist/generators/days_of_month.d.ts +4 -0
  63. package/dist/generators/days_of_month.d.ts.map +1 -0
  64. package/dist/generators/days_of_month.js +53 -0
  65. package/dist/generators/days_of_month.js.map +1 -0
  66. package/dist/generators/days_of_week.d.ts +6 -0
  67. package/dist/generators/days_of_week.d.ts.map +1 -0
  68. package/dist/generators/days_of_week.js +43 -0
  69. package/dist/generators/days_of_week.js.map +1 -0
  70. package/dist/generators/days_of_year.d.ts +4 -0
  71. package/dist/generators/days_of_year.d.ts.map +1 -0
  72. package/dist/generators/days_of_year.js +50 -0
  73. package/dist/generators/days_of_year.js.map +1 -0
  74. package/dist/generators/instant.d.ts +4 -0
  75. package/dist/generators/instant.d.ts.map +1 -0
  76. package/dist/generators/instant.js +15 -0
  77. package/dist/generators/instant.js.map +1 -0
  78. package/dist/generators/local_date_time.d.ts +4 -0
  79. package/dist/generators/local_date_time.d.ts.map +1 -0
  80. package/dist/generators/local_date_time.js +20 -0
  81. package/dist/generators/local_date_time.js.map +1 -0
  82. package/dist/generators/months_of_year.d.ts +4 -0
  83. package/dist/generators/months_of_year.d.ts.map +1 -0
  84. package/dist/generators/months_of_year.js +64 -0
  85. package/dist/generators/months_of_year.js.map +1 -0
  86. package/dist/generators/types.d.ts +10 -0
  87. package/dist/generators/types.d.ts.map +1 -0
  88. package/dist/generators/types.js +14 -0
  89. package/dist/generators/types.js.map +1 -0
  90. package/dist/generators/weeks_of_month.d.ts +4 -0
  91. package/dist/generators/weeks_of_month.d.ts.map +1 -0
  92. package/dist/generators/weeks_of_month.js +61 -0
  93. package/dist/generators/weeks_of_month.js.map +1 -0
  94. package/dist/generators/yearly.d.ts +4 -0
  95. package/dist/generators/yearly.d.ts.map +1 -0
  96. package/dist/generators/yearly.js +37 -0
  97. package/dist/generators/yearly.js.map +1 -0
  98. package/dist/in_memory_port.d.ts +19 -0
  99. package/dist/in_memory_port.d.ts.map +1 -0
  100. package/dist/in_memory_port.js +45 -0
  101. package/dist/in_memory_port.js.map +1 -0
  102. package/dist/index.d.ts +8 -0
  103. package/dist/index.d.ts.map +1 -0
  104. package/dist/index.js +7 -0
  105. package/dist/occurrence_key.d.ts +6 -0
  106. package/dist/occurrence_key.d.ts.map +1 -0
  107. package/dist/occurrence_key.js +25 -0
  108. package/dist/occurrence_key.js.map +1 -0
  109. package/dist/port.d.ts +16 -0
  110. package/dist/port.d.ts.map +1 -0
  111. package/dist/scheduler.d.ts +24 -0
  112. package/dist/scheduler.d.ts.map +1 -0
  113. package/dist/scheduler.js +241 -0
  114. package/dist/scheduler.js.map +1 -0
  115. package/dist/types.d.ts +150 -0
  116. package/dist/types.d.ts.map +1 -0
  117. package/dist/types.js +29 -0
  118. package/dist/types.js.map +1 -0
  119. package/dist/tz.d.ts +23 -0
  120. package/dist/tz.d.ts.map +1 -0
  121. package/dist/tz.js +131 -0
  122. package/dist/tz.js.map +1 -0
  123. package/dist/validate_rule.d.ts +5 -0
  124. package/dist/validate_rule.d.ts.map +1 -0
  125. package/dist/validate_rule.js +280 -0
  126. package/dist/validate_rule.js.map +1 -0
  127. package/package.json +76 -4
  128. package/src/__tests__/alterations.test.ts +177 -0
  129. package/src/__tests__/anchored.test.ts +107 -0
  130. package/src/__tests__/days_of_month.test.ts +162 -0
  131. package/src/__tests__/days_of_week.test.ts +134 -0
  132. package/src/__tests__/days_of_year.test.ts +158 -0
  133. package/src/__tests__/dispatcher.test.ts +317 -0
  134. package/src/__tests__/instant.test.ts +50 -0
  135. package/src/__tests__/local_date_time.test.ts +139 -0
  136. package/src/__tests__/months_of_year.test.ts +229 -0
  137. package/src/__tests__/scheduler.test.ts +888 -0
  138. package/src/__tests__/tz.test.ts +201 -0
  139. package/src/__tests__/validate_rule.test.ts +190 -0
  140. package/src/__tests__/weeks_of_month.test.ts +195 -0
  141. package/src/__tests__/yearly.test.ts +108 -0
  142. package/src/alterations.ts +108 -0
  143. package/src/dispatcher.ts +184 -0
  144. package/src/dispatcher_store.ts +26 -0
  145. package/src/generators/anchored.ts +69 -0
  146. package/src/generators/days_of_month.ts +100 -0
  147. package/src/generators/days_of_week.ts +101 -0
  148. package/src/generators/days_of_year.ts +88 -0
  149. package/src/generators/instant.ts +18 -0
  150. package/src/generators/local_date_time.ts +38 -0
  151. package/src/generators/months_of_year.ts +143 -0
  152. package/src/generators/types.ts +26 -0
  153. package/src/generators/weeks_of_month.ts +133 -0
  154. package/src/generators/yearly.ts +73 -0
  155. package/src/in_memory_port.ts +70 -0
  156. package/src/index.ts +25 -0
  157. package/src/occurrence_key.ts +42 -0
  158. package/src/port.ts +31 -0
  159. package/src/scheduler.ts +353 -0
  160. package/src/types.ts +369 -0
  161. package/src/tz.ts +246 -0
  162. package/src/validate_rule.ts +311 -0
  163. package/tsconfig.json +25 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,15 @@
1
+ import type { Alteration, ScheduleTemplate } from "./types.js";
2
+ export interface SchedulerPort<TPayload extends {
3
+ type: string;
4
+ }> {
5
+ listTemplates(start: number, end: number): Promise<ScheduleTemplate<TPayload>[]>;
6
+ getTemplate(id: string): Promise<ScheduleTemplate<TPayload> | null>;
7
+ addTemplate(template: ScheduleTemplate<TPayload>): Promise<void>;
8
+ updateTemplate(template: ScheduleTemplate<TPayload>): Promise<void>;
9
+ removeTemplate(id: string): Promise<void>;
10
+ listAlterations(start: number, end: number): Promise<Alteration<TPayload>[]>;
11
+ listAlterationsByTemplate(templateId: string): Promise<Alteration<TPayload>[]>;
12
+ addAlteration(alteration: Alteration<TPayload>): Promise<void>;
13
+ removeAlteration(id: string): Promise<void>;
14
+ removeAlterationsForTemplate(templateId: string): Promise<void>;
15
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,23 @@
1
+ import { type IEventBroadcast } from "@j13b/state";
2
+ import type { SchedulerPort } from "./port.js";
3
+ import type { ScheduleTemplate, ScheduledEvent, TimeSpan } from "./types.js";
4
+ export declare class Scheduler<TPayload extends {
5
+ type: string;
6
+ }> {
7
+ private readonly _port;
8
+ private readonly _changed;
9
+ constructor(port: SchedulerPort<TPayload>);
10
+ get changed(): IEventBroadcast<void>;
11
+ list(windowStart: number, windowEnd: number): Promise<ScheduledEvent<TPayload>[]>;
12
+ next(templateId: string, after: number): Promise<ScheduledEvent<TPayload> | null>;
13
+ nextDue(after: number, limit?: number): Promise<ScheduledEvent<TPayload>[]>;
14
+ add(template: ScheduleTemplate<TPayload>): Promise<void>;
15
+ update(delta: {
16
+ id: string;
17
+ } & Partial<Omit<ScheduleTemplate<TPayload>, "id">>): Promise<void>;
18
+ remove(id: string): Promise<void>;
19
+ reschedule(templateId: string, occurrenceKey: string, newStart: number, newTimeSpan?: TimeSpan, payloadOverride?: Partial<TPayload>): Promise<string>;
20
+ cancel(templateId: string, occurrenceKey: string): Promise<string>;
21
+ private _generateKeyed;
22
+ private _removeExistingAlteration;
23
+ }
@@ -0,0 +1,302 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Scheduler = void 0;
4
+ const state_1 = require("@j13b/state");
5
+ const alterations_js_1 = require("./alterations.js");
6
+ const anchored_js_1 = require("./generators/anchored.js");
7
+ const days_of_month_js_1 = require("./generators/days_of_month.js");
8
+ const days_of_week_js_1 = require("./generators/days_of_week.js");
9
+ const days_of_year_js_1 = require("./generators/days_of_year.js");
10
+ const instant_js_1 = require("./generators/instant.js");
11
+ const local_date_time_js_1 = require("./generators/local_date_time.js");
12
+ const months_of_year_js_1 = require("./generators/months_of_year.js");
13
+ const weeks_of_month_js_1 = require("./generators/weeks_of_month.js");
14
+ const yearly_js_1 = require("./generators/yearly.js");
15
+ const occurrence_key_js_1 = require("./occurrence_key.js");
16
+ const tz_js_1 = require("./tz.js");
17
+ const validate_rule_js_1 = require("./validate_rule.js");
18
+ const DAY_MS = 24 * 60 * 60 * 1000;
19
+ // Search horizon for next() / nextDue() before giving up. Five years covers
20
+ // every realistic use; if you need longer, call list() with an explicit
21
+ // window.
22
+ const NEXT_SEARCH_HORIZON_MS = 5 * 365 * DAY_MS;
23
+ class Scheduler {
24
+ _port;
25
+ // Fires after every successful write (add/update/remove/reschedule/cancel).
26
+ // The Dispatcher subscribes via .changed so a new event added during a
27
+ // sleep can interrupt the sleep and re-query nextDue().
28
+ _changed = new state_1.Event();
29
+ constructor(port) {
30
+ this._port = port;
31
+ }
32
+ get changed() {
33
+ return this._changed.broadcast;
34
+ }
35
+ // ─── Reads ──────────────────────────────────────────────────
36
+ // All occurrences whose [start, end) intersects [windowStart, windowEnd),
37
+ // sorted ascending by startTimestamp. Honors alterations.
38
+ async list(windowStart, windowEnd) {
39
+ if (windowEnd <= windowStart)
40
+ return [];
41
+ const [templates, alterations] = await Promise.all([
42
+ this._port.listTemplates(windowStart, windowEnd),
43
+ this._port.listAlterations(windowStart, windowEnd),
44
+ ]);
45
+ const out = [];
46
+ for (const tpl of templates) {
47
+ const keyed = this._generateKeyed(tpl, windowStart, windowEnd);
48
+ const events = (0, alterations_js_1.applyAlterations)(tpl.id, tpl.payload, keyed, alterations, tpl.timezone, windowStart, windowEnd);
49
+ out.push(...events);
50
+ }
51
+ out.sort((a, b) => a.startTimestamp - b.startTimestamp);
52
+ return out;
53
+ }
54
+ // Earliest occurrence of one template strictly after `after`. Returns null
55
+ // if no occurrence exists within NEXT_SEARCH_HORIZON_MS.
56
+ async next(templateId, after) {
57
+ const tpl = await this._port.getTemplate(templateId);
58
+ if (tpl == null)
59
+ return null;
60
+ // Expanding-window search. Doubles each iteration until horizon.
61
+ const horizonEnd = after + NEXT_SEARCH_HORIZON_MS;
62
+ let windowStart = after;
63
+ let windowSize = DAY_MS;
64
+ while (windowStart < horizonEnd) {
65
+ const windowEnd = Math.min(windowStart + windowSize, horizonEnd);
66
+ const alterations = await this._port.listAlterationsByTemplate(templateId);
67
+ const keyed = this._generateKeyed(tpl, windowStart, windowEnd);
68
+ const events = (0, alterations_js_1.applyAlterations)(tpl.id, tpl.payload, keyed, alterations, tpl.timezone, windowStart, windowEnd);
69
+ const firstAfter = events.find((e) => e.startTimestamp > after);
70
+ if (firstAfter != null)
71
+ return firstAfter;
72
+ windowStart = windowEnd;
73
+ windowSize = Math.min(windowSize * 2, 365 * DAY_MS);
74
+ }
75
+ return null;
76
+ }
77
+ // The next `limit` occurrences globally after `after`, sorted ascending.
78
+ // Defaults to 10. Searches in expanding windows like next().
79
+ async nextDue(after, limit = 10) {
80
+ if (limit <= 0)
81
+ return [];
82
+ const horizonEnd = after + NEXT_SEARCH_HORIZON_MS;
83
+ const out = [];
84
+ let windowStart = after;
85
+ let windowSize = DAY_MS;
86
+ while (windowStart < horizonEnd && out.length < limit) {
87
+ const windowEnd = Math.min(windowStart + windowSize, horizonEnd);
88
+ const events = await this.list(windowStart, windowEnd);
89
+ for (const e of events) {
90
+ if (e.startTimestamp > after) {
91
+ out.push(e);
92
+ if (out.length >= limit)
93
+ break;
94
+ }
95
+ }
96
+ windowStart = windowEnd;
97
+ windowSize = Math.min(windowSize * 2, 365 * DAY_MS);
98
+ }
99
+ return out;
100
+ }
101
+ // ─── Writes ─────────────────────────────────────────────────
102
+ async add(template) {
103
+ (0, validate_rule_js_1.validateRule)(template.rule);
104
+ _validateTimezone(template.timezone);
105
+ _validateDisambiguation(template.disambiguation);
106
+ const normalized = {
107
+ ...template,
108
+ rule: _normalizeRule(template.rule),
109
+ };
110
+ await this._port.addTemplate(normalized);
111
+ this._changed.notify();
112
+ }
113
+ // Partial update. Caller supplies any subset of the template fields plus
114
+ // the id. If the rule changes (kind or any field), all alterations for
115
+ // this template are wiped — same policy as activity-board, made explicit.
116
+ async update(delta) {
117
+ const existing = await this._port.getTemplate(delta.id);
118
+ if (existing == null)
119
+ throw new Error(`No template with id ${delta.id}`);
120
+ const merged = {
121
+ ...existing,
122
+ ...delta,
123
+ id: delta.id,
124
+ };
125
+ (0, validate_rule_js_1.validateRule)(merged.rule);
126
+ _validateTimezone(merged.timezone);
127
+ _validateDisambiguation(merged.disambiguation);
128
+ const updated = {
129
+ ...merged,
130
+ rule: _normalizeRule(merged.rule),
131
+ };
132
+ const ruleChanged = !_rulesEqual(existing.rule, updated.rule);
133
+ await this._port.updateTemplate(updated);
134
+ if (ruleChanged) {
135
+ await this._port.removeAlterationsForTemplate(updated.id);
136
+ }
137
+ this._changed.notify();
138
+ }
139
+ async remove(id) {
140
+ await this._port.removeAlterationsForTemplate(id);
141
+ await this._port.removeTemplate(id);
142
+ this._changed.notify();
143
+ }
144
+ // Adds or replaces a reschedule alteration for the given occurrence.
145
+ // Caller is responsible for providing a valid occurrenceKey (typically
146
+ // taken from a prior list() result). If `newTimeSpan` is omitted, the
147
+ // rescheduled occurrence inherits the original's UTC duration (i.e. moves
148
+ // in time but keeps the same length).
149
+ async reschedule(templateId, occurrenceKey, newStart, newTimeSpan, payloadOverride) {
150
+ if (newTimeSpan != null)
151
+ (0, validate_rule_js_1.validateTimeSpan)(newTimeSpan);
152
+ const id = `alt-${templateId}-${occurrenceKey}-${Date.now()}`;
153
+ await this._removeExistingAlteration(templateId, occurrenceKey);
154
+ const alt = {
155
+ id,
156
+ templateId,
157
+ occurrenceKey,
158
+ kind: "reschedule",
159
+ startTimestamp: newStart,
160
+ timeSpan: newTimeSpan,
161
+ payloadOverride,
162
+ };
163
+ await this._port.addAlteration(alt);
164
+ this._changed.notify();
165
+ return id;
166
+ }
167
+ async cancel(templateId, occurrenceKey) {
168
+ const id = `alt-${templateId}-${occurrenceKey}-${Date.now()}`;
169
+ await this._removeExistingAlteration(templateId, occurrenceKey);
170
+ const alt = {
171
+ id,
172
+ templateId,
173
+ occurrenceKey,
174
+ kind: "cancel",
175
+ };
176
+ await this._port.addAlteration(alt);
177
+ this._changed.notify();
178
+ return id;
179
+ }
180
+ // ─── Internals ──────────────────────────────────────────────
181
+ _generateKeyed(tpl, windowStart, windowEnd) {
182
+ const tz = tpl.timezone;
183
+ const dis = tpl.disambiguation ?? "compatible";
184
+ let occurrences;
185
+ switch (tpl.rule.kind) {
186
+ case "instant":
187
+ // Absolute start — no wall-clock ambiguity, disambiguation N/A.
188
+ occurrences = (0, instant_js_1.generateInstant)(tpl.rule, tz, windowStart, windowEnd);
189
+ break;
190
+ case "localDateTime":
191
+ occurrences = (0, local_date_time_js_1.generateLocalDateTime)(tpl.rule, tz, windowStart, windowEnd, dis);
192
+ break;
193
+ case "anchored":
194
+ // Absolute stepping — no wall-clock ambiguity, disambiguation N/A.
195
+ occurrences = (0, anchored_js_1.generateAnchored)(tpl.rule, tz, windowStart, windowEnd);
196
+ break;
197
+ case "daysOfWeek":
198
+ occurrences = (0, days_of_week_js_1.generateDaysOfWeek)(tpl.rule, tz, windowStart, windowEnd, dis);
199
+ break;
200
+ case "weeksOfMonth":
201
+ occurrences = (0, weeks_of_month_js_1.generateWeeksOfMonth)(tpl.rule, tz, windowStart, windowEnd, dis);
202
+ break;
203
+ case "monthsOfYear":
204
+ occurrences = (0, months_of_year_js_1.generateMonthsOfYear)(tpl.rule, tz, windowStart, windowEnd, dis);
205
+ break;
206
+ case "yearly":
207
+ occurrences = (0, yearly_js_1.generateYearly)(tpl.rule, tz, windowStart, windowEnd, dis);
208
+ break;
209
+ case "daysOfMonth":
210
+ occurrences = (0, days_of_month_js_1.generateDaysOfMonth)(tpl.rule, tz, windowStart, windowEnd, dis);
211
+ break;
212
+ case "daysOfYear":
213
+ occurrences = (0, days_of_year_js_1.generateDaysOfYear)(tpl.rule, tz, windowStart, windowEnd, dis);
214
+ break;
215
+ default: {
216
+ // Exhaustiveness guard — adding a new kind without handling it here
217
+ // is a compile error.
218
+ const _exhaustive = tpl.rule;
219
+ throw new Error(`Unknown rule kind: ${JSON.stringify(_exhaustive)}`);
220
+ }
221
+ }
222
+ return (0, occurrence_key_js_1.assignOccurrenceKeys)(occurrences, tz);
223
+ }
224
+ async _removeExistingAlteration(templateId, occurrenceKey) {
225
+ const existing = await this._port.listAlterationsByTemplate(templateId);
226
+ for (const alt of existing) {
227
+ if (alt.occurrenceKey === occurrenceKey) {
228
+ await this._port.removeAlteration(alt.id);
229
+ }
230
+ }
231
+ }
232
+ }
233
+ exports.Scheduler = Scheduler;
234
+ // ─── Validation ───────────────────────────────────────────────
235
+ // Returns a copy of `rule` with timeSpan defaulted when omitted, so
236
+ // generators don't have to handle undefined repeatedly.
237
+ function _normalizeRule(rule) {
238
+ if (rule.timeSpan != null)
239
+ return rule;
240
+ return { ...rule, timeSpan: { type: "duration", ms: 1 } };
241
+ }
242
+ function _validateTimezone(tz) {
243
+ if (typeof tz !== "string" || tz.length === 0) {
244
+ throw new Error("ScheduleTemplate.timezone is required (use 'UTC' for zone-free scheduling)");
245
+ }
246
+ if (!(0, tz_js_1.isValidTimeZone)(tz)) {
247
+ throw new Error(`Unknown timezone '${tz}' (expected an IANA zone name or 'UTC')`);
248
+ }
249
+ }
250
+ const _DISAMBIGUATIONS = [
251
+ "compatible",
252
+ "earlier",
253
+ "later",
254
+ "reject",
255
+ "skip",
256
+ ];
257
+ function _validateDisambiguation(d) {
258
+ if (d == null)
259
+ return;
260
+ if (!_DISAMBIGUATIONS.includes(d)) {
261
+ throw new Error(`Unknown disambiguation '${d}' (expected one of ${_DISAMBIGUATIONS.join(", ")})`);
262
+ }
263
+ }
264
+ function _rulesEqual(a, b) {
265
+ return JSON.stringify(_canonicalize(a)) === JSON.stringify(_canonicalize(b));
266
+ }
267
+ function _canonicalize(rule) {
268
+ // Sort + dedup any array selectors so reorderings or duplicates don't
269
+ // false-flag a rule change (which would wipe alterations).
270
+ if (rule.kind === "daysOfWeek") {
271
+ return { ...rule, days: [...new Set(rule.days)].sort((a, b) => a - b) };
272
+ }
273
+ if (rule.kind === "weeksOfMonth") {
274
+ return {
275
+ ...rule,
276
+ days: [...new Set(rule.days)].sort((a, b) => a - b),
277
+ weeks: [...new Set(rule.weeks)].sort((a, b) => a - b),
278
+ };
279
+ }
280
+ if (rule.kind === "monthsOfYear") {
281
+ return {
282
+ ...rule,
283
+ months: [...new Set(rule.months)].sort((a, b) => a - b),
284
+ weeks: [...new Set(rule.weeks)].sort((a, b) => a - b),
285
+ days: [...new Set(rule.days)].sort((a, b) => a - b),
286
+ };
287
+ }
288
+ if (rule.kind === "daysOfMonth") {
289
+ return {
290
+ ...rule,
291
+ months: [...new Set(rule.months)].sort((a, b) => a - b),
292
+ daysOfMonth: [...new Set(rule.daysOfMonth)].sort((a, b) => a - b),
293
+ };
294
+ }
295
+ if (rule.kind === "daysOfYear") {
296
+ return {
297
+ ...rule,
298
+ daysOfYear: [...new Set(rule.daysOfYear)].sort((a, b) => a - b),
299
+ };
300
+ }
301
+ return rule;
302
+ }
@@ -0,0 +1,149 @@
1
+ export declare enum Day {
2
+ Sunday = 0,
3
+ Monday = 1,
4
+ Tuesday = 2,
5
+ Wednesday = 3,
6
+ Thursday = 4,
7
+ Friday = 5,
8
+ Saturday = 6
9
+ }
10
+ export declare enum Month {
11
+ January = 0,
12
+ February = 1,
13
+ March = 2,
14
+ April = 3,
15
+ May = 4,
16
+ June = 5,
17
+ July = 6,
18
+ August = 7,
19
+ September = 8,
20
+ October = 9,
21
+ November = 10,
22
+ December = 11
23
+ }
24
+ export type TimeSpan = {
25
+ type: "duration";
26
+ ms: number;
27
+ } | {
28
+ type: "hourSpan";
29
+ hours: number;
30
+ };
31
+ export type RecurrenceRule = InstantRule | LocalDateTimeRule | AnchoredRule | DaysOfWeekRule | WeeksOfMonthRule | MonthsOfYearRule | YearlyRule | DaysOfMonthRule | DaysOfYearRule;
32
+ export interface InstantRule {
33
+ kind: "instant";
34
+ startOn: number;
35
+ timeSpan?: TimeSpan;
36
+ }
37
+ export interface LocalDateTimeRule {
38
+ kind: "localDateTime";
39
+ year: number;
40
+ month: Month;
41
+ day: number;
42
+ timeOfDay: number;
43
+ timeSpan?: TimeSpan;
44
+ }
45
+ export interface AnchoredRule {
46
+ kind: "anchored";
47
+ anchorDate: number;
48
+ intervalMs: number;
49
+ timeSpan?: TimeSpan;
50
+ seriesStart: number;
51
+ seriesEnd: number | null;
52
+ count?: number | null;
53
+ }
54
+ export interface DaysOfWeekRule {
55
+ kind: "daysOfWeek";
56
+ days: Day[];
57
+ weekInterval: number;
58
+ timeOfDay: number;
59
+ timeSpan?: TimeSpan;
60
+ seriesStart: number;
61
+ seriesEnd: number | null;
62
+ count?: number | null;
63
+ }
64
+ export interface WeeksOfMonthRule {
65
+ kind: "weeksOfMonth";
66
+ days: Day[];
67
+ weeks: number[];
68
+ monthInterval: number;
69
+ timeOfDay: number;
70
+ timeSpan?: TimeSpan;
71
+ seriesStart: number;
72
+ seriesEnd: number | null;
73
+ count?: number | null;
74
+ }
75
+ export interface MonthsOfYearRule {
76
+ kind: "monthsOfYear";
77
+ months: Month[];
78
+ weeks: number[];
79
+ days: Day[];
80
+ yearInterval: number;
81
+ timeOfDay: number;
82
+ timeSpan?: TimeSpan;
83
+ seriesStart: number;
84
+ seriesEnd: number | null;
85
+ count?: number | null;
86
+ }
87
+ export interface YearlyRule {
88
+ kind: "yearly";
89
+ month: Month;
90
+ day: number;
91
+ yearInterval: number;
92
+ timeOfDay: number;
93
+ timeSpan?: TimeSpan;
94
+ seriesStart: number;
95
+ seriesEnd: number | null;
96
+ count?: number | null;
97
+ }
98
+ export interface DaysOfMonthRule {
99
+ kind: "daysOfMonth";
100
+ months: Month[];
101
+ daysOfMonth: number[];
102
+ yearInterval: number;
103
+ timeOfDay: number;
104
+ timeSpan?: TimeSpan;
105
+ seriesStart: number;
106
+ seriesEnd: number | null;
107
+ count?: number | null;
108
+ }
109
+ export interface DaysOfYearRule {
110
+ kind: "daysOfYear";
111
+ daysOfYear: number[];
112
+ yearInterval: number;
113
+ timeOfDay: number;
114
+ timeSpan?: TimeSpan;
115
+ seriesStart: number;
116
+ seriesEnd: number | null;
117
+ count?: number | null;
118
+ }
119
+ export type Disambiguation = "compatible" | "earlier" | "later" | "reject" | "skip";
120
+ export interface ScheduleTemplate<TPayload extends {
121
+ type: string;
122
+ }> {
123
+ id: string;
124
+ rule: RecurrenceRule;
125
+ timezone: string;
126
+ disambiguation?: Disambiguation;
127
+ payload: TPayload;
128
+ }
129
+ export interface Alteration<TPayload extends {
130
+ type: string;
131
+ }> {
132
+ id: string;
133
+ templateId: string;
134
+ occurrenceKey: string;
135
+ kind: "reschedule" | "cancel";
136
+ startTimestamp?: number;
137
+ timeSpan?: TimeSpan;
138
+ payloadOverride?: Partial<TPayload>;
139
+ }
140
+ export interface ScheduledEvent<TPayload extends {
141
+ type: string;
142
+ }> {
143
+ templateId: string;
144
+ occurrenceKey: string;
145
+ startTimestamp: number;
146
+ endTimestamp: number;
147
+ payload: TPayload;
148
+ alterationId?: string;
149
+ }
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ // All types are plain data — safe to JSON-serialize and store in a JSON column.
3
+ // Generated occurrences carry a TPayload so consumers can attach whatever
4
+ // domain data they need (cron action, calendar event body, etc.).
5
+ //
6
+ // ─── DST gotchas for wall-clock rules ───────────────────────────────────────
7
+ //
8
+ // daysOfWeek / weekOfMonth / yearly rules use wall-clock fields (timeOfDay,
9
+ // day-of-week, month/day) interpreted in the template's `timezone`. In a tz
10
+ // that observes DST, two edge cases exist:
11
+ //
12
+ // 1. Spring-forward gap. On the spring-forward day, an hour is SKIPPED — in
13
+ // America/Denver, 02:00 MST jumps to 03:00 MDT, so wall times 02:00–02:59
14
+ // don't exist that day. If `timeOfDay` lands in this gap, we resolve to
15
+ // the post-transition offset (the default in Luxon and date-fns-tz),
16
+ // which means the event fires at a *different* wall-clock time than you
17
+ // asked for — typically one hour earlier in real time. RECOMMENDATION:
18
+ // don't pick `timeOfDay` between 02:00 and 02:59 in DST-observing zones.
19
+ // 03:00 is always safe.
20
+ //
21
+ // 2. TimeSpan spanning the gap. The TimeSpan type has two flavors:
22
+ // `{type:"duration",ms}` — exact UTC milliseconds, no DST adjustment.
23
+ // An 8h duration shift starting 23:00 spring-forward eve runs for
24
+ // exactly 8h real time; the worker's wall display reads "9 hours
25
+ // elapsed" because clocks jumped. Payroll-correct.
26
+ // `{type:"hourSpan",hours}` — wall-clock hours, flexes with DST.
27
+ // An 8h hourSpan shift starting 23:00 spring-forward eve ends at
28
+ // 07:00 next wall day regardless; real elapsed time is 7 actual
29
+ // hours. Calendar-display correct.
30
+ // Both are valid; pick based on whether you care about real elapsed
31
+ // time or wall-clock display.
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.Month = exports.Day = void 0;
34
+ var Day;
35
+ (function (Day) {
36
+ Day[Day["Sunday"] = 0] = "Sunday";
37
+ Day[Day["Monday"] = 1] = "Monday";
38
+ Day[Day["Tuesday"] = 2] = "Tuesday";
39
+ Day[Day["Wednesday"] = 3] = "Wednesday";
40
+ Day[Day["Thursday"] = 4] = "Thursday";
41
+ Day[Day["Friday"] = 5] = "Friday";
42
+ Day[Day["Saturday"] = 6] = "Saturday";
43
+ })(Day || (exports.Day = Day = {}));
44
+ var Month;
45
+ (function (Month) {
46
+ Month[Month["January"] = 0] = "January";
47
+ Month[Month["February"] = 1] = "February";
48
+ Month[Month["March"] = 2] = "March";
49
+ Month[Month["April"] = 3] = "April";
50
+ Month[Month["May"] = 4] = "May";
51
+ Month[Month["June"] = 5] = "June";
52
+ Month[Month["July"] = 6] = "July";
53
+ Month[Month["August"] = 7] = "August";
54
+ Month[Month["September"] = 8] = "September";
55
+ Month[Month["October"] = 9] = "October";
56
+ Month[Month["November"] = 10] = "November";
57
+ Month[Month["December"] = 11] = "December";
58
+ })(Month || (exports.Month = Month = {}));
@@ -0,0 +1,22 @@
1
+ import type { Day, Disambiguation, TimeSpan } from "./types.js";
2
+ export interface WallParts {
3
+ year: number;
4
+ month: number;
5
+ day: number;
6
+ hour: number;
7
+ minute: number;
8
+ second: number;
9
+ ms: number;
10
+ dayOfWeek: Day;
11
+ }
12
+ export declare function isValidTimeZone(tz: string): boolean;
13
+ export declare function utcToWallParts(utcEpochMs: number, tz?: string): WallParts;
14
+ export declare function isNonexistentWallTime(year: number, month: number, day: number, msOfDay: number, tz: string | undefined): boolean;
15
+ export declare function wallTimeToUtc(year: number, month: number, // 0-indexed
16
+ day: number, msOfDay: number, // 0..86_399_999, ms since wall-clock midnight
17
+ tz?: string, disambiguation?: Disambiguation): number;
18
+ export declare function dayOfWeekInTz(utcEpochMs: number, tz?: string): Day;
19
+ export declare function addDaysInTz(utcEpochMs: number, days: number, tz?: string): number;
20
+ export declare function addMonthsInTz(utcEpochMs: number, months: number, tz?: string): number;
21
+ export declare function monthLength(year: number, month: number): number;
22
+ export declare function computeSpanEnd(startUtc: number, span: TimeSpan | undefined, tz: string | undefined): number;