@shware/workflow 6.1.1 → 8.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 (192) hide show
  1. package/README.md +132 -35
  2. package/dist/aws/engine-step.cjs +4 -1
  3. package/dist/aws/engine-step.cjs.map +1 -1
  4. package/dist/aws/engine-step.d.cts.map +1 -1
  5. package/dist/aws/engine-step.d.mts.map +1 -1
  6. package/dist/aws/engine-step.mjs +4 -1
  7. package/dist/aws/engine-step.mjs.map +1 -1
  8. package/dist/aws/index.cjs +3 -2
  9. package/dist/aws/index.cjs.map +1 -1
  10. package/dist/aws/index.mjs +3 -2
  11. package/dist/aws/index.mjs.map +1 -1
  12. package/dist/client.cjs +66 -41
  13. package/dist/client.cjs.map +1 -1
  14. package/dist/client.d.cts +72 -22
  15. package/dist/client.d.cts.map +1 -1
  16. package/dist/client.d.mts +72 -22
  17. package/dist/client.d.mts.map +1 -1
  18. package/dist/client.mjs +66 -42
  19. package/dist/client.mjs.map +1 -1
  20. package/dist/cloudflare/index.d.cts +4 -3
  21. package/dist/cloudflare/index.d.mts +4 -3
  22. package/dist/cloudflare/runner.cjs +3 -1
  23. package/dist/cloudflare/runner.cjs.map +1 -1
  24. package/dist/cloudflare/runner.d.cts.map +1 -1
  25. package/dist/cloudflare/runner.d.mts.map +1 -1
  26. package/dist/cloudflare/runner.mjs +3 -1
  27. package/dist/cloudflare/runner.mjs.map +1 -1
  28. package/dist/cloudflare/worker.cjs +13 -2
  29. package/dist/cloudflare/worker.cjs.map +1 -1
  30. package/dist/cloudflare/worker.d.cts +9 -1
  31. package/dist/cloudflare/worker.d.cts.map +1 -1
  32. package/dist/cloudflare/worker.d.mts +9 -1
  33. package/dist/cloudflare/worker.d.mts.map +1 -1
  34. package/dist/cloudflare/worker.mjs +13 -2
  35. package/dist/cloudflare/worker.mjs.map +1 -1
  36. package/dist/contract.cjs.map +1 -1
  37. package/dist/contract.d.cts +4 -4
  38. package/dist/contract.d.mts +4 -4
  39. package/dist/contract.mjs.map +1 -1
  40. package/dist/dsl/trigger.cjs +2 -1
  41. package/dist/dsl/trigger.cjs.map +1 -1
  42. package/dist/dsl/trigger.d.cts +6 -0
  43. package/dist/dsl/trigger.d.cts.map +1 -1
  44. package/dist/dsl/trigger.d.mts +6 -0
  45. package/dist/dsl/trigger.d.mts.map +1 -1
  46. package/dist/dsl/trigger.mjs +2 -1
  47. package/dist/dsl/trigger.mjs.map +1 -1
  48. package/dist/email-sender.cjs +8 -1
  49. package/dist/email-sender.cjs.map +1 -1
  50. package/dist/email-sender.d.cts +2 -2
  51. package/dist/email-sender.d.cts.map +1 -1
  52. package/dist/email-sender.d.mts +2 -2
  53. package/dist/email-sender.d.mts.map +1 -1
  54. package/dist/email-sender.mjs +9 -2
  55. package/dist/email-sender.mjs.map +1 -1
  56. package/dist/email.cjs +5 -0
  57. package/dist/email.cjs.map +1 -1
  58. package/dist/email.d.cts +2 -0
  59. package/dist/email.d.cts.map +1 -1
  60. package/dist/email.d.mts +2 -0
  61. package/dist/email.d.mts.map +1 -1
  62. package/dist/email.mjs +5 -1
  63. package/dist/email.mjs.map +1 -1
  64. package/dist/engine/condition.cjs +7 -5
  65. package/dist/engine/condition.cjs.map +1 -1
  66. package/dist/engine/condition.d.cts +3 -3
  67. package/dist/engine/condition.d.cts.map +1 -1
  68. package/dist/engine/condition.d.mts +3 -3
  69. package/dist/engine/condition.d.mts.map +1 -1
  70. package/dist/engine/condition.mjs +7 -5
  71. package/dist/engine/condition.mjs.map +1 -1
  72. package/dist/engine/interpreter.cjs +11 -6
  73. package/dist/engine/interpreter.cjs.map +1 -1
  74. package/dist/engine/interpreter.d.cts.map +1 -1
  75. package/dist/engine/interpreter.d.mts.map +1 -1
  76. package/dist/engine/interpreter.mjs +11 -6
  77. package/dist/engine/interpreter.mjs.map +1 -1
  78. package/dist/engine/ports.d.cts +21 -3
  79. package/dist/engine/ports.d.cts.map +1 -1
  80. package/dist/engine/ports.d.mts +21 -3
  81. package/dist/engine/ports.d.mts.map +1 -1
  82. package/dist/host/batch.cjs +172 -0
  83. package/dist/host/batch.cjs.map +1 -0
  84. package/dist/host/batch.d.cts +88 -0
  85. package/dist/host/batch.d.cts.map +1 -0
  86. package/dist/host/batch.d.mts +88 -0
  87. package/dist/host/batch.d.mts.map +1 -0
  88. package/dist/host/batch.mjs +163 -0
  89. package/dist/host/batch.mjs.map +1 -0
  90. package/dist/host/execute.cjs +10 -6
  91. package/dist/host/execute.cjs.map +1 -1
  92. package/dist/host/execute.mjs +10 -6
  93. package/dist/host/execute.mjs.map +1 -1
  94. package/dist/host/http.cjs +31 -90
  95. package/dist/host/http.cjs.map +1 -1
  96. package/dist/host/http.d.cts +4 -2
  97. package/dist/host/http.d.cts.map +1 -1
  98. package/dist/host/http.d.mts +4 -2
  99. package/dist/host/http.d.mts.map +1 -1
  100. package/dist/host/http.mjs +25 -84
  101. package/dist/host/http.mjs.map +1 -1
  102. package/dist/host/ingest.cjs +164 -25
  103. package/dist/host/ingest.cjs.map +1 -1
  104. package/dist/host/ingest.d.cts +38 -8
  105. package/dist/host/ingest.d.cts.map +1 -1
  106. package/dist/host/ingest.d.mts +38 -8
  107. package/dist/host/ingest.d.mts.map +1 -1
  108. package/dist/host/ingest.mjs +161 -26
  109. package/dist/host/ingest.mjs.map +1 -1
  110. package/dist/host/journey-host.cjs +8 -1
  111. package/dist/host/journey-host.cjs.map +1 -1
  112. package/dist/host/journey-host.d.cts +10 -4
  113. package/dist/host/journey-host.d.cts.map +1 -1
  114. package/dist/host/journey-host.d.mts +10 -4
  115. package/dist/host/journey-host.d.mts.map +1 -1
  116. package/dist/host/journey-host.mjs +8 -1
  117. package/dist/host/journey-host.mjs.map +1 -1
  118. package/dist/host/platform.cjs.map +1 -1
  119. package/dist/host/platform.d.cts +2 -0
  120. package/dist/host/platform.d.cts.map +1 -1
  121. package/dist/host/platform.d.mts +2 -0
  122. package/dist/host/platform.d.mts.map +1 -1
  123. package/dist/host/platform.mjs.map +1 -1
  124. package/dist/index.cjs +1 -0
  125. package/dist/index.d.cts +2 -2
  126. package/dist/index.d.mts +2 -2
  127. package/dist/index.mjs +2 -2
  128. package/dist/ir.cjs +2 -1
  129. package/dist/ir.cjs.map +1 -1
  130. package/dist/ir.d.cts +3 -0
  131. package/dist/ir.d.cts.map +1 -1
  132. package/dist/ir.d.mts +3 -0
  133. package/dist/ir.d.mts.map +1 -1
  134. package/dist/ir.mjs +2 -1
  135. package/dist/ir.mjs.map +1 -1
  136. package/dist/reporting/observer.cjs +1 -0
  137. package/dist/reporting/observer.cjs.map +1 -1
  138. package/dist/reporting/observer.mjs +1 -0
  139. package/dist/reporting/observer.mjs.map +1 -1
  140. package/dist/store/facts.cjs +29 -9
  141. package/dist/store/facts.cjs.map +1 -1
  142. package/dist/store/facts.d.cts +7 -2
  143. package/dist/store/facts.d.cts.map +1 -1
  144. package/dist/store/facts.d.mts +7 -2
  145. package/dist/store/facts.d.mts.map +1 -1
  146. package/dist/store/facts.mjs +29 -9
  147. package/dist/store/facts.mjs.map +1 -1
  148. package/dist/store/identity.cjs +34 -0
  149. package/dist/store/identity.cjs.map +1 -0
  150. package/dist/store/identity.d.cts +13 -0
  151. package/dist/store/identity.d.cts.map +1 -0
  152. package/dist/store/identity.d.mts +13 -0
  153. package/dist/store/identity.d.mts.map +1 -0
  154. package/dist/store/identity.mjs +30 -0
  155. package/dist/store/identity.mjs.map +1 -0
  156. package/dist/store/index.cjs +5 -0
  157. package/dist/store/index.cjs.map +1 -1
  158. package/dist/store/index.d.cts +119 -25
  159. package/dist/store/index.d.cts.map +1 -1
  160. package/dist/store/index.d.mts +119 -25
  161. package/dist/store/index.d.mts.map +1 -1
  162. package/dist/store/index.mjs +2 -1
  163. package/dist/store/index.mjs.map +1 -1
  164. package/dist/store/postgres-reporting.cjs +4 -0
  165. package/dist/store/postgres-reporting.cjs.map +1 -1
  166. package/dist/store/postgres-reporting.d.cts +1 -0
  167. package/dist/store/postgres-reporting.d.cts.map +1 -1
  168. package/dist/store/postgres-reporting.d.mts +1 -0
  169. package/dist/store/postgres-reporting.d.mts.map +1 -1
  170. package/dist/store/postgres-reporting.mjs +4 -0
  171. package/dist/store/postgres-reporting.mjs.map +1 -1
  172. package/dist/store/postgres.cjs +271 -55
  173. package/dist/store/postgres.cjs.map +1 -1
  174. package/dist/store/postgres.d.cts +29 -7
  175. package/dist/store/postgres.d.cts.map +1 -1
  176. package/dist/store/postgres.d.mts +29 -7
  177. package/dist/store/postgres.d.mts.map +1 -1
  178. package/dist/store/postgres.mjs +272 -56
  179. package/dist/store/postgres.mjs.map +1 -1
  180. package/dist/store/tables.cjs +28 -9
  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 +28 -9
  187. package/dist/store/tables.mjs.map +1 -1
  188. package/dist/webpush.cjs.map +1 -1
  189. package/dist/webpush.d.cts +1 -1
  190. package/dist/webpush.d.mts +1 -1
  191. package/dist/webpush.mjs.map +1 -1
  192. package/package.json +1 -1
package/README.md CHANGED
@@ -264,8 +264,11 @@ export const journeys = new JourneyClient({
264
264
  token: process.env.WORKFLOW_API_TOKEN,
265
265
  });
266
266
 
267
- await journeys.identify(user.id, { email: user.email, name: user.name });
268
- await journeys.track(user.id, 'login', { method: 'google' }, { id: `login-${session.id}` });
267
+ await journeys.ingest({
268
+ userId: user.id,
269
+ profile: { email: user.email, name: user.name },
270
+ events: [{ event: 'login', payload: { method: 'google' }, id: `login-${session.id}` }],
271
+ });
269
272
  ```
270
273
 
271
274
  Identify the properties a journey reads before the event that starts it.
@@ -737,7 +740,7 @@ Discord; supply anything else with `journeyWorker({ messages })`.
737
740
  | Field | Holds |
738
741
  | ----------- | ----------------------------------------------------------------------------------------- |
739
742
  | `title` | The Studio title |
740
- | `contract` | The `defineContract(...)` every `/events` and `/identify` is validated against |
743
+ | `contract` | The `defineContract(...)` every `/ingest` is validated against |
741
744
  | `workflows` | The workflows to deploy |
742
745
  | `segments` | Named segments |
743
746
  | `actions` | Custom actions `.run()` uses |
@@ -779,24 +782,105 @@ throws `JourneyClientError` with its status and body.
779
782
  const journeys = new JourneyClient({ contract, origin, token });
780
783
  ```
781
784
 
782
- | Method | Endpoint | Does |
783
- | --------------------------------------------- | --------------------------------- | ------------------------------------------------------------------------- |
784
- | `identify(userId, props)` | `POST /identify` | Merge profile properties; `null` removes one |
785
- | `track(userId, event, payload, { id?, ts? })` | `POST /events` | Record an event; starts and wakes journeys |
786
- | `registerDevice(userId, { token, platform })` | `POST /devices` | Register a push token (`ios`, `android` or `web`) |
787
- | `unbindDevice(token)` | `POST /devices/unbind` | Signed out: the token stays, bound to nobody |
788
- | `removeDevice(token)` | `POST /devices/remove` | Forget a token for good |
789
- | `messageOpened(userId, messageId)` | `POST /messages/opened` | A journey push tapped open (the `message_id` it carried) |
790
- | `getSubscriptions(userId)` | `GET /subscriptions/…` | `{ email, push }`, each `{ all, topics }`, `true` = on |
791
- | `setSubscriptions(userId, patch)` | `PUT /subscriptions/…` | Change what the patch names; answers the new state |
792
- | `getSubscriptionsByMessage(messageId)` | `GET /subscriptions/by-message/…` | The recipient's record by a copy's id, plus `message: { channel, topic }` |
793
- | `setSubscriptionsByMessage(messageId, patch)` | `PUT /subscriptions/by-message/…` | The same patch, for the copy's recipient; answers as the GET does |
794
- | `forgetUser(userId)` | `POST /users/forget` | Stop the user's journeys and erase their data |
795
- | `deploy(bundle)` | `POST /deploy` | Deploy a compiled bundle |
796
-
797
- Pass an `id` to `track` whenever a caller may retry; see
785
+ | Method | Endpoint | Does |
786
+ | --------------------------------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------- |
787
+ | `ingest({ userId?, visitorId?, profile?, events? })` | `POST /ingest` | The one way in: merge a visitor, then the profile, then record the events in order |
788
+ | `registerDevice({ deviceId?, userId?, visitorId?, token, platform })` | `POST /devices` | Register a push token for whoever holds the device now |
789
+ | `unbindDevice(token)` | `POST /devices/unbind` | Signed out: the token stays, bound to nobody |
790
+ | `removeDevice(token)` | `POST /devices/remove` | Forget a token for good |
791
+ | `messageOpened(userId, messageId)` | `POST /messages/opened` | A journey push tapped open (the `message_id` it carried) |
792
+ | `getSubscriptions(userId)` | `GET /subscriptions/…` | `{ email, push }`, each `{ all, topics }`, `true` = on |
793
+ | `setSubscriptions(userId, patch)` | `PUT /subscriptions/…` | Change what the patch names; answers the new state |
794
+ | `getSubscriptionsByMessage(messageId)` | `GET /subscriptions/by-message/…` | The recipient's record by a copy's id, plus `message: { channel, topic }` |
795
+ | `setSubscriptionsByMessage(messageId, patch)` | `PUT /subscriptions/by-message/…` | The same patch, for the copy's recipient; answers as the GET does |
796
+ | `forgetUser(userId)` | `POST /users/forget` | Stop the user's journeys and erase their data |
797
+ | `forgetVisitor(visitorId)` | `POST /users/forget` | The same for a visitor who never signed in |
798
+ | `deploy(bundle)` | `POST /deploy` | Deploy a compiled bundle |
799
+
800
+ Pass each event's `id` whenever a caller may retry; see
798
801
  [Give each event an `id`](#give-each-event-an-id).
799
802
 
803
+ `ingest` is the only way to report: the profile and the events it belongs to travel
804
+ as one batch, so a journey an event starts always finds its recipient. With separate
805
+ calls an email step could run before the address arrives and settle as `no_recipient`.
806
+
807
+ ### Visitors and devices
808
+
809
+ Three ids, with different jobs:
810
+
811
+ | Id | What it says | At sign-out |
812
+ | ----------- | ------------------------------- | ---------------- |
813
+ | `userId` | Which account | No longer sent |
814
+ | `visitorId` | Who this is before signing in | Must be replaced |
815
+ | `deviceId` | Which device holds a push token | Stays |
816
+
817
+ `visitorId` is any stable id for one client, as long as it changes when a user signs
818
+ out; `visitorId()` from `@shware/analytics` already does. Journeys and events belong
819
+ to a user or a visitor, never to a device: one device can pass between people.
820
+
821
+ ```ts
822
+ await journeys.ingest({ visitorId, events }); // not signed in
823
+ await journeys.ingest({ userId, visitorId, profile, events }); // signed in
824
+ await journeys.registerDevice({ deviceId, visitorId, token, platform });
825
+ ```
826
+
827
+ Naming both ids says this visitor is this user. Their events, profile, devices and
828
+ opt-outs move to the user, once, and a later report of that visitor reaches the user.
829
+ A visitor already linked stays with its first user. Their running journeys carry on
830
+ as the user's, except one the user is already in, which stops.
831
+
832
+ A visitor enters a workflow only if its trigger asks for them. They have no address,
833
+ so such a journey reaches them by push, and an entry made as a visitor counts as the
834
+ user's once they sign in:
835
+
836
+ ```ts
837
+ workflow({ id: 'offer', trigger: trigger.event(e.browse, { visitors: true }) });
838
+ ```
839
+
840
+ Segments are about users: visitors are not swept and do not enter segment triggers.
841
+
842
+ A visitor who never signs in is deleted 90 days after they were last heard from, unless
843
+ a journey of theirs is still running. A link to a user is kept, so late reports still
844
+ reach the user and, once the user is erased, are refused.
845
+
846
+ A device table row holds only the current owner, so after a sign-out the previous user
847
+ can no longer be reached there. `unbindDevice` clears it at sign-out; the client then
848
+ registers again with its new `visitorId`.
849
+
850
+ ### `JourneyQueue`
851
+
852
+ `JourneyQueue` sends the same batch through a queue, for a producer that must
853
+ neither lose a report nor wait for the engine:
854
+
855
+ ```ts
856
+ const queue = new JourneyQueue({ contract, queue: env.JOURNEY_QUEUE });
857
+ await queue.ingest({
858
+ userId,
859
+ visitorId,
860
+ profile: { email, name },
861
+ events: [{ event: 'login', payload: { method }, id: rowId, ts }],
862
+ });
863
+ ```
864
+
865
+ Once `ingest` resolves the batch is durable. A rejection means it was not queued,
866
+ and the caller decides what follows. Export `journeys.queue` from the Worker and
867
+ declare the queue with a dead letter queue:
868
+
869
+ ```jsonc
870
+ "queues": {
871
+ "consumers": [
872
+ { "queue": "journey", "max_retries": 8, "dead_letter_queue": "journey-dead" }
873
+ ]
874
+ }
875
+ ```
876
+
877
+ The consumer retries a failed batch with a backoff from 30 seconds up to 15
878
+ minutes. It acknowledges a batch the contract refuses, logging its field paths,
879
+ and one for an erased user: neither can ever succeed. Delivery is at least once
880
+ and in no guaranteed order. Batches can arrive out of order, but event ids absorb
881
+ repeats and each profile property keeps its latest `at`, so send what must stay
882
+ in order as one batch.
883
+
800
884
  `getSubscriptions` and `setSubscriptions` are typed by the contract's
801
885
  `subscriptions.topics`, and a topic it does not declare is refused before the
802
886
  request. The record is topic × channel: `email` and `push` each list every
@@ -1031,12 +1115,12 @@ Checked against a real durable Lambda (Node 24, us-east-1).
1031
1115
  **Function URL: `AuthType: NONE`, with the API protected in the app.** Not every caller can
1032
1116
  sign a request:
1033
1117
 
1034
- | Caller | Routes |
1035
- | ------------------------------------------ | -------------------------------------------------------- |
1036
- | Your backend | `/events`, `/identify`, `/devices`, `/users/forget`, ... |
1037
- | Recipients' mail clients and browsers | `/t/o`, `/t/l`, `/t/u` |
1038
- | Gmail / Yahoo one-click unsubscribe | `POST /t/u/<message>` (RFC 8058) |
1039
- | iOS notification extension, service worker | `POST /t/d`, `POST /t/p` |
1118
+ | Caller | Routes |
1119
+ | ------------------------------------------ | ------------------------------------------- |
1120
+ | Your backend | `/ingest`, `/devices`, `/users/forget`, ... |
1121
+ | Recipients' mail clients and browsers | `/t/o`, `/t/l`, `/t/u` |
1122
+ | Gmail / Yahoo one-click unsubscribe | `POST /t/u/<message>` (RFC 8058) |
1123
+ | iOS notification extension, service worker | `POST /t/d`, `POST /t/p` |
1040
1124
 
1041
1125
  An alias has one URL and one auth type, and the tracking and one-click routes must stay public:
1042
1126
 
@@ -1197,7 +1281,9 @@ that version serves its first request, whatever share of traffic it has.
1197
1281
 
1198
1282
  `wrangler rollback` restores an older version, which does not apply itself over
1199
1283
  the newer one. Call `POST /bundle/deploy` after a rollback: it applies the
1200
- running version's bundle whatever is live.
1284
+ running version's bundle whatever is live, and records that version as the live
1285
+ one — so a newer version that serves again later puts its own bundle back, as a
1286
+ deploy always does.
1201
1287
 
1202
1288
  On Postgres the engine also keeps:
1203
1289
 
@@ -1223,16 +1309,19 @@ everything but `GET /health` answers 503.
1223
1309
  - `GET /health` is the only unauthenticated one. Every other route asks the
1224
1310
  host's `authorize`, or without one the `API_TOKEN` bearer: its scheme is
1225
1311
  matched case-insensitively and the token compared in constant time.
1226
- - `POST /events` takes `{ userId, event, payload?, ts?, id? }` and answers with
1227
- the id the occurrence was recorded under. Event names beginning with `$` are
1228
- reserved for internal use. `ts` is epoch milliseconds: a non-negative integer
1229
- no later than five minutes ahead of the host's clock (400 otherwise).
1230
- - `POST /identify` takes `{ userId, props }` and merges the properties into the
1231
- profile. Values are scalars (string, number, boolean); a property set to
1232
- `null` is removed, which is the only way to unset one. Nested objects are
1233
- refused with a 400.
1312
+ - `POST /ingest` takes `{ userId?, visitorId?, profile?, at?, events? }` with at
1313
+ least one id, each event being `{ event, payload?, ts?, id? }`, and answers
1314
+ `{ profile?, events }`, each event's result naming the id it was recorded under.
1315
+ The whole batch is checked first, one bad part refuses all of it; then a visitor
1316
+ merges, the profile is merged and the events recorded in order. Event names
1317
+ beginning with `$` are reserved, and a `userId` cannot begin with `visitor:`.
1318
+ `ts` is epoch milliseconds: a non-negative integer no later than five minutes
1319
+ ahead of the host's clock (400 otherwise). Profile values are scalars (string,
1320
+ number, boolean); a property set to `null` is removed, which is the only way to
1321
+ unset one. Nested objects are refused with a 400. A property written with a later
1322
+ `at` keeps its value.
1234
1323
  - A user with an erasure tombstone (`POST /users/forget`, finished or not) is
1235
- refused on `/events`, `/identify` and `/devices` with a 410: nothing is
1324
+ refused on `/ingest` and `/devices` with a 410: nothing is
1236
1325
  written about them between the stop of their journeys and the erase, and the
1237
1326
  id is never reactivated after it.
1238
1327
  - A body that is not JSON answers 400, never 500.
@@ -1304,6 +1393,14 @@ exception is an entry whose instance died: that row records a failure, not a
1304
1393
  journey the user received, so the next trigger reclaims it. Re-entry beyond that
1305
1394
  is a design question this package does not yet answer.
1306
1395
 
1396
+ The entry commits before the platform creates the instance, so a host cut off
1397
+ between the two leaves an entry with nothing behind it. Two things repair it: a
1398
+ retry of the same event (same `id` and `ts`) creates the missing instance — the
1399
+ instance id is the platform's identity too, so a second create is harmless — and
1400
+ the sweep's maintenance marks an entry left unadmitted for 15 minutes, whose
1401
+ instance the runtime does not know or reports ended, as failed, so the user's
1402
+ next trigger re-enters. Neither can start a second journey for the pair.
1403
+
1307
1404
  ## Exports
1308
1405
 
1309
1406
  | Subpath | Contents |
@@ -1,5 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_engine_retry = require("../engine/retry.cjs");
3
+ const require_store_identity = require("../store/identity.cjs");
4
+ require("../store/index.cjs");
3
5
  let _aws_durable_execution_sdk_js = require("@aws/durable-execution-sdk-js");
4
6
  // src/aws/engine-step.ts
5
7
  /**
@@ -57,7 +59,8 @@ var AwsEngineStep = class {
57
59
  settled: false
58
60
  };
59
61
  await this.context.step(name, async () => {
60
- await this.store.subscribe(this.instanceId, this.userId, events, Date.now());
62
+ const owner = await require_store_identity.instanceOwner(this.store, this.instanceId, this.userId);
63
+ await this.store.subscribe(this.instanceId, owner, events, Date.now());
61
64
  await this.executions.armCallback(this.instanceId, {
62
65
  id: callbackId,
63
66
  generation,
@@ -1 +1 @@
1
- {"version":3,"file":"engine-step.cjs","names":["isRetryable","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 { isRetryable } from '../engine/retry';\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/** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */\nexport function stepRetry(error: Error, attempt: number): RetryDecision {\n if (!isRetryable(error) || 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 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 /**\n * The timeout is ignored on purpose: the callback's own timeout was set from the deadline\n * when it was armed (`subscribe`), and only its expiry says the wait timed out.\n */\n async waitForWake(\n name: string,\n _wait: { timeoutMs: number; deadlineMs: number }\n ): 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":";;;;;;;;;;;;;;;AAuBA,SAAgB,UAAU,OAAc,SAAgC;CACtE,IAAI,CAACA,qBAAAA,YAAY,KAAK,KAAK,WAAW,GAAG,OAAO,EAAE,aAAa,MAAM;CACrE,OAAO;EAAE,aAAa;EAAM,OAAO,EAAE,SAAS,KAAK,IAAI,IAAI,KAAK,OAAO,EAAE;CAAE;AAC7E;AAUA,IAAa,gBAAb,MAAiD;CAK5B;CACA;CACA;CACA;CACA;CACA;CATnB,aAAqB;CACrB;CAEA,YACE,SACA,OACA,YACA,QACA,QACA,YACA;EANiB,KAAA,UAAA;EACA,KAAA,QAAA;EACA,KAAA,aAAA;EACA,KAAA,SAAA;EACA,KAAA,SAAA;EACA,KAAA,aAAA;CAChB;CAEH,GAAM,MAAc,IAAkC;EACpD,OAAO,KAAK,QAAQ,KAAK,YAAY,GAAG,GAAG,EAAE,eAAe,UAAU,CAAC;CACzE;CAEA,MAAM,MAAM,MAAc,IAA2B;EAEnD,IAAI,EAAE,KAAK,IAAI;EACf,MAAM,KAAK,QAAQ,KAAK,MAAM,EAAE,SAAS,KAAK,KAAK,KAAK,GAAI,EAAE,CAAC;CACjE;CAEA,MAAM,UACJ,MACA,QACA,UAAmC,CAAC,GACrB;EACf,MAAM,aAAa,EAAE,KAAK;EAC1B,MAAM,EAAE,eAAe;EAEvB,MAAM,UACJ,eAAe,KAAA,IACX,KAAA,IACA,MAAM,KAAK,QAAQ,KAAK,GAAG,KAAK,WAAW,YACzC,KAAK,IAAI,GAAG,KAAK,MAAM,aAAa,KAAK,IAAI,KAAK,GAAI,CAAC,CACzD;EACN,MAAM,CAAC,SAAS,cAAc,MAAM,KAAK,QAAQ,eAC/C,GAAG,KAAK,YACR,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CACtD;EACA,MAAM,WAAW,KAAK;EACtB,KAAK,QAAQ;GAAE;GAAS;GAAY;GAAY,SAAS;EAAM;EAC/D,MAAM,KAAK,QAAQ,KACjB,MACA,YAAY;GAGV,MAAM,KAAK,MAAM,UAAU,KAAK,YAAY,KAAK,QAAQ,QAAQ,KAAK,IAAI,CAAC;GAC3E,MAAM,KAAK,WAAW,YAAY,KAAK,YAAY;IACjD,IAAI;IACJ;IACA,YAAY;GACd,CAAC;GACD,IAAI,aAAa,KAAA,KAAa,CAAC,SAAS,SACtC,MAAM,KAAK,OAAO,iBAAiB,SAAS,YAAY,IAAI;EAEhE,GACA,EAAE,eAAe,UAAU,CAC7B;EACA,IAAI,aAAa,KAAA,GAAW,SAAS,UAAU;CACjD;CAEA,MAAM,YAAY,MAA6B;EAC7C,MAAM,QAAQ,KAAK;EACnB,MAAM,KAAK,QAAQ,KACjB,MACA,YAAY;GACV,MAAM,KAAK,MAAM,YAAY,KAAK,UAAU;GAC5C,IAAI,UAAU,KAAA,GAAW;GACzB,MAAM,KAAK,WAAW,cAAc,KAAK,YAAY,MAAM,UAAU;GAGrE,IAAI,CAAC,MAAM,SAAS,MAAM,KAAK,OAAO,iBAAiB,MAAM,YAAY,IAAI;EAC/E,GACA,EAAE,eAAe,UAAU,CAC7B;EACA,IAAI,UAAU,KAAA,GAAW,MAAM,UAAU;EACzC,KAAK,QAAQ,KAAA;CACf;;;;;CAMA,MAAM,YACJ,MACA,OAC8B;EAC9B,MAAM,QAAQ,KAAK;EACnB,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,gBAAgB,KAAK,mDAAmD;EAE1F,IAAI;GACF,MAAM,MAAM;GACZ,OAAO;EACT,SAAS,OAAO;GAEd,IAAI,iBAAiBC,8BAAAA,sBAAsB,OAAO;GAClD,MAAM;EACR,UAAU;GACR,MAAM,UAAU;EAClB;CACF;AACF"}
1
+ {"version":3,"file":"engine-step.cjs","names":["isRetryable","instanceOwner","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 { isRetryable } from '../engine/retry';\nimport { type ExecutionRegistry, type JourneyStore, instanceOwner } 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/** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */\nexport function stepRetry(error: Error, attempt: number): RetryDecision {\n if (!isRetryable(error) || 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 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 const owner = await instanceOwner(this.store, this.instanceId, this.userId);\n await this.store.subscribe(this.instanceId, owner, 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 /**\n * The timeout is ignored on purpose: the callback's own timeout was set from the deadline\n * when it was armed (`subscribe`), and only its expiry says the wait timed out.\n */\n async waitForWake(\n name: string,\n _wait: { timeoutMs: number; deadlineMs: number }\n ): 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":";;;;;;;;;;;;;;;;;AAuBA,SAAgB,UAAU,OAAc,SAAgC;CACtE,IAAI,CAACA,qBAAAA,YAAY,KAAK,KAAK,WAAW,GAAG,OAAO,EAAE,aAAa,MAAM;CACrE,OAAO;EAAE,aAAa;EAAM,OAAO,EAAE,SAAS,KAAK,IAAI,IAAI,KAAK,OAAO,EAAE;CAAE;AAC7E;AAUA,IAAa,gBAAb,MAAiD;CAK5B;CACA;CACA;CACA;CACA;CACA;CATnB,aAAqB;CACrB;CAEA,YACE,SACA,OACA,YACA,QACA,QACA,YACA;EANiB,KAAA,UAAA;EACA,KAAA,QAAA;EACA,KAAA,aAAA;EACA,KAAA,SAAA;EACA,KAAA,SAAA;EACA,KAAA,aAAA;CAChB;CAEH,GAAM,MAAc,IAAkC;EACpD,OAAO,KAAK,QAAQ,KAAK,YAAY,GAAG,GAAG,EAAE,eAAe,UAAU,CAAC;CACzE;CAEA,MAAM,MAAM,MAAc,IAA2B;EAEnD,IAAI,EAAE,KAAK,IAAI;EACf,MAAM,KAAK,QAAQ,KAAK,MAAM,EAAE,SAAS,KAAK,KAAK,KAAK,GAAI,EAAE,CAAC;CACjE;CAEA,MAAM,UACJ,MACA,QACA,UAAmC,CAAC,GACrB;EACf,MAAM,aAAa,EAAE,KAAK;EAC1B,MAAM,EAAE,eAAe;EAEvB,MAAM,UACJ,eAAe,KAAA,IACX,KAAA,IACA,MAAM,KAAK,QAAQ,KAAK,GAAG,KAAK,WAAW,YACzC,KAAK,IAAI,GAAG,KAAK,MAAM,aAAa,KAAK,IAAI,KAAK,GAAI,CAAC,CACzD;EACN,MAAM,CAAC,SAAS,cAAc,MAAM,KAAK,QAAQ,eAC/C,GAAG,KAAK,YACR,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CACtD;EACA,MAAM,WAAW,KAAK;EACtB,KAAK,QAAQ;GAAE;GAAS;GAAY;GAAY,SAAS;EAAM;EAC/D,MAAM,KAAK,QAAQ,KACjB,MACA,YAAY;GAGV,MAAM,QAAQ,MAAMC,uBAAAA,cAAc,KAAK,OAAO,KAAK,YAAY,KAAK,MAAM;GAC1E,MAAM,KAAK,MAAM,UAAU,KAAK,YAAY,OAAO,QAAQ,KAAK,IAAI,CAAC;GACrE,MAAM,KAAK,WAAW,YAAY,KAAK,YAAY;IACjD,IAAI;IACJ;IACA,YAAY;GACd,CAAC;GACD,IAAI,aAAa,KAAA,KAAa,CAAC,SAAS,SACtC,MAAM,KAAK,OAAO,iBAAiB,SAAS,YAAY,IAAI;EAEhE,GACA,EAAE,eAAe,UAAU,CAC7B;EACA,IAAI,aAAa,KAAA,GAAW,SAAS,UAAU;CACjD;CAEA,MAAM,YAAY,MAA6B;EAC7C,MAAM,QAAQ,KAAK;EACnB,MAAM,KAAK,QAAQ,KACjB,MACA,YAAY;GACV,MAAM,KAAK,MAAM,YAAY,KAAK,UAAU;GAC5C,IAAI,UAAU,KAAA,GAAW;GACzB,MAAM,KAAK,WAAW,cAAc,KAAK,YAAY,MAAM,UAAU;GAGrE,IAAI,CAAC,MAAM,SAAS,MAAM,KAAK,OAAO,iBAAiB,MAAM,YAAY,IAAI;EAC/E,GACA,EAAE,eAAe,UAAU,CAC7B;EACA,IAAI,UAAU,KAAA,GAAW,MAAM,UAAU;EACzC,KAAK,QAAQ,KAAA;CACf;;;;;CAMA,MAAM,YACJ,MACA,OAC8B;EAC9B,MAAM,QAAQ,KAAK;EACnB,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,gBAAgB,KAAK,mDAAmD;EAE1F,IAAI;GACF,MAAM,MAAM;GACZ,OAAO;EACT,SAAS,OAAO;GAEd,IAAI,iBAAiBC,8BAAAA,sBAAsB,OAAO;GAClD,MAAM;EACR,UAAU;GACR,MAAM,UAAU;EAClB;CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"engine-step.d.cts","names":[],"sources":["../../src/aws/engine-step.ts"],"mappings":";;;;;;;;;;;;;;;wBAuBgB,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,UACJ,cACA,2BACA;IAAW;MACV;EAoCG,YAAY,eAAe;;;;;EAsB3B,YACJ,cACA;IAAS;IAAmB;MAC3B"}
1
+ {"version":3,"file":"engine-step.d.cts","names":[],"sources":["../../src/aws/engine-step.ts"],"mappings":";;;;;;;;;;;;;;;wBAuBgB,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,UACJ,cACA,2BACA;IAAW;MACV;EAqCG,YAAY,eAAe;;;;;EAsB3B,YACJ,cACA;IAAS;IAAmB;MAC3B"}
@@ -1 +1 @@
1
- {"version":3,"file":"engine-step.d.mts","names":[],"sources":["../../src/aws/engine-step.ts"],"mappings":";;;;;;;;;;;;;;;wBAuBgB,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,UACJ,cACA,2BACA;IAAW;MACV;EAoCG,YAAY,eAAe;;;;;EAsB3B,YACJ,cACA;IAAS;IAAmB;MAC3B"}
1
+ {"version":3,"file":"engine-step.d.mts","names":[],"sources":["../../src/aws/engine-step.ts"],"mappings":";;;;;;;;;;;;;;;wBAuBgB,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,UACJ,cACA,2BACA;IAAW;MACV;EAqCG,YAAY,eAAe;;;;;EAsB3B,YACJ,cACA;IAAS;IAAmB;MAC3B"}
@@ -1,4 +1,6 @@
1
1
  import { isRetryable } from "../engine/retry.mjs";
2
+ import { instanceOwner } from "../store/identity.mjs";
3
+ import "../store/index.mjs";
2
4
  import { CallbackTimeoutError } from "@aws/durable-execution-sdk-js";
3
5
  // src/aws/engine-step.ts
4
6
  /**
@@ -56,7 +58,8 @@ var AwsEngineStep = class {
56
58
  settled: false
57
59
  };
58
60
  await this.context.step(name, async () => {
59
- await this.store.subscribe(this.instanceId, this.userId, events, Date.now());
61
+ const owner = await instanceOwner(this.store, this.instanceId, this.userId);
62
+ await this.store.subscribe(this.instanceId, owner, events, Date.now());
60
63
  await this.executions.armCallback(this.instanceId, {
61
64
  id: callbackId,
62
65
  generation,
@@ -1 +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 { isRetryable } from '../engine/retry';\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/** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */\nexport function stepRetry(error: Error, attempt: number): RetryDecision {\n if (!isRetryable(error) || 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 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 /**\n * The timeout is ignored on purpose: the callback's own timeout was set from the deadline\n * when it was armed (`subscribe`), and only its expiry says the wait timed out.\n */\n async waitForWake(\n name: string,\n _wait: { timeoutMs: number; deadlineMs: number }\n ): 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":";;;;;;;;;;;;;;AAuBA,SAAgB,UAAU,OAAc,SAAgC;CACtE,IAAI,CAAC,YAAY,KAAK,KAAK,WAAW,GAAG,OAAO,EAAE,aAAa,MAAM;CACrE,OAAO;EAAE,aAAa;EAAM,OAAO,EAAE,SAAS,KAAK,IAAI,IAAI,KAAK,OAAO,EAAE;CAAE;AAC7E;AAUA,IAAa,gBAAb,MAAiD;CAK5B;CACA;CACA;CACA;CACA;CACA;CATnB,aAAqB;CACrB;CAEA,YACE,SACA,OACA,YACA,QACA,QACA,YACA;EANiB,KAAA,UAAA;EACA,KAAA,QAAA;EACA,KAAA,aAAA;EACA,KAAA,SAAA;EACA,KAAA,SAAA;EACA,KAAA,aAAA;CAChB;CAEH,GAAM,MAAc,IAAkC;EACpD,OAAO,KAAK,QAAQ,KAAK,YAAY,GAAG,GAAG,EAAE,eAAe,UAAU,CAAC;CACzE;CAEA,MAAM,MAAM,MAAc,IAA2B;EAEnD,IAAI,EAAE,KAAK,IAAI;EACf,MAAM,KAAK,QAAQ,KAAK,MAAM,EAAE,SAAS,KAAK,KAAK,KAAK,GAAI,EAAE,CAAC;CACjE;CAEA,MAAM,UACJ,MACA,QACA,UAAmC,CAAC,GACrB;EACf,MAAM,aAAa,EAAE,KAAK;EAC1B,MAAM,EAAE,eAAe;EAEvB,MAAM,UACJ,eAAe,KAAA,IACX,KAAA,IACA,MAAM,KAAK,QAAQ,KAAK,GAAG,KAAK,WAAW,YACzC,KAAK,IAAI,GAAG,KAAK,MAAM,aAAa,KAAK,IAAI,KAAK,GAAI,CAAC,CACzD;EACN,MAAM,CAAC,SAAS,cAAc,MAAM,KAAK,QAAQ,eAC/C,GAAG,KAAK,YACR,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CACtD;EACA,MAAM,WAAW,KAAK;EACtB,KAAK,QAAQ;GAAE;GAAS;GAAY;GAAY,SAAS;EAAM;EAC/D,MAAM,KAAK,QAAQ,KACjB,MACA,YAAY;GAGV,MAAM,KAAK,MAAM,UAAU,KAAK,YAAY,KAAK,QAAQ,QAAQ,KAAK,IAAI,CAAC;GAC3E,MAAM,KAAK,WAAW,YAAY,KAAK,YAAY;IACjD,IAAI;IACJ;IACA,YAAY;GACd,CAAC;GACD,IAAI,aAAa,KAAA,KAAa,CAAC,SAAS,SACtC,MAAM,KAAK,OAAO,iBAAiB,SAAS,YAAY,IAAI;EAEhE,GACA,EAAE,eAAe,UAAU,CAC7B;EACA,IAAI,aAAa,KAAA,GAAW,SAAS,UAAU;CACjD;CAEA,MAAM,YAAY,MAA6B;EAC7C,MAAM,QAAQ,KAAK;EACnB,MAAM,KAAK,QAAQ,KACjB,MACA,YAAY;GACV,MAAM,KAAK,MAAM,YAAY,KAAK,UAAU;GAC5C,IAAI,UAAU,KAAA,GAAW;GACzB,MAAM,KAAK,WAAW,cAAc,KAAK,YAAY,MAAM,UAAU;GAGrE,IAAI,CAAC,MAAM,SAAS,MAAM,KAAK,OAAO,iBAAiB,MAAM,YAAY,IAAI;EAC/E,GACA,EAAE,eAAe,UAAU,CAC7B;EACA,IAAI,UAAU,KAAA,GAAW,MAAM,UAAU;EACzC,KAAK,QAAQ,KAAA;CACf;;;;;CAMA,MAAM,YACJ,MACA,OAC8B;EAC9B,MAAM,QAAQ,KAAK;EACnB,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,gBAAgB,KAAK,mDAAmD;EAE1F,IAAI;GACF,MAAM,MAAM;GACZ,OAAO;EACT,SAAS,OAAO;GAEd,IAAI,iBAAiB,sBAAsB,OAAO;GAClD,MAAM;EACR,UAAU;GACR,MAAM,UAAU;EAClB;CACF;AACF"}
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 { isRetryable } from '../engine/retry';\nimport { type ExecutionRegistry, type JourneyStore, instanceOwner } 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/** What the step retries, bounded: 6 attempts with exponential backoff up to a minute. */\nexport function stepRetry(error: Error, attempt: number): RetryDecision {\n if (!isRetryable(error) || 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 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 const owner = await instanceOwner(this.store, this.instanceId, this.userId);\n await this.store.subscribe(this.instanceId, owner, 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 /**\n * The timeout is ignored on purpose: the callback's own timeout was set from the deadline\n * when it was armed (`subscribe`), and only its expiry says the wait timed out.\n */\n async waitForWake(\n name: string,\n _wait: { timeoutMs: number; deadlineMs: number }\n ): 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":";;;;;;;;;;;;;;;;AAuBA,SAAgB,UAAU,OAAc,SAAgC;CACtE,IAAI,CAAC,YAAY,KAAK,KAAK,WAAW,GAAG,OAAO,EAAE,aAAa,MAAM;CACrE,OAAO;EAAE,aAAa;EAAM,OAAO,EAAE,SAAS,KAAK,IAAI,IAAI,KAAK,OAAO,EAAE;CAAE;AAC7E;AAUA,IAAa,gBAAb,MAAiD;CAK5B;CACA;CACA;CACA;CACA;CACA;CATnB,aAAqB;CACrB;CAEA,YACE,SACA,OACA,YACA,QACA,QACA,YACA;EANiB,KAAA,UAAA;EACA,KAAA,QAAA;EACA,KAAA,aAAA;EACA,KAAA,SAAA;EACA,KAAA,SAAA;EACA,KAAA,aAAA;CAChB;CAEH,GAAM,MAAc,IAAkC;EACpD,OAAO,KAAK,QAAQ,KAAK,YAAY,GAAG,GAAG,EAAE,eAAe,UAAU,CAAC;CACzE;CAEA,MAAM,MAAM,MAAc,IAA2B;EAEnD,IAAI,EAAE,KAAK,IAAI;EACf,MAAM,KAAK,QAAQ,KAAK,MAAM,EAAE,SAAS,KAAK,KAAK,KAAK,GAAI,EAAE,CAAC;CACjE;CAEA,MAAM,UACJ,MACA,QACA,UAAmC,CAAC,GACrB;EACf,MAAM,aAAa,EAAE,KAAK;EAC1B,MAAM,EAAE,eAAe;EAEvB,MAAM,UACJ,eAAe,KAAA,IACX,KAAA,IACA,MAAM,KAAK,QAAQ,KAAK,GAAG,KAAK,WAAW,YACzC,KAAK,IAAI,GAAG,KAAK,MAAM,aAAa,KAAK,IAAI,KAAK,GAAI,CAAC,CACzD;EACN,MAAM,CAAC,SAAS,cAAc,MAAM,KAAK,QAAQ,eAC/C,GAAG,KAAK,YACR,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CACtD;EACA,MAAM,WAAW,KAAK;EACtB,KAAK,QAAQ;GAAE;GAAS;GAAY;GAAY,SAAS;EAAM;EAC/D,MAAM,KAAK,QAAQ,KACjB,MACA,YAAY;GAGV,MAAM,QAAQ,MAAM,cAAc,KAAK,OAAO,KAAK,YAAY,KAAK,MAAM;GAC1E,MAAM,KAAK,MAAM,UAAU,KAAK,YAAY,OAAO,QAAQ,KAAK,IAAI,CAAC;GACrE,MAAM,KAAK,WAAW,YAAY,KAAK,YAAY;IACjD,IAAI;IACJ;IACA,YAAY;GACd,CAAC;GACD,IAAI,aAAa,KAAA,KAAa,CAAC,SAAS,SACtC,MAAM,KAAK,OAAO,iBAAiB,SAAS,YAAY,IAAI;EAEhE,GACA,EAAE,eAAe,UAAU,CAC7B;EACA,IAAI,aAAa,KAAA,GAAW,SAAS,UAAU;CACjD;CAEA,MAAM,YAAY,MAA6B;EAC7C,MAAM,QAAQ,KAAK;EACnB,MAAM,KAAK,QAAQ,KACjB,MACA,YAAY;GACV,MAAM,KAAK,MAAM,YAAY,KAAK,UAAU;GAC5C,IAAI,UAAU,KAAA,GAAW;GACzB,MAAM,KAAK,WAAW,cAAc,KAAK,YAAY,MAAM,UAAU;GAGrE,IAAI,CAAC,MAAM,SAAS,MAAM,KAAK,OAAO,iBAAiB,MAAM,YAAY,IAAI;EAC/E,GACA,EAAE,eAAe,UAAU,CAC7B;EACA,IAAI,UAAU,KAAA,GAAW,MAAM,UAAU;EACzC,KAAK,QAAQ,KAAA;CACf;;;;;CAMA,MAAM,YACJ,MACA,OAC8B;EAC9B,MAAM,QAAQ,KAAK;EACnB,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,gBAAgB,KAAK,mDAAmD;EAE1F,IAAI;GACF,MAAM,MAAM;GACZ,OAAO;EACT,SAAS,OAAO;GAEd,IAAI,iBAAiB,sBAAsB,OAAO;GAClD,MAAM;EACR,UAAU;GACR,MAAM,UAAU;EAClB;CACF;AACF"}
@@ -124,8 +124,9 @@ function journeyLambda(options) {
124
124
  const { store, env } = open(context);
125
125
  const request = require_aws_function_url.toRequest(event);
126
126
  if (new URL(request.url).pathname === "/deploy") return await require_aws_function_url.fromResponse(json({ error: "deploy by publishing a version (POST /bundle/deploy applies its own bundle)" }, 409), method);
127
- await host.applyOwnBundle(env, bundleVersion());
128
- return await require_aws_function_url.fromResponse(await host.handle(request, env, store), method);
127
+ const version = bundleVersion();
128
+ await host.applyOwnBundle(env, version);
129
+ return await require_aws_function_url.fromResponse(await host.handle(request, env, store, version), method);
129
130
  } catch (error) {
130
131
  console.error("[workflow] request failed", error);
131
132
  return require_aws_function_url.fromResponse(json({ error: "internal error" }, 500), method);
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["createJourneyHost","http2Fetch","longestJourneyMs","sdkLambdaApi","AsyncLocalStorage","AwsInstances","readWithRetry","PermanentError","executeJourney","AwsEngineStep","toRequest","fromResponse","sweepMaintenance","sweepSegmentsPaged","withDurableExecution","isFunctionUrlEvent","isRunInput"],"sources":["../../src/aws/index.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport {\n type DurableContext,\n type DurableExecutionInvocationInput,\n type DurableExecutionInvocationOutput,\n withDurableExecution,\n} from '@aws/durable-execution-sdk-js';\nimport { longestJourneyMs } from '../engine/duration';\nimport type { JourneyOutcome } from '../engine/ports';\nimport { PermanentError } from '../engine/retry';\nimport { executeJourney } from '../host/execute';\nimport { readWithRetry } from '../host/fresh-read';\nimport {\n SWEEP_PAGE_SIZE,\n type SweepResult,\n sweepMaintenance,\n sweepSegmentsPaged,\n} from '../host/ingest';\nimport { type JourneyHostOptions, createJourneyHost } from '../host/journey-host';\nimport type { JourneyEnv } from '../host/platform';\nimport { http2Fetch } from '../http2-client';\nimport type { BundleVersion, ExecutionRegistry, JourneyStore, StoreLease } from '../store/index';\nimport { AwsEngineStep } from './engine-step';\nimport {\n type FunctionUrlEvent,\n type FunctionUrlResult,\n fromResponse,\n isFunctionUrlEvent,\n toRequest,\n} from './function-url';\nimport { AwsInstances, type RunInput, isRunInput } from './instances';\nimport { type LambdaApi, sdkLambdaApi } from './lambda-api';\n\n/**\n * `@shware/workflow/aws` — the journey host as one Lambda durable function behind a Function\n * URL, on Postgres.\n *\n * The one handler tells three inputs apart by the event's top-level shape: a Function URL\n * request (the HTTP API — ingest, identify, devices, tracking, bundle, preview), an internal run\n * (one journey instance: one durable execution, started by an asynchronous invoke of this\n * function's own numbered version), and a scheduled maintenance tick. An HTTP body can never\n * pass for the other two: the Function URL always wraps it.\n *\n * const journeys = journeyLambda({\n * config,\n * env, // parsed from process.env (zod)\n * store: (env, database) => postgresJsStore(env.DATABASE_URL, database),\n * email: (env) => sesEmailTransport({ region: env.AWS_REGION }),\n * });\n * export const handler = journeys.handler;\n */\n\n/** The scheduled tick (EventBridge Scheduler's payload): the segment sweep and maintenance. */\nexport const SWEEP_EVENT_TYPE = '$workflow.sweep';\n\n/** What journeyLambda reads from its env: `JourneyEnv` without the binding it supplies itself. */\nexport type JourneyLambdaEnv = Omit<JourneyEnv, 'JOURNEY' | 'DB' | 'EMAIL'>;\n\nexport interface JourneyLambdaOptions<Env extends JourneyLambdaEnv> extends Omit<\n JourneyHostOptions<Env & JourneyEnv>,\n 'env' | 'store'\n> {\n /** The validated environment (parsed from `process.env` with zod by the app). */\n env: Env;\n /** The Postgres store: AWS needs its execution registry (`postgresJsStore`). */\n store: (env: Env, database: { schema: string }) => JourneyStore | StoreLease;\n /** The Lambda control plane; `@aws-sdk/client-lambda` with the execution role by default. */\n lambda?: LambdaApi;\n /** How long an unconfirmed start is retried (default 12 hours; keep it under the retention). */\n startRetryMs?: number;\n /**\n * How long an erasure waits after stopping an execution before it erases: an invocation\n * running at the stop goes on to its next checkpoint. Set it to at least the function's\n * timeout; 15 minutes (Lambda's maximum) by default.\n */\n stopDrainMs?: number;\n /**\n * The function's `DurableConfig.ExecutionTimeout`, in milliseconds. With it, a workflow that\n * can outlive it (`longestJourneyMs`) fails the handler's creation — a deploy's smoke test —\n * instead of being stopped by the platform midway and recorded as failed.\n */\n executionTimeoutMs?: number;\n}\n\n/**\n * The Lambda invocation context, as far as the handler's signature needs it — declared here so\n * the package's types do not depend on `@types/aws-lambda`. Lambda passes the full object.\n */\nexport interface LambdaContext {\n functionName: string;\n functionVersion: string;\n invokedFunctionArn: string;\n awsRequestId: string;\n getRemainingTimeInMillis(): number;\n}\n\nexport interface JourneyLambda {\n /** Export as the function's handler. */\n handler: (\n event: DurableExecutionInvocationInput,\n context: LambdaContext\n ) => Promise<DurableExecutionInvocationOutput>;\n}\n\ninterface Invocation {\n lease?: Required<StoreLease>;\n}\n\n/** This function's ARN without its qualifier, from the ARN it was invoked under. */\nfunction unqualified(invokedFunctionArn: string): string {\n return invokedFunctionArn.split(':').slice(0, 7).join(':');\n}\n\nconst json = (body: unknown, status = 200) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });\n\nexport function journeyLambda<Env extends JourneyLambdaEnv>(\n options: JourneyLambdaOptions<Env>\n): JourneyLambda {\n type HostEnv = Env & JourneyEnv;\n const {\n env: baseEnv,\n lambda: lambdaOption,\n startRetryMs,\n stopDrainMs,\n executionTimeoutMs,\n ...hostOptions\n } = options;\n const host = createJourneyHost<HostEnv>(\n { ...hostOptions, store: (env, database) => options.store(env, database) },\n // Node 24's fetch cannot reach APNs (HTTP/2 only): see http2-client.ts for when this can go\n { apnsFetch: http2Fetch() }\n );\n if (executionTimeoutMs !== undefined) {\n const tooLong = host\n .bundle()\n .workflows.filter((workflow) => longestJourneyMs(workflow) > executionTimeoutMs);\n if (tooLong.length > 0) {\n const days = (ms: number) => `${Math.ceil(ms / 86_400_000)} days`;\n throw new RangeError(\n `${tooLong.map((w) => `'${w.id}' can run ${days(longestJourneyMs(w))}`).join(', ')}, ` +\n `longer than the execution timeout (${days(executionTimeoutMs)}): raise ExecutionTimeout (up to 366 days)`\n );\n }\n }\n const lambda = lambdaOption ?? sdkLambdaApi();\n const ownHashes = new Set(host.bundle().workflows.map((workflow) => workflow.contentHash));\n const scope = new AsyncLocalStorage<Invocation>();\n\n /** The version this code was published as: `$LATEST` while developing. */\n const ownVersion = () => process.env.AWS_LAMBDA_FUNCTION_VERSION ?? '$LATEST';\n /** Numbered versions are ordered: the bundle compiled into a later one goes live by itself. */\n const bundleVersion = (): BundleVersion | undefined => {\n const version = ownVersion();\n return /^\\d+$/.test(version) ? { id: version, createdAt: Number(version) } : undefined;\n };\n\n /** The store and the env (with the instance port) of this invocation, opened on first use. */\n const open = (\n context: DurableContext\n ): { store: JourneyStore; env: HostEnv; executions: ExecutionRegistry } => {\n const invocation = scope.getStore();\n if (invocation === undefined) throw new Error('journeyLambda: outside an invocation');\n const placeholder = { ...baseEnv } as unknown as HostEnv;\n invocation.lease ??= host.lease(placeholder);\n const { store } = invocation.lease;\n const { executions } = store;\n const functionArn = unqualified(context.lambdaContext.invokedFunctionArn);\n const target = async (contentHash: string, version?: string): Promise<string> => {\n // An execution runs the code its IR was compiled with: the version whose bundle put the\n // route there, read in the same row as the hash — never a separately read \"live\" version,\n // which a release between the two reads would make another bundle's\n if (version !== undefined && /^\\d+$/.test(version)) return `${functionArn}:${version}`;\n // A forced deploy records no version: only this code's own workflows can run here\n if (ownHashes.has(contentHash)) return `${functionArn}:${ownVersion()}`;\n throw new Error(`no published version is known to carry workflow IR ${contentHash}`);\n };\n const instances = new AwsInstances({\n store,\n executions,\n lambda,\n target,\n ...(startRetryMs === undefined ? {} : { startRetryMs }),\n ...(stopDrainMs === undefined ? {} : { stopDrainMs }),\n });\n return { store, env: { ...baseEnv, JOURNEY: instances }, executions };\n };\n\n const run = async (input: RunInput, context: DurableContext): Promise<JourneyOutcome> => {\n const { store, env, executions } = open(context);\n const { instanceId, params } = input;\n // Only a start the router recorded may run, byte for byte: the function can be invoked by\n // anything its policy lets through, and an input of this shape would otherwise run any\n // workflow for any user. A stopped execution (an erasure) may not come back either. Outside\n // any step — a step would replay the first invocation's \"not stopped\" on every wake — so the\n // execution fails at once on a refusal; only the read itself is retried, briefly, when the\n // database stumbled.\n const record = await readWithRetry(() => executions.get(instanceId), {\n what: 'run entry',\n instanceId,\n });\n if (\n record === undefined ||\n record.stoppedAt !== undefined ||\n record.input !== JSON.stringify(input)\n ) {\n throw new PermanentError('journeyLambda: run input is not a recorded start');\n }\n // Running is the proof the start got through, whatever became of the invoke's response\n await context.step('$workflow:started', async () => {\n await executions.markSubmitted(\n instanceId,\n context.executionContext.durableExecutionArn,\n Date.now()\n );\n });\n return executeJourney(\n {\n instanceId,\n params,\n enteredAtMs: input.enteredAtMs,\n step: new AwsEngineStep(context, store, executions, lambda, params.userId, instanceId),\n },\n host.runParts(env, store, params)\n );\n };\n\n const http = (event: FunctionUrlEvent, context: DurableContext): Promise<FunctionUrlResult> => {\n const serve = async (): Promise<FunctionUrlResult> => {\n const method = event.requestContext.http.method;\n try {\n const { store, env } = open(context);\n const request = toRequest(event);\n // Only the bundle compiled into a version may route: one is never swapped in by hand\n if (new URL(request.url).pathname === '/deploy') {\n return await fromResponse(\n json(\n {\n error:\n 'deploy by publishing a version (POST /bundle/deploy applies its own bundle)',\n },\n 409\n ),\n method\n );\n }\n await host.applyOwnBundle(env, bundleVersion());\n return await fromResponse(await host.handle(request, env, store), method);\n } catch (error) {\n console.error('[workflow] request failed', error);\n return fromResponse(json({ error: 'internal error' }, 500), method);\n }\n };\n // A read (the bundle, a preview, a tracked link or pixel) answers straight from the handler:\n // its body can be large, and a step would checkpoint all of it. A write runs in one step,\n // never retried — the caller is waiting, and retries the request itself\n const method = event.requestContext.http.method.toUpperCase();\n if (method === 'GET' || method === 'HEAD') return serve();\n return context.step('http', serve, { retryStrategy: () => ({ shouldRetry: false }) });\n };\n\n /**\n * The sweep as durable steps, one per page of users: an invocation cut short (the function's\n * timeout) is re-invoked, and the re-invocation replays the pages already checkpointed\n * instead of evaluating everyone again. No step retries on its own — a page that failed is\n * evaluated again by the next tick.\n */\n const sweep = async (context: DurableContext): Promise<SweepResult> => {\n const once = { retryStrategy: () => ({ shouldRetry: false }) };\n await context.step(\n 'sweep:maintain',\n async () => {\n const { store, env } = open(context);\n await host.applyOwnBundle(env, bundleVersion());\n await sweepMaintenance(env, store);\n },\n once\n );\n let users = 0;\n const started: string[] = [];\n let after: string | undefined;\n for (let n = 0; ; n++) {\n const page = await context.step(\n `sweep:page:${n}`,\n async () => {\n const { store, env } = open(context);\n return sweepSegmentsPaged(env, store, { after, limit: SWEEP_PAGE_SIZE });\n },\n once\n );\n users += page.users;\n started.push(...page.started);\n if (!page.more || page.lastUserId === undefined) break;\n after = page.lastUserId;\n }\n const sizes = await context.step(\n 'sweep:snapshot',\n async () => {\n const { store } = open(context);\n return store.reporting.snapshotSegments(Date.now());\n },\n once\n );\n return { users, started, sizes };\n };\n\n const durable = withDurableExecution(async (input: unknown, context: DurableContext) => {\n if (isFunctionUrlEvent(input)) return http(input, context);\n if (isRunInput(input)) return run(input, context);\n if (\n typeof input === 'object' &&\n input !== null &&\n (input as { type?: unknown }).type === SWEEP_EVENT_TYPE\n ) {\n return sweep(context);\n }\n throw new Error('journeyLambda: unrecognized event (not a Function URL request, run or sweep)');\n });\n\n const handler = async (\n event: DurableExecutionInvocationInput,\n context: LambdaContext\n ): Promise<DurableExecutionInvocationOutput> => {\n const invocation: Invocation = {};\n try {\n // The durable wrapper reads the whole context Lambda passed, not just the fields above\n return await scope.run(invocation, () =>\n durable(event, context as Parameters<typeof durable>[1])\n );\n } finally {\n // Outside the durable wrapper: a suspended run returns PENDING without unwinding the\n // handler, so a `finally` in there would never release the connection\n await invocation.lease?.close().catch((error: unknown) => {\n console.error('[workflow] closing the store failed', error);\n });\n }\n };\n\n return { handler };\n}\n\nexport { AwsEngineStep, stepRetry } from './engine-step';\nexport { AwsInstances, RUN_EVENT_TYPE, executionName, isRunInput } from './instances';\nexport type { AwsInstancesOptions, RunInput } from './instances';\nexport { sdkLambdaApi } from './lambda-api';\nexport { bearerMatches, timingSafeEqual } from '../host/auth';\nexport type { Authorize } from '../host/auth';\nexport type { ExecutionState, LambdaApi } from './lambda-api';\nexport { fromResponse, isFunctionUrlEvent, toRequest } from './function-url';\nexport type { FunctionUrlEvent, FunctionUrlResult } from './function-url';\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["createJourneyHost","http2Fetch","longestJourneyMs","sdkLambdaApi","AsyncLocalStorage","AwsInstances","readWithRetry","PermanentError","executeJourney","AwsEngineStep","toRequest","fromResponse","sweepMaintenance","sweepSegmentsPaged","withDurableExecution","isFunctionUrlEvent","isRunInput"],"sources":["../../src/aws/index.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport {\n type DurableContext,\n type DurableExecutionInvocationInput,\n type DurableExecutionInvocationOutput,\n withDurableExecution,\n} from '@aws/durable-execution-sdk-js';\nimport { longestJourneyMs } from '../engine/duration';\nimport type { JourneyOutcome } from '../engine/ports';\nimport { PermanentError } from '../engine/retry';\nimport { executeJourney } from '../host/execute';\nimport { readWithRetry } from '../host/fresh-read';\nimport {\n SWEEP_PAGE_SIZE,\n type SweepResult,\n sweepMaintenance,\n sweepSegmentsPaged,\n} from '../host/ingest';\nimport { type JourneyHostOptions, createJourneyHost } from '../host/journey-host';\nimport type { JourneyEnv } from '../host/platform';\nimport { http2Fetch } from '../http2-client';\nimport type { BundleVersion, ExecutionRegistry, JourneyStore, StoreLease } from '../store/index';\nimport { AwsEngineStep } from './engine-step';\nimport {\n type FunctionUrlEvent,\n type FunctionUrlResult,\n fromResponse,\n isFunctionUrlEvent,\n toRequest,\n} from './function-url';\nimport { AwsInstances, type RunInput, isRunInput } from './instances';\nimport { type LambdaApi, sdkLambdaApi } from './lambda-api';\n\n/**\n * `@shware/workflow/aws` — the journey host as one Lambda durable function behind a Function\n * URL, on Postgres.\n *\n * The one handler tells three inputs apart by the event's top-level shape: a Function URL\n * request (the HTTP API — ingest, identify, devices, tracking, bundle, preview), an internal run\n * (one journey instance: one durable execution, started by an asynchronous invoke of this\n * function's own numbered version), and a scheduled maintenance tick. An HTTP body can never\n * pass for the other two: the Function URL always wraps it.\n *\n * const journeys = journeyLambda({\n * config,\n * env, // parsed from process.env (zod)\n * store: (env, database) => postgresJsStore(env.DATABASE_URL, database),\n * email: (env) => sesEmailTransport({ region: env.AWS_REGION }),\n * });\n * export const handler = journeys.handler;\n */\n\n/** The scheduled tick (EventBridge Scheduler's payload): the segment sweep and maintenance. */\nexport const SWEEP_EVENT_TYPE = '$workflow.sweep';\n\n/** What journeyLambda reads from its env: `JourneyEnv` without the binding it supplies itself. */\nexport type JourneyLambdaEnv = Omit<JourneyEnv, 'JOURNEY' | 'DB' | 'EMAIL'>;\n\nexport interface JourneyLambdaOptions<Env extends JourneyLambdaEnv> extends Omit<\n JourneyHostOptions<Env & JourneyEnv>,\n 'env' | 'store'\n> {\n /** The validated environment (parsed from `process.env` with zod by the app). */\n env: Env;\n /** The Postgres store: AWS needs its execution registry (`postgresJsStore`). */\n store: (env: Env, database: { schema: string }) => JourneyStore | StoreLease;\n /** The Lambda control plane; `@aws-sdk/client-lambda` with the execution role by default. */\n lambda?: LambdaApi;\n /** How long an unconfirmed start is retried (default 12 hours; keep it under the retention). */\n startRetryMs?: number;\n /**\n * How long an erasure waits after stopping an execution before it erases: an invocation\n * running at the stop goes on to its next checkpoint. Set it to at least the function's\n * timeout; 15 minutes (Lambda's maximum) by default.\n */\n stopDrainMs?: number;\n /**\n * The function's `DurableConfig.ExecutionTimeout`, in milliseconds. With it, a workflow that\n * can outlive it (`longestJourneyMs`) fails the handler's creation — a deploy's smoke test —\n * instead of being stopped by the platform midway and recorded as failed.\n */\n executionTimeoutMs?: number;\n}\n\n/**\n * The Lambda invocation context, as far as the handler's signature needs it — declared here so\n * the package's types do not depend on `@types/aws-lambda`. Lambda passes the full object.\n */\nexport interface LambdaContext {\n functionName: string;\n functionVersion: string;\n invokedFunctionArn: string;\n awsRequestId: string;\n getRemainingTimeInMillis(): number;\n}\n\nexport interface JourneyLambda {\n /** Export as the function's handler. */\n handler: (\n event: DurableExecutionInvocationInput,\n context: LambdaContext\n ) => Promise<DurableExecutionInvocationOutput>;\n}\n\ninterface Invocation {\n lease?: Required<StoreLease>;\n}\n\n/** This function's ARN without its qualifier, from the ARN it was invoked under. */\nfunction unqualified(invokedFunctionArn: string): string {\n return invokedFunctionArn.split(':').slice(0, 7).join(':');\n}\n\nconst json = (body: unknown, status = 200) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });\n\nexport function journeyLambda<Env extends JourneyLambdaEnv>(\n options: JourneyLambdaOptions<Env>\n): JourneyLambda {\n type HostEnv = Env & JourneyEnv;\n const {\n env: baseEnv,\n lambda: lambdaOption,\n startRetryMs,\n stopDrainMs,\n executionTimeoutMs,\n ...hostOptions\n } = options;\n const host = createJourneyHost<HostEnv>(\n { ...hostOptions, store: (env, database) => options.store(env, database) },\n // Node 24's fetch cannot reach APNs (HTTP/2 only): see http2-client.ts for when this can go\n { apnsFetch: http2Fetch() }\n );\n if (executionTimeoutMs !== undefined) {\n const tooLong = host\n .bundle()\n .workflows.filter((workflow) => longestJourneyMs(workflow) > executionTimeoutMs);\n if (tooLong.length > 0) {\n const days = (ms: number) => `${Math.ceil(ms / 86_400_000)} days`;\n throw new RangeError(\n `${tooLong.map((w) => `'${w.id}' can run ${days(longestJourneyMs(w))}`).join(', ')}, ` +\n `longer than the execution timeout (${days(executionTimeoutMs)}): raise ExecutionTimeout (up to 366 days)`\n );\n }\n }\n const lambda = lambdaOption ?? sdkLambdaApi();\n const ownHashes = new Set(host.bundle().workflows.map((workflow) => workflow.contentHash));\n const scope = new AsyncLocalStorage<Invocation>();\n\n /** The version this code was published as: `$LATEST` while developing. */\n const ownVersion = () => process.env.AWS_LAMBDA_FUNCTION_VERSION ?? '$LATEST';\n /** Numbered versions are ordered: the bundle compiled into a later one goes live by itself. */\n const bundleVersion = (): BundleVersion | undefined => {\n const version = ownVersion();\n return /^\\d+$/.test(version) ? { id: version, createdAt: Number(version) } : undefined;\n };\n\n /** The store and the env (with the instance port) of this invocation, opened on first use. */\n const open = (\n context: DurableContext\n ): { store: JourneyStore; env: HostEnv; executions: ExecutionRegistry } => {\n const invocation = scope.getStore();\n if (invocation === undefined) throw new Error('journeyLambda: outside an invocation');\n const placeholder = { ...baseEnv } as unknown as HostEnv;\n invocation.lease ??= host.lease(placeholder);\n const { store } = invocation.lease;\n const { executions } = store;\n const functionArn = unqualified(context.lambdaContext.invokedFunctionArn);\n const target = async (contentHash: string, version?: string): Promise<string> => {\n // An execution runs the code its IR was compiled with: the version whose bundle put the\n // route there, read in the same row as the hash — never a separately read \"live\" version,\n // which a release between the two reads would make another bundle's\n if (version !== undefined && /^\\d+$/.test(version)) return `${functionArn}:${version}`;\n // A forced deploy records no version: only this code's own workflows can run here\n if (ownHashes.has(contentHash)) return `${functionArn}:${ownVersion()}`;\n throw new Error(`no published version is known to carry workflow IR ${contentHash}`);\n };\n const instances = new AwsInstances({\n store,\n executions,\n lambda,\n target,\n ...(startRetryMs === undefined ? {} : { startRetryMs }),\n ...(stopDrainMs === undefined ? {} : { stopDrainMs }),\n });\n return { store, env: { ...baseEnv, JOURNEY: instances }, executions };\n };\n\n const run = async (input: RunInput, context: DurableContext): Promise<JourneyOutcome> => {\n const { store, env, executions } = open(context);\n const { instanceId, params } = input;\n // Only a start the router recorded may run, byte for byte: the function can be invoked by\n // anything its policy lets through, and an input of this shape would otherwise run any\n // workflow for any user. A stopped execution (an erasure) may not come back either. Outside\n // any step — a step would replay the first invocation's \"not stopped\" on every wake — so the\n // execution fails at once on a refusal; only the read itself is retried, briefly, when the\n // database stumbled.\n const record = await readWithRetry(() => executions.get(instanceId), {\n what: 'run entry',\n instanceId,\n });\n if (\n record === undefined ||\n record.stoppedAt !== undefined ||\n record.input !== JSON.stringify(input)\n ) {\n throw new PermanentError('journeyLambda: run input is not a recorded start');\n }\n // Running is the proof the start got through, whatever became of the invoke's response\n await context.step('$workflow:started', async () => {\n await executions.markSubmitted(\n instanceId,\n context.executionContext.durableExecutionArn,\n Date.now()\n );\n });\n return executeJourney(\n {\n instanceId,\n params,\n enteredAtMs: input.enteredAtMs,\n step: new AwsEngineStep(context, store, executions, lambda, params.userId, instanceId),\n },\n host.runParts(env, store, params)\n );\n };\n\n const http = (event: FunctionUrlEvent, context: DurableContext): Promise<FunctionUrlResult> => {\n const serve = async (): Promise<FunctionUrlResult> => {\n const method = event.requestContext.http.method;\n try {\n const { store, env } = open(context);\n const request = toRequest(event);\n // Only the bundle compiled into a version may route: one is never swapped in by hand\n if (new URL(request.url).pathname === '/deploy') {\n return await fromResponse(\n json(\n {\n error:\n 'deploy by publishing a version (POST /bundle/deploy applies its own bundle)',\n },\n 409\n ),\n method\n );\n }\n const version = bundleVersion();\n await host.applyOwnBundle(env, version);\n return await fromResponse(await host.handle(request, env, store, version), method);\n } catch (error) {\n console.error('[workflow] request failed', error);\n return fromResponse(json({ error: 'internal error' }, 500), method);\n }\n };\n // A read (the bundle, a preview, a tracked link or pixel) answers straight from the handler:\n // its body can be large, and a step would checkpoint all of it. A write runs in one step,\n // never retried — the caller is waiting, and retries the request itself\n const method = event.requestContext.http.method.toUpperCase();\n if (method === 'GET' || method === 'HEAD') return serve();\n return context.step('http', serve, { retryStrategy: () => ({ shouldRetry: false }) });\n };\n\n /**\n * The sweep as durable steps, one per page of users: an invocation cut short (the function's\n * timeout) is re-invoked, and the re-invocation replays the pages already checkpointed\n * instead of evaluating everyone again. No step retries on its own — a page that failed is\n * evaluated again by the next tick.\n */\n const sweep = async (context: DurableContext): Promise<SweepResult> => {\n const once = { retryStrategy: () => ({ shouldRetry: false }) };\n await context.step(\n 'sweep:maintain',\n async () => {\n const { store, env } = open(context);\n await host.applyOwnBundle(env, bundleVersion());\n await sweepMaintenance(env, store);\n },\n once\n );\n let users = 0;\n const started: string[] = [];\n let after: string | undefined;\n for (let n = 0; ; n++) {\n const page = await context.step(\n `sweep:page:${n}`,\n async () => {\n const { store, env } = open(context);\n return sweepSegmentsPaged(env, store, { after, limit: SWEEP_PAGE_SIZE });\n },\n once\n );\n users += page.users;\n started.push(...page.started);\n if (!page.more || page.lastUserId === undefined) break;\n after = page.lastUserId;\n }\n const sizes = await context.step(\n 'sweep:snapshot',\n async () => {\n const { store } = open(context);\n return store.reporting.snapshotSegments(Date.now());\n },\n once\n );\n return { users, started, sizes };\n };\n\n const durable = withDurableExecution(async (input: unknown, context: DurableContext) => {\n if (isFunctionUrlEvent(input)) return http(input, context);\n if (isRunInput(input)) return run(input, context);\n if (\n typeof input === 'object' &&\n input !== null &&\n (input as { type?: unknown }).type === SWEEP_EVENT_TYPE\n ) {\n return sweep(context);\n }\n throw new Error('journeyLambda: unrecognized event (not a Function URL request, run or sweep)');\n });\n\n const handler = async (\n event: DurableExecutionInvocationInput,\n context: LambdaContext\n ): Promise<DurableExecutionInvocationOutput> => {\n const invocation: Invocation = {};\n try {\n // The durable wrapper reads the whole context Lambda passed, not just the fields above\n return await scope.run(invocation, () =>\n durable(event, context as Parameters<typeof durable>[1])\n );\n } finally {\n // Outside the durable wrapper: a suspended run returns PENDING without unwinding the\n // handler, so a `finally` in there would never release the connection\n await invocation.lease?.close().catch((error: unknown) => {\n console.error('[workflow] closing the store failed', error);\n });\n }\n };\n\n return { handler };\n}\n\nexport { AwsEngineStep, stepRetry } from './engine-step';\nexport { AwsInstances, RUN_EVENT_TYPE, executionName, isRunInput } from './instances';\nexport type { AwsInstancesOptions, RunInput } from './instances';\nexport { sdkLambdaApi } from './lambda-api';\nexport { bearerMatches, timingSafeEqual } from '../host/auth';\nexport type { Authorize } from '../host/auth';\nexport type { ExecutionState, LambdaApi } from './lambda-api';\nexport { fromResponse, isFunctionUrlEvent, toRequest } from './function-url';\nexport type { FunctionUrlEvent, FunctionUrlResult } from './function-url';\n"],"mappings":"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"}
@@ -123,8 +123,9 @@ function journeyLambda(options) {
123
123
  const { store, env } = open(context);
124
124
  const request = toRequest(event);
125
125
  if (new URL(request.url).pathname === "/deploy") return await fromResponse(json({ error: "deploy by publishing a version (POST /bundle/deploy applies its own bundle)" }, 409), method);
126
- await host.applyOwnBundle(env, bundleVersion());
127
- return await fromResponse(await host.handle(request, env, store), method);
126
+ const version = bundleVersion();
127
+ await host.applyOwnBundle(env, version);
128
+ return await fromResponse(await host.handle(request, env, store, version), method);
128
129
  } catch (error) {
129
130
  console.error("[workflow] request failed", error);
130
131
  return fromResponse(json({ error: "internal error" }, 500), method);
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../src/aws/index.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport {\n type DurableContext,\n type DurableExecutionInvocationInput,\n type DurableExecutionInvocationOutput,\n withDurableExecution,\n} from '@aws/durable-execution-sdk-js';\nimport { longestJourneyMs } from '../engine/duration';\nimport type { JourneyOutcome } from '../engine/ports';\nimport { PermanentError } from '../engine/retry';\nimport { executeJourney } from '../host/execute';\nimport { readWithRetry } from '../host/fresh-read';\nimport {\n SWEEP_PAGE_SIZE,\n type SweepResult,\n sweepMaintenance,\n sweepSegmentsPaged,\n} from '../host/ingest';\nimport { type JourneyHostOptions, createJourneyHost } from '../host/journey-host';\nimport type { JourneyEnv } from '../host/platform';\nimport { http2Fetch } from '../http2-client';\nimport type { BundleVersion, ExecutionRegistry, JourneyStore, StoreLease } from '../store/index';\nimport { AwsEngineStep } from './engine-step';\nimport {\n type FunctionUrlEvent,\n type FunctionUrlResult,\n fromResponse,\n isFunctionUrlEvent,\n toRequest,\n} from './function-url';\nimport { AwsInstances, type RunInput, isRunInput } from './instances';\nimport { type LambdaApi, sdkLambdaApi } from './lambda-api';\n\n/**\n * `@shware/workflow/aws` — the journey host as one Lambda durable function behind a Function\n * URL, on Postgres.\n *\n * The one handler tells three inputs apart by the event's top-level shape: a Function URL\n * request (the HTTP API — ingest, identify, devices, tracking, bundle, preview), an internal run\n * (one journey instance: one durable execution, started by an asynchronous invoke of this\n * function's own numbered version), and a scheduled maintenance tick. An HTTP body can never\n * pass for the other two: the Function URL always wraps it.\n *\n * const journeys = journeyLambda({\n * config,\n * env, // parsed from process.env (zod)\n * store: (env, database) => postgresJsStore(env.DATABASE_URL, database),\n * email: (env) => sesEmailTransport({ region: env.AWS_REGION }),\n * });\n * export const handler = journeys.handler;\n */\n\n/** The scheduled tick (EventBridge Scheduler's payload): the segment sweep and maintenance. */\nexport const SWEEP_EVENT_TYPE = '$workflow.sweep';\n\n/** What journeyLambda reads from its env: `JourneyEnv` without the binding it supplies itself. */\nexport type JourneyLambdaEnv = Omit<JourneyEnv, 'JOURNEY' | 'DB' | 'EMAIL'>;\n\nexport interface JourneyLambdaOptions<Env extends JourneyLambdaEnv> extends Omit<\n JourneyHostOptions<Env & JourneyEnv>,\n 'env' | 'store'\n> {\n /** The validated environment (parsed from `process.env` with zod by the app). */\n env: Env;\n /** The Postgres store: AWS needs its execution registry (`postgresJsStore`). */\n store: (env: Env, database: { schema: string }) => JourneyStore | StoreLease;\n /** The Lambda control plane; `@aws-sdk/client-lambda` with the execution role by default. */\n lambda?: LambdaApi;\n /** How long an unconfirmed start is retried (default 12 hours; keep it under the retention). */\n startRetryMs?: number;\n /**\n * How long an erasure waits after stopping an execution before it erases: an invocation\n * running at the stop goes on to its next checkpoint. Set it to at least the function's\n * timeout; 15 minutes (Lambda's maximum) by default.\n */\n stopDrainMs?: number;\n /**\n * The function's `DurableConfig.ExecutionTimeout`, in milliseconds. With it, a workflow that\n * can outlive it (`longestJourneyMs`) fails the handler's creation — a deploy's smoke test —\n * instead of being stopped by the platform midway and recorded as failed.\n */\n executionTimeoutMs?: number;\n}\n\n/**\n * The Lambda invocation context, as far as the handler's signature needs it — declared here so\n * the package's types do not depend on `@types/aws-lambda`. Lambda passes the full object.\n */\nexport interface LambdaContext {\n functionName: string;\n functionVersion: string;\n invokedFunctionArn: string;\n awsRequestId: string;\n getRemainingTimeInMillis(): number;\n}\n\nexport interface JourneyLambda {\n /** Export as the function's handler. */\n handler: (\n event: DurableExecutionInvocationInput,\n context: LambdaContext\n ) => Promise<DurableExecutionInvocationOutput>;\n}\n\ninterface Invocation {\n lease?: Required<StoreLease>;\n}\n\n/** This function's ARN without its qualifier, from the ARN it was invoked under. */\nfunction unqualified(invokedFunctionArn: string): string {\n return invokedFunctionArn.split(':').slice(0, 7).join(':');\n}\n\nconst json = (body: unknown, status = 200) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });\n\nexport function journeyLambda<Env extends JourneyLambdaEnv>(\n options: JourneyLambdaOptions<Env>\n): JourneyLambda {\n type HostEnv = Env & JourneyEnv;\n const {\n env: baseEnv,\n lambda: lambdaOption,\n startRetryMs,\n stopDrainMs,\n executionTimeoutMs,\n ...hostOptions\n } = options;\n const host = createJourneyHost<HostEnv>(\n { ...hostOptions, store: (env, database) => options.store(env, database) },\n // Node 24's fetch cannot reach APNs (HTTP/2 only): see http2-client.ts for when this can go\n { apnsFetch: http2Fetch() }\n );\n if (executionTimeoutMs !== undefined) {\n const tooLong = host\n .bundle()\n .workflows.filter((workflow) => longestJourneyMs(workflow) > executionTimeoutMs);\n if (tooLong.length > 0) {\n const days = (ms: number) => `${Math.ceil(ms / 86_400_000)} days`;\n throw new RangeError(\n `${tooLong.map((w) => `'${w.id}' can run ${days(longestJourneyMs(w))}`).join(', ')}, ` +\n `longer than the execution timeout (${days(executionTimeoutMs)}): raise ExecutionTimeout (up to 366 days)`\n );\n }\n }\n const lambda = lambdaOption ?? sdkLambdaApi();\n const ownHashes = new Set(host.bundle().workflows.map((workflow) => workflow.contentHash));\n const scope = new AsyncLocalStorage<Invocation>();\n\n /** The version this code was published as: `$LATEST` while developing. */\n const ownVersion = () => process.env.AWS_LAMBDA_FUNCTION_VERSION ?? '$LATEST';\n /** Numbered versions are ordered: the bundle compiled into a later one goes live by itself. */\n const bundleVersion = (): BundleVersion | undefined => {\n const version = ownVersion();\n return /^\\d+$/.test(version) ? { id: version, createdAt: Number(version) } : undefined;\n };\n\n /** The store and the env (with the instance port) of this invocation, opened on first use. */\n const open = (\n context: DurableContext\n ): { store: JourneyStore; env: HostEnv; executions: ExecutionRegistry } => {\n const invocation = scope.getStore();\n if (invocation === undefined) throw new Error('journeyLambda: outside an invocation');\n const placeholder = { ...baseEnv } as unknown as HostEnv;\n invocation.lease ??= host.lease(placeholder);\n const { store } = invocation.lease;\n const { executions } = store;\n const functionArn = unqualified(context.lambdaContext.invokedFunctionArn);\n const target = async (contentHash: string, version?: string): Promise<string> => {\n // An execution runs the code its IR was compiled with: the version whose bundle put the\n // route there, read in the same row as the hash — never a separately read \"live\" version,\n // which a release between the two reads would make another bundle's\n if (version !== undefined && /^\\d+$/.test(version)) return `${functionArn}:${version}`;\n // A forced deploy records no version: only this code's own workflows can run here\n if (ownHashes.has(contentHash)) return `${functionArn}:${ownVersion()}`;\n throw new Error(`no published version is known to carry workflow IR ${contentHash}`);\n };\n const instances = new AwsInstances({\n store,\n executions,\n lambda,\n target,\n ...(startRetryMs === undefined ? {} : { startRetryMs }),\n ...(stopDrainMs === undefined ? {} : { stopDrainMs }),\n });\n return { store, env: { ...baseEnv, JOURNEY: instances }, executions };\n };\n\n const run = async (input: RunInput, context: DurableContext): Promise<JourneyOutcome> => {\n const { store, env, executions } = open(context);\n const { instanceId, params } = input;\n // Only a start the router recorded may run, byte for byte: the function can be invoked by\n // anything its policy lets through, and an input of this shape would otherwise run any\n // workflow for any user. A stopped execution (an erasure) may not come back either. Outside\n // any step — a step would replay the first invocation's \"not stopped\" on every wake — so the\n // execution fails at once on a refusal; only the read itself is retried, briefly, when the\n // database stumbled.\n const record = await readWithRetry(() => executions.get(instanceId), {\n what: 'run entry',\n instanceId,\n });\n if (\n record === undefined ||\n record.stoppedAt !== undefined ||\n record.input !== JSON.stringify(input)\n ) {\n throw new PermanentError('journeyLambda: run input is not a recorded start');\n }\n // Running is the proof the start got through, whatever became of the invoke's response\n await context.step('$workflow:started', async () => {\n await executions.markSubmitted(\n instanceId,\n context.executionContext.durableExecutionArn,\n Date.now()\n );\n });\n return executeJourney(\n {\n instanceId,\n params,\n enteredAtMs: input.enteredAtMs,\n step: new AwsEngineStep(context, store, executions, lambda, params.userId, instanceId),\n },\n host.runParts(env, store, params)\n );\n };\n\n const http = (event: FunctionUrlEvent, context: DurableContext): Promise<FunctionUrlResult> => {\n const serve = async (): Promise<FunctionUrlResult> => {\n const method = event.requestContext.http.method;\n try {\n const { store, env } = open(context);\n const request = toRequest(event);\n // Only the bundle compiled into a version may route: one is never swapped in by hand\n if (new URL(request.url).pathname === '/deploy') {\n return await fromResponse(\n json(\n {\n error:\n 'deploy by publishing a version (POST /bundle/deploy applies its own bundle)',\n },\n 409\n ),\n method\n );\n }\n await host.applyOwnBundle(env, bundleVersion());\n return await fromResponse(await host.handle(request, env, store), method);\n } catch (error) {\n console.error('[workflow] request failed', error);\n return fromResponse(json({ error: 'internal error' }, 500), method);\n }\n };\n // A read (the bundle, a preview, a tracked link or pixel) answers straight from the handler:\n // its body can be large, and a step would checkpoint all of it. A write runs in one step,\n // never retried — the caller is waiting, and retries the request itself\n const method = event.requestContext.http.method.toUpperCase();\n if (method === 'GET' || method === 'HEAD') return serve();\n return context.step('http', serve, { retryStrategy: () => ({ shouldRetry: false }) });\n };\n\n /**\n * The sweep as durable steps, one per page of users: an invocation cut short (the function's\n * timeout) is re-invoked, and the re-invocation replays the pages already checkpointed\n * instead of evaluating everyone again. No step retries on its own — a page that failed is\n * evaluated again by the next tick.\n */\n const sweep = async (context: DurableContext): Promise<SweepResult> => {\n const once = { retryStrategy: () => ({ shouldRetry: false }) };\n await context.step(\n 'sweep:maintain',\n async () => {\n const { store, env } = open(context);\n await host.applyOwnBundle(env, bundleVersion());\n await sweepMaintenance(env, store);\n },\n once\n );\n let users = 0;\n const started: string[] = [];\n let after: string | undefined;\n for (let n = 0; ; n++) {\n const page = await context.step(\n `sweep:page:${n}`,\n async () => {\n const { store, env } = open(context);\n return sweepSegmentsPaged(env, store, { after, limit: SWEEP_PAGE_SIZE });\n },\n once\n );\n users += page.users;\n started.push(...page.started);\n if (!page.more || page.lastUserId === undefined) break;\n after = page.lastUserId;\n }\n const sizes = await context.step(\n 'sweep:snapshot',\n async () => {\n const { store } = open(context);\n return store.reporting.snapshotSegments(Date.now());\n },\n once\n );\n return { users, started, sizes };\n };\n\n const durable = withDurableExecution(async (input: unknown, context: DurableContext) => {\n if (isFunctionUrlEvent(input)) return http(input, context);\n if (isRunInput(input)) return run(input, context);\n if (\n typeof input === 'object' &&\n input !== null &&\n (input as { type?: unknown }).type === SWEEP_EVENT_TYPE\n ) {\n return sweep(context);\n }\n throw new Error('journeyLambda: unrecognized event (not a Function URL request, run or sweep)');\n });\n\n const handler = async (\n event: DurableExecutionInvocationInput,\n context: LambdaContext\n ): Promise<DurableExecutionInvocationOutput> => {\n const invocation: Invocation = {};\n try {\n // The durable wrapper reads the whole context Lambda passed, not just the fields above\n return await scope.run(invocation, () =>\n durable(event, context as Parameters<typeof durable>[1])\n );\n } finally {\n // Outside the durable wrapper: a suspended run returns PENDING without unwinding the\n // handler, so a `finally` in there would never release the connection\n await invocation.lease?.close().catch((error: unknown) => {\n console.error('[workflow] closing the store failed', error);\n });\n }\n };\n\n return { handler };\n}\n\nexport { AwsEngineStep, stepRetry } from './engine-step';\nexport { AwsInstances, RUN_EVENT_TYPE, executionName, isRunInput } from './instances';\nexport type { AwsInstancesOptions, RunInput } from './instances';\nexport { sdkLambdaApi } from './lambda-api';\nexport { bearerMatches, timingSafeEqual } from '../host/auth';\nexport type { Authorize } from '../host/auth';\nexport type { ExecutionState, LambdaApi } from './lambda-api';\nexport { fromResponse, isFunctionUrlEvent, toRequest } from './function-url';\nexport type { FunctionUrlEvent, FunctionUrlResult } from './function-url';\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/aws/index.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport {\n type DurableContext,\n type DurableExecutionInvocationInput,\n type DurableExecutionInvocationOutput,\n withDurableExecution,\n} from '@aws/durable-execution-sdk-js';\nimport { longestJourneyMs } from '../engine/duration';\nimport type { JourneyOutcome } from '../engine/ports';\nimport { PermanentError } from '../engine/retry';\nimport { executeJourney } from '../host/execute';\nimport { readWithRetry } from '../host/fresh-read';\nimport {\n SWEEP_PAGE_SIZE,\n type SweepResult,\n sweepMaintenance,\n sweepSegmentsPaged,\n} from '../host/ingest';\nimport { type JourneyHostOptions, createJourneyHost } from '../host/journey-host';\nimport type { JourneyEnv } from '../host/platform';\nimport { http2Fetch } from '../http2-client';\nimport type { BundleVersion, ExecutionRegistry, JourneyStore, StoreLease } from '../store/index';\nimport { AwsEngineStep } from './engine-step';\nimport {\n type FunctionUrlEvent,\n type FunctionUrlResult,\n fromResponse,\n isFunctionUrlEvent,\n toRequest,\n} from './function-url';\nimport { AwsInstances, type RunInput, isRunInput } from './instances';\nimport { type LambdaApi, sdkLambdaApi } from './lambda-api';\n\n/**\n * `@shware/workflow/aws` — the journey host as one Lambda durable function behind a Function\n * URL, on Postgres.\n *\n * The one handler tells three inputs apart by the event's top-level shape: a Function URL\n * request (the HTTP API — ingest, identify, devices, tracking, bundle, preview), an internal run\n * (one journey instance: one durable execution, started by an asynchronous invoke of this\n * function's own numbered version), and a scheduled maintenance tick. An HTTP body can never\n * pass for the other two: the Function URL always wraps it.\n *\n * const journeys = journeyLambda({\n * config,\n * env, // parsed from process.env (zod)\n * store: (env, database) => postgresJsStore(env.DATABASE_URL, database),\n * email: (env) => sesEmailTransport({ region: env.AWS_REGION }),\n * });\n * export const handler = journeys.handler;\n */\n\n/** The scheduled tick (EventBridge Scheduler's payload): the segment sweep and maintenance. */\nexport const SWEEP_EVENT_TYPE = '$workflow.sweep';\n\n/** What journeyLambda reads from its env: `JourneyEnv` without the binding it supplies itself. */\nexport type JourneyLambdaEnv = Omit<JourneyEnv, 'JOURNEY' | 'DB' | 'EMAIL'>;\n\nexport interface JourneyLambdaOptions<Env extends JourneyLambdaEnv> extends Omit<\n JourneyHostOptions<Env & JourneyEnv>,\n 'env' | 'store'\n> {\n /** The validated environment (parsed from `process.env` with zod by the app). */\n env: Env;\n /** The Postgres store: AWS needs its execution registry (`postgresJsStore`). */\n store: (env: Env, database: { schema: string }) => JourneyStore | StoreLease;\n /** The Lambda control plane; `@aws-sdk/client-lambda` with the execution role by default. */\n lambda?: LambdaApi;\n /** How long an unconfirmed start is retried (default 12 hours; keep it under the retention). */\n startRetryMs?: number;\n /**\n * How long an erasure waits after stopping an execution before it erases: an invocation\n * running at the stop goes on to its next checkpoint. Set it to at least the function's\n * timeout; 15 minutes (Lambda's maximum) by default.\n */\n stopDrainMs?: number;\n /**\n * The function's `DurableConfig.ExecutionTimeout`, in milliseconds. With it, a workflow that\n * can outlive it (`longestJourneyMs`) fails the handler's creation — a deploy's smoke test —\n * instead of being stopped by the platform midway and recorded as failed.\n */\n executionTimeoutMs?: number;\n}\n\n/**\n * The Lambda invocation context, as far as the handler's signature needs it — declared here so\n * the package's types do not depend on `@types/aws-lambda`. Lambda passes the full object.\n */\nexport interface LambdaContext {\n functionName: string;\n functionVersion: string;\n invokedFunctionArn: string;\n awsRequestId: string;\n getRemainingTimeInMillis(): number;\n}\n\nexport interface JourneyLambda {\n /** Export as the function's handler. */\n handler: (\n event: DurableExecutionInvocationInput,\n context: LambdaContext\n ) => Promise<DurableExecutionInvocationOutput>;\n}\n\ninterface Invocation {\n lease?: Required<StoreLease>;\n}\n\n/** This function's ARN without its qualifier, from the ARN it was invoked under. */\nfunction unqualified(invokedFunctionArn: string): string {\n return invokedFunctionArn.split(':').slice(0, 7).join(':');\n}\n\nconst json = (body: unknown, status = 200) =>\n new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });\n\nexport function journeyLambda<Env extends JourneyLambdaEnv>(\n options: JourneyLambdaOptions<Env>\n): JourneyLambda {\n type HostEnv = Env & JourneyEnv;\n const {\n env: baseEnv,\n lambda: lambdaOption,\n startRetryMs,\n stopDrainMs,\n executionTimeoutMs,\n ...hostOptions\n } = options;\n const host = createJourneyHost<HostEnv>(\n { ...hostOptions, store: (env, database) => options.store(env, database) },\n // Node 24's fetch cannot reach APNs (HTTP/2 only): see http2-client.ts for when this can go\n { apnsFetch: http2Fetch() }\n );\n if (executionTimeoutMs !== undefined) {\n const tooLong = host\n .bundle()\n .workflows.filter((workflow) => longestJourneyMs(workflow) > executionTimeoutMs);\n if (tooLong.length > 0) {\n const days = (ms: number) => `${Math.ceil(ms / 86_400_000)} days`;\n throw new RangeError(\n `${tooLong.map((w) => `'${w.id}' can run ${days(longestJourneyMs(w))}`).join(', ')}, ` +\n `longer than the execution timeout (${days(executionTimeoutMs)}): raise ExecutionTimeout (up to 366 days)`\n );\n }\n }\n const lambda = lambdaOption ?? sdkLambdaApi();\n const ownHashes = new Set(host.bundle().workflows.map((workflow) => workflow.contentHash));\n const scope = new AsyncLocalStorage<Invocation>();\n\n /** The version this code was published as: `$LATEST` while developing. */\n const ownVersion = () => process.env.AWS_LAMBDA_FUNCTION_VERSION ?? '$LATEST';\n /** Numbered versions are ordered: the bundle compiled into a later one goes live by itself. */\n const bundleVersion = (): BundleVersion | undefined => {\n const version = ownVersion();\n return /^\\d+$/.test(version) ? { id: version, createdAt: Number(version) } : undefined;\n };\n\n /** The store and the env (with the instance port) of this invocation, opened on first use. */\n const open = (\n context: DurableContext\n ): { store: JourneyStore; env: HostEnv; executions: ExecutionRegistry } => {\n const invocation = scope.getStore();\n if (invocation === undefined) throw new Error('journeyLambda: outside an invocation');\n const placeholder = { ...baseEnv } as unknown as HostEnv;\n invocation.lease ??= host.lease(placeholder);\n const { store } = invocation.lease;\n const { executions } = store;\n const functionArn = unqualified(context.lambdaContext.invokedFunctionArn);\n const target = async (contentHash: string, version?: string): Promise<string> => {\n // An execution runs the code its IR was compiled with: the version whose bundle put the\n // route there, read in the same row as the hash — never a separately read \"live\" version,\n // which a release between the two reads would make another bundle's\n if (version !== undefined && /^\\d+$/.test(version)) return `${functionArn}:${version}`;\n // A forced deploy records no version: only this code's own workflows can run here\n if (ownHashes.has(contentHash)) return `${functionArn}:${ownVersion()}`;\n throw new Error(`no published version is known to carry workflow IR ${contentHash}`);\n };\n const instances = new AwsInstances({\n store,\n executions,\n lambda,\n target,\n ...(startRetryMs === undefined ? {} : { startRetryMs }),\n ...(stopDrainMs === undefined ? {} : { stopDrainMs }),\n });\n return { store, env: { ...baseEnv, JOURNEY: instances }, executions };\n };\n\n const run = async (input: RunInput, context: DurableContext): Promise<JourneyOutcome> => {\n const { store, env, executions } = open(context);\n const { instanceId, params } = input;\n // Only a start the router recorded may run, byte for byte: the function can be invoked by\n // anything its policy lets through, and an input of this shape would otherwise run any\n // workflow for any user. A stopped execution (an erasure) may not come back either. Outside\n // any step — a step would replay the first invocation's \"not stopped\" on every wake — so the\n // execution fails at once on a refusal; only the read itself is retried, briefly, when the\n // database stumbled.\n const record = await readWithRetry(() => executions.get(instanceId), {\n what: 'run entry',\n instanceId,\n });\n if (\n record === undefined ||\n record.stoppedAt !== undefined ||\n record.input !== JSON.stringify(input)\n ) {\n throw new PermanentError('journeyLambda: run input is not a recorded start');\n }\n // Running is the proof the start got through, whatever became of the invoke's response\n await context.step('$workflow:started', async () => {\n await executions.markSubmitted(\n instanceId,\n context.executionContext.durableExecutionArn,\n Date.now()\n );\n });\n return executeJourney(\n {\n instanceId,\n params,\n enteredAtMs: input.enteredAtMs,\n step: new AwsEngineStep(context, store, executions, lambda, params.userId, instanceId),\n },\n host.runParts(env, store, params)\n );\n };\n\n const http = (event: FunctionUrlEvent, context: DurableContext): Promise<FunctionUrlResult> => {\n const serve = async (): Promise<FunctionUrlResult> => {\n const method = event.requestContext.http.method;\n try {\n const { store, env } = open(context);\n const request = toRequest(event);\n // Only the bundle compiled into a version may route: one is never swapped in by hand\n if (new URL(request.url).pathname === '/deploy') {\n return await fromResponse(\n json(\n {\n error:\n 'deploy by publishing a version (POST /bundle/deploy applies its own bundle)',\n },\n 409\n ),\n method\n );\n }\n const version = bundleVersion();\n await host.applyOwnBundle(env, version);\n return await fromResponse(await host.handle(request, env, store, version), method);\n } catch (error) {\n console.error('[workflow] request failed', error);\n return fromResponse(json({ error: 'internal error' }, 500), method);\n }\n };\n // A read (the bundle, a preview, a tracked link or pixel) answers straight from the handler:\n // its body can be large, and a step would checkpoint all of it. A write runs in one step,\n // never retried — the caller is waiting, and retries the request itself\n const method = event.requestContext.http.method.toUpperCase();\n if (method === 'GET' || method === 'HEAD') return serve();\n return context.step('http', serve, { retryStrategy: () => ({ shouldRetry: false }) });\n };\n\n /**\n * The sweep as durable steps, one per page of users: an invocation cut short (the function's\n * timeout) is re-invoked, and the re-invocation replays the pages already checkpointed\n * instead of evaluating everyone again. No step retries on its own — a page that failed is\n * evaluated again by the next tick.\n */\n const sweep = async (context: DurableContext): Promise<SweepResult> => {\n const once = { retryStrategy: () => ({ shouldRetry: false }) };\n await context.step(\n 'sweep:maintain',\n async () => {\n const { store, env } = open(context);\n await host.applyOwnBundle(env, bundleVersion());\n await sweepMaintenance(env, store);\n },\n once\n );\n let users = 0;\n const started: string[] = [];\n let after: string | undefined;\n for (let n = 0; ; n++) {\n const page = await context.step(\n `sweep:page:${n}`,\n async () => {\n const { store, env } = open(context);\n return sweepSegmentsPaged(env, store, { after, limit: SWEEP_PAGE_SIZE });\n },\n once\n );\n users += page.users;\n started.push(...page.started);\n if (!page.more || page.lastUserId === undefined) break;\n after = page.lastUserId;\n }\n const sizes = await context.step(\n 'sweep:snapshot',\n async () => {\n const { store } = open(context);\n return store.reporting.snapshotSegments(Date.now());\n },\n once\n );\n return { users, started, sizes };\n };\n\n const durable = withDurableExecution(async (input: unknown, context: DurableContext) => {\n if (isFunctionUrlEvent(input)) return http(input, context);\n if (isRunInput(input)) return run(input, context);\n if (\n typeof input === 'object' &&\n input !== null &&\n (input as { type?: unknown }).type === SWEEP_EVENT_TYPE\n ) {\n return sweep(context);\n }\n throw new Error('journeyLambda: unrecognized event (not a Function URL request, run or sweep)');\n });\n\n const handler = async (\n event: DurableExecutionInvocationInput,\n context: LambdaContext\n ): Promise<DurableExecutionInvocationOutput> => {\n const invocation: Invocation = {};\n try {\n // The durable wrapper reads the whole context Lambda passed, not just the fields above\n return await scope.run(invocation, () =>\n durable(event, context as Parameters<typeof durable>[1])\n );\n } finally {\n // Outside the durable wrapper: a suspended run returns PENDING without unwinding the\n // handler, so a `finally` in there would never release the connection\n await invocation.lease?.close().catch((error: unknown) => {\n console.error('[workflow] closing the store failed', error);\n });\n }\n };\n\n return { handler };\n}\n\nexport { AwsEngineStep, stepRetry } from './engine-step';\nexport { AwsInstances, RUN_EVENT_TYPE, executionName, isRunInput } from './instances';\nexport type { AwsInstancesOptions, RunInput } from './instances';\nexport { sdkLambdaApi } from './lambda-api';\nexport { bearerMatches, timingSafeEqual } from '../host/auth';\nexport type { Authorize } from '../host/auth';\nexport type { ExecutionState, LambdaApi } from './lambda-api';\nexport { fromResponse, isFunctionUrlEvent, toRequest } from './function-url';\nexport type { FunctionUrlEvent, FunctionUrlResult } from './function-url';\n"],"mappings":"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"}