@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jared Barnes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,74 @@
1
- # Temporary Holding Version
1
+ # @j13b/scheduler
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A timezone-aware recurrence and dispatch engine. Pure-data rule DSL, IANA timezone support, DST-aware generators, and a cron-style dispatcher.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @j13b/scheduler
9
+ ```
10
+
11
+ ## Quick start
12
+
13
+ ```ts
14
+ import {
15
+ Scheduler,
16
+ InMemorySchedulerPort,
17
+ Dispatcher,
18
+ InMemoryDispatcherStore,
19
+ Day,
20
+ } from "@j13b/scheduler";
21
+
22
+ const port = new InMemorySchedulerPort();
23
+ const scheduler = new Scheduler(port);
24
+
25
+ await scheduler.add({
26
+ id: "standup",
27
+ timezone: "America/Denver",
28
+ rule: {
29
+ kind: "daysOfWeek",
30
+ days: [Day.Monday, Day.Tuesday, Day.Wednesday, Day.Thursday, Day.Friday],
31
+ timeOfDay: 9 * 60 * 60 * 1000, // 9:00 wall-clock
32
+ weekInterval: 1,
33
+ seriesStart: Date.UTC(2026, 0, 1),
34
+ seriesEnd: null, // open-ended
35
+ },
36
+ payload: { type: "standup" },
37
+ });
38
+
39
+ const dispatcher = new Dispatcher({
40
+ scheduler,
41
+ store: new InMemoryDispatcherStore(),
42
+ fire: async (event) => {
43
+ console.log(`Fire ${event.templateId} at ${new Date(event.startTimestamp).toISOString()}`);
44
+ },
45
+ });
46
+
47
+ await dispatcher.start();
48
+ ```
49
+
50
+ ## What's in the box
51
+
52
+ - **Recurrence rules** — `InstantRule`, `LocalDateTimeRule`, `AnchoredRule`, `DaysOfWeekRule`, `WeeksOfMonthRule`, `MonthsOfYearRule`, `YearlyRule`, `DaysOfMonthRule`, `DaysOfYearRule`. Pure-data, JSON-serializable, exhaustive TypeScript union. The two single-occurrence kinds: `InstantRule` fires at an absolute UTC instant (timezone-independent); `LocalDateTimeRule` fires once at a wall-clock date/time resolved (DST-aware) in the template's timezone.
53
+ - **TimeSpan** — two flavors of "how long does an occurrence last":
54
+ - `{ type: "duration", ms }` — fixed real-elapsed time (payroll-correct, ignores DST jumps).
55
+ - `{ type: "hourSpan", hours }` — wall-clock hours (calendar-correct, flexes with DST).
56
+ - **Timezone math** — built on `Intl.DateTimeFormat` over IANA zones, zero deps for date logic. Spring-forward gaps and fall-back overlaps are resolved by a configurable per-template `disambiguation` policy (`"compatible"` default, plus `"earlier"`, `"later"`, `"reject"`, and `"skip"`), mirroring the Temporal proposal.
57
+ - **Alterations** — per-occurrence overrides (skip, reschedule, replace).
58
+ - **Dispatcher** — drives a Scheduler in real time; catch-up pass for missed events, resumes early when the schedule changes.
59
+
60
+ ## API surface
61
+
62
+ See [`src/index.ts`](./src/index.ts) for the full export list. Notable types:
63
+
64
+ - `RecurrenceRule` — discriminated union over the nine rule kinds.
65
+ - `ScheduleTemplate` — `{ id, rule, timezone, disambiguation?, payload }`. `timezone` is required and validated at write time (use `"UTC"` for zone-free scheduling; it's ignored by the absolute `instant`/`anchored` rules).
66
+ - `validateRule(rule)` — checks a rule's whole shape (every required field present, typed and in range; no fields its kind lacks) and throws an `Error` naming the field and the form it expects. `Scheduler.add`/`update` run it, so a rule from JSON or an LLM fails with a clear message instead of crashing a generator. `validateTimeSpan` and `RULE_KINDS` are exported too.
67
+ - `ScheduledEvent` — `{ templateId, occurrenceKey, startTimestamp, endTimestamp, payload, alterationId? }`.
68
+ - `Disambiguation` — DST-edge resolution policy: `"compatible" | "earlier" | "later" | "reject" | "skip"`.
69
+ - `SchedulerPort` — async storage interface; an `InMemorySchedulerPort` ships with the package.
70
+ - `DispatcherStore` — durable record of the dispatcher's catch-up cursor.
71
+
72
+ ## License
73
+
74
+ MIT
@@ -0,0 +1,6 @@
1
+ import { KeyedOccurrence } from './occurrence_key.js';
2
+ import { Alteration, ScheduledEvent } from './types.js';
3
+ export declare function applyAlterations<TPayload extends {
4
+ type: string;
5
+ }>(templateId: string, basePayload: TPayload, keyedOccurrences: readonly KeyedOccurrence[], alterations: readonly Alteration<TPayload>[], tz: string | undefined, windowStart: number, windowEnd: number): ScheduledEvent<TPayload>[];
6
+ //# sourceMappingURL=alterations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alterations.d.ts","sourceRoot":"","sources":["../src/alterations.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAmB7D,wBAAgB,gBAAgB,CAAC,QAAQ,SAAS;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,EAChE,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,QAAQ,EACrB,gBAAgB,EAAE,SAAS,eAAe,EAAE,EAC5C,WAAW,EAAE,SAAS,UAAU,CAAC,QAAQ,CAAC,EAAE,EAC5C,EAAE,EAAE,MAAM,GAAG,SAAS,EACtB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,cAAc,CAAC,QAAQ,CAAC,EAAE,CAsE5B"}
@@ -0,0 +1,61 @@
1
+ import { intersects } from "./generators/types.js";
2
+ import { computeSpanEnd } from "./tz.js";
3
+ // src/alterations.ts
4
+ function applyAlterations(templateId, basePayload, keyedOccurrences, alterations, tz, windowStart, windowEnd) {
5
+ const altByKey = /* @__PURE__ */ new Map();
6
+ for (const alt of alterations) if (alt.templateId === templateId) altByKey.set(alt.occurrenceKey, alt);
7
+ const out = [];
8
+ const seenKeys = /* @__PURE__ */ new Set();
9
+ for (const occ of keyedOccurrences) {
10
+ seenKeys.add(occ.occurrenceKey);
11
+ const alt = altByKey.get(occ.occurrenceKey);
12
+ if (alt == null) {
13
+ out.push({
14
+ templateId,
15
+ occurrenceKey: occ.occurrenceKey,
16
+ startTimestamp: occ.startTimestamp,
17
+ endTimestamp: occ.endTimestamp,
18
+ payload: basePayload
19
+ });
20
+ continue;
21
+ }
22
+ if (alt.kind === "cancel") continue;
23
+ const start = alt.startTimestamp ?? occ.startTimestamp;
24
+ const end = alt.timeSpan != null ? computeSpanEnd(start, alt.timeSpan, tz) : start + (occ.endTimestamp - occ.startTimestamp);
25
+ if (!intersects(start, end, windowStart, windowEnd)) continue;
26
+ out.push({
27
+ templateId,
28
+ occurrenceKey: occ.occurrenceKey,
29
+ startTimestamp: start,
30
+ endTimestamp: end,
31
+ payload: _mergePayload(basePayload, alt.payloadOverride),
32
+ alterationId: alt.id
33
+ });
34
+ }
35
+ for (const alt of altByKey.values()) {
36
+ if (seenKeys.has(alt.occurrenceKey)) continue;
37
+ if (alt.kind !== "reschedule") continue;
38
+ if (alt.startTimestamp == null) continue;
39
+ const end = alt.timeSpan != null ? computeSpanEnd(alt.startTimestamp, alt.timeSpan, tz) : alt.startTimestamp + 1;
40
+ if (!intersects(alt.startTimestamp, end, windowStart, windowEnd)) continue;
41
+ out.push({
42
+ templateId,
43
+ occurrenceKey: alt.occurrenceKey,
44
+ startTimestamp: alt.startTimestamp,
45
+ endTimestamp: end,
46
+ payload: _mergePayload(basePayload, alt.payloadOverride),
47
+ alterationId: alt.id
48
+ });
49
+ }
50
+ return out.sort((a, b) => a.startTimestamp - b.startTimestamp);
51
+ }
52
+ function _mergePayload(base, override) {
53
+ if (override == null) return base;
54
+ return {
55
+ ...base,
56
+ ...override
57
+ };
58
+ }
59
+ export { applyAlterations };
60
+
61
+ //# sourceMappingURL=alterations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"alterations.js","names":[],"sources":["../src/alterations.ts"],"sourcesContent":["import { intersects } from \"./generators/types.js\";\nimport type { KeyedOccurrence } from \"./occurrence_key.js\";\nimport type { Alteration, ScheduledEvent } from \"./types.js\";\nimport { computeSpanEnd } from \"./tz.js\";\n\n// Apply a template's alterations to its base occurrences and emit the final\n// ScheduledEvents.\n//\n// Two passes:\n// 1. Walk base occurrences. Cancel suppresses; reschedule replaces (and\n// may move out of window, in which case it's not emitted).\n// 2. Walk reschedules whose original key is NOT in the base list (the\n// original was outside the window) but whose new time IS in the window.\n// Synthesize an event for them. This is what activity-board does in its\n// separate AlterationEventGenerator.\n//\n// `tz` is the template's timezone. Used to interpret hourSpan on the\n// alteration's timeSpan (if any). When the alteration carries no timeSpan,\n// we preserve the original occurrence's duration (end - start in UTC ms).\n//\n// Result is sorted ascending by startTimestamp.\nexport function applyAlterations<TPayload extends { type: string }>(\n templateId: string,\n basePayload: TPayload,\n keyedOccurrences: readonly KeyedOccurrence[],\n alterations: readonly Alteration<TPayload>[],\n tz: string | undefined,\n windowStart: number,\n windowEnd: number\n): ScheduledEvent<TPayload>[] {\n // Index alterations for THIS template by occurrence key. If multiple\n // alterations target the same key, last-write-wins.\n const altByKey = new Map<string, Alteration<TPayload>>();\n for (const alt of alterations) {\n if (alt.templateId === templateId) altByKey.set(alt.occurrenceKey, alt);\n }\n\n const out: ScheduledEvent<TPayload>[] = [];\n const seenKeys = new Set<string>();\n\n for (const occ of keyedOccurrences) {\n seenKeys.add(occ.occurrenceKey);\n const alt = altByKey.get(occ.occurrenceKey);\n\n if (alt == null) {\n out.push({\n templateId,\n occurrenceKey: occ.occurrenceKey,\n startTimestamp: occ.startTimestamp,\n endTimestamp: occ.endTimestamp,\n payload: basePayload,\n });\n continue;\n }\n if (alt.kind === \"cancel\") continue;\n\n // reschedule\n const start = alt.startTimestamp ?? occ.startTimestamp;\n // If the alteration specifies a timeSpan, use it (with tz semantics).\n // Otherwise preserve the original UTC duration.\n const end =\n alt.timeSpan != null\n ? computeSpanEnd(start, alt.timeSpan, tz)\n : start + (occ.endTimestamp - occ.startTimestamp);\n if (!intersects(start, end, windowStart, windowEnd)) continue;\n out.push({\n templateId,\n occurrenceKey: occ.occurrenceKey,\n startTimestamp: start,\n endTimestamp: end,\n payload: _mergePayload(basePayload, alt.payloadOverride),\n alterationId: alt.id,\n });\n }\n\n // Orphan reschedules: alteration's occurrenceKey isn't in the base list\n // (because the original fell outside the window) but the rescheduled time\n // is. Cancels for orphans are no-ops — there was nothing to cancel here.\n for (const alt of altByKey.values()) {\n if (seenKeys.has(alt.occurrenceKey)) continue;\n if (alt.kind !== \"reschedule\") continue;\n if (alt.startTimestamp == null) continue;\n // No original to inherit duration from; default to 1ms if no timeSpan.\n const end =\n alt.timeSpan != null\n ? computeSpanEnd(alt.startTimestamp, alt.timeSpan, tz)\n : alt.startTimestamp + 1;\n if (!intersects(alt.startTimestamp, end, windowStart, windowEnd)) continue;\n out.push({\n templateId,\n occurrenceKey: alt.occurrenceKey,\n startTimestamp: alt.startTimestamp,\n endTimestamp: end,\n payload: _mergePayload(basePayload, alt.payloadOverride),\n alterationId: alt.id,\n });\n }\n\n return out.sort((a, b) => a.startTimestamp - b.startTimestamp);\n}\n\nfunction _mergePayload<TPayload extends { type: string }>(\n base: TPayload,\n override: Partial<TPayload> | undefined\n): TPayload {\n if (override == null) return base;\n return { ...base, ...override } as TPayload;\n}\n"],"mappings":";;;AAqBA,SAAgB,iBACd,YACA,aACA,kBACA,aACA,IACA,aACA,WAC4B;CAG5B,MAAM,2BAAW,IAAI,IAAkC;CACvD,KAAK,MAAM,OAAO,aAChB,IAAI,IAAI,eAAe,YAAY,SAAS,IAAI,IAAI,eAAe,GAAG;CAGxE,MAAM,MAAkC,CAAC;CACzC,MAAM,2BAAW,IAAI,IAAY;CAEjC,KAAK,MAAM,OAAO,kBAAkB;EAClC,SAAS,IAAI,IAAI,aAAa;EAC9B,MAAM,MAAM,SAAS,IAAI,IAAI,aAAa;EAE1C,IAAI,OAAO,MAAM;GACf,IAAI,KAAK;IACP;IACA,eAAe,IAAI;IACnB,gBAAgB,IAAI;IACpB,cAAc,IAAI;IAClB,SAAS;GACX,CAAC;GACD;EACF;EACA,IAAI,IAAI,SAAS,UAAU;EAG3B,MAAM,QAAQ,IAAI,kBAAkB,IAAI;EAGxC,MAAM,MACJ,IAAI,YAAY,OACZ,eAAe,OAAO,IAAI,UAAU,EAAE,IACtC,SAAS,IAAI,eAAe,IAAI;EACtC,IAAI,CAAC,WAAW,OAAO,KAAK,aAAa,SAAS,GAAG;EACrD,IAAI,KAAK;GACP;GACA,eAAe,IAAI;GACnB,gBAAgB;GAChB,cAAc;GACd,SAAS,cAAc,aAAa,IAAI,eAAe;GACvD,cAAc,IAAI;EACpB,CAAC;CACH;CAKA,KAAK,MAAM,OAAO,SAAS,OAAO,GAAG;EACnC,IAAI,SAAS,IAAI,IAAI,aAAa,GAAG;EACrC,IAAI,IAAI,SAAS,cAAc;EAC/B,IAAI,IAAI,kBAAkB,MAAM;EAEhC,MAAM,MACJ,IAAI,YAAY,OACZ,eAAe,IAAI,gBAAgB,IAAI,UAAU,EAAE,IACnD,IAAI,iBAAiB;EAC3B,IAAI,CAAC,WAAW,IAAI,gBAAgB,KAAK,aAAa,SAAS,GAAG;EAClE,IAAI,KAAK;GACP;GACA,eAAe,IAAI;GACnB,gBAAgB,IAAI;GACpB,cAAc;GACd,SAAS,cAAc,aAAa,IAAI,eAAe;GACvD,cAAc,IAAI;EACpB,CAAC;CACH;CAEA,OAAO,IAAI,MAAM,GAAG,MAAM,EAAE,iBAAiB,EAAE,cAAc;AAC/D;AAEA,SAAS,cACP,MACA,UACU;CACV,IAAI,YAAY,MAAM,OAAO;CAC7B,OAAO;EAAE,GAAG;EAAM,GAAG;CAAS;AAChC"}
@@ -0,0 +1,5 @@
1
+ import type { KeyedOccurrence } from "./occurrence_key.js";
2
+ import type { Alteration, ScheduledEvent } from "./types.js";
3
+ export declare function applyAlterations<TPayload extends {
4
+ type: string;
5
+ }>(templateId: string, basePayload: TPayload, keyedOccurrences: readonly KeyedOccurrence[], alterations: readonly Alteration<TPayload>[], tz: string | undefined, windowStart: number, windowEnd: number): ScheduledEvent<TPayload>[];
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyAlterations = applyAlterations;
4
+ const types_js_1 = require("./generators/types.js");
5
+ const tz_js_1 = require("./tz.js");
6
+ // Apply a template's alterations to its base occurrences and emit the final
7
+ // ScheduledEvents.
8
+ //
9
+ // Two passes:
10
+ // 1. Walk base occurrences. Cancel suppresses; reschedule replaces (and
11
+ // may move out of window, in which case it's not emitted).
12
+ // 2. Walk reschedules whose original key is NOT in the base list (the
13
+ // original was outside the window) but whose new time IS in the window.
14
+ // Synthesize an event for them. This is what activity-board does in its
15
+ // separate AlterationEventGenerator.
16
+ //
17
+ // `tz` is the template's timezone. Used to interpret hourSpan on the
18
+ // alteration's timeSpan (if any). When the alteration carries no timeSpan,
19
+ // we preserve the original occurrence's duration (end - start in UTC ms).
20
+ //
21
+ // Result is sorted ascending by startTimestamp.
22
+ function applyAlterations(templateId, basePayload, keyedOccurrences, alterations, tz, windowStart, windowEnd) {
23
+ // Index alterations for THIS template by occurrence key. If multiple
24
+ // alterations target the same key, last-write-wins.
25
+ const altByKey = new Map();
26
+ for (const alt of alterations) {
27
+ if (alt.templateId === templateId)
28
+ altByKey.set(alt.occurrenceKey, alt);
29
+ }
30
+ const out = [];
31
+ const seenKeys = new Set();
32
+ for (const occ of keyedOccurrences) {
33
+ seenKeys.add(occ.occurrenceKey);
34
+ const alt = altByKey.get(occ.occurrenceKey);
35
+ if (alt == null) {
36
+ out.push({
37
+ templateId,
38
+ occurrenceKey: occ.occurrenceKey,
39
+ startTimestamp: occ.startTimestamp,
40
+ endTimestamp: occ.endTimestamp,
41
+ payload: basePayload,
42
+ });
43
+ continue;
44
+ }
45
+ if (alt.kind === "cancel")
46
+ continue;
47
+ // reschedule
48
+ const start = alt.startTimestamp ?? occ.startTimestamp;
49
+ // If the alteration specifies a timeSpan, use it (with tz semantics).
50
+ // Otherwise preserve the original UTC duration.
51
+ const end = alt.timeSpan != null
52
+ ? (0, tz_js_1.computeSpanEnd)(start, alt.timeSpan, tz)
53
+ : start + (occ.endTimestamp - occ.startTimestamp);
54
+ if (!(0, types_js_1.intersects)(start, end, windowStart, windowEnd))
55
+ continue;
56
+ out.push({
57
+ templateId,
58
+ occurrenceKey: occ.occurrenceKey,
59
+ startTimestamp: start,
60
+ endTimestamp: end,
61
+ payload: _mergePayload(basePayload, alt.payloadOverride),
62
+ alterationId: alt.id,
63
+ });
64
+ }
65
+ // Orphan reschedules: alteration's occurrenceKey isn't in the base list
66
+ // (because the original fell outside the window) but the rescheduled time
67
+ // is. Cancels for orphans are no-ops — there was nothing to cancel here.
68
+ for (const alt of altByKey.values()) {
69
+ if (seenKeys.has(alt.occurrenceKey))
70
+ continue;
71
+ if (alt.kind !== "reschedule")
72
+ continue;
73
+ if (alt.startTimestamp == null)
74
+ continue;
75
+ // No original to inherit duration from; default to 1ms if no timeSpan.
76
+ const end = alt.timeSpan != null
77
+ ? (0, tz_js_1.computeSpanEnd)(alt.startTimestamp, alt.timeSpan, tz)
78
+ : alt.startTimestamp + 1;
79
+ if (!(0, types_js_1.intersects)(alt.startTimestamp, end, windowStart, windowEnd))
80
+ continue;
81
+ out.push({
82
+ templateId,
83
+ occurrenceKey: alt.occurrenceKey,
84
+ startTimestamp: alt.startTimestamp,
85
+ endTimestamp: end,
86
+ payload: _mergePayload(basePayload, alt.payloadOverride),
87
+ alterationId: alt.id,
88
+ });
89
+ }
90
+ return out.sort((a, b) => a.startTimestamp - b.startTimestamp);
91
+ }
92
+ function _mergePayload(base, override) {
93
+ if (override == null)
94
+ return base;
95
+ return { ...base, ...override };
96
+ }
@@ -0,0 +1,38 @@
1
+ import { type DispatcherStore } from "./dispatcher_store.js";
2
+ import type { Scheduler } from "./scheduler.js";
3
+ import type { ScheduledEvent } from "./types.js";
4
+ export interface DispatcherConfig<TPayload extends {
5
+ type: string;
6
+ }> {
7
+ scheduler: Scheduler<TPayload>;
8
+ fire: (event: ScheduledEvent<TPayload>) => Promise<void> | void;
9
+ store?: DispatcherStore;
10
+ onError?: (err: unknown, event?: ScheduledEvent<TPayload>) => void;
11
+ idleTimeoutMs?: number;
12
+ now?: () => number;
13
+ setTimeout?: (fn: () => void, ms: number) => unknown;
14
+ clearTimeout?: (handle: unknown) => void;
15
+ }
16
+ export declare class Dispatcher<TPayload extends {
17
+ type: string;
18
+ }> {
19
+ private readonly _scheduler;
20
+ private readonly _fire;
21
+ private readonly _store;
22
+ private readonly _onError;
23
+ private readonly _idleTimeoutMs;
24
+ private readonly _now;
25
+ private readonly _setTimeout;
26
+ private readonly _clearTimeout;
27
+ private _running;
28
+ private _wakeResolve;
29
+ private _changeSubscription;
30
+ constructor(config: DispatcherConfig<TPayload>);
31
+ start(): Promise<void>;
32
+ stop(): void;
33
+ private _loop;
34
+ private _findNextTarget;
35
+ private _tick;
36
+ private _sleepUntil;
37
+ private _wake;
38
+ }
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Dispatcher = void 0;
4
+ const dispatcher_store_js_1 = require("./dispatcher_store.js");
5
+ // Default safety-net wait when nothing is scheduled. The Scheduler.changed
6
+ // event normally wakes us as soon as something gets added, so this is just
7
+ // a backstop in case a notification is missed.
8
+ const DEFAULT_IDLE_TIMEOUT_MS = 60 * 60 * 1000;
9
+ // Drives a Scheduler in real time. The loop:
10
+ // 1. Catch up: list(cursor, now) → fire each → advance cursor → save.
11
+ // 2. Find next: nextDue(now, 1) → wait until its startTimestamp.
12
+ // 3. Wait may end early via Scheduler.changed (a write happened) or stop().
13
+ // Catch-up handles crashes AND in-process adds we missed. The change event
14
+ // handles in-process adds while we're sleeping — the dispatcher wakes
15
+ // immediately and re-queries instead of firing the stale "next" event late.
16
+ class Dispatcher {
17
+ _scheduler;
18
+ _fire;
19
+ _store;
20
+ _onError;
21
+ _idleTimeoutMs;
22
+ _now;
23
+ _setTimeout;
24
+ _clearTimeout;
25
+ _running = false;
26
+ _wakeResolve = null;
27
+ _changeSubscription = null;
28
+ constructor(config) {
29
+ this._scheduler = config.scheduler;
30
+ this._fire = config.fire;
31
+ this._store = config.store ?? new dispatcher_store_js_1.InMemoryDispatcherStore();
32
+ this._onError = config.onError ?? defaultOnError;
33
+ this._idleTimeoutMs = config.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
34
+ this._now = config.now ?? Date.now;
35
+ this._setTimeout = config.setTimeout ?? ((fn, ms) => globalThis.setTimeout(fn, ms));
36
+ this._clearTimeout =
37
+ config.clearTimeout ?? ((h) => globalThis.clearTimeout(h));
38
+ }
39
+ // Starts the loop. Resolves after the first catch-up pass completes (so
40
+ // tests can await an initial sync). The loop keeps running in the
41
+ // background until stop() is called.
42
+ async start() {
43
+ if (this._running)
44
+ return;
45
+ this._running = true;
46
+ this._changeSubscription = this._scheduler.changed.subscribe(() => this._wake());
47
+ // First iteration in foreground so callers can await initial catch-up.
48
+ await this._tick();
49
+ if (this._running)
50
+ void this._loop();
51
+ }
52
+ // Halts the loop. Any in-flight fire() completes; subsequent fires are
53
+ // skipped. The Scheduler subscription is removed.
54
+ stop() {
55
+ if (!this._running)
56
+ return;
57
+ this._running = false;
58
+ if (this._changeSubscription != null) {
59
+ this._changeSubscription.unsubscribe();
60
+ this._changeSubscription = null;
61
+ }
62
+ this._wake();
63
+ }
64
+ // ─── Internals ──────────────────────────────────────────────
65
+ async _loop() {
66
+ while (this._running) {
67
+ const target = await this._findNextTarget();
68
+ if (!this._running)
69
+ return;
70
+ await this._sleepUntil(target);
71
+ if (!this._running)
72
+ return;
73
+ await this._tick();
74
+ }
75
+ }
76
+ // Returns the epoch-ms instant the dispatcher should next wake. Either
77
+ // the next due event's startTimestamp, or now + idleTimeoutMs.
78
+ async _findNextTarget() {
79
+ try {
80
+ const upcoming = await this._scheduler.nextDue(this._now(), 1);
81
+ const next = upcoming[0];
82
+ return next != null ? next.startTimestamp : this._now() + this._idleTimeoutMs;
83
+ }
84
+ catch (err) {
85
+ this._onError(err);
86
+ return this._now() + this._idleTimeoutMs;
87
+ }
88
+ }
89
+ // Catch-up pass: fire everything with start in [cursor, now]. We use
90
+ // `now + 1` as the half-open windowEnd so an event scheduled exactly at
91
+ // the wake instant is included (the wake from setTimeout lands on the
92
+ // event's startTimestamp, and we don't want to miss it). Advancing the
93
+ // cursor to `now + 1` likewise avoids re-listing the same event on the
94
+ // next tick.
95
+ async _tick() {
96
+ const cursor = (await this._store.loadCursor()) ?? this._now();
97
+ const now = this._now();
98
+ const windowEnd = now + 1;
99
+ if (windowEnd <= cursor) {
100
+ // Cursor is already at or past `now + 1` — happens on the very first
101
+ // tick when we just-saved-and-rewoke. Nothing to do.
102
+ return;
103
+ }
104
+ let events;
105
+ try {
106
+ events = await this._scheduler.list(cursor, windowEnd);
107
+ }
108
+ catch (err) {
109
+ this._onError(err);
110
+ // Don't advance cursor on a list failure — try again next tick.
111
+ return;
112
+ }
113
+ for (const event of events) {
114
+ if (!this._running)
115
+ return;
116
+ try {
117
+ await this._fire(event);
118
+ }
119
+ catch (err) {
120
+ this._onError(err, event);
121
+ }
122
+ }
123
+ await this._store.saveCursor(windowEnd);
124
+ }
125
+ // Sleeps until either the target epoch-ms instant, or wake() is called
126
+ // (via Scheduler.changed or stop()). No-op if target is already past.
127
+ async _sleepUntil(targetMs) {
128
+ const remaining = targetMs - this._now();
129
+ if (remaining <= 0)
130
+ return;
131
+ await new Promise((resolve) => {
132
+ let done = false;
133
+ const timer = this._setTimeout(() => {
134
+ if (done)
135
+ return;
136
+ done = true;
137
+ this._wakeResolve = null;
138
+ resolve();
139
+ }, remaining);
140
+ this._wakeResolve = () => {
141
+ if (done)
142
+ return;
143
+ done = true;
144
+ this._clearTimeout(timer);
145
+ this._wakeResolve = null;
146
+ resolve();
147
+ };
148
+ });
149
+ }
150
+ // Resolves any in-flight _sleepUntil. Called by stop() and by the
151
+ // Scheduler.changed subscription.
152
+ _wake() {
153
+ if (this._wakeResolve != null)
154
+ this._wakeResolve();
155
+ }
156
+ }
157
+ exports.Dispatcher = Dispatcher;
158
+ function defaultOnError(err, event) {
159
+ if (event != null) {
160
+ console.error(`[Dispatcher] fire failed for ${event.templateId}/${event.occurrenceKey}:`, err);
161
+ }
162
+ else {
163
+ console.error("[Dispatcher] error:", err);
164
+ }
165
+ }
@@ -0,0 +1,9 @@
1
+ export interface DispatcherStore {
2
+ loadCursor(): Promise<number | null>;
3
+ saveCursor(cursor: number): Promise<void>;
4
+ }
5
+ export declare class InMemoryDispatcherStore implements DispatcherStore {
6
+ private _cursor;
7
+ loadCursor(): Promise<number | null>;
8
+ saveCursor(cursor: number): Promise<void>;
9
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InMemoryDispatcherStore = void 0;
4
+ // Reference in-memory implementation. Useful for tests and short-lived
5
+ // processes that don't need crash recovery.
6
+ class InMemoryDispatcherStore {
7
+ _cursor = null;
8
+ async loadCursor() {
9
+ return this._cursor;
10
+ }
11
+ async saveCursor(cursor) {
12
+ this._cursor = cursor;
13
+ }
14
+ }
15
+ exports.InMemoryDispatcherStore = InMemoryDispatcherStore;
@@ -0,0 +1,3 @@
1
+ import type { AnchoredRule } from "../types.js";
2
+ import { type Occurrence } from "./types.js";
3
+ export declare function generateAnchored(rule: AnchoredRule, tz: string | undefined, windowStart: number, windowEnd: number): Occurrence[];
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateAnchored = generateAnchored;
4
+ const tz_js_1 = require("../tz.js");
5
+ const types_js_1 = require("./types.js");
6
+ // Anchored is tz-independent for STEPPING (every occurrence is exactly
7
+ // intervalMs ms from the anchor), but tz can still affect the end-of-
8
+ // occurrence when hourSpan is used. seriesStart/seriesEnd bound in UTC.
9
+ function generateAnchored(rule, tz, windowStart, windowEnd) {
10
+ if (rule.intervalMs <= 0) {
11
+ throw new Error(`AnchoredRule.intervalMs must be > 0 (got ${rule.intervalMs})`);
12
+ }
13
+ if (rule.count != null && rule.count < 0) {
14
+ throw new Error(`AnchoredRule.count must be >= 0 (got ${rule.count})`);
15
+ }
16
+ const seriesEnd = rule.seriesEnd ?? Number.POSITIVE_INFINITY;
17
+ const effectiveEnd = Math.min(windowEnd, seriesEnd);
18
+ const effectiveStart = Math.max(windowStart, rule.seriesStart);
19
+ if (effectiveStart >= effectiveEnd)
20
+ return [];
21
+ // If `count` is set we must start counting from the anchor; otherwise we
22
+ // can fast-forward to the window start.
23
+ let index;
24
+ let occurrenceStart;
25
+ if (rule.count != null) {
26
+ index = 0;
27
+ occurrenceStart = rule.anchorDate;
28
+ }
29
+ else {
30
+ if (effectiveStart <= rule.anchorDate) {
31
+ index = 0;
32
+ occurrenceStart = rule.anchorDate;
33
+ }
34
+ else {
35
+ // First occurrence at or after effectiveStart.
36
+ const stepsAhead = Math.ceil((effectiveStart - rule.anchorDate) / rule.intervalMs);
37
+ index = stepsAhead;
38
+ occurrenceStart = rule.anchorDate + stepsAhead * rule.intervalMs;
39
+ }
40
+ }
41
+ const out = [];
42
+ while (occurrenceStart < effectiveEnd) {
43
+ if (rule.count != null && index >= rule.count)
44
+ break;
45
+ const occEnd = (0, tz_js_1.computeSpanEnd)(occurrenceStart, rule.timeSpan, tz);
46
+ if ((0, types_js_1.intersects)(occurrenceStart, occEnd, windowStart, windowEnd) &&
47
+ occurrenceStart >= rule.seriesStart) {
48
+ out.push({ startTimestamp: occurrenceStart, endTimestamp: occEnd });
49
+ if (out.length > types_js_1.MAX_OCCURRENCES_PER_GENERATE) {
50
+ throw new types_js_1.TooManyOccurrencesError("anchored");
51
+ }
52
+ }
53
+ index++;
54
+ occurrenceStart += rule.intervalMs;
55
+ }
56
+ return out;
57
+ }
@@ -0,0 +1,3 @@
1
+ import type { DaysOfMonthRule, Disambiguation } from "../types.js";
2
+ import { type Occurrence } from "./types.js";
3
+ export declare function generateDaysOfMonth(rule: DaysOfMonthRule, tz: string | undefined, windowStart: number, windowEnd: number, disambiguation?: Disambiguation): Occurrence[];
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateDaysOfMonth = generateDaysOfMonth;
4
+ const tz_js_1 = require("../tz.js");
5
+ const types_js_1 = require("./types.js");
6
+ // Numerical date-based recurrence: months × daysOfMonth. Days clamp per
7
+ // month (day 31 in April → Apr 30, day 29 in Feb non-leap → Feb 28).
8
+ function generateDaysOfMonth(rule, tz, windowStart, windowEnd, disambiguation = "compatible") {
9
+ if (rule.yearInterval <= 0) {
10
+ throw new Error(`DaysOfMonthRule.yearInterval must be > 0 (got ${rule.yearInterval})`);
11
+ }
12
+ if (rule.months.length === 0) {
13
+ throw new Error("DaysOfMonthRule.months must be non-empty");
14
+ }
15
+ for (const m of rule.months) {
16
+ if (m < 0 || m > 11) {
17
+ throw new Error(`DaysOfMonthRule.months entries must be in 0..11 (got ${m})`);
18
+ }
19
+ }
20
+ if (rule.daysOfMonth.length === 0) {
21
+ throw new Error("DaysOfMonthRule.daysOfMonth must be non-empty");
22
+ }
23
+ for (const d of rule.daysOfMonth) {
24
+ if (d < 1 || d > 31) {
25
+ throw new Error(`DaysOfMonthRule.daysOfMonth entries must be in 1..31 (got ${d})`);
26
+ }
27
+ }
28
+ if (rule.count != null && rule.count < 0) {
29
+ throw new Error(`DaysOfMonthRule.count must be >= 0 (got ${rule.count})`);
30
+ }
31
+ const seriesEnd = rule.seriesEnd ?? Number.POSITIVE_INFINITY;
32
+ const effectiveEnd = Math.min(windowEnd, seriesEnd);
33
+ if (rule.seriesStart >= effectiveEnd)
34
+ return [];
35
+ const months = [...new Set(rule.months)].sort((a, b) => a - b);
36
+ const daysOfMonth = [...new Set(rule.daysOfMonth)].sort((a, b) => a - b);
37
+ const startYear = (0, tz_js_1.utcToWallParts)(rule.seriesStart, tz).year;
38
+ const endYear = (0, tz_js_1.utcToWallParts)(Math.min(effectiveEnd, Number.MAX_SAFE_INTEGER), tz).year;
39
+ const out = [];
40
+ let count = 0;
41
+ for (let year = startYear; year <= endYear; year += rule.yearInterval) {
42
+ const yearEmits = [];
43
+ for (const month of months) {
44
+ const len = (0, tz_js_1.monthLength)(year, month);
45
+ // Dedup clamped days so day-31 + day-30 in April don't both emit Apr 30.
46
+ const clampedDays = [...new Set(daysOfMonth.map((d) => Math.min(d, len)))];
47
+ for (const day of clampedDays) {
48
+ if (disambiguation === "skip" &&
49
+ (0, tz_js_1.isNonexistentWallTime)(year, month, day, rule.timeOfDay, tz)) {
50
+ continue;
51
+ }
52
+ const occStart = (0, tz_js_1.wallTimeToUtc)(year, month, day, rule.timeOfDay, tz, disambiguation);
53
+ const occEnd = (0, tz_js_1.computeSpanEnd)(occStart, rule.timeSpan, tz);
54
+ if (occStart < rule.seriesStart)
55
+ continue;
56
+ if (occStart >= seriesEnd)
57
+ continue;
58
+ if (rule.count != null && count >= rule.count) {
59
+ yearEmits.sort((a, b) => a.startTimestamp - b.startTimestamp);
60
+ out.push(...yearEmits);
61
+ return out;
62
+ }
63
+ count++;
64
+ if ((0, types_js_1.intersects)(occStart, occEnd, windowStart, windowEnd)) {
65
+ yearEmits.push({ startTimestamp: occStart, endTimestamp: occEnd });
66
+ if (out.length + yearEmits.length > types_js_1.MAX_OCCURRENCES_PER_GENERATE) {
67
+ throw new types_js_1.TooManyOccurrencesError("daysOfMonth");
68
+ }
69
+ }
70
+ }
71
+ }
72
+ yearEmits.sort((a, b) => a.startTimestamp - b.startTimestamp);
73
+ out.push(...yearEmits);
74
+ }
75
+ return out;
76
+ }