@shware/workflow 7.0.0 → 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 (154) hide show
  1. package/README.md +121 -34
  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/client.cjs +66 -41
  9. package/dist/client.cjs.map +1 -1
  10. package/dist/client.d.cts +72 -22
  11. package/dist/client.d.cts.map +1 -1
  12. package/dist/client.d.mts +72 -22
  13. package/dist/client.d.mts.map +1 -1
  14. package/dist/client.mjs +66 -42
  15. package/dist/client.mjs.map +1 -1
  16. package/dist/cloudflare/index.d.cts +4 -3
  17. package/dist/cloudflare/index.d.mts +4 -3
  18. package/dist/cloudflare/runner.cjs +3 -1
  19. package/dist/cloudflare/runner.cjs.map +1 -1
  20. package/dist/cloudflare/runner.d.cts.map +1 -1
  21. package/dist/cloudflare/runner.d.mts.map +1 -1
  22. package/dist/cloudflare/runner.mjs +3 -1
  23. package/dist/cloudflare/runner.mjs.map +1 -1
  24. package/dist/cloudflare/worker.cjs +12 -1
  25. package/dist/cloudflare/worker.cjs.map +1 -1
  26. package/dist/cloudflare/worker.d.cts +9 -1
  27. package/dist/cloudflare/worker.d.cts.map +1 -1
  28. package/dist/cloudflare/worker.d.mts +9 -1
  29. package/dist/cloudflare/worker.d.mts.map +1 -1
  30. package/dist/cloudflare/worker.mjs +12 -1
  31. package/dist/cloudflare/worker.mjs.map +1 -1
  32. package/dist/contract.cjs.map +1 -1
  33. package/dist/contract.d.cts +4 -4
  34. package/dist/contract.d.mts +4 -4
  35. package/dist/contract.mjs.map +1 -1
  36. package/dist/dsl/trigger.cjs +2 -1
  37. package/dist/dsl/trigger.cjs.map +1 -1
  38. package/dist/dsl/trigger.d.cts +6 -0
  39. package/dist/dsl/trigger.d.cts.map +1 -1
  40. package/dist/dsl/trigger.d.mts +6 -0
  41. package/dist/dsl/trigger.d.mts.map +1 -1
  42. package/dist/dsl/trigger.mjs +2 -1
  43. package/dist/dsl/trigger.mjs.map +1 -1
  44. package/dist/engine/condition.cjs +3 -3
  45. package/dist/engine/condition.cjs.map +1 -1
  46. package/dist/engine/condition.d.cts +3 -3
  47. package/dist/engine/condition.d.mts +3 -3
  48. package/dist/engine/condition.mjs +3 -3
  49. package/dist/engine/condition.mjs.map +1 -1
  50. package/dist/engine/interpreter.cjs +9 -5
  51. package/dist/engine/interpreter.cjs.map +1 -1
  52. package/dist/engine/interpreter.d.cts.map +1 -1
  53. package/dist/engine/interpreter.d.mts.map +1 -1
  54. package/dist/engine/interpreter.mjs +9 -5
  55. package/dist/engine/interpreter.mjs.map +1 -1
  56. package/dist/engine/ports.d.cts +2 -0
  57. package/dist/engine/ports.d.cts.map +1 -1
  58. package/dist/engine/ports.d.mts +2 -0
  59. package/dist/engine/ports.d.mts.map +1 -1
  60. package/dist/host/batch.cjs +172 -0
  61. package/dist/host/batch.cjs.map +1 -0
  62. package/dist/host/batch.d.cts +88 -0
  63. package/dist/host/batch.d.cts.map +1 -0
  64. package/dist/host/batch.d.mts +88 -0
  65. package/dist/host/batch.d.mts.map +1 -0
  66. package/dist/host/batch.mjs +163 -0
  67. package/dist/host/batch.mjs.map +1 -0
  68. package/dist/host/execute.cjs +10 -6
  69. package/dist/host/execute.cjs.map +1 -1
  70. package/dist/host/execute.mjs +10 -6
  71. package/dist/host/execute.mjs.map +1 -1
  72. package/dist/host/http.cjs +30 -89
  73. package/dist/host/http.cjs.map +1 -1
  74. package/dist/host/http.d.cts +1 -1
  75. package/dist/host/http.d.cts.map +1 -1
  76. package/dist/host/http.d.mts +1 -1
  77. package/dist/host/http.d.mts.map +1 -1
  78. package/dist/host/http.mjs +24 -83
  79. package/dist/host/http.mjs.map +1 -1
  80. package/dist/host/ingest.cjs +56 -13
  81. package/dist/host/ingest.cjs.map +1 -1
  82. package/dist/host/ingest.d.cts +17 -6
  83. package/dist/host/ingest.d.cts.map +1 -1
  84. package/dist/host/ingest.d.mts +17 -6
  85. package/dist/host/ingest.d.mts.map +1 -1
  86. package/dist/host/ingest.mjs +55 -14
  87. package/dist/host/ingest.mjs.map +1 -1
  88. package/dist/host/journey-host.cjs +6 -0
  89. package/dist/host/journey-host.cjs.map +1 -1
  90. package/dist/host/journey-host.d.cts +5 -2
  91. package/dist/host/journey-host.d.cts.map +1 -1
  92. package/dist/host/journey-host.d.mts +5 -2
  93. package/dist/host/journey-host.d.mts.map +1 -1
  94. package/dist/host/journey-host.mjs +6 -0
  95. package/dist/host/journey-host.mjs.map +1 -1
  96. package/dist/host/platform.cjs.map +1 -1
  97. package/dist/host/platform.d.cts +2 -0
  98. package/dist/host/platform.d.cts.map +1 -1
  99. package/dist/host/platform.d.mts +2 -0
  100. package/dist/host/platform.d.mts.map +1 -1
  101. package/dist/host/platform.mjs.map +1 -1
  102. package/dist/ir.cjs +2 -1
  103. package/dist/ir.cjs.map +1 -1
  104. package/dist/ir.d.cts +3 -0
  105. package/dist/ir.d.cts.map +1 -1
  106. package/dist/ir.d.mts +3 -0
  107. package/dist/ir.d.mts.map +1 -1
  108. package/dist/ir.mjs +2 -1
  109. package/dist/ir.mjs.map +1 -1
  110. package/dist/store/facts.cjs +13 -9
  111. package/dist/store/facts.cjs.map +1 -1
  112. package/dist/store/facts.d.cts +6 -2
  113. package/dist/store/facts.d.cts.map +1 -1
  114. package/dist/store/facts.d.mts +6 -2
  115. package/dist/store/facts.d.mts.map +1 -1
  116. package/dist/store/facts.mjs +13 -9
  117. package/dist/store/facts.mjs.map +1 -1
  118. package/dist/store/identity.cjs +34 -0
  119. package/dist/store/identity.cjs.map +1 -0
  120. package/dist/store/identity.d.cts +13 -0
  121. package/dist/store/identity.d.cts.map +1 -0
  122. package/dist/store/identity.d.mts +13 -0
  123. package/dist/store/identity.d.mts.map +1 -0
  124. package/dist/store/identity.mjs +30 -0
  125. package/dist/store/identity.mjs.map +1 -0
  126. package/dist/store/index.cjs +5 -0
  127. package/dist/store/index.cjs.map +1 -1
  128. package/dist/store/index.d.cts +49 -15
  129. package/dist/store/index.d.cts.map +1 -1
  130. package/dist/store/index.d.mts +49 -15
  131. package/dist/store/index.d.mts.map +1 -1
  132. package/dist/store/index.mjs +2 -1
  133. package/dist/store/index.mjs.map +1 -1
  134. package/dist/store/postgres.cjs +206 -44
  135. package/dist/store/postgres.cjs.map +1 -1
  136. package/dist/store/postgres.d.cts +17 -4
  137. package/dist/store/postgres.d.cts.map +1 -1
  138. package/dist/store/postgres.d.mts +17 -4
  139. package/dist/store/postgres.d.mts.map +1 -1
  140. package/dist/store/postgres.mjs +207 -45
  141. package/dist/store/postgres.mjs.map +1 -1
  142. package/dist/store/tables.cjs +24 -6
  143. package/dist/store/tables.cjs.map +1 -1
  144. package/dist/store/tables.d.cts +1 -0
  145. package/dist/store/tables.d.cts.map +1 -1
  146. package/dist/store/tables.d.mts +1 -0
  147. package/dist/store/tables.d.mts.map +1 -1
  148. package/dist/store/tables.mjs +24 -6
  149. package/dist/store/tables.mjs.map +1 -1
  150. package/dist/webpush.cjs.map +1 -1
  151. package/dist/webpush.d.cts +1 -1
  152. package/dist/webpush.d.mts +1 -1
  153. package/dist/webpush.mjs.map +1 -1
  154. package/package.json +1 -1
@@ -1,11 +1,17 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_ir = require("../ir.cjs");
3
3
  const require_store_erasure = require("./erasure.cjs");
4
+ const require_store_identity = require("./identity.cjs");
4
5
  const require_store_sql = require("./sql.cjs");
5
6
  const require_store_user_lock = require("./user-lock.cjs");
6
7
  const require_store_postgres_reporting = require("./postgres-reporting.cjs");
7
8
  const require_store_tables = require("./tables.cjs");
8
9
  // src/store/postgres.ts
10
+ /** Who a device row belongs to: its user, or before sign-in its visitor. */
11
+ function deviceOwner(row) {
12
+ if (row.user_id !== null) return row.user_id;
13
+ return row.visitor_id === null ? void 0 : require_store_identity.visitorSubject(row.visitor_id);
14
+ }
9
15
  /** information_schema's udt_name per column type. */
10
16
  const UDT = {
11
17
  uuid: "uuid",
@@ -185,16 +191,25 @@ var PostgresJourneyStore = class {
185
191
  const row = (await this.sql.query(`SELECT props FROM ${this.t.profile} WHERE user_id = $1`, [userId])).at(0);
186
192
  return row === void 0 ? void 0 : asJson(row.props);
187
193
  }
188
- async mergeProfile(userId, props) {
194
+ async mergeProfile(userId, props, at = Date.now()) {
189
195
  return this.sql.transaction(async (tx) => {
190
196
  await this.lockUser(tx, userId);
191
- const rows = await tx.query(`INSERT INTO ${this.t.profile} (user_id, props)
192
- SELECT $1, jsonb_strip_nulls($2::text::jsonb) ${this.notErased("$1")}
193
- ON CONFLICT (user_id) DO UPDATE SET props = jsonb_strip_nulls(${this.t.profile}.props || $3::text::jsonb)
197
+ const times = asJson((await tx.query(`SELECT props_at FROM ${this.t.profile} WHERE user_id = $1`, [userId]))[0]?.props_at ?? null) ?? {};
198
+ const patch = Object.fromEntries(Object.entries(props).filter(([key]) => (times[key] ?? Number.NEGATIVE_INFINITY) <= at));
199
+ const stamped = {
200
+ ...times,
201
+ ...Object.fromEntries(Object.keys(patch).map((k) => [k, at]))
202
+ };
203
+ const rows = await tx.query(`INSERT INTO ${this.t.profile} (user_id, props, props_at)
204
+ SELECT $1, jsonb_strip_nulls($2::text::jsonb), $4::text::jsonb ${this.notErased("$1")}
205
+ ON CONFLICT (user_id) DO UPDATE SET
206
+ props = jsonb_strip_nulls(${this.t.profile}.props || $3::text::jsonb),
207
+ props_at = $4::text::jsonb
194
208
  RETURNING props`, [
195
209
  userId,
196
- JSON.stringify(props),
197
- JSON.stringify(props)
210
+ JSON.stringify(patch),
211
+ JSON.stringify(patch),
212
+ JSON.stringify(stamped)
198
213
  ]);
199
214
  if (rows.length === 0) throw new require_store_erasure.UserErasedError(userId);
200
215
  return asJson(rows[0].props);
@@ -202,10 +217,10 @@ var PostgresJourneyStore = class {
202
217
  }
203
218
  async listUserIds(page) {
204
219
  const params = [];
205
- let where = "";
220
+ let where = ` WHERE user_id NOT LIKE '${require_store_identity.visitorSubject("")}%'`;
206
221
  if (page?.after !== void 0) {
207
222
  params.push(page.after);
208
- where = ` WHERE user_id > $${params.length}`;
223
+ where += ` AND user_id > $${params.length}`;
209
224
  }
210
225
  let limit = "";
211
226
  if (page?.limit !== void 0) {
@@ -214,23 +229,160 @@ var PostgresJourneyStore = class {
214
229
  }
215
230
  return (await this.sql.query(`SELECT user_id FROM (SELECT user_id FROM ${this.t.profile} UNION SELECT user_id FROM ${this.t.event}) u${where} ORDER BY user_id${limit}`, params)).map((row) => row.user_id);
216
231
  }
217
- listDevices(userId) {
218
- return this.sql.query(`SELECT token, platform FROM ${this.t.device} WHERE user_id = $1 ORDER BY ts DESC`, [userId]);
232
+ listDevices(subject) {
233
+ const visitorId = require_store_identity.visitorIdOf(subject);
234
+ return this.sql.query(visitorId === void 0 ? `SELECT token, platform FROM ${this.t.device} WHERE user_id = $1 ORDER BY updated_at DESC` : `SELECT token, platform FROM ${this.t.device} WHERE visitor_id = $1 AND user_id IS NULL ORDER BY updated_at DESC`, [visitorId ?? subject]);
219
235
  }
220
236
  async upsertDevice(device) {
221
- await this.sql.transaction(async (tx) => {
222
- await this.lockUser(tx, device.userId);
223
- if ((await tx.query(`INSERT INTO ${this.t.device} (token, user_id, platform, ts)
224
- SELECT $1, $2, $3, ${TS("$4")} ${this.notErased("$2")}
225
- ON CONFLICT (token) DO UPDATE SET user_id = EXCLUDED.user_id, platform = EXCLUDED.platform, ts = EXCLUDED.ts
237
+ const { deviceId = null, userId, visitorId = null, token, platform, at } = device;
238
+ const subject = userId ?? require_store_identity.visitorSubject(visitorId ?? "");
239
+ return this.sql.transaction(async (tx) => {
240
+ await this.lockUser(tx, subject);
241
+ const previous = await tx.query(`SELECT token, user_id, visitor_id FROM ${this.t.device} WHERE token = $1 OR device_id = $2 FOR UPDATE`, [token, deviceId]);
242
+ for (const row of previous) {
243
+ if (row.token === token) continue;
244
+ const owner = deviceOwner(row);
245
+ await tx.query(owner === void 0 || owner === subject ? `DELETE FROM ${this.t.device} WHERE token = $1` : `UPDATE ${this.t.device} SET device_id = NULL WHERE token = $1`, [row.token]);
246
+ }
247
+ if ((await tx.query(`INSERT INTO ${this.t.device} (device_id, user_id, visitor_id, token, platform, created_at, updated_at)
248
+ SELECT $1, $2, $3, $4, $5, ${TS("$6")}, ${TS("$6")} ${this.notErased("$7")}
249
+ ON CONFLICT (token) DO UPDATE SET
250
+ device_id = COALESCE(EXCLUDED.device_id, ${this.t.device}.device_id),
251
+ user_id = EXCLUDED.user_id,
252
+ visitor_id = COALESCE(EXCLUDED.visitor_id, ${this.t.device}.visitor_id),
253
+ platform = EXCLUDED.platform,
254
+ updated_at = EXCLUDED.updated_at
226
255
  RETURNING token`, [
227
- device.token,
228
- device.userId,
229
- device.platform,
230
- device.ts
231
- ])).length === 0) throw new require_store_erasure.UserErasedError(device.userId);
256
+ deviceId,
257
+ userId,
258
+ visitorId,
259
+ token,
260
+ platform,
261
+ at,
262
+ subject
263
+ ])).length === 0) throw new require_store_erasure.UserErasedError(subject);
264
+ const owners = previous.flatMap((row) => {
265
+ const owner = deviceOwner(row);
266
+ return owner === void 0 || owner === subject ? [] : [owner];
267
+ });
268
+ return [...new Set(owners)];
269
+ });
270
+ }
271
+ async linkedUser(visitorId) {
272
+ return (await this.sql.query(`SELECT user_id FROM ${this.t.visitor} WHERE visitor_id = $1`, [visitorId]))[0]?.user_id ?? void 0;
273
+ }
274
+ async touchVisitor(visitorId, at) {
275
+ return (await this.sql.query(`INSERT INTO ${this.t.visitor} (visitor_id, user_id, created_at, updated_at)
276
+ VALUES ($1, NULL, ${TS("$2")}, ${TS("$2")})
277
+ ON CONFLICT (visitor_id) DO UPDATE SET updated_at = EXCLUDED.updated_at
278
+ RETURNING user_id`, [visitorId, at]))[0]?.user_id ?? require_store_identity.visitorSubject(visitorId);
279
+ }
280
+ async pruneVisitors(before, limit) {
281
+ const idle = `user_id IS NULL AND updated_at < ${TS("$1")} AND NOT EXISTS (
282
+ SELECT 1 FROM ${this.t.entry} e
283
+ WHERE e.user_id = '${require_store_identity.visitorSubject("")}' || visitor_id AND e.status = 'running')`;
284
+ const candidates = await this.sql.query(`SELECT visitor_id FROM ${this.t.visitor} WHERE ${idle} ORDER BY updated_at LIMIT $2`, [before, limit]);
285
+ let pruned = 0;
286
+ for (const { visitor_id: visitorId } of candidates) {
287
+ const subject = require_store_identity.visitorSubject(visitorId);
288
+ await this.sql.transaction(async (tx) => {
289
+ await this.lockUser(tx, subject);
290
+ if ((await tx.query(`DELETE FROM ${this.t.visitor} WHERE visitor_id = $2 AND ${idle} RETURNING visitor_id`, [before, visitorId])).length === 0) return;
291
+ await this.deleteSubjectRows(tx, subject);
292
+ pruned += 1;
293
+ });
294
+ }
295
+ return pruned;
296
+ }
297
+ async entryOwner(instanceId) {
298
+ return (await this.sql.query(`SELECT user_id FROM ${this.t.entry} WHERE instance_id = $1`, [instanceId]))[0]?.user_id;
299
+ }
300
+ async linkVisitor(visitorId, userId, at) {
301
+ const subject = require_store_identity.visitorSubject(visitorId);
302
+ if (await this.linkedUser(visitorId) !== void 0) return null;
303
+ return this.sql.transaction(async (tx) => {
304
+ await require_store_user_lock.beginReadCommitted(tx);
305
+ for (const key of [subject, userId].sort()) await require_store_user_lock.lockUser(tx, this.schema, key);
306
+ if ((await tx.query(`INSERT INTO ${this.t.visitor} (visitor_id, user_id, created_at, updated_at)
307
+ SELECT $1, $2, ${TS("$3")}, ${TS("$3")} ${this.notErased("$2")}
308
+ ON CONFLICT (visitor_id) DO UPDATE SET user_id = EXCLUDED.user_id, updated_at = EXCLUDED.updated_at
309
+ WHERE ${this.t.visitor}.user_id IS NULL
310
+ RETURNING visitor_id`, [
311
+ visitorId,
312
+ userId,
313
+ at
314
+ ])).length === 0) {
315
+ if ((await tx.query(`SELECT 1 FROM ${this.t.erasure} WHERE user_id = $1`, [userId])).length > 0) throw new require_store_erasure.UserErasedError(userId);
316
+ return null;
317
+ }
318
+ const droppedIds = (await tx.query(`DELETE FROM ${this.t.entry} v USING ${this.t.entry} u
319
+ WHERE v.user_id = $1 AND u.user_id = $2 AND u.workflow = v.workflow
320
+ RETURNING v.instance_id`, [subject, userId])).map((row) => row.instance_id);
321
+ await tx.query(`DELETE FROM ${this.t.subscription} WHERE wake_handle = ANY($1::text[])`, [droppedIds]);
322
+ await tx.query(`DELETE FROM ${this.t.execution} WHERE instance_id = ANY($1::text[])`, [droppedIds]);
323
+ await tx.query(`DELETE FROM ${this.t.event} v USING ${this.t.event} u
324
+ WHERE v.user_id = $1 AND u.user_id = $2 AND u.event_id = v.event_id`, [subject, userId]);
325
+ const events = await tx.query(`SELECT DISTINCT name FROM ${this.t.event} WHERE user_id = $1`, [subject]);
326
+ await tx.query(`INSERT INTO ${this.t.suppression} (user_id, channel, topic, ts)
327
+ SELECT $2, channel, topic, ts FROM ${this.t.suppression} WHERE user_id = $1
328
+ ON CONFLICT (user_id, channel, topic) DO NOTHING`, [subject, userId]);
329
+ await tx.query(`DELETE FROM ${this.t.suppression} WHERE user_id = $1`, [subject]);
330
+ const props = await this.mergeProfileInto(tx, subject, userId);
331
+ for (const table of [
332
+ this.t.event,
333
+ this.t.entry,
334
+ this.t.execution,
335
+ this.t.subscription,
336
+ this.t.instance,
337
+ this.t.message
338
+ ]) await tx.query(`UPDATE ${table} SET user_id = $2 WHERE user_id = $1`, [subject, userId]);
339
+ await tx.query(`UPDATE ${this.t.device} SET user_id = $2, updated_at = ${TS("$3")}
340
+ WHERE visitor_id = $1 AND user_id IS NULL`, [
341
+ visitorId,
342
+ userId,
343
+ at
344
+ ]);
345
+ return {
346
+ dropped: droppedIds,
347
+ events: events.map((row) => row.name),
348
+ props
349
+ };
232
350
  });
233
351
  }
352
+ /**
353
+ * The visitor's properties under the user's, a key the user has a time for going to the later
354
+ * write, one a row older than the times going to the user if they hold it. The visitor's row
355
+ * goes. Answers the keys that moved.
356
+ */
357
+ async mergeProfileInto(tx, subject, userId) {
358
+ const rows = await tx.query(`SELECT user_id, props, props_at FROM ${this.t.profile} WHERE user_id = ANY($1::text[])`, [[subject, userId]]);
359
+ const from = rows.find((row) => row.user_id === subject);
360
+ if (from === void 0) return [];
361
+ const to = rows.find((row) => row.user_id === userId);
362
+ const fromTimes = asJson(from.props_at ?? null) ?? {};
363
+ const toTimes = asJson(to?.props_at ?? null) ?? {};
364
+ const toProps = to === void 0 ? {} : asJson(to.props);
365
+ const moved = Object.entries(asJson(from.props)).filter(([key]) => {
366
+ const written = toTimes[key];
367
+ return written === void 0 ? !Object.hasOwn(toProps, key) : (fromTimes[key] ?? 0) > written;
368
+ });
369
+ const props = {
370
+ ...toProps,
371
+ ...Object.fromEntries(moved)
372
+ };
373
+ const times = {
374
+ ...toTimes,
375
+ ...Object.fromEntries(moved.map(([key]) => [key, fromTimes[key] ?? 0]))
376
+ };
377
+ await tx.query(`INSERT INTO ${this.t.profile} (user_id, props, props_at) VALUES ($1, $2::text::jsonb, $3::text::jsonb)
378
+ ON CONFLICT (user_id) DO UPDATE SET props = EXCLUDED.props, props_at = EXCLUDED.props_at`, [
379
+ userId,
380
+ JSON.stringify(props),
381
+ JSON.stringify(times)
382
+ ]);
383
+ await tx.query(`DELETE FROM ${this.t.profile} WHERE user_id = $1`, [subject]);
384
+ return moved.map(([key]) => key);
385
+ }
234
386
  async requestErasure(userId, at) {
235
387
  await this.sql.transaction(async (tx) => {
236
388
  await this.lockUser(tx, userId);
@@ -270,27 +422,33 @@ var PostgresJourneyStore = class {
270
422
  async forgetUser(userId, at) {
271
423
  await this.sql.transaction(async (tx) => {
272
424
  await this.lockUser(tx, userId);
273
- for (const child of [
274
- this.t.messageOpen,
275
- this.t.messageClick,
276
- this.t.messageLink
277
- ]) await tx.query(`DELETE FROM ${child} WHERE message_id IN (SELECT id FROM ${this.t.message} WHERE user_id = $1)`, [userId]);
278
- for (const table of [
279
- this.t.message,
280
- this.t.instance,
281
- this.t.entry,
282
- this.t.execution,
283
- this.t.subscription,
284
- this.t.segmentStart,
285
- this.t.segmentMember,
286
- this.t.suppression,
287
- this.t.device,
288
- this.t.event,
289
- this.t.profile
290
- ]) await tx.query(`DELETE FROM ${table} WHERE user_id = $1`, [userId]);
425
+ await this.deleteSubjectRows(tx, userId);
291
426
  await tx.query(`UPDATE ${this.t.erasure} SET erased_at = COALESCE(erased_at, ${TS("$2")}) WHERE user_id = $1`, [userId, at]);
292
427
  });
293
428
  }
429
+ /** Everything kept about a user or visitor, on a transaction holding their lock. */
430
+ async deleteSubjectRows(tx, subject) {
431
+ for (const child of [
432
+ this.t.messageOpen,
433
+ this.t.messageClick,
434
+ this.t.messageLink
435
+ ]) await tx.query(`DELETE FROM ${child} WHERE message_id IN (SELECT id FROM ${this.t.message} WHERE user_id = $1)`, [subject]);
436
+ for (const table of [
437
+ this.t.message,
438
+ this.t.instance,
439
+ this.t.entry,
440
+ this.t.execution,
441
+ this.t.subscription,
442
+ this.t.segmentStart,
443
+ this.t.segmentMember,
444
+ this.t.suppression,
445
+ this.t.device,
446
+ this.t.event,
447
+ this.t.profile
448
+ ]) await tx.query(`DELETE FROM ${table} WHERE user_id = $1`, [subject]);
449
+ const visitorId = require_store_identity.visitorIdOf(subject);
450
+ if (visitorId !== void 0) await tx.query(`DELETE FROM ${this.t.device} WHERE visitor_id = $1 AND user_id IS NULL`, [visitorId]);
451
+ }
294
452
  async isSuppressed(userId, channel, topic) {
295
453
  return (await this.sql.query(`SELECT 1 AS x FROM ${this.t.suppression}
296
454
  WHERE user_id = $1 AND channel = $2 AND (topic IS NULL OR topic = $3::text)`, [
@@ -323,12 +481,14 @@ var PostgresJourneyStore = class {
323
481
  });
324
482
  }
325
483
  async removeDevice(token) {
326
- return (await this.sql.query(`DELETE FROM ${this.t.device} WHERE token = $1 RETURNING user_id`, [token]))[0]?.user_id ?? void 0;
484
+ const rows = await this.sql.query(`DELETE FROM ${this.t.device} WHERE token = $1 RETURNING user_id, visitor_id`, [token]);
485
+ return rows[0] === void 0 ? void 0 : deviceOwner(rows[0]);
327
486
  }
328
487
  async unbindDevice(token) {
329
- return (await this.sql.query(`WITH old AS (SELECT token, user_id FROM ${this.t.device} WHERE token = $1 FOR UPDATE)
330
- UPDATE ${this.t.device} d SET user_id = NULL FROM old WHERE d.token = old.token
331
- RETURNING old.user_id`, [token]))[0]?.user_id ?? void 0;
488
+ const rows = await this.sql.query(`WITH old AS (SELECT token, user_id, visitor_id FROM ${this.t.device} WHERE token = $1 FOR UPDATE)
489
+ UPDATE ${this.t.device} d SET user_id = NULL, visitor_id = NULL, updated_at = ${TS("$2")} FROM old WHERE d.token = old.token
490
+ RETURNING old.user_id, old.visitor_id`, [token, Date.now()]);
491
+ return rows[0] === void 0 ? void 0 : deviceOwner(rows[0]);
332
492
  }
333
493
  async listSegments() {
334
494
  return (await this.sql.query(`SELECT id, condition FROM ${this.t.segment} ORDER BY id`)).map((row) => ({
@@ -341,11 +501,12 @@ var PostgresJourneyStore = class {
341
501
  return row === void 0 ? void 0 : require_ir.ConditionIR.parse(asJson(row.condition));
342
502
  }
343
503
  async findTriggers(event) {
344
- return (await this.sql.query(`SELECT workflow, hash, "where" AS where_clause, filter, version FROM ${this.t.trigger} WHERE event = $1`, [event])).map((row) => ({
504
+ return (await this.sql.query(`SELECT workflow, hash, "where" AS where_clause, filter, visitors, version FROM ${this.t.trigger} WHERE event = $1`, [event])).map((row) => ({
345
505
  workflow: row.workflow,
346
506
  hash: row.hash,
347
507
  where: row.where_clause === null ? null : require_ir.ConditionIR.parse(asJson(row.where_clause)),
348
508
  filter: row.filter === null ? null : require_ir.ConditionIR.parse(asJson(row.filter)),
509
+ visitors: row.visitors === true,
349
510
  version: row.version ?? void 0
350
511
  }));
351
512
  }
@@ -412,12 +573,13 @@ var PostgresJourneyStore = class {
412
573
  JSON.stringify(segment.condition),
413
574
  segment.contentHash
414
575
  ]);
415
- for (const workflow of bundle.workflows) if (workflow.trigger.type === "event") await tx.query(`INSERT INTO ${this.t.trigger} (workflow, hash, event, "where", filter, version) VALUES ($1, $2, $3, $4::text::jsonb, $5::text::jsonb, $6)`, [
576
+ for (const workflow of bundle.workflows) if (workflow.trigger.type === "event") await tx.query(`INSERT INTO ${this.t.trigger} (workflow, hash, event, "where", filter, visitors, version) VALUES ($1, $2, $3, $4::text::jsonb, $5::text::jsonb, $6, $7)`, [
416
577
  workflow.id,
417
578
  workflow.contentHash,
418
579
  workflow.trigger.event,
419
580
  workflow.trigger.where === void 0 ? null : JSON.stringify(workflow.trigger.where),
420
581
  workflow.trigger.filter === void 0 ? null : JSON.stringify(workflow.trigger.filter),
582
+ workflow.trigger.visitors === true,
421
583
  version ?? null
422
584
  ]);
423
585
  else if (workflow.trigger.type === "segment") await tx.query(`INSERT INTO ${this.t.segmentTrigger} (workflow, hash, segment, version) VALUES ($1, $2, $3, $4)`, [