@shware/workflow 9.0.1 → 9.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- 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/dist/host/ingest.cjs +86 -3
- package/dist/host/ingest.cjs.map +1 -1
- package/dist/host/ingest.d.cts +30 -3
- package/dist/host/ingest.d.cts.map +1 -1
- package/dist/host/ingest.d.mts +30 -3
- package/dist/host/ingest.d.mts.map +1 -1
- package/dist/host/ingest.mjs +85 -4
- package/dist/host/ingest.mjs.map +1 -1
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +20 -0
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +20 -0
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres.cjs +14 -0
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +3 -1
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +3 -1
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +14 -0
- package/dist/store/postgres.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1404,6 +1404,17 @@ the sweep's maintenance marks an entry left unadmitted for 15 minutes, whose
|
|
|
1404
1404
|
instance the runtime does not know or reports ended, as failed, so the user's
|
|
1405
1405
|
next trigger re-enters. Neither can start a second journey for the pair.
|
|
1406
1406
|
|
|
1407
|
+
An instance can also end after it was admitted without finalizing its entry:
|
|
1408
|
+
terminated from the dashboard or the API, errored for good because a step kept
|
|
1409
|
+
failing, or evicted. The same maintenance samples the entries admitted more than
|
|
1410
|
+
15 minutes ago (a bounded random sample each sweep, so every entry is reached
|
|
1411
|
+
eventually however many journeys are running), and one whose instance the
|
|
1412
|
+
runtime reports `complete`, `errored` or `terminated`, or no longer knows, is
|
|
1413
|
+
marked failed, its wake subscriptions dropped and its ending reported — what the
|
|
1414
|
+
finalize step would have done. An instance still running, or one whose lookup
|
|
1415
|
+
fails for any other reason, is left alone. The next trigger of the pair then
|
|
1416
|
+
re-enters.
|
|
1417
|
+
|
|
1407
1418
|
## Exports
|
|
1408
1419
|
|
|
1409
1420
|
| Subpath | Contents |
|
|
@@ -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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"}
|
package/dist/host/ingest.cjs
CHANGED
|
@@ -119,6 +119,24 @@ async function refreshSegmentTriggers(env, store, userId, ts) {
|
|
|
119
119
|
* and act on each membership transition: entering records the membership and starts the
|
|
120
120
|
* segment's routed workflows, leaving drops it. A segment trigger therefore starts journeys for
|
|
121
121
|
* users who enter after it is deployed; a user already a member is not entered again.
|
|
122
|
+
*
|
|
123
|
+
* Why the membership writes and the starts are separate store calls, and why that is safe:
|
|
124
|
+
* - Each segment's transition is one store operation. `addSegmentMember` writes the membership
|
|
125
|
+
* and the starts it owes (`segment_start`) in one locked transaction; `removeSegmentMember`
|
|
126
|
+
* drops both in one transaction. No failure leaves a segment half-entered or half-left.
|
|
127
|
+
* - Starts come only from `pendingSegmentStarts`, which exists only once that transaction has
|
|
128
|
+
* committed, so a journey is never started for a membership that was not recorded. A start that
|
|
129
|
+
* fails stays pending and is retried by the next evaluation (the sweep, the user's next event);
|
|
130
|
+
* it is crossed off only once an instance is known to exist (`entryStarted`).
|
|
131
|
+
* - A retried or concurrent start cannot run a journey twice: `startJourney` goes through the
|
|
132
|
+
* entry ledger, keyed by (workflow, user) under the once policy, and the membership insert is
|
|
133
|
+
* `ON CONFLICT DO NOTHING`.
|
|
134
|
+
* - Segments are independent memberships. A failure on one leaves the others where they were;
|
|
135
|
+
* every evaluation recomputes each from facts and the stored membership, so the next one
|
|
136
|
+
* converges rather than building on a partial run.
|
|
137
|
+
* Folding every segment's writes into one transaction would add nothing: the starts have to happen
|
|
138
|
+
* after the commit anyway (a workflow instance cannot join a database transaction), which is what
|
|
139
|
+
* the `segment_start` outbox already provides.
|
|
122
140
|
*/
|
|
123
141
|
async function refreshSegments(env, store, userId, ts, routes, segments = [], definitions = /* @__PURE__ */ new Map()) {
|
|
124
142
|
if (require_store_identity.isVisitorSubject(userId)) return [];
|
|
@@ -199,6 +217,63 @@ async function failLostStarts(env, store, now) {
|
|
|
199
217
|
}
|
|
200
218
|
return failed;
|
|
201
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* How long after its admission an entry is left out of the lost-instance sample. No age proves
|
|
222
|
+
* anything about an admitted instance — it can end a moment after admitting itself, and a long
|
|
223
|
+
* wait runs for weeks — so this is not a judgment, only a filter: the freshest entries are the
|
|
224
|
+
* ones most likely to still be running, and the ones a platform status may not have settled
|
|
225
|
+
* for yet. Correctness rests on the lookup and the conditional update, not on the window.
|
|
226
|
+
*/
|
|
227
|
+
const LOST_INSTANCE_AFTER_MS = 9e5;
|
|
228
|
+
/**
|
|
229
|
+
* Admitted entries one maintenance run looks up on the platform: a random sample (see
|
|
230
|
+
* `JourneyStore.admittedEntries`), so every entry is reached eventually however many are
|
|
231
|
+
* running, and the sweep's platform calls stay bounded.
|
|
232
|
+
*/
|
|
233
|
+
const LOST_INSTANCE_BATCH = 50;
|
|
234
|
+
/**
|
|
235
|
+
* Instances that ended on the platform without finalizing their entry: terminated from outside
|
|
236
|
+
* (the dashboard, the API), errored for good because a finalize step kept failing, or no longer
|
|
237
|
+
* known to the runtime at all. The entry stays `running`, and the once-policy would bar the user
|
|
238
|
+
* from the workflow for good. Only admitted entries are looked at — an unadmitted one is
|
|
239
|
+
* `failLostStarts`' — and only a terminal status, or not found, ends one: an admitted instance
|
|
240
|
+
* did reach the runtime, so not found now means it finished and was evicted (as `stopInstance`
|
|
241
|
+
* reads it). A lookup that fails otherwise proves nothing and leaves the entry alone. The update
|
|
242
|
+
* is conditional on the entry still running under the instance id, so a run that finalized
|
|
243
|
+
* between the read and the lookup keeps its own status.
|
|
244
|
+
*
|
|
245
|
+
* `complete` ends in `failed` too: the run's finalize step is what records the real outcome, and
|
|
246
|
+
* an instance complete with its entry still running went without it, so what the journey would
|
|
247
|
+
* have recorded is unknown. Platform-neutral: on AWS the adapter's own maintenance reconciles
|
|
248
|
+
* these too (both writes are idempotent), and its `status` reads running through a stop's
|
|
249
|
+
* drain, so nothing is failed here that the adapter would still wait for.
|
|
250
|
+
*/
|
|
251
|
+
async function failLostInstances(env, store, now) {
|
|
252
|
+
const failed = [];
|
|
253
|
+
for (const entry of await store.admittedEntries(now - LOST_INSTANCE_AFTER_MS, LOST_INSTANCE_BATCH)) {
|
|
254
|
+
let status;
|
|
255
|
+
try {
|
|
256
|
+
const reported = await (await env.JOURNEY.get(entry.instanceId)).status?.();
|
|
257
|
+
if (reported === void 0 || !STOPPED_STATUSES.has(reported.status)) continue;
|
|
258
|
+
status = reported.status;
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (!(error instanceof require_host_platform.InstanceNotFoundError)) continue;
|
|
261
|
+
status = "not found";
|
|
262
|
+
}
|
|
263
|
+
if (!await store.failAdmittedEntry(entry.instanceId)) continue;
|
|
264
|
+
failed.push(entry.instanceId);
|
|
265
|
+
await store.unsubscribe(entry.instanceId);
|
|
266
|
+
await store.reporting.instanceFinished({
|
|
267
|
+
workflow: entry.workflow,
|
|
268
|
+
instanceId: entry.instanceId,
|
|
269
|
+
userId: entry.userId,
|
|
270
|
+
status: "failed",
|
|
271
|
+
at: now
|
|
272
|
+
});
|
|
273
|
+
console.error(`[workflow] ${entry.instanceId} (${entry.workflow}) ended on the platform (${status}) without finishing: marked failed, the next trigger re-enters`);
|
|
274
|
+
}
|
|
275
|
+
return failed;
|
|
276
|
+
}
|
|
202
277
|
/** How long a visitor who never signs in is kept after they were last heard from. */
|
|
203
278
|
const VISITOR_RETENTION_MS = 7776e6;
|
|
204
279
|
/** Visitors one sweep prunes: the next sweep goes on. */
|
|
@@ -207,9 +282,10 @@ const VISITOR_PRUNE_BATCH = 100;
|
|
|
207
282
|
const SWEEP_PAGE_SIZE = 500;
|
|
208
283
|
/**
|
|
209
284
|
* What the sweep does before it looks at any user: the runtime's own upkeep (AWS: unconfirmed
|
|
210
|
-
* starts, executions that ended unseen)
|
|
211
|
-
* stop). A failure of
|
|
212
|
-
* still pending afterwards are skipped by
|
|
285
|
+
* starts, executions that ended unseen), the entries whose start or instance was lost, and the
|
|
286
|
+
* erasures left unfinished (an instance would not stop). A failure of any is logged, never
|
|
287
|
+
* fatal: the pages still run. Users whose erasure is still pending afterwards are skipped by
|
|
288
|
+
* every page (`sweepSegmentsPaged`).
|
|
213
289
|
*/
|
|
214
290
|
async function sweepMaintenance(env, store) {
|
|
215
291
|
try {
|
|
@@ -222,6 +298,11 @@ async function sweepMaintenance(env, store) {
|
|
|
222
298
|
} catch (error) {
|
|
223
299
|
console.error("[workflow] reclaiming lost starts failed", error);
|
|
224
300
|
}
|
|
301
|
+
try {
|
|
302
|
+
await failLostInstances(env, store, Date.now());
|
|
303
|
+
} catch (error) {
|
|
304
|
+
console.error("[workflow] reclaiming lost instances failed", error);
|
|
305
|
+
}
|
|
225
306
|
try {
|
|
226
307
|
await store.pruneVisitors(Date.now() - VISITOR_RETENTION_MS, VISITOR_PRUNE_BATCH);
|
|
227
308
|
} catch (error) {
|
|
@@ -803,11 +884,13 @@ async function setSubscriptionsByMessage(env, store, messageId, patch, topics, n
|
|
|
803
884
|
};
|
|
804
885
|
}
|
|
805
886
|
exports.BundleIntegrityError = BundleIntegrityError;
|
|
887
|
+
exports.LOST_INSTANCE_AFTER_MS = LOST_INSTANCE_AFTER_MS;
|
|
806
888
|
exports.LOST_START_AFTER_MS = LOST_START_AFTER_MS;
|
|
807
889
|
exports.SUBSCRIPTION_CHANNELS = SUBSCRIPTION_CHANNELS;
|
|
808
890
|
exports.SWEEP_PAGE_SIZE = SWEEP_PAGE_SIZE;
|
|
809
891
|
exports.VISITOR_RETENTION_MS = VISITOR_RETENTION_MS;
|
|
810
892
|
exports.deployBundle = deployBundle;
|
|
893
|
+
exports.failLostInstances = failLostInstances;
|
|
811
894
|
exports.failLostStarts = failLostStarts;
|
|
812
895
|
exports.forgetUser = forgetUser;
|
|
813
896
|
exports.getSubscriptions = getSubscriptions;
|