@shware/workflow 2.0.1 → 3.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.
Files changed (228) hide show
  1. package/README.md +106 -2
  2. package/dist/apns-relay.cjs +4 -17
  3. package/dist/apns-relay.cjs.map +1 -1
  4. package/dist/apns-relay.mjs +4 -17
  5. package/dist/apns-relay.mjs.map +1 -1
  6. package/dist/aws/engine-step.cjs +104 -0
  7. package/dist/aws/engine-step.cjs.map +1 -0
  8. package/dist/aws/engine-step.d.cts +28 -0
  9. package/dist/aws/engine-step.d.cts.map +1 -0
  10. package/dist/aws/engine-step.d.mts +28 -0
  11. package/dist/aws/engine-step.d.mts.map +1 -0
  12. package/dist/aws/engine-step.mjs +102 -0
  13. package/dist/aws/engine-step.mjs.map +1 -0
  14. package/dist/aws/function-url.cjs +62 -0
  15. package/dist/aws/function-url.cjs.map +1 -0
  16. package/dist/aws/function-url.d.cts +38 -0
  17. package/dist/aws/function-url.d.cts.map +1 -0
  18. package/dist/aws/function-url.d.mts +38 -0
  19. package/dist/aws/function-url.d.mts.map +1 -0
  20. package/dist/aws/function-url.mjs +59 -0
  21. package/dist/aws/function-url.mjs.map +1 -0
  22. package/dist/aws/index.cjs +160 -0
  23. package/dist/aws/index.cjs.map +1 -0
  24. package/dist/aws/index.d.cts +68 -0
  25. package/dist/aws/index.d.cts.map +1 -0
  26. package/dist/aws/index.d.mts +68 -0
  27. package/dist/aws/index.d.mts.map +1 -0
  28. package/dist/aws/index.mjs +148 -0
  29. package/dist/aws/index.mjs.map +1 -0
  30. package/dist/aws/instances.cjs +196 -0
  31. package/dist/aws/instances.cjs.map +1 -0
  32. package/dist/aws/instances.d.cts +94 -0
  33. package/dist/aws/instances.d.cts.map +1 -0
  34. package/dist/aws/instances.d.mts +94 -0
  35. package/dist/aws/instances.d.mts.map +1 -0
  36. package/dist/aws/instances.mjs +192 -0
  37. package/dist/aws/instances.mjs.map +1 -0
  38. package/dist/aws/lambda-api.cjs +89 -0
  39. package/dist/aws/lambda-api.cjs.map +1 -0
  40. package/dist/aws/lambda-api.d.cts +43 -0
  41. package/dist/aws/lambda-api.d.cts.map +1 -0
  42. package/dist/aws/lambda-api.d.mts +43 -0
  43. package/dist/aws/lambda-api.d.mts.map +1 -0
  44. package/dist/aws/lambda-api.mjs +88 -0
  45. package/dist/aws/lambda-api.mjs.map +1 -0
  46. package/dist/client.d.cts +1 -1
  47. package/dist/client.d.mts +1 -1
  48. package/dist/cloudflare/bindings.cjs +19 -0
  49. package/dist/cloudflare/bindings.cjs.map +1 -1
  50. package/dist/cloudflare/bindings.d.cts +39 -0
  51. package/dist/cloudflare/bindings.d.cts.map +1 -1
  52. package/dist/cloudflare/bindings.d.mts +39 -0
  53. package/dist/cloudflare/bindings.d.mts.map +1 -1
  54. package/dist/cloudflare/bindings.mjs +18 -1
  55. package/dist/cloudflare/bindings.mjs.map +1 -1
  56. package/dist/cloudflare/index.cjs +32 -28
  57. package/dist/cloudflare/index.d.cts +8 -6
  58. package/dist/cloudflare/index.d.mts +8 -6
  59. package/dist/cloudflare/index.mjs +12 -10
  60. package/dist/cloudflare/runner.cjs +27 -76
  61. package/dist/cloudflare/runner.cjs.map +1 -1
  62. package/dist/cloudflare/runner.d.cts +7 -2
  63. package/dist/cloudflare/runner.d.cts.map +1 -1
  64. package/dist/cloudflare/runner.d.mts +7 -2
  65. package/dist/cloudflare/runner.d.mts.map +1 -1
  66. package/dist/cloudflare/runner.mjs +26 -75
  67. package/dist/cloudflare/runner.mjs.map +1 -1
  68. package/dist/cloudflare/worker.cjs +22 -203
  69. package/dist/cloudflare/worker.cjs.map +1 -1
  70. package/dist/cloudflare/worker.d.cts +10 -56
  71. package/dist/cloudflare/worker.d.cts.map +1 -1
  72. package/dist/cloudflare/worker.d.mts +10 -56
  73. package/dist/cloudflare/worker.d.mts.map +1 -1
  74. package/dist/cloudflare/worker.mjs +20 -199
  75. package/dist/cloudflare/worker.mjs.map +1 -1
  76. package/dist/email-sender.cjs +62 -0
  77. package/dist/email-sender.cjs.map +1 -0
  78. package/dist/email-sender.d.cts +70 -0
  79. package/dist/email-sender.d.cts.map +1 -0
  80. package/dist/email-sender.d.mts +70 -0
  81. package/dist/email-sender.d.mts.map +1 -0
  82. package/dist/email-sender.mjs +60 -0
  83. package/dist/email-sender.mjs.map +1 -0
  84. package/dist/email.cjs +21 -0
  85. package/dist/email.cjs.map +1 -1
  86. package/dist/email.d.cts +6 -0
  87. package/dist/email.d.cts.map +1 -1
  88. package/dist/email.d.mts +6 -0
  89. package/dist/email.d.mts.map +1 -1
  90. package/dist/email.mjs +21 -1
  91. package/dist/email.mjs.map +1 -1
  92. package/dist/engine/index.cjs +1 -1
  93. package/dist/engine/index.mjs +1 -1
  94. package/dist/engine/interpreter.cjs +1 -1
  95. package/dist/engine/interpreter.cjs.map +1 -1
  96. package/dist/engine/interpreter.mjs +1 -1
  97. package/dist/engine/interpreter.mjs.map +1 -1
  98. package/dist/engine/ports.d.cts +6 -1
  99. package/dist/engine/ports.d.cts.map +1 -1
  100. package/dist/engine/ports.d.mts +6 -1
  101. package/dist/engine/ports.d.mts.map +1 -1
  102. package/dist/{cloudflare → host}/channels.cjs +13 -11
  103. package/dist/host/channels.cjs.map +1 -0
  104. package/dist/{cloudflare → host}/channels.d.cts +8 -3
  105. package/dist/host/channels.d.cts.map +1 -0
  106. package/dist/{cloudflare → host}/channels.d.mts +8 -3
  107. package/dist/host/channels.d.mts.map +1 -0
  108. package/dist/{cloudflare → host}/channels.mjs +9 -7
  109. package/dist/host/channels.mjs.map +1 -0
  110. package/dist/host/execute.cjs +81 -0
  111. package/dist/host/execute.cjs.map +1 -0
  112. package/dist/host/execute.d.cts +37 -0
  113. package/dist/host/execute.d.cts.map +1 -0
  114. package/dist/host/execute.d.mts +37 -0
  115. package/dist/host/execute.d.mts.map +1 -0
  116. package/dist/host/execute.mjs +80 -0
  117. package/dist/host/execute.mjs.map +1 -0
  118. package/dist/host/journey-host.cjs +214 -0
  119. package/dist/host/journey-host.cjs.map +1 -0
  120. package/dist/host/journey-host.d.cts +105 -0
  121. package/dist/host/journey-host.d.cts.map +1 -0
  122. package/dist/host/journey-host.d.mts +105 -0
  123. package/dist/host/journey-host.d.mts.map +1 -0
  124. package/dist/host/journey-host.mjs +210 -0
  125. package/dist/host/journey-host.mjs.map +1 -0
  126. package/dist/{cloudflare → host}/router.cjs +34 -13
  127. package/dist/host/router.cjs.map +1 -0
  128. package/dist/{cloudflare → host}/router.d.cts +2 -2
  129. package/dist/host/router.d.cts.map +1 -0
  130. package/dist/{cloudflare → host}/router.d.mts +2 -2
  131. package/dist/host/router.d.mts.map +1 -0
  132. package/dist/{cloudflare → host}/router.mjs +34 -13
  133. package/dist/host/router.mjs.map +1 -0
  134. package/dist/host/senders.cjs +63 -0
  135. package/dist/host/senders.cjs.map +1 -0
  136. package/dist/host/senders.d.cts +31 -0
  137. package/dist/host/senders.d.cts.map +1 -0
  138. package/dist/host/senders.d.mts +31 -0
  139. package/dist/host/senders.d.mts.map +1 -0
  140. package/dist/host/senders.mjs +59 -0
  141. package/dist/host/senders.mjs.map +1 -0
  142. package/dist/http2-client.cjs +90 -0
  143. package/dist/http2-client.cjs.map +1 -0
  144. package/dist/http2-client.d.cts +47 -0
  145. package/dist/http2-client.d.cts.map +1 -0
  146. package/dist/http2-client.d.mts +47 -0
  147. package/dist/http2-client.d.mts.map +1 -0
  148. package/dist/http2-client.mjs +88 -0
  149. package/dist/http2-client.mjs.map +1 -0
  150. package/dist/index.cjs +1 -0
  151. package/dist/index.d.cts +2 -2
  152. package/dist/index.d.mts +2 -2
  153. package/dist/index.mjs +2 -2
  154. package/dist/react-email.cjs.map +1 -1
  155. package/dist/react-email.d.cts +1 -2
  156. package/dist/react-email.d.cts.map +1 -1
  157. package/dist/react-email.d.mts +1 -2
  158. package/dist/react-email.d.mts.map +1 -1
  159. package/dist/react-email.mjs.map +1 -1
  160. package/dist/store/d1.cjs +1 -0
  161. package/dist/store/d1.cjs.map +1 -1
  162. package/dist/store/d1.d.cts.map +1 -1
  163. package/dist/store/d1.d.mts.map +1 -1
  164. package/dist/store/d1.mjs +1 -0
  165. package/dist/store/d1.mjs.map +1 -1
  166. package/dist/store/index.cjs.map +1 -1
  167. package/dist/store/index.d.cts +74 -0
  168. package/dist/store/index.d.cts.map +1 -1
  169. package/dist/store/index.d.mts +74 -0
  170. package/dist/store/index.d.mts.map +1 -1
  171. package/dist/store/index.mjs.map +1 -1
  172. package/dist/store/postgres.cjs +177 -12
  173. package/dist/store/postgres.cjs.map +1 -1
  174. package/dist/store/postgres.d.cts +10 -1
  175. package/dist/store/postgres.d.cts.map +1 -1
  176. package/dist/store/postgres.d.mts +10 -1
  177. package/dist/store/postgres.d.mts.map +1 -1
  178. package/dist/store/postgres.mjs +177 -12
  179. package/dist/store/postgres.mjs.map +1 -1
  180. package/dist/store/tables.cjs +31 -2
  181. package/dist/store/tables.cjs.map +1 -1
  182. package/dist/store/tables.d.cts +1 -0
  183. package/dist/store/tables.d.cts.map +1 -1
  184. package/dist/store/tables.d.mts +1 -0
  185. package/dist/store/tables.d.mts.map +1 -1
  186. package/dist/store/tables.mjs +31 -2
  187. package/dist/store/tables.mjs.map +1 -1
  188. package/dist/transports/cloudflare-email.cjs +59 -0
  189. package/dist/transports/cloudflare-email.cjs.map +1 -0
  190. package/dist/transports/cloudflare-email.d.cts +38 -0
  191. package/dist/transports/cloudflare-email.d.cts.map +1 -0
  192. package/dist/transports/cloudflare-email.d.mts +38 -0
  193. package/dist/transports/cloudflare-email.d.mts.map +1 -0
  194. package/dist/transports/cloudflare-email.mjs +57 -0
  195. package/dist/transports/cloudflare-email.mjs.map +1 -0
  196. package/dist/transports/ses.cjs +91 -0
  197. package/dist/transports/ses.cjs.map +1 -0
  198. package/dist/transports/ses.d.cts +39 -0
  199. package/dist/transports/ses.d.cts.map +1 -0
  200. package/dist/transports/ses.d.mts +39 -0
  201. package/dist/transports/ses.d.mts.map +1 -0
  202. package/dist/transports/ses.mjs +89 -0
  203. package/dist/transports/ses.mjs.map +1 -0
  204. package/dist/transports/sigv4.cjs +83 -0
  205. package/dist/transports/sigv4.cjs.map +1 -0
  206. package/dist/transports/sigv4.d.cts +27 -0
  207. package/dist/transports/sigv4.d.cts.map +1 -0
  208. package/dist/transports/sigv4.d.mts +27 -0
  209. package/dist/transports/sigv4.d.mts.map +1 -0
  210. package/dist/transports/sigv4.mjs +82 -0
  211. package/dist/transports/sigv4.mjs.map +1 -0
  212. package/package.json +53 -1
  213. package/dist/cloudflare/channels.cjs.map +0 -1
  214. package/dist/cloudflare/channels.d.cts.map +0 -1
  215. package/dist/cloudflare/channels.d.mts.map +0 -1
  216. package/dist/cloudflare/channels.mjs.map +0 -1
  217. package/dist/cloudflare/router.cjs.map +0 -1
  218. package/dist/cloudflare/router.d.cts.map +0 -1
  219. package/dist/cloudflare/router.d.mts.map +0 -1
  220. package/dist/cloudflare/router.mjs.map +0 -1
  221. package/dist/cloudflare/senders.cjs +0 -121
  222. package/dist/cloudflare/senders.cjs.map +0 -1
  223. package/dist/cloudflare/senders.d.cts +0 -72
  224. package/dist/cloudflare/senders.d.cts.map +0 -1
  225. package/dist/cloudflare/senders.d.mts +0 -72
  226. package/dist/cloudflare/senders.d.mts.map +0 -1
  227. package/dist/cloudflare/senders.mjs +0 -115
  228. package/dist/cloudflare/senders.mjs.map +0 -1
@@ -0,0 +1,105 @@
1
+ import { BundleIR } from "../ir.cjs";
2
+ import { JourneyObserver, MessageSender } from "../engine/ports.cjs";
3
+ import { JourneyEnv, JourneyParams } from "../cloudflare/bindings.cjs";
4
+ import { BundleVersion, JourneyStore, StoreLease } from "../store/index.cjs";
5
+ import { JourneyRuntimeOptions } from "../runtime.cjs";
6
+ import { JourneyConfig } from "../config.cjs";
7
+ import { EmailTransport } from "../email-sender.cjs";
8
+ import { JourneyRunParts } from "./execute.cjs";
9
+ import { SweepResult } from "./router.cjs";
10
+ import * as core from "zod/v4/core";
11
+ //#region src/host/journey-host.d.ts
12
+ /**
13
+ * What a journey host is, apart from the runtime it runs on: the validated environment, the
14
+ * store lease, the outlets a run is wired to, the HTTP API (tracking, sweep, bundle, preview,
15
+ * ingest) and the bundle that goes live with the code. `journeyWorker` (Cloudflare) and
16
+ * `journeyLambda` (AWS) are this plus how their platform starts, wakes and stops instances.
17
+ */
18
+ export interface JourneyHostOptions<Env extends JourneyEnv> {
19
+ /**
20
+ * The project's `workflow.config.ts` (`defineConfig`): what the host
21
+ * deploys (`GET /bundle`, `POST /bundle/deploy` serve `compileConfig`), the
22
+ * template registries the senders, the validator and `/preview` render from,
23
+ * the contract every `/events` and `/identify` is checked against, the email
24
+ * defaults, actions, and the runtime / APNs / FCM settings. Secrets come
25
+ * from the env under the names `JourneyEnv` documents.
26
+ */
27
+ config: JourneyConfig;
28
+ /**
29
+ * The data plane, opened per request and per workflow run. Return a
30
+ * `StoreLease` when the client must be released afterwards (a Postgres
31
+ * connection); defaults to D1 + KV from the env's bindings. `database` is
32
+ * the config's (`schema` defaulting to `workflow`), for a Postgres store:
33
+ *
34
+ * store: (env, database) => postgresJsStore(env.DATABASE_URL, database)
35
+ */
36
+ store?: (env: Env, database: {
37
+ schema: string;
38
+ }) => JourneyStore | StoreLease;
39
+ /**
40
+ * Where journey emails go: a transport built from the validated env —
41
+ * `sesEmailTransport(...)`, `cloudflareEmailTransport(...)`, or your own. Which one is
42
+ * independent of where the workflow runs. Without it the email channel fails its sends,
43
+ * saying so.
44
+ */
45
+ email?: (env: Env) => EmailTransport;
46
+ /**
47
+ * Reporting hooks: node arrivals, every copy sent (with the tracking it should carry — click
48
+ * links and an open pixel for email, payload data for a push), and how each instance ended.
49
+ * By default a store that keeps reporting (Postgres) records them itself, and the host
50
+ * serves the email tracking routes (`/t/...`, on `WORKFLOW_ORIGIN`); override to report
51
+ * elsewhere.
52
+ */
53
+ observer?: (env: Env, store: JourneyStore) => JourneyObserver;
54
+ /**
55
+ * Message outlet override (tests, a custom gateway): replaces every channel, `email`
56
+ * included. By default the channels the config describes.
57
+ */
58
+ messages?: (env: Env, store: JourneyStore) => MessageSender;
59
+ /** Per-instance runtime options override (a tester in fast-forward); by default `config.runtime` for `ENVIRONMENT`. */
60
+ runtime?: (env: Env, params: JourneyParams) => JourneyRuntimeOptions;
61
+ /**
62
+ * The host's environment, as a zod schema over its vars and secrets (bindings pass through
63
+ * untouched): checked the first time the host runs, so a missing or malformed variable fails
64
+ * there — every problem listed — not at the send that needs it. Webhook templates read their
65
+ * URL from the parsed values: `webhook: (env: Env) => env.WEBHOOK_DISCORD_ALERTS`.
66
+ */
67
+ env?: core.$ZodType<object>;
68
+ /** 'enforce' (default) answers an event or patch the contract refuses with a 400; 'report' only logs it. */
69
+ contractMode?: 'enforce' | 'report';
70
+ }
71
+ /** The host's environment failed its schema (`env` option). */
72
+ export declare class EnvValidationError extends Error {
73
+ readonly issues: readonly string[];
74
+ readonly name = "EnvValidationError";
75
+ constructor(issues: readonly string[]);
76
+ }
77
+ export interface JourneyHost<Env extends JourneyEnv> {
78
+ config: JourneyConfig;
79
+ /** The env, parsed by the schema once per env object (throws EnvValidationError). */
80
+ checkEnv(env: Env): Env;
81
+ /** A store for one request or run, its segment windows scaled with the runtime. */
82
+ lease(env: Env): Required<StoreLease>;
83
+ /** The bundle compiled into this code. */
84
+ bundle(): BundleIR;
85
+ /** The outlets one run is wired to (the store is the run's). */
86
+ runParts(env: Env, store: JourneyStore, params: JourneyParams): JourneyRunParts;
87
+ /** The HTTP API over one store: tracking, sweep, bundle, preview, then the ingest router. */
88
+ handle(request: Request, env: Env, store: JourneyStore): Promise<Response>;
89
+ /** The scheduled sweep (`sweepSegments`) on a store of its own. */
90
+ sweep(env: Env): Promise<SweepResult>;
91
+ /**
92
+ * Make the bundle compiled into `version` live, once per process, only over an older live
93
+ * version (the store records it). Undefined when there is nothing to do; a failure is logged
94
+ * and retried a minute later.
95
+ */
96
+ applyOwnBundle(env: Env, version: BundleVersion | undefined): Promise<void> | undefined;
97
+ }
98
+ /** What the platform adapter itself contributes. */
99
+ export interface JourneyPlatform {
100
+ /** An HTTP/2 fetch for APNs, where the runtime's own fetch is HTTP/1.1 (Node). */
101
+ apnsFetch?: typeof fetch;
102
+ }
103
+ export declare function createJourneyHost<Env extends JourneyEnv>(options: JourneyHostOptions<Env>, platform?: JourneyPlatform): JourneyHost<Env>;
104
+ //#endregion
105
+ //# sourceMappingURL=journey-host.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journey-host.d.cts","names":[],"sources":["../../src/host/journey-host.ts"],"mappings":";;;;;;;;;;;;;;;;;iBAyCiB,mBAAmB,YAAY;;;;;;;;;EAS9C,QAAQ;;;;;;;;;EASR,SAAS,KAAK,KAAK;IAAY;QAAqB,eAAe;;;;;;;EAOnE,SAAS,KAAK,QAAQ;;;;;;;;EAQtB,YAAY,KAAK,KAAK,OAAO,iBAAiB;;;;;EAK9C,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,WAAW,KAAK,KAAK,QAAQ,kBAAkB;;;;;;;EAO/C,MAAM,KAAK;;EAEX;;;qBAIW,2BAA2B;WAEjB;WADH;EAClB,YAAqB;;iBAKN,YAAY,YAAY;EACvC,QAAQ;;EAER,SAAS,KAAK,MAAM;;EAEpB,MAAM,KAAK,MAAM,SAAS;;EAE1B,UAAU;;EAEV,SAAS,KAAK,KAAK,OAAO,cAAc,QAAQ,gBAAgB;;EAEhE,OAAO,SAAS,SAAS,KAAK,KAAK,OAAO,eAAe,QAAQ;;EAEjE,MAAM,KAAK,MAAM,QAAQ;;;;;;EAMzB,eAAe,KAAK,KAAK,SAAS,4BAA4B;;;iBAI/C;;EAEf,mBAAmB;;wBAGL,kBAAkB,YAAY,YAC5C,SAAS,mBAAmB,MAC5B,WAAU,kBACT,YAAY"}
@@ -0,0 +1,105 @@
1
+ import { BundleIR } from "../ir.mjs";
2
+ import { JourneyObserver, MessageSender } from "../engine/ports.mjs";
3
+ import { JourneyEnv, JourneyParams } from "../cloudflare/bindings.mjs";
4
+ import { BundleVersion, JourneyStore, StoreLease } from "../store/index.mjs";
5
+ import { JourneyRuntimeOptions } from "../runtime.mjs";
6
+ import { JourneyConfig } from "../config.mjs";
7
+ import { EmailTransport } from "../email-sender.mjs";
8
+ import { JourneyRunParts } from "./execute.mjs";
9
+ import { SweepResult } from "./router.mjs";
10
+ import * as core from "zod/v4/core";
11
+ //#region src/host/journey-host.d.ts
12
+ /**
13
+ * What a journey host is, apart from the runtime it runs on: the validated environment, the
14
+ * store lease, the outlets a run is wired to, the HTTP API (tracking, sweep, bundle, preview,
15
+ * ingest) and the bundle that goes live with the code. `journeyWorker` (Cloudflare) and
16
+ * `journeyLambda` (AWS) are this plus how their platform starts, wakes and stops instances.
17
+ */
18
+ export interface JourneyHostOptions<Env extends JourneyEnv> {
19
+ /**
20
+ * The project's `workflow.config.ts` (`defineConfig`): what the host
21
+ * deploys (`GET /bundle`, `POST /bundle/deploy` serve `compileConfig`), the
22
+ * template registries the senders, the validator and `/preview` render from,
23
+ * the contract every `/events` and `/identify` is checked against, the email
24
+ * defaults, actions, and the runtime / APNs / FCM settings. Secrets come
25
+ * from the env under the names `JourneyEnv` documents.
26
+ */
27
+ config: JourneyConfig;
28
+ /**
29
+ * The data plane, opened per request and per workflow run. Return a
30
+ * `StoreLease` when the client must be released afterwards (a Postgres
31
+ * connection); defaults to D1 + KV from the env's bindings. `database` is
32
+ * the config's (`schema` defaulting to `workflow`), for a Postgres store:
33
+ *
34
+ * store: (env, database) => postgresJsStore(env.DATABASE_URL, database)
35
+ */
36
+ store?: (env: Env, database: {
37
+ schema: string;
38
+ }) => JourneyStore | StoreLease;
39
+ /**
40
+ * Where journey emails go: a transport built from the validated env —
41
+ * `sesEmailTransport(...)`, `cloudflareEmailTransport(...)`, or your own. Which one is
42
+ * independent of where the workflow runs. Without it the email channel fails its sends,
43
+ * saying so.
44
+ */
45
+ email?: (env: Env) => EmailTransport;
46
+ /**
47
+ * Reporting hooks: node arrivals, every copy sent (with the tracking it should carry — click
48
+ * links and an open pixel for email, payload data for a push), and how each instance ended.
49
+ * By default a store that keeps reporting (Postgres) records them itself, and the host
50
+ * serves the email tracking routes (`/t/...`, on `WORKFLOW_ORIGIN`); override to report
51
+ * elsewhere.
52
+ */
53
+ observer?: (env: Env, store: JourneyStore) => JourneyObserver;
54
+ /**
55
+ * Message outlet override (tests, a custom gateway): replaces every channel, `email`
56
+ * included. By default the channels the config describes.
57
+ */
58
+ messages?: (env: Env, store: JourneyStore) => MessageSender;
59
+ /** Per-instance runtime options override (a tester in fast-forward); by default `config.runtime` for `ENVIRONMENT`. */
60
+ runtime?: (env: Env, params: JourneyParams) => JourneyRuntimeOptions;
61
+ /**
62
+ * The host's environment, as a zod schema over its vars and secrets (bindings pass through
63
+ * untouched): checked the first time the host runs, so a missing or malformed variable fails
64
+ * there — every problem listed — not at the send that needs it. Webhook templates read their
65
+ * URL from the parsed values: `webhook: (env: Env) => env.WEBHOOK_DISCORD_ALERTS`.
66
+ */
67
+ env?: core.$ZodType<object>;
68
+ /** 'enforce' (default) answers an event or patch the contract refuses with a 400; 'report' only logs it. */
69
+ contractMode?: 'enforce' | 'report';
70
+ }
71
+ /** The host's environment failed its schema (`env` option). */
72
+ export declare class EnvValidationError extends Error {
73
+ readonly issues: readonly string[];
74
+ readonly name = "EnvValidationError";
75
+ constructor(issues: readonly string[]);
76
+ }
77
+ export interface JourneyHost<Env extends JourneyEnv> {
78
+ config: JourneyConfig;
79
+ /** The env, parsed by the schema once per env object (throws EnvValidationError). */
80
+ checkEnv(env: Env): Env;
81
+ /** A store for one request or run, its segment windows scaled with the runtime. */
82
+ lease(env: Env): Required<StoreLease>;
83
+ /** The bundle compiled into this code. */
84
+ bundle(): BundleIR;
85
+ /** The outlets one run is wired to (the store is the run's). */
86
+ runParts(env: Env, store: JourneyStore, params: JourneyParams): JourneyRunParts;
87
+ /** The HTTP API over one store: tracking, sweep, bundle, preview, then the ingest router. */
88
+ handle(request: Request, env: Env, store: JourneyStore): Promise<Response>;
89
+ /** The scheduled sweep (`sweepSegments`) on a store of its own. */
90
+ sweep(env: Env): Promise<SweepResult>;
91
+ /**
92
+ * Make the bundle compiled into `version` live, once per process, only over an older live
93
+ * version (the store records it). Undefined when there is nothing to do; a failure is logged
94
+ * and retried a minute later.
95
+ */
96
+ applyOwnBundle(env: Env, version: BundleVersion | undefined): Promise<void> | undefined;
97
+ }
98
+ /** What the platform adapter itself contributes. */
99
+ export interface JourneyPlatform {
100
+ /** An HTTP/2 fetch for APNs, where the runtime's own fetch is HTTP/1.1 (Node). */
101
+ apnsFetch?: typeof fetch;
102
+ }
103
+ export declare function createJourneyHost<Env extends JourneyEnv>(options: JourneyHostOptions<Env>, platform?: JourneyPlatform): JourneyHost<Env>;
104
+ //#endregion
105
+ //# sourceMappingURL=journey-host.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journey-host.d.mts","names":[],"sources":["../../src/host/journey-host.ts"],"mappings":";;;;;;;;;;;;;;;;;iBAyCiB,mBAAmB,YAAY;;;;;;;;;EAS9C,QAAQ;;;;;;;;;EASR,SAAS,KAAK,KAAK;IAAY;QAAqB,eAAe;;;;;;;EAOnE,SAAS,KAAK,QAAQ;;;;;;;;EAQtB,YAAY,KAAK,KAAK,OAAO,iBAAiB;;;;;EAK9C,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,WAAW,KAAK,KAAK,QAAQ,kBAAkB;;;;;;;EAO/C,MAAM,KAAK;;EAEX;;;qBAIW,2BAA2B;WAEjB;WADH;EAClB,YAAqB;;iBAKN,YAAY,YAAY;EACvC,QAAQ;;EAER,SAAS,KAAK,MAAM;;EAEpB,MAAM,KAAK,MAAM,SAAS;;EAE1B,UAAU;;EAEV,SAAS,KAAK,KAAK,OAAO,cAAc,QAAQ,gBAAgB;;EAEhE,OAAO,SAAS,SAAS,KAAK,KAAK,OAAO,eAAe,QAAQ;;EAEjE,MAAM,KAAK,MAAM,QAAQ;;;;;;EAMzB,eAAe,KAAK,KAAK,SAAS,4BAA4B;;;iBAI/C;;EAEf,mBAAmB;;wBAGL,kBAAkB,YAAY,YAC5C,SAAS,mBAAmB,MAC5B,WAAU,kBACT,YAAY"}
@@ -0,0 +1,210 @@
1
+ import { assertEmailConfig } from "../email.mjs";
2
+ import { TemplateValidationError, indexTemplates, registryValidator } from "../templates.mjs";
3
+ import { compileConfig } from "../config.mjs";
4
+ import { ContractValidationError, checkEvent } from "../contract.mjs";
5
+ import "../store/tables.mjs";
6
+ import { registryRenderer } from "../react-email.mjs";
7
+ import { scaleSegmentWindows } from "../engine/time-scale.mjs";
8
+ import { d1Store } from "../cloudflare/d1-env.mjs";
9
+ import { handleTracking } from "../reporting/tracking.mjs";
10
+ import { SchemaMismatchError } from "../store/index.mjs";
11
+ import { deployBundle, handleRequest, ingestEvent, sweepSegments } from "./router.mjs";
12
+ import { RegistryActionInvoker } from "../engine/actions.mjs";
13
+ import { reportingObserver } from "../reporting/observer.mjs";
14
+ import { channelsFromConfig, runtimeFromConfig } from "./channels.mjs";
15
+ import * as z from "zod/mini";
16
+ //#region src/host/journey-host.ts
17
+ /** The host's environment failed its schema (`env` option). */
18
+ var EnvValidationError = class extends Error {
19
+ issues;
20
+ name = "EnvValidationError";
21
+ constructor(issues) {
22
+ super(`the host's environment is invalid:\n ${issues.join("\n ")}`);
23
+ this.issues = issues;
24
+ }
25
+ };
26
+ function createJourneyHost(options, platform = {}) {
27
+ const { config } = options;
28
+ const emailIds = indexTemplates("email", config.templates?.email);
29
+ assertEmailConfig(config, emailIds.values());
30
+ const checked = /* @__PURE__ */ new WeakMap();
31
+ const checkEnv = (env) => {
32
+ if (options.env === void 0) return env;
33
+ const cached = checked.get(env);
34
+ if (cached !== void 0) return cached;
35
+ const result = z.safeParse(options.env, env);
36
+ if (!result.success) throw new EnvValidationError(result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`));
37
+ const merged = {
38
+ ...env,
39
+ ...result.data
40
+ };
41
+ checked.set(env, merged);
42
+ return merged;
43
+ };
44
+ let compiled;
45
+ const bundle = () => compiled ??= compileConfig(config);
46
+ const emails = config.templates?.email;
47
+ const database = { schema: config.database?.schema ?? "workflow" };
48
+ const lease = (env) => {
49
+ const opened = options.store === void 0 ? d1Store(env) : options.store(env, database);
50
+ const noop = async () => {};
51
+ const factor = runtimeFromConfig(config, env).timeScale;
52
+ return "store" in opened ? {
53
+ store: scaleSegmentWindows(opened.store, factor),
54
+ close: opened.close ?? noop
55
+ } : {
56
+ store: scaleSegmentWindows(opened, factor),
57
+ close: noop
58
+ };
59
+ };
60
+ const messages = (env, store) => {
61
+ if (options.messages !== void 0) return options.messages(env, store);
62
+ return channelsFromConfig(config, env, {
63
+ store,
64
+ email: options.email?.(env),
65
+ apnsFetch: platform.apnsFetch
66
+ });
67
+ };
68
+ const observer = (env, store) => {
69
+ if (options.observer !== void 0) return options.observer(env, store);
70
+ return store.reporting === void 0 ? void 0 : reportingObserver(store.reporting, env.WORKFLOW_ORIGIN);
71
+ };
72
+ const runParts = (env, store, params) => ({
73
+ store,
74
+ env,
75
+ runtime: options.runtime === void 0 ? runtimeFromConfig(config, env) : options.runtime(env, params),
76
+ messages: messages(env, store),
77
+ actions: new RegistryActionInvoker(config.actions ?? []),
78
+ contract: config.contract,
79
+ validator: registryValidator(config.templates ?? {}),
80
+ observer: observer(env, store)
81
+ });
82
+ const json = (body, status = 200) => new Response(JSON.stringify(body), {
83
+ status,
84
+ headers: { "content-type": "application/json" }
85
+ });
86
+ const authorized = (request, env) => env.API_TOKEN === void 0 || request.headers.get("authorization") === `Bearer ${env.API_TOKEN}`;
87
+ const preview = emails === void 0 ? void 0 : registryRenderer(emails, config.email);
88
+ const handle = async (request, env, store) => {
89
+ const url = new URL(request.url);
90
+ if (url.pathname.startsWith("/t/") && store.reporting !== void 0) {
91
+ const openedEvent = config.webPush?.openedEvent;
92
+ const tracked = await handleTracking(request, store.reporting, {
93
+ legacyUnsubscribeUrl: config.email?.unsubscribeUrl,
94
+ product: config.title,
95
+ quickMs: config.email?.tracking?.quickClickMs,
96
+ pushOpened: openedEvent === void 0 ? void 0 : async ({ messageId, userId, at }) => {
97
+ const payload = { message_id: messageId };
98
+ const contract = config.contract;
99
+ const verdict = contract === void 0 ? void 0 : checkEvent(contract, openedEvent, payload);
100
+ if (verdict !== void 0 && !verdict.ok) throw new ContractValidationError(openedEvent, verdict.issues);
101
+ await ingestEvent(env, {
102
+ id: `${openedEvent}:${messageId}`,
103
+ userId,
104
+ event: openedEvent,
105
+ payload: verdict?.data ?? payload,
106
+ ts: at
107
+ }, store);
108
+ }
109
+ });
110
+ if (tracked !== void 0) return tracked;
111
+ }
112
+ if (url.pathname === "/segments/sweep") {
113
+ if (!authorized(request, env)) return json({ error: "unauthorized" }, 401);
114
+ if (request.method !== "POST") return json({ error: "method not allowed" }, 405);
115
+ return json(await sweepSegments(env, store));
116
+ }
117
+ if (url.pathname === "/bundle") {
118
+ if (!authorized(request, env)) return json({ error: "unauthorized" }, 401);
119
+ if (request.method !== "GET") return json({ error: "method not allowed" }, 405);
120
+ return json(bundle());
121
+ }
122
+ if (url.pathname === "/bundle/deploy") {
123
+ if (!authorized(request, env)) return json({ error: "unauthorized" }, 401);
124
+ if (request.method !== "POST") return json({ error: "method not allowed" }, 405);
125
+ try {
126
+ return json(await deployBundle(env, bundle(), store));
127
+ } catch (error) {
128
+ if (!(error instanceof SchemaMismatchError)) throw error;
129
+ return json({
130
+ error: error.message,
131
+ problems: error.problems
132
+ }, 503);
133
+ }
134
+ }
135
+ if (emails !== void 0 && preview !== void 0 && url.pathname.startsWith("/preview/")) {
136
+ if (!authorized(request, env)) return json({ error: "unauthorized" }, 401);
137
+ if (request.method !== "GET") return json({ error: "method not allowed" }, 405);
138
+ const key = url.pathname.slice(9);
139
+ if (!emailIds.has(key)) return json({ error: `unknown email template: ${key}` }, 404);
140
+ let props = {};
141
+ try {
142
+ props = JSON.parse(url.searchParams.get("props") ?? "{}");
143
+ } catch {
144
+ return json({ error: "props must be a JSON object" }, 400);
145
+ }
146
+ if (typeof props !== "object" || props === null || Array.isArray(props)) return json({ error: "props must be a JSON object" }, 400);
147
+ try {
148
+ const { subject, html } = await preview(key, props);
149
+ return new Response(html, { headers: {
150
+ "content-type": "text/html; charset=utf-8",
151
+ "x-subject": subject
152
+ } });
153
+ } catch (error) {
154
+ if (error instanceof TemplateValidationError) return json({
155
+ error: error.message,
156
+ issues: error.issues
157
+ }, 400);
158
+ throw error;
159
+ }
160
+ }
161
+ return handleRequest(request, env, store, {
162
+ contract: config.contract,
163
+ mode: options.contractMode
164
+ });
165
+ };
166
+ const sweep = async (env) => {
167
+ const { store, close } = lease(env);
168
+ try {
169
+ return await sweepSegments(env, store);
170
+ } finally {
171
+ await close();
172
+ }
173
+ };
174
+ let applying;
175
+ let retryAt = 0;
176
+ const applyOwnBundle = (env, version) => {
177
+ if (version === void 0 || version.id === "" || Number.isNaN(version.createdAt)) return;
178
+ if (applying !== void 0 || Date.now() < retryAt) return void 0;
179
+ applying = (async () => {
180
+ const { store, close } = lease(env);
181
+ try {
182
+ if (store.liveBundleVersion === void 0) return;
183
+ const live = await store.liveBundleVersion();
184
+ if (live !== void 0 && (live.id === version.id || live.createdAt >= version.createdAt)) return;
185
+ if ((await deployBundle(env, bundle(), store, version)).applied) console.log(`[workflow] version ${version.id} is live`);
186
+ } finally {
187
+ await close();
188
+ }
189
+ })().catch((error) => {
190
+ applying = void 0;
191
+ retryAt = Date.now() + 6e4;
192
+ console.error(`[workflow] could not make version ${version.id} live`, error);
193
+ });
194
+ return applying;
195
+ };
196
+ return {
197
+ config,
198
+ checkEnv,
199
+ lease,
200
+ bundle,
201
+ runParts,
202
+ handle,
203
+ sweep,
204
+ applyOwnBundle
205
+ };
206
+ }
207
+ //#endregion
208
+ export { EnvValidationError, createJourneyHost };
209
+
210
+ //# sourceMappingURL=journey-host.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"journey-host.mjs","names":[],"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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"}
@@ -3,16 +3,16 @@ const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
3
3
  const require_hash = require("../hash.cjs");
4
4
  const require_ir = require("../ir.cjs");
5
5
  const require_contract = require("../contract.cjs");
6
- const require_cloudflare_bindings = require("./bindings.cjs");
7
6
  const require_engine_bucket = require("../engine/bucket.cjs");
8
7
  const require_engine_condition = require("../engine/condition.cjs");
9
- const require_reporting_tracking = require("../reporting/tracking.cjs");
10
8
  const require_store_facts = require("../store/facts.cjs");
9
+ const require_cloudflare_bindings = require("../cloudflare/bindings.cjs");
10
+ const require_cloudflare_d1_env = require("../cloudflare/d1-env.cjs");
11
+ const require_reporting_tracking = require("../reporting/tracking.cjs");
11
12
  const require_store_index = require("../store/index.cjs");
12
- const require_cloudflare_d1_env = require("./d1-env.cjs");
13
13
  let zod_mini = require("zod/mini");
14
14
  zod_mini = require_runtime.__toESM(zod_mini, 1);
15
- //#region src/cloudflare/router.ts
15
+ //#region src/host/router.ts
16
16
  async function ingestEvent(env, input, store = require_cloudflare_d1_env.d1Store(env)) {
17
17
  if (input.event.startsWith("$")) throw new Error(`event name '${input.event}' is reserved ('$' prefix is internal)`);
18
18
  const ts = input.ts ?? Date.now();
@@ -44,8 +44,9 @@ async function wakeSubscribers(env, store, userId, event) {
44
44
  let instance;
45
45
  try {
46
46
  instance = await env.JOURNEY.get(handle);
47
- } catch {
48
- await store.unsubscribe(handle);
47
+ } catch (error) {
48
+ if (require_cloudflare_bindings.isInstanceNotFound(error)) await store.unsubscribe(handle);
49
+ else console.warn(`[workflow] looking up ${handle} to wake it failed`, error);
49
50
  continue;
50
51
  }
51
52
  try {
@@ -69,7 +70,7 @@ async function startTriggeredJourneys(env, store, input, ts) {
69
70
  const instanceId = await startJourney(env, store, route.workflow, route.hash, input.userId, ts, {
70
71
  event: input.event,
71
72
  payload: input.payload
72
- });
73
+ }, route.version);
73
74
  if (instanceId !== null) started.push(instanceId);
74
75
  }
75
76
  return started;
@@ -97,7 +98,8 @@ async function refreshSegments(env, store, userId, ts, routes, segments = [], de
97
98
  const list = bySegment.get(route.segment) ?? [];
98
99
  list.push({
99
100
  workflow: route.workflow,
100
- hash: route.hash
101
+ hash: route.hash,
102
+ ...route.version === void 0 ? {} : { version: route.version }
101
103
  });
102
104
  bySegment.set(route.segment, list);
103
105
  }
@@ -124,7 +126,7 @@ async function refreshSegments(env, store, userId, ts, routes, segments = [], de
124
126
  const instanceId = await startJourney(env, store, route.workflow, route.hash, userId, ts, {
125
127
  event: "$segment_entry",
126
128
  payload: { segment: segmentId }
127
- });
129
+ }, route.version);
128
130
  if (instanceId !== null) started.push(instanceId);
129
131
  await store.completeSegmentStart(segmentId, userId, route.workflow);
130
132
  } catch (error) {
@@ -143,6 +145,11 @@ async function refreshSegments(env, store, userId, ts, routes, segments = [], de
143
145
  * It walks users one by one: fine for thousands, a large audience wants SQL-side evaluation.
144
146
  */
145
147
  async function sweepSegments(env, store, now = Date.now()) {
148
+ try {
149
+ await env.JOURNEY.maintain?.();
150
+ } catch (error) {
151
+ console.error("[workflow] runtime maintenance failed", error);
152
+ }
146
153
  const erasing = /* @__PURE__ */ new Set();
147
154
  for (const userId of await store.pendingErasures()) try {
148
155
  const { erased } = await eraseUser(env, userId, store);
@@ -174,7 +181,7 @@ async function sweepSegments(env, store, now = Date.now()) {
174
181
  * ingests cannot double-enter). A pair whose previous instance is recorded as
175
182
  * 'failed' is reclaimed rather than barred forever over one outage.
176
183
  */
177
- async function startJourney(env, store, workflow, hash, userId, ts, trigger) {
184
+ async function startJourney(env, store, workflow, hash, userId, ts, trigger, version) {
178
185
  const instanceId = buildInstanceId(workflow, hash, userId, ts);
179
186
  const params = {
180
187
  workflowId: workflow,
@@ -182,20 +189,34 @@ async function startJourney(env, store, workflow, hash, userId, ts, trigger) {
182
189
  userId,
183
190
  trigger
184
191
  };
192
+ const execution = await env.JOURNEY.plan?.({
193
+ id: instanceId,
194
+ params,
195
+ ts,
196
+ version
197
+ });
185
198
  const outcome = await store.enterJourney({
186
199
  workflow,
187
200
  userId,
188
201
  instanceId,
189
202
  hash,
190
- ts
203
+ ts,
204
+ execution
191
205
  });
192
- if (outcome === null) return null;
206
+ if (outcome === null) {
207
+ await env.JOURNEY.resume?.({
208
+ workflow,
209
+ userId
210
+ });
211
+ return null;
212
+ }
193
213
  try {
194
214
  await env.JOURNEY.create({
195
215
  id: instanceId,
196
216
  params
197
217
  });
198
218
  } catch (error) {
219
+ if (execution !== void 0) throw error;
199
220
  if (outcome === "reclaimed") await store.setEntryStatus(instanceId, "failed");
200
221
  else await store.removeEntry(instanceId);
201
222
  throw error;
@@ -265,7 +286,7 @@ async function stopInstance(env, instanceId) {
265
286
  try {
266
287
  instance = await env.JOURNEY.get(instanceId);
267
288
  } catch (error) {
268
- if (/not[_ ]found/i.test(String(error))) return true;
289
+ if (require_cloudflare_bindings.isInstanceNotFound(error)) return true;
269
290
  console.warn(`[workflow] looking up ${instanceId} to stop it failed`, error);
270
291
  return false;
271
292
  }