@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,25 @@
1
+ /**
2
+ * Pure catch-up policy decisions.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+ import * as Effect from "effect/Effect";
7
+ import type { Cron } from "./Cron.ts";
8
+ import type { CatchUp as Policy } from "./Schedule.ts";
9
+ import { TriggerError } from "./TriggerError.ts";
10
+ /**
11
+ * The occurrences a trigger owes since it last fired, bounded by
12
+ * `maxCatchUp` and by the policy named in `Schedule.CatchUp`: `none` owes
13
+ * nothing, `one` owes only the most recent, and `all` owes every missed tick.
14
+ *
15
+ * Every policy answers to `maxCatchUp`, `one` included: a bound of zero says
16
+ * no occurrence may be caught up, so a missed occurrence under `one` is
17
+ * `catch_up_bound_exceeded` exactly as three missed occurrences under `all`
18
+ * are. The bound is validated before any policy branch, so an unusable bound
19
+ * is refused even where the policy owes nothing.
20
+ *
21
+ * @category computation
22
+ * @since 0.1.0
23
+ */
24
+ export declare const occurrences: (policy: Policy, maxCatchUp: number, lastFiredAt: Date | undefined, now: Date, cron: Cron) => Effect.Effect<ReadonlyArray<Date>, TriggerError>;
25
+ //# sourceMappingURL=CatchUp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CatchUp.d.ts","sourceRoot":"","sources":["../../src/CatchUp.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAErC,OAAO,KAAK,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,eAAe,CAAA;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,WAAW,GACtB,QAAQ,MAAM,EACd,YAAY,MAAM,EAClB,aAAa,IAAI,GAAG,SAAS,EAC7B,KAAK,IAAI,EACT,MAAM,IAAI,KACT,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,YAAY,CAwCjD,CAAA"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Pure catch-up policy decisions.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+ import * as Effect from "effect/Effect";
7
+ import * as CronSchedule from "./Cron.js";
8
+ import { TriggerError } from "./TriggerError.js";
9
+ /**
10
+ * The occurrences a trigger owes since it last fired, bounded by
11
+ * `maxCatchUp` and by the policy named in `Schedule.CatchUp`: `none` owes
12
+ * nothing, `one` owes only the most recent, and `all` owes every missed tick.
13
+ *
14
+ * Every policy answers to `maxCatchUp`, `one` included: a bound of zero says
15
+ * no occurrence may be caught up, so a missed occurrence under `one` is
16
+ * `catch_up_bound_exceeded` exactly as three missed occurrences under `all`
17
+ * are. The bound is validated before any policy branch, so an unusable bound
18
+ * is refused even where the policy owes nothing.
19
+ *
20
+ * @category computation
21
+ * @since 0.1.0
22
+ */
23
+ export const occurrences = (policy, maxCatchUp, lastFiredAt, now, cron) => {
24
+ if (!Number.isSafeInteger(maxCatchUp) || maxCatchUp < 0) {
25
+ return Effect.fail(new TriggerError({
26
+ code: "catch_up_bound_exceeded",
27
+ message: `maxCatchUp must be a non-negative safe integer, received ${maxCatchUp}`,
28
+ path: "maxCatchUp"
29
+ }));
30
+ }
31
+ if (lastFiredAt === undefined)
32
+ return Effect.succeed([]);
33
+ if (policy === "none")
34
+ return Effect.succeed([]);
35
+ if (policy === "one") {
36
+ return CronSchedule.previousAtOrBefore(cron, now).pipe(Effect.flatMap((latest) => latest.getTime() <= lastFiredAt.getTime()
37
+ ? Effect.succeed([])
38
+ : maxCatchUp < 1
39
+ ? Effect.fail(new TriggerError({
40
+ code: "catch_up_bound_exceeded",
41
+ message: `missed 1 occurrence; maxCatchUp is ${maxCatchUp}`
42
+ }))
43
+ : Effect.succeed([latest])));
44
+ }
45
+ return CronSchedule.occurrencesBetween(cron, lastFiredAt, now, maxCatchUp + 1).pipe(Effect.flatMap((missed) => missed.length > maxCatchUp
46
+ ? Effect.fail(new TriggerError({
47
+ code: "catch_up_bound_exceeded",
48
+ message: `missed ${missed.length} occurrences; maxCatchUp is ${maxCatchUp}`
49
+ }))
50
+ : Effect.succeed(missed)));
51
+ };
52
+ //# sourceMappingURL=CatchUp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CatchUp.js","sourceRoot":"","sources":["../../src/CatchUp.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,YAAY,MAAM,WAAW,CAAA;AAEzC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,MAAc,EACd,UAAkB,EAClB,WAA6B,EAC7B,GAAS,EACT,IAAU,EACwC,EAAE;IACpD,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,UAAU,CAAC,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;QACxD,OAAO,MAAM,CAAC,IAAI,CAChB,IAAI,YAAY,CAAC;YACf,IAAI,EAAE,yBAAyB;YAC/B,OAAO,EAAE,4DAA4D,UAAU,EAAE;YACjF,IAAI,EAAE,YAAY;SACnB,CAAC,CACH,CAAA;IACH,CAAC;IACD,IAAI,WAAW,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IACxD,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAChD,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;QACrB,OAAO,YAAY,CAAC,kBAAkB,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,IAAI,CACpD,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CACxB,MAAM,CAAC,OAAO,EAAE,IAAI,WAAW,CAAC,OAAO,EAAE;YACvC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAsB,EAAE,CAAC;YACzC,CAAC,CAAC,UAAU,GAAG,CAAC;gBAChB,CAAC,CAAC,MAAM,CAAC,IAAI,CACX,IAAI,YAAY,CAAC;oBACf,IAAI,EAAE,yBAAyB;oBAC/B,OAAO,EAAE,sCAAsC,UAAU,EAAE;iBAC5D,CAAC,CACH;gBACD,CAAC,CAAC,MAAM,CAAC,OAAO,CAAsB,CAAC,MAAM,CAAC,CAAC,CAClD,CACF,CAAA;IACH,CAAC;IACD,OAAO,YAAY,CAAC,kBAAkB,CAAC,IAAI,EAAE,WAAW,EAAE,GAAG,EAAE,UAAU,GAAG,CAAC,CAAC,CAAC,IAAI,CACjF,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CACxB,MAAM,CAAC,MAAM,GAAG,UAAU;QACxB,CAAC,CAAC,MAAM,CAAC,IAAI,CACX,IAAI,YAAY,CAAC;YACf,IAAI,EAAE,yBAAyB;YAC/B,OAAO,EAAE,UAAU,MAAM,CAAC,MAAM,+BAA+B,UAAU,EAAE;SAC5E,CAAC,CACH;QACD,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAC3B,CACF,CAAA;AACH,CAAC,CAAA","sourcesContent":["/**\n * Pure catch-up policy decisions.\n *\n * @since 0.1.0\n */\n\nimport * as Effect from \"effect/Effect\"\nimport type { Cron } from \"./Cron.ts\"\nimport * as CronSchedule from \"./Cron.ts\"\nimport type { CatchUp as Policy } from \"./Schedule.ts\"\nimport { TriggerError } from \"./TriggerError.ts\"\n\n/**\n * The occurrences a trigger owes since it last fired, bounded by\n * `maxCatchUp` and by the policy named in `Schedule.CatchUp`: `none` owes\n * nothing, `one` owes only the most recent, and `all` owes every missed tick.\n *\n * Every policy answers to `maxCatchUp`, `one` included: a bound of zero says\n * no occurrence may be caught up, so a missed occurrence under `one` is\n * `catch_up_bound_exceeded` exactly as three missed occurrences under `all`\n * are. The bound is validated before any policy branch, so an unusable bound\n * is refused even where the policy owes nothing.\n *\n * @category computation\n * @since 0.1.0\n */\nexport const occurrences = (\n policy: Policy,\n maxCatchUp: number,\n lastFiredAt: Date | undefined,\n now: Date,\n cron: Cron\n): Effect.Effect<ReadonlyArray<Date>, TriggerError> => {\n if (!Number.isSafeInteger(maxCatchUp) || maxCatchUp < 0) {\n return Effect.fail(\n new TriggerError({\n code: \"catch_up_bound_exceeded\",\n message: `maxCatchUp must be a non-negative safe integer, received ${maxCatchUp}`,\n path: \"maxCatchUp\"\n })\n )\n }\n if (lastFiredAt === undefined) return Effect.succeed([])\n if (policy === \"none\") return Effect.succeed([])\n if (policy === \"one\") {\n return CronSchedule.previousAtOrBefore(cron, now).pipe(\n Effect.flatMap((latest) =>\n latest.getTime() <= lastFiredAt.getTime()\n ? Effect.succeed<ReadonlyArray<Date>>([])\n : maxCatchUp < 1\n ? Effect.fail(\n new TriggerError({\n code: \"catch_up_bound_exceeded\",\n message: `missed 1 occurrence; maxCatchUp is ${maxCatchUp}`\n })\n )\n : Effect.succeed<ReadonlyArray<Date>>([latest])\n )\n )\n }\n return CronSchedule.occurrencesBetween(cron, lastFiredAt, now, maxCatchUp + 1).pipe(\n Effect.flatMap((missed) =>\n missed.length > maxCatchUp\n ? Effect.fail(\n new TriggerError({\n code: \"catch_up_bound_exceeded\",\n message: `missed ${missed.length} occurrences; maxCatchUp is ${maxCatchUp}`\n })\n )\n : Effect.succeed(missed)\n )\n )\n}\n"]}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Authority-free channel declarations.
3
+ *
4
+ * A channel authenticates opaque transport input, maps verified payloads to
5
+ * control-plane starts or signals, and may project run state outbound. It
6
+ * carries no execution authority: the target flow's envelope and permission
7
+ * checks apply unchanged.
8
+ *
9
+ * @see packages/smithers/agent/triggers/docs/api.md
10
+ * @since 0.1.0
11
+ */
12
+ import type { IdempotencyKey } from "@smthrs/control/ControlSchema";
13
+ import type { CredentialRef } from "@smthrs/control/Credential";
14
+ import type * as Effect from "effect/Effect";
15
+ import type * as Redacted from "effect/Redacted";
16
+ import type * as Schema from "effect/Schema";
17
+ import type { TriggerError } from "./TriggerError.ts";
18
+ /**
19
+ * Opaque inbound transport data. Verification must inspect this value before
20
+ * any payload decoding occurs.
21
+ *
22
+ * @category models
23
+ * @since 0.1.0
24
+ */
25
+ export interface RawInbound {
26
+ readonly body: Uint8Array;
27
+ readonly headers: Readonly<Record<string, string | undefined>>;
28
+ readonly idempotencyKey: IdempotencyKey;
29
+ }
30
+ /**
31
+ * A verified request that starts a flow.
32
+ *
33
+ * @category models
34
+ * @since 0.1.0
35
+ */
36
+ export interface Start {
37
+ readonly start: {
38
+ readonly flowId: string;
39
+ readonly input: unknown;
40
+ };
41
+ }
42
+ /**
43
+ * A verified request that resolves a waiting flow step.
44
+ *
45
+ * @category models
46
+ * @since 0.1.0
47
+ */
48
+ export interface Signal {
49
+ readonly signal: {
50
+ readonly runId: string;
51
+ readonly stepId: string;
52
+ readonly value: unknown;
53
+ };
54
+ }
55
+ /**
56
+ * The only operations an inbound channel may request.
57
+ *
58
+ * @category models
59
+ * @since 0.1.0
60
+ */
61
+ export type Inbound = Start | Signal;
62
+ /**
63
+ * Authenticates raw request bytes and headers against a credential reference.
64
+ *
65
+ * The credential arrives as a redacted reference rather than a secret, and it
66
+ * arrives per request rather than at declaration time: a verifier resolves it
67
+ * through the host's resolver when it needs the bytes. Dropping this parameter
68
+ * left a verifier no way to reach a secret except by closing over it in plain
69
+ * memory at declaration time, which is the shape the reference exists to
70
+ * prevent. `packages/smithers/control/src/WebhookChannel.ts` states the same contract
71
+ * for the control-plane side of the boundary.
72
+ *
73
+ * @category models
74
+ * @since 0.1.0
75
+ */
76
+ export type Verify = (raw: RawInbound, credential: Redacted.Redacted<CredentialRef>) => Effect.Effect<void, TriggerError>;
77
+ /**
78
+ * An authority-free bidirectional channel declaration.
79
+ *
80
+ * `inbound` only describes a control-plane start or signal. It cannot supply
81
+ * capabilities, grants, or an alternate execution envelope.
82
+ *
83
+ * @category declarations
84
+ * @since 0.1.0
85
+ */
86
+ export interface Channel<Payload, Run = never, Outbound = never> {
87
+ readonly name: string;
88
+ readonly verify: Verify;
89
+ readonly inbound: (payload: Payload) => Inbound;
90
+ readonly outbound?: ((run: Run) => Outbound) | undefined;
91
+ }
92
+ /**
93
+ * Configuration for a schema-declared channel.
94
+ *
95
+ * @category models
96
+ * @since 0.1.0
97
+ */
98
+ export interface Config<Payload, Run = never, Outbound = never> extends Channel<Payload, Run, Outbound> {
99
+ readonly schema: Schema.Schema<Payload>;
100
+ }
101
+ /**
102
+ * Declares a channel without adding authority or execution behavior.
103
+ *
104
+ * @category constructors
105
+ * @since 0.1.0
106
+ */
107
+ export declare const make: <Payload, Run = never, Outbound = never>(config: Channel<Payload, Run, Outbound>) => Channel<Payload, Run, Outbound>;
108
+ //# sourceMappingURL=Channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Channel.d.ts","sourceRoot":"","sources":["../../src/Channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAA;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAA;AAC/D,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAC5C,OAAO,KAAK,KAAK,QAAQ,MAAM,iBAAiB,CAAA;AAChD,OAAO,KAAK,KAAK,MAAM,MAAM,eAAe,CAAA;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;IAC9D,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAA;CACxC;AAED;;;;;GAKG;AACH,MAAM,WAAW,KAAK;IACpB,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;QACvB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;KACxB,CAAA;CACF;AAED;;;;;GAKG;AACH,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,MAAM,EAAE;QACf,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;QACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;QACvB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;KACxB,CAAA;CACF;AAED;;;;;GAKG;AACH,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,MAAM,CAAA;AAEpC;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,MAAM,GAAG,CACnB,GAAG,EAAE,UAAU,EACf,UAAU,EAAE,QAAQ,CAAC,QAAQ,CAAC,aAAa,CAAC,KACzC,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,CAAA;AAEtC;;;;;;;;GAQG;AACH,MAAM,WAAW,OAAO,CAAC,OAAO,EAAE,GAAG,GAAG,KAAK,EAAE,QAAQ,GAAG,KAAK;IAC7D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAA;IAC/C,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,GAAG,EAAE,GAAG,KAAK,QAAQ,CAAC,GAAG,SAAS,CAAA;CACzD;AAED;;;;;GAKG;AACH,MAAM,WAAW,MAAM,CAAC,OAAO,EAAE,GAAG,GAAG,KAAK,EAAE,QAAQ,GAAG,KAAK,CAAE,SAAQ,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAC;IACrG,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;CACxC;AAED;;;;;GAKG;AACH,eAAO,MAAM,IAAI,GAAI,OAAO,EAAE,GAAG,GAAG,KAAK,EAAE,QAAQ,GAAG,KAAK,EACzD,QAAQ,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAC,KACtC,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,QAAQ,CAAW,CAAA"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Authority-free channel declarations.
3
+ *
4
+ * A channel authenticates opaque transport input, maps verified payloads to
5
+ * control-plane starts or signals, and may project run state outbound. It
6
+ * carries no execution authority: the target flow's envelope and permission
7
+ * checks apply unchanged.
8
+ *
9
+ * @see packages/smithers/agent/triggers/docs/api.md
10
+ * @since 0.1.0
11
+ */
12
+ /**
13
+ * Declares a channel without adding authority or execution behavior.
14
+ *
15
+ * @category constructors
16
+ * @since 0.1.0
17
+ */
18
+ export const make = (config) => config;
19
+ //# sourceMappingURL=Channel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Channel.js","sourceRoot":"","sources":["../../src/Channel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAsGH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAClB,MAAuC,EACN,EAAE,CAAC,MAAM,CAAA","sourcesContent":["/**\n * Authority-free channel declarations.\n *\n * A channel authenticates opaque transport input, maps verified payloads to\n * control-plane starts or signals, and may project run state outbound. It\n * carries no execution authority: the target flow's envelope and permission\n * checks apply unchanged.\n *\n * @see packages/smithers/agent/triggers/docs/api.md\n * @since 0.1.0\n */\n\nimport type { IdempotencyKey } from \"@smthrs/control/ControlSchema\"\nimport type { CredentialRef } from \"@smthrs/control/Credential\"\nimport type * as Effect from \"effect/Effect\"\nimport type * as Redacted from \"effect/Redacted\"\nimport type * as Schema from \"effect/Schema\"\nimport type { TriggerError } from \"./TriggerError.ts\"\n\n/**\n * Opaque inbound transport data. Verification must inspect this value before\n * any payload decoding occurs.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface RawInbound {\n readonly body: Uint8Array\n readonly headers: Readonly<Record<string, string | undefined>>\n readonly idempotencyKey: IdempotencyKey\n}\n\n/**\n * A verified request that starts a flow.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Start {\n readonly start: {\n readonly flowId: string\n readonly input: unknown\n }\n}\n\n/**\n * A verified request that resolves a waiting flow step.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Signal {\n readonly signal: {\n readonly runId: string\n readonly stepId: string\n readonly value: unknown\n }\n}\n\n/**\n * The only operations an inbound channel may request.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Inbound = Start | Signal\n\n/**\n * Authenticates raw request bytes and headers against a credential reference.\n *\n * The credential arrives as a redacted reference rather than a secret, and it\n * arrives per request rather than at declaration time: a verifier resolves it\n * through the host's resolver when it needs the bytes. Dropping this parameter\n * left a verifier no way to reach a secret except by closing over it in plain\n * memory at declaration time, which is the shape the reference exists to\n * prevent. `packages/smithers/control/src/WebhookChannel.ts` states the same contract\n * for the control-plane side of the boundary.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Verify = (\n raw: RawInbound,\n credential: Redacted.Redacted<CredentialRef>\n) => Effect.Effect<void, TriggerError>\n\n/**\n * An authority-free bidirectional channel declaration.\n *\n * `inbound` only describes a control-plane start or signal. It cannot supply\n * capabilities, grants, or an alternate execution envelope.\n *\n * @category declarations\n * @since 0.1.0\n */\nexport interface Channel<Payload, Run = never, Outbound = never> {\n readonly name: string\n readonly verify: Verify\n readonly inbound: (payload: Payload) => Inbound\n readonly outbound?: ((run: Run) => Outbound) | undefined\n}\n\n/**\n * Configuration for a schema-declared channel.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Config<Payload, Run = never, Outbound = never> extends Channel<Payload, Run, Outbound> {\n readonly schema: Schema.Schema<Payload>\n}\n\n/**\n * Declares a channel without adding authority or execution behavior.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <Payload, Run = never, Outbound = never>(\n config: Channel<Payload, Run, Outbound>\n): Channel<Payload, Run, Outbound> => config\n"]}
@@ -0,0 +1,183 @@
1
+ /**
2
+ * The one claim decision both trigger stores apply.
3
+ *
4
+ * A claim is a pure function of the rows it reads: the trigger declaration,
5
+ * the fire row at the occurrence being claimed, and the fire row the active
6
+ * reservation holds. Each store gathers that snapshot in its own storage,
7
+ * calls {@link decide}, and applies the writes it answers. Writing the fences,
8
+ * the expired-reservation reclaim, and the supersede predecessor substitution
9
+ * once is what keeps `SqlTriggerStore` and the published in-memory store from
10
+ * drifting apart.
11
+ *
12
+ * @since 1.0.0-rc.0
13
+ */
14
+ import type { Overlap as Policy } from "./Trigger.ts";
15
+ import { TriggerError } from "./TriggerError.ts";
16
+ import { type Claim, type ClaimFire, type Outcome } from "./TriggerStore.ts";
17
+ /**
18
+ * A fire row as the decision reads it. `outcome` is `null` for a claimed but
19
+ * unsettled occurrence, matching the nullable column.
20
+ *
21
+ * @category models
22
+ * @since 1.0.0-rc.0
23
+ */
24
+ export interface Fire {
25
+ readonly outcome: Outcome | null;
26
+ readonly runId?: string | undefined;
27
+ }
28
+ /**
29
+ * Everything the decision reads, gathered by the store before it runs.
30
+ *
31
+ * `existingFire` is absent when this claim is what created the row.
32
+ * `activeFire` is the row at `reservationOccurrence(activeRunId)`, read after
33
+ * that insert, and is needed only while `activeRunId` is a reservation.
34
+ *
35
+ * @category models
36
+ * @since 1.0.0-rc.0
37
+ */
38
+ export interface Snapshot {
39
+ readonly revision: number;
40
+ readonly enabled: boolean;
41
+ readonly overlap: Policy;
42
+ readonly activeRunId?: string | undefined;
43
+ readonly activeClaimedAt?: number | undefined;
44
+ readonly pending?: number | undefined;
45
+ readonly existingFire?: Fire | undefined;
46
+ readonly activeFire?: Fire | undefined;
47
+ }
48
+ /**
49
+ * One durable edit a claim owes, in the order the store must apply them.
50
+ *
51
+ * `SetOutcome` with `whileOpen` may only settle a fire that is still `null` or
52
+ * `buffered`; the SQL store spells that as a `WHERE` clause.
53
+ * `ReleaseReservation` may only land while `expected` still owns the trigger.
54
+ *
55
+ * @category models
56
+ * @since 1.0.0-rc.0
57
+ */
58
+ export type Write = {
59
+ readonly _tag: "SetOutcome";
60
+ readonly occurrence: number;
61
+ readonly outcome: Outcome;
62
+ readonly whileOpen: boolean;
63
+ } | {
64
+ readonly _tag: "SetRunId";
65
+ readonly occurrence: number;
66
+ readonly runId: string;
67
+ } | {
68
+ readonly _tag: "ReleaseReservation";
69
+ readonly expected: string;
70
+ readonly activeRunId?: string | undefined;
71
+ readonly pending?: number | undefined;
72
+ } | {
73
+ readonly _tag: "AdvanceCursor";
74
+ readonly occurrence: number;
75
+ } | {
76
+ readonly _tag: "SetPending";
77
+ readonly occurrence: number;
78
+ } | {
79
+ readonly _tag: "Reserve";
80
+ readonly reservationId: string;
81
+ readonly claimedAt: number;
82
+ };
83
+ /**
84
+ * What a claim decided: a refusal the store fails with, or a claim outcome and
85
+ * the writes that make it durable. An unclaimed occurrence answers
86
+ * `{ claimed: false }` with no writes.
87
+ *
88
+ * @category models
89
+ * @since 1.0.0-rc.0
90
+ */
91
+ export type Decision = {
92
+ readonly _tag: "Refused";
93
+ readonly error: TriggerError;
94
+ } | {
95
+ readonly _tag: "Decided";
96
+ readonly claim: Claim;
97
+ readonly writes: ReadonlyArray<Write>;
98
+ };
99
+ /**
100
+ * The claim request, the rows it reads, the reservation id the store minted
101
+ * for it, and the store clock reading it was claimed at.
102
+ *
103
+ * @category models
104
+ * @since 1.0.0-rc.0
105
+ */
106
+ export interface Input {
107
+ readonly fire: ClaimFire;
108
+ readonly snapshot: Snapshot;
109
+ readonly reservationId: string;
110
+ readonly claimedAt: number;
111
+ }
112
+ type AdmittedDecision = Extract<Decision, {
113
+ readonly _tag: "Decided";
114
+ }>;
115
+ /**
116
+ * What a reader finds behind a trigger's active owner: nothing, a live owner,
117
+ * or an expired reservation to release.
118
+ *
119
+ * `expected` is the reservation the release must still find in place.
120
+ * `recovered` is the launched predecessor the release hands back, and
121
+ * `pending` is the occurrence it re-arms; both are absent when the reservation
122
+ * covered no unsettled fire.
123
+ *
124
+ * @category models
125
+ * @since 1.0.0-rc.0
126
+ */
127
+ export type Lease = {
128
+ readonly _tag: "Idle";
129
+ } | {
130
+ readonly _tag: "Held";
131
+ readonly runId: string;
132
+ } | {
133
+ readonly _tag: "Expired";
134
+ readonly expected: string;
135
+ readonly recovered?: string | undefined;
136
+ readonly pending?: number | undefined;
137
+ };
138
+ /**
139
+ * Applies the reservation lease to a trigger's active owner.
140
+ *
141
+ * `unfinished` is the fire row at `reservationOccurrence(activeRunId)` while
142
+ * that row is still `null` or `buffered`, and is absent otherwise. A process
143
+ * that died after claiming an occurrence but before launching it left the
144
+ * reservation; the expired lease releases it and re-arms both ordinary and
145
+ * buffered work.
146
+ *
147
+ * @category decision
148
+ * @since 1.0.0-rc.0
149
+ */
150
+ export declare const lease: (input: {
151
+ readonly activeRunId?: string | undefined;
152
+ readonly activeClaimedAt?: number | undefined;
153
+ readonly pending?: number | undefined;
154
+ readonly now: number;
155
+ readonly unfinished?: Fire | undefined;
156
+ }) => Lease;
157
+ /**
158
+ * The refusals a claim owes before it reads any fire row, in the order the
159
+ * stores apply them. A store checks these first so a refused claim leaves the
160
+ * ledger untouched; {@link decide} re-applies them so a store that only calls
161
+ * it is still fenced.
162
+ *
163
+ * @category decision
164
+ * @since 1.0.0-rc.0
165
+ */
166
+ export declare const refuse: (snapshot: Pick<Snapshot, "enabled" | "revision">, fire: ClaimFire) => TriggerError | undefined;
167
+ /**
168
+ * Applies the claim protocol to a snapshot.
169
+ *
170
+ * @category decision
171
+ * @since 1.0.0-rc.0
172
+ */
173
+ export declare const decide: (input: Input) => Decision;
174
+ /**
175
+ * Decides over a snapshot already admitted by {@link refuse} in the same
176
+ * transaction. This avoids checking the same immutable revision twice.
177
+ * @category decision
178
+ * @since 1.0.0-rc.0
179
+ * @private
180
+ */
181
+ export declare const decideFenced: ({ claimedAt, fire, reservationId, snapshot }: Input) => AdmittedDecision;
182
+ export {};
183
+ //# sourceMappingURL=ClaimDecision.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClaimDecision.d.ts","sourceRoot":"","sources":["../../src/ClaimDecision.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,172 @@
1
+ /**
2
+ * The one claim decision both trigger stores apply.
3
+ *
4
+ * A claim is a pure function of the rows it reads: the trigger declaration,
5
+ * the fire row at the occurrence being claimed, and the fire row the active
6
+ * reservation holds. Each store gathers that snapshot in its own storage,
7
+ * calls {@link decide}, and applies the writes it answers. Writing the fences,
8
+ * the expired-reservation reclaim, and the supersede predecessor substitution
9
+ * once is what keeps `SqlTriggerStore` and the published in-memory store from
10
+ * drifting apart.
11
+ *
12
+ * @since 1.0.0-rc.0
13
+ */
14
+ import * as Overlap from "./Overlap.js";
15
+ import { TriggerError } from "./TriggerError.js";
16
+ import { isReservation, reservationLeaseMs, reservationOccurrence } from "./TriggerStore.js";
17
+ const decided = (claim, writes) => ({ _tag: "Decided", claim, writes });
18
+ /**
19
+ * Applies the reservation lease to a trigger's active owner.
20
+ *
21
+ * `unfinished` is the fire row at `reservationOccurrence(activeRunId)` while
22
+ * that row is still `null` or `buffered`, and is absent otherwise. A process
23
+ * that died after claiming an occurrence but before launching it left the
24
+ * reservation; the expired lease releases it and re-arms both ordinary and
25
+ * buffered work.
26
+ *
27
+ * @category decision
28
+ * @since 1.0.0-rc.0
29
+ */
30
+ export const lease = (input) => {
31
+ const { activeClaimedAt, activeRunId, now, pending, unfinished } = input;
32
+ if (activeRunId === undefined)
33
+ return { _tag: "Idle" };
34
+ if (!isReservation(activeRunId) || (activeClaimedAt !== undefined && activeClaimedAt > now - reservationLeaseMs)) {
35
+ return { _tag: "Held", runId: activeRunId };
36
+ }
37
+ const occurrence = reservationOccurrence(activeRunId);
38
+ if (occurrence === undefined || unfinished === undefined)
39
+ return { _tag: "Expired", expected: activeRunId };
40
+ const predecessor = unfinished.runId;
41
+ return {
42
+ _tag: "Expired",
43
+ expected: activeRunId,
44
+ pending: Overlap.pendingAfter({ running: false, pending, due: occurrence }),
45
+ ...(predecessor !== undefined && !isReservation(predecessor) ? { recovered: predecessor } : {})
46
+ };
47
+ };
48
+ /**
49
+ * The refusals a claim owes before it reads any fire row, in the order the
50
+ * stores apply them. A store checks these first so a refused claim leaves the
51
+ * ledger untouched; {@link decide} re-applies them so a store that only calls
52
+ * it is still fenced.
53
+ *
54
+ * @category decision
55
+ * @since 1.0.0-rc.0
56
+ */
57
+ export const refuse = (snapshot, fire) => {
58
+ if (snapshot.revision !== fire.expectedRevision) {
59
+ return new TriggerError({
60
+ code: "revision_mismatch",
61
+ message: `trigger ${fire.triggerId} is at revision ${snapshot.revision}, not the claimed ${fire.expectedRevision}`
62
+ });
63
+ }
64
+ if (!snapshot.enabled) {
65
+ return new TriggerError({ code: "trigger_disabled", message: `trigger ${fire.triggerId} is disabled` });
66
+ }
67
+ return undefined;
68
+ };
69
+ /**
70
+ * Applies the claim protocol to a snapshot.
71
+ *
72
+ * @category decision
73
+ * @since 1.0.0-rc.0
74
+ */
75
+ export const decide = (input) => {
76
+ const refusal = refuse(input.snapshot, input.fire);
77
+ return refusal === undefined ? decideFenced(input) : { _tag: "Refused", error: refusal };
78
+ };
79
+ /**
80
+ * Decides over a snapshot already admitted by {@link refuse} in the same
81
+ * transaction. This avoids checking the same immutable revision twice.
82
+ * @category decision
83
+ * @since 1.0.0-rc.0
84
+ * @private
85
+ */
86
+ export const decideFenced = ({ claimedAt, fire, reservationId, snapshot }) => {
87
+ let activeRunId = snapshot.activeRunId;
88
+ let pending = snapshot.pending;
89
+ // A reservation with no claim timestamp predates the lease column. Nothing
90
+ // writes that shape now, so treating it as expired is the only way such a
91
+ // row is ever reclaimed.
92
+ const expiredReservation = activeRunId !== undefined && isReservation(activeRunId) &&
93
+ (snapshot.activeClaimedAt === undefined || snapshot.activeClaimedAt <= claimedAt - reservationLeaseMs)
94
+ ? activeRunId
95
+ : undefined;
96
+ const existing = snapshot.existingFire;
97
+ if (existing !== undefined) {
98
+ const resumableBuffer = fire.resumeBuffered === true && existing.outcome === "buffered";
99
+ const resumableReservation = existing.outcome === null &&
100
+ (activeRunId === undefined ||
101
+ (reservationOccurrence(activeRunId) === fire.occurrence && expiredReservation !== undefined));
102
+ const resumableSupersede = fire.resumeBuffered === true && existing.outcome === null &&
103
+ snapshot.overlap === "supersede" && activeRunId !== undefined && existing.runId === activeRunId;
104
+ if (!resumableBuffer && !resumableReservation && !resumableSupersede)
105
+ return decided({ claimed: false }, []);
106
+ }
107
+ const writes = [];
108
+ if (expiredReservation !== undefined) {
109
+ const expiredOccurrence = reservationOccurrence(expiredReservation);
110
+ const expiredFire = expiredOccurrence === undefined ? undefined : snapshot.activeFire;
111
+ if (expiredOccurrence !== undefined && expiredFire !== undefined &&
112
+ (expiredFire.outcome === null || expiredFire.outcome === "buffered")) {
113
+ if (snapshot.overlap === "supersede") {
114
+ const predecessor = expiredFire.runId;
115
+ activeRunId = predecessor !== undefined && !isReservation(predecessor) ? predecessor : undefined;
116
+ if (expiredOccurrence !== fire.occurrence) {
117
+ writes.push({ _tag: "SetOutcome", occurrence: expiredOccurrence, outcome: "superseded", whileOpen: false });
118
+ }
119
+ }
120
+ else {
121
+ activeRunId = undefined;
122
+ if (expiredOccurrence !== fire.occurrence) {
123
+ pending = Overlap.pendingAfter({ running: false, pending, due: expiredOccurrence });
124
+ }
125
+ }
126
+ }
127
+ else {
128
+ activeRunId = undefined;
129
+ }
130
+ writes.push({ _tag: "ReleaseReservation", expected: expiredReservation, activeRunId, pending });
131
+ }
132
+ const state = { running: activeRunId !== undefined, pending, due: fire.occurrence };
133
+ const action = Overlap.decide(snapshot.overlap, state);
134
+ // A skip or buffer is complete inside the claim transaction. A fire or
135
+ // supersede is only reserved here; its cursor advances when `recordResult`
136
+ // makes the launched run durable.
137
+ if (action === "skip" || action === "buffer") {
138
+ writes.push({
139
+ _tag: "SetOutcome",
140
+ occurrence: fire.occurrence,
141
+ outcome: action === "skip" ? "skipped" : "buffered",
142
+ whileOpen: false
143
+ });
144
+ writes.push({ _tag: "AdvanceCursor", occurrence: fire.occurrence });
145
+ if (action === "buffer")
146
+ writes.push({ _tag: "SetPending", occurrence: Overlap.pendingAfter(state) });
147
+ return decided({ claimed: true, action }, writes);
148
+ }
149
+ let supersededRunId = activeRunId;
150
+ if (action === "supersede" && activeRunId !== undefined) {
151
+ if (isReservation(activeRunId)) {
152
+ const activeOccurrence = reservationOccurrence(activeRunId);
153
+ if (activeOccurrence !== undefined) {
154
+ const predecessor = snapshot.activeFire?.runId;
155
+ if (predecessor !== undefined && !isReservation(predecessor))
156
+ supersededRunId = predecessor;
157
+ writes.push({ _tag: "SetOutcome", occurrence: activeOccurrence, outcome: "superseded", whileOpen: true });
158
+ }
159
+ }
160
+ if (supersededRunId !== undefined && !isReservation(supersededRunId)) {
161
+ writes.push({ _tag: "SetRunId", occurrence: fire.occurrence, runId: supersededRunId });
162
+ }
163
+ }
164
+ writes.push({ _tag: "Reserve", reservationId, claimedAt });
165
+ return decided({
166
+ claimed: true,
167
+ action,
168
+ reservationId,
169
+ ...(supersededRunId === undefined ? {} : { activeRunId: supersededRunId })
170
+ }, writes);
171
+ };
172
+ //# sourceMappingURL=ClaimDecision.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ClaimDecision.js","sourceRoot":"","sources":["../../src/ClaimDecision.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The one claim decision both trigger stores apply.\n *\n * A claim is a pure function of the rows it reads: the trigger declaration,\n * the fire row at the occurrence being claimed, and the fire row the active\n * reservation holds. Each store gathers that snapshot in its own storage,\n * calls {@link decide}, and applies the writes it answers. Writing the fences,\n * the expired-reservation reclaim, and the supersede predecessor substitution\n * once is what keeps `SqlTriggerStore` and the published in-memory store from\n * drifting apart.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as Overlap from \"./Overlap.ts\"\nimport type { Overlap as Policy } from \"./Trigger.ts\"\nimport { TriggerError } from \"./TriggerError.ts\"\nimport {\n type Claim,\n type ClaimFire,\n isReservation,\n type Outcome,\n reservationLeaseMs,\n reservationOccurrence\n} from \"./TriggerStore.ts\"\n\n/**\n * A fire row as the decision reads it. `outcome` is `null` for a claimed but\n * unsettled occurrence, matching the nullable column.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport interface Fire {\n readonly outcome: Outcome | null\n readonly runId?: string | undefined\n}\n\n/**\n * Everything the decision reads, gathered by the store before it runs.\n *\n * `existingFire` is absent when this claim is what created the row.\n * `activeFire` is the row at `reservationOccurrence(activeRunId)`, read after\n * that insert, and is needed only while `activeRunId` is a reservation.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport interface Snapshot {\n readonly revision: number\n readonly enabled: boolean\n readonly overlap: Policy\n readonly activeRunId?: string | undefined\n readonly activeClaimedAt?: number | undefined\n readonly pending?: number | undefined\n readonly existingFire?: Fire | undefined\n readonly activeFire?: Fire | undefined\n}\n\n/**\n * One durable edit a claim owes, in the order the store must apply them.\n *\n * `SetOutcome` with `whileOpen` may only settle a fire that is still `null` or\n * `buffered`; the SQL store spells that as a `WHERE` clause.\n * `ReleaseReservation` may only land while `expected` still owns the trigger.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport type Write =\n | {\n readonly _tag: \"SetOutcome\"\n readonly occurrence: number\n readonly outcome: Outcome\n readonly whileOpen: boolean\n }\n | { readonly _tag: \"SetRunId\"; readonly occurrence: number; readonly runId: string }\n | {\n readonly _tag: \"ReleaseReservation\"\n readonly expected: string\n readonly activeRunId?: string | undefined\n readonly pending?: number | undefined\n }\n | { readonly _tag: \"AdvanceCursor\"; readonly occurrence: number }\n | { readonly _tag: \"SetPending\"; readonly occurrence: number }\n | { readonly _tag: \"Reserve\"; readonly reservationId: string; readonly claimedAt: number }\n\n/**\n * What a claim decided: a refusal the store fails with, or a claim outcome and\n * the writes that make it durable. An unclaimed occurrence answers\n * `{ claimed: false }` with no writes.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport type Decision =\n | { readonly _tag: \"Refused\"; readonly error: TriggerError }\n | { readonly _tag: \"Decided\"; readonly claim: Claim; readonly writes: ReadonlyArray<Write> }\n\n/**\n * The claim request, the rows it reads, the reservation id the store minted\n * for it, and the store clock reading it was claimed at.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport interface Input {\n readonly fire: ClaimFire\n readonly snapshot: Snapshot\n readonly reservationId: string\n readonly claimedAt: number\n}\n\ntype AdmittedDecision = Extract<Decision, { readonly _tag: \"Decided\" }>\n\nconst decided = (claim: Claim, writes: ReadonlyArray<Write>): AdmittedDecision => ({ _tag: \"Decided\", claim, writes })\n\n/**\n * What a reader finds behind a trigger's active owner: nothing, a live owner,\n * or an expired reservation to release.\n *\n * `expected` is the reservation the release must still find in place.\n * `recovered` is the launched predecessor the release hands back, and\n * `pending` is the occurrence it re-arms; both are absent when the reservation\n * covered no unsettled fire.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport type Lease =\n | { readonly _tag: \"Idle\" }\n | { readonly _tag: \"Held\"; readonly runId: string }\n | {\n readonly _tag: \"Expired\"\n readonly expected: string\n readonly recovered?: string | undefined\n readonly pending?: number | undefined\n }\n\n/**\n * Applies the reservation lease to a trigger's active owner.\n *\n * `unfinished` is the fire row at `reservationOccurrence(activeRunId)` while\n * that row is still `null` or `buffered`, and is absent otherwise. A process\n * that died after claiming an occurrence but before launching it left the\n * reservation; the expired lease releases it and re-arms both ordinary and\n * buffered work.\n *\n * @category decision\n * @since 1.0.0-rc.0\n */\nexport const lease = (input: {\n readonly activeRunId?: string | undefined\n readonly activeClaimedAt?: number | undefined\n readonly pending?: number | undefined\n readonly now: number\n readonly unfinished?: Fire | undefined\n}): Lease => {\n const { activeClaimedAt, activeRunId, now, pending, unfinished } = input\n if (activeRunId === undefined) return { _tag: \"Idle\" }\n if (!isReservation(activeRunId) || (activeClaimedAt !== undefined && activeClaimedAt > now - reservationLeaseMs)) {\n return { _tag: \"Held\", runId: activeRunId }\n }\n const occurrence = reservationOccurrence(activeRunId)\n if (occurrence === undefined || unfinished === undefined) return { _tag: \"Expired\", expected: activeRunId }\n const predecessor = unfinished.runId\n return {\n _tag: \"Expired\",\n expected: activeRunId,\n pending: Overlap.pendingAfter({ running: false, pending, due: occurrence }),\n ...(predecessor !== undefined && !isReservation(predecessor) ? { recovered: predecessor } : {})\n }\n}\n\n/**\n * The refusals a claim owes before it reads any fire row, in the order the\n * stores apply them. A store checks these first so a refused claim leaves the\n * ledger untouched; {@link decide} re-applies them so a store that only calls\n * it is still fenced.\n *\n * @category decision\n * @since 1.0.0-rc.0\n */\nexport const refuse = (\n snapshot: Pick<Snapshot, \"enabled\" | \"revision\">,\n fire: ClaimFire\n): TriggerError | undefined => {\n if (snapshot.revision !== fire.expectedRevision) {\n return new TriggerError({\n code: \"revision_mismatch\",\n message: `trigger ${fire.triggerId} is at revision ${snapshot.revision}, not the claimed ${fire.expectedRevision}`\n })\n }\n if (!snapshot.enabled) {\n return new TriggerError({ code: \"trigger_disabled\", message: `trigger ${fire.triggerId} is disabled` })\n }\n return undefined\n}\n\n/**\n * Applies the claim protocol to a snapshot.\n *\n * @category decision\n * @since 1.0.0-rc.0\n */\nexport const decide = (input: Input): Decision => {\n const refusal = refuse(input.snapshot, input.fire)\n return refusal === undefined ? decideFenced(input) : { _tag: \"Refused\", error: refusal }\n}\n\n/**\n * Decides over a snapshot already admitted by {@link refuse} in the same\n * transaction. This avoids checking the same immutable revision twice.\n * @category decision\n * @since 1.0.0-rc.0\n * @private\n */\nexport const decideFenced = ({ claimedAt, fire, reservationId, snapshot }: Input): AdmittedDecision => {\n let activeRunId = snapshot.activeRunId\n let pending = snapshot.pending\n // A reservation with no claim timestamp predates the lease column. Nothing\n // writes that shape now, so treating it as expired is the only way such a\n // row is ever reclaimed.\n const expiredReservation = activeRunId !== undefined && isReservation(activeRunId) &&\n (snapshot.activeClaimedAt === undefined || snapshot.activeClaimedAt <= claimedAt - reservationLeaseMs)\n ? activeRunId\n : undefined\n const existing = snapshot.existingFire\n if (existing !== undefined) {\n const resumableBuffer = fire.resumeBuffered === true && existing.outcome === \"buffered\"\n const resumableReservation = existing.outcome === null &&\n (activeRunId === undefined ||\n (reservationOccurrence(activeRunId) === fire.occurrence && expiredReservation !== undefined))\n const resumableSupersede = fire.resumeBuffered === true && existing.outcome === null &&\n snapshot.overlap === \"supersede\" && activeRunId !== undefined && existing.runId === activeRunId\n if (!resumableBuffer && !resumableReservation && !resumableSupersede) return decided({ claimed: false }, [])\n }\n const writes: Array<Write> = []\n if (expiredReservation !== undefined) {\n const expiredOccurrence = reservationOccurrence(expiredReservation)\n const expiredFire = expiredOccurrence === undefined ? undefined : snapshot.activeFire\n if (\n expiredOccurrence !== undefined && expiredFire !== undefined &&\n (expiredFire.outcome === null || expiredFire.outcome === \"buffered\")\n ) {\n if (snapshot.overlap === \"supersede\") {\n const predecessor = expiredFire.runId\n activeRunId = predecessor !== undefined && !isReservation(predecessor) ? predecessor : undefined\n if (expiredOccurrence !== fire.occurrence) {\n writes.push({ _tag: \"SetOutcome\", occurrence: expiredOccurrence, outcome: \"superseded\", whileOpen: false })\n }\n } else {\n activeRunId = undefined\n if (expiredOccurrence !== fire.occurrence) {\n pending = Overlap.pendingAfter({ running: false, pending, due: expiredOccurrence })\n }\n }\n } else {\n activeRunId = undefined\n }\n writes.push({ _tag: \"ReleaseReservation\", expected: expiredReservation, activeRunId, pending })\n }\n const state: Overlap.State = { running: activeRunId !== undefined, pending, due: fire.occurrence }\n const action = Overlap.decide(snapshot.overlap, state)\n // A skip or buffer is complete inside the claim transaction. A fire or\n // supersede is only reserved here; its cursor advances when `recordResult`\n // makes the launched run durable.\n if (action === \"skip\" || action === \"buffer\") {\n writes.push({\n _tag: \"SetOutcome\",\n occurrence: fire.occurrence,\n outcome: action === \"skip\" ? \"skipped\" : \"buffered\",\n whileOpen: false\n })\n writes.push({ _tag: \"AdvanceCursor\", occurrence: fire.occurrence })\n if (action === \"buffer\") writes.push({ _tag: \"SetPending\", occurrence: Overlap.pendingAfter(state) })\n return decided({ claimed: true, action }, writes)\n }\n let supersededRunId = activeRunId\n if (action === \"supersede\" && activeRunId !== undefined) {\n if (isReservation(activeRunId)) {\n const activeOccurrence = reservationOccurrence(activeRunId)\n if (activeOccurrence !== undefined) {\n const predecessor = snapshot.activeFire?.runId\n if (predecessor !== undefined && !isReservation(predecessor)) supersededRunId = predecessor\n writes.push({ _tag: \"SetOutcome\", occurrence: activeOccurrence, outcome: \"superseded\", whileOpen: true })\n }\n }\n if (supersededRunId !== undefined && !isReservation(supersededRunId)) {\n writes.push({ _tag: \"SetRunId\", occurrence: fire.occurrence, runId: supersededRunId })\n }\n }\n writes.push({ _tag: \"Reserve\", reservationId, claimedAt })\n return decided(\n {\n claimed: true,\n action,\n reservationId,\n ...(supersededRunId === undefined ? {} : { activeRunId: supersededRunId })\n },\n writes\n )\n}\n"]}