@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
package/README.md CHANGED
@@ -169,15 +169,18 @@ export default defineConfig({
169
169
  // src/index.ts
170
170
  import { type JourneyEnv, journeyWorker } from '@shware/workflow/cloudflare';
171
171
  import { postgresJsStore } from '@shware/workflow/postgres-js';
172
+ import { cloudflareEmailTransport } from '@shware/workflow/transports/cloudflare-email';
172
173
  import config from '../workflow.config';
173
174
 
174
175
  interface Env extends JourneyEnv {
175
176
  HYPERDRIVE: { connectionString: string };
177
+ EMAIL: NonNullable<JourneyEnv['EMAIL']>;
176
178
  }
177
179
 
178
180
  const journeys = journeyWorker<Env>({
179
181
  config,
180
182
  store: (env, database) => postgresJsStore(env.HYPERDRIVE.connectionString, database),
183
+ email: (env) => cloudflareEmailTransport({ binding: env.EMAIL }),
181
184
  });
182
185
 
183
186
  /** The wrangler `workflows` binding's class_name. */
@@ -464,7 +467,7 @@ the compile fails.
464
467
  `emailTemplate({ id, from, replyTo?, unsubscribeUrl?, title, description?, props, subject, preview, component })`
465
468
  declares one. The Worker renders it with `@shware/workflow/react-email` (React's
466
469
  streaming renderer, which waits for react-email's `<Tailwind>`) and sends it
467
- through the `EMAIL` send_email binding, from the template's own `from`:
470
+ through the host's email transport, from the template's own `from`:
468
471
 
469
472
  ```ts
470
473
  // src/email/welcome.tsx
@@ -503,6 +506,23 @@ email: {
503
506
  },
504
507
  ```
505
508
 
509
+ The transport is the host's `email` option, chosen independently of where the
510
+ workflow runs. Rendering, tracking and the tracked-link commit happen before it,
511
+ the same for every provider (`EmailSender`):
512
+
513
+ | Transport | Import |
514
+ | -------------------------------------------------------------------------- | ---------------------------------------------- |
515
+ | `cloudflareEmailTransport({ binding })` — a Worker's `send_email` | `@shware/workflow/transports/cloudflare-email` |
516
+ | `cloudflareEmailTransport({ accountId, apiToken })` — REST, anywhere | `@shware/workflow/transports/cloudflare-email` |
517
+ | `sesEmailTransport({ region, credentials? })` — SES v2, SigV4 over `fetch` | `@shware/workflow/transports/ses` |
518
+
519
+ `sesEmailTransport` runs in a Worker and in Node alike. Without `credentials` it
520
+ reads `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` at send
521
+ time (a Lambda's execution role); a Worker passes them, or a provider, from its
522
+ env. Delivery is at least once: SES has no de-duplication key, so an email the
523
+ provider accepted just before the step's checkpoint failed can go out twice.
524
+ Without an `email` option the email channel fails its sends, saying so.
525
+
506
526
  An email step refuses to send when the profile has no `email`.
507
527
  `GET /preview/<id>?props=<JSON>` renders a template, with the subject in the
508
528
  `x-subject` header.
@@ -705,7 +725,7 @@ The default store needs three bindings, named as in `JourneyEnv`:
705
725
  | `JOURNEY` | Workflow | The `JourneyRunner` entrypoint |
706
726
 
707
727
  A Worker on Postgres needs only `JOURNEY`. Add the `EMAIL` send_email binding
708
- when the config registers email templates, and a `triggers.crons` entry with `scheduled`
728
+ when it sends through Cloudflare Email (`cloudflareEmailTransport({ binding: env.EMAIL })`), and a `triggers.crons` entry with `scheduled`
709
729
  exported to run the segment sweep.
710
730
 
711
731
  Set `API_TOKEN` in production. Without it every mutating endpoint is open, and
@@ -726,6 +746,90 @@ There are no migrations, and re-applying the file is not one: every statement is
726
746
  the first insert needing a new column fails. Recreate it instead. The demo has
727
747
  `pnpm db:reset`.
728
748
 
749
+ ## AWS Lambda
750
+
751
+ The same config runs as one Lambda durable function behind a Function URL, on
752
+ Postgres. `@aws/durable-execution-sdk-js` and `@aws-sdk/client-lambda` are optional
753
+ peer dependencies, needed only by `@shware/workflow/aws`. The durable SDK's own type
754
+ declarations reference `aws-lambda`, so a TypeScript project that type-checks
755
+ dependencies (no `skipLibCheck`) also needs `@types/aws-lambda`.
756
+
757
+ ```ts
758
+ // src/aws.ts
759
+ import { journeyLambda } from '@shware/workflow/aws';
760
+ import { postgresJsStore } from '@shware/workflow/postgres-js';
761
+ import { sesEmailTransport } from '@shware/workflow/transports/ses';
762
+ import config from '../workflow.config';
763
+ import { env } from './env.aws'; // process.env, parsed with zod
764
+
765
+ const journeys = journeyLambda({
766
+ config,
767
+ env,
768
+ store: (env, database) => postgresJsStore(env.DATABASE_URL, database),
769
+ email: (env) => sesEmailTransport({ region: env.AWS_REGION }),
770
+ });
771
+
772
+ export const handler = journeys.handler;
773
+ ```
774
+
775
+ One handler serves three inputs, told apart by the event's top-level shape, so
776
+ no HTTP body can pass for the others:
777
+
778
+ - **A Function URL request**: the HTTP API, as on Cloudflare. A journey it
779
+ starts is its own durable execution, submitted with an asynchronous `Invoke`
780
+ of this function's numbered version under a stable execution name, and the
781
+ request returns.
782
+ - **A run** (`$workflow.run`): one instance. Steps, delays and waits map onto
783
+ the durable SDK's `step`, `wait` and callbacks; a wait parks on a fresh
784
+ callback, recorded in Postgres, which the next relevant event completes.
785
+ - **A sweep** (`{ "type": "$workflow.sweep" }`, from EventBridge Scheduler, or
786
+ `POST /segments/sweep` from an existing cron): the segment sweep, plus
787
+ retrying starts whose submit was never confirmed and reconciling executions
788
+ the platform ended (timeout, stop) into the ledger. Without one, clock-only
789
+ segments and this upkeep do not happen.
790
+
791
+ A start is recorded with its entry in one transaction (the `execution` table),
792
+ so a lost `Invoke` response is retried with the same name and payload and never
793
+ re-entered as a second execution. Retries stop after 12 hours (`startRetryMs`,
794
+ keep it under the execution retention) and the start is left unresolved. A
795
+ failed entry that is entered again gets a new execution, never the closed one.
796
+
797
+ Erasing a user (`POST /users/forget`) stops their executions, then waits
798
+ `stopDrainMs` (15 minutes by default; at least the function's timeout) before
799
+ erasing: a stop does not interrupt an invocation already running, which goes on
800
+ to its next checkpoint. Until then the erasure answers `erased: false` and the
801
+ sweep finishes it.
802
+
803
+ Deploy one artifact per version: publish a numbered version and point the
804
+ `live` alias, which the Function URL is bound to, at it. Each version carries its
805
+ compiled bundle and makes it live by itself, only over an older one. Each route
806
+ records the version that applied it, and an instance it starts runs on that
807
+ version, so its IR and its code always match, even while an older version is
808
+ still serving; executions stay on the version that started them. `POST /deploy` of an arbitrary bundle is
809
+ refused.
810
+
811
+ ```yaml
812
+ Workflow:
813
+ Type: AWS::Serverless::Function
814
+ Properties:
815
+ Runtime: nodejs24.x
816
+ Handler: aws.handler
817
+ Timeout: 60
818
+ AutoPublishAlias: live
819
+ # ExecutionTimeout: the longest journey (waits, delays and the goal window), up to 366 days
820
+ DurableConfig: { ExecutionTimeout: 2592000, RetentionPeriodInDays: 30 }
821
+ FunctionUrlConfig: { AuthType: NONE, InvokeMode: BUFFERED }
822
+ ```
823
+
824
+ The execution role needs the durable checkpoint permissions,
825
+ `lambda:InvokeFunction` on its own versions, `lambda:SendDurableExecutionCallbackSuccess`,
826
+ `lambda:GetDurableExecution`, `lambda:ListDurableExecutionsByFunction` and
827
+ `lambda:StopDurableExecution`, plus `ses:SendEmail` when it sends through SES.
828
+ With `AuthType: NONE`, scope the public `lambda:InvokeFunction` grant to
829
+ `lambda:InvokedViaFunctionUrl = true` so the URL cannot be used to invoke the
830
+ function directly. APNs goes over Node's HTTP/2 client; a custom domain needs
831
+ CloudFront (or another HTTPS proxy) in front of the Function URL.
832
+
729
833
  ## Postgres
730
834
 
731
835
  A host on Postgres (through Hyperdrive, say) keeps Cloudflare Workflows for
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_http2_client = require("./http2-client.cjs");
2
3
  let node_http = require("node:http");
3
- let node_http2 = require("node:http2");
4
4
  //#region src/apns-relay.ts
5
5
  const DEFAULT_ALLOW = ["https://api.push.apple.com", "https://api.sandbox.push.apple.com"];
6
6
  /** Request headers APNs reads; everything else (host, content-length, …) is the relay's own business. */
@@ -16,19 +16,7 @@ const FORWARDED = [
16
16
  ];
17
17
  function startApnsRelay(options) {
18
18
  const allow = new Set(options.allow ?? DEFAULT_ALLOW);
19
- const sessions = /* @__PURE__ */ new Map();
20
- function session(upstream) {
21
- const existing = sessions.get(upstream);
22
- if (existing !== void 0 && !existing.closed && !existing.destroyed) return existing;
23
- const created = (0, node_http2.connect)(upstream);
24
- const forget = () => {
25
- if (sessions.get(upstream) === created) sessions.delete(upstream);
26
- };
27
- created.on("error", forget);
28
- created.on("close", forget);
29
- sessions.set(upstream, created);
30
- return created;
31
- }
19
+ const sessions = require_http2_client.http2Sessions();
32
20
  function relay(req, res) {
33
21
  const upstreamHeader = req.headers["apns-relay-upstream"];
34
22
  const upstream = Array.isArray(upstreamHeader) ? upstreamHeader[0] : upstreamHeader;
@@ -53,7 +41,7 @@ function startApnsRelay(options) {
53
41
  }
54
42
  let stream;
55
43
  try {
56
- stream = session(upstream).request(headers);
44
+ stream = sessions.session(upstream).request(headers);
57
45
  } catch (error) {
58
46
  res.writeHead(502, { "content-type": "application/json" });
59
47
  res.end(JSON.stringify({
@@ -91,8 +79,7 @@ function startApnsRelay(options) {
91
79
  resolve({
92
80
  port: typeof address === "object" && address !== null ? address.port : options.port,
93
81
  close: () => new Promise((done) => {
94
- for (const open of sessions.values()) open.close();
95
- sessions.clear();
82
+ sessions.close();
96
83
  server.close(() => done());
97
84
  })
98
85
  });
@@ -1 +1 @@
1
- {"version":3,"file":"apns-relay.cjs","names":["connect","createServer"],"sources":["../src/apns-relay.ts"],"sourcesContent":["import { type IncomingMessage, type ServerResponse, createServer } from 'node:http';\nimport { type ClientHttp2Session, connect } from 'node:http2';\n\n/**\n * `@shware/workflow/apns-relay` — a development-only HTTP/1.1 → HTTP/2 relay\n * for APNs, run under Node next to `wrangler dev`.\n *\n * APNs accepts HTTP/2 only, and workerd (the local Workers runtime) cannot\n * make an HTTP/2 subrequest, so a journey that pushes to an iPhone from a\n * local Worker has nowhere to go. A deployed Worker needs none of this: its\n * `fetch` negotiates HTTP/2 with Apple. The relay forwards each request to the\n * origin named in its `apns-relay-upstream` header (the sender sets it along\n * with `origin`), keeping one HTTP/2 session per upstream.\n *\n * Only Apple's two hosts are relayed by default; `allow` widens that for tests.\n */\n\nexport interface ApnsRelayOptions {\n port: number;\n /** Interface to listen on; loopback by default — the relay has no auth of its own. */\n hostname?: string;\n /** Upstream origins that may be relayed; Apple's production and sandbox hosts by default. */\n allow?: readonly string[];\n}\n\nexport interface ApnsRelay {\n port: number;\n close(): Promise<void>;\n}\n\nconst DEFAULT_ALLOW = ['https://api.push.apple.com', 'https://api.sandbox.push.apple.com'];\n\n/** Request headers APNs reads; everything else (host, content-length, …) is the relay's own business. */\nconst FORWARDED = [\n 'authorization',\n 'apns-topic',\n 'apns-push-type',\n 'apns-priority',\n 'apns-expiration',\n 'apns-collapse-id',\n 'apns-id',\n 'content-type',\n];\n\nexport function startApnsRelay(options: ApnsRelayOptions): Promise<ApnsRelay> {\n const allow = new Set(options.allow ?? DEFAULT_ALLOW);\n const sessions = new Map<string, ClientHttp2Session>();\n\n function session(upstream: string): ClientHttp2Session {\n const existing = sessions.get(upstream);\n if (existing !== undefined && !existing.closed && !existing.destroyed) return existing;\n const created = connect(upstream);\n const forget = () => {\n if (sessions.get(upstream) === created) sessions.delete(upstream);\n };\n created.on('error', forget);\n created.on('close', forget);\n sessions.set(upstream, created);\n return created;\n }\n\n function relay(req: IncomingMessage, res: ServerResponse): void {\n const upstreamHeader = req.headers['apns-relay-upstream'];\n const upstream = Array.isArray(upstreamHeader) ? upstreamHeader[0] : upstreamHeader;\n if (upstream === undefined || !allow.has(upstream)) {\n res.writeHead(400, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayUpstreamNotAllowed', upstream: upstream ?? null }));\n return;\n }\n\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => {\n const headers: Record<string, string> = {\n ':method': req.method ?? 'POST',\n ':path': req.url ?? '/',\n };\n for (const name of FORWARDED) {\n const value = req.headers[name];\n if (typeof value === 'string') headers[name] = value;\n }\n let stream;\n try {\n stream = session(upstream).request(headers);\n } catch (error) {\n res.writeHead(502, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayConnectFailed', error: String(error) }));\n return;\n }\n const body: Buffer[] = [];\n stream.on('response', (responseHeaders) => {\n const status = responseHeaders[':status'] ?? 502;\n const apnsId = responseHeaders['apns-id'];\n res.writeHead(status, {\n 'content-type': 'application/json',\n ...(typeof apnsId === 'string' ? { 'apns-id': apnsId } : {}),\n });\n });\n stream.on('data', (chunk: Buffer) => body.push(chunk));\n stream.on('end', () => res.end(Buffer.concat(body)));\n stream.on('error', (error) => {\n if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayUpstreamError', error: String(error) }));\n });\n stream.end(Buffer.concat(chunks));\n });\n }\n\n const server = createServer(relay);\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(options.port, options.hostname ?? '127.0.0.1', () => {\n const address = server.address();\n const port = typeof address === 'object' && address !== null ? address.port : options.port;\n resolve({\n port,\n close: () =>\n new Promise((done) => {\n for (const open of sessions.values()) open.close();\n sessions.clear();\n server.close(() => done());\n }),\n });\n });\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"apns-relay.cjs","names":["http2Sessions","createServer"],"sources":["../src/apns-relay.ts"],"sourcesContent":["import { type IncomingMessage, type ServerResponse, createServer } from 'node:http';\nimport { http2Sessions } from './http2-client';\n\n/**\n * `@shware/workflow/apns-relay` — a development-only HTTP/1.1 → HTTP/2 relay\n * for APNs, run under Node next to `wrangler dev`.\n *\n * APNs accepts HTTP/2 only, and workerd (the local Workers runtime) cannot\n * make an HTTP/2 subrequest, so a journey that pushes to an iPhone from a\n * local Worker has nowhere to go. A deployed Worker needs none of this: its\n * `fetch` negotiates HTTP/2 with Apple. The relay forwards each request to the\n * origin named in its `apns-relay-upstream` header (the sender sets it along\n * with `origin`), keeping one HTTP/2 session per upstream.\n *\n * Only Apple's two hosts are relayed by default; `allow` widens that for tests.\n */\n\nexport interface ApnsRelayOptions {\n port: number;\n /** Interface to listen on; loopback by default — the relay has no auth of its own. */\n hostname?: string;\n /** Upstream origins that may be relayed; Apple's production and sandbox hosts by default. */\n allow?: readonly string[];\n}\n\nexport interface ApnsRelay {\n port: number;\n close(): Promise<void>;\n}\n\nconst DEFAULT_ALLOW = ['https://api.push.apple.com', 'https://api.sandbox.push.apple.com'];\n\n/** Request headers APNs reads; everything else (host, content-length, …) is the relay's own business. */\nconst FORWARDED = [\n 'authorization',\n 'apns-topic',\n 'apns-push-type',\n 'apns-priority',\n 'apns-expiration',\n 'apns-collapse-id',\n 'apns-id',\n 'content-type',\n];\n\nexport function startApnsRelay(options: ApnsRelayOptions): Promise<ApnsRelay> {\n const allow = new Set(options.allow ?? DEFAULT_ALLOW);\n const sessions = http2Sessions();\n\n function relay(req: IncomingMessage, res: ServerResponse): void {\n const upstreamHeader = req.headers['apns-relay-upstream'];\n const upstream = Array.isArray(upstreamHeader) ? upstreamHeader[0] : upstreamHeader;\n if (upstream === undefined || !allow.has(upstream)) {\n res.writeHead(400, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayUpstreamNotAllowed', upstream: upstream ?? null }));\n return;\n }\n\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => {\n const headers: Record<string, string> = {\n ':method': req.method ?? 'POST',\n ':path': req.url ?? '/',\n };\n for (const name of FORWARDED) {\n const value = req.headers[name];\n if (typeof value === 'string') headers[name] = value;\n }\n let stream;\n try {\n stream = sessions.session(upstream).request(headers);\n } catch (error) {\n res.writeHead(502, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayConnectFailed', error: String(error) }));\n return;\n }\n const body: Buffer[] = [];\n stream.on('response', (responseHeaders) => {\n const status = responseHeaders[':status'] ?? 502;\n const apnsId = responseHeaders['apns-id'];\n res.writeHead(status, {\n 'content-type': 'application/json',\n ...(typeof apnsId === 'string' ? { 'apns-id': apnsId } : {}),\n });\n });\n stream.on('data', (chunk: Buffer) => body.push(chunk));\n stream.on('end', () => res.end(Buffer.concat(body)));\n stream.on('error', (error) => {\n if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayUpstreamError', error: String(error) }));\n });\n stream.end(Buffer.concat(chunks));\n });\n }\n\n const server = createServer(relay);\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(options.port, options.hostname ?? '127.0.0.1', () => {\n const address = server.address();\n const port = typeof address === 'object' && address !== null ? address.port : options.port;\n resolve({\n port,\n close: () =>\n new Promise((done) => {\n sessions.close();\n server.close(() => done());\n }),\n });\n });\n });\n}\n"],"mappings":";;;;AA8BA,MAAM,gBAAgB,CAAC,8BAA8B,oCAAoC;;AAGzF,MAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,SAAgB,eAAe,SAA+C;CAC5E,MAAM,QAAQ,IAAI,IAAI,QAAQ,SAAS,aAAa;CACpD,MAAM,WAAWA,qBAAAA,cAAc;CAE/B,SAAS,MAAM,KAAsB,KAA2B;EAC9D,MAAM,iBAAiB,IAAI,QAAQ;EACnC,MAAM,WAAW,MAAM,QAAQ,cAAc,IAAI,eAAe,KAAK;EACrE,IAAI,aAAa,KAAA,KAAa,CAAC,MAAM,IAAI,QAAQ,GAAG;GAClD,IAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;GACzD,IAAI,IAAI,KAAK,UAAU;IAAE,QAAQ;IAA2B,UAAU,YAAY;GAAK,CAAC,CAAC;GACzF;EACF;EAEA,MAAM,SAAmB,CAAC;EAC1B,IAAI,GAAG,SAAS,UAAkB,OAAO,KAAK,KAAK,CAAC;EACpD,IAAI,GAAG,aAAa;GAClB,MAAM,UAAkC;IACtC,WAAW,IAAI,UAAU;IACzB,SAAS,IAAI,OAAO;GACtB;GACA,KAAK,MAAM,QAAQ,WAAW;IAC5B,MAAM,QAAQ,IAAI,QAAQ;IAC1B,IAAI,OAAO,UAAU,UAAU,QAAQ,QAAQ;GACjD;GACA,IAAI;GACJ,IAAI;IACF,SAAS,SAAS,QAAQ,QAAQ,CAAC,CAAC,QAAQ,OAAO;GACrD,SAAS,OAAO;IACd,IAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;IACzD,IAAI,IAAI,KAAK,UAAU;KAAE,QAAQ;KAAsB,OAAO,OAAO,KAAK;IAAE,CAAC,CAAC;IAC9E;GACF;GACA,MAAM,OAAiB,CAAC;GACxB,OAAO,GAAG,aAAa,oBAAoB;IACzC,MAAM,SAAS,gBAAgB,cAAc;IAC7C,MAAM,SAAS,gBAAgB;IAC/B,IAAI,UAAU,QAAQ;KACpB,gBAAgB;KAChB,GAAI,OAAO,WAAW,WAAW,EAAE,WAAW,OAAO,IAAI,CAAC;IAC5D,CAAC;GACH,CAAC;GACD,OAAO,GAAG,SAAS,UAAkB,KAAK,KAAK,KAAK,CAAC;GACrD,OAAO,GAAG,aAAa,IAAI,IAAI,OAAO,OAAO,IAAI,CAAC,CAAC;GACnD,OAAO,GAAG,UAAU,UAAU;IAC5B,IAAI,CAAC,IAAI,aAAa,IAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;IAC/E,IAAI,IAAI,KAAK,UAAU;KAAE,QAAQ;KAAsB,OAAO,OAAO,KAAK;IAAE,CAAC,CAAC;GAChF,CAAC;GACD,OAAO,IAAI,OAAO,OAAO,MAAM,CAAC;EAClC,CAAC;CACH;CAEA,MAAM,UAAA,GAASC,UAAAA,aAAAA,CAAa,KAAK;CACjC,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,OAAO,KAAK,SAAS,MAAM;EAC3B,OAAO,OAAO,QAAQ,MAAM,QAAQ,YAAY,mBAAmB;GACjE,MAAM,UAAU,OAAO,QAAQ;GAE/B,QAAQ;IACN,MAFW,OAAO,YAAY,YAAY,YAAY,OAAO,QAAQ,OAAO,QAAQ;IAGpF,aACE,IAAI,SAAS,SAAS;KACpB,SAAS,MAAM;KACf,OAAO,YAAY,KAAK,CAAC;IAC3B,CAAC;GACL,CAAC;EACH,CAAC;CACH,CAAC;AACH"}
@@ -1,5 +1,5 @@
1
+ import { http2Sessions } from "./http2-client.mjs";
1
2
  import { createServer } from "node:http";
2
- import { connect } from "node:http2";
3
3
  //#region src/apns-relay.ts
4
4
  const DEFAULT_ALLOW = ["https://api.push.apple.com", "https://api.sandbox.push.apple.com"];
5
5
  /** Request headers APNs reads; everything else (host, content-length, …) is the relay's own business. */
@@ -15,19 +15,7 @@ const FORWARDED = [
15
15
  ];
16
16
  function startApnsRelay(options) {
17
17
  const allow = new Set(options.allow ?? DEFAULT_ALLOW);
18
- const sessions = /* @__PURE__ */ new Map();
19
- function session(upstream) {
20
- const existing = sessions.get(upstream);
21
- if (existing !== void 0 && !existing.closed && !existing.destroyed) return existing;
22
- const created = connect(upstream);
23
- const forget = () => {
24
- if (sessions.get(upstream) === created) sessions.delete(upstream);
25
- };
26
- created.on("error", forget);
27
- created.on("close", forget);
28
- sessions.set(upstream, created);
29
- return created;
30
- }
18
+ const sessions = http2Sessions();
31
19
  function relay(req, res) {
32
20
  const upstreamHeader = req.headers["apns-relay-upstream"];
33
21
  const upstream = Array.isArray(upstreamHeader) ? upstreamHeader[0] : upstreamHeader;
@@ -52,7 +40,7 @@ function startApnsRelay(options) {
52
40
  }
53
41
  let stream;
54
42
  try {
55
- stream = session(upstream).request(headers);
43
+ stream = sessions.session(upstream).request(headers);
56
44
  } catch (error) {
57
45
  res.writeHead(502, { "content-type": "application/json" });
58
46
  res.end(JSON.stringify({
@@ -90,8 +78,7 @@ function startApnsRelay(options) {
90
78
  resolve({
91
79
  port: typeof address === "object" && address !== null ? address.port : options.port,
92
80
  close: () => new Promise((done) => {
93
- for (const open of sessions.values()) open.close();
94
- sessions.clear();
81
+ sessions.close();
95
82
  server.close(() => done());
96
83
  })
97
84
  });
@@ -1 +1 @@
1
- {"version":3,"file":"apns-relay.mjs","names":[],"sources":["../src/apns-relay.ts"],"sourcesContent":["import { type IncomingMessage, type ServerResponse, createServer } from 'node:http';\nimport { type ClientHttp2Session, connect } from 'node:http2';\n\n/**\n * `@shware/workflow/apns-relay` — a development-only HTTP/1.1 → HTTP/2 relay\n * for APNs, run under Node next to `wrangler dev`.\n *\n * APNs accepts HTTP/2 only, and workerd (the local Workers runtime) cannot\n * make an HTTP/2 subrequest, so a journey that pushes to an iPhone from a\n * local Worker has nowhere to go. A deployed Worker needs none of this: its\n * `fetch` negotiates HTTP/2 with Apple. The relay forwards each request to the\n * origin named in its `apns-relay-upstream` header (the sender sets it along\n * with `origin`), keeping one HTTP/2 session per upstream.\n *\n * Only Apple's two hosts are relayed by default; `allow` widens that for tests.\n */\n\nexport interface ApnsRelayOptions {\n port: number;\n /** Interface to listen on; loopback by default — the relay has no auth of its own. */\n hostname?: string;\n /** Upstream origins that may be relayed; Apple's production and sandbox hosts by default. */\n allow?: readonly string[];\n}\n\nexport interface ApnsRelay {\n port: number;\n close(): Promise<void>;\n}\n\nconst DEFAULT_ALLOW = ['https://api.push.apple.com', 'https://api.sandbox.push.apple.com'];\n\n/** Request headers APNs reads; everything else (host, content-length, …) is the relay's own business. */\nconst FORWARDED = [\n 'authorization',\n 'apns-topic',\n 'apns-push-type',\n 'apns-priority',\n 'apns-expiration',\n 'apns-collapse-id',\n 'apns-id',\n 'content-type',\n];\n\nexport function startApnsRelay(options: ApnsRelayOptions): Promise<ApnsRelay> {\n const allow = new Set(options.allow ?? DEFAULT_ALLOW);\n const sessions = new Map<string, ClientHttp2Session>();\n\n function session(upstream: string): ClientHttp2Session {\n const existing = sessions.get(upstream);\n if (existing !== undefined && !existing.closed && !existing.destroyed) return existing;\n const created = connect(upstream);\n const forget = () => {\n if (sessions.get(upstream) === created) sessions.delete(upstream);\n };\n created.on('error', forget);\n created.on('close', forget);\n sessions.set(upstream, created);\n return created;\n }\n\n function relay(req: IncomingMessage, res: ServerResponse): void {\n const upstreamHeader = req.headers['apns-relay-upstream'];\n const upstream = Array.isArray(upstreamHeader) ? upstreamHeader[0] : upstreamHeader;\n if (upstream === undefined || !allow.has(upstream)) {\n res.writeHead(400, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayUpstreamNotAllowed', upstream: upstream ?? null }));\n return;\n }\n\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => {\n const headers: Record<string, string> = {\n ':method': req.method ?? 'POST',\n ':path': req.url ?? '/',\n };\n for (const name of FORWARDED) {\n const value = req.headers[name];\n if (typeof value === 'string') headers[name] = value;\n }\n let stream;\n try {\n stream = session(upstream).request(headers);\n } catch (error) {\n res.writeHead(502, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayConnectFailed', error: String(error) }));\n return;\n }\n const body: Buffer[] = [];\n stream.on('response', (responseHeaders) => {\n const status = responseHeaders[':status'] ?? 502;\n const apnsId = responseHeaders['apns-id'];\n res.writeHead(status, {\n 'content-type': 'application/json',\n ...(typeof apnsId === 'string' ? { 'apns-id': apnsId } : {}),\n });\n });\n stream.on('data', (chunk: Buffer) => body.push(chunk));\n stream.on('end', () => res.end(Buffer.concat(body)));\n stream.on('error', (error) => {\n if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayUpstreamError', error: String(error) }));\n });\n stream.end(Buffer.concat(chunks));\n });\n }\n\n const server = createServer(relay);\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(options.port, options.hostname ?? '127.0.0.1', () => {\n const address = server.address();\n const port = typeof address === 'object' && address !== null ? address.port : options.port;\n resolve({\n port,\n close: () =>\n new Promise((done) => {\n for (const open of sessions.values()) open.close();\n sessions.clear();\n server.close(() => done());\n }),\n });\n });\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"apns-relay.mjs","names":[],"sources":["../src/apns-relay.ts"],"sourcesContent":["import { type IncomingMessage, type ServerResponse, createServer } from 'node:http';\nimport { http2Sessions } from './http2-client';\n\n/**\n * `@shware/workflow/apns-relay` — a development-only HTTP/1.1 → HTTP/2 relay\n * for APNs, run under Node next to `wrangler dev`.\n *\n * APNs accepts HTTP/2 only, and workerd (the local Workers runtime) cannot\n * make an HTTP/2 subrequest, so a journey that pushes to an iPhone from a\n * local Worker has nowhere to go. A deployed Worker needs none of this: its\n * `fetch` negotiates HTTP/2 with Apple. The relay forwards each request to the\n * origin named in its `apns-relay-upstream` header (the sender sets it along\n * with `origin`), keeping one HTTP/2 session per upstream.\n *\n * Only Apple's two hosts are relayed by default; `allow` widens that for tests.\n */\n\nexport interface ApnsRelayOptions {\n port: number;\n /** Interface to listen on; loopback by default — the relay has no auth of its own. */\n hostname?: string;\n /** Upstream origins that may be relayed; Apple's production and sandbox hosts by default. */\n allow?: readonly string[];\n}\n\nexport interface ApnsRelay {\n port: number;\n close(): Promise<void>;\n}\n\nconst DEFAULT_ALLOW = ['https://api.push.apple.com', 'https://api.sandbox.push.apple.com'];\n\n/** Request headers APNs reads; everything else (host, content-length, …) is the relay's own business. */\nconst FORWARDED = [\n 'authorization',\n 'apns-topic',\n 'apns-push-type',\n 'apns-priority',\n 'apns-expiration',\n 'apns-collapse-id',\n 'apns-id',\n 'content-type',\n];\n\nexport function startApnsRelay(options: ApnsRelayOptions): Promise<ApnsRelay> {\n const allow = new Set(options.allow ?? DEFAULT_ALLOW);\n const sessions = http2Sessions();\n\n function relay(req: IncomingMessage, res: ServerResponse): void {\n const upstreamHeader = req.headers['apns-relay-upstream'];\n const upstream = Array.isArray(upstreamHeader) ? upstreamHeader[0] : upstreamHeader;\n if (upstream === undefined || !allow.has(upstream)) {\n res.writeHead(400, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayUpstreamNotAllowed', upstream: upstream ?? null }));\n return;\n }\n\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => {\n const headers: Record<string, string> = {\n ':method': req.method ?? 'POST',\n ':path': req.url ?? '/',\n };\n for (const name of FORWARDED) {\n const value = req.headers[name];\n if (typeof value === 'string') headers[name] = value;\n }\n let stream;\n try {\n stream = sessions.session(upstream).request(headers);\n } catch (error) {\n res.writeHead(502, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayConnectFailed', error: String(error) }));\n return;\n }\n const body: Buffer[] = [];\n stream.on('response', (responseHeaders) => {\n const status = responseHeaders[':status'] ?? 502;\n const apnsId = responseHeaders['apns-id'];\n res.writeHead(status, {\n 'content-type': 'application/json',\n ...(typeof apnsId === 'string' ? { 'apns-id': apnsId } : {}),\n });\n });\n stream.on('data', (chunk: Buffer) => body.push(chunk));\n stream.on('end', () => res.end(Buffer.concat(body)));\n stream.on('error', (error) => {\n if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayUpstreamError', error: String(error) }));\n });\n stream.end(Buffer.concat(chunks));\n });\n }\n\n const server = createServer(relay);\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(options.port, options.hostname ?? '127.0.0.1', () => {\n const address = server.address();\n const port = typeof address === 'object' && address !== null ? address.port : options.port;\n resolve({\n port,\n close: () =>\n new Promise((done) => {\n sessions.close();\n server.close(() => done());\n }),\n });\n });\n });\n}\n"],"mappings":";;;AA8BA,MAAM,gBAAgB,CAAC,8BAA8B,oCAAoC;;AAGzF,MAAM,YAAY;CAChB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,SAAgB,eAAe,SAA+C;CAC5E,MAAM,QAAQ,IAAI,IAAI,QAAQ,SAAS,aAAa;CACpD,MAAM,WAAW,cAAc;CAE/B,SAAS,MAAM,KAAsB,KAA2B;EAC9D,MAAM,iBAAiB,IAAI,QAAQ;EACnC,MAAM,WAAW,MAAM,QAAQ,cAAc,IAAI,eAAe,KAAK;EACrE,IAAI,aAAa,KAAA,KAAa,CAAC,MAAM,IAAI,QAAQ,GAAG;GAClD,IAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;GACzD,IAAI,IAAI,KAAK,UAAU;IAAE,QAAQ;IAA2B,UAAU,YAAY;GAAK,CAAC,CAAC;GACzF;EACF;EAEA,MAAM,SAAmB,CAAC;EAC1B,IAAI,GAAG,SAAS,UAAkB,OAAO,KAAK,KAAK,CAAC;EACpD,IAAI,GAAG,aAAa;GAClB,MAAM,UAAkC;IACtC,WAAW,IAAI,UAAU;IACzB,SAAS,IAAI,OAAO;GACtB;GACA,KAAK,MAAM,QAAQ,WAAW;IAC5B,MAAM,QAAQ,IAAI,QAAQ;IAC1B,IAAI,OAAO,UAAU,UAAU,QAAQ,QAAQ;GACjD;GACA,IAAI;GACJ,IAAI;IACF,SAAS,SAAS,QAAQ,QAAQ,CAAC,CAAC,QAAQ,OAAO;GACrD,SAAS,OAAO;IACd,IAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;IACzD,IAAI,IAAI,KAAK,UAAU;KAAE,QAAQ;KAAsB,OAAO,OAAO,KAAK;IAAE,CAAC,CAAC;IAC9E;GACF;GACA,MAAM,OAAiB,CAAC;GACxB,OAAO,GAAG,aAAa,oBAAoB;IACzC,MAAM,SAAS,gBAAgB,cAAc;IAC7C,MAAM,SAAS,gBAAgB;IAC/B,IAAI,UAAU,QAAQ;KACpB,gBAAgB;KAChB,GAAI,OAAO,WAAW,WAAW,EAAE,WAAW,OAAO,IAAI,CAAC;IAC5D,CAAC;GACH,CAAC;GACD,OAAO,GAAG,SAAS,UAAkB,KAAK,KAAK,KAAK,CAAC;GACrD,OAAO,GAAG,aAAa,IAAI,IAAI,OAAO,OAAO,IAAI,CAAC,CAAC;GACnD,OAAO,GAAG,UAAU,UAAU;IAC5B,IAAI,CAAC,IAAI,aAAa,IAAI,UAAU,KAAK,EAAE,gBAAgB,mBAAmB,CAAC;IAC/E,IAAI,IAAI,KAAK,UAAU;KAAE,QAAQ;KAAsB,OAAO,OAAO,KAAK;IAAE,CAAC,CAAC;GAChF,CAAC;GACD,OAAO,IAAI,OAAO,OAAO,MAAM,CAAC;EAClC,CAAC;CACH;CAEA,MAAM,SAAS,aAAa,KAAK;CACjC,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,OAAO,KAAK,SAAS,MAAM;EAC3B,OAAO,OAAO,QAAQ,MAAM,QAAQ,YAAY,mBAAmB;GACjE,MAAM,UAAU,OAAO,QAAQ;GAE/B,QAAQ;IACN,MAFW,OAAO,YAAY,YAAY,YAAY,OAAO,QAAQ,OAAO,QAAQ;IAGpF,aACE,IAAI,SAAS,SAAS;KACpB,SAAS,MAAM;KACf,OAAO,YAAY,KAAK,CAAC;IAC3B,CAAC;GACL,CAAC;EACH,CAAC;CACH,CAAC;AACH"}
@@ -0,0 +1,104 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _aws_durable_execution_sdk_js = require("@aws/durable-execution-sdk-js");
3
+ //#region src/aws/engine-step.ts
4
+ /**
5
+ * EngineStep over a Lambda durable execution.
6
+ *
7
+ * Steps, sleeps and wakes map onto the SDK's `step`, `wait` and callbacks. A wake handle is a
8
+ * callback, which is one-shot, so every wait arms a fresh one (the interpreter subscribes on
9
+ * every attempt): created where the durable context allows it — outside any step, so its id
10
+ * replays — then recorded in Postgres together with the subscription rows, numbered by a
11
+ * generation so a late wake or cleanup of an older wait cannot touch a newer one. The callback
12
+ * is only a hint; the facts the wait re-checks are Postgres'.
13
+ */
14
+ /** Refusals a retry would only repeat: a template's props, an emitted event's payload. */
15
+ const NON_RETRYABLE = /* @__PURE__ */ new Set(["TemplateValidationError", "ContractValidationError"]);
16
+ /** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */
17
+ function stepRetry(error, attempt) {
18
+ if (NON_RETRYABLE.has(error.name) || attempt >= 6) return { shouldRetry: false };
19
+ return {
20
+ shouldRetry: true,
21
+ delay: { seconds: Math.min(60, 2 ** attempt) }
22
+ };
23
+ }
24
+ var AwsEngineStep = class {
25
+ context;
26
+ store;
27
+ executions;
28
+ lambda;
29
+ userId;
30
+ instanceId;
31
+ generation = 0;
32
+ armed;
33
+ constructor(context, store, executions, lambda, userId, instanceId) {
34
+ this.context = context;
35
+ this.store = store;
36
+ this.executions = executions;
37
+ this.lambda = lambda;
38
+ this.userId = userId;
39
+ this.instanceId = instanceId;
40
+ }
41
+ do(name, fn) {
42
+ return this.context.step(name, () => fn(), { retryStrategy: stepRetry });
43
+ }
44
+ async sleep(name, ms) {
45
+ if (!(ms > 0)) return;
46
+ await this.context.wait(name, { seconds: Math.ceil(ms / 1e3) });
47
+ }
48
+ async sleepUntil(name, timestampMs) {
49
+ const remaining = await this.context.step(`${name}:remaining`, async () => timestampMs - Date.now());
50
+ await this.sleep(name, remaining);
51
+ }
52
+ async subscribe(name, events, options = {}) {
53
+ const generation = ++this.generation;
54
+ const { deadlineMs } = options;
55
+ const seconds = deadlineMs === void 0 ? void 0 : await this.context.step(`${name}:timeout`, async () => Math.max(1, Math.ceil((deadlineMs - Date.now()) / 1e3)));
56
+ const [promise, callbackId] = await this.context.createCallback(`${name}:callback`, seconds === void 0 ? {} : { timeout: { seconds } });
57
+ const previous = this.armed;
58
+ this.armed = {
59
+ promise,
60
+ callbackId,
61
+ generation,
62
+ settled: false
63
+ };
64
+ await this.context.step(name, async () => {
65
+ await this.store.subscribe(this.instanceId, this.userId, events, Date.now());
66
+ await this.executions.armCallback(this.instanceId, {
67
+ id: callbackId,
68
+ generation,
69
+ deadlineAt: deadlineMs
70
+ });
71
+ if (previous !== void 0 && !previous.settled) await this.lambda.completeCallback(previous.callbackId, "{}");
72
+ }, { retryStrategy: stepRetry });
73
+ if (previous !== void 0) previous.settled = true;
74
+ }
75
+ async unsubscribe(name) {
76
+ const armed = this.armed;
77
+ await this.context.step(name, async () => {
78
+ await this.store.unsubscribe(this.instanceId);
79
+ if (armed === void 0) return;
80
+ await this.executions.clearCallback(this.instanceId, armed.generation);
81
+ if (!armed.settled) await this.lambda.completeCallback(armed.callbackId, "{}");
82
+ }, { retryStrategy: stepRetry });
83
+ if (armed !== void 0) armed.settled = true;
84
+ this.armed = void 0;
85
+ }
86
+ async waitForWake(name, _timeoutMs) {
87
+ const armed = this.armed;
88
+ if (armed === void 0) throw new Error(`waitForWake '${name}' without a subscribe: nothing is armed to wake it`);
89
+ try {
90
+ await armed.promise;
91
+ return "event";
92
+ } catch (error) {
93
+ if (error instanceof _aws_durable_execution_sdk_js.CallbackTimeoutError) return "timeout";
94
+ throw error;
95
+ } finally {
96
+ armed.settled = true;
97
+ }
98
+ }
99
+ };
100
+ //#endregion
101
+ exports.AwsEngineStep = AwsEngineStep;
102
+ exports.stepRetry = stepRetry;
103
+
104
+ //# sourceMappingURL=engine-step.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-step.cjs","names":["CallbackTimeoutError"],"sources":["../../src/aws/engine-step.ts"],"sourcesContent":["import {\n CallbackTimeoutError,\n type DurableContext,\n type DurablePromise,\n type RetryDecision,\n} from '@aws/durable-execution-sdk-js';\nimport type { EngineStep } from '../engine/ports';\nimport type { ExecutionRegistry, JourneyStore } from '../store/index';\nimport type { LambdaApi } from './lambda-api';\n\n/**\n * EngineStep over a Lambda durable execution.\n *\n * Steps, sleeps and wakes map onto the SDK's `step`, `wait` and callbacks. A wake handle is a\n * callback, which is one-shot, so every wait arms a fresh one (the interpreter subscribes on\n * every attempt): created where the durable context allows it — outside any step, so its id\n * replays — then recorded in Postgres together with the subscription rows, numbered by a\n * generation so a late wake or cleanup of an older wait cannot touch a newer one. The callback\n * is only a hint; the facts the wait re-checks are Postgres'.\n */\n\n/** Refusals a retry would only repeat: a template's props, an emitted event's payload. */\nconst NON_RETRYABLE = new Set(['TemplateValidationError', 'ContractValidationError']);\n\n/** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */\nexport function stepRetry(error: Error, attempt: number): RetryDecision {\n if (NON_RETRYABLE.has(error.name) || attempt >= 6) return { shouldRetry: false };\n return { shouldRetry: true, delay: { seconds: Math.min(60, 2 ** attempt) } };\n}\n\ninterface Armed {\n promise: DurablePromise<string>;\n callbackId: string;\n generation: number;\n /** The wait consumed it (woken or timed out): there is nothing left to close. */\n settled: boolean;\n}\n\nexport class AwsEngineStep implements EngineStep {\n private generation = 0;\n private armed: Armed | undefined;\n\n constructor(\n private readonly context: DurableContext,\n private readonly store: JourneyStore,\n private readonly executions: ExecutionRegistry,\n private readonly lambda: LambdaApi,\n private readonly userId: string,\n private readonly instanceId: string\n ) {}\n\n do<T>(name: string, fn: () => Promise<T>): Promise<T> {\n return this.context.step(name, () => fn(), { retryStrategy: stepRetry });\n }\n\n async sleep(name: string, ms: number): Promise<void> {\n // A wait is whole seconds: rounded up, so it never ends early; nothing to wait at zero\n if (!(ms > 0)) return;\n await this.context.wait(name, { seconds: Math.ceil(ms / 1000) });\n }\n\n async sleepUntil(name: string, timestampMs: number): Promise<void> {\n // The remaining time is checkpointed: a replay must not recompute it from a later clock\n const remaining = await this.context.step(\n `${name}:remaining`,\n async () => timestampMs - Date.now()\n );\n await this.sleep(name, remaining);\n }\n\n async subscribe(\n name: string,\n events: readonly string[],\n options: { deadlineMs?: number } = {}\n ): Promise<void> {\n const generation = ++this.generation;\n const { deadlineMs } = options;\n // The callback's own timeout is the wait's deadline: fixed when it is created\n const seconds =\n deadlineMs === undefined\n ? undefined\n : await this.context.step(`${name}:timeout`, async () =>\n Math.max(1, Math.ceil((deadlineMs - Date.now()) / 1000))\n );\n const [promise, callbackId] = await this.context.createCallback(\n `${name}:callback`,\n seconds === undefined ? {} : { timeout: { seconds } }\n );\n const previous = this.armed;\n this.armed = { promise, callbackId, generation, settled: false };\n await this.context.step(\n name,\n async () => {\n // Rows first, then the callback: a wake that lands in between finds the rows and the\n // callback of the last wait — its completion is refused, and this wait checks the facts\n await this.store.subscribe(this.instanceId, this.userId, events, Date.now());\n await this.executions.armCallback(this.instanceId, {\n id: callbackId,\n generation,\n deadlineAt: deadlineMs,\n });\n if (previous !== undefined && !previous.settled) {\n await this.lambda.completeCallback(previous.callbackId, '{}');\n }\n },\n { retryStrategy: stepRetry }\n );\n if (previous !== undefined) previous.settled = true;\n }\n\n async unsubscribe(name: string): Promise<void> {\n const armed = this.armed;\n await this.context.step(\n name,\n async () => {\n await this.store.unsubscribe(this.instanceId);\n if (armed === undefined) return;\n await this.executions.clearCallback(this.instanceId, armed.generation);\n // The condition held before the wait parked: its callback is closed through the public\n // API (a closed one answers false), rather than left open until it times out\n if (!armed.settled) await this.lambda.completeCallback(armed.callbackId, '{}');\n },\n { retryStrategy: stepRetry }\n );\n if (armed !== undefined) armed.settled = true;\n this.armed = undefined;\n }\n\n async waitForWake(name: string, _timeoutMs: number): Promise<'event' | 'timeout'> {\n const armed = this.armed;\n if (armed === undefined) {\n throw new Error(`waitForWake '${name}' without a subscribe: nothing is armed to wake it`);\n }\n try {\n await armed.promise;\n return 'event';\n } catch (error) {\n // Only the callback's own expiry is a timeout; any other failure is a failure\n if (error instanceof CallbackTimeoutError) return 'timeout';\n throw error;\n } finally {\n armed.settled = true;\n }\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import { EngineStep } from "../engine/ports.cjs";
2
+ import { ExecutionRegistry, JourneyStore } from "../store/index.cjs";
3
+ import { LambdaApi } from "./lambda-api.cjs";
4
+ import { DurableContext, RetryDecision } from "@aws/durable-execution-sdk-js";
5
+ //#region src/aws/engine-step.d.ts
6
+ /** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */
7
+ export declare function stepRetry(error: Error, attempt: number): RetryDecision;
8
+ export declare class AwsEngineStep implements EngineStep {
9
+ private readonly context;
10
+ private readonly store;
11
+ private readonly executions;
12
+ private readonly lambda;
13
+ private readonly userId;
14
+ private readonly instanceId;
15
+ private generation;
16
+ private armed;
17
+ constructor(context: DurableContext, store: JourneyStore, executions: ExecutionRegistry, lambda: LambdaApi, userId: string, instanceId: string);
18
+ do<T>(name: string, fn: () => Promise<T>): Promise<T>;
19
+ sleep(name: string, ms: number): Promise<void>;
20
+ sleepUntil(name: string, timestampMs: number): Promise<void>;
21
+ subscribe(name: string, events: readonly string[], options?: {
22
+ deadlineMs?: number;
23
+ }): Promise<void>;
24
+ unsubscribe(name: string): Promise<void>;
25
+ waitForWake(name: string, _timeoutMs: number): Promise<'event' | 'timeout'>;
26
+ }
27
+ //#endregion
28
+ //# sourceMappingURL=engine-step.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-step.d.cts","names":[],"sources":["../../src/aws/engine-step.ts"],"mappings":";;;;;;wBAyBgB,UAAU,OAAO,OAAO,kBAAkB;qBAa7C,yBAAyB;mBAKjB;mBACA;mBACA;mBACA;mBACA;mBACA;UATX;UACA;EAER,YACmB,SAAS,gBACT,OAAO,cACP,YAAY,mBACZ,QAAQ,WACR,gBACA;EAGnB,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;EAI7C,MAAM,cAAc,aAAa;EAMjC,WAAW,cAAc,sBAAsB;EAS/C,UACJ,cACA,2BACA;IAAW;MACV;EAoCG,YAAY,eAAe;EAkB3B,YAAY,cAAc,qBAAqB"}
@@ -0,0 +1,28 @@
1
+ import { EngineStep } from "../engine/ports.mjs";
2
+ import { ExecutionRegistry, JourneyStore } from "../store/index.mjs";
3
+ import { LambdaApi } from "./lambda-api.mjs";
4
+ import { DurableContext, RetryDecision } from "@aws/durable-execution-sdk-js";
5
+ //#region src/aws/engine-step.d.ts
6
+ /** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */
7
+ export declare function stepRetry(error: Error, attempt: number): RetryDecision;
8
+ export declare class AwsEngineStep implements EngineStep {
9
+ private readonly context;
10
+ private readonly store;
11
+ private readonly executions;
12
+ private readonly lambda;
13
+ private readonly userId;
14
+ private readonly instanceId;
15
+ private generation;
16
+ private armed;
17
+ constructor(context: DurableContext, store: JourneyStore, executions: ExecutionRegistry, lambda: LambdaApi, userId: string, instanceId: string);
18
+ do<T>(name: string, fn: () => Promise<T>): Promise<T>;
19
+ sleep(name: string, ms: number): Promise<void>;
20
+ sleepUntil(name: string, timestampMs: number): Promise<void>;
21
+ subscribe(name: string, events: readonly string[], options?: {
22
+ deadlineMs?: number;
23
+ }): Promise<void>;
24
+ unsubscribe(name: string): Promise<void>;
25
+ waitForWake(name: string, _timeoutMs: number): Promise<'event' | 'timeout'>;
26
+ }
27
+ //#endregion
28
+ //# sourceMappingURL=engine-step.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-step.d.mts","names":[],"sources":["../../src/aws/engine-step.ts"],"mappings":";;;;;;wBAyBgB,UAAU,OAAO,OAAO,kBAAkB;qBAa7C,yBAAyB;mBAKjB;mBACA;mBACA;mBACA;mBACA;mBACA;UATX;UACA;EAER,YACmB,SAAS,gBACT,OAAO,cACP,YAAY,mBACZ,QAAQ,WACR,gBACA;EAGnB,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;EAI7C,MAAM,cAAc,aAAa;EAMjC,WAAW,cAAc,sBAAsB;EAS/C,UACJ,cACA,2BACA;IAAW;MACV;EAoCG,YAAY,eAAe;EAkB3B,YAAY,cAAc,qBAAqB"}
@@ -0,0 +1,102 @@
1
+ import { CallbackTimeoutError } from "@aws/durable-execution-sdk-js";
2
+ //#region src/aws/engine-step.ts
3
+ /**
4
+ * EngineStep over a Lambda durable execution.
5
+ *
6
+ * Steps, sleeps and wakes map onto the SDK's `step`, `wait` and callbacks. A wake handle is a
7
+ * callback, which is one-shot, so every wait arms a fresh one (the interpreter subscribes on
8
+ * every attempt): created where the durable context allows it — outside any step, so its id
9
+ * replays — then recorded in Postgres together with the subscription rows, numbered by a
10
+ * generation so a late wake or cleanup of an older wait cannot touch a newer one. The callback
11
+ * is only a hint; the facts the wait re-checks are Postgres'.
12
+ */
13
+ /** Refusals a retry would only repeat: a template's props, an emitted event's payload. */
14
+ const NON_RETRYABLE = /* @__PURE__ */ new Set(["TemplateValidationError", "ContractValidationError"]);
15
+ /** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */
16
+ function stepRetry(error, attempt) {
17
+ if (NON_RETRYABLE.has(error.name) || attempt >= 6) return { shouldRetry: false };
18
+ return {
19
+ shouldRetry: true,
20
+ delay: { seconds: Math.min(60, 2 ** attempt) }
21
+ };
22
+ }
23
+ var AwsEngineStep = class {
24
+ context;
25
+ store;
26
+ executions;
27
+ lambda;
28
+ userId;
29
+ instanceId;
30
+ generation = 0;
31
+ armed;
32
+ constructor(context, store, executions, lambda, userId, instanceId) {
33
+ this.context = context;
34
+ this.store = store;
35
+ this.executions = executions;
36
+ this.lambda = lambda;
37
+ this.userId = userId;
38
+ this.instanceId = instanceId;
39
+ }
40
+ do(name, fn) {
41
+ return this.context.step(name, () => fn(), { retryStrategy: stepRetry });
42
+ }
43
+ async sleep(name, ms) {
44
+ if (!(ms > 0)) return;
45
+ await this.context.wait(name, { seconds: Math.ceil(ms / 1e3) });
46
+ }
47
+ async sleepUntil(name, timestampMs) {
48
+ const remaining = await this.context.step(`${name}:remaining`, async () => timestampMs - Date.now());
49
+ await this.sleep(name, remaining);
50
+ }
51
+ async subscribe(name, events, options = {}) {
52
+ const generation = ++this.generation;
53
+ const { deadlineMs } = options;
54
+ const seconds = deadlineMs === void 0 ? void 0 : await this.context.step(`${name}:timeout`, async () => Math.max(1, Math.ceil((deadlineMs - Date.now()) / 1e3)));
55
+ const [promise, callbackId] = await this.context.createCallback(`${name}:callback`, seconds === void 0 ? {} : { timeout: { seconds } });
56
+ const previous = this.armed;
57
+ this.armed = {
58
+ promise,
59
+ callbackId,
60
+ generation,
61
+ settled: false
62
+ };
63
+ await this.context.step(name, async () => {
64
+ await this.store.subscribe(this.instanceId, this.userId, events, Date.now());
65
+ await this.executions.armCallback(this.instanceId, {
66
+ id: callbackId,
67
+ generation,
68
+ deadlineAt: deadlineMs
69
+ });
70
+ if (previous !== void 0 && !previous.settled) await this.lambda.completeCallback(previous.callbackId, "{}");
71
+ }, { retryStrategy: stepRetry });
72
+ if (previous !== void 0) previous.settled = true;
73
+ }
74
+ async unsubscribe(name) {
75
+ const armed = this.armed;
76
+ await this.context.step(name, async () => {
77
+ await this.store.unsubscribe(this.instanceId);
78
+ if (armed === void 0) return;
79
+ await this.executions.clearCallback(this.instanceId, armed.generation);
80
+ if (!armed.settled) await this.lambda.completeCallback(armed.callbackId, "{}");
81
+ }, { retryStrategy: stepRetry });
82
+ if (armed !== void 0) armed.settled = true;
83
+ this.armed = void 0;
84
+ }
85
+ async waitForWake(name, _timeoutMs) {
86
+ const armed = this.armed;
87
+ if (armed === void 0) throw new Error(`waitForWake '${name}' without a subscribe: nothing is armed to wake it`);
88
+ try {
89
+ await armed.promise;
90
+ return "event";
91
+ } catch (error) {
92
+ if (error instanceof CallbackTimeoutError) return "timeout";
93
+ throw error;
94
+ } finally {
95
+ armed.settled = true;
96
+ }
97
+ }
98
+ };
99
+ //#endregion
100
+ export { AwsEngineStep, stepRetry };
101
+
102
+ //# sourceMappingURL=engine-step.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine-step.mjs","names":[],"sources":["../../src/aws/engine-step.ts"],"sourcesContent":["import {\n CallbackTimeoutError,\n type DurableContext,\n type DurablePromise,\n type RetryDecision,\n} from '@aws/durable-execution-sdk-js';\nimport type { EngineStep } from '../engine/ports';\nimport type { ExecutionRegistry, JourneyStore } from '../store/index';\nimport type { LambdaApi } from './lambda-api';\n\n/**\n * EngineStep over a Lambda durable execution.\n *\n * Steps, sleeps and wakes map onto the SDK's `step`, `wait` and callbacks. A wake handle is a\n * callback, which is one-shot, so every wait arms a fresh one (the interpreter subscribes on\n * every attempt): created where the durable context allows it — outside any step, so its id\n * replays — then recorded in Postgres together with the subscription rows, numbered by a\n * generation so a late wake or cleanup of an older wait cannot touch a newer one. The callback\n * is only a hint; the facts the wait re-checks are Postgres'.\n */\n\n/** Refusals a retry would only repeat: a template's props, an emitted event's payload. */\nconst NON_RETRYABLE = new Set(['TemplateValidationError', 'ContractValidationError']);\n\n/** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */\nexport function stepRetry(error: Error, attempt: number): RetryDecision {\n if (NON_RETRYABLE.has(error.name) || attempt >= 6) return { shouldRetry: false };\n return { shouldRetry: true, delay: { seconds: Math.min(60, 2 ** attempt) } };\n}\n\ninterface Armed {\n promise: DurablePromise<string>;\n callbackId: string;\n generation: number;\n /** The wait consumed it (woken or timed out): there is nothing left to close. */\n settled: boolean;\n}\n\nexport class AwsEngineStep implements EngineStep {\n private generation = 0;\n private armed: Armed | undefined;\n\n constructor(\n private readonly context: DurableContext,\n private readonly store: JourneyStore,\n private readonly executions: ExecutionRegistry,\n private readonly lambda: LambdaApi,\n private readonly userId: string,\n private readonly instanceId: string\n ) {}\n\n do<T>(name: string, fn: () => Promise<T>): Promise<T> {\n return this.context.step(name, () => fn(), { retryStrategy: stepRetry });\n }\n\n async sleep(name: string, ms: number): Promise<void> {\n // A wait is whole seconds: rounded up, so it never ends early; nothing to wait at zero\n if (!(ms > 0)) return;\n await this.context.wait(name, { seconds: Math.ceil(ms / 1000) });\n }\n\n async sleepUntil(name: string, timestampMs: number): Promise<void> {\n // The remaining time is checkpointed: a replay must not recompute it from a later clock\n const remaining = await this.context.step(\n `${name}:remaining`,\n async () => timestampMs - Date.now()\n );\n await this.sleep(name, remaining);\n }\n\n async subscribe(\n name: string,\n events: readonly string[],\n options: { deadlineMs?: number } = {}\n ): Promise<void> {\n const generation = ++this.generation;\n const { deadlineMs } = options;\n // The callback's own timeout is the wait's deadline: fixed when it is created\n const seconds =\n deadlineMs === undefined\n ? undefined\n : await this.context.step(`${name}:timeout`, async () =>\n Math.max(1, Math.ceil((deadlineMs - Date.now()) / 1000))\n );\n const [promise, callbackId] = await this.context.createCallback(\n `${name}:callback`,\n seconds === undefined ? {} : { timeout: { seconds } }\n );\n const previous = this.armed;\n this.armed = { promise, callbackId, generation, settled: false };\n await this.context.step(\n name,\n async () => {\n // Rows first, then the callback: a wake that lands in between finds the rows and the\n // callback of the last wait — its completion is refused, and this wait checks the facts\n await this.store.subscribe(this.instanceId, this.userId, events, Date.now());\n await this.executions.armCallback(this.instanceId, {\n id: callbackId,\n generation,\n deadlineAt: deadlineMs,\n });\n if (previous !== undefined && !previous.settled) {\n await this.lambda.completeCallback(previous.callbackId, '{}');\n }\n },\n { retryStrategy: stepRetry }\n );\n if (previous !== undefined) previous.settled = true;\n }\n\n async unsubscribe(name: string): Promise<void> {\n const armed = this.armed;\n await this.context.step(\n name,\n async () => {\n await this.store.unsubscribe(this.instanceId);\n if (armed === undefined) return;\n await this.executions.clearCallback(this.instanceId, armed.generation);\n // The condition held before the wait parked: its callback is closed through the public\n // API (a closed one answers false), rather than left open until it times out\n if (!armed.settled) await this.lambda.completeCallback(armed.callbackId, '{}');\n },\n { retryStrategy: stepRetry }\n );\n if (armed !== undefined) armed.settled = true;\n this.armed = undefined;\n }\n\n async waitForWake(name: string, _timeoutMs: number): Promise<'event' | 'timeout'> {\n const armed = this.armed;\n if (armed === undefined) {\n throw new Error(`waitForWake '${name}' without a subscribe: nothing is armed to wake it`);\n }\n try {\n await armed.promise;\n return 'event';\n } catch (error) {\n // Only the callback's own expiry is a timeout; any other failure is a failure\n if (error instanceof CallbackTimeoutError) return 'timeout';\n throw error;\n } finally {\n armed.settled = true;\n }\n }\n}\n"],"mappings":"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"}