@shware/workflow 6.1.1 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/README.md +132 -35
  2. package/dist/aws/engine-step.cjs +4 -1
  3. package/dist/aws/engine-step.cjs.map +1 -1
  4. package/dist/aws/engine-step.d.cts.map +1 -1
  5. package/dist/aws/engine-step.d.mts.map +1 -1
  6. package/dist/aws/engine-step.mjs +4 -1
  7. package/dist/aws/engine-step.mjs.map +1 -1
  8. package/dist/aws/index.cjs +3 -2
  9. package/dist/aws/index.cjs.map +1 -1
  10. package/dist/aws/index.mjs +3 -2
  11. package/dist/aws/index.mjs.map +1 -1
  12. package/dist/client.cjs +66 -41
  13. package/dist/client.cjs.map +1 -1
  14. package/dist/client.d.cts +72 -22
  15. package/dist/client.d.cts.map +1 -1
  16. package/dist/client.d.mts +72 -22
  17. package/dist/client.d.mts.map +1 -1
  18. package/dist/client.mjs +66 -42
  19. package/dist/client.mjs.map +1 -1
  20. package/dist/cloudflare/index.d.cts +4 -3
  21. package/dist/cloudflare/index.d.mts +4 -3
  22. package/dist/cloudflare/runner.cjs +3 -1
  23. package/dist/cloudflare/runner.cjs.map +1 -1
  24. package/dist/cloudflare/runner.d.cts.map +1 -1
  25. package/dist/cloudflare/runner.d.mts.map +1 -1
  26. package/dist/cloudflare/runner.mjs +3 -1
  27. package/dist/cloudflare/runner.mjs.map +1 -1
  28. package/dist/cloudflare/worker.cjs +13 -2
  29. package/dist/cloudflare/worker.cjs.map +1 -1
  30. package/dist/cloudflare/worker.d.cts +9 -1
  31. package/dist/cloudflare/worker.d.cts.map +1 -1
  32. package/dist/cloudflare/worker.d.mts +9 -1
  33. package/dist/cloudflare/worker.d.mts.map +1 -1
  34. package/dist/cloudflare/worker.mjs +13 -2
  35. package/dist/cloudflare/worker.mjs.map +1 -1
  36. package/dist/contract.cjs.map +1 -1
  37. package/dist/contract.d.cts +4 -4
  38. package/dist/contract.d.mts +4 -4
  39. package/dist/contract.mjs.map +1 -1
  40. package/dist/dsl/trigger.cjs +2 -1
  41. package/dist/dsl/trigger.cjs.map +1 -1
  42. package/dist/dsl/trigger.d.cts +6 -0
  43. package/dist/dsl/trigger.d.cts.map +1 -1
  44. package/dist/dsl/trigger.d.mts +6 -0
  45. package/dist/dsl/trigger.d.mts.map +1 -1
  46. package/dist/dsl/trigger.mjs +2 -1
  47. package/dist/dsl/trigger.mjs.map +1 -1
  48. package/dist/email-sender.cjs +8 -1
  49. package/dist/email-sender.cjs.map +1 -1
  50. package/dist/email-sender.d.cts +2 -2
  51. package/dist/email-sender.d.cts.map +1 -1
  52. package/dist/email-sender.d.mts +2 -2
  53. package/dist/email-sender.d.mts.map +1 -1
  54. package/dist/email-sender.mjs +9 -2
  55. package/dist/email-sender.mjs.map +1 -1
  56. package/dist/email.cjs +5 -0
  57. package/dist/email.cjs.map +1 -1
  58. package/dist/email.d.cts +2 -0
  59. package/dist/email.d.cts.map +1 -1
  60. package/dist/email.d.mts +2 -0
  61. package/dist/email.d.mts.map +1 -1
  62. package/dist/email.mjs +5 -1
  63. package/dist/email.mjs.map +1 -1
  64. package/dist/engine/condition.cjs +7 -5
  65. package/dist/engine/condition.cjs.map +1 -1
  66. package/dist/engine/condition.d.cts +3 -3
  67. package/dist/engine/condition.d.cts.map +1 -1
  68. package/dist/engine/condition.d.mts +3 -3
  69. package/dist/engine/condition.d.mts.map +1 -1
  70. package/dist/engine/condition.mjs +7 -5
  71. package/dist/engine/condition.mjs.map +1 -1
  72. package/dist/engine/interpreter.cjs +11 -6
  73. package/dist/engine/interpreter.cjs.map +1 -1
  74. package/dist/engine/interpreter.d.cts.map +1 -1
  75. package/dist/engine/interpreter.d.mts.map +1 -1
  76. package/dist/engine/interpreter.mjs +11 -6
  77. package/dist/engine/interpreter.mjs.map +1 -1
  78. package/dist/engine/ports.d.cts +21 -3
  79. package/dist/engine/ports.d.cts.map +1 -1
  80. package/dist/engine/ports.d.mts +21 -3
  81. package/dist/engine/ports.d.mts.map +1 -1
  82. package/dist/host/batch.cjs +172 -0
  83. package/dist/host/batch.cjs.map +1 -0
  84. package/dist/host/batch.d.cts +88 -0
  85. package/dist/host/batch.d.cts.map +1 -0
  86. package/dist/host/batch.d.mts +88 -0
  87. package/dist/host/batch.d.mts.map +1 -0
  88. package/dist/host/batch.mjs +163 -0
  89. package/dist/host/batch.mjs.map +1 -0
  90. package/dist/host/execute.cjs +10 -6
  91. package/dist/host/execute.cjs.map +1 -1
  92. package/dist/host/execute.mjs +10 -6
  93. package/dist/host/execute.mjs.map +1 -1
  94. package/dist/host/http.cjs +31 -90
  95. package/dist/host/http.cjs.map +1 -1
  96. package/dist/host/http.d.cts +4 -2
  97. package/dist/host/http.d.cts.map +1 -1
  98. package/dist/host/http.d.mts +4 -2
  99. package/dist/host/http.d.mts.map +1 -1
  100. package/dist/host/http.mjs +25 -84
  101. package/dist/host/http.mjs.map +1 -1
  102. package/dist/host/ingest.cjs +164 -25
  103. package/dist/host/ingest.cjs.map +1 -1
  104. package/dist/host/ingest.d.cts +38 -8
  105. package/dist/host/ingest.d.cts.map +1 -1
  106. package/dist/host/ingest.d.mts +38 -8
  107. package/dist/host/ingest.d.mts.map +1 -1
  108. package/dist/host/ingest.mjs +161 -26
  109. package/dist/host/ingest.mjs.map +1 -1
  110. package/dist/host/journey-host.cjs +8 -1
  111. package/dist/host/journey-host.cjs.map +1 -1
  112. package/dist/host/journey-host.d.cts +10 -4
  113. package/dist/host/journey-host.d.cts.map +1 -1
  114. package/dist/host/journey-host.d.mts +10 -4
  115. package/dist/host/journey-host.d.mts.map +1 -1
  116. package/dist/host/journey-host.mjs +8 -1
  117. package/dist/host/journey-host.mjs.map +1 -1
  118. package/dist/host/platform.cjs.map +1 -1
  119. package/dist/host/platform.d.cts +2 -0
  120. package/dist/host/platform.d.cts.map +1 -1
  121. package/dist/host/platform.d.mts +2 -0
  122. package/dist/host/platform.d.mts.map +1 -1
  123. package/dist/host/platform.mjs.map +1 -1
  124. package/dist/index.cjs +1 -0
  125. package/dist/index.d.cts +2 -2
  126. package/dist/index.d.mts +2 -2
  127. package/dist/index.mjs +2 -2
  128. package/dist/ir.cjs +2 -1
  129. package/dist/ir.cjs.map +1 -1
  130. package/dist/ir.d.cts +3 -0
  131. package/dist/ir.d.cts.map +1 -1
  132. package/dist/ir.d.mts +3 -0
  133. package/dist/ir.d.mts.map +1 -1
  134. package/dist/ir.mjs +2 -1
  135. package/dist/ir.mjs.map +1 -1
  136. package/dist/reporting/observer.cjs +1 -0
  137. package/dist/reporting/observer.cjs.map +1 -1
  138. package/dist/reporting/observer.mjs +1 -0
  139. package/dist/reporting/observer.mjs.map +1 -1
  140. package/dist/store/facts.cjs +29 -9
  141. package/dist/store/facts.cjs.map +1 -1
  142. package/dist/store/facts.d.cts +7 -2
  143. package/dist/store/facts.d.cts.map +1 -1
  144. package/dist/store/facts.d.mts +7 -2
  145. package/dist/store/facts.d.mts.map +1 -1
  146. package/dist/store/facts.mjs +29 -9
  147. package/dist/store/facts.mjs.map +1 -1
  148. package/dist/store/identity.cjs +34 -0
  149. package/dist/store/identity.cjs.map +1 -0
  150. package/dist/store/identity.d.cts +13 -0
  151. package/dist/store/identity.d.cts.map +1 -0
  152. package/dist/store/identity.d.mts +13 -0
  153. package/dist/store/identity.d.mts.map +1 -0
  154. package/dist/store/identity.mjs +30 -0
  155. package/dist/store/identity.mjs.map +1 -0
  156. package/dist/store/index.cjs +5 -0
  157. package/dist/store/index.cjs.map +1 -1
  158. package/dist/store/index.d.cts +119 -25
  159. package/dist/store/index.d.cts.map +1 -1
  160. package/dist/store/index.d.mts +119 -25
  161. package/dist/store/index.d.mts.map +1 -1
  162. package/dist/store/index.mjs +2 -1
  163. package/dist/store/index.mjs.map +1 -1
  164. package/dist/store/postgres-reporting.cjs +4 -0
  165. package/dist/store/postgres-reporting.cjs.map +1 -1
  166. package/dist/store/postgres-reporting.d.cts +1 -0
  167. package/dist/store/postgres-reporting.d.cts.map +1 -1
  168. package/dist/store/postgres-reporting.d.mts +1 -0
  169. package/dist/store/postgres-reporting.d.mts.map +1 -1
  170. package/dist/store/postgres-reporting.mjs +4 -0
  171. package/dist/store/postgres-reporting.mjs.map +1 -1
  172. package/dist/store/postgres.cjs +271 -55
  173. package/dist/store/postgres.cjs.map +1 -1
  174. package/dist/store/postgres.d.cts +29 -7
  175. package/dist/store/postgres.d.cts.map +1 -1
  176. package/dist/store/postgres.d.mts +29 -7
  177. package/dist/store/postgres.d.mts.map +1 -1
  178. package/dist/store/postgres.mjs +272 -56
  179. package/dist/store/postgres.mjs.map +1 -1
  180. package/dist/store/tables.cjs +28 -9
  181. package/dist/store/tables.cjs.map +1 -1
  182. package/dist/store/tables.d.cts +1 -0
  183. package/dist/store/tables.d.cts.map +1 -1
  184. package/dist/store/tables.d.mts +1 -0
  185. package/dist/store/tables.d.mts.map +1 -1
  186. package/dist/store/tables.mjs +28 -9
  187. package/dist/store/tables.mjs.map +1 -1
  188. package/dist/webpush.cjs.map +1 -1
  189. package/dist/webpush.d.cts +1 -1
  190. package/dist/webpush.d.mts +1 -1
  191. package/dist/webpush.mjs.map +1 -1
  192. package/package.json +1 -1
package/dist/ir.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ir.mjs","names":[],"sources":["../src/ir.ts"],"sourcesContent":["import * as z from 'zod/mini';\n\n/**\n * IR (Intermediate Representation) — what the DSL compiles to: serializable\n * JSON. It is the three-way contract between the engine that interprets it,\n * the UI (react-flow) that renders it, and diff / audit tooling.\n *\n * Validation strategy: the zod/mini schemas are the authoritative definition.\n * TS types are either inferred from a schema or hand-written under the same\n * name (zod cannot infer recursive types; the hand-written type is anchored\n * with a ZodMiniType<T> annotation, so any drift fails to compile). IR crosses\n * process boundaries (database, network, UI), so runtime validation is a must.\n *\n * ## Versioning (two independent axes)\n *\n * 1. IR format version (irVersion): evolution of the IR schema itself. Adding\n * optional fields does not bump it; only structurally incompatible changes\n * do (changing a discriminant, dropping a field, altering semantics).\n * Readers dispatch on irVersion to the matching schema/migrator; writers\n * always write the current version.\n * 2. Content version (contentHash): a content-addressed id for a single\n * definition's **execution semantics** — the canonical-JSON hash after\n * metadata (meta / label / reason) is stripped, see hash.ts. It is used to:\n * - pin a version when a user enters, so an in-flight journey runs the\n * pinned version to completion (the default policy; compatible hot\n * updates are a future explicit-migration topic). Known exception:\n * segment conditions are resolved **by id at evaluation time** — a\n * segment redeploy changes behaviour of in-flight journeys that\n * reference it. Pinning covers the workflow's own structure; per-hash\n * segment snapshots are future work;\n * - diff on deploy: same hash means unchanged, skip publishing;\n * - audit: answer \"which version was this user on\" at any point.\n *\n * ## Node ids\n *\n * Derived from the structural path at compile time — stable and deterministic:\n * root level '0', '1', …; the j-th child of a branch's i-th case is\n * '{branchId}.c{i}.{j}', the default arm '{branchId}.o.{j}'; cohort arms use\n * the arm name '{cohortId}.{armName}.{j}'; a waitUntil timeout flow '{id}.t.{j}'.\n * Editing a node's parameters keeps its id; inserting a node shifts the ids of\n * its later siblings (a known trade-off — locating in-flight users relies on\n * the pinned version rather than cross-version id alignment, and cross-version\n * mapping is an explicit migrator's job).\n */\n\nexport const IR_VERSION = 2;\n\n/* -------------------------------- Base values ------------------------------- */\n\nexport const ScalarIR = z.union([z.string(), z.number(), z.boolean()]);\nexport type ScalarIR = z.infer<typeof ScalarIR>;\n\n/** Duration: `value` keeps the DSL literal ('23 hours', for display), `ms` is what the engine uses. */\n/*\n * TODO: `value` is hashed alongside `ms`, so '7 days' / '1 week' / '168 hours'\n * — identical to the engine, which only reads ms — produce three different\n * contentHashes. Rewording a duration therefore reports as a semantic change in\n * plan and repins in-flight users. Moving `value` out of the hash (it is a\n * display string) would fix it; deferred because it changes existing hashes.\n */\nexport const DurationIR = z.object({ value: z.string(), ms: z.number() });\nexport type DurationIR = z.infer<typeof DurationIR>;\n\n/** User-property reference — isomorphic to the DSL's UserPropertyRef once serialized (phantom type dropped). */\nexport const UserPropertyRefIR = z.object({\n type: z.literal('user_property'),\n path: z.string(),\n});\nexport type UserPropertyRefIR = z.infer<typeof UserPropertyRefIR>;\n\n/** A message prop / event payload value: either a literal or a user-property reference. */\nexport const PropValueIR = z.union([ScalarIR, UserPropertyRefIR]);\nexport type PropValueIR = z.infer<typeof PropValueIR>;\n\nexport const ChannelIR = z.enum(['email', 'sms', 'push', 'in_app', 'slack', 'discord', 'survey']);\nexport type ChannelIR = z.infer<typeof ChannelIR>;\n\nexport const WeekdayIR = z.enum(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']);\nexport type WeekdayIR = z.infer<typeof WeekdayIR>;\n\n/* --------------------------------- Provenance -------------------------------- */\n\n/**\n * Source location of the builder call that produced a definition — the IR's\n * source map. Lives under `meta`, so it is **excluded from contentHash**\n * (see hash.ts): provenance never affects execution identity or plan.\n * `file` is cwd-relative when compiled under Node, a URL under Vite dev.\n */\nexport const SourceLocIR = z.object({\n file: z.string(),\n line: z.number(),\n column: z.number(),\n});\nexport type SourceLocIR = z.infer<typeof SourceLocIR>;\n\n/** Node-level metadata: provenance only, for now. Excluded from contentHash. */\nexport const NodeMetaIR = z.object({\n loc: z.optional(SourceLocIR),\n});\nexport type NodeMetaIR = z.infer<typeof NodeMetaIR>;\n\n/* -------------------------------- Conditions -------------------------------- */\n\nexport const PropertyOperatorIR = z.enum([\n 'eq',\n 'ne',\n 'gt',\n 'gte',\n 'lt',\n 'lte',\n 'between',\n 'not_between',\n 'in_array',\n 'not_in_array',\n 'exists',\n 'not_exists',\n 'contains',\n 'not_contains',\n]);\nexport type PropertyOperatorIR = z.infer<typeof PropertyOperatorIR>;\n\n/**\n * Condition expression tree. The shape of value/values follows `op`:\n * eq/ne/gt/gte/lt/lte/contains/not_contains → value; in_array/not_in_array →\n * values; between/not_between → values = [min, max]; exists/not_exists → neither.\n *\n * `payload` predicates test the payload of an event (dotted paths reach nested\n * fields). They are only valid inside a `where` subtree — `performed.where`\n * and the event trigger's `where` — where an event row is in scope; the\n * compiler (compileBundle) and the evaluator both enforce the placement.\n * TODO: runtime refinement narrowing by op.\n */\n/**\n * Provenance rides on conditions too, so the studio can point at the exact\n * `eq(...)` / `and(...)` that produced a node — same rule as nodes: `meta` is\n * dropped before hashing, so carrying it never affects execution identity.\n */\ninterface ConditionBaseIR {\n meta?: NodeMetaIR | undefined;\n}\n\nexport type ConditionIR =\n | (ConditionBaseIR & { type: 'and'; conditions: ConditionIR[] })\n | (ConditionBaseIR & { type: 'or'; conditions: ConditionIR[] })\n | (ConditionBaseIR & { type: 'not'; condition: ConditionIR })\n /** Reference a segment by id (defined in SegmentIR, versioned separately). */\n | (ConditionBaseIR & { type: 'segment'; segment: string })\n | (ConditionBaseIR & {\n type: 'property';\n path: string;\n op: PropertyOperatorIR;\n value?: ScalarIR | undefined;\n values?: ScalarIR[] | undefined;\n })\n | (ConditionBaseIR & {\n /** Event payload predicate; `path` is dotted for nested fields ('tags.utm_source'). */\n type: 'payload';\n path: string;\n op: PropertyOperatorIR;\n value?: ScalarIR | undefined;\n values?: ScalarIR[] | undefined;\n })\n | (ConditionBaseIR & {\n type: 'performed';\n event: string;\n /** Payload filter: only events matching it count (a payload-only subtree). */\n where?: ConditionIR | undefined;\n within?: DurationIR | undefined;\n count?: number | undefined;\n })\n /**\n * The user can be reached on the channel right now: for push, the host's\n * device directory lists at least one device; for the others, the channel's\n * recipient property (email, phone, …) is set.\n */\n | (ConditionBaseIR & { type: 'reachable'; channel: ChannelIR });\n\nconst conditionBase = { meta: z.optional(NodeMetaIR) };\n\nexport const ConditionIR: z.ZodMiniType<ConditionIR> = z.lazy(() =>\n z.discriminatedUnion('type', [\n z.object({ ...conditionBase, type: z.literal('and'), conditions: z.array(ConditionIR) }),\n z.object({ ...conditionBase, type: z.literal('or'), conditions: z.array(ConditionIR) }),\n z.object({ ...conditionBase, type: z.literal('not'), condition: ConditionIR }),\n z.object({ ...conditionBase, type: z.literal('segment'), segment: z.string() }),\n z.object({\n ...conditionBase,\n type: z.literal('property'),\n path: z.string(),\n op: PropertyOperatorIR,\n value: z.optional(ScalarIR),\n values: z.optional(z.array(ScalarIR)),\n }),\n z.object({\n ...conditionBase,\n type: z.literal('payload'),\n path: z.string(),\n op: PropertyOperatorIR,\n value: z.optional(ScalarIR),\n values: z.optional(z.array(ScalarIR)),\n }),\n z.object({\n ...conditionBase,\n type: z.literal('performed'),\n event: z.string(),\n where: z.optional(ConditionIR),\n within: z.optional(DurationIR),\n count: z.optional(z.number()),\n }),\n z.object({ ...conditionBase, type: z.literal('reachable'), channel: ChannelIR }),\n ])\n);\n\n/* --------------------------------- Triggers --------------------------------- */\n\n/**\n * Trigger shapes. The event trigger has two independent gates: `where` tests\n * the triggering event's payload (payload-only subtree, evaluated against the\n * incoming event before anything else), `filter` tests the user's profile.\n */\nexport const TriggerIR = z.discriminatedUnion('type', [\n z.object({\n type: z.literal('event'),\n event: z.string(),\n where: z.optional(ConditionIR),\n filter: z.optional(ConditionIR),\n }),\n z.object({ type: z.literal('segment'), segment: z.string() }),\n z.object({ type: z.literal('date'), at: z.string() }),\n z.object({ type: z.literal('webhook') }),\n]);\nexport type TriggerIR = z.infer<typeof TriggerIR>;\n\n/* ----------------------------------- Nodes ---------------------------------- */\n\ninterface NodeBaseIR {\n /** Stable id derived from the structural path (rules in the file header). */\n id: string;\n /** Optional node name: UI title / observability handle (branch's optional first argument lands here). */\n label?: string | undefined;\n /** Metadata (provenance); excluded from contentHash. */\n meta?: NodeMetaIR | undefined;\n}\n\nexport type NodeIR =\n | (NodeBaseIR & {\n type: 'message';\n channel: ChannelIR;\n template: string;\n props: Record<string, PropValueIR>;\n /**\n * Push only: which of the user's devices get a copy. Absent means every\n * device the directory lists; 'latest' only the most recently registered.\n */\n devices?: 'latest' | undefined;\n /**\n * Email and push: the template's subscription topic, or null for a transactional message\n * (sent whatever the user opted out of). Absent for other channels, and in IR compiled\n * before topics existed: such a step checks the channel-level opt-out only, as it always did.\n */\n topic?: string | null | undefined;\n })\n | (NodeBaseIR & { type: 'delay'; duration: DurationIR })\n | (NodeBaseIR & { type: 'random_delay'; min: DurationIR; max: DurationIR })\n | (NodeBaseIR & {\n type: 'time_window';\n days: WeekdayIR[];\n between: [string, string];\n tz: string;\n })\n | (NodeBaseIR & {\n type: 'wait_until';\n condition: ConditionIR;\n timeout: DurationIR;\n onTimeout: 'continue' | 'exit' | NodeIR[];\n })\n | (NodeBaseIR & {\n type: 'branch';\n cases: { label?: string | undefined; condition: ConditionIR; flow: NodeIR[] }[];\n /** Default arm (the DSL's bare tail argument). Absent = fall through to the main line. */\n otherwise?: NodeIR[] | undefined;\n })\n | (NodeBaseIR & {\n type: 'filter';\n /** Audit-log line for the users this gate drops. Metadata: excluded from contentHash. */\n reason?: string | undefined;\n condition: ConditionIR;\n })\n | (NodeBaseIR & {\n type: 'cohort';\n /**\n * Explicit experiment key: bucketing hashes `userId:key` instead of the\n * structural node id, so the assignment survives node insertion/moves and\n * the experiment keeps one identity across workflow versions. Absent →\n * the node id is used (fine for one-off splits).\n */\n key?: string | undefined;\n /**\n * Arms in authoring order — the order the interpreter accumulates weights\n * in, so it decides which bucket range each arm owns. The DSL keeps that\n * promise by rejecting integer-like arm names, which JavaScript would\n * hoist to the front of the object in numeric order. Weights sum to 100.\n */\n arms: { name: string; weight: number; flow: NodeIR[] }[];\n })\n | (NodeBaseIR & {\n /** `reason` is the audit-log line. Metadata: excluded from contentHash. */\n type: 'exit';\n reason?: string | undefined;\n })\n | (NodeBaseIR & { type: 'send_event'; event: string; payload: Record<string, PropValueIR> })\n | (NodeBaseIR & {\n type: 'action';\n /** Registry name of the custom action; the code itself lives in the deployed Worker bundle. */\n action: string;\n /** Argument values; user_property references are resolved at run time, like message props. */\n args: Record<string, PropValueIR>;\n });\n\nconst nodeBase = { id: z.string(), label: z.optional(z.string()), meta: z.optional(NodeMetaIR) };\n\nexport const NodeIR: z.ZodMiniType<NodeIR> = z.lazy(() =>\n z.discriminatedUnion('type', [\n z.object({\n ...nodeBase,\n type: z.literal('message'),\n channel: ChannelIR,\n template: z.string(),\n props: z.record(z.string(), PropValueIR),\n devices: z.optional(z.literal('latest')),\n topic: z.optional(z.nullable(z.string())),\n }),\n z.object({ ...nodeBase, type: z.literal('delay'), duration: DurationIR }),\n z.object({ ...nodeBase, type: z.literal('random_delay'), min: DurationIR, max: DurationIR }),\n z.object({\n ...nodeBase,\n type: z.literal('time_window'),\n days: z.array(WeekdayIR),\n between: z.tuple([z.string(), z.string()]),\n tz: z.string(),\n }),\n z.object({\n ...nodeBase,\n type: z.literal('wait_until'),\n condition: ConditionIR,\n timeout: DurationIR,\n onTimeout: z.union([z.literal('continue'), z.literal('exit'), z.array(NodeIR)]),\n }),\n z.object({\n ...nodeBase,\n type: z.literal('branch'),\n cases: z.array(\n z.object({\n label: z.optional(z.string()),\n condition: ConditionIR,\n flow: z.array(NodeIR),\n })\n ),\n otherwise: z.optional(z.array(NodeIR)),\n }),\n z.object({\n ...nodeBase,\n type: z.literal('filter'),\n condition: ConditionIR,\n reason: z.optional(z.string()),\n }),\n z.object({\n ...nodeBase,\n type: z.literal('cohort'),\n key: z.optional(z.string()),\n arms: z.array(z.object({ name: z.string(), weight: z.number(), flow: z.array(NodeIR) })),\n }),\n z.object({ ...nodeBase, type: z.literal('exit'), reason: z.optional(z.string()) }),\n z.object({\n ...nodeBase,\n type: z.literal('send_event'),\n event: z.string(),\n payload: z.record(z.string(), PropValueIR),\n }),\n z.object({\n ...nodeBase,\n type: z.literal('action'),\n action: z.string(),\n args: z.record(z.string(), PropValueIR),\n }),\n ])\n);\n\n/* ---------------------- Top-level definitions and bundle --------------------- */\n\nexport const GoalIR = z.object({\n condition: ConditionIR,\n within: z.optional(DurationIR),\n exitOnMatch: z.boolean(),\n});\nexport type GoalIR = z.infer<typeof GoalIR>;\n\n/**\n * Human-facing metadata: **excluded from contentHash** (see hash.ts).\n * Editing a name or description does not touch execution semantics, so it never\n * invalidates the version in-flight users are pinned to, nor shows up in plan.\n */\nexport const MetaIR = z.object({\n /**\n * Human label. The asset's `id` is its identity — a wire key that routing\n * tables and the entry ledger are built on — so it is not something to\n * rename for readability. This is: it is metadata, dropped before hashing,\n * and the studio shows it in place of the key wherever it is set.\n */\n name: z.optional(z.string()),\n description: z.optional(z.string()),\n loc: z.optional(SourceLocIR),\n});\nexport type MetaIR = z.infer<typeof MetaIR>;\n\nexport const WorkflowIR = z.object({\n irVersion: z.literal(IR_VERSION),\n /** Wire identity (`workflow({ id })`): routing tables, the entry ledger and reporting are keyed on it. */\n id: z.string(),\n contentHash: z.string(),\n meta: z.optional(MetaIR),\n trigger: TriggerIR,\n goal: z.optional(GoalIR),\n exitWhen: z.optional(ConditionIR),\n flow: z.array(NodeIR),\n});\nexport type WorkflowIR = z.infer<typeof WorkflowIR>;\n\nexport const SegmentIR = z.object({\n irVersion: z.literal(IR_VERSION),\n /** Wire identity (`segment({ id })`): segment references and the membership store are keyed on it. */\n id: z.string(),\n contentHash: z.string(),\n /** Provenance plus the human label — MetaIR, same as a workflow's. */\n meta: z.optional(MetaIR),\n condition: ConditionIR,\n});\nexport type SegmentIR = z.infer<typeof SegmentIR>;\n\n/** Template manifest entry: IR records only the reference; the body lives in the template system. */\nexport const TemplateIR = z.object({\n irVersion: z.literal(IR_VERSION),\n key: z.string(),\n channel: ChannelIR,\n});\nexport type TemplateIR = z.infer<typeof TemplateIR>;\n\n/**\n * Action manifest entry: IR records the name; the implementation solidifies\n * into the Worker bundle at deploy time, never into IR (custom code stays out\n * of the data plane by design).\n */\nexport const ActionIR = z.object({\n irVersion: z.literal(IR_VERSION),\n name: z.string(),\n});\nexport type ActionIR = z.infer<typeof ActionIR>;\n\n/**\n * Deployment unit: the complete snapshot one `deploy` produces (terraform\n * apply, same mental model). The server diffs each definition's contentHash to\n * decide publish / skip / retire.\n */\n/**\n * The subscription topics the contract declares, carried by the bundle so the engine validates\n * `/subscriptions` and the templates' topics against the deployed list, not the SDK's word.\n */\nexport const SubscriptionsIR = z.object({\n topics: z.array(z.string()),\n});\nexport type SubscriptionsIR = z.infer<typeof SubscriptionsIR>;\n\nexport const BundleIR = z.object({\n irVersion: z.literal(IR_VERSION),\n workflows: z.array(WorkflowIR),\n segments: z.array(SegmentIR),\n templates: z.array(TemplateIR),\n actions: z.array(ActionIR),\n /** Absent in a bundle compiled before topics existed: no topics. */\n subscriptions: z.optional(SubscriptionsIR),\n});\nexport type BundleIR = z.infer<typeof BundleIR>;\n"],"mappings":"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"}
1
+ {"version":3,"file":"ir.mjs","names":[],"sources":["../src/ir.ts"],"sourcesContent":["import * as z from 'zod/mini';\n\n/**\n * IR (Intermediate Representation) — what the DSL compiles to: serializable\n * JSON. It is the three-way contract between the engine that interprets it,\n * the UI (react-flow) that renders it, and diff / audit tooling.\n *\n * Validation strategy: the zod/mini schemas are the authoritative definition.\n * TS types are either inferred from a schema or hand-written under the same\n * name (zod cannot infer recursive types; the hand-written type is anchored\n * with a ZodMiniType<T> annotation, so any drift fails to compile). IR crosses\n * process boundaries (database, network, UI), so runtime validation is a must.\n *\n * ## Versioning (two independent axes)\n *\n * 1. IR format version (irVersion): evolution of the IR schema itself. Adding\n * optional fields does not bump it; only structurally incompatible changes\n * do (changing a discriminant, dropping a field, altering semantics).\n * Readers dispatch on irVersion to the matching schema/migrator; writers\n * always write the current version.\n * 2. Content version (contentHash): a content-addressed id for a single\n * definition's **execution semantics** — the canonical-JSON hash after\n * metadata (meta / label / reason) is stripped, see hash.ts. It is used to:\n * - pin a version when a user enters, so an in-flight journey runs the\n * pinned version to completion (the default policy; compatible hot\n * updates are a future explicit-migration topic). Known exception:\n * segment conditions are resolved **by id at evaluation time** — a\n * segment redeploy changes behaviour of in-flight journeys that\n * reference it. Pinning covers the workflow's own structure; per-hash\n * segment snapshots are future work;\n * - diff on deploy: same hash means unchanged, skip publishing;\n * - audit: answer \"which version was this user on\" at any point.\n *\n * ## Node ids\n *\n * Derived from the structural path at compile time — stable and deterministic:\n * root level '0', '1', …; the j-th child of a branch's i-th case is\n * '{branchId}.c{i}.{j}', the default arm '{branchId}.o.{j}'; cohort arms use\n * the arm name '{cohortId}.{armName}.{j}'; a waitUntil timeout flow '{id}.t.{j}'.\n * Editing a node's parameters keeps its id; inserting a node shifts the ids of\n * its later siblings (a known trade-off — locating in-flight users relies on\n * the pinned version rather than cross-version id alignment, and cross-version\n * mapping is an explicit migrator's job).\n */\n\nexport const IR_VERSION = 2;\n\n/* -------------------------------- Base values ------------------------------- */\n\nexport const ScalarIR = z.union([z.string(), z.number(), z.boolean()]);\nexport type ScalarIR = z.infer<typeof ScalarIR>;\n\n/** Duration: `value` keeps the DSL literal ('23 hours', for display), `ms` is what the engine uses. */\n/*\n * TODO: `value` is hashed alongside `ms`, so '7 days' / '1 week' / '168 hours'\n * — identical to the engine, which only reads ms — produce three different\n * contentHashes. Rewording a duration therefore reports as a semantic change in\n * plan and repins in-flight users. Moving `value` out of the hash (it is a\n * display string) would fix it; deferred because it changes existing hashes.\n */\nexport const DurationIR = z.object({ value: z.string(), ms: z.number() });\nexport type DurationIR = z.infer<typeof DurationIR>;\n\n/** User-property reference — isomorphic to the DSL's UserPropertyRef once serialized (phantom type dropped). */\nexport const UserPropertyRefIR = z.object({\n type: z.literal('user_property'),\n path: z.string(),\n});\nexport type UserPropertyRefIR = z.infer<typeof UserPropertyRefIR>;\n\n/** A message prop / event payload value: either a literal or a user-property reference. */\nexport const PropValueIR = z.union([ScalarIR, UserPropertyRefIR]);\nexport type PropValueIR = z.infer<typeof PropValueIR>;\n\nexport const ChannelIR = z.enum(['email', 'sms', 'push', 'in_app', 'slack', 'discord', 'survey']);\nexport type ChannelIR = z.infer<typeof ChannelIR>;\n\nexport const WeekdayIR = z.enum(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']);\nexport type WeekdayIR = z.infer<typeof WeekdayIR>;\n\n/* --------------------------------- Provenance -------------------------------- */\n\n/**\n * Source location of the builder call that produced a definition — the IR's\n * source map. Lives under `meta`, so it is **excluded from contentHash**\n * (see hash.ts): provenance never affects execution identity or plan.\n * `file` is cwd-relative when compiled under Node, a URL under Vite dev.\n */\nexport const SourceLocIR = z.object({\n file: z.string(),\n line: z.number(),\n column: z.number(),\n});\nexport type SourceLocIR = z.infer<typeof SourceLocIR>;\n\n/** Node-level metadata: provenance only, for now. Excluded from contentHash. */\nexport const NodeMetaIR = z.object({\n loc: z.optional(SourceLocIR),\n});\nexport type NodeMetaIR = z.infer<typeof NodeMetaIR>;\n\n/* -------------------------------- Conditions -------------------------------- */\n\nexport const PropertyOperatorIR = z.enum([\n 'eq',\n 'ne',\n 'gt',\n 'gte',\n 'lt',\n 'lte',\n 'between',\n 'not_between',\n 'in_array',\n 'not_in_array',\n 'exists',\n 'not_exists',\n 'contains',\n 'not_contains',\n]);\nexport type PropertyOperatorIR = z.infer<typeof PropertyOperatorIR>;\n\n/**\n * Condition expression tree. The shape of value/values follows `op`:\n * eq/ne/gt/gte/lt/lte/contains/not_contains → value; in_array/not_in_array →\n * values; between/not_between → values = [min, max]; exists/not_exists → neither.\n *\n * `payload` predicates test the payload of an event (dotted paths reach nested\n * fields). They are only valid inside a `where` subtree — `performed.where`\n * and the event trigger's `where` — where an event row is in scope; the\n * compiler (compileBundle) and the evaluator both enforce the placement.\n * TODO: runtime refinement narrowing by op.\n */\n/**\n * Provenance rides on conditions too, so the studio can point at the exact\n * `eq(...)` / `and(...)` that produced a node — same rule as nodes: `meta` is\n * dropped before hashing, so carrying it never affects execution identity.\n */\ninterface ConditionBaseIR {\n meta?: NodeMetaIR | undefined;\n}\n\nexport type ConditionIR =\n | (ConditionBaseIR & { type: 'and'; conditions: ConditionIR[] })\n | (ConditionBaseIR & { type: 'or'; conditions: ConditionIR[] })\n | (ConditionBaseIR & { type: 'not'; condition: ConditionIR })\n /** Reference a segment by id (defined in SegmentIR, versioned separately). */\n | (ConditionBaseIR & { type: 'segment'; segment: string })\n | (ConditionBaseIR & {\n type: 'property';\n path: string;\n op: PropertyOperatorIR;\n value?: ScalarIR | undefined;\n values?: ScalarIR[] | undefined;\n })\n | (ConditionBaseIR & {\n /** Event payload predicate; `path` is dotted for nested fields ('tags.utm_source'). */\n type: 'payload';\n path: string;\n op: PropertyOperatorIR;\n value?: ScalarIR | undefined;\n values?: ScalarIR[] | undefined;\n })\n | (ConditionBaseIR & {\n type: 'performed';\n event: string;\n /** Payload filter: only events matching it count (a payload-only subtree). */\n where?: ConditionIR | undefined;\n within?: DurationIR | undefined;\n count?: number | undefined;\n })\n /**\n * The user can be reached on the channel right now: for push, the host's\n * device directory lists at least one device; for the others, the channel's\n * recipient property (email, phone, …) is set.\n */\n | (ConditionBaseIR & { type: 'reachable'; channel: ChannelIR });\n\nconst conditionBase = { meta: z.optional(NodeMetaIR) };\n\nexport const ConditionIR: z.ZodMiniType<ConditionIR> = z.lazy(() =>\n z.discriminatedUnion('type', [\n z.object({ ...conditionBase, type: z.literal('and'), conditions: z.array(ConditionIR) }),\n z.object({ ...conditionBase, type: z.literal('or'), conditions: z.array(ConditionIR) }),\n z.object({ ...conditionBase, type: z.literal('not'), condition: ConditionIR }),\n z.object({ ...conditionBase, type: z.literal('segment'), segment: z.string() }),\n z.object({\n ...conditionBase,\n type: z.literal('property'),\n path: z.string(),\n op: PropertyOperatorIR,\n value: z.optional(ScalarIR),\n values: z.optional(z.array(ScalarIR)),\n }),\n z.object({\n ...conditionBase,\n type: z.literal('payload'),\n path: z.string(),\n op: PropertyOperatorIR,\n value: z.optional(ScalarIR),\n values: z.optional(z.array(ScalarIR)),\n }),\n z.object({\n ...conditionBase,\n type: z.literal('performed'),\n event: z.string(),\n where: z.optional(ConditionIR),\n within: z.optional(DurationIR),\n count: z.optional(z.number()),\n }),\n z.object({ ...conditionBase, type: z.literal('reachable'), channel: ChannelIR }),\n ])\n);\n\n/* --------------------------------- Triggers --------------------------------- */\n\n/**\n * Trigger shapes. The event trigger has two independent gates: `where` tests\n * the triggering event's payload (payload-only subtree, evaluated against the\n * incoming event before anything else), `filter` tests the user's profile.\n */\nexport const TriggerIR = z.discriminatedUnion('type', [\n z.object({\n type: z.literal('event'),\n event: z.string(),\n where: z.optional(ConditionIR),\n filter: z.optional(ConditionIR),\n visitors: z.optional(z.literal(true)),\n }),\n z.object({ type: z.literal('segment'), segment: z.string() }),\n z.object({ type: z.literal('date'), at: z.string() }),\n z.object({ type: z.literal('webhook') }),\n]);\nexport type TriggerIR = z.infer<typeof TriggerIR>;\n\n/* ----------------------------------- Nodes ---------------------------------- */\n\ninterface NodeBaseIR {\n /** Stable id derived from the structural path (rules in the file header). */\n id: string;\n /** Optional node name: UI title / observability handle (branch's optional first argument lands here). */\n label?: string | undefined;\n /** Metadata (provenance); excluded from contentHash. */\n meta?: NodeMetaIR | undefined;\n}\n\nexport type NodeIR =\n | (NodeBaseIR & {\n type: 'message';\n channel: ChannelIR;\n template: string;\n props: Record<string, PropValueIR>;\n /**\n * Push only: which of the user's devices get a copy. Absent means every\n * device the directory lists; 'latest' only the most recently registered.\n */\n devices?: 'latest' | undefined;\n /**\n * Email and push: the template's subscription topic, or null for a transactional message\n * (sent whatever the user opted out of). Absent for other channels, and in IR compiled\n * before topics existed: such a step checks the channel-level opt-out only, as it always did.\n */\n topic?: string | null | undefined;\n })\n | (NodeBaseIR & { type: 'delay'; duration: DurationIR })\n | (NodeBaseIR & { type: 'random_delay'; min: DurationIR; max: DurationIR })\n | (NodeBaseIR & {\n type: 'time_window';\n days: WeekdayIR[];\n between: [string, string];\n tz: string;\n })\n | (NodeBaseIR & {\n type: 'wait_until';\n condition: ConditionIR;\n timeout: DurationIR;\n onTimeout: 'continue' | 'exit' | NodeIR[];\n })\n | (NodeBaseIR & {\n type: 'branch';\n cases: { label?: string | undefined; condition: ConditionIR; flow: NodeIR[] }[];\n /** Default arm (the DSL's bare tail argument). Absent = fall through to the main line. */\n otherwise?: NodeIR[] | undefined;\n })\n | (NodeBaseIR & {\n type: 'filter';\n /** Audit-log line for the users this gate drops. Metadata: excluded from contentHash. */\n reason?: string | undefined;\n condition: ConditionIR;\n })\n | (NodeBaseIR & {\n type: 'cohort';\n /**\n * Explicit experiment key: bucketing hashes `userId:key` instead of the\n * structural node id, so the assignment survives node insertion/moves and\n * the experiment keeps one identity across workflow versions. Absent →\n * the node id is used (fine for one-off splits).\n */\n key?: string | undefined;\n /**\n * Arms in authoring order — the order the interpreter accumulates weights\n * in, so it decides which bucket range each arm owns. The DSL keeps that\n * promise by rejecting integer-like arm names, which JavaScript would\n * hoist to the front of the object in numeric order. Weights sum to 100.\n */\n arms: { name: string; weight: number; flow: NodeIR[] }[];\n })\n | (NodeBaseIR & {\n /** `reason` is the audit-log line. Metadata: excluded from contentHash. */\n type: 'exit';\n reason?: string | undefined;\n })\n | (NodeBaseIR & { type: 'send_event'; event: string; payload: Record<string, PropValueIR> })\n | (NodeBaseIR & {\n type: 'action';\n /** Registry name of the custom action; the code itself lives in the deployed Worker bundle. */\n action: string;\n /** Argument values; user_property references are resolved at run time, like message props. */\n args: Record<string, PropValueIR>;\n });\n\nconst nodeBase = { id: z.string(), label: z.optional(z.string()), meta: z.optional(NodeMetaIR) };\n\nexport const NodeIR: z.ZodMiniType<NodeIR> = z.lazy(() =>\n z.discriminatedUnion('type', [\n z.object({\n ...nodeBase,\n type: z.literal('message'),\n channel: ChannelIR,\n template: z.string(),\n props: z.record(z.string(), PropValueIR),\n devices: z.optional(z.literal('latest')),\n topic: z.optional(z.nullable(z.string())),\n }),\n z.object({ ...nodeBase, type: z.literal('delay'), duration: DurationIR }),\n z.object({ ...nodeBase, type: z.literal('random_delay'), min: DurationIR, max: DurationIR }),\n z.object({\n ...nodeBase,\n type: z.literal('time_window'),\n days: z.array(WeekdayIR),\n between: z.tuple([z.string(), z.string()]),\n tz: z.string(),\n }),\n z.object({\n ...nodeBase,\n type: z.literal('wait_until'),\n condition: ConditionIR,\n timeout: DurationIR,\n onTimeout: z.union([z.literal('continue'), z.literal('exit'), z.array(NodeIR)]),\n }),\n z.object({\n ...nodeBase,\n type: z.literal('branch'),\n cases: z.array(\n z.object({\n label: z.optional(z.string()),\n condition: ConditionIR,\n flow: z.array(NodeIR),\n })\n ),\n otherwise: z.optional(z.array(NodeIR)),\n }),\n z.object({\n ...nodeBase,\n type: z.literal('filter'),\n condition: ConditionIR,\n reason: z.optional(z.string()),\n }),\n z.object({\n ...nodeBase,\n type: z.literal('cohort'),\n key: z.optional(z.string()),\n arms: z.array(z.object({ name: z.string(), weight: z.number(), flow: z.array(NodeIR) })),\n }),\n z.object({ ...nodeBase, type: z.literal('exit'), reason: z.optional(z.string()) }),\n z.object({\n ...nodeBase,\n type: z.literal('send_event'),\n event: z.string(),\n payload: z.record(z.string(), PropValueIR),\n }),\n z.object({\n ...nodeBase,\n type: z.literal('action'),\n action: z.string(),\n args: z.record(z.string(), PropValueIR),\n }),\n ])\n);\n\n/* ---------------------- Top-level definitions and bundle --------------------- */\n\nexport const GoalIR = z.object({\n condition: ConditionIR,\n within: z.optional(DurationIR),\n exitOnMatch: z.boolean(),\n});\nexport type GoalIR = z.infer<typeof GoalIR>;\n\n/**\n * Human-facing metadata: **excluded from contentHash** (see hash.ts).\n * Editing a name or description does not touch execution semantics, so it never\n * invalidates the version in-flight users are pinned to, nor shows up in plan.\n */\nexport const MetaIR = z.object({\n /**\n * Human label. The asset's `id` is its identity — a wire key that routing\n * tables and the entry ledger are built on — so it is not something to\n * rename for readability. This is: it is metadata, dropped before hashing,\n * and the studio shows it in place of the key wherever it is set.\n */\n name: z.optional(z.string()),\n description: z.optional(z.string()),\n loc: z.optional(SourceLocIR),\n});\nexport type MetaIR = z.infer<typeof MetaIR>;\n\nexport const WorkflowIR = z.object({\n irVersion: z.literal(IR_VERSION),\n /** Wire identity (`workflow({ id })`): routing tables, the entry ledger and reporting are keyed on it. */\n id: z.string(),\n contentHash: z.string(),\n meta: z.optional(MetaIR),\n trigger: TriggerIR,\n goal: z.optional(GoalIR),\n exitWhen: z.optional(ConditionIR),\n flow: z.array(NodeIR),\n});\nexport type WorkflowIR = z.infer<typeof WorkflowIR>;\n\nexport const SegmentIR = z.object({\n irVersion: z.literal(IR_VERSION),\n /** Wire identity (`segment({ id })`): segment references and the membership store are keyed on it. */\n id: z.string(),\n contentHash: z.string(),\n /** Provenance plus the human label — MetaIR, same as a workflow's. */\n meta: z.optional(MetaIR),\n condition: ConditionIR,\n});\nexport type SegmentIR = z.infer<typeof SegmentIR>;\n\n/** Template manifest entry: IR records only the reference; the body lives in the template system. */\nexport const TemplateIR = z.object({\n irVersion: z.literal(IR_VERSION),\n key: z.string(),\n channel: ChannelIR,\n});\nexport type TemplateIR = z.infer<typeof TemplateIR>;\n\n/**\n * Action manifest entry: IR records the name; the implementation solidifies\n * into the Worker bundle at deploy time, never into IR (custom code stays out\n * of the data plane by design).\n */\nexport const ActionIR = z.object({\n irVersion: z.literal(IR_VERSION),\n name: z.string(),\n});\nexport type ActionIR = z.infer<typeof ActionIR>;\n\n/**\n * Deployment unit: the complete snapshot one `deploy` produces (terraform\n * apply, same mental model). The server diffs each definition's contentHash to\n * decide publish / skip / retire.\n */\n/**\n * The subscription topics the contract declares, carried by the bundle so the engine validates\n * `/subscriptions` and the templates' topics against the deployed list, not the SDK's word.\n */\nexport const SubscriptionsIR = z.object({\n topics: z.array(z.string()),\n});\nexport type SubscriptionsIR = z.infer<typeof SubscriptionsIR>;\n\nexport const BundleIR = z.object({\n irVersion: z.literal(IR_VERSION),\n workflows: z.array(WorkflowIR),\n segments: z.array(SegmentIR),\n templates: z.array(TemplateIR),\n actions: z.array(ActionIR),\n /** Absent in a bundle compiled before topics existed: no topics. */\n subscriptions: z.optional(SubscriptionsIR),\n});\nexport type BundleIR = z.infer<typeof BundleIR>;\n"],"mappings":"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"}
@@ -18,6 +18,7 @@ function reportingObserver(reporting, origin) {
18
18
  return {
19
19
  started: (event) => reporting.instanceStarted(event),
20
20
  nodeReached: (event) => reporting.nodeReached(event),
21
+ messageSettled: (message) => reporting.messageSettled(message.idempotencyKey),
21
22
  async messagePrepared(message) {
22
23
  const id = await reporting.recordMessage({
23
24
  idempotencyKey: message.idempotencyKey,
@@ -1 +1 @@
1
- {"version":3,"file":"observer.cjs","names":[],"sources":["../../src/reporting/observer.ts"],"sourcesContent":["import type { JourneyObserver, OutboundMessage } from '../engine';\nimport type { JourneyReporting } from '../store';\n\n/**\n * The observer journeyWorker installs when the store keeps reporting: every node arrival, copy\n * and outcome recorded through `store.reporting`.\n *\n * A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,\n * an email's links become `<origin>/t/l/<message>/<position>` — `/t/u/...` for its unsubscribe\n * link, so what a click means is fixed when it is sent — and an open pixel `<origin>/t/o/<message>`\n * (served by the Worker, ./tracking.ts); the renderer gets the id too, for the email's\n * preferences link (`email.preferencesUrl?m=<message>`), and a push carries\n * `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),\n * plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.\n * The links are recorded when the sender commits the rewritten email, before it goes out.\n */\nexport function reportingObserver(\n reporting: JourneyReporting,\n origin: string | undefined\n): JourneyObserver {\n const base = origin?.replace(/\\/$/, '');\n\n return {\n started: (event) => reporting.instanceStarted(event),\n\n nodeReached: (event) => reporting.nodeReached(event),\n\n async messagePrepared(message) {\n const id = await reporting.recordMessage({\n idempotencyKey: message.idempotencyKey,\n workflow: message.workflow,\n instanceId: message.instanceId,\n nodeId: message.nodeId,\n template: message.template,\n channel: channelOf(message),\n userId: message.userId,\n topic:\n message.channel === 'email' || message.channel === 'push'\n ? (message.topic ?? null)\n : null,\n sentAt: Date.now(),\n });\n if (message.channel === 'email' && base !== undefined) {\n const urls: string[] = [];\n return {\n messageId: id,\n link: (url, purpose) => {\n urls.push(url);\n return `${base}/t/${purpose === 'unsubscribe' ? 'u' : 'l'}/${id}/${urls.length - 1}`;\n },\n pixel: `${base}/t/o/${id}`,\n unsubscribe: `${base}/t/u/${id}`,\n // Before the transport takes it, so no click can reach a link not yet recorded\n commit: () => reporting.recordLinks(id, urls),\n };\n }\n if (message.channel === 'push') {\n // The app reports the tap with `message_id`; a client that can reach the Worker on its\n // own (iOS's notification service extension, a browser's service worker) posts to these\n return {\n data: {\n message_id: id,\n ...(base === undefined\n ? {}\n : { delivered_url: `${base}/t/d/${id}`, opened_url: `${base}/t/p/${id}` }),\n },\n };\n }\n return undefined;\n },\n\n async messageSent(message, result) {\n await reporting.settleMessage(message.idempotencyKey, result, Date.now());\n },\n\n finished: (event) => reporting.instanceFinished(event),\n };\n}\n\n/** Where the copy lands — the Studio's metric channel: a push is reported per platform. */\nfunction channelOf(message: OutboundMessage): string {\n if (message.channel !== 'push') return message.channel;\n return `push_${message.device?.platform ?? 'unknown'}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,SAAgB,kBACd,WACA,QACiB;CACjB,MAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE;CAEtC,OAAO;EACL,UAAU,UAAU,UAAU,gBAAgB,KAAK;EAEnD,cAAc,UAAU,UAAU,YAAY,KAAK;EAEnD,MAAM,gBAAgB,SAAS;GAC7B,MAAM,KAAK,MAAM,UAAU,cAAc;IACvC,gBAAgB,QAAQ;IACxB,UAAU,QAAQ;IAClB,YAAY,QAAQ;IACpB,QAAQ,QAAQ;IAChB,UAAU,QAAQ;IAClB,SAAS,UAAU,OAAO;IAC1B,QAAQ,QAAQ;IAChB,OACE,QAAQ,YAAY,WAAW,QAAQ,YAAY,SAC9C,QAAQ,SAAS,OAClB;IACN,QAAQ,KAAK,IAAI;GACnB,CAAC;GACD,IAAI,QAAQ,YAAY,WAAW,SAAS,KAAA,GAAW;IACrD,MAAM,OAAiB,CAAC;IACxB,OAAO;KACL,WAAW;KACX,OAAO,KAAK,YAAY;MACtB,KAAK,KAAK,GAAG;MACb,OAAO,GAAG,KAAK,KAAK,YAAY,gBAAgB,MAAM,IAAI,GAAG,GAAG,GAAG,KAAK,SAAS;KACnF;KACA,OAAO,GAAG,KAAK,OAAO;KACtB,aAAa,GAAG,KAAK,OAAO;KAE5B,cAAc,UAAU,YAAY,IAAI,IAAI;IAC9C;GACF;GACA,IAAI,QAAQ,YAAY,QAGtB,OAAO,EACL,MAAM;IACJ,YAAY;IACZ,GAAI,SAAS,KAAA,IACT,CAAC,IACD;KAAE,eAAe,GAAG,KAAK,OAAO;KAAM,YAAY,GAAG,KAAK,OAAO;IAAK;GAC5E,EACF;EAGJ;EAEA,MAAM,YAAY,SAAS,QAAQ;GACjC,MAAM,UAAU,cAAc,QAAQ,gBAAgB,QAAQ,KAAK,IAAI,CAAC;EAC1E;EAEA,WAAW,UAAU,UAAU,iBAAiB,KAAK;CACvD;AACF;;AAGA,SAAS,UAAU,SAAkC;CACnD,IAAI,QAAQ,YAAY,QAAQ,OAAO,QAAQ;CAC/C,OAAO,QAAQ,QAAQ,QAAQ,YAAY;AAC7C"}
1
+ {"version":3,"file":"observer.cjs","names":[],"sources":["../../src/reporting/observer.ts"],"sourcesContent":["import type { JourneyObserver, OutboundMessage } from '../engine';\nimport type { JourneyReporting } from '../store';\n\n/**\n * The observer journeyWorker installs when the store keeps reporting: every node arrival, copy\n * and outcome recorded through `store.reporting`.\n *\n * A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,\n * an email's links become `<origin>/t/l/<message>/<position>` — `/t/u/...` for its unsubscribe\n * link, so what a click means is fixed when it is sent — and an open pixel `<origin>/t/o/<message>`\n * (served by the Worker, ./tracking.ts); the renderer gets the id too, for the email's\n * preferences link (`email.preferencesUrl?m=<message>`), and a push carries\n * `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),\n * plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.\n * The links are recorded when the sender commits the rewritten email, before it goes out.\n */\nexport function reportingObserver(\n reporting: JourneyReporting,\n origin: string | undefined\n): JourneyObserver {\n const base = origin?.replace(/\\/$/, '');\n\n return {\n started: (event) => reporting.instanceStarted(event),\n\n nodeReached: (event) => reporting.nodeReached(event),\n\n messageSettled: (message) => reporting.messageSettled(message.idempotencyKey),\n\n async messagePrepared(message) {\n const id = await reporting.recordMessage({\n idempotencyKey: message.idempotencyKey,\n workflow: message.workflow,\n instanceId: message.instanceId,\n nodeId: message.nodeId,\n template: message.template,\n channel: channelOf(message),\n userId: message.userId,\n topic:\n message.channel === 'email' || message.channel === 'push'\n ? (message.topic ?? null)\n : null,\n sentAt: Date.now(),\n });\n if (message.channel === 'email' && base !== undefined) {\n const urls: string[] = [];\n return {\n messageId: id,\n link: (url, purpose) => {\n urls.push(url);\n return `${base}/t/${purpose === 'unsubscribe' ? 'u' : 'l'}/${id}/${urls.length - 1}`;\n },\n pixel: `${base}/t/o/${id}`,\n unsubscribe: `${base}/t/u/${id}`,\n // Before the transport takes it, so no click can reach a link not yet recorded\n commit: () => reporting.recordLinks(id, urls),\n };\n }\n if (message.channel === 'push') {\n // The app reports the tap with `message_id`; a client that can reach the Worker on its\n // own (iOS's notification service extension, a browser's service worker) posts to these\n return {\n data: {\n message_id: id,\n ...(base === undefined\n ? {}\n : { delivered_url: `${base}/t/d/${id}`, opened_url: `${base}/t/p/${id}` }),\n },\n };\n }\n return undefined;\n },\n\n async messageSent(message, result) {\n await reporting.settleMessage(message.idempotencyKey, result, Date.now());\n },\n\n finished: (event) => reporting.instanceFinished(event),\n };\n}\n\n/** Where the copy lands — the Studio's metric channel: a push is reported per platform. */\nfunction channelOf(message: OutboundMessage): string {\n if (message.channel !== 'push') return message.channel;\n return `push_${message.device?.platform ?? 'unknown'}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgBA,SAAgB,kBACd,WACA,QACiB;CACjB,MAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE;CAEtC,OAAO;EACL,UAAU,UAAU,UAAU,gBAAgB,KAAK;EAEnD,cAAc,UAAU,UAAU,YAAY,KAAK;EAEnD,iBAAiB,YAAY,UAAU,eAAe,QAAQ,cAAc;EAE5E,MAAM,gBAAgB,SAAS;GAC7B,MAAM,KAAK,MAAM,UAAU,cAAc;IACvC,gBAAgB,QAAQ;IACxB,UAAU,QAAQ;IAClB,YAAY,QAAQ;IACpB,QAAQ,QAAQ;IAChB,UAAU,QAAQ;IAClB,SAAS,UAAU,OAAO;IAC1B,QAAQ,QAAQ;IAChB,OACE,QAAQ,YAAY,WAAW,QAAQ,YAAY,SAC9C,QAAQ,SAAS,OAClB;IACN,QAAQ,KAAK,IAAI;GACnB,CAAC;GACD,IAAI,QAAQ,YAAY,WAAW,SAAS,KAAA,GAAW;IACrD,MAAM,OAAiB,CAAC;IACxB,OAAO;KACL,WAAW;KACX,OAAO,KAAK,YAAY;MACtB,KAAK,KAAK,GAAG;MACb,OAAO,GAAG,KAAK,KAAK,YAAY,gBAAgB,MAAM,IAAI,GAAG,GAAG,GAAG,KAAK,SAAS;KACnF;KACA,OAAO,GAAG,KAAK,OAAO;KACtB,aAAa,GAAG,KAAK,OAAO;KAE5B,cAAc,UAAU,YAAY,IAAI,IAAI;IAC9C;GACF;GACA,IAAI,QAAQ,YAAY,QAGtB,OAAO,EACL,MAAM;IACJ,YAAY;IACZ,GAAI,SAAS,KAAA,IACT,CAAC,IACD;KAAE,eAAe,GAAG,KAAK,OAAO;KAAM,YAAY,GAAG,KAAK,OAAO;IAAK;GAC5E,EACF;EAGJ;EAEA,MAAM,YAAY,SAAS,QAAQ;GACjC,MAAM,UAAU,cAAc,QAAQ,gBAAgB,QAAQ,KAAK,IAAI,CAAC;EAC1E;EAEA,WAAW,UAAU,UAAU,iBAAiB,KAAK;CACvD;AACF;;AAGA,SAAS,UAAU,SAAkC;CACnD,IAAI,QAAQ,YAAY,QAAQ,OAAO,QAAQ;CAC/C,OAAO,QAAQ,QAAQ,QAAQ,YAAY;AAC7C"}
@@ -17,6 +17,7 @@ function reportingObserver(reporting, origin) {
17
17
  return {
18
18
  started: (event) => reporting.instanceStarted(event),
19
19
  nodeReached: (event) => reporting.nodeReached(event),
20
+ messageSettled: (message) => reporting.messageSettled(message.idempotencyKey),
20
21
  async messagePrepared(message) {
21
22
  const id = await reporting.recordMessage({
22
23
  idempotencyKey: message.idempotencyKey,
@@ -1 +1 @@
1
- {"version":3,"file":"observer.mjs","names":[],"sources":["../../src/reporting/observer.ts"],"sourcesContent":["import type { JourneyObserver, OutboundMessage } from '../engine';\nimport type { JourneyReporting } from '../store';\n\n/**\n * The observer journeyWorker installs when the store keeps reporting: every node arrival, copy\n * and outcome recorded through `store.reporting`.\n *\n * A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,\n * an email's links become `<origin>/t/l/<message>/<position>` — `/t/u/...` for its unsubscribe\n * link, so what a click means is fixed when it is sent — and an open pixel `<origin>/t/o/<message>`\n * (served by the Worker, ./tracking.ts); the renderer gets the id too, for the email's\n * preferences link (`email.preferencesUrl?m=<message>`), and a push carries\n * `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),\n * plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.\n * The links are recorded when the sender commits the rewritten email, before it goes out.\n */\nexport function reportingObserver(\n reporting: JourneyReporting,\n origin: string | undefined\n): JourneyObserver {\n const base = origin?.replace(/\\/$/, '');\n\n return {\n started: (event) => reporting.instanceStarted(event),\n\n nodeReached: (event) => reporting.nodeReached(event),\n\n async messagePrepared(message) {\n const id = await reporting.recordMessage({\n idempotencyKey: message.idempotencyKey,\n workflow: message.workflow,\n instanceId: message.instanceId,\n nodeId: message.nodeId,\n template: message.template,\n channel: channelOf(message),\n userId: message.userId,\n topic:\n message.channel === 'email' || message.channel === 'push'\n ? (message.topic ?? null)\n : null,\n sentAt: Date.now(),\n });\n if (message.channel === 'email' && base !== undefined) {\n const urls: string[] = [];\n return {\n messageId: id,\n link: (url, purpose) => {\n urls.push(url);\n return `${base}/t/${purpose === 'unsubscribe' ? 'u' : 'l'}/${id}/${urls.length - 1}`;\n },\n pixel: `${base}/t/o/${id}`,\n unsubscribe: `${base}/t/u/${id}`,\n // Before the transport takes it, so no click can reach a link not yet recorded\n commit: () => reporting.recordLinks(id, urls),\n };\n }\n if (message.channel === 'push') {\n // The app reports the tap with `message_id`; a client that can reach the Worker on its\n // own (iOS's notification service extension, a browser's service worker) posts to these\n return {\n data: {\n message_id: id,\n ...(base === undefined\n ? {}\n : { delivered_url: `${base}/t/d/${id}`, opened_url: `${base}/t/p/${id}` }),\n },\n };\n }\n return undefined;\n },\n\n async messageSent(message, result) {\n await reporting.settleMessage(message.idempotencyKey, result, Date.now());\n },\n\n finished: (event) => reporting.instanceFinished(event),\n };\n}\n\n/** Where the copy lands — the Studio's metric channel: a push is reported per platform. */\nfunction channelOf(message: OutboundMessage): string {\n if (message.channel !== 'push') return message.channel;\n return `push_${message.device?.platform ?? 'unknown'}`;\n}\n"],"mappings":";;;;;;;;;;;;;;AAgBA,SAAgB,kBACd,WACA,QACiB;CACjB,MAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE;CAEtC,OAAO;EACL,UAAU,UAAU,UAAU,gBAAgB,KAAK;EAEnD,cAAc,UAAU,UAAU,YAAY,KAAK;EAEnD,MAAM,gBAAgB,SAAS;GAC7B,MAAM,KAAK,MAAM,UAAU,cAAc;IACvC,gBAAgB,QAAQ;IACxB,UAAU,QAAQ;IAClB,YAAY,QAAQ;IACpB,QAAQ,QAAQ;IAChB,UAAU,QAAQ;IAClB,SAAS,UAAU,OAAO;IAC1B,QAAQ,QAAQ;IAChB,OACE,QAAQ,YAAY,WAAW,QAAQ,YAAY,SAC9C,QAAQ,SAAS,OAClB;IACN,QAAQ,KAAK,IAAI;GACnB,CAAC;GACD,IAAI,QAAQ,YAAY,WAAW,SAAS,KAAA,GAAW;IACrD,MAAM,OAAiB,CAAC;IACxB,OAAO;KACL,WAAW;KACX,OAAO,KAAK,YAAY;MACtB,KAAK,KAAK,GAAG;MACb,OAAO,GAAG,KAAK,KAAK,YAAY,gBAAgB,MAAM,IAAI,GAAG,GAAG,GAAG,KAAK,SAAS;KACnF;KACA,OAAO,GAAG,KAAK,OAAO;KACtB,aAAa,GAAG,KAAK,OAAO;KAE5B,cAAc,UAAU,YAAY,IAAI,IAAI;IAC9C;GACF;GACA,IAAI,QAAQ,YAAY,QAGtB,OAAO,EACL,MAAM;IACJ,YAAY;IACZ,GAAI,SAAS,KAAA,IACT,CAAC,IACD;KAAE,eAAe,GAAG,KAAK,OAAO;KAAM,YAAY,GAAG,KAAK,OAAO;IAAK;GAC5E,EACF;EAGJ;EAEA,MAAM,YAAY,SAAS,QAAQ;GACjC,MAAM,UAAU,cAAc,QAAQ,gBAAgB,QAAQ,KAAK,IAAI,CAAC;EAC1E;EAEA,WAAW,UAAU,UAAU,iBAAiB,KAAK;CACvD;AACF;;AAGA,SAAS,UAAU,SAAkC;CACnD,IAAI,QAAQ,YAAY,QAAQ,OAAO,QAAQ;CAC/C,OAAO,QAAQ,QAAQ,QAAQ,YAAY;AAC7C"}
1
+ {"version":3,"file":"observer.mjs","names":[],"sources":["../../src/reporting/observer.ts"],"sourcesContent":["import type { JourneyObserver, OutboundMessage } from '../engine';\nimport type { JourneyReporting } from '../store';\n\n/**\n * The observer journeyWorker installs when the store keeps reporting: every node arrival, copy\n * and outcome recorded through `store.reporting`.\n *\n * A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,\n * an email's links become `<origin>/t/l/<message>/<position>` — `/t/u/...` for its unsubscribe\n * link, so what a click means is fixed when it is sent — and an open pixel `<origin>/t/o/<message>`\n * (served by the Worker, ./tracking.ts); the renderer gets the id too, for the email's\n * preferences link (`email.preferencesUrl?m=<message>`), and a push carries\n * `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),\n * plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.\n * The links are recorded when the sender commits the rewritten email, before it goes out.\n */\nexport function reportingObserver(\n reporting: JourneyReporting,\n origin: string | undefined\n): JourneyObserver {\n const base = origin?.replace(/\\/$/, '');\n\n return {\n started: (event) => reporting.instanceStarted(event),\n\n nodeReached: (event) => reporting.nodeReached(event),\n\n messageSettled: (message) => reporting.messageSettled(message.idempotencyKey),\n\n async messagePrepared(message) {\n const id = await reporting.recordMessage({\n idempotencyKey: message.idempotencyKey,\n workflow: message.workflow,\n instanceId: message.instanceId,\n nodeId: message.nodeId,\n template: message.template,\n channel: channelOf(message),\n userId: message.userId,\n topic:\n message.channel === 'email' || message.channel === 'push'\n ? (message.topic ?? null)\n : null,\n sentAt: Date.now(),\n });\n if (message.channel === 'email' && base !== undefined) {\n const urls: string[] = [];\n return {\n messageId: id,\n link: (url, purpose) => {\n urls.push(url);\n return `${base}/t/${purpose === 'unsubscribe' ? 'u' : 'l'}/${id}/${urls.length - 1}`;\n },\n pixel: `${base}/t/o/${id}`,\n unsubscribe: `${base}/t/u/${id}`,\n // Before the transport takes it, so no click can reach a link not yet recorded\n commit: () => reporting.recordLinks(id, urls),\n };\n }\n if (message.channel === 'push') {\n // The app reports the tap with `message_id`; a client that can reach the Worker on its\n // own (iOS's notification service extension, a browser's service worker) posts to these\n return {\n data: {\n message_id: id,\n ...(base === undefined\n ? {}\n : { delivered_url: `${base}/t/d/${id}`, opened_url: `${base}/t/p/${id}` }),\n },\n };\n }\n return undefined;\n },\n\n async messageSent(message, result) {\n await reporting.settleMessage(message.idempotencyKey, result, Date.now());\n },\n\n finished: (event) => reporting.instanceFinished(event),\n };\n}\n\n/** Where the copy lands — the Studio's metric channel: a push is reported per platform. */\nfunction channelOf(message: OutboundMessage): string {\n if (message.channel !== 'push') return message.channel;\n return `push_${message.device?.platform ?? 'unknown'}`;\n}\n"],"mappings":";;;;;;;;;;;;;;AAgBA,SAAgB,kBACd,WACA,QACiB;CACjB,MAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE;CAEtC,OAAO;EACL,UAAU,UAAU,UAAU,gBAAgB,KAAK;EAEnD,cAAc,UAAU,UAAU,YAAY,KAAK;EAEnD,iBAAiB,YAAY,UAAU,eAAe,QAAQ,cAAc;EAE5E,MAAM,gBAAgB,SAAS;GAC7B,MAAM,KAAK,MAAM,UAAU,cAAc;IACvC,gBAAgB,QAAQ;IACxB,UAAU,QAAQ;IAClB,YAAY,QAAQ;IACpB,QAAQ,QAAQ;IAChB,UAAU,QAAQ;IAClB,SAAS,UAAU,OAAO;IAC1B,QAAQ,QAAQ;IAChB,OACE,QAAQ,YAAY,WAAW,QAAQ,YAAY,SAC9C,QAAQ,SAAS,OAClB;IACN,QAAQ,KAAK,IAAI;GACnB,CAAC;GACD,IAAI,QAAQ,YAAY,WAAW,SAAS,KAAA,GAAW;IACrD,MAAM,OAAiB,CAAC;IACxB,OAAO;KACL,WAAW;KACX,OAAO,KAAK,YAAY;MACtB,KAAK,KAAK,GAAG;MACb,OAAO,GAAG,KAAK,KAAK,YAAY,gBAAgB,MAAM,IAAI,GAAG,GAAG,GAAG,KAAK,SAAS;KACnF;KACA,OAAO,GAAG,KAAK,OAAO;KACtB,aAAa,GAAG,KAAK,OAAO;KAE5B,cAAc,UAAU,YAAY,IAAI,IAAI;IAC9C;GACF;GACA,IAAI,QAAQ,YAAY,QAGtB,OAAO,EACL,MAAM;IACJ,YAAY;IACZ,GAAI,SAAS,KAAA,IACT,CAAC,IACD;KAAE,eAAe,GAAG,KAAK,OAAO;KAAM,YAAY,GAAG,KAAK,OAAO;IAAK;GAC5E,EACF;EAGJ;EAEA,MAAM,YAAY,SAAS,QAAQ;GACjC,MAAM,UAAU,cAAc,QAAQ,gBAAgB,QAAQ,KAAK,IAAI,CAAC;EAC1E;EAEA,WAAW,UAAU,UAAU,iBAAiB,KAAK;CACvD;AACF;;AAGA,SAAS,UAAU,SAAkC;CACnD,IAAI,QAAQ,YAAY,QAAQ,OAAO,QAAQ;CAC/C,OAAO,QAAQ,QAAQ,QAAQ,YAAY;AAC7C"}
@@ -3,27 +3,46 @@ const require_engine_condition = require("../engine/condition.cjs");
3
3
  // src/store/facts.ts
4
4
  /** The profile path that reads the user's own id (`u.userId`). */
5
5
  const USER_ID_PROPERTY = "userId";
6
+ /** Payloads per round trip when a `where` filters events in JS. */
7
+ const PAYLOAD_PAGE = 500;
6
8
  /** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */
7
9
  var JourneyFactSource = class {
8
10
  store;
9
11
  userId;
10
- constructor(store, userId) {
12
+ owner;
13
+ constructor(store, userId, owner = () => Promise.resolve(userId)) {
11
14
  this.store = store;
12
15
  this.userId = userId;
16
+ this.owner = owner;
13
17
  }
14
18
  async countEvents(event, opts) {
15
19
  const window = {
16
20
  ...opts?.sinceMs === void 0 ? {} : { sinceMs: opts.sinceMs },
17
21
  ...opts?.untilMs === void 0 ? {} : { untilMs: opts.untilMs }
18
22
  };
19
- if (opts?.where === void 0) return this.store.countEvents(this.userId, event, window);
23
+ const atLeast = opts?.atLeast;
24
+ const userId = await this.owner();
25
+ if (opts?.where === void 0) return this.store.countEvents(userId, event, window, atLeast);
20
26
  const where = opts.where;
21
- return (await this.store.listEventPayloads(this.userId, event, window)).filter((payload) => require_engine_condition.matchesWhere(payload, where)).length;
27
+ let matched = 0;
28
+ let after;
29
+ for (;;) {
30
+ const page = await this.store.pageEventPayloads(userId, event, window, {
31
+ limit: PAYLOAD_PAGE,
32
+ after
33
+ });
34
+ for (const payload of page.payloads) {
35
+ if (require_engine_condition.matchesWhere(payload, where)) matched += 1;
36
+ if (atLeast !== void 0 && matched >= atLeast) return matched;
37
+ }
38
+ if (page.next === void 0) return matched;
39
+ after = page.next;
40
+ }
22
41
  }
23
42
  /**
24
43
  * The scalar boundary. Everything downstream — comparisons, subject
25
44
  * placeholders, resolved message props — is typed ScalarIR, but nothing so
26
- * far has checked that the stored JSON is one: /identify validates only that
45
+ * far has checked that the stored JSON is one: /ingest validates only that
27
46
  * `props` is a plain object, and the merge stores whatever nesting it is
28
47
  * handed. Left unchecked, `gt(score, 3)` on `['5']` coerces its way to true
29
48
  * while `eq(score, 5)` is false, and `String(recipient)` on an object
@@ -35,8 +54,9 @@ var JourneyFactSource = class {
35
54
  * place.
36
55
  */
37
56
  async getProperty(path) {
38
- if (path === "userId") return this.userId;
39
- const props = await this.store.getProfile(this.userId);
57
+ const userId = await this.owner();
58
+ if (path === "userId") return userId;
59
+ const props = await this.store.getProfile(userId);
40
60
  if (props === void 0) return void 0;
41
61
  if (!Object.hasOwn(props, path)) return void 0;
42
62
  const value = props[path];
@@ -47,10 +67,10 @@ var JourneyFactSource = class {
47
67
  }
48
68
  /** The user's opt-outs: the channel's, or with a topic that topic's as well. */
49
69
  async isSuppressed(channel, topic) {
50
- return this.store.isSuppressed(this.userId, channel, topic);
70
+ return this.store.isSuppressed(await this.owner(), channel, topic);
51
71
  }
52
- listDevices() {
53
- return this.store.listDevices(this.userId);
72
+ async listDevices() {
73
+ return this.store.listDevices(await this.owner());
54
74
  }
55
75
  };
56
76
  exports.JourneyFactSource = JourneyFactSource;
@@ -1 +1 @@
1
- {"version":3,"file":"facts.cjs","names":["matchesWhere"],"sources":["../../src/store/facts.ts"],"sourcesContent":["import { matchesWhere } from '../engine/condition';\nimport type { FactSource, PushDevice } from '../engine/ports';\nimport type { ConditionIR, ScalarIR } from '../ir';\nimport type { JourneyStore } from './index';\n\n/** The profile path that reads the user's own id (`u.userId`). */\nexport const USER_ID_PROPERTY = 'userId';\n\n/** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */\nexport class JourneyFactSource implements FactSource {\n constructor(\n private readonly store: JourneyStore,\n private readonly userId: string\n ) {}\n\n async countEvents(\n event: string,\n opts?: { sinceMs?: number; untilMs?: number; where?: ConditionIR }\n ): Promise<number> {\n const window = {\n ...(opts?.sinceMs === undefined ? {} : { sinceMs: opts.sinceMs }),\n ...(opts?.untilMs === undefined ? {} : { untilMs: opts.untilMs }),\n };\n if (opts?.where === undefined) return this.store.countEvents(this.userId, event, window);\n // Payload filtering happens in JS via the shared evaluator — one set of\n // where semantics with the in-memory FactSource; per-user row counts are small.\n const where = opts.where;\n const payloads = await this.store.listEventPayloads(this.userId, event, window);\n return payloads.filter((payload) => matchesWhere(payload, where)).length;\n }\n\n /**\n * The scalar boundary. Everything downstream — comparisons, subject\n * placeholders, resolved message props — is typed ScalarIR, but nothing so\n * far has checked that the stored JSON is one: /identify validates only that\n * `props` is a plain object, and the merge stores whatever nesting it is\n * handed. Left unchecked, `gt(score, 3)` on `['5']` coerces its way to true\n * while `eq(score, 5)` is false, and `String(recipient)` on an object\n * addresses an email to `[object Object]`.\n *\n * So a non-scalar value reads as absent, matching how matchesWhere treats a\n * non-scalar payload leaf. The port cannot say \"present but not a scalar\",\n * and guessing at a coercion is what produced the wrong branch in the first\n * place.\n */\n async getProperty(path: string): Promise<ScalarIR | undefined> {\n // The system field: the request's identity, never stored in the profile (contract.user.userId)\n if (path === USER_ID_PROPERTY) return this.userId;\n const props: Record<string, unknown> | undefined = await this.store.getProfile(this.userId);\n if (props === undefined) return undefined;\n // Own keys only: `props[path]` would otherwise find Object.prototype, so\n // `exists(u.toString)` held for every user with a profile row and a subject\n // placeholder rendered native-code text into the email.\n if (!Object.hasOwn(props, path)) return undefined;\n const value = props[path];\n // An explicit null means \"unset\" and behaves as not_exists. /identify\n // itself clears a property by removing the key (the merge drops nulls), so\n // this covers profiles written by other means — a direct SQL fixup, a\n // migration, an import.\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n return undefined;\n }\n\n getSegmentCondition(name: string): Promise<ConditionIR | undefined> {\n return this.store.getSegmentCondition(name);\n }\n\n /** The user's opt-outs: the channel's, or with a topic that topic's as well. */\n async isSuppressed(channel: string, topic?: string): Promise<boolean> {\n return this.store.isSuppressed(this.userId, channel, topic);\n }\n\n listDevices(): Promise<PushDevice[]> {\n return this.store.listDevices(this.userId);\n }\n}\n"],"mappings":";;;;AAMA,MAAa,mBAAmB;;AAGhC,IAAa,oBAAb,MAAqD;CAEhC;CACA;CAFnB,YACE,OACA,QACA;EAFiB,KAAA,QAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,YACJ,OACA,MACiB;EACjB,MAAM,SAAS;GACb,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;GAC/D,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;EACjE;EACA,IAAI,MAAM,UAAU,KAAA,GAAW,OAAO,KAAK,MAAM,YAAY,KAAK,QAAQ,OAAO,MAAM;EAGvF,MAAM,QAAQ,KAAK;EAEnB,QAAO,MADgB,KAAK,MAAM,kBAAkB,KAAK,QAAQ,OAAO,MAAM,EAAA,CAC9D,QAAQ,YAAYA,yBAAAA,aAAa,SAAS,KAAK,CAAC,CAAC,CAAC;CACpE;;;;;;;;;;;;;;;CAgBA,MAAM,YAAY,MAA6C;EAE7D,IAAI,SAAA,UAA2B,OAAO,KAAK;EAC3C,MAAM,QAA6C,MAAM,KAAK,MAAM,WAAW,KAAK,MAAM;EAC1F,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;EAIhC,IAAI,CAAC,OAAO,OAAO,OAAO,IAAI,GAAG,OAAO,KAAA;EACxC,MAAM,QAAQ,MAAM;EAKpB,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAC7E,OAAO;CAGX;CAEA,oBAAoB,MAAgD;EAClE,OAAO,KAAK,MAAM,oBAAoB,IAAI;CAC5C;;CAGA,MAAM,aAAa,SAAiB,OAAkC;EACpE,OAAO,KAAK,MAAM,aAAa,KAAK,QAAQ,SAAS,KAAK;CAC5D;CAEA,cAAqC;EACnC,OAAO,KAAK,MAAM,YAAY,KAAK,MAAM;CAC3C;AACF"}
1
+ {"version":3,"file":"facts.cjs","names":["matchesWhere"],"sources":["../../src/store/facts.ts"],"sourcesContent":["import { matchesWhere } from '../engine/condition';\nimport type { FactSource, PushDevice } from '../engine/ports';\nimport type { ConditionIR, ScalarIR } from '../ir';\nimport type { EventCursor, JourneyStore } from './index';\n\n/** The profile path that reads the user's own id (`u.userId`). */\nexport const USER_ID_PROPERTY = 'userId';\n\n/** Payloads per round trip when a `where` filters events in JS. */\nconst PAYLOAD_PAGE = 500;\n\n/** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */\nexport class JourneyFactSource implements FactSource {\n constructor(\n private readonly store: JourneyStore,\n private readonly userId: string,\n /** Who the facts are about now, for a journey that can change hands; `userId` otherwise. */\n private readonly owner: () => Promise<string> = () => Promise.resolve(userId)\n ) {}\n\n async countEvents(\n event: string,\n opts?: { sinceMs?: number; untilMs?: number; where?: ConditionIR; atLeast?: number }\n ): Promise<number> {\n const window = {\n ...(opts?.sinceMs === undefined ? {} : { sinceMs: opts.sinceMs }),\n ...(opts?.untilMs === undefined ? {} : { untilMs: opts.untilMs }),\n };\n const atLeast = opts?.atLeast;\n const userId = await this.owner();\n if (opts?.where === undefined) {\n return this.store.countEvents(userId, event, window, atLeast);\n }\n // Payload filtering stays in JS through the shared evaluator — one set of where semantics\n // with the in-memory FactSource — but a page at a time, stopping as soon as enough events\n // match. A heavy user's history is no longer loaded whole on every evaluation; only a user\n // with no match left to find is read to the end, and then in bounded pages.\n const where = opts.where;\n let matched = 0;\n let after: EventCursor | undefined;\n for (;;) {\n const page = await this.store.pageEventPayloads(userId, event, window, {\n limit: PAYLOAD_PAGE,\n after,\n });\n for (const payload of page.payloads) {\n if (matchesWhere(payload, where)) matched += 1;\n if (atLeast !== undefined && matched >= atLeast) return matched;\n }\n if (page.next === undefined) return matched;\n after = page.next;\n }\n }\n\n /**\n * The scalar boundary. Everything downstream — comparisons, subject\n * placeholders, resolved message props — is typed ScalarIR, but nothing so\n * far has checked that the stored JSON is one: /ingest validates only that\n * `props` is a plain object, and the merge stores whatever nesting it is\n * handed. Left unchecked, `gt(score, 3)` on `['5']` coerces its way to true\n * while `eq(score, 5)` is false, and `String(recipient)` on an object\n * addresses an email to `[object Object]`.\n *\n * So a non-scalar value reads as absent, matching how matchesWhere treats a\n * non-scalar payload leaf. The port cannot say \"present but not a scalar\",\n * and guessing at a coercion is what produced the wrong branch in the first\n * place.\n */\n async getProperty(path: string): Promise<ScalarIR | undefined> {\n // The system field: the request's identity, never stored in the profile (contract.user.userId)\n const userId = await this.owner();\n if (path === USER_ID_PROPERTY) return userId;\n const props: Record<string, unknown> | undefined = await this.store.getProfile(userId);\n if (props === undefined) return undefined;\n // Own keys only: `props[path]` would otherwise find Object.prototype, so\n // `exists(u.toString)` held for every user with a profile row and a subject\n // placeholder rendered native-code text into the email.\n if (!Object.hasOwn(props, path)) return undefined;\n const value = props[path];\n // An explicit null means \"unset\" and behaves as not_exists. /ingest\n // itself clears a property by removing the key (the merge drops nulls), so\n // this covers profiles written by other means — a direct SQL fixup, a\n // migration, an import.\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n return undefined;\n }\n\n getSegmentCondition(name: string): Promise<ConditionIR | undefined> {\n return this.store.getSegmentCondition(name);\n }\n\n /** The user's opt-outs: the channel's, or with a topic that topic's as well. */\n async isSuppressed(channel: string, topic?: string): Promise<boolean> {\n return this.store.isSuppressed(await this.owner(), channel, topic);\n }\n\n async listDevices(): Promise<PushDevice[]> {\n return this.store.listDevices(await this.owner());\n }\n}\n"],"mappings":";;;;AAMA,MAAa,mBAAmB;;AAGhC,MAAM,eAAe;;AAGrB,IAAa,oBAAb,MAAqD;CAEhC;CACA;CAEA;CAJnB,YACE,OACA,QAEA,cAAsD,QAAQ,QAAQ,MAAM,GAC5E;EAJiB,KAAA,QAAA;EACA,KAAA,SAAA;EAEA,KAAA,QAAA;CAChB;CAEH,MAAM,YACJ,OACA,MACiB;EACjB,MAAM,SAAS;GACb,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;GAC/D,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;EACjE;EACA,MAAM,UAAU,MAAM;EACtB,MAAM,SAAS,MAAM,KAAK,MAAM;EAChC,IAAI,MAAM,UAAU,KAAA,GAClB,OAAO,KAAK,MAAM,YAAY,QAAQ,OAAO,QAAQ,OAAO;EAM9D,MAAM,QAAQ,KAAK;EACnB,IAAI,UAAU;EACd,IAAI;EACJ,SAAS;GACP,MAAM,OAAO,MAAM,KAAK,MAAM,kBAAkB,QAAQ,OAAO,QAAQ;IACrE,OAAO;IACP;GACF,CAAC;GACD,KAAK,MAAM,WAAW,KAAK,UAAU;IACnC,IAAIA,yBAAAA,aAAa,SAAS,KAAK,GAAG,WAAW;IAC7C,IAAI,YAAY,KAAA,KAAa,WAAW,SAAS,OAAO;GAC1D;GACA,IAAI,KAAK,SAAS,KAAA,GAAW,OAAO;GACpC,QAAQ,KAAK;EACf;CACF;;;;;;;;;;;;;;;CAgBA,MAAM,YAAY,MAA6C;EAE7D,MAAM,SAAS,MAAM,KAAK,MAAM;EAChC,IAAI,SAAA,UAA2B,OAAO;EACtC,MAAM,QAA6C,MAAM,KAAK,MAAM,WAAW,MAAM;EACrF,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;EAIhC,IAAI,CAAC,OAAO,OAAO,OAAO,IAAI,GAAG,OAAO,KAAA;EACxC,MAAM,QAAQ,MAAM;EAKpB,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAC7E,OAAO;CAGX;CAEA,oBAAoB,MAAgD;EAClE,OAAO,KAAK,MAAM,oBAAoB,IAAI;CAC5C;;CAGA,MAAM,aAAa,SAAiB,OAAkC;EACpE,OAAO,KAAK,MAAM,aAAa,MAAM,KAAK,MAAM,GAAG,SAAS,KAAK;CACnE;CAEA,MAAM,cAAqC;EACzC,OAAO,KAAK,MAAM,YAAY,MAAM,KAAK,MAAM,CAAC;CAClD;AACF"}
@@ -7,16 +7,21 @@ export declare const USER_ID_PROPERTY = "userId";
7
7
  export declare class JourneyFactSource implements FactSource {
8
8
  private readonly store;
9
9
  private readonly userId;
10
- constructor(store: JourneyStore, userId: string);
10
+ /** Who the facts are about now, for a journey that can change hands; `userId` otherwise. */
11
+ private readonly owner;
12
+ constructor(store: JourneyStore, userId: string,
13
+ /** Who the facts are about now, for a journey that can change hands; `userId` otherwise. */
14
+ owner?: () => Promise<string>);
11
15
  countEvents(event: string, opts?: {
12
16
  sinceMs?: number;
13
17
  untilMs?: number;
14
18
  where?: ConditionIR;
19
+ atLeast?: number;
15
20
  }): Promise<number>;
16
21
  /**
17
22
  * The scalar boundary. Everything downstream — comparisons, subject
18
23
  * placeholders, resolved message props — is typed ScalarIR, but nothing so
19
- * far has checked that the stored JSON is one: /identify validates only that
24
+ * far has checked that the stored JSON is one: /ingest validates only that
20
25
  * `props` is a plain object, and the merge stores whatever nesting it is
21
26
  * handed. Left unchecked, `gt(score, 3)` on `['5']` coerces its way to true
22
27
  * while `eq(score, 5)` is false, and `String(recipient)` on an object
@@ -1 +1 @@
1
- {"version":3,"file":"facts.d.cts","names":[],"sources":["../../src/store/facts.ts"],"mappings":";;;;qBAMa;;qBAGA,6BAA6B;mBAErB;mBACA;EAFnB,YACmB,OAAO,cACP;EAGb,YACJ,eACA;IAAS;IAAkB;IAAkB,QAAQ;MACpD;;;;;;;;;;;;;;;EA2BG,YAAY,eAAe,QAAQ;EAoBzC,oBAAoB,eAAe,QAAQ;;EAKrC,aAAa,iBAAiB,iBAAiB;EAIrD,eAAe,QAAQ"}
1
+ {"version":3,"file":"facts.d.cts","names":[],"sources":["../../src/store/facts.ts"],"mappings":";;;;qBAMa;;qBAMA,6BAA6B;mBAErB;mBACA;;mBAEA;EAJnB,YACmB,OAAO,cACP;;EAEA,cAAa;EAG1B,YACJ,eACA;IAAS;IAAkB;IAAkB,QAAQ;IAAa;MACjE;;;;;;;;;;;;;;;EA6CG,YAAY,eAAe,QAAQ;EAqBzC,oBAAoB,eAAe,QAAQ;;EAKrC,aAAa,iBAAiB,iBAAiB;EAI/C,eAAe,QAAQ"}
@@ -7,16 +7,21 @@ export declare const USER_ID_PROPERTY = "userId";
7
7
  export declare class JourneyFactSource implements FactSource {
8
8
  private readonly store;
9
9
  private readonly userId;
10
- constructor(store: JourneyStore, userId: string);
10
+ /** Who the facts are about now, for a journey that can change hands; `userId` otherwise. */
11
+ private readonly owner;
12
+ constructor(store: JourneyStore, userId: string,
13
+ /** Who the facts are about now, for a journey that can change hands; `userId` otherwise. */
14
+ owner?: () => Promise<string>);
11
15
  countEvents(event: string, opts?: {
12
16
  sinceMs?: number;
13
17
  untilMs?: number;
14
18
  where?: ConditionIR;
19
+ atLeast?: number;
15
20
  }): Promise<number>;
16
21
  /**
17
22
  * The scalar boundary. Everything downstream — comparisons, subject
18
23
  * placeholders, resolved message props — is typed ScalarIR, but nothing so
19
- * far has checked that the stored JSON is one: /identify validates only that
24
+ * far has checked that the stored JSON is one: /ingest validates only that
20
25
  * `props` is a plain object, and the merge stores whatever nesting it is
21
26
  * handed. Left unchecked, `gt(score, 3)` on `['5']` coerces its way to true
22
27
  * while `eq(score, 5)` is false, and `String(recipient)` on an object
@@ -1 +1 @@
1
- {"version":3,"file":"facts.d.mts","names":[],"sources":["../../src/store/facts.ts"],"mappings":";;;;qBAMa;;qBAGA,6BAA6B;mBAErB;mBACA;EAFnB,YACmB,OAAO,cACP;EAGb,YACJ,eACA;IAAS;IAAkB;IAAkB,QAAQ;MACpD;;;;;;;;;;;;;;;EA2BG,YAAY,eAAe,QAAQ;EAoBzC,oBAAoB,eAAe,QAAQ;;EAKrC,aAAa,iBAAiB,iBAAiB;EAIrD,eAAe,QAAQ"}
1
+ {"version":3,"file":"facts.d.mts","names":[],"sources":["../../src/store/facts.ts"],"mappings":";;;;qBAMa;;qBAMA,6BAA6B;mBAErB;mBACA;;mBAEA;EAJnB,YACmB,OAAO,cACP;;EAEA,cAAa;EAG1B,YACJ,eACA;IAAS;IAAkB;IAAkB,QAAQ;IAAa;MACjE;;;;;;;;;;;;;;;EA6CG,YAAY,eAAe,QAAQ;EAqBzC,oBAAoB,eAAe,QAAQ;;EAKrC,aAAa,iBAAiB,iBAAiB;EAI/C,eAAe,QAAQ"}
@@ -2,27 +2,46 @@ import { matchesWhere } from "../engine/condition.mjs";
2
2
  // src/store/facts.ts
3
3
  /** The profile path that reads the user's own id (`u.userId`). */
4
4
  const USER_ID_PROPERTY = "userId";
5
+ /** Payloads per round trip when a `where` filters events in JS. */
6
+ const PAYLOAD_PAGE = 500;
5
7
  /** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */
6
8
  var JourneyFactSource = class {
7
9
  store;
8
10
  userId;
9
- constructor(store, userId) {
11
+ owner;
12
+ constructor(store, userId, owner = () => Promise.resolve(userId)) {
10
13
  this.store = store;
11
14
  this.userId = userId;
15
+ this.owner = owner;
12
16
  }
13
17
  async countEvents(event, opts) {
14
18
  const window = {
15
19
  ...opts?.sinceMs === void 0 ? {} : { sinceMs: opts.sinceMs },
16
20
  ...opts?.untilMs === void 0 ? {} : { untilMs: opts.untilMs }
17
21
  };
18
- if (opts?.where === void 0) return this.store.countEvents(this.userId, event, window);
22
+ const atLeast = opts?.atLeast;
23
+ const userId = await this.owner();
24
+ if (opts?.where === void 0) return this.store.countEvents(userId, event, window, atLeast);
19
25
  const where = opts.where;
20
- return (await this.store.listEventPayloads(this.userId, event, window)).filter((payload) => matchesWhere(payload, where)).length;
26
+ let matched = 0;
27
+ let after;
28
+ for (;;) {
29
+ const page = await this.store.pageEventPayloads(userId, event, window, {
30
+ limit: PAYLOAD_PAGE,
31
+ after
32
+ });
33
+ for (const payload of page.payloads) {
34
+ if (matchesWhere(payload, where)) matched += 1;
35
+ if (atLeast !== void 0 && matched >= atLeast) return matched;
36
+ }
37
+ if (page.next === void 0) return matched;
38
+ after = page.next;
39
+ }
21
40
  }
22
41
  /**
23
42
  * The scalar boundary. Everything downstream — comparisons, subject
24
43
  * placeholders, resolved message props — is typed ScalarIR, but nothing so
25
- * far has checked that the stored JSON is one: /identify validates only that
44
+ * far has checked that the stored JSON is one: /ingest validates only that
26
45
  * `props` is a plain object, and the merge stores whatever nesting it is
27
46
  * handed. Left unchecked, `gt(score, 3)` on `['5']` coerces its way to true
28
47
  * while `eq(score, 5)` is false, and `String(recipient)` on an object
@@ -34,8 +53,9 @@ var JourneyFactSource = class {
34
53
  * place.
35
54
  */
36
55
  async getProperty(path) {
37
- if (path === "userId") return this.userId;
38
- const props = await this.store.getProfile(this.userId);
56
+ const userId = await this.owner();
57
+ if (path === "userId") return userId;
58
+ const props = await this.store.getProfile(userId);
39
59
  if (props === void 0) return void 0;
40
60
  if (!Object.hasOwn(props, path)) return void 0;
41
61
  const value = props[path];
@@ -46,10 +66,10 @@ var JourneyFactSource = class {
46
66
  }
47
67
  /** The user's opt-outs: the channel's, or with a topic that topic's as well. */
48
68
  async isSuppressed(channel, topic) {
49
- return this.store.isSuppressed(this.userId, channel, topic);
69
+ return this.store.isSuppressed(await this.owner(), channel, topic);
50
70
  }
51
- listDevices() {
52
- return this.store.listDevices(this.userId);
71
+ async listDevices() {
72
+ return this.store.listDevices(await this.owner());
53
73
  }
54
74
  };
55
75
  export { JourneyFactSource, USER_ID_PROPERTY };
@@ -1 +1 @@
1
- {"version":3,"file":"facts.mjs","names":[],"sources":["../../src/store/facts.ts"],"sourcesContent":["import { matchesWhere } from '../engine/condition';\nimport type { FactSource, PushDevice } from '../engine/ports';\nimport type { ConditionIR, ScalarIR } from '../ir';\nimport type { JourneyStore } from './index';\n\n/** The profile path that reads the user's own id (`u.userId`). */\nexport const USER_ID_PROPERTY = 'userId';\n\n/** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */\nexport class JourneyFactSource implements FactSource {\n constructor(\n private readonly store: JourneyStore,\n private readonly userId: string\n ) {}\n\n async countEvents(\n event: string,\n opts?: { sinceMs?: number; untilMs?: number; where?: ConditionIR }\n ): Promise<number> {\n const window = {\n ...(opts?.sinceMs === undefined ? {} : { sinceMs: opts.sinceMs }),\n ...(opts?.untilMs === undefined ? {} : { untilMs: opts.untilMs }),\n };\n if (opts?.where === undefined) return this.store.countEvents(this.userId, event, window);\n // Payload filtering happens in JS via the shared evaluator — one set of\n // where semantics with the in-memory FactSource; per-user row counts are small.\n const where = opts.where;\n const payloads = await this.store.listEventPayloads(this.userId, event, window);\n return payloads.filter((payload) => matchesWhere(payload, where)).length;\n }\n\n /**\n * The scalar boundary. Everything downstream — comparisons, subject\n * placeholders, resolved message props — is typed ScalarIR, but nothing so\n * far has checked that the stored JSON is one: /identify validates only that\n * `props` is a plain object, and the merge stores whatever nesting it is\n * handed. Left unchecked, `gt(score, 3)` on `['5']` coerces its way to true\n * while `eq(score, 5)` is false, and `String(recipient)` on an object\n * addresses an email to `[object Object]`.\n *\n * So a non-scalar value reads as absent, matching how matchesWhere treats a\n * non-scalar payload leaf. The port cannot say \"present but not a scalar\",\n * and guessing at a coercion is what produced the wrong branch in the first\n * place.\n */\n async getProperty(path: string): Promise<ScalarIR | undefined> {\n // The system field: the request's identity, never stored in the profile (contract.user.userId)\n if (path === USER_ID_PROPERTY) return this.userId;\n const props: Record<string, unknown> | undefined = await this.store.getProfile(this.userId);\n if (props === undefined) return undefined;\n // Own keys only: `props[path]` would otherwise find Object.prototype, so\n // `exists(u.toString)` held for every user with a profile row and a subject\n // placeholder rendered native-code text into the email.\n if (!Object.hasOwn(props, path)) return undefined;\n const value = props[path];\n // An explicit null means \"unset\" and behaves as not_exists. /identify\n // itself clears a property by removing the key (the merge drops nulls), so\n // this covers profiles written by other means — a direct SQL fixup, a\n // migration, an import.\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n return undefined;\n }\n\n getSegmentCondition(name: string): Promise<ConditionIR | undefined> {\n return this.store.getSegmentCondition(name);\n }\n\n /** The user's opt-outs: the channel's, or with a topic that topic's as well. */\n async isSuppressed(channel: string, topic?: string): Promise<boolean> {\n return this.store.isSuppressed(this.userId, channel, topic);\n }\n\n listDevices(): Promise<PushDevice[]> {\n return this.store.listDevices(this.userId);\n }\n}\n"],"mappings":";;;AAMA,MAAa,mBAAmB;;AAGhC,IAAa,oBAAb,MAAqD;CAEhC;CACA;CAFnB,YACE,OACA,QACA;EAFiB,KAAA,QAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,YACJ,OACA,MACiB;EACjB,MAAM,SAAS;GACb,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;GAC/D,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;EACjE;EACA,IAAI,MAAM,UAAU,KAAA,GAAW,OAAO,KAAK,MAAM,YAAY,KAAK,QAAQ,OAAO,MAAM;EAGvF,MAAM,QAAQ,KAAK;EAEnB,QAAO,MADgB,KAAK,MAAM,kBAAkB,KAAK,QAAQ,OAAO,MAAM,EAAA,CAC9D,QAAQ,YAAY,aAAa,SAAS,KAAK,CAAC,CAAC,CAAC;CACpE;;;;;;;;;;;;;;;CAgBA,MAAM,YAAY,MAA6C;EAE7D,IAAI,SAAA,UAA2B,OAAO,KAAK;EAC3C,MAAM,QAA6C,MAAM,KAAK,MAAM,WAAW,KAAK,MAAM;EAC1F,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;EAIhC,IAAI,CAAC,OAAO,OAAO,OAAO,IAAI,GAAG,OAAO,KAAA;EACxC,MAAM,QAAQ,MAAM;EAKpB,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAC7E,OAAO;CAGX;CAEA,oBAAoB,MAAgD;EAClE,OAAO,KAAK,MAAM,oBAAoB,IAAI;CAC5C;;CAGA,MAAM,aAAa,SAAiB,OAAkC;EACpE,OAAO,KAAK,MAAM,aAAa,KAAK,QAAQ,SAAS,KAAK;CAC5D;CAEA,cAAqC;EACnC,OAAO,KAAK,MAAM,YAAY,KAAK,MAAM;CAC3C;AACF"}
1
+ {"version":3,"file":"facts.mjs","names":[],"sources":["../../src/store/facts.ts"],"sourcesContent":["import { matchesWhere } from '../engine/condition';\nimport type { FactSource, PushDevice } from '../engine/ports';\nimport type { ConditionIR, ScalarIR } from '../ir';\nimport type { EventCursor, JourneyStore } from './index';\n\n/** The profile path that reads the user's own id (`u.userId`). */\nexport const USER_ID_PROPERTY = 'userId';\n\n/** Payloads per round trip when a `where` filters events in JS. */\nconst PAYLOAD_PAGE = 500;\n\n/** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */\nexport class JourneyFactSource implements FactSource {\n constructor(\n private readonly store: JourneyStore,\n private readonly userId: string,\n /** Who the facts are about now, for a journey that can change hands; `userId` otherwise. */\n private readonly owner: () => Promise<string> = () => Promise.resolve(userId)\n ) {}\n\n async countEvents(\n event: string,\n opts?: { sinceMs?: number; untilMs?: number; where?: ConditionIR; atLeast?: number }\n ): Promise<number> {\n const window = {\n ...(opts?.sinceMs === undefined ? {} : { sinceMs: opts.sinceMs }),\n ...(opts?.untilMs === undefined ? {} : { untilMs: opts.untilMs }),\n };\n const atLeast = opts?.atLeast;\n const userId = await this.owner();\n if (opts?.where === undefined) {\n return this.store.countEvents(userId, event, window, atLeast);\n }\n // Payload filtering stays in JS through the shared evaluator — one set of where semantics\n // with the in-memory FactSource — but a page at a time, stopping as soon as enough events\n // match. A heavy user's history is no longer loaded whole on every evaluation; only a user\n // with no match left to find is read to the end, and then in bounded pages.\n const where = opts.where;\n let matched = 0;\n let after: EventCursor | undefined;\n for (;;) {\n const page = await this.store.pageEventPayloads(userId, event, window, {\n limit: PAYLOAD_PAGE,\n after,\n });\n for (const payload of page.payloads) {\n if (matchesWhere(payload, where)) matched += 1;\n if (atLeast !== undefined && matched >= atLeast) return matched;\n }\n if (page.next === undefined) return matched;\n after = page.next;\n }\n }\n\n /**\n * The scalar boundary. Everything downstream — comparisons, subject\n * placeholders, resolved message props — is typed ScalarIR, but nothing so\n * far has checked that the stored JSON is one: /ingest validates only that\n * `props` is a plain object, and the merge stores whatever nesting it is\n * handed. Left unchecked, `gt(score, 3)` on `['5']` coerces its way to true\n * while `eq(score, 5)` is false, and `String(recipient)` on an object\n * addresses an email to `[object Object]`.\n *\n * So a non-scalar value reads as absent, matching how matchesWhere treats a\n * non-scalar payload leaf. The port cannot say \"present but not a scalar\",\n * and guessing at a coercion is what produced the wrong branch in the first\n * place.\n */\n async getProperty(path: string): Promise<ScalarIR | undefined> {\n // The system field: the request's identity, never stored in the profile (contract.user.userId)\n const userId = await this.owner();\n if (path === USER_ID_PROPERTY) return userId;\n const props: Record<string, unknown> | undefined = await this.store.getProfile(userId);\n if (props === undefined) return undefined;\n // Own keys only: `props[path]` would otherwise find Object.prototype, so\n // `exists(u.toString)` held for every user with a profile row and a subject\n // placeholder rendered native-code text into the email.\n if (!Object.hasOwn(props, path)) return undefined;\n const value = props[path];\n // An explicit null means \"unset\" and behaves as not_exists. /ingest\n // itself clears a property by removing the key (the merge drops nulls), so\n // this covers profiles written by other means — a direct SQL fixup, a\n // migration, an import.\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n return undefined;\n }\n\n getSegmentCondition(name: string): Promise<ConditionIR | undefined> {\n return this.store.getSegmentCondition(name);\n }\n\n /** The user's opt-outs: the channel's, or with a topic that topic's as well. */\n async isSuppressed(channel: string, topic?: string): Promise<boolean> {\n return this.store.isSuppressed(await this.owner(), channel, topic);\n }\n\n async listDevices(): Promise<PushDevice[]> {\n return this.store.listDevices(await this.owner());\n }\n}\n"],"mappings":";;;AAMA,MAAa,mBAAmB;;AAGhC,MAAM,eAAe;;AAGrB,IAAa,oBAAb,MAAqD;CAEhC;CACA;CAEA;CAJnB,YACE,OACA,QAEA,cAAsD,QAAQ,QAAQ,MAAM,GAC5E;EAJiB,KAAA,QAAA;EACA,KAAA,SAAA;EAEA,KAAA,QAAA;CAChB;CAEH,MAAM,YACJ,OACA,MACiB;EACjB,MAAM,SAAS;GACb,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;GAC/D,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;EACjE;EACA,MAAM,UAAU,MAAM;EACtB,MAAM,SAAS,MAAM,KAAK,MAAM;EAChC,IAAI,MAAM,UAAU,KAAA,GAClB,OAAO,KAAK,MAAM,YAAY,QAAQ,OAAO,QAAQ,OAAO;EAM9D,MAAM,QAAQ,KAAK;EACnB,IAAI,UAAU;EACd,IAAI;EACJ,SAAS;GACP,MAAM,OAAO,MAAM,KAAK,MAAM,kBAAkB,QAAQ,OAAO,QAAQ;IACrE,OAAO;IACP;GACF,CAAC;GACD,KAAK,MAAM,WAAW,KAAK,UAAU;IACnC,IAAI,aAAa,SAAS,KAAK,GAAG,WAAW;IAC7C,IAAI,YAAY,KAAA,KAAa,WAAW,SAAS,OAAO;GAC1D;GACA,IAAI,KAAK,SAAS,KAAA,GAAW,OAAO;GACpC,QAAQ,KAAK;EACf;CACF;;;;;;;;;;;;;;;CAgBA,MAAM,YAAY,MAA6C;EAE7D,MAAM,SAAS,MAAM,KAAK,MAAM;EAChC,IAAI,SAAA,UAA2B,OAAO;EACtC,MAAM,QAA6C,MAAM,KAAK,MAAM,WAAW,MAAM;EACrF,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;EAIhC,IAAI,CAAC,OAAO,OAAO,OAAO,IAAI,GAAG,OAAO,KAAA;EACxC,MAAM,QAAQ,MAAM;EAKpB,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAC7E,OAAO;CAGX;CAEA,oBAAoB,MAAgD;EAClE,OAAO,KAAK,MAAM,oBAAoB,IAAI;CAC5C;;CAGA,MAAM,aAAa,SAAiB,OAAkC;EACpE,OAAO,KAAK,MAAM,aAAa,MAAM,KAAK,MAAM,GAAG,SAAS,KAAK;CACnE;CAEA,MAAM,cAAqC;EACzC,OAAO,KAAK,MAAM,YAAY,MAAM,KAAK,MAAM,CAAC;CAClD;AACF"}
@@ -0,0 +1,34 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ // src/store/identity.ts
3
+ /**
4
+ * Who the engine's rows are about: a user, or a visitor not signed in yet. A visitor is a user
5
+ * with a reserved id, so journeys, events, profiles and opt-outs work the same for both; signing
6
+ * in moves the visitor's rows to the user (`linkVisitor`). Callers never see the prefix: they
7
+ * pass `userId` and `visitorId` as two fields.
8
+ */
9
+ const VISITOR_PREFIX = "visitor:";
10
+ function visitorSubject(visitorId) {
11
+ return `${VISITOR_PREFIX}${visitorId}`;
12
+ }
13
+ function isVisitorSubject(subject) {
14
+ return subject.startsWith(VISITOR_PREFIX);
15
+ }
16
+ /** The visitor id inside a subject; undefined for a user. */
17
+ function visitorIdOf(subject) {
18
+ return isVisitorSubject(subject) ? subject.slice(8) : void 0;
19
+ }
20
+ /**
21
+ * Who an instance belongs to now. A journey a visitor started moves to the user when they sign in,
22
+ * with its entry: read per use, because a wait can outlast the sign-in. An instance a user started
23
+ * never moves, so it costs no read.
24
+ */
25
+ async function instanceOwner(store, instanceId, startedAs) {
26
+ if (!isVisitorSubject(startedAs)) return startedAs;
27
+ return await store.entryOwner(instanceId) ?? startedAs;
28
+ }
29
+ exports.instanceOwner = instanceOwner;
30
+ exports.isVisitorSubject = isVisitorSubject;
31
+ exports.visitorIdOf = visitorIdOf;
32
+ exports.visitorSubject = visitorSubject;
33
+
34
+ //# sourceMappingURL=identity.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.cjs","names":[],"sources":["../../src/store/identity.ts"],"sourcesContent":["/**\n * Who the engine's rows are about: a user, or a visitor not signed in yet. A visitor is a user\n * with a reserved id, so journeys, events, profiles and opt-outs work the same for both; signing\n * in moves the visitor's rows to the user (`linkVisitor`). Callers never see the prefix: they\n * pass `userId` and `visitorId` as two fields.\n */\nconst VISITOR_PREFIX = 'visitor:';\n\nexport function visitorSubject(visitorId: string): string {\n return `${VISITOR_PREFIX}${visitorId}`;\n}\n\nexport function isVisitorSubject(subject: string): boolean {\n return subject.startsWith(VISITOR_PREFIX);\n}\n\n/** The visitor id inside a subject; undefined for a user. */\nexport function visitorIdOf(subject: string): string | undefined {\n return isVisitorSubject(subject) ? subject.slice(VISITOR_PREFIX.length) : undefined;\n}\n\n/**\n * Who an instance belongs to now. A journey a visitor started moves to the user when they sign in,\n * with its entry: read per use, because a wait can outlast the sign-in. An instance a user started\n * never moves, so it costs no read.\n */\nexport async function instanceOwner(\n store: { entryOwner(instanceId: string): Promise<string | undefined> },\n instanceId: string,\n startedAs: string\n): Promise<string> {\n if (!isVisitorSubject(startedAs)) return startedAs;\n return (await store.entryOwner(instanceId)) ?? startedAs;\n}\n"],"mappings":";;;;;;;;AAMA,MAAM,iBAAiB;AAEvB,SAAgB,eAAe,WAA2B;CACxD,OAAO,GAAG,iBAAiB;AAC7B;AAEA,SAAgB,iBAAiB,SAA0B;CACzD,OAAO,QAAQ,WAAW,cAAc;AAC1C;;AAGA,SAAgB,YAAY,SAAqC;CAC/D,OAAO,iBAAiB,OAAO,IAAI,QAAQ,MAAM,CAAqB,IAAI,KAAA;AAC5E;;;;;;AAOA,eAAsB,cACpB,OACA,YACA,WACiB;CACjB,IAAI,CAAC,iBAAiB,SAAS,GAAG,OAAO;CACzC,OAAQ,MAAM,MAAM,WAAW,UAAU,KAAM;AACjD"}
@@ -0,0 +1,13 @@
1
+ export declare function visitorSubject(visitorId: string): string;
2
+ export declare function isVisitorSubject(subject: string): boolean;
3
+ /** The visitor id inside a subject; undefined for a user. */
4
+ export declare function visitorIdOf(subject: string): string | undefined;
5
+ /**
6
+ * Who an instance belongs to now. A journey a visitor started moves to the user when they sign in,
7
+ * with its entry: read per use, because a wait can outlast the sign-in. An instance a user started
8
+ * never moves, so it costs no read.
9
+ */
10
+ export declare function instanceOwner(store: {
11
+ entryOwner(instanceId: string): Promise<string | undefined>;
12
+ }, instanceId: string, startedAs: string): Promise<string>;
13
+ //# sourceMappingURL=identity.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.d.cts","names":[],"sources":["../../src/store/identity.ts"],"mappings":"A,wBAQgB,eAAe;wBAIf,iBAAiB;;wBAKjB,YAAY;;;;;;wBASN,cACpB;EAAS,WAAW,qBAAqB;GACzC,oBACA,oBACC"}
@@ -0,0 +1,13 @@
1
+ export declare function visitorSubject(visitorId: string): string;
2
+ export declare function isVisitorSubject(subject: string): boolean;
3
+ /** The visitor id inside a subject; undefined for a user. */
4
+ export declare function visitorIdOf(subject: string): string | undefined;
5
+ /**
6
+ * Who an instance belongs to now. A journey a visitor started moves to the user when they sign in,
7
+ * with its entry: read per use, because a wait can outlast the sign-in. An instance a user started
8
+ * never moves, so it costs no read.
9
+ */
10
+ export declare function instanceOwner(store: {
11
+ entryOwner(instanceId: string): Promise<string | undefined>;
12
+ }, instanceId: string, startedAs: string): Promise<string>;
13
+ //# sourceMappingURL=identity.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.d.mts","names":[],"sources":["../../src/store/identity.ts"],"mappings":"A,wBAQgB,eAAe;wBAIf,iBAAiB;;wBAKjB,YAAY;;;;;;wBASN,cACpB;EAAS,WAAW,qBAAqB;GACzC,oBACA,oBACC"}
@@ -0,0 +1,30 @@
1
+ // src/store/identity.ts
2
+ /**
3
+ * Who the engine's rows are about: a user, or a visitor not signed in yet. A visitor is a user
4
+ * with a reserved id, so journeys, events, profiles and opt-outs work the same for both; signing
5
+ * in moves the visitor's rows to the user (`linkVisitor`). Callers never see the prefix: they
6
+ * pass `userId` and `visitorId` as two fields.
7
+ */
8
+ const VISITOR_PREFIX = "visitor:";
9
+ function visitorSubject(visitorId) {
10
+ return `${VISITOR_PREFIX}${visitorId}`;
11
+ }
12
+ function isVisitorSubject(subject) {
13
+ return subject.startsWith(VISITOR_PREFIX);
14
+ }
15
+ /** The visitor id inside a subject; undefined for a user. */
16
+ function visitorIdOf(subject) {
17
+ return isVisitorSubject(subject) ? subject.slice(8) : void 0;
18
+ }
19
+ /**
20
+ * Who an instance belongs to now. A journey a visitor started moves to the user when they sign in,
21
+ * with its entry: read per use, because a wait can outlast the sign-in. An instance a user started
22
+ * never moves, so it costs no read.
23
+ */
24
+ async function instanceOwner(store, instanceId, startedAs) {
25
+ if (!isVisitorSubject(startedAs)) return startedAs;
26
+ return await store.entryOwner(instanceId) ?? startedAs;
27
+ }
28
+ export { instanceOwner, isVisitorSubject, visitorIdOf, visitorSubject };
29
+
30
+ //# sourceMappingURL=identity.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity.mjs","names":[],"sources":["../../src/store/identity.ts"],"sourcesContent":["/**\n * Who the engine's rows are about: a user, or a visitor not signed in yet. A visitor is a user\n * with a reserved id, so journeys, events, profiles and opt-outs work the same for both; signing\n * in moves the visitor's rows to the user (`linkVisitor`). Callers never see the prefix: they\n * pass `userId` and `visitorId` as two fields.\n */\nconst VISITOR_PREFIX = 'visitor:';\n\nexport function visitorSubject(visitorId: string): string {\n return `${VISITOR_PREFIX}${visitorId}`;\n}\n\nexport function isVisitorSubject(subject: string): boolean {\n return subject.startsWith(VISITOR_PREFIX);\n}\n\n/** The visitor id inside a subject; undefined for a user. */\nexport function visitorIdOf(subject: string): string | undefined {\n return isVisitorSubject(subject) ? subject.slice(VISITOR_PREFIX.length) : undefined;\n}\n\n/**\n * Who an instance belongs to now. A journey a visitor started moves to the user when they sign in,\n * with its entry: read per use, because a wait can outlast the sign-in. An instance a user started\n * never moves, so it costs no read.\n */\nexport async function instanceOwner(\n store: { entryOwner(instanceId: string): Promise<string | undefined> },\n instanceId: string,\n startedAs: string\n): Promise<string> {\n if (!isVisitorSubject(startedAs)) return startedAs;\n return (await store.entryOwner(instanceId)) ?? startedAs;\n}\n"],"mappings":";;;;;;;AAMA,MAAM,iBAAiB;AAEvB,SAAgB,eAAe,WAA2B;CACxD,OAAO,GAAG,iBAAiB;AAC7B;AAEA,SAAgB,iBAAiB,SAA0B;CACzD,OAAO,QAAQ,WAAW,cAAc;AAC1C;;AAGA,SAAgB,YAAY,SAAqC;CAC/D,OAAO,iBAAiB,OAAO,IAAI,QAAQ,MAAM,CAAqB,IAAI,KAAA;AAC5E;;;;;;AAOA,eAAsB,cACpB,OACA,YACA,WACiB;CACjB,IAAI,CAAC,iBAAiB,SAAS,GAAG,OAAO;CACzC,OAAQ,MAAM,MAAM,WAAW,UAAU,KAAM;AACjD"}
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_store_facts = require("./facts.cjs");
3
3
  const require_store_erasure = require("./erasure.cjs");
4
+ const require_store_identity = require("./identity.cjs");
4
5
  const require_store_postgres_reporting = require("./postgres-reporting.cjs");
5
6
  const require_store_tables = require("./tables.cjs");
6
7
  const require_store_postgres = require("./postgres.cjs");
@@ -24,7 +25,11 @@ exports.PostgresReporting = require_store_postgres_reporting.PostgresReporting;
24
25
  exports.SchemaMismatchError = SchemaMismatchError;
25
26
  exports.TABLES = require_store_tables.TABLES;
26
27
  exports.UserErasedError = require_store_erasure.UserErasedError;
28
+ exports.instanceOwner = require_store_identity.instanceOwner;
29
+ exports.isVisitorSubject = require_store_identity.isVisitorSubject;
27
30
  exports.renderDrizzleSchema = require_store_tables.renderDrizzleSchema;
28
31
  exports.renderPostgresDdl = require_store_tables.renderPostgresDdl;
32
+ exports.visitorIdOf = require_store_identity.visitorIdOf;
33
+ exports.visitorSubject = require_store_identity.visitorSubject;
29
34
 
30
35
  //# sourceMappingURL=index.cjs.map