@shware/workflow 1.0.0 → 2.0.1

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 (148) hide show
  1. package/README.md +42 -11
  2. package/dist/client.d.cts +1 -1
  3. package/dist/client.d.mts +1 -1
  4. package/dist/cloudflare/bindings.cjs.map +1 -1
  5. package/dist/cloudflare/bindings.d.cts +1 -1
  6. package/dist/cloudflare/bindings.d.mts +1 -1
  7. package/dist/cloudflare/bindings.mjs.map +1 -1
  8. package/dist/cloudflare/channels.cjs +1 -4
  9. package/dist/cloudflare/channels.cjs.map +1 -1
  10. package/dist/cloudflare/channels.d.cts +3 -3
  11. package/dist/cloudflare/channels.d.mts +3 -3
  12. package/dist/cloudflare/channels.mjs +1 -4
  13. package/dist/cloudflare/channels.mjs.map +1 -1
  14. package/dist/cloudflare/index.d.cts +4 -4
  15. package/dist/cloudflare/index.d.mts +4 -4
  16. package/dist/cloudflare/router.d.cts +1 -1
  17. package/dist/cloudflare/router.d.mts +1 -1
  18. package/dist/cloudflare/runner.d.cts +2 -2
  19. package/dist/cloudflare/runner.d.mts +2 -2
  20. package/dist/cloudflare/senders.cjs +15 -8
  21. package/dist/cloudflare/senders.cjs.map +1 -1
  22. package/dist/cloudflare/senders.d.cts +15 -11
  23. package/dist/cloudflare/senders.d.cts.map +1 -1
  24. package/dist/cloudflare/senders.d.mts +15 -11
  25. package/dist/cloudflare/senders.d.mts.map +1 -1
  26. package/dist/cloudflare/senders.mjs +15 -8
  27. package/dist/cloudflare/senders.mjs.map +1 -1
  28. package/dist/cloudflare/worker.cjs +7 -4
  29. package/dist/cloudflare/worker.cjs.map +1 -1
  30. package/dist/cloudflare/worker.d.cts +2 -2
  31. package/dist/cloudflare/worker.d.cts.map +1 -1
  32. package/dist/cloudflare/worker.d.mts +2 -2
  33. package/dist/cloudflare/worker.d.mts.map +1 -1
  34. package/dist/cloudflare/worker.mjs +8 -5
  35. package/dist/cloudflare/worker.mjs.map +1 -1
  36. package/dist/config.cjs +10 -11
  37. package/dist/config.cjs.map +1 -1
  38. package/dist/config.d.cts +10 -33
  39. package/dist/config.d.cts.map +1 -1
  40. package/dist/config.d.mts +10 -33
  41. package/dist/config.d.mts.map +1 -1
  42. package/dist/config.mjs +11 -11
  43. package/dist/config.mjs.map +1 -1
  44. package/dist/contract.cjs.map +1 -1
  45. package/dist/contract.d.cts +1 -1
  46. package/dist/contract.d.cts.map +1 -1
  47. package/dist/contract.d.mts +1 -1
  48. package/dist/contract.d.mts.map +1 -1
  49. package/dist/contract.mjs.map +1 -1
  50. package/dist/dsl/action.cjs.map +1 -1
  51. package/dist/dsl/action.d.cts +2 -2
  52. package/dist/dsl/action.d.cts.map +1 -1
  53. package/dist/dsl/action.d.mts +2 -2
  54. package/dist/dsl/action.d.mts.map +1 -1
  55. package/dist/dsl/action.mjs.map +1 -1
  56. package/dist/dsl/bundle.cjs.map +1 -1
  57. package/dist/dsl/bundle.d.cts +1 -1
  58. package/dist/dsl/bundle.d.mts +1 -1
  59. package/dist/dsl/bundle.mjs.map +1 -1
  60. package/dist/dsl/flow.cjs.map +1 -1
  61. package/dist/dsl/flow.d.cts +1 -1
  62. package/dist/dsl/flow.d.cts.map +1 -1
  63. package/dist/dsl/flow.d.mts +1 -1
  64. package/dist/dsl/flow.d.mts.map +1 -1
  65. package/dist/dsl/flow.mjs.map +1 -1
  66. package/dist/dsl/refs.cjs.map +1 -1
  67. package/dist/dsl/refs.d.cts +5 -4
  68. package/dist/dsl/refs.d.cts.map +1 -1
  69. package/dist/dsl/refs.d.mts +5 -4
  70. package/dist/dsl/refs.d.mts.map +1 -1
  71. package/dist/dsl/refs.mjs.map +1 -1
  72. package/dist/dsl/template.cjs.map +1 -1
  73. package/dist/dsl/template.d.cts +2 -2
  74. package/dist/dsl/template.d.cts.map +1 -1
  75. package/dist/dsl/template.d.mts +2 -2
  76. package/dist/dsl/template.d.mts.map +1 -1
  77. package/dist/dsl/template.mjs.map +1 -1
  78. package/dist/email-context.cjs +29 -0
  79. package/dist/email-context.cjs.map +1 -0
  80. package/dist/email-context.d.cts +11 -0
  81. package/dist/email-context.d.cts.map +1 -0
  82. package/dist/email-context.d.mts +11 -0
  83. package/dist/email-context.d.mts.map +1 -0
  84. package/dist/email-context.mjs +27 -0
  85. package/dist/email-context.mjs.map +1 -0
  86. package/dist/email.cjs +72 -0
  87. package/dist/email.cjs.map +1 -0
  88. package/dist/email.d.cts +63 -0
  89. package/dist/email.d.cts.map +1 -0
  90. package/dist/email.d.mts +63 -0
  91. package/dist/email.d.mts.map +1 -0
  92. package/dist/email.mjs +65 -0
  93. package/dist/email.mjs.map +1 -0
  94. package/dist/engine/index.d.cts +2 -2
  95. package/dist/engine/index.d.mts +2 -2
  96. package/dist/engine/interpreter.cjs.map +1 -1
  97. package/dist/engine/interpreter.d.cts.map +1 -1
  98. package/dist/engine/interpreter.d.mts.map +1 -1
  99. package/dist/engine/interpreter.mjs.map +1 -1
  100. package/dist/engine/ports.d.cts +7 -2
  101. package/dist/engine/ports.d.cts.map +1 -1
  102. package/dist/engine/ports.d.mts +7 -2
  103. package/dist/engine/ports.d.mts.map +1 -1
  104. package/dist/index.cjs +8 -1
  105. package/dist/index.d.cts +4 -3
  106. package/dist/index.d.mts +4 -3
  107. package/dist/index.mjs +3 -2
  108. package/dist/react-email.cjs +11 -3
  109. package/dist/react-email.cjs.map +1 -1
  110. package/dist/react-email.d.cts +2 -1
  111. package/dist/react-email.d.cts.map +1 -1
  112. package/dist/react-email.d.mts +2 -1
  113. package/dist/react-email.d.mts.map +1 -1
  114. package/dist/react-email.mjs +11 -3
  115. package/dist/react-email.mjs.map +1 -1
  116. package/dist/reporting/classify.cjs.map +1 -1
  117. package/dist/reporting/classify.d.cts +1 -1
  118. package/dist/reporting/classify.d.mts +1 -1
  119. package/dist/reporting/classify.mjs.map +1 -1
  120. package/dist/reporting/observer.cjs +5 -4
  121. package/dist/reporting/observer.cjs.map +1 -1
  122. package/dist/reporting/observer.d.cts +3 -2
  123. package/dist/reporting/observer.d.cts.map +1 -1
  124. package/dist/reporting/observer.d.mts +3 -2
  125. package/dist/reporting/observer.d.mts.map +1 -1
  126. package/dist/reporting/observer.mjs +5 -4
  127. package/dist/reporting/observer.mjs.map +1 -1
  128. package/dist/reporting/tracking.cjs +8 -13
  129. package/dist/reporting/tracking.cjs.map +1 -1
  130. package/dist/reporting/tracking.d.cts +18 -7
  131. package/dist/reporting/tracking.d.cts.map +1 -1
  132. package/dist/reporting/tracking.d.mts +18 -7
  133. package/dist/reporting/tracking.d.mts.map +1 -1
  134. package/dist/reporting/tracking.mjs +8 -13
  135. package/dist/reporting/tracking.mjs.map +1 -1
  136. package/dist/store/postgres-reporting.cjs +5 -5
  137. package/dist/store/postgres-reporting.cjs.map +1 -1
  138. package/dist/store/postgres-reporting.mjs +5 -5
  139. package/dist/store/postgres-reporting.mjs.map +1 -1
  140. package/dist/templates.cjs +6 -1
  141. package/dist/templates.cjs.map +1 -1
  142. package/dist/templates.d.cts +21 -2
  143. package/dist/templates.d.cts.map +1 -1
  144. package/dist/templates.d.mts +21 -2
  145. package/dist/templates.d.mts.map +1 -1
  146. package/dist/templates.mjs +6 -1
  147. package/dist/templates.mjs.map +1 -1
  148. package/package.json +11 -1
@@ -1 +1 @@
1
- {"version":3,"file":"interpreter.cjs","names":["evaluateCondition","RECIPIENT_PROPERTY","nextWindowStart","hashToBucket","relevantEvents"],"sources":["../../src/engine/interpreter.ts"],"sourcesContent":["import type { NodeIR, PropValueIR, ScalarIR, WorkflowIR } from '../ir';\nimport { hashToBucket } from './bucket';\nimport { evaluateCondition, relevantEvents } from './condition';\nimport type { FactSource, JourneyContext, JourneyOutcome, OutboundMessage } from './ports';\nimport { RECIPIENT_PROPERTY } from './recipient';\nimport { nextWindowStart } from './time-window';\n\n/**\n * IR interpreter: the execution core for one user's journey instance.\n *\n * Pure logic — it imports nothing platform-specific; every external effect\n * arrives through the JourneyContext ports, and durable semantics\n * (checkpoint/replay) are the EngineStep adapter's job.\n *\n * Step naming: an IR node id *is* the step name (derived from the structural\n * path, so it is deterministic and unique). When one node needs several steps,\n * a suffix is appended (`${id}:guard`, `${id}:check:${n}`).\n *\n * Guard semantics: goal / exitWhen are checked at journey entry and after\n * every suspension point (delay / random_delay / time_window / wait_until) —\n * the moments where wall-clock time passed and the user's state may have\n * moved. Between two synchronous nodes no meaningful time passes, so checking\n * there would only burn step budget (each checkpoint costs a state write).\n */\n\n/**\n * Cap on wait_until re-evaluations: beyond it we treat the wait as timed out,\n * so a wake-up storm cannot exhaust the platform's step budget. Each loop\n * iteration costs a handful of steps, so 64 checks stay well inside the\n * platform step budgets while absorbing far more wake churn than 16 did.\n */\nconst MAX_WAIT_CHECKS = 64;\n\n/** The user property holding an IANA timezone id, read when a time_window says tz: 'user'. Same convention-table status as RECIPIENT_PROPERTY. */\nconst TIMEZONE_PROPERTY = 'timezone';\n\ntype FlowSignal =\n | { kind: 'fall_through' }\n | { kind: 'exit'; reason?: string }\n | { kind: 'goal' }\n | { kind: 'exit_when' };\n\nconst FALL_THROUGH: FlowSignal = { kind: 'fall_through' };\n\n/**\n * Whether and when the instance converted — a fact of its own, apart from how it ends: with\n * `exitOnMatch: false` a conversion lets the flow go on, and the instance still ends `completed`\n * (or `exited`, `exit_when`) carrying it. Set only from checkpointed step results, so a replay\n * rebuilds the same state.\n */\ninterface GoalState {\n convertedAt?: number;\n}\n\nconst goalStates = new WeakMap<JourneyContext, GoalState>();\n\nfunction goalState(ctx: JourneyContext): GoalState {\n let state = goalStates.get(ctx);\n if (state === undefined) {\n state = {};\n goalStates.set(ctx, state);\n }\n return state;\n}\n\n/** When the goal's attribution window closes, measured from entry; none without `within`. */\nfunction goalDeadline(ir: WorkflowIR, ctx: JourneyContext): number | undefined {\n return ir.goal?.within === undefined ? undefined : ctx.enteredAtMs + ir.goal.within.ms;\n}\n\n/**\n * Whether the goal holds for events inside its window: after entry, and not after the window\n * closed — an instance resumed past the deadline must not count a purchase made after it. The\n * conversion time is when the check saw it, capped at the deadline.\n */\nasync function goalReached(\n ir: WorkflowIR,\n ctx: JourneyContext,\n now: number\n): Promise<{ at: number } | null> {\n const goal = ir.goal;\n if (goal === undefined) return null;\n const deadline = goalDeadline(ir, ctx);\n const met = await evaluateCondition(goal.condition, ctx.facts, now, {\n anchorMs: ctx.enteredAtMs,\n ...(deadline === undefined ? {} : { untilMs: deadline }),\n });\n return met ? { at: deadline === undefined ? now : Math.min(now, deadline) } : null;\n}\n\nexport async function runJourney(ir: WorkflowIR, ctx: JourneyContext): Promise<JourneyOutcome> {\n await started(ir, ctx);\n const entry = await checkGuards('entry', ir, ctx);\n let signal = entry ?? (await runFlow(ir.flow, ir, ctx));\n // The flow is done but the conversion window is not: keep watching it (see awaitTail)\n if (signal.kind === 'fall_through') signal = (await awaitTail(ir, ctx)) ?? FALL_THROUGH;\n const { convertedAt } = goalState(ctx);\n const converted = convertedAt === undefined ? {} : { convertedAt };\n switch (signal.kind) {\n case 'fall_through':\n return { status: 'completed', ...converted };\n case 'exit':\n return signal.reason !== undefined\n ? { status: 'exited', reason: signal.reason, ...converted }\n : { status: 'exited', ...converted };\n case 'goal':\n return { status: 'goal', ...converted };\n case 'exit_when':\n return { status: 'exit_when', ...converted };\n }\n}\n\nasync function runFlow(nodes: NodeIR[], ir: WorkflowIR, ctx: JourneyContext): Promise<FlowSignal> {\n for (const node of nodes) {\n await reached(node, ir, ctx);\n const signal = await runNode(node, ir, ctx);\n if (signal.kind !== 'fall_through') return signal;\n }\n return FALL_THROUGH;\n}\n\n/**\n * Tell the observer the instance entered — before the entry guards, which can end it before the\n * first node: an instance is counted from its entry, not from its first node.\n */\nasync function started(ir: WorkflowIR, ctx: JourneyContext): Promise<void> {\n const observer = ctx.observer;\n if (observer?.started === undefined) return;\n await ctx.step.do('entry:started', async () => {\n await observer.started?.({\n workflow: ir.id,\n instanceId: ctx.instanceId,\n userId: ctx.userId,\n at: ctx.enteredAtMs,\n });\n });\n}\n\n/**\n * Tell the observer the instance is at `node`. A step of its own, so a replay does not report\n * the nodes it passes on the way back again, and the arrival time stays the first one.\n */\nasync function reached(node: NodeIR, ir: WorkflowIR, ctx: JourneyContext): Promise<void> {\n const observer = ctx.observer;\n if (observer?.nodeReached === undefined) return;\n await ctx.step.do(`${node.id}:reached`, async () => {\n await observer.nodeReached?.({\n workflow: ir.id,\n instanceId: ctx.instanceId,\n userId: ctx.userId,\n nodeId: node.id,\n at: Date.now(),\n });\n });\n}\n\n/**\n * One copy out through the host's sender, with the observer on both sides: it may attach\n * tracking before, and hears how the transport took the copy after. A send that throws is\n * not reported — the step retries it, and reports when it settles. A report that fails after\n * the send is logged, never thrown: the step would retry and the transport — a webhook, a web\n * push, with no idempotency key of its own — would deliver the copy again.\n */\nasync function deliver(message: OutboundMessage, ctx: JourneyContext): Promise<void> {\n const observer = ctx.observer;\n const tracking = await observer?.messagePrepared?.(message);\n const copy = tracking === undefined ? message : { ...message, tracking };\n const result = await ctx.messages.send(copy);\n try {\n await observer?.messageSent?.(copy, result ?? { delivered: true });\n } catch (error) {\n console.error(`[workflow] reporting ${copy.idempotencyKey} as sent failed`, error);\n }\n}\n\n/**\n * Goal / exitWhen guards (see the header for when they run).\n *\n * The goal condition is anchored at workflow entry: a `performed` inside it\n * only counts events after the user entered, so pre-existing history cannot\n * register as a conversion. exitWhen is deliberately unanchored — \"user\n * unsubscribed\" should exit no matter when it happened.\n */\nasync function checkGuards(\n stepId: string,\n ir: WorkflowIR,\n ctx: JourneyContext\n): Promise<FlowSignal | null> {\n const goal = ir.goal;\n const exitWhen = ir.exitWhen;\n if (goal === undefined && exitWhen === undefined) return null;\n\n const state = goalState(ctx);\n const result = await ctx.step.do(`${stepId}:guard`, async () => {\n const now = Date.now();\n // Already converted (exitOnMatch: false): the conversion stands, no need to look again\n const goalHit =\n goal !== undefined && state.convertedAt === undefined\n ? await goalReached(ir, ctx, now)\n : null;\n const exitMet = exitWhen !== undefined && (await evaluateCondition(exitWhen, ctx.facts, now));\n return { goalAt: goalHit?.at ?? null, exitMet };\n });\n\n // A checkpoint from before conversions carried a time held `goalMet` instead: met at entry's anchor\n const legacy = result as { goalMet?: boolean };\n const goalAt =\n typeof result.goalAt === 'number'\n ? result.goalAt\n : legacy.goalMet === true\n ? ctx.enteredAtMs\n : null;\n if (goalAt !== null) {\n state.convertedAt ??= goalAt;\n if (goal?.exitOnMatch ?? true) return { kind: 'goal' };\n }\n if (result.exitMet) return { kind: 'exit_when' };\n return null;\n}\n\n/**\n * The host's template check, with the instance and node added to the message:\n * a refused message must point at the journey that produced it. The error\n * keeps its class (and `name`), so adapters can stop retrying it.\n */\nfunction validate(\n node: Extract<NodeIR, { type: 'message' }>,\n props: Record<string, ScalarIR | undefined>,\n ctx: JourneyContext\n): Record<string, ScalarIR | undefined> {\n if (ctx.validator === undefined) return props;\n try {\n return ctx.validator.validate({ channel: node.channel, template: node.template, props });\n } catch (error) {\n if (error instanceof Error) {\n error.message = `[${ctx.instanceId} ${node.id}] ${error.message}`;\n }\n throw error;\n }\n}\n\n/**\n * A push goes to devices, not to a profile address: one copy per device the\n * host's directory lists (or only the most recent, `devices: 'latest'`). Each\n * copy's idempotency key carries its token, so a retried step that already\n * reached some devices re-sends to the rest and the senders drop the repeats —\n * keyed by token rather than position, because the list can change between\n * attempts. A user with no device is skipped, not failed: being unreachable is\n * a state of the user, and `reachable('push')` is how a journey branches on it.\n */\n/**\n * A push: one step settles what goes out and to which devices, then each device gets a step of\n * its own — a failure on one device retries that device alone, and the copies already delivered\n * are not sent again (APNs and FCM carry the idempotency key but do not de-duplicate on it).\n */\nasync function sendPush(\n node: Extract<NodeIR, { type: 'message' }>,\n ir: WorkflowIR,\n ctx: JourneyContext\n): Promise<void> {\n const plan = await ctx.step.do(`${node.id}:send`, async () => {\n const props = validate(node, await resolveValues(node.props, ctx.facts), ctx);\n if (await ctx.facts.isSuppressed?.(node.channel)) return null;\n const listed = await ctx.facts.listDevices();\n const devices = node.devices === 'latest' ? listed.slice(0, 1) : listed;\n return { props, devices };\n });\n // null: the channel is opted out. Undefined: a checkpoint from before per-device steps, whose\n // single step already sent to every device.\n if (plan === null || plan === undefined) return;\n if (plan.devices.length === 0) {\n console.log(`[journey] push ${node.template} skipped: no device for user ${ctx.userId}`);\n return;\n }\n for (const [index, device] of plan.devices.entries()) {\n await ctx.step.do(`${node.id}:send:${index}`, async () => {\n await deliver(\n {\n workflow: ir.id,\n instanceId: ctx.instanceId,\n nodeId: node.id,\n channel: 'push',\n template: node.template,\n props: plan.props,\n userId: ctx.userId,\n recipient: device.token,\n device,\n idempotencyKey: `${ctx.instanceId}:${node.id}:${device.token}`,\n },\n ctx\n );\n });\n }\n}\n\nasync function runNode(node: NodeIR, ir: WorkflowIR, ctx: JourneyContext): Promise<FlowSignal> {\n switch (node.type) {\n case 'message': {\n if (node.channel === 'push') {\n await sendPush(node, ir, ctx);\n return FALL_THROUGH;\n }\n await ctx.step.do(`${node.id}:send`, async () => {\n const props = validate(node, await resolveValues(node.props, ctx.facts), ctx);\n // An opted-out channel (an email unsubscribe) is skipped, like a push with no device\n if (await ctx.facts.isSuppressed?.(node.channel)) return;\n const recipientPath = RECIPIENT_PROPERTY[node.channel];\n const recipient =\n recipientPath === undefined ? undefined : await ctx.facts.getProperty(recipientPath);\n await deliver(\n {\n workflow: ir.id,\n instanceId: ctx.instanceId,\n nodeId: node.id,\n channel: node.channel,\n template: node.template,\n props,\n userId: ctx.userId,\n recipient: recipient === undefined ? undefined : String(recipient),\n idempotencyKey: `${ctx.instanceId}:${node.id}`,\n },\n ctx\n );\n });\n return FALL_THROUGH;\n }\n\n case 'delay': {\n await ctx.step.sleep(node.id, node.duration.ms);\n return (await checkGuards(node.id, ir, ctx)) ?? FALL_THROUGH;\n }\n\n case 'random_delay': {\n // The random value is drawn and persisted inside the step, so replay does not re-roll it\n const ms = await ctx.step.do(`${node.id}:roll`, async () => {\n const span = node.max.ms - node.min.ms;\n return node.min.ms + Math.floor(Math.random() * Math.max(span, 0));\n });\n await ctx.step.sleep(`${node.id}:sleep`, ms);\n return (await checkGuards(node.id, ir, ctx)) ?? FALL_THROUGH;\n }\n\n case 'time_window': {\n const target = await ctx.step.do(`${node.id}:target`, async () => {\n const tz =\n node.tz === 'user'\n ? String((await ctx.facts.getProperty(TIMEZONE_PROPERTY)) ?? 'UTC')\n : node.tz;\n return nextWindowStart(Date.now(), node.days, node.between, tz);\n });\n if (target === null) return FALL_THROUGH; // already inside the window: no suspension, no guard\n await ctx.step.sleepUntil(`${node.id}:sleep`, target);\n return (await checkGuards(node.id, ir, ctx)) ?? FALL_THROUGH;\n }\n\n case 'wait_until': {\n const met = await waitUntil(node, ctx);\n // Time passed while waiting — guard before acting on the outcome (goal wins over the wait result)\n const guard = await checkGuards(node.id, ir, ctx);\n if (guard) return guard;\n if (met) return FALL_THROUGH;\n const onTimeout = node.onTimeout;\n if (onTimeout === 'continue') return FALL_THROUGH;\n if (onTimeout === 'exit') return { kind: 'exit', reason: `${node.id}:timeout` };\n return runFlow(onTimeout, ir, ctx);\n }\n\n case 'branch': {\n const matched = await ctx.step.do(`${node.id}:eval`, async () => {\n const now = Date.now();\n for (const [i, branchCase] of node.cases.entries()) {\n if (await evaluateCondition(branchCase.condition, ctx.facts, now)) {\n return i;\n }\n }\n return -1; // no match → default arm, or simply continue\n });\n const flow = matched >= 0 ? node.cases[matched]?.flow : node.otherwise;\n if (flow === undefined) return FALL_THROUGH;\n const signal = await runFlow(flow, ir, ctx);\n // Structural rejoin: once an arm falls through, execution resumes after the branch\n return signal;\n }\n\n case 'filter': {\n const pass = await ctx.step.do(`${node.id}:eval`, async () =>\n evaluateCondition(node.condition, ctx.facts, Date.now())\n );\n return pass ? FALL_THROUGH : { kind: 'exit', reason: node.reason ?? `${node.id}:filtered` };\n }\n\n case 'cohort': {\n // Deterministic bucketing, so replay needs no checkpoint. The explicit\n // experiment key (when set) keeps the assignment stable across workflow\n // versions; the structural id fallback is fine for one-off splits.\n const bucket = hashToBucket(`${ctx.userId}:${node.key ?? node.id}`);\n // Integer basis points on both sides (the DSL guarantees weights sit on\n // the 0.01% grid), so no float ever meets the comparison.\n let cumulative = 0;\n for (const arm of node.arms) {\n cumulative += Math.round(arm.weight * 100);\n if (bucket < cumulative) return runFlow(arm.flow, ir, ctx);\n }\n return FALL_THROUGH;\n }\n\n case 'exit':\n return node.reason !== undefined ? { kind: 'exit', reason: node.reason } : { kind: 'exit' };\n\n case 'send_event': {\n await ctx.step.do(`${node.id}:emit`, async () => {\n const payload = await resolveValues(node.payload, ctx.facts);\n try {\n await ctx.events.emit(node.event, payload, `${ctx.instanceId}:${node.id}`);\n } catch (error) {\n // A refused event must point at the journey that emitted it\n if (error instanceof Error)\n error.message = `[${ctx.instanceId} ${node.id}] ${error.message}`;\n throw error;\n }\n });\n return FALL_THROUGH;\n }\n\n case 'action': {\n // Custom code runs inside step.do: retried on throw, checkpointed on success\n const invoker = ctx.actions;\n if (invoker === undefined) {\n throw new Error(\n `action '${node.action}': this runtime has no ActionInvoker configured — register the action on the adapter (see engine/actions.ts)`\n );\n }\n await ctx.step.do(`${node.id}:run`, async () => {\n const args = await resolveValues(node.args, ctx.facts);\n await invoker.invoke({\n action: node.action,\n codeHash: node.codeHash,\n args,\n userId: ctx.userId,\n idempotencyKey: `${ctx.instanceId}:${node.id}`,\n });\n });\n return FALL_THROUGH;\n }\n }\n}\n\nasync function waitUntil(\n node: Extract<NodeIR, { type: 'wait_until' }>,\n ctx: JourneyContext\n): Promise<boolean> {\n const deadline = await ctx.step.do(\n `${node.id}:deadline`,\n async () => Date.now() + node.timeout.ms\n );\n const events = await ctx.step.do(`${node.id}:events`, async () =>\n relevantEvents(node.condition, ctx.facts)\n );\n // Anchor at the moment the wait began (deadline is checkpointed, so this is\n // replay-stable): `performed` inside the condition only counts events that\n // happen during the wait — pre-existing history must not satisfy it.\n const anchorMs = deadline - node.timeout.ms;\n const met = await waitLoop(node.id, ctx, deadline, events, async (now) =>\n (await evaluateCondition(node.condition, ctx.facts, now, { anchorMs })) ? true : null\n );\n await ctx.step.unsubscribe(`${node.id}:unsubscribe`);\n return met === true;\n}\n\n/**\n * The conversion window outlives the flow: a journey whose steps are done while its goal's\n * `within` is still open keeps watching until the window closes — the purchase that follows the\n * last offer email is that email's conversion, and ending the instance at the email would report\n * it as a plain completion. `exitWhen` still applies while it waits. Nothing to wait for without\n * a window, or once the instance has converted.\n */\nasync function awaitTail(ir: WorkflowIR, ctx: JourneyContext): Promise<FlowSignal | null> {\n const deadline = goalDeadline(ir, ctx);\n const goal = ir.goal;\n if (goal === undefined || deadline === undefined) return null;\n const state = goalState(ctx);\n if (state.convertedAt !== undefined) return null;\n const open = await ctx.step.do('goal:open', async () => deadline > Date.now());\n if (!open) return null;\n const exitWhen = ir.exitWhen;\n const events = await ctx.step.do('goal:events', async () => [\n ...new Set([\n ...(await relevantEvents(goal.condition, ctx.facts)),\n ...(exitWhen === undefined ? [] : await relevantEvents(exitWhen, ctx.facts)),\n ]),\n ]);\n const hit = await waitLoop('goal', ctx, deadline, events, async (now) => {\n const reached = await goalReached(ir, ctx, now);\n if (reached !== null) return { kind: 'goal' as const, at: reached.at };\n if (exitWhen !== undefined && (await evaluateCondition(exitWhen, ctx.facts, now))) {\n return { kind: 'exit_when' as const, at: now };\n }\n return null;\n });\n await ctx.step.unsubscribe('goal:unsubscribe');\n if (hit === null) return null;\n // A checkpoint from before the tail watched exitWhen recorded a plain `true`: the goal\n if ((hit as unknown) === true || hit.kind === 'goal') {\n state.convertedAt ??= (hit as { at?: number }).at ?? deadline;\n return { kind: 'goal' };\n }\n return { kind: 'exit_when' };\n}\n\n/**\n * Wait until `check` finds something or the deadline passes: subscribe, check, sleep until a\n * relevant event wakes the instance or time runs out. `check` runs inside a step, so its result\n * must be serializable; null means \"not yet\".\n */\nasync function waitLoop<R>(\n id: string,\n ctx: JourneyContext,\n deadline: number,\n events: string[],\n check: (now: number) => Promise<R | null>\n): Promise<R | null> {\n for (let attempt = 0; attempt < MAX_WAIT_CHECKS; attempt++) {\n // Subscribe before the check, every attempt: one subscribe arms at least\n // one wake-up (AWS callbacks are consumed on delivery, so each attempt\n // needs a fresh arm; Cloudflare rows make the repeat call idempotent), and\n // arming first closes the race where the event lands between the check and\n // the registration.\n await ctx.step.subscribe(`${id}:subscribe:${attempt}`, events);\n\n const met = await ctx.step.do(`${id}:check:${attempt}`, async () => check(Date.now()));\n // `false` is a checkpoint from before checks answered null for \"not yet\"\n if (met !== null && (met as unknown) !== false) return met;\n\n // Checkpointed: replay re-runs this control flow with wall-clock time far\n // ahead of the original run, so the branch must come from a persisted value.\n const remaining = await ctx.step.do(\n `${id}:remaining:${attempt}`,\n async () => deadline - Date.now()\n );\n // The condition was checked just above — expiring here needs no extra final check\n if (remaining <= 0) return null;\n\n const wake = await ctx.step.waitForWake(`${id}:wait:${attempt}`, remaining);\n if (wake === 'timeout') {\n // Final check after timeout: in a tight race the event may land exactly as the timer fires\n const final = await ctx.step.do(`${id}:final`, async () => check(Date.now()));\n return (final as unknown) === false ? null : final;\n }\n // Woken → back to the top of the loop to re-evaluate (at-least-once wake-up semantics)\n }\n return null; // check budget exhausted → treated as timed out (see MAX_WAIT_CHECKS)\n}\n\nasync function resolveValues(\n values: Record<string, PropValueIR>,\n facts: FactSource\n): Promise<Record<string, ScalarIR | undefined>> {\n const resolved: Record<string, ScalarIR | undefined> = {};\n for (const [key, value] of Object.entries(values)) {\n resolved[key] = typeof value === 'object' ? await facts.getProperty(value.path) : value;\n }\n return resolved;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"interpreter.cjs","names":["evaluateCondition","RECIPIENT_PROPERTY","nextWindowStart","hashToBucket","relevantEvents"],"sources":["../../src/engine/interpreter.ts"],"sourcesContent":["import type { NodeIR, PropValueIR, ScalarIR, WorkflowIR } from '../ir';\nimport { hashToBucket } from './bucket';\nimport { evaluateCondition, relevantEvents } from './condition';\nimport type {\n FactSource,\n JourneyContext,\n JourneyOutcome,\n OutboundMessage,\n PushDevice,\n} from './ports';\nimport { RECIPIENT_PROPERTY } from './recipient';\nimport { nextWindowStart } from './time-window';\n\n/**\n * IR interpreter: the execution core for one user's journey instance.\n *\n * Pure logic — it imports nothing platform-specific; every external effect\n * arrives through the JourneyContext ports, and durable semantics\n * (checkpoint/replay) are the EngineStep adapter's job.\n *\n * Step naming: an IR node id *is* the step name (derived from the structural\n * path, so it is deterministic and unique). When one node needs several steps,\n * a suffix is appended (`${id}:guard`, `${id}:check:${n}`).\n *\n * Guard semantics: goal / exitWhen are checked at journey entry and after\n * every suspension point (delay / random_delay / time_window / wait_until) —\n * the moments where wall-clock time passed and the user's state may have\n * moved. Between two synchronous nodes no meaningful time passes, so checking\n * there would only burn step budget (each checkpoint costs a state write).\n */\n\n/**\n * Cap on wait_until re-evaluations: beyond it we treat the wait as timed out,\n * so a wake-up storm cannot exhaust the platform's step budget. Each loop\n * iteration costs a handful of steps, so 64 checks stay well inside the\n * platform step budgets while absorbing far more wake churn than 16 did.\n */\nconst MAX_WAIT_CHECKS = 64;\n\n/** The user property holding an IANA timezone id, read when a time_window says tz: 'user'. Same convention-table status as RECIPIENT_PROPERTY. */\nconst TIMEZONE_PROPERTY = 'timezone';\n\ntype FlowSignal =\n | { kind: 'fall_through' }\n | { kind: 'exit'; reason?: string }\n | { kind: 'goal' }\n | { kind: 'exit_when' };\n\nconst FALL_THROUGH: FlowSignal = { kind: 'fall_through' };\n\n/**\n * Whether and when the instance converted — a fact of its own, apart from how it ends: with\n * `exitOnMatch: false` a conversion lets the flow go on, and the instance still ends `completed`\n * (or `exited`, `exit_when`) carrying it. Set only from checkpointed step results, so a replay\n * rebuilds the same state.\n */\ninterface GoalState {\n convertedAt?: number;\n}\n\nconst goalStates = new WeakMap<JourneyContext, GoalState>();\n\nfunction goalState(ctx: JourneyContext): GoalState {\n let state = goalStates.get(ctx);\n if (state === undefined) {\n state = {};\n goalStates.set(ctx, state);\n }\n return state;\n}\n\n/** When the goal's attribution window closes, measured from entry; none without `within`. */\nfunction goalDeadline(ir: WorkflowIR, ctx: JourneyContext): number | undefined {\n return ir.goal?.within === undefined ? undefined : ctx.enteredAtMs + ir.goal.within.ms;\n}\n\n/**\n * Whether the goal holds for events inside its window: after entry, and not after the window\n * closed — an instance resumed past the deadline must not count a purchase made after it. The\n * conversion time is when the check saw it, capped at the deadline.\n */\nasync function goalReached(\n ir: WorkflowIR,\n ctx: JourneyContext,\n now: number\n): Promise<{ at: number } | null> {\n const goal = ir.goal;\n if (goal === undefined) return null;\n const deadline = goalDeadline(ir, ctx);\n const met = await evaluateCondition(goal.condition, ctx.facts, now, {\n anchorMs: ctx.enteredAtMs,\n ...(deadline === undefined ? {} : { untilMs: deadline }),\n });\n return met ? { at: deadline === undefined ? now : Math.min(now, deadline) } : null;\n}\n\nexport async function runJourney(ir: WorkflowIR, ctx: JourneyContext): Promise<JourneyOutcome> {\n await started(ir, ctx);\n const entry = await checkGuards('entry', ir, ctx);\n let signal = entry ?? (await runFlow(ir.flow, ir, ctx));\n // The flow is done but the conversion window is not: keep watching it (see awaitTail)\n if (signal.kind === 'fall_through') signal = (await awaitTail(ir, ctx)) ?? FALL_THROUGH;\n const { convertedAt } = goalState(ctx);\n const converted = convertedAt === undefined ? {} : { convertedAt };\n switch (signal.kind) {\n case 'fall_through':\n return { status: 'completed', ...converted };\n case 'exit':\n return signal.reason !== undefined\n ? { status: 'exited', reason: signal.reason, ...converted }\n : { status: 'exited', ...converted };\n case 'goal':\n return { status: 'goal', ...converted };\n case 'exit_when':\n return { status: 'exit_when', ...converted };\n }\n}\n\nasync function runFlow(nodes: NodeIR[], ir: WorkflowIR, ctx: JourneyContext): Promise<FlowSignal> {\n for (const node of nodes) {\n await reached(node, ir, ctx);\n const signal = await runNode(node, ir, ctx);\n if (signal.kind !== 'fall_through') return signal;\n }\n return FALL_THROUGH;\n}\n\n/**\n * Tell the observer the instance entered — before the entry guards, which can end it before the\n * first node: an instance is counted from its entry, not from its first node.\n */\nasync function started(ir: WorkflowIR, ctx: JourneyContext): Promise<void> {\n const observer = ctx.observer;\n if (observer?.started === undefined) return;\n await ctx.step.do('entry:started', async () => {\n await observer.started?.({\n workflow: ir.id,\n instanceId: ctx.instanceId,\n userId: ctx.userId,\n at: ctx.enteredAtMs,\n });\n });\n}\n\n/**\n * Tell the observer the instance is at `node`. A step of its own, so a replay does not report\n * the nodes it passes on the way back again, and the arrival time stays the first one.\n */\nasync function reached(node: NodeIR, ir: WorkflowIR, ctx: JourneyContext): Promise<void> {\n const observer = ctx.observer;\n if (observer?.nodeReached === undefined) return;\n await ctx.step.do(`${node.id}:reached`, async () => {\n await observer.nodeReached?.({\n workflow: ir.id,\n instanceId: ctx.instanceId,\n userId: ctx.userId,\n nodeId: node.id,\n at: Date.now(),\n });\n });\n}\n\n/**\n * One copy out through the host's sender, with the observer on both sides: it may attach\n * tracking before, and hears how the transport took the copy after. A send that throws is\n * not reported — the step retries it, and reports when it settles. A report that fails after\n * the send is logged, never thrown: the step would retry and the transport — a webhook, a web\n * push, with no idempotency key of its own — would deliver the copy again.\n */\nasync function deliver(message: OutboundMessage, ctx: JourneyContext): Promise<void> {\n const observer = ctx.observer;\n const tracking = await observer?.messagePrepared?.(message);\n const copy = tracking === undefined ? message : { ...message, tracking };\n const result = await ctx.messages.send(copy);\n try {\n await observer?.messageSent?.(copy, result ?? { delivered: true });\n } catch (error) {\n console.error(`[workflow] reporting ${copy.idempotencyKey} as sent failed`, error);\n }\n}\n\n/**\n * Goal / exitWhen guards (see the header for when they run).\n *\n * The goal condition is anchored at workflow entry: a `performed` inside it\n * only counts events after the user entered, so pre-existing history cannot\n * register as a conversion. exitWhen is deliberately unanchored — \"user\n * unsubscribed\" should exit no matter when it happened.\n */\nasync function checkGuards(\n stepId: string,\n ir: WorkflowIR,\n ctx: JourneyContext\n): Promise<FlowSignal | null> {\n const goal = ir.goal;\n const exitWhen = ir.exitWhen;\n if (goal === undefined && exitWhen === undefined) return null;\n\n const state = goalState(ctx);\n const result = await ctx.step.do(`${stepId}:guard`, async () => {\n const now = Date.now();\n // Already converted (exitOnMatch: false): the conversion stands, no need to look again\n const goalHit =\n goal !== undefined && state.convertedAt === undefined\n ? await goalReached(ir, ctx, now)\n : null;\n const exitMet = exitWhen !== undefined && (await evaluateCondition(exitWhen, ctx.facts, now));\n return { goalAt: goalHit?.at ?? null, exitMet };\n });\n\n // A checkpoint from before conversions carried a time held `goalMet` instead: met at entry's anchor\n const legacy = result as { goalMet?: boolean };\n const goalAt =\n typeof result.goalAt === 'number'\n ? result.goalAt\n : legacy.goalMet === true\n ? ctx.enteredAtMs\n : null;\n if (goalAt !== null) {\n state.convertedAt ??= goalAt;\n if (goal?.exitOnMatch ?? true) return { kind: 'goal' };\n }\n if (result.exitMet) return { kind: 'exit_when' };\n return null;\n}\n\n/**\n * The host's template check, with the instance and node added to the message:\n * a refused message must point at the journey that produced it. The error\n * keeps its class (and `name`), so adapters can stop retrying it.\n */\nfunction validate(\n node: Extract<NodeIR, { type: 'message' }>,\n props: Record<string, ScalarIR | undefined>,\n ctx: JourneyContext\n): Record<string, ScalarIR | undefined> {\n if (ctx.validator === undefined) return props;\n try {\n return ctx.validator.validate({ channel: node.channel, template: node.template, props });\n } catch (error) {\n if (error instanceof Error) {\n error.message = `[${ctx.instanceId} ${node.id}] ${error.message}`;\n }\n throw error;\n }\n}\n\ntype PushPlan = { props: Record<string, ScalarIR | undefined>; devices: PushDevice[] };\n\n/**\n * A push goes to devices, not to a profile address: one copy per device the\n * host's directory lists (or only the most recent, `devices: 'latest'`). Each\n * copy's idempotency key carries its token, so a retried step that already\n * reached some devices re-sends to the rest and the senders drop the repeats —\n * keyed by token rather than position, because the list can change between\n * attempts. A user with no device is skipped, not failed: being unreachable is\n * a state of the user, and `reachable('push')` is how a journey branches on it.\n */\n/**\n * A push: one step settles what goes out and to which devices, then each device gets a step of\n * its own — a failure on one device retries that device alone, and the copies already delivered\n * are not sent again (APNs and FCM carry the idempotency key but do not de-duplicate on it).\n */\nasync function sendPush(\n node: Extract<NodeIR, { type: 'message' }>,\n ir: WorkflowIR,\n ctx: JourneyContext\n): Promise<void> {\n const plan = await ctx.step.do<PushPlan | null | undefined>(`${node.id}:send`, async () => {\n const props = validate(node, await resolveValues(node.props, ctx.facts), ctx);\n if (await ctx.facts.isSuppressed?.(node.channel)) return null;\n const listed = await ctx.facts.listDevices();\n const devices = node.devices === 'latest' ? listed.slice(0, 1) : listed;\n return { props, devices };\n });\n // null: the channel is opted out. Undefined: a checkpoint from before per-device steps, whose\n // single step already sent to every device.\n if (plan === null || plan === undefined) return;\n if (plan.devices.length === 0) {\n console.log(`[journey] push ${node.template} skipped: no device for user ${ctx.userId}`);\n return;\n }\n for (const [index, device] of plan.devices.entries()) {\n await ctx.step.do(`${node.id}:send:${index}`, async () => {\n await deliver(\n {\n workflow: ir.id,\n instanceId: ctx.instanceId,\n nodeId: node.id,\n channel: 'push',\n template: node.template,\n props: plan.props,\n userId: ctx.userId,\n recipient: device.token,\n device,\n idempotencyKey: `${ctx.instanceId}:${node.id}:${device.token}`,\n },\n ctx\n );\n });\n }\n}\n\nasync function runNode(node: NodeIR, ir: WorkflowIR, ctx: JourneyContext): Promise<FlowSignal> {\n switch (node.type) {\n case 'message': {\n if (node.channel === 'push') {\n await sendPush(node, ir, ctx);\n return FALL_THROUGH;\n }\n await ctx.step.do(`${node.id}:send`, async () => {\n const props = validate(node, await resolveValues(node.props, ctx.facts), ctx);\n // An opted-out channel (an email unsubscribe) is skipped, like a push with no device\n if (await ctx.facts.isSuppressed?.(node.channel)) return;\n const recipientPath = RECIPIENT_PROPERTY[node.channel];\n const recipient =\n recipientPath === undefined ? undefined : await ctx.facts.getProperty(recipientPath);\n await deliver(\n {\n workflow: ir.id,\n instanceId: ctx.instanceId,\n nodeId: node.id,\n channel: node.channel,\n template: node.template,\n props,\n userId: ctx.userId,\n recipient: recipient === undefined ? undefined : String(recipient),\n idempotencyKey: `${ctx.instanceId}:${node.id}`,\n },\n ctx\n );\n });\n return FALL_THROUGH;\n }\n\n case 'delay': {\n await ctx.step.sleep(node.id, node.duration.ms);\n return (await checkGuards(node.id, ir, ctx)) ?? FALL_THROUGH;\n }\n\n case 'random_delay': {\n // The random value is drawn and persisted inside the step, so replay does not re-roll it\n const ms = await ctx.step.do(`${node.id}:roll`, async () => {\n const span = node.max.ms - node.min.ms;\n return node.min.ms + Math.floor(Math.random() * Math.max(span, 0));\n });\n await ctx.step.sleep(`${node.id}:sleep`, ms);\n return (await checkGuards(node.id, ir, ctx)) ?? FALL_THROUGH;\n }\n\n case 'time_window': {\n const target = await ctx.step.do(`${node.id}:target`, async () => {\n const tz =\n node.tz === 'user'\n ? String((await ctx.facts.getProperty(TIMEZONE_PROPERTY)) ?? 'UTC')\n : node.tz;\n return nextWindowStart(Date.now(), node.days, node.between, tz);\n });\n if (target === null) return FALL_THROUGH; // already inside the window: no suspension, no guard\n await ctx.step.sleepUntil(`${node.id}:sleep`, target);\n return (await checkGuards(node.id, ir, ctx)) ?? FALL_THROUGH;\n }\n\n case 'wait_until': {\n const met = await waitUntil(node, ctx);\n // Time passed while waiting — guard before acting on the outcome (goal wins over the wait result)\n const guard = await checkGuards(node.id, ir, ctx);\n if (guard) return guard;\n if (met) return FALL_THROUGH;\n const onTimeout = node.onTimeout;\n if (onTimeout === 'continue') return FALL_THROUGH;\n if (onTimeout === 'exit') return { kind: 'exit', reason: `${node.id}:timeout` };\n return runFlow(onTimeout, ir, ctx);\n }\n\n case 'branch': {\n const matched = await ctx.step.do(`${node.id}:eval`, async () => {\n const now = Date.now();\n for (const [i, branchCase] of node.cases.entries()) {\n if (await evaluateCondition(branchCase.condition, ctx.facts, now)) {\n return i;\n }\n }\n return -1; // no match → default arm, or simply continue\n });\n const flow = matched >= 0 ? node.cases[matched]?.flow : node.otherwise;\n if (flow === undefined) return FALL_THROUGH;\n const signal = await runFlow(flow, ir, ctx);\n // Structural rejoin: once an arm falls through, execution resumes after the branch\n return signal;\n }\n\n case 'filter': {\n const pass = await ctx.step.do(`${node.id}:eval`, async () =>\n evaluateCondition(node.condition, ctx.facts, Date.now())\n );\n return pass ? FALL_THROUGH : { kind: 'exit', reason: node.reason ?? `${node.id}:filtered` };\n }\n\n case 'cohort': {\n // Deterministic bucketing, so replay needs no checkpoint. The explicit\n // experiment key (when set) keeps the assignment stable across workflow\n // versions; the structural id fallback is fine for one-off splits.\n const bucket = hashToBucket(`${ctx.userId}:${node.key ?? node.id}`);\n // Integer basis points on both sides (the DSL guarantees weights sit on\n // the 0.01% grid), so no float ever meets the comparison.\n let cumulative = 0;\n for (const arm of node.arms) {\n cumulative += Math.round(arm.weight * 100);\n if (bucket < cumulative) return runFlow(arm.flow, ir, ctx);\n }\n return FALL_THROUGH;\n }\n\n case 'exit':\n return node.reason !== undefined ? { kind: 'exit', reason: node.reason } : { kind: 'exit' };\n\n case 'send_event': {\n await ctx.step.do(`${node.id}:emit`, async () => {\n const payload = await resolveValues(node.payload, ctx.facts);\n try {\n await ctx.events.emit(node.event, payload, `${ctx.instanceId}:${node.id}`);\n } catch (error) {\n // A refused event must point at the journey that emitted it\n if (error instanceof Error)\n error.message = `[${ctx.instanceId} ${node.id}] ${error.message}`;\n throw error;\n }\n });\n return FALL_THROUGH;\n }\n\n case 'action': {\n // Custom code runs inside step.do: retried on throw, checkpointed on success\n const invoker = ctx.actions;\n if (invoker === undefined) {\n throw new Error(\n `action '${node.action}': this runtime has no ActionInvoker configured — register the action on the adapter (see engine/actions.ts)`\n );\n }\n await ctx.step.do(`${node.id}:run`, async () => {\n const args = await resolveValues(node.args, ctx.facts);\n await invoker.invoke({\n action: node.action,\n codeHash: node.codeHash,\n args,\n userId: ctx.userId,\n idempotencyKey: `${ctx.instanceId}:${node.id}`,\n });\n });\n return FALL_THROUGH;\n }\n }\n}\n\nasync function waitUntil(\n node: Extract<NodeIR, { type: 'wait_until' }>,\n ctx: JourneyContext\n): Promise<boolean> {\n const deadline = await ctx.step.do(\n `${node.id}:deadline`,\n async () => Date.now() + node.timeout.ms\n );\n const events = await ctx.step.do(`${node.id}:events`, async () =>\n relevantEvents(node.condition, ctx.facts)\n );\n // Anchor at the moment the wait began (deadline is checkpointed, so this is\n // replay-stable): `performed` inside the condition only counts events that\n // happen during the wait — pre-existing history must not satisfy it.\n const anchorMs = deadline - node.timeout.ms;\n const met = await waitLoop(node.id, ctx, deadline, events, async (now) =>\n (await evaluateCondition(node.condition, ctx.facts, now, { anchorMs })) ? true : null\n );\n await ctx.step.unsubscribe(`${node.id}:unsubscribe`);\n return met === true;\n}\n\n/**\n * The conversion window outlives the flow: a journey whose steps are done while its goal's\n * `within` is still open keeps watching until the window closes — the purchase that follows the\n * last offer email is that email's conversion, and ending the instance at the email would report\n * it as a plain completion. `exitWhen` still applies while it waits. Nothing to wait for without\n * a window, or once the instance has converted.\n */\nasync function awaitTail(ir: WorkflowIR, ctx: JourneyContext): Promise<FlowSignal | null> {\n const deadline = goalDeadline(ir, ctx);\n const goal = ir.goal;\n if (goal === undefined || deadline === undefined) return null;\n const state = goalState(ctx);\n if (state.convertedAt !== undefined) return null;\n const open = await ctx.step.do('goal:open', async () => deadline > Date.now());\n if (!open) return null;\n const exitWhen = ir.exitWhen;\n const events = await ctx.step.do('goal:events', async () => [\n ...new Set([\n ...(await relevantEvents(goal.condition, ctx.facts)),\n ...(exitWhen === undefined ? [] : await relevantEvents(exitWhen, ctx.facts)),\n ]),\n ]);\n const hit = await waitLoop('goal', ctx, deadline, events, async (now) => {\n const reached = await goalReached(ir, ctx, now);\n if (reached !== null) return { kind: 'goal' as const, at: reached.at };\n if (exitWhen !== undefined && (await evaluateCondition(exitWhen, ctx.facts, now))) {\n return { kind: 'exit_when' as const, at: now };\n }\n return null;\n });\n await ctx.step.unsubscribe('goal:unsubscribe');\n if (hit === null) return null;\n // A checkpoint from before the tail watched exitWhen recorded a plain `true`: the goal\n if ((hit as unknown) === true || hit.kind === 'goal') {\n state.convertedAt ??= (hit as { at?: number }).at ?? deadline;\n return { kind: 'goal' };\n }\n return { kind: 'exit_when' };\n}\n\n/**\n * Wait until `check` finds something or the deadline passes: subscribe, check, sleep until a\n * relevant event wakes the instance or time runs out. `check` runs inside a step, so its result\n * must be serializable; null means \"not yet\".\n */\nasync function waitLoop<R>(\n id: string,\n ctx: JourneyContext,\n deadline: number,\n events: string[],\n check: (now: number) => Promise<R | null>\n): Promise<R | null> {\n for (let attempt = 0; attempt < MAX_WAIT_CHECKS; attempt++) {\n // Subscribe before the check, every attempt: one subscribe arms at least\n // one wake-up (AWS callbacks are consumed on delivery, so each attempt\n // needs a fresh arm; Cloudflare rows make the repeat call idempotent), and\n // arming first closes the race where the event lands between the check and\n // the registration.\n await ctx.step.subscribe(`${id}:subscribe:${attempt}`, events);\n\n const met = await ctx.step.do(`${id}:check:${attempt}`, async () => check(Date.now()));\n // `false` is a checkpoint from before checks answered null for \"not yet\"\n if (met !== null && (met as unknown) !== false) return met;\n\n // Checkpointed: replay re-runs this control flow with wall-clock time far\n // ahead of the original run, so the branch must come from a persisted value.\n const remaining = await ctx.step.do(\n `${id}:remaining:${attempt}`,\n async () => deadline - Date.now()\n );\n // The condition was checked just above — expiring here needs no extra final check\n if (remaining <= 0) return null;\n\n const wake = await ctx.step.waitForWake(`${id}:wait:${attempt}`, remaining);\n if (wake === 'timeout') {\n // Final check after timeout: in a tight race the event may land exactly as the timer fires\n const final = await ctx.step.do(`${id}:final`, async () => check(Date.now()));\n return (final as unknown) === false ? null : final;\n }\n // Woken → back to the top of the loop to re-evaluate (at-least-once wake-up semantics)\n }\n return null; // check budget exhausted → treated as timed out (see MAX_WAIT_CHECKS)\n}\n\nasync function resolveValues(\n values: Record<string, PropValueIR>,\n facts: FactSource\n): Promise<Record<string, ScalarIR | undefined>> {\n const resolved: Record<string, ScalarIR | undefined> = {};\n for (const [key, value] of Object.entries(values)) {\n resolved[key] = typeof value === 'object' ? await facts.getProperty(value.path) : value;\n }\n return resolved;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,MAAM,kBAAkB;;AAGxB,MAAM,oBAAoB;AAQ1B,MAAM,eAA2B,EAAE,MAAM,eAAe;AAYxD,MAAM,6BAAa,IAAI,QAAmC;AAE1D,SAAS,UAAU,KAAgC;CACjD,IAAI,QAAQ,WAAW,IAAI,GAAG;CAC9B,IAAI,UAAU,KAAA,GAAW;EACvB,QAAQ,CAAC;EACT,WAAW,IAAI,KAAK,KAAK;CAC3B;CACA,OAAO;AACT;;AAGA,SAAS,aAAa,IAAgB,KAAyC;CAC7E,OAAO,GAAG,MAAM,WAAW,KAAA,IAAY,KAAA,IAAY,IAAI,cAAc,GAAG,KAAK,OAAO;AACtF;;;;;;AAOA,eAAe,YACb,IACA,KACA,KACgC;CAChC,MAAM,OAAO,GAAG;CAChB,IAAI,SAAS,KAAA,GAAW,OAAO;CAC/B,MAAM,WAAW,aAAa,IAAI,GAAG;CAKrC,OAAO,MAJWA,yBAAAA,kBAAkB,KAAK,WAAW,IAAI,OAAO,KAAK;EAClE,UAAU,IAAI;EACd,GAAI,aAAa,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,SAAS;CACxD,CAAC,IACY,EAAE,IAAI,aAAa,KAAA,IAAY,MAAM,KAAK,IAAI,KAAK,QAAQ,EAAE,IAAI;AAChF;AAEA,eAAsB,WAAW,IAAgB,KAA8C;CAC7F,MAAM,QAAQ,IAAI,GAAG;CAErB,IAAI,SAAS,MADO,YAAY,SAAS,IAAI,GAAG,KACzB,MAAM,QAAQ,GAAG,MAAM,IAAI,GAAG;CAErD,IAAI,OAAO,SAAS,gBAAgB,SAAU,MAAM,UAAU,IAAI,GAAG,KAAM;CAC3E,MAAM,EAAE,gBAAgB,UAAU,GAAG;CACrC,MAAM,YAAY,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY;CACjE,QAAQ,OAAO,MAAf;EACE,KAAK,gBACH,OAAO;GAAE,QAAQ;GAAa,GAAG;EAAU;EAC7C,KAAK,QACH,OAAO,OAAO,WAAW,KAAA,IACrB;GAAE,QAAQ;GAAU,QAAQ,OAAO;GAAQ,GAAG;EAAU,IACxD;GAAE,QAAQ;GAAU,GAAG;EAAU;EACvC,KAAK,QACH,OAAO;GAAE,QAAQ;GAAQ,GAAG;EAAU;EACxC,KAAK,aACH,OAAO;GAAE,QAAQ;GAAa,GAAG;EAAU;CAC/C;AACF;AAEA,eAAe,QAAQ,OAAiB,IAAgB,KAA0C;CAChG,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,QAAQ,MAAM,IAAI,GAAG;EAC3B,MAAM,SAAS,MAAM,QAAQ,MAAM,IAAI,GAAG;EAC1C,IAAI,OAAO,SAAS,gBAAgB,OAAO;CAC7C;CACA,OAAO;AACT;;;;;AAMA,eAAe,QAAQ,IAAgB,KAAoC;CACzE,MAAM,WAAW,IAAI;CACrB,IAAI,UAAU,YAAY,KAAA,GAAW;CACrC,MAAM,IAAI,KAAK,GAAG,iBAAiB,YAAY;EAC7C,MAAM,SAAS,UAAU;GACvB,UAAU,GAAG;GACb,YAAY,IAAI;GAChB,QAAQ,IAAI;GACZ,IAAI,IAAI;EACV,CAAC;CACH,CAAC;AACH;;;;;AAMA,eAAe,QAAQ,MAAc,IAAgB,KAAoC;CACvF,MAAM,WAAW,IAAI;CACrB,IAAI,UAAU,gBAAgB,KAAA,GAAW;CACzC,MAAM,IAAI,KAAK,GAAG,GAAG,KAAK,GAAG,WAAW,YAAY;EAClD,MAAM,SAAS,cAAc;GAC3B,UAAU,GAAG;GACb,YAAY,IAAI;GAChB,QAAQ,IAAI;GACZ,QAAQ,KAAK;GACb,IAAI,KAAK,IAAI;EACf,CAAC;CACH,CAAC;AACH;;;;;;;;AASA,eAAe,QAAQ,SAA0B,KAAoC;CACnF,MAAM,WAAW,IAAI;CACrB,MAAM,WAAW,MAAM,UAAU,kBAAkB,OAAO;CAC1D,MAAM,OAAO,aAAa,KAAA,IAAY,UAAU;EAAE,GAAG;EAAS;CAAS;CACvE,MAAM,SAAS,MAAM,IAAI,SAAS,KAAK,IAAI;CAC3C,IAAI;EACF,MAAM,UAAU,cAAc,MAAM,UAAU,EAAE,WAAW,KAAK,CAAC;CACnE,SAAS,OAAO;EACd,QAAQ,MAAM,wBAAwB,KAAK,eAAe,kBAAkB,KAAK;CACnF;AACF;;;;;;;;;AAUA,eAAe,YACb,QACA,IACA,KAC4B;CAC5B,MAAM,OAAO,GAAG;CAChB,MAAM,WAAW,GAAG;CACpB,IAAI,SAAS,KAAA,KAAa,aAAa,KAAA,GAAW,OAAO;CAEzD,MAAM,QAAQ,UAAU,GAAG;CAC3B,MAAM,SAAS,MAAM,IAAI,KAAK,GAAG,GAAG,OAAO,SAAS,YAAY;EAC9D,MAAM,MAAM,KAAK,IAAI;EAErB,MAAM,UACJ,SAAS,KAAA,KAAa,MAAM,gBAAgB,KAAA,IACxC,MAAM,YAAY,IAAI,KAAK,GAAG,IAC9B;EACN,MAAM,UAAU,aAAa,KAAA,KAAc,MAAMA,yBAAAA,kBAAkB,UAAU,IAAI,OAAO,GAAG;EAC3F,OAAO;GAAE,QAAQ,SAAS,MAAM;GAAM;EAAQ;CAChD,CAAC;CAGD,MAAM,SAAS;CACf,MAAM,SACJ,OAAO,OAAO,WAAW,WACrB,OAAO,SACP,OAAO,YAAY,OACjB,IAAI,cACJ;CACR,IAAI,WAAW,MAAM;EACnB,MAAM,gBAAgB;EACtB,IAAI,MAAM,eAAe,MAAM,OAAO,EAAE,MAAM,OAAO;CACvD;CACA,IAAI,OAAO,SAAS,OAAO,EAAE,MAAM,YAAY;CAC/C,OAAO;AACT;;;;;;AAOA,SAAS,SACP,MACA,OACA,KACsC;CACtC,IAAI,IAAI,cAAc,KAAA,GAAW,OAAO;CACxC,IAAI;EACF,OAAO,IAAI,UAAU,SAAS;GAAE,SAAS,KAAK;GAAS,UAAU,KAAK;GAAU;EAAM,CAAC;CACzF,SAAS,OAAO;EACd,IAAI,iBAAiB,OACnB,MAAM,UAAU,IAAI,IAAI,WAAW,GAAG,KAAK,GAAG,IAAI,MAAM;EAE1D,MAAM;CACR;AACF;;;;;;;;;;;;;;;AAkBA,eAAe,SACb,MACA,IACA,KACe;CACf,MAAM,OAAO,MAAM,IAAI,KAAK,GAAgC,GAAG,KAAK,GAAG,QAAQ,YAAY;EACzF,MAAM,QAAQ,SAAS,MAAM,MAAM,cAAc,KAAK,OAAO,IAAI,KAAK,GAAG,GAAG;EAC5E,IAAI,MAAM,IAAI,MAAM,eAAe,KAAK,OAAO,GAAG,OAAO;EACzD,MAAM,SAAS,MAAM,IAAI,MAAM,YAAY;EAE3C,OAAO;GAAE;GAAO,SADA,KAAK,YAAY,WAAW,OAAO,MAAM,GAAG,CAAC,IAAI;EACzC;CAC1B,CAAC;CAGD,IAAI,SAAS,QAAQ,SAAS,KAAA,GAAW;CACzC,IAAI,KAAK,QAAQ,WAAW,GAAG;EAC7B,QAAQ,IAAI,kBAAkB,KAAK,SAAS,+BAA+B,IAAI,QAAQ;EACvF;CACF;CACA,KAAK,MAAM,CAAC,OAAO,WAAW,KAAK,QAAQ,QAAQ,GACjD,MAAM,IAAI,KAAK,GAAG,GAAG,KAAK,GAAG,QAAQ,SAAS,YAAY;EACxD,MAAM,QACJ;GACE,UAAU,GAAG;GACb,YAAY,IAAI;GAChB,QAAQ,KAAK;GACb,SAAS;GACT,UAAU,KAAK;GACf,OAAO,KAAK;GACZ,QAAQ,IAAI;GACZ,WAAW,OAAO;GAClB;GACA,gBAAgB,GAAG,IAAI,WAAW,GAAG,KAAK,GAAG,GAAG,OAAO;EACzD,GACA,GACF;CACF,CAAC;AAEL;AAEA,eAAe,QAAQ,MAAc,IAAgB,KAA0C;CAC7F,QAAQ,KAAK,MAAb;EACE,KAAK;GACH,IAAI,KAAK,YAAY,QAAQ;IAC3B,MAAM,SAAS,MAAM,IAAI,GAAG;IAC5B,OAAO;GACT;GACA,MAAM,IAAI,KAAK,GAAG,GAAG,KAAK,GAAG,QAAQ,YAAY;IAC/C,MAAM,QAAQ,SAAS,MAAM,MAAM,cAAc,KAAK,OAAO,IAAI,KAAK,GAAG,GAAG;IAE5E,IAAI,MAAM,IAAI,MAAM,eAAe,KAAK,OAAO,GAAG;IAClD,MAAM,gBAAgBC,yBAAAA,mBAAmB,KAAK;IAC9C,MAAM,YACJ,kBAAkB,KAAA,IAAY,KAAA,IAAY,MAAM,IAAI,MAAM,YAAY,aAAa;IACrF,MAAM,QACJ;KACE,UAAU,GAAG;KACb,YAAY,IAAI;KAChB,QAAQ,KAAK;KACb,SAAS,KAAK;KACd,UAAU,KAAK;KACf;KACA,QAAQ,IAAI;KACZ,WAAW,cAAc,KAAA,IAAY,KAAA,IAAY,OAAO,SAAS;KACjE,gBAAgB,GAAG,IAAI,WAAW,GAAG,KAAK;IAC5C,GACA,GACF;GACF,CAAC;GACD,OAAO;EAGT,KAAK;GACH,MAAM,IAAI,KAAK,MAAM,KAAK,IAAI,KAAK,SAAS,EAAE;GAC9C,OAAQ,MAAM,YAAY,KAAK,IAAI,IAAI,GAAG,KAAM;EAGlD,KAAK,gBAAgB;GAEnB,MAAM,KAAK,MAAM,IAAI,KAAK,GAAG,GAAG,KAAK,GAAG,QAAQ,YAAY;IAC1D,MAAM,OAAO,KAAK,IAAI,KAAK,KAAK,IAAI;IACpC,OAAO,KAAK,IAAI,KAAK,KAAK,MAAM,KAAK,OAAO,IAAI,KAAK,IAAI,MAAM,CAAC,CAAC;GACnE,CAAC;GACD,MAAM,IAAI,KAAK,MAAM,GAAG,KAAK,GAAG,SAAS,EAAE;GAC3C,OAAQ,MAAM,YAAY,KAAK,IAAI,IAAI,GAAG,KAAM;EAClD;EAEA,KAAK,eAAe;GAClB,MAAM,SAAS,MAAM,IAAI,KAAK,GAAG,GAAG,KAAK,GAAG,UAAU,YAAY;IAChE,MAAM,KACJ,KAAK,OAAO,SACR,OAAQ,MAAM,IAAI,MAAM,YAAY,iBAAiB,KAAM,KAAK,IAChE,KAAK;IACX,OAAOC,2BAAAA,gBAAgB,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,SAAS,EAAE;GAChE,CAAC;GACD,IAAI,WAAW,MAAM,OAAO;GAC5B,MAAM,IAAI,KAAK,WAAW,GAAG,KAAK,GAAG,SAAS,MAAM;GACpD,OAAQ,MAAM,YAAY,KAAK,IAAI,IAAI,GAAG,KAAM;EAClD;EAEA,KAAK,cAAc;GACjB,MAAM,MAAM,MAAM,UAAU,MAAM,GAAG;GAErC,MAAM,QAAQ,MAAM,YAAY,KAAK,IAAI,IAAI,GAAG;GAChD,IAAI,OAAO,OAAO;GAClB,IAAI,KAAK,OAAO;GAChB,MAAM,YAAY,KAAK;GACvB,IAAI,cAAc,YAAY,OAAO;GACrC,IAAI,cAAc,QAAQ,OAAO;IAAE,MAAM;IAAQ,QAAQ,GAAG,KAAK,GAAG;GAAU;GAC9E,OAAO,QAAQ,WAAW,IAAI,GAAG;EACnC;EAEA,KAAK,UAAU;GACb,MAAM,UAAU,MAAM,IAAI,KAAK,GAAG,GAAG,KAAK,GAAG,QAAQ,YAAY;IAC/D,MAAM,MAAM,KAAK,IAAI;IACrB,KAAK,MAAM,CAAC,GAAG,eAAe,KAAK,MAAM,QAAQ,GAC/C,IAAI,MAAMF,yBAAAA,kBAAkB,WAAW,WAAW,IAAI,OAAO,GAAG,GAC9D,OAAO;IAGX,OAAO;GACT,CAAC;GACD,MAAM,OAAO,WAAW,IAAI,KAAK,MAAM,QAAQ,EAAE,OAAO,KAAK;GAC7D,IAAI,SAAS,KAAA,GAAW,OAAO;GAG/B,OAAO,MAFc,QAAQ,MAAM,IAAI,GAAG;EAG5C;EAEA,KAAK,UAIH,OAAO,MAHY,IAAI,KAAK,GAAG,GAAG,KAAK,GAAG,QAAQ,YAChDA,yBAAAA,kBAAkB,KAAK,WAAW,IAAI,OAAO,KAAK,IAAI,CAAC,CACzD,IACc,eAAe;GAAE,MAAM;GAAQ,QAAQ,KAAK,UAAU,GAAG,KAAK,GAAG;EAAW;EAG5F,KAAK,UAAU;GAIb,MAAM,SAASG,sBAAAA,aAAa,GAAG,IAAI,OAAO,GAAG,KAAK,OAAO,KAAK,IAAI;GAGlE,IAAI,aAAa;GACjB,KAAK,MAAM,OAAO,KAAK,MAAM;IAC3B,cAAc,KAAK,MAAM,IAAI,SAAS,GAAG;IACzC,IAAI,SAAS,YAAY,OAAO,QAAQ,IAAI,MAAM,IAAI,GAAG;GAC3D;GACA,OAAO;EACT;EAEA,KAAK,QACH,OAAO,KAAK,WAAW,KAAA,IAAY;GAAE,MAAM;GAAQ,QAAQ,KAAK;EAAO,IAAI,EAAE,MAAM,OAAO;EAE5F,KAAK;GACH,MAAM,IAAI,KAAK,GAAG,GAAG,KAAK,GAAG,QAAQ,YAAY;IAC/C,MAAM,UAAU,MAAM,cAAc,KAAK,SAAS,IAAI,KAAK;IAC3D,IAAI;KACF,MAAM,IAAI,OAAO,KAAK,KAAK,OAAO,SAAS,GAAG,IAAI,WAAW,GAAG,KAAK,IAAI;IAC3E,SAAS,OAAO;KAEd,IAAI,iBAAiB,OACnB,MAAM,UAAU,IAAI,IAAI,WAAW,GAAG,KAAK,GAAG,IAAI,MAAM;KAC1D,MAAM;IACR;GACF,CAAC;GACD,OAAO;EAGT,KAAK,UAAU;GAEb,MAAM,UAAU,IAAI;GACpB,IAAI,YAAY,KAAA,GACd,MAAM,IAAI,MACR,WAAW,KAAK,OAAO,6GACzB;GAEF,MAAM,IAAI,KAAK,GAAG,GAAG,KAAK,GAAG,OAAO,YAAY;IAC9C,MAAM,OAAO,MAAM,cAAc,KAAK,MAAM,IAAI,KAAK;IACrD,MAAM,QAAQ,OAAO;KACnB,QAAQ,KAAK;KACb,UAAU,KAAK;KACf;KACA,QAAQ,IAAI;KACZ,gBAAgB,GAAG,IAAI,WAAW,GAAG,KAAK;IAC5C,CAAC;GACH,CAAC;GACD,OAAO;EACT;CACF;AACF;AAEA,eAAe,UACb,MACA,KACkB;CAClB,MAAM,WAAW,MAAM,IAAI,KAAK,GAC9B,GAAG,KAAK,GAAG,YACX,YAAY,KAAK,IAAI,IAAI,KAAK,QAAQ,EACxC;CACA,MAAM,SAAS,MAAM,IAAI,KAAK,GAAG,GAAG,KAAK,GAAG,UAAU,YACpDC,yBAAAA,eAAe,KAAK,WAAW,IAAI,KAAK,CAC1C;CAIA,MAAM,WAAW,WAAW,KAAK,QAAQ;CACzC,MAAM,MAAM,MAAM,SAAS,KAAK,IAAI,KAAK,UAAU,QAAQ,OAAO,QAC/D,MAAMJ,yBAAAA,kBAAkB,KAAK,WAAW,IAAI,OAAO,KAAK,EAAE,SAAS,CAAC,IAAK,OAAO,IACnF;CACA,MAAM,IAAI,KAAK,YAAY,GAAG,KAAK,GAAG,aAAa;CACnD,OAAO,QAAQ;AACjB;;;;;;;;AASA,eAAe,UAAU,IAAgB,KAAiD;CACxF,MAAM,WAAW,aAAa,IAAI,GAAG;CACrC,MAAM,OAAO,GAAG;CAChB,IAAI,SAAS,KAAA,KAAa,aAAa,KAAA,GAAW,OAAO;CACzD,MAAM,QAAQ,UAAU,GAAG;CAC3B,IAAI,MAAM,gBAAgB,KAAA,GAAW,OAAO;CAE5C,IAAI,CAAC,MADc,IAAI,KAAK,GAAG,aAAa,YAAY,WAAW,KAAK,IAAI,CAAC,GAClE,OAAO;CAClB,MAAM,WAAW,GAAG;CAOpB,MAAM,MAAM,MAAM,SAAS,QAAQ,KAAK,UAAU,MAN7B,IAAI,KAAK,GAAG,eAAe,YAAY,CAC1D,mBAAG,IAAI,IAAI,CACT,GAAI,MAAMI,yBAAAA,eAAe,KAAK,WAAW,IAAI,KAAK,GAClD,GAAI,aAAa,KAAA,IAAY,CAAC,IAAI,MAAMA,yBAAAA,eAAe,UAAU,IAAI,KAAK,CAC5E,CAAC,CACH,CAAC,GACyD,OAAO,QAAQ;EACvE,MAAM,UAAU,MAAM,YAAY,IAAI,KAAK,GAAG;EAC9C,IAAI,YAAY,MAAM,OAAO;GAAE,MAAM;GAAiB,IAAI,QAAQ;EAAG;EACrE,IAAI,aAAa,KAAA,KAAc,MAAMJ,yBAAAA,kBAAkB,UAAU,IAAI,OAAO,GAAG,GAC7E,OAAO;GAAE,MAAM;GAAsB,IAAI;EAAI;EAE/C,OAAO;CACT,CAAC;CACD,MAAM,IAAI,KAAK,YAAY,kBAAkB;CAC7C,IAAI,QAAQ,MAAM,OAAO;CAEzB,IAAK,QAAoB,QAAQ,IAAI,SAAS,QAAQ;EACpD,MAAM,gBAAiB,IAAwB,MAAM;EACrD,OAAO,EAAE,MAAM,OAAO;CACxB;CACA,OAAO,EAAE,MAAM,YAAY;AAC7B;;;;;;AAOA,eAAe,SACb,IACA,KACA,UACA,QACA,OACmB;CACnB,KAAK,IAAI,UAAU,GAAG,UAAU,iBAAiB,WAAW;EAM1D,MAAM,IAAI,KAAK,UAAU,GAAG,GAAG,aAAa,WAAW,MAAM;EAE7D,MAAM,MAAM,MAAM,IAAI,KAAK,GAAG,GAAG,GAAG,SAAS,WAAW,YAAY,MAAM,KAAK,IAAI,CAAC,CAAC;EAErF,IAAI,QAAQ,QAAS,QAAoB,OAAO,OAAO;EAIvD,MAAM,YAAY,MAAM,IAAI,KAAK,GAC/B,GAAG,GAAG,aAAa,WACnB,YAAY,WAAW,KAAK,IAAI,CAClC;EAEA,IAAI,aAAa,GAAG,OAAO;EAG3B,IAAI,MADe,IAAI,KAAK,YAAY,GAAG,GAAG,QAAQ,WAAW,SAAS,MAC7D,WAAW;GAEtB,MAAM,QAAQ,MAAM,IAAI,KAAK,GAAG,GAAG,GAAG,SAAS,YAAY,MAAM,KAAK,IAAI,CAAC,CAAC;GAC5E,OAAQ,UAAsB,QAAQ,OAAO;EAC/C;CAEF;CACA,OAAO;AACT;AAEA,eAAe,cACb,QACA,OAC+C;CAC/C,MAAM,WAAiD,CAAC;CACxD,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAC9C,SAAS,OAAO,OAAO,UAAU,WAAW,MAAM,MAAM,YAAY,MAAM,IAAI,IAAI;CAEpF,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"interpreter.d.cts","names":[],"sources":["../../src/engine/interpreter.ts"],"mappings":";;;wBA0FsB,WAAW,IAAI,YAAY,KAAK,iBAAiB,QAAQ"}
1
+ {"version":3,"file":"interpreter.d.cts","names":[],"sources":["../../src/engine/interpreter.ts"],"mappings":";;;wBAgGsB,WAAW,IAAI,YAAY,KAAK,iBAAiB,QAAQ"}
@@ -1 +1 @@
1
- {"version":3,"file":"interpreter.d.mts","names":[],"sources":["../../src/engine/interpreter.ts"],"mappings":";;;wBA0FsB,WAAW,IAAI,YAAY,KAAK,iBAAiB,QAAQ"}
1
+ {"version":3,"file":"interpreter.d.mts","names":[],"sources":["../../src/engine/interpreter.ts"],"mappings":";;;wBAgGsB,WAAW,IAAI,YAAY,KAAK,iBAAiB,QAAQ"}
@@ -1 +1 @@
1
- {"version":3,"file":"interpreter.mjs","names":[],"sources":["../../src/engine/interpreter.ts"],"sourcesContent":["import type { NodeIR, PropValueIR, ScalarIR, WorkflowIR } from '../ir';\nimport { hashToBucket } from './bucket';\nimport { evaluateCondition, relevantEvents } from './condition';\nimport type { FactSource, JourneyContext, JourneyOutcome, OutboundMessage } from './ports';\nimport { RECIPIENT_PROPERTY } from './recipient';\nimport { nextWindowStart } from './time-window';\n\n/**\n * IR interpreter: the execution core for one user's journey instance.\n *\n * Pure logic — it imports nothing platform-specific; every external effect\n * arrives through the JourneyContext ports, and durable semantics\n * (checkpoint/replay) are the EngineStep adapter's job.\n *\n * Step naming: an IR node id *is* the step name (derived from the structural\n * path, so it is deterministic and unique). When one node needs several steps,\n * a suffix is appended (`${id}:guard`, `${id}:check:${n}`).\n *\n * Guard semantics: goal / exitWhen are checked at journey entry and after\n * every suspension point (delay / random_delay / time_window / wait_until) —\n * the moments where wall-clock time passed and the user's state may have\n * moved. Between two synchronous nodes no meaningful time passes, so checking\n * there would only burn step budget (each checkpoint costs a state write).\n */\n\n/**\n * Cap on wait_until re-evaluations: beyond it we treat the wait as timed out,\n * so a wake-up storm cannot exhaust the platform's step budget. Each loop\n * iteration costs a handful of steps, so 64 checks stay well inside the\n * platform step budgets while absorbing far more wake churn than 16 did.\n */\nconst MAX_WAIT_CHECKS = 64;\n\n/** The user property holding an IANA timezone id, read when a time_window says tz: 'user'. Same convention-table status as RECIPIENT_PROPERTY. */\nconst TIMEZONE_PROPERTY = 'timezone';\n\ntype FlowSignal =\n | { kind: 'fall_through' }\n | { kind: 'exit'; reason?: string }\n | { kind: 'goal' }\n | { kind: 'exit_when' };\n\nconst FALL_THROUGH: FlowSignal = { kind: 'fall_through' };\n\n/**\n * Whether and when the instance converted — a fact of its own, apart from how it ends: with\n * `exitOnMatch: false` a conversion lets the flow go on, and the instance still ends `completed`\n * (or `exited`, `exit_when`) carrying it. Set only from checkpointed step results, so a replay\n * rebuilds the same state.\n */\ninterface GoalState {\n convertedAt?: number;\n}\n\nconst goalStates = new WeakMap<JourneyContext, GoalState>();\n\nfunction goalState(ctx: JourneyContext): GoalState {\n let state = goalStates.get(ctx);\n if (state === undefined) {\n state = {};\n goalStates.set(ctx, state);\n }\n return state;\n}\n\n/** When the goal's attribution window closes, measured from entry; none without `within`. */\nfunction goalDeadline(ir: WorkflowIR, ctx: JourneyContext): number | undefined {\n return ir.goal?.within === undefined ? undefined : ctx.enteredAtMs + ir.goal.within.ms;\n}\n\n/**\n * Whether the goal holds for events inside its window: after entry, and not after the window\n * closed — an instance resumed past the deadline must not count a purchase made after it. The\n * conversion time is when the check saw it, capped at the deadline.\n */\nasync function goalReached(\n ir: WorkflowIR,\n ctx: JourneyContext,\n now: number\n): Promise<{ at: number } | null> {\n const goal = ir.goal;\n if (goal === undefined) return null;\n const deadline = goalDeadline(ir, ctx);\n const met = await evaluateCondition(goal.condition, ctx.facts, now, {\n anchorMs: ctx.enteredAtMs,\n ...(deadline === undefined ? {} : { untilMs: deadline }),\n });\n return met ? { at: deadline === undefined ? now : Math.min(now, deadline) } : null;\n}\n\nexport async function runJourney(ir: WorkflowIR, ctx: JourneyContext): Promise<JourneyOutcome> {\n await started(ir, ctx);\n const entry = await checkGuards('entry', ir, ctx);\n let signal = entry ?? (await runFlow(ir.flow, ir, ctx));\n // The flow is done but the conversion window is not: keep watching it (see awaitTail)\n if (signal.kind === 'fall_through') signal = (await awaitTail(ir, ctx)) ?? FALL_THROUGH;\n const { convertedAt } = goalState(ctx);\n const converted = convertedAt === undefined ? {} : { convertedAt };\n switch (signal.kind) {\n case 'fall_through':\n return { status: 'completed', ...converted };\n case 'exit':\n return signal.reason !== undefined\n ? { status: 'exited', reason: signal.reason, ...converted }\n : { status: 'exited', ...converted };\n case 'goal':\n return { status: 'goal', ...converted };\n case 'exit_when':\n return { status: 'exit_when', ...converted };\n }\n}\n\nasync function runFlow(nodes: NodeIR[], ir: WorkflowIR, ctx: JourneyContext): Promise<FlowSignal> {\n for (const node of nodes) {\n await reached(node, ir, ctx);\n const signal = await runNode(node, ir, ctx);\n if (signal.kind !== 'fall_through') return signal;\n }\n return FALL_THROUGH;\n}\n\n/**\n * Tell the observer the instance entered — before the entry guards, which can end it before the\n * first node: an instance is counted from its entry, not from its first node.\n */\nasync function started(ir: WorkflowIR, ctx: JourneyContext): Promise<void> {\n const observer = ctx.observer;\n if (observer?.started === undefined) return;\n await ctx.step.do('entry:started', async () => {\n await observer.started?.({\n workflow: ir.id,\n instanceId: ctx.instanceId,\n userId: ctx.userId,\n at: ctx.enteredAtMs,\n });\n });\n}\n\n/**\n * Tell the observer the instance is at `node`. A step of its own, so a replay does not report\n * the nodes it passes on the way back again, and the arrival time stays the first one.\n */\nasync function reached(node: NodeIR, ir: WorkflowIR, ctx: JourneyContext): Promise<void> {\n const observer = ctx.observer;\n if (observer?.nodeReached === undefined) return;\n await ctx.step.do(`${node.id}:reached`, async () => {\n await observer.nodeReached?.({\n workflow: ir.id,\n instanceId: ctx.instanceId,\n userId: ctx.userId,\n nodeId: node.id,\n at: Date.now(),\n });\n });\n}\n\n/**\n * One copy out through the host's sender, with the observer on both sides: it may attach\n * tracking before, and hears how the transport took the copy after. A send that throws is\n * not reported — the step retries it, and reports when it settles. A report that fails after\n * the send is logged, never thrown: the step would retry and the transport — a webhook, a web\n * push, with no idempotency key of its own — would deliver the copy again.\n */\nasync function deliver(message: OutboundMessage, ctx: JourneyContext): Promise<void> {\n const observer = ctx.observer;\n const tracking = await observer?.messagePrepared?.(message);\n const copy = tracking === undefined ? message : { ...message, tracking };\n const result = await ctx.messages.send(copy);\n try {\n await observer?.messageSent?.(copy, result ?? { delivered: true });\n } catch (error) {\n console.error(`[workflow] reporting ${copy.idempotencyKey} as sent failed`, error);\n }\n}\n\n/**\n * Goal / exitWhen guards (see the header for when they run).\n *\n * The goal condition is anchored at workflow entry: a `performed` inside it\n * only counts events after the user entered, so pre-existing history cannot\n * register as a conversion. exitWhen is deliberately unanchored — \"user\n * unsubscribed\" should exit no matter when it happened.\n */\nasync function checkGuards(\n stepId: string,\n ir: WorkflowIR,\n ctx: JourneyContext\n): Promise<FlowSignal | null> {\n const goal = ir.goal;\n const exitWhen = ir.exitWhen;\n if (goal === undefined && exitWhen === undefined) return null;\n\n const state = goalState(ctx);\n const result = await ctx.step.do(`${stepId}:guard`, async () => {\n const now = Date.now();\n // Already converted (exitOnMatch: false): the conversion stands, no need to look again\n const goalHit =\n goal !== undefined && state.convertedAt === undefined\n ? await goalReached(ir, ctx, now)\n : null;\n const exitMet = exitWhen !== undefined && (await evaluateCondition(exitWhen, ctx.facts, now));\n return { goalAt: goalHit?.at ?? null, exitMet };\n });\n\n // A checkpoint from before conversions carried a time held `goalMet` instead: met at entry's anchor\n const legacy = result as { goalMet?: boolean };\n const goalAt =\n typeof result.goalAt === 'number'\n ? result.goalAt\n : legacy.goalMet === true\n ? ctx.enteredAtMs\n : null;\n if (goalAt !== null) {\n state.convertedAt ??= goalAt;\n if (goal?.exitOnMatch ?? true) return { kind: 'goal' };\n }\n if (result.exitMet) return { kind: 'exit_when' };\n return null;\n}\n\n/**\n * The host's template check, with the instance and node added to the message:\n * a refused message must point at the journey that produced it. The error\n * keeps its class (and `name`), so adapters can stop retrying it.\n */\nfunction validate(\n node: Extract<NodeIR, { type: 'message' }>,\n props: Record<string, ScalarIR | undefined>,\n ctx: JourneyContext\n): Record<string, ScalarIR | undefined> {\n if (ctx.validator === undefined) return props;\n try {\n return ctx.validator.validate({ channel: node.channel, template: node.template, props });\n } catch (error) {\n if (error instanceof Error) {\n error.message = `[${ctx.instanceId} ${node.id}] ${error.message}`;\n }\n throw error;\n }\n}\n\n/**\n * A push goes to devices, not to a profile address: one copy per device the\n * host's directory lists (or only the most recent, `devices: 'latest'`). Each\n * copy's idempotency key carries its token, so a retried step that already\n * reached some devices re-sends to the rest and the senders drop the repeats —\n * keyed by token rather than position, because the list can change between\n * attempts. A user with no device is skipped, not failed: being unreachable is\n * a state of the user, and `reachable('push')` is how a journey branches on it.\n */\n/**\n * A push: one step settles what goes out and to which devices, then each device gets a step of\n * its own — a failure on one device retries that device alone, and the copies already delivered\n * are not sent again (APNs and FCM carry the idempotency key but do not de-duplicate on it).\n */\nasync function sendPush(\n node: Extract<NodeIR, { type: 'message' }>,\n ir: WorkflowIR,\n ctx: JourneyContext\n): Promise<void> {\n const plan = await ctx.step.do(`${node.id}:send`, async () => {\n const props = validate(node, await resolveValues(node.props, ctx.facts), ctx);\n if (await ctx.facts.isSuppressed?.(node.channel)) return null;\n const listed = await ctx.facts.listDevices();\n const devices = node.devices === 'latest' ? listed.slice(0, 1) : listed;\n return { props, devices };\n });\n // null: the channel is opted out. Undefined: a checkpoint from before per-device steps, whose\n // single step already sent to every device.\n if (plan === null || plan === undefined) return;\n if (plan.devices.length === 0) {\n console.log(`[journey] push ${node.template} skipped: no device for user ${ctx.userId}`);\n return;\n }\n for (const [index, device] of plan.devices.entries()) {\n await ctx.step.do(`${node.id}:send:${index}`, async () => {\n await deliver(\n {\n workflow: ir.id,\n instanceId: ctx.instanceId,\n nodeId: node.id,\n channel: 'push',\n template: node.template,\n props: plan.props,\n userId: ctx.userId,\n recipient: device.token,\n device,\n idempotencyKey: `${ctx.instanceId}:${node.id}:${device.token}`,\n },\n ctx\n );\n });\n }\n}\n\nasync function runNode(node: NodeIR, ir: WorkflowIR, ctx: JourneyContext): Promise<FlowSignal> {\n switch (node.type) {\n case 'message': {\n if (node.channel === 'push') {\n await sendPush(node, ir, ctx);\n return FALL_THROUGH;\n }\n await ctx.step.do(`${node.id}:send`, async () => {\n const props = validate(node, await resolveValues(node.props, ctx.facts), ctx);\n // An opted-out channel (an email unsubscribe) is skipped, like a push with no device\n if (await ctx.facts.isSuppressed?.(node.channel)) return;\n const recipientPath = RECIPIENT_PROPERTY[node.channel];\n const recipient =\n recipientPath === undefined ? undefined : await ctx.facts.getProperty(recipientPath);\n await deliver(\n {\n workflow: ir.id,\n instanceId: ctx.instanceId,\n nodeId: node.id,\n channel: node.channel,\n template: node.template,\n props,\n userId: ctx.userId,\n recipient: recipient === undefined ? undefined : String(recipient),\n idempotencyKey: `${ctx.instanceId}:${node.id}`,\n },\n ctx\n );\n });\n return FALL_THROUGH;\n }\n\n case 'delay': {\n await ctx.step.sleep(node.id, node.duration.ms);\n return (await checkGuards(node.id, ir, ctx)) ?? FALL_THROUGH;\n }\n\n case 'random_delay': {\n // The random value is drawn and persisted inside the step, so replay does not re-roll it\n const ms = await ctx.step.do(`${node.id}:roll`, async () => {\n const span = node.max.ms - node.min.ms;\n return node.min.ms + Math.floor(Math.random() * Math.max(span, 0));\n });\n await ctx.step.sleep(`${node.id}:sleep`, ms);\n return (await checkGuards(node.id, ir, ctx)) ?? FALL_THROUGH;\n }\n\n case 'time_window': {\n const target = await ctx.step.do(`${node.id}:target`, async () => {\n const tz =\n node.tz === 'user'\n ? String((await ctx.facts.getProperty(TIMEZONE_PROPERTY)) ?? 'UTC')\n : node.tz;\n return nextWindowStart(Date.now(), node.days, node.between, tz);\n });\n if (target === null) return FALL_THROUGH; // already inside the window: no suspension, no guard\n await ctx.step.sleepUntil(`${node.id}:sleep`, target);\n return (await checkGuards(node.id, ir, ctx)) ?? FALL_THROUGH;\n }\n\n case 'wait_until': {\n const met = await waitUntil(node, ctx);\n // Time passed while waiting — guard before acting on the outcome (goal wins over the wait result)\n const guard = await checkGuards(node.id, ir, ctx);\n if (guard) return guard;\n if (met) return FALL_THROUGH;\n const onTimeout = node.onTimeout;\n if (onTimeout === 'continue') return FALL_THROUGH;\n if (onTimeout === 'exit') return { kind: 'exit', reason: `${node.id}:timeout` };\n return runFlow(onTimeout, ir, ctx);\n }\n\n case 'branch': {\n const matched = await ctx.step.do(`${node.id}:eval`, async () => {\n const now = Date.now();\n for (const [i, branchCase] of node.cases.entries()) {\n if (await evaluateCondition(branchCase.condition, ctx.facts, now)) {\n return i;\n }\n }\n return -1; // no match → default arm, or simply continue\n });\n const flow = matched >= 0 ? node.cases[matched]?.flow : node.otherwise;\n if (flow === undefined) return FALL_THROUGH;\n const signal = await runFlow(flow, ir, ctx);\n // Structural rejoin: once an arm falls through, execution resumes after the branch\n return signal;\n }\n\n case 'filter': {\n const pass = await ctx.step.do(`${node.id}:eval`, async () =>\n evaluateCondition(node.condition, ctx.facts, Date.now())\n );\n return pass ? FALL_THROUGH : { kind: 'exit', reason: node.reason ?? `${node.id}:filtered` };\n }\n\n case 'cohort': {\n // Deterministic bucketing, so replay needs no checkpoint. The explicit\n // experiment key (when set) keeps the assignment stable across workflow\n // versions; the structural id fallback is fine for one-off splits.\n const bucket = hashToBucket(`${ctx.userId}:${node.key ?? node.id}`);\n // Integer basis points on both sides (the DSL guarantees weights sit on\n // the 0.01% grid), so no float ever meets the comparison.\n let cumulative = 0;\n for (const arm of node.arms) {\n cumulative += Math.round(arm.weight * 100);\n if (bucket < cumulative) return runFlow(arm.flow, ir, ctx);\n }\n return FALL_THROUGH;\n }\n\n case 'exit':\n return node.reason !== undefined ? { kind: 'exit', reason: node.reason } : { kind: 'exit' };\n\n case 'send_event': {\n await ctx.step.do(`${node.id}:emit`, async () => {\n const payload = await resolveValues(node.payload, ctx.facts);\n try {\n await ctx.events.emit(node.event, payload, `${ctx.instanceId}:${node.id}`);\n } catch (error) {\n // A refused event must point at the journey that emitted it\n if (error instanceof Error)\n error.message = `[${ctx.instanceId} ${node.id}] ${error.message}`;\n throw error;\n }\n });\n return FALL_THROUGH;\n }\n\n case 'action': {\n // Custom code runs inside step.do: retried on throw, checkpointed on success\n const invoker = ctx.actions;\n if (invoker === undefined) {\n throw new Error(\n `action '${node.action}': this runtime has no ActionInvoker configured — register the action on the adapter (see engine/actions.ts)`\n );\n }\n await ctx.step.do(`${node.id}:run`, async () => {\n const args = await resolveValues(node.args, ctx.facts);\n await invoker.invoke({\n action: node.action,\n codeHash: node.codeHash,\n args,\n userId: ctx.userId,\n idempotencyKey: `${ctx.instanceId}:${node.id}`,\n });\n });\n return FALL_THROUGH;\n }\n }\n}\n\nasync function waitUntil(\n node: Extract<NodeIR, { type: 'wait_until' }>,\n ctx: JourneyContext\n): Promise<boolean> {\n const deadline = await ctx.step.do(\n `${node.id}:deadline`,\n async () => Date.now() + node.timeout.ms\n );\n const events = await ctx.step.do(`${node.id}:events`, async () =>\n relevantEvents(node.condition, ctx.facts)\n );\n // Anchor at the moment the wait began (deadline is checkpointed, so this is\n // replay-stable): `performed` inside the condition only counts events that\n // happen during the wait — pre-existing history must not satisfy it.\n const anchorMs = deadline - node.timeout.ms;\n const met = await waitLoop(node.id, ctx, deadline, events, async (now) =>\n (await evaluateCondition(node.condition, ctx.facts, now, { anchorMs })) ? true : null\n );\n await ctx.step.unsubscribe(`${node.id}:unsubscribe`);\n return met === true;\n}\n\n/**\n * The conversion window outlives the flow: a journey whose steps are done while its goal's\n * `within` is still open keeps watching until the window closes — the purchase that follows the\n * last offer email is that email's conversion, and ending the instance at the email would report\n * it as a plain completion. `exitWhen` still applies while it waits. Nothing to wait for without\n * a window, or once the instance has converted.\n */\nasync function awaitTail(ir: WorkflowIR, ctx: JourneyContext): Promise<FlowSignal | null> {\n const deadline = goalDeadline(ir, ctx);\n const goal = ir.goal;\n if (goal === undefined || deadline === undefined) return null;\n const state = goalState(ctx);\n if (state.convertedAt !== undefined) return null;\n const open = await ctx.step.do('goal:open', async () => deadline > Date.now());\n if (!open) return null;\n const exitWhen = ir.exitWhen;\n const events = await ctx.step.do('goal:events', async () => [\n ...new Set([\n ...(await relevantEvents(goal.condition, ctx.facts)),\n ...(exitWhen === undefined ? [] : await relevantEvents(exitWhen, ctx.facts)),\n ]),\n ]);\n const hit = await waitLoop('goal', ctx, deadline, events, async (now) => {\n const reached = await goalReached(ir, ctx, now);\n if (reached !== null) return { kind: 'goal' as const, at: reached.at };\n if (exitWhen !== undefined && (await evaluateCondition(exitWhen, ctx.facts, now))) {\n return { kind: 'exit_when' as const, at: now };\n }\n return null;\n });\n await ctx.step.unsubscribe('goal:unsubscribe');\n if (hit === null) return null;\n // A checkpoint from before the tail watched exitWhen recorded a plain `true`: the goal\n if ((hit as unknown) === true || hit.kind === 'goal') {\n state.convertedAt ??= (hit as { at?: number }).at ?? deadline;\n return { kind: 'goal' };\n }\n return { kind: 'exit_when' };\n}\n\n/**\n * Wait until `check` finds something or the deadline passes: subscribe, check, sleep until a\n * relevant event wakes the instance or time runs out. `check` runs inside a step, so its result\n * must be serializable; null means \"not yet\".\n */\nasync function waitLoop<R>(\n id: string,\n ctx: JourneyContext,\n deadline: number,\n events: string[],\n check: (now: number) => Promise<R | null>\n): Promise<R | null> {\n for (let attempt = 0; attempt < MAX_WAIT_CHECKS; attempt++) {\n // Subscribe before the check, every attempt: one subscribe arms at least\n // one wake-up (AWS callbacks are consumed on delivery, so each attempt\n // needs a fresh arm; Cloudflare rows make the repeat call idempotent), and\n // arming first closes the race where the event lands between the check and\n // the registration.\n await ctx.step.subscribe(`${id}:subscribe:${attempt}`, events);\n\n const met = await ctx.step.do(`${id}:check:${attempt}`, async () => check(Date.now()));\n // `false` is a checkpoint from before checks answered null for \"not yet\"\n if (met !== null && (met as unknown) !== false) return met;\n\n // Checkpointed: replay re-runs this control flow with wall-clock time far\n // ahead of the original run, so the branch must come from a persisted value.\n const remaining = await ctx.step.do(\n `${id}:remaining:${attempt}`,\n async () => deadline - Date.now()\n );\n // The condition was checked just above — expiring here needs no extra final check\n if (remaining <= 0) return null;\n\n const wake = await ctx.step.waitForWake(`${id}:wait:${attempt}`, remaining);\n if (wake === 'timeout') {\n // Final check after timeout: in a tight race the event may land exactly as the timer fires\n const final = await ctx.step.do(`${id}:final`, async () => check(Date.now()));\n return (final as unknown) === false ? null : final;\n }\n // Woken → back to the top of the loop to re-evaluate (at-least-once wake-up semantics)\n }\n return null; // check budget exhausted → treated as timed out (see MAX_WAIT_CHECKS)\n}\n\nasync function resolveValues(\n values: Record<string, PropValueIR>,\n facts: FactSource\n): Promise<Record<string, ScalarIR | undefined>> {\n const resolved: Record<string, ScalarIR | undefined> = {};\n for (const [key, value] of Object.entries(values)) {\n resolved[key] = typeof value === 'object' ? await facts.getProperty(value.path) : value;\n }\n return resolved;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"interpreter.mjs","names":[],"sources":["../../src/engine/interpreter.ts"],"sourcesContent":["import type { NodeIR, PropValueIR, ScalarIR, WorkflowIR } from '../ir';\nimport { hashToBucket } from './bucket';\nimport { evaluateCondition, relevantEvents } from './condition';\nimport type {\n FactSource,\n JourneyContext,\n JourneyOutcome,\n OutboundMessage,\n PushDevice,\n} from './ports';\nimport { RECIPIENT_PROPERTY } from './recipient';\nimport { nextWindowStart } from './time-window';\n\n/**\n * IR interpreter: the execution core for one user's journey instance.\n *\n * Pure logic — it imports nothing platform-specific; every external effect\n * arrives through the JourneyContext ports, and durable semantics\n * (checkpoint/replay) are the EngineStep adapter's job.\n *\n * Step naming: an IR node id *is* the step name (derived from the structural\n * path, so it is deterministic and unique). When one node needs several steps,\n * a suffix is appended (`${id}:guard`, `${id}:check:${n}`).\n *\n * Guard semantics: goal / exitWhen are checked at journey entry and after\n * every suspension point (delay / random_delay / time_window / wait_until) —\n * the moments where wall-clock time passed and the user's state may have\n * moved. Between two synchronous nodes no meaningful time passes, so checking\n * there would only burn step budget (each checkpoint costs a state write).\n */\n\n/**\n * Cap on wait_until re-evaluations: beyond it we treat the wait as timed out,\n * so a wake-up storm cannot exhaust the platform's step budget. Each loop\n * iteration costs a handful of steps, so 64 checks stay well inside the\n * platform step budgets while absorbing far more wake churn than 16 did.\n */\nconst MAX_WAIT_CHECKS = 64;\n\n/** The user property holding an IANA timezone id, read when a time_window says tz: 'user'. Same convention-table status as RECIPIENT_PROPERTY. */\nconst TIMEZONE_PROPERTY = 'timezone';\n\ntype FlowSignal =\n | { kind: 'fall_through' }\n | { kind: 'exit'; reason?: string }\n | { kind: 'goal' }\n | { kind: 'exit_when' };\n\nconst FALL_THROUGH: FlowSignal = { kind: 'fall_through' };\n\n/**\n * Whether and when the instance converted — a fact of its own, apart from how it ends: with\n * `exitOnMatch: false` a conversion lets the flow go on, and the instance still ends `completed`\n * (or `exited`, `exit_when`) carrying it. Set only from checkpointed step results, so a replay\n * rebuilds the same state.\n */\ninterface GoalState {\n convertedAt?: number;\n}\n\nconst goalStates = new WeakMap<JourneyContext, GoalState>();\n\nfunction goalState(ctx: JourneyContext): GoalState {\n let state = goalStates.get(ctx);\n if (state === undefined) {\n state = {};\n goalStates.set(ctx, state);\n }\n return state;\n}\n\n/** When the goal's attribution window closes, measured from entry; none without `within`. */\nfunction goalDeadline(ir: WorkflowIR, ctx: JourneyContext): number | undefined {\n return ir.goal?.within === undefined ? undefined : ctx.enteredAtMs + ir.goal.within.ms;\n}\n\n/**\n * Whether the goal holds for events inside its window: after entry, and not after the window\n * closed — an instance resumed past the deadline must not count a purchase made after it. The\n * conversion time is when the check saw it, capped at the deadline.\n */\nasync function goalReached(\n ir: WorkflowIR,\n ctx: JourneyContext,\n now: number\n): Promise<{ at: number } | null> {\n const goal = ir.goal;\n if (goal === undefined) return null;\n const deadline = goalDeadline(ir, ctx);\n const met = await evaluateCondition(goal.condition, ctx.facts, now, {\n anchorMs: ctx.enteredAtMs,\n ...(deadline === undefined ? {} : { untilMs: deadline }),\n });\n return met ? { at: deadline === undefined ? now : Math.min(now, deadline) } : null;\n}\n\nexport async function runJourney(ir: WorkflowIR, ctx: JourneyContext): Promise<JourneyOutcome> {\n await started(ir, ctx);\n const entry = await checkGuards('entry', ir, ctx);\n let signal = entry ?? (await runFlow(ir.flow, ir, ctx));\n // The flow is done but the conversion window is not: keep watching it (see awaitTail)\n if (signal.kind === 'fall_through') signal = (await awaitTail(ir, ctx)) ?? FALL_THROUGH;\n const { convertedAt } = goalState(ctx);\n const converted = convertedAt === undefined ? {} : { convertedAt };\n switch (signal.kind) {\n case 'fall_through':\n return { status: 'completed', ...converted };\n case 'exit':\n return signal.reason !== undefined\n ? { status: 'exited', reason: signal.reason, ...converted }\n : { status: 'exited', ...converted };\n case 'goal':\n return { status: 'goal', ...converted };\n case 'exit_when':\n return { status: 'exit_when', ...converted };\n }\n}\n\nasync function runFlow(nodes: NodeIR[], ir: WorkflowIR, ctx: JourneyContext): Promise<FlowSignal> {\n for (const node of nodes) {\n await reached(node, ir, ctx);\n const signal = await runNode(node, ir, ctx);\n if (signal.kind !== 'fall_through') return signal;\n }\n return FALL_THROUGH;\n}\n\n/**\n * Tell the observer the instance entered — before the entry guards, which can end it before the\n * first node: an instance is counted from its entry, not from its first node.\n */\nasync function started(ir: WorkflowIR, ctx: JourneyContext): Promise<void> {\n const observer = ctx.observer;\n if (observer?.started === undefined) return;\n await ctx.step.do('entry:started', async () => {\n await observer.started?.({\n workflow: ir.id,\n instanceId: ctx.instanceId,\n userId: ctx.userId,\n at: ctx.enteredAtMs,\n });\n });\n}\n\n/**\n * Tell the observer the instance is at `node`. A step of its own, so a replay does not report\n * the nodes it passes on the way back again, and the arrival time stays the first one.\n */\nasync function reached(node: NodeIR, ir: WorkflowIR, ctx: JourneyContext): Promise<void> {\n const observer = ctx.observer;\n if (observer?.nodeReached === undefined) return;\n await ctx.step.do(`${node.id}:reached`, async () => {\n await observer.nodeReached?.({\n workflow: ir.id,\n instanceId: ctx.instanceId,\n userId: ctx.userId,\n nodeId: node.id,\n at: Date.now(),\n });\n });\n}\n\n/**\n * One copy out through the host's sender, with the observer on both sides: it may attach\n * tracking before, and hears how the transport took the copy after. A send that throws is\n * not reported — the step retries it, and reports when it settles. A report that fails after\n * the send is logged, never thrown: the step would retry and the transport — a webhook, a web\n * push, with no idempotency key of its own — would deliver the copy again.\n */\nasync function deliver(message: OutboundMessage, ctx: JourneyContext): Promise<void> {\n const observer = ctx.observer;\n const tracking = await observer?.messagePrepared?.(message);\n const copy = tracking === undefined ? message : { ...message, tracking };\n const result = await ctx.messages.send(copy);\n try {\n await observer?.messageSent?.(copy, result ?? { delivered: true });\n } catch (error) {\n console.error(`[workflow] reporting ${copy.idempotencyKey} as sent failed`, error);\n }\n}\n\n/**\n * Goal / exitWhen guards (see the header for when they run).\n *\n * The goal condition is anchored at workflow entry: a `performed` inside it\n * only counts events after the user entered, so pre-existing history cannot\n * register as a conversion. exitWhen is deliberately unanchored — \"user\n * unsubscribed\" should exit no matter when it happened.\n */\nasync function checkGuards(\n stepId: string,\n ir: WorkflowIR,\n ctx: JourneyContext\n): Promise<FlowSignal | null> {\n const goal = ir.goal;\n const exitWhen = ir.exitWhen;\n if (goal === undefined && exitWhen === undefined) return null;\n\n const state = goalState(ctx);\n const result = await ctx.step.do(`${stepId}:guard`, async () => {\n const now = Date.now();\n // Already converted (exitOnMatch: false): the conversion stands, no need to look again\n const goalHit =\n goal !== undefined && state.convertedAt === undefined\n ? await goalReached(ir, ctx, now)\n : null;\n const exitMet = exitWhen !== undefined && (await evaluateCondition(exitWhen, ctx.facts, now));\n return { goalAt: goalHit?.at ?? null, exitMet };\n });\n\n // A checkpoint from before conversions carried a time held `goalMet` instead: met at entry's anchor\n const legacy = result as { goalMet?: boolean };\n const goalAt =\n typeof result.goalAt === 'number'\n ? result.goalAt\n : legacy.goalMet === true\n ? ctx.enteredAtMs\n : null;\n if (goalAt !== null) {\n state.convertedAt ??= goalAt;\n if (goal?.exitOnMatch ?? true) return { kind: 'goal' };\n }\n if (result.exitMet) return { kind: 'exit_when' };\n return null;\n}\n\n/**\n * The host's template check, with the instance and node added to the message:\n * a refused message must point at the journey that produced it. The error\n * keeps its class (and `name`), so adapters can stop retrying it.\n */\nfunction validate(\n node: Extract<NodeIR, { type: 'message' }>,\n props: Record<string, ScalarIR | undefined>,\n ctx: JourneyContext\n): Record<string, ScalarIR | undefined> {\n if (ctx.validator === undefined) return props;\n try {\n return ctx.validator.validate({ channel: node.channel, template: node.template, props });\n } catch (error) {\n if (error instanceof Error) {\n error.message = `[${ctx.instanceId} ${node.id}] ${error.message}`;\n }\n throw error;\n }\n}\n\ntype PushPlan = { props: Record<string, ScalarIR | undefined>; devices: PushDevice[] };\n\n/**\n * A push goes to devices, not to a profile address: one copy per device the\n * host's directory lists (or only the most recent, `devices: 'latest'`). Each\n * copy's idempotency key carries its token, so a retried step that already\n * reached some devices re-sends to the rest and the senders drop the repeats —\n * keyed by token rather than position, because the list can change between\n * attempts. A user with no device is skipped, not failed: being unreachable is\n * a state of the user, and `reachable('push')` is how a journey branches on it.\n */\n/**\n * A push: one step settles what goes out and to which devices, then each device gets a step of\n * its own — a failure on one device retries that device alone, and the copies already delivered\n * are not sent again (APNs and FCM carry the idempotency key but do not de-duplicate on it).\n */\nasync function sendPush(\n node: Extract<NodeIR, { type: 'message' }>,\n ir: WorkflowIR,\n ctx: JourneyContext\n): Promise<void> {\n const plan = await ctx.step.do<PushPlan | null | undefined>(`${node.id}:send`, async () => {\n const props = validate(node, await resolveValues(node.props, ctx.facts), ctx);\n if (await ctx.facts.isSuppressed?.(node.channel)) return null;\n const listed = await ctx.facts.listDevices();\n const devices = node.devices === 'latest' ? listed.slice(0, 1) : listed;\n return { props, devices };\n });\n // null: the channel is opted out. Undefined: a checkpoint from before per-device steps, whose\n // single step already sent to every device.\n if (plan === null || plan === undefined) return;\n if (plan.devices.length === 0) {\n console.log(`[journey] push ${node.template} skipped: no device for user ${ctx.userId}`);\n return;\n }\n for (const [index, device] of plan.devices.entries()) {\n await ctx.step.do(`${node.id}:send:${index}`, async () => {\n await deliver(\n {\n workflow: ir.id,\n instanceId: ctx.instanceId,\n nodeId: node.id,\n channel: 'push',\n template: node.template,\n props: plan.props,\n userId: ctx.userId,\n recipient: device.token,\n device,\n idempotencyKey: `${ctx.instanceId}:${node.id}:${device.token}`,\n },\n ctx\n );\n });\n }\n}\n\nasync function runNode(node: NodeIR, ir: WorkflowIR, ctx: JourneyContext): Promise<FlowSignal> {\n switch (node.type) {\n case 'message': {\n if (node.channel === 'push') {\n await sendPush(node, ir, ctx);\n return FALL_THROUGH;\n }\n await ctx.step.do(`${node.id}:send`, async () => {\n const props = validate(node, await resolveValues(node.props, ctx.facts), ctx);\n // An opted-out channel (an email unsubscribe) is skipped, like a push with no device\n if (await ctx.facts.isSuppressed?.(node.channel)) return;\n const recipientPath = RECIPIENT_PROPERTY[node.channel];\n const recipient =\n recipientPath === undefined ? undefined : await ctx.facts.getProperty(recipientPath);\n await deliver(\n {\n workflow: ir.id,\n instanceId: ctx.instanceId,\n nodeId: node.id,\n channel: node.channel,\n template: node.template,\n props,\n userId: ctx.userId,\n recipient: recipient === undefined ? undefined : String(recipient),\n idempotencyKey: `${ctx.instanceId}:${node.id}`,\n },\n ctx\n );\n });\n return FALL_THROUGH;\n }\n\n case 'delay': {\n await ctx.step.sleep(node.id, node.duration.ms);\n return (await checkGuards(node.id, ir, ctx)) ?? FALL_THROUGH;\n }\n\n case 'random_delay': {\n // The random value is drawn and persisted inside the step, so replay does not re-roll it\n const ms = await ctx.step.do(`${node.id}:roll`, async () => {\n const span = node.max.ms - node.min.ms;\n return node.min.ms + Math.floor(Math.random() * Math.max(span, 0));\n });\n await ctx.step.sleep(`${node.id}:sleep`, ms);\n return (await checkGuards(node.id, ir, ctx)) ?? FALL_THROUGH;\n }\n\n case 'time_window': {\n const target = await ctx.step.do(`${node.id}:target`, async () => {\n const tz =\n node.tz === 'user'\n ? String((await ctx.facts.getProperty(TIMEZONE_PROPERTY)) ?? 'UTC')\n : node.tz;\n return nextWindowStart(Date.now(), node.days, node.between, tz);\n });\n if (target === null) return FALL_THROUGH; // already inside the window: no suspension, no guard\n await ctx.step.sleepUntil(`${node.id}:sleep`, target);\n return (await checkGuards(node.id, ir, ctx)) ?? FALL_THROUGH;\n }\n\n case 'wait_until': {\n const met = await waitUntil(node, ctx);\n // Time passed while waiting — guard before acting on the outcome (goal wins over the wait result)\n const guard = await checkGuards(node.id, ir, ctx);\n if (guard) return guard;\n if (met) return FALL_THROUGH;\n const onTimeout = node.onTimeout;\n if (onTimeout === 'continue') return FALL_THROUGH;\n if (onTimeout === 'exit') return { kind: 'exit', reason: `${node.id}:timeout` };\n return runFlow(onTimeout, ir, ctx);\n }\n\n case 'branch': {\n const matched = await ctx.step.do(`${node.id}:eval`, async () => {\n const now = Date.now();\n for (const [i, branchCase] of node.cases.entries()) {\n if (await evaluateCondition(branchCase.condition, ctx.facts, now)) {\n return i;\n }\n }\n return -1; // no match → default arm, or simply continue\n });\n const flow = matched >= 0 ? node.cases[matched]?.flow : node.otherwise;\n if (flow === undefined) return FALL_THROUGH;\n const signal = await runFlow(flow, ir, ctx);\n // Structural rejoin: once an arm falls through, execution resumes after the branch\n return signal;\n }\n\n case 'filter': {\n const pass = await ctx.step.do(`${node.id}:eval`, async () =>\n evaluateCondition(node.condition, ctx.facts, Date.now())\n );\n return pass ? FALL_THROUGH : { kind: 'exit', reason: node.reason ?? `${node.id}:filtered` };\n }\n\n case 'cohort': {\n // Deterministic bucketing, so replay needs no checkpoint. The explicit\n // experiment key (when set) keeps the assignment stable across workflow\n // versions; the structural id fallback is fine for one-off splits.\n const bucket = hashToBucket(`${ctx.userId}:${node.key ?? node.id}`);\n // Integer basis points on both sides (the DSL guarantees weights sit on\n // the 0.01% grid), so no float ever meets the comparison.\n let cumulative = 0;\n for (const arm of node.arms) {\n cumulative += Math.round(arm.weight * 100);\n if (bucket < cumulative) return runFlow(arm.flow, ir, ctx);\n }\n return FALL_THROUGH;\n }\n\n case 'exit':\n return node.reason !== undefined ? { kind: 'exit', reason: node.reason } : { kind: 'exit' };\n\n case 'send_event': {\n await ctx.step.do(`${node.id}:emit`, async () => {\n const payload = await resolveValues(node.payload, ctx.facts);\n try {\n await ctx.events.emit(node.event, payload, `${ctx.instanceId}:${node.id}`);\n } catch (error) {\n // A refused event must point at the journey that emitted it\n if (error instanceof Error)\n error.message = `[${ctx.instanceId} ${node.id}] ${error.message}`;\n throw error;\n }\n });\n return FALL_THROUGH;\n }\n\n case 'action': {\n // Custom code runs inside step.do: retried on throw, checkpointed on success\n const invoker = ctx.actions;\n if (invoker === undefined) {\n throw new Error(\n `action '${node.action}': this runtime has no ActionInvoker configured — register the action on the adapter (see engine/actions.ts)`\n );\n }\n await ctx.step.do(`${node.id}:run`, async () => {\n const args = await resolveValues(node.args, ctx.facts);\n await invoker.invoke({\n action: node.action,\n codeHash: node.codeHash,\n args,\n userId: ctx.userId,\n idempotencyKey: `${ctx.instanceId}:${node.id}`,\n });\n });\n return FALL_THROUGH;\n }\n }\n}\n\nasync function waitUntil(\n node: Extract<NodeIR, { type: 'wait_until' }>,\n ctx: JourneyContext\n): Promise<boolean> {\n const deadline = await ctx.step.do(\n `${node.id}:deadline`,\n async () => Date.now() + node.timeout.ms\n );\n const events = await ctx.step.do(`${node.id}:events`, async () =>\n relevantEvents(node.condition, ctx.facts)\n );\n // Anchor at the moment the wait began (deadline is checkpointed, so this is\n // replay-stable): `performed` inside the condition only counts events that\n // happen during the wait — pre-existing history must not satisfy it.\n const anchorMs = deadline - node.timeout.ms;\n const met = await waitLoop(node.id, ctx, deadline, events, async (now) =>\n (await evaluateCondition(node.condition, ctx.facts, now, { anchorMs })) ? true : null\n );\n await ctx.step.unsubscribe(`${node.id}:unsubscribe`);\n return met === true;\n}\n\n/**\n * The conversion window outlives the flow: a journey whose steps are done while its goal's\n * `within` is still open keeps watching until the window closes — the purchase that follows the\n * last offer email is that email's conversion, and ending the instance at the email would report\n * it as a plain completion. `exitWhen` still applies while it waits. Nothing to wait for without\n * a window, or once the instance has converted.\n */\nasync function awaitTail(ir: WorkflowIR, ctx: JourneyContext): Promise<FlowSignal | null> {\n const deadline = goalDeadline(ir, ctx);\n const goal = ir.goal;\n if (goal === undefined || deadline === undefined) return null;\n const state = goalState(ctx);\n if (state.convertedAt !== undefined) return null;\n const open = await ctx.step.do('goal:open', async () => deadline > Date.now());\n if (!open) return null;\n const exitWhen = ir.exitWhen;\n const events = await ctx.step.do('goal:events', async () => [\n ...new Set([\n ...(await relevantEvents(goal.condition, ctx.facts)),\n ...(exitWhen === undefined ? [] : await relevantEvents(exitWhen, ctx.facts)),\n ]),\n ]);\n const hit = await waitLoop('goal', ctx, deadline, events, async (now) => {\n const reached = await goalReached(ir, ctx, now);\n if (reached !== null) return { kind: 'goal' as const, at: reached.at };\n if (exitWhen !== undefined && (await evaluateCondition(exitWhen, ctx.facts, now))) {\n return { kind: 'exit_when' as const, at: now };\n }\n return null;\n });\n await ctx.step.unsubscribe('goal:unsubscribe');\n if (hit === null) return null;\n // A checkpoint from before the tail watched exitWhen recorded a plain `true`: the goal\n if ((hit as unknown) === true || hit.kind === 'goal') {\n state.convertedAt ??= (hit as { at?: number }).at ?? deadline;\n return { kind: 'goal' };\n }\n return { kind: 'exit_when' };\n}\n\n/**\n * Wait until `check` finds something or the deadline passes: subscribe, check, sleep until a\n * relevant event wakes the instance or time runs out. `check` runs inside a step, so its result\n * must be serializable; null means \"not yet\".\n */\nasync function waitLoop<R>(\n id: string,\n ctx: JourneyContext,\n deadline: number,\n events: string[],\n check: (now: number) => Promise<R | null>\n): Promise<R | null> {\n for (let attempt = 0; attempt < MAX_WAIT_CHECKS; attempt++) {\n // Subscribe before the check, every attempt: one subscribe arms at least\n // one wake-up (AWS callbacks are consumed on delivery, so each attempt\n // needs a fresh arm; Cloudflare rows make the repeat call idempotent), and\n // arming first closes the race where the event lands between the check and\n // the registration.\n await ctx.step.subscribe(`${id}:subscribe:${attempt}`, events);\n\n const met = await ctx.step.do(`${id}:check:${attempt}`, async () => check(Date.now()));\n // `false` is a checkpoint from before checks answered null for \"not yet\"\n if (met !== null && (met as unknown) !== false) return met;\n\n // Checkpointed: replay re-runs this control flow with wall-clock time far\n // ahead of the original run, so the branch must come from a persisted value.\n const remaining = await ctx.step.do(\n `${id}:remaining:${attempt}`,\n async () => deadline - Date.now()\n );\n // The condition was checked just above — expiring here needs no extra final check\n if (remaining <= 0) return null;\n\n const wake = await ctx.step.waitForWake(`${id}:wait:${attempt}`, remaining);\n if (wake === 'timeout') {\n // Final check after timeout: in a tight race the event may land exactly as the timer fires\n const final = await ctx.step.do(`${id}:final`, async () => check(Date.now()));\n return (final as unknown) === false ? null : final;\n }\n // Woken → back to the top of the loop to re-evaluate (at-least-once wake-up semantics)\n }\n return null; // check budget exhausted → treated as timed out (see MAX_WAIT_CHECKS)\n}\n\nasync function resolveValues(\n values: Record<string, PropValueIR>,\n facts: FactSource\n): Promise<Record<string, ScalarIR | undefined>> {\n const resolved: Record<string, ScalarIR | undefined> = {};\n for (const [key, value] of Object.entries(values)) {\n resolved[key] = typeof value === 'object' ? await facts.getProperty(value.path) : value;\n }\n return resolved;\n}\n"],"mappings":"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"}
@@ -93,14 +93,19 @@ export interface OutboundMessage {
93
93
  /** What the host's observer asked the sender to add for measuring this copy (see `MessageTracking`). */
94
94
  tracking?: MessageTracking | undefined;
95
95
  }
96
+ /** What following a tracked email link does: redirect to it, or confirm an unsubscribe. */
97
+ export type LinkPurpose = 'link' | 'unsubscribe';
96
98
  /**
97
99
  * Measuring a copy, as the built-in senders apply it: an email's links are routed through
98
100
  * `link` and `pixel` is appended as a 1×1 image; a push carries `data` in its payload.
99
101
  * Each is optional, and a sender ignores what its channel has no use for.
100
102
  */
101
103
  export interface MessageTracking {
102
- /** The original `http(s)` URL in, the URL that records the click and redirects out. */
103
- link?: (url: string) => string;
104
+ /**
105
+ * The original `http(s)` URL in, the URL that records the click and redirects out — or, for the
106
+ * email's unsubscribe link, asks the recipient to confirm.
107
+ */
108
+ link?: (url: string, purpose: LinkPurpose) => string;
104
109
  /** Loaded when the email is opened. */
105
110
  pixel?: string;
106
111
  /** Extra string keys in the push payload. */
@@ -1 +1 @@
1
- {"version":3,"file":"ports.d.cts","names":[],"sources":["../../src/engine/ports.ts"],"mappings":";;;;;;;;;;;;;;;;iBAiBiB;;EAEf,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;;EAGnD,MAAM,cAAc,aAAa;;EAGjC,WAAW,cAAc,sBAAsB;;;;;;;;;;;;EAa/C,UAAU,cAAc,4BAA4B;;EAGpD,YAAY,eAAe;;;;;EAM3B,YAAY,cAAc,oBAAoB;;;iBAI/B;EACf;EACA;;;iBAIe;;;;;;EAMf,YACE,eACA;IAAS;IAAkB;IAAkB,QAAQ;MACpD;EACH,YAAY,eAAe,QAAQ;;EAEnC,oBAAoB,eAAe,QAAQ;;EAE3C,eAAe,QAAQ;;EAEvB,cAAc,SAAS,YAAY;;iBAGpB;;EAEf;EACA;EACA;EACA,SAAS;EACT;;EAEA,OAAO,eAAe;EACtB;;;;;;EAMA;;EAEA,SAAS;;;;;;EAMT;;EAEA,WAAW;;;;;;;iBAQI;;EAEf,QAAQ;;EAER;;EAEA,OAAO;;;;;;EAMP,eAAe;;;;;;;iBAQA;EACf;EACA;;iBAGe;EACf,KAAK,SAAS,kBAAkB,eAAe;;;;;;;;;iBAUhC;;EAEf,SAAS;IACP;IACA;IACA;IACA;MACE;;EAEJ,aAAa;IACX;IACA;IACA;IACA;IACA;MACE;;EAEJ,iBAAiB,SAAS,kBAAkB,QAAQ;;EAEpD,aAAa,SAAS,iBAAiB;IAAU;MAAoB,aAAa;;EAElF,UAAU;IACR;IACA;IACA;IACA,QAAQ;IACR;;IAEA;MACE;;;;;;;;;iBAUW;EACf,SAAS;IACP,SAAS;IACT;IACA,OAAO,eAAe;MACpB,eAAe;;;iBAIJ;;;;;;;;EAQf,KAAK,eAAe,SAAS,eAAe,uBAAuB,aAAa;;;iBAIjE;;EAEf;;EAEA;;EAEA,MAAM,eAAe;EACrB;;EAEA;;;;;;;iBAQe;EACf,OAAO,YAAY,mBAAmB;;iBAGvB;EACf;;EAEA;;;;;;;EAOA;EACA,MAAM;EACN,OAAO;EACP,UAAU;EACV,QAAQ;;EAER,UAAU;;EAEV,YAAY;;EAEZ,WAAW;;;;;;;YAQD;EACN;;EACA;EAAkB;;EAClB;;EACA;;EACA"}
1
+ {"version":3,"file":"ports.d.cts","names":[],"sources":["../../src/engine/ports.ts"],"mappings":";;;;;;;;;;;;;;;;iBAiBiB;;EAEf,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;;EAGnD,MAAM,cAAc,aAAa;;EAGjC,WAAW,cAAc,sBAAsB;;;;;;;;;;;;EAa/C,UAAU,cAAc,4BAA4B;;EAGpD,YAAY,eAAe;;;;;EAM3B,YAAY,cAAc,oBAAoB;;;iBAI/B;EACf;EACA;;;iBAIe;;;;;;EAMf,YACE,eACA;IAAS;IAAkB;IAAkB,QAAQ;MACpD;EACH,YAAY,eAAe,QAAQ;;EAEnC,oBAAoB,eAAe,QAAQ;;EAE3C,eAAe,QAAQ;;EAEvB,cAAc,SAAS,YAAY;;iBAGpB;;EAEf;EACA;EACA;EACA,SAAS;EACT;;EAEA,OAAO,eAAe;EACtB;;;;;;EAMA;;EAEA,SAAS;;;;;;EAMT;;EAEA,WAAW;;;YAID;;;;;;iBAOK;;;;;EAKf,QAAQ,aAAa,SAAS;;EAE9B;;EAEA,OAAO;;;;;;EAMP,eAAe;;;;;;;iBAQA;EACf;EACA;;iBAGe;EACf,KAAK,SAAS,kBAAkB,eAAe;;;;;;;;;iBAUhC;;EAEf,SAAS;IACP;IACA;IACA;IACA;MACE;;EAEJ,aAAa;IACX;IACA;IACA;IACA;IACA;MACE;;EAEJ,iBAAiB,SAAS,kBAAkB,QAAQ;;EAEpD,aAAa,SAAS,iBAAiB;IAAU;MAAoB,aAAa;;EAElF,UAAU;IACR;IACA;IACA;IACA,QAAQ;IACR;;IAEA;MACE;;;;;;;;;iBAUW;EACf,SAAS;IACP,SAAS;IACT;IACA,OAAO,eAAe;MACpB,eAAe;;;iBAIJ;;;;;;;;EAQf,KAAK,eAAe,SAAS,eAAe,uBAAuB,aAAa;;;iBAIjE;;EAEf;;EAEA;;EAEA,MAAM,eAAe;EACrB;;EAEA;;;;;;;iBAQe;EACf,OAAO,YAAY,mBAAmB;;iBAGvB;EACf;;EAEA;;;;;;;EAOA;EACA,MAAM;EACN,OAAO;EACP,UAAU;EACV,QAAQ;;EAER,UAAU;;EAEV,YAAY;;EAEZ,WAAW;;;;;;;YAQD;EACN;;EACA;EAAkB;;EAClB;;EACA;;EACA"}
@@ -93,14 +93,19 @@ export interface OutboundMessage {
93
93
  /** What the host's observer asked the sender to add for measuring this copy (see `MessageTracking`). */
94
94
  tracking?: MessageTracking | undefined;
95
95
  }
96
+ /** What following a tracked email link does: redirect to it, or confirm an unsubscribe. */
97
+ export type LinkPurpose = 'link' | 'unsubscribe';
96
98
  /**
97
99
  * Measuring a copy, as the built-in senders apply it: an email's links are routed through
98
100
  * `link` and `pixel` is appended as a 1×1 image; a push carries `data` in its payload.
99
101
  * Each is optional, and a sender ignores what its channel has no use for.
100
102
  */
101
103
  export interface MessageTracking {
102
- /** The original `http(s)` URL in, the URL that records the click and redirects out. */
103
- link?: (url: string) => string;
104
+ /**
105
+ * The original `http(s)` URL in, the URL that records the click and redirects out — or, for the
106
+ * email's unsubscribe link, asks the recipient to confirm.
107
+ */
108
+ link?: (url: string, purpose: LinkPurpose) => string;
104
109
  /** Loaded when the email is opened. */
105
110
  pixel?: string;
106
111
  /** Extra string keys in the push payload. */
@@ -1 +1 @@
1
- {"version":3,"file":"ports.d.mts","names":[],"sources":["../../src/engine/ports.ts"],"mappings":";;;;;;;;;;;;;;;;iBAiBiB;;EAEf,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;;EAGnD,MAAM,cAAc,aAAa;;EAGjC,WAAW,cAAc,sBAAsB;;;;;;;;;;;;EAa/C,UAAU,cAAc,4BAA4B;;EAGpD,YAAY,eAAe;;;;;EAM3B,YAAY,cAAc,oBAAoB;;;iBAI/B;EACf;EACA;;;iBAIe;;;;;;EAMf,YACE,eACA;IAAS;IAAkB;IAAkB,QAAQ;MACpD;EACH,YAAY,eAAe,QAAQ;;EAEnC,oBAAoB,eAAe,QAAQ;;EAE3C,eAAe,QAAQ;;EAEvB,cAAc,SAAS,YAAY;;iBAGpB;;EAEf;EACA;EACA;EACA,SAAS;EACT;;EAEA,OAAO,eAAe;EACtB;;;;;;EAMA;;EAEA,SAAS;;;;;;EAMT;;EAEA,WAAW;;;;;;;iBAQI;;EAEf,QAAQ;;EAER;;EAEA,OAAO;;;;;;EAMP,eAAe;;;;;;;iBAQA;EACf;EACA;;iBAGe;EACf,KAAK,SAAS,kBAAkB,eAAe;;;;;;;;;iBAUhC;;EAEf,SAAS;IACP;IACA;IACA;IACA;MACE;;EAEJ,aAAa;IACX;IACA;IACA;IACA;IACA;MACE;;EAEJ,iBAAiB,SAAS,kBAAkB,QAAQ;;EAEpD,aAAa,SAAS,iBAAiB;IAAU;MAAoB,aAAa;;EAElF,UAAU;IACR;IACA;IACA;IACA,QAAQ;IACR;;IAEA;MACE;;;;;;;;;iBAUW;EACf,SAAS;IACP,SAAS;IACT;IACA,OAAO,eAAe;MACpB,eAAe;;;iBAIJ;;;;;;;;EAQf,KAAK,eAAe,SAAS,eAAe,uBAAuB,aAAa;;;iBAIjE;;EAEf;;EAEA;;EAEA,MAAM,eAAe;EACrB;;EAEA;;;;;;;iBAQe;EACf,OAAO,YAAY,mBAAmB;;iBAGvB;EACf;;EAEA;;;;;;;EAOA;EACA,MAAM;EACN,OAAO;EACP,UAAU;EACV,QAAQ;;EAER,UAAU;;EAEV,YAAY;;EAEZ,WAAW;;;;;;;YAQD;EACN;;EACA;EAAkB;;EAClB;;EACA;;EACA"}
1
+ {"version":3,"file":"ports.d.mts","names":[],"sources":["../../src/engine/ports.ts"],"mappings":";;;;;;;;;;;;;;;;iBAiBiB;;EAEf,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;;EAGnD,MAAM,cAAc,aAAa;;EAGjC,WAAW,cAAc,sBAAsB;;;;;;;;;;;;EAa/C,UAAU,cAAc,4BAA4B;;EAGpD,YAAY,eAAe;;;;;EAM3B,YAAY,cAAc,oBAAoB;;;iBAI/B;EACf;EACA;;;iBAIe;;;;;;EAMf,YACE,eACA;IAAS;IAAkB;IAAkB,QAAQ;MACpD;EACH,YAAY,eAAe,QAAQ;;EAEnC,oBAAoB,eAAe,QAAQ;;EAE3C,eAAe,QAAQ;;EAEvB,cAAc,SAAS,YAAY;;iBAGpB;;EAEf;EACA;EACA;EACA,SAAS;EACT;;EAEA,OAAO,eAAe;EACtB;;;;;;EAMA;;EAEA,SAAS;;;;;;EAMT;;EAEA,WAAW;;;YAID;;;;;;iBAOK;;;;;EAKf,QAAQ,aAAa,SAAS;;EAE9B;;EAEA,OAAO;;;;;;EAMP,eAAe;;;;;;;iBAQA;EACf;EACA;;iBAGe;EACf,KAAK,SAAS,kBAAkB,eAAe;;;;;;;;;iBAUhC;;EAEf,SAAS;IACP;IACA;IACA;IACA;MACE;;EAEJ,aAAa;IACX;IACA;IACA;IACA;IACA;MACE;;EAEJ,iBAAiB,SAAS,kBAAkB,QAAQ;;EAEpD,aAAa,SAAS,iBAAiB;IAAU;MAAoB,aAAa;;EAElF,UAAU;IACR;IACA;IACA;IACA,QAAQ;IACR;;IAEA;MACE;;;;;;;;;iBAUW;EACf,SAAS;IACP,SAAS;IACT;IACA,OAAO,eAAe;MACpB,eAAe;;;iBAIJ;;;;;;;;EAQf,KAAK,eAAe,SAAS,eAAe,uBAAuB,aAAa;;;iBAIjE;;EAEf;;EAEA;;EAEA,MAAM,eAAe;EACrB;;EAEA;;;;;;;iBAQe;EACf,OAAO,YAAY,mBAAmB;;iBAGvB;EACf;;EAEA;;;;;;;EAOA;EACA,MAAM;EACN,OAAO;EACP,UAAU;EACV,QAAQ;;EAER,UAAU;;EAEV,YAAY;;EAEZ,WAAW;;;;;;;YAQD;EACN;;EACA;EAAkB;;EAClB;;EACA;;EACA"}
package/dist/index.cjs CHANGED
@@ -4,6 +4,7 @@ const require_dsl_action = require("./dsl/action.cjs");
4
4
  const require_dsl_refs = require("./dsl/refs.cjs");
5
5
  const require_dsl_template = require("./dsl/template.cjs");
6
6
  const require_dsl_condition = require("./dsl/condition.cjs");
7
+ const require_email = require("./email.cjs");
7
8
  const require_templates = require("./templates.cjs");
8
9
  const require_dsl_flow = require("./dsl/flow.cjs");
9
10
  const require_dsl_trigger = require("./dsl/trigger.cjs");
@@ -42,7 +43,11 @@ exports.WorkflowIR = require_ir.WorkflowIR;
42
43
  exports.action = require_dsl_action.action;
43
44
  exports.and = require_dsl_condition.and;
44
45
  exports.apnsEnvironment = require_config.apnsEnvironment;
46
+ exports.assertEmailAddress = require_email.assertEmailAddress;
47
+ exports.assertEmailConfig = require_email.assertEmailConfig;
48
+ exports.assertEmailMetadata = require_email.assertEmailMetadata;
45
49
  exports.assertSupportedSchema = require_templates.assertSupportedSchema;
50
+ exports.assertUnsubscribeUrl = require_email.assertUnsubscribeUrl;
46
51
  exports.between = require_dsl_condition.between;
47
52
  exports.canonicalJSON = require_hash.canonicalJSON;
48
53
  exports.checkEvent = require_contract.checkEvent;
@@ -57,7 +62,7 @@ exports.eq = require_dsl_condition.eq;
57
62
  exports.event = require_dsl_refs.event;
58
63
  exports.exists = require_dsl_condition.exists;
59
64
  exports.flow = require_dsl_flow.flow;
60
- exports.formatEmailAddress = require_config.formatEmailAddress;
65
+ exports.formatEmailAddress = require_email.formatEmailAddress;
61
66
  exports.fullHash = require_hash.fullHash;
62
67
  exports.gt = require_dsl_condition.gt;
63
68
  exports.gte = require_dsl_condition.gte;
@@ -71,6 +76,7 @@ exports.isPushTemplate = require_templates.isPushTemplate;
71
76
  exports.isSemanticChange = require_plan.isSemanticChange;
72
77
  exports.isSlackTemplate = require_templates.isSlackTemplate;
73
78
  exports.isTemplate = require_templates.isTemplate;
79
+ exports.isUnsubscribeLink = require_email.isUnsubscribeLink;
74
80
  exports.lookupTemplate = require_templates.lookupTemplate;
75
81
  exports.lt = require_dsl_condition.lt;
76
82
  exports.lte = require_dsl_condition.lte;
@@ -90,6 +96,7 @@ exports.registryValidator = require_templates.registryValidator;
90
96
  exports.renderDiscordContent = require_templates.renderDiscordContent;
91
97
  exports.renderPushContent = require_templates.renderPushContent;
92
98
  exports.renderSlackContent = require_templates.renderSlackContent;
99
+ exports.resolveEmail = require_email.resolveEmail;
93
100
  exports.resolveRuntime = require_runtime.resolveRuntime;
94
101
  exports.segment = require_dsl_condition.segment;
95
102
  exports.semanticHash = require_hash.semanticHash;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ActionIR, BundleIR, ChannelIR, ConditionIR, DurationIR, GoalIR, IR_VERSION, MetaIR, NodeIR, NodeMetaIR, PropValueIR, PropertyOperatorIR, ScalarIR, SegmentIR, SourceLocIR, TemplateIR, TriggerIR, UserPropertyRefIR, WeekdayIR, WorkflowIR } from "./ir.cjs";
2
- import { AnyTemplate, CHANNEL_OF, ChannelTemplate, DiscordContent, DiscordTemplate, EmailTemplate, EmailTemplateDefinition, PropsSchema, PushContent, PushTemplate, PushTemplateDefinition, ResolvedProps, SlackContent, SlackTemplate, TemplateChannel, TemplateIssue, TemplateRegistry, TemplateValidationError, WebhookTemplateDefinition, assertSupportedSchema, discordTemplate, emailTemplate, indexTemplates, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate, isTemplate, lookupTemplate, parseTemplateProps, pushTemplate, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, slackTemplate } from "./templates.cjs";
2
+ import { EmailAddress, EmailConfig, ResolvedEmail, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertUnsubscribeUrl, formatEmailAddress, isUnsubscribeLink, resolveEmail } from "./email.cjs";
3
+ import { AnyTemplate, CHANNEL_OF, ChannelTemplate, DiscordContent, DiscordTemplate, EmailMetadata, EmailTemplate, EmailTemplateDefinition, PropsSchema, PushContent, PushTemplate, PushTemplateDefinition, ResolvedProps, SlackContent, SlackTemplate, TemplateChannel, TemplateIssue, TemplateRegistry, TemplateValidationError, WebhookTemplateDefinition, assertSupportedSchema, discordTemplate, emailTemplate, indexTemplates, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate, isTemplate, lookupTemplate, parseTemplateProps, pushTemplate, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, slackTemplate } from "./templates.cjs";
3
4
  import { ActionContext, ActionHandler, ActionRef, action } from "./dsl/action.cjs";
4
5
  import { Duration, EventMap, TimeOfDay, UserPropertyBase, Weekday } from "./dsl/base.cjs";
5
6
  import { EventRef, EventRefs, MessageArgs, PayloadRef, PayloadRefNode, PayloadRefs, PropInput, PropsInput, UserPropertyRef, UserRefs, event, user } from "./dsl/refs.cjs";
@@ -12,7 +13,7 @@ import { compileBundle } from "./dsl/bundle.cjs";
12
13
  import "./dsl/index.cjs";
13
14
  import { Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DeclaredOutput, EventPayload, ProfilePatch, checkEvent, defineContract, isContract } from "./contract.cjs";
14
15
  import { JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, resolveRuntime } from "./runtime.cjs";
15
- import { ApnsConfig, ConfigCompileError, DatabaseConfig, EmailAddress, EmailConfig, FcmConfig, JourneyConfig, JourneyConfigDefinition, WebPushConfig, apnsEnvironment, compileConfig, defineConfig, formatEmailAddress, isConfig } from "./config.cjs";
16
+ import { ApnsConfig, ConfigCompileError, DatabaseConfig, FcmConfig, JourneyConfig, JourneyConfigDefinition, WebPushConfig, apnsEnvironment, compileConfig, defineConfig, isConfig } from "./config.cjs";
16
17
  import { canonicalJSON, fullHash, semanticHash, sha256Hex, stripMeta, stripProvenance } from "./hash.cjs";
17
18
  import { BundlePlan, ChangeStatus, NodeChange, ResourceChange, WorkflowChange, isSemanticChange, plan } from "./plan.cjs";
18
- export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, CHANNEL_OF, ChangeStatus, type Channel, ChannelIR, ChannelTemplate, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailTemplate, EmailTemplateDefinition, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateDefinition, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, TemplateRegistry, TemplateValidationError, type TimeOfDay, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertSupportedSchema, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, lookupTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, trigger, user, workflow };
19
+ export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, CHANNEL_OF, ChangeStatus, type Channel, ChannelIR, ChannelTemplate, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailMetadata, EmailTemplate, EmailTemplateDefinition, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateDefinition, ResolvedEmail, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, TemplateRegistry, TemplateValidationError, type TimeOfDay, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertSupportedSchema, assertUnsubscribeUrl, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, isUnsubscribeLink, lookupTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveEmail, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, trigger, user, workflow };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { ActionIR, BundleIR, ChannelIR, ConditionIR, DurationIR, GoalIR, IR_VERSION, MetaIR, NodeIR, NodeMetaIR, PropValueIR, PropertyOperatorIR, ScalarIR, SegmentIR, SourceLocIR, TemplateIR, TriggerIR, UserPropertyRefIR, WeekdayIR, WorkflowIR } from "./ir.mjs";
2
- import { AnyTemplate, CHANNEL_OF, ChannelTemplate, DiscordContent, DiscordTemplate, EmailTemplate, EmailTemplateDefinition, PropsSchema, PushContent, PushTemplate, PushTemplateDefinition, ResolvedProps, SlackContent, SlackTemplate, TemplateChannel, TemplateIssue, TemplateRegistry, TemplateValidationError, WebhookTemplateDefinition, assertSupportedSchema, discordTemplate, emailTemplate, indexTemplates, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate, isTemplate, lookupTemplate, parseTemplateProps, pushTemplate, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, slackTemplate } from "./templates.mjs";
2
+ import { EmailAddress, EmailConfig, ResolvedEmail, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertUnsubscribeUrl, formatEmailAddress, isUnsubscribeLink, resolveEmail } from "./email.mjs";
3
+ import { AnyTemplate, CHANNEL_OF, ChannelTemplate, DiscordContent, DiscordTemplate, EmailMetadata, EmailTemplate, EmailTemplateDefinition, PropsSchema, PushContent, PushTemplate, PushTemplateDefinition, ResolvedProps, SlackContent, SlackTemplate, TemplateChannel, TemplateIssue, TemplateRegistry, TemplateValidationError, WebhookTemplateDefinition, assertSupportedSchema, discordTemplate, emailTemplate, indexTemplates, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate, isTemplate, lookupTemplate, parseTemplateProps, pushTemplate, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, slackTemplate } from "./templates.mjs";
3
4
  import { ActionContext, ActionHandler, ActionRef, action } from "./dsl/action.mjs";
4
5
  import { Duration, EventMap, TimeOfDay, UserPropertyBase, Weekday } from "./dsl/base.mjs";
5
6
  import { EventRef, EventRefs, MessageArgs, PayloadRef, PayloadRefNode, PayloadRefs, PropInput, PropsInput, UserPropertyRef, UserRefs, event, user } from "./dsl/refs.mjs";
@@ -12,7 +13,7 @@ import { compileBundle } from "./dsl/bundle.mjs";
12
13
  import "./dsl/index.mjs";
13
14
  import { Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DeclaredOutput, EventPayload, ProfilePatch, checkEvent, defineContract, isContract } from "./contract.mjs";
14
15
  import { JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, resolveRuntime } from "./runtime.mjs";
15
- import { ApnsConfig, ConfigCompileError, DatabaseConfig, EmailAddress, EmailConfig, FcmConfig, JourneyConfig, JourneyConfigDefinition, WebPushConfig, apnsEnvironment, compileConfig, defineConfig, formatEmailAddress, isConfig } from "./config.mjs";
16
+ import { ApnsConfig, ConfigCompileError, DatabaseConfig, FcmConfig, JourneyConfig, JourneyConfigDefinition, WebPushConfig, apnsEnvironment, compileConfig, defineConfig, isConfig } from "./config.mjs";
16
17
  import { canonicalJSON, fullHash, semanticHash, sha256Hex, stripMeta, stripProvenance } from "./hash.mjs";
17
18
  import { BundlePlan, ChangeStatus, NodeChange, ResourceChange, WorkflowChange, isSemanticChange, plan } from "./plan.mjs";
18
- export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, CHANNEL_OF, ChangeStatus, type Channel, ChannelIR, ChannelTemplate, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailTemplate, EmailTemplateDefinition, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateDefinition, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, TemplateRegistry, TemplateValidationError, type TimeOfDay, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertSupportedSchema, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, lookupTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, trigger, user, workflow };
19
+ export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, CHANNEL_OF, ChangeStatus, type Channel, ChannelIR, ChannelTemplate, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailMetadata, EmailTemplate, EmailTemplateDefinition, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateDefinition, ResolvedEmail, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, TemplateRegistry, TemplateValidationError, type TimeOfDay, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertSupportedSchema, assertUnsubscribeUrl, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, isUnsubscribeLink, lookupTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveEmail, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, trigger, user, workflow };
package/dist/index.mjs CHANGED
@@ -3,6 +3,7 @@ import { action } from "./dsl/action.mjs";
3
3
  import { event, user } from "./dsl/refs.mjs";
4
4
  import { template } from "./dsl/template.mjs";
5
5
  import { and, between, contains, eq, exists, gt, gte, inArray, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, performed, reachable, segment } from "./dsl/condition.mjs";
6
+ import { assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertUnsubscribeUrl, formatEmailAddress, isUnsubscribeLink, resolveEmail } from "./email.mjs";
6
7
  import { CHANNEL_OF, TemplateValidationError, assertSupportedSchema, discordTemplate, emailTemplate, indexTemplates, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSlackTemplate, isTemplate, lookupTemplate, parseTemplateProps, pushTemplate, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, slackTemplate } from "./templates.mjs";
7
8
  import { flow } from "./dsl/flow.mjs";
8
9
  import { trigger } from "./dsl/trigger.mjs";
@@ -10,8 +11,8 @@ import { ActionIR, BundleIR, ChannelIR, ConditionIR, DurationIR, GoalIR, IR_VERS
10
11
  import { workflow } from "./dsl/workflow.mjs";
11
12
  import { compileBundle } from "./dsl/bundle.mjs";
12
13
  import "./dsl/index.mjs";
13
- import { ConfigCompileError, apnsEnvironment, compileConfig, defineConfig, formatEmailAddress, isConfig } from "./config.mjs";
14
+ import { ConfigCompileError, apnsEnvironment, compileConfig, defineConfig, isConfig } from "./config.mjs";
14
15
  import { ContractValidationError, checkEvent, defineContract, isContract } from "./contract.mjs";
15
16
  import { isSemanticChange, plan } from "./plan.mjs";
16
17
  import { resolveRuntime } from "./runtime.mjs";
17
- export { ActionIR, BundleIR, CHANNEL_OF, ChannelIR, ConditionIR, ConfigCompileError, ContractValidationError, DurationIR, GoalIR, IR_VERSION, MetaIR, NodeIR, NodeMetaIR, PropValueIR, PropertyOperatorIR, ScalarIR, SegmentIR, SourceLocIR, TemplateIR, TemplateValidationError, TriggerIR, UserPropertyRefIR, WeekdayIR, WorkflowIR, action, and, apnsEnvironment, assertSupportedSchema, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, lookupTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, trigger, user, workflow };
18
+ export { ActionIR, BundleIR, CHANNEL_OF, ChannelIR, ConditionIR, ConfigCompileError, ContractValidationError, DurationIR, GoalIR, IR_VERSION, MetaIR, NodeIR, NodeMetaIR, PropValueIR, PropertyOperatorIR, ScalarIR, SegmentIR, SourceLocIR, TemplateIR, TemplateValidationError, TriggerIR, UserPropertyRefIR, WeekdayIR, WorkflowIR, action, and, apnsEnvironment, assertEmailAddress, assertEmailConfig, assertEmailMetadata, assertSupportedSchema, assertUnsubscribeUrl, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, indexTemplates, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, isTemplate, isUnsubscribeLink, lookupTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveEmail, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, trigger, user, workflow };
@@ -1,5 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_email = require("./email.cjs");
2
3
  const require_templates = require("./templates.cjs");
4
+ const require_email_context = require("./email-context.cjs");
3
5
  let react = require("react");
4
6
  let react_dom_server = require("react-dom/server");
5
7
  //#region src/react-email.ts
@@ -11,7 +13,11 @@ let react_dom_server = require("react-dom/server");
11
13
  * The registry holds `emailTemplate({...})` definitions, looked up by their `id`. Props are
12
14
  * parsed against the template's schema before the component renders — a
13
15
  * failure throws TemplateValidationError and nothing renders — and the
14
- * component renders as an element, so hooks and context work. Rendering
16
+ * component renders as an element inside an `EmailProvider` carrying the
17
+ * template's resolved metadata (`resolveEmail` against the config's `email`),
18
+ * so hooks and context work and a layout's `useEmailContext()` reads the
19
+ * unsubscribe link the email is sent with; the render returns that metadata
20
+ * with the subject and HTML, for the sender. Rendering
15
21
  * streams and waits for every Suspense boundary (`allReady`) — react-email's
16
22
  * `<Tailwind>` suspends while it compiles the classes, which synchronous
17
23
  * static markup cannot wait for — then prepends the XHTML doctype react-email
@@ -19,13 +25,15 @@ let react_dom_server = require("react-dom/server");
19
25
  * statically imports prettier and html-to-text for options never used here.
20
26
  */
21
27
  const DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
22
- function registryRenderer(emails) {
28
+ function registryRenderer(emails, config) {
23
29
  const index = require_templates.indexTemplates("email", emails);
24
30
  return async (id, props) => {
25
31
  const template = require_templates.lookupTemplate(index, "email", id);
26
32
  const parsed = require_templates.parseTemplateProps(template, props);
27
- const element = (0, react.createElement)(template.component, parsed);
33
+ const email = require_email.resolveEmail(template, config);
34
+ const element = (0, react.createElement)(require_email_context.EmailProvider, { email }, (0, react.createElement)(template.component, parsed));
28
35
  return {
36
+ ...email,
29
37
  subject: template.subject,
30
38
  html: DOCTYPE + await renderToHtml(element)
31
39
  };