@shipfox/api-triggers 5.0.0 → 6.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 (86) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +39 -0
  3. package/README.md +23 -3
  4. package/dist/core/dispatch-integration-event.d.ts +2 -0
  5. package/dist/core/dispatch-integration-event.d.ts.map +1 -1
  6. package/dist/core/dispatch-integration-event.js +9 -5
  7. package/dist/core/dispatch-integration-event.js.map +1 -1
  8. package/dist/core/drain-cron-schedules.d.ts +2 -0
  9. package/dist/core/drain-cron-schedules.d.ts.map +1 -1
  10. package/dist/core/drain-cron-schedules.js +1 -0
  11. package/dist/core/drain-cron-schedules.js.map +1 -1
  12. package/dist/core/fire-cron.d.ts +6 -2
  13. package/dist/core/fire-cron.d.ts.map +1 -1
  14. package/dist/core/fire-cron.js +8 -5
  15. package/dist/core/fire-cron.js.map +1 -1
  16. package/dist/core/fire-manual.d.ts +6 -2
  17. package/dist/core/fire-manual.d.ts.map +1 -1
  18. package/dist/core/fire-manual.js +8 -4
  19. package/dist/core/fire-manual.js.map +1 -1
  20. package/dist/core/route-event-to-job-listeners.d.ts +2 -0
  21. package/dist/core/route-event-to-job-listeners.d.ts.map +1 -1
  22. package/dist/core/route-event-to-job-listeners.js +2 -3
  23. package/dist/core/route-event-to-job-listeners.js.map +1 -1
  24. package/dist/core/workflows-client.d.ts +69 -0
  25. package/dist/core/workflows-client.d.ts.map +1 -0
  26. package/dist/core/workflows-client.js +20 -0
  27. package/dist/core/workflows-client.js.map +1 -0
  28. package/dist/index.d.ts +5 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +52 -50
  31. package/dist/index.js.map +1 -1
  32. package/dist/presentation/index.d.ts +2 -2
  33. package/dist/presentation/index.d.ts.map +1 -1
  34. package/dist/presentation/index.js +2 -2
  35. package/dist/presentation/index.js.map +1 -1
  36. package/dist/presentation/routes/fire-manual.d.ts +2 -1
  37. package/dist/presentation/routes/fire-manual.d.ts.map +1 -1
  38. package/dist/presentation/routes/fire-manual.js +61 -58
  39. package/dist/presentation/routes/fire-manual.js.map +1 -1
  40. package/dist/presentation/routes/index.d.ts +2 -1
  41. package/dist/presentation/routes/index.d.ts.map +1 -1
  42. package/dist/presentation/routes/index.js +23 -21
  43. package/dist/presentation/routes/index.js.map +1 -1
  44. package/dist/presentation/subscribers/index.d.ts +1 -1
  45. package/dist/presentation/subscribers/index.d.ts.map +1 -1
  46. package/dist/presentation/subscribers/index.js +1 -1
  47. package/dist/presentation/subscribers/index.js.map +1 -1
  48. package/dist/presentation/subscribers/on-integration-event-received.d.ts +2 -1
  49. package/dist/presentation/subscribers/on-integration-event-received.d.ts.map +1 -1
  50. package/dist/presentation/subscribers/on-integration-event-received.js +16 -13
  51. package/dist/presentation/subscribers/on-integration-event-received.js.map +1 -1
  52. package/dist/temporal/activities/drain-cron-batch.d.ts +2 -1
  53. package/dist/temporal/activities/drain-cron-batch.d.ts.map +1 -1
  54. package/dist/temporal/activities/drain-cron-batch.js +2 -1
  55. package/dist/temporal/activities/drain-cron-batch.js.map +1 -1
  56. package/dist/temporal/activities/index.d.ts +4 -3
  57. package/dist/temporal/activities/index.d.ts.map +1 -1
  58. package/dist/temporal/activities/index.js +2 -2
  59. package/dist/temporal/activities/index.js.map +1 -1
  60. package/dist/tsconfig.test.tsbuildinfo +1 -1
  61. package/package.json +14 -12
  62. package/src/core/dispatch-integration-event.history-failure.test.ts +11 -6
  63. package/src/core/dispatch-integration-event.test.ts +50 -36
  64. package/src/core/dispatch-integration-event.ts +8 -5
  65. package/src/core/drain-cron-schedules.test.ts +30 -29
  66. package/src/core/drain-cron-schedules.ts +3 -0
  67. package/src/core/fire-cron.test.ts +35 -24
  68. package/src/core/fire-cron.ts +9 -7
  69. package/src/core/fire-manual.test.ts +19 -22
  70. package/src/core/fire-manual.ts +9 -6
  71. package/src/core/record-trigger-history.test.ts +3 -5
  72. package/src/core/route-event-to-job-listeners.test.ts +5 -4
  73. package/src/core/route-event-to-job-listeners.ts +5 -4
  74. package/src/core/workflows-client.test.ts +73 -0
  75. package/src/core/workflows-client.ts +32 -0
  76. package/src/index.ts +49 -42
  77. package/src/presentation/index.ts +2 -2
  78. package/src/presentation/routes/fire-manual.test.ts +14 -30
  79. package/src/presentation/routes/fire-manual.ts +62 -54
  80. package/src/presentation/routes/index.ts +18 -15
  81. package/src/presentation/subscribers/index.ts +1 -1
  82. package/src/presentation/subscribers/on-integration-event-received.test.ts +6 -3
  83. package/src/presentation/subscribers/on-integration-event-received.ts +20 -16
  84. package/src/temporal/activities/drain-cron-batch.ts +5 -1
  85. package/src/temporal/activities/index.ts +3 -2
  86. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,6 +1,6 @@
1
1
  $ shipfox-swc && shipfox-temporal-bundle dist/temporal/workflows/index.js
2
- Successfully compiled: 56 files with swc (125.56ms)
3
- 2026-07-19T19:41:38.192Z [INFO] asset workflow-bundle-c6357fb5676e5edf087a.js 1.45 MiB [emitted] [immutable] (name: main)
2
+ Successfully compiled: 57 files with swc (584.37ms)
3
+ 2026-07-21T08:10:32.735Z [INFO] asset workflow-bundle-c6357fb5676e5edf087a.js 1.45 MiB [emitted] [immutable] (name: main)
4
4
  runtime modules 670 bytes 3 modules
5
5
  modules by path ../../../node_modules/.pnpm/ 424 KiB
6
6
  modules by path ../../../node_modules/.pnpm/@temporalio+common@1.18.1/node_modules/@temporal...(truncated) 155 KiB 34 modules
@@ -16,6 +16,6 @@ modules by path ./dist/temporal/workflows/ 3.72 KiB
16
16
  optional modules 30 bytes [optional]
17
17
  __temporal_custom_payload_converter (ignored) 15 bytes [optional] [built] [code generated]
18
18
  __temporal_custom_failure_converter (ignored) 15 bytes [optional] [built] [code generated]
19
- webpack 5.107.2 compiled successfully in 1035 ms
20
- 2026-07-19T19:41:38.196Z [INFO] Workflow bundle created { size: '1.45MB' }
19
+ webpack 5.107.2 compiled successfully in 4048 ms
20
+ 2026-07-21T08:10:32.755Z [INFO] Workflow bundle created { size: '1.45MB' }
21
21
  /home/runner/work/shipfox/shipfox/libs/api/triggers/dist/temporal/workflows/index.js: 1524360 bytes
package/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # @shipfox/api-triggers
2
2
 
3
+ ## 6.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 23563de: Moves Triggers to the injected Workflows inter-module contract with stable run idempotency and listener delivery commands.
8
+
9
+ ### Patch Changes
10
+
11
+ - f73da5d: Enforces bounded API context imports and routes inter-module consumers through producer contracts.
12
+ - Updated dependencies [a8f0545]
13
+ - Updated dependencies [0bb82a4]
14
+ - Updated dependencies [23563de]
15
+ - Updated dependencies [54ce48b]
16
+ - Updated dependencies [f4bc2eb]
17
+ - Updated dependencies [c0162b0]
18
+ - Updated dependencies [7ac43a4]
19
+ - Updated dependencies [f262539]
20
+ - Updated dependencies [a01e917]
21
+ - Updated dependencies [3bb4e26]
22
+ - Updated dependencies [8bdc149]
23
+ - Updated dependencies [f73da5d]
24
+ - Updated dependencies [3810996]
25
+ - Updated dependencies [23a4dc2]
26
+ - Updated dependencies [b00ed29]
27
+ - Updated dependencies [8aa7cd3]
28
+ - Updated dependencies [81f9544]
29
+ - Updated dependencies [4604a06]
30
+ - @shipfox/api-definitions-dto@6.0.0
31
+ - @shipfox/api-integration-core-dto@6.0.0
32
+ - @shipfox/api-projects@6.0.0
33
+ - @shipfox/api-workflows-dto@6.0.0
34
+ - @shipfox/node-module@0.4.0
35
+ - @shipfox/node-temporal@0.3.2
36
+ - @shipfox/node-drizzle@0.3.2
37
+ - @shipfox/node-outbox@0.2.4
38
+ - @shipfox/api-auth-context@6.0.0
39
+ - @shipfox/node-fastify@0.2.4
40
+ - @shipfox/inter-module@0.2.0
41
+
3
42
  ## 5.0.0
4
43
 
5
44
  ### Patch Changes
package/README.md CHANGED
@@ -11,12 +11,22 @@ runs and inspect received trigger events.
11
11
  Register the module with the API module runner:
12
12
 
13
13
  ```ts
14
- import {triggersModule} from '@shipfox/api-triggers';
14
+ import {createTriggersModule} from '@shipfox/api-triggers';
15
+ import {workflowsInterModuleContract} from '@shipfox/api-workflows-dto/inter-module';
15
16
  import {createApp, listen} from '@shipfox/node-fastify';
16
17
  import {initializeModules, startModuleWorkers} from '@shipfox/node-module';
17
-
18
+ import {
19
+ createInMemoryInterModuleTransport,
20
+ registerInterModulePresentations,
21
+ } from '@shipfox/node-module/inter-module';
22
+
23
+ const transport = createInMemoryInterModuleTransport();
24
+ const workflows = transport.createClient(workflowsInterModuleContract);
25
+ const modules = [createTriggersModule({workflows}) /* and other modules */];
26
+ registerInterModulePresentations({transport, modules});
27
+ transport.seal();
18
28
  const {auth, routes, workers} = await initializeModules({
19
- modules: [triggersModule /* and other modules */],
29
+ modules,
20
30
  });
21
31
 
22
32
  await createApp({auth, routes});
@@ -24,6 +34,16 @@ await startModuleWorkers({workers});
24
34
  await listen();
25
35
  ```
26
36
 
37
+ ### Migration from `triggersModule`
38
+
39
+ `triggersModule` is replaced by `createTriggersModule({workflows})`. The API
40
+ composition root creates the Workflows client from
41
+ `@shipfox/api-workflows-dto/inter-module`, passes it to Triggers, registers the
42
+ Workflows presentation, and seals the transport before the server starts. Cron
43
+ activities, integration subscribers, and the manual route use that one injected
44
+ client. Callers must keep the deterministic cron or integration key when they
45
+ retry a trigger command. The manual route creates one new key for each request.
46
+
27
47
  This adds:
28
48
 
29
49
  - triggers database migrations from `libs/api/triggers/drizzle`
@@ -1,4 +1,6 @@
1
+ import { type WorkflowsModuleClient } from './workflows-client.js';
1
2
  export interface DispatchIntegrationEventParams {
3
+ workflows: WorkflowsModuleClient;
2
4
  eventRef: string;
3
5
  workspaceId: string;
4
6
  provider: string;
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch-integration-event.d.ts","sourceRoot":"","sources":["../../src/core/dispatch-integration-event.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,IAAI,CAAC;CAClB;AAcD,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,8BAA8B,GACrC,OAAO,CAAC,IAAI,CAAC,CA+Gf"}
1
+ {"version":3,"file":"dispatch-integration-event.d.ts","sourceRoot":"","sources":["../../src/core/dispatch-integration-event.ts"],"names":[],"mappings":"AASA,OAAO,EAA2B,KAAK,qBAAqB,EAAC,MAAM,uBAAuB,CAAC;AAE3F,MAAM,WAAW,8BAA8B;IAC7C,SAAS,EAAE,qBAAqB,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,IAAI,CAAC;CAClB;AAcD,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,8BAA8B,GACrC,OAAO,CAAC,IAAI,CAAC,CAiHf"}
@@ -1,9 +1,9 @@
1
- import { isPermanentRunWorkflowError, runWorkflow } from '@shipfox/api-workflows';
2
1
  import { findMatchingSubscriptions } from '#db/subscriptions.js';
3
2
  import { eventOutcomeCount, eventReceivedCount, subscriptionTriggeredCount } from '#metrics/instance.js';
4
3
  import { evaluateTriggerFilter, readConfigInputs } from './config.js';
5
4
  import { beginTriggerHistory, toReason } from './record-trigger-history.js';
6
5
  import { routeEventToJobListeners } from './route-event-to-job-listeners.js';
6
+ import { isPermanentStartRunError } from './workflows-client.js';
7
7
  // Source-agnostic dispatcher: any inbound integration event fans out to every
8
8
  // workspace subscription registered for its (source, event) and to every listening
9
9
  // job subscribed to it, passing the raw payload through untouched. The module knows
@@ -56,8 +56,9 @@ export async function dispatchIntegrationEvent(params) {
56
56
  continue;
57
57
  }
58
58
  triggerEngagedCount += 1;
59
+ const inputs = readConfigInputs(subscription);
59
60
  try {
60
- const run = await runWorkflow({
61
+ const run = await params.workflows.startRunFromTrigger({
61
62
  workspaceId: subscription.workspaceId,
62
63
  projectId: subscription.projectId,
63
64
  definitionId: subscription.workflowDefinitionId,
@@ -68,8 +69,10 @@ export async function dispatchIntegrationEvent(params) {
68
69
  deliveryId: params.deliveryId,
69
70
  data: params.payload
70
71
  },
71
- inputs: readConfigInputs(subscription),
72
- triggerIdempotencyKey: `${subscription.id}:${params.eventRef}`
72
+ ...inputs === undefined ? {} : {
73
+ inputs
74
+ },
75
+ idempotencyKey: `${subscription.id}:${params.eventRef}`
73
76
  });
74
77
  await history.triggered(subscription, run);
75
78
  triggeredCount += 1;
@@ -80,13 +83,14 @@ export async function dispatchIntegrationEvent(params) {
80
83
  await history.dispatchErrored(subscription, toReason(error));
81
84
  // Track presence with a flag, not `firstTransientError === undefined`: a thrown
82
85
  // value of `undefined` is still a transient failure and must drive the replay.
83
- if (!isPermanentRunWorkflowError(error) && !sawTransientError) {
86
+ if (!isPermanentStartRunError(error) && !sawTransientError) {
84
87
  sawTransientError = true;
85
88
  firstTransientError = error;
86
89
  }
87
90
  }
88
91
  }
89
92
  const listenerResult = await routeEventToJobListeners({
93
+ workflows: params.workflows,
90
94
  history,
91
95
  eventRef: params.eventRef,
92
96
  workspaceId: params.workspaceId,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/dispatch-integration-event.ts"],"sourcesContent":["import {isPermanentRunWorkflowError, runWorkflow} from '@shipfox/api-workflows';\nimport {findMatchingSubscriptions} from '#db/subscriptions.js';\nimport {\n eventOutcomeCount,\n eventReceivedCount,\n subscriptionTriggeredCount,\n} from '#metrics/instance.js';\nimport {evaluateTriggerFilter, readConfigInputs} from './config.js';\nimport {beginTriggerHistory, toReason} from './record-trigger-history.js';\nimport {routeEventToJobListeners} from './route-event-to-job-listeners.js';\n\nexport interface DispatchIntegrationEventParams {\n eventRef: string;\n workspaceId: string;\n provider: string;\n source: string;\n event: string;\n deliveryId: string;\n connectionId: string;\n connectionName: string | null;\n payload: unknown;\n receivedAt: Date;\n}\n\n// Source-agnostic dispatcher: any inbound integration event fans out to every\n// workspace subscription registered for its (source, event) and to every listening\n// job subscribed to it, passing the raw payload through untouched. The module knows\n// nothing about github, gitlab, etc.\n//\n// Continue-on-error: every matched subscription and listener is attempted so one broken\n// subscription cannot starve its siblings. A permanent failure (deleted definition, project\n// mismatch) is recorded and skipped; a transient one is recorded and re-thrown so the outbox\n// replays the whole event and converges (succeeded siblings dedup on the idempotency key). The\n// event reaches a terminal outcome only when no transient error remains: `routed` if any run\n// was created or any listening job matched, `discarded` if nothing matched, otherwise `errored`.\n// History is best-effort; the thrown transient error, not the recorded outcome, drives the retry.\nexport async function dispatchIntegrationEvent(\n params: DispatchIntegrationEventParams,\n): Promise<void> {\n eventReceivedCount.add(1, {provider: params.provider});\n\n const history = await beginTriggerHistory({\n eventRef: params.eventRef,\n origin: 'integration',\n workspaceId: params.workspaceId,\n provider: params.provider,\n source: params.source,\n event: params.event,\n deliveryId: params.deliveryId,\n connectionId: params.connectionId,\n connectionName: params.connectionName,\n payload: (params.payload ?? null) as Record<string, unknown> | null,\n receivedAt: params.receivedAt,\n });\n\n const subscriptions = await findMatchingSubscriptions({\n workspaceId: params.workspaceId,\n source: params.source,\n event: params.event,\n });\n\n let triggeredCount = 0;\n let triggerEngagedCount = 0;\n let sawTransientError = false;\n let firstTransientError: unknown;\n\n for (const subscription of subscriptions) {\n const filterResult = evaluateTriggerFilter({\n subscription,\n source: params.source,\n event: params.event,\n payload: params.payload,\n });\n if (filterResult.kind === 'filtered') continue;\n if (filterResult.kind === 'filter-error') {\n triggerEngagedCount += 1;\n await history.filterErrored(subscription, filterResult.reason);\n continue;\n }\n triggerEngagedCount += 1;\n\n try {\n const run = await runWorkflow({\n workspaceId: subscription.workspaceId,\n projectId: subscription.projectId,\n definitionId: subscription.workflowDefinitionId,\n triggerPayload: {\n provider: params.provider,\n source: params.source,\n event: params.event,\n deliveryId: params.deliveryId,\n data: params.payload,\n },\n inputs: readConfigInputs(subscription),\n triggerIdempotencyKey: `${subscription.id}:${params.eventRef}`,\n });\n await history.triggered(subscription, run);\n triggeredCount += 1;\n subscriptionTriggeredCount.add(1, {provider: params.provider});\n } catch (error) {\n await history.dispatchErrored(subscription, toReason(error));\n // Track presence with a flag, not `firstTransientError === undefined`: a thrown\n // value of `undefined` is still a transient failure and must drive the replay.\n if (!isPermanentRunWorkflowError(error) && !sawTransientError) {\n sawTransientError = true;\n firstTransientError = error;\n }\n }\n }\n\n const listenerResult = await routeEventToJobListeners({\n history,\n eventRef: params.eventRef,\n workspaceId: params.workspaceId,\n provider: params.provider,\n source: params.source,\n event: params.event,\n deliveryId: params.deliveryId,\n payload: params.payload,\n receivedAt: params.receivedAt,\n });\n\n if (listenerResult.transientErrored && !sawTransientError) {\n sawTransientError = true;\n firstTransientError = listenerResult.transientError;\n }\n\n const totalMatchedCount = triggerEngagedCount + listenerResult.engagedCount;\n\n if (sawTransientError) {\n eventOutcomeCount.add(1, {provider: params.provider, outcome: 'failed'});\n await history.failed(totalMatchedCount);\n throw firstTransientError;\n }\n\n if (triggeredCount > 0 || listenerResult.acceptedJobCount > 0) {\n eventOutcomeCount.add(1, {provider: params.provider, outcome: 'routed'});\n await history.routed(totalMatchedCount);\n return;\n }\n\n if (totalMatchedCount === 0) {\n eventOutcomeCount.add(1, {provider: params.provider, outcome: 'discarded'});\n await history.discarded();\n return;\n }\n\n eventOutcomeCount.add(1, {provider: params.provider, outcome: 'errored'});\n await history.allErrored(totalMatchedCount);\n}\n"],"names":["isPermanentRunWorkflowError","runWorkflow","findMatchingSubscriptions","eventOutcomeCount","eventReceivedCount","subscriptionTriggeredCount","evaluateTriggerFilter","readConfigInputs","beginTriggerHistory","toReason","routeEventToJobListeners","dispatchIntegrationEvent","params","add","provider","history","eventRef","origin","workspaceId","source","event","deliveryId","connectionId","connectionName","payload","receivedAt","subscriptions","triggeredCount","triggerEngagedCount","sawTransientError","firstTransientError","subscription","filterResult","kind","filterErrored","reason","run","projectId","definitionId","workflowDefinitionId","triggerPayload","data","inputs","triggerIdempotencyKey","id","triggered","error","dispatchErrored","listenerResult","transientErrored","transientError","totalMatchedCount","engagedCount","outcome","failed","acceptedJobCount","routed","discarded","allErrored"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/core/dispatch-integration-event.ts"],"sourcesContent":["import {findMatchingSubscriptions} from '#db/subscriptions.js';\nimport {\n eventOutcomeCount,\n eventReceivedCount,\n subscriptionTriggeredCount,\n} from '#metrics/instance.js';\nimport {evaluateTriggerFilter, readConfigInputs} from './config.js';\nimport {beginTriggerHistory, toReason} from './record-trigger-history.js';\nimport {routeEventToJobListeners} from './route-event-to-job-listeners.js';\nimport {isPermanentStartRunError, type WorkflowsModuleClient} from './workflows-client.js';\n\nexport interface DispatchIntegrationEventParams {\n workflows: WorkflowsModuleClient;\n eventRef: string;\n workspaceId: string;\n provider: string;\n source: string;\n event: string;\n deliveryId: string;\n connectionId: string;\n connectionName: string | null;\n payload: unknown;\n receivedAt: Date;\n}\n\n// Source-agnostic dispatcher: any inbound integration event fans out to every\n// workspace subscription registered for its (source, event) and to every listening\n// job subscribed to it, passing the raw payload through untouched. The module knows\n// nothing about github, gitlab, etc.\n//\n// Continue-on-error: every matched subscription and listener is attempted so one broken\n// subscription cannot starve its siblings. A permanent failure (deleted definition, project\n// mismatch) is recorded and skipped; a transient one is recorded and re-thrown so the outbox\n// replays the whole event and converges (succeeded siblings dedup on the idempotency key). The\n// event reaches a terminal outcome only when no transient error remains: `routed` if any run\n// was created or any listening job matched, `discarded` if nothing matched, otherwise `errored`.\n// History is best-effort; the thrown transient error, not the recorded outcome, drives the retry.\nexport async function dispatchIntegrationEvent(\n params: DispatchIntegrationEventParams,\n): Promise<void> {\n eventReceivedCount.add(1, {provider: params.provider});\n\n const history = await beginTriggerHistory({\n eventRef: params.eventRef,\n origin: 'integration',\n workspaceId: params.workspaceId,\n provider: params.provider,\n source: params.source,\n event: params.event,\n deliveryId: params.deliveryId,\n connectionId: params.connectionId,\n connectionName: params.connectionName,\n payload: (params.payload ?? null) as Record<string, unknown> | null,\n receivedAt: params.receivedAt,\n });\n\n const subscriptions = await findMatchingSubscriptions({\n workspaceId: params.workspaceId,\n source: params.source,\n event: params.event,\n });\n\n let triggeredCount = 0;\n let triggerEngagedCount = 0;\n let sawTransientError = false;\n let firstTransientError: unknown;\n\n for (const subscription of subscriptions) {\n const filterResult = evaluateTriggerFilter({\n subscription,\n source: params.source,\n event: params.event,\n payload: params.payload,\n });\n if (filterResult.kind === 'filtered') continue;\n if (filterResult.kind === 'filter-error') {\n triggerEngagedCount += 1;\n await history.filterErrored(subscription, filterResult.reason);\n continue;\n }\n triggerEngagedCount += 1;\n\n const inputs = readConfigInputs(subscription);\n try {\n const run = await params.workflows.startRunFromTrigger({\n workspaceId: subscription.workspaceId,\n projectId: subscription.projectId,\n definitionId: subscription.workflowDefinitionId,\n triggerPayload: {\n provider: params.provider,\n source: params.source,\n event: params.event,\n deliveryId: params.deliveryId,\n data: params.payload,\n },\n ...(inputs === undefined ? {} : {inputs}),\n idempotencyKey: `${subscription.id}:${params.eventRef}`,\n });\n await history.triggered(subscription, run);\n triggeredCount += 1;\n subscriptionTriggeredCount.add(1, {provider: params.provider});\n } catch (error) {\n await history.dispatchErrored(subscription, toReason(error));\n // Track presence with a flag, not `firstTransientError === undefined`: a thrown\n // value of `undefined` is still a transient failure and must drive the replay.\n if (!isPermanentStartRunError(error) && !sawTransientError) {\n sawTransientError = true;\n firstTransientError = error;\n }\n }\n }\n\n const listenerResult = await routeEventToJobListeners({\n workflows: params.workflows,\n history,\n eventRef: params.eventRef,\n workspaceId: params.workspaceId,\n provider: params.provider,\n source: params.source,\n event: params.event,\n deliveryId: params.deliveryId,\n payload: params.payload,\n receivedAt: params.receivedAt,\n });\n\n if (listenerResult.transientErrored && !sawTransientError) {\n sawTransientError = true;\n firstTransientError = listenerResult.transientError;\n }\n\n const totalMatchedCount = triggerEngagedCount + listenerResult.engagedCount;\n\n if (sawTransientError) {\n eventOutcomeCount.add(1, {provider: params.provider, outcome: 'failed'});\n await history.failed(totalMatchedCount);\n throw firstTransientError;\n }\n\n if (triggeredCount > 0 || listenerResult.acceptedJobCount > 0) {\n eventOutcomeCount.add(1, {provider: params.provider, outcome: 'routed'});\n await history.routed(totalMatchedCount);\n return;\n }\n\n if (totalMatchedCount === 0) {\n eventOutcomeCount.add(1, {provider: params.provider, outcome: 'discarded'});\n await history.discarded();\n return;\n }\n\n eventOutcomeCount.add(1, {provider: params.provider, outcome: 'errored'});\n await history.allErrored(totalMatchedCount);\n}\n"],"names":["findMatchingSubscriptions","eventOutcomeCount","eventReceivedCount","subscriptionTriggeredCount","evaluateTriggerFilter","readConfigInputs","beginTriggerHistory","toReason","routeEventToJobListeners","isPermanentStartRunError","dispatchIntegrationEvent","params","add","provider","history","eventRef","origin","workspaceId","source","event","deliveryId","connectionId","connectionName","payload","receivedAt","subscriptions","triggeredCount","triggerEngagedCount","sawTransientError","firstTransientError","subscription","filterResult","kind","filterErrored","reason","inputs","run","workflows","startRunFromTrigger","projectId","definitionId","workflowDefinitionId","triggerPayload","data","undefined","idempotencyKey","id","triggered","error","dispatchErrored","listenerResult","transientErrored","transientError","totalMatchedCount","engagedCount","outcome","failed","acceptedJobCount","routed","discarded","allErrored"],"mappings":"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"}
@@ -1,4 +1,6 @@
1
+ import type { WorkflowsModuleClient } from './workflows-client.js';
1
2
  export interface DrainDueCronSchedulesParams {
3
+ readonly workflows: WorkflowsModuleClient;
2
4
  readonly batchSize: number;
3
5
  readonly jitterWindowSeconds: number;
4
6
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"drain-cron-schedules.d.ts","sourceRoot":"","sources":["../../src/core/drain-cron-schedules.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;CACzD;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,gBAAgB,CAAC,CAmD3B"}
1
+ {"version":3,"file":"drain-cron-schedules.d.ts","sourceRoot":"","sources":["../../src/core/drain-cron-schedules.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,uBAAuB,CAAC;AAEjE,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,SAAS,EAAE,qBAAqB,CAAC;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC;;;;OAIG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;CACzD;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,2BAA2B,GAClC,OAAO,CAAC,gBAAgB,CAAC,CAoD3B"}
@@ -47,6 +47,7 @@ import { fireCronSubscription } from './fire-cron.js';
47
47
  lastFiredAt: scheduledSlot
48
48
  });
49
49
  return await fireCronSubscription({
50
+ workflows: params.workflows,
50
51
  subscriptionId: schedule.subscriptionId,
51
52
  scheduledSlot
52
53
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/drain-cron-schedules.ts"],"sourcesContent":["import {logger} from '@shipfox/node-opentelemetry';\nimport {advanceCronSchedule, claimDueCronSchedules, selectDbNow} from '#db/cron-schedules.js';\nimport {db} from '#db/db.js';\nimport {computeNextFireAt} from './compute-next-fire-at.js';\nimport {fireCronSubscription} from './fire-cron.js';\n\nexport interface DrainDueCronSchedulesParams {\n readonly batchSize: number;\n readonly jitterWindowSeconds: number;\n /**\n * Liveness signal invoked once per processed schedule (e.g. the Temporal activity\n * heartbeat), so a batch stuck on a slow `runWorkflow` is visible rather than\n * looking hung until the start-to-close timeout fires.\n */\n readonly onScheduleProcessed?: (() => void) | undefined;\n}\n\nexport interface CronDrainSummary {\n readonly claimed: number;\n readonly fired: number;\n readonly errored: number;\n readonly retried: number;\n}\n\n/**\n * Claims one bounded batch of due cron schedules and, per row, advances the schedule\n * and fires the workflow. There is no internal drain loop, so the per-activity ceiling\n * stays a hard `batchSize`.\n *\n * The batch is claimed and processed inside a single transaction, so its `FOR UPDATE\n * SKIP LOCKED` locks are held until commit and concurrent activities/pods claim disjoint\n * rows. Each row runs in a nested savepoint that advances `next_fire_at` before firing:\n * on a transient failure the savepoint rolls back only that row's advance, leaving it due\n * for retry, without blocking the rest of the batch. Firing before the advance commits\n * makes a crash between the two re-fire the (idempotent) occurrence rather than skip it.\n */\nexport async function drainDueCronSchedules(\n params: DrainDueCronSchedulesParams,\n): Promise<CronDrainSummary> {\n let claimed = 0;\n let fired = 0;\n let errored = 0;\n let retried = 0;\n\n await db().transaction(async (tx) => {\n // One database-clock reading shared by the due check and every advance, so the two\n // never disagree under application/database clock skew.\n const now = await selectDbNow(tx);\n const due = await claimDueCronSchedules({tx, limit: params.batchSize, now});\n claimed = due.length;\n\n for (const schedule of due) {\n const scheduledSlot = schedule.nextFireAt;\n try {\n const result = await tx.transaction(async (rowTx) => {\n const nextFireAt = computeNextFireAt({\n cronExpression: schedule.cronExpression,\n timezone: schedule.timezone,\n from: now,\n subscriptionId: schedule.subscriptionId,\n jitterWindowSeconds: params.jitterWindowSeconds,\n });\n await advanceCronSchedule({\n tx: rowTx,\n subscriptionId: schedule.subscriptionId,\n nextFireAt,\n lastFiredAt: scheduledSlot,\n });\n return await fireCronSubscription({\n subscriptionId: schedule.subscriptionId,\n scheduledSlot,\n });\n });\n if (result.outcome === 'fired') fired += 1;\n else errored += 1;\n } catch (error) {\n // Transient failure: the savepoint rolled back the advance, so the schedule\n // stays due and the next tick (or an activity retry) re-claims it.\n retried += 1;\n logger().warn(\n {err: error, subscriptionId: schedule.subscriptionId},\n 'cron drain: transient fire failure; schedule left due for retry',\n );\n }\n params.onScheduleProcessed?.();\n }\n });\n\n return {claimed, fired, errored, retried};\n}\n"],"names":["logger","advanceCronSchedule","claimDueCronSchedules","selectDbNow","db","computeNextFireAt","fireCronSubscription","drainDueCronSchedules","params","claimed","fired","errored","retried","transaction","tx","now","due","limit","batchSize","length","schedule","scheduledSlot","nextFireAt","result","rowTx","cronExpression","timezone","from","subscriptionId","jitterWindowSeconds","lastFiredAt","outcome","error","warn","err","onScheduleProcessed"],"mappings":"AAAA,SAAQA,MAAM,QAAO,8BAA8B;AACnD,SAAQC,mBAAmB,EAAEC,qBAAqB,EAAEC,WAAW,QAAO,wBAAwB;AAC9F,SAAQC,EAAE,QAAO,YAAY;AAC7B,SAAQC,iBAAiB,QAAO,4BAA4B;AAC5D,SAAQC,oBAAoB,QAAO,iBAAiB;AAoBpD;;;;;;;;;;;CAWC,GACD,OAAO,eAAeC,sBACpBC,MAAmC;IAEnC,IAAIC,UAAU;IACd,IAAIC,QAAQ;IACZ,IAAIC,UAAU;IACd,IAAIC,UAAU;IAEd,MAAMR,KAAKS,WAAW,CAAC,OAAOC;QAC5B,mFAAmF;QACnF,wDAAwD;QACxD,MAAMC,MAAM,MAAMZ,YAAYW;QAC9B,MAAME,MAAM,MAAMd,sBAAsB;YAACY;YAAIG,OAAOT,OAAOU,SAAS;YAAEH;QAAG;QACzEN,UAAUO,IAAIG,MAAM;QAEpB,KAAK,MAAMC,YAAYJ,IAAK;YAC1B,MAAMK,gBAAgBD,SAASE,UAAU;YACzC,IAAI;gBACF,MAAMC,SAAS,MAAMT,GAAGD,WAAW,CAAC,OAAOW;oBACzC,MAAMF,aAAajB,kBAAkB;wBACnCoB,gBAAgBL,SAASK,cAAc;wBACvCC,UAAUN,SAASM,QAAQ;wBAC3BC,MAAMZ;wBACNa,gBAAgBR,SAASQ,cAAc;wBACvCC,qBAAqBrB,OAAOqB,mBAAmB;oBACjD;oBACA,MAAM5B,oBAAoB;wBACxBa,IAAIU;wBACJI,gBAAgBR,SAASQ,cAAc;wBACvCN;wBACAQ,aAAaT;oBACf;oBACA,OAAO,MAAMf,qBAAqB;wBAChCsB,gBAAgBR,SAASQ,cAAc;wBACvCP;oBACF;gBACF;gBACA,IAAIE,OAAOQ,OAAO,KAAK,SAASrB,SAAS;qBACpCC,WAAW;YAClB,EAAE,OAAOqB,OAAO;gBACd,4EAA4E;gBAC5E,mEAAmE;gBACnEpB,WAAW;gBACXZ,SAASiC,IAAI,CACX;oBAACC,KAAKF;oBAAOJ,gBAAgBR,SAASQ,cAAc;gBAAA,GACpD;YAEJ;YACApB,OAAO2B,mBAAmB;QAC5B;IACF;IAEA,OAAO;QAAC1B;QAASC;QAAOC;QAASC;IAAO;AAC1C"}
1
+ {"version":3,"sources":["../../src/core/drain-cron-schedules.ts"],"sourcesContent":["import {logger} from '@shipfox/node-opentelemetry';\nimport {advanceCronSchedule, claimDueCronSchedules, selectDbNow} from '#db/cron-schedules.js';\nimport {db} from '#db/db.js';\nimport {computeNextFireAt} from './compute-next-fire-at.js';\nimport {fireCronSubscription} from './fire-cron.js';\nimport type {WorkflowsModuleClient} from './workflows-client.js';\n\nexport interface DrainDueCronSchedulesParams {\n readonly workflows: WorkflowsModuleClient;\n readonly batchSize: number;\n readonly jitterWindowSeconds: number;\n /**\n * Liveness signal invoked once per processed schedule (e.g. the Temporal activity\n * heartbeat), so a batch stuck on a slow `runWorkflow` is visible rather than\n * looking hung until the start-to-close timeout fires.\n */\n readonly onScheduleProcessed?: (() => void) | undefined;\n}\n\nexport interface CronDrainSummary {\n readonly claimed: number;\n readonly fired: number;\n readonly errored: number;\n readonly retried: number;\n}\n\n/**\n * Claims one bounded batch of due cron schedules and, per row, advances the schedule\n * and fires the workflow. There is no internal drain loop, so the per-activity ceiling\n * stays a hard `batchSize`.\n *\n * The batch is claimed and processed inside a single transaction, so its `FOR UPDATE\n * SKIP LOCKED` locks are held until commit and concurrent activities/pods claim disjoint\n * rows. Each row runs in a nested savepoint that advances `next_fire_at` before firing:\n * on a transient failure the savepoint rolls back only that row's advance, leaving it due\n * for retry, without blocking the rest of the batch. Firing before the advance commits\n * makes a crash between the two re-fire the (idempotent) occurrence rather than skip it.\n */\nexport async function drainDueCronSchedules(\n params: DrainDueCronSchedulesParams,\n): Promise<CronDrainSummary> {\n let claimed = 0;\n let fired = 0;\n let errored = 0;\n let retried = 0;\n\n await db().transaction(async (tx) => {\n // One database-clock reading shared by the due check and every advance, so the two\n // never disagree under application/database clock skew.\n const now = await selectDbNow(tx);\n const due = await claimDueCronSchedules({tx, limit: params.batchSize, now});\n claimed = due.length;\n\n for (const schedule of due) {\n const scheduledSlot = schedule.nextFireAt;\n try {\n const result = await tx.transaction(async (rowTx) => {\n const nextFireAt = computeNextFireAt({\n cronExpression: schedule.cronExpression,\n timezone: schedule.timezone,\n from: now,\n subscriptionId: schedule.subscriptionId,\n jitterWindowSeconds: params.jitterWindowSeconds,\n });\n await advanceCronSchedule({\n tx: rowTx,\n subscriptionId: schedule.subscriptionId,\n nextFireAt,\n lastFiredAt: scheduledSlot,\n });\n return await fireCronSubscription({\n workflows: params.workflows,\n subscriptionId: schedule.subscriptionId,\n scheduledSlot,\n });\n });\n if (result.outcome === 'fired') fired += 1;\n else errored += 1;\n } catch (error) {\n // Transient failure: the savepoint rolled back the advance, so the schedule\n // stays due and the next tick (or an activity retry) re-claims it.\n retried += 1;\n logger().warn(\n {err: error, subscriptionId: schedule.subscriptionId},\n 'cron drain: transient fire failure; schedule left due for retry',\n );\n }\n params.onScheduleProcessed?.();\n }\n });\n\n return {claimed, fired, errored, retried};\n}\n"],"names":["logger","advanceCronSchedule","claimDueCronSchedules","selectDbNow","db","computeNextFireAt","fireCronSubscription","drainDueCronSchedules","params","claimed","fired","errored","retried","transaction","tx","now","due","limit","batchSize","length","schedule","scheduledSlot","nextFireAt","result","rowTx","cronExpression","timezone","from","subscriptionId","jitterWindowSeconds","lastFiredAt","workflows","outcome","error","warn","err","onScheduleProcessed"],"mappings":"AAAA,SAAQA,MAAM,QAAO,8BAA8B;AACnD,SAAQC,mBAAmB,EAAEC,qBAAqB,EAAEC,WAAW,QAAO,wBAAwB;AAC9F,SAAQC,EAAE,QAAO,YAAY;AAC7B,SAAQC,iBAAiB,QAAO,4BAA4B;AAC5D,SAAQC,oBAAoB,QAAO,iBAAiB;AAsBpD;;;;;;;;;;;CAWC,GACD,OAAO,eAAeC,sBACpBC,MAAmC;IAEnC,IAAIC,UAAU;IACd,IAAIC,QAAQ;IACZ,IAAIC,UAAU;IACd,IAAIC,UAAU;IAEd,MAAMR,KAAKS,WAAW,CAAC,OAAOC;QAC5B,mFAAmF;QACnF,wDAAwD;QACxD,MAAMC,MAAM,MAAMZ,YAAYW;QAC9B,MAAME,MAAM,MAAMd,sBAAsB;YAACY;YAAIG,OAAOT,OAAOU,SAAS;YAAEH;QAAG;QACzEN,UAAUO,IAAIG,MAAM;QAEpB,KAAK,MAAMC,YAAYJ,IAAK;YAC1B,MAAMK,gBAAgBD,SAASE,UAAU;YACzC,IAAI;gBACF,MAAMC,SAAS,MAAMT,GAAGD,WAAW,CAAC,OAAOW;oBACzC,MAAMF,aAAajB,kBAAkB;wBACnCoB,gBAAgBL,SAASK,cAAc;wBACvCC,UAAUN,SAASM,QAAQ;wBAC3BC,MAAMZ;wBACNa,gBAAgBR,SAASQ,cAAc;wBACvCC,qBAAqBrB,OAAOqB,mBAAmB;oBACjD;oBACA,MAAM5B,oBAAoB;wBACxBa,IAAIU;wBACJI,gBAAgBR,SAASQ,cAAc;wBACvCN;wBACAQ,aAAaT;oBACf;oBACA,OAAO,MAAMf,qBAAqB;wBAChCyB,WAAWvB,OAAOuB,SAAS;wBAC3BH,gBAAgBR,SAASQ,cAAc;wBACvCP;oBACF;gBACF;gBACA,IAAIE,OAAOS,OAAO,KAAK,SAAStB,SAAS;qBACpCC,WAAW;YAClB,EAAE,OAAOsB,OAAO;gBACd,4EAA4E;gBAC5E,mEAAmE;gBACnErB,WAAW;gBACXZ,SAASkC,IAAI,CACX;oBAACC,KAAKF;oBAAOL,gBAAgBR,SAASQ,cAAc;gBAAA,GACpD;YAEJ;YACApB,OAAO4B,mBAAmB;QAC5B;IACF;IAEA,OAAO;QAAC3B;QAASC;QAAOC;QAASC;IAAO;AAC1C"}
@@ -1,5 +1,6 @@
1
- import { type WorkflowRun } from '@shipfox/api-workflows';
1
+ import { type WorkflowsModuleClient } from './workflows-client.js';
2
2
  export interface FireCronSubscriptionParams {
3
+ workflows: WorkflowsModuleClient;
3
4
  subscriptionId: string;
4
5
  /**
5
6
  * The `next_fire_at` value the schedule held when it was claimed. Anchoring the
@@ -10,7 +11,10 @@ export interface FireCronSubscriptionParams {
10
11
  }
11
12
  export type FireCronSubscriptionResult = {
12
13
  outcome: 'fired';
13
- run: WorkflowRun;
14
+ run: {
15
+ id: string;
16
+ name: string;
17
+ };
14
18
  } | {
15
19
  outcome: 'errored';
16
20
  };
@@ -1 +1 @@
1
- {"version":3,"file":"fire-cron.d.ts","sourceRoot":"","sources":["../../src/core/fire-cron.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2C,KAAK,WAAW,EAAC,MAAM,wBAAwB,CAAC;AAOlG,MAAM,WAAW,0BAA0B;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,aAAa,EAAE,IAAI,CAAC;CACrB;AAED,MAAM,MAAM,0BAA0B,GAClC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,WAAW,CAAA;CAAC,GACpC;IAAC,OAAO,EAAE,SAAS,CAAA;CAAC,CAAC;AAEzB;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,0BAA0B,GACjC,OAAO,CAAC,0BAA0B,CAAC,CA2DrC"}
1
+ {"version":3,"file":"fire-cron.d.ts","sourceRoot":"","sources":["../../src/core/fire-cron.ts"],"names":[],"mappings":"AAKA,OAAO,EAA2B,KAAK,qBAAqB,EAAC,MAAM,uBAAuB,CAAC;AAE3F,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,qBAAqB,CAAC;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,aAAa,EAAE,IAAI,CAAC;CACrB;AAED,MAAM,MAAM,0BAA0B,GAClC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE;QAAC,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,CAAA;CAAC,GACnD;IAAC,OAAO,EAAE,SAAS,CAAA;CAAC,CAAC;AAEzB;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,0BAA0B,GACjC,OAAO,CAAC,0BAA0B,CAAC,CA4DrC"}
@@ -1,9 +1,9 @@
1
- import { isPermanentRunWorkflowError, runWorkflow } from '@shipfox/api-workflows';
2
1
  import { getTriggerSubscriptionById } from '#db/subscriptions.js';
3
2
  import { cronFiredCount, cronFireLag } from '#metrics/instance.js';
4
3
  import { readConfigInputs } from './config.js';
5
4
  import { TriggerSubscriptionNotCronError, TriggerSubscriptionNotFoundError } from './errors.js';
6
5
  import { beginTriggerHistory, toReason } from './record-trigger-history.js';
6
+ import { isPermanentStartRunError } from './workflows-client.js';
7
7
  /**
8
8
  * Fires one cron schedule occurrence. Mirrors `fireManualSubscription`, but is driven
9
9
  * by the drain activity rather than an HTTP route, and reports the outcome instead of
@@ -36,9 +36,10 @@ import { beginTriggerHistory, toReason } from './record-trigger-history.js';
36
36
  receivedAt: params.scheduledSlot,
37
37
  eventRef
38
38
  };
39
+ const inputs = readConfigInputs(subscription);
39
40
  let run;
40
41
  try {
41
- run = await runWorkflow({
42
+ run = await params.workflows.startRunFromTrigger({
42
43
  workspaceId: subscription.workspaceId,
43
44
  projectId: subscription.projectId,
44
45
  definitionId: subscription.workflowDefinitionId,
@@ -48,13 +49,15 @@ import { beginTriggerHistory, toReason } from './record-trigger-history.js';
48
49
  event: 'tick',
49
50
  scheduleId: subscription.id
50
51
  },
51
- inputs: readConfigInputs(subscription),
52
- triggerIdempotencyKey: eventRef
52
+ ...inputs === undefined ? {} : {
53
+ inputs
54
+ },
55
+ idempotencyKey: eventRef
53
56
  });
54
57
  } catch (error) {
55
58
  const failure = await beginTriggerHistory(historyBase);
56
59
  await failure.dispatchErrored(subscription, toReason(error));
57
- if (isPermanentRunWorkflowError(error)) {
60
+ if (isPermanentStartRunError(error)) {
58
61
  recordFire('errored', params.scheduledSlot);
59
62
  await failure.allErrored(1);
60
63
  return {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/fire-cron.ts"],"sourcesContent":["import {isPermanentRunWorkflowError, runWorkflow, type WorkflowRun} from '@shipfox/api-workflows';\nimport {getTriggerSubscriptionById} from '#db/subscriptions.js';\nimport {cronFiredCount, cronFireLag} from '#metrics/instance.js';\nimport {readConfigInputs} from './config.js';\nimport {TriggerSubscriptionNotCronError, TriggerSubscriptionNotFoundError} from './errors.js';\nimport {beginTriggerHistory, toReason} from './record-trigger-history.js';\n\nexport interface FireCronSubscriptionParams {\n subscriptionId: string;\n /**\n * The `next_fire_at` value the schedule held when it was claimed. Anchoring the\n * idempotency key and history `event_ref` on this slot, rather than wall-clock\n * time, makes a retry of the same occurrence a deterministic no-op.\n */\n scheduledSlot: Date;\n}\n\nexport type FireCronSubscriptionResult =\n | {outcome: 'fired'; run: WorkflowRun}\n | {outcome: 'errored'};\n\n/**\n * Fires one cron schedule occurrence. Mirrors `fireManualSubscription`, but is driven\n * by the drain activity rather than an HTTP route, and reports the outcome instead of\n * throwing on a permanent failure so the caller can advance the schedule past a\n * permanently broken definition:\n *\n * - `{outcome: 'fired'}` on success (or an idempotent replay of an already-created run),\n * - `{outcome: 'errored'}` on a permanent `runWorkflow` failure (recorded, schedule advances),\n * - throws on a transient failure so the caller rolls back the advance and retries.\n */\nexport async function fireCronSubscription(\n params: FireCronSubscriptionParams,\n): Promise<FireCronSubscriptionResult> {\n const subscription = await getTriggerSubscriptionById(params.subscriptionId);\n if (!subscription) throw new TriggerSubscriptionNotFoundError(params.subscriptionId);\n if (subscription.source !== 'cron') {\n throw new TriggerSubscriptionNotCronError(params.subscriptionId, subscription.source);\n }\n\n // Deterministic per-occurrence identity: unique across (subscription, slot), stable\n // across retries. Used for both the run idempotency key and the received-event ref.\n const eventRef = `${subscription.id}:${params.scheduledSlot.toISOString()}`;\n const historyBase = {\n origin: 'cron' as const,\n workspaceId: subscription.workspaceId,\n provider: null,\n source: subscription.source,\n event: subscription.event,\n deliveryId: null,\n connectionId: null,\n connectionName: null,\n payload: null,\n // The scheduled occurrence is the logical arrival time of the tick event.\n receivedAt: params.scheduledSlot,\n eventRef,\n };\n\n let run: WorkflowRun;\n try {\n run = await runWorkflow({\n workspaceId: subscription.workspaceId,\n projectId: subscription.projectId,\n definitionId: subscription.workflowDefinitionId,\n triggerPayload: {\n provider: 'cron',\n source: 'cron',\n event: 'tick',\n scheduleId: subscription.id,\n },\n inputs: readConfigInputs(subscription),\n triggerIdempotencyKey: eventRef,\n });\n } catch (error) {\n const failure = await beginTriggerHistory(historyBase);\n await failure.dispatchErrored(subscription, toReason(error));\n if (isPermanentRunWorkflowError(error)) {\n recordFire('errored', params.scheduledSlot);\n await failure.allErrored(1);\n return {outcome: 'errored'};\n }\n // Transient: leave the event non-terminal and re-throw so the caller rolls back\n // the advance and the schedule stays due for the next tick.\n await failure.failed(1);\n throw error;\n }\n\n const history = await beginTriggerHistory(historyBase);\n await history.triggered(subscription, run);\n recordFire('fired', params.scheduledSlot);\n await history.routed(1);\n return {outcome: 'fired', run};\n}\n\nfunction recordFire(outcome: 'fired' | 'errored', scheduledSlot: Date): void {\n cronFiredCount.add(1, {outcome});\n cronFireLag.record(Math.max(0, Date.now() - scheduledSlot.getTime()));\n}\n"],"names":["isPermanentRunWorkflowError","runWorkflow","getTriggerSubscriptionById","cronFiredCount","cronFireLag","readConfigInputs","TriggerSubscriptionNotCronError","TriggerSubscriptionNotFoundError","beginTriggerHistory","toReason","fireCronSubscription","params","subscription","subscriptionId","source","eventRef","id","scheduledSlot","toISOString","historyBase","origin","workspaceId","provider","event","deliveryId","connectionId","connectionName","payload","receivedAt","run","projectId","definitionId","workflowDefinitionId","triggerPayload","scheduleId","inputs","triggerIdempotencyKey","error","failure","dispatchErrored","recordFire","allErrored","outcome","failed","history","triggered","routed","add","record","Math","max","Date","now","getTime"],"mappings":"AAAA,SAAQA,2BAA2B,EAAEC,WAAW,QAAyB,yBAAyB;AAClG,SAAQC,0BAA0B,QAAO,uBAAuB;AAChE,SAAQC,cAAc,EAAEC,WAAW,QAAO,uBAAuB;AACjE,SAAQC,gBAAgB,QAAO,cAAc;AAC7C,SAAQC,+BAA+B,EAAEC,gCAAgC,QAAO,cAAc;AAC9F,SAAQC,mBAAmB,EAAEC,QAAQ,QAAO,8BAA8B;AAgB1E;;;;;;;;;CASC,GACD,OAAO,eAAeC,qBACpBC,MAAkC;IAElC,MAAMC,eAAe,MAAMV,2BAA2BS,OAAOE,cAAc;IAC3E,IAAI,CAACD,cAAc,MAAM,IAAIL,iCAAiCI,OAAOE,cAAc;IACnF,IAAID,aAAaE,MAAM,KAAK,QAAQ;QAClC,MAAM,IAAIR,gCAAgCK,OAAOE,cAAc,EAAED,aAAaE,MAAM;IACtF;IAEA,oFAAoF;IACpF,oFAAoF;IACpF,MAAMC,WAAW,GAAGH,aAAaI,EAAE,CAAC,CAAC,EAAEL,OAAOM,aAAa,CAACC,WAAW,IAAI;IAC3E,MAAMC,cAAc;QAClBC,QAAQ;QACRC,aAAaT,aAAaS,WAAW;QACrCC,UAAU;QACVR,QAAQF,aAAaE,MAAM;QAC3BS,OAAOX,aAAaW,KAAK;QACzBC,YAAY;QACZC,cAAc;QACdC,gBAAgB;QAChBC,SAAS;QACT,0EAA0E;QAC1EC,YAAYjB,OAAOM,aAAa;QAChCF;IACF;IAEA,IAAIc;IACJ,IAAI;QACFA,MAAM,MAAM5B,YAAY;YACtBoB,aAAaT,aAAaS,WAAW;YACrCS,WAAWlB,aAAakB,SAAS;YACjCC,cAAcnB,aAAaoB,oBAAoB;YAC/CC,gBAAgB;gBACdX,UAAU;gBACVR,QAAQ;gBACRS,OAAO;gBACPW,YAAYtB,aAAaI,EAAE;YAC7B;YACAmB,QAAQ9B,iBAAiBO;YACzBwB,uBAAuBrB;QACzB;IACF,EAAE,OAAOsB,OAAO;QACd,MAAMC,UAAU,MAAM9B,oBAAoBW;QAC1C,MAAMmB,QAAQC,eAAe,CAAC3B,cAAcH,SAAS4B;QACrD,IAAIrC,4BAA4BqC,QAAQ;YACtCG,WAAW,WAAW7B,OAAOM,aAAa;YAC1C,MAAMqB,QAAQG,UAAU,CAAC;YACzB,OAAO;gBAACC,SAAS;YAAS;QAC5B;QACA,gFAAgF;QAChF,4DAA4D;QAC5D,MAAMJ,QAAQK,MAAM,CAAC;QACrB,MAAMN;IACR;IAEA,MAAMO,UAAU,MAAMpC,oBAAoBW;IAC1C,MAAMyB,QAAQC,SAAS,CAACjC,cAAciB;IACtCW,WAAW,SAAS7B,OAAOM,aAAa;IACxC,MAAM2B,QAAQE,MAAM,CAAC;IACrB,OAAO;QAACJ,SAAS;QAASb;IAAG;AAC/B;AAEA,SAASW,WAAWE,OAA4B,EAAEzB,aAAmB;IACnEd,eAAe4C,GAAG,CAAC,GAAG;QAACL;IAAO;IAC9BtC,YAAY4C,MAAM,CAACC,KAAKC,GAAG,CAAC,GAAGC,KAAKC,GAAG,KAAKnC,cAAcoC,OAAO;AACnE"}
1
+ {"version":3,"sources":["../../src/core/fire-cron.ts"],"sourcesContent":["import {getTriggerSubscriptionById} from '#db/subscriptions.js';\nimport {cronFiredCount, cronFireLag} from '#metrics/instance.js';\nimport {readConfigInputs} from './config.js';\nimport {TriggerSubscriptionNotCronError, TriggerSubscriptionNotFoundError} from './errors.js';\nimport {beginTriggerHistory, toReason} from './record-trigger-history.js';\nimport {isPermanentStartRunError, type WorkflowsModuleClient} from './workflows-client.js';\n\nexport interface FireCronSubscriptionParams {\n workflows: WorkflowsModuleClient;\n subscriptionId: string;\n /**\n * The `next_fire_at` value the schedule held when it was claimed. Anchoring the\n * idempotency key and history `event_ref` on this slot, rather than wall-clock\n * time, makes a retry of the same occurrence a deterministic no-op.\n */\n scheduledSlot: Date;\n}\n\nexport type FireCronSubscriptionResult =\n | {outcome: 'fired'; run: {id: string; name: string}}\n | {outcome: 'errored'};\n\n/**\n * Fires one cron schedule occurrence. Mirrors `fireManualSubscription`, but is driven\n * by the drain activity rather than an HTTP route, and reports the outcome instead of\n * throwing on a permanent failure so the caller can advance the schedule past a\n * permanently broken definition:\n *\n * - `{outcome: 'fired'}` on success (or an idempotent replay of an already-created run),\n * - `{outcome: 'errored'}` on a permanent `runWorkflow` failure (recorded, schedule advances),\n * - throws on a transient failure so the caller rolls back the advance and retries.\n */\nexport async function fireCronSubscription(\n params: FireCronSubscriptionParams,\n): Promise<FireCronSubscriptionResult> {\n const subscription = await getTriggerSubscriptionById(params.subscriptionId);\n if (!subscription) throw new TriggerSubscriptionNotFoundError(params.subscriptionId);\n if (subscription.source !== 'cron') {\n throw new TriggerSubscriptionNotCronError(params.subscriptionId, subscription.source);\n }\n\n // Deterministic per-occurrence identity: unique across (subscription, slot), stable\n // across retries. Used for both the run idempotency key and the received-event ref.\n const eventRef = `${subscription.id}:${params.scheduledSlot.toISOString()}`;\n const historyBase = {\n origin: 'cron' as const,\n workspaceId: subscription.workspaceId,\n provider: null,\n source: subscription.source,\n event: subscription.event,\n deliveryId: null,\n connectionId: null,\n connectionName: null,\n payload: null,\n // The scheduled occurrence is the logical arrival time of the tick event.\n receivedAt: params.scheduledSlot,\n eventRef,\n };\n\n const inputs = readConfigInputs(subscription);\n let run: {id: string; name: string};\n try {\n run = await params.workflows.startRunFromTrigger({\n workspaceId: subscription.workspaceId,\n projectId: subscription.projectId,\n definitionId: subscription.workflowDefinitionId,\n triggerPayload: {\n provider: 'cron',\n source: 'cron',\n event: 'tick',\n scheduleId: subscription.id,\n },\n ...(inputs === undefined ? {} : {inputs}),\n idempotencyKey: eventRef,\n });\n } catch (error) {\n const failure = await beginTriggerHistory(historyBase);\n await failure.dispatchErrored(subscription, toReason(error));\n if (isPermanentStartRunError(error)) {\n recordFire('errored', params.scheduledSlot);\n await failure.allErrored(1);\n return {outcome: 'errored'};\n }\n // Transient: leave the event non-terminal and re-throw so the caller rolls back\n // the advance and the schedule stays due for the next tick.\n await failure.failed(1);\n throw error;\n }\n\n const history = await beginTriggerHistory(historyBase);\n await history.triggered(subscription, run);\n recordFire('fired', params.scheduledSlot);\n await history.routed(1);\n return {outcome: 'fired', run};\n}\n\nfunction recordFire(outcome: 'fired' | 'errored', scheduledSlot: Date): void {\n cronFiredCount.add(1, {outcome});\n cronFireLag.record(Math.max(0, Date.now() - scheduledSlot.getTime()));\n}\n"],"names":["getTriggerSubscriptionById","cronFiredCount","cronFireLag","readConfigInputs","TriggerSubscriptionNotCronError","TriggerSubscriptionNotFoundError","beginTriggerHistory","toReason","isPermanentStartRunError","fireCronSubscription","params","subscription","subscriptionId","source","eventRef","id","scheduledSlot","toISOString","historyBase","origin","workspaceId","provider","event","deliveryId","connectionId","connectionName","payload","receivedAt","inputs","run","workflows","startRunFromTrigger","projectId","definitionId","workflowDefinitionId","triggerPayload","scheduleId","undefined","idempotencyKey","error","failure","dispatchErrored","recordFire","allErrored","outcome","failed","history","triggered","routed","add","record","Math","max","Date","now","getTime"],"mappings":"AAAA,SAAQA,0BAA0B,QAAO,uBAAuB;AAChE,SAAQC,cAAc,EAAEC,WAAW,QAAO,uBAAuB;AACjE,SAAQC,gBAAgB,QAAO,cAAc;AAC7C,SAAQC,+BAA+B,EAAEC,gCAAgC,QAAO,cAAc;AAC9F,SAAQC,mBAAmB,EAAEC,QAAQ,QAAO,8BAA8B;AAC1E,SAAQC,wBAAwB,QAAmC,wBAAwB;AAiB3F;;;;;;;;;CASC,GACD,OAAO,eAAeC,qBACpBC,MAAkC;IAElC,MAAMC,eAAe,MAAMX,2BAA2BU,OAAOE,cAAc;IAC3E,IAAI,CAACD,cAAc,MAAM,IAAIN,iCAAiCK,OAAOE,cAAc;IACnF,IAAID,aAAaE,MAAM,KAAK,QAAQ;QAClC,MAAM,IAAIT,gCAAgCM,OAAOE,cAAc,EAAED,aAAaE,MAAM;IACtF;IAEA,oFAAoF;IACpF,oFAAoF;IACpF,MAAMC,WAAW,GAAGH,aAAaI,EAAE,CAAC,CAAC,EAAEL,OAAOM,aAAa,CAACC,WAAW,IAAI;IAC3E,MAAMC,cAAc;QAClBC,QAAQ;QACRC,aAAaT,aAAaS,WAAW;QACrCC,UAAU;QACVR,QAAQF,aAAaE,MAAM;QAC3BS,OAAOX,aAAaW,KAAK;QACzBC,YAAY;QACZC,cAAc;QACdC,gBAAgB;QAChBC,SAAS;QACT,0EAA0E;QAC1EC,YAAYjB,OAAOM,aAAa;QAChCF;IACF;IAEA,MAAMc,SAASzB,iBAAiBQ;IAChC,IAAIkB;IACJ,IAAI;QACFA,MAAM,MAAMnB,OAAOoB,SAAS,CAACC,mBAAmB,CAAC;YAC/CX,aAAaT,aAAaS,WAAW;YACrCY,WAAWrB,aAAaqB,SAAS;YACjCC,cAActB,aAAauB,oBAAoB;YAC/CC,gBAAgB;gBACdd,UAAU;gBACVR,QAAQ;gBACRS,OAAO;gBACPc,YAAYzB,aAAaI,EAAE;YAC7B;YACA,GAAIa,WAAWS,YAAY,CAAC,IAAI;gBAACT;YAAM,CAAC;YACxCU,gBAAgBxB;QAClB;IACF,EAAE,OAAOyB,OAAO;QACd,MAAMC,UAAU,MAAMlC,oBAAoBY;QAC1C,MAAMsB,QAAQC,eAAe,CAAC9B,cAAcJ,SAASgC;QACrD,IAAI/B,yBAAyB+B,QAAQ;YACnCG,WAAW,WAAWhC,OAAOM,aAAa;YAC1C,MAAMwB,QAAQG,UAAU,CAAC;YACzB,OAAO;gBAACC,SAAS;YAAS;QAC5B;QACA,gFAAgF;QAChF,4DAA4D;QAC5D,MAAMJ,QAAQK,MAAM,CAAC;QACrB,MAAMN;IACR;IAEA,MAAMO,UAAU,MAAMxC,oBAAoBY;IAC1C,MAAM4B,QAAQC,SAAS,CAACpC,cAAckB;IACtCa,WAAW,SAAShC,OAAOM,aAAa;IACxC,MAAM8B,QAAQE,MAAM,CAAC;IACrB,OAAO;QAACJ,SAAS;QAASf;IAAG;AAC/B;AAEA,SAASa,WAAWE,OAA4B,EAAE5B,aAAmB;IACnEf,eAAegD,GAAG,CAAC,GAAG;QAACL;IAAO;IAC9B1C,YAAYgD,MAAM,CAACC,KAAKC,GAAG,CAAC,GAAGC,KAAKC,GAAG,KAAKtC,cAAcuC,OAAO;AACnE"}
@@ -1,9 +1,13 @@
1
- import { type WorkflowRun } from '@shipfox/api-workflows';
1
+ import { type WorkflowsModuleClient } from './workflows-client.js';
2
2
  export interface FireManualSubscriptionParams {
3
+ workflows: WorkflowsModuleClient;
3
4
  subscriptionId: string;
4
5
  callerWorkspaceId: string;
5
6
  userId: string;
6
7
  inputs?: Record<string, unknown> | undefined;
7
8
  }
8
- export declare function fireManualSubscription(params: FireManualSubscriptionParams): Promise<WorkflowRun>;
9
+ export declare function fireManualSubscription(params: FireManualSubscriptionParams): Promise<{
10
+ id: string;
11
+ name: string;
12
+ }>;
9
13
  //# sourceMappingURL=fire-manual.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fire-manual.d.ts","sourceRoot":"","sources":["../../src/core/fire-manual.ts"],"names":[],"mappings":"AACA,OAAO,EAA2C,KAAK,WAAW,EAAC,MAAM,wBAAwB,CAAC;AAelG,MAAM,WAAW,4BAA4B;IAC3C,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;CAC9C;AAED,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,4BAA4B,GACnC,OAAO,CAAC,WAAW,CAAC,CAkEtB"}
1
+ {"version":3,"file":"fire-manual.d.ts","sourceRoot":"","sources":["../../src/core/fire-manual.ts"],"names":[],"mappings":"AAcA,OAAO,EAA2B,KAAK,qBAAqB,EAAC,MAAM,uBAAuB,CAAC;AAE3F,MAAM,WAAW,4BAA4B;IAC3C,SAAS,EAAE,qBAAqB,CAAC;IACjC,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;CAC9C;AAED,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,4BAA4B,GACnC,OAAO,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,CAAC,CAoErC"}
@@ -1,10 +1,10 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { isPermanentRunWorkflowError, runWorkflow } from '@shipfox/api-workflows';
3
2
  import { getTriggerSubscriptionById } from '#db/subscriptions.js';
4
3
  import { eventOutcomeCount, eventReceivedCount, subscriptionTriggeredCount } from '#metrics/instance.js';
5
4
  import { readConfigInputs } from './config.js';
6
5
  import { TriggerSubscriptionNotFoundError, TriggerSubscriptionNotManualError, TriggerWorkspaceMismatchError } from './errors.js';
7
6
  import { beginTriggerHistory, toReason } from './record-trigger-history.js';
7
+ import { isPermanentStartRunError } from './workflows-client.js';
8
8
  export async function fireManualSubscription(params) {
9
9
  const subscription = await getTriggerSubscriptionById(params.subscriptionId);
10
10
  if (!subscription) throw new TriggerSubscriptionNotFoundError(params.subscriptionId);
@@ -32,9 +32,10 @@ export async function fireManualSubscription(params) {
32
32
  eventReceivedCount.add(1, {
33
33
  provider: 'manual'
34
34
  });
35
+ const inputs = params.inputs ?? readConfigInputs(subscription);
35
36
  let run;
36
37
  try {
37
- run = await runWorkflow({
38
+ run = await params.workflows.startRunFromTrigger({
38
39
  workspaceId: subscription.workspaceId,
39
40
  projectId: subscription.projectId,
40
41
  definitionId: subscription.workflowDefinitionId,
@@ -45,7 +46,10 @@ export async function fireManualSubscription(params) {
45
46
  subscriptionId: subscription.id,
46
47
  userId: params.userId
47
48
  },
48
- inputs: params.inputs ?? readConfigInputs(subscription)
49
+ ...inputs === undefined ? {} : {
50
+ inputs
51
+ },
52
+ idempotencyKey: randomUUID()
49
53
  });
50
54
  } catch (error) {
51
55
  const failure = await beginTriggerHistory({
@@ -53,7 +57,7 @@ export async function fireManualSubscription(params) {
53
57
  eventRef: randomUUID()
54
58
  });
55
59
  await failure.dispatchErrored(subscription, toReason(error));
56
- if (isPermanentRunWorkflowError(error)) {
60
+ if (isPermanentStartRunError(error)) {
57
61
  eventOutcomeCount.add(1, {
58
62
  provider: 'manual',
59
63
  outcome: 'errored'
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/fire-manual.ts"],"sourcesContent":["import {randomUUID} from 'node:crypto';\nimport {isPermanentRunWorkflowError, runWorkflow, type WorkflowRun} from '@shipfox/api-workflows';\nimport {getTriggerSubscriptionById} from '#db/subscriptions.js';\nimport {\n eventOutcomeCount,\n eventReceivedCount,\n subscriptionTriggeredCount,\n} from '#metrics/instance.js';\nimport {readConfigInputs} from './config.js';\nimport {\n TriggerSubscriptionNotFoundError,\n TriggerSubscriptionNotManualError,\n TriggerWorkspaceMismatchError,\n} from './errors.js';\nimport {beginTriggerHistory, toReason} from './record-trigger-history.js';\n\nexport interface FireManualSubscriptionParams {\n subscriptionId: string;\n callerWorkspaceId: string;\n userId: string;\n inputs?: Record<string, unknown> | undefined;\n}\n\nexport async function fireManualSubscription(\n params: FireManualSubscriptionParams,\n): Promise<WorkflowRun> {\n const subscription = await getTriggerSubscriptionById(params.subscriptionId);\n if (!subscription) throw new TriggerSubscriptionNotFoundError(params.subscriptionId);\n if (subscription.source !== 'manual') {\n throw new TriggerSubscriptionNotManualError(params.subscriptionId, subscription.source);\n }\n // Defence in depth: unreachable from the HTTP route, but required for any direct caller.\n if (subscription.workspaceId !== params.callerWorkspaceId) {\n throw new TriggerWorkspaceMismatchError(\n params.subscriptionId,\n subscription.workspaceId,\n params.callerWorkspaceId,\n );\n }\n\n // Manual fires have no upstream event id. Use the run id after success; failed\n // attempts need a synthesized ref because there is no run to key on.\n const historyBase = {\n origin: 'manual' as const,\n workspaceId: subscription.workspaceId,\n provider: null,\n source: subscription.source,\n event: subscription.event,\n deliveryId: null,\n connectionId: null,\n connectionName: null,\n payload: null,\n receivedAt: new Date(),\n };\n\n eventReceivedCount.add(1, {provider: 'manual'});\n\n let run: WorkflowRun;\n try {\n run = await runWorkflow({\n workspaceId: subscription.workspaceId,\n projectId: subscription.projectId,\n definitionId: subscription.workflowDefinitionId,\n triggerPayload: {\n provider: 'manual',\n source: 'manual',\n event: 'fire',\n subscriptionId: subscription.id,\n userId: params.userId,\n },\n inputs: params.inputs ?? readConfigInputs(subscription),\n });\n } catch (error) {\n const failure = await beginTriggerHistory({...historyBase, eventRef: randomUUID()});\n await failure.dispatchErrored(subscription, toReason(error));\n if (isPermanentRunWorkflowError(error)) {\n eventOutcomeCount.add(1, {provider: 'manual', outcome: 'errored'});\n await failure.allErrored(1);\n } else {\n eventOutcomeCount.add(1, {provider: 'manual', outcome: 'failed'});\n await failure.failed(1);\n }\n throw error;\n }\n\n const history = await beginTriggerHistory({...historyBase, eventRef: run.id});\n await history.triggered(subscription, run);\n subscriptionTriggeredCount.add(1, {provider: 'manual'});\n eventOutcomeCount.add(1, {provider: 'manual', outcome: 'routed'});\n await history.routed(1);\n return run;\n}\n"],"names":["randomUUID","isPermanentRunWorkflowError","runWorkflow","getTriggerSubscriptionById","eventOutcomeCount","eventReceivedCount","subscriptionTriggeredCount","readConfigInputs","TriggerSubscriptionNotFoundError","TriggerSubscriptionNotManualError","TriggerWorkspaceMismatchError","beginTriggerHistory","toReason","fireManualSubscription","params","subscription","subscriptionId","source","workspaceId","callerWorkspaceId","historyBase","origin","provider","event","deliveryId","connectionId","connectionName","payload","receivedAt","Date","add","run","projectId","definitionId","workflowDefinitionId","triggerPayload","id","userId","inputs","error","failure","eventRef","dispatchErrored","outcome","allErrored","failed","history","triggered","routed"],"mappings":"AAAA,SAAQA,UAAU,QAAO,cAAc;AACvC,SAAQC,2BAA2B,EAAEC,WAAW,QAAyB,yBAAyB;AAClG,SAAQC,0BAA0B,QAAO,uBAAuB;AAChE,SACEC,iBAAiB,EACjBC,kBAAkB,EAClBC,0BAA0B,QACrB,uBAAuB;AAC9B,SAAQC,gBAAgB,QAAO,cAAc;AAC7C,SACEC,gCAAgC,EAChCC,iCAAiC,EACjCC,6BAA6B,QACxB,cAAc;AACrB,SAAQC,mBAAmB,EAAEC,QAAQ,QAAO,8BAA8B;AAS1E,OAAO,eAAeC,uBACpBC,MAAoC;IAEpC,MAAMC,eAAe,MAAMZ,2BAA2BW,OAAOE,cAAc;IAC3E,IAAI,CAACD,cAAc,MAAM,IAAIP,iCAAiCM,OAAOE,cAAc;IACnF,IAAID,aAAaE,MAAM,KAAK,UAAU;QACpC,MAAM,IAAIR,kCAAkCK,OAAOE,cAAc,EAAED,aAAaE,MAAM;IACxF;IACA,yFAAyF;IACzF,IAAIF,aAAaG,WAAW,KAAKJ,OAAOK,iBAAiB,EAAE;QACzD,MAAM,IAAIT,8BACRI,OAAOE,cAAc,EACrBD,aAAaG,WAAW,EACxBJ,OAAOK,iBAAiB;IAE5B;IAEA,+EAA+E;IAC/E,qEAAqE;IACrE,MAAMC,cAAc;QAClBC,QAAQ;QACRH,aAAaH,aAAaG,WAAW;QACrCI,UAAU;QACVL,QAAQF,aAAaE,MAAM;QAC3BM,OAAOR,aAAaQ,KAAK;QACzBC,YAAY;QACZC,cAAc;QACdC,gBAAgB;QAChBC,SAAS;QACTC,YAAY,IAAIC;IAClB;IAEAxB,mBAAmByB,GAAG,CAAC,GAAG;QAACR,UAAU;IAAQ;IAE7C,IAAIS;IACJ,IAAI;QACFA,MAAM,MAAM7B,YAAY;YACtBgB,aAAaH,aAAaG,WAAW;YACrCc,WAAWjB,aAAaiB,SAAS;YACjCC,cAAclB,aAAamB,oBAAoB;YAC/CC,gBAAgB;gBACdb,UAAU;gBACVL,QAAQ;gBACRM,OAAO;gBACPP,gBAAgBD,aAAaqB,EAAE;gBAC/BC,QAAQvB,OAAOuB,MAAM;YACvB;YACAC,QAAQxB,OAAOwB,MAAM,IAAI/B,iBAAiBQ;QAC5C;IACF,EAAE,OAAOwB,OAAO;QACd,MAAMC,UAAU,MAAM7B,oBAAoB;YAAC,GAAGS,WAAW;YAAEqB,UAAUzC;QAAY;QACjF,MAAMwC,QAAQE,eAAe,CAAC3B,cAAcH,SAAS2B;QACrD,IAAItC,4BAA4BsC,QAAQ;YACtCnC,kBAAkB0B,GAAG,CAAC,GAAG;gBAACR,UAAU;gBAAUqB,SAAS;YAAS;YAChE,MAAMH,QAAQI,UAAU,CAAC;QAC3B,OAAO;YACLxC,kBAAkB0B,GAAG,CAAC,GAAG;gBAACR,UAAU;gBAAUqB,SAAS;YAAQ;YAC/D,MAAMH,QAAQK,MAAM,CAAC;QACvB;QACA,MAAMN;IACR;IAEA,MAAMO,UAAU,MAAMnC,oBAAoB;QAAC,GAAGS,WAAW;QAAEqB,UAAUV,IAAIK,EAAE;IAAA;IAC3E,MAAMU,QAAQC,SAAS,CAAChC,cAAcgB;IACtCzB,2BAA2BwB,GAAG,CAAC,GAAG;QAACR,UAAU;IAAQ;IACrDlB,kBAAkB0B,GAAG,CAAC,GAAG;QAACR,UAAU;QAAUqB,SAAS;IAAQ;IAC/D,MAAMG,QAAQE,MAAM,CAAC;IACrB,OAAOjB;AACT"}
1
+ {"version":3,"sources":["../../src/core/fire-manual.ts"],"sourcesContent":["import {randomUUID} from 'node:crypto';\nimport {getTriggerSubscriptionById} from '#db/subscriptions.js';\nimport {\n eventOutcomeCount,\n eventReceivedCount,\n subscriptionTriggeredCount,\n} from '#metrics/instance.js';\nimport {readConfigInputs} from './config.js';\nimport {\n TriggerSubscriptionNotFoundError,\n TriggerSubscriptionNotManualError,\n TriggerWorkspaceMismatchError,\n} from './errors.js';\nimport {beginTriggerHistory, toReason} from './record-trigger-history.js';\nimport {isPermanentStartRunError, type WorkflowsModuleClient} from './workflows-client.js';\n\nexport interface FireManualSubscriptionParams {\n workflows: WorkflowsModuleClient;\n subscriptionId: string;\n callerWorkspaceId: string;\n userId: string;\n inputs?: Record<string, unknown> | undefined;\n}\n\nexport async function fireManualSubscription(\n params: FireManualSubscriptionParams,\n): Promise<{id: string; name: string}> {\n const subscription = await getTriggerSubscriptionById(params.subscriptionId);\n if (!subscription) throw new TriggerSubscriptionNotFoundError(params.subscriptionId);\n if (subscription.source !== 'manual') {\n throw new TriggerSubscriptionNotManualError(params.subscriptionId, subscription.source);\n }\n // Defence in depth: unreachable from the HTTP route, but required for any direct caller.\n if (subscription.workspaceId !== params.callerWorkspaceId) {\n throw new TriggerWorkspaceMismatchError(\n params.subscriptionId,\n subscription.workspaceId,\n params.callerWorkspaceId,\n );\n }\n\n // Manual fires have no upstream event id. Use the run id after success; failed\n // attempts need a synthesized ref because there is no run to key on.\n const historyBase = {\n origin: 'manual' as const,\n workspaceId: subscription.workspaceId,\n provider: null,\n source: subscription.source,\n event: subscription.event,\n deliveryId: null,\n connectionId: null,\n connectionName: null,\n payload: null,\n receivedAt: new Date(),\n };\n\n eventReceivedCount.add(1, {provider: 'manual'});\n\n const inputs = params.inputs ?? readConfigInputs(subscription);\n let run: {id: string; name: string};\n try {\n run = await params.workflows.startRunFromTrigger({\n workspaceId: subscription.workspaceId,\n projectId: subscription.projectId,\n definitionId: subscription.workflowDefinitionId,\n triggerPayload: {\n provider: 'manual',\n source: 'manual',\n event: 'fire',\n subscriptionId: subscription.id,\n userId: params.userId,\n },\n ...(inputs === undefined ? {} : {inputs}),\n idempotencyKey: randomUUID(),\n });\n } catch (error) {\n const failure = await beginTriggerHistory({...historyBase, eventRef: randomUUID()});\n await failure.dispatchErrored(subscription, toReason(error));\n if (isPermanentStartRunError(error)) {\n eventOutcomeCount.add(1, {provider: 'manual', outcome: 'errored'});\n await failure.allErrored(1);\n } else {\n eventOutcomeCount.add(1, {provider: 'manual', outcome: 'failed'});\n await failure.failed(1);\n }\n throw error;\n }\n\n const history = await beginTriggerHistory({...historyBase, eventRef: run.id});\n await history.triggered(subscription, run);\n subscriptionTriggeredCount.add(1, {provider: 'manual'});\n eventOutcomeCount.add(1, {provider: 'manual', outcome: 'routed'});\n await history.routed(1);\n return run;\n}\n"],"names":["randomUUID","getTriggerSubscriptionById","eventOutcomeCount","eventReceivedCount","subscriptionTriggeredCount","readConfigInputs","TriggerSubscriptionNotFoundError","TriggerSubscriptionNotManualError","TriggerWorkspaceMismatchError","beginTriggerHistory","toReason","isPermanentStartRunError","fireManualSubscription","params","subscription","subscriptionId","source","workspaceId","callerWorkspaceId","historyBase","origin","provider","event","deliveryId","connectionId","connectionName","payload","receivedAt","Date","add","inputs","run","workflows","startRunFromTrigger","projectId","definitionId","workflowDefinitionId","triggerPayload","id","userId","undefined","idempotencyKey","error","failure","eventRef","dispatchErrored","outcome","allErrored","failed","history","triggered","routed"],"mappings":"AAAA,SAAQA,UAAU,QAAO,cAAc;AACvC,SAAQC,0BAA0B,QAAO,uBAAuB;AAChE,SACEC,iBAAiB,EACjBC,kBAAkB,EAClBC,0BAA0B,QACrB,uBAAuB;AAC9B,SAAQC,gBAAgB,QAAO,cAAc;AAC7C,SACEC,gCAAgC,EAChCC,iCAAiC,EACjCC,6BAA6B,QACxB,cAAc;AACrB,SAAQC,mBAAmB,EAAEC,QAAQ,QAAO,8BAA8B;AAC1E,SAAQC,wBAAwB,QAAmC,wBAAwB;AAU3F,OAAO,eAAeC,uBACpBC,MAAoC;IAEpC,MAAMC,eAAe,MAAMb,2BAA2BY,OAAOE,cAAc;IAC3E,IAAI,CAACD,cAAc,MAAM,IAAIR,iCAAiCO,OAAOE,cAAc;IACnF,IAAID,aAAaE,MAAM,KAAK,UAAU;QACpC,MAAM,IAAIT,kCAAkCM,OAAOE,cAAc,EAAED,aAAaE,MAAM;IACxF;IACA,yFAAyF;IACzF,IAAIF,aAAaG,WAAW,KAAKJ,OAAOK,iBAAiB,EAAE;QACzD,MAAM,IAAIV,8BACRK,OAAOE,cAAc,EACrBD,aAAaG,WAAW,EACxBJ,OAAOK,iBAAiB;IAE5B;IAEA,+EAA+E;IAC/E,qEAAqE;IACrE,MAAMC,cAAc;QAClBC,QAAQ;QACRH,aAAaH,aAAaG,WAAW;QACrCI,UAAU;QACVL,QAAQF,aAAaE,MAAM;QAC3BM,OAAOR,aAAaQ,KAAK;QACzBC,YAAY;QACZC,cAAc;QACdC,gBAAgB;QAChBC,SAAS;QACTC,YAAY,IAAIC;IAClB;IAEAzB,mBAAmB0B,GAAG,CAAC,GAAG;QAACR,UAAU;IAAQ;IAE7C,MAAMS,SAASjB,OAAOiB,MAAM,IAAIzB,iBAAiBS;IACjD,IAAIiB;IACJ,IAAI;QACFA,MAAM,MAAMlB,OAAOmB,SAAS,CAACC,mBAAmB,CAAC;YAC/ChB,aAAaH,aAAaG,WAAW;YACrCiB,WAAWpB,aAAaoB,SAAS;YACjCC,cAAcrB,aAAasB,oBAAoB;YAC/CC,gBAAgB;gBACdhB,UAAU;gBACVL,QAAQ;gBACRM,OAAO;gBACPP,gBAAgBD,aAAawB,EAAE;gBAC/BC,QAAQ1B,OAAO0B,MAAM;YACvB;YACA,GAAIT,WAAWU,YAAY,CAAC,IAAI;gBAACV;YAAM,CAAC;YACxCW,gBAAgBzC;QAClB;IACF,EAAE,OAAO0C,OAAO;QACd,MAAMC,UAAU,MAAMlC,oBAAoB;YAAC,GAAGU,WAAW;YAAEyB,UAAU5C;QAAY;QACjF,MAAM2C,QAAQE,eAAe,CAAC/B,cAAcJ,SAASgC;QACrD,IAAI/B,yBAAyB+B,QAAQ;YACnCxC,kBAAkB2B,GAAG,CAAC,GAAG;gBAACR,UAAU;gBAAUyB,SAAS;YAAS;YAChE,MAAMH,QAAQI,UAAU,CAAC;QAC3B,OAAO;YACL7C,kBAAkB2B,GAAG,CAAC,GAAG;gBAACR,UAAU;gBAAUyB,SAAS;YAAQ;YAC/D,MAAMH,QAAQK,MAAM,CAAC;QACvB;QACA,MAAMN;IACR;IAEA,MAAMO,UAAU,MAAMxC,oBAAoB;QAAC,GAAGU,WAAW;QAAEyB,UAAUb,IAAIO,EAAE;IAAA;IAC3E,MAAMW,QAAQC,SAAS,CAACpC,cAAciB;IACtC3B,2BAA2ByB,GAAG,CAAC,GAAG;QAACR,UAAU;IAAQ;IACrDnB,kBAAkB2B,GAAG,CAAC,GAAG;QAACR,UAAU;QAAUyB,SAAS;IAAQ;IAC/D,MAAMG,QAAQE,MAAM,CAAC;IACrB,OAAOpB;AACT"}
@@ -1,5 +1,7 @@
1
1
  import { type TriggerHistoryRecorder } from './record-trigger-history.js';
2
+ import type { WorkflowsModuleClient } from './workflows-client.js';
2
3
  export interface RouteEventToJobListenersParams {
4
+ workflows: WorkflowsModuleClient;
3
5
  history: TriggerHistoryRecorder;
4
6
  eventRef: string;
5
7
  workspaceId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"route-event-to-job-listeners.d.ts","sourceRoot":"","sources":["../../src/core/route-event-to-job-listeners.ts"],"names":[],"mappings":"AAKA,OAAO,EAAC,KAAK,sBAAsB,EAAW,MAAM,6BAA6B,CAAC;AAElF,MAAM,WAAW,8BAA8B;IAC7C,OAAO,EAAE,sBAAsB,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,8BAA8B;IAC7C;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,8BAA8B,GACrC,OAAO,CAAC,8BAA8B,CAAC,CAmEzC"}
1
+ {"version":3,"file":"route-event-to-job-listeners.d.ts","sourceRoot":"","sources":["../../src/core/route-event-to-job-listeners.ts"],"names":[],"mappings":"AAIA,OAAO,EAAC,KAAK,sBAAsB,EAAW,MAAM,6BAA6B,CAAC;AAClF,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,uBAAuB,CAAC;AAEjE,MAAM,WAAW,8BAA8B;IAC7C,SAAS,EAAE,qBAAqB,CAAC;IACjC,OAAO,EAAE,sBAAsB,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,8BAA8B;IAC7C;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,OAAO,CAAC;IAC1B,cAAc,EAAE,OAAO,CAAC;CACzB;AAED,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,8BAA8B,GACrC,OAAO,CAAC,8BAA8B,CAAC,CAmEzC"}
@@ -1,4 +1,3 @@
1
- import { deliverEventToListener } from '@shipfox/api-workflows';
2
1
  import { logger } from '@shipfox/node-opentelemetry';
3
2
  import { findMatchingJobListenerSubscriptions } from '#db/job-listener-subscriptions.js';
4
3
  import { evaluateStoredFilter } from './config.js';
@@ -35,7 +34,7 @@ export async function routeEventToJobListeners(params) {
35
34
  for (const subscription of effectiveMatchByJobId.values()){
36
35
  const disposition = listenerDisposition(subscription);
37
36
  try {
38
- const result = await deliverEventToListener({
37
+ const result = await params.workflows.deliverEventToJobListener({
39
38
  jobId: subscription.jobId,
40
39
  disposition,
41
40
  eventRef: params.eventRef,
@@ -44,7 +43,7 @@ export async function routeEventToJobListeners(params) {
44
43
  event: params.event,
45
44
  provider: params.provider,
46
45
  payload: params.payload,
47
- receivedAt: params.receivedAt
46
+ receivedAt: params.receivedAt.toISOString()
48
47
  });
49
48
  if (!result.skipped) {
50
49
  acceptedJobCount += 1;
@@ -1 +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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"}
1
+ {"version":3,"sources":["../../src/core/route-event-to-job-listeners.ts"],"sourcesContent":["import {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';\nimport type {WorkflowsModuleClient} from './workflows-client.js';\n\nexport interface RouteEventToJobListenersParams {\n workflows: WorkflowsModuleClient;\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 params.workflows.deliverEventToJobListener({\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.toISOString(),\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): 'fire' | 'resolve' {\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":["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","workflows","deliverEventToJobListener","eventRef","deliveryId","provider","receivedAt","toISOString","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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"}