@shware/workflow 3.3.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -29
- package/dist/apns.cjs +113 -26
- package/dist/apns.cjs.map +1 -1
- package/dist/apns.d.cts +52 -6
- package/dist/apns.d.cts.map +1 -1
- package/dist/apns.d.mts +52 -6
- package/dist/apns.d.mts.map +1 -1
- package/dist/apns.mjs +111 -27
- package/dist/apns.mjs.map +1 -1
- package/dist/aws/engine-step.cjs +5 -5
- package/dist/aws/engine-step.cjs.map +1 -1
- package/dist/aws/engine-step.d.cts +8 -2
- package/dist/aws/engine-step.d.cts.map +1 -1
- package/dist/aws/engine-step.d.mts +8 -2
- package/dist/aws/engine-step.d.mts.map +1 -1
- package/dist/aws/engine-step.mjs +5 -5
- package/dist/aws/engine-step.mjs.map +1 -1
- package/dist/aws/index.cjs +47 -6
- package/dist/aws/index.cjs.map +1 -1
- package/dist/aws/index.d.cts +2 -2
- package/dist/aws/index.d.cts.map +1 -1
- package/dist/aws/index.d.mts +2 -2
- package/dist/aws/index.d.mts.map +1 -1
- package/dist/aws/index.mjs +47 -6
- package/dist/aws/index.mjs.map +1 -1
- package/dist/aws/instances.cjs +29 -6
- package/dist/aws/instances.cjs.map +1 -1
- package/dist/aws/instances.d.cts +11 -1
- package/dist/aws/instances.d.cts.map +1 -1
- package/dist/aws/instances.d.mts +11 -1
- package/dist/aws/instances.d.mts.map +1 -1
- package/dist/aws/instances.mjs +27 -4
- package/dist/aws/instances.mjs.map +1 -1
- package/dist/client.cjs +4 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +2 -2
- package/dist/client.d.cts.map +1 -1
- package/dist/client.d.mts +2 -2
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +4 -1
- package/dist/client.mjs.map +1 -1
- package/dist/cloudflare/bindings.cjs +28 -25
- package/dist/cloudflare/bindings.cjs.map +1 -1
- package/dist/cloudflare/bindings.d.cts +15 -129
- package/dist/cloudflare/bindings.d.cts.map +1 -1
- package/dist/cloudflare/bindings.d.mts +15 -129
- package/dist/cloudflare/bindings.d.mts.map +1 -1
- package/dist/cloudflare/bindings.mjs +28 -23
- package/dist/cloudflare/bindings.mjs.map +1 -1
- package/dist/cloudflare/d1-env.cjs +6 -6
- package/dist/cloudflare/d1-env.cjs.map +1 -1
- package/dist/cloudflare/d1-env.d.cts +5 -5
- package/dist/cloudflare/d1-env.d.mts +5 -5
- package/dist/cloudflare/d1-env.mjs +6 -6
- package/dist/cloudflare/d1-env.mjs.map +1 -1
- package/dist/cloudflare/index.cjs +28 -18
- package/dist/cloudflare/index.d.cts +8 -4
- package/dist/cloudflare/index.d.mts +8 -4
- package/dist/cloudflare/index.mjs +9 -5
- package/dist/cloudflare/runner.cjs +29 -14
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts +9 -2
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts +9 -2
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +28 -13
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/cloudflare/schema.sql +27 -6
- package/dist/cloudflare/worker.cjs +19 -9
- package/dist/cloudflare/worker.cjs.map +1 -1
- package/dist/cloudflare/worker.d.cts +2 -2
- package/dist/cloudflare/worker.d.cts.map +1 -1
- package/dist/cloudflare/worker.d.mts +2 -2
- package/dist/cloudflare/worker.d.mts.map +1 -1
- package/dist/cloudflare/worker.mjs +19 -9
- package/dist/cloudflare/worker.mjs.map +1 -1
- package/dist/config.cjs +21 -5
- package/dist/config.cjs.map +1 -1
- package/dist/config.mjs +21 -5
- package/dist/config.mjs.map +1 -1
- package/dist/dsl/action.cjs +3 -6
- package/dist/dsl/action.cjs.map +1 -1
- package/dist/dsl/action.d.cts +12 -11
- package/dist/dsl/action.d.cts.map +1 -1
- package/dist/dsl/action.d.mts +12 -11
- package/dist/dsl/action.d.mts.map +1 -1
- package/dist/dsl/action.mjs +3 -6
- package/dist/dsl/action.mjs.map +1 -1
- package/dist/dsl/bundle.cjs +35 -31
- package/dist/dsl/bundle.cjs.map +1 -1
- package/dist/dsl/bundle.d.cts.map +1 -1
- package/dist/dsl/bundle.d.mts.map +1 -1
- package/dist/dsl/bundle.mjs +35 -31
- package/dist/dsl/bundle.mjs.map +1 -1
- package/dist/dsl/condition.cjs +13 -1
- package/dist/dsl/condition.cjs.map +1 -1
- package/dist/dsl/condition.d.cts +9 -1
- package/dist/dsl/condition.d.cts.map +1 -1
- package/dist/dsl/condition.d.mts +9 -1
- package/dist/dsl/condition.d.mts.map +1 -1
- package/dist/dsl/condition.mjs +13 -2
- package/dist/dsl/condition.mjs.map +1 -1
- package/dist/dsl/flow.cjs +33 -11
- package/dist/dsl/flow.cjs.map +1 -1
- package/dist/dsl/flow.d.cts +9 -1
- package/dist/dsl/flow.d.cts.map +1 -1
- package/dist/dsl/flow.d.mts +9 -1
- package/dist/dsl/flow.d.mts.map +1 -1
- package/dist/dsl/flow.mjs +34 -13
- package/dist/dsl/flow.mjs.map +1 -1
- package/dist/dsl/workflow.cjs +2 -3
- package/dist/dsl/workflow.cjs.map +1 -1
- package/dist/dsl/workflow.d.cts.map +1 -1
- package/dist/dsl/workflow.d.mts.map +1 -1
- package/dist/dsl/workflow.mjs +3 -4
- package/dist/dsl/workflow.mjs.map +1 -1
- package/dist/engine/actions.cjs +1 -8
- package/dist/engine/actions.cjs.map +1 -1
- package/dist/engine/actions.d.cts +1 -12
- package/dist/engine/actions.d.cts.map +1 -1
- package/dist/engine/actions.d.mts +1 -12
- package/dist/engine/actions.d.mts.map +1 -1
- package/dist/engine/actions.mjs +1 -8
- package/dist/engine/actions.mjs.map +1 -1
- package/dist/engine/condition.cjs +25 -5
- package/dist/engine/condition.cjs.map +1 -1
- package/dist/engine/condition.d.cts +12 -3
- package/dist/engine/condition.d.cts.map +1 -1
- package/dist/engine/condition.d.mts +12 -3
- package/dist/engine/condition.d.mts.map +1 -1
- package/dist/engine/condition.mjs +25 -6
- package/dist/engine/condition.mjs.map +1 -1
- package/dist/engine/index.cjs +3 -0
- package/dist/engine/index.d.cts +5 -5
- package/dist/engine/index.d.mts +5 -5
- package/dist/engine/index.mjs +4 -4
- package/dist/engine/interpreter.cjs +27 -20
- package/dist/engine/interpreter.cjs.map +1 -1
- package/dist/engine/interpreter.d.cts.map +1 -1
- package/dist/engine/interpreter.d.mts.map +1 -1
- package/dist/engine/interpreter.mjs +27 -20
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +15 -6
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +15 -6
- package/dist/engine/ports.d.mts.map +1 -1
- package/dist/engine/retry.cjs +13 -0
- package/dist/engine/retry.cjs.map +1 -1
- package/dist/engine/retry.d.cts +12 -0
- package/dist/engine/retry.d.cts.map +1 -1
- package/dist/engine/retry.d.mts +12 -0
- package/dist/engine/retry.d.mts.map +1 -1
- package/dist/engine/retry.mjs +13 -1
- package/dist/engine/retry.mjs.map +1 -1
- package/dist/engine/time-scale.cjs +90 -15
- package/dist/engine/time-scale.cjs.map +1 -1
- package/dist/engine/time-scale.d.cts +9 -6
- package/dist/engine/time-scale.d.cts.map +1 -1
- package/dist/engine/time-scale.d.mts +9 -6
- package/dist/engine/time-scale.d.mts.map +1 -1
- package/dist/engine/time-scale.mjs +90 -16
- package/dist/engine/time-scale.mjs.map +1 -1
- package/dist/fcm.cjs +111 -38
- package/dist/fcm.cjs.map +1 -1
- package/dist/fcm.d.cts +45 -5
- package/dist/fcm.d.cts.map +1 -1
- package/dist/fcm.d.mts +45 -5
- package/dist/fcm.d.mts.map +1 -1
- package/dist/fcm.mjs +110 -39
- package/dist/fcm.mjs.map +1 -1
- package/dist/hash.cjs +6 -6
- package/dist/hash.cjs.map +1 -1
- package/dist/hash.mjs +6 -6
- package/dist/hash.mjs.map +1 -1
- package/dist/host/auth.cjs +49 -0
- package/dist/host/auth.cjs.map +1 -0
- package/dist/host/auth.d.cts +21 -0
- package/dist/host/auth.d.cts.map +1 -0
- package/dist/host/auth.d.mts +21 -0
- package/dist/host/auth.d.mts.map +1 -0
- package/dist/host/auth.mjs +47 -0
- package/dist/host/auth.mjs.map +1 -0
- package/dist/host/channels.cjs +118 -40
- package/dist/host/channels.cjs.map +1 -1
- package/dist/host/channels.d.cts +27 -1
- package/dist/host/channels.d.cts.map +1 -1
- package/dist/host/channels.d.mts +27 -1
- package/dist/host/channels.d.mts.map +1 -1
- package/dist/host/channels.mjs +121 -44
- package/dist/host/channels.mjs.map +1 -1
- package/dist/host/execute.cjs +14 -2
- package/dist/host/execute.cjs.map +1 -1
- package/dist/host/execute.d.cts +4 -4
- package/dist/host/execute.d.cts.map +1 -1
- package/dist/host/execute.d.mts +4 -4
- package/dist/host/execute.d.mts.map +1 -1
- package/dist/host/execute.mjs +13 -1
- package/dist/host/execute.mjs.map +1 -1
- package/dist/host/fresh-read.cjs +30 -0
- package/dist/host/fresh-read.cjs.map +1 -0
- package/dist/host/fresh-read.d.cts +18 -0
- package/dist/host/fresh-read.d.cts.map +1 -0
- package/dist/host/fresh-read.d.mts +18 -0
- package/dist/host/fresh-read.d.mts.map +1 -0
- package/dist/host/fresh-read.mjs +29 -0
- package/dist/host/fresh-read.mjs.map +1 -0
- package/dist/host/http.cjs +321 -0
- package/dist/host/http.cjs.map +1 -0
- package/dist/host/http.d.cts +54 -0
- package/dist/host/http.d.cts.map +1 -0
- package/dist/host/http.d.mts +54 -0
- package/dist/host/http.d.mts.map +1 -0
- package/dist/host/http.mjs +317 -0
- package/dist/host/http.mjs.map +1 -0
- package/dist/host/{router.cjs → ingest.cjs} +147 -175
- package/dist/host/ingest.cjs.map +1 -0
- package/dist/host/{router.d.cts → ingest.d.cts} +48 -30
- package/dist/host/ingest.d.cts.map +1 -0
- package/dist/host/{router.d.mts → ingest.d.mts} +48 -30
- package/dist/host/ingest.d.mts.map +1 -0
- package/dist/host/{router.mjs → ingest.mjs} +144 -173
- package/dist/host/ingest.mjs.map +1 -0
- package/dist/host/journey-host.cjs +65 -93
- package/dist/host/journey-host.cjs.map +1 -1
- package/dist/host/journey-host.d.cts +19 -26
- package/dist/host/journey-host.d.cts.map +1 -1
- package/dist/host/journey-host.d.mts +19 -26
- package/dist/host/journey-host.d.mts.map +1 -1
- package/dist/host/journey-host.mjs +65 -93
- package/dist/host/journey-host.mjs.map +1 -1
- package/dist/host/platform.cjs +21 -0
- package/dist/host/platform.cjs.map +1 -0
- package/dist/host/platform.d.cts +126 -0
- package/dist/host/platform.d.cts.map +1 -0
- package/dist/host/platform.d.mts +126 -0
- package/dist/host/platform.d.mts.map +1 -0
- package/dist/host/platform.mjs +19 -0
- package/dist/host/platform.mjs.map +1 -0
- package/dist/host/senders.cjs +18 -9
- package/dist/host/senders.cjs.map +1 -1
- package/dist/host/senders.d.cts +9 -4
- package/dist/host/senders.d.cts.map +1 -1
- package/dist/host/senders.d.mts +9 -4
- package/dist/host/senders.d.mts.map +1 -1
- package/dist/host/senders.mjs +18 -9
- package/dist/host/senders.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/ir.cjs +5 -7
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +3 -12
- package/dist/ir.d.cts.map +1 -1
- package/dist/ir.d.mts +3 -12
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +5 -7
- package/dist/ir.mjs.map +1 -1
- package/dist/plan.cjs +27 -33
- package/dist/plan.cjs.map +1 -1
- package/dist/plan.d.cts +1 -1
- package/dist/plan.d.cts.map +1 -1
- package/dist/plan.d.mts +1 -1
- package/dist/plan.d.mts.map +1 -1
- package/dist/plan.mjs +27 -33
- package/dist/plan.mjs.map +1 -1
- package/dist/reporting/tracking.cjs +14 -4
- package/dist/reporting/tracking.cjs.map +1 -1
- package/dist/reporting/tracking.d.cts +10 -10
- package/dist/reporting/tracking.d.cts.map +1 -1
- package/dist/reporting/tracking.d.mts +10 -10
- package/dist/reporting/tracking.d.mts.map +1 -1
- package/dist/reporting/tracking.mjs +14 -4
- package/dist/reporting/tracking.mjs.map +1 -1
- package/dist/store/d1.cjs +88 -29
- package/dist/store/d1.cjs.map +1 -1
- package/dist/store/d1.d.cts +29 -11
- package/dist/store/d1.d.cts.map +1 -1
- package/dist/store/d1.d.mts +29 -11
- package/dist/store/d1.d.mts.map +1 -1
- package/dist/store/d1.mjs +88 -29
- package/dist/store/d1.mjs.map +1 -1
- package/dist/store/erasure.cjs +22 -0
- package/dist/store/erasure.cjs.map +1 -0
- package/dist/store/erasure.d.cts +18 -0
- package/dist/store/erasure.d.cts.map +1 -0
- package/dist/store/erasure.d.mts +18 -0
- package/dist/store/erasure.d.mts.map +1 -0
- package/dist/store/erasure.mjs +21 -0
- package/dist/store/erasure.mjs.map +1 -0
- package/dist/store/index.cjs +2 -0
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +67 -13
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +67 -13
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs +2 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres-reporting.cjs +125 -62
- package/dist/store/postgres-reporting.cjs.map +1 -1
- package/dist/store/postgres-reporting.d.cts +30 -2
- package/dist/store/postgres-reporting.d.cts.map +1 -1
- package/dist/store/postgres-reporting.d.mts +30 -2
- package/dist/store/postgres-reporting.d.mts.map +1 -1
- package/dist/store/postgres-reporting.mjs +125 -62
- package/dist/store/postgres-reporting.mjs.map +1 -1
- package/dist/store/postgres.cjs +179 -60
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +44 -7
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +44 -7
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +179 -60
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +7 -5
- package/dist/store/tables.cjs.map +1 -1
- package/dist/store/tables.d.cts.map +1 -1
- package/dist/store/tables.d.mts.map +1 -1
- package/dist/store/tables.mjs +7 -5
- package/dist/store/tables.mjs.map +1 -1
- package/dist/store/user-lock.cjs +43 -0
- package/dist/store/user-lock.cjs.map +1 -0
- package/dist/store/user-lock.d.cts +32 -0
- package/dist/store/user-lock.d.cts.map +1 -0
- package/dist/store/user-lock.d.mts +32 -0
- package/dist/store/user-lock.d.mts.map +1 -0
- package/dist/store/user-lock.mjs +40 -0
- package/dist/store/user-lock.mjs.map +1 -0
- package/dist/transports/cloudflare-email.cjs +1 -1
- package/dist/transports/cloudflare-email.mjs +1 -1
- package/dist/transports/ses.cjs +47 -6
- package/dist/transports/ses.cjs.map +1 -1
- package/dist/transports/ses.d.cts +9 -1
- package/dist/transports/ses.d.cts.map +1 -1
- package/dist/transports/ses.d.mts +9 -1
- package/dist/transports/ses.d.mts.map +1 -1
- package/dist/transports/ses.mjs +47 -7
- package/dist/transports/ses.mjs.map +1 -1
- package/dist/webpush.cjs +133 -47
- package/dist/webpush.cjs.map +1 -1
- package/dist/webpush.d.cts +49 -8
- package/dist/webpush.d.cts.map +1 -1
- package/dist/webpush.d.mts +49 -8
- package/dist/webpush.d.mts.map +1 -1
- package/dist/webpush.mjs +130 -47
- package/dist/webpush.mjs.map +1 -1
- package/package.json +1 -1
- package/src/cloudflare/schema.sql +27 -6
- package/dist/host/router.cjs.map +0 -1
- package/dist/host/router.d.cts.map +0 -1
- package/dist/host/router.d.mts.map +0 -1
- package/dist/host/router.mjs.map +0 -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 {\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, { deadlineMs: deadline });\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, so a wake-up storm cannot exhaust the platform's step\n * budget. Each loop 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. Past the cap the wait\n * is not given up: it sleeps to its deadline and checks once more there (see waitLoop) — the\n * answer is still right at the deadline, it just stops being event-driven in between.\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 if (result.goalAt !== null) {\n state.convertedAt ??= result.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>(`${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\n if (plan === null) 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 // The remaining time is checkpointed: a replay must not recompute it from a later clock\n // (the target has passed by then, and a negative sleep is an error on some runtimes)\n const ms = await ctx.step.do(`${node.id}:remaining`, async () => target - Date.now());\n if (ms > 0) await ctx.step.sleep(`${node.id}:sleep`, ms);\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 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 if (hit.kind === 'goal') {\n state.convertedAt ??= hit.at;\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\". Null from here means the deadline passed with\n * nothing found — never a failure, and never a wait given up on early.\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, { deadlineMs: deadline });\n\n const met = await ctx.step.do(`${id}:check:${attempt}`, async () => check(Date.now()));\n if (met !== null) 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}`, {\n timeoutMs: remaining,\n deadlineMs: deadline,\n });\n // Final check after timeout: in a tight race the event may land exactly as the timer fires\n if (wake === 'timeout') return finalCheck(id, ctx, check);\n // Woken → back to the top of the loop to re-evaluate (at-least-once wake-up semantics)\n }\n // Check budget exhausted (see MAX_WAIT_CHECKS). Not a timeout: the wait keeps its deadline,\n // only the wake-ups are given up — sleep to the deadline, then check once more there. The\n // subscription rows stay until the caller's unsubscribe; a wake landing meanwhile is dropped\n // by the runtime (nothing is parked on it), which is the point.\n console.warn(\n `[journey] ${ctx.instanceId} ${id}: ${MAX_WAIT_CHECKS} wake-ups without the condition holding — sleeping to the deadline`\n );\n const budgetRemaining = await ctx.step.do(\n `${id}:budget:remaining`,\n async () => deadline - Date.now()\n );\n if (budgetRemaining > 0) await ctx.step.sleep(`${id}:budget:sleep`, budgetRemaining);\n return finalCheck(id, ctx, check);\n}\n\n/** The check once the deadline is reached — one step for all the ways of getting there. */\nasync function finalCheck<R>(\n id: string,\n ctx: JourneyContext,\n check: (now: number) => Promise<R | null>\n): Promise<R | null> {\n return ctx.step.do(`${id}:final`, async () => check(Date.now()));\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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interpreter.d.cts","names":[],"sources":["../../src/engine/interpreter.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"interpreter.d.cts","names":[],"sources":["../../src/engine/interpreter.ts"],"mappings":";;;wBAiGsB,WAAW,IAAI,YAAY,KAAK,iBAAiB,QAAQ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interpreter.d.mts","names":[],"sources":["../../src/engine/interpreter.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"interpreter.d.mts","names":[],"sources":["../../src/engine/interpreter.ts"],"mappings":";;;wBAiGsB,WAAW,IAAI,YAAY,KAAK,iBAAiB,QAAQ"}
|
|
@@ -21,10 +21,11 @@ import { nextWindowStart } from "./time-window.mjs";
|
|
|
21
21
|
* there would only burn step budget (each checkpoint costs a state write).
|
|
22
22
|
*/
|
|
23
23
|
/**
|
|
24
|
-
* Cap on wait_until re-evaluations
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* Cap on wait_until re-evaluations, so a wake-up storm cannot exhaust the platform's step
|
|
25
|
+
* budget. Each loop iteration costs a handful of steps, so 64 checks stay well inside the
|
|
26
|
+
* platform step budgets while absorbing far more wake churn than 16 did. Past the cap the wait
|
|
27
|
+
* is not given up: it sleeps to its deadline and checks once more there (see waitLoop) — the
|
|
28
|
+
* answer is still right at the deadline, it just stops being event-driven in between.
|
|
28
29
|
*/
|
|
29
30
|
const MAX_WAIT_CHECKS = 64;
|
|
30
31
|
/** The user property holding an IANA timezone id, read when a time_window says tz: 'user'. Same convention-table status as RECIPIENT_PROPERTY. */
|
|
@@ -170,10 +171,8 @@ async function checkGuards(stepId, ir, ctx) {
|
|
|
170
171
|
exitMet
|
|
171
172
|
};
|
|
172
173
|
});
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (goalAt !== null) {
|
|
176
|
-
state.convertedAt ??= goalAt;
|
|
174
|
+
if (result.goalAt !== null) {
|
|
175
|
+
state.convertedAt ??= result.goalAt;
|
|
177
176
|
if (goal?.exitOnMatch ?? true) return { kind: "goal" };
|
|
178
177
|
}
|
|
179
178
|
if (result.exitMet) return { kind: "exit_when" };
|
|
@@ -221,7 +220,7 @@ async function sendPush(node, ir, ctx) {
|
|
|
221
220
|
devices: node.devices === "latest" ? listed.slice(0, 1) : listed
|
|
222
221
|
};
|
|
223
222
|
});
|
|
224
|
-
if (plan === null
|
|
223
|
+
if (plan === null) return;
|
|
225
224
|
if (plan.devices.length === 0) {
|
|
226
225
|
console.log(`[journey] push ${node.template} skipped: no device for user ${ctx.userId}`);
|
|
227
226
|
return;
|
|
@@ -283,7 +282,8 @@ async function runNode(node, ir, ctx) {
|
|
|
283
282
|
return nextWindowStart(Date.now(), node.days, node.between, tz);
|
|
284
283
|
});
|
|
285
284
|
if (target === null) return FALL_THROUGH;
|
|
286
|
-
await ctx.step.
|
|
285
|
+
const ms = await ctx.step.do(`${node.id}:remaining`, async () => target - Date.now());
|
|
286
|
+
if (ms > 0) await ctx.step.sleep(`${node.id}:sleep`, ms);
|
|
287
287
|
return await checkGuards(node.id, ir, ctx) ?? FALL_THROUGH;
|
|
288
288
|
}
|
|
289
289
|
case "wait_until": {
|
|
@@ -344,7 +344,6 @@ async function runNode(node, ir, ctx) {
|
|
|
344
344
|
const args = await resolveValues(node.args, ctx.facts);
|
|
345
345
|
await invoker.invoke({
|
|
346
346
|
action: node.action,
|
|
347
|
-
codeHash: node.codeHash,
|
|
348
347
|
args,
|
|
349
348
|
userId: ctx.userId,
|
|
350
349
|
idempotencyKey: `${ctx.instanceId}:${node.id}`
|
|
@@ -391,8 +390,8 @@ async function awaitTail(ir, ctx) {
|
|
|
391
390
|
});
|
|
392
391
|
await ctx.step.unsubscribe("goal:unsubscribe");
|
|
393
392
|
if (hit === null) return null;
|
|
394
|
-
if (hit
|
|
395
|
-
state.convertedAt ??= hit.at
|
|
393
|
+
if (hit.kind === "goal") {
|
|
394
|
+
state.convertedAt ??= hit.at;
|
|
396
395
|
return { kind: "goal" };
|
|
397
396
|
}
|
|
398
397
|
return { kind: "exit_when" };
|
|
@@ -400,21 +399,29 @@ async function awaitTail(ir, ctx) {
|
|
|
400
399
|
/**
|
|
401
400
|
* Wait until `check` finds something or the deadline passes: subscribe, check, sleep until a
|
|
402
401
|
* relevant event wakes the instance or time runs out. `check` runs inside a step, so its result
|
|
403
|
-
* must be serializable; null means "not yet".
|
|
402
|
+
* must be serializable; null means "not yet". Null from here means the deadline passed with
|
|
403
|
+
* nothing found — never a failure, and never a wait given up on early.
|
|
404
404
|
*/
|
|
405
405
|
async function waitLoop(id, ctx, deadline, events, check) {
|
|
406
406
|
for (let attempt = 0; attempt < MAX_WAIT_CHECKS; attempt++) {
|
|
407
407
|
await ctx.step.subscribe(`${id}:subscribe:${attempt}`, events, { deadlineMs: deadline });
|
|
408
408
|
const met = await ctx.step.do(`${id}:check:${attempt}`, async () => check(Date.now()));
|
|
409
|
-
if (met !== null
|
|
409
|
+
if (met !== null) return met;
|
|
410
410
|
const remaining = await ctx.step.do(`${id}:remaining:${attempt}`, async () => deadline - Date.now());
|
|
411
411
|
if (remaining <= 0) return null;
|
|
412
|
-
if (await ctx.step.waitForWake(`${id}:wait:${attempt}`,
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
412
|
+
if (await ctx.step.waitForWake(`${id}:wait:${attempt}`, {
|
|
413
|
+
timeoutMs: remaining,
|
|
414
|
+
deadlineMs: deadline
|
|
415
|
+
}) === "timeout") return finalCheck(id, ctx, check);
|
|
416
416
|
}
|
|
417
|
-
|
|
417
|
+
console.warn(`[journey] ${ctx.instanceId} ${id}: ${MAX_WAIT_CHECKS} wake-ups without the condition holding — sleeping to the deadline`);
|
|
418
|
+
const budgetRemaining = await ctx.step.do(`${id}:budget:remaining`, async () => deadline - Date.now());
|
|
419
|
+
if (budgetRemaining > 0) await ctx.step.sleep(`${id}:budget:sleep`, budgetRemaining);
|
|
420
|
+
return finalCheck(id, ctx, check);
|
|
421
|
+
}
|
|
422
|
+
/** The check once the deadline is reached — one step for all the ways of getting there. */
|
|
423
|
+
async function finalCheck(id, ctx, check) {
|
|
424
|
+
return ctx.step.do(`${id}:final`, async () => check(Date.now()));
|
|
418
425
|
}
|
|
419
426
|
async function resolveValues(values, facts) {
|
|
420
427
|
const resolved = {};
|
|
@@ -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 {\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, { deadlineMs: deadline });\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, so a wake-up storm cannot exhaust the platform's step\n * budget. Each loop 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. Past the cap the wait\n * is not given up: it sleeps to its deadline and checks once more there (see waitLoop) — the\n * answer is still right at the deadline, it just stops being event-driven in between.\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 if (result.goalAt !== null) {\n state.convertedAt ??= result.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>(`${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\n if (plan === null) 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 // The remaining time is checkpointed: a replay must not recompute it from a later clock\n // (the target has passed by then, and a negative sleep is an error on some runtimes)\n const ms = await ctx.step.do(`${node.id}:remaining`, async () => target - Date.now());\n if (ms > 0) await ctx.step.sleep(`${node.id}:sleep`, ms);\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 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 if (hit.kind === 'goal') {\n state.convertedAt ??= hit.at;\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\". Null from here means the deadline passed with\n * nothing found — never a failure, and never a wait given up on early.\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, { deadlineMs: deadline });\n\n const met = await ctx.step.do(`${id}:check:${attempt}`, async () => check(Date.now()));\n if (met !== null) 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}`, {\n timeoutMs: remaining,\n deadlineMs: deadline,\n });\n // Final check after timeout: in a tight race the event may land exactly as the timer fires\n if (wake === 'timeout') return finalCheck(id, ctx, check);\n // Woken → back to the top of the loop to re-evaluate (at-least-once wake-up semantics)\n }\n // Check budget exhausted (see MAX_WAIT_CHECKS). Not a timeout: the wait keeps its deadline,\n // only the wake-ups are given up — sleep to the deadline, then check once more there. The\n // subscription rows stay until the caller's unsubscribe; a wake landing meanwhile is dropped\n // by the runtime (nothing is parked on it), which is the point.\n console.warn(\n `[journey] ${ctx.instanceId} ${id}: ${MAX_WAIT_CHECKS} wake-ups without the condition holding — sleeping to the deadline`\n );\n const budgetRemaining = await ctx.step.do(\n `${id}:budget:remaining`,\n async () => deadline - Date.now()\n );\n if (budgetRemaining > 0) await ctx.step.sleep(`${id}:budget:sleep`, budgetRemaining);\n return finalCheck(id, ctx, check);\n}\n\n/** The check once the deadline is reached — one step for all the ways of getting there. */\nasync function finalCheck<R>(\n id: string,\n ctx: JourneyContext,\n check: (now: number) => Promise<R | null>\n): Promise<R | null> {\n return ctx.step.do(`${id}:final`, async () => check(Date.now()));\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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"}
|
package/dist/engine/ports.d.cts
CHANGED
|
@@ -17,10 +17,11 @@ import { ChannelIR, ConditionIR, ScalarIR } from "../ir.cjs";
|
|
|
17
17
|
export interface EngineStep {
|
|
18
18
|
/** Durable step: fn's return value is checkpointed; on replay the result is reused without re-running. */
|
|
19
19
|
do<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Durable sleep: suspended time is not billed. The only sleep primitive — a wait until an
|
|
22
|
+
* absolute instant is the interpreter checkpointing the remaining time, then sleeping it.
|
|
23
|
+
*/
|
|
21
24
|
sleep(name: string, ms: number): Promise<void>;
|
|
22
|
-
/** Sleep until an absolute instant (used by time_window). */
|
|
23
|
-
sleepUntil(name: string, timestampMs: number): Promise<void>;
|
|
24
25
|
/**
|
|
25
26
|
* Durable: register interest in the named events (Cloudflare: write the
|
|
26
27
|
* subscription table; AWS: create a callback and store its id). Must
|
|
@@ -43,8 +44,18 @@ export interface EngineStep {
|
|
|
43
44
|
/**
|
|
44
45
|
* Wait for a wake-up or a timeout. A registration from `subscribe` must
|
|
45
46
|
* already exist — this call only parks the instance.
|
|
47
|
+
*
|
|
48
|
+
* Both numbers are checkpointed by the interpreter, so they are replay-stable: `timeoutMs`
|
|
49
|
+
* is how long this attempt may park (the remaining time when it was computed), `deadlineMs`
|
|
50
|
+
* the absolute instant the whole wait gives up. An adapter that learns of a timeout only by
|
|
51
|
+
* a thrown error (Cloudflare) decides against `deadlineMs` — the clock at the replay that
|
|
52
|
+
* first runs that decision is what matters, not when the park began; one whose wake handle
|
|
53
|
+
* carries its own timeout (AWS) set it from `deadlineMs` at subscribe and ignores both here.
|
|
46
54
|
*/
|
|
47
|
-
waitForWake(name: string,
|
|
55
|
+
waitForWake(name: string, wait: {
|
|
56
|
+
timeoutMs: number;
|
|
57
|
+
deadlineMs: number;
|
|
58
|
+
}): Promise<'event' | 'timeout'>;
|
|
48
59
|
}
|
|
49
60
|
/** One device a push can go to: the provider token (APNs device token, FCM registration token) and the platform that picks the provider. */
|
|
50
61
|
export interface PushDevice {
|
|
@@ -209,8 +220,6 @@ export interface EventSink {
|
|
|
209
220
|
export interface ActionInvocation {
|
|
210
221
|
/** Registry name; the code itself lives in the deployed bundle (dual-plane model, see dsl/action.ts). */
|
|
211
222
|
action: string;
|
|
212
|
-
/** Code-identity pin from the IR the journey is running; adapters compare it against the registered implementation. */
|
|
213
|
-
codeHash?: string | undefined;
|
|
214
223
|
/** Already resolved: user_property references have been replaced by actual values (same contract as message props). */
|
|
215
224
|
args: Record<string, ScalarIR | undefined>;
|
|
216
225
|
userId: string;
|
|
@@ -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
|
|
1
|
+
{"version":3,"file":"ports.d.cts","names":[],"sources":["../../src/engine/ports.ts"],"mappings":";;;;;;;;;;;;;;;;iBAiBiB;;EAEf,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;;;;;EAMnD,MAAM,cAAc,aAAa;;;;;;;;;;;;;;;EAgBjC,UACE,cACA,2BACA;IAAY;MACX;;EAGH,YAAY,eAAe;;;;;;;;;;;;EAa3B,YACE,cACA;IAAQ;IAAmB;MAC1B;;;iBAIY;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;;;;;;EAMA;;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,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/engine/ports.d.mts
CHANGED
|
@@ -17,10 +17,11 @@ import { ChannelIR, ConditionIR, ScalarIR } from "../ir.mjs";
|
|
|
17
17
|
export interface EngineStep {
|
|
18
18
|
/** Durable step: fn's return value is checkpointed; on replay the result is reused without re-running. */
|
|
19
19
|
do<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Durable sleep: suspended time is not billed. The only sleep primitive — a wait until an
|
|
22
|
+
* absolute instant is the interpreter checkpointing the remaining time, then sleeping it.
|
|
23
|
+
*/
|
|
21
24
|
sleep(name: string, ms: number): Promise<void>;
|
|
22
|
-
/** Sleep until an absolute instant (used by time_window). */
|
|
23
|
-
sleepUntil(name: string, timestampMs: number): Promise<void>;
|
|
24
25
|
/**
|
|
25
26
|
* Durable: register interest in the named events (Cloudflare: write the
|
|
26
27
|
* subscription table; AWS: create a callback and store its id). Must
|
|
@@ -43,8 +44,18 @@ export interface EngineStep {
|
|
|
43
44
|
/**
|
|
44
45
|
* Wait for a wake-up or a timeout. A registration from `subscribe` must
|
|
45
46
|
* already exist — this call only parks the instance.
|
|
47
|
+
*
|
|
48
|
+
* Both numbers are checkpointed by the interpreter, so they are replay-stable: `timeoutMs`
|
|
49
|
+
* is how long this attempt may park (the remaining time when it was computed), `deadlineMs`
|
|
50
|
+
* the absolute instant the whole wait gives up. An adapter that learns of a timeout only by
|
|
51
|
+
* a thrown error (Cloudflare) decides against `deadlineMs` — the clock at the replay that
|
|
52
|
+
* first runs that decision is what matters, not when the park began; one whose wake handle
|
|
53
|
+
* carries its own timeout (AWS) set it from `deadlineMs` at subscribe and ignores both here.
|
|
46
54
|
*/
|
|
47
|
-
waitForWake(name: string,
|
|
55
|
+
waitForWake(name: string, wait: {
|
|
56
|
+
timeoutMs: number;
|
|
57
|
+
deadlineMs: number;
|
|
58
|
+
}): Promise<'event' | 'timeout'>;
|
|
48
59
|
}
|
|
49
60
|
/** One device a push can go to: the provider token (APNs device token, FCM registration token) and the platform that picks the provider. */
|
|
50
61
|
export interface PushDevice {
|
|
@@ -209,8 +220,6 @@ export interface EventSink {
|
|
|
209
220
|
export interface ActionInvocation {
|
|
210
221
|
/** Registry name; the code itself lives in the deployed bundle (dual-plane model, see dsl/action.ts). */
|
|
211
222
|
action: string;
|
|
212
|
-
/** Code-identity pin from the IR the journey is running; adapters compare it against the registered implementation. */
|
|
213
|
-
codeHash?: string | undefined;
|
|
214
223
|
/** Already resolved: user_property references have been replaced by actual values (same contract as message props). */
|
|
215
224
|
args: Record<string, ScalarIR | undefined>;
|
|
216
225
|
userId: string;
|
|
@@ -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
|
|
1
|
+
{"version":3,"file":"ports.d.mts","names":[],"sources":["../../src/engine/ports.ts"],"mappings":";;;;;;;;;;;;;;;;iBAiBiB;;EAEf,GAAG,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;;;;;EAMnD,MAAM,cAAc,aAAa;;;;;;;;;;;;;;;EAgBjC,UACE,cACA,2BACA;IAAY;MACX;;EAGH,YAAY,eAAe;;;;;;;;;;;;EAa3B,YACE,cACA;IAAQ;IAAmB;MAC1B;;;iBAIY;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;;;;;;EAMA;;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,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/engine/retry.cjs
CHANGED
|
@@ -7,6 +7,18 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
7
7
|
* instead of burning the retry budget and delaying the journey. Every runtime adapter asks this
|
|
8
8
|
* one rule.
|
|
9
9
|
*/
|
|
10
|
+
/**
|
|
11
|
+
* A failure a retry would only repeat: a missing or malformed configuration, a provider refusing
|
|
12
|
+
* the request itself, an input the runtime cannot act on. Thrown inside a durable step, it ends
|
|
13
|
+
* the step at once (`isRetryable` is false) instead of burning the retry budget.
|
|
14
|
+
*/
|
|
15
|
+
var PermanentError = class extends Error {
|
|
16
|
+
name = "PermanentError";
|
|
17
|
+
retryable = false;
|
|
18
|
+
constructor(message, options) {
|
|
19
|
+
super(message, options);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
10
22
|
/** Errors the engine raises that are deterministic by construction. */
|
|
11
23
|
const DETERMINISTIC = /* @__PURE__ */ new Set(["TemplateValidationError", "ContractValidationError"]);
|
|
12
24
|
/**
|
|
@@ -23,6 +35,7 @@ function isRetryableStatus(status) {
|
|
|
23
35
|
return status === 408 || status === 429 || status >= 500;
|
|
24
36
|
}
|
|
25
37
|
//#endregion
|
|
38
|
+
exports.PermanentError = PermanentError;
|
|
26
39
|
exports.isRetryable = isRetryable;
|
|
27
40
|
exports.isRetryableStatus = isRetryableStatus;
|
|
28
41
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retry.cjs","names":[],"sources":["../../src/engine/retry.ts"],"sourcesContent":["/**\n * Which step failures a retry would only repeat. A durable step retries what it throws; an\n * error that is the same on every attempt — a template refusing its props, the contract\n * refusing an emitted event, a provider rejecting the request itself — ends the step at once\n * instead of burning the retry budget and delaying the journey. Every runtime adapter asks this\n * one rule.\n */\n\n/** Errors the engine raises that are deterministic by construction. */\nconst DETERMINISTIC = new Set(['TemplateValidationError', 'ContractValidationError']);\n\n/**\n * False for an error a retry cannot fix: one of the engine's deterministic refusals, or any\n * error that says so with `retryable: false` (the transports' 4xx answers).\n */\nexport function isRetryable(error: unknown): boolean {\n if (!(error instanceof Error)) return true;\n if (DETERMINISTIC.has(error.name)) return false;\n return (error as { retryable?: unknown }).retryable !== false;\n}\n\n/** An HTTP answer a retry can change: a timeout, throttling, or the provider's own fault. */\nexport function isRetryableStatus(status: number): boolean {\n return status === 408 || status === 429 || status >= 500;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"retry.cjs","names":[],"sources":["../../src/engine/retry.ts"],"sourcesContent":["/**\n * Which step failures a retry would only repeat. A durable step retries what it throws; an\n * error that is the same on every attempt — a template refusing its props, the contract\n * refusing an emitted event, a provider rejecting the request itself — ends the step at once\n * instead of burning the retry budget and delaying the journey. Every runtime adapter asks this\n * one rule.\n */\n\n/**\n * A failure a retry would only repeat: a missing or malformed configuration, a provider refusing\n * the request itself, an input the runtime cannot act on. Thrown inside a durable step, it ends\n * the step at once (`isRetryable` is false) instead of burning the retry budget.\n */\nexport class PermanentError extends Error {\n override readonly name: string = 'PermanentError';\n readonly retryable = false;\n constructor(message: string, options?: { cause?: unknown }) {\n super(message, options);\n }\n}\n\n/** Errors the engine raises that are deterministic by construction. */\nconst DETERMINISTIC = new Set(['TemplateValidationError', 'ContractValidationError']);\n\n/**\n * False for an error a retry cannot fix: one of the engine's deterministic refusals, or any\n * error that says so with `retryable: false` (the transports' 4xx answers).\n */\nexport function isRetryable(error: unknown): boolean {\n if (!(error instanceof Error)) return true;\n if (DETERMINISTIC.has(error.name)) return false;\n return (error as { retryable?: unknown }).retryable !== false;\n}\n\n/** An HTTP answer a retry can change: a timeout, throttling, or the provider's own fault. */\nexport function isRetryableStatus(status: number): boolean {\n return status === 408 || status === 429 || status >= 500;\n}\n"],"mappings":";;;;;;;;;;;;;;AAaA,IAAa,iBAAb,cAAoC,MAAM;CACxC,OAAiC;CACjC,YAAqB;CACrB,YAAY,SAAiB,SAA+B;EAC1D,MAAM,SAAS,OAAO;CACxB;AACF;;AAGA,MAAM,gCAAgB,IAAI,IAAI,CAAC,2BAA2B,yBAAyB,CAAC;;;;;AAMpF,SAAgB,YAAY,OAAyB;CACnD,IAAI,EAAE,iBAAiB,QAAQ,OAAO;CACtC,IAAI,cAAc,IAAI,MAAM,IAAI,GAAG,OAAO;CAC1C,OAAQ,MAAkC,cAAc;AAC1D;;AAGA,SAAgB,kBAAkB,QAAyB;CACzD,OAAO,WAAW,OAAO,WAAW,OAAO,UAAU;AACvD"}
|
package/dist/engine/retry.d.cts
CHANGED
|
@@ -6,6 +6,18 @@
|
|
|
6
6
|
* instead of burning the retry budget and delaying the journey. Every runtime adapter asks this
|
|
7
7
|
* one rule.
|
|
8
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* A failure a retry would only repeat: a missing or malformed configuration, a provider refusing
|
|
11
|
+
* the request itself, an input the runtime cannot act on. Thrown inside a durable step, it ends
|
|
12
|
+
* the step at once (`isRetryable` is false) instead of burning the retry budget.
|
|
13
|
+
*/
|
|
14
|
+
export declare class PermanentError extends Error {
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly retryable = false;
|
|
17
|
+
constructor(message: string, options?: {
|
|
18
|
+
cause?: unknown;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
9
21
|
/**
|
|
10
22
|
* False for an error a retry cannot fix: one of the engine's deterministic refusals, or any
|
|
11
23
|
* error that says so with `retryable: false` (the transports' 4xx answers).
|