@shware/workflow 2.0.0 → 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.
Files changed (282) hide show
  1. package/README.md +96 -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 +62 -0
  25. package/dist/aws/index.d.cts.map +1 -0
  26. package/dist/aws/index.d.mts +62 -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 +176 -0
  31. package/dist/aws/instances.cjs.map +1 -0
  32. package/dist/aws/instances.d.cts +75 -0
  33. package/dist/aws/instances.d.cts.map +1 -0
  34. package/dist/aws/instances.d.mts +75 -0
  35. package/dist/aws/instances.d.mts.map +1 -0
  36. package/dist/aws/instances.mjs +172 -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 +37 -0
  51. package/dist/cloudflare/bindings.d.cts.map +1 -1
  52. package/dist/cloudflare/bindings.d.mts +37 -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/config.cjs +2 -2
  77. package/dist/config.cjs.map +1 -1
  78. package/dist/config.d.cts +1 -1
  79. package/dist/config.d.mts +1 -1
  80. package/dist/config.mjs +2 -2
  81. package/dist/config.mjs.map +1 -1
  82. package/dist/contract.cjs.map +1 -1
  83. package/dist/contract.d.cts +1 -1
  84. package/dist/contract.d.cts.map +1 -1
  85. package/dist/contract.d.mts +1 -1
  86. package/dist/contract.d.mts.map +1 -1
  87. package/dist/contract.mjs.map +1 -1
  88. package/dist/dsl/action.cjs.map +1 -1
  89. package/dist/dsl/action.d.cts +2 -2
  90. package/dist/dsl/action.d.cts.map +1 -1
  91. package/dist/dsl/action.d.mts +2 -2
  92. package/dist/dsl/action.d.mts.map +1 -1
  93. package/dist/dsl/action.mjs.map +1 -1
  94. package/dist/dsl/bundle.cjs.map +1 -1
  95. package/dist/dsl/bundle.d.cts +1 -1
  96. package/dist/dsl/bundle.d.mts +1 -1
  97. package/dist/dsl/bundle.mjs.map +1 -1
  98. package/dist/dsl/flow.cjs.map +1 -1
  99. package/dist/dsl/flow.d.cts +1 -1
  100. package/dist/dsl/flow.d.cts.map +1 -1
  101. package/dist/dsl/flow.d.mts +1 -1
  102. package/dist/dsl/flow.d.mts.map +1 -1
  103. package/dist/dsl/flow.mjs.map +1 -1
  104. package/dist/dsl/refs.cjs.map +1 -1
  105. package/dist/dsl/refs.d.cts +5 -4
  106. package/dist/dsl/refs.d.cts.map +1 -1
  107. package/dist/dsl/refs.d.mts +5 -4
  108. package/dist/dsl/refs.d.mts.map +1 -1
  109. package/dist/dsl/refs.mjs.map +1 -1
  110. package/dist/dsl/template.cjs.map +1 -1
  111. package/dist/dsl/template.d.cts +2 -2
  112. package/dist/dsl/template.d.cts.map +1 -1
  113. package/dist/dsl/template.d.mts +2 -2
  114. package/dist/dsl/template.d.mts.map +1 -1
  115. package/dist/dsl/template.mjs.map +1 -1
  116. package/dist/email-sender.cjs +62 -0
  117. package/dist/email-sender.cjs.map +1 -0
  118. package/dist/email-sender.d.cts +70 -0
  119. package/dist/email-sender.d.cts.map +1 -0
  120. package/dist/email-sender.d.mts +70 -0
  121. package/dist/email-sender.d.mts.map +1 -0
  122. package/dist/email-sender.mjs +60 -0
  123. package/dist/email-sender.mjs.map +1 -0
  124. package/dist/email.cjs +21 -0
  125. package/dist/email.cjs.map +1 -1
  126. package/dist/email.d.cts +6 -0
  127. package/dist/email.d.cts.map +1 -1
  128. package/dist/email.d.mts +6 -0
  129. package/dist/email.d.mts.map +1 -1
  130. package/dist/email.mjs +21 -1
  131. package/dist/email.mjs.map +1 -1
  132. package/dist/engine/index.cjs +1 -1
  133. package/dist/engine/index.mjs +1 -1
  134. package/dist/engine/interpreter.cjs +1 -1
  135. package/dist/engine/interpreter.cjs.map +1 -1
  136. package/dist/engine/interpreter.d.cts.map +1 -1
  137. package/dist/engine/interpreter.d.mts.map +1 -1
  138. package/dist/engine/interpreter.mjs +1 -1
  139. package/dist/engine/interpreter.mjs.map +1 -1
  140. package/dist/engine/ports.d.cts +6 -1
  141. package/dist/engine/ports.d.cts.map +1 -1
  142. package/dist/engine/ports.d.mts +6 -1
  143. package/dist/engine/ports.d.mts.map +1 -1
  144. package/dist/{cloudflare → host}/channels.cjs +13 -11
  145. package/dist/host/channels.cjs.map +1 -0
  146. package/dist/{cloudflare → host}/channels.d.cts +8 -3
  147. package/dist/host/channels.d.cts.map +1 -0
  148. package/dist/{cloudflare → host}/channels.d.mts +8 -3
  149. package/dist/host/channels.d.mts.map +1 -0
  150. package/dist/{cloudflare → host}/channels.mjs +9 -7
  151. package/dist/host/channels.mjs.map +1 -0
  152. package/dist/host/execute.cjs +82 -0
  153. package/dist/host/execute.cjs.map +1 -0
  154. package/dist/host/execute.d.cts +41 -0
  155. package/dist/host/execute.d.cts.map +1 -0
  156. package/dist/host/execute.d.mts +41 -0
  157. package/dist/host/execute.d.mts.map +1 -0
  158. package/dist/host/execute.mjs +81 -0
  159. package/dist/host/execute.mjs.map +1 -0
  160. package/dist/host/journey-host.cjs +214 -0
  161. package/dist/host/journey-host.cjs.map +1 -0
  162. package/dist/host/journey-host.d.cts +105 -0
  163. package/dist/host/journey-host.d.cts.map +1 -0
  164. package/dist/host/journey-host.d.mts +105 -0
  165. package/dist/host/journey-host.d.mts.map +1 -0
  166. package/dist/host/journey-host.mjs +210 -0
  167. package/dist/host/journey-host.mjs.map +1 -0
  168. package/dist/{cloudflare → host}/router.cjs +28 -9
  169. package/dist/host/router.cjs.map +1 -0
  170. package/dist/{cloudflare → host}/router.d.cts +2 -2
  171. package/dist/host/router.d.cts.map +1 -0
  172. package/dist/{cloudflare → host}/router.d.mts +2 -2
  173. package/dist/host/router.d.mts.map +1 -0
  174. package/dist/{cloudflare → host}/router.mjs +28 -9
  175. package/dist/host/router.mjs.map +1 -0
  176. package/dist/host/senders.cjs +63 -0
  177. package/dist/host/senders.cjs.map +1 -0
  178. package/dist/host/senders.d.cts +31 -0
  179. package/dist/host/senders.d.cts.map +1 -0
  180. package/dist/host/senders.d.mts +31 -0
  181. package/dist/host/senders.d.mts.map +1 -0
  182. package/dist/host/senders.mjs +59 -0
  183. package/dist/host/senders.mjs.map +1 -0
  184. package/dist/http2-client.cjs +88 -0
  185. package/dist/http2-client.cjs.map +1 -0
  186. package/dist/http2-client.d.cts +44 -0
  187. package/dist/http2-client.d.cts.map +1 -0
  188. package/dist/http2-client.d.mts +44 -0
  189. package/dist/http2-client.d.mts.map +1 -0
  190. package/dist/http2-client.mjs +86 -0
  191. package/dist/http2-client.mjs.map +1 -0
  192. package/dist/index.cjs +1 -0
  193. package/dist/index.d.cts +2 -2
  194. package/dist/index.d.mts +2 -2
  195. package/dist/index.mjs +2 -2
  196. package/dist/react-email.cjs.map +1 -1
  197. package/dist/react-email.d.cts +1 -2
  198. package/dist/react-email.d.cts.map +1 -1
  199. package/dist/react-email.d.mts +1 -2
  200. package/dist/react-email.d.mts.map +1 -1
  201. package/dist/react-email.mjs.map +1 -1
  202. package/dist/reporting/tracking.cjs +5 -1
  203. package/dist/reporting/tracking.cjs.map +1 -1
  204. package/dist/reporting/tracking.d.cts.map +1 -1
  205. package/dist/reporting/tracking.d.mts.map +1 -1
  206. package/dist/reporting/tracking.mjs +5 -1
  207. package/dist/reporting/tracking.mjs.map +1 -1
  208. package/dist/store/d1.cjs +1 -0
  209. package/dist/store/d1.cjs.map +1 -1
  210. package/dist/store/d1.d.cts.map +1 -1
  211. package/dist/store/d1.d.mts.map +1 -1
  212. package/dist/store/d1.mjs +1 -0
  213. package/dist/store/d1.mjs.map +1 -1
  214. package/dist/store/index.cjs.map +1 -1
  215. package/dist/store/index.d.cts +63 -0
  216. package/dist/store/index.d.cts.map +1 -1
  217. package/dist/store/index.d.mts +63 -0
  218. package/dist/store/index.d.mts.map +1 -1
  219. package/dist/store/index.mjs.map +1 -1
  220. package/dist/store/postgres-reporting.cjs +5 -5
  221. package/dist/store/postgres-reporting.cjs.map +1 -1
  222. package/dist/store/postgres-reporting.mjs +5 -5
  223. package/dist/store/postgres-reporting.mjs.map +1 -1
  224. package/dist/store/postgres.cjs +143 -2
  225. package/dist/store/postgres.cjs.map +1 -1
  226. package/dist/store/postgres.d.cts +4 -1
  227. package/dist/store/postgres.d.cts.map +1 -1
  228. package/dist/store/postgres.d.mts +4 -1
  229. package/dist/store/postgres.d.mts.map +1 -1
  230. package/dist/store/postgres.mjs +143 -2
  231. package/dist/store/postgres.mjs.map +1 -1
  232. package/dist/store/tables.cjs +26 -0
  233. package/dist/store/tables.cjs.map +1 -1
  234. package/dist/store/tables.d.cts +1 -0
  235. package/dist/store/tables.d.cts.map +1 -1
  236. package/dist/store/tables.d.mts +1 -0
  237. package/dist/store/tables.d.mts.map +1 -1
  238. package/dist/store/tables.mjs +26 -0
  239. package/dist/store/tables.mjs.map +1 -1
  240. package/dist/templates.cjs.map +1 -1
  241. package/dist/templates.mjs.map +1 -1
  242. package/dist/transports/cloudflare-email.cjs +59 -0
  243. package/dist/transports/cloudflare-email.cjs.map +1 -0
  244. package/dist/transports/cloudflare-email.d.cts +38 -0
  245. package/dist/transports/cloudflare-email.d.cts.map +1 -0
  246. package/dist/transports/cloudflare-email.d.mts +38 -0
  247. package/dist/transports/cloudflare-email.d.mts.map +1 -0
  248. package/dist/transports/cloudflare-email.mjs +57 -0
  249. package/dist/transports/cloudflare-email.mjs.map +1 -0
  250. package/dist/transports/ses.cjs +91 -0
  251. package/dist/transports/ses.cjs.map +1 -0
  252. package/dist/transports/ses.d.cts +39 -0
  253. package/dist/transports/ses.d.cts.map +1 -0
  254. package/dist/transports/ses.d.mts +39 -0
  255. package/dist/transports/ses.d.mts.map +1 -0
  256. package/dist/transports/ses.mjs +89 -0
  257. package/dist/transports/ses.mjs.map +1 -0
  258. package/dist/transports/sigv4.cjs +83 -0
  259. package/dist/transports/sigv4.cjs.map +1 -0
  260. package/dist/transports/sigv4.d.cts +27 -0
  261. package/dist/transports/sigv4.d.cts.map +1 -0
  262. package/dist/transports/sigv4.d.mts +27 -0
  263. package/dist/transports/sigv4.d.mts.map +1 -0
  264. package/dist/transports/sigv4.mjs +82 -0
  265. package/dist/transports/sigv4.mjs.map +1 -0
  266. package/package.json +53 -1
  267. package/dist/cloudflare/channels.cjs.map +0 -1
  268. package/dist/cloudflare/channels.d.cts.map +0 -1
  269. package/dist/cloudflare/channels.d.mts.map +0 -1
  270. package/dist/cloudflare/channels.mjs.map +0 -1
  271. package/dist/cloudflare/router.cjs.map +0 -1
  272. package/dist/cloudflare/router.d.cts.map +0 -1
  273. package/dist/cloudflare/router.d.mts.map +0 -1
  274. package/dist/cloudflare/router.mjs.map +0 -1
  275. package/dist/cloudflare/senders.cjs +0 -121
  276. package/dist/cloudflare/senders.cjs.map +0 -1
  277. package/dist/cloudflare/senders.d.cts +0 -72
  278. package/dist/cloudflare/senders.d.cts.map +0 -1
  279. package/dist/cloudflare/senders.d.mts +0 -72
  280. package/dist/cloudflare/senders.d.mts.map +0 -1
  281. package/dist/cloudflare/senders.mjs +0 -115
  282. package/dist/cloudflare/senders.mjs.map +0 -1
@@ -0,0 +1,172 @@
1
+ import { murmur3 } from "../engine/bucket.mjs";
2
+ import { InstanceNotFoundError } from "../cloudflare/bindings.mjs";
3
+ //#region src/aws/instances.ts
4
+ /**
5
+ * The router's instance port over Lambda durable executions: an instance is one execution of
6
+ * this same function, started by an asynchronous self-invoke under a stable execution name;
7
+ * a wake completes the callback its current wait parks on; stopping stops the execution.
8
+ */
9
+ /** The top-level shape of an internal run: what the self-invoke sends, never an HTTP body. */
10
+ const RUN_EVENT_TYPE = "$workflow.run";
11
+ function isRunInput(event) {
12
+ if (typeof event !== "object" || event === null) return false;
13
+ const candidate = event;
14
+ return candidate.type === "$workflow.run" && candidate.requestContext === void 0 && typeof candidate.instanceId === "string" && typeof candidate.enteredAtMs === "number" && typeof candidate.params === "object" && candidate.params !== null;
15
+ }
16
+ const utf8 = new TextEncoder();
17
+ const NAME = /^[a-zA-Z0-9_-]{1,64}$/;
18
+ /**
19
+ * The durable execution name for an instance: the instance id itself when it fits, otherwise a
20
+ * prefix of it with a fingerprint of the whole — stable, so every retry of the start reuses it.
21
+ */
22
+ function executionName(instanceId) {
23
+ if (NAME.test(instanceId)) return instanceId;
24
+ const safe = instanceId.replace(/[^a-zA-Z0-9_-]/g, "-");
25
+ const fingerprint = `${murmur3(utf8.encode(instanceId)).toString(36)}${murmur3(utf8.encode(instanceId), 2538058380).toString(36)}`;
26
+ return `${safe.slice(0, 63 - fingerprint.length)}-${fingerprint}`;
27
+ }
28
+ const MINUTE = 6e4;
29
+ var AwsInstances = class {
30
+ options;
31
+ now;
32
+ startRetryMs;
33
+ reconcileEveryMs;
34
+ constructor(options) {
35
+ this.options = options;
36
+ this.now = options.now ?? Date.now;
37
+ this.startRetryMs = options.startRetryMs ?? 720 * MINUTE;
38
+ this.reconcileEveryMs = options.reconcileEveryMs ?? 15 * MINUTE;
39
+ }
40
+ async plan(options) {
41
+ const input = {
42
+ type: RUN_EVENT_TYPE,
43
+ instanceId: options.id,
44
+ params: options.params,
45
+ enteredAtMs: this.now()
46
+ };
47
+ return {
48
+ name: executionName(options.id),
49
+ target: await this.options.target(options.params.contentHash),
50
+ input: JSON.stringify(input)
51
+ };
52
+ }
53
+ async create(options) {
54
+ const record = await this.options.executions.get(options.id);
55
+ if (record === void 0) throw new Error(`no start is recorded for ${options.id}: the entry must be planned first`);
56
+ await this.submit(record);
57
+ return this.instance(options.id);
58
+ }
59
+ async get(id) {
60
+ if (await this.options.executions.get(id) === void 0) throw new InstanceNotFoundError(id);
61
+ return this.instance(id);
62
+ }
63
+ async resume(entry) {
64
+ for (const record of await this.options.executions.pendingStarts({
65
+ ...entry,
66
+ limit: 1
67
+ })) if (!await this.giveUpIfStale(record)) await this.submit(record);
68
+ }
69
+ async maintain() {
70
+ const now = this.now();
71
+ const failures = [];
72
+ for (const record of await this.options.executions.pendingStarts({
73
+ createdBefore: now - MINUTE,
74
+ limit: 25
75
+ })) try {
76
+ if (!await this.giveUpIfStale(record)) await this.submit(record);
77
+ } catch (error) {
78
+ failures.push(error);
79
+ }
80
+ for (const record of await this.options.executions.toReconcile(25, now - this.reconcileEveryMs)) try {
81
+ await this.reconcile(record, now);
82
+ } catch (error) {
83
+ failures.push(error);
84
+ }
85
+ if (failures.length > 0) throw failures[0];
86
+ }
87
+ async submit(record) {
88
+ const { arn } = await this.options.lambda.startExecution({
89
+ target: record.target,
90
+ name: record.name,
91
+ payload: record.input
92
+ });
93
+ await this.options.executions.markSubmitted(record.instanceId, arn, this.now());
94
+ }
95
+ /** Past the retry budget a start is unresolved for good: no retry could tell a duplicate apart. */
96
+ async giveUpIfStale(record) {
97
+ if (this.now() - record.createdAt <= this.startRetryMs) return false;
98
+ await this.options.executions.markUnresolved(record.instanceId, this.now());
99
+ console.error(`[workflow] the start of ${record.instanceId} (${record.name}) was never confirmed: left unresolved`);
100
+ return true;
101
+ }
102
+ async arnOf(record) {
103
+ if (record.arn !== void 0) return record.arn;
104
+ const arn = await this.options.lambda.findExecution(record.target, record.name);
105
+ if (arn !== void 0) await this.options.executions.setArn(record.instanceId, arn);
106
+ return arn;
107
+ }
108
+ /**
109
+ * Past the start retry budget, an execution still nowhere to be seen never started: any queued
110
+ * invoke has expired by then. Before that, not finding it says nothing.
111
+ */
112
+ vanished(record) {
113
+ return this.now() - record.createdAt > this.startRetryMs;
114
+ }
115
+ async reconcile(record, now) {
116
+ const arn = await this.arnOf(record);
117
+ const state = arn === void 0 ? void 0 : await this.options.lambda.executionState(arn);
118
+ if (state === "failed" || state === "stopped" || state === "timed_out" || state === void 0 && this.vanished(record)) {
119
+ const { store } = this.options;
120
+ await store.setEntryStatus(record.instanceId, "failed");
121
+ await store.reporting?.instanceFinished({
122
+ workflow: record.workflow,
123
+ instanceId: record.instanceId,
124
+ userId: record.userId,
125
+ status: "failed",
126
+ at: now
127
+ });
128
+ console.warn(`[workflow] ${record.instanceId} ended on the platform (${state ?? "unknown"}) without finishing: recorded as failed`);
129
+ }
130
+ await this.options.executions.markChecked(record.instanceId, now);
131
+ }
132
+ instance(id) {
133
+ const { executions, lambda } = this.options;
134
+ const current = async () => {
135
+ const record = await executions.get(id);
136
+ if (record === void 0) throw new InstanceNotFoundError(id);
137
+ return record;
138
+ };
139
+ return {
140
+ id,
141
+ sendEvent: async (event) => {
142
+ const { callbackId } = await current();
143
+ if (callbackId === void 0) return;
144
+ await lambda.completeCallback(callbackId, JSON.stringify(event.payload ?? {}));
145
+ },
146
+ terminate: async () => {
147
+ const record = await current();
148
+ if (record.submittedAt === void 0 && record.unresolvedAt === void 0) await executions.markUnresolved(id, this.now());
149
+ const arn = await this.arnOf(record);
150
+ if (arn === void 0) {
151
+ if (this.vanished(record)) return;
152
+ throw new Error(`cannot stop ${id} yet: its execution is not visible`);
153
+ }
154
+ if (!await lambda.stopExecution(arn)) throw new Error(`stopping ${id} failed`);
155
+ },
156
+ status: async () => {
157
+ const record = await current();
158
+ const arn = await this.arnOf(record);
159
+ if (arn === void 0) return { status: this.vanished(record) ? "terminated" : "running" };
160
+ const state = await lambda.executionState(arn);
161
+ if (state === "succeeded") return { status: "complete" };
162
+ if (state === "failed") return { status: "errored" };
163
+ if (state === "running") return { status: "running" };
164
+ return { status: "terminated" };
165
+ }
166
+ };
167
+ }
168
+ };
169
+ //#endregion
170
+ export { AwsInstances, RUN_EVENT_TYPE, executionName, isRunInput };
171
+
172
+ //# sourceMappingURL=instances.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instances.mjs","names":[],"sources":["../../src/aws/instances.ts"],"sourcesContent":["import {\n InstanceNotFoundError,\n type JourneyParams,\n type WorkflowBindingLike,\n type WorkflowInstanceLike,\n} from '../cloudflare/bindings';\nimport { murmur3 } from '../engine/bucket';\nimport type {\n ExecutionRecord,\n ExecutionRegistry,\n ExecutionStart,\n JourneyStore,\n} from '../store/index';\nimport type { LambdaApi } from './lambda-api';\n\n/**\n * The router's instance port over Lambda durable executions: an instance is one execution of\n * this same function, started by an asynchronous self-invoke under a stable execution name;\n * a wake completes the callback its current wait parks on; stopping stops the execution.\n */\n\n/** The top-level shape of an internal run: what the self-invoke sends, never an HTTP body. */\nexport const RUN_EVENT_TYPE = '$workflow.run';\n\nexport interface RunInput {\n type: typeof RUN_EVENT_TYPE;\n instanceId: string;\n params: JourneyParams;\n /** When the instance was created (as Cloudflare's event timestamp): its entry anchor, fixed with the start. */\n enteredAtMs: number;\n}\n\nexport function isRunInput(event: unknown): event is RunInput {\n if (typeof event !== 'object' || event === null) return false;\n const candidate = event as {\n type?: unknown;\n instanceId?: unknown;\n enteredAtMs?: unknown;\n params?: unknown;\n requestContext?: unknown;\n };\n return (\n candidate.type === RUN_EVENT_TYPE &&\n candidate.requestContext === undefined &&\n typeof candidate.instanceId === 'string' &&\n typeof candidate.enteredAtMs === 'number' &&\n typeof candidate.params === 'object' &&\n candidate.params !== null\n );\n}\n\nconst utf8 = new TextEncoder();\nconst NAME = /^[a-zA-Z0-9_-]{1,64}$/;\n\n/**\n * The durable execution name for an instance: the instance id itself when it fits, otherwise a\n * prefix of it with a fingerprint of the whole — stable, so every retry of the start reuses it.\n */\nexport function executionName(instanceId: string): string {\n if (NAME.test(instanceId)) return instanceId;\n const safe = instanceId.replace(/[^a-zA-Z0-9_-]/g, '-');\n const fingerprint = `${murmur3(utf8.encode(instanceId)).toString(36)}${murmur3(utf8.encode(instanceId), 0x9747b28c).toString(36)}`;\n return `${safe.slice(0, 63 - fingerprint.length)}-${fingerprint}`;\n}\n\nexport interface AwsInstancesOptions {\n store: JourneyStore;\n executions: ExecutionRegistry;\n lambda: LambdaApi;\n /** The qualified function ARN an execution pinned to `contentHash` must run on. */\n target(contentHash: string): Promise<string>;\n /**\n * How long an unconfirmed start keeps being retried. It must stay shorter than the durable\n * execution retention: past that the platform forgets the name, and a retry would start a\n * second execution. Twelve hours by default.\n */\n startRetryMs?: number;\n /** How stale a running instance's last check must be before maintenance looks again. */\n reconcileEveryMs?: number;\n now?: () => number;\n}\n\nconst MINUTE = 60_000;\n\nexport class AwsInstances implements WorkflowBindingLike {\n private readonly now: () => number;\n private readonly startRetryMs: number;\n private readonly reconcileEveryMs: number;\n\n constructor(private readonly options: AwsInstancesOptions) {\n this.now = options.now ?? Date.now;\n this.startRetryMs = options.startRetryMs ?? 12 * 60 * MINUTE;\n this.reconcileEveryMs = options.reconcileEveryMs ?? 15 * MINUTE;\n }\n\n async plan(options: { id: string; params: JourneyParams; ts: number }): Promise<ExecutionStart> {\n const input: RunInput = {\n type: RUN_EVENT_TYPE,\n instanceId: options.id,\n params: options.params,\n // The moment the instance is created, like Cloudflare's: an event's own ts may be long past\n enteredAtMs: this.now(),\n };\n return {\n name: executionName(options.id),\n target: await this.options.target(options.params.contentHash),\n input: JSON.stringify(input),\n };\n }\n\n async create(options: { id: string; params: unknown }): Promise<WorkflowInstanceLike> {\n const record = await this.options.executions.get(options.id);\n if (record === undefined) {\n throw new Error(`no start is recorded for ${options.id}: the entry must be planned first`);\n }\n await this.submit(record);\n return this.instance(options.id);\n }\n\n async get(id: string): Promise<WorkflowInstanceLike> {\n // A read of the registry alone: a Lambda failure must never read as \"no such instance\"\n if ((await this.options.executions.get(id)) === undefined) {\n throw new InstanceNotFoundError(id);\n }\n return this.instance(id);\n }\n\n async resume(entry: { workflow: string; userId: string }): Promise<void> {\n for (const record of await this.options.executions.pendingStarts({ ...entry, limit: 1 })) {\n if (!(await this.giveUpIfStale(record))) await this.submit(record);\n }\n }\n\n async maintain(): Promise<void> {\n const now = this.now();\n const failures: unknown[] = [];\n // Starts whose submit never confirmed: retried with the recorded name and payload\n for (const record of await this.options.executions.pendingStarts({\n createdBefore: now - MINUTE,\n limit: 25,\n })) {\n try {\n if (!(await this.giveUpIfStale(record))) await this.submit(record);\n } catch (error) {\n failures.push(error);\n }\n }\n // Executions the platform ended without the run finalizing (a timeout, a stop, a crash)\n for (const record of await this.options.executions.toReconcile(\n 25,\n now - this.reconcileEveryMs\n )) {\n try {\n await this.reconcile(record, now);\n } catch (error) {\n failures.push(error);\n }\n }\n if (failures.length > 0) throw failures[0];\n }\n\n private async submit(record: ExecutionRecord): Promise<void> {\n const { arn } = await this.options.lambda.startExecution({\n target: record.target,\n name: record.name,\n payload: record.input,\n });\n await this.options.executions.markSubmitted(record.instanceId, arn, this.now());\n }\n\n /** Past the retry budget a start is unresolved for good: no retry could tell a duplicate apart. */\n private async giveUpIfStale(record: ExecutionRecord): Promise<boolean> {\n if (this.now() - record.createdAt <= this.startRetryMs) return false;\n await this.options.executions.markUnresolved(record.instanceId, this.now());\n console.error(\n `[workflow] the start of ${record.instanceId} (${record.name}) was never confirmed: left unresolved`\n );\n return true;\n }\n\n private async arnOf(record: ExecutionRecord): Promise<string | undefined> {\n if (record.arn !== undefined) return record.arn;\n const arn = await this.options.lambda.findExecution(record.target, record.name);\n if (arn !== undefined) await this.options.executions.setArn(record.instanceId, arn);\n return arn;\n }\n\n /**\n * Past the start retry budget, an execution still nowhere to be seen never started: any queued\n * invoke has expired by then. Before that, not finding it says nothing.\n */\n private vanished(record: ExecutionRecord): boolean {\n return this.now() - record.createdAt > this.startRetryMs;\n }\n\n private async reconcile(record: ExecutionRecord, now: number): Promise<void> {\n const arn = await this.arnOf(record);\n const state = arn === undefined ? undefined : await this.options.lambda.executionState(arn);\n // Only a state that will never run another step — or one missing for good — ends the entry;\n // a run still queued, or not visible yet, is left alone\n const ended =\n state === 'failed' ||\n state === 'stopped' ||\n state === 'timed_out' ||\n (state === undefined && this.vanished(record));\n if (ended) {\n // Ended without its finalize step: the ledger must not show it running forever\n const { store } = this.options;\n await store.setEntryStatus(record.instanceId, 'failed');\n await store.reporting?.instanceFinished({\n workflow: record.workflow,\n instanceId: record.instanceId,\n userId: record.userId,\n status: 'failed',\n at: now,\n });\n console.warn(\n `[workflow] ${record.instanceId} ended on the platform (${state ?? 'unknown'}) without finishing: recorded as failed`\n );\n }\n await this.options.executions.markChecked(record.instanceId, now);\n }\n\n private instance(id: string): WorkflowInstanceLike {\n const { executions, lambda } = this.options;\n const current = async () => {\n const record = await executions.get(id);\n if (record === undefined) throw new InstanceNotFoundError(id);\n return record;\n };\n return {\n id,\n sendEvent: async (event) => {\n // The wait parked now, read at the moment of the wake: a newer wait's callback replaces\n // an older one, and a wake for none (the instance is between waits) is nothing to do —\n // its next wait re-reads the facts before parking\n const { callbackId } = await current();\n if (callbackId === undefined) return;\n await lambda.completeCallback(callbackId, JSON.stringify(event.payload ?? {}));\n },\n terminate: async () => {\n const record = await current();\n if (record.submittedAt === undefined && record.unresolvedAt === undefined) {\n // No retry may start it after this\n await executions.markUnresolved(id, this.now());\n }\n const arn = await this.arnOf(record);\n if (arn === undefined) {\n // Not visible yet is not stopped: the erasure stays pending, and the sweep retries\n if (this.vanished(record)) return;\n throw new Error(`cannot stop ${id} yet: its execution is not visible`);\n }\n if (!(await lambda.stopExecution(arn))) throw new Error(`stopping ${id} failed`);\n },\n status: async () => {\n const record = await current();\n const arn = await this.arnOf(record);\n // Not visible yet may still run: only one missing past the budget is gone\n if (arn === undefined) return { status: this.vanished(record) ? 'terminated' : 'running' };\n const state = await lambda.executionState(arn);\n // In the router's vocabulary (Cloudflare's): what will never run another step\n if (state === 'succeeded') return { status: 'complete' };\n if (state === 'failed') return { status: 'errored' };\n if (state === 'running') return { status: 'running' };\n return { status: 'terminated' };\n },\n };\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/aws/lambda-api.ts
3
+ const STATES = {
4
+ RUNNING: "running",
5
+ SUCCEEDED: "succeeded",
6
+ FAILED: "failed",
7
+ STOPPED: "stopped",
8
+ TIMED_OUT: "timed_out"
9
+ };
10
+ function errorName(error) {
11
+ return typeof error === "object" && error !== null ? String(error.name) : void 0;
12
+ }
13
+ const utf8 = new TextEncoder();
14
+ const fromUtf8 = new TextDecoder();
15
+ /** The default port, over `@aws-sdk/client-lambda` with the execution role's credentials. */
16
+ function sdkLambdaApi(options = {}) {
17
+ let loaded;
18
+ const load = () => loaded ??= import("@aws-sdk/client-lambda").then((sdk) => ({
19
+ sdk,
20
+ client: options.client ?? new sdk.LambdaClient(options.region === void 0 ? {} : { region: options.region })
21
+ }));
22
+ const executionState = async (arn) => {
23
+ const { sdk, client } = await load();
24
+ try {
25
+ const response = await client.send(new sdk.GetDurableExecutionCommand({ DurableExecutionArn: arn }));
26
+ return response.Status === void 0 ? void 0 : STATES[response.Status];
27
+ } catch (error) {
28
+ if (errorName(error) === "ResourceNotFoundException") return void 0;
29
+ throw error;
30
+ }
31
+ };
32
+ return {
33
+ executionState,
34
+ async startExecution({ target, name, payload }) {
35
+ const { sdk, client } = await load();
36
+ const response = await client.send(new sdk.InvokeCommand({
37
+ FunctionName: target,
38
+ InvocationType: "Event",
39
+ DurableExecutionName: name,
40
+ Payload: utf8.encode(payload)
41
+ }));
42
+ if (response.FunctionError !== void 0) {
43
+ const detail = response.Payload === void 0 ? "" : fromUtf8.decode(response.Payload);
44
+ throw new Error(`starting ${name} failed: ${response.FunctionError} ${detail}`.trim());
45
+ }
46
+ return { arn: response.DurableExecutionArn };
47
+ },
48
+ async completeCallback(callbackId, result) {
49
+ const { sdk, client } = await load();
50
+ try {
51
+ await client.send(new sdk.SendDurableExecutionCallbackSuccessCommand({
52
+ CallbackId: callbackId,
53
+ Result: utf8.encode(result)
54
+ }));
55
+ return true;
56
+ } catch (error) {
57
+ if (errorName(error) === "CallbackTimeoutException") return false;
58
+ throw error;
59
+ }
60
+ },
61
+ async findExecution(target, name) {
62
+ const { sdk, client } = await load();
63
+ const qualifier = target.split(":").at(7);
64
+ const functionName = target.split(":").slice(0, 7).join(":");
65
+ return ((await client.send(new sdk.ListDurableExecutionsByFunctionCommand({
66
+ FunctionName: functionName,
67
+ ...qualifier === void 0 ? {} : { Qualifier: qualifier },
68
+ DurableExecutionName: name,
69
+ MaxItems: 10
70
+ }))).DurableExecutions?.find((execution) => execution.DurableExecutionName === name))?.DurableExecutionArn;
71
+ },
72
+ async stopExecution(arn) {
73
+ const { sdk, client } = await load();
74
+ try {
75
+ await client.send(new sdk.StopDurableExecutionCommand({ DurableExecutionArn: arn }));
76
+ return true;
77
+ } catch (error) {
78
+ if (errorName(error) === "ResourceNotFoundException") return true;
79
+ const state = await executionState(arn).catch(() => void 0);
80
+ if (state !== void 0 && state !== "running") return true;
81
+ throw error;
82
+ }
83
+ }
84
+ };
85
+ }
86
+ //#endregion
87
+ exports.sdkLambdaApi = sdkLambdaApi;
88
+
89
+ //# sourceMappingURL=lambda-api.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lambda-api.cjs","names":[],"sources":["../../src/aws/lambda-api.ts"],"sourcesContent":["import type * as LambdaSdk from '@aws-sdk/client-lambda';\n\n/**\n * The Lambda control-plane calls the adapter makes, as a port: starting an execution of this\n * function, completing a wake callback, and looking at or stopping an execution. The default\n * goes through `@aws-sdk/client-lambda` (loaded on first use); tests pass a fake.\n */\n\n/** A durable execution's state as the adapter reads it. */\nexport type ExecutionState = 'running' | 'succeeded' | 'failed' | 'stopped' | 'timed_out';\n\nexport interface LambdaApi {\n /**\n * Asynchronous (`Event`) invoke of a qualified function ARN under a durable execution name:\n * idempotent — the same name and payload return the execution already started.\n */\n startExecution(input: {\n target: string;\n name: string;\n payload: string;\n }): Promise<{ arn?: string | undefined }>;\n /**\n * Complete a callback. `false` when it is already closed (completed, or timed out): the wait\n * it belonged to has moved on, which a wake-up treats as nothing to do.\n */\n completeCallback(callbackId: string, result: string): Promise<boolean>;\n /** The execution's state; undefined when the platform no longer knows it. */\n executionState(arn: string): Promise<ExecutionState | undefined>;\n /** The ARN of the execution started under `name`, if there is one. */\n findExecution(target: string, name: string): Promise<string | undefined>;\n /** Stop an execution for good; `true` also when it had already ended. */\n stopExecution(arn: string): Promise<boolean>;\n}\n\nconst STATES: Record<string, ExecutionState> = {\n RUNNING: 'running',\n SUCCEEDED: 'succeeded',\n FAILED: 'failed',\n STOPPED: 'stopped',\n TIMED_OUT: 'timed_out',\n};\n\nfunction errorName(error: unknown): string | undefined {\n return typeof error === 'object' && error !== null\n ? String((error as { name?: unknown }).name)\n : undefined;\n}\n\nconst utf8 = new TextEncoder();\nconst fromUtf8 = new TextDecoder();\n\ntype Sdk = typeof LambdaSdk;\n\n/** The default port, over `@aws-sdk/client-lambda` with the execution role's credentials. */\nexport function sdkLambdaApi(\n options: {\n region?: string;\n /** A configured client (endpoint, retries) instead of the default one. */\n client?: Pick<InstanceType<Sdk['LambdaClient']>, 'send'>;\n } = {}\n): LambdaApi {\n let loaded:\n | Promise<{ sdk: Sdk; client: Pick<InstanceType<Sdk['LambdaClient']>, 'send'> }>\n | undefined;\n const load = () =>\n (loaded ??= import('@aws-sdk/client-lambda').then((sdk) => ({\n sdk,\n client:\n options.client ??\n new sdk.LambdaClient(options.region === undefined ? {} : { region: options.region }),\n })));\n\n const executionState = async (arn: string): Promise<ExecutionState | undefined> => {\n const { sdk, client } = await load();\n try {\n const response = await client.send(\n new sdk.GetDurableExecutionCommand({ DurableExecutionArn: arn })\n );\n return response.Status === undefined ? undefined : STATES[response.Status];\n } catch (error) {\n if (errorName(error) === 'ResourceNotFoundException') return undefined;\n throw error;\n }\n };\n\n return {\n executionState,\n\n async startExecution({ target, name, payload }) {\n const { sdk, client } = await load();\n const response = await client.send(\n new sdk.InvokeCommand({\n FunctionName: target,\n InvocationType: 'Event',\n DurableExecutionName: name,\n Payload: utf8.encode(payload),\n })\n );\n if (response.FunctionError !== undefined) {\n const detail = response.Payload === undefined ? '' : fromUtf8.decode(response.Payload);\n throw new Error(`starting ${name} failed: ${response.FunctionError} ${detail}`.trim());\n }\n return { arn: response.DurableExecutionArn };\n },\n\n async completeCallback(callbackId, result) {\n const { sdk, client } = await load();\n try {\n await client.send(\n new sdk.SendDurableExecutionCallbackSuccessCommand({\n CallbackId: callbackId,\n Result: utf8.encode(result),\n })\n );\n return true;\n } catch (error) {\n // \"expired, or already closed\": the wait moved on. Anything else (throttling, a\n // permission) is a real failure — it must not pass for a wait that ended\n if (errorName(error) === 'CallbackTimeoutException') return false;\n throw error;\n }\n },\n\n async findExecution(target, name) {\n const { sdk, client } = await load();\n // The name is unique per function; the version is part of the target ARN\n const qualifier = target.split(':').at(7);\n const functionName = target.split(':').slice(0, 7).join(':');\n const response = await client.send(\n new sdk.ListDurableExecutionsByFunctionCommand({\n FunctionName: functionName,\n ...(qualifier === undefined ? {} : { Qualifier: qualifier }),\n DurableExecutionName: name,\n MaxItems: 10,\n })\n );\n const match = response.DurableExecutions?.find(\n (execution) => execution.DurableExecutionName === name\n );\n return match?.DurableExecutionArn;\n },\n\n async stopExecution(arn) {\n const { sdk, client } = await load();\n try {\n await client.send(new sdk.StopDurableExecutionCommand({ DurableExecutionArn: arn }));\n return true;\n } catch (error) {\n if (errorName(error) === 'ResourceNotFoundException') return true;\n // Stopping one that already ended is refused: it will not run again either way\n const state = await executionState(arn).catch(() => undefined);\n if (state !== undefined && state !== 'running') return true;\n throw error;\n }\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import * as LambdaSdk from "@aws-sdk/client-lambda";
2
+ //#region src/aws/lambda-api.d.ts
3
+ /**
4
+ * The Lambda control-plane calls the adapter makes, as a port: starting an execution of this
5
+ * function, completing a wake callback, and looking at or stopping an execution. The default
6
+ * goes through `@aws-sdk/client-lambda` (loaded on first use); tests pass a fake.
7
+ */
8
+ /** A durable execution's state as the adapter reads it. */
9
+ export type ExecutionState = 'running' | 'succeeded' | 'failed' | 'stopped' | 'timed_out';
10
+ export interface LambdaApi {
11
+ /**
12
+ * Asynchronous (`Event`) invoke of a qualified function ARN under a durable execution name:
13
+ * idempotent — the same name and payload return the execution already started.
14
+ */
15
+ startExecution(input: {
16
+ target: string;
17
+ name: string;
18
+ payload: string;
19
+ }): Promise<{
20
+ arn?: string | undefined;
21
+ }>;
22
+ /**
23
+ * Complete a callback. `false` when it is already closed (completed, or timed out): the wait
24
+ * it belonged to has moved on, which a wake-up treats as nothing to do.
25
+ */
26
+ completeCallback(callbackId: string, result: string): Promise<boolean>;
27
+ /** The execution's state; undefined when the platform no longer knows it. */
28
+ executionState(arn: string): Promise<ExecutionState | undefined>;
29
+ /** The ARN of the execution started under `name`, if there is one. */
30
+ findExecution(target: string, name: string): Promise<string | undefined>;
31
+ /** Stop an execution for good; `true` also when it had already ended. */
32
+ stopExecution(arn: string): Promise<boolean>;
33
+ }
34
+ type Sdk = typeof LambdaSdk;
35
+ /** The default port, over `@aws-sdk/client-lambda` with the execution role's credentials. */
36
+ export declare function sdkLambdaApi(options?: {
37
+ region?: string;
38
+ /** A configured client (endpoint, retries) instead of the default one. */
39
+ client?: Pick<InstanceType<Sdk['LambdaClient']>, 'send'>;
40
+ }): LambdaApi;
41
+ //#endregion
42
+ export {};
43
+ //# sourceMappingURL=lambda-api.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lambda-api.d.cts","names":[],"sources":["../../src/aws/lambda-api.ts"],"mappings":";;;;;;;;YASY;iBAEK;;;;;EAKf,eAAe;IACb;IACA;IACA;MACE;IAAU;;;;;;EAKd,iBAAiB,oBAAoB,iBAAiB;;EAEtD,eAAe,cAAc,QAAQ;;EAErC,cAAc,gBAAgB,eAAe;;EAE7C,cAAc,cAAc;;KAoBzB,aAAa;;wBAGF,aACd;EACE;;EAEA,SAAS,KAAK,aAAa;IAE5B"}
@@ -0,0 +1,43 @@
1
+ import * as LambdaSdk from "@aws-sdk/client-lambda";
2
+ //#region src/aws/lambda-api.d.ts
3
+ /**
4
+ * The Lambda control-plane calls the adapter makes, as a port: starting an execution of this
5
+ * function, completing a wake callback, and looking at or stopping an execution. The default
6
+ * goes through `@aws-sdk/client-lambda` (loaded on first use); tests pass a fake.
7
+ */
8
+ /** A durable execution's state as the adapter reads it. */
9
+ export type ExecutionState = 'running' | 'succeeded' | 'failed' | 'stopped' | 'timed_out';
10
+ export interface LambdaApi {
11
+ /**
12
+ * Asynchronous (`Event`) invoke of a qualified function ARN under a durable execution name:
13
+ * idempotent — the same name and payload return the execution already started.
14
+ */
15
+ startExecution(input: {
16
+ target: string;
17
+ name: string;
18
+ payload: string;
19
+ }): Promise<{
20
+ arn?: string | undefined;
21
+ }>;
22
+ /**
23
+ * Complete a callback. `false` when it is already closed (completed, or timed out): the wait
24
+ * it belonged to has moved on, which a wake-up treats as nothing to do.
25
+ */
26
+ completeCallback(callbackId: string, result: string): Promise<boolean>;
27
+ /** The execution's state; undefined when the platform no longer knows it. */
28
+ executionState(arn: string): Promise<ExecutionState | undefined>;
29
+ /** The ARN of the execution started under `name`, if there is one. */
30
+ findExecution(target: string, name: string): Promise<string | undefined>;
31
+ /** Stop an execution for good; `true` also when it had already ended. */
32
+ stopExecution(arn: string): Promise<boolean>;
33
+ }
34
+ type Sdk = typeof LambdaSdk;
35
+ /** The default port, over `@aws-sdk/client-lambda` with the execution role's credentials. */
36
+ export declare function sdkLambdaApi(options?: {
37
+ region?: string;
38
+ /** A configured client (endpoint, retries) instead of the default one. */
39
+ client?: Pick<InstanceType<Sdk['LambdaClient']>, 'send'>;
40
+ }): LambdaApi;
41
+ //#endregion
42
+ export {};
43
+ //# sourceMappingURL=lambda-api.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lambda-api.d.mts","names":[],"sources":["../../src/aws/lambda-api.ts"],"mappings":";;;;;;;;YASY;iBAEK;;;;;EAKf,eAAe;IACb;IACA;IACA;MACE;IAAU;;;;;;EAKd,iBAAiB,oBAAoB,iBAAiB;;EAEtD,eAAe,cAAc,QAAQ;;EAErC,cAAc,gBAAgB,eAAe;;EAE7C,cAAc,cAAc;;KAoBzB,aAAa;;wBAGF,aACd;EACE;;EAEA,SAAS,KAAK,aAAa;IAE5B"}
@@ -0,0 +1,88 @@
1
+ //#region src/aws/lambda-api.ts
2
+ const STATES = {
3
+ RUNNING: "running",
4
+ SUCCEEDED: "succeeded",
5
+ FAILED: "failed",
6
+ STOPPED: "stopped",
7
+ TIMED_OUT: "timed_out"
8
+ };
9
+ function errorName(error) {
10
+ return typeof error === "object" && error !== null ? String(error.name) : void 0;
11
+ }
12
+ const utf8 = new TextEncoder();
13
+ const fromUtf8 = new TextDecoder();
14
+ /** The default port, over `@aws-sdk/client-lambda` with the execution role's credentials. */
15
+ function sdkLambdaApi(options = {}) {
16
+ let loaded;
17
+ const load = () => loaded ??= import("@aws-sdk/client-lambda").then((sdk) => ({
18
+ sdk,
19
+ client: options.client ?? new sdk.LambdaClient(options.region === void 0 ? {} : { region: options.region })
20
+ }));
21
+ const executionState = async (arn) => {
22
+ const { sdk, client } = await load();
23
+ try {
24
+ const response = await client.send(new sdk.GetDurableExecutionCommand({ DurableExecutionArn: arn }));
25
+ return response.Status === void 0 ? void 0 : STATES[response.Status];
26
+ } catch (error) {
27
+ if (errorName(error) === "ResourceNotFoundException") return void 0;
28
+ throw error;
29
+ }
30
+ };
31
+ return {
32
+ executionState,
33
+ async startExecution({ target, name, payload }) {
34
+ const { sdk, client } = await load();
35
+ const response = await client.send(new sdk.InvokeCommand({
36
+ FunctionName: target,
37
+ InvocationType: "Event",
38
+ DurableExecutionName: name,
39
+ Payload: utf8.encode(payload)
40
+ }));
41
+ if (response.FunctionError !== void 0) {
42
+ const detail = response.Payload === void 0 ? "" : fromUtf8.decode(response.Payload);
43
+ throw new Error(`starting ${name} failed: ${response.FunctionError} ${detail}`.trim());
44
+ }
45
+ return { arn: response.DurableExecutionArn };
46
+ },
47
+ async completeCallback(callbackId, result) {
48
+ const { sdk, client } = await load();
49
+ try {
50
+ await client.send(new sdk.SendDurableExecutionCallbackSuccessCommand({
51
+ CallbackId: callbackId,
52
+ Result: utf8.encode(result)
53
+ }));
54
+ return true;
55
+ } catch (error) {
56
+ if (errorName(error) === "CallbackTimeoutException") return false;
57
+ throw error;
58
+ }
59
+ },
60
+ async findExecution(target, name) {
61
+ const { sdk, client } = await load();
62
+ const qualifier = target.split(":").at(7);
63
+ const functionName = target.split(":").slice(0, 7).join(":");
64
+ return ((await client.send(new sdk.ListDurableExecutionsByFunctionCommand({
65
+ FunctionName: functionName,
66
+ ...qualifier === void 0 ? {} : { Qualifier: qualifier },
67
+ DurableExecutionName: name,
68
+ MaxItems: 10
69
+ }))).DurableExecutions?.find((execution) => execution.DurableExecutionName === name))?.DurableExecutionArn;
70
+ },
71
+ async stopExecution(arn) {
72
+ const { sdk, client } = await load();
73
+ try {
74
+ await client.send(new sdk.StopDurableExecutionCommand({ DurableExecutionArn: arn }));
75
+ return true;
76
+ } catch (error) {
77
+ if (errorName(error) === "ResourceNotFoundException") return true;
78
+ const state = await executionState(arn).catch(() => void 0);
79
+ if (state !== void 0 && state !== "running") return true;
80
+ throw error;
81
+ }
82
+ }
83
+ };
84
+ }
85
+ //#endregion
86
+ export { sdkLambdaApi };
87
+
88
+ //# sourceMappingURL=lambda-api.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lambda-api.mjs","names":[],"sources":["../../src/aws/lambda-api.ts"],"sourcesContent":["import type * as LambdaSdk from '@aws-sdk/client-lambda';\n\n/**\n * The Lambda control-plane calls the adapter makes, as a port: starting an execution of this\n * function, completing a wake callback, and looking at or stopping an execution. The default\n * goes through `@aws-sdk/client-lambda` (loaded on first use); tests pass a fake.\n */\n\n/** A durable execution's state as the adapter reads it. */\nexport type ExecutionState = 'running' | 'succeeded' | 'failed' | 'stopped' | 'timed_out';\n\nexport interface LambdaApi {\n /**\n * Asynchronous (`Event`) invoke of a qualified function ARN under a durable execution name:\n * idempotent — the same name and payload return the execution already started.\n */\n startExecution(input: {\n target: string;\n name: string;\n payload: string;\n }): Promise<{ arn?: string | undefined }>;\n /**\n * Complete a callback. `false` when it is already closed (completed, or timed out): the wait\n * it belonged to has moved on, which a wake-up treats as nothing to do.\n */\n completeCallback(callbackId: string, result: string): Promise<boolean>;\n /** The execution's state; undefined when the platform no longer knows it. */\n executionState(arn: string): Promise<ExecutionState | undefined>;\n /** The ARN of the execution started under `name`, if there is one. */\n findExecution(target: string, name: string): Promise<string | undefined>;\n /** Stop an execution for good; `true` also when it had already ended. */\n stopExecution(arn: string): Promise<boolean>;\n}\n\nconst STATES: Record<string, ExecutionState> = {\n RUNNING: 'running',\n SUCCEEDED: 'succeeded',\n FAILED: 'failed',\n STOPPED: 'stopped',\n TIMED_OUT: 'timed_out',\n};\n\nfunction errorName(error: unknown): string | undefined {\n return typeof error === 'object' && error !== null\n ? String((error as { name?: unknown }).name)\n : undefined;\n}\n\nconst utf8 = new TextEncoder();\nconst fromUtf8 = new TextDecoder();\n\ntype Sdk = typeof LambdaSdk;\n\n/** The default port, over `@aws-sdk/client-lambda` with the execution role's credentials. */\nexport function sdkLambdaApi(\n options: {\n region?: string;\n /** A configured client (endpoint, retries) instead of the default one. */\n client?: Pick<InstanceType<Sdk['LambdaClient']>, 'send'>;\n } = {}\n): LambdaApi {\n let loaded:\n | Promise<{ sdk: Sdk; client: Pick<InstanceType<Sdk['LambdaClient']>, 'send'> }>\n | undefined;\n const load = () =>\n (loaded ??= import('@aws-sdk/client-lambda').then((sdk) => ({\n sdk,\n client:\n options.client ??\n new sdk.LambdaClient(options.region === undefined ? {} : { region: options.region }),\n })));\n\n const executionState = async (arn: string): Promise<ExecutionState | undefined> => {\n const { sdk, client } = await load();\n try {\n const response = await client.send(\n new sdk.GetDurableExecutionCommand({ DurableExecutionArn: arn })\n );\n return response.Status === undefined ? undefined : STATES[response.Status];\n } catch (error) {\n if (errorName(error) === 'ResourceNotFoundException') return undefined;\n throw error;\n }\n };\n\n return {\n executionState,\n\n async startExecution({ target, name, payload }) {\n const { sdk, client } = await load();\n const response = await client.send(\n new sdk.InvokeCommand({\n FunctionName: target,\n InvocationType: 'Event',\n DurableExecutionName: name,\n Payload: utf8.encode(payload),\n })\n );\n if (response.FunctionError !== undefined) {\n const detail = response.Payload === undefined ? '' : fromUtf8.decode(response.Payload);\n throw new Error(`starting ${name} failed: ${response.FunctionError} ${detail}`.trim());\n }\n return { arn: response.DurableExecutionArn };\n },\n\n async completeCallback(callbackId, result) {\n const { sdk, client } = await load();\n try {\n await client.send(\n new sdk.SendDurableExecutionCallbackSuccessCommand({\n CallbackId: callbackId,\n Result: utf8.encode(result),\n })\n );\n return true;\n } catch (error) {\n // \"expired, or already closed\": the wait moved on. Anything else (throttling, a\n // permission) is a real failure — it must not pass for a wait that ended\n if (errorName(error) === 'CallbackTimeoutException') return false;\n throw error;\n }\n },\n\n async findExecution(target, name) {\n const { sdk, client } = await load();\n // The name is unique per function; the version is part of the target ARN\n const qualifier = target.split(':').at(7);\n const functionName = target.split(':').slice(0, 7).join(':');\n const response = await client.send(\n new sdk.ListDurableExecutionsByFunctionCommand({\n FunctionName: functionName,\n ...(qualifier === undefined ? {} : { Qualifier: qualifier }),\n DurableExecutionName: name,\n MaxItems: 10,\n })\n );\n const match = response.DurableExecutions?.find(\n (execution) => execution.DurableExecutionName === name\n );\n return match?.DurableExecutionArn;\n },\n\n async stopExecution(arn) {\n const { sdk, client } = await load();\n try {\n await client.send(new sdk.StopDurableExecutionCommand({ DurableExecutionArn: arn }));\n return true;\n } catch (error) {\n if (errorName(error) === 'ResourceNotFoundException') return true;\n // Stopping one that already ended is refused: it will not run again either way\n const state = await executionState(arn).catch(() => undefined);\n if (state !== undefined && state !== 'running') return true;\n throw error;\n }\n },\n };\n}\n"],"mappings":"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"}
package/dist/client.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ProfileProps } from "./store/index.cjs";
2
2
  import { Contract, ContractIssue, ProfilePatch } from "./contract.cjs";
3
- import { DeployResult, IdentifyResult, IngestResult } from "./cloudflare/router.cjs";
3
+ import { DeployResult, IdentifyResult, IngestResult } from "./host/router.cjs";
4
4
  import "./cloudflare/index.cjs";
5
5
  import * as core from "zod/v4/core";
6
6
  //#region src/client.d.ts
package/dist/client.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ProfileProps } from "./store/index.mjs";
2
2
  import { Contract, ContractIssue, ProfilePatch } from "./contract.mjs";
3
- import { DeployResult, IdentifyResult, IngestResult } from "./cloudflare/router.mjs";
3
+ import { DeployResult, IdentifyResult, IngestResult } from "./host/router.mjs";
4
4
  import "./cloudflare/index.mjs";
5
5
  import * as core from "zod/v4/core";
6
6
  //#region src/client.d.ts
@@ -1,5 +1,22 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  //#region src/cloudflare/bindings.ts
3
+ /** The runtime does not know the instance: the only failure that lets the router drop its wakes. */
4
+ var InstanceNotFoundError = class extends Error {
5
+ instanceId;
6
+ name = "InstanceNotFoundError";
7
+ constructor(instanceId) {
8
+ super(`no such instance: ${instanceId}`);
9
+ this.instanceId = instanceId;
10
+ }
11
+ };
12
+ /**
13
+ * Does this lookup failure say the instance is gone, rather than that the lookup failed?
14
+ * Cloudflare reports an unknown id as `instance.not_found`.
15
+ */
16
+ function isInstanceNotFound(error) {
17
+ if (error instanceof InstanceNotFoundError) return true;
18
+ return /not[_ ]found|no such instance/i.test(String(error));
19
+ }
3
20
  /** The event type the router uses to wake waiting instances (sendEvent's `type`). */
4
21
  const WAKE_EVENT_TYPE = "wake";
5
22
  /**
@@ -21,8 +38,10 @@ function wakeExpired(startedAt, timeoutMs, now) {
21
38
  return now - startedAt >= Math.max(0, timeoutMs - WAKE_TIMEOUT_TOLERANCE_MS);
22
39
  }
23
40
  //#endregion
41
+ exports.InstanceNotFoundError = InstanceNotFoundError;
24
42
  exports.WAKE_EVENT_TYPE = WAKE_EVENT_TYPE;
25
43
  exports.WAKE_TIMEOUT_TOLERANCE_MS = WAKE_TIMEOUT_TOLERANCE_MS;
44
+ exports.isInstanceNotFound = isInstanceNotFound;
26
45
  exports.wakeExpired = wakeExpired;
27
46
 
28
47
  //# sourceMappingURL=bindings.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"bindings.cjs","names":[],"sources":["../../src/cloudflare/bindings.ts"],"sourcesContent":["import type { EmailAddress } from '../email';\n\n/**\n * A minimal structural interface over the Cloudflare bindings.\n *\n * @cloudflare/workers-types is deliberately not used: the global types it\n * injects clash with the DOM lib this package's react side needs. A structural\n * subset plus the ambient cloudflare:workers declaration is enough to compile,\n * and the real bindings satisfy it structurally at runtime.\n */\n\nexport interface KVNamespaceLike {\n get(key: string): Promise<string | null>;\n put(key: string, value: string): Promise<void>;\n}\n\n/** Structural subset of D1Result: `meta.changes` is how INSERT OR IGNORE reports whether it inserted. */\nexport interface D1RunResultLike {\n meta?: { changes?: number };\n}\n\nexport interface D1PreparedStatementLike {\n bind(...values: unknown[]): D1PreparedStatementLike;\n first<T = Record<string, unknown>>(): Promise<T | null>;\n all<T = Record<string, unknown>>(): Promise<{ results: T[] }>;\n run(): Promise<D1RunResultLike>;\n}\n\nexport interface D1DatabaseLike {\n prepare(sql: string): D1PreparedStatementLike;\n /** Statements run in order inside one transaction (real D1 semantics) — deploy relies on the atomicity. */\n batch(statements: D1PreparedStatementLike[]): Promise<unknown>;\n}\n\nexport interface WorkflowInstanceLike {\n id: string;\n sendEvent(event: { type: string; payload?: unknown }): Promise<void>;\n /** Stop the instance for good (Cloudflare Workflows' `terminate`). */\n terminate?(): Promise<void>;\n /** Where it stands (`complete`, `errored`, `terminated`, `running`, …). */\n status?(): Promise<{ status: string }>;\n}\n\nexport interface WorkflowBindingLike {\n create(options: { id: string; params: unknown }): Promise<WorkflowInstanceLike>;\n get(id: string): Promise<WorkflowInstanceLike>;\n}\n\n/**\n * Cloudflare Email Service's send_email binding (structural subset).\n *\n * `idempotencyKey` is part of the call because the send happens inside a\n * `step.do`: a step body re-runs after its fn resolved but before the\n * checkpoint committed, so a binding that drops the key mails the user twice.\n * The binding is app-supplied glue (nothing here can de-duplicate — there is\n * no state between two runs of the same step), which is why the port makes the\n * key impossible to miss rather than merely available.\n */\nexport interface EmailBindingLike {\n send(message: {\n from: EmailAddress;\n to: EmailAddress;\n replyTo?: EmailAddress;\n subject: string;\n html: string;\n /** `${instanceId}:${nodeId}`: stable across replays and retries — drop a send whose key was already delivered. */\n idempotencyKey: string;\n }): Promise<unknown>;\n}\n\n/** Every binding the journey engine needs (names match the wrangler config). */\nexport interface JourneyEnv {\n /**\n * BundleIR storage for the default D1 store (`wf:${contentHash}` → WorkflowIR JSON). Optional:\n * a Worker on another JourneyStore (Postgres, say) does not declare it.\n */\n WORKFLOW_KV?: KVNamespaceLike;\n /** Tables of the default D1 store (events / profiles / segments / triggers / entries / subscriptions). Optional, as above. */\n DB?: D1DatabaseLike;\n /** JourneyRunner's workflow binding (creating instances and waking them). */\n JOURNEY: WorkflowBindingLike;\n /** Optional message-delivery webhook (defaults to the console logging sender). */\n MESSAGE_WEBHOOK_URL?: string;\n /**\n * Bearer token required on every mutating HTTP endpoint (`Authorization:\n * Bearer <token>`). Unset = open — acceptable only for local dev; /deploy in\n * particular rewrites the whole routing table, so production must set this.\n */\n API_TOKEN?: string;\n /**\n * This Worker's public origin. With a store that keeps reporting (Postgres), journey emails'\n * links and open pixel point at its `/t/...` routes; without it emails go out untracked.\n */\n WORKFLOW_ORIGIN?: string;\n /** Which `workflow.config.ts` entry applies (`development`, `production`, …); production when unset. */\n ENVIRONMENT?: string;\n /** Cloudflare Email Sending, when the config has `emails`. */\n EMAIL?: EmailBindingLike;\n /** The APNs auth key (`.p8`, base64) named by the config's `apns.keyId`. */\n APNS_PRIVATE_KEY_BASE64?: string;\n /** The Firebase service account (base64 JSON) the config's `fcm` sends with. */\n GOOGLE_APPLICATION_CREDENTIALS_BASE64?: string;\n /** The VAPID key pair web pushes are signed with (`generateVapidKeys`); the config's `webPush` turns the channel on. */\n VAPID_PUBLIC_KEY?: string;\n VAPID_PRIVATE_KEY?: string;\n /**\n * Cloudflare's version metadata binding (wrangler `\"version_metadata\": { \"binding\":\n * \"CF_VERSION_METADATA\" }`). With it, and a store that records the live version (Postgres),\n * journeyWorker makes the bundle compiled into each new version live by itself: `wrangler\n * deploy` is the whole deploy.\n */\n CF_VERSION_METADATA?: { id: string; tag?: string; timestamp: string };\n /** Development only: the HTTP/2 relay `wrangler dev` sends APNs requests through (see `@shware/workflow/apns-relay`). */\n APNS_RELAY_ORIGIN?: string;\n}\n\n/** The event type the router uses to wake waiting instances (sendEvent's `type`). */\nexport const WAKE_EVENT_TYPE = 'wake';\n\n/** Journey instance parameters, passed by the ingest router when it creates one. */\nexport interface JourneyParams {\n workflowId: string;\n /** The IR version pinned at entry. */\n contentHash: string;\n userId: string;\n trigger: { event: string; payload: Record<string, unknown> };\n [key: string]: unknown;\n}\n\n/**\n * Slack on the deadline when deciding whether a thrown wait was a timeout: the\n * platform may surface the expiry a moment early, and a wait shorter than this\n * has no room to tell the two apart anyway.\n */\nexport const WAKE_TIMEOUT_TOLERANCE_MS = 5_000;\n\n/**\n * Did a wait that threw actually reach its deadline?\n *\n * CF signals a wait timeout by throwing, and so does every other failure in\n * there — a broken binding, an evicted instance, a malformed timeout. The\n * adapter used to read them all as timeouts, which made an infrastructure\n * fault indistinguishable from \"the user never did it\": the journey took its\n * onTimeout branch, early and silently. Only the clock can tell them apart.\n */\nexport function wakeExpired(startedAt: number, timeoutMs: number, now: number): boolean {\n return now - startedAt >= Math.max(0, timeoutMs - WAKE_TIMEOUT_TOLERANCE_MS);\n}\n"],"mappings":";;;AAqHA,MAAa,kBAAkB;;;;;;AAiB/B,MAAa,4BAA4B;;;;;;;;;;AAWzC,SAAgB,YAAY,WAAmB,WAAmB,KAAsB;CACtF,OAAO,MAAM,aAAa,KAAK,IAAI,GAAG,YAAY,yBAAyB;AAC7E"}
1
+ {"version":3,"file":"bindings.cjs","names":[],"sources":["../../src/cloudflare/bindings.ts"],"sourcesContent":["import type { EmailAddress } from '../email';\nimport type { ExecutionStart } from '../store/index';\n\n/**\n * A minimal structural interface over the Cloudflare bindings.\n *\n * @cloudflare/workers-types is deliberately not used: the global types it\n * injects clash with the DOM lib this package's react side needs. A structural\n * subset plus the ambient cloudflare:workers declaration is enough to compile,\n * and the real bindings satisfy it structurally at runtime.\n */\n\nexport interface KVNamespaceLike {\n get(key: string): Promise<string | null>;\n put(key: string, value: string): Promise<void>;\n}\n\n/** Structural subset of D1Result: `meta.changes` is how INSERT OR IGNORE reports whether it inserted. */\nexport interface D1RunResultLike {\n meta?: { changes?: number };\n}\n\nexport interface D1PreparedStatementLike {\n bind(...values: unknown[]): D1PreparedStatementLike;\n first<T = Record<string, unknown>>(): Promise<T | null>;\n all<T = Record<string, unknown>>(): Promise<{ results: T[] }>;\n run(): Promise<D1RunResultLike>;\n}\n\nexport interface D1DatabaseLike {\n prepare(sql: string): D1PreparedStatementLike;\n /** Statements run in order inside one transaction (real D1 semantics) — deploy relies on the atomicity. */\n batch(statements: D1PreparedStatementLike[]): Promise<unknown>;\n}\n\nexport interface WorkflowInstanceLike {\n id: string;\n sendEvent(event: { type: string; payload?: unknown }): Promise<void>;\n /** Stop the instance for good (Cloudflare Workflows' `terminate`). */\n terminate?(): Promise<void>;\n /** Where it stands (`complete`, `errored`, `terminated`, `running`, …). */\n status?(): Promise<{ status: string }>;\n}\n\n/**\n * How the router starts, wakes and stops instances: Cloudflare's workflow binding, or the AWS\n * adapter's equivalent over durable executions.\n */\nexport interface WorkflowBindingLike {\n create(options: { id: string; params: unknown }): Promise<WorkflowInstanceLike>;\n get(id: string): Promise<WorkflowInstanceLike>;\n /**\n * For a runtime whose start is a submit that can be lost in flight (AWS): the start to record\n * with the entry, in one transaction. A failed `create` then keeps the entry — its recorded\n * start is retried by the next ingest for the pair (`resume`) or by maintenance, never rolled\n * back and re-entered as a second execution.\n */\n plan?(options: { id: string; params: JourneyParams; ts: number }): Promise<ExecutionStart>;\n /** Retry the recorded, unconfirmed start of the (workflow, user) entry still running, if any. */\n resume?(entry: { workflow: string; userId: string }): Promise<void>;\n /**\n * Maintenance the sweep runs first: old unconfirmed starts retried (or given up on), and\n * executions the platform ended on its own (timeout, stop, crash) reconciled into the ledger.\n */\n maintain?(): Promise<void>;\n}\n\n/**\n * Cloudflare Email Service's send_email binding (structural subset).\n *\n * `idempotencyKey` is part of the call because the send happens inside a\n * `step.do`: a step body re-runs after its fn resolved but before the\n * checkpoint committed, so a binding that drops the key mails the user twice.\n * The binding is app-supplied glue (nothing here can de-duplicate — there is\n * no state between two runs of the same step), which is why the port makes the\n * key impossible to miss rather than merely available.\n */\nexport interface EmailBindingLike {\n send(message: {\n from: EmailAddress;\n to: EmailAddress;\n replyTo?: EmailAddress;\n subject: string;\n html: string;\n /** `${instanceId}:${nodeId}`: stable across replays and retries — drop a send whose key was already delivered. */\n idempotencyKey: string;\n }): Promise<unknown>;\n}\n\n/** Every binding the journey engine needs (names match the wrangler config). */\nexport interface JourneyEnv {\n /**\n * BundleIR storage for the default D1 store (`wf:${contentHash}` → WorkflowIR JSON). Optional:\n * a Worker on another JourneyStore (Postgres, say) does not declare it.\n */\n WORKFLOW_KV?: KVNamespaceLike;\n /** Tables of the default D1 store (events / profiles / segments / triggers / entries / subscriptions). Optional, as above. */\n DB?: D1DatabaseLike;\n /** JourneyRunner's workflow binding (creating instances and waking them). */\n JOURNEY: WorkflowBindingLike;\n /** Optional message-delivery webhook (defaults to the console logging sender). */\n MESSAGE_WEBHOOK_URL?: string;\n /**\n * Bearer token required on every mutating HTTP endpoint (`Authorization:\n * Bearer <token>`). Unset = open — acceptable only for local dev; /deploy in\n * particular rewrites the whole routing table, so production must set this.\n */\n API_TOKEN?: string;\n /**\n * This Worker's public origin. With a store that keeps reporting (Postgres), journey emails'\n * links and open pixel point at its `/t/...` routes; without it emails go out untracked.\n */\n WORKFLOW_ORIGIN?: string;\n /** Which `workflow.config.ts` entry applies (`development`, `production`, …); production when unset. */\n ENVIRONMENT?: string;\n /** Cloudflare Email Sending, when the config has `emails`. */\n EMAIL?: EmailBindingLike;\n /** The APNs auth key (`.p8`, base64) named by the config's `apns.keyId`. */\n APNS_PRIVATE_KEY_BASE64?: string;\n /** The Firebase service account (base64 JSON) the config's `fcm` sends with. */\n GOOGLE_APPLICATION_CREDENTIALS_BASE64?: string;\n /** The VAPID key pair web pushes are signed with (`generateVapidKeys`); the config's `webPush` turns the channel on. */\n VAPID_PUBLIC_KEY?: string;\n VAPID_PRIVATE_KEY?: string;\n /**\n * Cloudflare's version metadata binding (wrangler `\"version_metadata\": { \"binding\":\n * \"CF_VERSION_METADATA\" }`). With it, and a store that records the live version (Postgres),\n * journeyWorker makes the bundle compiled into each new version live by itself: `wrangler\n * deploy` is the whole deploy.\n */\n CF_VERSION_METADATA?: { id: string; tag?: string; timestamp: string };\n /** Development only: the HTTP/2 relay `wrangler dev` sends APNs requests through (see `@shware/workflow/apns-relay`). */\n APNS_RELAY_ORIGIN?: string;\n}\n\n/** The runtime does not know the instance: the only failure that lets the router drop its wakes. */\nexport class InstanceNotFoundError extends Error {\n override readonly name = 'InstanceNotFoundError';\n constructor(readonly instanceId: string) {\n super(`no such instance: ${instanceId}`);\n }\n}\n\n/**\n * Does this lookup failure say the instance is gone, rather than that the lookup failed?\n * Cloudflare reports an unknown id as `instance.not_found`.\n */\nexport function isInstanceNotFound(error: unknown): boolean {\n if (error instanceof InstanceNotFoundError) return true;\n return /not[_ ]found|no such instance/i.test(String(error));\n}\n\n/** The event type the router uses to wake waiting instances (sendEvent's `type`). */\nexport const WAKE_EVENT_TYPE = 'wake';\n\n/** Journey instance parameters, passed by the ingest router when it creates one. */\nexport interface JourneyParams {\n workflowId: string;\n /** The IR version pinned at entry. */\n contentHash: string;\n userId: string;\n trigger: { event: string; payload: Record<string, unknown> };\n [key: string]: unknown;\n}\n\n/**\n * Slack on the deadline when deciding whether a thrown wait was a timeout: the\n * platform may surface the expiry a moment early, and a wait shorter than this\n * has no room to tell the two apart anyway.\n */\nexport const WAKE_TIMEOUT_TOLERANCE_MS = 5_000;\n\n/**\n * Did a wait that threw actually reach its deadline?\n *\n * CF signals a wait timeout by throwing, and so does every other failure in\n * there — a broken binding, an evicted instance, a malformed timeout. The\n * adapter used to read them all as timeouts, which made an infrastructure\n * fault indistinguishable from \"the user never did it\": the journey took its\n * onTimeout branch, early and silently. Only the clock can tell them apart.\n */\nexport function wakeExpired(startedAt: number, timeoutMs: number, now: number): boolean {\n return now - startedAt >= Math.max(0, timeoutMs - WAKE_TIMEOUT_TOLERANCE_MS);\n}\n"],"mappings":";;;AAwIA,IAAa,wBAAb,cAA2C,MAAM;CAE1B;CADrB,OAAyB;CACzB,YAAY,YAA6B;EACvC,MAAM,qBAAqB,YAAY;EADpB,KAAA,aAAA;CAErB;AACF;;;;;AAMA,SAAgB,mBAAmB,OAAyB;CAC1D,IAAI,iBAAiB,uBAAuB,OAAO;CACnD,OAAO,iCAAiC,KAAK,OAAO,KAAK,CAAC;AAC5D;;AAGA,MAAa,kBAAkB;;;;;;AAiB/B,MAAa,4BAA4B;;;;;;;;;;AAWzC,SAAgB,YAAY,WAAmB,WAAmB,KAAsB;CACtF,OAAO,MAAM,aAAa,KAAK,IAAI,GAAG,YAAY,yBAAyB;AAC7E"}