@shware/workflow 2.0.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -2
- package/dist/apns-relay.cjs +4 -17
- package/dist/apns-relay.cjs.map +1 -1
- package/dist/apns-relay.mjs +4 -17
- package/dist/apns-relay.mjs.map +1 -1
- package/dist/aws/engine-step.cjs +104 -0
- package/dist/aws/engine-step.cjs.map +1 -0
- package/dist/aws/engine-step.d.cts +28 -0
- package/dist/aws/engine-step.d.cts.map +1 -0
- package/dist/aws/engine-step.d.mts +28 -0
- package/dist/aws/engine-step.d.mts.map +1 -0
- package/dist/aws/engine-step.mjs +102 -0
- package/dist/aws/engine-step.mjs.map +1 -0
- package/dist/aws/function-url.cjs +62 -0
- package/dist/aws/function-url.cjs.map +1 -0
- package/dist/aws/function-url.d.cts +38 -0
- package/dist/aws/function-url.d.cts.map +1 -0
- package/dist/aws/function-url.d.mts +38 -0
- package/dist/aws/function-url.d.mts.map +1 -0
- package/dist/aws/function-url.mjs +59 -0
- package/dist/aws/function-url.mjs.map +1 -0
- package/dist/aws/index.cjs +160 -0
- package/dist/aws/index.cjs.map +1 -0
- package/dist/aws/index.d.cts +62 -0
- package/dist/aws/index.d.cts.map +1 -0
- package/dist/aws/index.d.mts +62 -0
- package/dist/aws/index.d.mts.map +1 -0
- package/dist/aws/index.mjs +148 -0
- package/dist/aws/index.mjs.map +1 -0
- package/dist/aws/instances.cjs +176 -0
- package/dist/aws/instances.cjs.map +1 -0
- package/dist/aws/instances.d.cts +75 -0
- package/dist/aws/instances.d.cts.map +1 -0
- package/dist/aws/instances.d.mts +75 -0
- package/dist/aws/instances.d.mts.map +1 -0
- package/dist/aws/instances.mjs +172 -0
- package/dist/aws/instances.mjs.map +1 -0
- package/dist/aws/lambda-api.cjs +89 -0
- package/dist/aws/lambda-api.cjs.map +1 -0
- package/dist/aws/lambda-api.d.cts +43 -0
- package/dist/aws/lambda-api.d.cts.map +1 -0
- package/dist/aws/lambda-api.d.mts +43 -0
- package/dist/aws/lambda-api.d.mts.map +1 -0
- package/dist/aws/lambda-api.mjs +88 -0
- package/dist/aws/lambda-api.mjs.map +1 -0
- package/dist/client.d.cts +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/cloudflare/bindings.cjs +19 -0
- package/dist/cloudflare/bindings.cjs.map +1 -1
- package/dist/cloudflare/bindings.d.cts +37 -0
- package/dist/cloudflare/bindings.d.cts.map +1 -1
- package/dist/cloudflare/bindings.d.mts +37 -0
- package/dist/cloudflare/bindings.d.mts.map +1 -1
- package/dist/cloudflare/bindings.mjs +18 -1
- package/dist/cloudflare/bindings.mjs.map +1 -1
- package/dist/cloudflare/index.cjs +32 -28
- package/dist/cloudflare/index.d.cts +8 -6
- package/dist/cloudflare/index.d.mts +8 -6
- package/dist/cloudflare/index.mjs +12 -10
- package/dist/cloudflare/runner.cjs +27 -76
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts +7 -2
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts +7 -2
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +26 -75
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/cloudflare/worker.cjs +22 -203
- package/dist/cloudflare/worker.cjs.map +1 -1
- package/dist/cloudflare/worker.d.cts +10 -56
- package/dist/cloudflare/worker.d.cts.map +1 -1
- package/dist/cloudflare/worker.d.mts +10 -56
- package/dist/cloudflare/worker.d.mts.map +1 -1
- package/dist/cloudflare/worker.mjs +20 -199
- package/dist/cloudflare/worker.mjs.map +1 -1
- package/dist/email-sender.cjs +62 -0
- package/dist/email-sender.cjs.map +1 -0
- package/dist/email-sender.d.cts +70 -0
- package/dist/email-sender.d.cts.map +1 -0
- package/dist/email-sender.d.mts +70 -0
- package/dist/email-sender.d.mts.map +1 -0
- package/dist/email-sender.mjs +60 -0
- package/dist/email-sender.mjs.map +1 -0
- package/dist/email.cjs +21 -0
- package/dist/email.cjs.map +1 -1
- package/dist/email.d.cts +6 -0
- package/dist/email.d.cts.map +1 -1
- package/dist/email.d.mts +6 -0
- package/dist/email.d.mts.map +1 -1
- package/dist/email.mjs +21 -1
- package/dist/email.mjs.map +1 -1
- package/dist/engine/index.cjs +1 -1
- package/dist/engine/index.mjs +1 -1
- package/dist/engine/interpreter.cjs +1 -1
- package/dist/engine/interpreter.cjs.map +1 -1
- package/dist/engine/interpreter.mjs +1 -1
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +6 -1
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +6 -1
- package/dist/engine/ports.d.mts.map +1 -1
- package/dist/{cloudflare → host}/channels.cjs +13 -11
- package/dist/host/channels.cjs.map +1 -0
- package/dist/{cloudflare → host}/channels.d.cts +8 -3
- package/dist/host/channels.d.cts.map +1 -0
- package/dist/{cloudflare → host}/channels.d.mts +8 -3
- package/dist/host/channels.d.mts.map +1 -0
- package/dist/{cloudflare → host}/channels.mjs +9 -7
- package/dist/host/channels.mjs.map +1 -0
- package/dist/host/execute.cjs +82 -0
- package/dist/host/execute.cjs.map +1 -0
- package/dist/host/execute.d.cts +41 -0
- package/dist/host/execute.d.cts.map +1 -0
- package/dist/host/execute.d.mts +41 -0
- package/dist/host/execute.d.mts.map +1 -0
- package/dist/host/execute.mjs +81 -0
- package/dist/host/execute.mjs.map +1 -0
- package/dist/host/journey-host.cjs +214 -0
- package/dist/host/journey-host.cjs.map +1 -0
- package/dist/host/journey-host.d.cts +105 -0
- package/dist/host/journey-host.d.cts.map +1 -0
- package/dist/host/journey-host.d.mts +105 -0
- package/dist/host/journey-host.d.mts.map +1 -0
- package/dist/host/journey-host.mjs +210 -0
- package/dist/host/journey-host.mjs.map +1 -0
- package/dist/{cloudflare → host}/router.cjs +28 -9
- package/dist/host/router.cjs.map +1 -0
- package/dist/{cloudflare → host}/router.d.cts +2 -2
- package/dist/host/router.d.cts.map +1 -0
- package/dist/{cloudflare → host}/router.d.mts +2 -2
- package/dist/host/router.d.mts.map +1 -0
- package/dist/{cloudflare → host}/router.mjs +28 -9
- package/dist/host/router.mjs.map +1 -0
- package/dist/host/senders.cjs +63 -0
- package/dist/host/senders.cjs.map +1 -0
- package/dist/host/senders.d.cts +31 -0
- package/dist/host/senders.d.cts.map +1 -0
- package/dist/host/senders.d.mts +31 -0
- package/dist/host/senders.d.mts.map +1 -0
- package/dist/host/senders.mjs +59 -0
- package/dist/host/senders.mjs.map +1 -0
- package/dist/http2-client.cjs +88 -0
- package/dist/http2-client.cjs.map +1 -0
- package/dist/http2-client.d.cts +44 -0
- package/dist/http2-client.d.cts.map +1 -0
- package/dist/http2-client.d.mts +44 -0
- package/dist/http2-client.d.mts.map +1 -0
- package/dist/http2-client.mjs +86 -0
- package/dist/http2-client.mjs.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/react-email.cjs.map +1 -1
- package/dist/react-email.d.cts +1 -2
- package/dist/react-email.d.cts.map +1 -1
- package/dist/react-email.d.mts +1 -2
- package/dist/react-email.d.mts.map +1 -1
- package/dist/react-email.mjs.map +1 -1
- package/dist/store/d1.cjs +1 -0
- package/dist/store/d1.cjs.map +1 -1
- package/dist/store/d1.d.cts.map +1 -1
- package/dist/store/d1.d.mts.map +1 -1
- package/dist/store/d1.mjs +1 -0
- package/dist/store/d1.mjs.map +1 -1
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +63 -0
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +63 -0
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres.cjs +143 -2
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +4 -1
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +4 -1
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +143 -2
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +26 -0
- package/dist/store/tables.cjs.map +1 -1
- package/dist/store/tables.d.cts +1 -0
- package/dist/store/tables.d.cts.map +1 -1
- package/dist/store/tables.d.mts +1 -0
- package/dist/store/tables.d.mts.map +1 -1
- package/dist/store/tables.mjs +26 -0
- package/dist/store/tables.mjs.map +1 -1
- package/dist/transports/cloudflare-email.cjs +59 -0
- package/dist/transports/cloudflare-email.cjs.map +1 -0
- package/dist/transports/cloudflare-email.d.cts +38 -0
- package/dist/transports/cloudflare-email.d.cts.map +1 -0
- package/dist/transports/cloudflare-email.d.mts +38 -0
- package/dist/transports/cloudflare-email.d.mts.map +1 -0
- package/dist/transports/cloudflare-email.mjs +57 -0
- package/dist/transports/cloudflare-email.mjs.map +1 -0
- package/dist/transports/ses.cjs +91 -0
- package/dist/transports/ses.cjs.map +1 -0
- package/dist/transports/ses.d.cts +39 -0
- package/dist/transports/ses.d.cts.map +1 -0
- package/dist/transports/ses.d.mts +39 -0
- package/dist/transports/ses.d.mts.map +1 -0
- package/dist/transports/ses.mjs +89 -0
- package/dist/transports/ses.mjs.map +1 -0
- package/dist/transports/sigv4.cjs +83 -0
- package/dist/transports/sigv4.cjs.map +1 -0
- package/dist/transports/sigv4.d.cts +27 -0
- package/dist/transports/sigv4.d.cts.map +1 -0
- package/dist/transports/sigv4.d.mts +27 -0
- package/dist/transports/sigv4.d.mts.map +1 -0
- package/dist/transports/sigv4.mjs +82 -0
- package/dist/transports/sigv4.mjs.map +1 -0
- package/package.json +53 -1
- package/dist/cloudflare/channels.cjs.map +0 -1
- package/dist/cloudflare/channels.d.cts.map +0 -1
- package/dist/cloudflare/channels.d.mts.map +0 -1
- package/dist/cloudflare/channels.mjs.map +0 -1
- package/dist/cloudflare/router.cjs.map +0 -1
- package/dist/cloudflare/router.d.cts.map +0 -1
- package/dist/cloudflare/router.d.mts.map +0 -1
- package/dist/cloudflare/router.mjs.map +0 -1
- package/dist/cloudflare/senders.cjs +0 -121
- package/dist/cloudflare/senders.cjs.map +0 -1
- package/dist/cloudflare/senders.d.cts +0 -72
- package/dist/cloudflare/senders.d.cts.map +0 -1
- package/dist/cloudflare/senders.d.mts +0 -72
- package/dist/cloudflare/senders.d.mts.map +0 -1
- package/dist/cloudflare/senders.mjs +0 -115
- package/dist/cloudflare/senders.mjs.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ApnsPushSender, apnsKeyFromBase64 } from "../apns.mjs";
|
|
2
2
|
import { indexTemplates, lookupTemplate, renderDiscordContent, renderSlackContent } from "../templates.mjs";
|
|
3
3
|
import { apnsEnvironment } from "../config.mjs";
|
|
4
|
+
import { EmailSender } from "../email-sender.mjs";
|
|
4
5
|
import { FcmPushSender, serviceAccountFromBase64 } from "../fcm.mjs";
|
|
5
6
|
import { resolveRuntime } from "../runtime.mjs";
|
|
6
7
|
import { registryPushRenderer } from "../push.mjs";
|
|
@@ -8,11 +9,11 @@ import { registryRenderer } from "../react-email.mjs";
|
|
|
8
9
|
import { DiscordWebhookSender, SlackWebhookSender } from "../webhooks.mjs";
|
|
9
10
|
import { WebPushSender } from "../webpush.mjs";
|
|
10
11
|
import { removeDevice } from "./router.mjs";
|
|
11
|
-
import {
|
|
12
|
-
//#region src/
|
|
12
|
+
import { routeByChannel, routeByPlatform } from "./senders.mjs";
|
|
13
|
+
//#region src/host/channels.ts
|
|
13
14
|
/**
|
|
14
15
|
* The outlets a `JourneyConfig` describes, assembled against the environment:
|
|
15
|
-
* email through the
|
|
16
|
+
* email through the host's transport (SES, Cloudflare Email, ...) with each template's addresses, iOS pushes
|
|
16
17
|
* straight to APNs with the configured key (the `.p8` from
|
|
17
18
|
* `APNS_PRIVATE_KEY_BASE64`), Android pushes through FCM with the service
|
|
18
19
|
* account from `GOOGLE_APPLICATION_CREDENTIALS_BASE64`. A channel the config
|
|
@@ -56,9 +57,10 @@ function channelsFromConfig(config, env, options = {}) {
|
|
|
56
57
|
const email = () => {
|
|
57
58
|
const registry = config.templates?.email;
|
|
58
59
|
if (registry === void 0) return unavailable("no email templates are registered");
|
|
59
|
-
|
|
60
|
-
return
|
|
61
|
-
|
|
60
|
+
const transport = options.email;
|
|
61
|
+
if (transport === void 0) return unavailable("no email transport is configured (the host's `email` option)");
|
|
62
|
+
return new EmailSender({
|
|
63
|
+
transport,
|
|
62
64
|
render: registryRenderer(registry, config.email)
|
|
63
65
|
});
|
|
64
66
|
};
|
|
@@ -80,7 +82,7 @@ function channelsFromConfig(config, env, options = {}) {
|
|
|
80
82
|
render,
|
|
81
83
|
onUnregistered,
|
|
82
84
|
...env.APNS_RELAY_ORIGIN === void 0 ? {} : { origin: env.APNS_RELAY_ORIGIN },
|
|
83
|
-
...options.fetch === void 0 ? {} : { fetch: options.fetch }
|
|
85
|
+
...(options.apnsFetch ?? options.fetch) === void 0 ? {} : { fetch: options.apnsFetch ?? options.fetch }
|
|
84
86
|
}));
|
|
85
87
|
const fcm = config.fcm;
|
|
86
88
|
if (fcm !== void 0) platforms.android = lazy(() => new FcmPushSender({
|
|
@@ -0,0 +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":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_contract = require("../contract.cjs");
|
|
3
|
+
const require_runtime = require("../runtime.cjs");
|
|
4
|
+
const require_engine_interpreter = require("../engine/interpreter.cjs");
|
|
5
|
+
const require_engine_time_scale = require("../engine/time-scale.cjs");
|
|
6
|
+
const require_store_facts = require("../store/facts.cjs");
|
|
7
|
+
const require_host_router = require("./router.cjs");
|
|
8
|
+
//#region src/host/execute.ts
|
|
9
|
+
/** Development aid: echo each outbound message before handing it to the real sender. */
|
|
10
|
+
function logging(sender) {
|
|
11
|
+
return { async send(message) {
|
|
12
|
+
console.log(`[journey] ${message.channel} ${message.template} → ${message.recipient ?? message.userId} ${JSON.stringify(message.props)}`);
|
|
13
|
+
return sender.send(message);
|
|
14
|
+
} };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The IR is read outside a step: content addressing makes it immutable, so the read is
|
|
18
|
+
* replay-safe.
|
|
19
|
+
*/
|
|
20
|
+
async function executeJourney(input, parts) {
|
|
21
|
+
const { instanceId, params, step } = input;
|
|
22
|
+
const { workflowId, contentHash, userId } = params;
|
|
23
|
+
const { store, env, observer, contract } = parts;
|
|
24
|
+
const stored = await store.getWorkflowIR(contentHash);
|
|
25
|
+
if (stored === void 0) throw new Error(`WorkflowIR not found: ${workflowId}@${contentHash}`);
|
|
26
|
+
const runtime = require_runtime.resolveRuntime({ current: parts.runtime }, "current");
|
|
27
|
+
const ir = require_engine_time_scale.scaleDurations(stored, runtime.timeScale);
|
|
28
|
+
const finished = (status, convertedAt) => observer?.finished?.({
|
|
29
|
+
workflow: workflowId,
|
|
30
|
+
instanceId,
|
|
31
|
+
userId,
|
|
32
|
+
status,
|
|
33
|
+
at: Date.now(),
|
|
34
|
+
...convertedAt === void 0 ? {} : { convertedAt }
|
|
35
|
+
});
|
|
36
|
+
const messages = runtime.logMessages ? logging(parts.messages) : parts.messages;
|
|
37
|
+
const events = { emit: async (name, payload, id) => {
|
|
38
|
+
let accepted = payload;
|
|
39
|
+
if (contract !== void 0) {
|
|
40
|
+
const verdict = require_contract.checkEvent(contract, name, payload);
|
|
41
|
+
if (!verdict.ok) throw new require_contract.ContractValidationError(name, verdict.issues);
|
|
42
|
+
accepted = verdict.data;
|
|
43
|
+
}
|
|
44
|
+
await require_host_router.ingestEvent(env, {
|
|
45
|
+
id,
|
|
46
|
+
userId,
|
|
47
|
+
event: name,
|
|
48
|
+
payload: accepted
|
|
49
|
+
}, store);
|
|
50
|
+
} };
|
|
51
|
+
let outcome;
|
|
52
|
+
try {
|
|
53
|
+
outcome = await require_engine_interpreter.runJourney(ir, {
|
|
54
|
+
userId,
|
|
55
|
+
instanceId,
|
|
56
|
+
enteredAtMs: input.enteredAtMs,
|
|
57
|
+
step,
|
|
58
|
+
facts: new require_store_facts.JourneyFactSource(store, userId),
|
|
59
|
+
messages,
|
|
60
|
+
events,
|
|
61
|
+
actions: parts.actions,
|
|
62
|
+
validator: parts.validator,
|
|
63
|
+
observer
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
await step.do("finalize:failed", async () => {
|
|
67
|
+
await store.setEntryStatus(instanceId, "failed");
|
|
68
|
+
await finished("failed");
|
|
69
|
+
});
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
await step.do("finalize", async () => {
|
|
73
|
+
await store.setEntryStatus(instanceId, outcome.status);
|
|
74
|
+
await store.unsubscribe(instanceId);
|
|
75
|
+
await finished(outcome.status, outcome.convertedAt);
|
|
76
|
+
});
|
|
77
|
+
return outcome;
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
exports.executeJourney = executeJourney;
|
|
81
|
+
|
|
82
|
+
//# sourceMappingURL=execute.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute.cjs","names":["resolveRuntime","scaleDurations","checkEvent","ContractValidationError","ingestEvent","runJourney","JourneyFactSource"],"sources":["../../src/host/execute.ts"],"sourcesContent":["import type { JourneyEnv, JourneyParams } from '../cloudflare/bindings';\nimport { 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 { ingestEvent } from './router';\n\n/**\n * One journey instance run to its end, on whatever durable runtime supplies the `EngineStep`:\n * the IR loaded by the hash pinned at entry, the outlets wired, the interpreter run, and the\n * ledger finalized. Cloudflare's runner and the Lambda handler both come through here; only\n * 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\n/**\n * The IR is read outside a step: content addressing makes it immutable, so the read is\n * replay-safe.\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 stored = await store.getWorkflowIR(contentHash);\n if (stored === undefined) {\n throw new Error(`WorkflowIR not found: ${workflowId}@${contentHash}`);\n }\n const runtime = resolveRuntime({ current: parts.runtime }, 'current');\n // Development runs the journey in fast-forward; the pinned hash still names the version.\n const ir = scaleDurations(stored, runtime.timeScale);\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 let outcome: JourneyOutcome;\n try {\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 are deliberately left in place. They are cleaned up on\n * every ordinary ending, but here the wake rows are the only evidence of\n * what the instance was waiting for, and dropping them would also break\n * the instance if the platform does re-run it.\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 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":";;;;;;;;;AAkDA,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;;;;;AAMA,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,SAAS,MAAM,MAAM,cAAc,WAAW;CACpD,IAAI,WAAW,KAAA,GACb,MAAM,IAAI,MAAM,yBAAyB,WAAW,GAAG,aAAa;CAEtE,MAAM,UAAUA,gBAAAA,eAAe,EAAE,SAAS,MAAM,QAAQ,GAAG,SAAS;CAEpE,MAAM,KAAKC,0BAAAA,eAAe,QAAQ,QAAQ,SAAS;CAEnD,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;CAEA,IAAI;CACJ,IAAI;EACF,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;EAed,MAAM,KAAK,GAAG,mBAAmB,YAAY;GAC3C,MAAM,MAAM,eAAe,YAAY,QAAQ;GAC/C,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,41 @@
|
|
|
1
|
+
import { ActionInvoker, EngineStep, JourneyObserver, JourneyOutcome, MessageSender, MessageValidator } from "../engine/ports.cjs";
|
|
2
|
+
import { JourneyEnv, JourneyParams } from "../cloudflare/bindings.cjs";
|
|
3
|
+
import { JourneyStore } from "../store/index.cjs";
|
|
4
|
+
import { Contract } from "../contract.cjs";
|
|
5
|
+
import { JourneyRuntimeOptions } from "../runtime.cjs";
|
|
6
|
+
//#region src/host/execute.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* One journey instance run to its end, on whatever durable runtime supplies the `EngineStep`:
|
|
9
|
+
* the IR loaded by the hash pinned at entry, the outlets wired, the interpreter run, and the
|
|
10
|
+
* ledger finalized. Cloudflare's runner and the Lambda handler both come through here; only
|
|
11
|
+
* their EngineStep differs.
|
|
12
|
+
*/
|
|
13
|
+
/** Everything a run is wired to, assembled by the host. */
|
|
14
|
+
export interface JourneyRunParts {
|
|
15
|
+
store: JourneyStore;
|
|
16
|
+
/**
|
|
17
|
+
* The env the emitted events are ingested with: its `JOURNEY` starts and wakes instances on
|
|
18
|
+
* this runtime.
|
|
19
|
+
*/
|
|
20
|
+
env: JourneyEnv;
|
|
21
|
+
runtime: JourneyRuntimeOptions;
|
|
22
|
+
messages: MessageSender;
|
|
23
|
+
actions: ActionInvoker;
|
|
24
|
+
contract?: Contract | undefined;
|
|
25
|
+
validator?: MessageValidator | undefined;
|
|
26
|
+
observer?: JourneyObserver | undefined;
|
|
27
|
+
}
|
|
28
|
+
export interface JourneyRunInput {
|
|
29
|
+
instanceId: string;
|
|
30
|
+
params: JourneyParams;
|
|
31
|
+
/** When the trigger happened: replay-stable, never Date.now() at run time. */
|
|
32
|
+
enteredAtMs: number;
|
|
33
|
+
step: EngineStep;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The IR is read outside a step: content addressing makes it immutable, so the read is
|
|
37
|
+
* replay-safe.
|
|
38
|
+
*/
|
|
39
|
+
export declare function executeJourney(input: JourneyRunInput, parts: JourneyRunParts): Promise<JourneyOutcome>;
|
|
40
|
+
//#endregion
|
|
41
|
+
//# sourceMappingURL=execute.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute.d.cts","names":[],"sources":["../../src/host/execute.ts"],"mappings":";;;;;;;;;;;;;iBA0BiB;EACf,OAAO;;;;;EAKP,KAAK;EACL,SAAS;EACT,UAAU;EACV,SAAS;EACT,WAAW;EACX,YAAY;EACZ,WAAW;;iBAGI;EACf;EACA,QAAQ;;EAER;EACA,MAAM;;;;;;wBAmBc,eACpB,OAAO,iBACP,OAAO,kBACN,QAAQ"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ActionInvoker, EngineStep, JourneyObserver, JourneyOutcome, MessageSender, MessageValidator } from "../engine/ports.mjs";
|
|
2
|
+
import { JourneyEnv, JourneyParams } from "../cloudflare/bindings.mjs";
|
|
3
|
+
import { JourneyStore } from "../store/index.mjs";
|
|
4
|
+
import { Contract } from "../contract.mjs";
|
|
5
|
+
import { JourneyRuntimeOptions } from "../runtime.mjs";
|
|
6
|
+
//#region src/host/execute.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* One journey instance run to its end, on whatever durable runtime supplies the `EngineStep`:
|
|
9
|
+
* the IR loaded by the hash pinned at entry, the outlets wired, the interpreter run, and the
|
|
10
|
+
* ledger finalized. Cloudflare's runner and the Lambda handler both come through here; only
|
|
11
|
+
* their EngineStep differs.
|
|
12
|
+
*/
|
|
13
|
+
/** Everything a run is wired to, assembled by the host. */
|
|
14
|
+
export interface JourneyRunParts {
|
|
15
|
+
store: JourneyStore;
|
|
16
|
+
/**
|
|
17
|
+
* The env the emitted events are ingested with: its `JOURNEY` starts and wakes instances on
|
|
18
|
+
* this runtime.
|
|
19
|
+
*/
|
|
20
|
+
env: JourneyEnv;
|
|
21
|
+
runtime: JourneyRuntimeOptions;
|
|
22
|
+
messages: MessageSender;
|
|
23
|
+
actions: ActionInvoker;
|
|
24
|
+
contract?: Contract | undefined;
|
|
25
|
+
validator?: MessageValidator | undefined;
|
|
26
|
+
observer?: JourneyObserver | undefined;
|
|
27
|
+
}
|
|
28
|
+
export interface JourneyRunInput {
|
|
29
|
+
instanceId: string;
|
|
30
|
+
params: JourneyParams;
|
|
31
|
+
/** When the trigger happened: replay-stable, never Date.now() at run time. */
|
|
32
|
+
enteredAtMs: number;
|
|
33
|
+
step: EngineStep;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The IR is read outside a step: content addressing makes it immutable, so the read is
|
|
37
|
+
* replay-safe.
|
|
38
|
+
*/
|
|
39
|
+
export declare function executeJourney(input: JourneyRunInput, parts: JourneyRunParts): Promise<JourneyOutcome>;
|
|
40
|
+
//#endregion
|
|
41
|
+
//# sourceMappingURL=execute.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute.d.mts","names":[],"sources":["../../src/host/execute.ts"],"mappings":";;;;;;;;;;;;;iBA0BiB;EACf,OAAO;;;;;EAKP,KAAK;EACL,SAAS;EACT,UAAU;EACV,SAAS;EACT,WAAW;EACX,YAAY;EACZ,WAAW;;iBAGI;EACf;EACA,QAAQ;;EAER;EACA,MAAM;;;;;;wBAmBc,eACpB,OAAO,iBACP,OAAO,kBACN,QAAQ"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ContractValidationError, checkEvent } from "../contract.mjs";
|
|
2
|
+
import { resolveRuntime } from "../runtime.mjs";
|
|
3
|
+
import { runJourney } from "../engine/interpreter.mjs";
|
|
4
|
+
import { scaleDurations } from "../engine/time-scale.mjs";
|
|
5
|
+
import { JourneyFactSource } from "../store/facts.mjs";
|
|
6
|
+
import { ingestEvent } from "./router.mjs";
|
|
7
|
+
//#region src/host/execute.ts
|
|
8
|
+
/** Development aid: echo each outbound message before handing it to the real sender. */
|
|
9
|
+
function logging(sender) {
|
|
10
|
+
return { async send(message) {
|
|
11
|
+
console.log(`[journey] ${message.channel} ${message.template} → ${message.recipient ?? message.userId} ${JSON.stringify(message.props)}`);
|
|
12
|
+
return sender.send(message);
|
|
13
|
+
} };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The IR is read outside a step: content addressing makes it immutable, so the read is
|
|
17
|
+
* replay-safe.
|
|
18
|
+
*/
|
|
19
|
+
async function executeJourney(input, parts) {
|
|
20
|
+
const { instanceId, params, step } = input;
|
|
21
|
+
const { workflowId, contentHash, userId } = params;
|
|
22
|
+
const { store, env, observer, contract } = parts;
|
|
23
|
+
const stored = await store.getWorkflowIR(contentHash);
|
|
24
|
+
if (stored === void 0) throw new Error(`WorkflowIR not found: ${workflowId}@${contentHash}`);
|
|
25
|
+
const runtime = resolveRuntime({ current: parts.runtime }, "current");
|
|
26
|
+
const ir = scaleDurations(stored, runtime.timeScale);
|
|
27
|
+
const finished = (status, convertedAt) => observer?.finished?.({
|
|
28
|
+
workflow: workflowId,
|
|
29
|
+
instanceId,
|
|
30
|
+
userId,
|
|
31
|
+
status,
|
|
32
|
+
at: Date.now(),
|
|
33
|
+
...convertedAt === void 0 ? {} : { convertedAt }
|
|
34
|
+
});
|
|
35
|
+
const messages = runtime.logMessages ? logging(parts.messages) : parts.messages;
|
|
36
|
+
const events = { emit: async (name, payload, id) => {
|
|
37
|
+
let accepted = payload;
|
|
38
|
+
if (contract !== void 0) {
|
|
39
|
+
const verdict = checkEvent(contract, name, payload);
|
|
40
|
+
if (!verdict.ok) throw new ContractValidationError(name, verdict.issues);
|
|
41
|
+
accepted = verdict.data;
|
|
42
|
+
}
|
|
43
|
+
await ingestEvent(env, {
|
|
44
|
+
id,
|
|
45
|
+
userId,
|
|
46
|
+
event: name,
|
|
47
|
+
payload: accepted
|
|
48
|
+
}, store);
|
|
49
|
+
} };
|
|
50
|
+
let outcome;
|
|
51
|
+
try {
|
|
52
|
+
outcome = await runJourney(ir, {
|
|
53
|
+
userId,
|
|
54
|
+
instanceId,
|
|
55
|
+
enteredAtMs: input.enteredAtMs,
|
|
56
|
+
step,
|
|
57
|
+
facts: new JourneyFactSource(store, userId),
|
|
58
|
+
messages,
|
|
59
|
+
events,
|
|
60
|
+
actions: parts.actions,
|
|
61
|
+
validator: parts.validator,
|
|
62
|
+
observer
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
await step.do("finalize:failed", async () => {
|
|
66
|
+
await store.setEntryStatus(instanceId, "failed");
|
|
67
|
+
await finished("failed");
|
|
68
|
+
});
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
await step.do("finalize", async () => {
|
|
72
|
+
await store.setEntryStatus(instanceId, outcome.status);
|
|
73
|
+
await store.unsubscribe(instanceId);
|
|
74
|
+
await finished(outcome.status, outcome.convertedAt);
|
|
75
|
+
});
|
|
76
|
+
return outcome;
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
export { executeJourney };
|
|
80
|
+
|
|
81
|
+
//# sourceMappingURL=execute.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute.mjs","names":[],"sources":["../../src/host/execute.ts"],"sourcesContent":["import type { JourneyEnv, JourneyParams } from '../cloudflare/bindings';\nimport { 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 { ingestEvent } from './router';\n\n/**\n * One journey instance run to its end, on whatever durable runtime supplies the `EngineStep`:\n * the IR loaded by the hash pinned at entry, the outlets wired, the interpreter run, and the\n * ledger finalized. Cloudflare's runner and the Lambda handler both come through here; only\n * 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\n/**\n * The IR is read outside a step: content addressing makes it immutable, so the read is\n * replay-safe.\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 stored = await store.getWorkflowIR(contentHash);\n if (stored === undefined) {\n throw new Error(`WorkflowIR not found: ${workflowId}@${contentHash}`);\n }\n const runtime = resolveRuntime({ current: parts.runtime }, 'current');\n // Development runs the journey in fast-forward; the pinned hash still names the version.\n const ir = scaleDurations(stored, runtime.timeScale);\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 let outcome: JourneyOutcome;\n try {\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 are deliberately left in place. They are cleaned up on\n * every ordinary ending, but here the wake rows are the only evidence of\n * what the instance was waiting for, and dropping them would also break\n * the instance if the platform does re-run it.\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 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":";;;;;;;;AAkDA,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;;;;;AAMA,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,SAAS,MAAM,MAAM,cAAc,WAAW;CACpD,IAAI,WAAW,KAAA,GACb,MAAM,IAAI,MAAM,yBAAyB,WAAW,GAAG,aAAa;CAEtE,MAAM,UAAU,eAAe,EAAE,SAAS,MAAM,QAAQ,GAAG,SAAS;CAEpE,MAAM,KAAK,eAAe,QAAQ,QAAQ,SAAS;CAEnD,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;CAEA,IAAI;CACJ,IAAI;EACF,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;EAed,MAAM,KAAK,GAAG,mBAAmB,YAAY;GAC3C,MAAM,MAAM,eAAe,YAAY,QAAQ;GAC/C,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,214 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
3
|
+
const require_email = require("../email.cjs");
|
|
4
|
+
const require_templates = require("../templates.cjs");
|
|
5
|
+
const require_config = require("../config.cjs");
|
|
6
|
+
const require_contract = require("../contract.cjs");
|
|
7
|
+
require("../store/tables.cjs");
|
|
8
|
+
const require_react_email = require("../react-email.cjs");
|
|
9
|
+
const require_engine_time_scale = require("../engine/time-scale.cjs");
|
|
10
|
+
const require_cloudflare_d1_env = require("../cloudflare/d1-env.cjs");
|
|
11
|
+
const require_reporting_tracking = require("../reporting/tracking.cjs");
|
|
12
|
+
const require_store_index = require("../store/index.cjs");
|
|
13
|
+
const require_host_router = require("./router.cjs");
|
|
14
|
+
const require_engine_actions = require("../engine/actions.cjs");
|
|
15
|
+
const require_reporting_observer = require("../reporting/observer.cjs");
|
|
16
|
+
const require_host_channels = require("./channels.cjs");
|
|
17
|
+
let zod_mini = require("zod/mini");
|
|
18
|
+
zod_mini = require_runtime.__toESM(zod_mini, 1);
|
|
19
|
+
//#region src/host/journey-host.ts
|
|
20
|
+
/** The host's environment failed its schema (`env` option). */
|
|
21
|
+
var EnvValidationError = class extends Error {
|
|
22
|
+
issues;
|
|
23
|
+
name = "EnvValidationError";
|
|
24
|
+
constructor(issues) {
|
|
25
|
+
super(`the host's environment is invalid:\n ${issues.join("\n ")}`);
|
|
26
|
+
this.issues = issues;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
function createJourneyHost(options, platform = {}) {
|
|
30
|
+
const { config } = options;
|
|
31
|
+
const emailIds = require_templates.indexTemplates("email", config.templates?.email);
|
|
32
|
+
require_email.assertEmailConfig(config, emailIds.values());
|
|
33
|
+
const checked = /* @__PURE__ */ new WeakMap();
|
|
34
|
+
const checkEnv = (env) => {
|
|
35
|
+
if (options.env === void 0) return env;
|
|
36
|
+
const cached = checked.get(env);
|
|
37
|
+
if (cached !== void 0) return cached;
|
|
38
|
+
const result = zod_mini.safeParse(options.env, env);
|
|
39
|
+
if (!result.success) throw new EnvValidationError(result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`));
|
|
40
|
+
const merged = {
|
|
41
|
+
...env,
|
|
42
|
+
...result.data
|
|
43
|
+
};
|
|
44
|
+
checked.set(env, merged);
|
|
45
|
+
return merged;
|
|
46
|
+
};
|
|
47
|
+
let compiled;
|
|
48
|
+
const bundle = () => compiled ??= require_config.compileConfig(config);
|
|
49
|
+
const emails = config.templates?.email;
|
|
50
|
+
const database = { schema: config.database?.schema ?? "workflow" };
|
|
51
|
+
const lease = (env) => {
|
|
52
|
+
const opened = options.store === void 0 ? require_cloudflare_d1_env.d1Store(env) : options.store(env, database);
|
|
53
|
+
const noop = async () => {};
|
|
54
|
+
const factor = require_host_channels.runtimeFromConfig(config, env).timeScale;
|
|
55
|
+
return "store" in opened ? {
|
|
56
|
+
store: require_engine_time_scale.scaleSegmentWindows(opened.store, factor),
|
|
57
|
+
close: opened.close ?? noop
|
|
58
|
+
} : {
|
|
59
|
+
store: require_engine_time_scale.scaleSegmentWindows(opened, factor),
|
|
60
|
+
close: noop
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
const messages = (env, store) => {
|
|
64
|
+
if (options.messages !== void 0) return options.messages(env, store);
|
|
65
|
+
return require_host_channels.channelsFromConfig(config, env, {
|
|
66
|
+
store,
|
|
67
|
+
email: options.email?.(env),
|
|
68
|
+
apnsFetch: platform.apnsFetch
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
const observer = (env, store) => {
|
|
72
|
+
if (options.observer !== void 0) return options.observer(env, store);
|
|
73
|
+
return store.reporting === void 0 ? void 0 : require_reporting_observer.reportingObserver(store.reporting, env.WORKFLOW_ORIGIN);
|
|
74
|
+
};
|
|
75
|
+
const runParts = (env, store, params) => ({
|
|
76
|
+
store,
|
|
77
|
+
env,
|
|
78
|
+
runtime: options.runtime === void 0 ? require_host_channels.runtimeFromConfig(config, env) : options.runtime(env, params),
|
|
79
|
+
messages: messages(env, store),
|
|
80
|
+
actions: new require_engine_actions.RegistryActionInvoker(config.actions ?? []),
|
|
81
|
+
contract: config.contract,
|
|
82
|
+
validator: require_templates.registryValidator(config.templates ?? {}),
|
|
83
|
+
observer: observer(env, store)
|
|
84
|
+
});
|
|
85
|
+
const json = (body, status = 200) => new Response(JSON.stringify(body), {
|
|
86
|
+
status,
|
|
87
|
+
headers: { "content-type": "application/json" }
|
|
88
|
+
});
|
|
89
|
+
const authorized = (request, env) => env.API_TOKEN === void 0 || request.headers.get("authorization") === `Bearer ${env.API_TOKEN}`;
|
|
90
|
+
const preview = emails === void 0 ? void 0 : require_react_email.registryRenderer(emails, config.email);
|
|
91
|
+
const handle = async (request, env, store) => {
|
|
92
|
+
const url = new URL(request.url);
|
|
93
|
+
if (url.pathname.startsWith("/t/") && store.reporting !== void 0) {
|
|
94
|
+
const openedEvent = config.webPush?.openedEvent;
|
|
95
|
+
const tracked = await require_reporting_tracking.handleTracking(request, store.reporting, {
|
|
96
|
+
legacyUnsubscribeUrl: config.email?.unsubscribeUrl,
|
|
97
|
+
product: config.title,
|
|
98
|
+
quickMs: config.email?.tracking?.quickClickMs,
|
|
99
|
+
pushOpened: openedEvent === void 0 ? void 0 : async ({ messageId, userId, at }) => {
|
|
100
|
+
const payload = { message_id: messageId };
|
|
101
|
+
const contract = config.contract;
|
|
102
|
+
const verdict = contract === void 0 ? void 0 : require_contract.checkEvent(contract, openedEvent, payload);
|
|
103
|
+
if (verdict !== void 0 && !verdict.ok) throw new require_contract.ContractValidationError(openedEvent, verdict.issues);
|
|
104
|
+
await require_host_router.ingestEvent(env, {
|
|
105
|
+
id: `${openedEvent}:${messageId}`,
|
|
106
|
+
userId,
|
|
107
|
+
event: openedEvent,
|
|
108
|
+
payload: verdict?.data ?? payload,
|
|
109
|
+
ts: at
|
|
110
|
+
}, store);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
if (tracked !== void 0) return tracked;
|
|
114
|
+
}
|
|
115
|
+
if (url.pathname === "/segments/sweep") {
|
|
116
|
+
if (!authorized(request, env)) return json({ error: "unauthorized" }, 401);
|
|
117
|
+
if (request.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
118
|
+
return json(await require_host_router.sweepSegments(env, store));
|
|
119
|
+
}
|
|
120
|
+
if (url.pathname === "/bundle") {
|
|
121
|
+
if (!authorized(request, env)) return json({ error: "unauthorized" }, 401);
|
|
122
|
+
if (request.method !== "GET") return json({ error: "method not allowed" }, 405);
|
|
123
|
+
return json(bundle());
|
|
124
|
+
}
|
|
125
|
+
if (url.pathname === "/bundle/deploy") {
|
|
126
|
+
if (!authorized(request, env)) return json({ error: "unauthorized" }, 401);
|
|
127
|
+
if (request.method !== "POST") return json({ error: "method not allowed" }, 405);
|
|
128
|
+
try {
|
|
129
|
+
return json(await require_host_router.deployBundle(env, bundle(), store));
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (!(error instanceof require_store_index.SchemaMismatchError)) throw error;
|
|
132
|
+
return json({
|
|
133
|
+
error: error.message,
|
|
134
|
+
problems: error.problems
|
|
135
|
+
}, 503);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (emails !== void 0 && preview !== void 0 && url.pathname.startsWith("/preview/")) {
|
|
139
|
+
if (!authorized(request, env)) return json({ error: "unauthorized" }, 401);
|
|
140
|
+
if (request.method !== "GET") return json({ error: "method not allowed" }, 405);
|
|
141
|
+
const key = url.pathname.slice(9);
|
|
142
|
+
if (!emailIds.has(key)) return json({ error: `unknown email template: ${key}` }, 404);
|
|
143
|
+
let props = {};
|
|
144
|
+
try {
|
|
145
|
+
props = JSON.parse(url.searchParams.get("props") ?? "{}");
|
|
146
|
+
} catch {
|
|
147
|
+
return json({ error: "props must be a JSON object" }, 400);
|
|
148
|
+
}
|
|
149
|
+
if (typeof props !== "object" || props === null || Array.isArray(props)) return json({ error: "props must be a JSON object" }, 400);
|
|
150
|
+
try {
|
|
151
|
+
const { subject, html } = await preview(key, props);
|
|
152
|
+
return new Response(html, { headers: {
|
|
153
|
+
"content-type": "text/html; charset=utf-8",
|
|
154
|
+
"x-subject": subject
|
|
155
|
+
} });
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (error instanceof require_templates.TemplateValidationError) return json({
|
|
158
|
+
error: error.message,
|
|
159
|
+
issues: error.issues
|
|
160
|
+
}, 400);
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return require_host_router.handleRequest(request, env, store, {
|
|
165
|
+
contract: config.contract,
|
|
166
|
+
mode: options.contractMode
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
const sweep = async (env) => {
|
|
170
|
+
const { store, close } = lease(env);
|
|
171
|
+
try {
|
|
172
|
+
return await require_host_router.sweepSegments(env, store);
|
|
173
|
+
} finally {
|
|
174
|
+
await close();
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
let applying;
|
|
178
|
+
let retryAt = 0;
|
|
179
|
+
const applyOwnBundle = (env, version) => {
|
|
180
|
+
if (version === void 0 || version.id === "" || Number.isNaN(version.createdAt)) return;
|
|
181
|
+
if (applying !== void 0 || Date.now() < retryAt) return void 0;
|
|
182
|
+
applying = (async () => {
|
|
183
|
+
const { store, close } = lease(env);
|
|
184
|
+
try {
|
|
185
|
+
if (store.liveBundleVersion === void 0) return;
|
|
186
|
+
const live = await store.liveBundleVersion();
|
|
187
|
+
if (live !== void 0 && (live.id === version.id || live.createdAt >= version.createdAt)) return;
|
|
188
|
+
if ((await require_host_router.deployBundle(env, bundle(), store, version)).applied) console.log(`[workflow] version ${version.id} is live`);
|
|
189
|
+
} finally {
|
|
190
|
+
await close();
|
|
191
|
+
}
|
|
192
|
+
})().catch((error) => {
|
|
193
|
+
applying = void 0;
|
|
194
|
+
retryAt = Date.now() + 6e4;
|
|
195
|
+
console.error(`[workflow] could not make version ${version.id} live`, error);
|
|
196
|
+
});
|
|
197
|
+
return applying;
|
|
198
|
+
};
|
|
199
|
+
return {
|
|
200
|
+
config,
|
|
201
|
+
checkEnv,
|
|
202
|
+
lease,
|
|
203
|
+
bundle,
|
|
204
|
+
runParts,
|
|
205
|
+
handle,
|
|
206
|
+
sweep,
|
|
207
|
+
applyOwnBundle
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
//#endregion
|
|
211
|
+
exports.EnvValidationError = EnvValidationError;
|
|
212
|
+
exports.createJourneyHost = createJourneyHost;
|
|
213
|
+
|
|
214
|
+
//# sourceMappingURL=journey-host.cjs.map
|
|
@@ -0,0 +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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"}
|