@smthrs/triggers 0.0.0-stage → 1.0.0-rc.3

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 (226) hide show
  1. package/CHANGELOG.md +219 -0
  2. package/LICENSE +21 -0
  3. package/README.md +109 -2
  4. package/dist/cjs/CatchUp.d.ts +25 -0
  5. package/dist/cjs/CatchUp.d.ts.map +1 -0
  6. package/dist/cjs/CatchUp.js +72 -0
  7. package/dist/cjs/CatchUp.js.map +7 -0
  8. package/dist/cjs/Channel.d.ts +108 -0
  9. package/dist/cjs/Channel.d.ts.map +1 -0
  10. package/dist/cjs/Channel.js +25 -0
  11. package/dist/cjs/Channel.js.map +7 -0
  12. package/dist/cjs/ClaimDecision.d.ts +183 -0
  13. package/dist/cjs/ClaimDecision.d.ts.map +1 -0
  14. package/dist/cjs/ClaimDecision.js +144 -0
  15. package/dist/cjs/ClaimDecision.js.map +7 -0
  16. package/dist/cjs/Cron.d.ts +91 -0
  17. package/dist/cjs/Cron.d.ts.map +1 -0
  18. package/dist/cjs/Cron.js +108 -0
  19. package/dist/cjs/Cron.js.map +7 -0
  20. package/dist/cjs/DispatchReader.d.ts +77 -0
  21. package/dist/cjs/DispatchReader.d.ts.map +1 -0
  22. package/dist/cjs/DispatchReader.js +121 -0
  23. package/dist/cjs/DispatchReader.js.map +7 -0
  24. package/dist/cjs/Overlap.d.ts +40 -0
  25. package/dist/cjs/Overlap.d.ts.map +1 -0
  26. package/dist/cjs/Overlap.js +37 -0
  27. package/dist/cjs/Overlap.js.map +7 -0
  28. package/dist/cjs/Schedule.d.ts +96 -0
  29. package/dist/cjs/Schedule.d.ts.map +1 -0
  30. package/dist/cjs/Schedule.js +61 -0
  31. package/dist/cjs/Schedule.js.map +7 -0
  32. package/dist/cjs/Scheduler.d.ts +248 -0
  33. package/dist/cjs/Scheduler.d.ts.map +1 -0
  34. package/dist/cjs/Scheduler.js +647 -0
  35. package/dist/cjs/Scheduler.js.map +7 -0
  36. package/dist/cjs/SqlTriggerStore.d.ts +29 -0
  37. package/dist/cjs/SqlTriggerStore.d.ts.map +1 -0
  38. package/dist/cjs/SqlTriggerStore.js +483 -0
  39. package/dist/cjs/SqlTriggerStore.js.map +7 -0
  40. package/dist/cjs/Trigger.d.ts +81 -0
  41. package/dist/cjs/Trigger.d.ts.map +1 -0
  42. package/dist/cjs/Trigger.js +54 -0
  43. package/dist/cjs/Trigger.js.map +7 -0
  44. package/dist/cjs/TriggerError.d.ts +58 -0
  45. package/dist/cjs/TriggerError.d.ts.map +1 -0
  46. package/dist/cjs/TriggerError.js +83 -0
  47. package/dist/cjs/TriggerError.js.map +7 -0
  48. package/dist/cjs/TriggerStore.d.ts +411 -0
  49. package/dist/cjs/TriggerStore.d.ts.map +1 -0
  50. package/dist/cjs/TriggerStore.js +127 -0
  51. package/dist/cjs/TriggerStore.js.map +7 -0
  52. package/dist/cjs/Webhook.d.ts +118 -0
  53. package/dist/cjs/Webhook.d.ts.map +1 -0
  54. package/dist/cjs/Webhook.js +153 -0
  55. package/dist/cjs/Webhook.js.map +7 -0
  56. package/dist/cjs/index.d.ts +30 -0
  57. package/dist/cjs/index.d.ts.map +1 -0
  58. package/dist/cjs/index.js +57 -0
  59. package/dist/cjs/index.js.map +7 -0
  60. package/dist/cjs/internal/ActiveRuns.d.ts +51 -0
  61. package/dist/cjs/internal/ActiveRuns.d.ts.map +1 -0
  62. package/dist/cjs/internal/ActiveRuns.js +56 -0
  63. package/dist/cjs/internal/ActiveRuns.js.map +7 -0
  64. package/dist/cjs/internal/DueOccurrences.d.ts +32 -0
  65. package/dist/cjs/internal/DueOccurrences.d.ts.map +1 -0
  66. package/dist/cjs/internal/DueOccurrences.js +80 -0
  67. package/dist/cjs/internal/DueOccurrences.js.map +7 -0
  68. package/dist/cjs/internal/WallClock.d.ts +59 -0
  69. package/dist/cjs/internal/WallClock.d.ts.map +1 -0
  70. package/dist/cjs/internal/WallClock.js +132 -0
  71. package/dist/cjs/internal/WallClock.js.map +7 -0
  72. package/dist/cjs/migrations/0001_triggers.d.ts +15 -0
  73. package/dist/cjs/migrations/0001_triggers.d.ts.map +1 -0
  74. package/dist/cjs/migrations/0001_triggers.js +64 -0
  75. package/dist/cjs/migrations/0001_triggers.js.map +7 -0
  76. package/dist/cjs/migrations/0002_reservation_lease.d.ts +15 -0
  77. package/dist/cjs/migrations/0002_reservation_lease.d.ts.map +1 -0
  78. package/dist/cjs/migrations/0002_reservation_lease.js +41 -0
  79. package/dist/cjs/migrations/0002_reservation_lease.js.map +7 -0
  80. package/dist/cjs/migrations/0003_heartbeat.d.ts +16 -0
  81. package/dist/cjs/migrations/0003_heartbeat.d.ts.map +1 -0
  82. package/dist/cjs/migrations/0003_heartbeat.js +44 -0
  83. package/dist/cjs/migrations/0003_heartbeat.js.map +7 -0
  84. package/dist/cjs/migrations/0004_fire_run_index.d.ts +17 -0
  85. package/dist/cjs/migrations/0004_fire_run_index.d.ts.map +1 -0
  86. package/dist/cjs/migrations/0004_fire_run_index.js +40 -0
  87. package/dist/cjs/migrations/0004_fire_run_index.js.map +7 -0
  88. package/dist/cjs/migrations/index.d.ts +33 -0
  89. package/dist/cjs/migrations/index.d.ts.map +1 -0
  90. package/dist/cjs/migrations/index.js +50 -0
  91. package/dist/cjs/migrations/index.js.map +7 -0
  92. package/dist/cjs/package.json +1 -0
  93. package/dist/cjs/test/TestTriggers.d.ts +12 -0
  94. package/dist/cjs/test/TestTriggers.d.ts.map +1 -0
  95. package/dist/cjs/test/TestTriggers.js +421 -0
  96. package/dist/cjs/test/TestTriggers.js.map +7 -0
  97. package/dist/esm/CatchUp.d.ts +25 -0
  98. package/dist/esm/CatchUp.d.ts.map +1 -0
  99. package/dist/esm/CatchUp.js +52 -0
  100. package/dist/esm/CatchUp.js.map +1 -0
  101. package/dist/esm/Channel.d.ts +108 -0
  102. package/dist/esm/Channel.d.ts.map +1 -0
  103. package/dist/esm/Channel.js +19 -0
  104. package/dist/esm/Channel.js.map +1 -0
  105. package/dist/esm/ClaimDecision.d.ts +183 -0
  106. package/dist/esm/ClaimDecision.d.ts.map +1 -0
  107. package/dist/esm/ClaimDecision.js +172 -0
  108. package/dist/esm/ClaimDecision.js.map +1 -0
  109. package/dist/esm/Cron.d.ts +91 -0
  110. package/dist/esm/Cron.d.ts.map +1 -0
  111. package/dist/esm/Cron.js +132 -0
  112. package/dist/esm/Cron.js.map +1 -0
  113. package/dist/esm/DispatchReader.d.ts +77 -0
  114. package/dist/esm/DispatchReader.d.ts.map +1 -0
  115. package/dist/esm/DispatchReader.js +152 -0
  116. package/dist/esm/DispatchReader.js.map +1 -0
  117. package/dist/esm/Overlap.d.ts +40 -0
  118. package/dist/esm/Overlap.d.ts.map +1 -0
  119. package/dist/esm/Overlap.js +31 -0
  120. package/dist/esm/Overlap.js.map +1 -0
  121. package/dist/esm/Schedule.d.ts +96 -0
  122. package/dist/esm/Schedule.d.ts.map +1 -0
  123. package/dist/esm/Schedule.js +73 -0
  124. package/dist/esm/Schedule.js.map +1 -0
  125. package/dist/esm/Scheduler.d.ts +248 -0
  126. package/dist/esm/Scheduler.d.ts.map +1 -0
  127. package/dist/esm/Scheduler.js +734 -0
  128. package/dist/esm/Scheduler.js.map +1 -0
  129. package/dist/esm/SqlTriggerStore.d.ts +29 -0
  130. package/dist/esm/SqlTriggerStore.d.ts.map +1 -0
  131. package/dist/esm/SqlTriggerStore.js +483 -0
  132. package/dist/esm/SqlTriggerStore.js.map +1 -0
  133. package/dist/esm/Trigger.d.ts +81 -0
  134. package/dist/esm/Trigger.d.ts.map +1 -0
  135. package/dist/esm/Trigger.js +56 -0
  136. package/dist/esm/Trigger.js.map +1 -0
  137. package/dist/esm/TriggerError.d.ts +58 -0
  138. package/dist/esm/TriggerError.d.ts.map +1 -0
  139. package/dist/esm/TriggerError.js +110 -0
  140. package/dist/esm/TriggerError.js.map +1 -0
  141. package/dist/esm/TriggerStore.d.ts +411 -0
  142. package/dist/esm/TriggerStore.d.ts.map +1 -0
  143. package/dist/esm/TriggerStore.js +209 -0
  144. package/dist/esm/TriggerStore.js.map +1 -0
  145. package/dist/esm/Webhook.d.ts +118 -0
  146. package/dist/esm/Webhook.d.ts.map +1 -0
  147. package/dist/esm/Webhook.js +186 -0
  148. package/dist/esm/Webhook.js.map +1 -0
  149. package/dist/esm/index.d.ts +30 -0
  150. package/dist/esm/index.d.ts.map +1 -0
  151. package/dist/esm/index.js +30 -0
  152. package/dist/esm/index.js.map +1 -0
  153. package/dist/esm/internal/ActiveRuns.d.ts +51 -0
  154. package/dist/esm/internal/ActiveRuns.d.ts.map +1 -0
  155. package/dist/esm/internal/ActiveRuns.js +34 -0
  156. package/dist/esm/internal/ActiveRuns.js.map +1 -0
  157. package/dist/esm/internal/DueOccurrences.d.ts +32 -0
  158. package/dist/esm/internal/DueOccurrences.d.ts.map +1 -0
  159. package/dist/esm/internal/DueOccurrences.js +55 -0
  160. package/dist/esm/internal/DueOccurrences.js.map +1 -0
  161. package/dist/esm/internal/WallClock.d.ts +59 -0
  162. package/dist/esm/internal/WallClock.d.ts.map +1 -0
  163. package/dist/esm/internal/WallClock.js +155 -0
  164. package/dist/esm/internal/WallClock.js.map +1 -0
  165. package/dist/esm/migrations/0001_triggers.d.ts +15 -0
  166. package/dist/esm/migrations/0001_triggers.d.ts.map +1 -0
  167. package/dist/esm/migrations/0001_triggers.js +42 -0
  168. package/dist/esm/migrations/0001_triggers.js.map +1 -0
  169. package/dist/esm/migrations/0002_reservation_lease.d.ts +15 -0
  170. package/dist/esm/migrations/0002_reservation_lease.d.ts.map +1 -0
  171. package/dist/esm/migrations/0002_reservation_lease.js +19 -0
  172. package/dist/esm/migrations/0002_reservation_lease.js.map +1 -0
  173. package/dist/esm/migrations/0003_heartbeat.d.ts +16 -0
  174. package/dist/esm/migrations/0003_heartbeat.d.ts.map +1 -0
  175. package/dist/esm/migrations/0003_heartbeat.js +23 -0
  176. package/dist/esm/migrations/0003_heartbeat.js.map +1 -0
  177. package/dist/esm/migrations/0004_fire_run_index.d.ts +17 -0
  178. package/dist/esm/migrations/0004_fire_run_index.d.ts.map +1 -0
  179. package/dist/esm/migrations/0004_fire_run_index.js +20 -0
  180. package/dist/esm/migrations/0004_fire_run_index.js.map +1 -0
  181. package/dist/esm/migrations/index.d.ts +33 -0
  182. package/dist/esm/migrations/index.d.ts.map +1 -0
  183. package/dist/esm/migrations/index.js +37 -0
  184. package/dist/esm/migrations/index.js.map +1 -0
  185. package/dist/esm/test/TestTriggers.d.ts +12 -0
  186. package/dist/esm/test/TestTriggers.d.ts.map +1 -0
  187. package/dist/esm/test/TestTriggers.js +439 -0
  188. package/dist/esm/test/TestTriggers.js.map +1 -0
  189. package/docs/README.md +140 -0
  190. package/docs/api.md +1186 -0
  191. package/docs/concepts/channels.md +103 -0
  192. package/docs/concepts/claim-protocol.md +229 -0
  193. package/docs/concepts/policies.md +124 -0
  194. package/docs/concepts/schedules.md +134 -0
  195. package/docs/guides/choose-a-policy.md +113 -0
  196. package/docs/guides/declare-a-trigger.md +136 -0
  197. package/docs/guides/ingest-a-webhook.md +164 -0
  198. package/docs/guides/run-the-scheduler.md +173 -0
  199. package/docs/guides/testing.md +157 -0
  200. package/docs/installation.md +81 -0
  201. package/docs/quickstart.md +163 -0
  202. package/docs/troubleshooting.md +281 -0
  203. package/package.json +241 -3
  204. package/src/CatchUp.ts +73 -0
  205. package/src/Channel.ts +121 -0
  206. package/src/ClaimDecision.ts +303 -0
  207. package/src/Cron.ts +173 -0
  208. package/src/DispatchReader.ts +179 -0
  209. package/src/Overlap.ts +53 -0
  210. package/src/Schedule.ts +112 -0
  211. package/src/Scheduler.ts +1154 -0
  212. package/src/SqlTriggerStore.ts +638 -0
  213. package/src/Trigger.ts +84 -0
  214. package/src/TriggerError.ts +129 -0
  215. package/src/TriggerStore.ts +514 -0
  216. package/src/Webhook.ts +270 -0
  217. package/src/index.ts +30 -0
  218. package/src/internal/ActiveRuns.ts +83 -0
  219. package/src/internal/DueOccurrences.ts +101 -0
  220. package/src/internal/WallClock.ts +172 -0
  221. package/src/migrations/0001_triggers.ts +43 -0
  222. package/src/migrations/0002_reservation_lease.ts +20 -0
  223. package/src/migrations/0003_heartbeat.ts +24 -0
  224. package/src/migrations/0004_fire_run_index.ts +21 -0
  225. package/src/migrations/index.ts +40 -0
  226. package/src/test/TestTriggers.ts +563 -0
@@ -0,0 +1,647 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var Scheduler_exports = {};
30
+ __export(Scheduler_exports, {
31
+ Runner: () => Runner,
32
+ Scheduler: () => Scheduler,
33
+ defaultConcurrency: () => defaultConcurrency,
34
+ defaultFireRetention: () => defaultFireRetention,
35
+ defaultHost: () => defaultHost,
36
+ durationBoundaries: () => durationBoundaries,
37
+ idempotencyKey: () => idempotencyKey,
38
+ layer: () => layer,
39
+ layerControlRunner: () => layerControlRunner,
40
+ layerNoop: () => layerNoop,
41
+ layerNoopRunner: () => layerNoopRunner,
42
+ make: () => make,
43
+ makeNoop: () => makeNoop,
44
+ makeNoopRunner: () => makeNoopRunner,
45
+ makeRunner: () => makeRunner,
46
+ parkedAttempts: () => parkedAttempts
47
+ });
48
+ module.exports = __toCommonJS(Scheduler_exports);
49
+ var Control = __toESM(require("@smthrs/control/Control"), 1);
50
+ var Cause = __toESM(require("effect/Cause"), 1);
51
+ var Clock = __toESM(require("effect/Clock"), 1);
52
+ var Context = __toESM(require("effect/Context"), 1);
53
+ var Duration = __toESM(require("effect/Duration"), 1);
54
+ var Effect = __toESM(require("effect/Effect"), 1);
55
+ var Fiber = __toESM(require("effect/Fiber"), 1);
56
+ var Layer = __toESM(require("effect/Layer"), 1);
57
+ var Metric = __toESM(require("effect/Metric"), 1);
58
+ var Option = __toESM(require("effect/Option"), 1);
59
+ var Ref = __toESM(require("effect/Ref"), 1);
60
+ var Result = __toESM(require("effect/Result"), 1);
61
+ var Semaphore = __toESM(require("effect/Semaphore"), 1);
62
+ var ActiveRuns = __toESM(require("./internal/ActiveRuns.js"), 1);
63
+ var DueOccurrences = __toESM(require("./internal/DueOccurrences.js"), 1);
64
+ var import_TriggerError = require("./TriggerError.js");
65
+ var import_TriggerStore = require("./TriggerStore.js");
66
+ class Runner extends Context.Service()("flows/triggers/Scheduler/Runner") {
67
+ }
68
+ const makeRunner = (implementation) => Runner.of(implementation);
69
+ const makeNoopRunner = (overrides = {}) => makeRunner({
70
+ start: (input) => Effect.succeed(input.idempotencyKey),
71
+ inspect: () => Effect.succeed("completed"),
72
+ cancel: () => Effect.void,
73
+ ...overrides
74
+ });
75
+ const layerNoopRunner = (overrides = {}) => Layer.succeed(Runner)(makeNoopRunner(overrides));
76
+ const runnerError = (message, cause) => new import_TriggerError.TriggerError({
77
+ code: "runner",
78
+ message,
79
+ ...cause === void 0 ? {} : { cause }
80
+ });
81
+ const translateRunnerFailure = (effect, message) => effect.pipe(
82
+ Effect.mapError((error) => error instanceof import_TriggerError.TriggerError ? error : runnerError(message, error)),
83
+ Effect.catchDefect((defect) => Effect.fail(runnerError(message, defect)))
84
+ );
85
+ const invalidOption = (field, requirement) => new import_TriggerError.TriggerError({
86
+ code: "invalid_options",
87
+ message: `${field} must be ${requirement}`,
88
+ path: field
89
+ });
90
+ const duration = (input, field) => Option.match(Duration.fromInput(input), {
91
+ onNone: () => Effect.fail(invalidOption(field, "a valid Effect duration")),
92
+ onSome: (value) => (
93
+ // Zero polls a CPU-tight loop and an infinite interval never completes,
94
+ // and `Duration.fromInput` accepts both.
95
+ Duration.isFinite(value) && Duration.toMillis(value) > 0 ? Effect.succeed(value) : Effect.fail(invalidOption(field, "a finite positive duration"))
96
+ )
97
+ });
98
+ const stoppedMessage = (runId, state) => {
99
+ switch (state) {
100
+ case "failed":
101
+ return `Run ${runId} failed`;
102
+ case "cancelled":
103
+ return `Run ${runId} was cancelled`;
104
+ case "missing":
105
+ return `Run ${runId} is unknown to the runner`;
106
+ }
107
+ };
108
+ const runIdFromReceipt = (receipt) => {
109
+ switch (receipt._tag) {
110
+ case "Accepted":
111
+ case "AlreadyApplied":
112
+ return receipt.runId === void 0 ? Effect.fail(runnerError(`Control ${receipt._tag} receipt did not include a run id`)) : Effect.succeed(receipt.runId);
113
+ case "Terminal":
114
+ return Effect.succeed(receipt.runId);
115
+ case "Conflict":
116
+ return Effect.fail(runnerError(`Control rejected the scheduled run: ${receipt.message}`));
117
+ }
118
+ };
119
+ const parkedAttempts = 8;
120
+ const runApprovedPlan = (control, plan, key, attempt) => control.run({
121
+ _tag: "Plan",
122
+ planId: plan.planId,
123
+ digest: plan.digest,
124
+ envelope: plan.envelope,
125
+ idempotencyKey: key
126
+ }).pipe(
127
+ Effect.mapError((error) => runnerError("Control could not launch the scheduled run", error)),
128
+ Effect.flatMap((receipt) => {
129
+ if (receipt._tag !== "Parked") return runIdFromReceipt(receipt);
130
+ if (attempt >= parkedAttempts) {
131
+ return Effect.fail(
132
+ runnerError(
133
+ `Control plan ${plan.planId} is still parked awaiting approval after ${attempt} attempts`
134
+ )
135
+ );
136
+ }
137
+ return Effect.logInfo(`A scheduled plan is parked awaiting approval, attempt ${attempt}`).pipe(
138
+ Effect.andThen(Effect.sleep(Duration.millis(1e3 * 2 ** (attempt - 1)))),
139
+ Effect.andThen(Effect.suspend(() => runApprovedPlan(control, plan, key, attempt + 1)))
140
+ );
141
+ })
142
+ );
143
+ const settledState = (status) => status === "cancelled" || status === "completed" || status === "failed" ? status : void 0;
144
+ const layerControlRunner = Layer.effect(
145
+ Runner,
146
+ Effect.gen(function* () {
147
+ const control = yield* Control.Control;
148
+ return makeRunner({
149
+ start: (input) => translateRunnerFailure(
150
+ control.plan(input).pipe(
151
+ Effect.flatMap((plan) => runApprovedPlan(control, plan, input.idempotencyKey, 1))
152
+ ),
153
+ "Control could not launch the scheduled run"
154
+ ),
155
+ inspect: (runId) => translateRunnerFailure(
156
+ control.list({ _tag: "runs", filters: { runId }, limit: 1 }).pipe(
157
+ Effect.map((response) => {
158
+ const run = response._tag === "runs" ? response.items.find((candidate) => candidate.runId === runId) : void 0;
159
+ if (run === void 0) return "missing";
160
+ return settledState(run.status) ?? "active";
161
+ })
162
+ ),
163
+ `Control could not inspect run ${runId}`
164
+ ),
165
+ cancel: (runId) => translateRunnerFailure(
166
+ control.cancel({
167
+ runId,
168
+ idempotencyKey: `trigger-cancel:${runId}`
169
+ }).pipe(
170
+ Effect.flatMap((receipt) => {
171
+ switch (receipt._tag) {
172
+ case "Accepted":
173
+ case "AlreadyApplied":
174
+ case "Terminal":
175
+ return Effect.void;
176
+ case "Conflict":
177
+ return Effect.fail(runnerError(`Control refused scheduled cancellation: ${receipt.message}`));
178
+ case "Parked":
179
+ return Effect.fail(runnerError(`Control returned a Parked receipt for cancellation of run ${runId}`));
180
+ }
181
+ })
182
+ ),
183
+ `Control could not cancel run ${runId}`
184
+ )
185
+ });
186
+ })
187
+ );
188
+ const defaultHost = "local";
189
+ const defaultConcurrency = 4;
190
+ const defaultFireRetention = Duration.days(30);
191
+ const pruneEvery = 60 * 60 * 1e3;
192
+ const durationBoundaries = [10, 100, 1e3, 1e4, 6e4, 3e5];
193
+ const firesRecorded = Metric.counter("smithers.triggers.fires");
194
+ const tickDuration = Metric.histogram("smithers.triggers.tick_duration_ms", { boundaries: durationBoundaries });
195
+ const launchDuration = Metric.histogram("smithers.triggers.launch_duration_ms", { boundaries: durationBoundaries });
196
+ const timed = (histogram, effect) => Effect.gen(function* () {
197
+ const started = yield* Clock.currentTimeMillis;
198
+ return yield* effect.pipe(
199
+ Effect.ensuring(
200
+ Effect.flatMap(Clock.currentTimeMillis, (ended) => Metric.update(histogram, ended - started))
201
+ )
202
+ );
203
+ });
204
+ const deadline = (effect, limit, what) => Effect.timeoutOrElse(effect, {
205
+ duration: limit,
206
+ orElse: () => Effect.fail(
207
+ new import_TriggerError.TriggerError({
208
+ code: "runner_timeout",
209
+ message: `${what} did not answer within ${Duration.toMillis(limit)} ms`
210
+ })
211
+ )
212
+ });
213
+ class Scheduler extends Context.Service()("flows/triggers/Scheduler") {
214
+ }
215
+ const idempotencyKey = (triggerId, occurrence) => `${triggerId}:${new Date(occurrence).toISOString()}`;
216
+ const attempted = (annotations, work, effect) => Effect.catchCause(Effect.as(effect, true), (cause) => Cause.hasInterrupts(cause) ? Effect.failCause(cause) : Effect.as(
217
+ Effect.annotateLogs(Effect.logWarning(`A trigger ${work} failed`, cause), annotations),
218
+ false
219
+ ));
220
+ const isolate = (annotations, work, effect) => Effect.asVoid(attempted(annotations, work, effect));
221
+ const make = (options = {}) => Effect.gen(function* () {
222
+ const parentScope = yield* Effect.scope;
223
+ const providedStore = yield* import_TriggerStore.TriggerStore;
224
+ const store = {
225
+ ...providedStore,
226
+ recordResult: (result) => providedStore.recordResult(result).pipe(
227
+ Effect.tap(() => Metric.update(firesRecorded.pipe(Metric.withAttributes({ outcome: result.outcome })), 1))
228
+ )
229
+ };
230
+ const active = yield* ActiveRuns.make;
231
+ const observedAt = yield* Ref.make(/* @__PURE__ */ new Map());
232
+ const semaphore = yield* Semaphore.make(1);
233
+ const runPollInterval = yield* duration(options.runPollInterval ?? "15 seconds", "runPollInterval");
234
+ const concurrency = options.concurrency ?? defaultConcurrency;
235
+ if (!Number.isInteger(concurrency) || concurrency < 1) {
236
+ return yield* Effect.fail(invalidOption("concurrency", "a positive integer"));
237
+ }
238
+ const startTimeout = yield* duration(options.startTimeout ?? "4 minutes", "startTimeout");
239
+ const inspectTimeout = yield* duration(options.inspectTimeout ?? "30 seconds", "inspectTimeout");
240
+ const cancelTimeout = yield* duration(options.cancelTimeout ?? "30 seconds", "cancelTimeout");
241
+ const fireRetention = yield* duration(options.fireRetention ?? defaultFireRetention, "fireRetention");
242
+ const lastPrunedAt = yield* Ref.make(void 0);
243
+ const provided = yield* Runner;
244
+ const runner = {
245
+ start: (input) => deadline(provided.start(input), startTimeout, `Runner.start for ${input.idempotencyKey}`),
246
+ inspect: (runId) => deadline(provided.inspect(runId), inspectTimeout, `Runner.inspect for run ${runId}`),
247
+ cancel: (runId) => deadline(provided.cancel(runId), cancelTimeout, `Runner.cancel for run ${runId}`)
248
+ };
249
+ const observe = (triggerId, occurrence) => Ref.update(observedAt, (current) => {
250
+ const existing = current.get(triggerId);
251
+ if (existing !== void 0 && existing >= occurrence) return current;
252
+ return new Map(current).set(triggerId, occurrence);
253
+ });
254
+ const inspectStored = (runId) => (0, import_TriggerStore.isReservation)(runId) ? Effect.succeed("active") : runner.inspect(runId);
255
+ const occurrenceOf = (triggerId, runId) => {
256
+ const reserved = (0, import_TriggerStore.reservationOccurrence)(runId);
257
+ if (reserved !== void 0) return Effect.succeed(reserved);
258
+ return store.activeOccurrence(triggerId, runId).pipe(
259
+ Effect.map((occurrence) => Option.isSome(occurrence) ? occurrence.value : Number.NEGATIVE_INFINITY)
260
+ );
261
+ };
262
+ const recordSettled = (triggerId, occurrence, runId, state) => state === "completed" ? store.recordResult({ triggerId, occurrence, outcome: "completed", runId }) : store.recordResult({ triggerId, occurrence, outcome: "failed", runId, error: stoppedMessage(runId, state) });
263
+ const settleRecovered = (triggerId, occurrence, runId, state) => Number.isFinite(occurrence) ? recordSettled(triggerId, occurrence, runId, state) : store.clearActive(triggerId, runId);
264
+ const storedActive = (triggerId, held) => held !== void 0 && held.activeRunId === void 0 ? Effect.succeed(Option.none()) : store.activeRun(triggerId);
265
+ const recoverStored = (trigger, runId) => Effect.gen(function* () {
266
+ const occurrence = yield* occurrenceOf(trigger.id, runId);
267
+ const state = yield* inspectStored(runId);
268
+ if (state !== "active") {
269
+ yield* settleRecovered(trigger.id, occurrence, runId, state);
270
+ return void 0;
271
+ }
272
+ const recovered = { occurrence, runId };
273
+ yield* active.take(trigger.id, recovered);
274
+ return recovered;
275
+ });
276
+ const resolveActive = (trigger, held) => Effect.gen(function* () {
277
+ const local = yield* active.get(trigger.id);
278
+ if (local !== void 0) {
279
+ if (local.fiber !== void 0) return local;
280
+ if (!(0, import_TriggerStore.isReservation)(local.runId)) {
281
+ const state = yield* inspectStored(local.runId);
282
+ if (state === "active") return local;
283
+ yield* settleRecovered(trigger.id, local.occurrence, local.runId, state);
284
+ yield* active.remove(trigger.id, local.occurrence);
285
+ } else {
286
+ const stored2 = yield* storedActive(trigger.id, held);
287
+ if (Option.isNone(stored2)) {
288
+ yield* active.remove(trigger.id, local.occurrence);
289
+ return void 0;
290
+ }
291
+ if (stored2.value === local.runId) return local;
292
+ yield* active.remove(trigger.id, local.occurrence);
293
+ return yield* recoverStored(trigger, stored2.value);
294
+ }
295
+ }
296
+ const stored = yield* storedActive(trigger.id, held);
297
+ if (Option.isNone(stored)) return void 0;
298
+ return yield* recoverStored(trigger, stored.value);
299
+ });
300
+ const recordFailed = (trigger, occurrence, error, runId) => store.recordResult({
301
+ triggerId: trigger.id,
302
+ occurrence,
303
+ outcome: "failed",
304
+ error: error.message,
305
+ runId
306
+ });
307
+ const inspectRun = (triggerId, runId) => Effect.gen(function* () {
308
+ for (let attempt = 0; ; attempt++) {
309
+ const inspected = yield* runner.inspect(runId).pipe(
310
+ Effect.map(Option.some),
311
+ Effect.catchCause(
312
+ (cause) => Cause.hasInterrupts(cause) ? Effect.failCause(cause) : Effect.logWarning("A trigger run inspection failed", cause).pipe(
313
+ Effect.annotateLogs({ triggerId, runId, attempt: String(attempt + 1) }),
314
+ Effect.as(Option.none())
315
+ )
316
+ )
317
+ );
318
+ if (Option.isSome(inspected)) return inspected;
319
+ if (attempt === 3) {
320
+ yield* Effect.logWarning("A trigger run monitor detached after inspection retries").pipe(
321
+ Effect.annotateLogs({ triggerId, runId })
322
+ );
323
+ return Option.none();
324
+ }
325
+ yield* Effect.sleep(Math.min(Duration.toMillis(runPollInterval) * 2 ** attempt, 6e4));
326
+ }
327
+ });
328
+ const launch = (trigger, occurrence, reservation, preserveBuffered) => Effect.gen(function* () {
329
+ yield* active.take(trigger.id, { occurrence, runId: reservation });
330
+ let runId;
331
+ let launchRecorded = false;
332
+ let completed = false;
333
+ const lifecycle = Effect.gen(function* () {
334
+ runId = yield* timed(
335
+ launchDuration,
336
+ runner.start({
337
+ flowId: trigger.flowId,
338
+ input: trigger.input,
339
+ idempotencyKey: idempotencyKey(trigger.id, occurrence)
340
+ })
341
+ ).pipe(
342
+ Effect.withSpan("Scheduler.launch", {
343
+ attributes: { triggerId: trigger.id, occurrence: new Date(occurrence).toISOString() }
344
+ })
345
+ );
346
+ const startedRunId = runId;
347
+ yield* active.update(trigger.id, occurrence, (entry) => ({ ...entry, runId: startedRunId }));
348
+ yield* store.recordResult({
349
+ triggerId: trigger.id,
350
+ occurrence,
351
+ outcome: "launched",
352
+ runId,
353
+ reservationId: reservation
354
+ });
355
+ launchRecorded = true;
356
+ let state;
357
+ while (true) {
358
+ const inspected = yield* inspectRun(trigger.id, runId);
359
+ if (Option.isNone(inspected)) return;
360
+ state = inspected.value;
361
+ if (state !== "active") break;
362
+ yield* Effect.sleep(runPollInterval);
363
+ }
364
+ yield* recordSettled(trigger.id, occurrence, runId, state);
365
+ completed = true;
366
+ }).pipe(
367
+ Effect.catch(
368
+ (error) => Effect.gen(function* () {
369
+ if (launchRecorded) {
370
+ yield* Effect.logWarning("A trigger run monitor could not persist completion", error).pipe(
371
+ Effect.annotateLogs({ triggerId: trigger.id, runId })
372
+ );
373
+ return;
374
+ }
375
+ if (error.code === "stale_owner" && runId !== void 0) {
376
+ yield* Effect.logWarning("A trigger launch lost its reservation", error).pipe(
377
+ Effect.annotateLogs({ triggerId: trigger.id, occurrence: String(occurrence) })
378
+ );
379
+ const losingRunId = runId;
380
+ yield* isolate(
381
+ { triggerId: trigger.id, runId },
382
+ "losing run reconciliation",
383
+ Effect.gen(function* () {
384
+ const held = yield* store.inspect(trigger.id);
385
+ if (held.activeRunId === losingRunId || held.activeRunId !== void 0 && (0, import_TriggerStore.reservationOccurrence)(held.activeRunId) === occurrence) return;
386
+ yield* runner.cancel(losingRunId);
387
+ })
388
+ ).pipe(Effect.ignore);
389
+ return;
390
+ }
391
+ if (preserveBuffered) {
392
+ yield* isolate(
393
+ { triggerId: trigger.id },
394
+ "buffered launch compensation",
395
+ store.restorePending({ triggerId: trigger.id, occurrence, reservationId: reservation })
396
+ ).pipe(Effect.ignore);
397
+ } else {
398
+ if (runId !== void 0 || error.code === "runner_timeout") {
399
+ yield* isolate(
400
+ { triggerId: trigger.id },
401
+ "launch compensation",
402
+ store.restorePending({
403
+ triggerId: trigger.id,
404
+ occurrence,
405
+ reservationId: reservation
406
+ })
407
+ ).pipe(Effect.ignore);
408
+ } else {
409
+ yield* recordFailed(trigger, occurrence, error, reservation).pipe(Effect.ignore);
410
+ }
411
+ }
412
+ yield* Effect.logWarning("A trigger launch failed", error).pipe(
413
+ Effect.annotateLogs({ triggerId: trigger.id, occurrence: String(occurrence) })
414
+ );
415
+ })
416
+ ),
417
+ // Interrupting this fiber detaches the monitor; it never cancels the
418
+ // run. The run is durable and outlives this process, so a deploy or
419
+ // any other scope closure must leave it alone: the next incarnation
420
+ // re-attaches through `resolveActive`. Cancellation is a deliberate
421
+ // act and belongs to `cancelActive` alone.
422
+ Effect.ensuring(Effect.suspend(
423
+ () => launchRecorded && !completed ? active.update(trigger.id, occurrence, (entry) => ({ ...entry, fiber: void 0 })) : active.remove(trigger.id, occurrence)
424
+ ))
425
+ );
426
+ const fiber = yield* Effect.forkIn(
427
+ Effect.scoped(lifecycle).pipe(
428
+ Effect.onExit(
429
+ (exit) => exit._tag === "Failure" && !Cause.hasInterrupts(exit.cause) ? Effect.logWarning(
430
+ launchRecorded ? "A trigger run monitor failed" : "A trigger launch failed",
431
+ exit.cause
432
+ ).pipe(
433
+ Effect.annotateLogs({
434
+ triggerId: trigger.id,
435
+ occurrence: String(occurrence),
436
+ runId: runId ?? reservation
437
+ })
438
+ ) : Effect.void
439
+ )
440
+ ),
441
+ parentScope,
442
+ { startImmediately: true }
443
+ );
444
+ if (fiber.pollUnsafe() === void 0) {
445
+ yield* active.update(trigger.id, occurrence, (entry) => ({ ...entry, fiber }));
446
+ }
447
+ });
448
+ const cancelActive = (trigger, prior, replacementOccurrence, replacementReservation, queueReplacement) => Effect.gen(function* () {
449
+ if (!(0, import_TriggerStore.isReservation)(prior.runId)) {
450
+ yield* runner.cancel(prior.runId).pipe(
451
+ Effect.catch((error) => {
452
+ const restore = queueReplacement ? isolate(
453
+ { triggerId: trigger.id },
454
+ "supersede compensation",
455
+ store.restorePending({
456
+ triggerId: trigger.id,
457
+ occurrence: replacementOccurrence,
458
+ reservationId: replacementReservation
459
+ })
460
+ ) : Effect.void;
461
+ return restore.pipe(
462
+ Effect.andThen(Effect.fail(error))
463
+ );
464
+ })
465
+ );
466
+ }
467
+ if (Number.isFinite(prior.occurrence) && !(0, import_TriggerStore.isReservation)(prior.runId)) {
468
+ yield* store.recordResult({
469
+ triggerId: trigger.id,
470
+ occurrence: prior.occurrence,
471
+ outcome: "superseded",
472
+ runId: prior.runId
473
+ });
474
+ }
475
+ if (prior.fiber !== void 0) yield* Fiber.interrupt(prior.fiber);
476
+ yield* active.remove(trigger.id, prior.occurrence);
477
+ });
478
+ const dispatchClaimed = (trigger, occurrence, claim, resumeBuffered = false) => Effect.gen(function* () {
479
+ switch (claim.action) {
480
+ case "fire":
481
+ yield* launch(trigger, occurrence, claim.reservationId, resumeBuffered);
482
+ return;
483
+ case "skip":
484
+ yield* store.recordResult({
485
+ triggerId: trigger.id,
486
+ occurrence,
487
+ outcome: "skipped"
488
+ });
489
+ return;
490
+ case "buffer":
491
+ yield* store.recordResult({
492
+ triggerId: trigger.id,
493
+ occurrence,
494
+ outcome: "buffered"
495
+ });
496
+ return;
497
+ case "supersede": {
498
+ const superseded = claim.activeRunId;
499
+ if (superseded !== void 0) {
500
+ const local = yield* active.get(trigger.id);
501
+ yield* cancelActive(
502
+ trigger,
503
+ local !== void 0 && local.runId === superseded ? { ...local, runId: superseded } : {
504
+ occurrence: yield* occurrenceOf(trigger.id, superseded),
505
+ runId: superseded
506
+ },
507
+ occurrence,
508
+ claim.reservationId,
509
+ !resumeBuffered
510
+ );
511
+ }
512
+ yield* launch(trigger, occurrence, claim.reservationId, resumeBuffered);
513
+ return;
514
+ }
515
+ }
516
+ });
517
+ const claimOnce = (trigger, occurrence) => store.claimFire({
518
+ triggerId: trigger.id,
519
+ occurrence,
520
+ expectedRevision: trigger.revision
521
+ }).pipe(
522
+ Effect.flatMap((claim) => claim.claimed ? dispatchClaimed(trigger, occurrence, claim) : Effect.void)
523
+ );
524
+ const withRevisionRefresh = (trigger, claim) => claim(trigger).pipe(
525
+ Effect.catch(
526
+ (error) => error.code !== "revision_mismatch" ? Effect.fail(error) : store.get(trigger.id).pipe(
527
+ Effect.flatMap(
528
+ (refreshed) => Option.isNone(refreshed) || refreshed.value.revision === trigger.revision ? Effect.fail(error) : claim(refreshed.value)
529
+ )
530
+ )
531
+ )
532
+ );
533
+ const resumePending = (trigger, held) => (
534
+ // A listed row holding neither a buffered occurrence nor a run has
535
+ // nothing for `claimPending` to take: only expiring a reservation
536
+ // re-arms a pending occurrence, and there is no reservation to expire.
537
+ held !== void 0 && held.pendingAt === void 0 && held.activeRunId === void 0 ? Effect.void : withRevisionRefresh(trigger, (current) => store.claimPending({
538
+ triggerId: current.id,
539
+ expectedRevision: current.revision
540
+ }).pipe(
541
+ Effect.flatMap((pending) => {
542
+ if (Option.isNone(pending)) return Effect.void;
543
+ const occurrence = pending.value.occurrence;
544
+ const claim = pending.value.claim;
545
+ if (!claim.claimed) return Effect.void;
546
+ return dispatchClaimed(
547
+ current,
548
+ occurrence,
549
+ claim,
550
+ true
551
+ ).pipe(
552
+ Effect.onError(
553
+ () => isolate(
554
+ { triggerId: current.id },
555
+ "buffered launch compensation",
556
+ claim.action === "fire" || claim.action === "supersede" ? store.restorePending({ triggerId: current.id, occurrence, reservationId: claim.reservationId }) : store.setPending({ triggerId: current.id, occurrence })
557
+ ).pipe(Effect.ignore)
558
+ )
559
+ );
560
+ })
561
+ ))
562
+ );
563
+ const processTrigger = (trigger, held, refreshed = false) => Effect.gen(function* () {
564
+ const now = yield* Clock.currentTimeMillis;
565
+ const running = yield* resolveActive(trigger, held);
566
+ if (!trigger.enabled) return;
567
+ if (running === void 0 || trigger.overlap === "supersede") yield* resumePending(trigger, held);
568
+ const observed = (yield* Ref.get(observedAt)).get(trigger.id);
569
+ const due = yield* DueOccurrences.compute(trigger, now, observed);
570
+ let dispatched;
571
+ let interrupted = false;
572
+ let stale = false;
573
+ for (const occurrence of due.occurrences) {
574
+ const settledHere = yield* attempted(
575
+ { triggerId: trigger.id },
576
+ `dispatch of occurrence ${occurrence}`,
577
+ claimOnce(trigger, occurrence).pipe(
578
+ Effect.catch(
579
+ (error) => error.code === "revision_mismatch" ? Effect.sync(() => {
580
+ stale = true;
581
+ }) : Effect.fail(error)
582
+ )
583
+ )
584
+ );
585
+ if (stale) break;
586
+ if (settledHere && !interrupted) dispatched = occurrence;
587
+ if (!settledHere) interrupted = true;
588
+ }
589
+ if (stale) {
590
+ if (dispatched !== void 0) yield* observe(trigger.id, dispatched);
591
+ if (refreshed) return;
592
+ const current = yield* store.get(trigger.id);
593
+ if (Option.isNone(current) || current.value.revision === trigger.revision) return;
594
+ return yield* processTrigger(current.value, void 0, true);
595
+ }
596
+ if (!interrupted) return yield* observe(trigger.id, due.watermark);
597
+ if (dispatched !== void 0) yield* observe(trigger.id, dispatched);
598
+ }).pipe(Effect.withSpan("Scheduler.processTrigger", { attributes: { triggerId: trigger.id } }));
599
+ const host = options.host ?? defaultHost;
600
+ const runOnce = semaphore.withPermits(1)(
601
+ Effect.gen(function* () {
602
+ yield* isolate({ host }, "heartbeat", store.heartbeat(host));
603
+ const now = yield* Clock.currentTimeMillis;
604
+ const last = yield* Ref.get(lastPrunedAt);
605
+ if (last === void 0 || now - last >= pruneEvery) {
606
+ yield* Ref.set(lastPrunedAt, now);
607
+ yield* isolate(
608
+ { host },
609
+ "fire ledger prune",
610
+ Effect.asVoid(store.pruneFires({ olderThan: now - Duration.toMillis(fireRetention) }))
611
+ );
612
+ }
613
+ const listing = yield* store.list();
614
+ yield* Effect.forEach(
615
+ listing,
616
+ (row) => isolate(
617
+ { triggerId: row.triggerId },
618
+ "tick",
619
+ Result.isFailure(row.trigger) ? Effect.fail(row.trigger.failure) : processTrigger(row.trigger.success, row)
620
+ ),
621
+ { concurrency, discard: true }
622
+ );
623
+ }).pipe((tick) => timed(tickDuration, tick), Effect.withSpan("Scheduler.runOnce", { attributes: { host } }))
624
+ );
625
+ return Scheduler.of({ runOnce });
626
+ });
627
+ const makeNoop = () => Scheduler.of({ runOnce: Effect.void });
628
+ const layer = (options = {}) => Layer.effect(
629
+ Scheduler,
630
+ Effect.gen(function* () {
631
+ const scheduler = yield* make(options);
632
+ const pollInterval = yield* duration(options.pollInterval ?? "1 minute", "pollInterval");
633
+ yield* Effect.forkScoped(
634
+ Effect.forever(
635
+ scheduler.runOnce.pipe(
636
+ Effect.catchCause(
637
+ (cause) => Cause.hasInterrupts(cause) ? Effect.failCause(cause) : Effect.logWarning("A trigger scheduler tick failed", cause)
638
+ ),
639
+ Effect.andThen(Effect.sleep(pollInterval))
640
+ )
641
+ )
642
+ );
643
+ return scheduler;
644
+ })
645
+ );
646
+ const layerNoop = Layer.succeed(Scheduler)(makeNoop());
647
+ //# sourceMappingURL=Scheduler.js.map