@shware/workflow 3.1.0 → 3.2.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/README.md +4 -2
- package/dist/aws/engine-step.cjs +2 -3
- package/dist/aws/engine-step.cjs.map +1 -1
- package/dist/aws/engine-step.d.cts +10 -0
- package/dist/aws/engine-step.d.cts.map +1 -1
- package/dist/aws/engine-step.d.mts +10 -0
- package/dist/aws/engine-step.d.mts.map +1 -1
- package/dist/aws/engine-step.mjs +2 -3
- package/dist/aws/engine-step.mjs.map +1 -1
- package/dist/aws/index.cjs +9 -1
- package/dist/aws/index.cjs.map +1 -1
- package/dist/aws/index.d.cts +6 -0
- package/dist/aws/index.d.cts.map +1 -1
- package/dist/aws/index.d.mts +6 -0
- package/dist/aws/index.d.mts.map +1 -1
- package/dist/aws/index.mjs +9 -1
- package/dist/aws/index.mjs.map +1 -1
- package/dist/cloudflare/runner.cjs +4 -5
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +4 -5
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/engine/duration.cjs +34 -0
- package/dist/engine/duration.cjs.map +1 -0
- package/dist/engine/duration.d.cts +5 -0
- package/dist/engine/duration.d.cts.map +1 -0
- package/dist/engine/duration.d.mts +5 -0
- package/dist/engine/duration.d.mts.map +1 -0
- package/dist/engine/duration.mjs +33 -0
- package/dist/engine/duration.mjs.map +1 -0
- package/dist/engine/index.cjs +5 -0
- package/dist/engine/index.d.cts +3 -1
- package/dist/engine/index.d.mts +3 -1
- package/dist/engine/index.mjs +3 -1
- package/dist/engine/retry.cjs +29 -0
- package/dist/engine/retry.cjs.map +1 -0
- package/dist/engine/retry.d.cts +17 -0
- package/dist/engine/retry.d.cts.map +1 -0
- package/dist/engine/retry.d.mts +17 -0
- package/dist/engine/retry.d.mts.map +1 -0
- package/dist/engine/retry.mjs +27 -0
- package/dist/engine/retry.mjs.map +1 -0
- package/dist/host/journey-host.cjs +15 -0
- package/dist/host/journey-host.cjs.map +1 -1
- package/dist/host/journey-host.d.cts.map +1 -1
- package/dist/host/journey-host.d.mts.map +1 -1
- package/dist/host/journey-host.mjs +15 -0
- package/dist/host/journey-host.mjs.map +1 -1
- package/dist/transports/cloudflare-email.cjs +4 -0
- package/dist/transports/cloudflare-email.cjs.map +1 -1
- package/dist/transports/cloudflare-email.d.cts +2 -0
- package/dist/transports/cloudflare-email.d.cts.map +1 -1
- package/dist/transports/cloudflare-email.d.mts +2 -0
- package/dist/transports/cloudflare-email.d.mts.map +1 -1
- package/dist/transports/cloudflare-email.mjs +4 -0
- package/dist/transports/cloudflare-email.mjs.map +1 -1
- package/dist/transports/ses.cjs +4 -0
- package/dist/transports/ses.cjs.map +1 -1
- package/dist/transports/ses.d.cts +2 -0
- package/dist/transports/ses.d.cts.map +1 -1
- package/dist/transports/ses.d.mts +2 -0
- package/dist/transports/ses.d.mts.map +1 -1
- package/dist/transports/ses.mjs +4 -0
- package/dist/transports/ses.mjs.map +1 -1
- package/dist/transports/sigv4.cjs +6 -1
- package/dist/transports/sigv4.cjs.map +1 -1
- package/dist/transports/sigv4.d.cts.map +1 -1
- package/dist/transports/sigv4.d.mts.map +1 -1
- package/dist/transports/sigv4.mjs +6 -1
- package/dist/transports/sigv4.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.mjs","names":[],"sources":["../../src/cloudflare/runner.ts"],"sourcesContent":["import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers';\nimport { NonRetryableError } from 'cloudflare:workflows';\nimport type { Contract } from '../contract';\nimport { type RegisteredAction, RegistryActionInvoker } from '../engine/actions';\nimport type {\n ActionInvoker,\n EngineStep,\n JourneyObserver,\n JourneyOutcome,\n MessageSender,\n MessageValidator,\n} from '../engine/ports';\nimport { type JourneyRunParts, executeJourney } from '../host/execute';\nimport { LogMessageSender, WebhookMessageSender } from '../host/senders';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport { type JourneyEnv, type JourneyParams, WAKE_EVENT_TYPE, wakeExpired } from './bindings';\nimport { d1Store } from './d1-env';\n\n/**\n * Adapts CF's WorkflowStep to EngineStep.\n * subscribe/unsubscribe write the subscription table inside durable steps (the\n * router wakes instances from that table); waitForWake parks on CF's\n * waitForEvent, which signals a timeout by throwing — and so does every other\n * failure in there, so only the clock can tell them apart (see wakeExpired).\n */\n/** Refusals a retry would only repeat: a template's props, an emitted event's payload. */\nconst NON_RETRYABLE = new Set(['TemplateValidationError', 'ContractValidationError']);\n\nclass CfEngineStep implements EngineStep {\n constructor(\n private readonly step: WorkflowStep,\n private readonly store: JourneyStore,\n private readonly userId: string,\n private readonly instanceId: string\n ) {}\n\n /**\n * A template refusing its props, or the contract refusing an emitted event,\n * is deterministic — a retry hits the same refusal — so it ends the step at\n * once instead of burning the retry budget.\n */\n do<T>(name: string, fn: () => Promise<T>): Promise<T> {\n return this.step.do(name, async () => {\n try {\n return await fn();\n } catch (error) {\n if (error instanceof Error && NON_RETRYABLE.has(error.name)) {\n throw new NonRetryableError(error.message, error.name);\n }\n throw error;\n }\n });\n }\n\n sleep(name: string, ms: number): Promise<void> {\n return this.step.sleep(name, ms);\n }\n\n sleepUntil(name: string, timestampMs: number): Promise<void> {\n return this.step.sleepUntil(name, timestampMs);\n }\n\n subscribe(name: string, events: readonly string[]): Promise<void> {\n // Idempotent in the store: the interpreter re-subscribes on every wait\n // attempt (one-shot-callback contract) and registrations persist here.\n return this.step.do(name, () =>\n this.store.subscribe(this.instanceId, this.userId, events, Date.now())\n );\n }\n\n unsubscribe(name: string): Promise<void> {\n return this.step.do(name, () => this.store.unsubscribe(this.instanceId));\n }\n\n async waitForWake(name: string, timeoutMs: number): Promise<'event' | 'timeout'> {\n const startedAt = Date.now();\n try {\n await this.step.waitForEvent(name, { type: WAKE_EVENT_TYPE, timeout: timeoutMs });\n return 'event';\n } catch (error) {\n /*\n * Only the deadline can tell a real timeout from a failure (see\n * wakeExpired). `startedAt` is not replay-stable, but it is read only\n * inside the step body below, and a step body runs on the live attempt\n * alone — a replay returns the persisted verdict without touching the\n * clock.\n */\n const timedOut = await this.step.do(`${name}:timed-out`, async () =>\n wakeExpired(startedAt, timeoutMs, Date.now())\n );\n if (timedOut) return 'timeout';\n throw error;\n }\n }\n}\n\n/**\n * The generic journey executor: one class runs every workflow, because IR is\n * data. It is loaded from the store by the contentHash pinned at entry —\n * content addressing makes it immutable, so reading it outside a step is\n * replay-safe.\n */\nexport class JourneyRunner extends WorkflowEntrypoint<JourneyEnv, JourneyParams> {\n /**\n * The data plane. D1 + KV from the env by default; an app on Postgres\n * overrides this (and `closeStore` if its client needs closing):\n *\n * protected override createStore() { return new PostgresJourneyStore(client(this.env)); }\n */\n protected createStore(): JourneyStore {\n return d1Store(this.env);\n }\n\n /** Called once `run` is over, success or not — release whatever `createStore` opened. */\n protected async closeStore(_store: JourneyStore): Promise<void> {}\n\n /**\n * Runtime options for this instance — typed configuration, resolved by the app (typically\n * `resolveRuntime(config.runtime, mode)` over its workflow.config.ts, with the mode coming from\n * an ENVIRONMENT var). The instance params are passed so the decision can also be per user:\n * a tester account in production can run in fast-forward while everyone else runs in real\n * time. Defaults: real time, quiet.\n *\n * protected override runtime({ userId }) {\n * return resolveRuntime(workflowConfig.runtime, isTester(userId) ? 'test' : this.env.ENVIRONMENT);\n * }\n */\n protected runtime(_params: JourneyParams): JourneyRuntimeOptions {\n return resolveRuntime(undefined, 'production');\n }\n\n /**\n * Message outlet; an app overrides this to plug in real channels (say\n * an EmailSender over a transport with a react-email renderer). By default it picks the webhook\n * or logging sender based on the environment.\n */\n protected createMessageSender(): MessageSender {\n return this.env.MESSAGE_WEBHOOK_URL\n ? new WebhookMessageSender(this.env.MESSAGE_WEBHOOK_URL)\n : new LogMessageSender();\n }\n\n /**\n * Custom-action registry; an app overrides this returning the same\n * `action(...)` objects its workflows reference (an ActionRef is a\n * RegisteredAction structurally) — the code plane of the dual-plane model:\n *\n * protected override actions() { return [syncCrm, issueCoupon]; }\n */\n protected actions(): readonly RegisteredAction[] {\n return [];\n }\n\n /**\n * The contract events this journey emits (`.sendEvent()`) are checked\n * against, exactly like ingest; journeyWorker passes its config's.\n */\n protected contract(): Contract | undefined {\n return undefined;\n }\n\n /**\n * Template props check before every send; an app overrides this with\n * `registryValidator({ emails, pushes })` (journeyWorker does it from its\n * registries). By default props go out as resolved.\n */\n protected createMessageValidator(): MessageValidator | undefined {\n return undefined;\n }\n\n /**\n * Reporting hooks: node arrivals, every copy sent, how each instance ended. The host stores\n * what it needs for its dashboards; by default nothing is reported.\n */\n protected createObserver(_store: JourneyStore): JourneyObserver | undefined {\n return undefined;\n }\n\n /** Override for a different version policy ('warn' by default) or a fully custom invoker (e.g. webhook degradation for hosted tenants). */\n protected createActionInvoker(): ActionInvoker {\n return new RegistryActionInvoker(this.actions());\n }\n\n /**\n * Everything one run is wired to. By default it is composed from the methods above; a host\n * that assembles the whole set itself (journeyWorker) overrides this one instead.\n */\n protected createRunParts(store: JourneyStore, params: JourneyParams): JourneyRunParts {\n return {\n store,\n env: this.env,\n runtime: this.runtime(params),\n messages: this.createMessageSender(),\n actions: this.createActionInvoker(),\n contract: this.contract(),\n validator: this.createMessageValidator(),\n observer: this.createObserver(store),\n };\n }\n\n async run(event: WorkflowEvent<JourneyParams>, step: WorkflowStep): Promise<JourneyOutcome> {\n const store = this.createStore();\n try {\n const { userId } = event.payload;\n return await executeJourney(\n {\n instanceId: event.instanceId,\n params: event.payload,\n // Trigger-event creation time: replay-stable, unlike Date.now() here\n enteredAtMs: event.timestamp.getTime(),\n step: new CfEngineStep(step, store, userId, event.instanceId),\n },\n this.createRunParts(store, event.payload)\n );\n } finally {\n await this.closeStore(store);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA2BA,MAAM,gCAAgB,IAAI,IAAI,CAAC,2BAA2B,yBAAyB,CAAC;AAEpF,IAAM,eAAN,MAAyC;CAEpB;CACA;CACA;CACA;CAJnB,YACE,MACA,OACA,QACA,YACA;EAJiB,KAAA,OAAA;EACA,KAAA,QAAA;EACA,KAAA,SAAA;EACA,KAAA,aAAA;CAChB;;;;;;CAOH,GAAM,MAAc,IAAkC;EACpD,OAAO,KAAK,KAAK,GAAG,MAAM,YAAY;GACpC,IAAI;IACF,OAAO,MAAM,GAAG;GAClB,SAAS,OAAO;IACd,IAAI,iBAAiB,SAAS,cAAc,IAAI,MAAM,IAAI,GACxD,MAAM,IAAI,kBAAkB,MAAM,SAAS,MAAM,IAAI;IAEvD,MAAM;GACR;EACF,CAAC;CACH;CAEA,MAAM,MAAc,IAA2B;EAC7C,OAAO,KAAK,KAAK,MAAM,MAAM,EAAE;CACjC;CAEA,WAAW,MAAc,aAAoC;EAC3D,OAAO,KAAK,KAAK,WAAW,MAAM,WAAW;CAC/C;CAEA,UAAU,MAAc,QAA0C;EAGhE,OAAO,KAAK,KAAK,GAAG,YAClB,KAAK,MAAM,UAAU,KAAK,YAAY,KAAK,QAAQ,QAAQ,KAAK,IAAI,CAAC,CACvE;CACF;CAEA,YAAY,MAA6B;EACvC,OAAO,KAAK,KAAK,GAAG,YAAY,KAAK,MAAM,YAAY,KAAK,UAAU,CAAC;CACzE;CAEA,MAAM,YAAY,MAAc,WAAiD;EAC/E,MAAM,YAAY,KAAK,IAAI;EAC3B,IAAI;GACF,MAAM,KAAK,KAAK,aAAa,MAAM;IAAE,MAAM;IAAiB,SAAS;GAAU,CAAC;GAChF,OAAO;EACT,SAAS,OAAO;GAWd,IAAI,MAHmB,KAAK,KAAK,GAAG,GAAG,KAAK,aAAa,YACvD,YAAY,WAAW,WAAW,KAAK,IAAI,CAAC,CAC9C,GACc,OAAO;GACrB,MAAM;EACR;CACF;AACF;;;;;;;AAQA,IAAa,gBAAb,cAAmC,mBAA8C;;;;;;;CAO/E,cAAsC;EACpC,OAAO,QAAQ,KAAK,GAAG;CACzB;;CAGA,MAAgB,WAAW,QAAqC,CAAC;;;;;;;;;;;;CAajE,QAAkB,SAA+C;EAC/D,OAAO,eAAe,KAAA,GAAW,YAAY;CAC/C;;;;;;CAOA,sBAA+C;EAC7C,OAAO,KAAK,IAAI,sBACZ,IAAI,qBAAqB,KAAK,IAAI,mBAAmB,IACrD,IAAI,iBAAiB;CAC3B;;;;;;;;CASA,UAAiD;EAC/C,OAAO,CAAC;CACV;;;;;CAMA,WAA2C,CAE3C;;;;;;CAOA,yBAAiE,CAEjE;;;;;CAMA,eAAyB,QAAmD,CAE5E;;CAGA,sBAA+C;EAC7C,OAAO,IAAI,sBAAsB,KAAK,QAAQ,CAAC;CACjD;;;;;CAMA,eAAyB,OAAqB,QAAwC;EACpF,OAAO;GACL;GACA,KAAK,KAAK;GACV,SAAS,KAAK,QAAQ,MAAM;GAC5B,UAAU,KAAK,oBAAoB;GACnC,SAAS,KAAK,oBAAoB;GAClC,UAAU,KAAK,SAAS;GACxB,WAAW,KAAK,uBAAuB;GACvC,UAAU,KAAK,eAAe,KAAK;EACrC;CACF;CAEA,MAAM,IAAI,OAAqC,MAA6C;EAC1F,MAAM,QAAQ,KAAK,YAAY;EAC/B,IAAI;GACF,MAAM,EAAE,WAAW,MAAM;GACzB,OAAO,MAAM,eACX;IACE,YAAY,MAAM;IAClB,QAAQ,MAAM;IAEd,aAAa,MAAM,UAAU,QAAQ;IACrC,MAAM,IAAI,aAAa,MAAM,OAAO,QAAQ,MAAM,UAAU;GAC9D,GACA,KAAK,eAAe,OAAO,MAAM,OAAO,CAC1C;EACF,UAAU;GACR,MAAM,KAAK,WAAW,KAAK;EAC7B;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"runner.mjs","names":[],"sources":["../../src/cloudflare/runner.ts"],"sourcesContent":["import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers';\nimport { NonRetryableError } from 'cloudflare:workflows';\nimport type { Contract } from '../contract';\nimport { type RegisteredAction, RegistryActionInvoker } from '../engine/actions';\nimport type {\n ActionInvoker,\n EngineStep,\n JourneyObserver,\n JourneyOutcome,\n MessageSender,\n MessageValidator,\n} from '../engine/ports';\nimport { isRetryable } from '../engine/retry';\nimport { type JourneyRunParts, executeJourney } from '../host/execute';\nimport { LogMessageSender, WebhookMessageSender } from '../host/senders';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport { type JourneyEnv, type JourneyParams, WAKE_EVENT_TYPE, wakeExpired } from './bindings';\nimport { d1Store } from './d1-env';\n\n/**\n * Adapts CF's WorkflowStep to EngineStep.\n * subscribe/unsubscribe write the subscription table inside durable steps (the\n * router wakes instances from that table); waitForWake parks on CF's\n * waitForEvent, which signals a timeout by throwing — and so does every other\n * failure in there, so only the clock can tell them apart (see wakeExpired).\n */\nclass CfEngineStep implements EngineStep {\n constructor(\n private readonly step: WorkflowStep,\n private readonly store: JourneyStore,\n private readonly userId: string,\n private readonly instanceId: string\n ) {}\n\n /**\n * A failure a retry would only repeat (`isRetryable`: a template refusing its props, the\n * contract refusing an emitted event, a provider rejecting the request) ends the step at\n * once instead of burning the retry budget.\n */\n do<T>(name: string, fn: () => Promise<T>): Promise<T> {\n return this.step.do(name, async () => {\n try {\n return await fn();\n } catch (error) {\n if (error instanceof Error && !isRetryable(error)) {\n throw new NonRetryableError(error.message, error.name);\n }\n throw error;\n }\n });\n }\n\n sleep(name: string, ms: number): Promise<void> {\n return this.step.sleep(name, ms);\n }\n\n sleepUntil(name: string, timestampMs: number): Promise<void> {\n return this.step.sleepUntil(name, timestampMs);\n }\n\n subscribe(name: string, events: readonly string[]): Promise<void> {\n // Idempotent in the store: the interpreter re-subscribes on every wait\n // attempt (one-shot-callback contract) and registrations persist here.\n return this.step.do(name, () =>\n this.store.subscribe(this.instanceId, this.userId, events, Date.now())\n );\n }\n\n unsubscribe(name: string): Promise<void> {\n return this.step.do(name, () => this.store.unsubscribe(this.instanceId));\n }\n\n async waitForWake(name: string, timeoutMs: number): Promise<'event' | 'timeout'> {\n const startedAt = Date.now();\n try {\n await this.step.waitForEvent(name, { type: WAKE_EVENT_TYPE, timeout: timeoutMs });\n return 'event';\n } catch (error) {\n /*\n * Only the deadline can tell a real timeout from a failure (see\n * wakeExpired). `startedAt` is not replay-stable, but it is read only\n * inside the step body below, and a step body runs on the live attempt\n * alone — a replay returns the persisted verdict without touching the\n * clock.\n */\n const timedOut = await this.step.do(`${name}:timed-out`, async () =>\n wakeExpired(startedAt, timeoutMs, Date.now())\n );\n if (timedOut) return 'timeout';\n throw error;\n }\n }\n}\n\n/**\n * The generic journey executor: one class runs every workflow, because IR is\n * data. It is loaded from the store by the contentHash pinned at entry —\n * content addressing makes it immutable, so reading it outside a step is\n * replay-safe.\n */\nexport class JourneyRunner extends WorkflowEntrypoint<JourneyEnv, JourneyParams> {\n /**\n * The data plane. D1 + KV from the env by default; an app on Postgres\n * overrides this (and `closeStore` if its client needs closing):\n *\n * protected override createStore() { return new PostgresJourneyStore(client(this.env)); }\n */\n protected createStore(): JourneyStore {\n return d1Store(this.env);\n }\n\n /** Called once `run` is over, success or not — release whatever `createStore` opened. */\n protected async closeStore(_store: JourneyStore): Promise<void> {}\n\n /**\n * Runtime options for this instance — typed configuration, resolved by the app (typically\n * `resolveRuntime(config.runtime, mode)` over its workflow.config.ts, with the mode coming from\n * an ENVIRONMENT var). The instance params are passed so the decision can also be per user:\n * a tester account in production can run in fast-forward while everyone else runs in real\n * time. Defaults: real time, quiet.\n *\n * protected override runtime({ userId }) {\n * return resolveRuntime(workflowConfig.runtime, isTester(userId) ? 'test' : this.env.ENVIRONMENT);\n * }\n */\n protected runtime(_params: JourneyParams): JourneyRuntimeOptions {\n return resolveRuntime(undefined, 'production');\n }\n\n /**\n * Message outlet; an app overrides this to plug in real channels (say\n * an EmailSender over a transport with a react-email renderer). By default it picks the webhook\n * or logging sender based on the environment.\n */\n protected createMessageSender(): MessageSender {\n return this.env.MESSAGE_WEBHOOK_URL\n ? new WebhookMessageSender(this.env.MESSAGE_WEBHOOK_URL)\n : new LogMessageSender();\n }\n\n /**\n * Custom-action registry; an app overrides this returning the same\n * `action(...)` objects its workflows reference (an ActionRef is a\n * RegisteredAction structurally) — the code plane of the dual-plane model:\n *\n * protected override actions() { return [syncCrm, issueCoupon]; }\n */\n protected actions(): readonly RegisteredAction[] {\n return [];\n }\n\n /**\n * The contract events this journey emits (`.sendEvent()`) are checked\n * against, exactly like ingest; journeyWorker passes its config's.\n */\n protected contract(): Contract | undefined {\n return undefined;\n }\n\n /**\n * Template props check before every send; an app overrides this with\n * `registryValidator({ emails, pushes })` (journeyWorker does it from its\n * registries). By default props go out as resolved.\n */\n protected createMessageValidator(): MessageValidator | undefined {\n return undefined;\n }\n\n /**\n * Reporting hooks: node arrivals, every copy sent, how each instance ended. The host stores\n * what it needs for its dashboards; by default nothing is reported.\n */\n protected createObserver(_store: JourneyStore): JourneyObserver | undefined {\n return undefined;\n }\n\n /** Override for a different version policy ('warn' by default) or a fully custom invoker (e.g. webhook degradation for hosted tenants). */\n protected createActionInvoker(): ActionInvoker {\n return new RegistryActionInvoker(this.actions());\n }\n\n /**\n * Everything one run is wired to. By default it is composed from the methods above; a host\n * that assembles the whole set itself (journeyWorker) overrides this one instead.\n */\n protected createRunParts(store: JourneyStore, params: JourneyParams): JourneyRunParts {\n return {\n store,\n env: this.env,\n runtime: this.runtime(params),\n messages: this.createMessageSender(),\n actions: this.createActionInvoker(),\n contract: this.contract(),\n validator: this.createMessageValidator(),\n observer: this.createObserver(store),\n };\n }\n\n async run(event: WorkflowEvent<JourneyParams>, step: WorkflowStep): Promise<JourneyOutcome> {\n const store = this.createStore();\n try {\n const { userId } = event.payload;\n return await executeJourney(\n {\n instanceId: event.instanceId,\n params: event.payload,\n // Trigger-event creation time: replay-stable, unlike Date.now() here\n enteredAtMs: event.timestamp.getTime(),\n step: new CfEngineStep(step, store, userId, event.instanceId),\n },\n this.createRunParts(store, event.payload)\n );\n } finally {\n await this.closeStore(store);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA2BA,IAAM,eAAN,MAAyC;CAEpB;CACA;CACA;CACA;CAJnB,YACE,MACA,OACA,QACA,YACA;EAJiB,KAAA,OAAA;EACA,KAAA,QAAA;EACA,KAAA,SAAA;EACA,KAAA,aAAA;CAChB;;;;;;CAOH,GAAM,MAAc,IAAkC;EACpD,OAAO,KAAK,KAAK,GAAG,MAAM,YAAY;GACpC,IAAI;IACF,OAAO,MAAM,GAAG;GAClB,SAAS,OAAO;IACd,IAAI,iBAAiB,SAAS,CAAC,YAAY,KAAK,GAC9C,MAAM,IAAI,kBAAkB,MAAM,SAAS,MAAM,IAAI;IAEvD,MAAM;GACR;EACF,CAAC;CACH;CAEA,MAAM,MAAc,IAA2B;EAC7C,OAAO,KAAK,KAAK,MAAM,MAAM,EAAE;CACjC;CAEA,WAAW,MAAc,aAAoC;EAC3D,OAAO,KAAK,KAAK,WAAW,MAAM,WAAW;CAC/C;CAEA,UAAU,MAAc,QAA0C;EAGhE,OAAO,KAAK,KAAK,GAAG,YAClB,KAAK,MAAM,UAAU,KAAK,YAAY,KAAK,QAAQ,QAAQ,KAAK,IAAI,CAAC,CACvE;CACF;CAEA,YAAY,MAA6B;EACvC,OAAO,KAAK,KAAK,GAAG,YAAY,KAAK,MAAM,YAAY,KAAK,UAAU,CAAC;CACzE;CAEA,MAAM,YAAY,MAAc,WAAiD;EAC/E,MAAM,YAAY,KAAK,IAAI;EAC3B,IAAI;GACF,MAAM,KAAK,KAAK,aAAa,MAAM;IAAE,MAAM;IAAiB,SAAS;GAAU,CAAC;GAChF,OAAO;EACT,SAAS,OAAO;GAWd,IAAI,MAHmB,KAAK,KAAK,GAAG,GAAG,KAAK,aAAa,YACvD,YAAY,WAAW,WAAW,KAAK,IAAI,CAAC,CAC9C,GACc,OAAO;GACrB,MAAM;EACR;CACF;AACF;;;;;;;AAQA,IAAa,gBAAb,cAAmC,mBAA8C;;;;;;;CAO/E,cAAsC;EACpC,OAAO,QAAQ,KAAK,GAAG;CACzB;;CAGA,MAAgB,WAAW,QAAqC,CAAC;;;;;;;;;;;;CAajE,QAAkB,SAA+C;EAC/D,OAAO,eAAe,KAAA,GAAW,YAAY;CAC/C;;;;;;CAOA,sBAA+C;EAC7C,OAAO,KAAK,IAAI,sBACZ,IAAI,qBAAqB,KAAK,IAAI,mBAAmB,IACrD,IAAI,iBAAiB;CAC3B;;;;;;;;CASA,UAAiD;EAC/C,OAAO,CAAC;CACV;;;;;CAMA,WAA2C,CAE3C;;;;;;CAOA,yBAAiE,CAEjE;;;;;CAMA,eAAyB,QAAmD,CAE5E;;CAGA,sBAA+C;EAC7C,OAAO,IAAI,sBAAsB,KAAK,QAAQ,CAAC;CACjD;;;;;CAMA,eAAyB,OAAqB,QAAwC;EACpF,OAAO;GACL;GACA,KAAK,KAAK;GACV,SAAS,KAAK,QAAQ,MAAM;GAC5B,UAAU,KAAK,oBAAoB;GACnC,SAAS,KAAK,oBAAoB;GAClC,UAAU,KAAK,SAAS;GACxB,WAAW,KAAK,uBAAuB;GACvC,UAAU,KAAK,eAAe,KAAK;EACrC;CACF;CAEA,MAAM,IAAI,OAAqC,MAA6C;EAC1F,MAAM,QAAQ,KAAK,YAAY;EAC/B,IAAI;GACF,MAAM,EAAE,WAAW,MAAM;GACzB,OAAO,MAAM,eACX;IACE,YAAY,MAAM;IAClB,QAAQ,MAAM;IAEd,aAAa,MAAM,UAAU,QAAQ;IACrC,MAAM,IAAI,aAAa,MAAM,OAAO,QAAQ,MAAM,UAAU;GAC9D,GACA,KAAK,eAAe,OAAO,MAAM,OAAO,CAC1C;EACF,UAAU;GACR,MAAM,KAAK,WAAW,KAAK;EAC7B;CACF;AACF"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/engine/duration.ts
|
|
3
|
+
/**
|
|
4
|
+
* The longest an instance of a workflow can live, from its IR: every delay at its longest, every
|
|
5
|
+
* wait to its timeout, a time window as a full week (the furthest the next allowed slot can be),
|
|
6
|
+
* the longest arm of a branch or cohort — and at least the goal's window, which keeps an instance
|
|
7
|
+
* watching after its last step. Message, action and event steps count as instant. A runtime that
|
|
8
|
+
* caps an instance's lifetime (a Lambda durable execution's `ExecutionTimeout`) checks its
|
|
9
|
+
* workflows against it, rather than finding out when the platform stops one midway.
|
|
10
|
+
*/
|
|
11
|
+
const WEEK_MS = 6048e5;
|
|
12
|
+
function flowMs(nodes) {
|
|
13
|
+
let total = 0;
|
|
14
|
+
for (const node of nodes) total += nodeMs(node);
|
|
15
|
+
return total;
|
|
16
|
+
}
|
|
17
|
+
function nodeMs(node) {
|
|
18
|
+
switch (node.type) {
|
|
19
|
+
case "delay": return node.duration.ms;
|
|
20
|
+
case "random_delay": return Math.max(node.min.ms, node.max.ms);
|
|
21
|
+
case "time_window": return WEEK_MS;
|
|
22
|
+
case "wait_until": return node.timeout.ms + (Array.isArray(node.onTimeout) ? flowMs(node.onTimeout) : 0);
|
|
23
|
+
case "branch": return Math.max(flowMs(node.otherwise ?? []), ...node.cases.map((branch) => flowMs(branch.flow)));
|
|
24
|
+
case "cohort": return Math.max(0, ...node.arms.map((arm) => flowMs(arm.flow)));
|
|
25
|
+
default: return 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function longestJourneyMs(ir) {
|
|
29
|
+
return Math.max(flowMs(ir.flow), ir.goal?.within?.ms ?? 0);
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
exports.longestJourneyMs = longestJourneyMs;
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=duration.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration.cjs","names":[],"sources":["../../src/engine/duration.ts"],"sourcesContent":["import type { NodeIR, WorkflowIR } from '../ir';\n\n/**\n * The longest an instance of a workflow can live, from its IR: every delay at its longest, every\n * wait to its timeout, a time window as a full week (the furthest the next allowed slot can be),\n * the longest arm of a branch or cohort — and at least the goal's window, which keeps an instance\n * watching after its last step. Message, action and event steps count as instant. A runtime that\n * caps an instance's lifetime (a Lambda durable execution's `ExecutionTimeout`) checks its\n * workflows against it, rather than finding out when the platform stops one midway.\n */\n\nconst WEEK_MS = 7 * 24 * 3_600_000;\n\nfunction flowMs(nodes: readonly NodeIR[]): number {\n let total = 0;\n for (const node of nodes) total += nodeMs(node);\n return total;\n}\n\nfunction nodeMs(node: NodeIR): number {\n switch (node.type) {\n case 'delay':\n return node.duration.ms;\n case 'random_delay':\n return Math.max(node.min.ms, node.max.ms);\n case 'time_window':\n return WEEK_MS;\n case 'wait_until':\n return node.timeout.ms + (Array.isArray(node.onTimeout) ? flowMs(node.onTimeout) : 0);\n case 'branch':\n return Math.max(\n flowMs(node.otherwise ?? []),\n ...node.cases.map((branch) => flowMs(branch.flow))\n );\n case 'cohort':\n return Math.max(0, ...node.arms.map((arm) => flowMs(arm.flow)));\n default:\n return 0;\n }\n}\n\nexport function longestJourneyMs(ir: Pick<WorkflowIR, 'flow' | 'goal'>): number {\n return Math.max(flowMs(ir.flow), ir.goal?.within?.ms ?? 0);\n}\n"],"mappings":";;;;;;;;;;AAWA,MAAM,UAAU;AAEhB,SAAS,OAAO,OAAkC;CAChD,IAAI,QAAQ;CACZ,KAAK,MAAM,QAAQ,OAAO,SAAS,OAAO,IAAI;CAC9C,OAAO;AACT;AAEA,SAAS,OAAO,MAAsB;CACpC,QAAQ,KAAK,MAAb;EACE,KAAK,SACH,OAAO,KAAK,SAAS;EACvB,KAAK,gBACH,OAAO,KAAK,IAAI,KAAK,IAAI,IAAI,KAAK,IAAI,EAAE;EAC1C,KAAK,eACH,OAAO;EACT,KAAK,cACH,OAAO,KAAK,QAAQ,MAAM,MAAM,QAAQ,KAAK,SAAS,IAAI,OAAO,KAAK,SAAS,IAAI;EACrF,KAAK,UACH,OAAO,KAAK,IACV,OAAO,KAAK,aAAa,CAAC,CAAC,GAC3B,GAAG,KAAK,MAAM,KAAK,WAAW,OAAO,OAAO,IAAI,CAAC,CACnD;EACF,KAAK,UACH,OAAO,KAAK,IAAI,GAAG,GAAG,KAAK,KAAK,KAAK,QAAQ,OAAO,IAAI,IAAI,CAAC,CAAC;EAChE,SACE,OAAO;CACX;AACF;AAEA,SAAgB,iBAAiB,IAA+C;CAC9E,OAAO,KAAK,IAAI,OAAO,GAAG,IAAI,GAAG,GAAG,MAAM,QAAQ,MAAM,CAAC;AAC3D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration.d.cts","names":[],"sources":["../../src/engine/duration.ts"],"mappings":";;wBAyCgB,iBAAiB,IAAI,KAAK"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration.d.mts","names":[],"sources":["../../src/engine/duration.ts"],"mappings":";;wBAyCgB,iBAAiB,IAAI,KAAK"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/engine/duration.ts
|
|
2
|
+
/**
|
|
3
|
+
* The longest an instance of a workflow can live, from its IR: every delay at its longest, every
|
|
4
|
+
* wait to its timeout, a time window as a full week (the furthest the next allowed slot can be),
|
|
5
|
+
* the longest arm of a branch or cohort — and at least the goal's window, which keeps an instance
|
|
6
|
+
* watching after its last step. Message, action and event steps count as instant. A runtime that
|
|
7
|
+
* caps an instance's lifetime (a Lambda durable execution's `ExecutionTimeout`) checks its
|
|
8
|
+
* workflows against it, rather than finding out when the platform stops one midway.
|
|
9
|
+
*/
|
|
10
|
+
const WEEK_MS = 6048e5;
|
|
11
|
+
function flowMs(nodes) {
|
|
12
|
+
let total = 0;
|
|
13
|
+
for (const node of nodes) total += nodeMs(node);
|
|
14
|
+
return total;
|
|
15
|
+
}
|
|
16
|
+
function nodeMs(node) {
|
|
17
|
+
switch (node.type) {
|
|
18
|
+
case "delay": return node.duration.ms;
|
|
19
|
+
case "random_delay": return Math.max(node.min.ms, node.max.ms);
|
|
20
|
+
case "time_window": return WEEK_MS;
|
|
21
|
+
case "wait_until": return node.timeout.ms + (Array.isArray(node.onTimeout) ? flowMs(node.onTimeout) : 0);
|
|
22
|
+
case "branch": return Math.max(flowMs(node.otherwise ?? []), ...node.cases.map((branch) => flowMs(branch.flow)));
|
|
23
|
+
case "cohort": return Math.max(0, ...node.arms.map((arm) => flowMs(arm.flow)));
|
|
24
|
+
default: return 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function longestJourneyMs(ir) {
|
|
28
|
+
return Math.max(flowMs(ir.flow), ir.goal?.within?.ms ?? 0);
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { longestJourneyMs };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=duration.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"duration.mjs","names":[],"sources":["../../src/engine/duration.ts"],"sourcesContent":["import type { NodeIR, WorkflowIR } from '../ir';\n\n/**\n * The longest an instance of a workflow can live, from its IR: every delay at its longest, every\n * wait to its timeout, a time window as a full week (the furthest the next allowed slot can be),\n * the longest arm of a branch or cohort — and at least the goal's window, which keeps an instance\n * watching after its last step. Message, action and event steps count as instant. A runtime that\n * caps an instance's lifetime (a Lambda durable execution's `ExecutionTimeout`) checks its\n * workflows against it, rather than finding out when the platform stops one midway.\n */\n\nconst WEEK_MS = 7 * 24 * 3_600_000;\n\nfunction flowMs(nodes: readonly NodeIR[]): number {\n let total = 0;\n for (const node of nodes) total += nodeMs(node);\n return total;\n}\n\nfunction nodeMs(node: NodeIR): number {\n switch (node.type) {\n case 'delay':\n return node.duration.ms;\n case 'random_delay':\n return Math.max(node.min.ms, node.max.ms);\n case 'time_window':\n return WEEK_MS;\n case 'wait_until':\n return node.timeout.ms + (Array.isArray(node.onTimeout) ? flowMs(node.onTimeout) : 0);\n case 'branch':\n return Math.max(\n flowMs(node.otherwise ?? []),\n ...node.cases.map((branch) => flowMs(branch.flow))\n );\n case 'cohort':\n return Math.max(0, ...node.arms.map((arm) => flowMs(arm.flow)));\n default:\n return 0;\n }\n}\n\nexport function longestJourneyMs(ir: Pick<WorkflowIR, 'flow' | 'goal'>): number {\n return Math.max(flowMs(ir.flow), ir.goal?.within?.ms ?? 0);\n}\n"],"mappings":";;;;;;;;;AAWA,MAAM,UAAU;AAEhB,SAAS,OAAO,OAAkC;CAChD,IAAI,QAAQ;CACZ,KAAK,MAAM,QAAQ,OAAO,SAAS,OAAO,IAAI;CAC9C,OAAO;AACT;AAEA,SAAS,OAAO,MAAsB;CACpC,QAAQ,KAAK,MAAb;EACE,KAAK,SACH,OAAO,KAAK,SAAS;EACvB,KAAK,gBACH,OAAO,KAAK,IAAI,KAAK,IAAI,IAAI,KAAK,IAAI,EAAE;EAC1C,KAAK,eACH,OAAO;EACT,KAAK,cACH,OAAO,KAAK,QAAQ,MAAM,MAAM,QAAQ,KAAK,SAAS,IAAI,OAAO,KAAK,SAAS,IAAI;EACrF,KAAK,UACH,OAAO,KAAK,IACV,OAAO,KAAK,aAAa,CAAC,CAAC,GAC3B,GAAG,KAAK,MAAM,KAAK,WAAW,OAAO,OAAO,IAAI,CAAC,CACnD;EACF,KAAK,UACH,OAAO,KAAK,IAAI,GAAG,GAAG,KAAK,KAAK,KAAK,QAAQ,OAAO,IAAI,IAAI,CAAC,CAAC;EAChE,SACE,OAAO;CACX;AACF;AAEA,SAAgB,iBAAiB,IAA+C;CAC9E,OAAO,KAAK,IAAI,OAAO,GAAG,IAAI,GAAG,GAAG,MAAM,QAAQ,MAAM,CAAC;AAC3D"}
|
package/dist/engine/index.cjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_engine_retry = require("./retry.cjs");
|
|
3
|
+
const require_engine_duration = require("./duration.cjs");
|
|
2
4
|
const require_engine_bucket = require("./bucket.cjs");
|
|
3
5
|
const require_engine_condition = require("./condition.cjs");
|
|
4
6
|
const require_engine_time_window = require("./time-window.cjs");
|
|
@@ -10,6 +12,9 @@ exports.PROFILE_UPDATED_EVENT = require_engine_condition.PROFILE_UPDATED_EVENT;
|
|
|
10
12
|
exports.RegistryActionInvoker = require_engine_actions.RegistryActionInvoker;
|
|
11
13
|
exports.evaluateCondition = require_engine_condition.evaluateCondition;
|
|
12
14
|
exports.hashToBucket = require_engine_bucket.hashToBucket;
|
|
15
|
+
exports.isRetryable = require_engine_retry.isRetryable;
|
|
16
|
+
exports.isRetryableStatus = require_engine_retry.isRetryableStatus;
|
|
17
|
+
exports.longestJourneyMs = require_engine_duration.longestJourneyMs;
|
|
13
18
|
exports.matchesWhere = require_engine_condition.matchesWhere;
|
|
14
19
|
exports.murmur3 = require_engine_bucket.murmur3;
|
|
15
20
|
exports.nextWindowStart = require_engine_time_window.nextWindowStart;
|
package/dist/engine/index.d.cts
CHANGED
|
@@ -5,4 +5,6 @@ import { DEVICES_UPDATED_EVENT, EvaluateOptions, PROFILE_UPDATED_EVENT, evaluate
|
|
|
5
5
|
import { runJourney } from "./interpreter.cjs";
|
|
6
6
|
import { scaleDurations, scaleSegmentWindows } from "./time-scale.cjs";
|
|
7
7
|
import { nextWindowStart, resolveTimeZone } from "./time-window.cjs";
|
|
8
|
-
|
|
8
|
+
import { longestJourneyMs } from "./duration.cjs";
|
|
9
|
+
import { isRetryable, isRetryableStatus } from "./retry.cjs";
|
|
10
|
+
export { type ActionInvocation, type ActionInvoker, type ActionVersionPolicy, DEVICES_UPDATED_EVENT, type EngineStep, type EvaluateOptions, type EventSink, type FactSource, type JourneyContext, type JourneyObserver, type JourneyOutcome, type LinkPurpose, type MessageSender, type MessageTracking, type OutboundMessage, PROFILE_UPDATED_EVENT, type PushDevice, type RegisteredAction, RegistryActionInvoker, type SendResult, evaluateCondition, hashToBucket, isRetryable, isRetryableStatus, longestJourneyMs, matchesWhere, murmur3, nextWindowStart, relevantEvents, resolveTimeZone, runJourney, scaleDurations, scaleSegmentWindows };
|
package/dist/engine/index.d.mts
CHANGED
|
@@ -5,4 +5,6 @@ import { DEVICES_UPDATED_EVENT, EvaluateOptions, PROFILE_UPDATED_EVENT, evaluate
|
|
|
5
5
|
import { runJourney } from "./interpreter.mjs";
|
|
6
6
|
import { scaleDurations, scaleSegmentWindows } from "./time-scale.mjs";
|
|
7
7
|
import { nextWindowStart, resolveTimeZone } from "./time-window.mjs";
|
|
8
|
-
|
|
8
|
+
import { longestJourneyMs } from "./duration.mjs";
|
|
9
|
+
import { isRetryable, isRetryableStatus } from "./retry.mjs";
|
|
10
|
+
export { type ActionInvocation, type ActionInvoker, type ActionVersionPolicy, DEVICES_UPDATED_EVENT, type EngineStep, type EvaluateOptions, type EventSink, type FactSource, type JourneyContext, type JourneyObserver, type JourneyOutcome, type LinkPurpose, type MessageSender, type MessageTracking, type OutboundMessage, PROFILE_UPDATED_EVENT, type PushDevice, type RegisteredAction, RegistryActionInvoker, type SendResult, evaluateCondition, hashToBucket, isRetryable, isRetryableStatus, longestJourneyMs, matchesWhere, murmur3, nextWindowStart, relevantEvents, resolveTimeZone, runJourney, scaleDurations, scaleSegmentWindows };
|
package/dist/engine/index.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { isRetryable, isRetryableStatus } from "./retry.mjs";
|
|
2
|
+
import { longestJourneyMs } from "./duration.mjs";
|
|
1
3
|
import { hashToBucket, murmur3 } from "./bucket.mjs";
|
|
2
4
|
import { DEVICES_UPDATED_EVENT, PROFILE_UPDATED_EVENT, evaluateCondition, matchesWhere, relevantEvents } from "./condition.mjs";
|
|
3
5
|
import { nextWindowStart, resolveTimeZone } from "./time-window.mjs";
|
|
4
6
|
import { runJourney } from "./interpreter.mjs";
|
|
5
7
|
import { scaleDurations, scaleSegmentWindows } from "./time-scale.mjs";
|
|
6
8
|
import { RegistryActionInvoker } from "./actions.mjs";
|
|
7
|
-
export { DEVICES_UPDATED_EVENT, PROFILE_UPDATED_EVENT, RegistryActionInvoker, evaluateCondition, hashToBucket, matchesWhere, murmur3, nextWindowStart, relevantEvents, resolveTimeZone, runJourney, scaleDurations, scaleSegmentWindows };
|
|
9
|
+
export { DEVICES_UPDATED_EVENT, PROFILE_UPDATED_EVENT, RegistryActionInvoker, evaluateCondition, hashToBucket, isRetryable, isRetryableStatus, longestJourneyMs, matchesWhere, murmur3, nextWindowStart, relevantEvents, resolveTimeZone, runJourney, scaleDurations, scaleSegmentWindows };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/engine/retry.ts
|
|
3
|
+
/**
|
|
4
|
+
* Which step failures a retry would only repeat. A durable step retries what it throws; an
|
|
5
|
+
* error that is the same on every attempt — a template refusing its props, the contract
|
|
6
|
+
* refusing an emitted event, a provider rejecting the request itself — ends the step at once
|
|
7
|
+
* instead of burning the retry budget and delaying the journey. Every runtime adapter asks this
|
|
8
|
+
* one rule.
|
|
9
|
+
*/
|
|
10
|
+
/** Errors the engine raises that are deterministic by construction. */
|
|
11
|
+
const DETERMINISTIC = /* @__PURE__ */ new Set(["TemplateValidationError", "ContractValidationError"]);
|
|
12
|
+
/**
|
|
13
|
+
* False for an error a retry cannot fix: one of the engine's deterministic refusals, or any
|
|
14
|
+
* error that says so with `retryable: false` (the transports' 4xx answers).
|
|
15
|
+
*/
|
|
16
|
+
function isRetryable(error) {
|
|
17
|
+
if (!(error instanceof Error)) return true;
|
|
18
|
+
if (DETERMINISTIC.has(error.name)) return false;
|
|
19
|
+
return error.retryable !== false;
|
|
20
|
+
}
|
|
21
|
+
/** An HTTP answer a retry can change: a timeout, throttling, or the provider's own fault. */
|
|
22
|
+
function isRetryableStatus(status) {
|
|
23
|
+
return status === 408 || status === 429 || status >= 500;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
exports.isRetryable = isRetryable;
|
|
27
|
+
exports.isRetryableStatus = isRetryableStatus;
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=retry.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.cjs","names":[],"sources":["../../src/engine/retry.ts"],"sourcesContent":["/**\n * Which step failures a retry would only repeat. A durable step retries what it throws; an\n * error that is the same on every attempt — a template refusing its props, the contract\n * refusing an emitted event, a provider rejecting the request itself — ends the step at once\n * instead of burning the retry budget and delaying the journey. Every runtime adapter asks this\n * one rule.\n */\n\n/** Errors the engine raises that are deterministic by construction. */\nconst DETERMINISTIC = new Set(['TemplateValidationError', 'ContractValidationError']);\n\n/**\n * False for an error a retry cannot fix: one of the engine's deterministic refusals, or any\n * error that says so with `retryable: false` (the transports' 4xx answers).\n */\nexport function isRetryable(error: unknown): boolean {\n if (!(error instanceof Error)) return true;\n if (DETERMINISTIC.has(error.name)) return false;\n return (error as { retryable?: unknown }).retryable !== false;\n}\n\n/** An HTTP answer a retry can change: a timeout, throttling, or the provider's own fault. */\nexport function isRetryableStatus(status: number): boolean {\n return status === 408 || status === 429 || status >= 500;\n}\n"],"mappings":";;;;;;;;;;AASA,MAAM,gCAAgB,IAAI,IAAI,CAAC,2BAA2B,yBAAyB,CAAC;;;;;AAMpF,SAAgB,YAAY,OAAyB;CACnD,IAAI,EAAE,iBAAiB,QAAQ,OAAO;CACtC,IAAI,cAAc,IAAI,MAAM,IAAI,GAAG,OAAO;CAC1C,OAAQ,MAAkC,cAAc;AAC1D;;AAGA,SAAgB,kBAAkB,QAAyB;CACzD,OAAO,WAAW,OAAO,WAAW,OAAO,UAAU;AACvD"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/engine/retry.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Which step failures a retry would only repeat. A durable step retries what it throws; an
|
|
4
|
+
* error that is the same on every attempt — a template refusing its props, the contract
|
|
5
|
+
* refusing an emitted event, a provider rejecting the request itself — ends the step at once
|
|
6
|
+
* instead of burning the retry budget and delaying the journey. Every runtime adapter asks this
|
|
7
|
+
* one rule.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* False for an error a retry cannot fix: one of the engine's deterministic refusals, or any
|
|
11
|
+
* error that says so with `retryable: false` (the transports' 4xx answers).
|
|
12
|
+
*/
|
|
13
|
+
export declare function isRetryable(error: unknown): boolean;
|
|
14
|
+
/** An HTTP answer a retry can change: a timeout, throttling, or the provider's own fault. */
|
|
15
|
+
export declare function isRetryableStatus(status: number): boolean;
|
|
16
|
+
//#endregion
|
|
17
|
+
//# sourceMappingURL=retry.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.cts","names":[],"sources":["../../src/engine/retry.ts"],"mappings":";;;;;;;;;;;;wBAegB,YAAY;;wBAOZ,kBAAkB"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/engine/retry.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Which step failures a retry would only repeat. A durable step retries what it throws; an
|
|
4
|
+
* error that is the same on every attempt — a template refusing its props, the contract
|
|
5
|
+
* refusing an emitted event, a provider rejecting the request itself — ends the step at once
|
|
6
|
+
* instead of burning the retry budget and delaying the journey. Every runtime adapter asks this
|
|
7
|
+
* one rule.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* False for an error a retry cannot fix: one of the engine's deterministic refusals, or any
|
|
11
|
+
* error that says so with `retryable: false` (the transports' 4xx answers).
|
|
12
|
+
*/
|
|
13
|
+
export declare function isRetryable(error: unknown): boolean;
|
|
14
|
+
/** An HTTP answer a retry can change: a timeout, throttling, or the provider's own fault. */
|
|
15
|
+
export declare function isRetryableStatus(status: number): boolean;
|
|
16
|
+
//#endregion
|
|
17
|
+
//# sourceMappingURL=retry.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.d.mts","names":[],"sources":["../../src/engine/retry.ts"],"mappings":";;;;;;;;;;;;wBAegB,YAAY;;wBAOZ,kBAAkB"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/engine/retry.ts
|
|
2
|
+
/**
|
|
3
|
+
* Which step failures a retry would only repeat. A durable step retries what it throws; an
|
|
4
|
+
* error that is the same on every attempt — a template refusing its props, the contract
|
|
5
|
+
* refusing an emitted event, a provider rejecting the request itself — ends the step at once
|
|
6
|
+
* instead of burning the retry budget and delaying the journey. Every runtime adapter asks this
|
|
7
|
+
* one rule.
|
|
8
|
+
*/
|
|
9
|
+
/** Errors the engine raises that are deterministic by construction. */
|
|
10
|
+
const DETERMINISTIC = /* @__PURE__ */ new Set(["TemplateValidationError", "ContractValidationError"]);
|
|
11
|
+
/**
|
|
12
|
+
* False for an error a retry cannot fix: one of the engine's deterministic refusals, or any
|
|
13
|
+
* error that says so with `retryable: false` (the transports' 4xx answers).
|
|
14
|
+
*/
|
|
15
|
+
function isRetryable(error) {
|
|
16
|
+
if (!(error instanceof Error)) return true;
|
|
17
|
+
if (DETERMINISTIC.has(error.name)) return false;
|
|
18
|
+
return error.retryable !== false;
|
|
19
|
+
}
|
|
20
|
+
/** An HTTP answer a retry can change: a timeout, throttling, or the provider's own fault. */
|
|
21
|
+
function isRetryableStatus(status) {
|
|
22
|
+
return status === 408 || status === 429 || status >= 500;
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
export { isRetryable, isRetryableStatus };
|
|
26
|
+
|
|
27
|
+
//# sourceMappingURL=retry.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"retry.mjs","names":[],"sources":["../../src/engine/retry.ts"],"sourcesContent":["/**\n * Which step failures a retry would only repeat. A durable step retries what it throws; an\n * error that is the same on every attempt — a template refusing its props, the contract\n * refusing an emitted event, a provider rejecting the request itself — ends the step at once\n * instead of burning the retry budget and delaying the journey. Every runtime adapter asks this\n * one rule.\n */\n\n/** Errors the engine raises that are deterministic by construction. */\nconst DETERMINISTIC = new Set(['TemplateValidationError', 'ContractValidationError']);\n\n/**\n * False for an error a retry cannot fix: one of the engine's deterministic refusals, or any\n * error that says so with `retryable: false` (the transports' 4xx answers).\n */\nexport function isRetryable(error: unknown): boolean {\n if (!(error instanceof Error)) return true;\n if (DETERMINISTIC.has(error.name)) return false;\n return (error as { retryable?: unknown }).retryable !== false;\n}\n\n/** An HTTP answer a retry can change: a timeout, throttling, or the provider's own fault. */\nexport function isRetryableStatus(status: number): boolean {\n return status === 408 || status === 429 || status >= 500;\n}\n"],"mappings":";;;;;;;;;AASA,MAAM,gCAAgB,IAAI,IAAI,CAAC,2BAA2B,yBAAyB,CAAC;;;;;AAMpF,SAAgB,YAAY,OAAyB;CACnD,IAAI,EAAE,iBAAiB,QAAQ,OAAO;CACtC,IAAI,cAAc,IAAI,MAAM,IAAI,GAAG,OAAO;CAC1C,OAAQ,MAAkC,cAAc;AAC1D;;AAGA,SAAgB,kBAAkB,QAAyB;CACzD,OAAO,WAAW,OAAO,WAAW,OAAO,UAAU;AACvD"}
|
|
@@ -26,10 +26,25 @@ var EnvValidationError = class extends Error {
|
|
|
26
26
|
this.issues = issues;
|
|
27
27
|
}
|
|
28
28
|
};
|
|
29
|
+
function sendsEmail(nodes) {
|
|
30
|
+
return nodes.some((node) => {
|
|
31
|
+
switch (node.type) {
|
|
32
|
+
case "message": return node.channel === "email";
|
|
33
|
+
case "wait_until": return Array.isArray(node.onTimeout) && sendsEmail(node.onTimeout);
|
|
34
|
+
case "branch": return node.cases.some((branch) => sendsEmail(branch.flow)) || sendsEmail(node.otherwise ?? []);
|
|
35
|
+
case "cohort": return node.arms.some((arm) => sendsEmail(arm.flow));
|
|
36
|
+
default: return false;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
29
40
|
function createJourneyHost(options, platform = {}) {
|
|
30
41
|
const { config } = options;
|
|
31
42
|
const emailIds = require_templates.indexTemplates("email", config.templates?.email);
|
|
32
43
|
require_email.assertEmailConfig(config, emailIds.values());
|
|
44
|
+
if (options.email === void 0 && options.messages === void 0) {
|
|
45
|
+
const emailing = require_config.compileConfig(config).workflows.filter((workflow) => sendsEmail(workflow.flow));
|
|
46
|
+
if (emailing.length > 0) throw new TypeError(`workflows ${emailing.map((w) => `'${w.id}'`).join(", ")} send email, but the host has no \`email\` transport: pass \`email: (env) => cloudflareEmailTransport(...)\` or \`sesEmailTransport(...)\``);
|
|
47
|
+
}
|
|
33
48
|
const checked = /* @__PURE__ */ new WeakMap();
|
|
34
49
|
const checkEnv = (env) => {
|
|
35
50
|
if (options.env === void 0) return env;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"journey-host.cjs","names":["indexTemplates","z","compileConfig","d1Store","runtimeFromConfig","scaleSegmentWindows","channelsFromConfig","reportingObserver","RegistryActionInvoker","registryValidator","registryRenderer","handleTracking","checkEvent","ContractValidationError","ingestEvent","sweepSegments","deployBundle","SchemaMismatchError","TemplateValidationError","handleRequest"],"sources":["../../src/host/journey-host.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport type * as core from 'zod/v4/core';\nimport type { JourneyEnv, JourneyParams } from '../cloudflare/bindings';\nimport { d1Store } from '../cloudflare/d1-env';\nimport { type JourneyConfig, compileConfig } from '../config';\nimport { ContractValidationError, checkEvent } from '../contract';\nimport { assertEmailConfig } from '../email';\nimport type { EmailTransport } from '../email-sender';\nimport { RegistryActionInvoker } from '../engine/actions';\nimport type { JourneyObserver, MessageSender } from '../engine/ports';\nimport { scaleSegmentWindows } from '../engine/time-scale';\nimport type { BundleIR, ScalarIR } from '../ir';\nimport { registryRenderer } from '../react-email';\nimport { reportingObserver } from '../reporting/observer';\nimport { handleTracking } from '../reporting/tracking';\nimport type { JourneyRuntimeOptions } from '../runtime';\nimport {\n type BundleVersion,\n type JourneyStore,\n SchemaMismatchError,\n type StoreLease,\n} from '../store/index';\nimport { DEFAULT_SCHEMA } from '../store/tables';\nimport { TemplateValidationError, indexTemplates, registryValidator } from '../templates';\nimport { channelsFromConfig, runtimeFromConfig } from './channels';\nimport type { JourneyRunParts } from './execute';\nimport {\n type SweepResult,\n deployBundle,\n handleRequest,\n ingestEvent,\n sweepSegments,\n} from './router';\n\n/**\n * What a journey host is, apart from the runtime it runs on: the validated environment, the\n * store lease, the outlets a run is wired to, the HTTP API (tracking, sweep, bundle, preview,\n * ingest) and the bundle that goes live with the code. `journeyWorker` (Cloudflare) and\n * `journeyLambda` (AWS) are this plus how their platform starts, wakes and stops instances.\n */\n\nexport interface JourneyHostOptions<Env extends JourneyEnv> {\n /**\n * The project's `workflow.config.ts` (`defineConfig`): what the host\n * deploys (`GET /bundle`, `POST /bundle/deploy` serve `compileConfig`), the\n * template registries the senders, the validator and `/preview` render from,\n * the contract every `/events` and `/identify` is checked against, the email\n * defaults, actions, and the runtime / APNs / FCM settings. Secrets come\n * from the env under the names `JourneyEnv` documents.\n */\n config: JourneyConfig;\n /**\n * The data plane, opened per request and per workflow run. Return a\n * `StoreLease` when the client must be released afterwards (a Postgres\n * connection); defaults to D1 + KV from the env's bindings. `database` is\n * the config's (`schema` defaulting to `workflow`), for a Postgres store:\n *\n * store: (env, database) => postgresJsStore(env.DATABASE_URL, database)\n */\n store?: (env: Env, database: { schema: string }) => JourneyStore | StoreLease;\n /**\n * Where journey emails go: a transport built from the validated env —\n * `sesEmailTransport(...)`, `cloudflareEmailTransport(...)`, or your own. Which one is\n * independent of where the workflow runs. Without it the email channel fails its sends,\n * saying so.\n */\n email?: (env: Env) => EmailTransport;\n /**\n * Reporting hooks: node arrivals, every copy sent (with the tracking it should carry — click\n * links and an open pixel for email, payload data for a push), and how each instance ended.\n * By default a store that keeps reporting (Postgres) records them itself, and the host\n * serves the email tracking routes (`/t/...`, on `WORKFLOW_ORIGIN`); override to report\n * elsewhere.\n */\n observer?: (env: Env, store: JourneyStore) => JourneyObserver;\n /**\n * Message outlet override (tests, a custom gateway): replaces every channel, `email`\n * included. By default the channels the config describes.\n */\n messages?: (env: Env, store: JourneyStore) => MessageSender;\n /** Per-instance runtime options override (a tester in fast-forward); by default `config.runtime` for `ENVIRONMENT`. */\n runtime?: (env: Env, params: JourneyParams) => JourneyRuntimeOptions;\n /**\n * The host's environment, as a zod schema over its vars and secrets (bindings pass through\n * untouched): checked the first time the host runs, so a missing or malformed variable fails\n * there — every problem listed — not at the send that needs it. Webhook templates read their\n * URL from the parsed values: `webhook: (env: Env) => env.WEBHOOK_DISCORD_ALERTS`.\n */\n env?: core.$ZodType<object>;\n /** 'enforce' (default) answers an event or patch the contract refuses with a 400; 'report' only logs it. */\n contractMode?: 'enforce' | 'report';\n}\n\n/** The host's environment failed its schema (`env` option). */\nexport class EnvValidationError extends Error {\n override readonly name = 'EnvValidationError';\n constructor(readonly issues: readonly string[]) {\n super(`the host's environment is invalid:\\n ${issues.join('\\n ')}`);\n }\n}\n\nexport interface JourneyHost<Env extends JourneyEnv> {\n config: JourneyConfig;\n /** The env, parsed by the schema once per env object (throws EnvValidationError). */\n checkEnv(env: Env): Env;\n /** A store for one request or run, its segment windows scaled with the runtime. */\n lease(env: Env): Required<StoreLease>;\n /** The bundle compiled into this code. */\n bundle(): BundleIR;\n /** The outlets one run is wired to (the store is the run's). */\n runParts(env: Env, store: JourneyStore, params: JourneyParams): JourneyRunParts;\n /** The HTTP API over one store: tracking, sweep, bundle, preview, then the ingest router. */\n handle(request: Request, env: Env, store: JourneyStore): Promise<Response>;\n /** The scheduled sweep (`sweepSegments`) on a store of its own. */\n sweep(env: Env): Promise<SweepResult>;\n /**\n * Make the bundle compiled into `version` live, once per process, only over an older live\n * version (the store records it). Undefined when there is nothing to do; a failure is logged\n * and retried a minute later.\n */\n applyOwnBundle(env: Env, version: BundleVersion | undefined): Promise<void> | undefined;\n}\n\n/** What the platform adapter itself contributes. */\nexport interface JourneyPlatform {\n /** An HTTP/2 fetch for APNs, where the runtime's own fetch is HTTP/1.1 (Node). */\n apnsFetch?: typeof fetch;\n}\n\nexport function createJourneyHost<Env extends JourneyEnv>(\n options: JourneyHostOptions<Env>,\n platform: JourneyPlatform = {}\n): JourneyHost<Env> {\n const { config } = options;\n const emailIds = indexTemplates('email', config.templates?.email);\n // A config still on `emails` (its sender would be silently dropped) fails here, not at a send\n assertEmailConfig(config, emailIds.values());\n // Parsed once per env object: the runtime hands the same one to every request of an isolate\n const checked = new WeakMap<object, Env>();\n const checkEnv = (env: Env): Env => {\n if (options.env === undefined) return env;\n const cached = checked.get(env);\n if (cached !== undefined) return cached;\n const result = z.safeParse(options.env, env);\n if (!result.success) {\n // Names and reasons only: the values are secrets\n throw new EnvValidationError(\n result.error.issues.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)\n );\n }\n const merged: Env = { ...env, ...result.data };\n checked.set(env, merged);\n return merged;\n };\n let compiled: BundleIR | undefined;\n const bundle = () => (compiled ??= compileConfig(config));\n const emails = config.templates?.email;\n const database = { schema: config.database?.schema ?? DEFAULT_SCHEMA };\n // In fast-forward, segment windows scale with the journeys reading them (time-scale.ts)\n const lease = (env: Env): Required<StoreLease> => {\n const opened = options.store === undefined ? d1Store(env) : options.store(env, database);\n const noop = async () => {};\n const factor = runtimeFromConfig(config, env).timeScale;\n return 'store' in opened\n ? { store: scaleSegmentWindows(opened.store, factor), close: opened.close ?? noop }\n : { store: scaleSegmentWindows(opened, factor), close: noop };\n };\n\n const messages = (env: Env, store: JourneyStore): MessageSender => {\n if (options.messages !== undefined) return options.messages(env, store);\n return channelsFromConfig(config, env, {\n store,\n email: options.email?.(env),\n apnsFetch: platform.apnsFetch,\n });\n };\n\n const observer = (env: Env, store: JourneyStore): JourneyObserver | undefined => {\n if (options.observer !== undefined) return options.observer(env, store);\n return store.reporting === undefined\n ? undefined\n : reportingObserver(store.reporting, env.WORKFLOW_ORIGIN);\n };\n\n const runParts = (env: Env, store: JourneyStore, params: JourneyParams): JourneyRunParts => ({\n store,\n env,\n runtime:\n options.runtime === undefined ? runtimeFromConfig(config, env) : options.runtime(env, params),\n messages: messages(env, store),\n // An action ref is a registered action structurally: the same object carries name, code and hash\n actions: new RegistryActionInvoker(config.actions ?? []),\n contract: config.contract,\n validator: registryValidator(config.templates ?? {}),\n observer: observer(env, store),\n });\n\n const json = (body: unknown, status = 200): Response =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n\n const authorized = (request: Request, env: Env): boolean =>\n env.API_TOKEN === undefined ||\n request.headers.get('authorization') === `Bearer ${env.API_TOKEN}`;\n\n const preview = emails === undefined ? undefined : registryRenderer(emails, config.email);\n\n const handle = async (request: Request, env: Env, store: JourneyStore): Promise<Response> => {\n const url = new URL(request.url);\n // Opened by recipients, so no bearer: the routes only record, and redirect to recorded URLs\n if (url.pathname.startsWith('/t/') && store.reporting !== undefined) {\n const openedEvent = config.webPush?.openedEvent;\n const tracked = await handleTracking(request, store.reporting, {\n legacyUnsubscribeUrl: config.email?.unsubscribeUrl,\n product: config.title,\n quickMs: config.email?.tracking?.quickClickMs,\n pushOpened:\n openedEvent === undefined\n ? undefined\n : async ({ messageId, userId, at }) => {\n const payload: Record<string, unknown> = { message_id: messageId };\n const contract = config.contract;\n const verdict =\n contract === undefined ? undefined : checkEvent(contract, openedEvent, payload);\n if (verdict !== undefined && !verdict.ok) {\n throw new ContractValidationError(openedEvent, verdict.issues);\n }\n // Keyed by the copy: a second tap of the same notification is the same event\n await ingestEvent(\n env,\n {\n id: `${openedEvent}:${messageId}`,\n userId,\n event: openedEvent,\n payload: verdict?.data ?? payload,\n ts: at,\n },\n store\n );\n },\n });\n if (tracked !== undefined) return tracked;\n }\n if (url.pathname === '/segments/sweep') {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'POST') return json({ error: 'method not allowed' }, 405);\n return json(await sweepSegments(env, store));\n }\n if (url.pathname === '/bundle') {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'GET') return json({ error: 'method not allowed' }, 405);\n return json(bundle());\n }\n if (url.pathname === '/bundle/deploy') {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'POST') return json({ error: 'method not allowed' }, 405);\n try {\n return json(await deployBundle(env, bundle(), store));\n } catch (error) {\n if (!(error instanceof SchemaMismatchError)) throw error;\n return json({ error: error.message, problems: error.problems }, 503);\n }\n }\n if (emails !== undefined && preview !== undefined && url.pathname.startsWith('/preview/')) {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'GET') return json({ error: 'method not allowed' }, 405);\n const key = url.pathname.slice('/preview/'.length);\n if (!emailIds.has(key)) return json({ error: `unknown email template: ${key}` }, 404);\n // `?props=<JSON object>`: JSON keeps numbers and booleans typed, which query strings cannot\n let props: unknown = {};\n try {\n props = JSON.parse(url.searchParams.get('props') ?? '{}');\n } catch {\n return json({ error: 'props must be a JSON object' }, 400);\n }\n if (typeof props !== 'object' || props === null || Array.isArray(props)) {\n return json({ error: 'props must be a JSON object' }, 400);\n }\n try {\n const { subject, html } = await preview(key, props as Record<string, ScalarIR>);\n return new Response(html, {\n headers: { 'content-type': 'text/html; charset=utf-8', 'x-subject': subject },\n });\n } catch (error) {\n // The query string is the props: a refusal is the caller's to fix, with the field paths\n if (error instanceof TemplateValidationError) {\n return json({ error: error.message, issues: error.issues }, 400);\n }\n throw error;\n }\n }\n return handleRequest(request, env, store, {\n contract: config.contract,\n mode: options.contractMode,\n });\n };\n\n const sweep = async (env: Env) => {\n const { store, close } = lease(env);\n try {\n return await sweepSegments(env, store);\n } finally {\n await close();\n }\n };\n\n // The bundle compiled into this version goes live on its own, once per process: the first\n // request or scheduled run reads which version's bundle is live (one row) and applies this one\n // only over an older version, so one still serving during a rollout can never put its routes\n // back. A failure (say the migrations have not run) is logged and retried a minute later. Only\n // the request that starts the check gets its promise back: a request context cannot wait on\n // another's I/O.\n let applying: Promise<void> | undefined;\n let retryAt = 0;\n const applyOwnBundle = (env: Env, version: BundleVersion | undefined) => {\n if (version === undefined || version.id === '' || Number.isNaN(version.createdAt)) {\n return undefined;\n }\n if (applying !== undefined || Date.now() < retryAt) return undefined;\n applying = (async () => {\n const { store, close } = lease(env);\n try {\n if (store.liveBundleVersion === undefined) return; // D1: POST /bundle/deploy\n const live = await store.liveBundleVersion();\n if (live !== undefined && (live.id === version.id || live.createdAt >= version.createdAt))\n return;\n const result = await deployBundle(env, bundle(), store, version);\n if (result.applied) console.log(`[workflow] version ${version.id} is live`);\n } finally {\n await close();\n }\n })().catch((error: unknown) => {\n applying = undefined;\n retryAt = Date.now() + 60_000;\n console.error(`[workflow] could not make version ${version.id} live`, error);\n });\n return applying;\n };\n\n return { config, checkEnv, lease, bundle, runParts, handle, sweep, applyOwnBundle };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"journey-host.cjs","names":["indexTemplates","compileConfig","z","d1Store","runtimeFromConfig","scaleSegmentWindows","channelsFromConfig","reportingObserver","RegistryActionInvoker","registryValidator","registryRenderer","handleTracking","checkEvent","ContractValidationError","ingestEvent","sweepSegments","deployBundle","SchemaMismatchError","TemplateValidationError","handleRequest"],"sources":["../../src/host/journey-host.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport type * as core from 'zod/v4/core';\nimport type { JourneyEnv, JourneyParams } from '../cloudflare/bindings';\nimport { d1Store } from '../cloudflare/d1-env';\nimport { type JourneyConfig, compileConfig } from '../config';\nimport { ContractValidationError, checkEvent } from '../contract';\nimport { assertEmailConfig } from '../email';\nimport type { EmailTransport } from '../email-sender';\nimport { RegistryActionInvoker } from '../engine/actions';\nimport type { JourneyObserver, MessageSender } from '../engine/ports';\nimport { scaleSegmentWindows } from '../engine/time-scale';\nimport type { BundleIR, NodeIR, ScalarIR } from '../ir';\nimport { registryRenderer } from '../react-email';\nimport { reportingObserver } from '../reporting/observer';\nimport { handleTracking } from '../reporting/tracking';\nimport type { JourneyRuntimeOptions } from '../runtime';\nimport {\n type BundleVersion,\n type JourneyStore,\n SchemaMismatchError,\n type StoreLease,\n} from '../store/index';\nimport { DEFAULT_SCHEMA } from '../store/tables';\nimport { TemplateValidationError, indexTemplates, registryValidator } from '../templates';\nimport { channelsFromConfig, runtimeFromConfig } from './channels';\nimport type { JourneyRunParts } from './execute';\nimport {\n type SweepResult,\n deployBundle,\n handleRequest,\n ingestEvent,\n sweepSegments,\n} from './router';\n\n/**\n * What a journey host is, apart from the runtime it runs on: the validated environment, the\n * store lease, the outlets a run is wired to, the HTTP API (tracking, sweep, bundle, preview,\n * ingest) and the bundle that goes live with the code. `journeyWorker` (Cloudflare) and\n * `journeyLambda` (AWS) are this plus how their platform starts, wakes and stops instances.\n */\n\nexport interface JourneyHostOptions<Env extends JourneyEnv> {\n /**\n * The project's `workflow.config.ts` (`defineConfig`): what the host\n * deploys (`GET /bundle`, `POST /bundle/deploy` serve `compileConfig`), the\n * template registries the senders, the validator and `/preview` render from,\n * the contract every `/events` and `/identify` is checked against, the email\n * defaults, actions, and the runtime / APNs / FCM settings. Secrets come\n * from the env under the names `JourneyEnv` documents.\n */\n config: JourneyConfig;\n /**\n * The data plane, opened per request and per workflow run. Return a\n * `StoreLease` when the client must be released afterwards (a Postgres\n * connection); defaults to D1 + KV from the env's bindings. `database` is\n * the config's (`schema` defaulting to `workflow`), for a Postgres store:\n *\n * store: (env, database) => postgresJsStore(env.DATABASE_URL, database)\n */\n store?: (env: Env, database: { schema: string }) => JourneyStore | StoreLease;\n /**\n * Where journey emails go: a transport built from the validated env —\n * `sesEmailTransport(...)`, `cloudflareEmailTransport(...)`, or your own. Which one is\n * independent of where the workflow runs. Without it the email channel fails its sends,\n * saying so.\n */\n email?: (env: Env) => EmailTransport;\n /**\n * Reporting hooks: node arrivals, every copy sent (with the tracking it should carry — click\n * links and an open pixel for email, payload data for a push), and how each instance ended.\n * By default a store that keeps reporting (Postgres) records them itself, and the host\n * serves the email tracking routes (`/t/...`, on `WORKFLOW_ORIGIN`); override to report\n * elsewhere.\n */\n observer?: (env: Env, store: JourneyStore) => JourneyObserver;\n /**\n * Message outlet override (tests, a custom gateway): replaces every channel, `email`\n * included. By default the channels the config describes.\n */\n messages?: (env: Env, store: JourneyStore) => MessageSender;\n /** Per-instance runtime options override (a tester in fast-forward); by default `config.runtime` for `ENVIRONMENT`. */\n runtime?: (env: Env, params: JourneyParams) => JourneyRuntimeOptions;\n /**\n * The host's environment, as a zod schema over its vars and secrets (bindings pass through\n * untouched): checked the first time the host runs, so a missing or malformed variable fails\n * there — every problem listed — not at the send that needs it. Webhook templates read their\n * URL from the parsed values: `webhook: (env: Env) => env.WEBHOOK_DISCORD_ALERTS`.\n */\n env?: core.$ZodType<object>;\n /** 'enforce' (default) answers an event or patch the contract refuses with a 400; 'report' only logs it. */\n contractMode?: 'enforce' | 'report';\n}\n\n/** The host's environment failed its schema (`env` option). */\nexport class EnvValidationError extends Error {\n override readonly name = 'EnvValidationError';\n constructor(readonly issues: readonly string[]) {\n super(`the host's environment is invalid:\\n ${issues.join('\\n ')}`);\n }\n}\n\nexport interface JourneyHost<Env extends JourneyEnv> {\n config: JourneyConfig;\n /** The env, parsed by the schema once per env object (throws EnvValidationError). */\n checkEnv(env: Env): Env;\n /** A store for one request or run, its segment windows scaled with the runtime. */\n lease(env: Env): Required<StoreLease>;\n /** The bundle compiled into this code. */\n bundle(): BundleIR;\n /** The outlets one run is wired to (the store is the run's). */\n runParts(env: Env, store: JourneyStore, params: JourneyParams): JourneyRunParts;\n /** The HTTP API over one store: tracking, sweep, bundle, preview, then the ingest router. */\n handle(request: Request, env: Env, store: JourneyStore): Promise<Response>;\n /** The scheduled sweep (`sweepSegments`) on a store of its own. */\n sweep(env: Env): Promise<SweepResult>;\n /**\n * Make the bundle compiled into `version` live, once per process, only over an older live\n * version (the store records it). Undefined when there is nothing to do; a failure is logged\n * and retried a minute later.\n */\n applyOwnBundle(env: Env, version: BundleVersion | undefined): Promise<void> | undefined;\n}\n\n/** What the platform adapter itself contributes. */\nexport interface JourneyPlatform {\n /** An HTTP/2 fetch for APNs, where the runtime's own fetch is HTTP/1.1 (Node). */\n apnsFetch?: typeof fetch;\n}\n\nfunction sendsEmail(nodes: readonly NodeIR[]): boolean {\n return nodes.some((node) => {\n switch (node.type) {\n case 'message':\n return node.channel === 'email';\n case 'wait_until':\n return Array.isArray(node.onTimeout) && sendsEmail(node.onTimeout);\n case 'branch':\n return (\n node.cases.some((branch) => sendsEmail(branch.flow)) || sendsEmail(node.otherwise ?? [])\n );\n case 'cohort':\n return node.arms.some((arm) => sendsEmail(arm.flow));\n default:\n return false;\n }\n });\n}\n\nexport function createJourneyHost<Env extends JourneyEnv>(\n options: JourneyHostOptions<Env>,\n platform: JourneyPlatform = {}\n): JourneyHost<Env> {\n const { config } = options;\n const emailIds = indexTemplates('email', config.templates?.email);\n // A config still on `emails` (its sender would be silently dropped) fails here, not at a send\n assertEmailConfig(config, emailIds.values());\n // A journey that emails with nowhere to send fails here too, not at its first email\n if (options.email === undefined && options.messages === undefined) {\n const emailing = compileConfig(config).workflows.filter((workflow) =>\n sendsEmail(workflow.flow)\n );\n if (emailing.length > 0) {\n throw new TypeError(\n `workflows ${emailing.map((w) => `'${w.id}'`).join(', ')} send email, but the host has no ` +\n '`email` transport: pass `email: (env) => cloudflareEmailTransport(...)` or `sesEmailTransport(...)`'\n );\n }\n }\n // Parsed once per env object: the runtime hands the same one to every request of an isolate\n const checked = new WeakMap<object, Env>();\n const checkEnv = (env: Env): Env => {\n if (options.env === undefined) return env;\n const cached = checked.get(env);\n if (cached !== undefined) return cached;\n const result = z.safeParse(options.env, env);\n if (!result.success) {\n // Names and reasons only: the values are secrets\n throw new EnvValidationError(\n result.error.issues.map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)\n );\n }\n const merged: Env = { ...env, ...result.data };\n checked.set(env, merged);\n return merged;\n };\n let compiled: BundleIR | undefined;\n const bundle = () => (compiled ??= compileConfig(config));\n const emails = config.templates?.email;\n const database = { schema: config.database?.schema ?? DEFAULT_SCHEMA };\n // In fast-forward, segment windows scale with the journeys reading them (time-scale.ts)\n const lease = (env: Env): Required<StoreLease> => {\n const opened = options.store === undefined ? d1Store(env) : options.store(env, database);\n const noop = async () => {};\n const factor = runtimeFromConfig(config, env).timeScale;\n return 'store' in opened\n ? { store: scaleSegmentWindows(opened.store, factor), close: opened.close ?? noop }\n : { store: scaleSegmentWindows(opened, factor), close: noop };\n };\n\n const messages = (env: Env, store: JourneyStore): MessageSender => {\n if (options.messages !== undefined) return options.messages(env, store);\n return channelsFromConfig(config, env, {\n store,\n email: options.email?.(env),\n apnsFetch: platform.apnsFetch,\n });\n };\n\n const observer = (env: Env, store: JourneyStore): JourneyObserver | undefined => {\n if (options.observer !== undefined) return options.observer(env, store);\n return store.reporting === undefined\n ? undefined\n : reportingObserver(store.reporting, env.WORKFLOW_ORIGIN);\n };\n\n const runParts = (env: Env, store: JourneyStore, params: JourneyParams): JourneyRunParts => ({\n store,\n env,\n runtime:\n options.runtime === undefined ? runtimeFromConfig(config, env) : options.runtime(env, params),\n messages: messages(env, store),\n // An action ref is a registered action structurally: the same object carries name, code and hash\n actions: new RegistryActionInvoker(config.actions ?? []),\n contract: config.contract,\n validator: registryValidator(config.templates ?? {}),\n observer: observer(env, store),\n });\n\n const json = (body: unknown, status = 200): Response =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n\n const authorized = (request: Request, env: Env): boolean =>\n env.API_TOKEN === undefined ||\n request.headers.get('authorization') === `Bearer ${env.API_TOKEN}`;\n\n const preview = emails === undefined ? undefined : registryRenderer(emails, config.email);\n\n const handle = async (request: Request, env: Env, store: JourneyStore): Promise<Response> => {\n const url = new URL(request.url);\n // Opened by recipients, so no bearer: the routes only record, and redirect to recorded URLs\n if (url.pathname.startsWith('/t/') && store.reporting !== undefined) {\n const openedEvent = config.webPush?.openedEvent;\n const tracked = await handleTracking(request, store.reporting, {\n legacyUnsubscribeUrl: config.email?.unsubscribeUrl,\n product: config.title,\n quickMs: config.email?.tracking?.quickClickMs,\n pushOpened:\n openedEvent === undefined\n ? undefined\n : async ({ messageId, userId, at }) => {\n const payload: Record<string, unknown> = { message_id: messageId };\n const contract = config.contract;\n const verdict =\n contract === undefined ? undefined : checkEvent(contract, openedEvent, payload);\n if (verdict !== undefined && !verdict.ok) {\n throw new ContractValidationError(openedEvent, verdict.issues);\n }\n // Keyed by the copy: a second tap of the same notification is the same event\n await ingestEvent(\n env,\n {\n id: `${openedEvent}:${messageId}`,\n userId,\n event: openedEvent,\n payload: verdict?.data ?? payload,\n ts: at,\n },\n store\n );\n },\n });\n if (tracked !== undefined) return tracked;\n }\n if (url.pathname === '/segments/sweep') {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'POST') return json({ error: 'method not allowed' }, 405);\n return json(await sweepSegments(env, store));\n }\n if (url.pathname === '/bundle') {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'GET') return json({ error: 'method not allowed' }, 405);\n return json(bundle());\n }\n if (url.pathname === '/bundle/deploy') {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'POST') return json({ error: 'method not allowed' }, 405);\n try {\n return json(await deployBundle(env, bundle(), store));\n } catch (error) {\n if (!(error instanceof SchemaMismatchError)) throw error;\n return json({ error: error.message, problems: error.problems }, 503);\n }\n }\n if (emails !== undefined && preview !== undefined && url.pathname.startsWith('/preview/')) {\n if (!authorized(request, env)) return json({ error: 'unauthorized' }, 401);\n if (request.method !== 'GET') return json({ error: 'method not allowed' }, 405);\n const key = url.pathname.slice('/preview/'.length);\n if (!emailIds.has(key)) return json({ error: `unknown email template: ${key}` }, 404);\n // `?props=<JSON object>`: JSON keeps numbers and booleans typed, which query strings cannot\n let props: unknown = {};\n try {\n props = JSON.parse(url.searchParams.get('props') ?? '{}');\n } catch {\n return json({ error: 'props must be a JSON object' }, 400);\n }\n if (typeof props !== 'object' || props === null || Array.isArray(props)) {\n return json({ error: 'props must be a JSON object' }, 400);\n }\n try {\n const { subject, html } = await preview(key, props as Record<string, ScalarIR>);\n return new Response(html, {\n headers: { 'content-type': 'text/html; charset=utf-8', 'x-subject': subject },\n });\n } catch (error) {\n // The query string is the props: a refusal is the caller's to fix, with the field paths\n if (error instanceof TemplateValidationError) {\n return json({ error: error.message, issues: error.issues }, 400);\n }\n throw error;\n }\n }\n return handleRequest(request, env, store, {\n contract: config.contract,\n mode: options.contractMode,\n });\n };\n\n const sweep = async (env: Env) => {\n const { store, close } = lease(env);\n try {\n return await sweepSegments(env, store);\n } finally {\n await close();\n }\n };\n\n // The bundle compiled into this version goes live on its own, once per process: the first\n // request or scheduled run reads which version's bundle is live (one row) and applies this one\n // only over an older version, so one still serving during a rollout can never put its routes\n // back. A failure (say the migrations have not run) is logged and retried a minute later. Only\n // the request that starts the check gets its promise back: a request context cannot wait on\n // another's I/O.\n let applying: Promise<void> | undefined;\n let retryAt = 0;\n const applyOwnBundle = (env: Env, version: BundleVersion | undefined) => {\n if (version === undefined || version.id === '' || Number.isNaN(version.createdAt)) {\n return undefined;\n }\n if (applying !== undefined || Date.now() < retryAt) return undefined;\n applying = (async () => {\n const { store, close } = lease(env);\n try {\n if (store.liveBundleVersion === undefined) return; // D1: POST /bundle/deploy\n const live = await store.liveBundleVersion();\n if (live !== undefined && (live.id === version.id || live.createdAt >= version.createdAt))\n return;\n const result = await deployBundle(env, bundle(), store, version);\n if (result.applied) console.log(`[workflow] version ${version.id} is live`);\n } finally {\n await close();\n }\n })().catch((error: unknown) => {\n applying = undefined;\n retryAt = Date.now() + 60_000;\n console.error(`[workflow] could not make version ${version.id} live`, error);\n });\n return applying;\n };\n\n return { config, checkEnv, lease, bundle, runParts, handle, sweep, applyOwnBundle };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"journey-host.d.cts","names":[],"sources":["../../src/host/journey-host.ts"],"mappings":";;;;;;;;;;;;;;;;;iBAyCiB,mBAAmB,YAAY;;;;;;;;;EAS9C,QAAQ;;;;;;;;;EASR,SAAS,KAAK,KAAK;IAAY;QAAqB,eAAe;;;;;;;EAOnE,SAAS,KAAK,QAAQ;;;;;;;;EAQtB,YAAY,KAAK,KAAK,OAAO,iBAAiB;;;;;EAK9C,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,WAAW,KAAK,KAAK,QAAQ,kBAAkB;;;;;;;EAO/C,MAAM,KAAK;;EAEX;;;qBAIW,2BAA2B;WAEjB;WADH;EAClB,YAAqB;;iBAKN,YAAY,YAAY;EACvC,QAAQ;;EAER,SAAS,KAAK,MAAM;;EAEpB,MAAM,KAAK,MAAM,SAAS;;EAE1B,UAAU;;EAEV,SAAS,KAAK,KAAK,OAAO,cAAc,QAAQ,gBAAgB;;EAEhE,OAAO,SAAS,SAAS,KAAK,KAAK,OAAO,eAAe,QAAQ;;EAEjE,MAAM,KAAK,MAAM,QAAQ;;;;;;EAMzB,eAAe,KAAK,KAAK,SAAS,4BAA4B;;;iBAI/C;;EAEf,mBAAmB;;
|
|
1
|
+
{"version":3,"file":"journey-host.d.cts","names":[],"sources":["../../src/host/journey-host.ts"],"mappings":";;;;;;;;;;;;;;;;;iBAyCiB,mBAAmB,YAAY;;;;;;;;;EAS9C,QAAQ;;;;;;;;;EASR,SAAS,KAAK,KAAK;IAAY;QAAqB,eAAe;;;;;;;EAOnE,SAAS,KAAK,QAAQ;;;;;;;;EAQtB,YAAY,KAAK,KAAK,OAAO,iBAAiB;;;;;EAK9C,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,WAAW,KAAK,KAAK,QAAQ,kBAAkB;;;;;;;EAO/C,MAAM,KAAK;;EAEX;;;qBAIW,2BAA2B;WAEjB;WADH;EAClB,YAAqB;;iBAKN,YAAY,YAAY;EACvC,QAAQ;;EAER,SAAS,KAAK,MAAM;;EAEpB,MAAM,KAAK,MAAM,SAAS;;EAE1B,UAAU;;EAEV,SAAS,KAAK,KAAK,OAAO,cAAc,QAAQ,gBAAgB;;EAEhE,OAAO,SAAS,SAAS,KAAK,KAAK,OAAO,eAAe,QAAQ;;EAEjE,MAAM,KAAK,MAAM,QAAQ;;;;;;EAMzB,eAAe,KAAK,KAAK,SAAS,4BAA4B;;;iBAI/C;;EAEf,mBAAmB;;wBAsBL,kBAAkB,YAAY,YAC5C,SAAS,mBAAmB,MAC5B,WAAU,kBACT,YAAY"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"journey-host.d.mts","names":[],"sources":["../../src/host/journey-host.ts"],"mappings":";;;;;;;;;;;;;;;;;iBAyCiB,mBAAmB,YAAY;;;;;;;;;EAS9C,QAAQ;;;;;;;;;EASR,SAAS,KAAK,KAAK;IAAY;QAAqB,eAAe;;;;;;;EAOnE,SAAS,KAAK,QAAQ;;;;;;;;EAQtB,YAAY,KAAK,KAAK,OAAO,iBAAiB;;;;;EAK9C,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,WAAW,KAAK,KAAK,QAAQ,kBAAkB;;;;;;;EAO/C,MAAM,KAAK;;EAEX;;;qBAIW,2BAA2B;WAEjB;WADH;EAClB,YAAqB;;iBAKN,YAAY,YAAY;EACvC,QAAQ;;EAER,SAAS,KAAK,MAAM;;EAEpB,MAAM,KAAK,MAAM,SAAS;;EAE1B,UAAU;;EAEV,SAAS,KAAK,KAAK,OAAO,cAAc,QAAQ,gBAAgB;;EAEhE,OAAO,SAAS,SAAS,KAAK,KAAK,OAAO,eAAe,QAAQ;;EAEjE,MAAM,KAAK,MAAM,QAAQ;;;;;;EAMzB,eAAe,KAAK,KAAK,SAAS,4BAA4B;;;iBAI/C;;EAEf,mBAAmB;;
|
|
1
|
+
{"version":3,"file":"journey-host.d.mts","names":[],"sources":["../../src/host/journey-host.ts"],"mappings":";;;;;;;;;;;;;;;;;iBAyCiB,mBAAmB,YAAY;;;;;;;;;EAS9C,QAAQ;;;;;;;;;EASR,SAAS,KAAK,KAAK;IAAY;QAAqB,eAAe;;;;;;;EAOnE,SAAS,KAAK,QAAQ;;;;;;;;EAQtB,YAAY,KAAK,KAAK,OAAO,iBAAiB;;;;;EAK9C,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,WAAW,KAAK,KAAK,QAAQ,kBAAkB;;;;;;;EAO/C,MAAM,KAAK;;EAEX;;;qBAIW,2BAA2B;WAEjB;WADH;EAClB,YAAqB;;iBAKN,YAAY,YAAY;EACvC,QAAQ;;EAER,SAAS,KAAK,MAAM;;EAEpB,MAAM,KAAK,MAAM,SAAS;;EAE1B,UAAU;;EAEV,SAAS,KAAK,KAAK,OAAO,cAAc,QAAQ,gBAAgB;;EAEhE,OAAO,SAAS,SAAS,KAAK,KAAK,OAAO,eAAe,QAAQ;;EAEjE,MAAM,KAAK,MAAM,QAAQ;;;;;;EAMzB,eAAe,KAAK,KAAK,SAAS,4BAA4B;;;iBAI/C;;EAEf,mBAAmB;;wBAsBL,kBAAkB,YAAY,YAC5C,SAAS,mBAAmB,MAC5B,WAAU,kBACT,YAAY"}
|
|
@@ -23,10 +23,25 @@ var EnvValidationError = class extends Error {
|
|
|
23
23
|
this.issues = issues;
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
|
+
function sendsEmail(nodes) {
|
|
27
|
+
return nodes.some((node) => {
|
|
28
|
+
switch (node.type) {
|
|
29
|
+
case "message": return node.channel === "email";
|
|
30
|
+
case "wait_until": return Array.isArray(node.onTimeout) && sendsEmail(node.onTimeout);
|
|
31
|
+
case "branch": return node.cases.some((branch) => sendsEmail(branch.flow)) || sendsEmail(node.otherwise ?? []);
|
|
32
|
+
case "cohort": return node.arms.some((arm) => sendsEmail(arm.flow));
|
|
33
|
+
default: return false;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
26
37
|
function createJourneyHost(options, platform = {}) {
|
|
27
38
|
const { config } = options;
|
|
28
39
|
const emailIds = indexTemplates("email", config.templates?.email);
|
|
29
40
|
assertEmailConfig(config, emailIds.values());
|
|
41
|
+
if (options.email === void 0 && options.messages === void 0) {
|
|
42
|
+
const emailing = compileConfig(config).workflows.filter((workflow) => sendsEmail(workflow.flow));
|
|
43
|
+
if (emailing.length > 0) throw new TypeError(`workflows ${emailing.map((w) => `'${w.id}'`).join(", ")} send email, but the host has no \`email\` transport: pass \`email: (env) => cloudflareEmailTransport(...)\` or \`sesEmailTransport(...)\``);
|
|
44
|
+
}
|
|
30
45
|
const checked = /* @__PURE__ */ new WeakMap();
|
|
31
46
|
const checkEnv = (env) => {
|
|
32
47
|
if (options.env === void 0) return env;
|