@shipfox/api-triggers 2.0.0

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 (331) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +224 -0
  5. package/LICENSE +21 -0
  6. package/README.md +300 -0
  7. package/dist/config.d.ts +10 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +42 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/core/compute-next-fire-at.d.ts +9 -0
  12. package/dist/core/compute-next-fire-at.d.ts.map +1 -0
  13. package/dist/core/compute-next-fire-at.js +28 -0
  14. package/dist/core/compute-next-fire-at.js.map +1 -0
  15. package/dist/core/config.d.ts +26 -0
  16. package/dist/core/config.d.ts.map +1 -0
  17. package/dist/core/config.js +68 -0
  18. package/dist/core/config.js.map +1 -0
  19. package/dist/core/dispatch-integration-event.d.ts +14 -0
  20. package/dist/core/dispatch-integration-event.d.ts.map +1 -0
  21. package/dist/core/dispatch-integration-event.js +136 -0
  22. package/dist/core/dispatch-integration-event.js.map +1 -0
  23. package/dist/core/drain-cron-schedules.d.ts +30 -0
  24. package/dist/core/drain-cron-schedules.d.ts.map +1 -0
  25. package/dist/core/drain-cron-schedules.js +76 -0
  26. package/dist/core/drain-cron-schedules.js.map +1 -0
  27. package/dist/core/entities/cron-schedule.d.ts +11 -0
  28. package/dist/core/entities/cron-schedule.d.ts.map +1 -0
  29. package/dist/core/entities/cron-schedule.js +3 -0
  30. package/dist/core/entities/cron-schedule.js.map +1 -0
  31. package/dist/core/entities/decision.d.ts +23 -0
  32. package/dist/core/entities/decision.d.ts.map +1 -0
  33. package/dist/core/entities/decision.js +7 -0
  34. package/dist/core/entities/decision.js.map +1 -0
  35. package/dist/core/entities/index.d.ts +3 -0
  36. package/dist/core/entities/index.d.ts.map +1 -0
  37. package/dist/core/entities/index.js +3 -0
  38. package/dist/core/entities/index.js.map +1 -0
  39. package/dist/core/entities/job-listener-subscription.d.ts +14 -0
  40. package/dist/core/entities/job-listener-subscription.d.ts.map +1 -0
  41. package/dist/core/entities/job-listener-subscription.js +3 -0
  42. package/dist/core/entities/job-listener-subscription.js.map +1 -0
  43. package/dist/core/entities/received-event.d.ts +28 -0
  44. package/dist/core/entities/received-event.d.ts.map +1 -0
  45. package/dist/core/entities/received-event.js +14 -0
  46. package/dist/core/entities/received-event.js.map +1 -0
  47. package/dist/core/entities/subscription.d.ts +13 -0
  48. package/dist/core/entities/subscription.d.ts.map +1 -0
  49. package/dist/core/entities/subscription.js +3 -0
  50. package/dist/core/entities/subscription.js.map +1 -0
  51. package/dist/core/errors.d.ts +25 -0
  52. package/dist/core/errors.d.ts.map +1 -0
  53. package/dist/core/errors.js +41 -0
  54. package/dist/core/errors.js.map +1 -0
  55. package/dist/core/fire-cron.d.ts +28 -0
  56. package/dist/core/fire-cron.d.ts.map +1 -0
  57. package/dist/core/fire-cron.js +85 -0
  58. package/dist/core/fire-cron.js.map +1 -0
  59. package/dist/core/fire-manual.d.ts +9 -0
  60. package/dist/core/fire-manual.d.ts.map +1 -0
  61. package/dist/core/fire-manual.js +87 -0
  62. package/dist/core/fire-manual.js.map +1 -0
  63. package/dist/core/index.d.ts +11 -0
  64. package/dist/core/index.d.ts.map +1 -0
  65. package/dist/core/index.js +10 -0
  66. package/dist/core/index.js.map +1 -0
  67. package/dist/core/record-trigger-history.d.ts +35 -0
  68. package/dist/core/record-trigger-history.d.ts.map +1 -0
  69. package/dist/core/record-trigger-history.js +75 -0
  70. package/dist/core/record-trigger-history.js.map +1 -0
  71. package/dist/core/route-event-to-job-listeners.d.ts +31 -0
  72. package/dist/core/route-event-to-job-listeners.d.ts.map +1 -0
  73. package/dist/core/route-event-to-job-listeners.js +140 -0
  74. package/dist/core/route-event-to-job-listeners.js.map +1 -0
  75. package/dist/db/cron-schedules.d.ts +36 -0
  76. package/dist/db/cron-schedules.d.ts.map +1 -0
  77. package/dist/db/cron-schedules.js +104 -0
  78. package/dist/db/cron-schedules.js.map +1 -0
  79. package/dist/db/db.d.ts +2515 -0
  80. package/dist/db/db.d.ts.map +1 -0
  81. package/dist/db/db.js +28 -0
  82. package/dist/db/db.js.map +1 -0
  83. package/dist/db/event-history.d.ts +50 -0
  84. package/dist/db/event-history.d.ts.map +1 -0
  85. package/dist/db/event-history.js +207 -0
  86. package/dist/db/event-history.js.map +1 -0
  87. package/dist/db/event-queries.d.ts +38 -0
  88. package/dist/db/event-queries.d.ts.map +1 -0
  89. package/dist/db/event-queries.js +78 -0
  90. package/dist/db/event-queries.js.map +1 -0
  91. package/dist/db/index.d.ts +11 -0
  92. package/dist/db/index.d.ts.map +1 -0
  93. package/dist/db/index.js +14 -0
  94. package/dist/db/index.js.map +1 -0
  95. package/dist/db/job-listener-subscriptions.d.ts +24 -0
  96. package/dist/db/job-listener-subscriptions.d.ts.map +1 -0
  97. package/dist/db/job-listener-subscriptions.js +69 -0
  98. package/dist/db/job-listener-subscriptions.js.map +1 -0
  99. package/dist/db/received-events.d.ts +7 -0
  100. package/dist/db/received-events.d.ts.map +1 -0
  101. package/dist/db/received-events.js +11 -0
  102. package/dist/db/received-events.js.map +1 -0
  103. package/dist/db/schema/common.d.ts +2 -0
  104. package/dist/db/schema/common.d.ts.map +1 -0
  105. package/dist/db/schema/common.js +4 -0
  106. package/dist/db/schema/common.js.map +1 -0
  107. package/dist/db/schema/cron-schedules.d.ts +148 -0
  108. package/dist/db/schema/cron-schedules.d.ts.map +1 -0
  109. package/dist/db/schema/cron-schedules.js +39 -0
  110. package/dist/db/schema/cron-schedules.js.map +1 -0
  111. package/dist/db/schema/decisions.d.ts +284 -0
  112. package/dist/db/schema/decisions.d.ts.map +1 -0
  113. package/dist/db/schema/decisions.js +70 -0
  114. package/dist/db/schema/decisions.js.map +1 -0
  115. package/dist/db/schema/job-listener-subscriptions.d.ts +185 -0
  116. package/dist/db/schema/job-listener-subscriptions.d.ts.map +1 -0
  117. package/dist/db/schema/job-listener-subscriptions.js +41 -0
  118. package/dist/db/schema/job-listener-subscriptions.js.map +1 -0
  119. package/dist/db/schema/outbox.d.ts +195 -0
  120. package/dist/db/schema/outbox.d.ts.map +1 -0
  121. package/dist/db/schema/outbox.js +5 -0
  122. package/dist/db/schema/outbox.js.map +1 -0
  123. package/dist/db/schema/received-events.d.ts +544 -0
  124. package/dist/db/schema/received-events.d.ts.map +1 -0
  125. package/dist/db/schema/received-events.js +86 -0
  126. package/dist/db/schema/received-events.js.map +1 -0
  127. package/dist/db/schema/subscriptions.d.ts +182 -0
  128. package/dist/db/schema/subscriptions.d.ts.map +1 -0
  129. package/dist/db/schema/subscriptions.js +39 -0
  130. package/dist/db/schema/subscriptions.js.map +1 -0
  131. package/dist/db/subscriptions.d.ts +26 -0
  132. package/dist/db/subscriptions.d.ts.map +1 -0
  133. package/dist/db/subscriptions.js +103 -0
  134. package/dist/db/subscriptions.js.map +1 -0
  135. package/dist/index.d.ts +7 -0
  136. package/dist/index.d.ts.map +1 -0
  137. package/dist/index.js +68 -0
  138. package/dist/index.js.map +1 -0
  139. package/dist/metrics/index.d.ts +3 -0
  140. package/dist/metrics/index.d.ts.map +1 -0
  141. package/dist/metrics/index.js +4 -0
  142. package/dist/metrics/index.js.map +1 -0
  143. package/dist/metrics/instance.d.ts +15 -0
  144. package/dist/metrics/instance.d.ts.map +1 -0
  145. package/dist/metrics/instance.js +32 -0
  146. package/dist/metrics/instance.js.map +1 -0
  147. package/dist/metrics/service.d.ts +2 -0
  148. package/dist/metrics/service.d.ts.map +1 -0
  149. package/dist/metrics/service.js +15 -0
  150. package/dist/metrics/service.js.map +1 -0
  151. package/dist/presentation/dto/trigger-events.d.ts +7 -0
  152. package/dist/presentation/dto/trigger-events.d.ts.map +1 -0
  153. package/dist/presentation/dto/trigger-events.js +47 -0
  154. package/dist/presentation/dto/trigger-events.js.map +1 -0
  155. package/dist/presentation/index.d.ts +3 -0
  156. package/dist/presentation/index.d.ts.map +1 -0
  157. package/dist/presentation/index.js +4 -0
  158. package/dist/presentation/index.js.map +1 -0
  159. package/dist/presentation/routes/fire-manual.d.ts +2 -0
  160. package/dist/presentation/routes/fire-manual.d.ts.map +1 -0
  161. package/dist/presentation/routes/fire-manual.js +71 -0
  162. package/dist/presentation/routes/fire-manual.js.map +1 -0
  163. package/dist/presentation/routes/get-trigger-event.d.ts +2 -0
  164. package/dist/presentation/routes/get-trigger-event.d.ts.map +1 -0
  165. package/dist/presentation/routes/get-trigger-event.js +37 -0
  166. package/dist/presentation/routes/get-trigger-event.js.map +1 -0
  167. package/dist/presentation/routes/index.d.ts +3 -0
  168. package/dist/presentation/routes/index.d.ts.map +1 -0
  169. package/dist/presentation/routes/index.js +27 -0
  170. package/dist/presentation/routes/index.js.map +1 -0
  171. package/dist/presentation/routes/list-trigger-event-facets.d.ts +2 -0
  172. package/dist/presentation/routes/list-trigger-event-facets.d.ts.map +1 -0
  173. package/dist/presentation/routes/list-trigger-event-facets.js +29 -0
  174. package/dist/presentation/routes/list-trigger-event-facets.js.map +1 -0
  175. package/dist/presentation/routes/list-trigger-events.d.ts +2 -0
  176. package/dist/presentation/routes/list-trigger-events.d.ts.map +1 -0
  177. package/dist/presentation/routes/list-trigger-events.js +56 -0
  178. package/dist/presentation/routes/list-trigger-events.js.map +1 -0
  179. package/dist/presentation/subscribers/index.d.ts +6 -0
  180. package/dist/presentation/subscribers/index.d.ts.map +1 -0
  181. package/dist/presentation/subscribers/index.js +7 -0
  182. package/dist/presentation/subscribers/index.js.map +1 -0
  183. package/dist/presentation/subscribers/on-definition-deleted.d.ts +3 -0
  184. package/dist/presentation/subscribers/on-definition-deleted.d.ts.map +1 -0
  185. package/dist/presentation/subscribers/on-definition-deleted.js +8 -0
  186. package/dist/presentation/subscribers/on-definition-deleted.js.map +1 -0
  187. package/dist/presentation/subscribers/on-definition-resolved.d.ts +3 -0
  188. package/dist/presentation/subscribers/on-definition-resolved.d.ts.map +1 -0
  189. package/dist/presentation/subscribers/on-definition-resolved.js +11 -0
  190. package/dist/presentation/subscribers/on-definition-resolved.js.map +1 -0
  191. package/dist/presentation/subscribers/on-integration-event-received.d.ts +4 -0
  192. package/dist/presentation/subscribers/on-integration-event-received.d.ts.map +1 -0
  193. package/dist/presentation/subscribers/on-integration-event-received.js +17 -0
  194. package/dist/presentation/subscribers/on-integration-event-received.js.map +1 -0
  195. package/dist/presentation/subscribers/on-job-activated.d.ts +3 -0
  196. package/dist/presentation/subscribers/on-job-activated.d.ts.map +1 -0
  197. package/dist/presentation/subscribers/on-job-activated.js +13 -0
  198. package/dist/presentation/subscribers/on-job-activated.js.map +1 -0
  199. package/dist/presentation/subscribers/on-job-terminated.d.ts +3 -0
  200. package/dist/presentation/subscribers/on-job-terminated.d.ts.map +1 -0
  201. package/dist/presentation/subscribers/on-job-terminated.js +6 -0
  202. package/dist/presentation/subscribers/on-job-terminated.js.map +1 -0
  203. package/dist/temporal/activities/drain-cron-batch.d.ts +4 -0
  204. package/dist/temporal/activities/drain-cron-batch.d.ts.map +1 -0
  205. package/dist/temporal/activities/drain-cron-batch.js +20 -0
  206. package/dist/temporal/activities/drain-cron-batch.js.map +1 -0
  207. package/dist/temporal/activities/index.d.ts +10 -0
  208. package/dist/temporal/activities/index.d.ts.map +1 -0
  209. package/dist/temporal/activities/index.js +15 -0
  210. package/dist/temporal/activities/index.js.map +1 -0
  211. package/dist/temporal/activities/prune-trigger-events.d.ts +4 -0
  212. package/dist/temporal/activities/prune-trigger-events.d.ts.map +1 -0
  213. package/dist/temporal/activities/prune-trigger-events.js +11 -0
  214. package/dist/temporal/activities/prune-trigger-events.js.map +1 -0
  215. package/dist/temporal/constants.d.ts +7 -0
  216. package/dist/temporal/constants.d.ts.map +1 -0
  217. package/dist/temporal/constants.js +7 -0
  218. package/dist/temporal/constants.js.map +1 -0
  219. package/dist/temporal/workflows/cron-tick-cron.d.ts +9 -0
  220. package/dist/temporal/workflows/cron-tick-cron.d.ts.map +1 -0
  221. package/dist/temporal/workflows/cron-tick-cron.js +54 -0
  222. package/dist/temporal/workflows/cron-tick-cron.js.map +1 -0
  223. package/dist/temporal/workflows/index.d.ts +3 -0
  224. package/dist/temporal/workflows/index.d.ts.map +1 -0
  225. package/dist/temporal/workflows/index.js +4 -0
  226. package/dist/temporal/workflows/index.js.map +1 -0
  227. package/dist/temporal/workflows/prune-trigger-events-cron.d.ts +2 -0
  228. package/dist/temporal/workflows/prune-trigger-events-cron.d.ts.map +1 -0
  229. package/dist/temporal/workflows/prune-trigger-events-cron.js +18 -0
  230. package/dist/temporal/workflows/prune-trigger-events-cron.js.map +1 -0
  231. package/dist/tsconfig.test.tsbuildinfo +1 -0
  232. package/drizzle/0000_initial.sql +109 -0
  233. package/drizzle/meta/0000_snapshot.json +905 -0
  234. package/drizzle/meta/_journal.json +13 -0
  235. package/drizzle.config.ts +14 -0
  236. package/package.json +83 -0
  237. package/src/config.test.ts +33 -0
  238. package/src/config.ts +53 -0
  239. package/src/core/compute-next-fire-at.test.ts +69 -0
  240. package/src/core/compute-next-fire-at.ts +37 -0
  241. package/src/core/config.test.ts +213 -0
  242. package/src/core/config.ts +86 -0
  243. package/src/core/dispatch-integration-event.history-failure.test.ts +66 -0
  244. package/src/core/dispatch-integration-event.test.ts +1006 -0
  245. package/src/core/dispatch-integration-event.ts +150 -0
  246. package/src/core/drain-cron-schedules.test.ts +265 -0
  247. package/src/core/drain-cron-schedules.ts +90 -0
  248. package/src/core/entities/cron-schedule.ts +10 -0
  249. package/src/core/entities/decision.ts +24 -0
  250. package/src/core/entities/index.ts +5 -0
  251. package/src/core/entities/job-listener-subscription.ts +14 -0
  252. package/src/core/entities/received-event.ts +36 -0
  253. package/src/core/entities/subscription.ts +12 -0
  254. package/src/core/errors.ts +63 -0
  255. package/src/core/fire-cron.test.ts +146 -0
  256. package/src/core/fire-cron.ts +98 -0
  257. package/src/core/fire-manual.test.ts +157 -0
  258. package/src/core/fire-manual.ts +92 -0
  259. package/src/core/index.ts +34 -0
  260. package/src/core/record-trigger-history.test.ts +159 -0
  261. package/src/core/record-trigger-history.ts +145 -0
  262. package/src/core/route-event-to-job-listeners.test.ts +369 -0
  263. package/src/core/route-event-to-job-listeners.ts +184 -0
  264. package/src/db/cron-schedules.test.ts +358 -0
  265. package/src/db/cron-schedules.ts +163 -0
  266. package/src/db/db.ts +31 -0
  267. package/src/db/event-history.test.ts +421 -0
  268. package/src/db/event-history.ts +279 -0
  269. package/src/db/event-queries.ts +140 -0
  270. package/src/db/index.ts +51 -0
  271. package/src/db/job-listener-subscriptions.test.ts +270 -0
  272. package/src/db/job-listener-subscriptions.ts +133 -0
  273. package/src/db/received-events.ts +16 -0
  274. package/src/db/schema/common.ts +3 -0
  275. package/src/db/schema/cron-schedules.ts +37 -0
  276. package/src/db/schema/decisions.test.ts +279 -0
  277. package/src/db/schema/decisions.ts +77 -0
  278. package/src/db/schema/job-listener-subscriptions.ts +65 -0
  279. package/src/db/schema/outbox.ts +4 -0
  280. package/src/db/schema/received-events.test.ts +135 -0
  281. package/src/db/schema/received-events.ts +93 -0
  282. package/src/db/schema/subscriptions.ts +46 -0
  283. package/src/db/subscriptions.test.ts +340 -0
  284. package/src/db/subscriptions.ts +179 -0
  285. package/src/index.ts +108 -0
  286. package/src/metrics/index.ts +8 -0
  287. package/src/metrics/instance.ts +33 -0
  288. package/src/metrics/service.ts +18 -0
  289. package/src/presentation/dto/trigger-events.test.ts +144 -0
  290. package/src/presentation/dto/trigger-events.ts +60 -0
  291. package/src/presentation/index.ts +6 -0
  292. package/src/presentation/routes/fire-manual.test.ts +118 -0
  293. package/src/presentation/routes/fire-manual.ts +70 -0
  294. package/src/presentation/routes/get-trigger-event.test.ts +184 -0
  295. package/src/presentation/routes/get-trigger-event.ts +37 -0
  296. package/src/presentation/routes/index.ts +21 -0
  297. package/src/presentation/routes/list-trigger-event-facets.test.ts +116 -0
  298. package/src/presentation/routes/list-trigger-event-facets.ts +29 -0
  299. package/src/presentation/routes/list-trigger-events.test.ts +379 -0
  300. package/src/presentation/routes/list-trigger-events.ts +68 -0
  301. package/src/presentation/subscribers/index.ts +5 -0
  302. package/src/presentation/subscribers/on-definition-deleted.test.ts +42 -0
  303. package/src/presentation/subscribers/on-definition-deleted.ts +6 -0
  304. package/src/presentation/subscribers/on-definition-resolved.test.ts +67 -0
  305. package/src/presentation/subscribers/on-definition-resolved.ts +11 -0
  306. package/src/presentation/subscribers/on-integration-event-received.test.ts +53 -0
  307. package/src/presentation/subscribers/on-integration-event-received.ts +21 -0
  308. package/src/presentation/subscribers/on-job-activated.ts +14 -0
  309. package/src/presentation/subscribers/on-job-terminated.ts +6 -0
  310. package/src/temporal/activities/drain-cron-batch.ts +20 -0
  311. package/src/temporal/activities/index.ts +15 -0
  312. package/src/temporal/activities/prune-trigger-events.test.ts +78 -0
  313. package/src/temporal/activities/prune-trigger-events.ts +9 -0
  314. package/src/temporal/constants.ts +7 -0
  315. package/src/temporal/workflows/cron-tick-cron.ts +56 -0
  316. package/src/temporal/workflows/index.ts +2 -0
  317. package/src/temporal/workflows/prune-trigger-events-cron.ts +17 -0
  318. package/test/env.ts +8 -0
  319. package/test/factories/cron-schedule.ts +34 -0
  320. package/test/factories/decision.ts +49 -0
  321. package/test/factories/job-listener-subscription.ts +42 -0
  322. package/test/factories/received-event.ts +49 -0
  323. package/test/factories/subscription.ts +38 -0
  324. package/test/globalSetup.ts +20 -0
  325. package/test/index.ts +5 -0
  326. package/test/setup.ts +17 -0
  327. package/tsconfig.build.json +9 -0
  328. package/tsconfig.build.tsbuildinfo +1 -0
  329. package/tsconfig.json +3 -0
  330. package/tsconfig.test.json +8 -0
  331. package/vitest.config.ts +11 -0
@@ -0,0 +1,140 @@
1
+ import { deliverEventToListener } from '@shipfox/api-workflows';
2
+ import { logger } from '@shipfox/node-opentelemetry';
3
+ import { findMatchingJobListenerSubscriptions } from '#db/job-listener-subscriptions.js';
4
+ import { evaluateStoredFilter } from './config.js';
5
+ import { toReason } from './record-trigger-history.js';
6
+ export async function routeEventToJobListeners(params) {
7
+ const subscriptions = await findMatchingJobListenerSubscriptions({
8
+ workspaceId: params.workspaceId,
9
+ source: params.source,
10
+ event: params.event
11
+ });
12
+ let filterErrorCount = 0;
13
+ const effectiveMatchByJobId = new Map();
14
+ for (const subscription of subscriptions){
15
+ const filterResult = evaluateListenerFilter({
16
+ subscription,
17
+ payload: params.payload
18
+ });
19
+ if (filterResult.kind === 'filtered') continue;
20
+ if (filterResult.kind === 'filter-error') {
21
+ filterErrorCount += 1;
22
+ await params.history.listenerFilterErrored(subscription, filterResult.reason);
23
+ continue;
24
+ }
25
+ const previous = effectiveMatchByJobId.get(subscription.jobId);
26
+ if (!previous || shouldReplaceEffectiveMatcher(previous, subscription)) {
27
+ effectiveMatchByJobId.set(subscription.jobId, subscription);
28
+ }
29
+ }
30
+ let acceptedJobCount = 0;
31
+ let deliveredCount = 0;
32
+ let sawTransientError = false;
33
+ let dispatchErrorCount = 0;
34
+ let firstTransientError;
35
+ for (const subscription of effectiveMatchByJobId.values()){
36
+ const disposition = listenerDisposition(subscription);
37
+ try {
38
+ const result = await deliverEventToListener({
39
+ jobId: subscription.jobId,
40
+ disposition,
41
+ eventRef: params.eventRef,
42
+ deliveryId: params.deliveryId,
43
+ source: params.source,
44
+ event: params.event,
45
+ provider: params.provider,
46
+ payload: params.payload,
47
+ receivedAt: params.receivedAt
48
+ });
49
+ if (!result.skipped) {
50
+ acceptedJobCount += 1;
51
+ await params.history.listenerTriggered(subscription);
52
+ }
53
+ if (result.buffered) deliveredCount += 1;
54
+ } catch (error) {
55
+ dispatchErrorCount += 1;
56
+ await params.history.listenerDispatchErrored(subscription, toReason(error));
57
+ if (!sawTransientError) {
58
+ sawTransientError = true;
59
+ firstTransientError = error;
60
+ }
61
+ }
62
+ }
63
+ return {
64
+ engagedCount: filterErrorCount + acceptedJobCount + dispatchErrorCount,
65
+ matchedJobCount: effectiveMatchByJobId.size,
66
+ acceptedJobCount,
67
+ deliveredCount,
68
+ transientErrored: sawTransientError,
69
+ transientError: sawTransientError ? firstTransientError : undefined
70
+ };
71
+ }
72
+ function evaluateListenerFilter(params) {
73
+ const filter = params.subscription.config.filter;
74
+ if (filter === null || filter === undefined) return {
75
+ kind: 'matched'
76
+ };
77
+ if (typeof filter !== 'string' || filter.trim() === '') {
78
+ return evaluateStoredFilter({
79
+ value: filter,
80
+ context: {
81
+ event: params.payload
82
+ },
83
+ invalidReason: 'Listener subscription filter must be a non-empty string when set',
84
+ evaluationFailedReason: 'Listener filter evaluation failed'
85
+ });
86
+ }
87
+ const snapshot = readFilterSnapshot(params.subscription);
88
+ if (snapshot.kind === 'invalid') return snapshot.result;
89
+ return evaluateStoredFilter({
90
+ value: filter,
91
+ context: {
92
+ ...snapshot.value,
93
+ event: params.payload
94
+ },
95
+ invalidReason: 'Listener subscription filter must be a non-empty string when set',
96
+ evaluationFailedReason: 'Listener filter evaluation failed'
97
+ });
98
+ }
99
+ function readFilterSnapshot(subscription) {
100
+ const value = subscription.config.filter_snapshot;
101
+ if (value === undefined) return {
102
+ kind: 'valid',
103
+ value: {}
104
+ };
105
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
106
+ return {
107
+ kind: 'valid',
108
+ value: value
109
+ };
110
+ }
111
+ return {
112
+ kind: 'invalid',
113
+ result: {
114
+ kind: 'filter-error',
115
+ reason: 'Listener filter snapshot must be an object when set'
116
+ }
117
+ };
118
+ }
119
+ function listenerDisposition(subscription) {
120
+ return subscription.kind === 'until' ? 'resolve' : 'fire';
121
+ }
122
+ function listenerMatcherSortKey(subscription) {
123
+ const kindRank = subscription.kind === 'until' ? '0' : '1';
124
+ return `${kindRank}:${subscription.matcherOrdinal.toString().padStart(10, '0')}`;
125
+ }
126
+ function shouldReplaceEffectiveMatcher(previous, candidate) {
127
+ const previousKey = listenerMatcherSortKey(previous);
128
+ const candidateKey = listenerMatcherSortKey(candidate);
129
+ if (candidateKey !== previousKey) return candidateKey < previousKey;
130
+ logger().warn({
131
+ jobId: candidate.jobId,
132
+ keptSubscriptionId: previous.id,
133
+ candidateSubscriptionId: candidate.id,
134
+ matcherKind: candidate.kind,
135
+ matcherOrdinal: candidate.matcherOrdinal
136
+ }, 'duplicate job listener matcher key encountered; choosing deterministic subscription id order');
137
+ return candidate.id < previous.id;
138
+ }
139
+
140
+ //# sourceMappingURL=route-event-to-job-listeners.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/core/route-event-to-job-listeners.ts"],"sourcesContent":["import {deliverEventToListener, type JobListenerEventDisposition} from '@shipfox/api-workflows';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {findMatchingJobListenerSubscriptions} from '#db/job-listener-subscriptions.js';\nimport {evaluateStoredFilter, type StoredFilterEvaluation} from './config.js';\nimport type {JobListenerSubscription} from './entities/job-listener-subscription.js';\nimport {type TriggerHistoryRecorder, toReason} from './record-trigger-history.js';\n\nexport interface RouteEventToJobListenersParams {\n history: TriggerHistoryRecorder;\n eventRef: string;\n workspaceId: string;\n provider: string;\n source: string;\n event: string;\n deliveryId: string;\n payload: unknown;\n receivedAt: Date;\n}\n\nexport interface RouteEventToJobListenersResult {\n /**\n * Listener outcomes that should contribute to received_event.matched_count:\n * filter errors, accepted deliveries, and dispatch errors. Stale skipped jobs\n * are intentionally excluded because they did not produce an auditable decision.\n */\n engagedCount: number;\n /**\n * Jobs with an effective matcher after source/event/filter checks, including\n * stale skipped jobs.\n */\n matchedJobCount: number;\n acceptedJobCount: number;\n deliveredCount: number;\n transientErrored: boolean;\n transientError: unknown;\n}\n\nexport async function routeEventToJobListeners(\n params: RouteEventToJobListenersParams,\n): Promise<RouteEventToJobListenersResult> {\n const subscriptions = await findMatchingJobListenerSubscriptions({\n workspaceId: params.workspaceId,\n source: params.source,\n event: params.event,\n });\n\n let filterErrorCount = 0;\n const effectiveMatchByJobId = new Map<string, JobListenerSubscription>();\n for (const subscription of subscriptions) {\n const filterResult = evaluateListenerFilter({subscription, payload: params.payload});\n if (filterResult.kind === 'filtered') continue;\n if (filterResult.kind === 'filter-error') {\n filterErrorCount += 1;\n await params.history.listenerFilterErrored(subscription, filterResult.reason);\n continue;\n }\n\n const previous = effectiveMatchByJobId.get(subscription.jobId);\n if (!previous || shouldReplaceEffectiveMatcher(previous, subscription)) {\n effectiveMatchByJobId.set(subscription.jobId, subscription);\n }\n }\n\n let acceptedJobCount = 0;\n let deliveredCount = 0;\n let sawTransientError = false;\n let dispatchErrorCount = 0;\n let firstTransientError: unknown;\n\n for (const subscription of effectiveMatchByJobId.values()) {\n const disposition = listenerDisposition(subscription);\n try {\n const result = await deliverEventToListener({\n jobId: subscription.jobId,\n disposition,\n eventRef: params.eventRef,\n deliveryId: params.deliveryId,\n source: params.source,\n event: params.event,\n provider: params.provider,\n payload: params.payload,\n receivedAt: params.receivedAt,\n });\n if (!result.skipped) {\n acceptedJobCount += 1;\n await params.history.listenerTriggered(subscription);\n }\n if (result.buffered) deliveredCount += 1;\n } catch (error) {\n dispatchErrorCount += 1;\n await params.history.listenerDispatchErrored(subscription, toReason(error));\n if (!sawTransientError) {\n sawTransientError = true;\n firstTransientError = error;\n }\n }\n }\n\n return {\n engagedCount: filterErrorCount + acceptedJobCount + dispatchErrorCount,\n matchedJobCount: effectiveMatchByJobId.size,\n acceptedJobCount,\n deliveredCount,\n transientErrored: sawTransientError,\n transientError: sawTransientError ? firstTransientError : undefined,\n };\n}\n\ninterface EvaluateListenerFilterParams {\n subscription: JobListenerSubscription;\n payload: unknown;\n}\n\nfunction evaluateListenerFilter(params: EvaluateListenerFilterParams): StoredFilterEvaluation {\n const filter = params.subscription.config.filter;\n if (filter === null || filter === undefined) return {kind: 'matched'};\n if (typeof filter !== 'string' || filter.trim() === '') {\n return evaluateStoredFilter({\n value: filter,\n context: {event: params.payload},\n invalidReason: 'Listener subscription filter must be a non-empty string when set',\n evaluationFailedReason: 'Listener filter evaluation failed',\n });\n }\n\n const snapshot = readFilterSnapshot(params.subscription);\n if (snapshot.kind === 'invalid') return snapshot.result;\n\n return evaluateStoredFilter({\n value: filter,\n context: {...snapshot.value, event: params.payload},\n invalidReason: 'Listener subscription filter must be a non-empty string when set',\n evaluationFailedReason: 'Listener filter evaluation failed',\n });\n}\n\nfunction readFilterSnapshot(\n subscription: JobListenerSubscription,\n):\n | {kind: 'valid'; value: Record<string, unknown>}\n | {kind: 'invalid'; result: Extract<StoredFilterEvaluation, {kind: 'filter-error'}>} {\n const value = subscription.config.filter_snapshot;\n if (value === undefined) return {kind: 'valid', value: {}};\n if (value !== null && typeof value === 'object' && !Array.isArray(value)) {\n return {kind: 'valid', value: value as Record<string, unknown>};\n }\n return {\n kind: 'invalid',\n result: {\n kind: 'filter-error',\n reason: 'Listener filter snapshot must be an object when set',\n },\n };\n}\n\nfunction listenerDisposition(subscription: JobListenerSubscription): JobListenerEventDisposition {\n return subscription.kind === 'until' ? 'resolve' : 'fire';\n}\n\nfunction listenerMatcherSortKey(subscription: JobListenerSubscription): string {\n const kindRank = subscription.kind === 'until' ? '0' : '1';\n return `${kindRank}:${subscription.matcherOrdinal.toString().padStart(10, '0')}`;\n}\n\nfunction shouldReplaceEffectiveMatcher(\n previous: JobListenerSubscription,\n candidate: JobListenerSubscription,\n): boolean {\n const previousKey = listenerMatcherSortKey(previous);\n const candidateKey = listenerMatcherSortKey(candidate);\n if (candidateKey !== previousKey) return candidateKey < previousKey;\n\n logger().warn(\n {\n jobId: candidate.jobId,\n keptSubscriptionId: previous.id,\n candidateSubscriptionId: candidate.id,\n matcherKind: candidate.kind,\n matcherOrdinal: candidate.matcherOrdinal,\n },\n 'duplicate job listener matcher key encountered; choosing deterministic subscription id order',\n );\n return candidate.id < previous.id;\n}\n"],"names":["deliverEventToListener","logger","findMatchingJobListenerSubscriptions","evaluateStoredFilter","toReason","routeEventToJobListeners","params","subscriptions","workspaceId","source","event","filterErrorCount","effectiveMatchByJobId","Map","subscription","filterResult","evaluateListenerFilter","payload","kind","history","listenerFilterErrored","reason","previous","get","jobId","shouldReplaceEffectiveMatcher","set","acceptedJobCount","deliveredCount","sawTransientError","dispatchErrorCount","firstTransientError","values","disposition","listenerDisposition","result","eventRef","deliveryId","provider","receivedAt","skipped","listenerTriggered","buffered","error","listenerDispatchErrored","engagedCount","matchedJobCount","size","transientErrored","transientError","undefined","filter","config","trim","value","context","invalidReason","evaluationFailedReason","snapshot","readFilterSnapshot","filter_snapshot","Array","isArray","listenerMatcherSortKey","kindRank","matcherOrdinal","toString","padStart","candidate","previousKey","candidateKey","warn","keptSubscriptionId","id","candidateSubscriptionId","matcherKind"],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import type { CronSchedule } from '#core/entities/cron-schedule.js';
2
+ import { type Executor, type Tx } from './db.js';
3
+ export interface SyncCronScheduleParams {
4
+ readonly tx: Tx;
5
+ readonly subscriptionId: string;
6
+ readonly workspaceId: string;
7
+ readonly triggerConfig: Record<string, unknown> | undefined;
8
+ }
9
+ export declare function syncCronSchedule(params: SyncCronScheduleParams): Promise<void>;
10
+ export interface DeleteCronScheduleForSubscriptionParams {
11
+ readonly tx: Tx;
12
+ readonly subscriptionId: string;
13
+ }
14
+ export declare function deleteCronScheduleForSubscription(params: DeleteCronScheduleForSubscriptionParams): Promise<void>;
15
+ export declare function selectDbNow(executor: Executor): Promise<Date>;
16
+ export interface ClaimDueCronSchedulesParams {
17
+ readonly tx: Tx;
18
+ readonly limit: number;
19
+ readonly now: Date;
20
+ }
21
+ /**
22
+ * Claims schedules due as of `now` (the shared database clock captured for this tick)
23
+ * and holds `FOR UPDATE SKIP LOCKED` locks until the caller's transaction ends, so
24
+ * concurrent drains partition due rows instead of double-claiming them.
25
+ */
26
+ export declare function claimDueCronSchedules(params: ClaimDueCronSchedulesParams): Promise<CronSchedule[]>;
27
+ export interface AdvanceCronScheduleParams {
28
+ readonly tx: Tx;
29
+ readonly subscriptionId: string;
30
+ readonly nextFireAt: Date;
31
+ readonly lastFiredAt: Date;
32
+ }
33
+ export declare function advanceCronSchedule(params: AdvanceCronScheduleParams): Promise<void>;
34
+ export declare function countDueCronSchedules(): Promise<number>;
35
+ export declare function getCronScheduleBySubscriptionId(subscriptionId: string): Promise<CronSchedule | undefined>;
36
+ //# sourceMappingURL=cron-schedules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron-schedules.d.ts","sourceRoot":"","sources":["../../src/db/cron-schedules.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAK,KAAK,QAAQ,EAAE,KAAK,EAAE,EAAC,MAAM,SAAS,CAAC;AAKnD,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC;IAChB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;CAC7D;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkDpF;AAED,MAAM,WAAW,uCAAuC;IACtD,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC;IAChB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,wBAAsB,iCAAiC,CACrD,MAAM,EAAE,uCAAuC,GAC9C,OAAO,CAAC,IAAI,CAAC,CAIf;AAID,wBAAsB,WAAW,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAKnE;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC;IAChB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,YAAY,EAAE,CAAC,CASzB;AAED,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC;IAChB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC;IAC1B,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAC;CAC5B;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CAS1F;AAED,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,MAAM,CAAC,CAM7D;AAED,wBAAsB,+BAA+B,CACnD,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CASnC"}
@@ -0,0 +1,104 @@
1
+ import { logger } from '@shipfox/node-opentelemetry';
2
+ import { triggerSourceConfigSchemas } from '@shipfox/workflow-document';
3
+ import { eq, lte, sql } from 'drizzle-orm';
4
+ import { config } from '#config.js';
5
+ import { computeNextFireAt } from '#core/compute-next-fire-at.js';
6
+ import { db } from './db.js';
7
+ import { toCronSchedule, triggersCronSchedules } from './schema/cron-schedules.js';
8
+ const defaultCronTimezone = 'UTC';
9
+ export async function syncCronSchedule(params) {
10
+ const parsed = triggerSourceConfigSchemas.cron.safeParse(params.triggerConfig ?? {});
11
+ if (!parsed.success || parsed.data.schedule === undefined) {
12
+ logger().warn({
13
+ subscriptionId: params.subscriptionId
14
+ }, 'cron trigger missing/invalid schedule config; skipping cron row');
15
+ await deleteCronScheduleForSubscription(params);
16
+ return;
17
+ }
18
+ const schedule = parsed.data.schedule;
19
+ const timezone = parsed.data.timezone ?? defaultCronTimezone;
20
+ let nextFireAt;
21
+ try {
22
+ nextFireAt = computeNextFireAt({
23
+ cronExpression: schedule,
24
+ timezone,
25
+ from: new Date(),
26
+ subscriptionId: params.subscriptionId,
27
+ jitterWindowSeconds: config.TRIGGER_CRON_JITTER_WINDOW_SECONDS
28
+ });
29
+ } catch (error) {
30
+ logger().warn({
31
+ err: error,
32
+ reason: errorMessage(error),
33
+ subscriptionId: params.subscriptionId
34
+ }, 'cron trigger schedule could not be parsed; skipping cron row');
35
+ await deleteCronScheduleForSubscription(params);
36
+ return;
37
+ }
38
+ await params.tx.insert(triggersCronSchedules).values({
39
+ subscriptionId: params.subscriptionId,
40
+ workspaceId: params.workspaceId,
41
+ cronExpression: schedule,
42
+ timezone,
43
+ nextFireAt
44
+ }).onConflictDoUpdate({
45
+ target: triggersCronSchedules.subscriptionId,
46
+ set: {
47
+ workspaceId: sql`excluded.workspace_id`,
48
+ cronExpression: sql`excluded.cron_expression`,
49
+ timezone: sql`excluded.timezone`,
50
+ updatedAt: sql`case when ${triggersCronSchedules.workspaceId} <> excluded.workspace_id or ${triggersCronSchedules.cronExpression} <> excluded.cron_expression or ${triggersCronSchedules.timezone} <> excluded.timezone then ${new Date()} else ${triggersCronSchedules.updatedAt} end`,
51
+ nextFireAt: sql`case when ${triggersCronSchedules.cronExpression} <> excluded.cron_expression or ${triggersCronSchedules.timezone} <> excluded.timezone then excluded.next_fire_at else ${triggersCronSchedules.nextFireAt} end`
52
+ }
53
+ });
54
+ }
55
+ export async function deleteCronScheduleForSubscription(params) {
56
+ await params.tx.delete(triggersCronSchedules).where(eq(triggersCronSchedules.subscriptionId, params.subscriptionId));
57
+ }
58
+ // Reads the database clock so a tick's due check and its next-fire computation share
59
+ // one time reference, immune to application/database clock skew.
60
+ export async function selectDbNow(executor) {
61
+ const result = await executor.execute(sql`select now() as now`);
62
+ const raw = result.rows[0]?.now;
63
+ if (raw === undefined) throw new Error('Failed to read the database clock');
64
+ return raw instanceof Date ? raw : new Date(raw);
65
+ }
66
+ /**
67
+ * Claims schedules due as of `now` (the shared database clock captured for this tick)
68
+ * and holds `FOR UPDATE SKIP LOCKED` locks until the caller's transaction ends, so
69
+ * concurrent drains partition due rows instead of double-claiming them.
70
+ */ export async function claimDueCronSchedules(params) {
71
+ const rows = await params.tx.select().from(triggersCronSchedules).where(lte(triggersCronSchedules.nextFireAt, params.now)).orderBy(triggersCronSchedules.nextFireAt).limit(params.limit).for('update', {
72
+ skipLocked: true
73
+ });
74
+ return rows.map(toCronSchedule);
75
+ }
76
+ export async function advanceCronSchedule(params) {
77
+ await params.tx.update(triggersCronSchedules).set({
78
+ nextFireAt: params.nextFireAt,
79
+ lastFiredAt: params.lastFiredAt,
80
+ updatedAt: new Date()
81
+ }).where(eq(triggersCronSchedules.subscriptionId, params.subscriptionId));
82
+ }
83
+ export async function countDueCronSchedules() {
84
+ const [row] = await db().select({
85
+ count: sql`count(*)::int`
86
+ }).from(triggersCronSchedules).where(lte(triggersCronSchedules.nextFireAt, sql`now()`));
87
+ return row?.count ?? 0;
88
+ }
89
+ export async function getCronScheduleBySubscriptionId(subscriptionId) {
90
+ const rows = await db().select().from(triggersCronSchedules).where(eq(triggersCronSchedules.subscriptionId, subscriptionId)).limit(1);
91
+ const row = rows[0];
92
+ if (!row) return undefined;
93
+ return toCronSchedule(row);
94
+ }
95
+ function errorMessage(error) {
96
+ if (error instanceof Error) return error.message;
97
+ try {
98
+ return String(error);
99
+ } catch {
100
+ return '[unprintable thrown value]';
101
+ }
102
+ }
103
+
104
+ //# sourceMappingURL=cron-schedules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/db/cron-schedules.ts"],"sourcesContent":["import {logger} from '@shipfox/node-opentelemetry';\nimport {triggerSourceConfigSchemas} from '@shipfox/workflow-document';\nimport {eq, lte, sql} from 'drizzle-orm';\nimport {config} from '#config.js';\nimport {computeNextFireAt} from '#core/compute-next-fire-at.js';\nimport type {CronSchedule} from '#core/entities/cron-schedule.js';\nimport {db, type Executor, type Tx} from './db.js';\nimport {toCronSchedule, triggersCronSchedules} from './schema/cron-schedules.js';\n\nconst defaultCronTimezone = 'UTC';\n\nexport interface SyncCronScheduleParams {\n readonly tx: Tx;\n readonly subscriptionId: string;\n readonly workspaceId: string;\n readonly triggerConfig: Record<string, unknown> | undefined;\n}\n\nexport async function syncCronSchedule(params: SyncCronScheduleParams): Promise<void> {\n const parsed = triggerSourceConfigSchemas.cron.safeParse(params.triggerConfig ?? {});\n if (!parsed.success || parsed.data.schedule === undefined) {\n logger().warn(\n {subscriptionId: params.subscriptionId},\n 'cron trigger missing/invalid schedule config; skipping cron row',\n );\n await deleteCronScheduleForSubscription(params);\n return;\n }\n\n const schedule = parsed.data.schedule;\n const timezone = parsed.data.timezone ?? defaultCronTimezone;\n let nextFireAt: Date;\n try {\n nextFireAt = computeNextFireAt({\n cronExpression: schedule,\n timezone,\n from: new Date(),\n subscriptionId: params.subscriptionId,\n jitterWindowSeconds: config.TRIGGER_CRON_JITTER_WINDOW_SECONDS,\n });\n } catch (error) {\n logger().warn(\n {err: error, reason: errorMessage(error), subscriptionId: params.subscriptionId},\n 'cron trigger schedule could not be parsed; skipping cron row',\n );\n await deleteCronScheduleForSubscription(params);\n return;\n }\n\n await params.tx\n .insert(triggersCronSchedules)\n .values({\n subscriptionId: params.subscriptionId,\n workspaceId: params.workspaceId,\n cronExpression: schedule,\n timezone,\n nextFireAt,\n })\n .onConflictDoUpdate({\n target: triggersCronSchedules.subscriptionId,\n set: {\n workspaceId: sql`excluded.workspace_id`,\n cronExpression: sql`excluded.cron_expression`,\n timezone: sql`excluded.timezone`,\n updatedAt: sql`case when ${triggersCronSchedules.workspaceId} <> excluded.workspace_id or ${triggersCronSchedules.cronExpression} <> excluded.cron_expression or ${triggersCronSchedules.timezone} <> excluded.timezone then ${new Date()} else ${triggersCronSchedules.updatedAt} end`,\n nextFireAt: sql`case when ${triggersCronSchedules.cronExpression} <> excluded.cron_expression or ${triggersCronSchedules.timezone} <> excluded.timezone then excluded.next_fire_at else ${triggersCronSchedules.nextFireAt} end`,\n },\n });\n}\n\nexport interface DeleteCronScheduleForSubscriptionParams {\n readonly tx: Tx;\n readonly subscriptionId: string;\n}\n\nexport async function deleteCronScheduleForSubscription(\n params: DeleteCronScheduleForSubscriptionParams,\n): Promise<void> {\n await params.tx\n .delete(triggersCronSchedules)\n .where(eq(triggersCronSchedules.subscriptionId, params.subscriptionId));\n}\n\n// Reads the database clock so a tick's due check and its next-fire computation share\n// one time reference, immune to application/database clock skew.\nexport async function selectDbNow(executor: Executor): Promise<Date> {\n const result = await executor.execute<{now: Date | string}>(sql`select now() as now`);\n const raw = result.rows[0]?.now;\n if (raw === undefined) throw new Error('Failed to read the database clock');\n return raw instanceof Date ? raw : new Date(raw);\n}\n\nexport interface ClaimDueCronSchedulesParams {\n readonly tx: Tx;\n readonly limit: number;\n readonly now: Date;\n}\n\n/**\n * Claims schedules due as of `now` (the shared database clock captured for this tick)\n * and holds `FOR UPDATE SKIP LOCKED` locks until the caller's transaction ends, so\n * concurrent drains partition due rows instead of double-claiming them.\n */\nexport async function claimDueCronSchedules(\n params: ClaimDueCronSchedulesParams,\n): Promise<CronSchedule[]> {\n const rows = await params.tx\n .select()\n .from(triggersCronSchedules)\n .where(lte(triggersCronSchedules.nextFireAt, params.now))\n .orderBy(triggersCronSchedules.nextFireAt)\n .limit(params.limit)\n .for('update', {skipLocked: true});\n return rows.map(toCronSchedule);\n}\n\nexport interface AdvanceCronScheduleParams {\n readonly tx: Tx;\n readonly subscriptionId: string;\n readonly nextFireAt: Date;\n readonly lastFiredAt: Date;\n}\n\nexport async function advanceCronSchedule(params: AdvanceCronScheduleParams): Promise<void> {\n await params.tx\n .update(triggersCronSchedules)\n .set({\n nextFireAt: params.nextFireAt,\n lastFiredAt: params.lastFiredAt,\n updatedAt: new Date(),\n })\n .where(eq(triggersCronSchedules.subscriptionId, params.subscriptionId));\n}\n\nexport async function countDueCronSchedules(): Promise<number> {\n const [row] = await db()\n .select({count: sql<number>`count(*)::int`})\n .from(triggersCronSchedules)\n .where(lte(triggersCronSchedules.nextFireAt, sql`now()`));\n return row?.count ?? 0;\n}\n\nexport async function getCronScheduleBySubscriptionId(\n subscriptionId: string,\n): Promise<CronSchedule | undefined> {\n const rows = await db()\n .select()\n .from(triggersCronSchedules)\n .where(eq(triggersCronSchedules.subscriptionId, subscriptionId))\n .limit(1);\n const row = rows[0];\n if (!row) return undefined;\n return toCronSchedule(row);\n}\n\nfunction errorMessage(error: unknown): string {\n if (error instanceof Error) return error.message;\n try {\n return String(error);\n } catch {\n return '[unprintable thrown value]';\n }\n}\n"],"names":["logger","triggerSourceConfigSchemas","eq","lte","sql","config","computeNextFireAt","db","toCronSchedule","triggersCronSchedules","defaultCronTimezone","syncCronSchedule","params","parsed","cron","safeParse","triggerConfig","success","data","schedule","undefined","warn","subscriptionId","deleteCronScheduleForSubscription","timezone","nextFireAt","cronExpression","from","Date","jitterWindowSeconds","TRIGGER_CRON_JITTER_WINDOW_SECONDS","error","err","reason","errorMessage","tx","insert","values","workspaceId","onConflictDoUpdate","target","set","updatedAt","delete","where","selectDbNow","executor","result","execute","raw","rows","now","Error","claimDueCronSchedules","select","orderBy","limit","for","skipLocked","map","advanceCronSchedule","update","lastFiredAt","countDueCronSchedules","row","count","getCronScheduleBySubscriptionId","message","String"],"mappings":"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"}