@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
@@ -56,6 +56,25 @@ export interface SweepResult {
56
56
  size: number;
57
57
  }[];
58
58
  }
59
+ /**
60
+ * How long an entry may wait for its runner's admission before maintenance looks for its
61
+ * instance. A create takes well under a second and a queued instance is found by `get`, so this
62
+ * only has to outlast a create still in flight — generously, since a wrong call fails a journey.
63
+ */
64
+ export declare const LOST_START_AFTER_MS: number;
65
+ /**
66
+ * Starts lost between the entry and the platform's create — the Worker cut off there, or a
67
+ * create that failed with its rollback failing too — leave a `running` entry nothing will ever
68
+ * admit, and the once-policy would then bar the user from the workflow for good. An instance that
69
+ * ended before admitting itself leaves the same. An entry old enough, unadmitted, whose instance
70
+ * the runtime does not know or reports ended, is marked `failed`: the pair's next trigger reclaims
71
+ * it. Nothing of such an instance ran, since admission precedes its first effect. Platform-neutral:
72
+ * on AWS a planned start is always known to `get` (its registry row), and reads as ended only once
73
+ * its retries have given up, so only a start with nothing behind it is touched.
74
+ */
75
+ export declare function failLostStarts(env: JourneyEnv, store: JourneyStore, now: number): Promise<string[]>;
76
+ /** How long a visitor who never signs in is kept after they were last heard from. */
77
+ export declare const VISITOR_RETENTION_MS: number;
59
78
  /** How many users one sweep page evaluates: a unit of resumable work (AWS checkpoints each). */
60
79
  export declare const SWEEP_PAGE_SIZE = 500;
61
80
  /**
@@ -106,17 +125,26 @@ export interface IdentifyResult {
106
125
  * and re-evaluate segment triggers. The merge is one statement in the store, so
107
126
  * two identifies landing together cannot lose each other's fields.
108
127
  */
109
- export declare function identifyUser(env: JourneyEnv, userId: string, props: ProfileProps, store: JourneyStore): Promise<IdentifyResult>;
128
+ export declare function identifyUser(env: JourneyEnv, userId: string, props: ProfileProps, store: JourneyStore,
129
+ /** When these values were read; a property written later stays. The Worker's clock by default. */
130
+ at?: number): Promise<IdentifyResult>;
110
131
  export interface DeviceRegistration {
111
- userId: string;
132
+ deviceId?: string | undefined;
133
+ userId?: string | undefined;
134
+ visitorId?: string | undefined;
112
135
  token: string;
113
136
  platform: string;
114
- ts?: number;
115
137
  }
116
138
  /**
117
- * A push token registered for the user (or moved to them from another account: a token belongs
118
- * to one user at a time). Wakes the waits that read devices (`reachable('push')` inside
119
- * `waitUntil`), for the new owner and the previous one.
139
+ * The visitor signed in as the user: what the engine knows of them moves to the user, once. Their
140
+ * journeys carry on as the user's (`instanceOwner`), except one the user is already in, which
141
+ * stops. Answers nothing to do when already linked: every signed-in batch names its visitor.
142
+ */
143
+ export declare function linkVisitor(env: JourneyEnv, visitorId: string, userId: string, store: JourneyStore, at?: number): Promise<void>;
144
+ /**
145
+ * A push token registered for its current owner: the user, or before sign-in the visitor. The
146
+ * device holds only that, so a token moves to whoever registered it last. Wakes the waits that read
147
+ * devices (`reachable('push')` inside `waitUntil`), for the new owner and the previous one.
120
148
  */
121
149
  export declare function registerDevice(env: JourneyEnv, device: DeviceRegistration, store: JourneyStore): Promise<{
122
150
  ok: true;
@@ -177,9 +205,11 @@ export declare class BundleIntegrityError extends Error {
177
205
  * Bundle deploy, terraform-apply style: workflow bodies first (content-addressed, so a failed
178
206
  * deploy leaves only harmless orphans), then the routing swap. With a `version` the swap happens
179
207
  * only over an older live version (`replaceRoutesIfNewer`) — how journeyWorker applies the bundle
180
- * compiled into it; without one it is forced, whatever is live.
208
+ * compiled into it — unless `force` is set: then it happens whatever is live, and the version is
209
+ * recorded as live (`POST /bundle/deploy`). Without a version it is forced and records nothing
210
+ * (`POST /deploy`, a hand-posted bundle).
181
211
  */
182
- export declare function deployBundle(env: JourneyEnv, bundle: unknown, store: JourneyStore, version?: BundleVersion): Promise<DeployResult>;
212
+ export declare function deployBundle(env: JourneyEnv, bundle: unknown, store: JourneyStore, version?: BundleVersion, force?: boolean): Promise<DeployResult>;
183
213
  /** The channels a user opts out of per topic: `suppression` rows are keyed by (user, channel, topic). */
184
214
  export declare const SUBSCRIPTION_CHANNELS: readonly ['email', 'push'];
185
215
  export type SubscriptionChannel = (typeof SUBSCRIPTION_CHANNELS)[number];
@@ -1 +1 @@
1
- {"version":3,"file":"ingest.d.cts","names":[],"sources":["../../src/host/ingest.ts"],"mappings":";;;iBAgDiB;EACf;EACA;EACA,UAAU;EACV;;;;;;;;;;;;;;;EAeA;;iBAGe;;EAEf;;EAEA;EACA;EACA;;wBAGoB,YACpB,KAAK,YACL,OAAO,aACP,OAAO,eACN,QAAQ;;;;;;wBAwCW,gBACpB,KAAK,YACL,OAAO,cACP,gBACA,4BACC;;;;;;;;;wBA4EmB,uBACpB,KAAK,YACL,OAAO,cACP,gBACA,aACC;iBA0Fc;EACf;;EAEA;;EAEA;IAAS;IAAiB;;;;qBAIf;;;;;;;wBAQS,iBAAiB,KAAK,YAAY,OAAO,eAAe;iBAkB7D;;EAEf;;EAEA;;EAEA;;EAEA;;;;;;;;wBASoB,mBACpB,KAAK,YACL,OAAO,cACP;EAAQ;EAA4B;EAA4B;IAC/D,QAAQ;;;;;;;;;wBAsCW,cACpB,KAAK,YACL,OAAO,cACP,eACC,QAAQ;iBAgFM;EACf;EACA,OAAO;EACP;EACA;;;;;;;wBAQoB,aACpB,KAAK,YACL,gBACA,OAAO,cACP,OAAO,eACN,QAAQ;iBAiBM;EACf;EACA;EACA;EACA;;;;;;;wBAQoB,eACpB,KAAK,YACL,QAAQ,oBACR,OAAO,eACN;EAAU;EAAU;;;;;;;;;;;wBA2FD,WACpB,KAAK,YACL,gBACA,OAAO,cACP,eACC;EAAU;EAAU;EAAiB;;;;;;;wBAUlB,aACpB,KAAK,YACL,eACA,OAAO,eACN;EAAU;EAAU;;;wBAQD,aACpB,KAAK,YACL,eACA,OAAO,eACN;EAAU;EAAU;;iBASN;EACf;;;;;;;;EAQA;IAAY;IAAkB,SAAS;;;;;;EAKvC;;;qBAIW,6BAA6B;EACxC,YAAY;;;;;;;;wBA8CQ,aACpB,KAAK,YACL,iBACA,OAAO,cACP,UAAU,gBACT,QAAQ;;qBA2BE;YACD,8BAA8B;;;;;;iBAOzB,qBAAqB;;;;;EAKpC;;;;;;EAMA,QAAQ,OAAO;;;;;;;iBAQA,cAAc;EAC7B,OAAO,qBAAqB;EAC5B,MAAM,qBAAqB;;;iBAIZ,0BAA0B;EACzC;EACA,SAAS,QAAQ,OAAO;;;;;;;iBAQT,mBAAmB;EAClC,QAAQ,0BAA0B;EAClC,OAAO,0BAA0B;;;iBAIlB;EACf;EACA;;;;;;wBAOc,wBACd,eACA;EACG;EAAU,OAAO;;EAAyB;EAAW,QAAQ;;;wBA8D5C,iBACpB,OAAO,cACP,gBACA,4BACC,QAAQ;;;;;;;;;wBAaW,iBACpB,KAAK,YACL,OAAO,cACP,gBACA,OAAO,oBACP,2BACA,eACC,QAAQ;;iBAwBM,cAAc;EAC7B,SAAS;EACT,OAAO;;;;;;;iBAQQ,qBAAqB,mCAAmC,cAAc;EACrF,SAAS,cAAc;;;;;;;;;wBAUH,oBACpB,OAAO,cACP,oBACC;EAAU;EAAgB,SAAS;;;wBAchB,0BACpB,OAAO,cACP,mBACA,4BACC,QAAQ;;wBAOW,0BACpB,KAAK,YACL,OAAO,cACP,mBACA,OAAO,oBACP,2BACA,eACC,QAAQ"}
1
+ {"version":3,"file":"ingest.d.cts","names":[],"sources":["../../src/host/ingest.ts"],"mappings":";;;iBAiDiB;EACf;EACA;EACA,UAAU;EACV;;;;;;;;;;;;;;;EAeA;;iBAGe;;EAEf;;EAEA;EACA;EACA;;wBAGoB,YACpB,KAAK,YACL,OAAO,aACP,OAAO,eACN,QAAQ;;;;;;wBAwCW,gBACpB,KAAK,YACL,OAAO,cACP,gBACA,4BACC;;;;;;;;;wBAgFmB,uBACpB,KAAK,YACL,OAAO,cACP,gBACA,aACC;iBAkGc;EACf;;EAEA;;EAEA;IAAS;IAAiB;;;;;;;;qBAQf;;;;;;;;;;;wBAeS,eACpB,KAAK,YACL,OAAO,cACP,cACC;;qBAsBU;;qBAKA;;;;;;;wBAQS,iBAAiB,KAAK,YAAY,OAAO,eAAe;iBA4B7D;;EAEf;;EAEA;;EAEA;;EAEA;;;;;;;;wBASoB,mBACpB,KAAK,YACL,OAAO,cACP;EAAQ;EAA4B;EAA4B;IAC/D,QAAQ;;;;;;;;;wBAsCW,cACpB,KAAK,YACL,OAAO,cACP,eACC,QAAQ;iBAuJM;EACf;EACA,OAAO;EACP;EACA;;;;;;;wBAQoB,aACpB,KAAK,YACL,gBACA,OAAO,cACP,OAAO;;AAEP,cACC,QAAQ;iBAiBM;EACf;EACA;EACA;EACA;EACA;;;;;;;wBAQoB,YACpB,KAAK,YACL,mBACA,gBACA,OAAO,cACP,cACC;;;;;;wBA2BmB,eACpB,KAAK,YACL,QAAQ,oBACR,OAAO,eACN;EAAU;EAAU;;;;;;;;;;;wBA2GD,WACpB,KAAK,YACL,gBACA,OAAO,cACP,eACC;EAAU;EAAU;EAAiB;;;;;;;wBAUlB,aACpB,KAAK,YACL,eACA,OAAO,eACN;EAAU;EAAU;;;wBAQD,aACpB,KAAK,YACL,eACA,OAAO,eACN;EAAU;EAAU;;iBASN;EACf;;;;;;;;EAQA;IAAY;IAAkB,SAAS;;;;;;EAKvC;;;qBAIW,6BAA6B;EACxC,YAAY;;;;;;;;;;wBAgDQ,aACpB,KAAK,YACL,iBACA,OAAO,cACP,UAAU,eACV,kBACC,QAAQ;;qBA2BE;YACD,8BAA8B;;;;;;iBAOzB,qBAAqB;;;;;EAKpC;;;;;;EAMA,QAAQ,OAAO;;;;;;;iBAQA,cAAc;EAC7B,OAAO,qBAAqB;EAC5B,MAAM,qBAAqB;;;iBAIZ,0BAA0B;EACzC;EACA,SAAS,QAAQ,OAAO;;;;;;;iBAQT,mBAAmB;EAClC,QAAQ,0BAA0B;EAClC,OAAO,0BAA0B;;;iBAIlB;EACf;EACA;;;;;;wBAOc,wBACd,eACA;EACG;EAAU,OAAO;;EAAyB;EAAW,QAAQ;;;wBA8D5C,iBACpB,OAAO,cACP,gBACA,4BACC,QAAQ;;;;;;;;;wBAaW,iBACpB,KAAK,YACL,OAAO,cACP,gBACA,OAAO,oBACP,2BACA,eACC,QAAQ;;iBAwBM,cAAc;EAC7B,SAAS;EACT,OAAO;;;;;;;iBAQQ,qBAAqB,mCAAmC,cAAc;EACrF,SAAS,cAAc;;;;;;;;;wBAUH,oBACpB,OAAO,cACP,oBACC;EAAU;EAAgB,SAAS;;;wBAchB,0BACpB,OAAO,cACP,mBACA,4BACC,QAAQ;;wBAOW,0BACpB,KAAK,YACL,OAAO,cACP,mBACA,OAAO,oBACP,2BACA,eACC,QAAQ"}
@@ -56,6 +56,25 @@ export interface SweepResult {
56
56
  size: number;
57
57
  }[];
58
58
  }
59
+ /**
60
+ * How long an entry may wait for its runner's admission before maintenance looks for its
61
+ * instance. A create takes well under a second and a queued instance is found by `get`, so this
62
+ * only has to outlast a create still in flight — generously, since a wrong call fails a journey.
63
+ */
64
+ export declare const LOST_START_AFTER_MS: number;
65
+ /**
66
+ * Starts lost between the entry and the platform's create — the Worker cut off there, or a
67
+ * create that failed with its rollback failing too — leave a `running` entry nothing will ever
68
+ * admit, and the once-policy would then bar the user from the workflow for good. An instance that
69
+ * ended before admitting itself leaves the same. An entry old enough, unadmitted, whose instance
70
+ * the runtime does not know or reports ended, is marked `failed`: the pair's next trigger reclaims
71
+ * it. Nothing of such an instance ran, since admission precedes its first effect. Platform-neutral:
72
+ * on AWS a planned start is always known to `get` (its registry row), and reads as ended only once
73
+ * its retries have given up, so only a start with nothing behind it is touched.
74
+ */
75
+ export declare function failLostStarts(env: JourneyEnv, store: JourneyStore, now: number): Promise<string[]>;
76
+ /** How long a visitor who never signs in is kept after they were last heard from. */
77
+ export declare const VISITOR_RETENTION_MS: number;
59
78
  /** How many users one sweep page evaluates: a unit of resumable work (AWS checkpoints each). */
60
79
  export declare const SWEEP_PAGE_SIZE = 500;
61
80
  /**
@@ -106,17 +125,26 @@ export interface IdentifyResult {
106
125
  * and re-evaluate segment triggers. The merge is one statement in the store, so
107
126
  * two identifies landing together cannot lose each other's fields.
108
127
  */
109
- export declare function identifyUser(env: JourneyEnv, userId: string, props: ProfileProps, store: JourneyStore): Promise<IdentifyResult>;
128
+ export declare function identifyUser(env: JourneyEnv, userId: string, props: ProfileProps, store: JourneyStore,
129
+ /** When these values were read; a property written later stays. The Worker's clock by default. */
130
+ at?: number): Promise<IdentifyResult>;
110
131
  export interface DeviceRegistration {
111
- userId: string;
132
+ deviceId?: string | undefined;
133
+ userId?: string | undefined;
134
+ visitorId?: string | undefined;
112
135
  token: string;
113
136
  platform: string;
114
- ts?: number;
115
137
  }
116
138
  /**
117
- * A push token registered for the user (or moved to them from another account: a token belongs
118
- * to one user at a time). Wakes the waits that read devices (`reachable('push')` inside
119
- * `waitUntil`), for the new owner and the previous one.
139
+ * The visitor signed in as the user: what the engine knows of them moves to the user, once. Their
140
+ * journeys carry on as the user's (`instanceOwner`), except one the user is already in, which
141
+ * stops. Answers nothing to do when already linked: every signed-in batch names its visitor.
142
+ */
143
+ export declare function linkVisitor(env: JourneyEnv, visitorId: string, userId: string, store: JourneyStore, at?: number): Promise<void>;
144
+ /**
145
+ * A push token registered for its current owner: the user, or before sign-in the visitor. The
146
+ * device holds only that, so a token moves to whoever registered it last. Wakes the waits that read
147
+ * devices (`reachable('push')` inside `waitUntil`), for the new owner and the previous one.
120
148
  */
121
149
  export declare function registerDevice(env: JourneyEnv, device: DeviceRegistration, store: JourneyStore): Promise<{
122
150
  ok: true;
@@ -177,9 +205,11 @@ export declare class BundleIntegrityError extends Error {
177
205
  * Bundle deploy, terraform-apply style: workflow bodies first (content-addressed, so a failed
178
206
  * deploy leaves only harmless orphans), then the routing swap. With a `version` the swap happens
179
207
  * only over an older live version (`replaceRoutesIfNewer`) — how journeyWorker applies the bundle
180
- * compiled into it; without one it is forced, whatever is live.
208
+ * compiled into it — unless `force` is set: then it happens whatever is live, and the version is
209
+ * recorded as live (`POST /bundle/deploy`). Without a version it is forced and records nothing
210
+ * (`POST /deploy`, a hand-posted bundle).
181
211
  */
182
- export declare function deployBundle(env: JourneyEnv, bundle: unknown, store: JourneyStore, version?: BundleVersion): Promise<DeployResult>;
212
+ export declare function deployBundle(env: JourneyEnv, bundle: unknown, store: JourneyStore, version?: BundleVersion, force?: boolean): Promise<DeployResult>;
183
213
  /** The channels a user opts out of per topic: `suppression` rows are keyed by (user, channel, topic). */
184
214
  export declare const SUBSCRIPTION_CHANNELS: readonly ['email', 'push'];
185
215
  export type SubscriptionChannel = (typeof SUBSCRIPTION_CHANNELS)[number];
@@ -1 +1 @@
1
- {"version":3,"file":"ingest.d.mts","names":[],"sources":["../../src/host/ingest.ts"],"mappings":";;;iBAgDiB;EACf;EACA;EACA,UAAU;EACV;;;;;;;;;;;;;;;EAeA;;iBAGe;;EAEf;;EAEA;EACA;EACA;;wBAGoB,YACpB,KAAK,YACL,OAAO,aACP,OAAO,eACN,QAAQ;;;;;;wBAwCW,gBACpB,KAAK,YACL,OAAO,cACP,gBACA,4BACC;;;;;;;;;wBA4EmB,uBACpB,KAAK,YACL,OAAO,cACP,gBACA,aACC;iBA0Fc;EACf;;EAEA;;EAEA;IAAS;IAAiB;;;;qBAIf;;;;;;;wBAQS,iBAAiB,KAAK,YAAY,OAAO,eAAe;iBAkB7D;;EAEf;;EAEA;;EAEA;;EAEA;;;;;;;;wBASoB,mBACpB,KAAK,YACL,OAAO,cACP;EAAQ;EAA4B;EAA4B;IAC/D,QAAQ;;;;;;;;;wBAsCW,cACpB,KAAK,YACL,OAAO,cACP,eACC,QAAQ;iBAgFM;EACf;EACA,OAAO;EACP;EACA;;;;;;;wBAQoB,aACpB,KAAK,YACL,gBACA,OAAO,cACP,OAAO,eACN,QAAQ;iBAiBM;EACf;EACA;EACA;EACA;;;;;;;wBAQoB,eACpB,KAAK,YACL,QAAQ,oBACR,OAAO,eACN;EAAU;EAAU;;;;;;;;;;;wBA2FD,WACpB,KAAK,YACL,gBACA,OAAO,cACP,eACC;EAAU;EAAU;EAAiB;;;;;;;wBAUlB,aACpB,KAAK,YACL,eACA,OAAO,eACN;EAAU;EAAU;;;wBAQD,aACpB,KAAK,YACL,eACA,OAAO,eACN;EAAU;EAAU;;iBASN;EACf;;;;;;;;EAQA;IAAY;IAAkB,SAAS;;;;;;EAKvC;;;qBAIW,6BAA6B;EACxC,YAAY;;;;;;;;wBA8CQ,aACpB,KAAK,YACL,iBACA,OAAO,cACP,UAAU,gBACT,QAAQ;;qBA2BE;YACD,8BAA8B;;;;;;iBAOzB,qBAAqB;;;;;EAKpC;;;;;;EAMA,QAAQ,OAAO;;;;;;;iBAQA,cAAc;EAC7B,OAAO,qBAAqB;EAC5B,MAAM,qBAAqB;;;iBAIZ,0BAA0B;EACzC;EACA,SAAS,QAAQ,OAAO;;;;;;;iBAQT,mBAAmB;EAClC,QAAQ,0BAA0B;EAClC,OAAO,0BAA0B;;;iBAIlB;EACf;EACA;;;;;;wBAOc,wBACd,eACA;EACG;EAAU,OAAO;;EAAyB;EAAW,QAAQ;;;wBA8D5C,iBACpB,OAAO,cACP,gBACA,4BACC,QAAQ;;;;;;;;;wBAaW,iBACpB,KAAK,YACL,OAAO,cACP,gBACA,OAAO,oBACP,2BACA,eACC,QAAQ;;iBAwBM,cAAc;EAC7B,SAAS;EACT,OAAO;;;;;;;iBAQQ,qBAAqB,mCAAmC,cAAc;EACrF,SAAS,cAAc;;;;;;;;;wBAUH,oBACpB,OAAO,cACP,oBACC;EAAU;EAAgB,SAAS;;;wBAchB,0BACpB,OAAO,cACP,mBACA,4BACC,QAAQ;;wBAOW,0BACpB,KAAK,YACL,OAAO,cACP,mBACA,OAAO,oBACP,2BACA,eACC,QAAQ"}
1
+ {"version":3,"file":"ingest.d.mts","names":[],"sources":["../../src/host/ingest.ts"],"mappings":";;;iBAiDiB;EACf;EACA;EACA,UAAU;EACV;;;;;;;;;;;;;;;EAeA;;iBAGe;;EAEf;;EAEA;EACA;EACA;;wBAGoB,YACpB,KAAK,YACL,OAAO,aACP,OAAO,eACN,QAAQ;;;;;;wBAwCW,gBACpB,KAAK,YACL,OAAO,cACP,gBACA,4BACC;;;;;;;;;wBAgFmB,uBACpB,KAAK,YACL,OAAO,cACP,gBACA,aACC;iBAkGc;EACf;;EAEA;;EAEA;IAAS;IAAiB;;;;;;;;qBAQf;;;;;;;;;;;wBAeS,eACpB,KAAK,YACL,OAAO,cACP,cACC;;qBAsBU;;qBAKA;;;;;;;wBAQS,iBAAiB,KAAK,YAAY,OAAO,eAAe;iBA4B7D;;EAEf;;EAEA;;EAEA;;EAEA;;;;;;;;wBASoB,mBACpB,KAAK,YACL,OAAO,cACP;EAAQ;EAA4B;EAA4B;IAC/D,QAAQ;;;;;;;;;wBAsCW,cACpB,KAAK,YACL,OAAO,cACP,eACC,QAAQ;iBAuJM;EACf;EACA,OAAO;EACP;EACA;;;;;;;wBAQoB,aACpB,KAAK,YACL,gBACA,OAAO,cACP,OAAO;;AAEP,cACC,QAAQ;iBAiBM;EACf;EACA;EACA;EACA;EACA;;;;;;;wBAQoB,YACpB,KAAK,YACL,mBACA,gBACA,OAAO,cACP,cACC;;;;;;wBA2BmB,eACpB,KAAK,YACL,QAAQ,oBACR,OAAO,eACN;EAAU;EAAU;;;;;;;;;;;wBA2GD,WACpB,KAAK,YACL,gBACA,OAAO,cACP,eACC;EAAU;EAAU;EAAiB;;;;;;;wBAUlB,aACpB,KAAK,YACL,eACA,OAAO,eACN;EAAU;EAAU;;;wBAQD,aACpB,KAAK,YACL,eACA,OAAO,eACN;EAAU;EAAU;;iBASN;EACf;;;;;;;;EAQA;IAAY;IAAkB,SAAS;;;;;;EAKvC;;;qBAIW,6BAA6B;EACxC,YAAY;;;;;;;;;;wBAgDQ,aACpB,KAAK,YACL,iBACA,OAAO,cACP,UAAU,eACV,kBACC,QAAQ;;qBA2BE;YACD,8BAA8B;;;;;;iBAOzB,qBAAqB;;;;;EAKpC;;;;;;EAMA,QAAQ,OAAO;;;;;;;iBAQA,cAAc;EAC7B,OAAO,qBAAqB;EAC5B,MAAM,qBAAqB;;;iBAIZ,0BAA0B;EACzC;EACA,SAAS,QAAQ,OAAO;;;;;;;iBAQT,mBAAmB;EAClC,QAAQ,0BAA0B;EAClC,OAAO,0BAA0B;;;iBAIlB;EACf;EACA;;;;;;wBAOc,wBACd,eACA;EACG;EAAU,OAAO;;EAAyB;EAAW,QAAQ;;;wBA8D5C,iBACpB,OAAO,cACP,gBACA,4BACC,QAAQ;;;;;;;;;wBAaW,iBACpB,KAAK,YACL,OAAO,cACP,gBACA,OAAO,oBACP,2BACA,eACC,QAAQ;;iBAwBM,cAAc;EAC7B,SAAS;EACT,OAAO;;;;;;;iBAQQ,qBAAqB,mCAAmC,cAAc;EACrF,SAAS,cAAc;;;;;;;;;wBAUH,oBACpB,OAAO,cACP,oBACC;EAAU;EAAgB,SAAS;;;wBAchB,0BACpB,OAAO,cACP,mBACA,4BACC,QAAQ;;wBAOW,0BACpB,KAAK,YACL,OAAO,cACP,mBACA,OAAO,oBACP,2BACA,eACC,QAAQ"}
@@ -6,6 +6,7 @@ import { BundleIR } from "../ir.mjs";
6
6
  import { isMessageId } from "../reporting/tracking.mjs";
7
7
  import { JourneyFactSource } from "../store/facts.mjs";
8
8
  import { UserErasedError } from "../store/erasure.mjs";
9
+ import { isVisitorSubject } from "../store/identity.mjs";
9
10
  import { SchemaMismatchError } from "../store/index.mjs";
10
11
  import { InstanceNotFoundError, WAKE_EVENT_TYPE } from "./platform.mjs";
11
12
  // src/host/ingest.ts
@@ -86,6 +87,7 @@ async function wakeSubscribers(env, store, userId, events) {
86
87
  async function startTriggeredJourneys(env, store, input, ts) {
87
88
  const started = [];
88
89
  for (const route of await store.findTriggers(input.event)) {
90
+ if (isVisitorSubject(input.userId) && !route.visitors) continue;
89
91
  if (route.where !== null && !matchesWhere(input.payload, route.where)) continue;
90
92
  if (route.filter !== null) {
91
93
  const facts = new JourneyFactSource(store, input.userId);
@@ -93,7 +95,8 @@ async function startTriggeredJourneys(env, store, input, ts) {
93
95
  }
94
96
  const instanceId = await startJourney(env, store, route.workflow, route.hash, input.userId, ts, {
95
97
  event: input.event,
96
- payload: input.payload
98
+ payload: input.payload,
99
+ ts
97
100
  }, route.version);
98
101
  if (instanceId !== null) started.push(instanceId);
99
102
  }
@@ -117,6 +120,7 @@ async function refreshSegmentTriggers(env, store, userId, ts) {
117
120
  * users who enter after it is deployed; a user already a member is not entered again.
118
121
  */
119
122
  async function refreshSegments(env, store, userId, ts, routes, segments = [], definitions = /* @__PURE__ */ new Map()) {
123
+ if (isVisitorSubject(userId)) return [];
120
124
  const bySegment = new Map(segments.map((name) => [name, []]));
121
125
  for (const route of routes) {
122
126
  const list = bySegment.get(route.segment) ?? [];
@@ -152,7 +156,7 @@ async function refreshSegments(env, store, userId, ts, routes, segments = [], de
152
156
  payload: { segment: segmentId }
153
157
  }, route.version);
154
158
  if (instanceId !== null) started.push(instanceId);
155
- await store.completeSegmentStart(segmentId, userId, route.workflow);
159
+ if (instanceId !== null || await entryStarted(env, store, route.workflow, userId)) await store.completeSegmentStart(segmentId, userId, route.workflow);
156
160
  } catch (error) {
157
161
  failures.push(error);
158
162
  }
@@ -161,6 +165,43 @@ async function refreshSegments(env, store, userId, ts, routes, segments = [], de
161
165
  if (failures.length > 0) throw failures[0];
162
166
  return started;
163
167
  }
168
+ /**
169
+ * How long an entry may wait for its runner's admission before maintenance looks for its
170
+ * instance. A create takes well under a second and a queued instance is found by `get`, so this
171
+ * only has to outlast a create still in flight — generously, since a wrong call fails a journey.
172
+ */
173
+ const LOST_START_AFTER_MS = 9e5;
174
+ /** Entries one maintenance run examines, oldest first; the next run takes the rest. */
175
+ const LOST_START_BATCH = 25;
176
+ /**
177
+ * Starts lost between the entry and the platform's create — the Worker cut off there, or a
178
+ * create that failed with its rollback failing too — leave a `running` entry nothing will ever
179
+ * admit, and the once-policy would then bar the user from the workflow for good. An instance that
180
+ * ended before admitting itself leaves the same. An entry old enough, unadmitted, whose instance
181
+ * the runtime does not know or reports ended, is marked `failed`: the pair's next trigger reclaims
182
+ * it. Nothing of such an instance ran, since admission precedes its first effect. Platform-neutral:
183
+ * on AWS a planned start is always known to `get` (its registry row), and reads as ended only once
184
+ * its retries have given up, so only a start with nothing behind it is touched.
185
+ */
186
+ async function failLostStarts(env, store, now) {
187
+ const failed = [];
188
+ for (const entry of await store.unadmittedEntries(now - LOST_START_AFTER_MS, LOST_START_BATCH)) {
189
+ try {
190
+ if (!await hasEnded(await env.JOURNEY.get(entry.instanceId))) continue;
191
+ } catch (error) {
192
+ if (!(error instanceof InstanceNotFoundError)) continue;
193
+ }
194
+ if (await store.failUnadmittedEntry(entry.instanceId)) {
195
+ failed.push(entry.instanceId);
196
+ console.error(`[workflow] the start of ${entry.instanceId} (${entry.workflow}) was lost before its instance existed: marked failed, the next trigger re-enters`);
197
+ }
198
+ }
199
+ return failed;
200
+ }
201
+ /** How long a visitor who never signs in is kept after they were last heard from. */
202
+ const VISITOR_RETENTION_MS = 7776e6;
203
+ /** Visitors one sweep prunes: the next sweep goes on. */
204
+ const VISITOR_PRUNE_BATCH = 100;
164
205
  /** How many users one sweep page evaluates: a unit of resumable work (AWS checkpoints each). */
165
206
  const SWEEP_PAGE_SIZE = 500;
166
207
  /**
@@ -175,6 +216,16 @@ async function sweepMaintenance(env, store) {
175
216
  } catch (error) {
176
217
  console.error("[workflow] runtime maintenance failed", error);
177
218
  }
219
+ try {
220
+ await failLostStarts(env, store, Date.now());
221
+ } catch (error) {
222
+ console.error("[workflow] reclaiming lost starts failed", error);
223
+ }
224
+ try {
225
+ await store.pruneVisitors(Date.now() - VISITOR_RETENTION_MS, VISITOR_PRUNE_BATCH);
226
+ } catch (error) {
227
+ console.error("[workflow] pruning idle visitors failed", error);
228
+ }
178
229
  for (const userId of await store.pendingErasures()) try {
179
230
  await eraseUser(env, userId, store, Date.now());
180
231
  } catch (error) {
@@ -246,6 +297,50 @@ async function sweepSegments(env, store, now = Date.now()) {
246
297
  };
247
298
  }
248
299
  /**
300
+ * Whether the (workflow, user) entry's start is known to have happened: the runner admitted
301
+ * itself, the entry already ended, or the runtime knows its instance and it has not ended. An
302
+ * unadmitted entry whose instance cannot be found, cannot be looked up right now, or ended without
303
+ * admitting itself is not; the caller asks again.
304
+ */
305
+ async function entryStarted(env, store, workflow, userId) {
306
+ const entry = await store.getEntry(workflow, userId);
307
+ if (entry === void 0 || entry.status === "failed") return false;
308
+ if (entry.admitted || entry.status !== "running") return true;
309
+ try {
310
+ return !await hasEnded(await env.JOURNEY.get(entry.instanceId));
311
+ } catch {
312
+ return false;
313
+ }
314
+ }
315
+ /**
316
+ * Create the instance, idempotently by id, on a runtime that does not record its starts
317
+ * (Cloudflare): a create the platform refuses because the id already exists — a concurrent
318
+ * retry of the same start won the race — is the instance we wanted, not a failure. Rolling the
319
+ * entry back over it would strand a running instance with no ledger row, and it would never be
320
+ * admitted. A create whose instance the runtime cannot find failed, and so did one refused by an
321
+ * instance that already ended: the id stays taken by that corpse, which will never admit itself,
322
+ * and taking it for the start would leave a running entry nothing ever fails. True when this call
323
+ * created it, false when it already existed.
324
+ */
325
+ async function createInstance(env, id, params) {
326
+ try {
327
+ await env.JOURNEY.create({
328
+ id,
329
+ params
330
+ });
331
+ return true;
332
+ } catch (error) {
333
+ let ended;
334
+ try {
335
+ ended = await hasEnded(await env.JOURNEY.get(id));
336
+ } catch {
337
+ throw error;
338
+ }
339
+ if (ended) throw error;
340
+ return false;
341
+ }
342
+ }
343
+ /**
249
344
  * Create one journey instance behind the entry ledger. Returns null when the
250
345
  * once-policy blocks the entry (the store's attempt is atomic, so concurrent
251
346
  * ingests cannot double-enter). A pair whose previous instance is recorded as
@@ -274,17 +369,23 @@ async function startJourney(env, store, workflow, hash, userId, ts, trigger, ver
274
369
  execution
275
370
  });
276
371
  if (outcome === null) {
277
- await env.JOURNEY.resume?.({
278
- workflow,
279
- userId
280
- });
281
- return null;
372
+ if (env.JOURNEY.resume !== void 0) {
373
+ await env.JOURNEY.resume({
374
+ workflow,
375
+ userId
376
+ });
377
+ return null;
378
+ }
379
+ const entry = await store.getEntry(workflow, userId);
380
+ if (entry?.instanceId !== instanceId || entry.status !== "running" || entry.admitted) return null;
381
+ return await createInstance(env, instanceId, params) ? instanceId : null;
282
382
  }
283
383
  try {
284
- await env.JOURNEY.create({
384
+ if (execution !== void 0) await env.JOURNEY.create({
285
385
  id: instanceId,
286
386
  params
287
387
  });
388
+ else await createInstance(env, instanceId, params);
288
389
  } catch (error) {
289
390
  if (execution !== void 0) throw error;
290
391
  if (outcome === "reclaimed") await store.setEntryStatus(instanceId, "failed");
@@ -309,30 +410,58 @@ function buildInstanceId(workflow, hash, userId, ts) {
309
410
  * and re-evaluate segment triggers. The merge is one statement in the store, so
310
411
  * two identifies landing together cannot lose each other's fields.
311
412
  */
312
- async function identifyUser(env, userId, props, store) {
413
+ async function identifyUser(env, userId, props, store, at) {
313
414
  await refuseIfErased(store, userId);
314
415
  const ts = Date.now();
315
416
  return {
316
417
  ok: true,
317
- props: await store.mergeProfile(userId, props),
418
+ props: await store.mergeProfile(userId, props, at ?? ts),
318
419
  woke: await wakeSubscribers(env, store, userId, Object.keys(props).map(profileUpdatedEvent)),
319
420
  started: await refreshSegmentTriggers(env, store, userId, ts)
320
421
  };
321
422
  }
322
423
  /**
323
- * A push token registered for the user (or moved to them from another account: a token belongs
324
- * to one user at a time). Wakes the waits that read devices (`reachable('push')` inside
325
- * `waitUntil`), for the new owner and the previous one.
424
+ * The visitor signed in as the user: what the engine knows of them moves to the user, once. Their
425
+ * journeys carry on as the user's (`instanceOwner`), except one the user is already in, which
426
+ * stops. Answers nothing to do when already linked: every signed-in batch names its visitor.
427
+ */
428
+ async function linkVisitor(env, visitorId, userId, store, at = Date.now()) {
429
+ await refuseIfErased(store, userId);
430
+ const linked = await store.linkedUser(visitorId);
431
+ if (linked !== void 0) {
432
+ if (linked !== userId) console.warn(`[workflow] visitor ${visitorId} stays with its first user, not ${userId}`);
433
+ return;
434
+ }
435
+ const merge = await store.linkVisitor(visitorId, userId, at);
436
+ if (merge === null) return;
437
+ for (const instanceId of merge.dropped) await stopInstance(env, instanceId);
438
+ await wakeSubscribers(env, store, userId, [
439
+ DEVICES_UPDATED_EVENT,
440
+ ...merge.events,
441
+ ...merge.props.map(profileUpdatedEvent)
442
+ ]);
443
+ await refreshSegmentTriggers(env, store, userId, at);
444
+ }
445
+ /**
446
+ * A push token registered for its current owner: the user, or before sign-in the visitor. The
447
+ * device holds only that, so a token moves to whoever registered it last. Wakes the waits that read
448
+ * devices (`reachable('push')` inside `waitUntil`), for the new owner and the previous one.
326
449
  */
327
450
  async function registerDevice(env, device, store) {
328
- await refuseIfErased(store, device.userId);
329
- const previous = await store.removeDevice(device.token);
330
- await store.upsertDevice({
331
- ...device,
332
- ts: device.ts ?? Date.now()
451
+ const { deviceId, userId, visitorId, token, platform } = device;
452
+ if (userId !== void 0 && visitorId !== void 0) await linkVisitor(env, visitorId, userId, store);
453
+ const subject = userId ?? await store.touchVisitor(visitorId ?? "", Date.now());
454
+ await refuseIfErased(store, subject);
455
+ const previous = await store.upsertDevice({
456
+ deviceId,
457
+ userId: isVisitorSubject(subject) ? null : subject,
458
+ visitorId,
459
+ token,
460
+ platform,
461
+ at: Date.now()
333
462
  });
334
- let woke = await wakeSubscribers(env, store, device.userId, [DEVICES_UPDATED_EVENT]);
335
- if (previous !== void 0 && previous !== device.userId) woke += await wakeSubscribers(env, store, previous, [DEVICES_UPDATED_EVENT]);
463
+ let woke = await wakeSubscribers(env, store, subject, [DEVICES_UPDATED_EVENT]);
464
+ for (const owner of previous) woke += await wakeSubscribers(env, store, owner, [DEVICES_UPDATED_EVENT]);
336
465
  return {
337
466
  ok: true,
338
467
  woke
@@ -344,6 +473,11 @@ const STOPPED_STATUSES = /* @__PURE__ */ new Set([
344
473
  "errored",
345
474
  "terminated"
346
475
  ]);
476
+ /** Whether the instance reports a terminal state. An unreadable status says nothing: false. */
477
+ async function hasEnded(instance) {
478
+ const status = await instance.status?.().catch(() => void 0);
479
+ return status !== void 0 && STOPPED_STATUSES.has(status.status);
480
+ }
347
481
  /**
348
482
  * Stop one admitted instance for good; true once it is known not to run again: terminated, or
349
483
  * already in a terminal state (terminate refuses those). Any other failure is not taken for
@@ -368,8 +502,7 @@ async function stopInstance(env, instanceId) {
368
502
  await instance.terminate();
369
503
  return true;
370
504
  } catch (error) {
371
- const status = await instance.status?.().catch(() => void 0);
372
- if (status !== void 0 && STOPPED_STATUSES.has(status.status)) return true;
505
+ if (await hasEnded(instance)) return true;
373
506
  console.warn(`[workflow] terminating ${instanceId} failed`, error);
374
507
  return false;
375
508
  }
@@ -479,16 +612,18 @@ function verifyBundleHashes(parsed) {
479
612
  * Bundle deploy, terraform-apply style: workflow bodies first (content-addressed, so a failed
480
613
  * deploy leaves only harmless orphans), then the routing swap. With a `version` the swap happens
481
614
  * only over an older live version (`replaceRoutesIfNewer`) — how journeyWorker applies the bundle
482
- * compiled into it; without one it is forced, whatever is live.
615
+ * compiled into it — unless `force` is set: then it happens whatever is live, and the version is
616
+ * recorded as live (`POST /bundle/deploy`). Without a version it is forced and records nothing
617
+ * (`POST /deploy`, a hand-posted bundle).
483
618
  */
484
- async function deployBundle(env, bundle, store, version) {
619
+ async function deployBundle(env, bundle, store, version, force = false) {
485
620
  const parsed = BundleIR.parse(bundle);
486
621
  verifyBundleHashes(parsed);
487
622
  const problems = await store.checkSchema();
488
623
  if (problems.length > 0) throw new SchemaMismatchError(problems);
489
624
  for (const workflow of parsed.workflows) await store.putWorkflowIR(workflow);
490
625
  let applied = true;
491
- if (version === void 0) await store.replaceRoutes(parsed);
626
+ if (version === void 0 || force) await store.replaceRoutes(parsed, version);
492
627
  else applied = await store.replaceRoutesIfNewer(parsed, version);
493
628
  const unrouted = parsed.workflows.filter((workflow) => workflow.trigger.type === "date" || workflow.trigger.type === "webhook").map((workflow) => ({
494
629
  workflow: workflow.id,
@@ -660,6 +795,6 @@ async function setSubscriptionsByMessage(env, store, messageId, patch, topics, n
660
795
  message: owner.message
661
796
  };
662
797
  }
663
- export { BundleIntegrityError, SUBSCRIPTION_CHANNELS, SWEEP_PAGE_SIZE, deployBundle, forgetUser, getSubscriptions, getSubscriptionsByMessage, identifyUser, ingestEvent, parseSubscriptionsPatch, refreshSegmentTriggers, registerDevice, removeDevice, resolveMessageOwner, setSubscriptions, setSubscriptionsByMessage, sweepMaintenance, sweepSegments, sweepSegmentsPaged, unbindDevice, wakeSubscribers };
798
+ export { BundleIntegrityError, LOST_START_AFTER_MS, SUBSCRIPTION_CHANNELS, SWEEP_PAGE_SIZE, VISITOR_RETENTION_MS, deployBundle, failLostStarts, forgetUser, getSubscriptions, getSubscriptionsByMessage, identifyUser, ingestEvent, linkVisitor, parseSubscriptionsPatch, refreshSegmentTriggers, registerDevice, removeDevice, resolveMessageOwner, setSubscriptions, setSubscriptionsByMessage, sweepMaintenance, sweepSegments, sweepSegmentsPaged, unbindDevice, wakeSubscribers };
664
799
 
665
800
  //# sourceMappingURL=ingest.mjs.map