@shware/workflow 9.0.1 → 9.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine/time-scale.cjs +33 -11
- package/dist/engine/time-scale.cjs.map +1 -1
- package/dist/engine/time-scale.d.cts +12 -6
- package/dist/engine/time-scale.d.cts.map +1 -1
- package/dist/engine/time-scale.d.mts +12 -6
- package/dist/engine/time-scale.d.mts.map +1 -1
- package/dist/engine/time-scale.mjs +33 -11
- package/dist/engine/time-scale.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -10,8 +10,10 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
10
10
|
* timeouts, goal and `performed(... within)` windows inside the workflow), so
|
|
11
11
|
* the journey keeps its shape, just faster. What does not scale: wall-clock
|
|
12
12
|
* `timeWindow`s (they are times of day, not durations). Segment definitions
|
|
13
|
-
* live in the store rather than the workflow body;
|
|
14
|
-
* scales them as they are read
|
|
13
|
+
* and trigger routes live in the store rather than the workflow body;
|
|
14
|
+
* `scaleSegmentWindows` scales them as they are read — one segment for an
|
|
15
|
+
* event-driven evaluation, all of them for the scheduled sweep, a route's
|
|
16
|
+
* profile gate at ingest — so a segment's `within` keeps pace everywhere.
|
|
15
17
|
*
|
|
16
18
|
* The walk follows the IR's own structure — the fields that hold a Duration
|
|
17
19
|
* are known — so a message prop or an event payload that happens to look like
|
|
@@ -105,20 +107,40 @@ function scale(duration, factor) {
|
|
|
105
107
|
};
|
|
106
108
|
}
|
|
107
109
|
/**
|
|
108
|
-
* A store whose segment definitions come back scaled by `factor` — what
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
110
|
+
* A store whose segment definitions come back scaled by `factor` — what the journey host leases
|
|
111
|
+
* in fast-forward, so a segment's windows (`performed(e.login, { within: '7 days' })`) shrink
|
|
112
|
+
* with the journeys that reference it, and with the segment triggers it routes. Every read that
|
|
113
|
+
* hands out a stored condition is scaled: `getSegmentCondition` (event-driven evaluation),
|
|
114
|
+
* `listSegments` (the scheduled sweep, which narrows candidates in SQL and evaluates with those
|
|
115
|
+
* definitions) and `findTriggers` (the profile gate an event trigger checks before it starts a
|
|
116
|
+
* journey, which `scaleDurations` scales inside the journey too). Every other method is the
|
|
117
|
+
* store's own.
|
|
112
118
|
*/
|
|
113
119
|
function scaleSegmentWindows(store, factor) {
|
|
114
120
|
if (!(factor > 0) || factor === 1) return store;
|
|
115
121
|
return new Proxy(store, { get(target, property) {
|
|
116
|
-
if (property === "getSegmentCondition") return async (name) => {
|
|
117
|
-
const condition = await target.getSegmentCondition(name);
|
|
118
|
-
return condition === void 0 ? void 0 : scaleCondition(condition, factor);
|
|
119
|
-
};
|
|
120
122
|
const value = Reflect.get(target, property, target);
|
|
121
|
-
|
|
123
|
+
if (typeof value !== "function") return value;
|
|
124
|
+
switch (property) {
|
|
125
|
+
case "getSegmentCondition": return async (name) => {
|
|
126
|
+
const condition = await target.getSegmentCondition(name);
|
|
127
|
+
return condition === void 0 ? void 0 : scaleCondition(condition, factor);
|
|
128
|
+
};
|
|
129
|
+
case "listSegments": return async () => {
|
|
130
|
+
return (await value.call(target)).map(({ id, condition }) => ({
|
|
131
|
+
id,
|
|
132
|
+
condition: scaleCondition(condition, factor)
|
|
133
|
+
}));
|
|
134
|
+
};
|
|
135
|
+
case "findTriggers": return async (event) => {
|
|
136
|
+
const scaled = (route) => ({
|
|
137
|
+
...route,
|
|
138
|
+
filter: route.filter === null ? null : scaleCondition(route.filter, factor)
|
|
139
|
+
});
|
|
140
|
+
return (await value.call(target, event)).map(scaled);
|
|
141
|
+
};
|
|
142
|
+
default: return value.bind(target);
|
|
143
|
+
}
|
|
122
144
|
} });
|
|
123
145
|
}
|
|
124
146
|
exports.scaleCondition = scaleCondition;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-scale.cjs","names":[],"sources":["../../src/engine/time-scale.ts"],"sourcesContent":["import type { ConditionIR, DurationIR, NodeIR, WorkflowIR } from '../ir';\n\n/**\n * Debug time scale: divide every duration in a workflow's IR by a factor so a\n * journey written in days plays out in seconds — with a factor of 3600, a\n * `delay('2 days')` sleeps 48 s, a `waitUntil(..., { timeout: '3 days' })`\n * gives up after 72 s, and a goal `within: '30 days'` closes after 12 min.\n *\n * Everything that is a Duration scales together (delays, random delays, wait\n * timeouts, goal and `performed(... within)` windows inside the workflow), so\n * the journey keeps its shape, just faster. What does not scale: wall-clock\n * `timeWindow`s (they are times of day, not durations). Segment definitions\n * live in the store rather than the workflow body
|
|
1
|
+
{"version":3,"file":"time-scale.cjs","names":[],"sources":["../../src/engine/time-scale.ts"],"sourcesContent":["import type { ConditionIR, DurationIR, NodeIR, WorkflowIR } from '../ir';\n\n/**\n * Debug time scale: divide every duration in a workflow's IR by a factor so a\n * journey written in days plays out in seconds — with a factor of 3600, a\n * `delay('2 days')` sleeps 48 s, a `waitUntil(..., { timeout: '3 days' })`\n * gives up after 72 s, and a goal `within: '30 days'` closes after 12 min.\n *\n * Everything that is a Duration scales together (delays, random delays, wait\n * timeouts, goal and `performed(... within)` windows inside the workflow), so\n * the journey keeps its shape, just faster. What does not scale: wall-clock\n * `timeWindow`s (they are times of day, not durations). Segment definitions\n * and trigger routes live in the store rather than the workflow body;\n * `scaleSegmentWindows` scales them as they are read — one segment for an\n * event-driven evaluation, all of them for the scheduled sweep, a route's\n * profile gate at ingest — so a segment's `within` keeps pace everywhere.\n *\n * The walk follows the IR's own structure — the fields that hold a Duration\n * are known — so a message prop or an event payload that happens to look like\n * one (`{ value, ms }`) is left alone. The source text is kept so the studio\n * still shows what was written.\n */\nexport function scaleDurations(ir: WorkflowIR, factor: number): WorkflowIR {\n if (!(factor > 0) || factor === 1) return ir;\n return {\n ...ir,\n trigger:\n ir.trigger.type === 'event'\n ? {\n ...ir.trigger,\n ...(ir.trigger.filter === undefined\n ? {}\n : { filter: scaleCondition(ir.trigger.filter, factor) }),\n }\n : ir.trigger,\n ...(ir.goal === undefined\n ? {}\n : {\n goal: {\n ...ir.goal,\n condition: scaleCondition(ir.goal.condition, factor),\n ...(ir.goal.within === undefined ? {} : { within: scale(ir.goal.within, factor) }),\n },\n }),\n ...(ir.exitWhen === undefined ? {} : { exitWhen: scaleCondition(ir.exitWhen, factor) }),\n flow: scaleFlow(ir.flow, factor),\n };\n}\n\n/** A condition with its `performed(... within)` windows divided by `factor`. */\nexport function scaleCondition(condition: ConditionIR, factor: number): ConditionIR {\n if (!(factor > 0) || factor === 1) return condition;\n switch (condition.type) {\n case 'and':\n case 'or':\n return {\n ...condition,\n conditions: condition.conditions.map((child) => scaleCondition(child, factor)),\n };\n case 'not':\n return { ...condition, condition: scaleCondition(condition.condition, factor) };\n case 'performed':\n return condition.within === undefined\n ? condition\n : { ...condition, within: scale(condition.within, factor) };\n default:\n return condition;\n }\n}\n\nfunction scaleFlow(nodes: readonly NodeIR[], factor: number): NodeIR[] {\n return nodes.map((node) => scaleNode(node, factor));\n}\n\nfunction scaleNode(node: NodeIR, factor: number): NodeIR {\n switch (node.type) {\n case 'delay':\n return { ...node, duration: scale(node.duration, factor) };\n case 'random_delay':\n return { ...node, min: scale(node.min, factor), max: scale(node.max, factor) };\n case 'wait_until':\n return {\n ...node,\n condition: scaleCondition(node.condition, factor),\n timeout: scale(node.timeout, factor),\n onTimeout: Array.isArray(node.onTimeout)\n ? scaleFlow(node.onTimeout, factor)\n : node.onTimeout,\n };\n case 'branch':\n return {\n ...node,\n cases: node.cases.map((branch) => ({\n ...branch,\n condition: scaleCondition(branch.condition, factor),\n flow: scaleFlow(branch.flow, factor),\n })),\n ...(node.otherwise === undefined ? {} : { otherwise: scaleFlow(node.otherwise, factor) }),\n };\n case 'filter':\n return { ...node, condition: scaleCondition(node.condition, factor) };\n case 'cohort':\n return {\n ...node,\n arms: node.arms.map((arm) => ({ ...arm, flow: scaleFlow(arm.flow, factor) })),\n };\n default:\n return node;\n }\n}\n\nfunction scale(duration: DurationIR, factor: number): DurationIR {\n return { value: duration.value, ms: Math.max(0, Math.round(duration.ms / factor)) };\n}\n\n/**\n * A store whose segment definitions come back scaled by `factor` — what the journey host leases\n * in fast-forward, so a segment's windows (`performed(e.login, { within: '7 days' })`) shrink\n * with the journeys that reference it, and with the segment triggers it routes. Every read that\n * hands out a stored condition is scaled: `getSegmentCondition` (event-driven evaluation),\n * `listSegments` (the scheduled sweep, which narrows candidates in SQL and evaluates with those\n * definitions) and `findTriggers` (the profile gate an event trigger checks before it starts a\n * journey, which `scaleDurations` scales inside the journey too). Every other method is the\n * store's own.\n */\nexport function scaleSegmentWindows<\n S extends { getSegmentCondition(name: string): Promise<ConditionIR | undefined> },\n>(store: S, factor: number): S {\n if (!(factor > 0) || factor === 1) return store;\n return new Proxy(store, {\n get(target, property) {\n const value: unknown = Reflect.get(target, property, target);\n if (typeof value !== 'function') return value;\n switch (property) {\n case 'getSegmentCondition':\n return async (name: string) => {\n const condition = await target.getSegmentCondition(name);\n return condition === undefined ? undefined : scaleCondition(condition, factor);\n };\n case 'listSegments':\n return async () => {\n const segments = (await value.call(target)) as { id: string; condition: ConditionIR }[];\n return segments.map(({ id, condition }) => ({\n id,\n condition: scaleCondition(condition, factor),\n }));\n };\n case 'findTriggers':\n return async (event: string) => {\n // Only the profile gate: `where` is payload-only, so it holds no window to scale\n const scaled = (route: { filter: ConditionIR | null }) => ({\n ...route,\n filter: route.filter === null ? null : scaleCondition(route.filter, factor),\n });\n return ((await value.call(target, event)) as { filter: ConditionIR | null }[]).map(\n scaled\n );\n };\n default:\n return value.bind(target);\n }\n },\n });\n}\n"],"mappings":"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"}
|
|
@@ -9,8 +9,10 @@ import { ConditionIR, WorkflowIR } from "../ir.cjs";
|
|
|
9
9
|
* timeouts, goal and `performed(... within)` windows inside the workflow), so
|
|
10
10
|
* the journey keeps its shape, just faster. What does not scale: wall-clock
|
|
11
11
|
* `timeWindow`s (they are times of day, not durations). Segment definitions
|
|
12
|
-
* live in the store rather than the workflow body;
|
|
13
|
-
* scales them as they are read
|
|
12
|
+
* and trigger routes live in the store rather than the workflow body;
|
|
13
|
+
* `scaleSegmentWindows` scales them as they are read — one segment for an
|
|
14
|
+
* event-driven evaluation, all of them for the scheduled sweep, a route's
|
|
15
|
+
* profile gate at ingest — so a segment's `within` keeps pace everywhere.
|
|
14
16
|
*
|
|
15
17
|
* The walk follows the IR's own structure — the fields that hold a Duration
|
|
16
18
|
* are known — so a message prop or an event payload that happens to look like
|
|
@@ -21,10 +23,14 @@ export declare function scaleDurations(ir: WorkflowIR, factor: number): Workflow
|
|
|
21
23
|
/** A condition with its `performed(... within)` windows divided by `factor`. */
|
|
22
24
|
export declare function scaleCondition(condition: ConditionIR, factor: number): ConditionIR;
|
|
23
25
|
/**
|
|
24
|
-
* A store whose segment definitions come back scaled by `factor` — what
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* A store whose segment definitions come back scaled by `factor` — what the journey host leases
|
|
27
|
+
* in fast-forward, so a segment's windows (`performed(e.login, { within: '7 days' })`) shrink
|
|
28
|
+
* with the journeys that reference it, and with the segment triggers it routes. Every read that
|
|
29
|
+
* hands out a stored condition is scaled: `getSegmentCondition` (event-driven evaluation),
|
|
30
|
+
* `listSegments` (the scheduled sweep, which narrows candidates in SQL and evaluates with those
|
|
31
|
+
* definitions) and `findTriggers` (the profile gate an event trigger checks before it starts a
|
|
32
|
+
* journey, which `scaleDurations` scales inside the journey too). Every other method is the
|
|
33
|
+
* store's own.
|
|
28
34
|
*/
|
|
29
35
|
export declare function scaleSegmentWindows<S extends {
|
|
30
36
|
getSegmentCondition(name: string): Promise<ConditionIR | undefined>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-scale.d.cts","names":[],"sources":["../../src/engine/time-scale.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"time-scale.d.cts","names":[],"sources":["../../src/engine/time-scale.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;wBAsBgB,eAAe,IAAI,YAAY,iBAAiB;;wBA4BhD,eAAe,WAAW,aAAa,iBAAiB;;;;;;;;;;;wBA2ExD,oBACd;EAAY,oBAAoB,eAAe,QAAQ;GACvD,OAAO,GAAG,iBAAiB"}
|
|
@@ -9,8 +9,10 @@ import { ConditionIR, WorkflowIR } from "../ir.mjs";
|
|
|
9
9
|
* timeouts, goal and `performed(... within)` windows inside the workflow), so
|
|
10
10
|
* the journey keeps its shape, just faster. What does not scale: wall-clock
|
|
11
11
|
* `timeWindow`s (they are times of day, not durations). Segment definitions
|
|
12
|
-
* live in the store rather than the workflow body;
|
|
13
|
-
* scales them as they are read
|
|
12
|
+
* and trigger routes live in the store rather than the workflow body;
|
|
13
|
+
* `scaleSegmentWindows` scales them as they are read — one segment for an
|
|
14
|
+
* event-driven evaluation, all of them for the scheduled sweep, a route's
|
|
15
|
+
* profile gate at ingest — so a segment's `within` keeps pace everywhere.
|
|
14
16
|
*
|
|
15
17
|
* The walk follows the IR's own structure — the fields that hold a Duration
|
|
16
18
|
* are known — so a message prop or an event payload that happens to look like
|
|
@@ -21,10 +23,14 @@ export declare function scaleDurations(ir: WorkflowIR, factor: number): Workflow
|
|
|
21
23
|
/** A condition with its `performed(... within)` windows divided by `factor`. */
|
|
22
24
|
export declare function scaleCondition(condition: ConditionIR, factor: number): ConditionIR;
|
|
23
25
|
/**
|
|
24
|
-
* A store whose segment definitions come back scaled by `factor` — what
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* A store whose segment definitions come back scaled by `factor` — what the journey host leases
|
|
27
|
+
* in fast-forward, so a segment's windows (`performed(e.login, { within: '7 days' })`) shrink
|
|
28
|
+
* with the journeys that reference it, and with the segment triggers it routes. Every read that
|
|
29
|
+
* hands out a stored condition is scaled: `getSegmentCondition` (event-driven evaluation),
|
|
30
|
+
* `listSegments` (the scheduled sweep, which narrows candidates in SQL and evaluates with those
|
|
31
|
+
* definitions) and `findTriggers` (the profile gate an event trigger checks before it starts a
|
|
32
|
+
* journey, which `scaleDurations` scales inside the journey too). Every other method is the
|
|
33
|
+
* store's own.
|
|
28
34
|
*/
|
|
29
35
|
export declare function scaleSegmentWindows<S extends {
|
|
30
36
|
getSegmentCondition(name: string): Promise<ConditionIR | undefined>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-scale.d.mts","names":[],"sources":["../../src/engine/time-scale.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"time-scale.d.mts","names":[],"sources":["../../src/engine/time-scale.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;wBAsBgB,eAAe,IAAI,YAAY,iBAAiB;;wBA4BhD,eAAe,WAAW,aAAa,iBAAiB;;;;;;;;;;;wBA2ExD,oBACd;EAAY,oBAAoB,eAAe,QAAQ;GACvD,OAAO,GAAG,iBAAiB"}
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
* timeouts, goal and `performed(... within)` windows inside the workflow), so
|
|
10
10
|
* the journey keeps its shape, just faster. What does not scale: wall-clock
|
|
11
11
|
* `timeWindow`s (they are times of day, not durations). Segment definitions
|
|
12
|
-
* live in the store rather than the workflow body;
|
|
13
|
-
* scales them as they are read
|
|
12
|
+
* and trigger routes live in the store rather than the workflow body;
|
|
13
|
+
* `scaleSegmentWindows` scales them as they are read — one segment for an
|
|
14
|
+
* event-driven evaluation, all of them for the scheduled sweep, a route's
|
|
15
|
+
* profile gate at ingest — so a segment's `within` keeps pace everywhere.
|
|
14
16
|
*
|
|
15
17
|
* The walk follows the IR's own structure — the fields that hold a Duration
|
|
16
18
|
* are known — so a message prop or an event payload that happens to look like
|
|
@@ -104,20 +106,40 @@ function scale(duration, factor) {
|
|
|
104
106
|
};
|
|
105
107
|
}
|
|
106
108
|
/**
|
|
107
|
-
* A store whose segment definitions come back scaled by `factor` — what
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
109
|
+
* A store whose segment definitions come back scaled by `factor` — what the journey host leases
|
|
110
|
+
* in fast-forward, so a segment's windows (`performed(e.login, { within: '7 days' })`) shrink
|
|
111
|
+
* with the journeys that reference it, and with the segment triggers it routes. Every read that
|
|
112
|
+
* hands out a stored condition is scaled: `getSegmentCondition` (event-driven evaluation),
|
|
113
|
+
* `listSegments` (the scheduled sweep, which narrows candidates in SQL and evaluates with those
|
|
114
|
+
* definitions) and `findTriggers` (the profile gate an event trigger checks before it starts a
|
|
115
|
+
* journey, which `scaleDurations` scales inside the journey too). Every other method is the
|
|
116
|
+
* store's own.
|
|
111
117
|
*/
|
|
112
118
|
function scaleSegmentWindows(store, factor) {
|
|
113
119
|
if (!(factor > 0) || factor === 1) return store;
|
|
114
120
|
return new Proxy(store, { get(target, property) {
|
|
115
|
-
if (property === "getSegmentCondition") return async (name) => {
|
|
116
|
-
const condition = await target.getSegmentCondition(name);
|
|
117
|
-
return condition === void 0 ? void 0 : scaleCondition(condition, factor);
|
|
118
|
-
};
|
|
119
121
|
const value = Reflect.get(target, property, target);
|
|
120
|
-
|
|
122
|
+
if (typeof value !== "function") return value;
|
|
123
|
+
switch (property) {
|
|
124
|
+
case "getSegmentCondition": return async (name) => {
|
|
125
|
+
const condition = await target.getSegmentCondition(name);
|
|
126
|
+
return condition === void 0 ? void 0 : scaleCondition(condition, factor);
|
|
127
|
+
};
|
|
128
|
+
case "listSegments": return async () => {
|
|
129
|
+
return (await value.call(target)).map(({ id, condition }) => ({
|
|
130
|
+
id,
|
|
131
|
+
condition: scaleCondition(condition, factor)
|
|
132
|
+
}));
|
|
133
|
+
};
|
|
134
|
+
case "findTriggers": return async (event) => {
|
|
135
|
+
const scaled = (route) => ({
|
|
136
|
+
...route,
|
|
137
|
+
filter: route.filter === null ? null : scaleCondition(route.filter, factor)
|
|
138
|
+
});
|
|
139
|
+
return (await value.call(target, event)).map(scaled);
|
|
140
|
+
};
|
|
141
|
+
default: return value.bind(target);
|
|
142
|
+
}
|
|
121
143
|
} });
|
|
122
144
|
}
|
|
123
145
|
export { scaleCondition, scaleDurations, scaleSegmentWindows };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-scale.mjs","names":[],"sources":["../../src/engine/time-scale.ts"],"sourcesContent":["import type { ConditionIR, DurationIR, NodeIR, WorkflowIR } from '../ir';\n\n/**\n * Debug time scale: divide every duration in a workflow's IR by a factor so a\n * journey written in days plays out in seconds — with a factor of 3600, a\n * `delay('2 days')` sleeps 48 s, a `waitUntil(..., { timeout: '3 days' })`\n * gives up after 72 s, and a goal `within: '30 days'` closes after 12 min.\n *\n * Everything that is a Duration scales together (delays, random delays, wait\n * timeouts, goal and `performed(... within)` windows inside the workflow), so\n * the journey keeps its shape, just faster. What does not scale: wall-clock\n * `timeWindow`s (they are times of day, not durations). Segment definitions\n * live in the store rather than the workflow body
|
|
1
|
+
{"version":3,"file":"time-scale.mjs","names":[],"sources":["../../src/engine/time-scale.ts"],"sourcesContent":["import type { ConditionIR, DurationIR, NodeIR, WorkflowIR } from '../ir';\n\n/**\n * Debug time scale: divide every duration in a workflow's IR by a factor so a\n * journey written in days plays out in seconds — with a factor of 3600, a\n * `delay('2 days')` sleeps 48 s, a `waitUntil(..., { timeout: '3 days' })`\n * gives up after 72 s, and a goal `within: '30 days'` closes after 12 min.\n *\n * Everything that is a Duration scales together (delays, random delays, wait\n * timeouts, goal and `performed(... within)` windows inside the workflow), so\n * the journey keeps its shape, just faster. What does not scale: wall-clock\n * `timeWindow`s (they are times of day, not durations). Segment definitions\n * and trigger routes live in the store rather than the workflow body;\n * `scaleSegmentWindows` scales them as they are read — one segment for an\n * event-driven evaluation, all of them for the scheduled sweep, a route's\n * profile gate at ingest — so a segment's `within` keeps pace everywhere.\n *\n * The walk follows the IR's own structure — the fields that hold a Duration\n * are known — so a message prop or an event payload that happens to look like\n * one (`{ value, ms }`) is left alone. The source text is kept so the studio\n * still shows what was written.\n */\nexport function scaleDurations(ir: WorkflowIR, factor: number): WorkflowIR {\n if (!(factor > 0) || factor === 1) return ir;\n return {\n ...ir,\n trigger:\n ir.trigger.type === 'event'\n ? {\n ...ir.trigger,\n ...(ir.trigger.filter === undefined\n ? {}\n : { filter: scaleCondition(ir.trigger.filter, factor) }),\n }\n : ir.trigger,\n ...(ir.goal === undefined\n ? {}\n : {\n goal: {\n ...ir.goal,\n condition: scaleCondition(ir.goal.condition, factor),\n ...(ir.goal.within === undefined ? {} : { within: scale(ir.goal.within, factor) }),\n },\n }),\n ...(ir.exitWhen === undefined ? {} : { exitWhen: scaleCondition(ir.exitWhen, factor) }),\n flow: scaleFlow(ir.flow, factor),\n };\n}\n\n/** A condition with its `performed(... within)` windows divided by `factor`. */\nexport function scaleCondition(condition: ConditionIR, factor: number): ConditionIR {\n if (!(factor > 0) || factor === 1) return condition;\n switch (condition.type) {\n case 'and':\n case 'or':\n return {\n ...condition,\n conditions: condition.conditions.map((child) => scaleCondition(child, factor)),\n };\n case 'not':\n return { ...condition, condition: scaleCondition(condition.condition, factor) };\n case 'performed':\n return condition.within === undefined\n ? condition\n : { ...condition, within: scale(condition.within, factor) };\n default:\n return condition;\n }\n}\n\nfunction scaleFlow(nodes: readonly NodeIR[], factor: number): NodeIR[] {\n return nodes.map((node) => scaleNode(node, factor));\n}\n\nfunction scaleNode(node: NodeIR, factor: number): NodeIR {\n switch (node.type) {\n case 'delay':\n return { ...node, duration: scale(node.duration, factor) };\n case 'random_delay':\n return { ...node, min: scale(node.min, factor), max: scale(node.max, factor) };\n case 'wait_until':\n return {\n ...node,\n condition: scaleCondition(node.condition, factor),\n timeout: scale(node.timeout, factor),\n onTimeout: Array.isArray(node.onTimeout)\n ? scaleFlow(node.onTimeout, factor)\n : node.onTimeout,\n };\n case 'branch':\n return {\n ...node,\n cases: node.cases.map((branch) => ({\n ...branch,\n condition: scaleCondition(branch.condition, factor),\n flow: scaleFlow(branch.flow, factor),\n })),\n ...(node.otherwise === undefined ? {} : { otherwise: scaleFlow(node.otherwise, factor) }),\n };\n case 'filter':\n return { ...node, condition: scaleCondition(node.condition, factor) };\n case 'cohort':\n return {\n ...node,\n arms: node.arms.map((arm) => ({ ...arm, flow: scaleFlow(arm.flow, factor) })),\n };\n default:\n return node;\n }\n}\n\nfunction scale(duration: DurationIR, factor: number): DurationIR {\n return { value: duration.value, ms: Math.max(0, Math.round(duration.ms / factor)) };\n}\n\n/**\n * A store whose segment definitions come back scaled by `factor` — what the journey host leases\n * in fast-forward, so a segment's windows (`performed(e.login, { within: '7 days' })`) shrink\n * with the journeys that reference it, and with the segment triggers it routes. Every read that\n * hands out a stored condition is scaled: `getSegmentCondition` (event-driven evaluation),\n * `listSegments` (the scheduled sweep, which narrows candidates in SQL and evaluates with those\n * definitions) and `findTriggers` (the profile gate an event trigger checks before it starts a\n * journey, which `scaleDurations` scales inside the journey too). Every other method is the\n * store's own.\n */\nexport function scaleSegmentWindows<\n S extends { getSegmentCondition(name: string): Promise<ConditionIR | undefined> },\n>(store: S, factor: number): S {\n if (!(factor > 0) || factor === 1) return store;\n return new Proxy(store, {\n get(target, property) {\n const value: unknown = Reflect.get(target, property, target);\n if (typeof value !== 'function') return value;\n switch (property) {\n case 'getSegmentCondition':\n return async (name: string) => {\n const condition = await target.getSegmentCondition(name);\n return condition === undefined ? undefined : scaleCondition(condition, factor);\n };\n case 'listSegments':\n return async () => {\n const segments = (await value.call(target)) as { id: string; condition: ConditionIR }[];\n return segments.map(({ id, condition }) => ({\n id,\n condition: scaleCondition(condition, factor),\n }));\n };\n case 'findTriggers':\n return async (event: string) => {\n // Only the profile gate: `where` is payload-only, so it holds no window to scale\n const scaled = (route: { filter: ConditionIR | null }) => ({\n ...route,\n filter: route.filter === null ? null : scaleCondition(route.filter, factor),\n });\n return ((await value.call(target, event)) as { filter: ConditionIR | null }[]).map(\n scaled\n );\n };\n default:\n return value.bind(target);\n }\n },\n });\n}\n"],"mappings":"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"}
|