@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.
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +224 -0
- package/LICENSE +21 -0
- package/README.md +300 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +42 -0
- package/dist/config.js.map +1 -0
- package/dist/core/compute-next-fire-at.d.ts +9 -0
- package/dist/core/compute-next-fire-at.d.ts.map +1 -0
- package/dist/core/compute-next-fire-at.js +28 -0
- package/dist/core/compute-next-fire-at.js.map +1 -0
- package/dist/core/config.d.ts +26 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +68 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/dispatch-integration-event.d.ts +14 -0
- package/dist/core/dispatch-integration-event.d.ts.map +1 -0
- package/dist/core/dispatch-integration-event.js +136 -0
- package/dist/core/dispatch-integration-event.js.map +1 -0
- package/dist/core/drain-cron-schedules.d.ts +30 -0
- package/dist/core/drain-cron-schedules.d.ts.map +1 -0
- package/dist/core/drain-cron-schedules.js +76 -0
- package/dist/core/drain-cron-schedules.js.map +1 -0
- package/dist/core/entities/cron-schedule.d.ts +11 -0
- package/dist/core/entities/cron-schedule.d.ts.map +1 -0
- package/dist/core/entities/cron-schedule.js +3 -0
- package/dist/core/entities/cron-schedule.js.map +1 -0
- package/dist/core/entities/decision.d.ts +23 -0
- package/dist/core/entities/decision.d.ts.map +1 -0
- package/dist/core/entities/decision.js +7 -0
- package/dist/core/entities/decision.js.map +1 -0
- package/dist/core/entities/index.d.ts +3 -0
- package/dist/core/entities/index.d.ts.map +1 -0
- package/dist/core/entities/index.js +3 -0
- package/dist/core/entities/index.js.map +1 -0
- package/dist/core/entities/job-listener-subscription.d.ts +14 -0
- package/dist/core/entities/job-listener-subscription.d.ts.map +1 -0
- package/dist/core/entities/job-listener-subscription.js +3 -0
- package/dist/core/entities/job-listener-subscription.js.map +1 -0
- package/dist/core/entities/received-event.d.ts +28 -0
- package/dist/core/entities/received-event.d.ts.map +1 -0
- package/dist/core/entities/received-event.js +14 -0
- package/dist/core/entities/received-event.js.map +1 -0
- package/dist/core/entities/subscription.d.ts +13 -0
- package/dist/core/entities/subscription.d.ts.map +1 -0
- package/dist/core/entities/subscription.js +3 -0
- package/dist/core/entities/subscription.js.map +1 -0
- package/dist/core/errors.d.ts +25 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +41 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/fire-cron.d.ts +28 -0
- package/dist/core/fire-cron.d.ts.map +1 -0
- package/dist/core/fire-cron.js +85 -0
- package/dist/core/fire-cron.js.map +1 -0
- package/dist/core/fire-manual.d.ts +9 -0
- package/dist/core/fire-manual.d.ts.map +1 -0
- package/dist/core/fire-manual.js +87 -0
- package/dist/core/fire-manual.js.map +1 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +10 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/record-trigger-history.d.ts +35 -0
- package/dist/core/record-trigger-history.d.ts.map +1 -0
- package/dist/core/record-trigger-history.js +75 -0
- package/dist/core/record-trigger-history.js.map +1 -0
- package/dist/core/route-event-to-job-listeners.d.ts +31 -0
- package/dist/core/route-event-to-job-listeners.d.ts.map +1 -0
- package/dist/core/route-event-to-job-listeners.js +140 -0
- package/dist/core/route-event-to-job-listeners.js.map +1 -0
- package/dist/db/cron-schedules.d.ts +36 -0
- package/dist/db/cron-schedules.d.ts.map +1 -0
- package/dist/db/cron-schedules.js +104 -0
- package/dist/db/cron-schedules.js.map +1 -0
- package/dist/db/db.d.ts +2515 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +28 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/event-history.d.ts +50 -0
- package/dist/db/event-history.d.ts.map +1 -0
- package/dist/db/event-history.js +207 -0
- package/dist/db/event-history.js.map +1 -0
- package/dist/db/event-queries.d.ts +38 -0
- package/dist/db/event-queries.d.ts.map +1 -0
- package/dist/db/event-queries.js +78 -0
- package/dist/db/event-queries.js.map +1 -0
- package/dist/db/index.d.ts +11 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +14 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/job-listener-subscriptions.d.ts +24 -0
- package/dist/db/job-listener-subscriptions.d.ts.map +1 -0
- package/dist/db/job-listener-subscriptions.js +69 -0
- package/dist/db/job-listener-subscriptions.js.map +1 -0
- package/dist/db/received-events.d.ts +7 -0
- package/dist/db/received-events.d.ts.map +1 -0
- package/dist/db/received-events.js +11 -0
- package/dist/db/received-events.js.map +1 -0
- package/dist/db/schema/common.d.ts +2 -0
- package/dist/db/schema/common.d.ts.map +1 -0
- package/dist/db/schema/common.js +4 -0
- package/dist/db/schema/common.js.map +1 -0
- package/dist/db/schema/cron-schedules.d.ts +148 -0
- package/dist/db/schema/cron-schedules.d.ts.map +1 -0
- package/dist/db/schema/cron-schedules.js +39 -0
- package/dist/db/schema/cron-schedules.js.map +1 -0
- package/dist/db/schema/decisions.d.ts +284 -0
- package/dist/db/schema/decisions.d.ts.map +1 -0
- package/dist/db/schema/decisions.js +70 -0
- package/dist/db/schema/decisions.js.map +1 -0
- package/dist/db/schema/job-listener-subscriptions.d.ts +185 -0
- package/dist/db/schema/job-listener-subscriptions.d.ts.map +1 -0
- package/dist/db/schema/job-listener-subscriptions.js +41 -0
- package/dist/db/schema/job-listener-subscriptions.js.map +1 -0
- package/dist/db/schema/outbox.d.ts +195 -0
- package/dist/db/schema/outbox.d.ts.map +1 -0
- package/dist/db/schema/outbox.js +5 -0
- package/dist/db/schema/outbox.js.map +1 -0
- package/dist/db/schema/received-events.d.ts +544 -0
- package/dist/db/schema/received-events.d.ts.map +1 -0
- package/dist/db/schema/received-events.js +86 -0
- package/dist/db/schema/received-events.js.map +1 -0
- package/dist/db/schema/subscriptions.d.ts +182 -0
- package/dist/db/schema/subscriptions.d.ts.map +1 -0
- package/dist/db/schema/subscriptions.js +39 -0
- package/dist/db/schema/subscriptions.js.map +1 -0
- package/dist/db/subscriptions.d.ts +26 -0
- package/dist/db/subscriptions.d.ts.map +1 -0
- package/dist/db/subscriptions.js +103 -0
- package/dist/db/subscriptions.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/index.d.ts +3 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +4 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/instance.d.ts +15 -0
- package/dist/metrics/instance.d.ts.map +1 -0
- package/dist/metrics/instance.js +32 -0
- package/dist/metrics/instance.js.map +1 -0
- package/dist/metrics/service.d.ts +2 -0
- package/dist/metrics/service.d.ts.map +1 -0
- package/dist/metrics/service.js +15 -0
- package/dist/metrics/service.js.map +1 -0
- package/dist/presentation/dto/trigger-events.d.ts +7 -0
- package/dist/presentation/dto/trigger-events.d.ts.map +1 -0
- package/dist/presentation/dto/trigger-events.js +47 -0
- package/dist/presentation/dto/trigger-events.js.map +1 -0
- package/dist/presentation/index.d.ts +3 -0
- package/dist/presentation/index.d.ts.map +1 -0
- package/dist/presentation/index.js +4 -0
- package/dist/presentation/index.js.map +1 -0
- package/dist/presentation/routes/fire-manual.d.ts +2 -0
- package/dist/presentation/routes/fire-manual.d.ts.map +1 -0
- package/dist/presentation/routes/fire-manual.js +71 -0
- package/dist/presentation/routes/fire-manual.js.map +1 -0
- package/dist/presentation/routes/get-trigger-event.d.ts +2 -0
- package/dist/presentation/routes/get-trigger-event.d.ts.map +1 -0
- package/dist/presentation/routes/get-trigger-event.js +37 -0
- package/dist/presentation/routes/get-trigger-event.js.map +1 -0
- package/dist/presentation/routes/index.d.ts +3 -0
- package/dist/presentation/routes/index.d.ts.map +1 -0
- package/dist/presentation/routes/index.js +27 -0
- package/dist/presentation/routes/index.js.map +1 -0
- package/dist/presentation/routes/list-trigger-event-facets.d.ts +2 -0
- package/dist/presentation/routes/list-trigger-event-facets.d.ts.map +1 -0
- package/dist/presentation/routes/list-trigger-event-facets.js +29 -0
- package/dist/presentation/routes/list-trigger-event-facets.js.map +1 -0
- package/dist/presentation/routes/list-trigger-events.d.ts +2 -0
- package/dist/presentation/routes/list-trigger-events.d.ts.map +1 -0
- package/dist/presentation/routes/list-trigger-events.js +56 -0
- package/dist/presentation/routes/list-trigger-events.js.map +1 -0
- package/dist/presentation/subscribers/index.d.ts +6 -0
- package/dist/presentation/subscribers/index.d.ts.map +1 -0
- package/dist/presentation/subscribers/index.js +7 -0
- package/dist/presentation/subscribers/index.js.map +1 -0
- package/dist/presentation/subscribers/on-definition-deleted.d.ts +3 -0
- package/dist/presentation/subscribers/on-definition-deleted.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-definition-deleted.js +8 -0
- package/dist/presentation/subscribers/on-definition-deleted.js.map +1 -0
- package/dist/presentation/subscribers/on-definition-resolved.d.ts +3 -0
- package/dist/presentation/subscribers/on-definition-resolved.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-definition-resolved.js +11 -0
- package/dist/presentation/subscribers/on-definition-resolved.js.map +1 -0
- package/dist/presentation/subscribers/on-integration-event-received.d.ts +4 -0
- package/dist/presentation/subscribers/on-integration-event-received.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-integration-event-received.js +17 -0
- package/dist/presentation/subscribers/on-integration-event-received.js.map +1 -0
- package/dist/presentation/subscribers/on-job-activated.d.ts +3 -0
- package/dist/presentation/subscribers/on-job-activated.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-job-activated.js +13 -0
- package/dist/presentation/subscribers/on-job-activated.js.map +1 -0
- package/dist/presentation/subscribers/on-job-terminated.d.ts +3 -0
- package/dist/presentation/subscribers/on-job-terminated.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-job-terminated.js +6 -0
- package/dist/presentation/subscribers/on-job-terminated.js.map +1 -0
- package/dist/temporal/activities/drain-cron-batch.d.ts +4 -0
- package/dist/temporal/activities/drain-cron-batch.d.ts.map +1 -0
- package/dist/temporal/activities/drain-cron-batch.js +20 -0
- package/dist/temporal/activities/drain-cron-batch.js.map +1 -0
- package/dist/temporal/activities/index.d.ts +10 -0
- package/dist/temporal/activities/index.d.ts.map +1 -0
- package/dist/temporal/activities/index.js +15 -0
- package/dist/temporal/activities/index.js.map +1 -0
- package/dist/temporal/activities/prune-trigger-events.d.ts +4 -0
- package/dist/temporal/activities/prune-trigger-events.d.ts.map +1 -0
- package/dist/temporal/activities/prune-trigger-events.js +11 -0
- package/dist/temporal/activities/prune-trigger-events.js.map +1 -0
- package/dist/temporal/constants.d.ts +7 -0
- package/dist/temporal/constants.d.ts.map +1 -0
- package/dist/temporal/constants.js +7 -0
- package/dist/temporal/constants.js.map +1 -0
- package/dist/temporal/workflows/cron-tick-cron.d.ts +9 -0
- package/dist/temporal/workflows/cron-tick-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/cron-tick-cron.js +54 -0
- package/dist/temporal/workflows/cron-tick-cron.js.map +1 -0
- package/dist/temporal/workflows/index.d.ts +3 -0
- package/dist/temporal/workflows/index.d.ts.map +1 -0
- package/dist/temporal/workflows/index.js +4 -0
- package/dist/temporal/workflows/index.js.map +1 -0
- package/dist/temporal/workflows/prune-trigger-events-cron.d.ts +2 -0
- package/dist/temporal/workflows/prune-trigger-events-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/prune-trigger-events-cron.js +18 -0
- package/dist/temporal/workflows/prune-trigger-events-cron.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/drizzle/0000_initial.sql +109 -0
- package/drizzle/meta/0000_snapshot.json +905 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +14 -0
- package/package.json +83 -0
- package/src/config.test.ts +33 -0
- package/src/config.ts +53 -0
- package/src/core/compute-next-fire-at.test.ts +69 -0
- package/src/core/compute-next-fire-at.ts +37 -0
- package/src/core/config.test.ts +213 -0
- package/src/core/config.ts +86 -0
- package/src/core/dispatch-integration-event.history-failure.test.ts +66 -0
- package/src/core/dispatch-integration-event.test.ts +1006 -0
- package/src/core/dispatch-integration-event.ts +150 -0
- package/src/core/drain-cron-schedules.test.ts +265 -0
- package/src/core/drain-cron-schedules.ts +90 -0
- package/src/core/entities/cron-schedule.ts +10 -0
- package/src/core/entities/decision.ts +24 -0
- package/src/core/entities/index.ts +5 -0
- package/src/core/entities/job-listener-subscription.ts +14 -0
- package/src/core/entities/received-event.ts +36 -0
- package/src/core/entities/subscription.ts +12 -0
- package/src/core/errors.ts +63 -0
- package/src/core/fire-cron.test.ts +146 -0
- package/src/core/fire-cron.ts +98 -0
- package/src/core/fire-manual.test.ts +157 -0
- package/src/core/fire-manual.ts +92 -0
- package/src/core/index.ts +34 -0
- package/src/core/record-trigger-history.test.ts +159 -0
- package/src/core/record-trigger-history.ts +145 -0
- package/src/core/route-event-to-job-listeners.test.ts +369 -0
- package/src/core/route-event-to-job-listeners.ts +184 -0
- package/src/db/cron-schedules.test.ts +358 -0
- package/src/db/cron-schedules.ts +163 -0
- package/src/db/db.ts +31 -0
- package/src/db/event-history.test.ts +421 -0
- package/src/db/event-history.ts +279 -0
- package/src/db/event-queries.ts +140 -0
- package/src/db/index.ts +51 -0
- package/src/db/job-listener-subscriptions.test.ts +270 -0
- package/src/db/job-listener-subscriptions.ts +133 -0
- package/src/db/received-events.ts +16 -0
- package/src/db/schema/common.ts +3 -0
- package/src/db/schema/cron-schedules.ts +37 -0
- package/src/db/schema/decisions.test.ts +279 -0
- package/src/db/schema/decisions.ts +77 -0
- package/src/db/schema/job-listener-subscriptions.ts +65 -0
- package/src/db/schema/outbox.ts +4 -0
- package/src/db/schema/received-events.test.ts +135 -0
- package/src/db/schema/received-events.ts +93 -0
- package/src/db/schema/subscriptions.ts +46 -0
- package/src/db/subscriptions.test.ts +340 -0
- package/src/db/subscriptions.ts +179 -0
- package/src/index.ts +108 -0
- package/src/metrics/index.ts +8 -0
- package/src/metrics/instance.ts +33 -0
- package/src/metrics/service.ts +18 -0
- package/src/presentation/dto/trigger-events.test.ts +144 -0
- package/src/presentation/dto/trigger-events.ts +60 -0
- package/src/presentation/index.ts +6 -0
- package/src/presentation/routes/fire-manual.test.ts +118 -0
- package/src/presentation/routes/fire-manual.ts +70 -0
- package/src/presentation/routes/get-trigger-event.test.ts +184 -0
- package/src/presentation/routes/get-trigger-event.ts +37 -0
- package/src/presentation/routes/index.ts +21 -0
- package/src/presentation/routes/list-trigger-event-facets.test.ts +116 -0
- package/src/presentation/routes/list-trigger-event-facets.ts +29 -0
- package/src/presentation/routes/list-trigger-events.test.ts +379 -0
- package/src/presentation/routes/list-trigger-events.ts +68 -0
- package/src/presentation/subscribers/index.ts +5 -0
- package/src/presentation/subscribers/on-definition-deleted.test.ts +42 -0
- package/src/presentation/subscribers/on-definition-deleted.ts +6 -0
- package/src/presentation/subscribers/on-definition-resolved.test.ts +67 -0
- package/src/presentation/subscribers/on-definition-resolved.ts +11 -0
- package/src/presentation/subscribers/on-integration-event-received.test.ts +53 -0
- package/src/presentation/subscribers/on-integration-event-received.ts +21 -0
- package/src/presentation/subscribers/on-job-activated.ts +14 -0
- package/src/presentation/subscribers/on-job-terminated.ts +6 -0
- package/src/temporal/activities/drain-cron-batch.ts +20 -0
- package/src/temporal/activities/index.ts +15 -0
- package/src/temporal/activities/prune-trigger-events.test.ts +78 -0
- package/src/temporal/activities/prune-trigger-events.ts +9 -0
- package/src/temporal/constants.ts +7 -0
- package/src/temporal/workflows/cron-tick-cron.ts +56 -0
- package/src/temporal/workflows/index.ts +2 -0
- package/src/temporal/workflows/prune-trigger-events-cron.ts +17 -0
- package/test/env.ts +8 -0
- package/test/factories/cron-schedule.ts +34 -0
- package/test/factories/decision.ts +49 -0
- package/test/factories/job-listener-subscription.ts +42 -0
- package/test/factories/received-event.ts +49 -0
- package/test/factories/subscription.ts +38 -0
- package/test/globalSetup.ts +20 -0
- package/test/index.ts +5 -0
- package/test/setup.ts +17 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {isPermanentRunWorkflowError, runWorkflow} from '@shipfox/api-workflows';
|
|
2
|
+
import {findMatchingSubscriptions} from '#db/subscriptions.js';
|
|
3
|
+
import {
|
|
4
|
+
eventOutcomeCount,
|
|
5
|
+
eventReceivedCount,
|
|
6
|
+
subscriptionTriggeredCount,
|
|
7
|
+
} from '#metrics/instance.js';
|
|
8
|
+
import {evaluateTriggerFilter, readConfigInputs} from './config.js';
|
|
9
|
+
import {beginTriggerHistory, toReason} from './record-trigger-history.js';
|
|
10
|
+
import {routeEventToJobListeners} from './route-event-to-job-listeners.js';
|
|
11
|
+
|
|
12
|
+
export interface DispatchIntegrationEventParams {
|
|
13
|
+
eventRef: string;
|
|
14
|
+
workspaceId: string;
|
|
15
|
+
provider: string;
|
|
16
|
+
source: string;
|
|
17
|
+
event: string;
|
|
18
|
+
deliveryId: string;
|
|
19
|
+
connectionId: string;
|
|
20
|
+
connectionName: string | null;
|
|
21
|
+
payload: unknown;
|
|
22
|
+
receivedAt: Date;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Source-agnostic dispatcher: any inbound integration event fans out to every
|
|
26
|
+
// workspace subscription registered for its (source, event) and to every listening
|
|
27
|
+
// job subscribed to it, passing the raw payload through untouched. The module knows
|
|
28
|
+
// nothing about github, gitlab, etc.
|
|
29
|
+
//
|
|
30
|
+
// Continue-on-error: every matched subscription and listener is attempted so one broken
|
|
31
|
+
// subscription cannot starve its siblings. A permanent failure (deleted definition, project
|
|
32
|
+
// mismatch) is recorded and skipped; a transient one is recorded and re-thrown so the outbox
|
|
33
|
+
// replays the whole event and converges (succeeded siblings dedup on the idempotency key). The
|
|
34
|
+
// event reaches a terminal outcome only when no transient error remains: `routed` if any run
|
|
35
|
+
// was created or any listening job matched, `discarded` if nothing matched, otherwise `errored`.
|
|
36
|
+
// History is best-effort; the thrown transient error, not the recorded outcome, drives the retry.
|
|
37
|
+
export async function dispatchIntegrationEvent(
|
|
38
|
+
params: DispatchIntegrationEventParams,
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
eventReceivedCount.add(1, {provider: params.provider});
|
|
41
|
+
|
|
42
|
+
const history = await beginTriggerHistory({
|
|
43
|
+
eventRef: params.eventRef,
|
|
44
|
+
origin: 'integration',
|
|
45
|
+
workspaceId: params.workspaceId,
|
|
46
|
+
provider: params.provider,
|
|
47
|
+
source: params.source,
|
|
48
|
+
event: params.event,
|
|
49
|
+
deliveryId: params.deliveryId,
|
|
50
|
+
connectionId: params.connectionId,
|
|
51
|
+
connectionName: params.connectionName,
|
|
52
|
+
payload: (params.payload ?? null) as Record<string, unknown> | null,
|
|
53
|
+
receivedAt: params.receivedAt,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const subscriptions = await findMatchingSubscriptions({
|
|
57
|
+
workspaceId: params.workspaceId,
|
|
58
|
+
source: params.source,
|
|
59
|
+
event: params.event,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
let triggeredCount = 0;
|
|
63
|
+
let triggerEngagedCount = 0;
|
|
64
|
+
let sawTransientError = false;
|
|
65
|
+
let firstTransientError: unknown;
|
|
66
|
+
|
|
67
|
+
for (const subscription of subscriptions) {
|
|
68
|
+
const filterResult = evaluateTriggerFilter({
|
|
69
|
+
subscription,
|
|
70
|
+
source: params.source,
|
|
71
|
+
event: params.event,
|
|
72
|
+
payload: params.payload,
|
|
73
|
+
});
|
|
74
|
+
if (filterResult.kind === 'filtered') continue;
|
|
75
|
+
if (filterResult.kind === 'filter-error') {
|
|
76
|
+
triggerEngagedCount += 1;
|
|
77
|
+
await history.filterErrored(subscription, filterResult.reason);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
triggerEngagedCount += 1;
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const run = await runWorkflow({
|
|
84
|
+
workspaceId: subscription.workspaceId,
|
|
85
|
+
projectId: subscription.projectId,
|
|
86
|
+
definitionId: subscription.workflowDefinitionId,
|
|
87
|
+
triggerPayload: {
|
|
88
|
+
provider: params.provider,
|
|
89
|
+
source: params.source,
|
|
90
|
+
event: params.event,
|
|
91
|
+
deliveryId: params.deliveryId,
|
|
92
|
+
data: params.payload,
|
|
93
|
+
},
|
|
94
|
+
inputs: readConfigInputs(subscription),
|
|
95
|
+
triggerIdempotencyKey: `${subscription.id}:${params.eventRef}`,
|
|
96
|
+
});
|
|
97
|
+
await history.triggered(subscription, run);
|
|
98
|
+
triggeredCount += 1;
|
|
99
|
+
subscriptionTriggeredCount.add(1, {provider: params.provider});
|
|
100
|
+
} catch (error) {
|
|
101
|
+
await history.dispatchErrored(subscription, toReason(error));
|
|
102
|
+
// Track presence with a flag, not `firstTransientError === undefined`: a thrown
|
|
103
|
+
// value of `undefined` is still a transient failure and must drive the replay.
|
|
104
|
+
if (!isPermanentRunWorkflowError(error) && !sawTransientError) {
|
|
105
|
+
sawTransientError = true;
|
|
106
|
+
firstTransientError = error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const listenerResult = await routeEventToJobListeners({
|
|
112
|
+
history,
|
|
113
|
+
eventRef: params.eventRef,
|
|
114
|
+
workspaceId: params.workspaceId,
|
|
115
|
+
provider: params.provider,
|
|
116
|
+
source: params.source,
|
|
117
|
+
event: params.event,
|
|
118
|
+
deliveryId: params.deliveryId,
|
|
119
|
+
payload: params.payload,
|
|
120
|
+
receivedAt: params.receivedAt,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (listenerResult.transientErrored && !sawTransientError) {
|
|
124
|
+
sawTransientError = true;
|
|
125
|
+
firstTransientError = listenerResult.transientError;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const totalMatchedCount = triggerEngagedCount + listenerResult.engagedCount;
|
|
129
|
+
|
|
130
|
+
if (sawTransientError) {
|
|
131
|
+
eventOutcomeCount.add(1, {provider: params.provider, outcome: 'failed'});
|
|
132
|
+
await history.failed(totalMatchedCount);
|
|
133
|
+
throw firstTransientError;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (triggeredCount > 0 || listenerResult.acceptedJobCount > 0) {
|
|
137
|
+
eventOutcomeCount.add(1, {provider: params.provider, outcome: 'routed'});
|
|
138
|
+
await history.routed(totalMatchedCount);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (totalMatchedCount === 0) {
|
|
143
|
+
eventOutcomeCount.add(1, {provider: params.provider, outcome: 'discarded'});
|
|
144
|
+
await history.discarded();
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
eventOutcomeCount.add(1, {provider: params.provider, outcome: 'errored'});
|
|
149
|
+
await history.allErrored(totalMatchedCount);
|
|
150
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import {eq} from 'drizzle-orm';
|
|
2
|
+
import type {CronSchedule} from '#core/entities/cron-schedule.js';
|
|
3
|
+
import type {TriggerSubscription} from '#core/entities/subscription.js';
|
|
4
|
+
import {countDueCronSchedules} from '#db/cron-schedules.js';
|
|
5
|
+
import {db} from '#db/db.js';
|
|
6
|
+
import {triggersCronSchedules} from '#db/schema/cron-schedules.js';
|
|
7
|
+
import {triggersDecisions} from '#db/schema/decisions.js';
|
|
8
|
+
import {triggersReceivedEvents} from '#db/schema/received-events.js';
|
|
9
|
+
import {cronScheduleFactory, triggerSubscriptionFactory} from '#test/index.js';
|
|
10
|
+
|
|
11
|
+
const runWorkflow = vi.fn();
|
|
12
|
+
|
|
13
|
+
vi.mock('@shipfox/api-workflows', () => {
|
|
14
|
+
class DefinitionNotFoundError extends Error {
|
|
15
|
+
constructor(definitionId: string) {
|
|
16
|
+
super(`Definition not found: ${definitionId}`);
|
|
17
|
+
this.name = 'DefinitionNotFoundError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
runWorkflow: (...args: unknown[]) => runWorkflow(...args),
|
|
22
|
+
DefinitionNotFoundError,
|
|
23
|
+
isPermanentRunWorkflowError: (error: unknown) => error instanceof DefinitionNotFoundError,
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
import {DefinitionNotFoundError} from '@shipfox/api-workflows';
|
|
28
|
+
|
|
29
|
+
// Import after mocks so the function under test sees the spy.
|
|
30
|
+
const {drainDueCronSchedules} = await import('./drain-cron-schedules.js');
|
|
31
|
+
|
|
32
|
+
const MINUTE_MS = 60 * 1000;
|
|
33
|
+
|
|
34
|
+
// The drain claim is global over a shared test DB, so assertions target the specific
|
|
35
|
+
// subscriptions each test creates (via the deterministic idempotency key) and use
|
|
36
|
+
// lower bounds for any global count, mirroring the prune activity test.
|
|
37
|
+
interface DueCron {
|
|
38
|
+
subscription: TriggerSubscription;
|
|
39
|
+
schedule: CronSchedule;
|
|
40
|
+
key: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function createCronSchedule(params: {
|
|
44
|
+
nextFireAt: Date;
|
|
45
|
+
cronExpression?: string;
|
|
46
|
+
config?: Record<string, unknown>;
|
|
47
|
+
}): Promise<DueCron> {
|
|
48
|
+
const subscription = await triggerSubscriptionFactory.create({
|
|
49
|
+
source: 'cron',
|
|
50
|
+
event: 'tick',
|
|
51
|
+
config: params.config ?? {},
|
|
52
|
+
});
|
|
53
|
+
const schedule = await cronScheduleFactory.create({
|
|
54
|
+
subscriptionId: subscription.id,
|
|
55
|
+
workspaceId: subscription.workspaceId,
|
|
56
|
+
cronExpression: params.cronExpression ?? '*/5 * * * *',
|
|
57
|
+
timezone: 'UTC',
|
|
58
|
+
nextFireAt: params.nextFireAt,
|
|
59
|
+
});
|
|
60
|
+
return {subscription, schedule, key: `${subscription.id}:${schedule.nextFireAt.toISOString()}`};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function callsWithKey(key: string) {
|
|
64
|
+
return runWorkflow.mock.calls.filter(
|
|
65
|
+
([params]) => (params as {triggerIdempotencyKey?: string}).triggerIdempotencyKey === key,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function reload(subscriptionId: string) {
|
|
70
|
+
const [row] = await db()
|
|
71
|
+
.select()
|
|
72
|
+
.from(triggersCronSchedules)
|
|
73
|
+
.where(eq(triggersCronSchedules.subscriptionId, subscriptionId));
|
|
74
|
+
if (!row) throw new Error('cron schedule row not found');
|
|
75
|
+
return row;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
describe('drainDueCronSchedules', () => {
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
runWorkflow.mockReset();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('fires a due schedule exactly once, records cron history, and advances next_fire_at', async () => {
|
|
84
|
+
const {subscription, schedule, key} = await createCronSchedule({
|
|
85
|
+
nextFireAt: new Date(Date.now() - MINUTE_MS),
|
|
86
|
+
});
|
|
87
|
+
const run = {id: crypto.randomUUID(), name: 'Cron run'};
|
|
88
|
+
runWorkflow.mockResolvedValue(run);
|
|
89
|
+
|
|
90
|
+
const summary = await drainDueCronSchedules({batchSize: 1000, jitterWindowSeconds: 0});
|
|
91
|
+
|
|
92
|
+
expect(callsWithKey(key)).toHaveLength(1);
|
|
93
|
+
expect(summary.fired).toBeGreaterThanOrEqual(1);
|
|
94
|
+
const [event] = await db()
|
|
95
|
+
.select()
|
|
96
|
+
.from(triggersReceivedEvents)
|
|
97
|
+
.where(eq(triggersReceivedEvents.eventRef, key));
|
|
98
|
+
if (!event) throw new Error('received event not found');
|
|
99
|
+
expect(event.origin).toBe('cron');
|
|
100
|
+
expect(event.outcome).toBe('routed');
|
|
101
|
+
const decisions = await db()
|
|
102
|
+
.select()
|
|
103
|
+
.from(triggersDecisions)
|
|
104
|
+
.where(eq(triggersDecisions.receivedEventId, event.id));
|
|
105
|
+
expect(decisions[0]?.decision).toBe('triggered');
|
|
106
|
+
expect(decisions[0]?.runId).toBe(run.id);
|
|
107
|
+
const row = await reload(subscription.id);
|
|
108
|
+
expect(row.nextFireAt.getTime()).toBeGreaterThan(Date.now());
|
|
109
|
+
expect(row.lastFiredAt?.getTime()).toBe(schedule.nextFireAt.getTime());
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('does not claim a schedule whose next fire time is still in the future', async () => {
|
|
113
|
+
const future = new Date(Date.now() + 60 * MINUTE_MS);
|
|
114
|
+
const {subscription, key} = await createCronSchedule({nextFireAt: future});
|
|
115
|
+
runWorkflow.mockResolvedValue({id: crypto.randomUUID(), name: 'r'});
|
|
116
|
+
|
|
117
|
+
await drainDueCronSchedules({batchSize: 1000, jitterWindowSeconds: 0});
|
|
118
|
+
|
|
119
|
+
expect(callsWithKey(key)).toHaveLength(0);
|
|
120
|
+
const row = await reload(subscription.id);
|
|
121
|
+
expect(row.nextFireAt.getTime()).toBe(future.getTime());
|
|
122
|
+
expect(row.lastFiredAt).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('overdue schedule fires once and skips the intermediate windows', async () => {
|
|
126
|
+
const {subscription, key} = await createCronSchedule({
|
|
127
|
+
nextFireAt: new Date(Date.now() - 60 * MINUTE_MS),
|
|
128
|
+
cronExpression: '*/5 * * * *',
|
|
129
|
+
});
|
|
130
|
+
runWorkflow.mockResolvedValue({id: crypto.randomUUID(), name: 'r'});
|
|
131
|
+
|
|
132
|
+
await drainDueCronSchedules({batchSize: 1000, jitterWindowSeconds: 0});
|
|
133
|
+
|
|
134
|
+
expect(callsWithKey(key)).toHaveLength(1);
|
|
135
|
+
const row = await reload(subscription.id);
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
expect(row.nextFireAt.getTime()).toBeGreaterThan(now);
|
|
138
|
+
expect(row.nextFireAt.getTime()).toBeLessThan(now + 6 * MINUTE_MS);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('passes the deterministic idempotency key and the subscription inputs to runWorkflow', async () => {
|
|
142
|
+
const {subscription, key} = await createCronSchedule({
|
|
143
|
+
nextFireAt: new Date(Date.now() - MINUTE_MS),
|
|
144
|
+
config: {with: {environment: 'staging'}},
|
|
145
|
+
});
|
|
146
|
+
runWorkflow.mockResolvedValue({id: crypto.randomUUID(), name: 'r'});
|
|
147
|
+
|
|
148
|
+
await drainDueCronSchedules({batchSize: 1000, jitterWindowSeconds: 0});
|
|
149
|
+
|
|
150
|
+
const [params] = callsWithKey(key)[0] as [Record<string, unknown>];
|
|
151
|
+
expect(params.triggerIdempotencyKey).toBe(key);
|
|
152
|
+
expect(params.inputs).toEqual({environment: 'staging'});
|
|
153
|
+
expect(params.triggerPayload).toEqual({
|
|
154
|
+
provider: 'cron',
|
|
155
|
+
source: 'cron',
|
|
156
|
+
event: 'tick',
|
|
157
|
+
scheduleId: subscription.id,
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('records a dispatch-error decision and still advances on a permanent runWorkflow failure', async () => {
|
|
162
|
+
const {subscription, key} = await createCronSchedule({
|
|
163
|
+
nextFireAt: new Date(Date.now() - MINUTE_MS),
|
|
164
|
+
});
|
|
165
|
+
runWorkflow.mockRejectedValue(new DefinitionNotFoundError('def-gone'));
|
|
166
|
+
|
|
167
|
+
await drainDueCronSchedules({batchSize: 1000, jitterWindowSeconds: 0});
|
|
168
|
+
|
|
169
|
+
const [event] = await db()
|
|
170
|
+
.select()
|
|
171
|
+
.from(triggersReceivedEvents)
|
|
172
|
+
.where(eq(triggersReceivedEvents.eventRef, key));
|
|
173
|
+
if (!event) throw new Error('received event not found');
|
|
174
|
+
expect(event.outcome).toBe('errored');
|
|
175
|
+
const decisions = await db()
|
|
176
|
+
.select()
|
|
177
|
+
.from(triggersDecisions)
|
|
178
|
+
.where(eq(triggersDecisions.receivedEventId, event.id));
|
|
179
|
+
expect(decisions[0]?.decision).toBe('dispatch-error');
|
|
180
|
+
const row = await reload(subscription.id);
|
|
181
|
+
expect(row.nextFireAt.getTime()).toBeGreaterThan(Date.now());
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('leaves the schedule due and unfired on a transient runWorkflow failure', async () => {
|
|
185
|
+
const {subscription, schedule} = await createCronSchedule({
|
|
186
|
+
nextFireAt: new Date(Date.now() - MINUTE_MS),
|
|
187
|
+
});
|
|
188
|
+
runWorkflow.mockRejectedValue(new Error('database unavailable'));
|
|
189
|
+
|
|
190
|
+
await drainDueCronSchedules({batchSize: 1000, jitterWindowSeconds: 0});
|
|
191
|
+
|
|
192
|
+
const row = await reload(subscription.id);
|
|
193
|
+
expect(row.nextFireAt.getTime()).toBe(schedule.nextFireAt.getTime());
|
|
194
|
+
expect(row.lastFiredAt).toBeNull();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('invokes the liveness callback once per claimed schedule', async () => {
|
|
198
|
+
await createCronSchedule({nextFireAt: new Date(Date.now() - MINUTE_MS)});
|
|
199
|
+
runWorkflow.mockResolvedValue({id: crypto.randomUUID(), name: 'r'});
|
|
200
|
+
const onScheduleProcessed = vi.fn();
|
|
201
|
+
|
|
202
|
+
const summary = await drainDueCronSchedules({
|
|
203
|
+
batchSize: 1000,
|
|
204
|
+
jitterWindowSeconds: 0,
|
|
205
|
+
onScheduleProcessed,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(summary.claimed).toBeGreaterThanOrEqual(1);
|
|
209
|
+
expect(onScheduleProcessed).toHaveBeenCalledTimes(summary.claimed);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('claims at most batchSize schedules per drain and leaves the rest due', async () => {
|
|
213
|
+
const mine: DueCron[] = [];
|
|
214
|
+
for (let index = 0; index < 5; index += 1) {
|
|
215
|
+
mine.push(await createCronSchedule({nextFireAt: new Date(Date.now() - MINUTE_MS)}));
|
|
216
|
+
}
|
|
217
|
+
runWorkflow.mockResolvedValue({id: crypto.randomUUID(), name: 'r'});
|
|
218
|
+
|
|
219
|
+
const summary = await drainDueCronSchedules({batchSize: 3, jitterWindowSeconds: 0});
|
|
220
|
+
|
|
221
|
+
expect(summary.claimed).toBe(3);
|
|
222
|
+
expect(runWorkflow).toHaveBeenCalledTimes(3);
|
|
223
|
+
// The capped drain advanced at most 3 rows total, so at least 2 of my 5 stay due.
|
|
224
|
+
let stillDue = 0;
|
|
225
|
+
for (const {subscription} of mine) {
|
|
226
|
+
const row = await reload(subscription.id);
|
|
227
|
+
if (row.nextFireAt.getTime() <= Date.now()) stillDue += 1;
|
|
228
|
+
}
|
|
229
|
+
expect(stillDue).toBeGreaterThanOrEqual(2);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test('two concurrent drains fire each due schedule exactly once (SKIP LOCKED)', async () => {
|
|
233
|
+
const mine: DueCron[] = [];
|
|
234
|
+
for (let index = 0; index < 4; index += 1) {
|
|
235
|
+
mine.push(await createCronSchedule({nextFireAt: new Date(Date.now() - MINUTE_MS)}));
|
|
236
|
+
}
|
|
237
|
+
// Hold the batch locks briefly so both drains are in flight while claiming.
|
|
238
|
+
runWorkflow.mockImplementation(async () => {
|
|
239
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
240
|
+
return {id: crypto.randomUUID(), name: 'r'};
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
await Promise.all([
|
|
244
|
+
drainDueCronSchedules({batchSize: 1000, jitterWindowSeconds: 0}),
|
|
245
|
+
drainDueCronSchedules({batchSize: 1000, jitterWindowSeconds: 0}),
|
|
246
|
+
]);
|
|
247
|
+
|
|
248
|
+
for (const {subscription, schedule, key} of mine) {
|
|
249
|
+
expect(callsWithKey(key)).toHaveLength(1);
|
|
250
|
+
const row = await reload(subscription.id);
|
|
251
|
+
expect(row.lastFiredAt?.getTime()).toBe(schedule.nextFireAt.getTime());
|
|
252
|
+
expect(row.nextFireAt.getTime()).toBeGreaterThan(Date.now());
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('countDueCronSchedules (backlog gauge source)', () => {
|
|
258
|
+
test('counts a schedule whose next fire time is due', async () => {
|
|
259
|
+
await createCronSchedule({nextFireAt: new Date(Date.now() - MINUTE_MS)});
|
|
260
|
+
|
|
261
|
+
const due = await countDueCronSchedules();
|
|
262
|
+
|
|
263
|
+
expect(due).toBeGreaterThanOrEqual(1);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {logger} from '@shipfox/node-opentelemetry';
|
|
2
|
+
import {advanceCronSchedule, claimDueCronSchedules, selectDbNow} from '#db/cron-schedules.js';
|
|
3
|
+
import {db} from '#db/db.js';
|
|
4
|
+
import {computeNextFireAt} from './compute-next-fire-at.js';
|
|
5
|
+
import {fireCronSubscription} from './fire-cron.js';
|
|
6
|
+
|
|
7
|
+
export interface DrainDueCronSchedulesParams {
|
|
8
|
+
readonly batchSize: number;
|
|
9
|
+
readonly jitterWindowSeconds: number;
|
|
10
|
+
/**
|
|
11
|
+
* Liveness signal invoked once per processed schedule (e.g. the Temporal activity
|
|
12
|
+
* heartbeat), so a batch stuck on a slow `runWorkflow` is visible rather than
|
|
13
|
+
* looking hung until the start-to-close timeout fires.
|
|
14
|
+
*/
|
|
15
|
+
readonly onScheduleProcessed?: (() => void) | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CronDrainSummary {
|
|
19
|
+
readonly claimed: number;
|
|
20
|
+
readonly fired: number;
|
|
21
|
+
readonly errored: number;
|
|
22
|
+
readonly retried: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Claims one bounded batch of due cron schedules and, per row, advances the schedule
|
|
27
|
+
* and fires the workflow. There is no internal drain loop, so the per-activity ceiling
|
|
28
|
+
* stays a hard `batchSize`.
|
|
29
|
+
*
|
|
30
|
+
* The batch is claimed and processed inside a single transaction, so its `FOR UPDATE
|
|
31
|
+
* SKIP LOCKED` locks are held until commit and concurrent activities/pods claim disjoint
|
|
32
|
+
* rows. Each row runs in a nested savepoint that advances `next_fire_at` before firing:
|
|
33
|
+
* on a transient failure the savepoint rolls back only that row's advance, leaving it due
|
|
34
|
+
* for retry, without blocking the rest of the batch. Firing before the advance commits
|
|
35
|
+
* makes a crash between the two re-fire the (idempotent) occurrence rather than skip it.
|
|
36
|
+
*/
|
|
37
|
+
export async function drainDueCronSchedules(
|
|
38
|
+
params: DrainDueCronSchedulesParams,
|
|
39
|
+
): Promise<CronDrainSummary> {
|
|
40
|
+
let claimed = 0;
|
|
41
|
+
let fired = 0;
|
|
42
|
+
let errored = 0;
|
|
43
|
+
let retried = 0;
|
|
44
|
+
|
|
45
|
+
await db().transaction(async (tx) => {
|
|
46
|
+
// One database-clock reading shared by the due check and every advance, so the two
|
|
47
|
+
// never disagree under application/database clock skew.
|
|
48
|
+
const now = await selectDbNow(tx);
|
|
49
|
+
const due = await claimDueCronSchedules({tx, limit: params.batchSize, now});
|
|
50
|
+
claimed = due.length;
|
|
51
|
+
|
|
52
|
+
for (const schedule of due) {
|
|
53
|
+
const scheduledSlot = schedule.nextFireAt;
|
|
54
|
+
try {
|
|
55
|
+
const result = await tx.transaction(async (rowTx) => {
|
|
56
|
+
const nextFireAt = computeNextFireAt({
|
|
57
|
+
cronExpression: schedule.cronExpression,
|
|
58
|
+
timezone: schedule.timezone,
|
|
59
|
+
from: now,
|
|
60
|
+
subscriptionId: schedule.subscriptionId,
|
|
61
|
+
jitterWindowSeconds: params.jitterWindowSeconds,
|
|
62
|
+
});
|
|
63
|
+
await advanceCronSchedule({
|
|
64
|
+
tx: rowTx,
|
|
65
|
+
subscriptionId: schedule.subscriptionId,
|
|
66
|
+
nextFireAt,
|
|
67
|
+
lastFiredAt: scheduledSlot,
|
|
68
|
+
});
|
|
69
|
+
return await fireCronSubscription({
|
|
70
|
+
subscriptionId: schedule.subscriptionId,
|
|
71
|
+
scheduledSlot,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
if (result.outcome === 'fired') fired += 1;
|
|
75
|
+
else errored += 1;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
// Transient failure: the savepoint rolled back the advance, so the schedule
|
|
78
|
+
// stays due and the next tick (or an activity retry) re-claims it.
|
|
79
|
+
retried += 1;
|
|
80
|
+
logger().warn(
|
|
81
|
+
{err: error, subscriptionId: schedule.subscriptionId},
|
|
82
|
+
'cron drain: transient fire failure; schedule left due for retry',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
params.onScheduleProcessed?.();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return {claimed, fired, errored, retried};
|
|
90
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type {JobListenerMatcherKind} from './job-listener-subscription.js';
|
|
2
|
+
|
|
3
|
+
export const triggerDecisionOutcomes = ['triggered', 'filter-error', 'dispatch-error'] as const;
|
|
4
|
+
export type TriggerDecisionOutcome = (typeof triggerDecisionOutcomes)[number];
|
|
5
|
+
export type TriggerDecisionSubscriptionKind = 'trigger' | 'listener';
|
|
6
|
+
|
|
7
|
+
export interface TriggerDecision {
|
|
8
|
+
id: string;
|
|
9
|
+
receivedEventId: string;
|
|
10
|
+
subscriptionKind: TriggerDecisionSubscriptionKind;
|
|
11
|
+
subscriptionId: string;
|
|
12
|
+
subscriptionName: string;
|
|
13
|
+
workflowDefinitionId: string | null;
|
|
14
|
+
projectId: string | null;
|
|
15
|
+
workflowRunId: string | null;
|
|
16
|
+
jobId: string | null;
|
|
17
|
+
matcherKind: JobListenerMatcherKind | null;
|
|
18
|
+
matcherOrdinal: number | null;
|
|
19
|
+
decision: TriggerDecisionOutcome;
|
|
20
|
+
runId: string | null;
|
|
21
|
+
runName: string | null;
|
|
22
|
+
reason: string | null;
|
|
23
|
+
createdAt: Date;
|
|
24
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type JobListenerMatcherKind = 'on' | 'until';
|
|
2
|
+
|
|
3
|
+
export interface JobListenerSubscription {
|
|
4
|
+
id: string;
|
|
5
|
+
workspaceId: string;
|
|
6
|
+
workflowRunId: string;
|
|
7
|
+
jobId: string;
|
|
8
|
+
kind: JobListenerMatcherKind;
|
|
9
|
+
matcherOrdinal: number;
|
|
10
|
+
source: string;
|
|
11
|
+
event: string;
|
|
12
|
+
config: Record<string, unknown>;
|
|
13
|
+
createdAt: Date;
|
|
14
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const triggerEventOrigins = ['integration', 'manual', 'cron'] as const;
|
|
2
|
+
export type TriggerEventOrigin = (typeof triggerEventOrigins)[number];
|
|
3
|
+
|
|
4
|
+
export const triggerEventOutcomes = [
|
|
5
|
+
'received',
|
|
6
|
+
'routed',
|
|
7
|
+
'discarded',
|
|
8
|
+
'failed',
|
|
9
|
+
'errored',
|
|
10
|
+
] as const;
|
|
11
|
+
export type TriggerEventOutcome = (typeof triggerEventOutcomes)[number];
|
|
12
|
+
|
|
13
|
+
export interface TriggerReceivedEvent {
|
|
14
|
+
id: string;
|
|
15
|
+
eventRef: string;
|
|
16
|
+
origin: TriggerEventOrigin;
|
|
17
|
+
workspaceId: string;
|
|
18
|
+
provider: string | null;
|
|
19
|
+
source: string;
|
|
20
|
+
event: string;
|
|
21
|
+
deliveryId: string | null;
|
|
22
|
+
connectionId: string | null;
|
|
23
|
+
connectionName: string | null;
|
|
24
|
+
outcome: TriggerEventOutcome;
|
|
25
|
+
matchedCount: number;
|
|
26
|
+
payload: Record<string, unknown> | null;
|
|
27
|
+
receivedAt: Date;
|
|
28
|
+
processedAt: Date | null;
|
|
29
|
+
createdAt: Date;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Trigger event shape for list read models.
|
|
34
|
+
* It omits payload because webhook bodies can be large/untrusted and only detail views render them.
|
|
35
|
+
*/
|
|
36
|
+
export type TriggerReceivedEventSummary = Omit<TriggerReceivedEvent, 'payload'>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface TriggerSubscription {
|
|
2
|
+
id: string;
|
|
3
|
+
workspaceId: string;
|
|
4
|
+
projectId: string;
|
|
5
|
+
workflowDefinitionId: string;
|
|
6
|
+
name: string;
|
|
7
|
+
source: string;
|
|
8
|
+
event: string;
|
|
9
|
+
config: Record<string, unknown>;
|
|
10
|
+
createdAt: Date;
|
|
11
|
+
updatedAt: Date;
|
|
12
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export class TriggerSubscriptionNotFoundError extends Error {
|
|
2
|
+
readonly subscriptionId: string;
|
|
3
|
+
|
|
4
|
+
constructor(subscriptionId: string) {
|
|
5
|
+
super(`Trigger subscription not found: ${subscriptionId}`);
|
|
6
|
+
this.name = 'TriggerSubscriptionNotFoundError';
|
|
7
|
+
this.subscriptionId = subscriptionId;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class ManualTriggerNotFoundError extends Error {
|
|
12
|
+
readonly workflowDefinitionId: string;
|
|
13
|
+
|
|
14
|
+
constructor(workflowDefinitionId: string) {
|
|
15
|
+
super(`Workflow definition ${workflowDefinitionId} has no manual trigger`);
|
|
16
|
+
this.name = 'ManualTriggerNotFoundError';
|
|
17
|
+
this.workflowDefinitionId = workflowDefinitionId;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class TriggerSubscriptionNotManualError extends Error {
|
|
22
|
+
readonly subscriptionId: string;
|
|
23
|
+
readonly source: string;
|
|
24
|
+
|
|
25
|
+
constructor(subscriptionId: string, source: string) {
|
|
26
|
+
super(
|
|
27
|
+
`Trigger subscription ${subscriptionId} has source '${source}', expected 'manual' for manual fire`,
|
|
28
|
+
);
|
|
29
|
+
this.name = 'TriggerSubscriptionNotManualError';
|
|
30
|
+
this.subscriptionId = subscriptionId;
|
|
31
|
+
this.source = source;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class TriggerSubscriptionNotCronError extends Error {
|
|
36
|
+
readonly subscriptionId: string;
|
|
37
|
+
readonly source: string;
|
|
38
|
+
|
|
39
|
+
constructor(subscriptionId: string, source: string) {
|
|
40
|
+
super(
|
|
41
|
+
`Trigger subscription ${subscriptionId} has source '${source}', expected 'cron' for cron fire`,
|
|
42
|
+
);
|
|
43
|
+
this.name = 'TriggerSubscriptionNotCronError';
|
|
44
|
+
this.subscriptionId = subscriptionId;
|
|
45
|
+
this.source = source;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class TriggerWorkspaceMismatchError extends Error {
|
|
50
|
+
readonly subscriptionId: string;
|
|
51
|
+
readonly subscriptionWorkspaceId: string;
|
|
52
|
+
readonly callerWorkspaceId: string;
|
|
53
|
+
|
|
54
|
+
constructor(subscriptionId: string, subscriptionWorkspaceId: string, callerWorkspaceId: string) {
|
|
55
|
+
super(
|
|
56
|
+
`Trigger subscription ${subscriptionId} belongs to workspace ${subscriptionWorkspaceId}, not ${callerWorkspaceId}`,
|
|
57
|
+
);
|
|
58
|
+
this.name = 'TriggerWorkspaceMismatchError';
|
|
59
|
+
this.subscriptionId = subscriptionId;
|
|
60
|
+
this.subscriptionWorkspaceId = subscriptionWorkspaceId;
|
|
61
|
+
this.callerWorkspaceId = callerWorkspaceId;
|
|
62
|
+
}
|
|
63
|
+
}
|