@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,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1783263628394,
9
+ "tag": "0000_initial",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,14 @@
1
+ import {defineConfig} from 'drizzle-kit';
2
+
3
+ export default defineConfig({
4
+ schema: [
5
+ './src/db/schema/cron-schedules.ts',
6
+ './src/db/schema/decisions.ts',
7
+ './src/db/schema/job-listener-subscriptions.ts',
8
+ './src/db/schema/outbox.ts',
9
+ './src/db/schema/received-events.ts',
10
+ './src/db/schema/subscriptions.ts',
11
+ ],
12
+ out: './drizzle',
13
+ dialect: 'postgresql',
14
+ });
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@shipfox/api-triggers",
3
+ "license": "MIT",
4
+ "version": "2.0.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
8
+ "directory": "libs/api/triggers"
9
+ },
10
+ "private": false,
11
+ "type": "module",
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "imports": {
15
+ "#test/*": "./test/*",
16
+ "#*": {
17
+ "workspace-source": "./src/*",
18
+ "development": "./src/*",
19
+ "default": "./dist/*"
20
+ }
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "development": {
25
+ "types": "./src/index.ts",
26
+ "default": "./src/index.ts"
27
+ },
28
+ "default": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ }
32
+ }
33
+ },
34
+ "dependencies": {
35
+ "@temporalio/workflow": "^1.16.1",
36
+ "cron-parser": "^5.6.1",
37
+ "drizzle-orm": "^0.45.2",
38
+ "zod": "^4.4.3",
39
+ "@shipfox/api-definitions-dto": "2.0.0",
40
+ "@shipfox/api-projects": "2.0.0",
41
+ "@shipfox/api-triggers-dto": "2.0.0",
42
+ "@shipfox/api-integration-core-dto": "2.0.0",
43
+ "@shipfox/api-workflows-dto": "2.0.0",
44
+ "@shipfox/api-workflows": "2.0.0",
45
+ "@shipfox/config": "1.2.1",
46
+ "@shipfox/api-auth-context": "2.0.0",
47
+ "@shipfox/node-drizzle": "0.2.1",
48
+ "@shipfox/node-opentelemetry": "0.5.0",
49
+ "@shipfox/node-fastify": "0.2.1",
50
+ "@shipfox/node-outbox": "0.2.1",
51
+ "@shipfox/node-module": "0.2.0",
52
+ "@shipfox/node-postgres": "0.4.1",
53
+ "@shipfox/node-temporal": "0.2.0",
54
+ "@shipfox/workflow-document": "2.0.1",
55
+ "@shipfox/expression": "1.1.1"
56
+ },
57
+ "devDependencies": {
58
+ "@temporalio/activity": "^1.16.1",
59
+ "@temporalio/client": "^1.16.1",
60
+ "@temporalio/common": "^1.16.1",
61
+ "@temporalio/testing": "^1.16.1",
62
+ "@temporalio/worker": "^1.16.1",
63
+ "@types/pg": "^8.15.5",
64
+ "drizzle-kit": "^0.31.10",
65
+ "fastify": "^5.3.3",
66
+ "fastify-type-provider-zod": "^6.0.0",
67
+ "fishery": "^2.2.2",
68
+ "@shipfox/swc": "1.2.5",
69
+ "@shipfox/ts-config": "1.3.8",
70
+ "@shipfox/typescript": "1.1.6",
71
+ "@shipfox/biome": "1.8.1",
72
+ "@shipfox/vitest": "1.2.2"
73
+ },
74
+ "scripts": {
75
+ "build": "shipfox-swc",
76
+ "check": "shipfox-biome-check",
77
+ "check:fix": "shipfox-biome-check --write",
78
+ "test": "shipfox-vitest-run",
79
+ "test:watch": "shipfox-vitest-watch",
80
+ "type": "shipfox-tsc-check",
81
+ "type:emit": "shipfox-tsc-emit"
82
+ }
83
+ }
@@ -0,0 +1,33 @@
1
+ import {assertCronConfigWithinBounds, assertRetentionDaysWithinBounds} from './config.js';
2
+
3
+ const RETENTION_ERROR = /TRIGGER_EVENT_RETENTION_DAYS/;
4
+ const CRON_JITTER_ERROR = /TRIGGER_CRON_JITTER_WINDOW_SECONDS/;
5
+
6
+ describe('assertRetentionDaysWithinBounds', () => {
7
+ test.each([1, 30, 365])('accepts a finite window of at least 1 day (%p)', (days) => {
8
+ expect(() => assertRetentionDaysWithinBounds(days)).not.toThrow();
9
+ });
10
+
11
+ test.each([
12
+ 0,
13
+ -1,
14
+ Number.NaN,
15
+ Number.POSITIVE_INFINITY,
16
+ ])('rejects a window below 1 day or non-finite (%p)', (days) => {
17
+ expect(() => assertRetentionDaysWithinBounds(days)).toThrow(RETENTION_ERROR);
18
+ });
19
+ });
20
+
21
+ describe('assertCronConfigWithinBounds', () => {
22
+ test.each([0, 1, 60, 600])('accepts a finite non-negative jitter window (%p)', (seconds) => {
23
+ expect(() => assertCronConfigWithinBounds(seconds)).not.toThrow();
24
+ });
25
+
26
+ test.each([
27
+ -1,
28
+ Number.NaN,
29
+ Number.POSITIVE_INFINITY,
30
+ ])('rejects a negative or non-finite jitter window (%p)', (seconds) => {
31
+ expect(() => assertCronConfigWithinBounds(seconds)).toThrow(CRON_JITTER_ERROR);
32
+ });
33
+ });
package/src/config.ts ADDED
@@ -0,0 +1,53 @@
1
+ import {createConfig, num} from '@shipfox/config';
2
+
3
+ export const config = createConfig({
4
+ TRIGGER_EVENT_RETENTION_DAYS: num({
5
+ desc: 'How many days a recorded trigger event (and its decisions) is kept before the hourly prune cron deletes it. Must be at least 1; a smaller value moves the cutoff to now or the future and would delete freshly recorded events. Defaults to 30.',
6
+ default: 30,
7
+ }),
8
+ TRIGGER_CRON_JITTER_WINDOW_SECONDS: num({
9
+ desc: 'Maximum number of seconds added as deterministic jitter to a cron trigger fire time. Use 0 to disable jitter. Values below about 60 seconds are usually no-ops when cron ticking runs at minute resolution; multi-minute windows spread large schedule herds.',
10
+ default: 0,
11
+ }),
12
+ TRIGGER_CRON_FANOUT: num({
13
+ desc: 'How many drain activities the once-per-minute cron tick runs in parallel. Each activity claims and fires one bounded batch, so the per-minute ceiling is TRIGGER_CRON_FANOUT times TRIGGER_CRON_CLAIM_BATCH runs. Raise it (and pod count) to fire more schedules per minute. Must be at least 1. Defaults to 2.',
14
+ default: 2,
15
+ }),
16
+ TRIGGER_CRON_CLAIM_BATCH: num({
17
+ desc: 'How many due cron schedules a single drain activity claims and fires per tick. Bounds the load a cron burst can put on the database and the run-creation path. Must be at least 1. Defaults to 100.',
18
+ default: 100,
19
+ }),
20
+ });
21
+
22
+ export function assertRetentionDaysWithinBounds(days: number): void {
23
+ if (!Number.isFinite(days) || days < 1) {
24
+ throw new Error(
25
+ `TRIGGER_EVENT_RETENTION_DAYS must be a finite number of at least 1, received ${days}.`,
26
+ );
27
+ }
28
+ }
29
+
30
+ assertRetentionDaysWithinBounds(config.TRIGGER_EVENT_RETENTION_DAYS);
31
+
32
+ export function assertCronConfigWithinBounds(jitterWindowSeconds: number): void {
33
+ if (!Number.isFinite(jitterWindowSeconds) || jitterWindowSeconds < 0) {
34
+ throw new Error(
35
+ `TRIGGER_CRON_JITTER_WINDOW_SECONDS must be a finite non-negative number, received ${jitterWindowSeconds}.`,
36
+ );
37
+ }
38
+ }
39
+
40
+ assertCronConfigWithinBounds(config.TRIGGER_CRON_JITTER_WINDOW_SECONDS);
41
+
42
+ export function assertCronThroughputWithinBounds(fanout: number, claimBatch: number): void {
43
+ if (!Number.isInteger(fanout) || fanout < 1) {
44
+ throw new Error(`TRIGGER_CRON_FANOUT must be an integer of at least 1, received ${fanout}.`);
45
+ }
46
+ if (!Number.isInteger(claimBatch) || claimBatch < 1) {
47
+ throw new Error(
48
+ `TRIGGER_CRON_CLAIM_BATCH must be an integer of at least 1, received ${claimBatch}.`,
49
+ );
50
+ }
51
+ }
52
+
53
+ assertCronThroughputWithinBounds(config.TRIGGER_CRON_FANOUT, config.TRIGGER_CRON_CLAIM_BATCH);
@@ -0,0 +1,69 @@
1
+ import {computeNextFireAt} from './compute-next-fire-at.js';
2
+
3
+ describe('computeNextFireAt', () => {
4
+ test('returns the next occurrence strictly after the from instant', () => {
5
+ const result = computeNextFireAt({
6
+ cronExpression: '0 2 * * *',
7
+ timezone: 'UTC',
8
+ from: new Date('2026-01-01T02:00:00.000Z'),
9
+ subscriptionId: crypto.randomUUID(),
10
+ jitterWindowSeconds: 0,
11
+ });
12
+
13
+ expect(result).toEqual(new Date('2026-01-02T02:00:00.000Z'));
14
+ });
15
+
16
+ test('evaluates the schedule in the supplied timezone', () => {
17
+ const result = computeNextFireAt({
18
+ cronExpression: '0 2 * * *',
19
+ timezone: 'America/New_York',
20
+ from: new Date('2026-01-01T00:00:00.000Z'),
21
+ subscriptionId: crypto.randomUUID(),
22
+ jitterWindowSeconds: 0,
23
+ });
24
+
25
+ expect(result).toEqual(new Date('2026-01-01T07:00:00.000Z'));
26
+ });
27
+
28
+ test('applies deterministic jitter for the subscription', () => {
29
+ const params = {
30
+ cronExpression: '0 2 * * *',
31
+ timezone: 'UTC',
32
+ from: new Date('2026-01-01T00:00:00.000Z'),
33
+ subscriptionId: crypto.randomUUID(),
34
+ jitterWindowSeconds: 600,
35
+ };
36
+
37
+ const first = computeNextFireAt(params);
38
+ const second = computeNextFireAt(params);
39
+
40
+ expect(second).toEqual(first);
41
+ expect(first.getTime()).toBeGreaterThanOrEqual(new Date('2026-01-01T02:00:00.000Z').getTime());
42
+ expect(first.getTime()).toBeLessThan(new Date('2026-01-02T02:00:00.000Z').getTime());
43
+ });
44
+
45
+ test('does not jitter past the following occurrence', () => {
46
+ const result = computeNextFireAt({
47
+ cronExpression: '*/5 * * * *',
48
+ timezone: 'UTC',
49
+ from: new Date('2026-01-01T00:00:00.000Z'),
50
+ subscriptionId: crypto.randomUUID(),
51
+ jitterWindowSeconds: 60 * 60,
52
+ });
53
+
54
+ expect(result.getTime()).toBeGreaterThanOrEqual(new Date('2026-01-01T00:05:00.000Z').getTime());
55
+ expect(result.getTime()).toBeLessThan(new Date('2026-01-01T00:10:00.000Z').getTime());
56
+ });
57
+
58
+ test('returns the exact occurrence when the jitter window is disabled', () => {
59
+ const result = computeNextFireAt({
60
+ cronExpression: '*/5 * * * *',
61
+ timezone: 'UTC',
62
+ from: new Date('2026-01-01T00:00:00.000Z'),
63
+ subscriptionId: crypto.randomUUID(),
64
+ jitterWindowSeconds: 0,
65
+ });
66
+
67
+ expect(result).toEqual(new Date('2026-01-01T00:05:00.000Z'));
68
+ });
69
+ });
@@ -0,0 +1,37 @@
1
+ import {CronExpressionParser} from 'cron-parser';
2
+
3
+ export interface ComputeNextFireAtParams {
4
+ readonly cronExpression: string;
5
+ readonly timezone: string;
6
+ readonly from: Date;
7
+ readonly subscriptionId: string;
8
+ readonly jitterWindowSeconds: number;
9
+ }
10
+
11
+ export function computeNextFireAt(params: ComputeNextFireAtParams): Date {
12
+ const iterator = CronExpressionParser.parse(params.cronExpression, {
13
+ currentDate: params.from,
14
+ tz: params.timezone,
15
+ });
16
+ const occurrence = iterator.next().toDate();
17
+ const following = iterator.next().toDate();
18
+ const gapMs = following.getTime() - occurrence.getTime();
19
+ const offsetMs = jitterOffsetMs(params.subscriptionId, params.jitterWindowSeconds, gapMs);
20
+ return new Date(occurrence.getTime() + offsetMs);
21
+ }
22
+
23
+ function jitterOffsetMs(subscriptionId: string, windowSeconds: number, gapMs: number): number {
24
+ const windowMs = Math.floor(windowSeconds * 1000);
25
+ const clampMs = Math.min(windowMs, gapMs);
26
+ if (clampMs <= 0) return 0;
27
+ return hashStringToUint32(subscriptionId) % clampMs;
28
+ }
29
+
30
+ function hashStringToUint32(value: string): number {
31
+ let hash = 0x811c9dc5;
32
+ for (let index = 0; index < value.length; index += 1) {
33
+ hash ^= value.charCodeAt(index);
34
+ hash = Math.imul(hash, 0x01000193);
35
+ }
36
+ return hash >>> 0;
37
+ }
@@ -0,0 +1,213 @@
1
+ import {evaluateStoredFilter, evaluateTriggerFilter, readConfigInputs} from './config.js';
2
+ import type {TriggerSubscription} from './entities/subscription.js';
3
+
4
+ function subscriptionWithConfig(config: Record<string, unknown>): TriggerSubscription {
5
+ return {
6
+ id: crypto.randomUUID(),
7
+ workspaceId: crypto.randomUUID(),
8
+ projectId: crypto.randomUUID(),
9
+ workflowDefinitionId: crypto.randomUUID(),
10
+ name: 'test',
11
+ source: 'github',
12
+ event: 'push',
13
+ config,
14
+ createdAt: new Date(),
15
+ updatedAt: new Date(),
16
+ };
17
+ }
18
+
19
+ describe('readConfigInputs', () => {
20
+ test('returns the with object when it is a plain object', () => {
21
+ const inputs = readConfigInputs(subscriptionWithConfig({with: {env: 'staging'}}));
22
+
23
+ expect(inputs).toEqual({env: 'staging'});
24
+ });
25
+
26
+ test('returns undefined when with is missing', () => {
27
+ const inputs = readConfigInputs(subscriptionWithConfig({on: 'main'}));
28
+
29
+ expect(inputs).toBeUndefined();
30
+ });
31
+
32
+ test('returns undefined when with is null', () => {
33
+ const inputs = readConfigInputs(subscriptionWithConfig({with: null}));
34
+
35
+ expect(inputs).toBeUndefined();
36
+ });
37
+
38
+ test('returns undefined when with is an array', () => {
39
+ const inputs = readConfigInputs(subscriptionWithConfig({with: ['env']}));
40
+
41
+ expect(inputs).toBeUndefined();
42
+ });
43
+
44
+ test('returns undefined when with is a primitive', () => {
45
+ const inputs = readConfigInputs(subscriptionWithConfig({with: 'staging'}));
46
+
47
+ expect(inputs).toBeUndefined();
48
+ });
49
+ });
50
+
51
+ describe('evaluateTriggerFilter', () => {
52
+ test('returns matched when filter is missing', () => {
53
+ const result = evaluateTriggerFilter({
54
+ subscription: subscriptionWithConfig({}),
55
+ source: 'github',
56
+ event: 'push',
57
+ payload: {ref: 'refs/heads/main'},
58
+ });
59
+
60
+ expect(result).toEqual({kind: 'matched'});
61
+ });
62
+
63
+ test('evaluates filter expressions against the event payload and trigger identity', () => {
64
+ const subscription = subscriptionWithConfig({
65
+ filter:
66
+ 'event.ref == "refs/heads/main" && event.repository.full_name == "shipfox/platform" && trigger.source == "github" && trigger.event == "push"',
67
+ });
68
+
69
+ const matches = evaluateTriggerFilter({
70
+ subscription,
71
+ source: 'github',
72
+ event: 'push',
73
+ payload: {
74
+ ref: 'refs/heads/main',
75
+ repository: {full_name: 'shipfox/platform'},
76
+ },
77
+ });
78
+ const misses = evaluateTriggerFilter({
79
+ subscription,
80
+ source: 'github',
81
+ event: 'push',
82
+ payload: {
83
+ ref: 'refs/heads/main',
84
+ repository: {full_name: 'shipfox/docs'},
85
+ },
86
+ });
87
+
88
+ expect(matches).toEqual({kind: 'matched'});
89
+ expect(misses).toEqual({kind: 'filtered'});
90
+ });
91
+
92
+ test('returns filter-error when the stored filter cannot be parsed', () => {
93
+ const subscription = subscriptionWithConfig({filter: 'event.ref =='});
94
+
95
+ const result = evaluateTriggerFilter({
96
+ subscription,
97
+ source: 'github',
98
+ event: 'push',
99
+ payload: {ref: 'refs/heads/main'},
100
+ });
101
+
102
+ expect(result.kind).toBe('filter-error');
103
+ if (result.kind !== 'filter-error') throw new Error('expected filter-error');
104
+ expect(result.reason).not.toBe('Invalid workflow expression');
105
+ });
106
+
107
+ test('returns filter-error when filter evaluation throws', () => {
108
+ const subscription = subscriptionWithConfig({filter: 'event.ref.size() > 1'});
109
+
110
+ const result = evaluateTriggerFilter({
111
+ subscription,
112
+ source: 'github',
113
+ event: 'push',
114
+ payload: {},
115
+ });
116
+
117
+ expect(result).toEqual({kind: 'filter-error', reason: 'Trigger filter evaluation failed'});
118
+ });
119
+
120
+ test('returns filtered when the stored filter evaluates to a non-boolean value', () => {
121
+ const subscription = subscriptionWithConfig({filter: 'event.ref'});
122
+
123
+ const result = evaluateTriggerFilter({
124
+ subscription,
125
+ source: 'github',
126
+ event: 'push',
127
+ payload: {ref: 'refs/heads/main'},
128
+ });
129
+
130
+ expect(result).toEqual({kind: 'filtered'});
131
+ });
132
+
133
+ test.each([
134
+ {name: 'blank', filter: ' '},
135
+ {name: 'non-string', filter: ['event.ref == "refs/heads/main"']},
136
+ ])('returns filter-error when the stored filter is $name', ({filter}) => {
137
+ const subscription = subscriptionWithConfig({filter});
138
+
139
+ const result = evaluateTriggerFilter({
140
+ subscription,
141
+ source: 'github',
142
+ event: 'push',
143
+ payload: {ref: 'refs/heads/main'},
144
+ });
145
+
146
+ expect(result).toEqual({
147
+ kind: 'filter-error',
148
+ reason: 'Trigger subscription filter must be a non-empty string when set',
149
+ });
150
+ });
151
+ });
152
+
153
+ describe('evaluateStoredFilter', () => {
154
+ const invalidReason = 'Stored filter must be a non-empty string when set';
155
+ const evaluationFailedReason = 'Stored filter evaluation failed';
156
+
157
+ function evaluate(value: unknown, context: Record<string, unknown>) {
158
+ return evaluateStoredFilter({value, context, invalidReason, evaluationFailedReason});
159
+ }
160
+
161
+ test('returns matched when filter is missing', () => {
162
+ const result = evaluate(undefined, {event: {ref: 'refs/heads/main'}});
163
+
164
+ expect(result).toEqual({kind: 'matched'});
165
+ });
166
+
167
+ test('evaluates against the provided context', () => {
168
+ const value = 'event.issue.number == jobs.build.outputs.pr_number';
169
+
170
+ const matches = evaluate(value, {
171
+ event: {issue: {number: 42}},
172
+ jobs: {build: {outputs: {pr_number: 42}}},
173
+ });
174
+ const misses = evaluate(value, {
175
+ event: {issue: {number: 7}},
176
+ jobs: {build: {outputs: {pr_number: 42}}},
177
+ });
178
+
179
+ expect(matches).toEqual({kind: 'matched'});
180
+ expect(misses).toEqual({kind: 'filtered'});
181
+ });
182
+
183
+ test('returns filter-error when the filter cannot be parsed', () => {
184
+ const result = evaluate('event.ref ==', {event: {ref: 'refs/heads/main'}});
185
+
186
+ expect(result.kind).toBe('filter-error');
187
+ if (result.kind !== 'filter-error') throw new Error('expected filter-error');
188
+ expect(result.reason).not.toBe('Invalid workflow expression');
189
+ });
190
+
191
+ test('returns filter-error when evaluation throws', () => {
192
+ const result = evaluate('jobs.build.outputs.pr_number == 42', {
193
+ event: {issue: {number: 42}},
194
+ });
195
+
196
+ expect(result).toEqual({kind: 'filter-error', reason: evaluationFailedReason});
197
+ });
198
+
199
+ test('returns filtered when the stored filter evaluates to a non-boolean value', () => {
200
+ const result = evaluate('event.ref', {event: {ref: 'refs/heads/main'}});
201
+
202
+ expect(result).toEqual({kind: 'filtered'});
203
+ });
204
+
205
+ test.each([
206
+ {name: 'blank', filter: ' '},
207
+ {name: 'non-string', filter: ['event.ref == "refs/heads/main"']},
208
+ ])('returns filter-error when the stored filter is $name', ({filter}) => {
209
+ const result = evaluate(filter, {event: {ref: 'refs/heads/main'}});
210
+
211
+ expect(result).toEqual({kind: 'filter-error', reason: invalidReason});
212
+ });
213
+ });
@@ -0,0 +1,86 @@
1
+ import {
2
+ createWorkflowExpression,
3
+ evaluateWorkflowPredicateFailClosed,
4
+ InvalidWorkflowExpressionError,
5
+ type WorkflowExpression,
6
+ } from '@shipfox/expression';
7
+ import type {TriggerSubscription} from './entities/subscription.js';
8
+
9
+ // Narrow the jsonb projection at the read boundary: the parser writes the right shapes,
10
+ // but the column is unconstrained and these values reach run inputs.
11
+
12
+ export function readConfigInputs(
13
+ subscription: TriggerSubscription,
14
+ ): Record<string, unknown> | undefined {
15
+ const value = subscription.config.with;
16
+ if (value === null || value === undefined) return undefined;
17
+ if (typeof value !== 'object' || Array.isArray(value)) return undefined;
18
+ return value as Record<string, unknown>;
19
+ }
20
+
21
+ export type TriggerFilterEvaluation =
22
+ | {kind: 'matched'}
23
+ | {kind: 'filtered'}
24
+ | {kind: 'filter-error'; reason: string};
25
+
26
+ export type StoredFilterEvaluation = TriggerFilterEvaluation;
27
+
28
+ export interface EvaluateStoredFilterParams {
29
+ value: unknown;
30
+ context: Record<string, unknown>;
31
+ invalidReason: string;
32
+ evaluationFailedReason: string;
33
+ }
34
+
35
+ export function evaluateStoredFilter(params: EvaluateStoredFilterParams): StoredFilterEvaluation {
36
+ const {value} = params;
37
+ if (value === null || value === undefined) return {kind: 'matched'};
38
+ if (typeof value !== 'string' || value.trim() === '') {
39
+ return {
40
+ kind: 'filter-error',
41
+ reason: params.invalidReason,
42
+ };
43
+ }
44
+
45
+ let expression: WorkflowExpression;
46
+ try {
47
+ expression = createWorkflowExpression({source: value, check: {mode: 'syntax'}});
48
+ } catch (error) {
49
+ return {kind: 'filter-error', reason: reasonFrom(error)};
50
+ }
51
+
52
+ const result = evaluateWorkflowPredicateFailClosed(expression, params.context);
53
+
54
+ if (result.evaluationFailed) {
55
+ return {kind: 'filter-error', reason: params.evaluationFailedReason};
56
+ }
57
+
58
+ return result.value ? {kind: 'matched'} : {kind: 'filtered'};
59
+ }
60
+
61
+ export interface EvaluateTriggerFilterParams {
62
+ subscription: TriggerSubscription;
63
+ source: string;
64
+ event: string;
65
+ payload: unknown;
66
+ }
67
+
68
+ export function evaluateTriggerFilter(
69
+ params: EvaluateTriggerFilterParams,
70
+ ): TriggerFilterEvaluation {
71
+ return evaluateStoredFilter({
72
+ value: params.subscription.config.filter,
73
+ context: {
74
+ event: params.payload,
75
+ trigger: {source: params.source, event: params.event},
76
+ },
77
+ invalidReason: 'Trigger subscription filter must be a non-empty string when set',
78
+ evaluationFailedReason: 'Trigger filter evaluation failed',
79
+ });
80
+ }
81
+
82
+ function reasonFrom(error: unknown): string {
83
+ if (error instanceof InvalidWorkflowExpressionError) return error.reason;
84
+ if (error instanceof Error) return error.message;
85
+ return String(error);
86
+ }
@@ -0,0 +1,66 @@
1
+ import {triggerSubscriptionFactory} from '#test/index.js';
2
+
3
+ const runWorkflow = vi.fn();
4
+ const deliverEventToListener = vi.fn();
5
+ const insertReceivedEvent = vi.fn();
6
+
7
+ vi.mock('@shipfox/api-workflows', () => ({
8
+ runWorkflow: (...args: unknown[]) => runWorkflow(...args),
9
+ deliverEventToListener: (...args: unknown[]) => deliverEventToListener(...args),
10
+ isPermanentRunWorkflowError: () => false,
11
+ }));
12
+
13
+ vi.mock('#db/event-history.js', () => ({
14
+ insertReceivedEvent: (...args: unknown[]) => insertReceivedEvent(...args),
15
+ markReceivedEventDiscarded: vi.fn(),
16
+ markReceivedEventRouted: vi.fn(),
17
+ markReceivedEventFailed: vi.fn(),
18
+ markReceivedEventErrored: vi.fn(),
19
+ upsertTriggeredDecision: vi.fn(),
20
+ upsertDispatchErrorDecision: vi.fn(),
21
+ upsertFilterErrorDecision: vi.fn(),
22
+ upsertListenerTriggeredDecision: vi.fn(),
23
+ upsertListenerDispatchErrorDecision: vi.fn(),
24
+ upsertListenerFilterErrorDecision: vi.fn(),
25
+ }));
26
+
27
+ // Import after mocks so the code under test sees the spies.
28
+ const {dispatchIntegrationEvent} = await import('./dispatch-integration-event.js');
29
+
30
+ describe('dispatchIntegrationEvent resilience to history-write failure', () => {
31
+ beforeEach(() => {
32
+ runWorkflow.mockReset();
33
+ deliverEventToListener.mockReset();
34
+ deliverEventToListener.mockResolvedValue({buffered: true, skipped: false});
35
+ insertReceivedEvent.mockReset();
36
+ insertReceivedEvent.mockRejectedValue(new Error('history db down'));
37
+ });
38
+
39
+ test('still fires runWorkflow and does not throw when history recording fails', async () => {
40
+ runWorkflow.mockResolvedValue({id: crypto.randomUUID(), name: 'Build'});
41
+ const workspaceId = crypto.randomUUID();
42
+ await triggerSubscriptionFactory.create({
43
+ workspaceId,
44
+ source: 'github',
45
+ event: 'push',
46
+ config: {},
47
+ });
48
+
49
+ await expect(
50
+ dispatchIntegrationEvent({
51
+ eventRef: crypto.randomUUID(),
52
+ workspaceId,
53
+ provider: 'github',
54
+ source: 'github',
55
+ event: 'push',
56
+ connectionId: crypto.randomUUID(),
57
+ connectionName: 'Acme Production',
58
+ deliveryId: crypto.randomUUID(),
59
+ receivedAt: new Date(),
60
+ payload: {ref: 'main'},
61
+ }),
62
+ ).resolves.toBeUndefined();
63
+
64
+ expect(runWorkflow).toHaveBeenCalledTimes(1);
65
+ });
66
+ });