@shware/workflow 7.0.0 → 9.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 (166) hide show
  1. package/README.md +125 -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 -3
  9. package/dist/aws/index.cjs.map +1 -1
  10. package/dist/aws/index.mjs +3 -3
  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 +5 -4
  21. package/dist/cloudflare/index.d.mts +5 -4
  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 +12 -1
  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 +12 -1
  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/engine/condition.cjs +3 -3
  49. package/dist/engine/condition.cjs.map +1 -1
  50. package/dist/engine/condition.d.cts +3 -3
  51. package/dist/engine/condition.d.mts +3 -3
  52. package/dist/engine/condition.mjs +3 -3
  53. package/dist/engine/condition.mjs.map +1 -1
  54. package/dist/engine/interpreter.cjs +9 -5
  55. package/dist/engine/interpreter.cjs.map +1 -1
  56. package/dist/engine/interpreter.d.cts.map +1 -1
  57. package/dist/engine/interpreter.d.mts.map +1 -1
  58. package/dist/engine/interpreter.mjs +9 -5
  59. package/dist/engine/interpreter.mjs.map +1 -1
  60. package/dist/engine/ports.d.cts +2 -0
  61. package/dist/engine/ports.d.cts.map +1 -1
  62. package/dist/engine/ports.d.mts +2 -0
  63. package/dist/engine/ports.d.mts.map +1 -1
  64. package/dist/host/batch.cjs +172 -0
  65. package/dist/host/batch.cjs.map +1 -0
  66. package/dist/host/batch.d.cts +88 -0
  67. package/dist/host/batch.d.cts.map +1 -0
  68. package/dist/host/batch.d.mts +88 -0
  69. package/dist/host/batch.d.mts.map +1 -0
  70. package/dist/host/batch.mjs +163 -0
  71. package/dist/host/batch.mjs.map +1 -0
  72. package/dist/host/execute.cjs +10 -6
  73. package/dist/host/execute.cjs.map +1 -1
  74. package/dist/host/execute.mjs +10 -6
  75. package/dist/host/execute.mjs.map +1 -1
  76. package/dist/host/http.cjs +30 -89
  77. package/dist/host/http.cjs.map +1 -1
  78. package/dist/host/http.d.cts +1 -1
  79. package/dist/host/http.d.cts.map +1 -1
  80. package/dist/host/http.d.mts +1 -1
  81. package/dist/host/http.d.mts.map +1 -1
  82. package/dist/host/http.mjs +24 -83
  83. package/dist/host/http.mjs.map +1 -1
  84. package/dist/host/ingest.cjs +84 -35
  85. package/dist/host/ingest.cjs.map +1 -1
  86. package/dist/host/ingest.d.cts +29 -16
  87. package/dist/host/ingest.d.cts.map +1 -1
  88. package/dist/host/ingest.d.mts +29 -16
  89. package/dist/host/ingest.d.mts.map +1 -1
  90. package/dist/host/ingest.mjs +83 -36
  91. package/dist/host/ingest.mjs.map +1 -1
  92. package/dist/host/journey-host.cjs +6 -0
  93. package/dist/host/journey-host.cjs.map +1 -1
  94. package/dist/host/journey-host.d.cts +5 -2
  95. package/dist/host/journey-host.d.cts.map +1 -1
  96. package/dist/host/journey-host.d.mts +5 -2
  97. package/dist/host/journey-host.d.mts.map +1 -1
  98. package/dist/host/journey-host.mjs +6 -0
  99. package/dist/host/journey-host.mjs.map +1 -1
  100. package/dist/host/platform.cjs.map +1 -1
  101. package/dist/host/platform.d.cts +2 -0
  102. package/dist/host/platform.d.cts.map +1 -1
  103. package/dist/host/platform.d.mts +2 -0
  104. package/dist/host/platform.d.mts.map +1 -1
  105. package/dist/host/platform.mjs.map +1 -1
  106. package/dist/ir.cjs +2 -1
  107. package/dist/ir.cjs.map +1 -1
  108. package/dist/ir.d.cts +3 -0
  109. package/dist/ir.d.cts.map +1 -1
  110. package/dist/ir.d.mts +3 -0
  111. package/dist/ir.d.mts.map +1 -1
  112. package/dist/ir.mjs +2 -1
  113. package/dist/ir.mjs.map +1 -1
  114. package/dist/store/condition-sql.cjs +144 -0
  115. package/dist/store/condition-sql.cjs.map +1 -0
  116. package/dist/store/condition-sql.d.cts +38 -0
  117. package/dist/store/condition-sql.d.cts.map +1 -0
  118. package/dist/store/condition-sql.d.mts +38 -0
  119. package/dist/store/condition-sql.d.mts.map +1 -0
  120. package/dist/store/condition-sql.mjs +143 -0
  121. package/dist/store/condition-sql.mjs.map +1 -0
  122. package/dist/store/facts.cjs +13 -9
  123. package/dist/store/facts.cjs.map +1 -1
  124. package/dist/store/facts.d.cts +6 -2
  125. package/dist/store/facts.d.cts.map +1 -1
  126. package/dist/store/facts.d.mts +6 -2
  127. package/dist/store/facts.d.mts.map +1 -1
  128. package/dist/store/facts.mjs +13 -9
  129. package/dist/store/facts.mjs.map +1 -1
  130. package/dist/store/identity.cjs +34 -0
  131. package/dist/store/identity.cjs.map +1 -0
  132. package/dist/store/identity.d.cts +13 -0
  133. package/dist/store/identity.d.cts.map +1 -0
  134. package/dist/store/identity.d.mts +13 -0
  135. package/dist/store/identity.d.mts.map +1 -0
  136. package/dist/store/identity.mjs +30 -0
  137. package/dist/store/identity.mjs.map +1 -0
  138. package/dist/store/index.cjs +5 -0
  139. package/dist/store/index.cjs.map +1 -1
  140. package/dist/store/index.d.cts +67 -21
  141. package/dist/store/index.d.cts.map +1 -1
  142. package/dist/store/index.d.mts +67 -21
  143. package/dist/store/index.d.mts.map +1 -1
  144. package/dist/store/index.mjs +2 -1
  145. package/dist/store/index.mjs.map +1 -1
  146. package/dist/store/postgres.cjs +248 -54
  147. package/dist/store/postgres.cjs.map +1 -1
  148. package/dist/store/postgres.d.cts +25 -7
  149. package/dist/store/postgres.d.cts.map +1 -1
  150. package/dist/store/postgres.d.mts +25 -7
  151. package/dist/store/postgres.d.mts.map +1 -1
  152. package/dist/store/postgres.mjs +249 -55
  153. package/dist/store/postgres.mjs.map +1 -1
  154. package/dist/store/tables.cjs +24 -6
  155. package/dist/store/tables.cjs.map +1 -1
  156. package/dist/store/tables.d.cts +1 -0
  157. package/dist/store/tables.d.cts.map +1 -1
  158. package/dist/store/tables.d.mts +1 -0
  159. package/dist/store/tables.d.mts.map +1 -1
  160. package/dist/store/tables.mjs +24 -6
  161. package/dist/store/tables.mjs.map +1 -1
  162. package/dist/webpush.cjs.map +1 -1
  163. package/dist/webpush.d.cts +1 -1
  164. package/dist/webpush.d.mts +1 -1
  165. package/dist/webpush.mjs.map +1 -1
  166. package/package.json +1 -1
package/dist/client.cjs CHANGED
@@ -26,57 +26,77 @@ var JourneyValidationError = class extends Error {
26
26
  this.name = "JourneyValidationError";
27
27
  }
28
28
  };
29
- var JourneyClient = class {
29
+ /**
30
+ * The batch as it travels: checked against the contract, with the time and the event ids and
31
+ * timestamps fixed here, so every redelivery is the same report.
32
+ */
33
+ function prepareBatch(contract, input) {
34
+ const { userId, visitorId, profile, events } = input;
35
+ if (contract !== void 0) {
36
+ if (profile !== void 0) {
37
+ const result = contract.parseProfilePatch(profile);
38
+ if (!result.ok) throw new JourneyValidationError("/ingest", void 0, result.issues);
39
+ }
40
+ for (const { event, payload } of events ?? []) {
41
+ const result = contract.parseEvent(event, payload ?? {});
42
+ if (!result.ok) throw new JourneyValidationError("/ingest", event, result.issues);
43
+ }
44
+ }
45
+ const now = Date.now();
46
+ return {
47
+ ...userId === void 0 ? {} : { userId },
48
+ ...visitorId === void 0 ? {} : { visitorId },
49
+ ...profile === void 0 ? {} : {
50
+ profile,
51
+ at: now
52
+ },
53
+ ...events === void 0 ? {} : { events: events.map((e) => ({
54
+ event: e.event,
55
+ payload: e.payload ?? {},
56
+ id: e.id ?? crypto.randomUUID(),
57
+ ts: e.ts ?? now
58
+ })) }
59
+ };
60
+ }
61
+ /**
62
+ * `JourneyClient.ingest` through a queue, for a producer that must not lose a report and must not
63
+ * wait for the engine: once `ingest` resolves the batch is durable, and the consumer retries it
64
+ * until it is applied. Delivery is at least once and in no guaranteed order: the batch is the unit
65
+ * that stays in order and event ids and property times absorb the rest. A rejected `ingest` was
66
+ * not queued; the caller decides (a 5xx so its client retries, say).
67
+ */
68
+ var JourneyQueue = class {
30
69
  options;
31
70
  constructor(options) {
32
71
  this.options = options;
33
72
  }
34
- /** Merge profile properties (`null` clears one). Every property a journey reads must be identified before the event that starts it. */
35
- identify(userId, props) {
36
- const contract = this.options.contract;
37
- if (contract !== void 0) {
38
- const result = contract.parseProfilePatch(props);
39
- if (!result.ok) return Promise.reject(new JourneyValidationError("/identify", void 0, result.issues));
40
- }
41
- return this.post("/identify", {
42
- userId,
43
- props
44
- });
73
+ async ingest(input) {
74
+ await this.options.queue.send(prepareBatch(this.options.contract, input));
75
+ }
76
+ };
77
+ var JourneyClient = class {
78
+ options;
79
+ constructor(options) {
80
+ this.options = options;
45
81
  }
46
82
  /**
47
- * Record an event; the Worker starts every journey whose trigger matches it
48
- * and wakes waits on it. A caller that retries should pass the same `id`
49
- * each time: the Worker stores one occurrence per id, so a retry after a
50
- * timeout cannot double-count the event. `ts` defaults to the Worker's clock.
83
+ * The one way in: a profile and the events it belongs to, as one batch. A visitor who signed in
84
+ * merges first, then the profile, then the events in order, so a journey cannot start without
85
+ * its recipient. A caller that retries should pass each event's `id`: the Worker stores one
86
+ * occurrence per id. `ts` defaults to now. `JourneyQueue` is the same report through a queue.
51
87
  */
52
- track(userId, event, ...args) {
53
- const [payload = {}, options = {}] = args;
54
- const contract = this.options.contract;
55
- if (contract !== void 0) {
56
- const result = contract.parseEvent(event, payload);
57
- if (!result.ok) return Promise.reject(new JourneyValidationError("/events", event, result.issues));
58
- }
59
- return this.post("/events", {
60
- userId,
61
- event,
62
- payload,
63
- ...options.id === void 0 ? {} : { id: options.id },
64
- ...options.ts === void 0 ? {} : { ts: options.ts }
65
- });
88
+ async ingest(input) {
89
+ return this.post("/ingest", prepareBatch(this.options.contract, input));
66
90
  }
67
91
  /**
68
- * A push token the app registered for the user (call it on every registration: a token moves
69
- * to the latest user). The engine keeps devices itself — pushes go to them, a token APNs / FCM
70
- * reports as gone is dropped — and journeys waiting on `reachable('push')` wake.
92
+ * A push token registered for whoever holds the device now (call it on every registration): a
93
+ * token moves to the latest owner, and a `deviceId` seen with a new token replaces the old one.
94
+ * The engine keeps devices itself — pushes go to them, a token APNs / FCM reports as gone is
95
+ * dropped — and journeys waiting on `reachable('push')` wake. Naming both ids merges the visitor,
96
+ * as `ingest` does.
71
97
  */
72
- registerDevice(userId, device) {
73
- const { token, platform, ts } = device;
74
- return this.post("/devices", {
75
- userId,
76
- token,
77
- platform,
78
- ...ts === void 0 ? {} : { ts }
79
- });
98
+ registerDevice(device) {
99
+ return this.post("/devices", device);
80
100
  }
81
101
  /**
82
102
  * A journey push tapped open: the `message_id` the push carried, reported for the user who
@@ -97,6 +117,10 @@ var JourneyClient = class {
97
117
  forgetUser(userId) {
98
118
  return this.post("/users/forget", { userId });
99
119
  }
120
+ /** `forgetUser` for a visitor who never signed in. */
121
+ forgetVisitor(visitorId) {
122
+ return this.post("/users/forget", { visitorId });
123
+ }
100
124
  /** The user signed out on that device: the token stays registered, bound to nobody. */
101
125
  unbindDevice(token) {
102
126
  return this.post("/devices/unbind", { token });
@@ -186,6 +210,7 @@ const subscriptionsPath = (userId) => `/subscriptions/${encodeURIComponent(userI
186
210
  const subscriptionsByMessagePath = (messageId) => `/subscriptions/by-message/${encodeURIComponent(messageId)}`;
187
211
  exports.JourneyClient = JourneyClient;
188
212
  exports.JourneyClientError = JourneyClientError;
213
+ exports.JourneyQueue = JourneyQueue;
189
214
  exports.JourneyValidationError = JourneyValidationError;
190
215
 
191
216
  //# sourceMappingURL=client.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.cjs","names":["SUBSCRIPTION_CHANNELS"],"sources":["../src/client.ts"],"sourcesContent":["import type * as core from 'zod/v4/core';\nimport type { Contract, ContractIssue, ProfilePatch, Topic } from './contract';\nimport {\n type DeployResult,\n type IdentifyResult,\n type IngestResult,\n type MessageSubscriptions,\n SUBSCRIPTION_CHANNELS,\n type Subscriptions,\n type SubscriptionsPatch,\n} from './host/ingest';\nimport type { ProfileProps } from './store';\n\n/**\n * `@shware/workflow/client` — the producer side of the ingest API, for the\n * service that knows what users do (an api server, a webhook handler). Plain\n * fetch, no dependencies; errors carry the status and body so a caller can\n * log them without failing the request that triggered the report.\n */\n\nexport interface JourneyClientOptions<C extends Contract | undefined = undefined> {\n /**\n * The shared `defineContract(...)`: types `track` (the event name picks the\n * payload type) and `identify`, and checks both before anything is sent.\n */\n contract?: C;\n /** The journey Worker's origin, e.g. `https://workflow.example.com`. */\n origin: string;\n /** Its API_TOKEN, sent as a bearer; omit against an open local instance. */\n token?: string;\n /** Fetch implementation (defaults to the global one). */\n fetch?: typeof fetch;\n}\n\nexport class JourneyClientError extends Error {\n constructor(\n readonly path: string,\n readonly status: number,\n readonly body: string\n ) {\n super(`journey ${path} failed: ${status} ${body}`);\n this.name = 'JourneyClientError';\n }\n}\n\n/** A payload or profile patch the contract refuses — raised before the request, with the field paths. */\nexport class JourneyValidationError extends Error {\n constructor(\n readonly path: string,\n readonly event: string | undefined,\n readonly issues: ContractIssue[]\n ) {\n super(\n `journey ${path}${event === undefined ? '' : ` '${event}'`} refused by the contract: ${issues\n .map((i) => `${i.path || '(root)'} ${i.message}`)\n .join('; ')}`\n );\n this.name = 'JourneyValidationError';\n }\n}\n\ntype EventsOf<C> = C extends Contract<infer E> ? E : never;\n/** Event names: the contract's, or any string without one. */\nexport type ClientEventName<C> = [C] extends [undefined] ? string : keyof EventsOf<C> & string;\ntype TrackArgs<C, K> = [C] extends [undefined]\n ? [payload?: Record<string, unknown>, options?: TrackOptions]\n : K extends keyof EventsOf<C>\n ? [payload: core.input<EventsOf<C>[K]>, options?: TrackOptions]\n : never;\ntype IdentifyProps<C> = [C] extends [undefined]\n ? ProfileProps\n : C extends Contract<Record<string, core.$ZodObject>, infer P>\n ? ProfilePatch<P>\n : never;\n\n/** The topics the client's contract declares; any string without a contract. */\nexport type ClientTopic<C> = [C] extends [undefined] ? string : Topic<C>;\n\nexport type {\n ChannelSubscriptions,\n ChannelSubscriptionsPatch,\n MessageOrigin,\n MessageSubscriptions,\n SubscriptionChannel,\n Subscriptions,\n SubscriptionsPatch,\n} from './host/ingest';\n\nexport interface TrackOptions {\n /** The occurrence's identity: a retry with the same id is recorded once. */\n id?: string;\n /** When it happened (epoch ms); the Worker's clock when omitted. */\n ts?: number;\n}\n\nexport class JourneyClient<C extends Contract | undefined = undefined> {\n constructor(private readonly options: JourneyClientOptions<C>) {}\n\n /** Merge profile properties (`null` clears one). Every property a journey reads must be identified before the event that starts it. */\n identify(userId: string, props: IdentifyProps<C>): Promise<IdentifyResult> {\n const contract = this.options.contract;\n if (contract !== undefined) {\n const result = contract.parseProfilePatch(props);\n if (!result.ok)\n return Promise.reject(new JourneyValidationError('/identify', undefined, result.issues));\n }\n return this.post<IdentifyResult>('/identify', { userId, props });\n }\n\n /**\n * Record an event; the Worker starts every journey whose trigger matches it\n * and wakes waits on it. A caller that retries should pass the same `id`\n * each time: the Worker stores one occurrence per id, so a retry after a\n * timeout cannot double-count the event. `ts` defaults to the Worker's clock.\n */\n track<K extends ClientEventName<C>>(\n userId: string,\n event: K,\n ...args: TrackArgs<C, K>\n ): Promise<IngestResult> {\n const [payload = {}, options = {}] = args as [Record<string, unknown>?, TrackOptions?];\n const contract = this.options.contract;\n if (contract !== undefined) {\n const result = contract.parseEvent(event, payload);\n if (!result.ok)\n return Promise.reject(new JourneyValidationError('/events', event, result.issues));\n }\n return this.post<IngestResult>('/events', {\n userId,\n event,\n payload,\n ...(options.id === undefined ? {} : { id: options.id }),\n ...(options.ts === undefined ? {} : { ts: options.ts }),\n });\n }\n\n /**\n * A push token the app registered for the user (call it on every registration: a token moves\n * to the latest user). The engine keeps devices itself — pushes go to them, a token APNs / FCM\n * reports as gone is dropped — and journeys waiting on `reachable('push')` wake.\n */\n registerDevice(\n userId: string,\n device: { token: string; platform: 'ios' | 'android' | 'web'; ts?: number }\n ): Promise<{ ok: true; woke: number }> {\n const { token, platform, ts } = device;\n return this.post('/devices', {\n userId,\n token,\n platform,\n ...(ts === undefined ? {} : { ts }),\n });\n }\n\n /**\n * A journey push tapped open: the `message_id` the push carried, reported for the user who\n * tapped it. Recorded as a human open on the copy (an email's opens are tracked by the Worker).\n */\n messageOpened(userId: string, messageId: string): Promise<{ ok: true; recorded: boolean }> {\n return this.post('/messages/opened', { userId, messageId });\n }\n\n /**\n * A deleted account: the engine records the erasure, stops the user's running journeys, then\n * erases everything it keeps about them (profile, devices, events, reporting). Once this\n * resolves the request is durable: `erased: false` means an instance would not stop yet, and\n * the Worker's scheduled sweep finishes the job.\n */\n forgetUser(userId: string): Promise<{ ok: true; erased: boolean; pending: string[] }> {\n return this.post('/users/forget', { userId });\n }\n\n /** The user signed out on that device: the token stays registered, bound to nobody. */\n unbindDevice(token: string): Promise<{ ok: true; woke: number }> {\n return this.post('/devices/unbind', { token });\n }\n\n /** Forget a push token for good (an uninstall the app learned of, a token it replaced). */\n removeDevice(token: string): Promise<{ ok: true; woke: number }> {\n return this.post('/devices/remove', { token });\n }\n\n /**\n * Where the user stands, topic × channel: for `email` and for `push`, `all` (false once they\n * opted out of the whole channel) and each declared topic (false once they opted out of it on\n * that channel) — the engine's record, which a preference page shows as it is. The two are\n * independent flags: a topic answers its own opt-out even while `all` is false, so `all: false`\n * masks every topic however they read (nothing is sent on the channel until it is true again).\n * Rejects with a 410 for an erased user.\n */\n getSubscriptions(userId: string): Promise<Subscriptions<ClientTopic<C>>> {\n return this.request('GET', subscriptionsPath(userId));\n }\n\n /**\n * Set subscriptions: only what the patch names changes, per channel (`{ email: { topics: {\n * newsletter: false } } }` opts out of one topic's emails, `{ push: { all: false } }` of every\n * push — the in-app opt-out App Store Review Guideline 4.5.4 asks for). `all: true` clears the\n * channel-wide opt-out only: a topic opted out on its own stays off until the patch names it,\n * so a preference page's \"subscribe to all\" sends `all: true` plus every topic `true`. Answers\n * the subscriptions as they now stand. With a contract, a topic it does not declare is refused\n * here, before the request; the engine refuses it too.\n */\n setSubscriptions(\n userId: string,\n patch: SubscriptionsPatch<ClientTopic<C>>\n ): Promise<Subscriptions<ClientTopic<C>>> {\n const refused = this.undeclaredTopics(patch);\n if (refused !== undefined) return Promise.reject(refused);\n return this.request('PUT', subscriptionsPath(userId), patch);\n }\n\n /**\n * `getSubscriptions` for the recipient of one copy, by the message id an email's preferences\n * link carries (`email.preferencesUrl?m=<message id>`): how a preference page works without a\n * sign-in. The answer adds `message: { channel, topic }`, the copy the page was opened from,\n * so it can highlight that topic. Rejects with a 404 for an id that names no copy — never\n * sent, or erased with its user — and for a transactional one; the page should show the link\n * as expired. The message id is the capability, so the app proxies this behind its own API\n * and rate-limits it.\n */\n getSubscriptionsByMessage(messageId: string): Promise<MessageSubscriptions<ClientTopic<C>>> {\n return this.request('GET', subscriptionsByMessagePath(messageId));\n }\n\n /** `setSubscriptions` for the recipient of one copy; the patch and the answer are those of `getSubscriptionsByMessage`. */\n setSubscriptionsByMessage(\n messageId: string,\n patch: SubscriptionsPatch<ClientTopic<C>>\n ): Promise<MessageSubscriptions<ClientTopic<C>>> {\n const refused = this.undeclaredTopics(patch);\n if (refused !== undefined) return Promise.reject(refused);\n return this.request('PUT', subscriptionsByMessagePath(messageId), patch);\n }\n\n /** With a contract, the refusal of a patch naming a topic it does not declare; undefined when the patch is fine. */\n private undeclaredTopics(\n patch: SubscriptionsPatch<ClientTopic<C>>\n ): JourneyValidationError | undefined {\n const contract = this.options.contract;\n if (contract === undefined) return undefined;\n const issues: ContractIssue[] = [];\n for (const channel of SUBSCRIPTION_CHANNELS) {\n for (const topic of Object.keys(patch[channel]?.topics ?? {})) {\n // `includes`, not the predicate: narrowing `string` by a `topic is string` leaves `never`\n if (contract.topics.includes(topic)) continue;\n issues.push({ path: `${channel}.topics.${topic}`, message: 'not a declared topic' });\n }\n }\n return issues.length > 0\n ? new JourneyValidationError('/subscriptions', undefined, issues)\n : undefined;\n }\n\n /** Deploy a compiled bundle (BundleIR). */\n deploy(bundle: unknown): Promise<DeployResult> {\n return this.post<DeployResult>('/deploy', bundle);\n }\n\n private post<T>(path: string, body: unknown): Promise<T> {\n return this.request<T>('POST', path, body);\n }\n\n private async request<T>(\n method: 'GET' | 'POST' | 'PUT',\n path: string,\n body?: unknown\n ): Promise<T> {\n const headers: Record<string, string> = {};\n if (body !== undefined) headers['content-type'] = 'application/json';\n if (this.options.token !== undefined) headers.authorization = `Bearer ${this.options.token}`;\n const doFetch = this.options.fetch ?? fetch;\n const response = await doFetch(`${this.options.origin.replace(/\\/$/, '')}${path}`, {\n method,\n headers,\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n });\n if (!response.ok) throw new JourneyClientError(path, response.status, await response.text());\n return (await response.json()) as T;\n }\n}\n\nconst subscriptionsPath = (userId: string) => `/subscriptions/${encodeURIComponent(userId)}`;\nconst subscriptionsByMessagePath = (messageId: string) =>\n `/subscriptions/by-message/${encodeURIComponent(messageId)}`;\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.cjs","names":["SUBSCRIPTION_CHANNELS"],"sources":["../src/client.ts"],"sourcesContent":["import type * as core from 'zod/v4/core';\nimport type { Contract, ContractIssue, ProfilePatch, Topic } from './contract';\nimport type { BatchResult, JourneyBatch } from './host/batch';\nimport {\n type DeployResult,\n type MessageSubscriptions,\n SUBSCRIPTION_CHANNELS,\n type Subscriptions,\n type SubscriptionsPatch,\n} from './host/ingest';\nimport type { ProfileProps } from './store';\n\n/**\n * `@shware/workflow/client` — the producer side of the ingest API, for the\n * service that knows what users do (an api server, a webhook handler). Plain\n * fetch, no dependencies; errors carry the status and body so a caller can\n * log them without failing the request that triggered the report.\n */\n\nexport interface JourneyClientOptions<C extends Contract | undefined = undefined> {\n /**\n * The shared `defineContract(...)`: types `ingest` (the event name picks the payload type, the\n * profile patch is the contract's), and checks both before anything is sent.\n */\n contract?: C;\n /** The journey Worker's origin, e.g. `https://workflow.example.com`. */\n origin: string;\n /** Its API_TOKEN, sent as a bearer; omit against an open local instance. */\n token?: string;\n /** Fetch implementation (defaults to the global one). */\n fetch?: typeof fetch;\n}\n\nexport class JourneyClientError extends Error {\n constructor(\n readonly path: string,\n readonly status: number,\n readonly body: string\n ) {\n super(`journey ${path} failed: ${status} ${body}`);\n this.name = 'JourneyClientError';\n }\n}\n\n/** A payload or profile patch the contract refuses — raised before the request, with the field paths. */\nexport class JourneyValidationError extends Error {\n constructor(\n readonly path: string,\n readonly event: string | undefined,\n readonly issues: ContractIssue[]\n ) {\n super(\n `journey ${path}${event === undefined ? '' : ` '${event}'`} refused by the contract: ${issues\n .map((i) => `${i.path || '(root)'} ${i.message}`)\n .join('; ')}`\n );\n this.name = 'JourneyValidationError';\n }\n}\n\ntype EventsOf<C> = C extends Contract<infer E> ? E : never;\ntype IdentifyProps<C> = [C] extends [undefined]\n ? ProfileProps\n : C extends Contract<Record<string, core.$ZodObject>, infer P>\n ? ProfilePatch<P>\n : never;\n\n/** The topics the client's contract declares; any string without a contract. */\nexport type ClientTopic<C> = [C] extends [undefined] ? string : Topic<C>;\n\nexport type { BatchEvent, BatchResult, JourneyBatch } from './host/batch';\nexport type {\n ChannelSubscriptions,\n ChannelSubscriptionsPatch,\n MessageOrigin,\n MessageSubscriptions,\n SubscriptionChannel,\n Subscriptions,\n SubscriptionsPatch,\n} from './host/ingest';\n\n/** One event of a batch: with a contract, the name picks the payload type. */\nexport type BatchEventInput<C> = [C] extends [undefined]\n ? { event: string; payload?: Record<string, unknown>; id?: string; ts?: number }\n : {\n [K in keyof EventsOf<C> & string]: {\n event: K;\n payload: core.input<EventsOf<C>[K]>;\n id?: string;\n ts?: number;\n };\n }[keyof EventsOf<C> & string];\n\n/**\n * Who a report or device is about: a signed-in user, or a visitor before sign-in (a stable id for\n * one client, renewed when a user signs out). Naming both says this visitor is this user: what the\n * engine knows of the visitor moves to the user, once.\n */\nexport type JourneyIdentity =\n | { userId: string; visitorId?: string | undefined }\n | { userId?: undefined; visitorId: string };\n\n/** What the profile now says and what they just did, as one report. */\nexport type BatchInput<C> = JourneyIdentity & {\n /** Merged before the first event: a journey the events start finds this recipient. */\n profile?: IdentifyProps<C>;\n events?: readonly BatchEventInput<C>[];\n};\n\n/**\n * The batch as it travels: checked against the contract, with the time and the event ids and\n * timestamps fixed here, so every redelivery is the same report.\n */\nfunction prepareBatch<C extends Contract | undefined>(\n contract: C,\n input: BatchInput<C>\n): JourneyBatch {\n const { userId, visitorId, profile, events } = input as {\n userId?: string;\n visitorId?: string;\n profile?: ProfileProps;\n events?: readonly {\n event: string;\n payload?: Record<string, unknown>;\n id?: string;\n ts?: number;\n }[];\n };\n if (contract !== undefined) {\n if (profile !== undefined) {\n const result = contract.parseProfilePatch(profile);\n if (!result.ok) throw new JourneyValidationError('/ingest', undefined, result.issues);\n }\n for (const { event, payload } of events ?? []) {\n const result = contract.parseEvent(event, payload ?? {});\n if (!result.ok) throw new JourneyValidationError('/ingest', event, result.issues);\n }\n }\n const now = Date.now();\n return {\n ...(userId === undefined ? {} : { userId }),\n ...(visitorId === undefined ? {} : { visitorId }),\n ...(profile === undefined ? {} : { profile, at: now }),\n ...(events === undefined\n ? {}\n : {\n events: events.map((e) => ({\n event: e.event,\n payload: e.payload ?? {},\n id: e.id ?? crypto.randomUUID(),\n ts: e.ts ?? now,\n })),\n }),\n };\n}\n\n/** The producer side of a queue (the subset of a Cloudflare Queues binding used). */\nexport interface JourneyQueueBinding {\n send(body: JourneyBatch): Promise<unknown>;\n}\n\nexport interface JourneyQueueOptions<C extends Contract | undefined = undefined> {\n /** The shared `defineContract(...)`: types `ingest` and checks each batch before it is sent. */\n contract?: C;\n /** The queue `journeyWorker`'s `queue` handler consumes. */\n queue: JourneyQueueBinding;\n}\n\n/**\n * `JourneyClient.ingest` through a queue, for a producer that must not lose a report and must not\n * wait for the engine: once `ingest` resolves the batch is durable, and the consumer retries it\n * until it is applied. Delivery is at least once and in no guaranteed order: the batch is the unit\n * that stays in order and event ids and property times absorb the rest. A rejected `ingest` was\n * not queued; the caller decides (a 5xx so its client retries, say).\n */\nexport class JourneyQueue<C extends Contract | undefined = undefined> {\n constructor(private readonly options: JourneyQueueOptions<C>) {}\n\n async ingest(input: BatchInput<C>): Promise<void> {\n await this.options.queue.send(prepareBatch(this.options.contract as C, input));\n }\n}\n\nexport class JourneyClient<C extends Contract | undefined = undefined> {\n constructor(private readonly options: JourneyClientOptions<C>) {}\n\n /**\n * The one way in: a profile and the events it belongs to, as one batch. A visitor who signed in\n * merges first, then the profile, then the events in order, so a journey cannot start without\n * its recipient. A caller that retries should pass each event's `id`: the Worker stores one\n * occurrence per id. `ts` defaults to now. `JourneyQueue` is the same report through a queue.\n */\n async ingest(input: BatchInput<C>): Promise<BatchResult> {\n return this.post<BatchResult>('/ingest', prepareBatch(this.options.contract as C, input));\n }\n\n /**\n * A push token registered for whoever holds the device now (call it on every registration): a\n * token moves to the latest owner, and a `deviceId` seen with a new token replaces the old one.\n * The engine keeps devices itself — pushes go to them, a token APNs / FCM reports as gone is\n * dropped — and journeys waiting on `reachable('push')` wake. Naming both ids merges the visitor,\n * as `ingest` does.\n */\n registerDevice(\n device: JourneyIdentity & {\n deviceId?: string;\n token: string;\n platform: 'ios' | 'android' | 'web';\n }\n ): Promise<{ ok: true; woke: number }> {\n return this.post('/devices', device);\n }\n\n /**\n * A journey push tapped open: the `message_id` the push carried, reported for the user who\n * tapped it. Recorded as a human open on the copy (an email's opens are tracked by the Worker).\n */\n messageOpened(userId: string, messageId: string): Promise<{ ok: true; recorded: boolean }> {\n return this.post('/messages/opened', { userId, messageId });\n }\n\n /**\n * A deleted account: the engine records the erasure, stops the user's running journeys, then\n * erases everything it keeps about them (profile, devices, events, reporting). Once this\n * resolves the request is durable: `erased: false` means an instance would not stop yet, and\n * the Worker's scheduled sweep finishes the job.\n */\n forgetUser(userId: string): Promise<{ ok: true; erased: boolean; pending: string[] }> {\n return this.post('/users/forget', { userId });\n }\n\n /** `forgetUser` for a visitor who never signed in. */\n forgetVisitor(visitorId: string): Promise<{ ok: true; erased: boolean; pending: string[] }> {\n return this.post('/users/forget', { visitorId });\n }\n\n /** The user signed out on that device: the token stays registered, bound to nobody. */\n unbindDevice(token: string): Promise<{ ok: true; woke: number }> {\n return this.post('/devices/unbind', { token });\n }\n\n /** Forget a push token for good (an uninstall the app learned of, a token it replaced). */\n removeDevice(token: string): Promise<{ ok: true; woke: number }> {\n return this.post('/devices/remove', { token });\n }\n\n /**\n * Where the user stands, topic × channel: for `email` and for `push`, `all` (false once they\n * opted out of the whole channel) and each declared topic (false once they opted out of it on\n * that channel) — the engine's record, which a preference page shows as it is. The two are\n * independent flags: a topic answers its own opt-out even while `all` is false, so `all: false`\n * masks every topic however they read (nothing is sent on the channel until it is true again).\n * Rejects with a 410 for an erased user.\n */\n getSubscriptions(userId: string): Promise<Subscriptions<ClientTopic<C>>> {\n return this.request('GET', subscriptionsPath(userId));\n }\n\n /**\n * Set subscriptions: only what the patch names changes, per channel (`{ email: { topics: {\n * newsletter: false } } }` opts out of one topic's emails, `{ push: { all: false } }` of every\n * push — the in-app opt-out App Store Review Guideline 4.5.4 asks for). `all: true` clears the\n * channel-wide opt-out only: a topic opted out on its own stays off until the patch names it,\n * so a preference page's \"subscribe to all\" sends `all: true` plus every topic `true`. Answers\n * the subscriptions as they now stand. With a contract, a topic it does not declare is refused\n * here, before the request; the engine refuses it too.\n */\n setSubscriptions(\n userId: string,\n patch: SubscriptionsPatch<ClientTopic<C>>\n ): Promise<Subscriptions<ClientTopic<C>>> {\n const refused = this.undeclaredTopics(patch);\n if (refused !== undefined) return Promise.reject(refused);\n return this.request('PUT', subscriptionsPath(userId), patch);\n }\n\n /**\n * `getSubscriptions` for the recipient of one copy, by the message id an email's preferences\n * link carries (`email.preferencesUrl?m=<message id>`): how a preference page works without a\n * sign-in. The answer adds `message: { channel, topic }`, the copy the page was opened from,\n * so it can highlight that topic. Rejects with a 404 for an id that names no copy — never\n * sent, or erased with its user — and for a transactional one; the page should show the link\n * as expired. The message id is the capability, so the app proxies this behind its own API\n * and rate-limits it.\n */\n getSubscriptionsByMessage(messageId: string): Promise<MessageSubscriptions<ClientTopic<C>>> {\n return this.request('GET', subscriptionsByMessagePath(messageId));\n }\n\n /** `setSubscriptions` for the recipient of one copy; the patch and the answer are those of `getSubscriptionsByMessage`. */\n setSubscriptionsByMessage(\n messageId: string,\n patch: SubscriptionsPatch<ClientTopic<C>>\n ): Promise<MessageSubscriptions<ClientTopic<C>>> {\n const refused = this.undeclaredTopics(patch);\n if (refused !== undefined) return Promise.reject(refused);\n return this.request('PUT', subscriptionsByMessagePath(messageId), patch);\n }\n\n /** With a contract, the refusal of a patch naming a topic it does not declare; undefined when the patch is fine. */\n private undeclaredTopics(\n patch: SubscriptionsPatch<ClientTopic<C>>\n ): JourneyValidationError | undefined {\n const contract = this.options.contract;\n if (contract === undefined) return undefined;\n const issues: ContractIssue[] = [];\n for (const channel of SUBSCRIPTION_CHANNELS) {\n for (const topic of Object.keys(patch[channel]?.topics ?? {})) {\n // `includes`, not the predicate: narrowing `string` by a `topic is string` leaves `never`\n if (contract.topics.includes(topic)) continue;\n issues.push({ path: `${channel}.topics.${topic}`, message: 'not a declared topic' });\n }\n }\n return issues.length > 0\n ? new JourneyValidationError('/subscriptions', undefined, issues)\n : undefined;\n }\n\n /** Deploy a compiled bundle (BundleIR). */\n deploy(bundle: unknown): Promise<DeployResult> {\n return this.post<DeployResult>('/deploy', bundle);\n }\n\n private post<T>(path: string, body: unknown): Promise<T> {\n return this.request<T>('POST', path, body);\n }\n\n private async request<T>(\n method: 'GET' | 'POST' | 'PUT',\n path: string,\n body?: unknown\n ): Promise<T> {\n const headers: Record<string, string> = {};\n if (body !== undefined) headers['content-type'] = 'application/json';\n if (this.options.token !== undefined) headers.authorization = `Bearer ${this.options.token}`;\n const doFetch = this.options.fetch ?? fetch;\n const response = await doFetch(`${this.options.origin.replace(/\\/$/, '')}${path}`, {\n method,\n headers,\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n });\n if (!response.ok) throw new JourneyClientError(path, response.status, await response.text());\n return (await response.json()) as T;\n }\n}\n\nconst subscriptionsPath = (userId: string) => `/subscriptions/${encodeURIComponent(userId)}`;\nconst subscriptionsByMessagePath = (messageId: string) =>\n `/subscriptions/by-message/${encodeURIComponent(messageId)}`;\n"],"mappings":"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"}
package/dist/client.d.cts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Contract, ContractIssue, ProfilePatch, Topic } from "./contract.cjs";
2
2
  import { ProfileProps } from "./store/index.cjs";
3
- import { ChannelSubscriptions, ChannelSubscriptionsPatch, DeployResult, IdentifyResult, IngestResult, MessageOrigin, MessageSubscriptions, SubscriptionChannel, Subscriptions, SubscriptionsPatch } from "./host/ingest.cjs";
3
+ import { ChannelSubscriptions, ChannelSubscriptionsPatch, DeployResult, MessageOrigin, MessageSubscriptions, SubscriptionChannel, Subscriptions, SubscriptionsPatch } from "./host/ingest.cjs";
4
+ import { BatchEvent, BatchResult, JourneyBatch } from "./host/batch.cjs";
4
5
  import * as core from "zod/v4/core";
5
6
  /**
6
7
  * `@shware/workflow/client` — the producer side of the ingest API, for the
@@ -10,8 +11,8 @@ import * as core from "zod/v4/core";
10
11
  */
11
12
  export interface JourneyClientOptions<C extends Contract | undefined = undefined> {
12
13
  /**
13
- * The shared `defineContract(...)`: types `track` (the event name picks the
14
- * payload type) and `identify`, and checks both before anything is sent.
14
+ * The shared `defineContract(...)`: types `ingest` (the event name picks the payload type, the
15
+ * profile patch is the contract's), and checks both before anything is sent.
15
16
  */
16
17
  contract?: C;
17
18
  /** The journey Worker's origin, e.g. `https://workflow.example.com`. */
@@ -35,39 +36,82 @@ export declare class JourneyValidationError extends Error {
35
36
  constructor(path: string, event: string | undefined, issues: ContractIssue[]);
36
37
  }
37
38
  type EventsOf<C> = C extends Contract<infer E> ? E : never;
38
- /** Event names: the contract's, or any string without one. */
39
- export type ClientEventName<C> = [C] extends [undefined] ? string : keyof EventsOf<C> & string;
40
- type TrackArgs<C, K> = [C] extends [undefined] ? [payload?: Record<string, unknown>, options?: TrackOptions] : K extends keyof EventsOf<C> ? [payload: core.input<EventsOf<C>[K]>, options?: TrackOptions] : never;
41
39
  type IdentifyProps<C> = [C] extends [undefined] ? ProfileProps : C extends Contract<Record<string, core.$ZodObject>, infer P> ? ProfilePatch<P> : never;
42
40
  /** The topics the client's contract declares; any string without a contract. */
43
41
  export type ClientTopic<C> = [C] extends [undefined] ? string : Topic<C>;
44
- export interface TrackOptions {
45
- /** The occurrence's identity: a retry with the same id is recorded once. */
42
+ /** One event of a batch: with a contract, the name picks the payload type. */
43
+ export type BatchEventInput<C> = [C] extends [undefined] ? {
44
+ event: string;
45
+ payload?: Record<string, unknown>;
46
46
  id?: string;
47
- /** When it happened (epoch ms); the Worker's clock when omitted. */
48
47
  ts?: number;
48
+ } : { [K in keyof EventsOf<C> & string]: {
49
+ event: K;
50
+ payload: core.input<EventsOf<C>[K]>;
51
+ id?: string;
52
+ ts?: number;
53
+ }; }[keyof EventsOf<C> & string];
54
+ /**
55
+ * Who a report or device is about: a signed-in user, or a visitor before sign-in (a stable id for
56
+ * one client, renewed when a user signs out). Naming both says this visitor is this user: what the
57
+ * engine knows of the visitor moves to the user, once.
58
+ */
59
+ export type JourneyIdentity = {
60
+ userId: string;
61
+ visitorId?: string | undefined;
62
+ } | {
63
+ userId?: undefined;
64
+ visitorId: string;
65
+ };
66
+ /** What the profile now says and what they just did, as one report. */
67
+ export type BatchInput<C> = JourneyIdentity & {
68
+ /** Merged before the first event: a journey the events start finds this recipient. */
69
+ profile?: IdentifyProps<C>;
70
+ events?: readonly BatchEventInput<C>[];
71
+ };
72
+ /** The producer side of a queue (the subset of a Cloudflare Queues binding used). */
73
+ export interface JourneyQueueBinding {
74
+ send(body: JourneyBatch): Promise<unknown>;
75
+ }
76
+ export interface JourneyQueueOptions<C extends Contract | undefined = undefined> {
77
+ /** The shared `defineContract(...)`: types `ingest` and checks each batch before it is sent. */
78
+ contract?: C;
79
+ /** The queue `journeyWorker`'s `queue` handler consumes. */
80
+ queue: JourneyQueueBinding;
81
+ }
82
+ /**
83
+ * `JourneyClient.ingest` through a queue, for a producer that must not lose a report and must not
84
+ * wait for the engine: once `ingest` resolves the batch is durable, and the consumer retries it
85
+ * until it is applied. Delivery is at least once and in no guaranteed order: the batch is the unit
86
+ * that stays in order and event ids and property times absorb the rest. A rejected `ingest` was
87
+ * not queued; the caller decides (a 5xx so its client retries, say).
88
+ */
89
+ export declare class JourneyQueue<C extends Contract | undefined = undefined> {
90
+ private readonly options;
91
+ constructor(options: JourneyQueueOptions<C>);
92
+ ingest(input: BatchInput<C>): Promise<void>;
49
93
  }
50
94
  export declare class JourneyClient<C extends Contract | undefined = undefined> {
51
95
  private readonly options;
52
96
  constructor(options: JourneyClientOptions<C>);
53
- /** Merge profile properties (`null` clears one). Every property a journey reads must be identified before the event that starts it. */
54
- identify(userId: string, props: IdentifyProps<C>): Promise<IdentifyResult>;
55
97
  /**
56
- * Record an event; the Worker starts every journey whose trigger matches it
57
- * and wakes waits on it. A caller that retries should pass the same `id`
58
- * each time: the Worker stores one occurrence per id, so a retry after a
59
- * timeout cannot double-count the event. `ts` defaults to the Worker's clock.
98
+ * The one way in: a profile and the events it belongs to, as one batch. A visitor who signed in
99
+ * merges first, then the profile, then the events in order, so a journey cannot start without
100
+ * its recipient. A caller that retries should pass each event's `id`: the Worker stores one
101
+ * occurrence per id. `ts` defaults to now. `JourneyQueue` is the same report through a queue.
60
102
  */
61
- track<K extends ClientEventName<C>>(userId: string, event: K, ...args: TrackArgs<C, K>): Promise<IngestResult>;
103
+ ingest(input: BatchInput<C>): Promise<BatchResult>;
62
104
  /**
63
- * A push token the app registered for the user (call it on every registration: a token moves
64
- * to the latest user). The engine keeps devices itself — pushes go to them, a token APNs / FCM
65
- * reports as gone is dropped — and journeys waiting on `reachable('push')` wake.
105
+ * A push token registered for whoever holds the device now (call it on every registration): a
106
+ * token moves to the latest owner, and a `deviceId` seen with a new token replaces the old one.
107
+ * The engine keeps devices itself — pushes go to them, a token APNs / FCM reports as gone is
108
+ * dropped — and journeys waiting on `reachable('push')` wake. Naming both ids merges the visitor,
109
+ * as `ingest` does.
66
110
  */
67
- registerDevice(userId: string, device: {
111
+ registerDevice(device: JourneyIdentity & {
112
+ deviceId?: string;
68
113
  token: string;
69
114
  platform: 'ios' | 'android' | 'web';
70
- ts?: number;
71
115
  }): Promise<{
72
116
  ok: true;
73
117
  woke: number;
@@ -91,6 +135,12 @@ export declare class JourneyClient<C extends Contract | undefined = undefined> {
91
135
  erased: boolean;
92
136
  pending: string[];
93
137
  }>;
138
+ /** `forgetUser` for a visitor who never signed in. */
139
+ forgetVisitor(visitorId: string): Promise<{
140
+ ok: true;
141
+ erased: boolean;
142
+ pending: string[];
143
+ }>;
94
144
  /** The user signed out on that device: the token stays registered, bound to nobody. */
95
145
  unbindDevice(token: string): Promise<{
96
146
  ok: true;
@@ -139,5 +189,5 @@ export declare class JourneyClient<C extends Contract | undefined = undefined> {
139
189
  private post;
140
190
  private request;
141
191
  }
142
- export type { ChannelSubscriptions, ChannelSubscriptionsPatch, MessageOrigin, MessageSubscriptions, SubscriptionChannel, Subscriptions, SubscriptionsPatch };
192
+ export type { BatchEvent, BatchResult, ChannelSubscriptions, ChannelSubscriptionsPatch, JourneyBatch, MessageOrigin, MessageSubscriptions, SubscriptionChannel, Subscriptions, SubscriptionsPatch };
143
193
  //# sourceMappingURL=client.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.cts","names":[],"sources":["../src/client.ts"],"mappings":";;;;;;;;;;iBAoBiB,qBAAqB,UAAU;;;;;EAK9C,WAAW;;EAEX;;EAEA;;EAEA,eAAe;;qBAGJ,2BAA2B;WAE3B;WACA;WACA;EAHX,YACW,cACA,gBACA;;;qBAQA,+BAA+B;WAE/B;WACA;WACA,QAAQ;EAHnB,YACW,cACA,2BACA,QAAQ;;KAWhB,SAAS,KAAK,UAAU,eAAe,KAAK;;YAErC,gBAAgB,MAAM,wCAAwC,SAAS;KAC9E,UAAU,GAAG,MAAM,0BACnB,UAAU,yBAAyB,UAAU,gBAC9C,gBAAgB,SAAS,MACtB,SAAS,KAAK,MAAM,SAAS,GAAG,KAAK,UAAU;KAEjD,cAAc,MAAM,yBACrB,eACA,UAAU,SAAS,eAAe,KAAK,mBAAmB,KACxD,aAAa;;YAIP,YAAY,MAAM,kCAAkC,MAAM;iBAYrD;;EAEf;;EAEA;;qBAGW,cAAc,UAAU;mBACN;EAA7B,YAA6B,SAAS,qBAAqB;;EAG3D,SAAS,gBAAgB,OAAO,cAAc,KAAK,QAAQ;;;;;;;EAgB3D,MAAM,UAAU,gBAAgB,IAC9B,gBACA,OAAO,MACJ,MAAM,UAAU,GAAG,KACrB,QAAQ;;;;;;EAsBX,eACE,gBACA;IAAU;IAAe;IAAqC;MAC7D;IAAU;IAAU;;;;;;EAcvB,cAAc,gBAAgB,oBAAoB;IAAU;IAAU;;;;;;;;EAUtE,WAAW,iBAAiB;IAAU;IAAU;IAAiB;;;EAKjE,aAAa,gBAAgB;IAAU;IAAU;;;EAKjD,aAAa,gBAAgB;IAAU;IAAU;;;;;;;;;;EAYjD,iBAAiB,iBAAiB,QAAQ,cAAc,YAAY;;;;;;;;;;EAapE,iBACE,gBACA,OAAO,mBAAmB,YAAY,MACrC,QAAQ,cAAc,YAAY;;;;;;;;;;EAerC,0BAA0B,oBAAoB,QAAQ,qBAAqB,YAAY;;EAKvF,0BACE,mBACA,OAAO,mBAAmB,YAAY,MACrC,QAAQ,qBAAqB,YAAY;;UAOpC;;EAmBR,OAAO,kBAAkB,QAAQ;UAIzB;UAIM"}
1
+ {"version":3,"file":"client.d.cts","names":[],"sources":["../src/client.ts"],"mappings":";;;;;;;;;;;iBAmBiB,qBAAqB,UAAU;;;;;EAK9C,WAAW;;EAEX;;EAEA;;EAEA,eAAe;;qBAGJ,2BAA2B;WAE3B;WACA;WACA;EAHX,YACW,cACA,gBACA;;;qBAQA,+BAA+B;WAE/B;WACA;WACA,QAAQ;EAHnB,YACW,cACA,2BACA,QAAQ;;KAWhB,SAAS,KAAK,UAAU,eAAe,KAAK;KAC5C,cAAc,MAAM,yBACrB,eACA,UAAU,SAAS,eAAe,KAAK,mBAAmB,KACxD,aAAa;;YAIP,YAAY,MAAM,kCAAkC,MAAM;;YAc1D,gBAAgB,MAAM;EAC5B;EAAe,UAAU;EAAyB;EAAa;OAE9D,WAAW,SAAS;EACnB,OAAO;EACP,SAAS,KAAK,MAAM,SAAS,GAAG;EAChC;EACA;WAEI,SAAS;;;;;;YAOT;EACN;EAAgB;;EAChB;EAAoB;;;YAGd,WAAW,KAAK;;EAE1B,UAAU,cAAc;EACxB,kBAAkB,gBAAgB;;;iBAmDnB;EACf,KAAK,MAAM,eAAe;;iBAGX,oBAAoB,UAAU;;EAE7C,WAAW;;EAEX,OAAO;;;;;;;;;qBAUI,aAAa,UAAU;mBACL;EAA7B,YAA6B,SAAS,oBAAoB;EAEpD,OAAO,OAAO,WAAW,KAAK;;qBAKzB,cAAc,UAAU;mBACN;EAA7B,YAA6B,SAAS,qBAAqB;;;;;;;EAQrD,OAAO,OAAO,WAAW,KAAK,QAAQ;;;;;;;;EAW5C,eACE,QAAQ;IACN;IACA;IACA;MAED;IAAU;IAAU;;;;;;EAQvB,cAAc,gBAAgB,oBAAoB;IAAU;IAAU;;;;;;;;EAUtE,WAAW,iBAAiB;IAAU;IAAU;IAAiB;;;EAKjE,cAAc,oBAAoB;IAAU;IAAU;IAAiB;;;EAKvE,aAAa,gBAAgB;IAAU;IAAU;;;EAKjD,aAAa,gBAAgB;IAAU;IAAU;;;;;;;;;;EAYjD,iBAAiB,iBAAiB,QAAQ,cAAc,YAAY;;;;;;;;;;EAapE,iBACE,gBACA,OAAO,mBAAmB,YAAY,MACrC,QAAQ,cAAc,YAAY;;;;;;;;;;EAerC,0BAA0B,oBAAoB,QAAQ,qBAAqB,YAAY;;EAKvF,0BACE,mBACA,OAAO,mBAAmB,YAAY,MACrC,QAAQ,qBAAqB,YAAY;;UAOpC;;EAmBR,OAAO,kBAAkB,QAAQ;UAIzB;UAIM"}
package/dist/client.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Contract, ContractIssue, ProfilePatch, Topic } from "./contract.mjs";
2
2
  import { ProfileProps } from "./store/index.mjs";
3
- import { ChannelSubscriptions, ChannelSubscriptionsPatch, DeployResult, IdentifyResult, IngestResult, MessageOrigin, MessageSubscriptions, SubscriptionChannel, Subscriptions, SubscriptionsPatch } from "./host/ingest.mjs";
3
+ import { ChannelSubscriptions, ChannelSubscriptionsPatch, DeployResult, MessageOrigin, MessageSubscriptions, SubscriptionChannel, Subscriptions, SubscriptionsPatch } from "./host/ingest.mjs";
4
+ import { BatchEvent, BatchResult, JourneyBatch } from "./host/batch.mjs";
4
5
  import * as core from "zod/v4/core";
5
6
  /**
6
7
  * `@shware/workflow/client` — the producer side of the ingest API, for the
@@ -10,8 +11,8 @@ import * as core from "zod/v4/core";
10
11
  */
11
12
  export interface JourneyClientOptions<C extends Contract | undefined = undefined> {
12
13
  /**
13
- * The shared `defineContract(...)`: types `track` (the event name picks the
14
- * payload type) and `identify`, and checks both before anything is sent.
14
+ * The shared `defineContract(...)`: types `ingest` (the event name picks the payload type, the
15
+ * profile patch is the contract's), and checks both before anything is sent.
15
16
  */
16
17
  contract?: C;
17
18
  /** The journey Worker's origin, e.g. `https://workflow.example.com`. */
@@ -35,39 +36,82 @@ export declare class JourneyValidationError extends Error {
35
36
  constructor(path: string, event: string | undefined, issues: ContractIssue[]);
36
37
  }
37
38
  type EventsOf<C> = C extends Contract<infer E> ? E : never;
38
- /** Event names: the contract's, or any string without one. */
39
- export type ClientEventName<C> = [C] extends [undefined] ? string : keyof EventsOf<C> & string;
40
- type TrackArgs<C, K> = [C] extends [undefined] ? [payload?: Record<string, unknown>, options?: TrackOptions] : K extends keyof EventsOf<C> ? [payload: core.input<EventsOf<C>[K]>, options?: TrackOptions] : never;
41
39
  type IdentifyProps<C> = [C] extends [undefined] ? ProfileProps : C extends Contract<Record<string, core.$ZodObject>, infer P> ? ProfilePatch<P> : never;
42
40
  /** The topics the client's contract declares; any string without a contract. */
43
41
  export type ClientTopic<C> = [C] extends [undefined] ? string : Topic<C>;
44
- export interface TrackOptions {
45
- /** The occurrence's identity: a retry with the same id is recorded once. */
42
+ /** One event of a batch: with a contract, the name picks the payload type. */
43
+ export type BatchEventInput<C> = [C] extends [undefined] ? {
44
+ event: string;
45
+ payload?: Record<string, unknown>;
46
46
  id?: string;
47
- /** When it happened (epoch ms); the Worker's clock when omitted. */
48
47
  ts?: number;
48
+ } : { [K in keyof EventsOf<C> & string]: {
49
+ event: K;
50
+ payload: core.input<EventsOf<C>[K]>;
51
+ id?: string;
52
+ ts?: number;
53
+ }; }[keyof EventsOf<C> & string];
54
+ /**
55
+ * Who a report or device is about: a signed-in user, or a visitor before sign-in (a stable id for
56
+ * one client, renewed when a user signs out). Naming both says this visitor is this user: what the
57
+ * engine knows of the visitor moves to the user, once.
58
+ */
59
+ export type JourneyIdentity = {
60
+ userId: string;
61
+ visitorId?: string | undefined;
62
+ } | {
63
+ userId?: undefined;
64
+ visitorId: string;
65
+ };
66
+ /** What the profile now says and what they just did, as one report. */
67
+ export type BatchInput<C> = JourneyIdentity & {
68
+ /** Merged before the first event: a journey the events start finds this recipient. */
69
+ profile?: IdentifyProps<C>;
70
+ events?: readonly BatchEventInput<C>[];
71
+ };
72
+ /** The producer side of a queue (the subset of a Cloudflare Queues binding used). */
73
+ export interface JourneyQueueBinding {
74
+ send(body: JourneyBatch): Promise<unknown>;
75
+ }
76
+ export interface JourneyQueueOptions<C extends Contract | undefined = undefined> {
77
+ /** The shared `defineContract(...)`: types `ingest` and checks each batch before it is sent. */
78
+ contract?: C;
79
+ /** The queue `journeyWorker`'s `queue` handler consumes. */
80
+ queue: JourneyQueueBinding;
81
+ }
82
+ /**
83
+ * `JourneyClient.ingest` through a queue, for a producer that must not lose a report and must not
84
+ * wait for the engine: once `ingest` resolves the batch is durable, and the consumer retries it
85
+ * until it is applied. Delivery is at least once and in no guaranteed order: the batch is the unit
86
+ * that stays in order and event ids and property times absorb the rest. A rejected `ingest` was
87
+ * not queued; the caller decides (a 5xx so its client retries, say).
88
+ */
89
+ export declare class JourneyQueue<C extends Contract | undefined = undefined> {
90
+ private readonly options;
91
+ constructor(options: JourneyQueueOptions<C>);
92
+ ingest(input: BatchInput<C>): Promise<void>;
49
93
  }
50
94
  export declare class JourneyClient<C extends Contract | undefined = undefined> {
51
95
  private readonly options;
52
96
  constructor(options: JourneyClientOptions<C>);
53
- /** Merge profile properties (`null` clears one). Every property a journey reads must be identified before the event that starts it. */
54
- identify(userId: string, props: IdentifyProps<C>): Promise<IdentifyResult>;
55
97
  /**
56
- * Record an event; the Worker starts every journey whose trigger matches it
57
- * and wakes waits on it. A caller that retries should pass the same `id`
58
- * each time: the Worker stores one occurrence per id, so a retry after a
59
- * timeout cannot double-count the event. `ts` defaults to the Worker's clock.
98
+ * The one way in: a profile and the events it belongs to, as one batch. A visitor who signed in
99
+ * merges first, then the profile, then the events in order, so a journey cannot start without
100
+ * its recipient. A caller that retries should pass each event's `id`: the Worker stores one
101
+ * occurrence per id. `ts` defaults to now. `JourneyQueue` is the same report through a queue.
60
102
  */
61
- track<K extends ClientEventName<C>>(userId: string, event: K, ...args: TrackArgs<C, K>): Promise<IngestResult>;
103
+ ingest(input: BatchInput<C>): Promise<BatchResult>;
62
104
  /**
63
- * A push token the app registered for the user (call it on every registration: a token moves
64
- * to the latest user). The engine keeps devices itself — pushes go to them, a token APNs / FCM
65
- * reports as gone is dropped — and journeys waiting on `reachable('push')` wake.
105
+ * A push token registered for whoever holds the device now (call it on every registration): a
106
+ * token moves to the latest owner, and a `deviceId` seen with a new token replaces the old one.
107
+ * The engine keeps devices itself — pushes go to them, a token APNs / FCM reports as gone is
108
+ * dropped — and journeys waiting on `reachable('push')` wake. Naming both ids merges the visitor,
109
+ * as `ingest` does.
66
110
  */
67
- registerDevice(userId: string, device: {
111
+ registerDevice(device: JourneyIdentity & {
112
+ deviceId?: string;
68
113
  token: string;
69
114
  platform: 'ios' | 'android' | 'web';
70
- ts?: number;
71
115
  }): Promise<{
72
116
  ok: true;
73
117
  woke: number;
@@ -91,6 +135,12 @@ export declare class JourneyClient<C extends Contract | undefined = undefined> {
91
135
  erased: boolean;
92
136
  pending: string[];
93
137
  }>;
138
+ /** `forgetUser` for a visitor who never signed in. */
139
+ forgetVisitor(visitorId: string): Promise<{
140
+ ok: true;
141
+ erased: boolean;
142
+ pending: string[];
143
+ }>;
94
144
  /** The user signed out on that device: the token stays registered, bound to nobody. */
95
145
  unbindDevice(token: string): Promise<{
96
146
  ok: true;
@@ -139,5 +189,5 @@ export declare class JourneyClient<C extends Contract | undefined = undefined> {
139
189
  private post;
140
190
  private request;
141
191
  }
142
- export type { ChannelSubscriptions, ChannelSubscriptionsPatch, MessageOrigin, MessageSubscriptions, SubscriptionChannel, Subscriptions, SubscriptionsPatch };
192
+ export type { BatchEvent, BatchResult, ChannelSubscriptions, ChannelSubscriptionsPatch, JourneyBatch, MessageOrigin, MessageSubscriptions, SubscriptionChannel, Subscriptions, SubscriptionsPatch };
143
193
  //# sourceMappingURL=client.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.mts","names":[],"sources":["../src/client.ts"],"mappings":";;;;;;;;;;iBAoBiB,qBAAqB,UAAU;;;;;EAK9C,WAAW;;EAEX;;EAEA;;EAEA,eAAe;;qBAGJ,2BAA2B;WAE3B;WACA;WACA;EAHX,YACW,cACA,gBACA;;;qBAQA,+BAA+B;WAE/B;WACA;WACA,QAAQ;EAHnB,YACW,cACA,2BACA,QAAQ;;KAWhB,SAAS,KAAK,UAAU,eAAe,KAAK;;YAErC,gBAAgB,MAAM,wCAAwC,SAAS;KAC9E,UAAU,GAAG,MAAM,0BACnB,UAAU,yBAAyB,UAAU,gBAC9C,gBAAgB,SAAS,MACtB,SAAS,KAAK,MAAM,SAAS,GAAG,KAAK,UAAU;KAEjD,cAAc,MAAM,yBACrB,eACA,UAAU,SAAS,eAAe,KAAK,mBAAmB,KACxD,aAAa;;YAIP,YAAY,MAAM,kCAAkC,MAAM;iBAYrD;;EAEf;;EAEA;;qBAGW,cAAc,UAAU;mBACN;EAA7B,YAA6B,SAAS,qBAAqB;;EAG3D,SAAS,gBAAgB,OAAO,cAAc,KAAK,QAAQ;;;;;;;EAgB3D,MAAM,UAAU,gBAAgB,IAC9B,gBACA,OAAO,MACJ,MAAM,UAAU,GAAG,KACrB,QAAQ;;;;;;EAsBX,eACE,gBACA;IAAU;IAAe;IAAqC;MAC7D;IAAU;IAAU;;;;;;EAcvB,cAAc,gBAAgB,oBAAoB;IAAU;IAAU;;;;;;;;EAUtE,WAAW,iBAAiB;IAAU;IAAU;IAAiB;;;EAKjE,aAAa,gBAAgB;IAAU;IAAU;;;EAKjD,aAAa,gBAAgB;IAAU;IAAU;;;;;;;;;;EAYjD,iBAAiB,iBAAiB,QAAQ,cAAc,YAAY;;;;;;;;;;EAapE,iBACE,gBACA,OAAO,mBAAmB,YAAY,MACrC,QAAQ,cAAc,YAAY;;;;;;;;;;EAerC,0BAA0B,oBAAoB,QAAQ,qBAAqB,YAAY;;EAKvF,0BACE,mBACA,OAAO,mBAAmB,YAAY,MACrC,QAAQ,qBAAqB,YAAY;;UAOpC;;EAmBR,OAAO,kBAAkB,QAAQ;UAIzB;UAIM"}
1
+ {"version":3,"file":"client.d.mts","names":[],"sources":["../src/client.ts"],"mappings":";;;;;;;;;;;iBAmBiB,qBAAqB,UAAU;;;;;EAK9C,WAAW;;EAEX;;EAEA;;EAEA,eAAe;;qBAGJ,2BAA2B;WAE3B;WACA;WACA;EAHX,YACW,cACA,gBACA;;;qBAQA,+BAA+B;WAE/B;WACA;WACA,QAAQ;EAHnB,YACW,cACA,2BACA,QAAQ;;KAWhB,SAAS,KAAK,UAAU,eAAe,KAAK;KAC5C,cAAc,MAAM,yBACrB,eACA,UAAU,SAAS,eAAe,KAAK,mBAAmB,KACxD,aAAa;;YAIP,YAAY,MAAM,kCAAkC,MAAM;;YAc1D,gBAAgB,MAAM;EAC5B;EAAe,UAAU;EAAyB;EAAa;OAE9D,WAAW,SAAS;EACnB,OAAO;EACP,SAAS,KAAK,MAAM,SAAS,GAAG;EAChC;EACA;WAEI,SAAS;;;;;;YAOT;EACN;EAAgB;;EAChB;EAAoB;;;YAGd,WAAW,KAAK;;EAE1B,UAAU,cAAc;EACxB,kBAAkB,gBAAgB;;;iBAmDnB;EACf,KAAK,MAAM,eAAe;;iBAGX,oBAAoB,UAAU;;EAE7C,WAAW;;EAEX,OAAO;;;;;;;;;qBAUI,aAAa,UAAU;mBACL;EAA7B,YAA6B,SAAS,oBAAoB;EAEpD,OAAO,OAAO,WAAW,KAAK;;qBAKzB,cAAc,UAAU;mBACN;EAA7B,YAA6B,SAAS,qBAAqB;;;;;;;EAQrD,OAAO,OAAO,WAAW,KAAK,QAAQ;;;;;;;;EAW5C,eACE,QAAQ;IACN;IACA;IACA;MAED;IAAU;IAAU;;;;;;EAQvB,cAAc,gBAAgB,oBAAoB;IAAU;IAAU;;;;;;;;EAUtE,WAAW,iBAAiB;IAAU;IAAU;IAAiB;;;EAKjE,cAAc,oBAAoB;IAAU;IAAU;IAAiB;;;EAKvE,aAAa,gBAAgB;IAAU;IAAU;;;EAKjD,aAAa,gBAAgB;IAAU;IAAU;;;;;;;;;;EAYjD,iBAAiB,iBAAiB,QAAQ,cAAc,YAAY;;;;;;;;;;EAapE,iBACE,gBACA,OAAO,mBAAmB,YAAY,MACrC,QAAQ,cAAc,YAAY;;;;;;;;;;EAerC,0BAA0B,oBAAoB,QAAQ,qBAAqB,YAAY;;EAKvF,0BACE,mBACA,OAAO,mBAAmB,YAAY,MACrC,QAAQ,qBAAqB,YAAY;;UAOpC;;EAmBR,OAAO,kBAAkB,QAAQ;UAIzB;UAIM"}