@shware/workflow 0.1.0 → 0.2.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.
@@ -97,6 +97,8 @@ function performed(event, whereOrOpts, maybeOpts) {
97
97
  * (`POST /devices/changed`) or profile updates respectively.
98
98
  *
99
99
  * .branch([reachable('push'), (w) => w.push(tip)], (w) => w.email(tip))
100
+ *
101
+ * (`.prefer([t.push.tip, t.email.tip], props)` is this branch for one push and one email.)
100
102
  */
101
103
  function reachable(channel) {
102
104
  return cond({
@@ -1 +1 @@
1
- {"version":3,"file":"condition.cjs","names":["PAYLOAD_PATH","captureLoc","payloadRefs","durationIR"],"sources":["../../src/dsl/condition.ts"],"sourcesContent":["import type { ConditionIR, SourceLocIR } from '../ir';\nimport { captureLoc } from '../provenance';\nimport { type Duration, durationIR } from './base';\nimport {\n type EventRef,\n PAYLOAD_PATH,\n type PayloadRef,\n type PayloadRefs,\n type UserPropertyRef,\n payloadRefs,\n} from './refs';\nimport type { Channel } from './template';\n\n/**\n * Conditions — drizzle-style predicate expressions: free functions (eq/gt/…)\n * composed with and/or/not, plus `segment` for naming an expression as a\n * top-level asset. A condition is an opaque handle wrapping its ConditionIR.\n *\n * Type techniques:\n * - Types flow in from the reference's phantom type; operator applicability is\n * a signature constraint (gt only takes string|number refs, contains only\n * string ones), mirroring customer.io's condition panel.\n * - Value parameters are always NoInfer: types come from the reference only,\n * which stops eq(ref, 'typo') from widening T to include the bad value.\n *\n * TODO: JSON array 'where at least one'、JSON object 'has the property'。\n */\n\n/** Opaque condition handle — what predicates, combinators and segments all produce. */\nexport interface Condition {\n readonly __condition: true;\n}\n\ninterface ConditionInternal extends Condition {\n readonly ir: ConditionIR;\n}\n\nconst cond = (ir: ConditionIR, loc?: SourceLocIR): Condition => {\n const impl: ConditionInternal = {\n __condition: true,\n ir: loc === undefined ? ir : { ...ir, meta: { loc } },\n };\n return impl;\n};\n\n/** @internal unwrap a condition to its IR — used by flow/trigger/workflow builders */\nexport const condIR = (c: Condition): ConditionIR => {\n // Partial: the cast is unsound for hand-rolled objects — exactly what this checks\n const ir = (c as Partial<ConditionInternal>).ir;\n if (ir === undefined) {\n // A hand-rolled `{ __condition: true }` would otherwise surface as a\n // baffling zod error at toIR() time, far away from the actual mistake.\n throw new Error(\n 'Condition was not created by this DSL — use the predicates (eq/gt/…), combinators (and/or/not) or segment()'\n );\n }\n return ir;\n};\n\ntype Scalar = string | number | boolean;\n\n/**\n * Predicates accept either kind of reference: a user property (profile\n * condition) or an event payload field (only valid inside a `where` — the\n * bundle compiler enforces the placement, since both produce `Condition`).\n */\ntype CondRef<T> = UserPropertyRef<T> | PayloadRef<T>;\n\n/**\n * A payload ref identifies itself by a symbol key, and a user-property ref by\n * the absence of one. Nothing here reads a *string* key off the reference: a\n * payload ref hands out sub-refs for every string key, so a field named `type`\n * or `path` would otherwise answer in place of the ref itself.\n */\nconst payloadPathOf = (ref: CondRef<unknown>): string | undefined =>\n (ref as Partial<PayloadRef<unknown>>)[PAYLOAD_PATH];\n\nfunction prop(\n ref: CondRef<unknown>,\n op:\n | 'eq'\n | 'ne'\n | 'gt'\n | 'gte'\n | 'lt'\n | 'lte'\n | 'between'\n | 'not_between'\n | 'in_array'\n | 'not_in_array'\n | 'exists'\n | 'not_exists'\n | 'contains'\n | 'not_contains',\n value?: Scalar,\n values?: readonly Scalar[],\n loc?: SourceLocIR\n): Condition {\n const payloadPath = payloadPathOf(ref);\n return cond(\n {\n type: payloadPath !== undefined ? 'payload' : 'property',\n path: payloadPath ?? (ref as UserPropertyRef<unknown>).path,\n op,\n ...(value !== undefined ? { value } : {}),\n ...(values !== undefined ? { values: [...values] } : {}),\n },\n loc\n );\n}\n\nexport function eq<T>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'eq', value as Scalar, undefined, captureLoc(eq));\n}\nexport function ne<T>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'ne', value as Scalar, undefined, captureLoc(ne));\n}\nexport function gt<T extends string | number>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'gt', value, undefined, captureLoc(gt));\n}\nexport function gte<T extends string | number>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'gte', value, undefined, captureLoc(gte));\n}\nexport function lt<T extends string | number>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'lt', value, undefined, captureLoc(lt));\n}\nexport function lte<T extends string | number>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'lte', value, undefined, captureLoc(lte));\n}\nexport function between<T extends string | number>(\n ref: CondRef<T>,\n min: NoInfer<T>,\n max: NoInfer<T>\n): Condition {\n return prop(ref, 'between', undefined, [min, max], captureLoc(between));\n}\nexport function notBetween<T extends string | number>(\n ref: CondRef<T>,\n min: NoInfer<T>,\n max: NoInfer<T>\n): Condition {\n return prop(ref, 'not_between', undefined, [min, max], captureLoc(notBetween));\n}\nexport function inArray<T>(ref: CondRef<T>, values: readonly NoInfer<T>[]): Condition {\n return prop(ref, 'in_array', undefined, values as readonly Scalar[], captureLoc(inArray));\n}\nexport function notInArray<T>(ref: CondRef<T>, values: readonly NoInfer<T>[]): Condition {\n return prop(ref, 'not_in_array', undefined, values as readonly Scalar[], captureLoc(notInArray));\n}\nexport function exists(ref: CondRef<unknown>): Condition {\n return prop(ref, 'exists', undefined, undefined, captureLoc(exists));\n}\nexport function notExists(ref: CondRef<unknown>): Condition {\n return prop(ref, 'not_exists', undefined, undefined, captureLoc(notExists));\n}\nexport function contains<T extends string>(ref: CondRef<T>, value: string): Condition {\n return prop(ref, 'contains', value, undefined, captureLoc(contains));\n}\nexport function notContains<T extends string>(ref: CondRef<T>, value: string): Condition {\n return prop(ref, 'not_contains', value, undefined, captureLoc(notContains));\n}\n\nexport interface PerformedOptions {\n within?: Duration;\n count?: number;\n}\n\n/** A payload where clause: an arrow taking the typed payload-ref table. Runs once at construction — the result is a serializable condition tree in IR, never a closure. */\nexport type PayloadWhere<P> = (p: PayloadRefs<P>) => Condition;\n\n/**\n * Event predicate: performed an event (optionally within a window / at least N\n * times). \"Did not perform\" is `not(performed(...))` — expressed by the\n * combinator, so there is no separate notPerformed.\n *\n * The optional second argument narrows which events count by their payload —\n * an arrow receiving a typed payload reference table, with the ordinary\n * predicates applying. It compiles to a `where` subtree in IR (data the UI\n * renders), not a runtime closure:\n *\n * performed(e.login)\n * performed(e.login, (p) => and(eq(p.platform, 'web'), eq(p.tags.utm_source, 'meta')))\n * performed(e.purchase, (p) => gt(p.value, 100), { within: '30 days', count: 2 })\n * performed(e.purchase, { within: '30 days' }) // options without a where\n *\n * Time anchoring: evaluated inside a `goal` the count starts at workflow\n * entry, inside `waitUntil` at the moment the wait began (the engine anchors\n * them — a conversion or a wake must not be satisfied by pre-existing\n * history). Everywhere else (branch/filter/exitWhen/segments) it looks over\n * full history, bounded only by `within`.\n */\nexport function performed<P>(event: EventRef<P>, opts?: PerformedOptions): Condition;\nexport function performed<P>(\n event: EventRef<P>,\n where: PayloadWhere<P>,\n opts?: PerformedOptions\n): Condition;\nexport function performed<P>(\n event: EventRef<P>,\n whereOrOpts?: PayloadWhere<P> | PerformedOptions,\n maybeOpts?: PerformedOptions\n): Condition {\n const where = typeof whereOrOpts === 'function' ? whereOrOpts : undefined;\n const opts = typeof whereOrOpts === 'function' ? maybeOpts : whereOrOpts;\n return cond(\n {\n type: 'performed',\n event: event.name,\n ...(where !== undefined ? { where: condIR(where(payloadRefs<P>())) } : {}),\n ...(opts?.within !== undefined ? { within: durationIR(opts.within) } : {}),\n ...(opts?.count !== undefined ? { count: opts.count } : {}),\n },\n captureLoc(performed)\n );\n}\n\n/**\n * The user can be reached on a channel right now. For push it asks the host's\n * device directory (at least one device listed) — the engine keeps no device\n * state, so this is the way to branch on \"has the app on a device that allowed\n * notifications\"; for the other channels it checks the recipient property\n * (`email`, `phone`, …). Inside `waitUntil` it wakes on device changes\n * (`POST /devices/changed`) or profile updates respectively.\n *\n * .branch([reachable('push'), (w) => w.push(tip)], (w) => w.email(tip))\n */\nexport function reachable(channel: Channel): Condition {\n return cond({ type: 'reachable', channel }, captureLoc(reachable));\n}\n\n/** Condition combinators: nest freely. */\nexport function and(...conditions: readonly Condition[]): Condition {\n return cond({ type: 'and', conditions: conditions.map(condIR) }, captureLoc(and));\n}\nexport function or(...conditions: readonly Condition[]): Condition {\n return cond({ type: 'or', conditions: conditions.map(condIR) }, captureLoc(or));\n}\nexport function not(condition: Condition): Condition {\n return cond({ type: 'not', condition: condIR(condition) }, captureLoc(not));\n}\n\n/**\n * Segment: a named condition expression and a top-level asset — it shows up in\n * the UI sidebar, is reusable across workflows, is referenced by id in IR,\n * and has a continuously materialized membership count (the prerequisite for\n * segment triggers). Anonymous reuse is just a const expression; a one-off\n * check is an inline expression at the use site.\n * TODO, to match customer.io's panel: form / page / device / screen /\n * opt-out / message data(email opened、sms clicked、webhook …)\n */\nexport interface SegmentRef extends Condition {\n readonly __segment: true;\n /** Wire identity: segment references and the membership store are keyed on it (`SegmentIR.id`). */\n readonly id: string;\n}\n\n/** @internal the compiled shape behind a SegmentRef — compileBundle reads definition/loc off it */\nexport interface SegmentInternal extends SegmentRef {\n readonly ir: ConditionIR;\n /** Human label / description; metadata, so never part of the hash. */\n readonly meta: SegmentMeta | undefined;\n /** The segment's own definition (SegmentIR.condition), as opposed to a reference by id. */\n readonly definition: ConditionIR;\n /** Callsite of the segment() call (provenance; lands in SegmentIR.meta.loc). */\n readonly loc: SourceLocIR | undefined;\n}\n\n/** Labels for a segment: what the studio shows instead of the wire key. */\nexport interface SegmentMeta {\n name?: string;\n description?: string;\n}\n\nexport interface SegmentDefinition extends SegmentMeta {\n /**\n * Wire identity — by-name references and the membership store are keyed on\n * it, so it is not something to rename for readability.\n */\n id: string;\n /** Who is in the segment; the predicates already checked its types. */\n condition: Condition;\n}\n\n/**\n * Segment definition. `name` and `description` are optional labels: metadata,\n * dropped before hashing, and what the studio shows.\n *\n * export const purchaser = segment({\n * id: 'purchaser',\n * name: 'Purchased (last 30d)',\n * condition: performed(e.purchase, { within: '30 days' }),\n * });\n */\nexport function segment({ id, condition, name, description }: SegmentDefinition): SegmentRef {\n const meta: SegmentMeta = {\n ...(name === undefined ? {} : { name }),\n ...(description === undefined ? {} : { description }),\n };\n const impl: SegmentInternal = {\n __condition: true,\n __segment: true,\n id,\n ir: { type: 'segment', segment: id },\n definition: condIR(condition),\n loc: captureLoc(segment),\n meta: Object.keys(meta).length === 0 ? undefined : meta,\n };\n return impl;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"condition.cjs","names":["PAYLOAD_PATH","captureLoc","payloadRefs","durationIR"],"sources":["../../src/dsl/condition.ts"],"sourcesContent":["import type { ConditionIR, SourceLocIR } from '../ir';\nimport { captureLoc } from '../provenance';\nimport { type Duration, durationIR } from './base';\nimport {\n type EventRef,\n PAYLOAD_PATH,\n type PayloadRef,\n type PayloadRefs,\n type UserPropertyRef,\n payloadRefs,\n} from './refs';\nimport type { Channel } from './template';\n\n/**\n * Conditions — drizzle-style predicate expressions: free functions (eq/gt/…)\n * composed with and/or/not, plus `segment` for naming an expression as a\n * top-level asset. A condition is an opaque handle wrapping its ConditionIR.\n *\n * Type techniques:\n * - Types flow in from the reference's phantom type; operator applicability is\n * a signature constraint (gt only takes string|number refs, contains only\n * string ones), mirroring customer.io's condition panel.\n * - Value parameters are always NoInfer: types come from the reference only,\n * which stops eq(ref, 'typo') from widening T to include the bad value.\n *\n * TODO: JSON array 'where at least one'、JSON object 'has the property'。\n */\n\n/** Opaque condition handle — what predicates, combinators and segments all produce. */\nexport interface Condition {\n readonly __condition: true;\n}\n\ninterface ConditionInternal extends Condition {\n readonly ir: ConditionIR;\n}\n\nconst cond = (ir: ConditionIR, loc?: SourceLocIR): Condition => {\n const impl: ConditionInternal = {\n __condition: true,\n ir: loc === undefined ? ir : { ...ir, meta: { loc } },\n };\n return impl;\n};\n\n/** @internal unwrap a condition to its IR — used by flow/trigger/workflow builders */\nexport const condIR = (c: Condition): ConditionIR => {\n // Partial: the cast is unsound for hand-rolled objects — exactly what this checks\n const ir = (c as Partial<ConditionInternal>).ir;\n if (ir === undefined) {\n // A hand-rolled `{ __condition: true }` would otherwise surface as a\n // baffling zod error at toIR() time, far away from the actual mistake.\n throw new Error(\n 'Condition was not created by this DSL — use the predicates (eq/gt/…), combinators (and/or/not) or segment()'\n );\n }\n return ir;\n};\n\ntype Scalar = string | number | boolean;\n\n/**\n * Predicates accept either kind of reference: a user property (profile\n * condition) or an event payload field (only valid inside a `where` — the\n * bundle compiler enforces the placement, since both produce `Condition`).\n */\ntype CondRef<T> = UserPropertyRef<T> | PayloadRef<T>;\n\n/**\n * A payload ref identifies itself by a symbol key, and a user-property ref by\n * the absence of one. Nothing here reads a *string* key off the reference: a\n * payload ref hands out sub-refs for every string key, so a field named `type`\n * or `path` would otherwise answer in place of the ref itself.\n */\nconst payloadPathOf = (ref: CondRef<unknown>): string | undefined =>\n (ref as Partial<PayloadRef<unknown>>)[PAYLOAD_PATH];\n\nfunction prop(\n ref: CondRef<unknown>,\n op:\n | 'eq'\n | 'ne'\n | 'gt'\n | 'gte'\n | 'lt'\n | 'lte'\n | 'between'\n | 'not_between'\n | 'in_array'\n | 'not_in_array'\n | 'exists'\n | 'not_exists'\n | 'contains'\n | 'not_contains',\n value?: Scalar,\n values?: readonly Scalar[],\n loc?: SourceLocIR\n): Condition {\n const payloadPath = payloadPathOf(ref);\n return cond(\n {\n type: payloadPath !== undefined ? 'payload' : 'property',\n path: payloadPath ?? (ref as UserPropertyRef<unknown>).path,\n op,\n ...(value !== undefined ? { value } : {}),\n ...(values !== undefined ? { values: [...values] } : {}),\n },\n loc\n );\n}\n\nexport function eq<T>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'eq', value as Scalar, undefined, captureLoc(eq));\n}\nexport function ne<T>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'ne', value as Scalar, undefined, captureLoc(ne));\n}\nexport function gt<T extends string | number>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'gt', value, undefined, captureLoc(gt));\n}\nexport function gte<T extends string | number>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'gte', value, undefined, captureLoc(gte));\n}\nexport function lt<T extends string | number>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'lt', value, undefined, captureLoc(lt));\n}\nexport function lte<T extends string | number>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'lte', value, undefined, captureLoc(lte));\n}\nexport function between<T extends string | number>(\n ref: CondRef<T>,\n min: NoInfer<T>,\n max: NoInfer<T>\n): Condition {\n return prop(ref, 'between', undefined, [min, max], captureLoc(between));\n}\nexport function notBetween<T extends string | number>(\n ref: CondRef<T>,\n min: NoInfer<T>,\n max: NoInfer<T>\n): Condition {\n return prop(ref, 'not_between', undefined, [min, max], captureLoc(notBetween));\n}\nexport function inArray<T>(ref: CondRef<T>, values: readonly NoInfer<T>[]): Condition {\n return prop(ref, 'in_array', undefined, values as readonly Scalar[], captureLoc(inArray));\n}\nexport function notInArray<T>(ref: CondRef<T>, values: readonly NoInfer<T>[]): Condition {\n return prop(ref, 'not_in_array', undefined, values as readonly Scalar[], captureLoc(notInArray));\n}\nexport function exists(ref: CondRef<unknown>): Condition {\n return prop(ref, 'exists', undefined, undefined, captureLoc(exists));\n}\nexport function notExists(ref: CondRef<unknown>): Condition {\n return prop(ref, 'not_exists', undefined, undefined, captureLoc(notExists));\n}\nexport function contains<T extends string>(ref: CondRef<T>, value: string): Condition {\n return prop(ref, 'contains', value, undefined, captureLoc(contains));\n}\nexport function notContains<T extends string>(ref: CondRef<T>, value: string): Condition {\n return prop(ref, 'not_contains', value, undefined, captureLoc(notContains));\n}\n\nexport interface PerformedOptions {\n within?: Duration;\n count?: number;\n}\n\n/** A payload where clause: an arrow taking the typed payload-ref table. Runs once at construction — the result is a serializable condition tree in IR, never a closure. */\nexport type PayloadWhere<P> = (p: PayloadRefs<P>) => Condition;\n\n/**\n * Event predicate: performed an event (optionally within a window / at least N\n * times). \"Did not perform\" is `not(performed(...))` — expressed by the\n * combinator, so there is no separate notPerformed.\n *\n * The optional second argument narrows which events count by their payload —\n * an arrow receiving a typed payload reference table, with the ordinary\n * predicates applying. It compiles to a `where` subtree in IR (data the UI\n * renders), not a runtime closure:\n *\n * performed(e.login)\n * performed(e.login, (p) => and(eq(p.platform, 'web'), eq(p.tags.utm_source, 'meta')))\n * performed(e.purchase, (p) => gt(p.value, 100), { within: '30 days', count: 2 })\n * performed(e.purchase, { within: '30 days' }) // options without a where\n *\n * Time anchoring: evaluated inside a `goal` the count starts at workflow\n * entry, inside `waitUntil` at the moment the wait began (the engine anchors\n * them — a conversion or a wake must not be satisfied by pre-existing\n * history). Everywhere else (branch/filter/exitWhen/segments) it looks over\n * full history, bounded only by `within`.\n */\nexport function performed<P>(event: EventRef<P>, opts?: PerformedOptions): Condition;\nexport function performed<P>(\n event: EventRef<P>,\n where: PayloadWhere<P>,\n opts?: PerformedOptions\n): Condition;\nexport function performed<P>(\n event: EventRef<P>,\n whereOrOpts?: PayloadWhere<P> | PerformedOptions,\n maybeOpts?: PerformedOptions\n): Condition {\n const where = typeof whereOrOpts === 'function' ? whereOrOpts : undefined;\n const opts = typeof whereOrOpts === 'function' ? maybeOpts : whereOrOpts;\n return cond(\n {\n type: 'performed',\n event: event.name,\n ...(where !== undefined ? { where: condIR(where(payloadRefs<P>())) } : {}),\n ...(opts?.within !== undefined ? { within: durationIR(opts.within) } : {}),\n ...(opts?.count !== undefined ? { count: opts.count } : {}),\n },\n captureLoc(performed)\n );\n}\n\n/**\n * The user can be reached on a channel right now. For push it asks the host's\n * device directory (at least one device listed) — the engine keeps no device\n * state, so this is the way to branch on \"has the app on a device that allowed\n * notifications\"; for the other channels it checks the recipient property\n * (`email`, `phone`, …). Inside `waitUntil` it wakes on device changes\n * (`POST /devices/changed`) or profile updates respectively.\n *\n * .branch([reachable('push'), (w) => w.push(tip)], (w) => w.email(tip))\n *\n * (`.prefer([t.push.tip, t.email.tip], props)` is this branch for one push and one email.)\n */\nexport function reachable(channel: Channel): Condition {\n return cond({ type: 'reachable', channel }, captureLoc(reachable));\n}\n\n/** Condition combinators: nest freely. */\nexport function and(...conditions: readonly Condition[]): Condition {\n return cond({ type: 'and', conditions: conditions.map(condIR) }, captureLoc(and));\n}\nexport function or(...conditions: readonly Condition[]): Condition {\n return cond({ type: 'or', conditions: conditions.map(condIR) }, captureLoc(or));\n}\nexport function not(condition: Condition): Condition {\n return cond({ type: 'not', condition: condIR(condition) }, captureLoc(not));\n}\n\n/**\n * Segment: a named condition expression and a top-level asset — it shows up in\n * the UI sidebar, is reusable across workflows, is referenced by id in IR,\n * and has a continuously materialized membership count (the prerequisite for\n * segment triggers). Anonymous reuse is just a const expression; a one-off\n * check is an inline expression at the use site.\n * TODO, to match customer.io's panel: form / page / device / screen /\n * opt-out / message data(email opened、sms clicked、webhook …)\n */\nexport interface SegmentRef extends Condition {\n readonly __segment: true;\n /** Wire identity: segment references and the membership store are keyed on it (`SegmentIR.id`). */\n readonly id: string;\n}\n\n/** @internal the compiled shape behind a SegmentRef — compileBundle reads definition/loc off it */\nexport interface SegmentInternal extends SegmentRef {\n readonly ir: ConditionIR;\n /** Human label / description; metadata, so never part of the hash. */\n readonly meta: SegmentMeta | undefined;\n /** The segment's own definition (SegmentIR.condition), as opposed to a reference by id. */\n readonly definition: ConditionIR;\n /** Callsite of the segment() call (provenance; lands in SegmentIR.meta.loc). */\n readonly loc: SourceLocIR | undefined;\n}\n\n/** Labels for a segment: what the studio shows instead of the wire key. */\nexport interface SegmentMeta {\n name?: string;\n description?: string;\n}\n\nexport interface SegmentDefinition extends SegmentMeta {\n /**\n * Wire identity — by-name references and the membership store are keyed on\n * it, so it is not something to rename for readability.\n */\n id: string;\n /** Who is in the segment; the predicates already checked its types. */\n condition: Condition;\n}\n\n/**\n * Segment definition. `name` and `description` are optional labels: metadata,\n * dropped before hashing, and what the studio shows.\n *\n * export const purchaser = segment({\n * id: 'purchaser',\n * name: 'Purchased (last 30d)',\n * condition: performed(e.purchase, { within: '30 days' }),\n * });\n */\nexport function segment({ id, condition, name, description }: SegmentDefinition): SegmentRef {\n const meta: SegmentMeta = {\n ...(name === undefined ? {} : { name }),\n ...(description === undefined ? {} : { description }),\n };\n const impl: SegmentInternal = {\n __condition: true,\n __segment: true,\n id,\n ir: { type: 'segment', segment: id },\n definition: condIR(condition),\n loc: captureLoc(segment),\n meta: Object.keys(meta).length === 0 ? undefined : meta,\n };\n return impl;\n}\n"],"mappings":"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"}
@@ -81,6 +81,8 @@ export declare function performed<P>(event: EventRef<P>, where: PayloadWhere<P>,
81
81
  * (`POST /devices/changed`) or profile updates respectively.
82
82
  *
83
83
  * .branch([reachable('push'), (w) => w.push(tip)], (w) => w.email(tip))
84
+ *
85
+ * (`.prefer([t.push.tip, t.email.tip], props)` is this branch for one push and one email.)
84
86
  */
85
87
  export declare function reachable(channel: Channel): Condition;
86
88
  /** Condition combinators: nest freely. */
@@ -1 +1 @@
1
- {"version":3,"file":"condition.d.cts","names":[],"sources":["../../src/dsl/condition.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;iBA6BiB;WACN;;;qBAgBE,SAAM,GAAO,cAAY;;;;;;KAoBjC,QAAQ,KAAK,gBAAgB,KAAK,WAAW;wBA6ClC,GAAG,GAAG,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAG3C,GAAG,GAAG,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAG3C,GAAG,2BAA2B,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAGnE,IAAI,2BAA2B,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAGpE,GAAG,2BAA2B,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAGnE,IAAI,2BAA2B,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAGpE,QAAQ,2BACtB,KAAK,QAAQ,IACb,KAAK,QAAQ,IACb,KAAK,QAAQ,KACZ;wBAGa,WAAW,2BACzB,KAAK,QAAQ,IACb,KAAK,QAAQ,IACb,KAAK,QAAQ,KACZ;wBAGa,QAAQ,GAAG,KAAK,QAAQ,IAAI,iBAAiB,QAAQ,OAAO;wBAG5D,WAAW,GAAG,KAAK,QAAQ,IAAI,iBAAiB,QAAQ,OAAO;wBAG/D,OAAO,KAAK,mBAAmB;wBAG/B,UAAU,KAAK,mBAAmB;wBAGlC,SAAS,kBAAkB,KAAK,QAAQ,IAAI,gBAAgB;wBAG5D,YAAY,kBAAkB,KAAK,QAAQ,IAAI,gBAAgB;iBAI9D;EACf,SAAS;EACT;;;YAIU,aAAa,MAAM,GAAG,YAAY,OAAO;;;;;;;;;;;;;;;;;;;;;;wBAuBrC,UAAU,GAAG,OAAO,SAAS,IAAI,OAAO,mBAAmB;wBAC3D,UAAU,GACxB,OAAO,SAAS,IAChB,OAAO,aAAa,IACpB,OAAO,mBACN;;;;;;;;;;;wBA8Ba,UAAU,SAAS,UAAU;;wBAK7B,OAAO,qBAAqB,cAAc;wBAG1C,MAAM,qBAAqB,cAAc;wBAGzC,IAAI,WAAW,YAAY;;;;;;;;;;iBAa1B,mBAAmB;WACzB;;WAEA;;;iBAIM,wBAAwB;WAC9B,IAAI;;WAEJ,MAAM;;WAEN,YAAY;;WAEZ,KAAK;;;iBAIC;EACf;EACA;;iBAGe,0BAA0B;;;;;EAKzC;;EAEA,WAAW;;;;;;;;;;;;wBAaG,UAAU,IAAI,WAAW,MAAM,eAAe,oBAAoB"}
1
+ {"version":3,"file":"condition.d.cts","names":[],"sources":["../../src/dsl/condition.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;iBA6BiB;WACN;;;qBAgBE,SAAM,GAAO,cAAY;;;;;;KAoBjC,QAAQ,KAAK,gBAAgB,KAAK,WAAW;wBA6ClC,GAAG,GAAG,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAG3C,GAAG,GAAG,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAG3C,GAAG,2BAA2B,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAGnE,IAAI,2BAA2B,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAGpE,GAAG,2BAA2B,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAGnE,IAAI,2BAA2B,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAGpE,QAAQ,2BACtB,KAAK,QAAQ,IACb,KAAK,QAAQ,IACb,KAAK,QAAQ,KACZ;wBAGa,WAAW,2BACzB,KAAK,QAAQ,IACb,KAAK,QAAQ,IACb,KAAK,QAAQ,KACZ;wBAGa,QAAQ,GAAG,KAAK,QAAQ,IAAI,iBAAiB,QAAQ,OAAO;wBAG5D,WAAW,GAAG,KAAK,QAAQ,IAAI,iBAAiB,QAAQ,OAAO;wBAG/D,OAAO,KAAK,mBAAmB;wBAG/B,UAAU,KAAK,mBAAmB;wBAGlC,SAAS,kBAAkB,KAAK,QAAQ,IAAI,gBAAgB;wBAG5D,YAAY,kBAAkB,KAAK,QAAQ,IAAI,gBAAgB;iBAI9D;EACf,SAAS;EACT;;;YAIU,aAAa,MAAM,GAAG,YAAY,OAAO;;;;;;;;;;;;;;;;;;;;;;wBAuBrC,UAAU,GAAG,OAAO,SAAS,IAAI,OAAO,mBAAmB;wBAC3D,UAAU,GACxB,OAAO,SAAS,IAChB,OAAO,aAAa,IACpB,OAAO,mBACN;;;;;;;;;;;;;wBAgCa,UAAU,SAAS,UAAU;;wBAK7B,OAAO,qBAAqB,cAAc;wBAG1C,MAAM,qBAAqB,cAAc;wBAGzC,IAAI,WAAW,YAAY;;;;;;;;;;iBAa1B,mBAAmB;WACzB;;WAEA;;;iBAIM,wBAAwB;WAC9B,IAAI;;WAEJ,MAAM;;WAEN,YAAY;;WAEZ,KAAK;;;iBAIC;EACf;EACA;;iBAGe,0BAA0B;;;;;EAKzC;;EAEA,WAAW;;;;;;;;;;;;wBAaG,UAAU,IAAI,WAAW,MAAM,eAAe,oBAAoB"}
@@ -81,6 +81,8 @@ export declare function performed<P>(event: EventRef<P>, where: PayloadWhere<P>,
81
81
  * (`POST /devices/changed`) or profile updates respectively.
82
82
  *
83
83
  * .branch([reachable('push'), (w) => w.push(tip)], (w) => w.email(tip))
84
+ *
85
+ * (`.prefer([t.push.tip, t.email.tip], props)` is this branch for one push and one email.)
84
86
  */
85
87
  export declare function reachable(channel: Channel): Condition;
86
88
  /** Condition combinators: nest freely. */
@@ -1 +1 @@
1
- {"version":3,"file":"condition.d.mts","names":[],"sources":["../../src/dsl/condition.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;iBA6BiB;WACN;;;qBAgBE,SAAM,GAAO,cAAY;;;;;;KAoBjC,QAAQ,KAAK,gBAAgB,KAAK,WAAW;wBA6ClC,GAAG,GAAG,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAG3C,GAAG,GAAG,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAG3C,GAAG,2BAA2B,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAGnE,IAAI,2BAA2B,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAGpE,GAAG,2BAA2B,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAGnE,IAAI,2BAA2B,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAGpE,QAAQ,2BACtB,KAAK,QAAQ,IACb,KAAK,QAAQ,IACb,KAAK,QAAQ,KACZ;wBAGa,WAAW,2BACzB,KAAK,QAAQ,IACb,KAAK,QAAQ,IACb,KAAK,QAAQ,KACZ;wBAGa,QAAQ,GAAG,KAAK,QAAQ,IAAI,iBAAiB,QAAQ,OAAO;wBAG5D,WAAW,GAAG,KAAK,QAAQ,IAAI,iBAAiB,QAAQ,OAAO;wBAG/D,OAAO,KAAK,mBAAmB;wBAG/B,UAAU,KAAK,mBAAmB;wBAGlC,SAAS,kBAAkB,KAAK,QAAQ,IAAI,gBAAgB;wBAG5D,YAAY,kBAAkB,KAAK,QAAQ,IAAI,gBAAgB;iBAI9D;EACf,SAAS;EACT;;;YAIU,aAAa,MAAM,GAAG,YAAY,OAAO;;;;;;;;;;;;;;;;;;;;;;wBAuBrC,UAAU,GAAG,OAAO,SAAS,IAAI,OAAO,mBAAmB;wBAC3D,UAAU,GACxB,OAAO,SAAS,IAChB,OAAO,aAAa,IACpB,OAAO,mBACN;;;;;;;;;;;wBA8Ba,UAAU,SAAS,UAAU;;wBAK7B,OAAO,qBAAqB,cAAc;wBAG1C,MAAM,qBAAqB,cAAc;wBAGzC,IAAI,WAAW,YAAY;;;;;;;;;;iBAa1B,mBAAmB;WACzB;;WAEA;;;iBAIM,wBAAwB;WAC9B,IAAI;;WAEJ,MAAM;;WAEN,YAAY;;WAEZ,KAAK;;;iBAIC;EACf;EACA;;iBAGe,0BAA0B;;;;;EAKzC;;EAEA,WAAW;;;;;;;;;;;;wBAaG,UAAU,IAAI,WAAW,MAAM,eAAe,oBAAoB"}
1
+ {"version":3,"file":"condition.d.mts","names":[],"sources":["../../src/dsl/condition.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;iBA6BiB;WACN;;;qBAgBE,SAAM,GAAO,cAAY;;;;;;KAoBjC,QAAQ,KAAK,gBAAgB,KAAK,WAAW;wBA6ClC,GAAG,GAAG,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAG3C,GAAG,GAAG,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAG3C,GAAG,2BAA2B,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAGnE,IAAI,2BAA2B,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAGpE,GAAG,2BAA2B,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAGnE,IAAI,2BAA2B,KAAK,QAAQ,IAAI,OAAO,QAAQ,KAAK;wBAGpE,QAAQ,2BACtB,KAAK,QAAQ,IACb,KAAK,QAAQ,IACb,KAAK,QAAQ,KACZ;wBAGa,WAAW,2BACzB,KAAK,QAAQ,IACb,KAAK,QAAQ,IACb,KAAK,QAAQ,KACZ;wBAGa,QAAQ,GAAG,KAAK,QAAQ,IAAI,iBAAiB,QAAQ,OAAO;wBAG5D,WAAW,GAAG,KAAK,QAAQ,IAAI,iBAAiB,QAAQ,OAAO;wBAG/D,OAAO,KAAK,mBAAmB;wBAG/B,UAAU,KAAK,mBAAmB;wBAGlC,SAAS,kBAAkB,KAAK,QAAQ,IAAI,gBAAgB;wBAG5D,YAAY,kBAAkB,KAAK,QAAQ,IAAI,gBAAgB;iBAI9D;EACf,SAAS;EACT;;;YAIU,aAAa,MAAM,GAAG,YAAY,OAAO;;;;;;;;;;;;;;;;;;;;;;wBAuBrC,UAAU,GAAG,OAAO,SAAS,IAAI,OAAO,mBAAmB;wBAC3D,UAAU,GACxB,OAAO,SAAS,IAChB,OAAO,aAAa,IACpB,OAAO,mBACN;;;;;;;;;;;;;wBAgCa,UAAU,SAAS,UAAU;;wBAK7B,OAAO,qBAAqB,cAAc;wBAG1C,MAAM,qBAAqB,cAAc;wBAGzC,IAAI,WAAW,YAAY;;;;;;;;;;iBAa1B,mBAAmB;WACzB;;WAEA;;;iBAIM,wBAAwB;WAC9B,IAAI;;WAEJ,MAAM;;WAEN,YAAY;;WAEZ,KAAK;;;iBAIC;EACf;EACA;;iBAGe,0BAA0B;;;;;EAKzC;;EAEA,WAAW;;;;;;;;;;;;wBAaG,UAAU,IAAI,WAAW,MAAM,eAAe,oBAAoB"}
@@ -96,6 +96,8 @@ function performed(event, whereOrOpts, maybeOpts) {
96
96
  * (`POST /devices/changed`) or profile updates respectively.
97
97
  *
98
98
  * .branch([reachable('push'), (w) => w.push(tip)], (w) => w.email(tip))
99
+ *
100
+ * (`.prefer([t.push.tip, t.email.tip], props)` is this branch for one push and one email.)
99
101
  */
100
102
  function reachable(channel) {
101
103
  return cond({
@@ -1 +1 @@
1
- {"version":3,"file":"condition.mjs","names":[],"sources":["../../src/dsl/condition.ts"],"sourcesContent":["import type { ConditionIR, SourceLocIR } from '../ir';\nimport { captureLoc } from '../provenance';\nimport { type Duration, durationIR } from './base';\nimport {\n type EventRef,\n PAYLOAD_PATH,\n type PayloadRef,\n type PayloadRefs,\n type UserPropertyRef,\n payloadRefs,\n} from './refs';\nimport type { Channel } from './template';\n\n/**\n * Conditions — drizzle-style predicate expressions: free functions (eq/gt/…)\n * composed with and/or/not, plus `segment` for naming an expression as a\n * top-level asset. A condition is an opaque handle wrapping its ConditionIR.\n *\n * Type techniques:\n * - Types flow in from the reference's phantom type; operator applicability is\n * a signature constraint (gt only takes string|number refs, contains only\n * string ones), mirroring customer.io's condition panel.\n * - Value parameters are always NoInfer: types come from the reference only,\n * which stops eq(ref, 'typo') from widening T to include the bad value.\n *\n * TODO: JSON array 'where at least one'、JSON object 'has the property'。\n */\n\n/** Opaque condition handle — what predicates, combinators and segments all produce. */\nexport interface Condition {\n readonly __condition: true;\n}\n\ninterface ConditionInternal extends Condition {\n readonly ir: ConditionIR;\n}\n\nconst cond = (ir: ConditionIR, loc?: SourceLocIR): Condition => {\n const impl: ConditionInternal = {\n __condition: true,\n ir: loc === undefined ? ir : { ...ir, meta: { loc } },\n };\n return impl;\n};\n\n/** @internal unwrap a condition to its IR — used by flow/trigger/workflow builders */\nexport const condIR = (c: Condition): ConditionIR => {\n // Partial: the cast is unsound for hand-rolled objects — exactly what this checks\n const ir = (c as Partial<ConditionInternal>).ir;\n if (ir === undefined) {\n // A hand-rolled `{ __condition: true }` would otherwise surface as a\n // baffling zod error at toIR() time, far away from the actual mistake.\n throw new Error(\n 'Condition was not created by this DSL — use the predicates (eq/gt/…), combinators (and/or/not) or segment()'\n );\n }\n return ir;\n};\n\ntype Scalar = string | number | boolean;\n\n/**\n * Predicates accept either kind of reference: a user property (profile\n * condition) or an event payload field (only valid inside a `where` — the\n * bundle compiler enforces the placement, since both produce `Condition`).\n */\ntype CondRef<T> = UserPropertyRef<T> | PayloadRef<T>;\n\n/**\n * A payload ref identifies itself by a symbol key, and a user-property ref by\n * the absence of one. Nothing here reads a *string* key off the reference: a\n * payload ref hands out sub-refs for every string key, so a field named `type`\n * or `path` would otherwise answer in place of the ref itself.\n */\nconst payloadPathOf = (ref: CondRef<unknown>): string | undefined =>\n (ref as Partial<PayloadRef<unknown>>)[PAYLOAD_PATH];\n\nfunction prop(\n ref: CondRef<unknown>,\n op:\n | 'eq'\n | 'ne'\n | 'gt'\n | 'gte'\n | 'lt'\n | 'lte'\n | 'between'\n | 'not_between'\n | 'in_array'\n | 'not_in_array'\n | 'exists'\n | 'not_exists'\n | 'contains'\n | 'not_contains',\n value?: Scalar,\n values?: readonly Scalar[],\n loc?: SourceLocIR\n): Condition {\n const payloadPath = payloadPathOf(ref);\n return cond(\n {\n type: payloadPath !== undefined ? 'payload' : 'property',\n path: payloadPath ?? (ref as UserPropertyRef<unknown>).path,\n op,\n ...(value !== undefined ? { value } : {}),\n ...(values !== undefined ? { values: [...values] } : {}),\n },\n loc\n );\n}\n\nexport function eq<T>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'eq', value as Scalar, undefined, captureLoc(eq));\n}\nexport function ne<T>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'ne', value as Scalar, undefined, captureLoc(ne));\n}\nexport function gt<T extends string | number>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'gt', value, undefined, captureLoc(gt));\n}\nexport function gte<T extends string | number>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'gte', value, undefined, captureLoc(gte));\n}\nexport function lt<T extends string | number>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'lt', value, undefined, captureLoc(lt));\n}\nexport function lte<T extends string | number>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'lte', value, undefined, captureLoc(lte));\n}\nexport function between<T extends string | number>(\n ref: CondRef<T>,\n min: NoInfer<T>,\n max: NoInfer<T>\n): Condition {\n return prop(ref, 'between', undefined, [min, max], captureLoc(between));\n}\nexport function notBetween<T extends string | number>(\n ref: CondRef<T>,\n min: NoInfer<T>,\n max: NoInfer<T>\n): Condition {\n return prop(ref, 'not_between', undefined, [min, max], captureLoc(notBetween));\n}\nexport function inArray<T>(ref: CondRef<T>, values: readonly NoInfer<T>[]): Condition {\n return prop(ref, 'in_array', undefined, values as readonly Scalar[], captureLoc(inArray));\n}\nexport function notInArray<T>(ref: CondRef<T>, values: readonly NoInfer<T>[]): Condition {\n return prop(ref, 'not_in_array', undefined, values as readonly Scalar[], captureLoc(notInArray));\n}\nexport function exists(ref: CondRef<unknown>): Condition {\n return prop(ref, 'exists', undefined, undefined, captureLoc(exists));\n}\nexport function notExists(ref: CondRef<unknown>): Condition {\n return prop(ref, 'not_exists', undefined, undefined, captureLoc(notExists));\n}\nexport function contains<T extends string>(ref: CondRef<T>, value: string): Condition {\n return prop(ref, 'contains', value, undefined, captureLoc(contains));\n}\nexport function notContains<T extends string>(ref: CondRef<T>, value: string): Condition {\n return prop(ref, 'not_contains', value, undefined, captureLoc(notContains));\n}\n\nexport interface PerformedOptions {\n within?: Duration;\n count?: number;\n}\n\n/** A payload where clause: an arrow taking the typed payload-ref table. Runs once at construction — the result is a serializable condition tree in IR, never a closure. */\nexport type PayloadWhere<P> = (p: PayloadRefs<P>) => Condition;\n\n/**\n * Event predicate: performed an event (optionally within a window / at least N\n * times). \"Did not perform\" is `not(performed(...))` — expressed by the\n * combinator, so there is no separate notPerformed.\n *\n * The optional second argument narrows which events count by their payload —\n * an arrow receiving a typed payload reference table, with the ordinary\n * predicates applying. It compiles to a `where` subtree in IR (data the UI\n * renders), not a runtime closure:\n *\n * performed(e.login)\n * performed(e.login, (p) => and(eq(p.platform, 'web'), eq(p.tags.utm_source, 'meta')))\n * performed(e.purchase, (p) => gt(p.value, 100), { within: '30 days', count: 2 })\n * performed(e.purchase, { within: '30 days' }) // options without a where\n *\n * Time anchoring: evaluated inside a `goal` the count starts at workflow\n * entry, inside `waitUntil` at the moment the wait began (the engine anchors\n * them — a conversion or a wake must not be satisfied by pre-existing\n * history). Everywhere else (branch/filter/exitWhen/segments) it looks over\n * full history, bounded only by `within`.\n */\nexport function performed<P>(event: EventRef<P>, opts?: PerformedOptions): Condition;\nexport function performed<P>(\n event: EventRef<P>,\n where: PayloadWhere<P>,\n opts?: PerformedOptions\n): Condition;\nexport function performed<P>(\n event: EventRef<P>,\n whereOrOpts?: PayloadWhere<P> | PerformedOptions,\n maybeOpts?: PerformedOptions\n): Condition {\n const where = typeof whereOrOpts === 'function' ? whereOrOpts : undefined;\n const opts = typeof whereOrOpts === 'function' ? maybeOpts : whereOrOpts;\n return cond(\n {\n type: 'performed',\n event: event.name,\n ...(where !== undefined ? { where: condIR(where(payloadRefs<P>())) } : {}),\n ...(opts?.within !== undefined ? { within: durationIR(opts.within) } : {}),\n ...(opts?.count !== undefined ? { count: opts.count } : {}),\n },\n captureLoc(performed)\n );\n}\n\n/**\n * The user can be reached on a channel right now. For push it asks the host's\n * device directory (at least one device listed) — the engine keeps no device\n * state, so this is the way to branch on \"has the app on a device that allowed\n * notifications\"; for the other channels it checks the recipient property\n * (`email`, `phone`, …). Inside `waitUntil` it wakes on device changes\n * (`POST /devices/changed`) or profile updates respectively.\n *\n * .branch([reachable('push'), (w) => w.push(tip)], (w) => w.email(tip))\n */\nexport function reachable(channel: Channel): Condition {\n return cond({ type: 'reachable', channel }, captureLoc(reachable));\n}\n\n/** Condition combinators: nest freely. */\nexport function and(...conditions: readonly Condition[]): Condition {\n return cond({ type: 'and', conditions: conditions.map(condIR) }, captureLoc(and));\n}\nexport function or(...conditions: readonly Condition[]): Condition {\n return cond({ type: 'or', conditions: conditions.map(condIR) }, captureLoc(or));\n}\nexport function not(condition: Condition): Condition {\n return cond({ type: 'not', condition: condIR(condition) }, captureLoc(not));\n}\n\n/**\n * Segment: a named condition expression and a top-level asset — it shows up in\n * the UI sidebar, is reusable across workflows, is referenced by id in IR,\n * and has a continuously materialized membership count (the prerequisite for\n * segment triggers). Anonymous reuse is just a const expression; a one-off\n * check is an inline expression at the use site.\n * TODO, to match customer.io's panel: form / page / device / screen /\n * opt-out / message data(email opened、sms clicked、webhook …)\n */\nexport interface SegmentRef extends Condition {\n readonly __segment: true;\n /** Wire identity: segment references and the membership store are keyed on it (`SegmentIR.id`). */\n readonly id: string;\n}\n\n/** @internal the compiled shape behind a SegmentRef — compileBundle reads definition/loc off it */\nexport interface SegmentInternal extends SegmentRef {\n readonly ir: ConditionIR;\n /** Human label / description; metadata, so never part of the hash. */\n readonly meta: SegmentMeta | undefined;\n /** The segment's own definition (SegmentIR.condition), as opposed to a reference by id. */\n readonly definition: ConditionIR;\n /** Callsite of the segment() call (provenance; lands in SegmentIR.meta.loc). */\n readonly loc: SourceLocIR | undefined;\n}\n\n/** Labels for a segment: what the studio shows instead of the wire key. */\nexport interface SegmentMeta {\n name?: string;\n description?: string;\n}\n\nexport interface SegmentDefinition extends SegmentMeta {\n /**\n * Wire identity — by-name references and the membership store are keyed on\n * it, so it is not something to rename for readability.\n */\n id: string;\n /** Who is in the segment; the predicates already checked its types. */\n condition: Condition;\n}\n\n/**\n * Segment definition. `name` and `description` are optional labels: metadata,\n * dropped before hashing, and what the studio shows.\n *\n * export const purchaser = segment({\n * id: 'purchaser',\n * name: 'Purchased (last 30d)',\n * condition: performed(e.purchase, { within: '30 days' }),\n * });\n */\nexport function segment({ id, condition, name, description }: SegmentDefinition): SegmentRef {\n const meta: SegmentMeta = {\n ...(name === undefined ? {} : { name }),\n ...(description === undefined ? {} : { description }),\n };\n const impl: SegmentInternal = {\n __condition: true,\n __segment: true,\n id,\n ir: { type: 'segment', segment: id },\n definition: condIR(condition),\n loc: captureLoc(segment),\n meta: Object.keys(meta).length === 0 ? undefined : meta,\n };\n return impl;\n}\n"],"mappings":";;;;AAqCA,MAAM,QAAQ,IAAiB,QAAiC;CAK9D,OAAO;EAHL,aAAa;EACb,IAAI,QAAQ,KAAA,IAAY,KAAK;GAAE,GAAG;GAAI,MAAM,EAAE,IAAI;EAAE;CAE5C;AACZ;;AAGA,MAAa,UAAU,MAA8B;CAEnD,MAAM,KAAM,EAAiC;CAC7C,IAAI,OAAO,KAAA,GAGT,MAAM,IAAI,MACR,6GACF;CAEF,OAAO;AACT;;;;;;;AAiBA,MAAM,iBAAiB,QACpB,IAAqC;AAExC,SAAS,KACP,KACA,IAeA,OACA,QACA,KACW;CACX,MAAM,cAAc,cAAc,GAAG;CACrC,OAAO,KACL;EACE,MAAM,gBAAgB,KAAA,IAAY,YAAY;EAC9C,MAAM,eAAgB,IAAiC;EACvD;EACA,GAAI,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;EACvC,GAAI,WAAW,KAAA,IAAY,EAAE,QAAQ,CAAC,GAAG,MAAM,EAAE,IAAI,CAAC;CACxD,GACA,GACF;AACF;AAEA,SAAgB,GAAM,KAAiB,OAA8B;CACnE,OAAO,KAAK,KAAK,MAAM,OAAiB,KAAA,GAAW,WAAW,EAAE,CAAC;AACnE;AACA,SAAgB,GAAM,KAAiB,OAA8B;CACnE,OAAO,KAAK,KAAK,MAAM,OAAiB,KAAA,GAAW,WAAW,EAAE,CAAC;AACnE;AACA,SAAgB,GAA8B,KAAiB,OAA8B;CAC3F,OAAO,KAAK,KAAK,MAAM,OAAO,KAAA,GAAW,WAAW,EAAE,CAAC;AACzD;AACA,SAAgB,IAA+B,KAAiB,OAA8B;CAC5F,OAAO,KAAK,KAAK,OAAO,OAAO,KAAA,GAAW,WAAW,GAAG,CAAC;AAC3D;AACA,SAAgB,GAA8B,KAAiB,OAA8B;CAC3F,OAAO,KAAK,KAAK,MAAM,OAAO,KAAA,GAAW,WAAW,EAAE,CAAC;AACzD;AACA,SAAgB,IAA+B,KAAiB,OAA8B;CAC5F,OAAO,KAAK,KAAK,OAAO,OAAO,KAAA,GAAW,WAAW,GAAG,CAAC;AAC3D;AACA,SAAgB,QACd,KACA,KACA,KACW;CACX,OAAO,KAAK,KAAK,WAAW,KAAA,GAAW,CAAC,KAAK,GAAG,GAAG,WAAW,OAAO,CAAC;AACxE;AACA,SAAgB,WACd,KACA,KACA,KACW;CACX,OAAO,KAAK,KAAK,eAAe,KAAA,GAAW,CAAC,KAAK,GAAG,GAAG,WAAW,UAAU,CAAC;AAC/E;AACA,SAAgB,QAAW,KAAiB,QAA0C;CACpF,OAAO,KAAK,KAAK,YAAY,KAAA,GAAW,QAA6B,WAAW,OAAO,CAAC;AAC1F;AACA,SAAgB,WAAc,KAAiB,QAA0C;CACvF,OAAO,KAAK,KAAK,gBAAgB,KAAA,GAAW,QAA6B,WAAW,UAAU,CAAC;AACjG;AACA,SAAgB,OAAO,KAAkC;CACvD,OAAO,KAAK,KAAK,UAAU,KAAA,GAAW,KAAA,GAAW,WAAW,MAAM,CAAC;AACrE;AACA,SAAgB,UAAU,KAAkC;CAC1D,OAAO,KAAK,KAAK,cAAc,KAAA,GAAW,KAAA,GAAW,WAAW,SAAS,CAAC;AAC5E;AACA,SAAgB,SAA2B,KAAiB,OAA0B;CACpF,OAAO,KAAK,KAAK,YAAY,OAAO,KAAA,GAAW,WAAW,QAAQ,CAAC;AACrE;AACA,SAAgB,YAA8B,KAAiB,OAA0B;CACvF,OAAO,KAAK,KAAK,gBAAgB,OAAO,KAAA,GAAW,WAAW,WAAW,CAAC;AAC5E;AAqCA,SAAgB,UACd,OACA,aACA,WACW;CACX,MAAM,QAAQ,OAAO,gBAAgB,aAAa,cAAc,KAAA;CAChE,MAAM,OAAO,OAAO,gBAAgB,aAAa,YAAY;CAC7D,OAAO,KACL;EACE,MAAM;EACN,OAAO,MAAM;EACb,GAAI,UAAU,KAAA,IAAY,EAAE,OAAO,OAAO,MAAM,YAAe,CAAC,CAAC,EAAE,IAAI,CAAC;EACxE,GAAI,MAAM,WAAW,KAAA,IAAY,EAAE,QAAQ,WAAW,KAAK,MAAM,EAAE,IAAI,CAAC;EACxE,GAAI,MAAM,UAAU,KAAA,IAAY,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;CAC3D,GACA,WAAW,SAAS,CACtB;AACF;;;;;;;;;;;AAYA,SAAgB,UAAU,SAA6B;CACrD,OAAO,KAAK;EAAE,MAAM;EAAa;CAAQ,GAAG,WAAW,SAAS,CAAC;AACnE;;AAGA,SAAgB,IAAI,GAAG,YAA6C;CAClE,OAAO,KAAK;EAAE,MAAM;EAAO,YAAY,WAAW,IAAI,MAAM;CAAE,GAAG,WAAW,GAAG,CAAC;AAClF;AACA,SAAgB,GAAG,GAAG,YAA6C;CACjE,OAAO,KAAK;EAAE,MAAM;EAAM,YAAY,WAAW,IAAI,MAAM;CAAE,GAAG,WAAW,EAAE,CAAC;AAChF;AACA,SAAgB,IAAI,WAAiC;CACnD,OAAO,KAAK;EAAE,MAAM;EAAO,WAAW,OAAO,SAAS;CAAE,GAAG,WAAW,GAAG,CAAC;AAC5E;;;;;;;;;;;AAsDA,SAAgB,QAAQ,EAAE,IAAI,WAAW,MAAM,eAA8C;CAC3F,MAAM,OAAoB;EACxB,GAAI,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,KAAK;EACrC,GAAI,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY;CACrD;CAUA,OAAO;EARL,aAAa;EACb,WAAW;EACX;EACA,IAAI;GAAE,MAAM;GAAW,SAAS;EAAG;EACnC,YAAY,OAAO,SAAS;EAC5B,KAAK,WAAW,OAAO;EACvB,MAAM,OAAO,KAAK,IAAI,CAAC,CAAC,WAAW,IAAI,KAAA,IAAY;CAE3C;AACZ"}
1
+ {"version":3,"file":"condition.mjs","names":[],"sources":["../../src/dsl/condition.ts"],"sourcesContent":["import type { ConditionIR, SourceLocIR } from '../ir';\nimport { captureLoc } from '../provenance';\nimport { type Duration, durationIR } from './base';\nimport {\n type EventRef,\n PAYLOAD_PATH,\n type PayloadRef,\n type PayloadRefs,\n type UserPropertyRef,\n payloadRefs,\n} from './refs';\nimport type { Channel } from './template';\n\n/**\n * Conditions — drizzle-style predicate expressions: free functions (eq/gt/…)\n * composed with and/or/not, plus `segment` for naming an expression as a\n * top-level asset. A condition is an opaque handle wrapping its ConditionIR.\n *\n * Type techniques:\n * - Types flow in from the reference's phantom type; operator applicability is\n * a signature constraint (gt only takes string|number refs, contains only\n * string ones), mirroring customer.io's condition panel.\n * - Value parameters are always NoInfer: types come from the reference only,\n * which stops eq(ref, 'typo') from widening T to include the bad value.\n *\n * TODO: JSON array 'where at least one'、JSON object 'has the property'。\n */\n\n/** Opaque condition handle — what predicates, combinators and segments all produce. */\nexport interface Condition {\n readonly __condition: true;\n}\n\ninterface ConditionInternal extends Condition {\n readonly ir: ConditionIR;\n}\n\nconst cond = (ir: ConditionIR, loc?: SourceLocIR): Condition => {\n const impl: ConditionInternal = {\n __condition: true,\n ir: loc === undefined ? ir : { ...ir, meta: { loc } },\n };\n return impl;\n};\n\n/** @internal unwrap a condition to its IR — used by flow/trigger/workflow builders */\nexport const condIR = (c: Condition): ConditionIR => {\n // Partial: the cast is unsound for hand-rolled objects — exactly what this checks\n const ir = (c as Partial<ConditionInternal>).ir;\n if (ir === undefined) {\n // A hand-rolled `{ __condition: true }` would otherwise surface as a\n // baffling zod error at toIR() time, far away from the actual mistake.\n throw new Error(\n 'Condition was not created by this DSL — use the predicates (eq/gt/…), combinators (and/or/not) or segment()'\n );\n }\n return ir;\n};\n\ntype Scalar = string | number | boolean;\n\n/**\n * Predicates accept either kind of reference: a user property (profile\n * condition) or an event payload field (only valid inside a `where` — the\n * bundle compiler enforces the placement, since both produce `Condition`).\n */\ntype CondRef<T> = UserPropertyRef<T> | PayloadRef<T>;\n\n/**\n * A payload ref identifies itself by a symbol key, and a user-property ref by\n * the absence of one. Nothing here reads a *string* key off the reference: a\n * payload ref hands out sub-refs for every string key, so a field named `type`\n * or `path` would otherwise answer in place of the ref itself.\n */\nconst payloadPathOf = (ref: CondRef<unknown>): string | undefined =>\n (ref as Partial<PayloadRef<unknown>>)[PAYLOAD_PATH];\n\nfunction prop(\n ref: CondRef<unknown>,\n op:\n | 'eq'\n | 'ne'\n | 'gt'\n | 'gte'\n | 'lt'\n | 'lte'\n | 'between'\n | 'not_between'\n | 'in_array'\n | 'not_in_array'\n | 'exists'\n | 'not_exists'\n | 'contains'\n | 'not_contains',\n value?: Scalar,\n values?: readonly Scalar[],\n loc?: SourceLocIR\n): Condition {\n const payloadPath = payloadPathOf(ref);\n return cond(\n {\n type: payloadPath !== undefined ? 'payload' : 'property',\n path: payloadPath ?? (ref as UserPropertyRef<unknown>).path,\n op,\n ...(value !== undefined ? { value } : {}),\n ...(values !== undefined ? { values: [...values] } : {}),\n },\n loc\n );\n}\n\nexport function eq<T>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'eq', value as Scalar, undefined, captureLoc(eq));\n}\nexport function ne<T>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'ne', value as Scalar, undefined, captureLoc(ne));\n}\nexport function gt<T extends string | number>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'gt', value, undefined, captureLoc(gt));\n}\nexport function gte<T extends string | number>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'gte', value, undefined, captureLoc(gte));\n}\nexport function lt<T extends string | number>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'lt', value, undefined, captureLoc(lt));\n}\nexport function lte<T extends string | number>(ref: CondRef<T>, value: NoInfer<T>): Condition {\n return prop(ref, 'lte', value, undefined, captureLoc(lte));\n}\nexport function between<T extends string | number>(\n ref: CondRef<T>,\n min: NoInfer<T>,\n max: NoInfer<T>\n): Condition {\n return prop(ref, 'between', undefined, [min, max], captureLoc(between));\n}\nexport function notBetween<T extends string | number>(\n ref: CondRef<T>,\n min: NoInfer<T>,\n max: NoInfer<T>\n): Condition {\n return prop(ref, 'not_between', undefined, [min, max], captureLoc(notBetween));\n}\nexport function inArray<T>(ref: CondRef<T>, values: readonly NoInfer<T>[]): Condition {\n return prop(ref, 'in_array', undefined, values as readonly Scalar[], captureLoc(inArray));\n}\nexport function notInArray<T>(ref: CondRef<T>, values: readonly NoInfer<T>[]): Condition {\n return prop(ref, 'not_in_array', undefined, values as readonly Scalar[], captureLoc(notInArray));\n}\nexport function exists(ref: CondRef<unknown>): Condition {\n return prop(ref, 'exists', undefined, undefined, captureLoc(exists));\n}\nexport function notExists(ref: CondRef<unknown>): Condition {\n return prop(ref, 'not_exists', undefined, undefined, captureLoc(notExists));\n}\nexport function contains<T extends string>(ref: CondRef<T>, value: string): Condition {\n return prop(ref, 'contains', value, undefined, captureLoc(contains));\n}\nexport function notContains<T extends string>(ref: CondRef<T>, value: string): Condition {\n return prop(ref, 'not_contains', value, undefined, captureLoc(notContains));\n}\n\nexport interface PerformedOptions {\n within?: Duration;\n count?: number;\n}\n\n/** A payload where clause: an arrow taking the typed payload-ref table. Runs once at construction — the result is a serializable condition tree in IR, never a closure. */\nexport type PayloadWhere<P> = (p: PayloadRefs<P>) => Condition;\n\n/**\n * Event predicate: performed an event (optionally within a window / at least N\n * times). \"Did not perform\" is `not(performed(...))` — expressed by the\n * combinator, so there is no separate notPerformed.\n *\n * The optional second argument narrows which events count by their payload —\n * an arrow receiving a typed payload reference table, with the ordinary\n * predicates applying. It compiles to a `where` subtree in IR (data the UI\n * renders), not a runtime closure:\n *\n * performed(e.login)\n * performed(e.login, (p) => and(eq(p.platform, 'web'), eq(p.tags.utm_source, 'meta')))\n * performed(e.purchase, (p) => gt(p.value, 100), { within: '30 days', count: 2 })\n * performed(e.purchase, { within: '30 days' }) // options without a where\n *\n * Time anchoring: evaluated inside a `goal` the count starts at workflow\n * entry, inside `waitUntil` at the moment the wait began (the engine anchors\n * them — a conversion or a wake must not be satisfied by pre-existing\n * history). Everywhere else (branch/filter/exitWhen/segments) it looks over\n * full history, bounded only by `within`.\n */\nexport function performed<P>(event: EventRef<P>, opts?: PerformedOptions): Condition;\nexport function performed<P>(\n event: EventRef<P>,\n where: PayloadWhere<P>,\n opts?: PerformedOptions\n): Condition;\nexport function performed<P>(\n event: EventRef<P>,\n whereOrOpts?: PayloadWhere<P> | PerformedOptions,\n maybeOpts?: PerformedOptions\n): Condition {\n const where = typeof whereOrOpts === 'function' ? whereOrOpts : undefined;\n const opts = typeof whereOrOpts === 'function' ? maybeOpts : whereOrOpts;\n return cond(\n {\n type: 'performed',\n event: event.name,\n ...(where !== undefined ? { where: condIR(where(payloadRefs<P>())) } : {}),\n ...(opts?.within !== undefined ? { within: durationIR(opts.within) } : {}),\n ...(opts?.count !== undefined ? { count: opts.count } : {}),\n },\n captureLoc(performed)\n );\n}\n\n/**\n * The user can be reached on a channel right now. For push it asks the host's\n * device directory (at least one device listed) — the engine keeps no device\n * state, so this is the way to branch on \"has the app on a device that allowed\n * notifications\"; for the other channels it checks the recipient property\n * (`email`, `phone`, …). Inside `waitUntil` it wakes on device changes\n * (`POST /devices/changed`) or profile updates respectively.\n *\n * .branch([reachable('push'), (w) => w.push(tip)], (w) => w.email(tip))\n *\n * (`.prefer([t.push.tip, t.email.tip], props)` is this branch for one push and one email.)\n */\nexport function reachable(channel: Channel): Condition {\n return cond({ type: 'reachable', channel }, captureLoc(reachable));\n}\n\n/** Condition combinators: nest freely. */\nexport function and(...conditions: readonly Condition[]): Condition {\n return cond({ type: 'and', conditions: conditions.map(condIR) }, captureLoc(and));\n}\nexport function or(...conditions: readonly Condition[]): Condition {\n return cond({ type: 'or', conditions: conditions.map(condIR) }, captureLoc(or));\n}\nexport function not(condition: Condition): Condition {\n return cond({ type: 'not', condition: condIR(condition) }, captureLoc(not));\n}\n\n/**\n * Segment: a named condition expression and a top-level asset — it shows up in\n * the UI sidebar, is reusable across workflows, is referenced by id in IR,\n * and has a continuously materialized membership count (the prerequisite for\n * segment triggers). Anonymous reuse is just a const expression; a one-off\n * check is an inline expression at the use site.\n * TODO, to match customer.io's panel: form / page / device / screen /\n * opt-out / message data(email opened、sms clicked、webhook …)\n */\nexport interface SegmentRef extends Condition {\n readonly __segment: true;\n /** Wire identity: segment references and the membership store are keyed on it (`SegmentIR.id`). */\n readonly id: string;\n}\n\n/** @internal the compiled shape behind a SegmentRef — compileBundle reads definition/loc off it */\nexport interface SegmentInternal extends SegmentRef {\n readonly ir: ConditionIR;\n /** Human label / description; metadata, so never part of the hash. */\n readonly meta: SegmentMeta | undefined;\n /** The segment's own definition (SegmentIR.condition), as opposed to a reference by id. */\n readonly definition: ConditionIR;\n /** Callsite of the segment() call (provenance; lands in SegmentIR.meta.loc). */\n readonly loc: SourceLocIR | undefined;\n}\n\n/** Labels for a segment: what the studio shows instead of the wire key. */\nexport interface SegmentMeta {\n name?: string;\n description?: string;\n}\n\nexport interface SegmentDefinition extends SegmentMeta {\n /**\n * Wire identity — by-name references and the membership store are keyed on\n * it, so it is not something to rename for readability.\n */\n id: string;\n /** Who is in the segment; the predicates already checked its types. */\n condition: Condition;\n}\n\n/**\n * Segment definition. `name` and `description` are optional labels: metadata,\n * dropped before hashing, and what the studio shows.\n *\n * export const purchaser = segment({\n * id: 'purchaser',\n * name: 'Purchased (last 30d)',\n * condition: performed(e.purchase, { within: '30 days' }),\n * });\n */\nexport function segment({ id, condition, name, description }: SegmentDefinition): SegmentRef {\n const meta: SegmentMeta = {\n ...(name === undefined ? {} : { name }),\n ...(description === undefined ? {} : { description }),\n };\n const impl: SegmentInternal = {\n __condition: true,\n __segment: true,\n id,\n ir: { type: 'segment', segment: id },\n definition: condIR(condition),\n loc: captureLoc(segment),\n meta: Object.keys(meta).length === 0 ? undefined : meta,\n };\n return impl;\n}\n"],"mappings":"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"}
package/dist/dsl/flow.cjs CHANGED
@@ -3,6 +3,23 @@ const require_provenance = require("../provenance.cjs");
3
3
  const require_dsl_base = require("./base.cjs");
4
4
  const require_dsl_condition = require("./condition.cjs");
5
5
  //#region src/dsl/flow.ts
6
+ const PREFER_CHANNELS = /* @__PURE__ */ new Set(["push", "email"]);
7
+ /** prefer() is typed strictly, but a JS caller (or an `any`) reaches it unchecked — fail at the call, not at send time. */
8
+ function checkPreferArgs(candidates, props, options) {
9
+ if (!Array.isArray(candidates) || candidates.length !== 2) throw new TypeError("prefer(): candidates must be exactly two templates — one push, one email");
10
+ for (const ref of candidates) {
11
+ const { channel, key } = ref ?? {};
12
+ if (!PREFER_CHANNELS.has(channel) || typeof key !== "string" || key === "") throw new TypeError("prefer(): each candidate must be a push or email template reference (t.push.<key> / t.email.<key>)");
13
+ }
14
+ if (candidates[0].channel === candidates[1].channel) throw new TypeError("prefer(): candidates must be one push and one email template");
15
+ if (typeof props !== "object" || props === null || Array.isArray(props)) throw new TypeError("prefer(): props must be an object — pass {} when the templates declare none");
16
+ if (options === void 0) return;
17
+ if (typeof options !== "object" || options === null || Array.isArray(options)) throw new TypeError("prefer(): options must be an object");
18
+ for (const [name, value] of Object.entries(options)) {
19
+ if (name !== "label") throw new TypeError(`prefer(): unknown option '${name}' — props go in the second argument, options take only 'label'`);
20
+ if (typeof value !== "string") throw new TypeError("prefer(): 'label' must be a string");
21
+ }
22
+ }
6
23
  /**
7
24
  * A cohort arm name has two jobs, and both constrain its spelling:
8
25
  * - it is interpolated into the node ids of the arm's flow ('{id}.{name}.{j}'),
@@ -23,6 +40,16 @@ function resolveSubFlow(sub) {
23
40
  }
24
41
  return structuredClone(sub.nodes);
25
42
  }
43
+ function messageNode(channel, tpl, props, devices) {
44
+ return {
45
+ id: "",
46
+ type: "message",
47
+ channel,
48
+ template: tpl.key,
49
+ props: props ?? {},
50
+ ...devices !== void 0 ? { devices } : {}
51
+ };
52
+ }
26
53
  /** @internal extended by WorkflowBuilderImpl; not part of the public API */
27
54
  var FlowBuilderImpl = class {
28
55
  /** Nodes as collected; ids are assigned in one pass inside toIR, so '' is a placeholder here. */
@@ -36,14 +63,7 @@ var FlowBuilderImpl = class {
36
63
  return this;
37
64
  }
38
65
  message(channel, tpl, props, loc, devices) {
39
- return this.pushNode({
40
- id: "",
41
- type: "message",
42
- channel,
43
- template: tpl.key,
44
- props: props ?? {},
45
- ...devices !== void 0 ? { devices } : {}
46
- }, loc);
66
+ return this.pushNode(messageNode(channel, tpl, props, devices), loc);
47
67
  }
48
68
  email(t, ...args) {
49
69
  return this.message("email", t, args[0], require_provenance.captureLoc(this.email));
@@ -68,6 +88,30 @@ var FlowBuilderImpl = class {
68
88
  survey(t, ...args) {
69
89
  return this.message("survey", t, args[0], require_provenance.captureLoc(this.survey));
70
90
  }
91
+ prefer(candidates, props, options) {
92
+ checkPreferArgs(candidates, props, options);
93
+ const loc = require_provenance.captureLoc(this.prefer);
94
+ const meta = loc !== void 0 ? { meta: { loc } } : {};
95
+ const [first, second] = candidates;
96
+ const arm = (tpl) => [{
97
+ ...messageNode(tpl.channel, tpl, { ...props }),
98
+ ...meta
99
+ }];
100
+ return this.pushNode({
101
+ id: "",
102
+ type: "branch",
103
+ ...options?.label !== void 0 ? { label: options.label } : {},
104
+ cases: [{
105
+ condition: {
106
+ type: "reachable",
107
+ channel: first.channel,
108
+ ...meta
109
+ },
110
+ flow: arm(first)
111
+ }],
112
+ otherwise: arm(second)
113
+ }, loc);
114
+ }
71
115
  delay(d) {
72
116
  const loc = require_provenance.captureLoc(this.delay);
73
117
  if (typeof d === "string") return this.pushNode({
@@ -1 +1 @@
1
- {"version":3,"file":"flow.cjs","names":["captureLoc","durationIR","timeOfDayMinutes","condIR"],"sources":["../../src/dsl/flow.ts"],"sourcesContent":["import type { ChannelIR, ConditionIR, NodeIR, PropValueIR, SourceLocIR } from '../ir';\nimport { captureLoc } from '../provenance';\nimport type { ActionRef } from './action';\nimport { type Duration, type TimeOfDay, type Weekday, durationIR, timeOfDayMinutes } from './base';\nimport { type Condition, condIR } from './condition';\nimport type { EventRef, MessageArgs, PropInput } from './refs';\nimport type { Channel, TemplateRef } from './template';\n\n/**\n * The flow chain — FlowBuilder is the DSL's step surface (messages, delays,\n * flow control, data), and its implementation is the compiler front half: each\n * chained call executes no logic, it just collects one IR node (ids are\n * assigned later, in workflow.toIR()). Sub-flow callbacks run exactly once at\n * construction — builder in, builder out — so the output is always a static\n * tree.\n *\n * Every public method also captures its callsite (provenance): the loc lands\n * in the node's hash-excluded meta and is the anchor for future in-place\n * editing from the studio.\n */\n\ntype Scalar = string | number | boolean;\n\n/** An emitted field: a scalar (or a profile ref to one); a nested field cannot be sent. */\ntype EmitValue<T> = [NonNullable<T>] extends [Scalar] ? PropInput<T> : never;\n\n/** The payload `sendEvent` accepts for an event: its fields, scalar ones only. */\nexport type EmitPayload<P> = { [K in keyof P]: EmitValue<P[K]> };\n\n/** Payload may be omitted when the event declares no required field. */\nexport type EmitArgs<P> =\n Record<never, never> extends P ? [payload?: EmitPayload<P>] : [payload: EmitPayload<P>];\n\n/** Per-node push options. */\nexport interface PushOptions {\n /** Which devices get a copy: 'all' (the default) or only the most recently registered. */\n devices?: 'all' | 'latest';\n}\n\n/** A reusable flow fragment (what `flow(...)` returns), referenced directly by branch arms. */\nexport interface Flow {\n readonly __flow: true;\n}\n\ninterface FlowInternal extends Flow {\n readonly nodes: NodeIR[];\n}\n\n/** Sub-flow: a named fragment, or an inline callback (runs once at construction — builder in, builder out). */\nexport type SubFlow = Flow | ((w: FlowBuilder) => FlowBuilder);\n\n/** A branch arm: the [condition, sub-flow] tuple — an [if, then] ordering convention. */\nexport type BranchCase = readonly [condition: Condition, flow: SubFlow];\n\n/**\n * A branch argument: either a tuple arm, or a bare sub-flow meaning the default\n * arm. The bare form may appear once, and must come last (checked at runtime).\n */\nexport type BranchArm = BranchCase | SubFlow;\n\n/**\n * A cohort arm name has two jobs, and both constrain its spelling:\n * - it is interpolated into the node ids of the arm's flow ('{id}.{name}.{j}'),\n * and an id is a durable step name — a dot would fake a nesting level and can\n * collide with a real one, an empty name gives '{id}..{j}';\n * - it is an object key, and JavaScript lists integer-like keys first in\n * ascending numeric order however the author wrote them. The arms array is\n * documented (and interpreted) as authoring order, and the bucketing walk\n * accumulates weights along it, so an arm named '0' added to a live\n * experiment would prepend itself and re-bucket everyone already in it.\n */\nconst ARM_NAME = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\nfunction resolveSubFlow(sub: SubFlow): NodeIR[] {\n if (typeof sub === 'function') {\n const builder = new FlowBuilderImpl();\n const returned: unknown = sub(builder);\n // An async callback would add its nodes after construction is over — after ids were assigned\n if (typeof (returned as { then?: unknown } | undefined)?.then === 'function') {\n throw new TypeError(\n 'sub-flow callbacks must be synchronous: they run once, while the workflow is defined'\n );\n }\n return builder.nodes;\n }\n /*\n * A named fragment is a value, and the convention is to reuse it — two branch\n * arms may hold the same one. Handing out its own nodes aliases them into\n * both places, and assignIds writes ids *in place*: the second prefix\n * overwrites the first, so the two placements end up as one object under one\n * id — one durable step name, one message idempotency key, one bucket seed.\n * A copy per placement is what makes a fragment behave like a value.\n */\n return structuredClone((sub as FlowInternal).nodes);\n}\n\nexport interface FlowBuilder {\n /* ---- Messages: every channel is one message node; separate methods buy channel-level typing ---- */\n email<P extends object>(template: TemplateRef<'email', P>, ...props: MessageArgs<P>): this;\n sms<P extends object>(template: TemplateRef<'sms', P>, ...props: MessageArgs<P>): this;\n /**\n * Push to the user's devices — every device the host's directory lists, or\n * only the most recently registered with `{ devices: 'latest' }`. A user with\n * no device is skipped (branch on `reachable('push')` to fall back to email).\n */\n push<P extends object>(\n template: TemplateRef<'push', P>,\n ...args: [...props: MessageArgs<P>, options?: PushOptions]\n ): this;\n inApp<P extends object>(template: TemplateRef<'in_app', P>, ...props: MessageArgs<P>): this;\n slack<P extends object>(template: TemplateRef<'slack', P>, ...props: MessageArgs<P>): this;\n discord<P extends object>(template: TemplateRef<'discord', P>, ...props: MessageArgs<P>): this;\n survey<P extends object>(template: TemplateRef<'survey', P>, ...props: MessageArgs<P>): this;\n\n /* --------------------------------- Delays --------------------------------- */\n /** Time Delay; passing { min, max } makes it a Randomized Delay. */\n delay(duration: Duration): this;\n delay(range: { min: Duration; max: Duration }): this;\n\n /** Time Window: hold until inside the window (e.g. weekdays 09:00–17:00). tz: 'user' uses the user's timezone. */\n timeWindow(opts: {\n days?: readonly Weekday[];\n between: readonly [TimeOfDay, TimeOfDay];\n tz?: 'user' | (string & {});\n }): this;\n\n /**\n * Wait Until: continue on the main line once the condition holds, or take\n * onTimeout when it expires (default 'continue'). On the engine side this is\n * \"await event racing a timer\" — a native primitive of durable runtimes.\n */\n waitUntil(\n condition: Condition,\n opts: { timeout: Duration; onTimeout?: 'continue' | 'exit' | SubFlow }\n ): this;\n\n /**\n * Splice a fragment in place: a named `flow(...)` or a callback, expanded\n * once while the workflow is defined — a composition step before compiling,\n * not a node (the IR has no trace of it, and ids are assigned over the whole\n * workflow as if the steps were written inline). Each use gets its own copy,\n * so one fragment can be used in several places and workflows.\n *\n * const preferPush = (push, email, props) =>\n * flow((w) => w.branch([reachable('push'), (b) => b.push(push, props)], (b) => b.email(email, props)));\n * workflow({ id: 'onboarding', ... }).use(preferPush(tipPush, tipEmail, { name: u.name }))\n */\n use(fragment: SubFlow): this;\n\n /* ------------------------------ Flow Control ------------------------------ */\n /**\n * Conditional branch: ordered first-match (same semantics as Step Functions\n * Choice / Knock branch). This one primitive covers both the UI's True/False\n * Branch (two arms) and Multi-Split Branch (N arms). Arms are\n * [condition, sub-flow] tuples; a bare sub-flow in tail position is the\n * default arm. With no match and no default, execution simply continues.\n * Rejoin is structural: after the matching arm finishes, execution resumes\n * at the node following the branch — to opt out, call .exit() inside the arm.\n *\n * .branch([activeSubscriber, upgradeFlow], firstTimeFlow) // True/False\n * .branch([vip, vipFlow], [trial, trialFlow], defaultFlow) // Multi-Split\n * .branch([not(usedStaging), eduFlow]) // single arm: no match continues\n *\n * The optional first argument `label` becomes the node's name in IR (UI title\n * / observability handle). It is not a jump target — there is no goto here;\n * looping is a future re-entry-policy topic (see sendEvent). Arm labels in\n * the UI are derived from the condition.\n */\n branch(...arms: readonly BranchArm[]): this;\n branch(label: string, ...arms: readonly BranchArm[]): this;\n\n /**\n * Gate: exit unless the condition holds, otherwise continue. A first-class\n * node rather than sugar over branch — same semantics as Zapier's \"only\n * continue if\" / Iterable's Filter tile, and rendered on its own in the UI.\n * `reason` is the audit-log line — metadata, so rewording it is free\n * (excluded from contentHash, see hash.ts).\n */\n filter(condition: Condition, opts?: { reason?: string }): this;\n\n /**\n * Random Cohort Branch (A/B): weights must sum to 100 (checked at runtime).\n * `key` names the experiment: bucketing hashes `userId:key` instead of the\n * structural node id, so assignments survive node insertion/moves and the\n * experiment keeps one identity across workflow versions — set it for any\n * A/B whose results you intend to read.\n *\n * Arm names become node id segments and are read back in authoring order, so\n * they are restricted to letters, digits, '_' and '-', never starting with a\n * digit (checked at runtime).\n */\n cohort(arms: Record<string, { weight: number; flow?: SubFlow }>, opts?: { key?: string }): this;\n\n /**\n * Exit: end the whole workflow immediately (the UI's Exit node); `reason`\n * goes into the audit log and is metadata, so rewording it neither repins\n * in-flight journeys nor shows up in plan. Inside a branch arm this means\n * \"terminate, do not rejoin\" — it stops here instead of falling through,\n * while an arm without exit rejoins naturally.\n */\n exit(reason?: string): this;\n\n /* ---------------------------------- Data ---------------------------------- */\n /**\n * Emit a typed event (customer.io's Send Event). The event comes from an\n * e.xxx reference, so payload types flow in the same way trigger.event works.\n * This is how workflows compose: an event can trigger another workflow, and\n * the resulting call graph is statically analyzable. Payload values may\n * reference u.xxx. Only scalar fields can be sent (IR carries literals and\n * profile refs, not nested JSON): an event whose payload requires a nested\n * field cannot be emitted from a journey, and the types say so.\n *\n * Note on loops: a trailing self-triggering sendEvent will become the\n * official periodic-flow shape *once a re-entry policy exists* — today the\n * entry policy is once-per-user, so a self-trigger cannot re-enter and does\n * not loop.\n * TODO: the rest of the Data category — outbound webhook / profile update /\n * journey attributes.\n */\n sendEvent<P extends object>(event: EventRef<P>, ...payload: EmitArgs<P>): this;\n\n /**\n * Run a custom action (see dsl/action.ts for the model). The chain records\n * identity only — name, args, codeHash — never the code: the implementation\n * ships inside the deployed Worker bundle and is resolved by name from the\n * runtime registry. Args are typed by the ActionRef and may reference u.xxx,\n * resolved at run time exactly like message props. Executed inside a durable\n * step: retried on throw, checkpointed on success.\n */\n run<A extends object>(action: ActionRef<A>, ...args: MessageArgs<A>): this;\n}\n\n/* oxlint-disable typescript/unbound-method --\n * `captureLoc(this.xxx)` passes the method by identity as the stack-trace\n * boundary for Error.captureStackTrace; it is never invoked, so `this`\n * scoping cannot go wrong. */\n/** @internal extended by WorkflowBuilderImpl; not part of the public API */\nexport class FlowBuilderImpl implements FlowBuilder {\n /** Nodes as collected; ids are assigned in one pass inside toIR, so '' is a placeholder here. */\n readonly nodes: NodeIR[] = [];\n\n /** Provenance: the callsite (captured in the public method) lands in meta.loc, outside the hash. */\n private pushNode(node: NodeIR, loc: SourceLocIR | undefined): this {\n this.nodes.push({ ...node, ...(loc !== undefined ? { meta: { loc } } : {}) });\n return this;\n }\n\n private message(\n channel: ChannelIR,\n tpl: TemplateRef<Channel, object>,\n props: object | undefined,\n loc: SourceLocIR | undefined,\n devices?: 'latest'\n ): this {\n return this.pushNode(\n {\n id: '',\n type: 'message',\n channel,\n template: tpl.key,\n props: (props ?? {}) as Record<string, PropValueIR>,\n // Absent for 'all', so adding the option left every existing push's hash as it was\n ...(devices !== undefined ? { devices } : {}),\n },\n loc\n );\n }\n\n email<P extends object>(t: TemplateRef<'email', P>, ...args: MessageArgs<P>): this {\n return this.message('email', t, args[0], captureLoc(this.email));\n }\n sms<P extends object>(t: TemplateRef<'sms', P>, ...args: MessageArgs<P>): this {\n return this.message('sms', t, args[0], captureLoc(this.sms));\n }\n push<P extends object>(\n t: TemplateRef<'push', P>,\n ...args: [...props: MessageArgs<P>, options?: PushOptions]\n ): this {\n const [props, options] = args as unknown as [object | undefined, PushOptions | undefined];\n const devices = options?.devices === 'latest' ? 'latest' : undefined;\n return this.message('push', t, props, captureLoc(this.push), devices);\n }\n inApp<P extends object>(t: TemplateRef<'in_app', P>, ...args: MessageArgs<P>): this {\n return this.message('in_app', t, args[0], captureLoc(this.inApp));\n }\n slack<P extends object>(t: TemplateRef<'slack', P>, ...args: MessageArgs<P>): this {\n return this.message('slack', t, args[0], captureLoc(this.slack));\n }\n discord<P extends object>(t: TemplateRef<'discord', P>, ...args: MessageArgs<P>): this {\n return this.message('discord', t, args[0], captureLoc(this.discord));\n }\n survey<P extends object>(t: TemplateRef<'survey', P>, ...args: MessageArgs<P>): this {\n return this.message('survey', t, args[0], captureLoc(this.survey));\n }\n\n delay(duration: Duration): this;\n delay(range: { min: Duration; max: Duration }): this;\n delay(d: Duration | { min: Duration; max: Duration }): this {\n const loc = captureLoc(this.delay);\n if (typeof d === 'string') {\n return this.pushNode({ id: '', type: 'delay', duration: durationIR(d) }, loc);\n }\n const min = durationIR(d.min);\n const max = durationIR(d.max);\n /*\n * The interpreter draws `min + random(max - min)` with the span clamped at\n * zero, so a backwards range does not fail — it quietly collapses into a\n * fixed `min` sleep. Reject it here, where the author can still see it.\n */\n if (max.ms < min.ms) {\n throw new Error(\n `delay(): 'max' must not be shorter than 'min', got { min: '${d.min}', max: '${d.max}' }`\n );\n }\n return this.pushNode({ id: '', type: 'random_delay', min, max }, loc);\n }\n\n timeWindow(opts: {\n days?: readonly Weekday[];\n between: readonly [TimeOfDay, TimeOfDay];\n tz?: 'user' | (string & {});\n }): this {\n const loc = captureLoc(this.timeWindow);\n const start = timeOfDayMinutes(opts.between[0]);\n const end = timeOfDayMinutes(opts.between[1]);\n if (end <= start) {\n throw new Error(\n `timeWindow(): 'between' must open before it closes within one day, got [${opts.between[0]}, ${opts.between[1]}] (overnight windows are not supported yet)`\n );\n }\n return this.pushNode(\n {\n id: '',\n type: 'time_window',\n days: [...(opts.days ?? (['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const))],\n between: [opts.between[0], opts.between[1]],\n tz: opts.tz ?? 'user',\n },\n loc\n );\n }\n\n waitUntil(\n condition: Condition,\n opts: { timeout: Duration; onTimeout?: 'continue' | 'exit' | SubFlow }\n ): this {\n const loc = captureLoc(this.waitUntil);\n const onTimeout = opts.onTimeout ?? 'continue';\n return this.pushNode(\n {\n id: '',\n type: 'wait_until',\n condition: condIR(condition),\n timeout: durationIR(opts.timeout),\n onTimeout:\n onTimeout === 'continue' || onTimeout === 'exit' ? onTimeout : resolveSubFlow(onTimeout),\n },\n loc\n );\n }\n\n use(fragment: SubFlow): this {\n // The fragment's nodes carry their own provenance (where the fragment was written)\n this.nodes.push(...resolveSubFlow(fragment));\n return this;\n }\n\n branch(...arms: readonly BranchArm[]): this;\n branch(label: string, ...arms: readonly BranchArm[]): this;\n branch(...args: readonly (string | BranchArm)[]): this {\n const loc = captureLoc(this.branch);\n const label = typeof args[0] === 'string' ? args[0] : undefined;\n const arms = (label === undefined ? args : args.slice(1)) as readonly BranchArm[];\n\n const cases: { condition: ConditionIR; flow: NodeIR[] }[] = [];\n let otherwise: NodeIR[] | undefined;\n arms.forEach((arm, i) => {\n if (Array.isArray(arm)) {\n if (otherwise !== undefined) {\n throw new Error('branch(): default arm (bare sub-flow) must be the last argument');\n }\n const [condition, sub] = arm as BranchCase;\n cases.push({ condition: condIR(condition), flow: resolveSubFlow(sub) });\n } else {\n if (otherwise !== undefined) {\n throw new Error('branch(): only one default arm (bare sub-flow) is allowed');\n }\n if (i !== arms.length - 1) {\n throw new Error('branch(): default arm (bare sub-flow) must be the last argument');\n }\n otherwise = resolveSubFlow(arm as SubFlow);\n }\n });\n\n return this.pushNode(\n {\n id: '',\n type: 'branch',\n ...(label !== undefined ? { label } : {}),\n cases,\n ...(otherwise !== undefined ? { otherwise } : {}),\n },\n loc\n );\n }\n\n filter(condition: Condition, opts?: { reason?: string }): this {\n return this.pushNode(\n {\n id: '',\n type: 'filter',\n condition: condIR(condition),\n ...(opts?.reason !== undefined ? { reason: opts.reason } : {}),\n },\n captureLoc(this.filter)\n );\n }\n\n cohort(arms: Record<string, { weight: number; flow?: SubFlow }>, opts?: { key?: string }): this {\n const loc = captureLoc(this.cohort);\n const entries = Object.entries(arms);\n /*\n * Weights live on a 0.01% grid: the runtime buckets into 10,000 slots\n * (basis points), so anything finer would be silently quantized — reject\n * it here instead. Converting to integer basis points also makes the\n * sum check exact; no epsilon needed.\n */\n const basisPoints = entries.map(([name, a]) => {\n if (!ARM_NAME.test(name)) {\n throw new Error(\n `cohort(): arm name '${name}' must start with a letter or '_' and contain only letters, digits, '_' or '-' — arm names become node id segments, and integer-like names would silently reorder the arms`\n );\n }\n const bp = a.weight * 100;\n if (Math.abs(bp - Math.round(bp)) > 1e-6) {\n throw new Error(\n `cohort(): arm '${name}' weight ${a.weight} is finer than the 0.01% bucketing grid — use at most two decimal places`\n );\n }\n /*\n * The sum check alone accepts -50 / 150: the runtime walks the arms\n * accumulating basis points and takes the first whose cumulative total\n * clears the bucket, so a negative weight hands 100% of users to its\n * neighbour instead of splitting anything.\n */\n if (bp < 0) {\n throw new Error(`cohort(): arm '${name}' weight ${a.weight} must not be negative`);\n }\n return Math.round(bp);\n });\n const total = basisPoints.reduce((sum, bp) => sum + bp, 0);\n if (total !== 10_000) {\n throw new Error(`cohort(): weights must sum to 100, got ${total / 100}`);\n }\n return this.pushNode(\n {\n id: '',\n type: 'cohort',\n ...(opts?.key !== undefined ? { key: opts.key } : {}),\n arms: entries.map(([name, a]) => ({\n name,\n weight: a.weight,\n flow: a.flow ? resolveSubFlow(a.flow) : [],\n })),\n },\n loc\n );\n }\n\n exit(reason?: string): this {\n return this.pushNode(\n { id: '', type: 'exit', ...(reason !== undefined ? { reason } : {}) },\n captureLoc(this.exit)\n );\n }\n\n sendEvent<P extends object>(ev: EventRef<P>, ...payload: EmitArgs<P>): this {\n const values = (payload[0] ?? {}) as Record<string, unknown>;\n for (const [field, value] of Object.entries(values)) {\n const ref =\n typeof value === 'object' &&\n value !== null &&\n (value as { type?: unknown }).type === 'user_property';\n if (!ref && !['string', 'number', 'boolean'].includes(typeof value)) {\n throw new TypeError(\n `sendEvent('${ev.name}'): '${field}' is not a scalar or a u.xxx reference — an emitted payload carries scalar fields only`\n );\n }\n }\n return this.pushNode(\n {\n id: '',\n type: 'send_event',\n event: ev.name,\n payload: values as Record<string, PropValueIR>,\n },\n captureLoc(this.sendEvent)\n );\n }\n\n run<A extends object>(a: ActionRef<A>, ...args: MessageArgs<A>): this {\n return this.pushNode(\n {\n id: '',\n type: 'action',\n action: a.name,\n args: (args[0] ?? {}) as Record<string, PropValueIR>,\n codeHash: a.codeHash,\n },\n captureLoc(this.run)\n );\n }\n}\n/* oxlint-enable typescript/unbound-method */\n\n/**\n * A reusable flow fragment: a free function. Once personalization goes through\n * u.xxx and events through e.xxx, the flow layer carries no schema generics.\n */\nexport function flow(build: (w: FlowBuilder) => FlowBuilder): Flow {\n const builder = new FlowBuilderImpl();\n build(builder);\n const impl: FlowInternal = { __flow: true, nodes: builder.nodes };\n return impl;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"flow.cjs","names":["captureLoc","durationIR","timeOfDayMinutes","condIR"],"sources":["../../src/dsl/flow.ts"],"sourcesContent":["import type { ChannelIR, ConditionIR, NodeIR, PropValueIR, SourceLocIR } from '../ir';\nimport { captureLoc } from '../provenance';\nimport type { ActionRef } from './action';\nimport { type Duration, type TimeOfDay, type Weekday, durationIR, timeOfDayMinutes } from './base';\nimport { type Condition, condIR } from './condition';\nimport type { EventRef, MessageArgs, PropInput, PropsInput } from './refs';\nimport type { Channel, TemplateRef } from './template';\n\n/**\n * The flow chain — FlowBuilder is the DSL's step surface (messages, delays,\n * flow control, data), and its implementation is the compiler front half: each\n * chained call executes no logic, it just collects one IR node (ids are\n * assigned later, in workflow.toIR()). Sub-flow callbacks run exactly once at\n * construction — builder in, builder out — so the output is always a static\n * tree.\n *\n * Every public method also captures its callsite (provenance): the loc lands\n * in the node's hash-excluded meta and is the anchor for future in-place\n * editing from the studio.\n */\n\ntype Scalar = string | number | boolean;\n\n/** An emitted field: a scalar (or a profile ref to one); a nested field cannot be sent. */\ntype EmitValue<T> = [NonNullable<T>] extends [Scalar] ? PropInput<T> : never;\n\n/** The payload `sendEvent` accepts for an event: its fields, scalar ones only. */\nexport type EmitPayload<P> = { [K in keyof P]: EmitValue<P[K]> };\n\n/** Payload may be omitted when the event declares no required field. */\nexport type EmitArgs<P> =\n Record<never, never> extends P ? [payload?: EmitPayload<P>] : [payload: EmitPayload<P>];\n\n/** Per-node push options. */\nexport interface PushOptions {\n /** Which devices get a copy: 'all' (the default) or only the most recently registered. */\n devices?: 'all' | 'latest';\n}\n\n/** `prefer()` options. */\nexport interface PreferOptions {\n /** The generated branch's name in IR (UI title / observability handle), as `branch(label, …)`. */\n label?: string;\n}\n\n/** The candidates `prefer()` takes: one push and one email template, in priority order. */\ntype PreferPair =\n | readonly [TemplateRef<'push', object>, TemplateRef<'email', object>]\n | readonly [TemplateRef<'email', object>, TemplateRef<'push', object>];\n\ntype RefProps<R> = R extends TemplateRef<infer _C, infer P> ? P : never;\ntype NormalizeProps<P> = { -readonly [K in keyof P]: P[K] };\ntype Equal<A, B> =\n (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;\n\n/** Both candidates render the one props object, so they must declare the same props type — not merely overlap. */\ntype SameProps<T extends PreferPair> =\n Equal<NormalizeProps<RefProps<T[0]>>, NormalizeProps<RefProps<T[1]>>> extends true\n ? unknown\n : { readonly __prefer_error__: 'Both templates must declare the same props type' };\n\n/** Distributes, so a field only one member of a union variable has still counts as extra. */\ntype KeysOfUnion<A> = A extends unknown ? keyof A : never;\n/** Rejects known extra fields, which `{}` (an empty props schema) would otherwise accept. */\ntype NoExtra<A, P> = Record<Exclude<KeysOfUnion<A>, keyof P>, never>;\n\nconst PREFER_CHANNELS: ReadonlySet<unknown> = new Set(['push', 'email']);\n\n/** prefer() is typed strictly, but a JS caller (or an `any`) reaches it unchecked — fail at the call, not at send time. */\nfunction checkPreferArgs(candidates: unknown, props: unknown, options: unknown): void {\n if (!Array.isArray(candidates) || candidates.length !== 2) {\n throw new TypeError('prefer(): candidates must be exactly two templates — one push, one email');\n }\n for (const ref of candidates as unknown[]) {\n const { channel, key } = (ref ?? {}) as Partial<TemplateRef>;\n if (!PREFER_CHANNELS.has(channel) || typeof key !== 'string' || key === '') {\n throw new TypeError(\n 'prefer(): each candidate must be a push or email template reference (t.push.<key> / t.email.<key>)'\n );\n }\n }\n if ((candidates[0] as TemplateRef).channel === (candidates[1] as TemplateRef).channel) {\n throw new TypeError('prefer(): candidates must be one push and one email template');\n }\n if (typeof props !== 'object' || props === null || Array.isArray(props)) {\n throw new TypeError(\n 'prefer(): props must be an object — pass {} when the templates declare none'\n );\n }\n if (options === undefined) return;\n if (typeof options !== 'object' || options === null || Array.isArray(options)) {\n throw new TypeError('prefer(): options must be an object');\n }\n for (const [name, value] of Object.entries(options)) {\n if (name !== 'label') {\n throw new TypeError(\n `prefer(): unknown option '${name}' — props go in the second argument, options take only 'label'`\n );\n }\n if (typeof value !== 'string') throw new TypeError(\"prefer(): 'label' must be a string\");\n }\n}\n\n/** A reusable flow fragment (what `flow(...)` returns), referenced directly by branch arms. */\nexport interface Flow {\n readonly __flow: true;\n}\n\ninterface FlowInternal extends Flow {\n readonly nodes: NodeIR[];\n}\n\n/** Sub-flow: a named fragment, or an inline callback (runs once at construction — builder in, builder out). */\nexport type SubFlow = Flow | ((w: FlowBuilder) => FlowBuilder);\n\n/** A branch arm: the [condition, sub-flow] tuple — an [if, then] ordering convention. */\nexport type BranchCase = readonly [condition: Condition, flow: SubFlow];\n\n/**\n * A branch argument: either a tuple arm, or a bare sub-flow meaning the default\n * arm. The bare form may appear once, and must come last (checked at runtime).\n */\nexport type BranchArm = BranchCase | SubFlow;\n\n/**\n * A cohort arm name has two jobs, and both constrain its spelling:\n * - it is interpolated into the node ids of the arm's flow ('{id}.{name}.{j}'),\n * and an id is a durable step name — a dot would fake a nesting level and can\n * collide with a real one, an empty name gives '{id}..{j}';\n * - it is an object key, and JavaScript lists integer-like keys first in\n * ascending numeric order however the author wrote them. The arms array is\n * documented (and interpreted) as authoring order, and the bucketing walk\n * accumulates weights along it, so an arm named '0' added to a live\n * experiment would prepend itself and re-bucket everyone already in it.\n */\nconst ARM_NAME = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\nfunction resolveSubFlow(sub: SubFlow): NodeIR[] {\n if (typeof sub === 'function') {\n const builder = new FlowBuilderImpl();\n const returned: unknown = sub(builder);\n // An async callback would add its nodes after construction is over — after ids were assigned\n if (typeof (returned as { then?: unknown } | undefined)?.then === 'function') {\n throw new TypeError(\n 'sub-flow callbacks must be synchronous: they run once, while the workflow is defined'\n );\n }\n return builder.nodes;\n }\n /*\n * A named fragment is a value, and the convention is to reuse it — two branch\n * arms may hold the same one. Handing out its own nodes aliases them into\n * both places, and assignIds writes ids *in place*: the second prefix\n * overwrites the first, so the two placements end up as one object under one\n * id — one durable step name, one message idempotency key, one bucket seed.\n * A copy per placement is what makes a fragment behave like a value.\n */\n return structuredClone((sub as FlowInternal).nodes);\n}\n\nexport interface FlowBuilder {\n /* ---- Messages: every channel is one message node; separate methods buy channel-level typing ---- */\n email<P extends object>(template: TemplateRef<'email', P>, ...props: MessageArgs<P>): this;\n sms<P extends object>(template: TemplateRef<'sms', P>, ...props: MessageArgs<P>): this;\n /**\n * Push to the user's devices — every device the host's directory lists, or\n * only the most recently registered with `{ devices: 'latest' }`. A user with\n * no device is skipped (`prefer([pushTpl, emailTpl], props)` falls back to email).\n */\n push<P extends object>(\n template: TemplateRef<'push', P>,\n ...args: [...props: MessageArgs<P>, options?: PushOptions]\n ): this;\n inApp<P extends object>(template: TemplateRef<'in_app', P>, ...props: MessageArgs<P>): this;\n slack<P extends object>(template: TemplateRef<'slack', P>, ...props: MessageArgs<P>): this;\n discord<P extends object>(template: TemplateRef<'discord', P>, ...props: MessageArgs<P>): this;\n survey<P extends object>(template: TemplateRef<'survey', P>, ...props: MessageArgs<P>): this;\n\n /**\n * Send one message on the first channel the user can be reached on: one push\n * and one email template, in priority order, rendered with the same props.\n * Sugar for the branch it compiles to — same IR, ids and hash as writing\n *\n * .branch(label, [reachable(first.channel), (b) => b.<first>(first, props)], (b) => b.<second>(second, props))\n *\n * so the choice is made once, when the step is reached, and a first choice\n * that fails to send retries as itself — it never falls through to the\n * second. Props are always passed (`{}` for templates that declare none),\n * and both templates must declare the same props type; for differing props\n * or other channels, write the branch.\n *\n * .prefer([t.push.tip, t.email.tip], { name: u.name }, { label: 'tip by channel' })\n */\n prefer<const T extends PreferPair, const A extends PropsInput<NoInfer<RefProps<T[0]>>>>(\n candidates: T & SameProps<NoInfer<T>>,\n props: A & NoInfer<NoExtra<A, RefProps<T[0]>>>,\n options?: PreferOptions\n ): this;\n\n /* --------------------------------- Delays --------------------------------- */\n /** Time Delay; passing { min, max } makes it a Randomized Delay. */\n delay(duration: Duration): this;\n delay(range: { min: Duration; max: Duration }): this;\n\n /** Time Window: hold until inside the window (e.g. weekdays 09:00–17:00). tz: 'user' uses the user's timezone. */\n timeWindow(opts: {\n days?: readonly Weekday[];\n between: readonly [TimeOfDay, TimeOfDay];\n tz?: 'user' | (string & {});\n }): this;\n\n /**\n * Wait Until: continue on the main line once the condition holds, or take\n * onTimeout when it expires (default 'continue'). On the engine side this is\n * \"await event racing a timer\" — a native primitive of durable runtimes.\n */\n waitUntil(\n condition: Condition,\n opts: { timeout: Duration; onTimeout?: 'continue' | 'exit' | SubFlow }\n ): this;\n\n /**\n * Splice a fragment in place: a named `flow(...)` or a callback, expanded\n * once while the workflow is defined — a composition step before compiling,\n * not a node (the IR has no trace of it, and ids are assigned over the whole\n * workflow as if the steps were written inline). Each use gets its own copy,\n * so one fragment can be used in several places and workflows.\n *\n * const officeHours = flow((w) => w.timeWindow({ days: ['mon', 'tue', 'wed', 'thu', 'fri'], between: ['09:00', '17:00'] }));\n * workflow({ id: 'onboarding', ... }).delay('1 day').use(officeHours).email(t.email.tip, { name: u.name })\n */\n use(fragment: SubFlow): this;\n\n /* ------------------------------ Flow Control ------------------------------ */\n /**\n * Conditional branch: ordered first-match (same semantics as Step Functions\n * Choice / Knock branch). This one primitive covers both the UI's True/False\n * Branch (two arms) and Multi-Split Branch (N arms). Arms are\n * [condition, sub-flow] tuples; a bare sub-flow in tail position is the\n * default arm. With no match and no default, execution simply continues.\n * Rejoin is structural: after the matching arm finishes, execution resumes\n * at the node following the branch — to opt out, call .exit() inside the arm.\n *\n * .branch([activeSubscriber, upgradeFlow], firstTimeFlow) // True/False\n * .branch([vip, vipFlow], [trial, trialFlow], defaultFlow) // Multi-Split\n * .branch([not(usedStaging), eduFlow]) // single arm: no match continues\n *\n * The optional first argument `label` becomes the node's name in IR (UI title\n * / observability handle). It is not a jump target — there is no goto here;\n * looping is a future re-entry-policy topic (see sendEvent). Arm labels in\n * the UI are derived from the condition.\n */\n branch(...arms: readonly BranchArm[]): this;\n branch(label: string, ...arms: readonly BranchArm[]): this;\n\n /**\n * Gate: exit unless the condition holds, otherwise continue. A first-class\n * node rather than sugar over branch — same semantics as Zapier's \"only\n * continue if\" / Iterable's Filter tile, and rendered on its own in the UI.\n * `reason` is the audit-log line — metadata, so rewording it is free\n * (excluded from contentHash, see hash.ts).\n */\n filter(condition: Condition, opts?: { reason?: string }): this;\n\n /**\n * Random Cohort Branch (A/B): weights must sum to 100 (checked at runtime).\n * `key` names the experiment: bucketing hashes `userId:key` instead of the\n * structural node id, so assignments survive node insertion/moves and the\n * experiment keeps one identity across workflow versions — set it for any\n * A/B whose results you intend to read.\n *\n * Arm names become node id segments and are read back in authoring order, so\n * they are restricted to letters, digits, '_' and '-', never starting with a\n * digit (checked at runtime).\n */\n cohort(arms: Record<string, { weight: number; flow?: SubFlow }>, opts?: { key?: string }): this;\n\n /**\n * Exit: end the whole workflow immediately (the UI's Exit node); `reason`\n * goes into the audit log and is metadata, so rewording it neither repins\n * in-flight journeys nor shows up in plan. Inside a branch arm this means\n * \"terminate, do not rejoin\" — it stops here instead of falling through,\n * while an arm without exit rejoins naturally.\n */\n exit(reason?: string): this;\n\n /* ---------------------------------- Data ---------------------------------- */\n /**\n * Emit a typed event (customer.io's Send Event). The event comes from an\n * e.xxx reference, so payload types flow in the same way trigger.event works.\n * This is how workflows compose: an event can trigger another workflow, and\n * the resulting call graph is statically analyzable. Payload values may\n * reference u.xxx. Only scalar fields can be sent (IR carries literals and\n * profile refs, not nested JSON): an event whose payload requires a nested\n * field cannot be emitted from a journey, and the types say so.\n *\n * Note on loops: a trailing self-triggering sendEvent will become the\n * official periodic-flow shape *once a re-entry policy exists* — today the\n * entry policy is once-per-user, so a self-trigger cannot re-enter and does\n * not loop.\n * TODO: the rest of the Data category — outbound webhook / profile update /\n * journey attributes.\n */\n sendEvent<P extends object>(event: EventRef<P>, ...payload: EmitArgs<P>): this;\n\n /**\n * Run a custom action (see dsl/action.ts for the model). The chain records\n * identity only — name, args, codeHash — never the code: the implementation\n * ships inside the deployed Worker bundle and is resolved by name from the\n * runtime registry. Args are typed by the ActionRef and may reference u.xxx,\n * resolved at run time exactly like message props. Executed inside a durable\n * step: retried on throw, checkpointed on success.\n */\n run<A extends object>(action: ActionRef<A>, ...args: MessageArgs<A>): this;\n}\n\nfunction messageNode(\n channel: ChannelIR,\n tpl: TemplateRef<Channel, object>,\n props: object | undefined,\n devices?: 'latest'\n): NodeIR {\n return {\n id: '',\n type: 'message',\n channel,\n template: tpl.key,\n props: (props ?? {}) as Record<string, PropValueIR>,\n // Absent for 'all', so adding the option left every existing push's hash as it was\n ...(devices !== undefined ? { devices } : {}),\n };\n}\n\n/* oxlint-disable typescript/unbound-method --\n * `captureLoc(this.xxx)` passes the method by identity as the stack-trace\n * boundary for Error.captureStackTrace; it is never invoked, so `this`\n * scoping cannot go wrong. */\n/** @internal extended by WorkflowBuilderImpl; not part of the public API */\nexport class FlowBuilderImpl implements FlowBuilder {\n /** Nodes as collected; ids are assigned in one pass inside toIR, so '' is a placeholder here. */\n readonly nodes: NodeIR[] = [];\n\n /** Provenance: the callsite (captured in the public method) lands in meta.loc, outside the hash. */\n private pushNode(node: NodeIR, loc: SourceLocIR | undefined): this {\n this.nodes.push({ ...node, ...(loc !== undefined ? { meta: { loc } } : {}) });\n return this;\n }\n\n private message(\n channel: ChannelIR,\n tpl: TemplateRef<Channel, object>,\n props: object | undefined,\n loc: SourceLocIR | undefined,\n devices?: 'latest'\n ): this {\n return this.pushNode(messageNode(channel, tpl, props, devices), loc);\n }\n\n email<P extends object>(t: TemplateRef<'email', P>, ...args: MessageArgs<P>): this {\n return this.message('email', t, args[0], captureLoc(this.email));\n }\n sms<P extends object>(t: TemplateRef<'sms', P>, ...args: MessageArgs<P>): this {\n return this.message('sms', t, args[0], captureLoc(this.sms));\n }\n push<P extends object>(\n t: TemplateRef<'push', P>,\n ...args: [...props: MessageArgs<P>, options?: PushOptions]\n ): this {\n const [props, options] = args as unknown as [object | undefined, PushOptions | undefined];\n const devices = options?.devices === 'latest' ? 'latest' : undefined;\n return this.message('push', t, props, captureLoc(this.push), devices);\n }\n inApp<P extends object>(t: TemplateRef<'in_app', P>, ...args: MessageArgs<P>): this {\n return this.message('in_app', t, args[0], captureLoc(this.inApp));\n }\n slack<P extends object>(t: TemplateRef<'slack', P>, ...args: MessageArgs<P>): this {\n return this.message('slack', t, args[0], captureLoc(this.slack));\n }\n discord<P extends object>(t: TemplateRef<'discord', P>, ...args: MessageArgs<P>): this {\n return this.message('discord', t, args[0], captureLoc(this.discord));\n }\n survey<P extends object>(t: TemplateRef<'survey', P>, ...args: MessageArgs<P>): this {\n return this.message('survey', t, args[0], captureLoc(this.survey));\n }\n\n prefer<const T extends PreferPair, const A extends PropsInput<NoInfer<RefProps<T[0]>>>>(\n candidates: T & SameProps<NoInfer<T>>,\n props: A & NoInfer<NoExtra<A, RefProps<T[0]>>>,\n options?: PreferOptions\n ): this {\n checkPreferArgs(candidates, props, options);\n // Every generated node points at the author's prefer() call, not at this method's body\n const loc = captureLoc(this.prefer);\n const meta = loc !== undefined ? { meta: { loc } } : {};\n const [first, second] = candidates as readonly TemplateRef<'push' | 'email', object>[];\n // A copy per message, as two separately written calls would hold\n const arm = (tpl: TemplateRef<'push' | 'email', object>): NodeIR[] => [\n { ...messageNode(tpl.channel, tpl, { ...props }), ...meta },\n ];\n return this.pushNode(\n {\n id: '',\n type: 'branch',\n ...(options?.label !== undefined ? { label: options.label } : {}),\n cases: [\n { condition: { type: 'reachable', channel: first.channel, ...meta }, flow: arm(first) },\n ],\n otherwise: arm(second),\n },\n loc\n );\n }\n\n delay(duration: Duration): this;\n delay(range: { min: Duration; max: Duration }): this;\n delay(d: Duration | { min: Duration; max: Duration }): this {\n const loc = captureLoc(this.delay);\n if (typeof d === 'string') {\n return this.pushNode({ id: '', type: 'delay', duration: durationIR(d) }, loc);\n }\n const min = durationIR(d.min);\n const max = durationIR(d.max);\n /*\n * The interpreter draws `min + random(max - min)` with the span clamped at\n * zero, so a backwards range does not fail — it quietly collapses into a\n * fixed `min` sleep. Reject it here, where the author can still see it.\n */\n if (max.ms < min.ms) {\n throw new Error(\n `delay(): 'max' must not be shorter than 'min', got { min: '${d.min}', max: '${d.max}' }`\n );\n }\n return this.pushNode({ id: '', type: 'random_delay', min, max }, loc);\n }\n\n timeWindow(opts: {\n days?: readonly Weekday[];\n between: readonly [TimeOfDay, TimeOfDay];\n tz?: 'user' | (string & {});\n }): this {\n const loc = captureLoc(this.timeWindow);\n const start = timeOfDayMinutes(opts.between[0]);\n const end = timeOfDayMinutes(opts.between[1]);\n if (end <= start) {\n throw new Error(\n `timeWindow(): 'between' must open before it closes within one day, got [${opts.between[0]}, ${opts.between[1]}] (overnight windows are not supported yet)`\n );\n }\n return this.pushNode(\n {\n id: '',\n type: 'time_window',\n days: [...(opts.days ?? (['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const))],\n between: [opts.between[0], opts.between[1]],\n tz: opts.tz ?? 'user',\n },\n loc\n );\n }\n\n waitUntil(\n condition: Condition,\n opts: { timeout: Duration; onTimeout?: 'continue' | 'exit' | SubFlow }\n ): this {\n const loc = captureLoc(this.waitUntil);\n const onTimeout = opts.onTimeout ?? 'continue';\n return this.pushNode(\n {\n id: '',\n type: 'wait_until',\n condition: condIR(condition),\n timeout: durationIR(opts.timeout),\n onTimeout:\n onTimeout === 'continue' || onTimeout === 'exit' ? onTimeout : resolveSubFlow(onTimeout),\n },\n loc\n );\n }\n\n use(fragment: SubFlow): this {\n // The fragment's nodes carry their own provenance (where the fragment was written)\n this.nodes.push(...resolveSubFlow(fragment));\n return this;\n }\n\n branch(...arms: readonly BranchArm[]): this;\n branch(label: string, ...arms: readonly BranchArm[]): this;\n branch(...args: readonly (string | BranchArm)[]): this {\n const loc = captureLoc(this.branch);\n const label = typeof args[0] === 'string' ? args[0] : undefined;\n const arms = (label === undefined ? args : args.slice(1)) as readonly BranchArm[];\n\n const cases: { condition: ConditionIR; flow: NodeIR[] }[] = [];\n let otherwise: NodeIR[] | undefined;\n arms.forEach((arm, i) => {\n if (Array.isArray(arm)) {\n if (otherwise !== undefined) {\n throw new Error('branch(): default arm (bare sub-flow) must be the last argument');\n }\n const [condition, sub] = arm as BranchCase;\n cases.push({ condition: condIR(condition), flow: resolveSubFlow(sub) });\n } else {\n if (otherwise !== undefined) {\n throw new Error('branch(): only one default arm (bare sub-flow) is allowed');\n }\n if (i !== arms.length - 1) {\n throw new Error('branch(): default arm (bare sub-flow) must be the last argument');\n }\n otherwise = resolveSubFlow(arm as SubFlow);\n }\n });\n\n return this.pushNode(\n {\n id: '',\n type: 'branch',\n ...(label !== undefined ? { label } : {}),\n cases,\n ...(otherwise !== undefined ? { otherwise } : {}),\n },\n loc\n );\n }\n\n filter(condition: Condition, opts?: { reason?: string }): this {\n return this.pushNode(\n {\n id: '',\n type: 'filter',\n condition: condIR(condition),\n ...(opts?.reason !== undefined ? { reason: opts.reason } : {}),\n },\n captureLoc(this.filter)\n );\n }\n\n cohort(arms: Record<string, { weight: number; flow?: SubFlow }>, opts?: { key?: string }): this {\n const loc = captureLoc(this.cohort);\n const entries = Object.entries(arms);\n /*\n * Weights live on a 0.01% grid: the runtime buckets into 10,000 slots\n * (basis points), so anything finer would be silently quantized — reject\n * it here instead. Converting to integer basis points also makes the\n * sum check exact; no epsilon needed.\n */\n const basisPoints = entries.map(([name, a]) => {\n if (!ARM_NAME.test(name)) {\n throw new Error(\n `cohort(): arm name '${name}' must start with a letter or '_' and contain only letters, digits, '_' or '-' — arm names become node id segments, and integer-like names would silently reorder the arms`\n );\n }\n const bp = a.weight * 100;\n if (Math.abs(bp - Math.round(bp)) > 1e-6) {\n throw new Error(\n `cohort(): arm '${name}' weight ${a.weight} is finer than the 0.01% bucketing grid — use at most two decimal places`\n );\n }\n /*\n * The sum check alone accepts -50 / 150: the runtime walks the arms\n * accumulating basis points and takes the first whose cumulative total\n * clears the bucket, so a negative weight hands 100% of users to its\n * neighbour instead of splitting anything.\n */\n if (bp < 0) {\n throw new Error(`cohort(): arm '${name}' weight ${a.weight} must not be negative`);\n }\n return Math.round(bp);\n });\n const total = basisPoints.reduce((sum, bp) => sum + bp, 0);\n if (total !== 10_000) {\n throw new Error(`cohort(): weights must sum to 100, got ${total / 100}`);\n }\n return this.pushNode(\n {\n id: '',\n type: 'cohort',\n ...(opts?.key !== undefined ? { key: opts.key } : {}),\n arms: entries.map(([name, a]) => ({\n name,\n weight: a.weight,\n flow: a.flow ? resolveSubFlow(a.flow) : [],\n })),\n },\n loc\n );\n }\n\n exit(reason?: string): this {\n return this.pushNode(\n { id: '', type: 'exit', ...(reason !== undefined ? { reason } : {}) },\n captureLoc(this.exit)\n );\n }\n\n sendEvent<P extends object>(ev: EventRef<P>, ...payload: EmitArgs<P>): this {\n const values = (payload[0] ?? {}) as Record<string, unknown>;\n for (const [field, value] of Object.entries(values)) {\n const ref =\n typeof value === 'object' &&\n value !== null &&\n (value as { type?: unknown }).type === 'user_property';\n if (!ref && !['string', 'number', 'boolean'].includes(typeof value)) {\n throw new TypeError(\n `sendEvent('${ev.name}'): '${field}' is not a scalar or a u.xxx reference — an emitted payload carries scalar fields only`\n );\n }\n }\n return this.pushNode(\n {\n id: '',\n type: 'send_event',\n event: ev.name,\n payload: values as Record<string, PropValueIR>,\n },\n captureLoc(this.sendEvent)\n );\n }\n\n run<A extends object>(a: ActionRef<A>, ...args: MessageArgs<A>): this {\n return this.pushNode(\n {\n id: '',\n type: 'action',\n action: a.name,\n args: (args[0] ?? {}) as Record<string, PropValueIR>,\n codeHash: a.codeHash,\n },\n captureLoc(this.run)\n );\n }\n}\n/* oxlint-enable typescript/unbound-method */\n\n/**\n * A reusable flow fragment: a free function. Once personalization goes through\n * u.xxx and events through e.xxx, the flow layer carries no schema generics.\n */\nexport function flow(build: (w: FlowBuilder) => FlowBuilder): Flow {\n const builder = new FlowBuilderImpl();\n build(builder);\n const impl: FlowInternal = { __flow: true, nodes: builder.nodes };\n return impl;\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { NodeIR } from "../ir.cjs";
2
2
  import { ActionRef } from "./action.cjs";
3
3
  import { Duration, TimeOfDay, Weekday } from "./base.cjs";
4
- import { EventRef, MessageArgs, PropInput } from "./refs.cjs";
4
+ import { EventRef, MessageArgs, PropInput, PropsInput } from "./refs.cjs";
5
5
  import { TemplateRef } from "./template.cjs";
6
6
  import { Condition } from "./condition.cjs";
7
7
  //#region src/dsl/flow.d.ts
@@ -29,6 +29,24 @@ export interface PushOptions {
29
29
  /** Which devices get a copy: 'all' (the default) or only the most recently registered. */
30
30
  devices?: 'all' | 'latest';
31
31
  }
32
+ /** `prefer()` options. */
33
+ export interface PreferOptions {
34
+ /** The generated branch's name in IR (UI title / observability handle), as `branch(label, …)`. */
35
+ label?: string;
36
+ }
37
+ /** The candidates `prefer()` takes: one push and one email template, in priority order. */
38
+ type PreferPair = readonly [TemplateRef<'push', object>, TemplateRef<'email', object>] | readonly [TemplateRef<'email', object>, TemplateRef<'push', object>];
39
+ type RefProps<R> = R extends TemplateRef<infer _C, infer P> ? P : never;
40
+ type NormalizeProps<P> = { -readonly [K in keyof P]: P[K]; };
41
+ type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => (T extends B ? 1 : 2)) ? true : false;
42
+ /** Both candidates render the one props object, so they must declare the same props type — not merely overlap. */
43
+ type SameProps<T extends PreferPair> = Equal<NormalizeProps<RefProps<T[0]>>, NormalizeProps<RefProps<T[1]>>> extends true ? unknown : {
44
+ readonly __prefer_error__: 'Both templates must declare the same props type';
45
+ };
46
+ /** Distributes, so a field only one member of a union variable has still counts as extra. */
47
+ type KeysOfUnion<A> = A extends unknown ? keyof A : never;
48
+ /** Rejects known extra fields, which `{}` (an empty props schema) would otherwise accept. */
49
+ type NoExtra<A, P> = Record<Exclude<KeysOfUnion<A>, keyof P>, never>;
32
50
  /** A reusable flow fragment (what `flow(...)` returns), referenced directly by branch arms. */
33
51
  export interface Flow {
34
52
  readonly __flow: true;
@@ -48,13 +66,29 @@ export interface FlowBuilder {
48
66
  /**
49
67
  * Push to the user's devices — every device the host's directory lists, or
50
68
  * only the most recently registered with `{ devices: 'latest' }`. A user with
51
- * no device is skipped (branch on `reachable('push')` to fall back to email).
69
+ * no device is skipped (`prefer([pushTpl, emailTpl], props)` falls back to email).
52
70
  */
53
71
  push<P extends object>(template: TemplateRef<'push', P>, ...args: [...props: MessageArgs<P>, options?: PushOptions]): this;
54
72
  inApp<P extends object>(template: TemplateRef<'in_app', P>, ...props: MessageArgs<P>): this;
55
73
  slack<P extends object>(template: TemplateRef<'slack', P>, ...props: MessageArgs<P>): this;
56
74
  discord<P extends object>(template: TemplateRef<'discord', P>, ...props: MessageArgs<P>): this;
57
75
  survey<P extends object>(template: TemplateRef<'survey', P>, ...props: MessageArgs<P>): this;
76
+ /**
77
+ * Send one message on the first channel the user can be reached on: one push
78
+ * and one email template, in priority order, rendered with the same props.
79
+ * Sugar for the branch it compiles to — same IR, ids and hash as writing
80
+ *
81
+ * .branch(label, [reachable(first.channel), (b) => b.<first>(first, props)], (b) => b.<second>(second, props))
82
+ *
83
+ * so the choice is made once, when the step is reached, and a first choice
84
+ * that fails to send retries as itself — it never falls through to the
85
+ * second. Props are always passed (`{}` for templates that declare none),
86
+ * and both templates must declare the same props type; for differing props
87
+ * or other channels, write the branch.
88
+ *
89
+ * .prefer([t.push.tip, t.email.tip], { name: u.name }, { label: 'tip by channel' })
90
+ */
91
+ prefer<const T extends PreferPair, const A extends PropsInput<NoInfer<RefProps<T[0]>>>>(candidates: T & SameProps<NoInfer<T>>, props: A & NoInfer<NoExtra<A, RefProps<T[0]>>>, options?: PreferOptions): this;
58
92
  /** Time Delay; passing { min, max } makes it a Randomized Delay. */
59
93
  delay(duration: Duration): this;
60
94
  delay(range: {
@@ -83,9 +117,8 @@ export interface FlowBuilder {
83
117
  * workflow as if the steps were written inline). Each use gets its own copy,
84
118
  * so one fragment can be used in several places and workflows.
85
119
  *
86
- * const preferPush = (push, email, props) =>
87
- * flow((w) => w.branch([reachable('push'), (b) => b.push(push, props)], (b) => b.email(email, props)));
88
- * workflow({ id: 'onboarding', ... }).use(preferPush(tipPush, tipEmail, { name: u.name }))
120
+ * const officeHours = flow((w) => w.timeWindow({ days: ['mon', 'tue', 'wed', 'thu', 'fri'], between: ['09:00', '17:00'] }));
121
+ * workflow({ id: 'onboarding', ... }).delay('1 day').use(officeHours).email(t.email.tip, { name: u.name })
89
122
  */
90
123
  use(fragment: SubFlow): this;
91
124
  /**
@@ -184,6 +217,7 @@ export declare class FlowBuilderImpl implements FlowBuilder {
184
217
  slack<P extends object>(t: TemplateRef<'slack', P>, ...args: MessageArgs<P>): this;
185
218
  discord<P extends object>(t: TemplateRef<'discord', P>, ...args: MessageArgs<P>): this;
186
219
  survey<P extends object>(t: TemplateRef<'survey', P>, ...args: MessageArgs<P>): this;
220
+ prefer<const T extends PreferPair, const A extends PropsInput<NoInfer<RefProps<T[0]>>>>(candidates: T & SameProps<NoInfer<T>>, props: A & NoInfer<NoExtra<A, RefProps<T[0]>>>, options?: PreferOptions): this;
187
221
  delay(duration: Duration): this;
188
222
  delay(range: {
189
223
  min: Duration;
@@ -1 +1 @@
1
- {"version":3,"file":"flow.d.cts","names":[],"sources":["../../src/dsl/flow.ts"],"mappings":";;;;;;;;;;;;;;;;;;;KAqBK;;KAGA,UAAU,MAAM,YAAY,aAAa,UAAU,UAAU;;YAGtD,YAAY,QAAQ,WAAW,IAAI,UAAU,EAAE;;YAG/C,SAAS,KACnB,6BAA6B,KAAK,UAAU,YAAY,OAAO,SAAS,YAAY;;iBAGrE;;EAEf;;;iBAIe;WACN;;;YAQC,UAAU,SAAS,GAAG,gBAAgB;;YAGtC,uBAAuB,WAAW,WAAW,MAAM;;;;;YAMnD,YAAY,aAAa;iBAsCpB;EAEf,MAAM,kBAAkB,UAAU,qBAAqB,OAAO,OAAO,YAAY;EACjF,IAAI,kBAAkB,UAAU,mBAAmB,OAAO,OAAO,YAAY;;;;;;EAM7E,KAAK,kBACH,UAAU,oBAAoB,OAC3B,UAAU,OAAO,YAAY,IAAI,UAAU;EAEhD,MAAM,kBAAkB,UAAU,sBAAsB,OAAO,OAAO,YAAY;EAClF,MAAM,kBAAkB,UAAU,qBAAqB,OAAO,OAAO,YAAY;EACjF,QAAQ,kBAAkB,UAAU,uBAAuB,OAAO,OAAO,YAAY;EACrF,OAAO,kBAAkB,UAAU,sBAAsB,OAAO,OAAO,YAAY;;EAInF,MAAM,UAAU;EAChB,MAAM;IAAS,KAAK;IAAU,KAAK;;;EAGnC,WAAW;IACT,gBAAgB;IAChB,mBAAmB,WAAW;IAC9B;;;;;;;EAQF,UACE,WAAW,WACX;IAAQ,SAAS;IAAU,kCAAkC;;;;;;;;;;;;;EAc/D,IAAI,UAAU;;;;;;;;;;;;;;;;;;;EAqBd,UAAU,eAAe;EACzB,OAAO,kBAAkB,eAAe;;;;;;;;EASxC,OAAO,WAAW,WAAW;IAAS;;;;;;;;;;;;;EAatC,OAAO,MAAM;IAAiB;IAAgB,OAAO;MAAY;IAAS;;;;;;;;;EAS1E,KAAK;;;;;;;;;;;;;;;;;EAmBL,UAAU,kBAAkB,OAAO,SAAS,OAAO,SAAS,SAAS;;;;;;;;;EAUrE,IAAI,kBAAkB,QAAQ,UAAU,OAAO,MAAM,YAAY;;;qBAQtD,2BAA2B;;WAE7B,OAAO;;UAGR;UAKA;EAqBR,MAAM,kBAAkB,GAAG,qBAAqB,OAAO,MAAM,YAAY;EAGzE,IAAI,kBAAkB,GAAG,mBAAmB,OAAO,MAAM,YAAY;EAGrE,KAAK,kBACH,GAAG,oBAAoB,OACpB,UAAU,OAAO,YAAY,IAAI,UAAU;EAMhD,MAAM,kBAAkB,GAAG,sBAAsB,OAAO,MAAM,YAAY;EAG1E,MAAM,kBAAkB,GAAG,qBAAqB,OAAO,MAAM,YAAY;EAGzE,QAAQ,kBAAkB,GAAG,uBAAuB,OAAO,MAAM,YAAY;EAG7E,OAAO,kBAAkB,GAAG,sBAAsB,OAAO,MAAM,YAAY;EAI3E,MAAM,UAAU;EAChB,MAAM;IAAS,KAAK;IAAU,KAAK;;EAqBnC,WAAW;IACT,gBAAgB;IAChB,mBAAmB,WAAW;IAC9B;;EAsBF,UACE,WAAW,WACX;IAAQ,SAAS;IAAU,kCAAkC;;EAiB/D,IAAI,UAAU;EAMd,UAAU,eAAe;EACzB,OAAO,kBAAkB,eAAe;EAsCxC,OAAO,WAAW,WAAW;IAAS;;EAYtC,OAAO,MAAM;IAAiB;IAAgB,OAAO;MAAY;IAAS;;EAmD1E,KAAK;EAOL,UAAU,kBAAkB,IAAI,SAAS,OAAO,SAAS,SAAS;EAwBlE,IAAI,kBAAkB,GAAG,UAAU,OAAO,MAAM,YAAY;;;;;;wBAmB9C,KAAK,QAAQ,GAAG,gBAAgB,cAAc"}
1
+ {"version":3,"file":"flow.d.cts","names":[],"sources":["../../src/dsl/flow.ts"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { NodeIR } from "../ir.mjs";
2
2
  import { ActionRef } from "./action.mjs";
3
3
  import { Duration, TimeOfDay, Weekday } from "./base.mjs";
4
- import { EventRef, MessageArgs, PropInput } from "./refs.mjs";
4
+ import { EventRef, MessageArgs, PropInput, PropsInput } from "./refs.mjs";
5
5
  import { TemplateRef } from "./template.mjs";
6
6
  import { Condition } from "./condition.mjs";
7
7
  //#region src/dsl/flow.d.ts
@@ -29,6 +29,24 @@ export interface PushOptions {
29
29
  /** Which devices get a copy: 'all' (the default) or only the most recently registered. */
30
30
  devices?: 'all' | 'latest';
31
31
  }
32
+ /** `prefer()` options. */
33
+ export interface PreferOptions {
34
+ /** The generated branch's name in IR (UI title / observability handle), as `branch(label, …)`. */
35
+ label?: string;
36
+ }
37
+ /** The candidates `prefer()` takes: one push and one email template, in priority order. */
38
+ type PreferPair = readonly [TemplateRef<'push', object>, TemplateRef<'email', object>] | readonly [TemplateRef<'email', object>, TemplateRef<'push', object>];
39
+ type RefProps<R> = R extends TemplateRef<infer _C, infer P> ? P : never;
40
+ type NormalizeProps<P> = { -readonly [K in keyof P]: P[K]; };
41
+ type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => (T extends B ? 1 : 2)) ? true : false;
42
+ /** Both candidates render the one props object, so they must declare the same props type — not merely overlap. */
43
+ type SameProps<T extends PreferPair> = Equal<NormalizeProps<RefProps<T[0]>>, NormalizeProps<RefProps<T[1]>>> extends true ? unknown : {
44
+ readonly __prefer_error__: 'Both templates must declare the same props type';
45
+ };
46
+ /** Distributes, so a field only one member of a union variable has still counts as extra. */
47
+ type KeysOfUnion<A> = A extends unknown ? keyof A : never;
48
+ /** Rejects known extra fields, which `{}` (an empty props schema) would otherwise accept. */
49
+ type NoExtra<A, P> = Record<Exclude<KeysOfUnion<A>, keyof P>, never>;
32
50
  /** A reusable flow fragment (what `flow(...)` returns), referenced directly by branch arms. */
33
51
  export interface Flow {
34
52
  readonly __flow: true;
@@ -48,13 +66,29 @@ export interface FlowBuilder {
48
66
  /**
49
67
  * Push to the user's devices — every device the host's directory lists, or
50
68
  * only the most recently registered with `{ devices: 'latest' }`. A user with
51
- * no device is skipped (branch on `reachable('push')` to fall back to email).
69
+ * no device is skipped (`prefer([pushTpl, emailTpl], props)` falls back to email).
52
70
  */
53
71
  push<P extends object>(template: TemplateRef<'push', P>, ...args: [...props: MessageArgs<P>, options?: PushOptions]): this;
54
72
  inApp<P extends object>(template: TemplateRef<'in_app', P>, ...props: MessageArgs<P>): this;
55
73
  slack<P extends object>(template: TemplateRef<'slack', P>, ...props: MessageArgs<P>): this;
56
74
  discord<P extends object>(template: TemplateRef<'discord', P>, ...props: MessageArgs<P>): this;
57
75
  survey<P extends object>(template: TemplateRef<'survey', P>, ...props: MessageArgs<P>): this;
76
+ /**
77
+ * Send one message on the first channel the user can be reached on: one push
78
+ * and one email template, in priority order, rendered with the same props.
79
+ * Sugar for the branch it compiles to — same IR, ids and hash as writing
80
+ *
81
+ * .branch(label, [reachable(first.channel), (b) => b.<first>(first, props)], (b) => b.<second>(second, props))
82
+ *
83
+ * so the choice is made once, when the step is reached, and a first choice
84
+ * that fails to send retries as itself — it never falls through to the
85
+ * second. Props are always passed (`{}` for templates that declare none),
86
+ * and both templates must declare the same props type; for differing props
87
+ * or other channels, write the branch.
88
+ *
89
+ * .prefer([t.push.tip, t.email.tip], { name: u.name }, { label: 'tip by channel' })
90
+ */
91
+ prefer<const T extends PreferPair, const A extends PropsInput<NoInfer<RefProps<T[0]>>>>(candidates: T & SameProps<NoInfer<T>>, props: A & NoInfer<NoExtra<A, RefProps<T[0]>>>, options?: PreferOptions): this;
58
92
  /** Time Delay; passing { min, max } makes it a Randomized Delay. */
59
93
  delay(duration: Duration): this;
60
94
  delay(range: {
@@ -83,9 +117,8 @@ export interface FlowBuilder {
83
117
  * workflow as if the steps were written inline). Each use gets its own copy,
84
118
  * so one fragment can be used in several places and workflows.
85
119
  *
86
- * const preferPush = (push, email, props) =>
87
- * flow((w) => w.branch([reachable('push'), (b) => b.push(push, props)], (b) => b.email(email, props)));
88
- * workflow({ id: 'onboarding', ... }).use(preferPush(tipPush, tipEmail, { name: u.name }))
120
+ * const officeHours = flow((w) => w.timeWindow({ days: ['mon', 'tue', 'wed', 'thu', 'fri'], between: ['09:00', '17:00'] }));
121
+ * workflow({ id: 'onboarding', ... }).delay('1 day').use(officeHours).email(t.email.tip, { name: u.name })
89
122
  */
90
123
  use(fragment: SubFlow): this;
91
124
  /**
@@ -184,6 +217,7 @@ export declare class FlowBuilderImpl implements FlowBuilder {
184
217
  slack<P extends object>(t: TemplateRef<'slack', P>, ...args: MessageArgs<P>): this;
185
218
  discord<P extends object>(t: TemplateRef<'discord', P>, ...args: MessageArgs<P>): this;
186
219
  survey<P extends object>(t: TemplateRef<'survey', P>, ...args: MessageArgs<P>): this;
220
+ prefer<const T extends PreferPair, const A extends PropsInput<NoInfer<RefProps<T[0]>>>>(candidates: T & SameProps<NoInfer<T>>, props: A & NoInfer<NoExtra<A, RefProps<T[0]>>>, options?: PreferOptions): this;
187
221
  delay(duration: Duration): this;
188
222
  delay(range: {
189
223
  min: Duration;
@@ -1 +1 @@
1
- {"version":3,"file":"flow.d.mts","names":[],"sources":["../../src/dsl/flow.ts"],"mappings":";;;;;;;;;;;;;;;;;;;KAqBK;;KAGA,UAAU,MAAM,YAAY,aAAa,UAAU,UAAU;;YAGtD,YAAY,QAAQ,WAAW,IAAI,UAAU,EAAE;;YAG/C,SAAS,KACnB,6BAA6B,KAAK,UAAU,YAAY,OAAO,SAAS,YAAY;;iBAGrE;;EAEf;;;iBAIe;WACN;;;YAQC,UAAU,SAAS,GAAG,gBAAgB;;YAGtC,uBAAuB,WAAW,WAAW,MAAM;;;;;YAMnD,YAAY,aAAa;iBAsCpB;EAEf,MAAM,kBAAkB,UAAU,qBAAqB,OAAO,OAAO,YAAY;EACjF,IAAI,kBAAkB,UAAU,mBAAmB,OAAO,OAAO,YAAY;;;;;;EAM7E,KAAK,kBACH,UAAU,oBAAoB,OAC3B,UAAU,OAAO,YAAY,IAAI,UAAU;EAEhD,MAAM,kBAAkB,UAAU,sBAAsB,OAAO,OAAO,YAAY;EAClF,MAAM,kBAAkB,UAAU,qBAAqB,OAAO,OAAO,YAAY;EACjF,QAAQ,kBAAkB,UAAU,uBAAuB,OAAO,OAAO,YAAY;EACrF,OAAO,kBAAkB,UAAU,sBAAsB,OAAO,OAAO,YAAY;;EAInF,MAAM,UAAU;EAChB,MAAM;IAAS,KAAK;IAAU,KAAK;;;EAGnC,WAAW;IACT,gBAAgB;IAChB,mBAAmB,WAAW;IAC9B;;;;;;;EAQF,UACE,WAAW,WACX;IAAQ,SAAS;IAAU,kCAAkC;;;;;;;;;;;;;EAc/D,IAAI,UAAU;;;;;;;;;;;;;;;;;;;EAqBd,UAAU,eAAe;EACzB,OAAO,kBAAkB,eAAe;;;;;;;;EASxC,OAAO,WAAW,WAAW;IAAS;;;;;;;;;;;;;EAatC,OAAO,MAAM;IAAiB;IAAgB,OAAO;MAAY;IAAS;;;;;;;;;EAS1E,KAAK;;;;;;;;;;;;;;;;;EAmBL,UAAU,kBAAkB,OAAO,SAAS,OAAO,SAAS,SAAS;;;;;;;;;EAUrE,IAAI,kBAAkB,QAAQ,UAAU,OAAO,MAAM,YAAY;;;qBAQtD,2BAA2B;;WAE7B,OAAO;;UAGR;UAKA;EAqBR,MAAM,kBAAkB,GAAG,qBAAqB,OAAO,MAAM,YAAY;EAGzE,IAAI,kBAAkB,GAAG,mBAAmB,OAAO,MAAM,YAAY;EAGrE,KAAK,kBACH,GAAG,oBAAoB,OACpB,UAAU,OAAO,YAAY,IAAI,UAAU;EAMhD,MAAM,kBAAkB,GAAG,sBAAsB,OAAO,MAAM,YAAY;EAG1E,MAAM,kBAAkB,GAAG,qBAAqB,OAAO,MAAM,YAAY;EAGzE,QAAQ,kBAAkB,GAAG,uBAAuB,OAAO,MAAM,YAAY;EAG7E,OAAO,kBAAkB,GAAG,sBAAsB,OAAO,MAAM,YAAY;EAI3E,MAAM,UAAU;EAChB,MAAM;IAAS,KAAK;IAAU,KAAK;;EAqBnC,WAAW;IACT,gBAAgB;IAChB,mBAAmB,WAAW;IAC9B;;EAsBF,UACE,WAAW,WACX;IAAQ,SAAS;IAAU,kCAAkC;;EAiB/D,IAAI,UAAU;EAMd,UAAU,eAAe;EACzB,OAAO,kBAAkB,eAAe;EAsCxC,OAAO,WAAW,WAAW;IAAS;;EAYtC,OAAO,MAAM;IAAiB;IAAgB,OAAO;MAAY;IAAS;;EAmD1E,KAAK;EAOL,UAAU,kBAAkB,IAAI,SAAS,OAAO,SAAS,SAAS;EAwBlE,IAAI,kBAAkB,GAAG,UAAU,OAAO,MAAM,YAAY;;;;;;wBAmB9C,KAAK,QAAQ,GAAG,gBAAgB,cAAc"}
1
+ {"version":3,"file":"flow.d.mts","names":[],"sources":["../../src/dsl/flow.ts"],"mappings":"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"}
package/dist/dsl/flow.mjs CHANGED
@@ -2,6 +2,23 @@ import { captureLoc } from "../provenance.mjs";
2
2
  import { durationIR, timeOfDayMinutes } from "./base.mjs";
3
3
  import { condIR } from "./condition.mjs";
4
4
  //#region src/dsl/flow.ts
5
+ const PREFER_CHANNELS = /* @__PURE__ */ new Set(["push", "email"]);
6
+ /** prefer() is typed strictly, but a JS caller (or an `any`) reaches it unchecked — fail at the call, not at send time. */
7
+ function checkPreferArgs(candidates, props, options) {
8
+ if (!Array.isArray(candidates) || candidates.length !== 2) throw new TypeError("prefer(): candidates must be exactly two templates — one push, one email");
9
+ for (const ref of candidates) {
10
+ const { channel, key } = ref ?? {};
11
+ if (!PREFER_CHANNELS.has(channel) || typeof key !== "string" || key === "") throw new TypeError("prefer(): each candidate must be a push or email template reference (t.push.<key> / t.email.<key>)");
12
+ }
13
+ if (candidates[0].channel === candidates[1].channel) throw new TypeError("prefer(): candidates must be one push and one email template");
14
+ if (typeof props !== "object" || props === null || Array.isArray(props)) throw new TypeError("prefer(): props must be an object — pass {} when the templates declare none");
15
+ if (options === void 0) return;
16
+ if (typeof options !== "object" || options === null || Array.isArray(options)) throw new TypeError("prefer(): options must be an object");
17
+ for (const [name, value] of Object.entries(options)) {
18
+ if (name !== "label") throw new TypeError(`prefer(): unknown option '${name}' — props go in the second argument, options take only 'label'`);
19
+ if (typeof value !== "string") throw new TypeError("prefer(): 'label' must be a string");
20
+ }
21
+ }
5
22
  /**
6
23
  * A cohort arm name has two jobs, and both constrain its spelling:
7
24
  * - it is interpolated into the node ids of the arm's flow ('{id}.{name}.{j}'),
@@ -22,6 +39,16 @@ function resolveSubFlow(sub) {
22
39
  }
23
40
  return structuredClone(sub.nodes);
24
41
  }
42
+ function messageNode(channel, tpl, props, devices) {
43
+ return {
44
+ id: "",
45
+ type: "message",
46
+ channel,
47
+ template: tpl.key,
48
+ props: props ?? {},
49
+ ...devices !== void 0 ? { devices } : {}
50
+ };
51
+ }
25
52
  /** @internal extended by WorkflowBuilderImpl; not part of the public API */
26
53
  var FlowBuilderImpl = class {
27
54
  /** Nodes as collected; ids are assigned in one pass inside toIR, so '' is a placeholder here. */
@@ -35,14 +62,7 @@ var FlowBuilderImpl = class {
35
62
  return this;
36
63
  }
37
64
  message(channel, tpl, props, loc, devices) {
38
- return this.pushNode({
39
- id: "",
40
- type: "message",
41
- channel,
42
- template: tpl.key,
43
- props: props ?? {},
44
- ...devices !== void 0 ? { devices } : {}
45
- }, loc);
65
+ return this.pushNode(messageNode(channel, tpl, props, devices), loc);
46
66
  }
47
67
  email(t, ...args) {
48
68
  return this.message("email", t, args[0], captureLoc(this.email));
@@ -67,6 +87,30 @@ var FlowBuilderImpl = class {
67
87
  survey(t, ...args) {
68
88
  return this.message("survey", t, args[0], captureLoc(this.survey));
69
89
  }
90
+ prefer(candidates, props, options) {
91
+ checkPreferArgs(candidates, props, options);
92
+ const loc = captureLoc(this.prefer);
93
+ const meta = loc !== void 0 ? { meta: { loc } } : {};
94
+ const [first, second] = candidates;
95
+ const arm = (tpl) => [{
96
+ ...messageNode(tpl.channel, tpl, { ...props }),
97
+ ...meta
98
+ }];
99
+ return this.pushNode({
100
+ id: "",
101
+ type: "branch",
102
+ ...options?.label !== void 0 ? { label: options.label } : {},
103
+ cases: [{
104
+ condition: {
105
+ type: "reachable",
106
+ channel: first.channel,
107
+ ...meta
108
+ },
109
+ flow: arm(first)
110
+ }],
111
+ otherwise: arm(second)
112
+ }, loc);
113
+ }
70
114
  delay(d) {
71
115
  const loc = captureLoc(this.delay);
72
116
  if (typeof d === "string") return this.pushNode({
@@ -1 +1 @@
1
- {"version":3,"file":"flow.mjs","names":[],"sources":["../../src/dsl/flow.ts"],"sourcesContent":["import type { ChannelIR, ConditionIR, NodeIR, PropValueIR, SourceLocIR } from '../ir';\nimport { captureLoc } from '../provenance';\nimport type { ActionRef } from './action';\nimport { type Duration, type TimeOfDay, type Weekday, durationIR, timeOfDayMinutes } from './base';\nimport { type Condition, condIR } from './condition';\nimport type { EventRef, MessageArgs, PropInput } from './refs';\nimport type { Channel, TemplateRef } from './template';\n\n/**\n * The flow chain — FlowBuilder is the DSL's step surface (messages, delays,\n * flow control, data), and its implementation is the compiler front half: each\n * chained call executes no logic, it just collects one IR node (ids are\n * assigned later, in workflow.toIR()). Sub-flow callbacks run exactly once at\n * construction — builder in, builder out — so the output is always a static\n * tree.\n *\n * Every public method also captures its callsite (provenance): the loc lands\n * in the node's hash-excluded meta and is the anchor for future in-place\n * editing from the studio.\n */\n\ntype Scalar = string | number | boolean;\n\n/** An emitted field: a scalar (or a profile ref to one); a nested field cannot be sent. */\ntype EmitValue<T> = [NonNullable<T>] extends [Scalar] ? PropInput<T> : never;\n\n/** The payload `sendEvent` accepts for an event: its fields, scalar ones only. */\nexport type EmitPayload<P> = { [K in keyof P]: EmitValue<P[K]> };\n\n/** Payload may be omitted when the event declares no required field. */\nexport type EmitArgs<P> =\n Record<never, never> extends P ? [payload?: EmitPayload<P>] : [payload: EmitPayload<P>];\n\n/** Per-node push options. */\nexport interface PushOptions {\n /** Which devices get a copy: 'all' (the default) or only the most recently registered. */\n devices?: 'all' | 'latest';\n}\n\n/** A reusable flow fragment (what `flow(...)` returns), referenced directly by branch arms. */\nexport interface Flow {\n readonly __flow: true;\n}\n\ninterface FlowInternal extends Flow {\n readonly nodes: NodeIR[];\n}\n\n/** Sub-flow: a named fragment, or an inline callback (runs once at construction — builder in, builder out). */\nexport type SubFlow = Flow | ((w: FlowBuilder) => FlowBuilder);\n\n/** A branch arm: the [condition, sub-flow] tuple — an [if, then] ordering convention. */\nexport type BranchCase = readonly [condition: Condition, flow: SubFlow];\n\n/**\n * A branch argument: either a tuple arm, or a bare sub-flow meaning the default\n * arm. The bare form may appear once, and must come last (checked at runtime).\n */\nexport type BranchArm = BranchCase | SubFlow;\n\n/**\n * A cohort arm name has two jobs, and both constrain its spelling:\n * - it is interpolated into the node ids of the arm's flow ('{id}.{name}.{j}'),\n * and an id is a durable step name — a dot would fake a nesting level and can\n * collide with a real one, an empty name gives '{id}..{j}';\n * - it is an object key, and JavaScript lists integer-like keys first in\n * ascending numeric order however the author wrote them. The arms array is\n * documented (and interpreted) as authoring order, and the bucketing walk\n * accumulates weights along it, so an arm named '0' added to a live\n * experiment would prepend itself and re-bucket everyone already in it.\n */\nconst ARM_NAME = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\nfunction resolveSubFlow(sub: SubFlow): NodeIR[] {\n if (typeof sub === 'function') {\n const builder = new FlowBuilderImpl();\n const returned: unknown = sub(builder);\n // An async callback would add its nodes after construction is over — after ids were assigned\n if (typeof (returned as { then?: unknown } | undefined)?.then === 'function') {\n throw new TypeError(\n 'sub-flow callbacks must be synchronous: they run once, while the workflow is defined'\n );\n }\n return builder.nodes;\n }\n /*\n * A named fragment is a value, and the convention is to reuse it — two branch\n * arms may hold the same one. Handing out its own nodes aliases them into\n * both places, and assignIds writes ids *in place*: the second prefix\n * overwrites the first, so the two placements end up as one object under one\n * id — one durable step name, one message idempotency key, one bucket seed.\n * A copy per placement is what makes a fragment behave like a value.\n */\n return structuredClone((sub as FlowInternal).nodes);\n}\n\nexport interface FlowBuilder {\n /* ---- Messages: every channel is one message node; separate methods buy channel-level typing ---- */\n email<P extends object>(template: TemplateRef<'email', P>, ...props: MessageArgs<P>): this;\n sms<P extends object>(template: TemplateRef<'sms', P>, ...props: MessageArgs<P>): this;\n /**\n * Push to the user's devices — every device the host's directory lists, or\n * only the most recently registered with `{ devices: 'latest' }`. A user with\n * no device is skipped (branch on `reachable('push')` to fall back to email).\n */\n push<P extends object>(\n template: TemplateRef<'push', P>,\n ...args: [...props: MessageArgs<P>, options?: PushOptions]\n ): this;\n inApp<P extends object>(template: TemplateRef<'in_app', P>, ...props: MessageArgs<P>): this;\n slack<P extends object>(template: TemplateRef<'slack', P>, ...props: MessageArgs<P>): this;\n discord<P extends object>(template: TemplateRef<'discord', P>, ...props: MessageArgs<P>): this;\n survey<P extends object>(template: TemplateRef<'survey', P>, ...props: MessageArgs<P>): this;\n\n /* --------------------------------- Delays --------------------------------- */\n /** Time Delay; passing { min, max } makes it a Randomized Delay. */\n delay(duration: Duration): this;\n delay(range: { min: Duration; max: Duration }): this;\n\n /** Time Window: hold until inside the window (e.g. weekdays 09:00–17:00). tz: 'user' uses the user's timezone. */\n timeWindow(opts: {\n days?: readonly Weekday[];\n between: readonly [TimeOfDay, TimeOfDay];\n tz?: 'user' | (string & {});\n }): this;\n\n /**\n * Wait Until: continue on the main line once the condition holds, or take\n * onTimeout when it expires (default 'continue'). On the engine side this is\n * \"await event racing a timer\" — a native primitive of durable runtimes.\n */\n waitUntil(\n condition: Condition,\n opts: { timeout: Duration; onTimeout?: 'continue' | 'exit' | SubFlow }\n ): this;\n\n /**\n * Splice a fragment in place: a named `flow(...)` or a callback, expanded\n * once while the workflow is defined — a composition step before compiling,\n * not a node (the IR has no trace of it, and ids are assigned over the whole\n * workflow as if the steps were written inline). Each use gets its own copy,\n * so one fragment can be used in several places and workflows.\n *\n * const preferPush = (push, email, props) =>\n * flow((w) => w.branch([reachable('push'), (b) => b.push(push, props)], (b) => b.email(email, props)));\n * workflow({ id: 'onboarding', ... }).use(preferPush(tipPush, tipEmail, { name: u.name }))\n */\n use(fragment: SubFlow): this;\n\n /* ------------------------------ Flow Control ------------------------------ */\n /**\n * Conditional branch: ordered first-match (same semantics as Step Functions\n * Choice / Knock branch). This one primitive covers both the UI's True/False\n * Branch (two arms) and Multi-Split Branch (N arms). Arms are\n * [condition, sub-flow] tuples; a bare sub-flow in tail position is the\n * default arm. With no match and no default, execution simply continues.\n * Rejoin is structural: after the matching arm finishes, execution resumes\n * at the node following the branch — to opt out, call .exit() inside the arm.\n *\n * .branch([activeSubscriber, upgradeFlow], firstTimeFlow) // True/False\n * .branch([vip, vipFlow], [trial, trialFlow], defaultFlow) // Multi-Split\n * .branch([not(usedStaging), eduFlow]) // single arm: no match continues\n *\n * The optional first argument `label` becomes the node's name in IR (UI title\n * / observability handle). It is not a jump target — there is no goto here;\n * looping is a future re-entry-policy topic (see sendEvent). Arm labels in\n * the UI are derived from the condition.\n */\n branch(...arms: readonly BranchArm[]): this;\n branch(label: string, ...arms: readonly BranchArm[]): this;\n\n /**\n * Gate: exit unless the condition holds, otherwise continue. A first-class\n * node rather than sugar over branch — same semantics as Zapier's \"only\n * continue if\" / Iterable's Filter tile, and rendered on its own in the UI.\n * `reason` is the audit-log line — metadata, so rewording it is free\n * (excluded from contentHash, see hash.ts).\n */\n filter(condition: Condition, opts?: { reason?: string }): this;\n\n /**\n * Random Cohort Branch (A/B): weights must sum to 100 (checked at runtime).\n * `key` names the experiment: bucketing hashes `userId:key` instead of the\n * structural node id, so assignments survive node insertion/moves and the\n * experiment keeps one identity across workflow versions — set it for any\n * A/B whose results you intend to read.\n *\n * Arm names become node id segments and are read back in authoring order, so\n * they are restricted to letters, digits, '_' and '-', never starting with a\n * digit (checked at runtime).\n */\n cohort(arms: Record<string, { weight: number; flow?: SubFlow }>, opts?: { key?: string }): this;\n\n /**\n * Exit: end the whole workflow immediately (the UI's Exit node); `reason`\n * goes into the audit log and is metadata, so rewording it neither repins\n * in-flight journeys nor shows up in plan. Inside a branch arm this means\n * \"terminate, do not rejoin\" — it stops here instead of falling through,\n * while an arm without exit rejoins naturally.\n */\n exit(reason?: string): this;\n\n /* ---------------------------------- Data ---------------------------------- */\n /**\n * Emit a typed event (customer.io's Send Event). The event comes from an\n * e.xxx reference, so payload types flow in the same way trigger.event works.\n * This is how workflows compose: an event can trigger another workflow, and\n * the resulting call graph is statically analyzable. Payload values may\n * reference u.xxx. Only scalar fields can be sent (IR carries literals and\n * profile refs, not nested JSON): an event whose payload requires a nested\n * field cannot be emitted from a journey, and the types say so.\n *\n * Note on loops: a trailing self-triggering sendEvent will become the\n * official periodic-flow shape *once a re-entry policy exists* — today the\n * entry policy is once-per-user, so a self-trigger cannot re-enter and does\n * not loop.\n * TODO: the rest of the Data category — outbound webhook / profile update /\n * journey attributes.\n */\n sendEvent<P extends object>(event: EventRef<P>, ...payload: EmitArgs<P>): this;\n\n /**\n * Run a custom action (see dsl/action.ts for the model). The chain records\n * identity only — name, args, codeHash — never the code: the implementation\n * ships inside the deployed Worker bundle and is resolved by name from the\n * runtime registry. Args are typed by the ActionRef and may reference u.xxx,\n * resolved at run time exactly like message props. Executed inside a durable\n * step: retried on throw, checkpointed on success.\n */\n run<A extends object>(action: ActionRef<A>, ...args: MessageArgs<A>): this;\n}\n\n/* oxlint-disable typescript/unbound-method --\n * `captureLoc(this.xxx)` passes the method by identity as the stack-trace\n * boundary for Error.captureStackTrace; it is never invoked, so `this`\n * scoping cannot go wrong. */\n/** @internal extended by WorkflowBuilderImpl; not part of the public API */\nexport class FlowBuilderImpl implements FlowBuilder {\n /** Nodes as collected; ids are assigned in one pass inside toIR, so '' is a placeholder here. */\n readonly nodes: NodeIR[] = [];\n\n /** Provenance: the callsite (captured in the public method) lands in meta.loc, outside the hash. */\n private pushNode(node: NodeIR, loc: SourceLocIR | undefined): this {\n this.nodes.push({ ...node, ...(loc !== undefined ? { meta: { loc } } : {}) });\n return this;\n }\n\n private message(\n channel: ChannelIR,\n tpl: TemplateRef<Channel, object>,\n props: object | undefined,\n loc: SourceLocIR | undefined,\n devices?: 'latest'\n ): this {\n return this.pushNode(\n {\n id: '',\n type: 'message',\n channel,\n template: tpl.key,\n props: (props ?? {}) as Record<string, PropValueIR>,\n // Absent for 'all', so adding the option left every existing push's hash as it was\n ...(devices !== undefined ? { devices } : {}),\n },\n loc\n );\n }\n\n email<P extends object>(t: TemplateRef<'email', P>, ...args: MessageArgs<P>): this {\n return this.message('email', t, args[0], captureLoc(this.email));\n }\n sms<P extends object>(t: TemplateRef<'sms', P>, ...args: MessageArgs<P>): this {\n return this.message('sms', t, args[0], captureLoc(this.sms));\n }\n push<P extends object>(\n t: TemplateRef<'push', P>,\n ...args: [...props: MessageArgs<P>, options?: PushOptions]\n ): this {\n const [props, options] = args as unknown as [object | undefined, PushOptions | undefined];\n const devices = options?.devices === 'latest' ? 'latest' : undefined;\n return this.message('push', t, props, captureLoc(this.push), devices);\n }\n inApp<P extends object>(t: TemplateRef<'in_app', P>, ...args: MessageArgs<P>): this {\n return this.message('in_app', t, args[0], captureLoc(this.inApp));\n }\n slack<P extends object>(t: TemplateRef<'slack', P>, ...args: MessageArgs<P>): this {\n return this.message('slack', t, args[0], captureLoc(this.slack));\n }\n discord<P extends object>(t: TemplateRef<'discord', P>, ...args: MessageArgs<P>): this {\n return this.message('discord', t, args[0], captureLoc(this.discord));\n }\n survey<P extends object>(t: TemplateRef<'survey', P>, ...args: MessageArgs<P>): this {\n return this.message('survey', t, args[0], captureLoc(this.survey));\n }\n\n delay(duration: Duration): this;\n delay(range: { min: Duration; max: Duration }): this;\n delay(d: Duration | { min: Duration; max: Duration }): this {\n const loc = captureLoc(this.delay);\n if (typeof d === 'string') {\n return this.pushNode({ id: '', type: 'delay', duration: durationIR(d) }, loc);\n }\n const min = durationIR(d.min);\n const max = durationIR(d.max);\n /*\n * The interpreter draws `min + random(max - min)` with the span clamped at\n * zero, so a backwards range does not fail — it quietly collapses into a\n * fixed `min` sleep. Reject it here, where the author can still see it.\n */\n if (max.ms < min.ms) {\n throw new Error(\n `delay(): 'max' must not be shorter than 'min', got { min: '${d.min}', max: '${d.max}' }`\n );\n }\n return this.pushNode({ id: '', type: 'random_delay', min, max }, loc);\n }\n\n timeWindow(opts: {\n days?: readonly Weekday[];\n between: readonly [TimeOfDay, TimeOfDay];\n tz?: 'user' | (string & {});\n }): this {\n const loc = captureLoc(this.timeWindow);\n const start = timeOfDayMinutes(opts.between[0]);\n const end = timeOfDayMinutes(opts.between[1]);\n if (end <= start) {\n throw new Error(\n `timeWindow(): 'between' must open before it closes within one day, got [${opts.between[0]}, ${opts.between[1]}] (overnight windows are not supported yet)`\n );\n }\n return this.pushNode(\n {\n id: '',\n type: 'time_window',\n days: [...(opts.days ?? (['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const))],\n between: [opts.between[0], opts.between[1]],\n tz: opts.tz ?? 'user',\n },\n loc\n );\n }\n\n waitUntil(\n condition: Condition,\n opts: { timeout: Duration; onTimeout?: 'continue' | 'exit' | SubFlow }\n ): this {\n const loc = captureLoc(this.waitUntil);\n const onTimeout = opts.onTimeout ?? 'continue';\n return this.pushNode(\n {\n id: '',\n type: 'wait_until',\n condition: condIR(condition),\n timeout: durationIR(opts.timeout),\n onTimeout:\n onTimeout === 'continue' || onTimeout === 'exit' ? onTimeout : resolveSubFlow(onTimeout),\n },\n loc\n );\n }\n\n use(fragment: SubFlow): this {\n // The fragment's nodes carry their own provenance (where the fragment was written)\n this.nodes.push(...resolveSubFlow(fragment));\n return this;\n }\n\n branch(...arms: readonly BranchArm[]): this;\n branch(label: string, ...arms: readonly BranchArm[]): this;\n branch(...args: readonly (string | BranchArm)[]): this {\n const loc = captureLoc(this.branch);\n const label = typeof args[0] === 'string' ? args[0] : undefined;\n const arms = (label === undefined ? args : args.slice(1)) as readonly BranchArm[];\n\n const cases: { condition: ConditionIR; flow: NodeIR[] }[] = [];\n let otherwise: NodeIR[] | undefined;\n arms.forEach((arm, i) => {\n if (Array.isArray(arm)) {\n if (otherwise !== undefined) {\n throw new Error('branch(): default arm (bare sub-flow) must be the last argument');\n }\n const [condition, sub] = arm as BranchCase;\n cases.push({ condition: condIR(condition), flow: resolveSubFlow(sub) });\n } else {\n if (otherwise !== undefined) {\n throw new Error('branch(): only one default arm (bare sub-flow) is allowed');\n }\n if (i !== arms.length - 1) {\n throw new Error('branch(): default arm (bare sub-flow) must be the last argument');\n }\n otherwise = resolveSubFlow(arm as SubFlow);\n }\n });\n\n return this.pushNode(\n {\n id: '',\n type: 'branch',\n ...(label !== undefined ? { label } : {}),\n cases,\n ...(otherwise !== undefined ? { otherwise } : {}),\n },\n loc\n );\n }\n\n filter(condition: Condition, opts?: { reason?: string }): this {\n return this.pushNode(\n {\n id: '',\n type: 'filter',\n condition: condIR(condition),\n ...(opts?.reason !== undefined ? { reason: opts.reason } : {}),\n },\n captureLoc(this.filter)\n );\n }\n\n cohort(arms: Record<string, { weight: number; flow?: SubFlow }>, opts?: { key?: string }): this {\n const loc = captureLoc(this.cohort);\n const entries = Object.entries(arms);\n /*\n * Weights live on a 0.01% grid: the runtime buckets into 10,000 slots\n * (basis points), so anything finer would be silently quantized — reject\n * it here instead. Converting to integer basis points also makes the\n * sum check exact; no epsilon needed.\n */\n const basisPoints = entries.map(([name, a]) => {\n if (!ARM_NAME.test(name)) {\n throw new Error(\n `cohort(): arm name '${name}' must start with a letter or '_' and contain only letters, digits, '_' or '-' — arm names become node id segments, and integer-like names would silently reorder the arms`\n );\n }\n const bp = a.weight * 100;\n if (Math.abs(bp - Math.round(bp)) > 1e-6) {\n throw new Error(\n `cohort(): arm '${name}' weight ${a.weight} is finer than the 0.01% bucketing grid — use at most two decimal places`\n );\n }\n /*\n * The sum check alone accepts -50 / 150: the runtime walks the arms\n * accumulating basis points and takes the first whose cumulative total\n * clears the bucket, so a negative weight hands 100% of users to its\n * neighbour instead of splitting anything.\n */\n if (bp < 0) {\n throw new Error(`cohort(): arm '${name}' weight ${a.weight} must not be negative`);\n }\n return Math.round(bp);\n });\n const total = basisPoints.reduce((sum, bp) => sum + bp, 0);\n if (total !== 10_000) {\n throw new Error(`cohort(): weights must sum to 100, got ${total / 100}`);\n }\n return this.pushNode(\n {\n id: '',\n type: 'cohort',\n ...(opts?.key !== undefined ? { key: opts.key } : {}),\n arms: entries.map(([name, a]) => ({\n name,\n weight: a.weight,\n flow: a.flow ? resolveSubFlow(a.flow) : [],\n })),\n },\n loc\n );\n }\n\n exit(reason?: string): this {\n return this.pushNode(\n { id: '', type: 'exit', ...(reason !== undefined ? { reason } : {}) },\n captureLoc(this.exit)\n );\n }\n\n sendEvent<P extends object>(ev: EventRef<P>, ...payload: EmitArgs<P>): this {\n const values = (payload[0] ?? {}) as Record<string, unknown>;\n for (const [field, value] of Object.entries(values)) {\n const ref =\n typeof value === 'object' &&\n value !== null &&\n (value as { type?: unknown }).type === 'user_property';\n if (!ref && !['string', 'number', 'boolean'].includes(typeof value)) {\n throw new TypeError(\n `sendEvent('${ev.name}'): '${field}' is not a scalar or a u.xxx reference — an emitted payload carries scalar fields only`\n );\n }\n }\n return this.pushNode(\n {\n id: '',\n type: 'send_event',\n event: ev.name,\n payload: values as Record<string, PropValueIR>,\n },\n captureLoc(this.sendEvent)\n );\n }\n\n run<A extends object>(a: ActionRef<A>, ...args: MessageArgs<A>): this {\n return this.pushNode(\n {\n id: '',\n type: 'action',\n action: a.name,\n args: (args[0] ?? {}) as Record<string, PropValueIR>,\n codeHash: a.codeHash,\n },\n captureLoc(this.run)\n );\n }\n}\n/* oxlint-enable typescript/unbound-method */\n\n/**\n * A reusable flow fragment: a free function. Once personalization goes through\n * u.xxx and events through e.xxx, the flow layer carries no schema generics.\n */\nexport function flow(build: (w: FlowBuilder) => FlowBuilder): Flow {\n const builder = new FlowBuilderImpl();\n build(builder);\n const impl: FlowInternal = { __flow: true, nodes: builder.nodes };\n return impl;\n}\n"],"mappings":";;;;;;;;;;;;;;;AAuEA,MAAM,WAAW;AAEjB,SAAS,eAAe,KAAwB;CAC9C,IAAI,OAAO,QAAQ,YAAY;EAC7B,MAAM,UAAU,IAAI,gBAAgB;EAGpC,IAAI,OAFsB,IAAI,OAEX,CAAC,EAAqC,SAAS,YAChE,MAAM,IAAI,UACR,sFACF;EAEF,OAAO,QAAQ;CACjB;CASA,OAAO,gBAAiB,IAAqB,KAAK;AACpD;;AA+IA,IAAa,kBAAb,MAAoD;;CAElD,QAA2B,CAAC;;CAG5B,SAAiB,MAAc,KAAoC;EACjE,KAAK,MAAM,KAAK;GAAE,GAAG;GAAM,GAAI,QAAQ,KAAA,IAAY,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC;EAAG,CAAC;EAC5E,OAAO;CACT;CAEA,QACE,SACA,KACA,OACA,KACA,SACM;EACN,OAAO,KAAK,SACV;GACE,IAAI;GACJ,MAAM;GACN;GACA,UAAU,IAAI;GACd,OAAQ,SAAS,CAAC;GAElB,GAAI,YAAY,KAAA,IAAY,EAAE,QAAQ,IAAI,CAAC;EAC7C,GACA,GACF;CACF;CAEA,MAAwB,GAA4B,GAAG,MAA4B;EACjF,OAAO,KAAK,QAAQ,SAAS,GAAG,KAAK,IAAI,WAAW,KAAK,KAAK,CAAC;CACjE;CACA,IAAsB,GAA0B,GAAG,MAA4B;EAC7E,OAAO,KAAK,QAAQ,OAAO,GAAG,KAAK,IAAI,WAAW,KAAK,GAAG,CAAC;CAC7D;CACA,KACE,GACA,GAAG,MACG;EACN,MAAM,CAAC,OAAO,WAAW;EACzB,MAAM,UAAU,SAAS,YAAY,WAAW,WAAW,KAAA;EAC3D,OAAO,KAAK,QAAQ,QAAQ,GAAG,OAAO,WAAW,KAAK,IAAI,GAAG,OAAO;CACtE;CACA,MAAwB,GAA6B,GAAG,MAA4B;EAClF,OAAO,KAAK,QAAQ,UAAU,GAAG,KAAK,IAAI,WAAW,KAAK,KAAK,CAAC;CAClE;CACA,MAAwB,GAA4B,GAAG,MAA4B;EACjF,OAAO,KAAK,QAAQ,SAAS,GAAG,KAAK,IAAI,WAAW,KAAK,KAAK,CAAC;CACjE;CACA,QAA0B,GAA8B,GAAG,MAA4B;EACrF,OAAO,KAAK,QAAQ,WAAW,GAAG,KAAK,IAAI,WAAW,KAAK,OAAO,CAAC;CACrE;CACA,OAAyB,GAA6B,GAAG,MAA4B;EACnF,OAAO,KAAK,QAAQ,UAAU,GAAG,KAAK,IAAI,WAAW,KAAK,MAAM,CAAC;CACnE;CAIA,MAAM,GAAsD;EAC1D,MAAM,MAAM,WAAW,KAAK,KAAK;EACjC,IAAI,OAAO,MAAM,UACf,OAAO,KAAK,SAAS;GAAE,IAAI;GAAI,MAAM;GAAS,UAAU,WAAW,CAAC;EAAE,GAAG,GAAG;EAE9E,MAAM,MAAM,WAAW,EAAE,GAAG;EAC5B,MAAM,MAAM,WAAW,EAAE,GAAG;EAM5B,IAAI,IAAI,KAAK,IAAI,IACf,MAAM,IAAI,MACR,8DAA8D,EAAE,IAAI,WAAW,EAAE,IAAI,IACvF;EAEF,OAAO,KAAK,SAAS;GAAE,IAAI;GAAI,MAAM;GAAgB;GAAK;EAAI,GAAG,GAAG;CACtE;CAEA,WAAW,MAIF;EACP,MAAM,MAAM,WAAW,KAAK,UAAU;EACtC,MAAM,QAAQ,iBAAiB,KAAK,QAAQ,EAAE;EAE9C,IADY,iBAAiB,KAAK,QAAQ,EACpC,KAAK,OACT,MAAM,IAAI,MACR,2EAA2E,KAAK,QAAQ,GAAG,IAAI,KAAK,QAAQ,GAAG,4CACjH;EAEF,OAAO,KAAK,SACV;GACE,IAAI;GACJ,MAAM;GACN,MAAM,CAAC,GAAI,KAAK,QAAS;IAAC;IAAO;IAAO;IAAO;IAAO;IAAO;IAAO;GAAK,CAAY;GACrF,SAAS,CAAC,KAAK,QAAQ,IAAI,KAAK,QAAQ,EAAE;GAC1C,IAAI,KAAK,MAAM;EACjB,GACA,GACF;CACF;CAEA,UACE,WACA,MACM;EACN,MAAM,MAAM,WAAW,KAAK,SAAS;EACrC,MAAM,YAAY,KAAK,aAAa;EACpC,OAAO,KAAK,SACV;GACE,IAAI;GACJ,MAAM;GACN,WAAW,OAAO,SAAS;GAC3B,SAAS,WAAW,KAAK,OAAO;GAChC,WACE,cAAc,cAAc,cAAc,SAAS,YAAY,eAAe,SAAS;EAC3F,GACA,GACF;CACF;CAEA,IAAI,UAAyB;EAE3B,KAAK,MAAM,KAAK,GAAG,eAAe,QAAQ,CAAC;EAC3C,OAAO;CACT;CAIA,OAAO,GAAG,MAA6C;EACrD,MAAM,MAAM,WAAW,KAAK,MAAM;EAClC,MAAM,QAAQ,OAAO,KAAK,OAAO,WAAW,KAAK,KAAK,KAAA;EACtD,MAAM,OAAQ,UAAU,KAAA,IAAY,OAAO,KAAK,MAAM,CAAC;EAEvD,MAAM,QAAsD,CAAC;EAC7D,IAAI;EACJ,KAAK,SAAS,KAAK,MAAM;GACvB,IAAI,MAAM,QAAQ,GAAG,GAAG;IACtB,IAAI,cAAc,KAAA,GAChB,MAAM,IAAI,MAAM,iEAAiE;IAEnF,MAAM,CAAC,WAAW,OAAO;IACzB,MAAM,KAAK;KAAE,WAAW,OAAO,SAAS;KAAG,MAAM,eAAe,GAAG;IAAE,CAAC;GACxE,OAAO;IACL,IAAI,cAAc,KAAA,GAChB,MAAM,IAAI,MAAM,2DAA2D;IAE7E,IAAI,MAAM,KAAK,SAAS,GACtB,MAAM,IAAI,MAAM,iEAAiE;IAEnF,YAAY,eAAe,GAAc;GAC3C;EACF,CAAC;EAED,OAAO,KAAK,SACV;GACE,IAAI;GACJ,MAAM;GACN,GAAI,UAAU,KAAA,IAAY,EAAE,MAAM,IAAI,CAAC;GACvC;GACA,GAAI,cAAc,KAAA,IAAY,EAAE,UAAU,IAAI,CAAC;EACjD,GACA,GACF;CACF;CAEA,OAAO,WAAsB,MAAkC;EAC7D,OAAO,KAAK,SACV;GACE,IAAI;GACJ,MAAM;GACN,WAAW,OAAO,SAAS;GAC3B,GAAI,MAAM,WAAW,KAAA,IAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;EAC9D,GACA,WAAW,KAAK,MAAM,CACxB;CACF;CAEA,OAAO,MAA0D,MAA+B;EAC9F,MAAM,MAAM,WAAW,KAAK,MAAM;EAClC,MAAM,UAAU,OAAO,QAAQ,IAAI;EA8BnC,MAAM,QAvBc,QAAQ,KAAK,CAAC,MAAM,OAAO;GAC7C,IAAI,CAAC,SAAS,KAAK,IAAI,GACrB,MAAM,IAAI,MACR,uBAAuB,KAAK,2KAC9B;GAEF,MAAM,KAAK,EAAE,SAAS;GACtB,IAAI,KAAK,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC,IAAI,MAClC,MAAM,IAAI,MACR,kBAAkB,KAAK,WAAW,EAAE,OAAO,yEAC7C;GAQF,IAAI,KAAK,GACP,MAAM,IAAI,MAAM,kBAAkB,KAAK,WAAW,EAAE,OAAO,sBAAsB;GAEnF,OAAO,KAAK,MAAM,EAAE;EACtB,CACwB,CAAC,CAAC,QAAQ,KAAK,OAAO,MAAM,IAAI,CAAC;EACzD,IAAI,UAAU,KACZ,MAAM,IAAI,MAAM,0CAA0C,QAAQ,KAAK;EAEzE,OAAO,KAAK,SACV;GACE,IAAI;GACJ,MAAM;GACN,GAAI,MAAM,QAAQ,KAAA,IAAY,EAAE,KAAK,KAAK,IAAI,IAAI,CAAC;GACnD,MAAM,QAAQ,KAAK,CAAC,MAAM,QAAQ;IAChC;IACA,QAAQ,EAAE;IACV,MAAM,EAAE,OAAO,eAAe,EAAE,IAAI,IAAI,CAAC;GAC3C,EAAE;EACJ,GACA,GACF;CACF;CAEA,KAAK,QAAuB;EAC1B,OAAO,KAAK,SACV;GAAE,IAAI;GAAI,MAAM;GAAQ,GAAI,WAAW,KAAA,IAAY,EAAE,OAAO,IAAI,CAAC;EAAG,GACpE,WAAW,KAAK,IAAI,CACtB;CACF;CAEA,UAA4B,IAAiB,GAAG,SAA4B;EAC1E,MAAM,SAAU,QAAQ,MAAM,CAAC;EAC/B,KAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,MAAM,GAKhD,IAAI,EAHF,OAAO,UAAU,YACjB,UAAU,QACT,MAA6B,SAAS,oBAC7B,CAAC;GAAC;GAAU;GAAU;EAAS,CAAC,CAAC,SAAS,OAAO,KAAK,GAChE,MAAM,IAAI,UACR,cAAc,GAAG,KAAK,OAAO,MAAM,uFACrC;EAGJ,OAAO,KAAK,SACV;GACE,IAAI;GACJ,MAAM;GACN,OAAO,GAAG;GACV,SAAS;EACX,GACA,WAAW,KAAK,SAAS,CAC3B;CACF;CAEA,IAAsB,GAAiB,GAAG,MAA4B;EACpE,OAAO,KAAK,SACV;GACE,IAAI;GACJ,MAAM;GACN,QAAQ,EAAE;GACV,MAAO,KAAK,MAAM,CAAC;GACnB,UAAU,EAAE;EACd,GACA,WAAW,KAAK,GAAG,CACrB;CACF;AACF;;;;;AAOA,SAAgB,KAAK,OAA8C;CACjE,MAAM,UAAU,IAAI,gBAAgB;CACpC,MAAM,OAAO;CAEb,OAAO;EADsB,QAAQ;EAAM,OAAO,QAAQ;CAChD;AACZ"}
1
+ {"version":3,"file":"flow.mjs","names":[],"sources":["../../src/dsl/flow.ts"],"sourcesContent":["import type { ChannelIR, ConditionIR, NodeIR, PropValueIR, SourceLocIR } from '../ir';\nimport { captureLoc } from '../provenance';\nimport type { ActionRef } from './action';\nimport { type Duration, type TimeOfDay, type Weekday, durationIR, timeOfDayMinutes } from './base';\nimport { type Condition, condIR } from './condition';\nimport type { EventRef, MessageArgs, PropInput, PropsInput } from './refs';\nimport type { Channel, TemplateRef } from './template';\n\n/**\n * The flow chain — FlowBuilder is the DSL's step surface (messages, delays,\n * flow control, data), and its implementation is the compiler front half: each\n * chained call executes no logic, it just collects one IR node (ids are\n * assigned later, in workflow.toIR()). Sub-flow callbacks run exactly once at\n * construction — builder in, builder out — so the output is always a static\n * tree.\n *\n * Every public method also captures its callsite (provenance): the loc lands\n * in the node's hash-excluded meta and is the anchor for future in-place\n * editing from the studio.\n */\n\ntype Scalar = string | number | boolean;\n\n/** An emitted field: a scalar (or a profile ref to one); a nested field cannot be sent. */\ntype EmitValue<T> = [NonNullable<T>] extends [Scalar] ? PropInput<T> : never;\n\n/** The payload `sendEvent` accepts for an event: its fields, scalar ones only. */\nexport type EmitPayload<P> = { [K in keyof P]: EmitValue<P[K]> };\n\n/** Payload may be omitted when the event declares no required field. */\nexport type EmitArgs<P> =\n Record<never, never> extends P ? [payload?: EmitPayload<P>] : [payload: EmitPayload<P>];\n\n/** Per-node push options. */\nexport interface PushOptions {\n /** Which devices get a copy: 'all' (the default) or only the most recently registered. */\n devices?: 'all' | 'latest';\n}\n\n/** `prefer()` options. */\nexport interface PreferOptions {\n /** The generated branch's name in IR (UI title / observability handle), as `branch(label, …)`. */\n label?: string;\n}\n\n/** The candidates `prefer()` takes: one push and one email template, in priority order. */\ntype PreferPair =\n | readonly [TemplateRef<'push', object>, TemplateRef<'email', object>]\n | readonly [TemplateRef<'email', object>, TemplateRef<'push', object>];\n\ntype RefProps<R> = R extends TemplateRef<infer _C, infer P> ? P : never;\ntype NormalizeProps<P> = { -readonly [K in keyof P]: P[K] };\ntype Equal<A, B> =\n (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false;\n\n/** Both candidates render the one props object, so they must declare the same props type — not merely overlap. */\ntype SameProps<T extends PreferPair> =\n Equal<NormalizeProps<RefProps<T[0]>>, NormalizeProps<RefProps<T[1]>>> extends true\n ? unknown\n : { readonly __prefer_error__: 'Both templates must declare the same props type' };\n\n/** Distributes, so a field only one member of a union variable has still counts as extra. */\ntype KeysOfUnion<A> = A extends unknown ? keyof A : never;\n/** Rejects known extra fields, which `{}` (an empty props schema) would otherwise accept. */\ntype NoExtra<A, P> = Record<Exclude<KeysOfUnion<A>, keyof P>, never>;\n\nconst PREFER_CHANNELS: ReadonlySet<unknown> = new Set(['push', 'email']);\n\n/** prefer() is typed strictly, but a JS caller (or an `any`) reaches it unchecked — fail at the call, not at send time. */\nfunction checkPreferArgs(candidates: unknown, props: unknown, options: unknown): void {\n if (!Array.isArray(candidates) || candidates.length !== 2) {\n throw new TypeError('prefer(): candidates must be exactly two templates — one push, one email');\n }\n for (const ref of candidates as unknown[]) {\n const { channel, key } = (ref ?? {}) as Partial<TemplateRef>;\n if (!PREFER_CHANNELS.has(channel) || typeof key !== 'string' || key === '') {\n throw new TypeError(\n 'prefer(): each candidate must be a push or email template reference (t.push.<key> / t.email.<key>)'\n );\n }\n }\n if ((candidates[0] as TemplateRef).channel === (candidates[1] as TemplateRef).channel) {\n throw new TypeError('prefer(): candidates must be one push and one email template');\n }\n if (typeof props !== 'object' || props === null || Array.isArray(props)) {\n throw new TypeError(\n 'prefer(): props must be an object — pass {} when the templates declare none'\n );\n }\n if (options === undefined) return;\n if (typeof options !== 'object' || options === null || Array.isArray(options)) {\n throw new TypeError('prefer(): options must be an object');\n }\n for (const [name, value] of Object.entries(options)) {\n if (name !== 'label') {\n throw new TypeError(\n `prefer(): unknown option '${name}' — props go in the second argument, options take only 'label'`\n );\n }\n if (typeof value !== 'string') throw new TypeError(\"prefer(): 'label' must be a string\");\n }\n}\n\n/** A reusable flow fragment (what `flow(...)` returns), referenced directly by branch arms. */\nexport interface Flow {\n readonly __flow: true;\n}\n\ninterface FlowInternal extends Flow {\n readonly nodes: NodeIR[];\n}\n\n/** Sub-flow: a named fragment, or an inline callback (runs once at construction — builder in, builder out). */\nexport type SubFlow = Flow | ((w: FlowBuilder) => FlowBuilder);\n\n/** A branch arm: the [condition, sub-flow] tuple — an [if, then] ordering convention. */\nexport type BranchCase = readonly [condition: Condition, flow: SubFlow];\n\n/**\n * A branch argument: either a tuple arm, or a bare sub-flow meaning the default\n * arm. The bare form may appear once, and must come last (checked at runtime).\n */\nexport type BranchArm = BranchCase | SubFlow;\n\n/**\n * A cohort arm name has two jobs, and both constrain its spelling:\n * - it is interpolated into the node ids of the arm's flow ('{id}.{name}.{j}'),\n * and an id is a durable step name — a dot would fake a nesting level and can\n * collide with a real one, an empty name gives '{id}..{j}';\n * - it is an object key, and JavaScript lists integer-like keys first in\n * ascending numeric order however the author wrote them. The arms array is\n * documented (and interpreted) as authoring order, and the bucketing walk\n * accumulates weights along it, so an arm named '0' added to a live\n * experiment would prepend itself and re-bucket everyone already in it.\n */\nconst ARM_NAME = /^[A-Za-z_][A-Za-z0-9_-]*$/;\n\nfunction resolveSubFlow(sub: SubFlow): NodeIR[] {\n if (typeof sub === 'function') {\n const builder = new FlowBuilderImpl();\n const returned: unknown = sub(builder);\n // An async callback would add its nodes after construction is over — after ids were assigned\n if (typeof (returned as { then?: unknown } | undefined)?.then === 'function') {\n throw new TypeError(\n 'sub-flow callbacks must be synchronous: they run once, while the workflow is defined'\n );\n }\n return builder.nodes;\n }\n /*\n * A named fragment is a value, and the convention is to reuse it — two branch\n * arms may hold the same one. Handing out its own nodes aliases them into\n * both places, and assignIds writes ids *in place*: the second prefix\n * overwrites the first, so the two placements end up as one object under one\n * id — one durable step name, one message idempotency key, one bucket seed.\n * A copy per placement is what makes a fragment behave like a value.\n */\n return structuredClone((sub as FlowInternal).nodes);\n}\n\nexport interface FlowBuilder {\n /* ---- Messages: every channel is one message node; separate methods buy channel-level typing ---- */\n email<P extends object>(template: TemplateRef<'email', P>, ...props: MessageArgs<P>): this;\n sms<P extends object>(template: TemplateRef<'sms', P>, ...props: MessageArgs<P>): this;\n /**\n * Push to the user's devices — every device the host's directory lists, or\n * only the most recently registered with `{ devices: 'latest' }`. A user with\n * no device is skipped (`prefer([pushTpl, emailTpl], props)` falls back to email).\n */\n push<P extends object>(\n template: TemplateRef<'push', P>,\n ...args: [...props: MessageArgs<P>, options?: PushOptions]\n ): this;\n inApp<P extends object>(template: TemplateRef<'in_app', P>, ...props: MessageArgs<P>): this;\n slack<P extends object>(template: TemplateRef<'slack', P>, ...props: MessageArgs<P>): this;\n discord<P extends object>(template: TemplateRef<'discord', P>, ...props: MessageArgs<P>): this;\n survey<P extends object>(template: TemplateRef<'survey', P>, ...props: MessageArgs<P>): this;\n\n /**\n * Send one message on the first channel the user can be reached on: one push\n * and one email template, in priority order, rendered with the same props.\n * Sugar for the branch it compiles to — same IR, ids and hash as writing\n *\n * .branch(label, [reachable(first.channel), (b) => b.<first>(first, props)], (b) => b.<second>(second, props))\n *\n * so the choice is made once, when the step is reached, and a first choice\n * that fails to send retries as itself — it never falls through to the\n * second. Props are always passed (`{}` for templates that declare none),\n * and both templates must declare the same props type; for differing props\n * or other channels, write the branch.\n *\n * .prefer([t.push.tip, t.email.tip], { name: u.name }, { label: 'tip by channel' })\n */\n prefer<const T extends PreferPair, const A extends PropsInput<NoInfer<RefProps<T[0]>>>>(\n candidates: T & SameProps<NoInfer<T>>,\n props: A & NoInfer<NoExtra<A, RefProps<T[0]>>>,\n options?: PreferOptions\n ): this;\n\n /* --------------------------------- Delays --------------------------------- */\n /** Time Delay; passing { min, max } makes it a Randomized Delay. */\n delay(duration: Duration): this;\n delay(range: { min: Duration; max: Duration }): this;\n\n /** Time Window: hold until inside the window (e.g. weekdays 09:00–17:00). tz: 'user' uses the user's timezone. */\n timeWindow(opts: {\n days?: readonly Weekday[];\n between: readonly [TimeOfDay, TimeOfDay];\n tz?: 'user' | (string & {});\n }): this;\n\n /**\n * Wait Until: continue on the main line once the condition holds, or take\n * onTimeout when it expires (default 'continue'). On the engine side this is\n * \"await event racing a timer\" — a native primitive of durable runtimes.\n */\n waitUntil(\n condition: Condition,\n opts: { timeout: Duration; onTimeout?: 'continue' | 'exit' | SubFlow }\n ): this;\n\n /**\n * Splice a fragment in place: a named `flow(...)` or a callback, expanded\n * once while the workflow is defined — a composition step before compiling,\n * not a node (the IR has no trace of it, and ids are assigned over the whole\n * workflow as if the steps were written inline). Each use gets its own copy,\n * so one fragment can be used in several places and workflows.\n *\n * const officeHours = flow((w) => w.timeWindow({ days: ['mon', 'tue', 'wed', 'thu', 'fri'], between: ['09:00', '17:00'] }));\n * workflow({ id: 'onboarding', ... }).delay('1 day').use(officeHours).email(t.email.tip, { name: u.name })\n */\n use(fragment: SubFlow): this;\n\n /* ------------------------------ Flow Control ------------------------------ */\n /**\n * Conditional branch: ordered first-match (same semantics as Step Functions\n * Choice / Knock branch). This one primitive covers both the UI's True/False\n * Branch (two arms) and Multi-Split Branch (N arms). Arms are\n * [condition, sub-flow] tuples; a bare sub-flow in tail position is the\n * default arm. With no match and no default, execution simply continues.\n * Rejoin is structural: after the matching arm finishes, execution resumes\n * at the node following the branch — to opt out, call .exit() inside the arm.\n *\n * .branch([activeSubscriber, upgradeFlow], firstTimeFlow) // True/False\n * .branch([vip, vipFlow], [trial, trialFlow], defaultFlow) // Multi-Split\n * .branch([not(usedStaging), eduFlow]) // single arm: no match continues\n *\n * The optional first argument `label` becomes the node's name in IR (UI title\n * / observability handle). It is not a jump target — there is no goto here;\n * looping is a future re-entry-policy topic (see sendEvent). Arm labels in\n * the UI are derived from the condition.\n */\n branch(...arms: readonly BranchArm[]): this;\n branch(label: string, ...arms: readonly BranchArm[]): this;\n\n /**\n * Gate: exit unless the condition holds, otherwise continue. A first-class\n * node rather than sugar over branch — same semantics as Zapier's \"only\n * continue if\" / Iterable's Filter tile, and rendered on its own in the UI.\n * `reason` is the audit-log line — metadata, so rewording it is free\n * (excluded from contentHash, see hash.ts).\n */\n filter(condition: Condition, opts?: { reason?: string }): this;\n\n /**\n * Random Cohort Branch (A/B): weights must sum to 100 (checked at runtime).\n * `key` names the experiment: bucketing hashes `userId:key` instead of the\n * structural node id, so assignments survive node insertion/moves and the\n * experiment keeps one identity across workflow versions — set it for any\n * A/B whose results you intend to read.\n *\n * Arm names become node id segments and are read back in authoring order, so\n * they are restricted to letters, digits, '_' and '-', never starting with a\n * digit (checked at runtime).\n */\n cohort(arms: Record<string, { weight: number; flow?: SubFlow }>, opts?: { key?: string }): this;\n\n /**\n * Exit: end the whole workflow immediately (the UI's Exit node); `reason`\n * goes into the audit log and is metadata, so rewording it neither repins\n * in-flight journeys nor shows up in plan. Inside a branch arm this means\n * \"terminate, do not rejoin\" — it stops here instead of falling through,\n * while an arm without exit rejoins naturally.\n */\n exit(reason?: string): this;\n\n /* ---------------------------------- Data ---------------------------------- */\n /**\n * Emit a typed event (customer.io's Send Event). The event comes from an\n * e.xxx reference, so payload types flow in the same way trigger.event works.\n * This is how workflows compose: an event can trigger another workflow, and\n * the resulting call graph is statically analyzable. Payload values may\n * reference u.xxx. Only scalar fields can be sent (IR carries literals and\n * profile refs, not nested JSON): an event whose payload requires a nested\n * field cannot be emitted from a journey, and the types say so.\n *\n * Note on loops: a trailing self-triggering sendEvent will become the\n * official periodic-flow shape *once a re-entry policy exists* — today the\n * entry policy is once-per-user, so a self-trigger cannot re-enter and does\n * not loop.\n * TODO: the rest of the Data category — outbound webhook / profile update /\n * journey attributes.\n */\n sendEvent<P extends object>(event: EventRef<P>, ...payload: EmitArgs<P>): this;\n\n /**\n * Run a custom action (see dsl/action.ts for the model). The chain records\n * identity only — name, args, codeHash — never the code: the implementation\n * ships inside the deployed Worker bundle and is resolved by name from the\n * runtime registry. Args are typed by the ActionRef and may reference u.xxx,\n * resolved at run time exactly like message props. Executed inside a durable\n * step: retried on throw, checkpointed on success.\n */\n run<A extends object>(action: ActionRef<A>, ...args: MessageArgs<A>): this;\n}\n\nfunction messageNode(\n channel: ChannelIR,\n tpl: TemplateRef<Channel, object>,\n props: object | undefined,\n devices?: 'latest'\n): NodeIR {\n return {\n id: '',\n type: 'message',\n channel,\n template: tpl.key,\n props: (props ?? {}) as Record<string, PropValueIR>,\n // Absent for 'all', so adding the option left every existing push's hash as it was\n ...(devices !== undefined ? { devices } : {}),\n };\n}\n\n/* oxlint-disable typescript/unbound-method --\n * `captureLoc(this.xxx)` passes the method by identity as the stack-trace\n * boundary for Error.captureStackTrace; it is never invoked, so `this`\n * scoping cannot go wrong. */\n/** @internal extended by WorkflowBuilderImpl; not part of the public API */\nexport class FlowBuilderImpl implements FlowBuilder {\n /** Nodes as collected; ids are assigned in one pass inside toIR, so '' is a placeholder here. */\n readonly nodes: NodeIR[] = [];\n\n /** Provenance: the callsite (captured in the public method) lands in meta.loc, outside the hash. */\n private pushNode(node: NodeIR, loc: SourceLocIR | undefined): this {\n this.nodes.push({ ...node, ...(loc !== undefined ? { meta: { loc } } : {}) });\n return this;\n }\n\n private message(\n channel: ChannelIR,\n tpl: TemplateRef<Channel, object>,\n props: object | undefined,\n loc: SourceLocIR | undefined,\n devices?: 'latest'\n ): this {\n return this.pushNode(messageNode(channel, tpl, props, devices), loc);\n }\n\n email<P extends object>(t: TemplateRef<'email', P>, ...args: MessageArgs<P>): this {\n return this.message('email', t, args[0], captureLoc(this.email));\n }\n sms<P extends object>(t: TemplateRef<'sms', P>, ...args: MessageArgs<P>): this {\n return this.message('sms', t, args[0], captureLoc(this.sms));\n }\n push<P extends object>(\n t: TemplateRef<'push', P>,\n ...args: [...props: MessageArgs<P>, options?: PushOptions]\n ): this {\n const [props, options] = args as unknown as [object | undefined, PushOptions | undefined];\n const devices = options?.devices === 'latest' ? 'latest' : undefined;\n return this.message('push', t, props, captureLoc(this.push), devices);\n }\n inApp<P extends object>(t: TemplateRef<'in_app', P>, ...args: MessageArgs<P>): this {\n return this.message('in_app', t, args[0], captureLoc(this.inApp));\n }\n slack<P extends object>(t: TemplateRef<'slack', P>, ...args: MessageArgs<P>): this {\n return this.message('slack', t, args[0], captureLoc(this.slack));\n }\n discord<P extends object>(t: TemplateRef<'discord', P>, ...args: MessageArgs<P>): this {\n return this.message('discord', t, args[0], captureLoc(this.discord));\n }\n survey<P extends object>(t: TemplateRef<'survey', P>, ...args: MessageArgs<P>): this {\n return this.message('survey', t, args[0], captureLoc(this.survey));\n }\n\n prefer<const T extends PreferPair, const A extends PropsInput<NoInfer<RefProps<T[0]>>>>(\n candidates: T & SameProps<NoInfer<T>>,\n props: A & NoInfer<NoExtra<A, RefProps<T[0]>>>,\n options?: PreferOptions\n ): this {\n checkPreferArgs(candidates, props, options);\n // Every generated node points at the author's prefer() call, not at this method's body\n const loc = captureLoc(this.prefer);\n const meta = loc !== undefined ? { meta: { loc } } : {};\n const [first, second] = candidates as readonly TemplateRef<'push' | 'email', object>[];\n // A copy per message, as two separately written calls would hold\n const arm = (tpl: TemplateRef<'push' | 'email', object>): NodeIR[] => [\n { ...messageNode(tpl.channel, tpl, { ...props }), ...meta },\n ];\n return this.pushNode(\n {\n id: '',\n type: 'branch',\n ...(options?.label !== undefined ? { label: options.label } : {}),\n cases: [\n { condition: { type: 'reachable', channel: first.channel, ...meta }, flow: arm(first) },\n ],\n otherwise: arm(second),\n },\n loc\n );\n }\n\n delay(duration: Duration): this;\n delay(range: { min: Duration; max: Duration }): this;\n delay(d: Duration | { min: Duration; max: Duration }): this {\n const loc = captureLoc(this.delay);\n if (typeof d === 'string') {\n return this.pushNode({ id: '', type: 'delay', duration: durationIR(d) }, loc);\n }\n const min = durationIR(d.min);\n const max = durationIR(d.max);\n /*\n * The interpreter draws `min + random(max - min)` with the span clamped at\n * zero, so a backwards range does not fail — it quietly collapses into a\n * fixed `min` sleep. Reject it here, where the author can still see it.\n */\n if (max.ms < min.ms) {\n throw new Error(\n `delay(): 'max' must not be shorter than 'min', got { min: '${d.min}', max: '${d.max}' }`\n );\n }\n return this.pushNode({ id: '', type: 'random_delay', min, max }, loc);\n }\n\n timeWindow(opts: {\n days?: readonly Weekday[];\n between: readonly [TimeOfDay, TimeOfDay];\n tz?: 'user' | (string & {});\n }): this {\n const loc = captureLoc(this.timeWindow);\n const start = timeOfDayMinutes(opts.between[0]);\n const end = timeOfDayMinutes(opts.between[1]);\n if (end <= start) {\n throw new Error(\n `timeWindow(): 'between' must open before it closes within one day, got [${opts.between[0]}, ${opts.between[1]}] (overnight windows are not supported yet)`\n );\n }\n return this.pushNode(\n {\n id: '',\n type: 'time_window',\n days: [...(opts.days ?? (['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] as const))],\n between: [opts.between[0], opts.between[1]],\n tz: opts.tz ?? 'user',\n },\n loc\n );\n }\n\n waitUntil(\n condition: Condition,\n opts: { timeout: Duration; onTimeout?: 'continue' | 'exit' | SubFlow }\n ): this {\n const loc = captureLoc(this.waitUntil);\n const onTimeout = opts.onTimeout ?? 'continue';\n return this.pushNode(\n {\n id: '',\n type: 'wait_until',\n condition: condIR(condition),\n timeout: durationIR(opts.timeout),\n onTimeout:\n onTimeout === 'continue' || onTimeout === 'exit' ? onTimeout : resolveSubFlow(onTimeout),\n },\n loc\n );\n }\n\n use(fragment: SubFlow): this {\n // The fragment's nodes carry their own provenance (where the fragment was written)\n this.nodes.push(...resolveSubFlow(fragment));\n return this;\n }\n\n branch(...arms: readonly BranchArm[]): this;\n branch(label: string, ...arms: readonly BranchArm[]): this;\n branch(...args: readonly (string | BranchArm)[]): this {\n const loc = captureLoc(this.branch);\n const label = typeof args[0] === 'string' ? args[0] : undefined;\n const arms = (label === undefined ? args : args.slice(1)) as readonly BranchArm[];\n\n const cases: { condition: ConditionIR; flow: NodeIR[] }[] = [];\n let otherwise: NodeIR[] | undefined;\n arms.forEach((arm, i) => {\n if (Array.isArray(arm)) {\n if (otherwise !== undefined) {\n throw new Error('branch(): default arm (bare sub-flow) must be the last argument');\n }\n const [condition, sub] = arm as BranchCase;\n cases.push({ condition: condIR(condition), flow: resolveSubFlow(sub) });\n } else {\n if (otherwise !== undefined) {\n throw new Error('branch(): only one default arm (bare sub-flow) is allowed');\n }\n if (i !== arms.length - 1) {\n throw new Error('branch(): default arm (bare sub-flow) must be the last argument');\n }\n otherwise = resolveSubFlow(arm as SubFlow);\n }\n });\n\n return this.pushNode(\n {\n id: '',\n type: 'branch',\n ...(label !== undefined ? { label } : {}),\n cases,\n ...(otherwise !== undefined ? { otherwise } : {}),\n },\n loc\n );\n }\n\n filter(condition: Condition, opts?: { reason?: string }): this {\n return this.pushNode(\n {\n id: '',\n type: 'filter',\n condition: condIR(condition),\n ...(opts?.reason !== undefined ? { reason: opts.reason } : {}),\n },\n captureLoc(this.filter)\n );\n }\n\n cohort(arms: Record<string, { weight: number; flow?: SubFlow }>, opts?: { key?: string }): this {\n const loc = captureLoc(this.cohort);\n const entries = Object.entries(arms);\n /*\n * Weights live on a 0.01% grid: the runtime buckets into 10,000 slots\n * (basis points), so anything finer would be silently quantized — reject\n * it here instead. Converting to integer basis points also makes the\n * sum check exact; no epsilon needed.\n */\n const basisPoints = entries.map(([name, a]) => {\n if (!ARM_NAME.test(name)) {\n throw new Error(\n `cohort(): arm name '${name}' must start with a letter or '_' and contain only letters, digits, '_' or '-' — arm names become node id segments, and integer-like names would silently reorder the arms`\n );\n }\n const bp = a.weight * 100;\n if (Math.abs(bp - Math.round(bp)) > 1e-6) {\n throw new Error(\n `cohort(): arm '${name}' weight ${a.weight} is finer than the 0.01% bucketing grid — use at most two decimal places`\n );\n }\n /*\n * The sum check alone accepts -50 / 150: the runtime walks the arms\n * accumulating basis points and takes the first whose cumulative total\n * clears the bucket, so a negative weight hands 100% of users to its\n * neighbour instead of splitting anything.\n */\n if (bp < 0) {\n throw new Error(`cohort(): arm '${name}' weight ${a.weight} must not be negative`);\n }\n return Math.round(bp);\n });\n const total = basisPoints.reduce((sum, bp) => sum + bp, 0);\n if (total !== 10_000) {\n throw new Error(`cohort(): weights must sum to 100, got ${total / 100}`);\n }\n return this.pushNode(\n {\n id: '',\n type: 'cohort',\n ...(opts?.key !== undefined ? { key: opts.key } : {}),\n arms: entries.map(([name, a]) => ({\n name,\n weight: a.weight,\n flow: a.flow ? resolveSubFlow(a.flow) : [],\n })),\n },\n loc\n );\n }\n\n exit(reason?: string): this {\n return this.pushNode(\n { id: '', type: 'exit', ...(reason !== undefined ? { reason } : {}) },\n captureLoc(this.exit)\n );\n }\n\n sendEvent<P extends object>(ev: EventRef<P>, ...payload: EmitArgs<P>): this {\n const values = (payload[0] ?? {}) as Record<string, unknown>;\n for (const [field, value] of Object.entries(values)) {\n const ref =\n typeof value === 'object' &&\n value !== null &&\n (value as { type?: unknown }).type === 'user_property';\n if (!ref && !['string', 'number', 'boolean'].includes(typeof value)) {\n throw new TypeError(\n `sendEvent('${ev.name}'): '${field}' is not a scalar or a u.xxx reference — an emitted payload carries scalar fields only`\n );\n }\n }\n return this.pushNode(\n {\n id: '',\n type: 'send_event',\n event: ev.name,\n payload: values as Record<string, PropValueIR>,\n },\n captureLoc(this.sendEvent)\n );\n }\n\n run<A extends object>(a: ActionRef<A>, ...args: MessageArgs<A>): this {\n return this.pushNode(\n {\n id: '',\n type: 'action',\n action: a.name,\n args: (args[0] ?? {}) as Record<string, PropValueIR>,\n codeHash: a.codeHash,\n },\n captureLoc(this.run)\n );\n }\n}\n/* oxlint-enable typescript/unbound-method */\n\n/**\n * A reusable flow fragment: a free function. Once personalization goes through\n * u.xxx and events through e.xxx, the flow layer carries no schema generics.\n */\nexport function flow(build: (w: FlowBuilder) => FlowBuilder): Flow {\n const builder = new FlowBuilderImpl();\n build(builder);\n const impl: FlowInternal = { __flow: true, nodes: builder.nodes };\n return impl;\n}\n"],"mappings":"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"}
@@ -3,8 +3,8 @@ import { Duration, EventMap, TimeOfDay, UserPropertyBase, Weekday } from "./base
3
3
  import { EventRef, EventRefs, MessageArgs, PayloadRef, PayloadRefNode, PayloadRefs, PropInput, PropsInput, UserPropertyRef, UserRefs, event, user } from "./refs.cjs";
4
4
  import { Channel, EmptyProps, PropsOf, TemplateFactory, TemplateRef, TemplateRefTable, TemplateRefs, TemplateRegistry, template, templates } from "./template.cjs";
5
5
  import { Condition, PayloadWhere, PerformedOptions, SegmentDefinition, SegmentRef, and, between, contains, eq, exists, gt, gte, inArray, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, performed, reachable, segment } from "./condition.cjs";
6
- import { BranchArm, BranchCase, EmitArgs, EmitPayload, Flow, FlowBuilder, PushOptions, SubFlow, flow } from "./flow.cjs";
6
+ import { BranchArm, BranchCase, EmitArgs, EmitPayload, Flow, FlowBuilder, PreferOptions, PushOptions, SubFlow, flow } from "./flow.cjs";
7
7
  import { DateTime, EventTriggerOptions, TriggerFactory, TriggerRef, trigger } from "./trigger.cjs";
8
8
  import { GoalOptions, WorkflowBuilder, WorkflowDefinition, workflow } from "./workflow.cjs";
9
9
  import { compileBundle } from "./bundle.cjs";
10
- export { type ActionContext, type ActionHandler, type ActionRef, type BranchArm, type BranchCase, type Channel, type Condition, type DateTime, type Duration, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, type EventRef, type EventRefs, type EventTriggerOptions, type Flow, type FlowBuilder, type GoalOptions, type MessageArgs, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PropInput, type PropsInput, type PropsOf, type PushOptions, type SegmentDefinition, type SegmentRef, type SubFlow, type TemplateFactory, type TemplateRef, type TemplateRefTable, type TemplateRefs, type TemplateRegistry, type TimeOfDay, type TriggerFactory, type TriggerRef, type UserPropertyBase, type UserPropertyRef, type UserRefs, type Weekday, type WorkflowBuilder, type WorkflowDefinition, action, and, between, compileBundle, contains, eq, event, exists, flow, gt, gte, inArray, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, performed, reachable, segment, template, templates, trigger, user, workflow };
10
+ export { type ActionContext, type ActionHandler, type ActionRef, type BranchArm, type BranchCase, type Channel, type Condition, type DateTime, type Duration, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, type EventRef, type EventRefs, type EventTriggerOptions, type Flow, type FlowBuilder, type GoalOptions, type MessageArgs, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, type PropInput, type PropsInput, type PropsOf, type PushOptions, type SegmentDefinition, type SegmentRef, type SubFlow, type TemplateFactory, type TemplateRef, type TemplateRefTable, type TemplateRefs, type TemplateRegistry, type TimeOfDay, type TriggerFactory, type TriggerRef, type UserPropertyBase, type UserPropertyRef, type UserRefs, type Weekday, type WorkflowBuilder, type WorkflowDefinition, action, and, between, compileBundle, contains, eq, event, exists, flow, gt, gte, inArray, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, performed, reachable, segment, template, templates, trigger, user, workflow };
@@ -3,8 +3,8 @@ import { Duration, EventMap, TimeOfDay, UserPropertyBase, Weekday } from "./base
3
3
  import { EventRef, EventRefs, MessageArgs, PayloadRef, PayloadRefNode, PayloadRefs, PropInput, PropsInput, UserPropertyRef, UserRefs, event, user } from "./refs.mjs";
4
4
  import { Channel, EmptyProps, PropsOf, TemplateFactory, TemplateRef, TemplateRefTable, TemplateRefs, TemplateRegistry, template, templates } from "./template.mjs";
5
5
  import { Condition, PayloadWhere, PerformedOptions, SegmentDefinition, SegmentRef, and, between, contains, eq, exists, gt, gte, inArray, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, performed, reachable, segment } from "./condition.mjs";
6
- import { BranchArm, BranchCase, EmitArgs, EmitPayload, Flow, FlowBuilder, PushOptions, SubFlow, flow } from "./flow.mjs";
6
+ import { BranchArm, BranchCase, EmitArgs, EmitPayload, Flow, FlowBuilder, PreferOptions, PushOptions, SubFlow, flow } from "./flow.mjs";
7
7
  import { DateTime, EventTriggerOptions, TriggerFactory, TriggerRef, trigger } from "./trigger.mjs";
8
8
  import { GoalOptions, WorkflowBuilder, WorkflowDefinition, workflow } from "./workflow.mjs";
9
9
  import { compileBundle } from "./bundle.mjs";
10
- export { type ActionContext, type ActionHandler, type ActionRef, type BranchArm, type BranchCase, type Channel, type Condition, type DateTime, type Duration, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, type EventRef, type EventRefs, type EventTriggerOptions, type Flow, type FlowBuilder, type GoalOptions, type MessageArgs, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PropInput, type PropsInput, type PropsOf, type PushOptions, type SegmentDefinition, type SegmentRef, type SubFlow, type TemplateFactory, type TemplateRef, type TemplateRefTable, type TemplateRefs, type TemplateRegistry, type TimeOfDay, type TriggerFactory, type TriggerRef, type UserPropertyBase, type UserPropertyRef, type UserRefs, type Weekday, type WorkflowBuilder, type WorkflowDefinition, action, and, between, compileBundle, contains, eq, event, exists, flow, gt, gte, inArray, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, performed, reachable, segment, template, templates, trigger, user, workflow };
10
+ export { type ActionContext, type ActionHandler, type ActionRef, type BranchArm, type BranchCase, type Channel, type Condition, type DateTime, type Duration, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, type EventRef, type EventRefs, type EventTriggerOptions, type Flow, type FlowBuilder, type GoalOptions, type MessageArgs, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, type PropInput, type PropsInput, type PropsOf, type PushOptions, type SegmentDefinition, type SegmentRef, type SubFlow, type TemplateFactory, type TemplateRef, type TemplateRefTable, type TemplateRefs, type TemplateRegistry, type TimeOfDay, type TriggerFactory, type TriggerRef, type UserPropertyBase, type UserPropertyRef, type UserRefs, type Weekday, type WorkflowBuilder, type WorkflowDefinition, action, and, between, compileBundle, contains, eq, event, exists, flow, gt, gte, inArray, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, performed, reachable, segment, template, templates, trigger, user, workflow };
package/dist/index.d.cts CHANGED
@@ -5,7 +5,7 @@ import { Duration, EventMap, TimeOfDay, UserPropertyBase, Weekday } from "./dsl/
5
5
  import { EventRef, EventRefs, MessageArgs, PayloadRef, PayloadRefNode, PayloadRefs, PropInput, PropsInput, UserPropertyRef, UserRefs, event, user } from "./dsl/refs.cjs";
6
6
  import { Channel, EmptyProps, PropsOf, TemplateFactory, TemplateRef, TemplateRefTable, TemplateRefs, TemplateRegistry, template, templates } from "./dsl/template.cjs";
7
7
  import { Condition, PayloadWhere, PerformedOptions, SegmentDefinition, SegmentRef, and, between, contains, eq, exists, gt, gte, inArray, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, performed, reachable, segment } from "./dsl/condition.cjs";
8
- import { BranchArm, BranchCase, EmitArgs, EmitPayload, Flow, FlowBuilder, PushOptions, SubFlow, flow } from "./dsl/flow.cjs";
8
+ import { BranchArm, BranchCase, EmitArgs, EmitPayload, Flow, FlowBuilder, PreferOptions, PushOptions, SubFlow, flow } from "./dsl/flow.cjs";
9
9
  import { DateTime, EventTriggerOptions, TriggerFactory, TriggerRef, trigger } from "./dsl/trigger.cjs";
10
10
  import { GoalOptions, WorkflowBuilder, WorkflowDefinition, workflow } from "./dsl/workflow.cjs";
11
11
  import { compileBundle } from "./dsl/bundle.cjs";
@@ -15,4 +15,4 @@ import { JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, resolveRuntim
15
15
  import { ApnsConfig, ConfigCompileError, DatabaseConfig, EmailAddress, EmailConfig, FcmConfig, JourneyConfig, JourneyConfigDefinition, WebPushConfig, apnsEnvironment, compileConfig, defineConfig, formatEmailAddress, isConfig } from "./config.cjs";
16
16
  import { canonicalJSON, fullHash, semanticHash, sha256Hex, stripMeta, stripProvenance } from "./hash.cjs";
17
17
  import { BundlePlan, ChangeStatus, NodeChange, ResourceChange, WorkflowChange, isSemanticChange, plan } from "./plan.cjs";
18
- export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, ChangeStatus, type Channel, ChannelIR, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailTemplate, EmailTemplateDefinition, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateDefinition, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, type TemplateRefTable, type TemplateRefs, type TemplateRegistry, TemplateValidationError, type TimeOfDay, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertSupportedSchema, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, templates, trigger, user, workflow };
18
+ export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, ChangeStatus, type Channel, ChannelIR, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailTemplate, EmailTemplateDefinition, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateDefinition, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, type TemplateRefTable, type TemplateRefs, type TemplateRegistry, TemplateValidationError, type TimeOfDay, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertSupportedSchema, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, templates, trigger, user, workflow };
package/dist/index.d.mts CHANGED
@@ -5,7 +5,7 @@ import { Duration, EventMap, TimeOfDay, UserPropertyBase, Weekday } from "./dsl/
5
5
  import { EventRef, EventRefs, MessageArgs, PayloadRef, PayloadRefNode, PayloadRefs, PropInput, PropsInput, UserPropertyRef, UserRefs, event, user } from "./dsl/refs.mjs";
6
6
  import { Channel, EmptyProps, PropsOf, TemplateFactory, TemplateRef, TemplateRefTable, TemplateRefs, TemplateRegistry, template, templates } from "./dsl/template.mjs";
7
7
  import { Condition, PayloadWhere, PerformedOptions, SegmentDefinition, SegmentRef, and, between, contains, eq, exists, gt, gte, inArray, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, performed, reachable, segment } from "./dsl/condition.mjs";
8
- import { BranchArm, BranchCase, EmitArgs, EmitPayload, Flow, FlowBuilder, PushOptions, SubFlow, flow } from "./dsl/flow.mjs";
8
+ import { BranchArm, BranchCase, EmitArgs, EmitPayload, Flow, FlowBuilder, PreferOptions, PushOptions, SubFlow, flow } from "./dsl/flow.mjs";
9
9
  import { DateTime, EventTriggerOptions, TriggerFactory, TriggerRef, trigger } from "./dsl/trigger.mjs";
10
10
  import { GoalOptions, WorkflowBuilder, WorkflowDefinition, workflow } from "./dsl/workflow.mjs";
11
11
  import { compileBundle } from "./dsl/bundle.mjs";
@@ -15,4 +15,4 @@ import { JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, resolveRuntim
15
15
  import { ApnsConfig, ConfigCompileError, DatabaseConfig, EmailAddress, EmailConfig, FcmConfig, JourneyConfig, JourneyConfigDefinition, WebPushConfig, apnsEnvironment, compileConfig, defineConfig, formatEmailAddress, isConfig } from "./config.mjs";
16
16
  import { canonicalJSON, fullHash, semanticHash, sha256Hex, stripMeta, stripProvenance } from "./hash.mjs";
17
17
  import { BundlePlan, ChangeStatus, NodeChange, ResourceChange, WorkflowChange, isSemanticChange, plan } from "./plan.mjs";
18
- export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, ChangeStatus, type Channel, ChannelIR, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailTemplate, EmailTemplateDefinition, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateDefinition, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, type TemplateRefTable, type TemplateRefs, type TemplateRegistry, TemplateValidationError, type TimeOfDay, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertSupportedSchema, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, templates, trigger, user, workflow };
18
+ export { type ActionContext, type ActionHandler, ActionIR, type ActionRef, AnyTemplate, ApnsConfig, type BranchArm, type BranchCase, BundleIR, BundlePlan, ChangeStatus, type Channel, ChannelIR, type Condition, ConditionIR, ConfigCompileError, Contract, ContractDefinition, ContractIssue, ContractResult, ContractValidationError, DatabaseConfig, type DateTime, DeclaredOutput, DiscordContent, DiscordTemplate, type Duration, DurationIR, EmailAddress, EmailConfig, EmailTemplate, EmailTemplateDefinition, type EmitArgs, type EmitPayload, type EmptyProps, type EventMap, EventPayload, type EventRef, type EventRefs, type EventTriggerOptions, FcmConfig, type Flow, type FlowBuilder, GoalIR, type GoalOptions, IR_VERSION, JourneyConfig, JourneyConfigDefinition, JourneyMode, JourneyRuntimeConfig, JourneyRuntimeOptions, type MessageArgs, MetaIR, NodeChange, NodeIR, NodeMetaIR, type PayloadRef, type PayloadRefNode, type PayloadRefs, type PayloadWhere, type PerformedOptions, type PreferOptions, ProfilePatch, type PropInput, PropValueIR, PropertyOperatorIR, type PropsInput, type PropsOf, PropsSchema, PushContent, type PushOptions, PushTemplate, PushTemplateDefinition, ResolvedProps, ResourceChange, ScalarIR, type SegmentDefinition, SegmentIR, type SegmentRef, SlackContent, SlackTemplate, SourceLocIR, type SubFlow, TemplateChannel, type TemplateFactory, TemplateIR, TemplateIssue, type TemplateRef, type TemplateRefTable, type TemplateRefs, type TemplateRegistry, TemplateValidationError, type TimeOfDay, type TriggerFactory, TriggerIR, type TriggerRef, type UserPropertyBase, type UserPropertyRef, UserPropertyRefIR, type UserRefs, WebPushConfig, WebhookTemplateDefinition, type Weekday, WeekdayIR, type WorkflowBuilder, WorkflowChange, type WorkflowDefinition, WorkflowIR, action, and, apnsEnvironment, assertSupportedSchema, between, canonicalJSON, checkEvent, compileBundle, compileConfig, contains, defineConfig, defineContract, discordTemplate, emailTemplate, eq, event, exists, flow, formatEmailAddress, fullHash, gt, gte, inArray, isConfig, isContract, isDiscordTemplate, isEmailTemplate, isPushTemplate, isSemanticChange, isSlackTemplate, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, parseTemplateProps, performed, plan, pushTemplate, reachable, registryValidator, renderDiscordContent, renderPushContent, renderSlackContent, resolveRuntime, segment, semanticHash, sha256Hex, slackTemplate, stripMeta, stripProvenance, template, templates, trigger, user, workflow };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shware/workflow",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/ShwareHQ/shware-sdk.git"