@shware/workflow 3.3.0 → 4.1.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 +230 -40
- package/dist/apns.cjs +113 -26
- package/dist/apns.cjs.map +1 -1
- package/dist/apns.d.cts +52 -6
- package/dist/apns.d.cts.map +1 -1
- package/dist/apns.d.mts +52 -6
- package/dist/apns.d.mts.map +1 -1
- package/dist/apns.mjs +111 -27
- package/dist/apns.mjs.map +1 -1
- package/dist/aws/engine-step.cjs +5 -5
- package/dist/aws/engine-step.cjs.map +1 -1
- package/dist/aws/engine-step.d.cts +8 -2
- package/dist/aws/engine-step.d.cts.map +1 -1
- package/dist/aws/engine-step.d.mts +8 -2
- package/dist/aws/engine-step.d.mts.map +1 -1
- package/dist/aws/engine-step.mjs +5 -5
- package/dist/aws/engine-step.mjs.map +1 -1
- package/dist/aws/index.cjs +50 -6
- package/dist/aws/index.cjs.map +1 -1
- package/dist/aws/index.d.cts +4 -3
- package/dist/aws/index.d.cts.map +1 -1
- package/dist/aws/index.d.mts +4 -3
- package/dist/aws/index.d.mts.map +1 -1
- package/dist/aws/index.mjs +49 -7
- package/dist/aws/index.mjs.map +1 -1
- package/dist/aws/instances.cjs +29 -6
- package/dist/aws/instances.cjs.map +1 -1
- package/dist/aws/instances.d.cts +11 -1
- package/dist/aws/instances.d.cts.map +1 -1
- package/dist/aws/instances.d.mts +11 -1
- package/dist/aws/instances.d.mts.map +1 -1
- package/dist/aws/instances.mjs +27 -4
- package/dist/aws/instances.mjs.map +1 -1
- package/dist/client.cjs +4 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +2 -2
- package/dist/client.d.cts.map +1 -1
- package/dist/client.d.mts +2 -2
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +4 -1
- package/dist/client.mjs.map +1 -1
- package/dist/cloudflare/bindings.cjs +28 -25
- package/dist/cloudflare/bindings.cjs.map +1 -1
- package/dist/cloudflare/bindings.d.cts +15 -129
- package/dist/cloudflare/bindings.d.cts.map +1 -1
- package/dist/cloudflare/bindings.d.mts +15 -129
- package/dist/cloudflare/bindings.d.mts.map +1 -1
- package/dist/cloudflare/bindings.mjs +28 -23
- package/dist/cloudflare/bindings.mjs.map +1 -1
- package/dist/cloudflare/d1-env.cjs +6 -6
- package/dist/cloudflare/d1-env.cjs.map +1 -1
- package/dist/cloudflare/d1-env.d.cts +5 -5
- package/dist/cloudflare/d1-env.d.mts +5 -5
- package/dist/cloudflare/d1-env.mjs +6 -6
- package/dist/cloudflare/d1-env.mjs.map +1 -1
- package/dist/cloudflare/index.cjs +29 -18
- package/dist/cloudflare/index.d.cts +8 -4
- package/dist/cloudflare/index.d.mts +8 -4
- package/dist/cloudflare/index.mjs +9 -5
- package/dist/cloudflare/runner.cjs +29 -14
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts +9 -2
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts +9 -2
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +28 -13
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/cloudflare/schema.sql +27 -6
- package/dist/cloudflare/worker.cjs +19 -9
- package/dist/cloudflare/worker.cjs.map +1 -1
- package/dist/cloudflare/worker.d.cts +2 -2
- package/dist/cloudflare/worker.d.cts.map +1 -1
- package/dist/cloudflare/worker.d.mts +2 -2
- package/dist/cloudflare/worker.d.mts.map +1 -1
- package/dist/cloudflare/worker.mjs +19 -9
- package/dist/cloudflare/worker.mjs.map +1 -1
- package/dist/config.cjs +21 -5
- package/dist/config.cjs.map +1 -1
- package/dist/config.mjs +21 -5
- package/dist/config.mjs.map +1 -1
- package/dist/dsl/action.cjs +3 -6
- package/dist/dsl/action.cjs.map +1 -1
- package/dist/dsl/action.d.cts +12 -11
- package/dist/dsl/action.d.cts.map +1 -1
- package/dist/dsl/action.d.mts +12 -11
- package/dist/dsl/action.d.mts.map +1 -1
- package/dist/dsl/action.mjs +3 -6
- package/dist/dsl/action.mjs.map +1 -1
- package/dist/dsl/bundle.cjs +35 -31
- package/dist/dsl/bundle.cjs.map +1 -1
- package/dist/dsl/bundle.d.cts.map +1 -1
- package/dist/dsl/bundle.d.mts.map +1 -1
- package/dist/dsl/bundle.mjs +35 -31
- package/dist/dsl/bundle.mjs.map +1 -1
- package/dist/dsl/condition.cjs +13 -1
- package/dist/dsl/condition.cjs.map +1 -1
- package/dist/dsl/condition.d.cts +9 -1
- package/dist/dsl/condition.d.cts.map +1 -1
- package/dist/dsl/condition.d.mts +9 -1
- package/dist/dsl/condition.d.mts.map +1 -1
- package/dist/dsl/condition.mjs +13 -2
- package/dist/dsl/condition.mjs.map +1 -1
- package/dist/dsl/flow.cjs +33 -11
- package/dist/dsl/flow.cjs.map +1 -1
- package/dist/dsl/flow.d.cts +9 -1
- package/dist/dsl/flow.d.cts.map +1 -1
- package/dist/dsl/flow.d.mts +9 -1
- package/dist/dsl/flow.d.mts.map +1 -1
- package/dist/dsl/flow.mjs +34 -13
- package/dist/dsl/flow.mjs.map +1 -1
- package/dist/dsl/workflow.cjs +2 -3
- package/dist/dsl/workflow.cjs.map +1 -1
- package/dist/dsl/workflow.d.cts.map +1 -1
- package/dist/dsl/workflow.d.mts.map +1 -1
- package/dist/dsl/workflow.mjs +3 -4
- package/dist/dsl/workflow.mjs.map +1 -1
- package/dist/engine/actions.cjs +1 -8
- package/dist/engine/actions.cjs.map +1 -1
- package/dist/engine/actions.d.cts +1 -12
- package/dist/engine/actions.d.cts.map +1 -1
- package/dist/engine/actions.d.mts +1 -12
- package/dist/engine/actions.d.mts.map +1 -1
- package/dist/engine/actions.mjs +1 -8
- package/dist/engine/actions.mjs.map +1 -1
- package/dist/engine/condition.cjs +25 -5
- package/dist/engine/condition.cjs.map +1 -1
- package/dist/engine/condition.d.cts +12 -3
- package/dist/engine/condition.d.cts.map +1 -1
- package/dist/engine/condition.d.mts +12 -3
- package/dist/engine/condition.d.mts.map +1 -1
- package/dist/engine/condition.mjs +25 -6
- package/dist/engine/condition.mjs.map +1 -1
- package/dist/engine/index.cjs +3 -0
- package/dist/engine/index.d.cts +5 -5
- package/dist/engine/index.d.mts +5 -5
- package/dist/engine/index.mjs +4 -4
- package/dist/engine/interpreter.cjs +27 -20
- package/dist/engine/interpreter.cjs.map +1 -1
- package/dist/engine/interpreter.d.cts.map +1 -1
- package/dist/engine/interpreter.d.mts.map +1 -1
- package/dist/engine/interpreter.mjs +27 -20
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +15 -6
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +15 -6
- package/dist/engine/ports.d.mts.map +1 -1
- package/dist/engine/retry.cjs +13 -0
- package/dist/engine/retry.cjs.map +1 -1
- package/dist/engine/retry.d.cts +12 -0
- package/dist/engine/retry.d.cts.map +1 -1
- package/dist/engine/retry.d.mts +12 -0
- package/dist/engine/retry.d.mts.map +1 -1
- package/dist/engine/retry.mjs +13 -1
- package/dist/engine/retry.mjs.map +1 -1
- package/dist/engine/time-scale.cjs +90 -15
- package/dist/engine/time-scale.cjs.map +1 -1
- package/dist/engine/time-scale.d.cts +9 -6
- package/dist/engine/time-scale.d.cts.map +1 -1
- package/dist/engine/time-scale.d.mts +9 -6
- package/dist/engine/time-scale.d.mts.map +1 -1
- package/dist/engine/time-scale.mjs +90 -16
- package/dist/engine/time-scale.mjs.map +1 -1
- package/dist/fcm.cjs +111 -38
- package/dist/fcm.cjs.map +1 -1
- package/dist/fcm.d.cts +45 -5
- package/dist/fcm.d.cts.map +1 -1
- package/dist/fcm.d.mts +45 -5
- package/dist/fcm.d.mts.map +1 -1
- package/dist/fcm.mjs +110 -39
- package/dist/fcm.mjs.map +1 -1
- package/dist/hash.cjs +6 -6
- package/dist/hash.cjs.map +1 -1
- package/dist/hash.mjs +6 -6
- package/dist/hash.mjs.map +1 -1
- package/dist/host/auth.cjs +49 -0
- package/dist/host/auth.cjs.map +1 -0
- package/dist/host/auth.d.cts +35 -0
- package/dist/host/auth.d.cts.map +1 -0
- package/dist/host/auth.d.mts +35 -0
- package/dist/host/auth.d.mts.map +1 -0
- package/dist/host/auth.mjs +46 -0
- package/dist/host/auth.mjs.map +1 -0
- package/dist/host/channels.cjs +118 -40
- package/dist/host/channels.cjs.map +1 -1
- package/dist/host/channels.d.cts +27 -1
- package/dist/host/channels.d.cts.map +1 -1
- package/dist/host/channels.d.mts +27 -1
- package/dist/host/channels.d.mts.map +1 -1
- package/dist/host/channels.mjs +121 -44
- package/dist/host/channels.mjs.map +1 -1
- package/dist/host/execute.cjs +14 -2
- package/dist/host/execute.cjs.map +1 -1
- package/dist/host/execute.d.cts +4 -4
- package/dist/host/execute.d.cts.map +1 -1
- package/dist/host/execute.d.mts +4 -4
- package/dist/host/execute.d.mts.map +1 -1
- package/dist/host/execute.mjs +13 -1
- package/dist/host/execute.mjs.map +1 -1
- package/dist/host/fresh-read.cjs +30 -0
- package/dist/host/fresh-read.cjs.map +1 -0
- package/dist/host/fresh-read.d.cts +18 -0
- package/dist/host/fresh-read.d.cts.map +1 -0
- package/dist/host/fresh-read.d.mts +18 -0
- package/dist/host/fresh-read.d.mts.map +1 -0
- package/dist/host/fresh-read.mjs +29 -0
- package/dist/host/fresh-read.mjs.map +1 -0
- package/dist/host/http.cjs +340 -0
- package/dist/host/http.cjs.map +1 -0
- package/dist/host/http.d.cts +58 -0
- package/dist/host/http.d.cts.map +1 -0
- package/dist/host/http.d.mts +58 -0
- package/dist/host/http.d.mts.map +1 -0
- package/dist/host/http.mjs +336 -0
- package/dist/host/http.mjs.map +1 -0
- package/dist/host/{router.cjs → ingest.cjs} +147 -175
- package/dist/host/ingest.cjs.map +1 -0
- package/dist/host/{router.d.cts → ingest.d.cts} +48 -30
- package/dist/host/ingest.d.cts.map +1 -0
- package/dist/host/{router.d.mts → ingest.d.mts} +48 -30
- package/dist/host/ingest.d.mts.map +1 -0
- package/dist/host/{router.mjs → ingest.mjs} +144 -173
- package/dist/host/ingest.mjs.map +1 -0
- package/dist/host/journey-host.cjs +66 -93
- package/dist/host/journey-host.cjs.map +1 -1
- package/dist/host/journey-host.d.cts +31 -26
- package/dist/host/journey-host.d.cts.map +1 -1
- package/dist/host/journey-host.d.mts +31 -26
- package/dist/host/journey-host.d.mts.map +1 -1
- package/dist/host/journey-host.mjs +66 -93
- package/dist/host/journey-host.mjs.map +1 -1
- package/dist/host/platform.cjs +21 -0
- package/dist/host/platform.cjs.map +1 -0
- package/dist/host/platform.d.cts +126 -0
- package/dist/host/platform.d.cts.map +1 -0
- package/dist/host/platform.d.mts +126 -0
- package/dist/host/platform.d.mts.map +1 -0
- package/dist/host/platform.mjs +19 -0
- package/dist/host/platform.mjs.map +1 -0
- package/dist/host/senders.cjs +18 -9
- package/dist/host/senders.cjs.map +1 -1
- package/dist/host/senders.d.cts +9 -4
- package/dist/host/senders.d.cts.map +1 -1
- package/dist/host/senders.d.mts +9 -4
- package/dist/host/senders.d.mts.map +1 -1
- package/dist/host/senders.mjs +18 -9
- package/dist/host/senders.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/ir.cjs +5 -7
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +3 -12
- package/dist/ir.d.cts.map +1 -1
- package/dist/ir.d.mts +3 -12
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +5 -7
- package/dist/ir.mjs.map +1 -1
- package/dist/plan.cjs +27 -33
- package/dist/plan.cjs.map +1 -1
- package/dist/plan.d.cts +1 -1
- package/dist/plan.d.cts.map +1 -1
- package/dist/plan.d.mts +1 -1
- package/dist/plan.d.mts.map +1 -1
- package/dist/plan.mjs +27 -33
- package/dist/plan.mjs.map +1 -1
- package/dist/reporting/tracking.cjs +14 -4
- package/dist/reporting/tracking.cjs.map +1 -1
- package/dist/reporting/tracking.d.cts +10 -10
- package/dist/reporting/tracking.d.cts.map +1 -1
- package/dist/reporting/tracking.d.mts +10 -10
- package/dist/reporting/tracking.d.mts.map +1 -1
- package/dist/reporting/tracking.mjs +14 -4
- package/dist/reporting/tracking.mjs.map +1 -1
- package/dist/store/d1.cjs +88 -29
- package/dist/store/d1.cjs.map +1 -1
- package/dist/store/d1.d.cts +29 -11
- package/dist/store/d1.d.cts.map +1 -1
- package/dist/store/d1.d.mts +29 -11
- package/dist/store/d1.d.mts.map +1 -1
- package/dist/store/d1.mjs +88 -29
- package/dist/store/d1.mjs.map +1 -1
- package/dist/store/erasure.cjs +22 -0
- package/dist/store/erasure.cjs.map +1 -0
- package/dist/store/erasure.d.cts +18 -0
- package/dist/store/erasure.d.cts.map +1 -0
- package/dist/store/erasure.d.mts +18 -0
- package/dist/store/erasure.d.mts.map +1 -0
- package/dist/store/erasure.mjs +21 -0
- package/dist/store/erasure.mjs.map +1 -0
- package/dist/store/index.cjs +2 -0
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +67 -13
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +67 -13
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs +2 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres-reporting.cjs +125 -62
- package/dist/store/postgres-reporting.cjs.map +1 -1
- package/dist/store/postgres-reporting.d.cts +30 -2
- package/dist/store/postgres-reporting.d.cts.map +1 -1
- package/dist/store/postgres-reporting.d.mts +30 -2
- package/dist/store/postgres-reporting.d.mts.map +1 -1
- package/dist/store/postgres-reporting.mjs +125 -62
- package/dist/store/postgres-reporting.mjs.map +1 -1
- package/dist/store/postgres.cjs +179 -60
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +44 -7
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +44 -7
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +179 -60
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +7 -5
- package/dist/store/tables.cjs.map +1 -1
- package/dist/store/tables.d.cts.map +1 -1
- package/dist/store/tables.d.mts.map +1 -1
- package/dist/store/tables.mjs +7 -5
- package/dist/store/tables.mjs.map +1 -1
- package/dist/store/user-lock.cjs +43 -0
- package/dist/store/user-lock.cjs.map +1 -0
- package/dist/store/user-lock.d.cts +32 -0
- package/dist/store/user-lock.d.cts.map +1 -0
- package/dist/store/user-lock.d.mts +32 -0
- package/dist/store/user-lock.d.mts.map +1 -0
- package/dist/store/user-lock.mjs +40 -0
- package/dist/store/user-lock.mjs.map +1 -0
- package/dist/transports/cloudflare-email.cjs +1 -1
- package/dist/transports/cloudflare-email.mjs +1 -1
- package/dist/transports/ses.cjs +47 -6
- package/dist/transports/ses.cjs.map +1 -1
- package/dist/transports/ses.d.cts +9 -1
- package/dist/transports/ses.d.cts.map +1 -1
- package/dist/transports/ses.d.mts +9 -1
- package/dist/transports/ses.d.mts.map +1 -1
- package/dist/transports/ses.mjs +47 -7
- package/dist/transports/ses.mjs.map +1 -1
- package/dist/webpush.cjs +133 -47
- package/dist/webpush.cjs.map +1 -1
- package/dist/webpush.d.cts +49 -8
- package/dist/webpush.d.cts.map +1 -1
- package/dist/webpush.d.mts +49 -8
- package/dist/webpush.d.mts.map +1 -1
- package/dist/webpush.mjs +130 -47
- package/dist/webpush.mjs.map +1 -1
- package/package.json +1 -1
- package/src/cloudflare/schema.sql +27 -6
- package/dist/host/router.cjs.map +0 -1
- package/dist/host/router.d.cts.map +0 -1
- package/dist/host/router.d.mts.map +0 -1
- package/dist/host/router.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channels.mjs","names":[],"sources":["../../src/host/channels.ts"],"sourcesContent":["import { ApnsPushSender, apnsKeyFromBase64 } from '../apns';\nimport type { JourneyEnv } from '../cloudflare/bindings';\nimport { type JourneyConfig, apnsEnvironment } from '../config';\nimport { EmailSender, type EmailTransport } from '../email-sender';\nimport type { MessageSender, OutboundMessage } from '../engine/ports';\nimport { FcmPushSender, serviceAccountFromBase64 } from '../fcm';\nimport { registryPushRenderer } from '../push';\nimport { registryRenderer } from '../react-email';\nimport { type JourneyMode, type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport {\n type DiscordTemplate,\n type SlackTemplate,\n indexTemplates,\n lookupTemplate,\n renderDiscordContent,\n renderSlackContent,\n} from '../templates';\nimport { DiscordWebhookSender, SlackWebhookSender } from '../webhooks';\nimport { WebPushSender } from '../webpush';\nimport { removeDevice } from './router';\nimport { routeByChannel, routeByPlatform } from './senders';\n\n/**\n * The outlets a `JourneyConfig` describes, assembled against the environment:\n * email through the host's transport (SES, Cloudflare Email, ...) with each template's addresses, iOS pushes\n * straight to APNs with the configured key (the `.p8` from\n * `APNS_PRIVATE_KEY_BASE64`), Android pushes through FCM with the service\n * account from `GOOGLE_APPLICATION_CREDENTIALS_BASE64`. A channel the config\n * leaves out is simply absent, and a journey that sends on it fails that step\n * with a message saying so — rather than the host having to spell out every\n * sender by hand.\n */\n\n/** Which config entry applies: wrangler's `ENVIRONMENT` var, production when unset. */\nexport function journeyMode(env: JourneyEnv): JourneyMode {\n return env.ENVIRONMENT ?? 'production';\n}\n\n/** The runtime options for this environment's mode. */\nexport function runtimeFromConfig(\n config: Pick<JourneyConfig, 'runtime'> | undefined,\n env: JourneyEnv\n): Required<JourneyRuntimeOptions> {\n return resolveRuntime(config?.runtime, journeyMode(env));\n}\n\n/** A sender that fails every message with the reason the channel cannot deliver. */\nfunction unavailable(reason: string): MessageSender {\n return {\n async send(message) {\n throw new Error(`cannot send '${message.template}' on ${message.channel}: ${reason}`);\n },\n };\n}\n\nfunction requireSecret(\n env: JourneyEnv,\n name:\n | 'APNS_PRIVATE_KEY_BASE64'\n | 'GOOGLE_APPLICATION_CREDENTIALS_BASE64'\n | 'VAPID_PUBLIC_KEY'\n | 'VAPID_PRIVATE_KEY'\n): string {\n const value = env[name];\n if (typeof value !== 'string' || value.length === 0) {\n throw new Error(`${name} is not set: the environment must hold it for the configured channel`);\n }\n return value;\n}\n\n/**\n * A sender built on its first message. A platform's secret is only required\n * when something is actually sent there: a journey that only emails must not\n * fail because the APNs key is missing from a local `.dev.vars`.\n */\nfunction lazy(build: () => MessageSender): MessageSender {\n let sender: MessageSender | undefined;\n return {\n async send(message) {\n sender ??= build();\n return sender.send(message);\n },\n };\n}\n\n/** The channels slice of the config — what `channelsFromConfig` reads. */\nexport type ChannelsConfig = Pick<\n JourneyConfig,\n 'templates' | 'email' | 'apns' | 'fcm' | 'webPush' | 'runtime'\n>;\n\nexport function channelsFromConfig(\n config: ChannelsConfig,\n env: JourneyEnv,\n options: {\n /** The store a token APNs / FCM / a push service reports as gone is removed from. */\n store?: JourneyStore | undefined;\n /** Where finished emails go — the host's choice, independent of where the workflow runs. */\n email?: EmailTransport | undefined;\n fetch?: typeof fetch;\n /** APNs is HTTP/2 only: a Node host passes an HTTP/2 fetch here (its own fetch cannot). */\n apnsFetch?: typeof fetch | undefined;\n } = {}\n): MessageSender {\n const mode = journeyMode(env);\n\n const email = (): MessageSender => {\n const registry = config.templates?.email;\n if (registry === undefined) return unavailable('no email templates are registered');\n const transport = options.email;\n if (transport === undefined) {\n return unavailable(\"no email transport is configured (the host's `email` option)\");\n }\n // Each template names its own sender: there is no global one to require\n return new EmailSender({ transport, render: registryRenderer(registry, config.email) });\n };\n\n const push = (): MessageSender => {\n const registry = config.templates?.push;\n if (registry === undefined) return unavailable('no push templates are registered');\n const render = registryPushRenderer(registry);\n // A token the provider reports as gone is forgotten (nothing left to deliver to), waking the\n // waits that read devices, as a removal through the API does\n const onUnregistered = async (message: OutboundMessage) => {\n if (message.device !== undefined && options.store !== undefined) {\n await removeDevice(env, message.device.token, options.store);\n }\n };\n const platforms: Record<string, MessageSender> = {};\n const apns = config.apns;\n if (apns !== undefined) {\n platforms.ios = lazy(\n () =>\n new ApnsPushSender({\n keyId: apns.keyId,\n teamId: apns.teamId,\n privateKey: apnsKeyFromBase64(requireSecret(env, 'APNS_PRIVATE_KEY_BASE64')),\n topic: apns.topic,\n environment: apnsEnvironment(apns, mode),\n render,\n onUnregistered,\n ...(env.APNS_RELAY_ORIGIN === undefined ? {} : { origin: env.APNS_RELAY_ORIGIN }),\n ...((options.apnsFetch ?? options.fetch) === undefined\n ? {}\n : { fetch: options.apnsFetch ?? options.fetch }),\n })\n );\n }\n const fcm = config.fcm;\n if (fcm !== undefined) {\n platforms.android = lazy(\n () =>\n new FcmPushSender({\n serviceAccount: serviceAccountFromBase64(\n requireSecret(env, 'GOOGLE_APPLICATION_CREDENTIALS_BASE64')\n ),\n render,\n onUnregistered,\n ...(fcm.androidChannelId === undefined\n ? {}\n : { android: { channel_id: fcm.androidChannelId } }),\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n }\n const webPush = config.webPush;\n if (webPush !== undefined) {\n platforms.web = lazy(\n () =>\n new WebPushSender({\n vapid: {\n subject: webPush.subject,\n publicKey: requireSecret(env, 'VAPID_PUBLIC_KEY'),\n privateKey: requireSecret(env, 'VAPID_PRIVATE_KEY'),\n },\n render,\n onUnregistered,\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n }\n if (Object.keys(platforms).length === 0)\n return unavailable('none of apns, fcm or webPush is configured');\n return routeByPlatform(platforms);\n };\n\n /**\n * A webhook template's URL, read from the environment the Worker validated at startup (its\n * `env` schema): a template whose variable is unset fails the step, naming the template.\n */\n const webhookUrl = (channel: 'discord' | 'slack', template: DiscordTemplate | SlackTemplate) => {\n const url = template.webhook(env as unknown as Record<string, unknown>);\n if (typeof url !== 'string' || url === '') {\n throw new Error(\n `${channel} template '${template.id}': its webhook URL is not set in the environment`\n );\n }\n return url;\n };\n\n const discord = (): MessageSender => {\n const registry = config.templates?.discord;\n if (registry === undefined) return unavailable('no discord templates are registered');\n const index = indexTemplates('discord', registry);\n return new DiscordWebhookSender({\n render: (id, props) => {\n const template = lookupTemplate(index, 'discord', id);\n return {\n url: webhookUrl('discord', template),\n content: renderDiscordContent(template, props),\n };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n const slack = (): MessageSender => {\n const registry = config.templates?.slack;\n if (registry === undefined) return unavailable('no slack templates are registered');\n const index = indexTemplates('slack', registry);\n return new SlackWebhookSender({\n render: (id, props) => {\n const template = lookupTemplate(index, 'slack', id);\n return { url: webhookUrl('slack', template), content: renderSlackContent(template, props) };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n return routeByChannel({ email: email(), push: push(), discord: discord(), slack: slack() });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,YAAY,KAA8B;CACxD,OAAO,IAAI,eAAe;AAC5B;;AAGA,SAAgB,kBACd,QACA,KACiC;CACjC,OAAO,eAAe,QAAQ,SAAS,YAAY,GAAG,CAAC;AACzD;;AAGA,SAAS,YAAY,QAA+B;CAClD,OAAO,EACL,MAAM,KAAK,SAAS;EAClB,MAAM,IAAI,MAAM,gBAAgB,QAAQ,SAAS,OAAO,QAAQ,QAAQ,IAAI,QAAQ;CACtF,EACF;AACF;AAEA,SAAS,cACP,KACA,MAKQ;CACR,MAAM,QAAQ,IAAI;CAClB,IAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAChD,MAAM,IAAI,MAAM,GAAG,KAAK,qEAAqE;CAE/F,OAAO;AACT;;;;;;AAOA,SAAS,KAAK,OAA2C;CACvD,IAAI;CACJ,OAAO,EACL,MAAM,KAAK,SAAS;EAClB,WAAW,MAAM;EACjB,OAAO,OAAO,KAAK,OAAO;CAC5B,EACF;AACF;AAQA,SAAgB,mBACd,QACA,KACA,UAQI,CAAC,GACU;CACf,MAAM,OAAO,YAAY,GAAG;CAE5B,MAAM,cAA6B;EACjC,MAAM,WAAW,OAAO,WAAW;EACnC,IAAI,aAAa,KAAA,GAAW,OAAO,YAAY,mCAAmC;EAClF,MAAM,YAAY,QAAQ;EAC1B,IAAI,cAAc,KAAA,GAChB,OAAO,YAAY,8DAA8D;EAGnF,OAAO,IAAI,YAAY;GAAE;GAAW,QAAQ,iBAAiB,UAAU,OAAO,KAAK;EAAE,CAAC;CACxF;CAEA,MAAM,aAA4B;EAChC,MAAM,WAAW,OAAO,WAAW;EACnC,IAAI,aAAa,KAAA,GAAW,OAAO,YAAY,kCAAkC;EACjF,MAAM,SAAS,qBAAqB,QAAQ;EAG5C,MAAM,iBAAiB,OAAO,YAA6B;GACzD,IAAI,QAAQ,WAAW,KAAA,KAAa,QAAQ,UAAU,KAAA,GACpD,MAAM,aAAa,KAAK,QAAQ,OAAO,OAAO,QAAQ,KAAK;EAE/D;EACA,MAAM,YAA2C,CAAC;EAClD,MAAM,OAAO,OAAO;EACpB,IAAI,SAAS,KAAA,GACX,UAAU,MAAM,WAEZ,IAAI,eAAe;GACjB,OAAO,KAAK;GACZ,QAAQ,KAAK;GACb,YAAY,kBAAkB,cAAc,KAAK,yBAAyB,CAAC;GAC3E,OAAO,KAAK;GACZ,aAAa,gBAAgB,MAAM,IAAI;GACvC;GACA;GACA,GAAI,IAAI,sBAAsB,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ,IAAI,kBAAkB;GAC/E,IAAK,QAAQ,aAAa,QAAQ,WAAW,KAAA,IACzC,CAAC,IACD,EAAE,OAAO,QAAQ,aAAa,QAAQ,MAAM;EAClD,CAAC,CACL;EAEF,MAAM,MAAM,OAAO;EACnB,IAAI,QAAQ,KAAA,GACV,UAAU,UAAU,WAEhB,IAAI,cAAc;GAChB,gBAAgB,yBACd,cAAc,KAAK,uCAAuC,CAC5D;GACA;GACA;GACA,GAAI,IAAI,qBAAqB,KAAA,IACzB,CAAC,IACD,EAAE,SAAS,EAAE,YAAY,IAAI,iBAAiB,EAAE;GACpD,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,QAAQ,MAAM;EAChE,CAAC,CACL;EAEF,MAAM,UAAU,OAAO;EACvB,IAAI,YAAY,KAAA,GACd,UAAU,MAAM,WAEZ,IAAI,cAAc;GAChB,OAAO;IACL,SAAS,QAAQ;IACjB,WAAW,cAAc,KAAK,kBAAkB;IAChD,YAAY,cAAc,KAAK,mBAAmB;GACpD;GACA;GACA;GACA,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,QAAQ,MAAM;EAChE,CAAC,CACL;EAEF,IAAI,OAAO,KAAK,SAAS,CAAC,CAAC,WAAW,GACpC,OAAO,YAAY,4CAA4C;EACjE,OAAO,gBAAgB,SAAS;CAClC;;;;;CAMA,MAAM,cAAc,SAA8B,aAA8C;EAC9F,MAAM,MAAM,SAAS,QAAQ,GAAyC;EACtE,IAAI,OAAO,QAAQ,YAAY,QAAQ,IACrC,MAAM,IAAI,MACR,GAAG,QAAQ,aAAa,SAAS,GAAG,iDACtC;EAEF,OAAO;CACT;CAEA,MAAM,gBAA+B;EACnC,MAAM,WAAW,OAAO,WAAW;EACnC,IAAI,aAAa,KAAA,GAAW,OAAO,YAAY,qCAAqC;EACpF,MAAM,QAAQ,eAAe,WAAW,QAAQ;EAChD,OAAO,IAAI,qBAAqB;GAC9B,SAAS,IAAI,UAAU;IACrB,MAAM,WAAW,eAAe,OAAO,WAAW,EAAE;IACpD,OAAO;KACL,KAAK,WAAW,WAAW,QAAQ;KACnC,SAAS,qBAAqB,UAAU,KAAK;IAC/C;GACF;GACA,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,QAAQ,MAAM;EAChE,CAAC;CACH;CAEA,MAAM,cAA6B;EACjC,MAAM,WAAW,OAAO,WAAW;EACnC,IAAI,aAAa,KAAA,GAAW,OAAO,YAAY,mCAAmC;EAClF,MAAM,QAAQ,eAAe,SAAS,QAAQ;EAC9C,OAAO,IAAI,mBAAmB;GAC5B,SAAS,IAAI,UAAU;IACrB,MAAM,WAAW,eAAe,OAAO,SAAS,EAAE;IAClD,OAAO;KAAE,KAAK,WAAW,SAAS,QAAQ;KAAG,SAAS,mBAAmB,UAAU,KAAK;IAAE;GAC5F;GACA,GAAI,QAAQ,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO,QAAQ,MAAM;EAChE,CAAC;CACH;CAEA,OAAO,eAAe;EAAE,OAAO,MAAM;EAAG,MAAM,KAAK;EAAG,SAAS,QAAQ;EAAG,OAAO,MAAM;CAAE,CAAC;AAC5F"}
|
|
1
|
+
{"version":3,"file":"channels.mjs","names":["APNS_DEAD_TOKEN_REASONS","FCM_DEAD_TOKEN_REASONS","WEB_PUSH_DEAD_TOKEN_REASONS"],"sources":["../../src/host/channels.ts"],"sourcesContent":["import {\n DEAD_TOKEN_REASONS as APNS_DEAD_TOKEN_REASONS,\n ApnsProviderToken,\n ApnsPushSender,\n apnsKeyFromBase64,\n} from '../apns';\nimport { type JourneyConfig, apnsEnvironment } from '../config';\nimport { EmailSender, type EmailTransport } from '../email-sender';\nimport type { MessageSender } from '../engine/ports';\nimport { PermanentError } from '../engine/retry';\nimport {\n DEAD_TOKEN_REASONS as FCM_DEAD_TOKEN_REASONS,\n FcmAccessToken,\n FcmPushSender,\n serviceAccountFromBase64,\n} from '../fcm';\nimport { registryPushRenderer } from '../push';\nimport { registryRenderer } from '../react-email';\nimport { type JourneyMode, type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport {\n type DiscordTemplate,\n type SlackTemplate,\n indexTemplates,\n lookupTemplate,\n renderDiscordContent,\n renderSlackContent,\n} from '../templates';\nimport { DiscordWebhookSender, SlackWebhookSender } from '../webhooks';\nimport {\n VapidSigner,\n DEAD_TOKEN_REASONS as WEB_PUSH_DEAD_TOKEN_REASONS,\n WebPushSender,\n} from '../webpush';\nimport { removeDevice } from './ingest';\nimport type { JourneyEnv } from './platform';\nimport { routeByChannel, routeByPlatform } from './senders';\n\n/**\n * The outlets a `JourneyConfig` describes, assembled against the environment:\n * email through the host's transport (SES, Cloudflare Email, ...) with each template's addresses, iOS pushes\n * straight to APNs with the configured key (the `.p8` from\n * `APNS_PRIVATE_KEY_BASE64`), Android pushes through FCM with the service\n * account from `GOOGLE_APPLICATION_CREDENTIALS_BASE64`. A channel the config\n * leaves out is simply absent, and a journey that sends on it fails that step\n * with a message saying so — rather than the host having to spell out every\n * sender by hand.\n */\n\n/** Which config entry applies: wrangler's `ENVIRONMENT` var, production when unset. */\nexport function journeyMode(env: JourneyEnv): JourneyMode {\n return env.ENVIRONMENT ?? 'production';\n}\n\n/** The runtime options for this environment's mode. */\nexport function runtimeFromConfig(\n config: Pick<JourneyConfig, 'runtime'> | undefined,\n env: JourneyEnv\n): Required<JourneyRuntimeOptions> {\n return resolveRuntime(config?.runtime, journeyMode(env));\n}\n\n/**\n * A sender that fails every message with the reason the channel cannot deliver — for good: the\n * configuration is the same on every attempt, so the step does not retry into it.\n */\nfunction unavailable(reason: string): MessageSender {\n return {\n async send(message) {\n throw new PermanentError(\n `cannot send '${message.template}' on ${message.channel}: ${reason}`\n );\n },\n };\n}\n\nfunction requireSecret(\n env: JourneyEnv,\n name:\n | 'APNS_PRIVATE_KEY_BASE64'\n | 'GOOGLE_APPLICATION_CREDENTIALS_BASE64'\n | 'VAPID_PUBLIC_KEY'\n | 'VAPID_PRIVATE_KEY'\n): string {\n const value = env[name];\n if (typeof value !== 'string' || value.length === 0) {\n // Missing on this attempt, missing on the next: the deployment has to change, not the step\n throw new PermanentError(\n `${name} is not set: the environment must hold it for the configured channel`\n );\n }\n return value;\n}\n\n/**\n * The provider credentials — `ApnsProviderToken`, `FcmAccessToken`, `VapidSigner` — for as long\n * as the host lives. `channelsFromConfig` runs once per workflow run (every wake of every\n * instance) and builds the senders anew each time; what must outlive the run is the credential\n * state they hold: the APNs provider JWT must be reused for at least 20 minutes (Apple answers a\n * sender that re-signs on every push with 429 `TooManyProviderTokenUpdates`), the FCM access\n * token is good for an hour, the VAPID key import is wasted work twice.\n *\n * The cache is the host's (`createJourneyHost` makes one and hands it to every run), not the\n * run's `env`: a Lambda invocation builds a fresh env object for each run (its instance port is\n * in it), so a cache keyed on env would miss on every wake. Inside, the key names the provider,\n * the ids that identify the key and a digest of the secret — so a rotated secret or a reloaded\n * host mints a new credential instead of reusing the old, and the secret itself is in no key and\n * no log. Nothing that shapes the sender (topic, environment, relay, registry, store) is in it:\n * the sender is built per call around the shared credential.\n */\nexport interface ProviderCache {\n /** The credential under `key`, built on the first call and returned as is after that. */\n get<T>(key: string, build: () => T): T;\n}\n\nexport function createProviderCache(): ProviderCache {\n const credentials = new Map<string, unknown>();\n return {\n get<T>(key: string, build: () => T): T {\n if (!credentials.has(key)) credentials.set(key, build());\n return credentials.get(key) as T;\n },\n };\n}\n\n/** A number for each fetch implementation, so a credential minted through one is not reused with another. */\nconst identities = new WeakMap<object, number>();\nlet nextIdentity = 1;\nfunction identity(value: object | undefined): string {\n if (value === undefined) return 'global';\n let id = identities.get(value);\n if (id === undefined) {\n id = nextIdentity++;\n identities.set(value, id);\n }\n return String(id);\n}\n\n/** The first 8 hex of the secret's SHA-256: enough to tell a rotation apart, nothing of the secret. */\nasync function secretDigest(secret: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(secret));\n return Array.from(new Uint8Array(digest).slice(0, 4), (b) =>\n b.toString(16).padStart(2, '0')\n ).join('');\n}\n\n/**\n * A sender built on its first message, then kept for this call. `build` reads the secrets (and\n * so may throw a `PermanentError` naming the missing variable): a platform's secret is only\n * required when something is actually sent there, so a journey that only emails must not fail\n * because the APNs key is missing from a local `.dev.vars`. A build that failed is not kept.\n */\nfunction lazy(build: () => Promise<MessageSender>): MessageSender {\n let sender: Promise<MessageSender> | undefined;\n return {\n async send(message) {\n sender ??= build().catch((error: unknown) => {\n sender = undefined;\n throw error;\n });\n return (await sender).send(message);\n },\n };\n}\n\n/** The channels slice of the config — what `channelsFromConfig` reads. */\nexport type ChannelsConfig = Pick<\n JourneyConfig,\n 'templates' | 'email' | 'apns' | 'fcm' | 'webPush' | 'runtime'\n>;\n\nexport function channelsFromConfig(\n config: ChannelsConfig,\n env: JourneyEnv,\n options: {\n /** The store a token APNs / FCM / a push service reports as gone is removed from. */\n store?: JourneyStore | undefined;\n /** Where finished emails go — the host's choice, independent of where the workflow runs. */\n email?: EmailTransport | undefined;\n fetch?: typeof fetch;\n /** APNs is HTTP/2 only: a Node host passes an HTTP/2 fetch here (its own fetch cannot). */\n apnsFetch?: typeof fetch | undefined;\n /**\n * Where the provider credentials live across runs (`createProviderCache`): the host's, made\n * once with it. Without one, this call's credentials are its own — nothing is reused.\n */\n providers?: ProviderCache | undefined;\n } = {}\n): MessageSender {\n const mode = journeyMode(env);\n const providers = options.providers ?? createProviderCache();\n\n const email = (): MessageSender => {\n const registry = config.templates?.email;\n if (registry === undefined) return unavailable('no email templates are registered');\n const transport = options.email;\n if (transport === undefined) {\n return unavailable(\"no email transport is configured (the host's `email` option)\");\n }\n // Each template names its own sender: there is no global one to require\n return new EmailSender({ transport, render: registryRenderer(registry, config.email) });\n };\n\n const push = (): MessageSender => {\n const registry = config.templates?.push;\n if (registry === undefined) return unavailable('no push templates are registered');\n const render = registryPushRenderer(registry);\n /**\n * A token the provider reports as gone is forgotten (nothing left to deliver to), waking the\n * waits that read devices, as a removal through the API does. Done here, per run, on the\n * settled result, so the credential cached across runs (see `ProviderCache`) never holds a\n * run's store.\n */\n const dropDeadTokens = (\n provider: MessageSender,\n deadTokenReasons: ReadonlySet<string>\n ): MessageSender => ({\n async send(message) {\n const result = await provider.send(message);\n if (\n result !== undefined &&\n deadTokenReasons.has(result.reason) &&\n message.device !== undefined &&\n options.store !== undefined\n ) {\n await removeDevice(env, message.device.token, options.store);\n }\n return result;\n },\n });\n const platforms: Record<string, MessageSender> = {};\n const apns = config.apns;\n if (apns !== undefined) {\n const apnsFetch = options.apnsFetch ?? options.fetch;\n const environment = apnsEnvironment(apns, mode);\n platforms.ios = dropDeadTokens(\n lazy(async () => {\n const secret = requireSecret(env, 'APNS_PRIVATE_KEY_BASE64');\n const credentials = providers.get(\n JSON.stringify(['apns', apns.keyId, apns.teamId, await secretDigest(secret)]),\n () =>\n new ApnsProviderToken({\n keyId: apns.keyId,\n teamId: apns.teamId,\n privateKey: apnsKeyFromBase64(secret),\n })\n );\n return new ApnsPushSender({\n credentials,\n topic: apns.topic,\n environment,\n render,\n ...(env.APNS_RELAY_ORIGIN === undefined ? {} : { origin: env.APNS_RELAY_ORIGIN }),\n ...(apnsFetch === undefined ? {} : { fetch: apnsFetch }),\n });\n }),\n APNS_DEAD_TOKEN_REASONS\n );\n }\n const fcm = config.fcm;\n if (fcm !== undefined) {\n platforms.android = dropDeadTokens(\n lazy(async () => {\n const secret = requireSecret(env, 'GOOGLE_APPLICATION_CREDENTIALS_BASE64');\n const serviceAccount = serviceAccountFromBase64(secret);\n // The token endpoint is called through the fetch: another fetch is another credential\n const credentials = providers.get(\n JSON.stringify([\n 'fcm',\n serviceAccount.client_email,\n serviceAccount.project_id,\n identity(options.fetch),\n await secretDigest(secret),\n ]),\n () =>\n new FcmAccessToken({\n serviceAccount,\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n return new FcmPushSender({\n credentials,\n render,\n ...(fcm.androidChannelId === undefined\n ? {}\n : { android: { channel_id: fcm.androidChannelId } }),\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n }),\n FCM_DEAD_TOKEN_REASONS\n );\n }\n const webPush = config.webPush;\n if (webPush !== undefined) {\n platforms.web = dropDeadTokens(\n lazy(async () => {\n const publicKey = requireSecret(env, 'VAPID_PUBLIC_KEY');\n const privateKey = requireSecret(env, 'VAPID_PRIVATE_KEY');\n const credentials = providers.get(\n JSON.stringify(['vapid', webPush.subject, publicKey, await secretDigest(privateKey)]),\n () => new VapidSigner({ subject: webPush.subject, publicKey, privateKey })\n );\n return new WebPushSender({\n credentials,\n render,\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n }),\n WEB_PUSH_DEAD_TOKEN_REASONS\n );\n }\n if (Object.keys(platforms).length === 0)\n return unavailable('none of apns, fcm or webPush is configured');\n return routeByPlatform(platforms);\n };\n\n /**\n * A webhook template's URL, read from the environment the Worker validated at startup (its\n * `env` schema): a template whose variable is unset fails the step, naming the template.\n */\n const webhookUrl = (channel: 'discord' | 'slack', template: DiscordTemplate | SlackTemplate) => {\n const url = template.webhook(env as unknown as Record<string, unknown>);\n if (typeof url !== 'string' || url === '') {\n throw new PermanentError(\n `${channel} template '${template.id}': its webhook URL is not set in the environment`\n );\n }\n return url;\n };\n\n const discord = (): MessageSender => {\n const registry = config.templates?.discord;\n if (registry === undefined) return unavailable('no discord templates are registered');\n const index = indexTemplates('discord', registry);\n return new DiscordWebhookSender({\n render: (id, props) => {\n const template = lookupTemplate(index, 'discord', id);\n return {\n url: webhookUrl('discord', template),\n content: renderDiscordContent(template, props),\n };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n const slack = (): MessageSender => {\n const registry = config.templates?.slack;\n if (registry === undefined) return unavailable('no slack templates are registered');\n const index = indexTemplates('slack', registry);\n return new SlackWebhookSender({\n render: (id, props) => {\n const template = lookupTemplate(index, 'slack', id);\n return { url: webhookUrl('slack', template), content: renderSlackContent(template, props) };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n return routeByChannel({ email: email(), push: push(), discord: discord(), slack: slack() });\n}\n"],"mappings":"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"}
|
package/dist/host/execute.cjs
CHANGED
|
@@ -4,7 +4,8 @@ const require_runtime = require("../runtime.cjs");
|
|
|
4
4
|
const require_engine_interpreter = require("../engine/interpreter.cjs");
|
|
5
5
|
const require_engine_time_scale = require("../engine/time-scale.cjs");
|
|
6
6
|
const require_store_facts = require("../store/facts.cjs");
|
|
7
|
-
const
|
|
7
|
+
const require_host_fresh_read = require("./fresh-read.cjs");
|
|
8
|
+
const require_host_ingest = require("./ingest.cjs");
|
|
8
9
|
//#region src/host/execute.ts
|
|
9
10
|
/** Development aid: echo each outbound message before handing it to the real sender. */
|
|
10
11
|
function logging(sender) {
|
|
@@ -34,13 +35,23 @@ async function executeJourney(input, parts) {
|
|
|
34
35
|
if (!verdict.ok) throw new require_contract.ContractValidationError(name, verdict.issues);
|
|
35
36
|
accepted = verdict.data;
|
|
36
37
|
}
|
|
37
|
-
await
|
|
38
|
+
await require_host_ingest.ingestEvent(env, {
|
|
38
39
|
id,
|
|
39
40
|
userId,
|
|
40
41
|
event: name,
|
|
41
42
|
payload: accepted
|
|
42
43
|
}, store);
|
|
43
44
|
} };
|
|
45
|
+
if (!await require_host_fresh_read.readWithRetry(() => store.admitInstance(instanceId, userId), {
|
|
46
|
+
what: "admission",
|
|
47
|
+
instanceId
|
|
48
|
+
})) {
|
|
49
|
+
console.warn(`[workflow] instance ${instanceId} of ${userId} refused admission: not running`);
|
|
50
|
+
return {
|
|
51
|
+
status: "exited",
|
|
52
|
+
reason: "erased"
|
|
53
|
+
};
|
|
54
|
+
}
|
|
44
55
|
let outcome;
|
|
45
56
|
try {
|
|
46
57
|
const stored = await step.do("$workflow:ir", async () => {
|
|
@@ -64,6 +75,7 @@ async function executeJourney(input, parts) {
|
|
|
64
75
|
} catch (error) {
|
|
65
76
|
await step.do("finalize:failed", async () => {
|
|
66
77
|
await store.setEntryStatus(instanceId, "failed");
|
|
78
|
+
await store.unsubscribe(instanceId);
|
|
67
79
|
await finished("failed");
|
|
68
80
|
});
|
|
69
81
|
throw error;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execute.cjs","names":["resolveRuntime","checkEvent","ContractValidationError","ingestEvent","scaleDurations","runJourney","JourneyFactSource"],"sources":["../../src/host/execute.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"execute.cjs","names":["resolveRuntime","checkEvent","ContractValidationError","ingestEvent","readWithRetry","scaleDurations","runJourney","JourneyFactSource"],"sources":["../../src/host/execute.ts"],"sourcesContent":["import { type Contract, ContractValidationError, checkEvent } from '../contract';\nimport { runJourney } from '../engine/interpreter';\nimport type {\n ActionInvoker,\n EngineStep,\n EventSink,\n JourneyObserver,\n JourneyOutcome,\n MessageSender,\n MessageValidator,\n} from '../engine/ports';\nimport { scaleDurations } from '../engine/time-scale';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport { JourneyFactSource } from '../store/facts';\nimport type { JourneyStore } from '../store/index';\nimport { readWithRetry } from './fresh-read';\nimport { ingestEvent } from './ingest';\nimport type { JourneyEnv, JourneyParams } from './platform';\n\n/**\n * One journey instance run to its end, on whatever durable runtime supplies the `EngineStep`:\n * the runner admitted against the user's erasure tombstone, the IR loaded by the hash pinned at\n * entry, the outlets wired, the interpreter run, and the ledger finalized. Cloudflare's runner\n * and the Lambda handler both come through here; only their EngineStep differs.\n */\n\n/** Everything a run is wired to, assembled by the host. */\nexport interface JourneyRunParts {\n store: JourneyStore;\n /**\n * The env the emitted events are ingested with: its `JOURNEY` starts and wakes instances on\n * this runtime.\n */\n env: JourneyEnv;\n runtime: JourneyRuntimeOptions;\n messages: MessageSender;\n actions: ActionInvoker;\n contract?: Contract | undefined;\n validator?: MessageValidator | undefined;\n observer?: JourneyObserver | undefined;\n}\n\nexport interface JourneyRunInput {\n instanceId: string;\n params: JourneyParams;\n /** When the trigger happened: replay-stable, never Date.now() at run time. */\n enteredAtMs: number;\n step: EngineStep;\n}\n\n/** Development aid: echo each outbound message before handing it to the real sender. */\nfunction logging(sender: MessageSender): MessageSender {\n return {\n async send(message) {\n console.log(\n `[journey] ${message.channel} ${message.template} → ${message.recipient ?? message.userId} ${JSON.stringify(message.props)}`\n );\n return sender.send(message);\n },\n };\n}\n\nexport async function executeJourney(\n input: JourneyRunInput,\n parts: JourneyRunParts\n): Promise<JourneyOutcome> {\n const { instanceId, params, step } = input;\n const { workflowId, contentHash, userId } = params;\n const { store, env, observer, contract } = parts;\n\n const runtime = resolveRuntime({ current: parts.runtime }, 'current');\n\n const finished = (status: JourneyOutcome['status'] | 'failed', convertedAt?: number) =>\n observer?.finished?.({\n workflow: workflowId,\n instanceId,\n userId,\n status,\n at: Date.now(),\n ...(convertedAt === undefined ? {} : { convertedAt }),\n });\n\n const messages = runtime.logMessages ? logging(parts.messages) : parts.messages;\n\n // send_event feeds straight back into the router logic — an in-process call\n // forming the event edge between workflows. The step's identity travels as\n // the event id, so a retried emit records the occurrence once.\n // An emitted event passes the same contract check as one arriving over HTTP.\n const events: EventSink = {\n emit: async (name, payload, id) => {\n let accepted: Record<string, unknown> = payload;\n if (contract !== undefined) {\n const verdict = checkEvent(contract, name, payload);\n if (!verdict.ok) throw new ContractValidationError(name, verdict.issues);\n accepted = verdict.data;\n }\n await ingestEvent(env, { id, userId, event: name, payload: accepted }, store);\n },\n };\n\n /*\n * Admission: the ledger entry was committed before the runtime was asked to create this\n * instance, so an erasure can land in between (the entry is not found by `get`, the erase\n * goes through, and then the create lands). The runner is the only one who can refuse that\n * late run, and it must do so before its first observable effect — `observer.started`, the\n * IR read, any action or send.\n *\n * Outside any durable step, on purpose: a step would checkpoint the first invocation's\n * \"admitted\" and replay it on every wake and every rebuild after an eviction, which is\n * exactly when the tombstone may have arrived since. `admitInstance` is idempotent — the\n * entry's `admitted_at` keeps its first value — so a replay re-admits the same instance, and\n * the store makes the admission mutually exclusive with the erasure: either the entry is\n * admitted first and the erasure finds it and stops it, or the tombstone commits first and\n * this read answers false. Only the read itself is retried, briefly, when the database\n * stumbled; one that keeps failing throws, so the instance errors without running anything,\n * its entry stays, and the erasure protocol (or a later wake) decides what becomes of it.\n */\n const admitted = await readWithRetry(() => store.admitInstance(instanceId, userId), {\n what: 'admission',\n instanceId,\n });\n if (!admitted) {\n /*\n * No finalize either way: the entry is gone (erased, or never ours to run) or is being\n * erased, and the finalize steps would write it back — a status on an entry the erasure\n * deletes, a `finished` the reporting records as a new row of an erased user. The\n * subscriptions, if any, go with the user's data.\n */\n console.warn(`[workflow] instance ${instanceId} of ${userId} refused admission: not running`);\n return { status: 'exited', reason: 'erased' };\n }\n\n let outcome: JourneyOutcome;\n try {\n // In a step: a read that fails is retried like any other, and one that keeps failing ends\n // the instance through the failure path below rather than leaving its entry 'running'.\n // Content addressing makes the IR immutable, so the checkpointed copy is the one to replay\n const stored = await step.do('$workflow:ir', async () => {\n const found = await store.getWorkflowIR(contentHash);\n if (found === undefined)\n throw new Error(`WorkflowIR not found: ${workflowId}@${contentHash}`);\n return found;\n });\n // Development runs the journey in fast-forward; the pinned hash still names the version.\n const ir = scaleDurations(stored, runtime.timeScale);\n outcome = await runJourney(ir, {\n userId,\n instanceId,\n enteredAtMs: input.enteredAtMs,\n step,\n facts: new JourneyFactSource(store, userId),\n messages,\n events,\n actions: parts.actions,\n validator: parts.validator,\n observer,\n });\n } catch (error) {\n /*\n * An error escaping run() ends the instance, and the ledger is the only\n * place anyone looks afterwards: leaving it at 'running' meant a dead\n * journey stayed indistinguishable from a waiting one, in the dashboard\n * and in the once-policy, forever.\n *\n * Subscriptions go too, as on every ordinary ending: left behind, the\n * rows would only make every later event of the user poke a dead\n * instance (one wake call to the runtime each, refused every time).\n *\n * A paused instance never gets here: a durable wait suspends the run\n * without throwing through it.\n */\n await step.do('finalize:failed', async () => {\n await store.setEntryStatus(instanceId, 'failed');\n await store.unsubscribe(instanceId);\n await finished('failed');\n });\n throw error;\n }\n\n await step.do('finalize', async () => {\n await store.setEntryStatus(instanceId, outcome.status);\n await store.unsubscribe(instanceId);\n await finished(outcome.status, outcome.convertedAt);\n });\n\n return outcome;\n}\n"],"mappings":";;;;;;;;;;AAmDA,SAAS,QAAQ,QAAsC;CACrD,OAAO,EACL,MAAM,KAAK,SAAS;EAClB,QAAQ,IACN,aAAa,QAAQ,QAAQ,GAAG,QAAQ,SAAS,KAAK,QAAQ,aAAa,QAAQ,OAAO,GAAG,KAAK,UAAU,QAAQ,KAAK,GAC3H;EACA,OAAO,OAAO,KAAK,OAAO;CAC5B,EACF;AACF;AAEA,eAAsB,eACpB,OACA,OACyB;CACzB,MAAM,EAAE,YAAY,QAAQ,SAAS;CACrC,MAAM,EAAE,YAAY,aAAa,WAAW;CAC5C,MAAM,EAAE,OAAO,KAAK,UAAU,aAAa;CAE3C,MAAM,UAAUA,gBAAAA,eAAe,EAAE,SAAS,MAAM,QAAQ,GAAG,SAAS;CAEpE,MAAM,YAAY,QAA6C,gBAC7D,UAAU,WAAW;EACnB,UAAU;EACV;EACA;EACA;EACA,IAAI,KAAK,IAAI;EACb,GAAI,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY;CACrD,CAAC;CAEH,MAAM,WAAW,QAAQ,cAAc,QAAQ,MAAM,QAAQ,IAAI,MAAM;CAMvE,MAAM,SAAoB,EACxB,MAAM,OAAO,MAAM,SAAS,OAAO;EACjC,IAAI,WAAoC;EACxC,IAAI,aAAa,KAAA,GAAW;GAC1B,MAAM,UAAUC,iBAAAA,WAAW,UAAU,MAAM,OAAO;GAClD,IAAI,CAAC,QAAQ,IAAI,MAAM,IAAIC,iBAAAA,wBAAwB,MAAM,QAAQ,MAAM;GACvE,WAAW,QAAQ;EACrB;EACA,MAAMC,oBAAAA,YAAY,KAAK;GAAE;GAAI;GAAQ,OAAO;GAAM,SAAS;EAAS,GAAG,KAAK;CAC9E,EACF;CAuBA,IAAI,CAAC,MAJkBC,wBAAAA,oBAAoB,MAAM,cAAc,YAAY,MAAM,GAAG;EAClF,MAAM;EACN;CACF,CAAC,GACc;EAOb,QAAQ,KAAK,uBAAuB,WAAW,MAAM,OAAO,gCAAgC;EAC5F,OAAO;GAAE,QAAQ;GAAU,QAAQ;EAAS;CAC9C;CAEA,IAAI;CACJ,IAAI;EAIF,MAAM,SAAS,MAAM,KAAK,GAAG,gBAAgB,YAAY;GACvD,MAAM,QAAQ,MAAM,MAAM,cAAc,WAAW;GACnD,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,yBAAyB,WAAW,GAAG,aAAa;GACtE,OAAO;EACT,CAAC;EAED,MAAM,KAAKC,0BAAAA,eAAe,QAAQ,QAAQ,SAAS;EACnD,UAAU,MAAMC,2BAAAA,WAAW,IAAI;GAC7B;GACA;GACA,aAAa,MAAM;GACnB;GACA,OAAO,IAAIC,oBAAAA,kBAAkB,OAAO,MAAM;GAC1C;GACA;GACA,SAAS,MAAM;GACf,WAAW,MAAM;GACjB;EACF,CAAC;CACH,SAAS,OAAO;EAcd,MAAM,KAAK,GAAG,mBAAmB,YAAY;GAC3C,MAAM,MAAM,eAAe,YAAY,QAAQ;GAC/C,MAAM,MAAM,YAAY,UAAU;GAClC,MAAM,SAAS,QAAQ;EACzB,CAAC;EACD,MAAM;CACR;CAEA,MAAM,KAAK,GAAG,YAAY,YAAY;EACpC,MAAM,MAAM,eAAe,YAAY,QAAQ,MAAM;EACrD,MAAM,MAAM,YAAY,UAAU;EAClC,MAAM,SAAS,QAAQ,QAAQ,QAAQ,WAAW;CACpD,CAAC;CAED,OAAO;AACT"}
|
package/dist/host/execute.d.cts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { ActionInvoker, EngineStep, JourneyObserver, JourneyOutcome, MessageSender, MessageValidator } from "../engine/ports.cjs";
|
|
2
|
-
import { JourneyEnv, JourneyParams } from "
|
|
2
|
+
import { JourneyEnv, JourneyParams } from "./platform.cjs";
|
|
3
3
|
import { JourneyStore } from "../store/index.cjs";
|
|
4
4
|
import { Contract } from "../contract.cjs";
|
|
5
5
|
import { JourneyRuntimeOptions } from "../runtime.cjs";
|
|
6
6
|
//#region src/host/execute.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* One journey instance run to its end, on whatever durable runtime supplies the `EngineStep`:
|
|
9
|
-
* the
|
|
10
|
-
*
|
|
11
|
-
* their EngineStep differs.
|
|
9
|
+
* the runner admitted against the user's erasure tombstone, the IR loaded by the hash pinned at
|
|
10
|
+
* entry, the outlets wired, the interpreter run, and the ledger finalized. Cloudflare's runner
|
|
11
|
+
* and the Lambda handler both come through here; only their EngineStep differs.
|
|
12
12
|
*/
|
|
13
13
|
/** Everything a run is wired to, assembled by the host. */
|
|
14
14
|
export interface JourneyRunParts {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execute.d.cts","names":[],"sources":["../../src/host/execute.ts"],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"execute.d.cts","names":[],"sources":["../../src/host/execute.ts"],"mappings":";;;;;;;;;;;;;iBA2BiB;EACf,OAAO;;;;;EAKP,KAAK;EACL,SAAS;EACT,UAAU;EACV,SAAS;EACT,WAAW;EACX,YAAY;EACZ,WAAW;;iBAGI;EACf;EACA,QAAQ;;EAER;EACA,MAAM;;wBAec,eACpB,OAAO,iBACP,OAAO,kBACN,QAAQ"}
|
package/dist/host/execute.d.mts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { ActionInvoker, EngineStep, JourneyObserver, JourneyOutcome, MessageSender, MessageValidator } from "../engine/ports.mjs";
|
|
2
|
-
import { JourneyEnv, JourneyParams } from "
|
|
2
|
+
import { JourneyEnv, JourneyParams } from "./platform.mjs";
|
|
3
3
|
import { JourneyStore } from "../store/index.mjs";
|
|
4
4
|
import { Contract } from "../contract.mjs";
|
|
5
5
|
import { JourneyRuntimeOptions } from "../runtime.mjs";
|
|
6
6
|
//#region src/host/execute.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* One journey instance run to its end, on whatever durable runtime supplies the `EngineStep`:
|
|
9
|
-
* the
|
|
10
|
-
*
|
|
11
|
-
* their EngineStep differs.
|
|
9
|
+
* the runner admitted against the user's erasure tombstone, the IR loaded by the hash pinned at
|
|
10
|
+
* entry, the outlets wired, the interpreter run, and the ledger finalized. Cloudflare's runner
|
|
11
|
+
* and the Lambda handler both come through here; only their EngineStep differs.
|
|
12
12
|
*/
|
|
13
13
|
/** Everything a run is wired to, assembled by the host. */
|
|
14
14
|
export interface JourneyRunParts {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execute.d.mts","names":[],"sources":["../../src/host/execute.ts"],"mappings":";;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"execute.d.mts","names":[],"sources":["../../src/host/execute.ts"],"mappings":";;;;;;;;;;;;;iBA2BiB;EACf,OAAO;;;;;EAKP,KAAK;EACL,SAAS;EACT,UAAU;EACV,SAAS;EACT,WAAW;EACX,YAAY;EACZ,WAAW;;iBAGI;EACf;EACA,QAAQ;;EAER;EACA,MAAM;;wBAec,eACpB,OAAO,iBACP,OAAO,kBACN,QAAQ"}
|
package/dist/host/execute.mjs
CHANGED
|
@@ -3,7 +3,8 @@ import { resolveRuntime } from "../runtime.mjs";
|
|
|
3
3
|
import { runJourney } from "../engine/interpreter.mjs";
|
|
4
4
|
import { scaleDurations } from "../engine/time-scale.mjs";
|
|
5
5
|
import { JourneyFactSource } from "../store/facts.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { readWithRetry } from "./fresh-read.mjs";
|
|
7
|
+
import { ingestEvent } from "./ingest.mjs";
|
|
7
8
|
//#region src/host/execute.ts
|
|
8
9
|
/** Development aid: echo each outbound message before handing it to the real sender. */
|
|
9
10
|
function logging(sender) {
|
|
@@ -40,6 +41,16 @@ async function executeJourney(input, parts) {
|
|
|
40
41
|
payload: accepted
|
|
41
42
|
}, store);
|
|
42
43
|
} };
|
|
44
|
+
if (!await readWithRetry(() => store.admitInstance(instanceId, userId), {
|
|
45
|
+
what: "admission",
|
|
46
|
+
instanceId
|
|
47
|
+
})) {
|
|
48
|
+
console.warn(`[workflow] instance ${instanceId} of ${userId} refused admission: not running`);
|
|
49
|
+
return {
|
|
50
|
+
status: "exited",
|
|
51
|
+
reason: "erased"
|
|
52
|
+
};
|
|
53
|
+
}
|
|
43
54
|
let outcome;
|
|
44
55
|
try {
|
|
45
56
|
const stored = await step.do("$workflow:ir", async () => {
|
|
@@ -63,6 +74,7 @@ async function executeJourney(input, parts) {
|
|
|
63
74
|
} catch (error) {
|
|
64
75
|
await step.do("finalize:failed", async () => {
|
|
65
76
|
await store.setEntryStatus(instanceId, "failed");
|
|
77
|
+
await store.unsubscribe(instanceId);
|
|
66
78
|
await finished("failed");
|
|
67
79
|
});
|
|
68
80
|
throw error;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execute.mjs","names":[],"sources":["../../src/host/execute.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"execute.mjs","names":[],"sources":["../../src/host/execute.ts"],"sourcesContent":["import { type Contract, ContractValidationError, checkEvent } from '../contract';\nimport { runJourney } from '../engine/interpreter';\nimport type {\n ActionInvoker,\n EngineStep,\n EventSink,\n JourneyObserver,\n JourneyOutcome,\n MessageSender,\n MessageValidator,\n} from '../engine/ports';\nimport { scaleDurations } from '../engine/time-scale';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport { JourneyFactSource } from '../store/facts';\nimport type { JourneyStore } from '../store/index';\nimport { readWithRetry } from './fresh-read';\nimport { ingestEvent } from './ingest';\nimport type { JourneyEnv, JourneyParams } from './platform';\n\n/**\n * One journey instance run to its end, on whatever durable runtime supplies the `EngineStep`:\n * the runner admitted against the user's erasure tombstone, the IR loaded by the hash pinned at\n * entry, the outlets wired, the interpreter run, and the ledger finalized. Cloudflare's runner\n * and the Lambda handler both come through here; only their EngineStep differs.\n */\n\n/** Everything a run is wired to, assembled by the host. */\nexport interface JourneyRunParts {\n store: JourneyStore;\n /**\n * The env the emitted events are ingested with: its `JOURNEY` starts and wakes instances on\n * this runtime.\n */\n env: JourneyEnv;\n runtime: JourneyRuntimeOptions;\n messages: MessageSender;\n actions: ActionInvoker;\n contract?: Contract | undefined;\n validator?: MessageValidator | undefined;\n observer?: JourneyObserver | undefined;\n}\n\nexport interface JourneyRunInput {\n instanceId: string;\n params: JourneyParams;\n /** When the trigger happened: replay-stable, never Date.now() at run time. */\n enteredAtMs: number;\n step: EngineStep;\n}\n\n/** Development aid: echo each outbound message before handing it to the real sender. */\nfunction logging(sender: MessageSender): MessageSender {\n return {\n async send(message) {\n console.log(\n `[journey] ${message.channel} ${message.template} → ${message.recipient ?? message.userId} ${JSON.stringify(message.props)}`\n );\n return sender.send(message);\n },\n };\n}\n\nexport async function executeJourney(\n input: JourneyRunInput,\n parts: JourneyRunParts\n): Promise<JourneyOutcome> {\n const { instanceId, params, step } = input;\n const { workflowId, contentHash, userId } = params;\n const { store, env, observer, contract } = parts;\n\n const runtime = resolveRuntime({ current: parts.runtime }, 'current');\n\n const finished = (status: JourneyOutcome['status'] | 'failed', convertedAt?: number) =>\n observer?.finished?.({\n workflow: workflowId,\n instanceId,\n userId,\n status,\n at: Date.now(),\n ...(convertedAt === undefined ? {} : { convertedAt }),\n });\n\n const messages = runtime.logMessages ? logging(parts.messages) : parts.messages;\n\n // send_event feeds straight back into the router logic — an in-process call\n // forming the event edge between workflows. The step's identity travels as\n // the event id, so a retried emit records the occurrence once.\n // An emitted event passes the same contract check as one arriving over HTTP.\n const events: EventSink = {\n emit: async (name, payload, id) => {\n let accepted: Record<string, unknown> = payload;\n if (contract !== undefined) {\n const verdict = checkEvent(contract, name, payload);\n if (!verdict.ok) throw new ContractValidationError(name, verdict.issues);\n accepted = verdict.data;\n }\n await ingestEvent(env, { id, userId, event: name, payload: accepted }, store);\n },\n };\n\n /*\n * Admission: the ledger entry was committed before the runtime was asked to create this\n * instance, so an erasure can land in between (the entry is not found by `get`, the erase\n * goes through, and then the create lands). The runner is the only one who can refuse that\n * late run, and it must do so before its first observable effect — `observer.started`, the\n * IR read, any action or send.\n *\n * Outside any durable step, on purpose: a step would checkpoint the first invocation's\n * \"admitted\" and replay it on every wake and every rebuild after an eviction, which is\n * exactly when the tombstone may have arrived since. `admitInstance` is idempotent — the\n * entry's `admitted_at` keeps its first value — so a replay re-admits the same instance, and\n * the store makes the admission mutually exclusive with the erasure: either the entry is\n * admitted first and the erasure finds it and stops it, or the tombstone commits first and\n * this read answers false. Only the read itself is retried, briefly, when the database\n * stumbled; one that keeps failing throws, so the instance errors without running anything,\n * its entry stays, and the erasure protocol (or a later wake) decides what becomes of it.\n */\n const admitted = await readWithRetry(() => store.admitInstance(instanceId, userId), {\n what: 'admission',\n instanceId,\n });\n if (!admitted) {\n /*\n * No finalize either way: the entry is gone (erased, or never ours to run) or is being\n * erased, and the finalize steps would write it back — a status on an entry the erasure\n * deletes, a `finished` the reporting records as a new row of an erased user. The\n * subscriptions, if any, go with the user's data.\n */\n console.warn(`[workflow] instance ${instanceId} of ${userId} refused admission: not running`);\n return { status: 'exited', reason: 'erased' };\n }\n\n let outcome: JourneyOutcome;\n try {\n // In a step: a read that fails is retried like any other, and one that keeps failing ends\n // the instance through the failure path below rather than leaving its entry 'running'.\n // Content addressing makes the IR immutable, so the checkpointed copy is the one to replay\n const stored = await step.do('$workflow:ir', async () => {\n const found = await store.getWorkflowIR(contentHash);\n if (found === undefined)\n throw new Error(`WorkflowIR not found: ${workflowId}@${contentHash}`);\n return found;\n });\n // Development runs the journey in fast-forward; the pinned hash still names the version.\n const ir = scaleDurations(stored, runtime.timeScale);\n outcome = await runJourney(ir, {\n userId,\n instanceId,\n enteredAtMs: input.enteredAtMs,\n step,\n facts: new JourneyFactSource(store, userId),\n messages,\n events,\n actions: parts.actions,\n validator: parts.validator,\n observer,\n });\n } catch (error) {\n /*\n * An error escaping run() ends the instance, and the ledger is the only\n * place anyone looks afterwards: leaving it at 'running' meant a dead\n * journey stayed indistinguishable from a waiting one, in the dashboard\n * and in the once-policy, forever.\n *\n * Subscriptions go too, as on every ordinary ending: left behind, the\n * rows would only make every later event of the user poke a dead\n * instance (one wake call to the runtime each, refused every time).\n *\n * A paused instance never gets here: a durable wait suspends the run\n * without throwing through it.\n */\n await step.do('finalize:failed', async () => {\n await store.setEntryStatus(instanceId, 'failed');\n await store.unsubscribe(instanceId);\n await finished('failed');\n });\n throw error;\n }\n\n await step.do('finalize', async () => {\n await store.setEntryStatus(instanceId, outcome.status);\n await store.unsubscribe(instanceId);\n await finished(outcome.status, outcome.convertedAt);\n });\n\n return outcome;\n}\n"],"mappings":";;;;;;;;;AAmDA,SAAS,QAAQ,QAAsC;CACrD,OAAO,EACL,MAAM,KAAK,SAAS;EAClB,QAAQ,IACN,aAAa,QAAQ,QAAQ,GAAG,QAAQ,SAAS,KAAK,QAAQ,aAAa,QAAQ,OAAO,GAAG,KAAK,UAAU,QAAQ,KAAK,GAC3H;EACA,OAAO,OAAO,KAAK,OAAO;CAC5B,EACF;AACF;AAEA,eAAsB,eACpB,OACA,OACyB;CACzB,MAAM,EAAE,YAAY,QAAQ,SAAS;CACrC,MAAM,EAAE,YAAY,aAAa,WAAW;CAC5C,MAAM,EAAE,OAAO,KAAK,UAAU,aAAa;CAE3C,MAAM,UAAU,eAAe,EAAE,SAAS,MAAM,QAAQ,GAAG,SAAS;CAEpE,MAAM,YAAY,QAA6C,gBAC7D,UAAU,WAAW;EACnB,UAAU;EACV;EACA;EACA;EACA,IAAI,KAAK,IAAI;EACb,GAAI,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY;CACrD,CAAC;CAEH,MAAM,WAAW,QAAQ,cAAc,QAAQ,MAAM,QAAQ,IAAI,MAAM;CAMvE,MAAM,SAAoB,EACxB,MAAM,OAAO,MAAM,SAAS,OAAO;EACjC,IAAI,WAAoC;EACxC,IAAI,aAAa,KAAA,GAAW;GAC1B,MAAM,UAAU,WAAW,UAAU,MAAM,OAAO;GAClD,IAAI,CAAC,QAAQ,IAAI,MAAM,IAAI,wBAAwB,MAAM,QAAQ,MAAM;GACvE,WAAW,QAAQ;EACrB;EACA,MAAM,YAAY,KAAK;GAAE;GAAI;GAAQ,OAAO;GAAM,SAAS;EAAS,GAAG,KAAK;CAC9E,EACF;CAuBA,IAAI,CAAC,MAJkB,oBAAoB,MAAM,cAAc,YAAY,MAAM,GAAG;EAClF,MAAM;EACN;CACF,CAAC,GACc;EAOb,QAAQ,KAAK,uBAAuB,WAAW,MAAM,OAAO,gCAAgC;EAC5F,OAAO;GAAE,QAAQ;GAAU,QAAQ;EAAS;CAC9C;CAEA,IAAI;CACJ,IAAI;EAIF,MAAM,SAAS,MAAM,KAAK,GAAG,gBAAgB,YAAY;GACvD,MAAM,QAAQ,MAAM,MAAM,cAAc,WAAW;GACnD,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,yBAAyB,WAAW,GAAG,aAAa;GACtE,OAAO;EACT,CAAC;EAED,MAAM,KAAK,eAAe,QAAQ,QAAQ,SAAS;EACnD,UAAU,MAAM,WAAW,IAAI;GAC7B;GACA;GACA,aAAa,MAAM;GACnB;GACA,OAAO,IAAI,kBAAkB,OAAO,MAAM;GAC1C;GACA;GACA,SAAS,MAAM;GACf,WAAW,MAAM;GACjB;EACF,CAAC;CACH,SAAS,OAAO;EAcd,MAAM,KAAK,GAAG,mBAAmB,YAAY;GAC3C,MAAM,MAAM,eAAe,YAAY,QAAQ;GAC/C,MAAM,MAAM,YAAY,UAAU;GAClC,MAAM,SAAS,QAAQ;EACzB,CAAC;EACD,MAAM;CACR;CAEA,MAAM,KAAK,GAAG,YAAY,YAAY;EACpC,MAAM,MAAM,eAAe,YAAY,QAAQ,MAAM;EACrD,MAAM,MAAM,YAAY,UAAU;EAClC,MAAM,SAAS,QAAQ,QAAQ,QAAQ,WAAW;CACpD,CAAC;CAED,OAAO;AACT"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_engine_retry = require("../engine/retry.cjs");
|
|
3
|
+
//#region src/host/fresh-read.ts
|
|
4
|
+
/**
|
|
5
|
+
* A read that runs outside any durable step — a runner's admission at its entry, the registry
|
|
6
|
+
* check on AWS — which must see the record as it is now (an erasure since the last checkpoint)
|
|
7
|
+
* and so cannot be checkpointed: a step would replay the first invocation's verdict on every
|
|
8
|
+
* wake. Only the read itself is retried, a bounded number of times, on a transient failure (a
|
|
9
|
+
* dropped connection, a timeout); what it returns is never retried — a refusal is the caller's
|
|
10
|
+
* — and a `PermanentError` (or any error that says `retryable: false`) ends it at once.
|
|
11
|
+
*/
|
|
12
|
+
async function readWithRetry(fn, about, budget = {
|
|
13
|
+
attempts: 3,
|
|
14
|
+
delaysMs: [100, 300]
|
|
15
|
+
}) {
|
|
16
|
+
for (let attempt = 1;; attempt++) try {
|
|
17
|
+
return await fn();
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (attempt >= budget.attempts || !require_engine_retry.isRetryable(error)) throw error;
|
|
20
|
+
const delayMs = budget.delaysMs[Math.min(attempt, budget.delaysMs.length) - 1] ?? 0;
|
|
21
|
+
console.warn(`[workflow] ${about.what}: read failed for instance ${about.instanceId} (attempt ${attempt} of ${budget.attempts}), retrying in ${delayMs} ms`, error);
|
|
22
|
+
await new Promise((resolve) => {
|
|
23
|
+
setTimeout(resolve, delayMs);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
exports.readWithRetry = readWithRetry;
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=fresh-read.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fresh-read.cjs","names":["isRetryable"],"sources":["../../src/host/fresh-read.ts"],"sourcesContent":["import { isRetryable } from '../engine/retry';\n\n/**\n * A read that runs outside any durable step — a runner's admission at its entry, the registry\n * check on AWS — which must see the record as it is now (an erasure since the last checkpoint)\n * and so cannot be checkpointed: a step would replay the first invocation's verdict on every\n * wake. Only the read itself is retried, a bounded number of times, on a transient failure (a\n * dropped connection, a timeout); what it returns is never retried — a refusal is the caller's\n * — and a `PermanentError` (or any error that says `retryable: false`) ends it at once.\n */\nexport async function readWithRetry<T>(\n fn: () => Promise<T>,\n about: { what: string; instanceId: string },\n budget: { attempts: number; delaysMs: readonly number[] } = { attempts: 3, delaysMs: [100, 300] }\n): Promise<T> {\n for (let attempt = 1; ; attempt++) {\n try {\n return await fn();\n } catch (error) {\n if (attempt >= budget.attempts || !isRetryable(error)) throw error;\n const delayMs = budget.delaysMs[Math.min(attempt, budget.delaysMs.length) - 1] ?? 0;\n console.warn(\n `[workflow] ${about.what}: read failed for instance ${about.instanceId} (attempt ${attempt} of ${budget.attempts}), retrying in ${delayMs} ms`,\n error\n );\n await new Promise<void>((resolve) => {\n setTimeout(resolve, delayMs);\n });\n }\n }\n}\n"],"mappings":";;;;;;;;;;;AAUA,eAAsB,cACpB,IACA,OACA,SAA4D;CAAE,UAAU;CAAG,UAAU,CAAC,KAAK,GAAG;AAAE,GACpF;CACZ,KAAK,IAAI,UAAU,IAAK,WACtB,IAAI;EACF,OAAO,MAAM,GAAG;CAClB,SAAS,OAAO;EACd,IAAI,WAAW,OAAO,YAAY,CAACA,qBAAAA,YAAY,KAAK,GAAG,MAAM;EAC7D,MAAM,UAAU,OAAO,SAAS,KAAK,IAAI,SAAS,OAAO,SAAS,MAAM,IAAI,MAAM;EAClF,QAAQ,KACN,cAAc,MAAM,KAAK,6BAA6B,MAAM,WAAW,YAAY,QAAQ,MAAM,OAAO,SAAS,iBAAiB,QAAQ,MAC1I,KACF;EACA,MAAM,IAAI,SAAe,YAAY;GACnC,WAAW,SAAS,OAAO;EAC7B,CAAC;CACH;AAEJ"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/host/fresh-read.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A read that runs outside any durable step — a runner's admission at its entry, the registry
|
|
4
|
+
* check on AWS — which must see the record as it is now (an erasure since the last checkpoint)
|
|
5
|
+
* and so cannot be checkpointed: a step would replay the first invocation's verdict on every
|
|
6
|
+
* wake. Only the read itself is retried, a bounded number of times, on a transient failure (a
|
|
7
|
+
* dropped connection, a timeout); what it returns is never retried — a refusal is the caller's
|
|
8
|
+
* — and a `PermanentError` (or any error that says `retryable: false`) ends it at once.
|
|
9
|
+
*/
|
|
10
|
+
export declare function readWithRetry<T>(fn: () => Promise<T>, about: {
|
|
11
|
+
what: string;
|
|
12
|
+
instanceId: string;
|
|
13
|
+
}, budget?: {
|
|
14
|
+
attempts: number;
|
|
15
|
+
delaysMs: readonly number[];
|
|
16
|
+
}): Promise<T>;
|
|
17
|
+
//#endregion
|
|
18
|
+
//# sourceMappingURL=fresh-read.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fresh-read.d.cts","names":[],"sources":["../../src/host/fresh-read.ts"],"mappings":";;;;;;;;;wBAUsB,cAAc,GAClC,UAAU,QAAQ,IAClB;EAAS;EAAc;GACvB;EAAU;EAAkB;IAC3B,QAAQ"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//#region src/host/fresh-read.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A read that runs outside any durable step — a runner's admission at its entry, the registry
|
|
4
|
+
* check on AWS — which must see the record as it is now (an erasure since the last checkpoint)
|
|
5
|
+
* and so cannot be checkpointed: a step would replay the first invocation's verdict on every
|
|
6
|
+
* wake. Only the read itself is retried, a bounded number of times, on a transient failure (a
|
|
7
|
+
* dropped connection, a timeout); what it returns is never retried — a refusal is the caller's
|
|
8
|
+
* — and a `PermanentError` (or any error that says `retryable: false`) ends it at once.
|
|
9
|
+
*/
|
|
10
|
+
export declare function readWithRetry<T>(fn: () => Promise<T>, about: {
|
|
11
|
+
what: string;
|
|
12
|
+
instanceId: string;
|
|
13
|
+
}, budget?: {
|
|
14
|
+
attempts: number;
|
|
15
|
+
delaysMs: readonly number[];
|
|
16
|
+
}): Promise<T>;
|
|
17
|
+
//#endregion
|
|
18
|
+
//# sourceMappingURL=fresh-read.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fresh-read.d.mts","names":[],"sources":["../../src/host/fresh-read.ts"],"mappings":";;;;;;;;;wBAUsB,cAAc,GAClC,UAAU,QAAQ,IAClB;EAAS;EAAc;GACvB;EAAU;EAAkB;IAC3B,QAAQ"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { isRetryable } from "../engine/retry.mjs";
|
|
2
|
+
//#region src/host/fresh-read.ts
|
|
3
|
+
/**
|
|
4
|
+
* A read that runs outside any durable step — a runner's admission at its entry, the registry
|
|
5
|
+
* check on AWS — which must see the record as it is now (an erasure since the last checkpoint)
|
|
6
|
+
* and so cannot be checkpointed: a step would replay the first invocation's verdict on every
|
|
7
|
+
* wake. Only the read itself is retried, a bounded number of times, on a transient failure (a
|
|
8
|
+
* dropped connection, a timeout); what it returns is never retried — a refusal is the caller's
|
|
9
|
+
* — and a `PermanentError` (or any error that says `retryable: false`) ends it at once.
|
|
10
|
+
*/
|
|
11
|
+
async function readWithRetry(fn, about, budget = {
|
|
12
|
+
attempts: 3,
|
|
13
|
+
delaysMs: [100, 300]
|
|
14
|
+
}) {
|
|
15
|
+
for (let attempt = 1;; attempt++) try {
|
|
16
|
+
return await fn();
|
|
17
|
+
} catch (error) {
|
|
18
|
+
if (attempt >= budget.attempts || !isRetryable(error)) throw error;
|
|
19
|
+
const delayMs = budget.delaysMs[Math.min(attempt, budget.delaysMs.length) - 1] ?? 0;
|
|
20
|
+
console.warn(`[workflow] ${about.what}: read failed for instance ${about.instanceId} (attempt ${attempt} of ${budget.attempts}), retrying in ${delayMs} ms`, error);
|
|
21
|
+
await new Promise((resolve) => {
|
|
22
|
+
setTimeout(resolve, delayMs);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//#endregion
|
|
27
|
+
export { readWithRetry };
|
|
28
|
+
|
|
29
|
+
//# sourceMappingURL=fresh-read.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fresh-read.mjs","names":[],"sources":["../../src/host/fresh-read.ts"],"sourcesContent":["import { isRetryable } from '../engine/retry';\n\n/**\n * A read that runs outside any durable step — a runner's admission at its entry, the registry\n * check on AWS — which must see the record as it is now (an erasure since the last checkpoint)\n * and so cannot be checkpointed: a step would replay the first invocation's verdict on every\n * wake. Only the read itself is retried, a bounded number of times, on a transient failure (a\n * dropped connection, a timeout); what it returns is never retried — a refusal is the caller's\n * — and a `PermanentError` (or any error that says `retryable: false`) ends it at once.\n */\nexport async function readWithRetry<T>(\n fn: () => Promise<T>,\n about: { what: string; instanceId: string },\n budget: { attempts: number; delaysMs: readonly number[] } = { attempts: 3, delaysMs: [100, 300] }\n): Promise<T> {\n for (let attempt = 1; ; attempt++) {\n try {\n return await fn();\n } catch (error) {\n if (attempt >= budget.attempts || !isRetryable(error)) throw error;\n const delayMs = budget.delaysMs[Math.min(attempt, budget.delaysMs.length) - 1] ?? 0;\n console.warn(\n `[workflow] ${about.what}: read failed for instance ${about.instanceId} (attempt ${attempt} of ${budget.attempts}), retrying in ${delayMs} ms`,\n error\n );\n await new Promise<void>((resolve) => {\n setTimeout(resolve, delayMs);\n });\n }\n }\n}\n"],"mappings":";;;;;;;;;;AAUA,eAAsB,cACpB,IACA,OACA,SAA4D;CAAE,UAAU;CAAG,UAAU,CAAC,KAAK,GAAG;AAAE,GACpF;CACZ,KAAK,IAAI,UAAU,IAAK,WACtB,IAAI;EACF,OAAO,MAAM,GAAG;CAClB,SAAS,OAAO;EACd,IAAI,WAAW,OAAO,YAAY,CAAC,YAAY,KAAK,GAAG,MAAM;EAC7D,MAAM,UAAU,OAAO,SAAS,KAAK,IAAI,SAAS,OAAO,SAAS,MAAM,IAAI,MAAM;EAClF,QAAQ,KACN,cAAc,MAAM,KAAK,6BAA6B,MAAM,WAAW,YAAY,QAAQ,MAAM,OAAO,SAAS,iBAAiB,QAAQ,MAC1I,KACF;EACA,MAAM,IAAI,SAAe,YAAY;GACnC,WAAW,SAAS,OAAO;EAC7B,CAAC;CACH;AAEJ"}
|