@shware/workflow 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -29
- package/dist/apns.cjs +113 -26
- package/dist/apns.cjs.map +1 -1
- package/dist/apns.d.cts +52 -6
- package/dist/apns.d.cts.map +1 -1
- package/dist/apns.d.mts +52 -6
- package/dist/apns.d.mts.map +1 -1
- package/dist/apns.mjs +111 -27
- package/dist/apns.mjs.map +1 -1
- package/dist/aws/engine-step.cjs +5 -5
- package/dist/aws/engine-step.cjs.map +1 -1
- package/dist/aws/engine-step.d.cts +8 -2
- package/dist/aws/engine-step.d.cts.map +1 -1
- package/dist/aws/engine-step.d.mts +8 -2
- package/dist/aws/engine-step.d.mts.map +1 -1
- package/dist/aws/engine-step.mjs +5 -5
- package/dist/aws/engine-step.mjs.map +1 -1
- package/dist/aws/index.cjs +47 -6
- package/dist/aws/index.cjs.map +1 -1
- package/dist/aws/index.d.cts +2 -2
- package/dist/aws/index.d.cts.map +1 -1
- package/dist/aws/index.d.mts +2 -2
- package/dist/aws/index.d.mts.map +1 -1
- package/dist/aws/index.mjs +47 -6
- package/dist/aws/index.mjs.map +1 -1
- package/dist/aws/instances.cjs +29 -6
- package/dist/aws/instances.cjs.map +1 -1
- package/dist/aws/instances.d.cts +11 -1
- package/dist/aws/instances.d.cts.map +1 -1
- package/dist/aws/instances.d.mts +11 -1
- package/dist/aws/instances.d.mts.map +1 -1
- package/dist/aws/instances.mjs +27 -4
- package/dist/aws/instances.mjs.map +1 -1
- package/dist/client.cjs +4 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +2 -2
- package/dist/client.d.cts.map +1 -1
- package/dist/client.d.mts +2 -2
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +4 -1
- package/dist/client.mjs.map +1 -1
- package/dist/cloudflare/bindings.cjs +28 -25
- package/dist/cloudflare/bindings.cjs.map +1 -1
- package/dist/cloudflare/bindings.d.cts +17 -129
- package/dist/cloudflare/bindings.d.cts.map +1 -1
- package/dist/cloudflare/bindings.d.mts +17 -129
- package/dist/cloudflare/bindings.d.mts.map +1 -1
- package/dist/cloudflare/bindings.mjs +28 -23
- package/dist/cloudflare/bindings.mjs.map +1 -1
- package/dist/cloudflare/d1-env.cjs +6 -6
- package/dist/cloudflare/d1-env.cjs.map +1 -1
- package/dist/cloudflare/d1-env.d.cts +5 -5
- package/dist/cloudflare/d1-env.d.mts +5 -5
- package/dist/cloudflare/d1-env.mjs +6 -6
- package/dist/cloudflare/d1-env.mjs.map +1 -1
- package/dist/cloudflare/index.cjs +28 -18
- package/dist/cloudflare/index.d.cts +8 -4
- package/dist/cloudflare/index.d.mts +8 -4
- package/dist/cloudflare/index.mjs +9 -5
- package/dist/cloudflare/runner.cjs +29 -14
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts +9 -2
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts +9 -2
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +28 -13
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/cloudflare/schema.sql +27 -6
- package/dist/cloudflare/worker.cjs +19 -9
- package/dist/cloudflare/worker.cjs.map +1 -1
- package/dist/cloudflare/worker.d.cts +2 -2
- package/dist/cloudflare/worker.d.cts.map +1 -1
- package/dist/cloudflare/worker.d.mts +2 -2
- package/dist/cloudflare/worker.d.mts.map +1 -1
- package/dist/cloudflare/worker.mjs +19 -9
- package/dist/cloudflare/worker.mjs.map +1 -1
- package/dist/config.cjs +21 -5
- package/dist/config.cjs.map +1 -1
- package/dist/config.mjs +21 -5
- package/dist/config.mjs.map +1 -1
- package/dist/dsl/action.cjs +3 -6
- package/dist/dsl/action.cjs.map +1 -1
- package/dist/dsl/action.d.cts +12 -11
- package/dist/dsl/action.d.cts.map +1 -1
- package/dist/dsl/action.d.mts +12 -11
- package/dist/dsl/action.d.mts.map +1 -1
- package/dist/dsl/action.mjs +3 -6
- package/dist/dsl/action.mjs.map +1 -1
- package/dist/dsl/bundle.cjs +35 -31
- package/dist/dsl/bundle.cjs.map +1 -1
- package/dist/dsl/bundle.d.cts.map +1 -1
- package/dist/dsl/bundle.d.mts.map +1 -1
- package/dist/dsl/bundle.mjs +35 -31
- package/dist/dsl/bundle.mjs.map +1 -1
- package/dist/dsl/condition.cjs +13 -1
- package/dist/dsl/condition.cjs.map +1 -1
- package/dist/dsl/condition.d.cts +9 -1
- package/dist/dsl/condition.d.cts.map +1 -1
- package/dist/dsl/condition.d.mts +9 -1
- package/dist/dsl/condition.d.mts.map +1 -1
- package/dist/dsl/condition.mjs +13 -2
- package/dist/dsl/condition.mjs.map +1 -1
- package/dist/dsl/flow.cjs +33 -11
- package/dist/dsl/flow.cjs.map +1 -1
- package/dist/dsl/flow.d.cts +9 -1
- package/dist/dsl/flow.d.cts.map +1 -1
- package/dist/dsl/flow.d.mts +9 -1
- package/dist/dsl/flow.d.mts.map +1 -1
- package/dist/dsl/flow.mjs +34 -13
- package/dist/dsl/flow.mjs.map +1 -1
- package/dist/dsl/workflow.cjs +2 -3
- package/dist/dsl/workflow.cjs.map +1 -1
- package/dist/dsl/workflow.d.cts.map +1 -1
- package/dist/dsl/workflow.d.mts.map +1 -1
- package/dist/dsl/workflow.mjs +3 -4
- package/dist/dsl/workflow.mjs.map +1 -1
- package/dist/email-sender.cjs +24 -0
- package/dist/email-sender.cjs.map +1 -1
- package/dist/email-sender.d.cts +18 -0
- package/dist/email-sender.d.cts.map +1 -1
- package/dist/email-sender.d.mts +18 -0
- package/dist/email-sender.d.mts.map +1 -1
- package/dist/email-sender.mjs +24 -1
- package/dist/email-sender.mjs.map +1 -1
- package/dist/engine/actions.cjs +1 -8
- package/dist/engine/actions.cjs.map +1 -1
- package/dist/engine/actions.d.cts +1 -12
- package/dist/engine/actions.d.cts.map +1 -1
- package/dist/engine/actions.d.mts +1 -12
- package/dist/engine/actions.d.mts.map +1 -1
- package/dist/engine/actions.mjs +1 -8
- package/dist/engine/actions.mjs.map +1 -1
- package/dist/engine/condition.cjs +25 -5
- package/dist/engine/condition.cjs.map +1 -1
- package/dist/engine/condition.d.cts +12 -3
- package/dist/engine/condition.d.cts.map +1 -1
- package/dist/engine/condition.d.mts +12 -3
- package/dist/engine/condition.d.mts.map +1 -1
- package/dist/engine/condition.mjs +25 -6
- package/dist/engine/condition.mjs.map +1 -1
- package/dist/engine/index.cjs +3 -0
- package/dist/engine/index.d.cts +5 -5
- package/dist/engine/index.d.mts +5 -5
- package/dist/engine/index.mjs +4 -4
- package/dist/engine/interpreter.cjs +27 -20
- package/dist/engine/interpreter.cjs.map +1 -1
- package/dist/engine/interpreter.d.cts.map +1 -1
- package/dist/engine/interpreter.d.mts.map +1 -1
- package/dist/engine/interpreter.mjs +27 -20
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +21 -6
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +21 -6
- package/dist/engine/ports.d.mts.map +1 -1
- package/dist/engine/retry.cjs +13 -0
- package/dist/engine/retry.cjs.map +1 -1
- package/dist/engine/retry.d.cts +12 -0
- package/dist/engine/retry.d.cts.map +1 -1
- package/dist/engine/retry.d.mts +12 -0
- package/dist/engine/retry.d.mts.map +1 -1
- package/dist/engine/retry.mjs +13 -1
- package/dist/engine/retry.mjs.map +1 -1
- package/dist/engine/time-scale.cjs +90 -15
- package/dist/engine/time-scale.cjs.map +1 -1
- package/dist/engine/time-scale.d.cts +9 -6
- package/dist/engine/time-scale.d.cts.map +1 -1
- package/dist/engine/time-scale.d.mts +9 -6
- package/dist/engine/time-scale.d.mts.map +1 -1
- package/dist/engine/time-scale.mjs +90 -16
- package/dist/engine/time-scale.mjs.map +1 -1
- package/dist/fcm.cjs +111 -38
- package/dist/fcm.cjs.map +1 -1
- package/dist/fcm.d.cts +45 -5
- package/dist/fcm.d.cts.map +1 -1
- package/dist/fcm.d.mts +45 -5
- package/dist/fcm.d.mts.map +1 -1
- package/dist/fcm.mjs +110 -39
- package/dist/fcm.mjs.map +1 -1
- package/dist/hash.cjs +6 -6
- package/dist/hash.cjs.map +1 -1
- package/dist/hash.mjs +6 -6
- package/dist/hash.mjs.map +1 -1
- package/dist/host/auth.cjs +49 -0
- package/dist/host/auth.cjs.map +1 -0
- package/dist/host/auth.d.cts +21 -0
- package/dist/host/auth.d.cts.map +1 -0
- package/dist/host/auth.d.mts +21 -0
- package/dist/host/auth.d.mts.map +1 -0
- package/dist/host/auth.mjs +47 -0
- package/dist/host/auth.mjs.map +1 -0
- package/dist/host/channels.cjs +118 -40
- package/dist/host/channels.cjs.map +1 -1
- package/dist/host/channels.d.cts +27 -1
- package/dist/host/channels.d.cts.map +1 -1
- package/dist/host/channels.d.mts +27 -1
- package/dist/host/channels.d.mts.map +1 -1
- package/dist/host/channels.mjs +121 -44
- package/dist/host/channels.mjs.map +1 -1
- package/dist/host/execute.cjs +14 -2
- package/dist/host/execute.cjs.map +1 -1
- package/dist/host/execute.d.cts +4 -4
- package/dist/host/execute.d.cts.map +1 -1
- package/dist/host/execute.d.mts +4 -4
- package/dist/host/execute.d.mts.map +1 -1
- package/dist/host/execute.mjs +13 -1
- package/dist/host/execute.mjs.map +1 -1
- package/dist/host/fresh-read.cjs +30 -0
- package/dist/host/fresh-read.cjs.map +1 -0
- package/dist/host/fresh-read.d.cts +18 -0
- package/dist/host/fresh-read.d.cts.map +1 -0
- package/dist/host/fresh-read.d.mts +18 -0
- package/dist/host/fresh-read.d.mts.map +1 -0
- package/dist/host/fresh-read.mjs +29 -0
- package/dist/host/fresh-read.mjs.map +1 -0
- package/dist/host/http.cjs +321 -0
- package/dist/host/http.cjs.map +1 -0
- package/dist/host/http.d.cts +54 -0
- package/dist/host/http.d.cts.map +1 -0
- package/dist/host/http.d.mts +54 -0
- package/dist/host/http.d.mts.map +1 -0
- package/dist/host/http.mjs +317 -0
- package/dist/host/http.mjs.map +1 -0
- package/dist/host/{router.cjs → ingest.cjs} +147 -175
- package/dist/host/ingest.cjs.map +1 -0
- package/dist/host/{router.d.cts → ingest.d.cts} +48 -30
- package/dist/host/ingest.d.cts.map +1 -0
- package/dist/host/{router.d.mts → ingest.d.mts} +48 -30
- package/dist/host/ingest.d.mts.map +1 -0
- package/dist/host/{router.mjs → ingest.mjs} +144 -173
- package/dist/host/ingest.mjs.map +1 -0
- package/dist/host/journey-host.cjs +65 -93
- package/dist/host/journey-host.cjs.map +1 -1
- package/dist/host/journey-host.d.cts +19 -26
- package/dist/host/journey-host.d.cts.map +1 -1
- package/dist/host/journey-host.d.mts +19 -26
- package/dist/host/journey-host.d.mts.map +1 -1
- package/dist/host/journey-host.mjs +65 -93
- package/dist/host/journey-host.mjs.map +1 -1
- package/dist/host/platform.cjs +21 -0
- package/dist/host/platform.cjs.map +1 -0
- package/dist/host/platform.d.cts +126 -0
- package/dist/host/platform.d.cts.map +1 -0
- package/dist/host/platform.d.mts +126 -0
- package/dist/host/platform.d.mts.map +1 -0
- package/dist/host/platform.mjs +19 -0
- package/dist/host/platform.mjs.map +1 -0
- package/dist/host/senders.cjs +18 -9
- package/dist/host/senders.cjs.map +1 -1
- package/dist/host/senders.d.cts +9 -4
- package/dist/host/senders.d.cts.map +1 -1
- package/dist/host/senders.d.mts +9 -4
- package/dist/host/senders.d.mts.map +1 -1
- package/dist/host/senders.mjs +18 -9
- package/dist/host/senders.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/ir.cjs +5 -7
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +3 -12
- package/dist/ir.d.cts.map +1 -1
- package/dist/ir.d.mts +3 -12
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +5 -7
- package/dist/ir.mjs.map +1 -1
- package/dist/plan.cjs +27 -33
- package/dist/plan.cjs.map +1 -1
- package/dist/plan.d.cts +1 -1
- package/dist/plan.d.cts.map +1 -1
- package/dist/plan.d.mts +1 -1
- package/dist/plan.d.mts.map +1 -1
- package/dist/plan.mjs +27 -33
- package/dist/plan.mjs.map +1 -1
- package/dist/reporting/observer.cjs +1 -0
- package/dist/reporting/observer.cjs.map +1 -1
- package/dist/reporting/observer.mjs +1 -0
- package/dist/reporting/observer.mjs.map +1 -1
- package/dist/reporting/tracking.cjs +19 -4
- package/dist/reporting/tracking.cjs.map +1 -1
- package/dist/reporting/tracking.d.cts +13 -11
- package/dist/reporting/tracking.d.cts.map +1 -1
- package/dist/reporting/tracking.d.mts +13 -11
- package/dist/reporting/tracking.d.mts.map +1 -1
- package/dist/reporting/tracking.mjs +19 -4
- package/dist/reporting/tracking.mjs.map +1 -1
- package/dist/store/d1.cjs +88 -29
- package/dist/store/d1.cjs.map +1 -1
- package/dist/store/d1.d.cts +29 -11
- package/dist/store/d1.d.cts.map +1 -1
- package/dist/store/d1.d.mts +29 -11
- package/dist/store/d1.d.mts.map +1 -1
- package/dist/store/d1.mjs +88 -29
- package/dist/store/d1.mjs.map +1 -1
- package/dist/store/erasure.cjs +22 -0
- package/dist/store/erasure.cjs.map +1 -0
- package/dist/store/erasure.d.cts +18 -0
- package/dist/store/erasure.d.cts.map +1 -0
- package/dist/store/erasure.d.mts +18 -0
- package/dist/store/erasure.d.mts.map +1 -0
- package/dist/store/erasure.mjs +21 -0
- package/dist/store/erasure.mjs.map +1 -0
- package/dist/store/index.cjs +2 -0
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +67 -13
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +67 -13
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs +2 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres-reporting.cjs +125 -62
- package/dist/store/postgres-reporting.cjs.map +1 -1
- package/dist/store/postgres-reporting.d.cts +30 -2
- package/dist/store/postgres-reporting.d.cts.map +1 -1
- package/dist/store/postgres-reporting.d.mts +30 -2
- package/dist/store/postgres-reporting.d.mts.map +1 -1
- package/dist/store/postgres-reporting.mjs +125 -62
- package/dist/store/postgres-reporting.mjs.map +1 -1
- package/dist/store/postgres.cjs +179 -60
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +44 -7
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +44 -7
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +179 -60
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +7 -5
- package/dist/store/tables.cjs.map +1 -1
- package/dist/store/tables.d.cts.map +1 -1
- package/dist/store/tables.d.mts.map +1 -1
- package/dist/store/tables.mjs +7 -5
- package/dist/store/tables.mjs.map +1 -1
- package/dist/store/user-lock.cjs +43 -0
- package/dist/store/user-lock.cjs.map +1 -0
- package/dist/store/user-lock.d.cts +32 -0
- package/dist/store/user-lock.d.cts.map +1 -0
- package/dist/store/user-lock.d.mts +32 -0
- package/dist/store/user-lock.d.mts.map +1 -0
- package/dist/store/user-lock.mjs +40 -0
- package/dist/store/user-lock.mjs.map +1 -0
- package/dist/transports/cloudflare-email.cjs +3 -2
- package/dist/transports/cloudflare-email.cjs.map +1 -1
- package/dist/transports/cloudflare-email.mjs +3 -2
- package/dist/transports/cloudflare-email.mjs.map +1 -1
- package/dist/transports/ses.cjs +52 -7
- package/dist/transports/ses.cjs.map +1 -1
- package/dist/transports/ses.d.cts +9 -1
- package/dist/transports/ses.d.cts.map +1 -1
- package/dist/transports/ses.d.mts +9 -1
- package/dist/transports/ses.d.mts.map +1 -1
- package/dist/transports/ses.mjs +52 -8
- package/dist/transports/ses.mjs.map +1 -1
- package/dist/webpush.cjs +133 -47
- package/dist/webpush.cjs.map +1 -1
- package/dist/webpush.d.cts +49 -8
- package/dist/webpush.d.cts.map +1 -1
- package/dist/webpush.d.mts +49 -8
- package/dist/webpush.d.mts.map +1 -1
- package/dist/webpush.mjs +130 -47
- package/dist/webpush.mjs.map +1 -1
- package/package.json +1 -1
- package/src/cloudflare/schema.sql +27 -6
- package/dist/host/router.cjs.map +0 -1
- package/dist/host/router.d.cts.map +0 -1
- package/dist/host/router.d.mts.map +0 -1
- package/dist/host/router.mjs.map +0 -1
package/dist/dsl/bundle.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_hash = require("../hash.cjs");
|
|
3
2
|
const require_templates = require("../templates.cjs");
|
|
3
|
+
const require_dsl_flow = require("./flow.cjs");
|
|
4
|
+
const require_hash = require("../hash.cjs");
|
|
4
5
|
const require_ir = require("../ir.cjs");
|
|
5
6
|
//#region src/dsl/bundle.ts
|
|
6
7
|
/**
|
|
@@ -32,8 +33,7 @@ function compileBundle(input) {
|
|
|
32
33
|
}));
|
|
33
34
|
const actions = (input.actions ?? []).map((a) => ({
|
|
34
35
|
irVersion: 2,
|
|
35
|
-
name: a.name
|
|
36
|
-
codeHash: a.codeHash
|
|
36
|
+
name: a.name
|
|
37
37
|
}));
|
|
38
38
|
const workflows = input.workflows.map((builder) => builder.toIR());
|
|
39
39
|
assertUniqueNames(workflows.map((w) => w.id), "workflow");
|
|
@@ -41,8 +41,9 @@ function compileBundle(input) {
|
|
|
41
41
|
assertUniqueNames(templates.map((t) => `${t.channel}:${t.key}`), "template");
|
|
42
42
|
assertUniqueNames(actions.map((a) => a.name), "action");
|
|
43
43
|
assertSegmentRefsResolve(workflows, segments);
|
|
44
|
+
assertNoSegmentCycles(segments);
|
|
44
45
|
assertConditionPlacement(workflows, segments);
|
|
45
|
-
assertActionRefsResolve(workflows, actions);
|
|
46
|
+
assertActionRefsResolve(workflows, input.workflows, input.actions ?? []);
|
|
46
47
|
return require_ir.BundleIR.parse({
|
|
47
48
|
irVersion: 2,
|
|
48
49
|
workflows,
|
|
@@ -110,36 +111,39 @@ function collectNodeRefs(nodes, into) {
|
|
|
110
111
|
case "cohort": for (const arm of node.arms) collectNodeRefs(arm.flow, into);
|
|
111
112
|
}
|
|
112
113
|
}
|
|
113
|
-
function assertActionRefsResolve(workflows, actions) {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
const used
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (manifestHash === void 0) throw new Error(`compileBundle: workflow '${workflow.id}' runs action '${name}' that is not in the bundle — pass it in 'actions'`);
|
|
121
|
-
for (const codeHash of codeHashes) if (codeHash !== void 0 && codeHash !== manifestHash) throw new Error(`compileBundle: workflow '${workflow.id}' runs action '${name}' whose code differs from the one in 'actions' — two definitions are sharing that name`);
|
|
114
|
+
function assertActionRefsResolve(workflows, builders, actions) {
|
|
115
|
+
const listed = new Map(actions.map((a) => [a.name, a]));
|
|
116
|
+
workflows.forEach((workflow, i) => {
|
|
117
|
+
for (const used of require_dsl_flow.actionUses(builders[i])) {
|
|
118
|
+
const manifest = listed.get(used.name);
|
|
119
|
+
if (manifest === void 0) throw new Error(`compileBundle: workflow '${workflow.id}' runs action '${used.name}' that is not in the bundle — pass it in 'actions'`);
|
|
120
|
+
if (manifest !== used && manifest.handler !== used.handler) throw new Error(`compileBundle: workflow '${workflow.id}' runs action '${used.name}' that is not the definition listed in 'actions' — two definitions are sharing that name`);
|
|
122
121
|
}
|
|
123
|
-
}
|
|
122
|
+
});
|
|
124
123
|
}
|
|
125
|
-
/**
|
|
126
|
-
function
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
case "wait_until":
|
|
135
|
-
if (Array.isArray(node.onTimeout)) collectActionRefs(node.onTimeout, into);
|
|
136
|
-
break;
|
|
137
|
-
case "branch":
|
|
138
|
-
for (const branchCase of node.cases) collectActionRefs(branchCase.flow, into);
|
|
139
|
-
if (node.otherwise) collectActionRefs(node.otherwise, into);
|
|
140
|
-
break;
|
|
141
|
-
case "cohort": for (const arm of node.arms) collectActionRefs(arm.flow, into);
|
|
124
|
+
/** Walk segment → segment references depth-first; the first back edge is reported as the cycle path. */
|
|
125
|
+
function assertNoSegmentCycles(segments) {
|
|
126
|
+
const refsOf = /* @__PURE__ */ new Map();
|
|
127
|
+
for (const segment of segments) {
|
|
128
|
+
const refs = /* @__PURE__ */ new Set();
|
|
129
|
+
collectConditionRefs(segment.condition, refs);
|
|
130
|
+
refsOf.set(segment.id, refs);
|
|
142
131
|
}
|
|
132
|
+
const done = /* @__PURE__ */ new Set();
|
|
133
|
+
const path = [];
|
|
134
|
+
const visit = (id) => {
|
|
135
|
+
const at = path.indexOf(id);
|
|
136
|
+
if (at !== -1) {
|
|
137
|
+
const cycle = [...path.slice(at), id].join(" → ");
|
|
138
|
+
throw new Error(`compileBundle: segments reference each other in a cycle: ${cycle}`);
|
|
139
|
+
}
|
|
140
|
+
if (done.has(id)) return;
|
|
141
|
+
path.push(id);
|
|
142
|
+
for (const ref of refsOf.get(id) ?? []) visit(ref);
|
|
143
|
+
path.pop();
|
|
144
|
+
done.add(id);
|
|
145
|
+
};
|
|
146
|
+
for (const id of refsOf.keys()) visit(id);
|
|
143
147
|
}
|
|
144
148
|
function assertConditionPlacement(workflows, segments) {
|
|
145
149
|
for (const workflow of workflows) {
|
package/dist/dsl/bundle.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle.cjs","names":["semanticHash","CHANNEL_OF","BundleIRSchema"],"sources":["../../src/dsl/bundle.ts"],"sourcesContent":["import { semanticHash } from '../hash';\nimport {\n type BundleIR,\n BundleIR as BundleIRSchema,\n type ConditionIR,\n IR_VERSION,\n type NodeIR,\n type SegmentIR,\n type WorkflowIR,\n} from '../ir';\nimport { type AnyTemplate, CHANNEL_OF } from '../templates';\nimport type { ActionRef } from './action';\nimport type { SegmentInternal, SegmentRef } from './condition';\nimport type { WorkflowBuilder } from './workflow';\n\n/**\n * The deployment unit — compileBundle gathers workflows, segments and the\n * template manifest into one BundleIR snapshot (terraform-apply mental model);\n * the server diffs each definition by contentHash (see plan.ts).\n */\nexport function compileBundle(input: {\n workflows: readonly WorkflowBuilder[];\n segments?: readonly SegmentRef[];\n /** The template manifest: these definitions, by channel and id. */\n templates?: readonly AnyTemplate[];\n actions?: readonly ActionRef[];\n}): BundleIR {\n const segments = (input.segments ?? []).map((segment) => {\n const { id, definition, loc, meta } = segment as SegmentInternal;\n const compiled: SegmentIR = {\n irVersion: IR_VERSION,\n id,\n // Labels and provenance both live in meta, which stripMeta drops before hashing\n contentHash: semanticHash(definition),\n condition: definition,\n };\n const merged = {\n ...(meta?.name !== undefined ? { name: meta.name } : {}),\n ...(meta?.description !== undefined ? { description: meta.description } : {}),\n ...(loc !== undefined ? { loc } : {}),\n };\n if (Object.keys(merged).length > 0) compiled.meta = merged;\n return compiled;\n });\n\n const templates = (input.templates ?? []).map((template) => ({\n irVersion: IR_VERSION,\n key: template.id,\n channel: CHANNEL_OF[template.kind],\n }));\n\n // Manifest only — name and code-identity hash; the implementation ships in the Worker bundle, never in IR\n const actions = (input.actions ?? []).map((a) => ({\n irVersion: IR_VERSION,\n name: a.name,\n codeHash: a.codeHash,\n }));\n\n const workflows = input.workflows.map((builder) => builder.toIR());\n\n // Names are wire identity (routing tables, entry ledger, by-name refs):\n // duplicates would silently last-write-win on deploy, so fail the compile.\n assertUniqueNames(\n workflows.map((w) => w.id),\n 'workflow'\n );\n assertUniqueNames(\n segments.map((s) => s.id),\n 'segment'\n );\n // A message node names its template by channel and key, so the key is unique per channel:\n // an email and a push of one nudge may share it\n assertUniqueNames(\n templates.map((t) => `${t.channel}:${t.key}`),\n 'template'\n );\n assertUniqueNames(\n actions.map((a) => a.name),\n 'action'\n );\n\n // Deploy replaces the whole segment table, so every by-name reference must\n // resolve inside this bundle — catching it here turns a runtime \"Unknown\n // segment\" in some user's journey into a compile error.\n assertSegmentRefsResolve(workflows, segments);\n\n // Payload predicates and profile predicates share the Condition type, so\n // placement is checked here: payload only inside where-trees, and where-trees\n // contain nothing but payload predicates and combinators.\n assertConditionPlacement(workflows, segments);\n\n // Same rationale as segments: the runtime registry is looked up by name, so\n // an action a workflow runs must be in the bundle — and its codeHash must\n // agree, or two same-named definitions are competing for one identity.\n assertActionRefsResolve(workflows, actions);\n\n return BundleIRSchema.parse({ irVersion: IR_VERSION, workflows, segments, templates, actions });\n}\n\nfunction assertUniqueNames(names: readonly string[], kind: string): void {\n const seen = new Set<string>();\n for (const name of names) {\n if (seen.has(name)) throw new Error(`compileBundle: duplicate ${kind} name '${name}'`);\n seen.add(name);\n }\n}\n\nfunction assertSegmentRefsResolve(\n workflows: readonly WorkflowIR[],\n segments: readonly SegmentIR[]\n): void {\n const defined = new Set(segments.map((s) => s.id));\n const check = (refs: ReadonlySet<string>, owner: string): void => {\n for (const ref of refs) {\n if (!defined.has(ref)) {\n throw new Error(\n `compileBundle: ${owner} references segment '${ref}' that is not in the bundle — pass it in 'segments'`\n );\n }\n }\n };\n for (const workflow of workflows) {\n check(referencedSegments(workflow), `workflow '${workflow.id}'`);\n }\n // Segments may reference other segments; those must resolve too\n for (const segment of segments) {\n const refs = new Set<string>();\n collectConditionRefs(segment.condition, refs);\n check(refs, `segment '${segment.id}'`);\n }\n}\n\nfunction referencedSegments(workflow: WorkflowIR): Set<string> {\n const refs = new Set<string>();\n if (workflow.trigger.type === 'segment') refs.add(workflow.trigger.segment);\n if (workflow.trigger.type === 'event' && workflow.trigger.filter !== undefined) {\n collectConditionRefs(workflow.trigger.filter, refs);\n }\n if (workflow.goal !== undefined) collectConditionRefs(workflow.goal.condition, refs);\n if (workflow.exitWhen !== undefined) collectConditionRefs(workflow.exitWhen, refs);\n collectNodeRefs(workflow.flow, refs);\n return refs;\n}\n\nfunction collectConditionRefs(condition: ConditionIR, into: Set<string>): void {\n switch (condition.type) {\n case 'and':\n case 'or':\n for (const child of condition.conditions) collectConditionRefs(child, into);\n break;\n case 'not':\n collectConditionRefs(condition.condition, into);\n break;\n case 'segment':\n into.add(condition.segment);\n break;\n default:\n break;\n }\n}\n\nfunction collectNodeRefs(nodes: readonly NodeIR[], into: Set<string>): void {\n for (const node of nodes) {\n switch (node.type) {\n case 'wait_until':\n collectConditionRefs(node.condition, into);\n if (Array.isArray(node.onTimeout)) collectNodeRefs(node.onTimeout, into);\n break;\n case 'branch':\n for (const branchCase of node.cases) {\n collectConditionRefs(branchCase.condition, into);\n collectNodeRefs(branchCase.flow, into);\n }\n if (node.otherwise) collectNodeRefs(node.otherwise, into);\n break;\n case 'filter':\n collectConditionRefs(node.condition, into);\n break;\n case 'cohort':\n for (const arm of node.arms) collectNodeRefs(arm.flow, into);\n break;\n default:\n break;\n }\n }\n}\n\nfunction assertActionRefsResolve(\n workflows: readonly WorkflowIR[],\n actions: readonly { name: string; codeHash: string }[]\n): void {\n const defined = new Map(actions.map((a) => [a.name, a.codeHash]));\n for (const workflow of workflows) {\n const used = new Map<string, Set<string | undefined>>();\n collectActionRefs(workflow.flow, used);\n for (const [name, codeHashes] of used) {\n const manifestHash = defined.get(name);\n if (manifestHash === undefined) {\n throw new Error(\n `compileBundle: workflow '${workflow.id}' runs action '${name}' that is not in the bundle — pass it in 'actions'`\n );\n }\n // Every node's hash is checked, not just one per name: with two\n // definitions under one name, whichever the manifest holds, the *other*\n // node is the one that would meet a mismatched implementation at run time.\n for (const codeHash of codeHashes) {\n if (codeHash !== undefined && codeHash !== manifestHash) {\n throw new Error(\n `compileBundle: workflow '${workflow.id}' runs action '${name}' whose code differs from the one in 'actions' — two definitions are sharing that name`\n );\n }\n }\n }\n }\n}\n\n/** name → every codeHash seen under it; a name with two is exactly the case worth failing on. */\nfunction collectActionRefs(\n nodes: readonly NodeIR[],\n into: Map<string, Set<string | undefined>>\n): void {\n for (const node of nodes) {\n switch (node.type) {\n case 'action': {\n const seen = into.get(node.action) ?? new Set<string | undefined>();\n seen.add(node.codeHash);\n into.set(node.action, seen);\n break;\n }\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) collectActionRefs(node.onTimeout, into);\n break;\n case 'branch':\n for (const branchCase of node.cases) collectActionRefs(branchCase.flow, into);\n if (node.otherwise) collectActionRefs(node.otherwise, into);\n break;\n case 'cohort':\n for (const arm of node.arms) collectActionRefs(arm.flow, into);\n break;\n default:\n break;\n }\n }\n}\n\n/* --------------------- payload / profile placement checks --------------------- */\n\nfunction assertConditionPlacement(\n workflows: readonly WorkflowIR[],\n segments: readonly SegmentIR[]\n): void {\n for (const workflow of workflows) {\n if (workflow.trigger.type === 'event') {\n if (workflow.trigger.where !== undefined) {\n assertWhereTree(workflow.trigger.where, `workflow '${workflow.id}' trigger where`);\n }\n if (workflow.trigger.filter !== undefined) {\n assertProfileTree(workflow.trigger.filter, `workflow '${workflow.id}' trigger filter`);\n }\n }\n if (workflow.goal !== undefined) {\n assertProfileTree(workflow.goal.condition, `workflow '${workflow.id}' goal`);\n }\n if (workflow.exitWhen !== undefined) {\n assertProfileTree(workflow.exitWhen, `workflow '${workflow.id}' exitWhen`);\n }\n assertNodeTrees(workflow.flow, `workflow '${workflow.id}'`);\n }\n for (const segment of segments) {\n assertProfileTree(segment.condition, `segment '${segment.id}'`);\n }\n}\n\n/** A profile-context tree: payload predicates may appear only under a performed.where. */\nfunction assertProfileTree(condition: ConditionIR, owner: string): void {\n switch (condition.type) {\n case 'and':\n case 'or':\n for (const child of condition.conditions) assertProfileTree(child, owner);\n break;\n case 'not':\n assertProfileTree(condition.condition, owner);\n break;\n case 'performed':\n if (condition.where !== undefined) assertWhereTree(condition.where, owner);\n break;\n case 'payload':\n throw new Error(\n `compileBundle: ${owner} uses a payload predicate outside a where clause — payload refs are only valid inside performed({ where }) or trigger.event({ where })`\n );\n default:\n break; // property / segment\n }\n}\n\n/** A where tree: payload predicates and combinators only — no profile facts in event scope. */\nfunction assertWhereTree(condition: ConditionIR, owner: string): void {\n switch (condition.type) {\n case 'and':\n case 'or':\n for (const child of condition.conditions) assertWhereTree(child, owner);\n break;\n case 'not':\n assertWhereTree(condition.condition, owner);\n break;\n case 'payload':\n break;\n default:\n throw new Error(\n `compileBundle: ${owner} where clause may only contain payload predicates and and/or/not, found '${condition.type}'`\n );\n }\n}\n\nfunction assertNodeTrees(nodes: readonly NodeIR[], owner: string): void {\n for (const node of nodes) {\n switch (node.type) {\n case 'wait_until':\n assertProfileTree(node.condition, owner);\n if (Array.isArray(node.onTimeout)) assertNodeTrees(node.onTimeout, owner);\n break;\n case 'branch':\n for (const branchCase of node.cases) {\n assertProfileTree(branchCase.condition, owner);\n assertNodeTrees(branchCase.flow, owner);\n }\n if (node.otherwise) assertNodeTrees(node.otherwise, owner);\n break;\n case 'filter':\n assertProfileTree(node.condition, owner);\n break;\n case 'cohort':\n for (const arm of node.arms) assertNodeTrees(arm.flow, owner);\n break;\n default:\n break;\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"bundle.cjs","names":["semanticHash","CHANNEL_OF","BundleIRSchema","actionUses"],"sources":["../../src/dsl/bundle.ts"],"sourcesContent":["import { semanticHash } from '../hash';\nimport {\n type BundleIR,\n BundleIR as BundleIRSchema,\n type ConditionIR,\n IR_VERSION,\n type NodeIR,\n type SegmentIR,\n type WorkflowIR,\n} from '../ir';\nimport { type AnyTemplate, CHANNEL_OF } from '../templates';\nimport type { ActionRef } from './action';\nimport type { SegmentInternal, SegmentRef } from './condition';\nimport { actionUses } from './flow';\nimport type { WorkflowBuilder } from './workflow';\n\n/**\n * The deployment unit — compileBundle gathers workflows, segments and the\n * template manifest into one BundleIR snapshot (terraform-apply mental model);\n * the server diffs each definition by contentHash (see plan.ts).\n */\nexport function compileBundle(input: {\n workflows: readonly WorkflowBuilder[];\n segments?: readonly SegmentRef[];\n /** The template manifest: these definitions, by channel and id. */\n templates?: readonly AnyTemplate[];\n actions?: readonly ActionRef[];\n}): BundleIR {\n const segments = (input.segments ?? []).map((segment) => {\n const { id, definition, loc, meta } = segment as SegmentInternal;\n const compiled: SegmentIR = {\n irVersion: IR_VERSION,\n id,\n // Labels and provenance both live in meta, which stripMeta drops before hashing\n contentHash: semanticHash(definition),\n condition: definition,\n };\n const merged = {\n ...(meta?.name !== undefined ? { name: meta.name } : {}),\n ...(meta?.description !== undefined ? { description: meta.description } : {}),\n ...(loc !== undefined ? { loc } : {}),\n };\n if (Object.keys(merged).length > 0) compiled.meta = merged;\n return compiled;\n });\n\n const templates = (input.templates ?? []).map((template) => ({\n irVersion: IR_VERSION,\n key: template.id,\n channel: CHANNEL_OF[template.kind],\n }));\n\n // Manifest only — names; the implementation ships in the Worker bundle, never in IR\n const actions = (input.actions ?? []).map((a) => ({ irVersion: IR_VERSION, name: a.name }));\n\n const workflows = input.workflows.map((builder) => builder.toIR());\n\n // Names are wire identity (routing tables, entry ledger, by-name refs):\n // duplicates would silently last-write-win on deploy, so fail the compile.\n assertUniqueNames(\n workflows.map((w) => w.id),\n 'workflow'\n );\n assertUniqueNames(\n segments.map((s) => s.id),\n 'segment'\n );\n // A message node names its template by channel and key, so the key is unique per channel:\n // an email and a push of one nudge may share it\n assertUniqueNames(\n templates.map((t) => `${t.channel}:${t.key}`),\n 'template'\n );\n assertUniqueNames(\n actions.map((a) => a.name),\n 'action'\n );\n\n // Deploy replaces the whole segment table, so every by-name reference must\n // resolve inside this bundle — catching it here turns a runtime \"Unknown\n // segment\" in some user's journey into a compile error.\n assertSegmentRefsResolve(workflows, segments);\n\n // A segment is evaluated by evaluating the segments it names; a cycle among\n // them has no evaluation order and would recurse until the stack ran out —\n // at run time, in some user's journey. Caught here, with the cycle spelled out.\n assertNoSegmentCycles(segments);\n\n // Payload predicates and profile predicates share the Condition type, so\n // placement is checked here: payload only inside where-trees, and where-trees\n // contain nothing but payload predicates and combinators.\n assertConditionPlacement(workflows, segments);\n\n // Same rationale as segments: the runtime registry is looked up by name, so\n // an action a workflow runs must be in the bundle — and it must be the very\n // definition listed there, or two same-named definitions are competing for\n // one identity (the IR names it; only the compile can tell them apart).\n assertActionRefsResolve(workflows, input.workflows, input.actions ?? []);\n\n return BundleIRSchema.parse({ irVersion: IR_VERSION, workflows, segments, templates, actions });\n}\n\nfunction assertUniqueNames(names: readonly string[], kind: string): void {\n const seen = new Set<string>();\n for (const name of names) {\n if (seen.has(name)) throw new Error(`compileBundle: duplicate ${kind} name '${name}'`);\n seen.add(name);\n }\n}\n\nfunction assertSegmentRefsResolve(\n workflows: readonly WorkflowIR[],\n segments: readonly SegmentIR[]\n): void {\n const defined = new Set(segments.map((s) => s.id));\n const check = (refs: ReadonlySet<string>, owner: string): void => {\n for (const ref of refs) {\n if (!defined.has(ref)) {\n throw new Error(\n `compileBundle: ${owner} references segment '${ref}' that is not in the bundle — pass it in 'segments'`\n );\n }\n }\n };\n for (const workflow of workflows) {\n check(referencedSegments(workflow), `workflow '${workflow.id}'`);\n }\n // Segments may reference other segments; those must resolve too\n for (const segment of segments) {\n const refs = new Set<string>();\n collectConditionRefs(segment.condition, refs);\n check(refs, `segment '${segment.id}'`);\n }\n}\n\nfunction referencedSegments(workflow: WorkflowIR): Set<string> {\n const refs = new Set<string>();\n if (workflow.trigger.type === 'segment') refs.add(workflow.trigger.segment);\n if (workflow.trigger.type === 'event' && workflow.trigger.filter !== undefined) {\n collectConditionRefs(workflow.trigger.filter, refs);\n }\n if (workflow.goal !== undefined) collectConditionRefs(workflow.goal.condition, refs);\n if (workflow.exitWhen !== undefined) collectConditionRefs(workflow.exitWhen, refs);\n collectNodeRefs(workflow.flow, refs);\n return refs;\n}\n\nfunction collectConditionRefs(condition: ConditionIR, into: Set<string>): void {\n switch (condition.type) {\n case 'and':\n case 'or':\n for (const child of condition.conditions) collectConditionRefs(child, into);\n break;\n case 'not':\n collectConditionRefs(condition.condition, into);\n break;\n case 'segment':\n into.add(condition.segment);\n break;\n default:\n break;\n }\n}\n\nfunction collectNodeRefs(nodes: readonly NodeIR[], into: Set<string>): void {\n for (const node of nodes) {\n switch (node.type) {\n case 'wait_until':\n collectConditionRefs(node.condition, into);\n if (Array.isArray(node.onTimeout)) collectNodeRefs(node.onTimeout, into);\n break;\n case 'branch':\n for (const branchCase of node.cases) {\n collectConditionRefs(branchCase.condition, into);\n collectNodeRefs(branchCase.flow, into);\n }\n if (node.otherwise) collectNodeRefs(node.otherwise, into);\n break;\n case 'filter':\n collectConditionRefs(node.condition, into);\n break;\n case 'cohort':\n for (const arm of node.arms) collectNodeRefs(arm.flow, into);\n break;\n default:\n break;\n }\n }\n}\n\nfunction assertActionRefsResolve(\n workflows: readonly WorkflowIR[],\n builders: readonly WorkflowBuilder[],\n actions: readonly ActionRef[]\n): void {\n const listed = new Map(actions.map((a) => [a.name, a]));\n workflows.forEach((workflow, i) => {\n for (const used of actionUses(builders[i])) {\n const manifest = listed.get(used.name);\n if (manifest === undefined) {\n throw new Error(\n `compileBundle: workflow '${workflow.id}' runs action '${used.name}' that is not in the bundle — pass it in 'actions'`\n );\n }\n // Identity, not source text: the printed handler depends on the bundler.\n // Every use is checked, not one per name — with two definitions under one\n // name, whichever is listed, the *other* node would run the wrong code.\n if (manifest !== used && manifest.handler !== used.handler) {\n throw new Error(\n `compileBundle: workflow '${workflow.id}' runs action '${used.name}' that is not the definition listed in 'actions' — two definitions are sharing that name`\n );\n }\n }\n });\n}\n\n/** Walk segment → segment references depth-first; the first back edge is reported as the cycle path. */\nfunction assertNoSegmentCycles(segments: readonly SegmentIR[]): void {\n const refsOf = new Map<string, Set<string>>();\n for (const segment of segments) {\n const refs = new Set<string>();\n collectConditionRefs(segment.condition, refs);\n refsOf.set(segment.id, refs);\n }\n const done = new Set<string>();\n const path: string[] = [];\n const visit = (id: string): void => {\n const at = path.indexOf(id);\n if (at !== -1) {\n const cycle = [...path.slice(at), id].join(' → ');\n throw new Error(`compileBundle: segments reference each other in a cycle: ${cycle}`);\n }\n if (done.has(id)) return;\n path.push(id);\n for (const ref of refsOf.get(id) ?? []) visit(ref);\n path.pop();\n done.add(id);\n };\n for (const id of refsOf.keys()) visit(id);\n}\n\n/* --------------------- payload / profile placement checks --------------------- */\n\nfunction assertConditionPlacement(\n workflows: readonly WorkflowIR[],\n segments: readonly SegmentIR[]\n): void {\n for (const workflow of workflows) {\n if (workflow.trigger.type === 'event') {\n if (workflow.trigger.where !== undefined) {\n assertWhereTree(workflow.trigger.where, `workflow '${workflow.id}' trigger where`);\n }\n if (workflow.trigger.filter !== undefined) {\n assertProfileTree(workflow.trigger.filter, `workflow '${workflow.id}' trigger filter`);\n }\n }\n if (workflow.goal !== undefined) {\n assertProfileTree(workflow.goal.condition, `workflow '${workflow.id}' goal`);\n }\n if (workflow.exitWhen !== undefined) {\n assertProfileTree(workflow.exitWhen, `workflow '${workflow.id}' exitWhen`);\n }\n assertNodeTrees(workflow.flow, `workflow '${workflow.id}'`);\n }\n for (const segment of segments) {\n assertProfileTree(segment.condition, `segment '${segment.id}'`);\n }\n}\n\n/** A profile-context tree: payload predicates may appear only under a performed.where. */\nfunction assertProfileTree(condition: ConditionIR, owner: string): void {\n switch (condition.type) {\n case 'and':\n case 'or':\n for (const child of condition.conditions) assertProfileTree(child, owner);\n break;\n case 'not':\n assertProfileTree(condition.condition, owner);\n break;\n case 'performed':\n if (condition.where !== undefined) assertWhereTree(condition.where, owner);\n break;\n case 'payload':\n throw new Error(\n `compileBundle: ${owner} uses a payload predicate outside a where clause — payload refs are only valid inside performed({ where }) or trigger.event({ where })`\n );\n default:\n break; // property / segment\n }\n}\n\n/** A where tree: payload predicates and combinators only — no profile facts in event scope. */\nfunction assertWhereTree(condition: ConditionIR, owner: string): void {\n switch (condition.type) {\n case 'and':\n case 'or':\n for (const child of condition.conditions) assertWhereTree(child, owner);\n break;\n case 'not':\n assertWhereTree(condition.condition, owner);\n break;\n case 'payload':\n break;\n default:\n throw new Error(\n `compileBundle: ${owner} where clause may only contain payload predicates and and/or/not, found '${condition.type}'`\n );\n }\n}\n\nfunction assertNodeTrees(nodes: readonly NodeIR[], owner: string): void {\n for (const node of nodes) {\n switch (node.type) {\n case 'wait_until':\n assertProfileTree(node.condition, owner);\n if (Array.isArray(node.onTimeout)) assertNodeTrees(node.onTimeout, owner);\n break;\n case 'branch':\n for (const branchCase of node.cases) {\n assertProfileTree(branchCase.condition, owner);\n assertNodeTrees(branchCase.flow, owner);\n }\n if (node.otherwise) assertNodeTrees(node.otherwise, owner);\n break;\n case 'filter':\n assertProfileTree(node.condition, owner);\n break;\n case 'cohort':\n for (const arm of node.arms) assertNodeTrees(arm.flow, owner);\n break;\n default:\n break;\n }\n }\n}\n"],"mappings":";;;;;;;;;;;AAqBA,SAAgB,cAAc,OAMjB;CACX,MAAM,YAAY,MAAM,YAAY,CAAC,EAAA,CAAG,KAAK,YAAY;EACvD,MAAM,EAAE,IAAI,YAAY,KAAK,SAAS;EACtC,MAAM,WAAsB;GAC1B,WAAA;GACA;GAEA,aAAaA,aAAAA,aAAa,UAAU;GACpC,WAAW;EACb;EACA,MAAM,SAAS;GACb,GAAI,MAAM,SAAS,KAAA,IAAY,EAAE,MAAM,KAAK,KAAK,IAAI,CAAC;GACtD,GAAI,MAAM,gBAAgB,KAAA,IAAY,EAAE,aAAa,KAAK,YAAY,IAAI,CAAC;GAC3E,GAAI,QAAQ,KAAA,IAAY,EAAE,IAAI,IAAI,CAAC;EACrC;EACA,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GAAG,SAAS,OAAO;EACpD,OAAO;CACT,CAAC;CAED,MAAM,aAAa,MAAM,aAAa,CAAC,EAAA,CAAG,KAAK,cAAc;EAC3D,WAAA;EACA,KAAK,SAAS;EACd,SAASC,kBAAAA,WAAW,SAAS;CAC/B,EAAE;CAGF,MAAM,WAAW,MAAM,WAAW,CAAC,EAAA,CAAG,KAAK,OAAO;EAAE,WAAA;EAAuB,MAAM,EAAE;CAAK,EAAE;CAE1F,MAAM,YAAY,MAAM,UAAU,KAAK,YAAY,QAAQ,KAAK,CAAC;CAIjE,kBACE,UAAU,KAAK,MAAM,EAAE,EAAE,GACzB,UACF;CACA,kBACE,SAAS,KAAK,MAAM,EAAE,EAAE,GACxB,SACF;CAGA,kBACE,UAAU,KAAK,MAAM,GAAG,EAAE,QAAQ,GAAG,EAAE,KAAK,GAC5C,UACF;CACA,kBACE,QAAQ,KAAK,MAAM,EAAE,IAAI,GACzB,QACF;CAKA,yBAAyB,WAAW,QAAQ;CAK5C,sBAAsB,QAAQ;CAK9B,yBAAyB,WAAW,QAAQ;CAM5C,wBAAwB,WAAW,MAAM,WAAW,MAAM,WAAW,CAAC,CAAC;CAEvE,OAAOC,WAAAA,SAAe,MAAM;EAAE,WAAA;EAAuB;EAAW;EAAU;EAAW;CAAQ,CAAC;AAChG;AAEA,SAAS,kBAAkB,OAA0B,MAAoB;CACvE,MAAM,uBAAO,IAAI,IAAY;CAC7B,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,KAAK,IAAI,IAAI,GAAG,MAAM,IAAI,MAAM,4BAA4B,KAAK,SAAS,KAAK,EAAE;EACrF,KAAK,IAAI,IAAI;CACf;AACF;AAEA,SAAS,yBACP,WACA,UACM;CACN,MAAM,UAAU,IAAI,IAAI,SAAS,KAAK,MAAM,EAAE,EAAE,CAAC;CACjD,MAAM,SAAS,MAA2B,UAAwB;EAChE,KAAK,MAAM,OAAO,MAChB,IAAI,CAAC,QAAQ,IAAI,GAAG,GAClB,MAAM,IAAI,MACR,kBAAkB,MAAM,uBAAuB,IAAI,oDACrD;CAGN;CACA,KAAK,MAAM,YAAY,WACrB,MAAM,mBAAmB,QAAQ,GAAG,aAAa,SAAS,GAAG,EAAE;CAGjE,KAAK,MAAM,WAAW,UAAU;EAC9B,MAAM,uBAAO,IAAI,IAAY;EAC7B,qBAAqB,QAAQ,WAAW,IAAI;EAC5C,MAAM,MAAM,YAAY,QAAQ,GAAG,EAAE;CACvC;AACF;AAEA,SAAS,mBAAmB,UAAmC;CAC7D,MAAM,uBAAO,IAAI,IAAY;CAC7B,IAAI,SAAS,QAAQ,SAAS,WAAW,KAAK,IAAI,SAAS,QAAQ,OAAO;CAC1E,IAAI,SAAS,QAAQ,SAAS,WAAW,SAAS,QAAQ,WAAW,KAAA,GACnE,qBAAqB,SAAS,QAAQ,QAAQ,IAAI;CAEpD,IAAI,SAAS,SAAS,KAAA,GAAW,qBAAqB,SAAS,KAAK,WAAW,IAAI;CACnF,IAAI,SAAS,aAAa,KAAA,GAAW,qBAAqB,SAAS,UAAU,IAAI;CACjF,gBAAgB,SAAS,MAAM,IAAI;CACnC,OAAO;AACT;AAEA,SAAS,qBAAqB,WAAwB,MAAyB;CAC7E,QAAQ,UAAU,MAAlB;EACE,KAAK;EACL,KAAK;GACH,KAAK,MAAM,SAAS,UAAU,YAAY,qBAAqB,OAAO,IAAI;GAC1E;EACF,KAAK;GACH,qBAAqB,UAAU,WAAW,IAAI;GAC9C;EACF,KAAK,WACH,KAAK,IAAI,UAAU,OAAO;CAI9B;AACF;AAEA,SAAS,gBAAgB,OAA0B,MAAyB;CAC1E,KAAK,MAAM,QAAQ,OACjB,QAAQ,KAAK,MAAb;EACE,KAAK;GACH,qBAAqB,KAAK,WAAW,IAAI;GACzC,IAAI,MAAM,QAAQ,KAAK,SAAS,GAAG,gBAAgB,KAAK,WAAW,IAAI;GACvE;EACF,KAAK;GACH,KAAK,MAAM,cAAc,KAAK,OAAO;IACnC,qBAAqB,WAAW,WAAW,IAAI;IAC/C,gBAAgB,WAAW,MAAM,IAAI;GACvC;GACA,IAAI,KAAK,WAAW,gBAAgB,KAAK,WAAW,IAAI;GACxD;EACF,KAAK;GACH,qBAAqB,KAAK,WAAW,IAAI;GACzC;EACF,KAAK,UACH,KAAK,MAAM,OAAO,KAAK,MAAM,gBAAgB,IAAI,MAAM,IAAI;CAI/D;AAEJ;AAEA,SAAS,wBACP,WACA,UACA,SACM;CACN,MAAM,SAAS,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;CACtD,UAAU,SAAS,UAAU,MAAM;EACjC,KAAK,MAAM,QAAQC,iBAAAA,WAAW,SAAS,EAAE,GAAG;GAC1C,MAAM,WAAW,OAAO,IAAI,KAAK,IAAI;GACrC,IAAI,aAAa,KAAA,GACf,MAAM,IAAI,MACR,4BAA4B,SAAS,GAAG,iBAAiB,KAAK,KAAK,mDACrE;GAKF,IAAI,aAAa,QAAQ,SAAS,YAAY,KAAK,SACjD,MAAM,IAAI,MACR,4BAA4B,SAAS,GAAG,iBAAiB,KAAK,KAAK,yFACrE;EAEJ;CACF,CAAC;AACH;;AAGA,SAAS,sBAAsB,UAAsC;CACnE,MAAM,yBAAS,IAAI,IAAyB;CAC5C,KAAK,MAAM,WAAW,UAAU;EAC9B,MAAM,uBAAO,IAAI,IAAY;EAC7B,qBAAqB,QAAQ,WAAW,IAAI;EAC5C,OAAO,IAAI,QAAQ,IAAI,IAAI;CAC7B;CACA,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,OAAiB,CAAC;CACxB,MAAM,SAAS,OAAqB;EAClC,MAAM,KAAK,KAAK,QAAQ,EAAE;EAC1B,IAAI,OAAO,IAAI;GACb,MAAM,QAAQ,CAAC,GAAG,KAAK,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,KAAK,KAAK;GAChD,MAAM,IAAI,MAAM,4DAA4D,OAAO;EACrF;EACA,IAAI,KAAK,IAAI,EAAE,GAAG;EAClB,KAAK,KAAK,EAAE;EACZ,KAAK,MAAM,OAAO,OAAO,IAAI,EAAE,KAAK,CAAC,GAAG,MAAM,GAAG;EACjD,KAAK,IAAI;EACT,KAAK,IAAI,EAAE;CACb;CACA,KAAK,MAAM,MAAM,OAAO,KAAK,GAAG,MAAM,EAAE;AAC1C;AAIA,SAAS,yBACP,WACA,UACM;CACN,KAAK,MAAM,YAAY,WAAW;EAChC,IAAI,SAAS,QAAQ,SAAS,SAAS;GACrC,IAAI,SAAS,QAAQ,UAAU,KAAA,GAC7B,gBAAgB,SAAS,QAAQ,OAAO,aAAa,SAAS,GAAG,gBAAgB;GAEnF,IAAI,SAAS,QAAQ,WAAW,KAAA,GAC9B,kBAAkB,SAAS,QAAQ,QAAQ,aAAa,SAAS,GAAG,iBAAiB;EAEzF;EACA,IAAI,SAAS,SAAS,KAAA,GACpB,kBAAkB,SAAS,KAAK,WAAW,aAAa,SAAS,GAAG,OAAO;EAE7E,IAAI,SAAS,aAAa,KAAA,GACxB,kBAAkB,SAAS,UAAU,aAAa,SAAS,GAAG,WAAW;EAE3E,gBAAgB,SAAS,MAAM,aAAa,SAAS,GAAG,EAAE;CAC5D;CACA,KAAK,MAAM,WAAW,UACpB,kBAAkB,QAAQ,WAAW,YAAY,QAAQ,GAAG,EAAE;AAElE;;AAGA,SAAS,kBAAkB,WAAwB,OAAqB;CACtE,QAAQ,UAAU,MAAlB;EACE,KAAK;EACL,KAAK;GACH,KAAK,MAAM,SAAS,UAAU,YAAY,kBAAkB,OAAO,KAAK;GACxE;EACF,KAAK;GACH,kBAAkB,UAAU,WAAW,KAAK;GAC5C;EACF,KAAK;GACH,IAAI,UAAU,UAAU,KAAA,GAAW,gBAAgB,UAAU,OAAO,KAAK;GACzE;EACF,KAAK,WACH,MAAM,IAAI,MACR,kBAAkB,MAAM,uIAC1B;CAGJ;AACF;;AAGA,SAAS,gBAAgB,WAAwB,OAAqB;CACpE,QAAQ,UAAU,MAAlB;EACE,KAAK;EACL,KAAK;GACH,KAAK,MAAM,SAAS,UAAU,YAAY,gBAAgB,OAAO,KAAK;GACtE;EACF,KAAK;GACH,gBAAgB,UAAU,WAAW,KAAK;GAC1C;EACF,KAAK,WACH;EACF,SACE,MAAM,IAAI,MACR,kBAAkB,MAAM,2EAA2E,UAAU,KAAK,EACpH;CACJ;AACF;AAEA,SAAS,gBAAgB,OAA0B,OAAqB;CACtE,KAAK,MAAM,QAAQ,OACjB,QAAQ,KAAK,MAAb;EACE,KAAK;GACH,kBAAkB,KAAK,WAAW,KAAK;GACvC,IAAI,MAAM,QAAQ,KAAK,SAAS,GAAG,gBAAgB,KAAK,WAAW,KAAK;GACxE;EACF,KAAK;GACH,KAAK,MAAM,cAAc,KAAK,OAAO;IACnC,kBAAkB,WAAW,WAAW,KAAK;IAC7C,gBAAgB,WAAW,MAAM,KAAK;GACxC;GACA,IAAI,KAAK,WAAW,gBAAgB,KAAK,WAAW,KAAK;GACzD;EACF,KAAK;GACH,kBAAkB,KAAK,WAAW,KAAK;GACvC;EACF,KAAK,UACH,KAAK,MAAM,OAAO,KAAK,MAAM,gBAAgB,IAAI,MAAM,KAAK;CAIhE;AAEJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle.d.cts","names":[],"sources":["../../src/dsl/bundle.ts"],"mappings":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"bundle.d.cts","names":[],"sources":["../../src/dsl/bundle.ts"],"mappings":";;;;;;;;;;;wBAqBgB,cAAc;EAC5B,oBAAoB;EACpB,oBAAoB;;EAEpB,qBAAqB;EACrB,mBAAmB;IACjB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle.d.mts","names":[],"sources":["../../src/dsl/bundle.ts"],"mappings":";;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"bundle.d.mts","names":[],"sources":["../../src/dsl/bundle.ts"],"mappings":";;;;;;;;;;;wBAqBgB,cAAc;EAC5B,oBAAoB;EACpB,oBAAoB;;EAEpB,qBAAqB;EACrB,mBAAmB;IACjB"}
|
package/dist/dsl/bundle.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { semanticHash } from "../hash.mjs";
|
|
2
1
|
import { CHANNEL_OF } from "../templates.mjs";
|
|
2
|
+
import { actionUses } from "./flow.mjs";
|
|
3
|
+
import { semanticHash } from "../hash.mjs";
|
|
3
4
|
import { BundleIR } from "../ir.mjs";
|
|
4
5
|
//#region src/dsl/bundle.ts
|
|
5
6
|
/**
|
|
@@ -31,8 +32,7 @@ function compileBundle(input) {
|
|
|
31
32
|
}));
|
|
32
33
|
const actions = (input.actions ?? []).map((a) => ({
|
|
33
34
|
irVersion: 2,
|
|
34
|
-
name: a.name
|
|
35
|
-
codeHash: a.codeHash
|
|
35
|
+
name: a.name
|
|
36
36
|
}));
|
|
37
37
|
const workflows = input.workflows.map((builder) => builder.toIR());
|
|
38
38
|
assertUniqueNames(workflows.map((w) => w.id), "workflow");
|
|
@@ -40,8 +40,9 @@ function compileBundle(input) {
|
|
|
40
40
|
assertUniqueNames(templates.map((t) => `${t.channel}:${t.key}`), "template");
|
|
41
41
|
assertUniqueNames(actions.map((a) => a.name), "action");
|
|
42
42
|
assertSegmentRefsResolve(workflows, segments);
|
|
43
|
+
assertNoSegmentCycles(segments);
|
|
43
44
|
assertConditionPlacement(workflows, segments);
|
|
44
|
-
assertActionRefsResolve(workflows, actions);
|
|
45
|
+
assertActionRefsResolve(workflows, input.workflows, input.actions ?? []);
|
|
45
46
|
return BundleIR.parse({
|
|
46
47
|
irVersion: 2,
|
|
47
48
|
workflows,
|
|
@@ -109,36 +110,39 @@ function collectNodeRefs(nodes, into) {
|
|
|
109
110
|
case "cohort": for (const arm of node.arms) collectNodeRefs(arm.flow, into);
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
|
-
function assertActionRefsResolve(workflows, actions) {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
const used
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (manifestHash === void 0) throw new Error(`compileBundle: workflow '${workflow.id}' runs action '${name}' that is not in the bundle — pass it in 'actions'`);
|
|
120
|
-
for (const codeHash of codeHashes) if (codeHash !== void 0 && codeHash !== manifestHash) throw new Error(`compileBundle: workflow '${workflow.id}' runs action '${name}' whose code differs from the one in 'actions' — two definitions are sharing that name`);
|
|
113
|
+
function assertActionRefsResolve(workflows, builders, actions) {
|
|
114
|
+
const listed = new Map(actions.map((a) => [a.name, a]));
|
|
115
|
+
workflows.forEach((workflow, i) => {
|
|
116
|
+
for (const used of actionUses(builders[i])) {
|
|
117
|
+
const manifest = listed.get(used.name);
|
|
118
|
+
if (manifest === void 0) throw new Error(`compileBundle: workflow '${workflow.id}' runs action '${used.name}' that is not in the bundle — pass it in 'actions'`);
|
|
119
|
+
if (manifest !== used && manifest.handler !== used.handler) throw new Error(`compileBundle: workflow '${workflow.id}' runs action '${used.name}' that is not the definition listed in 'actions' — two definitions are sharing that name`);
|
|
121
120
|
}
|
|
122
|
-
}
|
|
121
|
+
});
|
|
123
122
|
}
|
|
124
|
-
/**
|
|
125
|
-
function
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
break;
|
|
132
|
-
}
|
|
133
|
-
case "wait_until":
|
|
134
|
-
if (Array.isArray(node.onTimeout)) collectActionRefs(node.onTimeout, into);
|
|
135
|
-
break;
|
|
136
|
-
case "branch":
|
|
137
|
-
for (const branchCase of node.cases) collectActionRefs(branchCase.flow, into);
|
|
138
|
-
if (node.otherwise) collectActionRefs(node.otherwise, into);
|
|
139
|
-
break;
|
|
140
|
-
case "cohort": for (const arm of node.arms) collectActionRefs(arm.flow, into);
|
|
123
|
+
/** Walk segment → segment references depth-first; the first back edge is reported as the cycle path. */
|
|
124
|
+
function assertNoSegmentCycles(segments) {
|
|
125
|
+
const refsOf = /* @__PURE__ */ new Map();
|
|
126
|
+
for (const segment of segments) {
|
|
127
|
+
const refs = /* @__PURE__ */ new Set();
|
|
128
|
+
collectConditionRefs(segment.condition, refs);
|
|
129
|
+
refsOf.set(segment.id, refs);
|
|
141
130
|
}
|
|
131
|
+
const done = /* @__PURE__ */ new Set();
|
|
132
|
+
const path = [];
|
|
133
|
+
const visit = (id) => {
|
|
134
|
+
const at = path.indexOf(id);
|
|
135
|
+
if (at !== -1) {
|
|
136
|
+
const cycle = [...path.slice(at), id].join(" → ");
|
|
137
|
+
throw new Error(`compileBundle: segments reference each other in a cycle: ${cycle}`);
|
|
138
|
+
}
|
|
139
|
+
if (done.has(id)) return;
|
|
140
|
+
path.push(id);
|
|
141
|
+
for (const ref of refsOf.get(id) ?? []) visit(ref);
|
|
142
|
+
path.pop();
|
|
143
|
+
done.add(id);
|
|
144
|
+
};
|
|
145
|
+
for (const id of refsOf.keys()) visit(id);
|
|
142
146
|
}
|
|
143
147
|
function assertConditionPlacement(workflows, segments) {
|
|
144
148
|
for (const workflow of workflows) {
|
package/dist/dsl/bundle.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle.mjs","names":["BundleIRSchema"],"sources":["../../src/dsl/bundle.ts"],"sourcesContent":["import { semanticHash } from '../hash';\nimport {\n type BundleIR,\n BundleIR as BundleIRSchema,\n type ConditionIR,\n IR_VERSION,\n type NodeIR,\n type SegmentIR,\n type WorkflowIR,\n} from '../ir';\nimport { type AnyTemplate, CHANNEL_OF } from '../templates';\nimport type { ActionRef } from './action';\nimport type { SegmentInternal, SegmentRef } from './condition';\nimport type { WorkflowBuilder } from './workflow';\n\n/**\n * The deployment unit — compileBundle gathers workflows, segments and the\n * template manifest into one BundleIR snapshot (terraform-apply mental model);\n * the server diffs each definition by contentHash (see plan.ts).\n */\nexport function compileBundle(input: {\n workflows: readonly WorkflowBuilder[];\n segments?: readonly SegmentRef[];\n /** The template manifest: these definitions, by channel and id. */\n templates?: readonly AnyTemplate[];\n actions?: readonly ActionRef[];\n}): BundleIR {\n const segments = (input.segments ?? []).map((segment) => {\n const { id, definition, loc, meta } = segment as SegmentInternal;\n const compiled: SegmentIR = {\n irVersion: IR_VERSION,\n id,\n // Labels and provenance both live in meta, which stripMeta drops before hashing\n contentHash: semanticHash(definition),\n condition: definition,\n };\n const merged = {\n ...(meta?.name !== undefined ? { name: meta.name } : {}),\n ...(meta?.description !== undefined ? { description: meta.description } : {}),\n ...(loc !== undefined ? { loc } : {}),\n };\n if (Object.keys(merged).length > 0) compiled.meta = merged;\n return compiled;\n });\n\n const templates = (input.templates ?? []).map((template) => ({\n irVersion: IR_VERSION,\n key: template.id,\n channel: CHANNEL_OF[template.kind],\n }));\n\n // Manifest only — name and code-identity hash; the implementation ships in the Worker bundle, never in IR\n const actions = (input.actions ?? []).map((a) => ({\n irVersion: IR_VERSION,\n name: a.name,\n codeHash: a.codeHash,\n }));\n\n const workflows = input.workflows.map((builder) => builder.toIR());\n\n // Names are wire identity (routing tables, entry ledger, by-name refs):\n // duplicates would silently last-write-win on deploy, so fail the compile.\n assertUniqueNames(\n workflows.map((w) => w.id),\n 'workflow'\n );\n assertUniqueNames(\n segments.map((s) => s.id),\n 'segment'\n );\n // A message node names its template by channel and key, so the key is unique per channel:\n // an email and a push of one nudge may share it\n assertUniqueNames(\n templates.map((t) => `${t.channel}:${t.key}`),\n 'template'\n );\n assertUniqueNames(\n actions.map((a) => a.name),\n 'action'\n );\n\n // Deploy replaces the whole segment table, so every by-name reference must\n // resolve inside this bundle — catching it here turns a runtime \"Unknown\n // segment\" in some user's journey into a compile error.\n assertSegmentRefsResolve(workflows, segments);\n\n // Payload predicates and profile predicates share the Condition type, so\n // placement is checked here: payload only inside where-trees, and where-trees\n // contain nothing but payload predicates and combinators.\n assertConditionPlacement(workflows, segments);\n\n // Same rationale as segments: the runtime registry is looked up by name, so\n // an action a workflow runs must be in the bundle — and its codeHash must\n // agree, or two same-named definitions are competing for one identity.\n assertActionRefsResolve(workflows, actions);\n\n return BundleIRSchema.parse({ irVersion: IR_VERSION, workflows, segments, templates, actions });\n}\n\nfunction assertUniqueNames(names: readonly string[], kind: string): void {\n const seen = new Set<string>();\n for (const name of names) {\n if (seen.has(name)) throw new Error(`compileBundle: duplicate ${kind} name '${name}'`);\n seen.add(name);\n }\n}\n\nfunction assertSegmentRefsResolve(\n workflows: readonly WorkflowIR[],\n segments: readonly SegmentIR[]\n): void {\n const defined = new Set(segments.map((s) => s.id));\n const check = (refs: ReadonlySet<string>, owner: string): void => {\n for (const ref of refs) {\n if (!defined.has(ref)) {\n throw new Error(\n `compileBundle: ${owner} references segment '${ref}' that is not in the bundle — pass it in 'segments'`\n );\n }\n }\n };\n for (const workflow of workflows) {\n check(referencedSegments(workflow), `workflow '${workflow.id}'`);\n }\n // Segments may reference other segments; those must resolve too\n for (const segment of segments) {\n const refs = new Set<string>();\n collectConditionRefs(segment.condition, refs);\n check(refs, `segment '${segment.id}'`);\n }\n}\n\nfunction referencedSegments(workflow: WorkflowIR): Set<string> {\n const refs = new Set<string>();\n if (workflow.trigger.type === 'segment') refs.add(workflow.trigger.segment);\n if (workflow.trigger.type === 'event' && workflow.trigger.filter !== undefined) {\n collectConditionRefs(workflow.trigger.filter, refs);\n }\n if (workflow.goal !== undefined) collectConditionRefs(workflow.goal.condition, refs);\n if (workflow.exitWhen !== undefined) collectConditionRefs(workflow.exitWhen, refs);\n collectNodeRefs(workflow.flow, refs);\n return refs;\n}\n\nfunction collectConditionRefs(condition: ConditionIR, into: Set<string>): void {\n switch (condition.type) {\n case 'and':\n case 'or':\n for (const child of condition.conditions) collectConditionRefs(child, into);\n break;\n case 'not':\n collectConditionRefs(condition.condition, into);\n break;\n case 'segment':\n into.add(condition.segment);\n break;\n default:\n break;\n }\n}\n\nfunction collectNodeRefs(nodes: readonly NodeIR[], into: Set<string>): void {\n for (const node of nodes) {\n switch (node.type) {\n case 'wait_until':\n collectConditionRefs(node.condition, into);\n if (Array.isArray(node.onTimeout)) collectNodeRefs(node.onTimeout, into);\n break;\n case 'branch':\n for (const branchCase of node.cases) {\n collectConditionRefs(branchCase.condition, into);\n collectNodeRefs(branchCase.flow, into);\n }\n if (node.otherwise) collectNodeRefs(node.otherwise, into);\n break;\n case 'filter':\n collectConditionRefs(node.condition, into);\n break;\n case 'cohort':\n for (const arm of node.arms) collectNodeRefs(arm.flow, into);\n break;\n default:\n break;\n }\n }\n}\n\nfunction assertActionRefsResolve(\n workflows: readonly WorkflowIR[],\n actions: readonly { name: string; codeHash: string }[]\n): void {\n const defined = new Map(actions.map((a) => [a.name, a.codeHash]));\n for (const workflow of workflows) {\n const used = new Map<string, Set<string | undefined>>();\n collectActionRefs(workflow.flow, used);\n for (const [name, codeHashes] of used) {\n const manifestHash = defined.get(name);\n if (manifestHash === undefined) {\n throw new Error(\n `compileBundle: workflow '${workflow.id}' runs action '${name}' that is not in the bundle — pass it in 'actions'`\n );\n }\n // Every node's hash is checked, not just one per name: with two\n // definitions under one name, whichever the manifest holds, the *other*\n // node is the one that would meet a mismatched implementation at run time.\n for (const codeHash of codeHashes) {\n if (codeHash !== undefined && codeHash !== manifestHash) {\n throw new Error(\n `compileBundle: workflow '${workflow.id}' runs action '${name}' whose code differs from the one in 'actions' — two definitions are sharing that name`\n );\n }\n }\n }\n }\n}\n\n/** name → every codeHash seen under it; a name with two is exactly the case worth failing on. */\nfunction collectActionRefs(\n nodes: readonly NodeIR[],\n into: Map<string, Set<string | undefined>>\n): void {\n for (const node of nodes) {\n switch (node.type) {\n case 'action': {\n const seen = into.get(node.action) ?? new Set<string | undefined>();\n seen.add(node.codeHash);\n into.set(node.action, seen);\n break;\n }\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) collectActionRefs(node.onTimeout, into);\n break;\n case 'branch':\n for (const branchCase of node.cases) collectActionRefs(branchCase.flow, into);\n if (node.otherwise) collectActionRefs(node.otherwise, into);\n break;\n case 'cohort':\n for (const arm of node.arms) collectActionRefs(arm.flow, into);\n break;\n default:\n break;\n }\n }\n}\n\n/* --------------------- payload / profile placement checks --------------------- */\n\nfunction assertConditionPlacement(\n workflows: readonly WorkflowIR[],\n segments: readonly SegmentIR[]\n): void {\n for (const workflow of workflows) {\n if (workflow.trigger.type === 'event') {\n if (workflow.trigger.where !== undefined) {\n assertWhereTree(workflow.trigger.where, `workflow '${workflow.id}' trigger where`);\n }\n if (workflow.trigger.filter !== undefined) {\n assertProfileTree(workflow.trigger.filter, `workflow '${workflow.id}' trigger filter`);\n }\n }\n if (workflow.goal !== undefined) {\n assertProfileTree(workflow.goal.condition, `workflow '${workflow.id}' goal`);\n }\n if (workflow.exitWhen !== undefined) {\n assertProfileTree(workflow.exitWhen, `workflow '${workflow.id}' exitWhen`);\n }\n assertNodeTrees(workflow.flow, `workflow '${workflow.id}'`);\n }\n for (const segment of segments) {\n assertProfileTree(segment.condition, `segment '${segment.id}'`);\n }\n}\n\n/** A profile-context tree: payload predicates may appear only under a performed.where. */\nfunction assertProfileTree(condition: ConditionIR, owner: string): void {\n switch (condition.type) {\n case 'and':\n case 'or':\n for (const child of condition.conditions) assertProfileTree(child, owner);\n break;\n case 'not':\n assertProfileTree(condition.condition, owner);\n break;\n case 'performed':\n if (condition.where !== undefined) assertWhereTree(condition.where, owner);\n break;\n case 'payload':\n throw new Error(\n `compileBundle: ${owner} uses a payload predicate outside a where clause — payload refs are only valid inside performed({ where }) or trigger.event({ where })`\n );\n default:\n break; // property / segment\n }\n}\n\n/** A where tree: payload predicates and combinators only — no profile facts in event scope. */\nfunction assertWhereTree(condition: ConditionIR, owner: string): void {\n switch (condition.type) {\n case 'and':\n case 'or':\n for (const child of condition.conditions) assertWhereTree(child, owner);\n break;\n case 'not':\n assertWhereTree(condition.condition, owner);\n break;\n case 'payload':\n break;\n default:\n throw new Error(\n `compileBundle: ${owner} where clause may only contain payload predicates and and/or/not, found '${condition.type}'`\n );\n }\n}\n\nfunction assertNodeTrees(nodes: readonly NodeIR[], owner: string): void {\n for (const node of nodes) {\n switch (node.type) {\n case 'wait_until':\n assertProfileTree(node.condition, owner);\n if (Array.isArray(node.onTimeout)) assertNodeTrees(node.onTimeout, owner);\n break;\n case 'branch':\n for (const branchCase of node.cases) {\n assertProfileTree(branchCase.condition, owner);\n assertNodeTrees(branchCase.flow, owner);\n }\n if (node.otherwise) assertNodeTrees(node.otherwise, owner);\n break;\n case 'filter':\n assertProfileTree(node.condition, owner);\n break;\n case 'cohort':\n for (const arm of node.arms) assertNodeTrees(arm.flow, owner);\n break;\n default:\n break;\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"bundle.mjs","names":["BundleIRSchema"],"sources":["../../src/dsl/bundle.ts"],"sourcesContent":["import { semanticHash } from '../hash';\nimport {\n type BundleIR,\n BundleIR as BundleIRSchema,\n type ConditionIR,\n IR_VERSION,\n type NodeIR,\n type SegmentIR,\n type WorkflowIR,\n} from '../ir';\nimport { type AnyTemplate, CHANNEL_OF } from '../templates';\nimport type { ActionRef } from './action';\nimport type { SegmentInternal, SegmentRef } from './condition';\nimport { actionUses } from './flow';\nimport type { WorkflowBuilder } from './workflow';\n\n/**\n * The deployment unit — compileBundle gathers workflows, segments and the\n * template manifest into one BundleIR snapshot (terraform-apply mental model);\n * the server diffs each definition by contentHash (see plan.ts).\n */\nexport function compileBundle(input: {\n workflows: readonly WorkflowBuilder[];\n segments?: readonly SegmentRef[];\n /** The template manifest: these definitions, by channel and id. */\n templates?: readonly AnyTemplate[];\n actions?: readonly ActionRef[];\n}): BundleIR {\n const segments = (input.segments ?? []).map((segment) => {\n const { id, definition, loc, meta } = segment as SegmentInternal;\n const compiled: SegmentIR = {\n irVersion: IR_VERSION,\n id,\n // Labels and provenance both live in meta, which stripMeta drops before hashing\n contentHash: semanticHash(definition),\n condition: definition,\n };\n const merged = {\n ...(meta?.name !== undefined ? { name: meta.name } : {}),\n ...(meta?.description !== undefined ? { description: meta.description } : {}),\n ...(loc !== undefined ? { loc } : {}),\n };\n if (Object.keys(merged).length > 0) compiled.meta = merged;\n return compiled;\n });\n\n const templates = (input.templates ?? []).map((template) => ({\n irVersion: IR_VERSION,\n key: template.id,\n channel: CHANNEL_OF[template.kind],\n }));\n\n // Manifest only — names; the implementation ships in the Worker bundle, never in IR\n const actions = (input.actions ?? []).map((a) => ({ irVersion: IR_VERSION, name: a.name }));\n\n const workflows = input.workflows.map((builder) => builder.toIR());\n\n // Names are wire identity (routing tables, entry ledger, by-name refs):\n // duplicates would silently last-write-win on deploy, so fail the compile.\n assertUniqueNames(\n workflows.map((w) => w.id),\n 'workflow'\n );\n assertUniqueNames(\n segments.map((s) => s.id),\n 'segment'\n );\n // A message node names its template by channel and key, so the key is unique per channel:\n // an email and a push of one nudge may share it\n assertUniqueNames(\n templates.map((t) => `${t.channel}:${t.key}`),\n 'template'\n );\n assertUniqueNames(\n actions.map((a) => a.name),\n 'action'\n );\n\n // Deploy replaces the whole segment table, so every by-name reference must\n // resolve inside this bundle — catching it here turns a runtime \"Unknown\n // segment\" in some user's journey into a compile error.\n assertSegmentRefsResolve(workflows, segments);\n\n // A segment is evaluated by evaluating the segments it names; a cycle among\n // them has no evaluation order and would recurse until the stack ran out —\n // at run time, in some user's journey. Caught here, with the cycle spelled out.\n assertNoSegmentCycles(segments);\n\n // Payload predicates and profile predicates share the Condition type, so\n // placement is checked here: payload only inside where-trees, and where-trees\n // contain nothing but payload predicates and combinators.\n assertConditionPlacement(workflows, segments);\n\n // Same rationale as segments: the runtime registry is looked up by name, so\n // an action a workflow runs must be in the bundle — and it must be the very\n // definition listed there, or two same-named definitions are competing for\n // one identity (the IR names it; only the compile can tell them apart).\n assertActionRefsResolve(workflows, input.workflows, input.actions ?? []);\n\n return BundleIRSchema.parse({ irVersion: IR_VERSION, workflows, segments, templates, actions });\n}\n\nfunction assertUniqueNames(names: readonly string[], kind: string): void {\n const seen = new Set<string>();\n for (const name of names) {\n if (seen.has(name)) throw new Error(`compileBundle: duplicate ${kind} name '${name}'`);\n seen.add(name);\n }\n}\n\nfunction assertSegmentRefsResolve(\n workflows: readonly WorkflowIR[],\n segments: readonly SegmentIR[]\n): void {\n const defined = new Set(segments.map((s) => s.id));\n const check = (refs: ReadonlySet<string>, owner: string): void => {\n for (const ref of refs) {\n if (!defined.has(ref)) {\n throw new Error(\n `compileBundle: ${owner} references segment '${ref}' that is not in the bundle — pass it in 'segments'`\n );\n }\n }\n };\n for (const workflow of workflows) {\n check(referencedSegments(workflow), `workflow '${workflow.id}'`);\n }\n // Segments may reference other segments; those must resolve too\n for (const segment of segments) {\n const refs = new Set<string>();\n collectConditionRefs(segment.condition, refs);\n check(refs, `segment '${segment.id}'`);\n }\n}\n\nfunction referencedSegments(workflow: WorkflowIR): Set<string> {\n const refs = new Set<string>();\n if (workflow.trigger.type === 'segment') refs.add(workflow.trigger.segment);\n if (workflow.trigger.type === 'event' && workflow.trigger.filter !== undefined) {\n collectConditionRefs(workflow.trigger.filter, refs);\n }\n if (workflow.goal !== undefined) collectConditionRefs(workflow.goal.condition, refs);\n if (workflow.exitWhen !== undefined) collectConditionRefs(workflow.exitWhen, refs);\n collectNodeRefs(workflow.flow, refs);\n return refs;\n}\n\nfunction collectConditionRefs(condition: ConditionIR, into: Set<string>): void {\n switch (condition.type) {\n case 'and':\n case 'or':\n for (const child of condition.conditions) collectConditionRefs(child, into);\n break;\n case 'not':\n collectConditionRefs(condition.condition, into);\n break;\n case 'segment':\n into.add(condition.segment);\n break;\n default:\n break;\n }\n}\n\nfunction collectNodeRefs(nodes: readonly NodeIR[], into: Set<string>): void {\n for (const node of nodes) {\n switch (node.type) {\n case 'wait_until':\n collectConditionRefs(node.condition, into);\n if (Array.isArray(node.onTimeout)) collectNodeRefs(node.onTimeout, into);\n break;\n case 'branch':\n for (const branchCase of node.cases) {\n collectConditionRefs(branchCase.condition, into);\n collectNodeRefs(branchCase.flow, into);\n }\n if (node.otherwise) collectNodeRefs(node.otherwise, into);\n break;\n case 'filter':\n collectConditionRefs(node.condition, into);\n break;\n case 'cohort':\n for (const arm of node.arms) collectNodeRefs(arm.flow, into);\n break;\n default:\n break;\n }\n }\n}\n\nfunction assertActionRefsResolve(\n workflows: readonly WorkflowIR[],\n builders: readonly WorkflowBuilder[],\n actions: readonly ActionRef[]\n): void {\n const listed = new Map(actions.map((a) => [a.name, a]));\n workflows.forEach((workflow, i) => {\n for (const used of actionUses(builders[i])) {\n const manifest = listed.get(used.name);\n if (manifest === undefined) {\n throw new Error(\n `compileBundle: workflow '${workflow.id}' runs action '${used.name}' that is not in the bundle — pass it in 'actions'`\n );\n }\n // Identity, not source text: the printed handler depends on the bundler.\n // Every use is checked, not one per name — with two definitions under one\n // name, whichever is listed, the *other* node would run the wrong code.\n if (manifest !== used && manifest.handler !== used.handler) {\n throw new Error(\n `compileBundle: workflow '${workflow.id}' runs action '${used.name}' that is not the definition listed in 'actions' — two definitions are sharing that name`\n );\n }\n }\n });\n}\n\n/** Walk segment → segment references depth-first; the first back edge is reported as the cycle path. */\nfunction assertNoSegmentCycles(segments: readonly SegmentIR[]): void {\n const refsOf = new Map<string, Set<string>>();\n for (const segment of segments) {\n const refs = new Set<string>();\n collectConditionRefs(segment.condition, refs);\n refsOf.set(segment.id, refs);\n }\n const done = new Set<string>();\n const path: string[] = [];\n const visit = (id: string): void => {\n const at = path.indexOf(id);\n if (at !== -1) {\n const cycle = [...path.slice(at), id].join(' → ');\n throw new Error(`compileBundle: segments reference each other in a cycle: ${cycle}`);\n }\n if (done.has(id)) return;\n path.push(id);\n for (const ref of refsOf.get(id) ?? []) visit(ref);\n path.pop();\n done.add(id);\n };\n for (const id of refsOf.keys()) visit(id);\n}\n\n/* --------------------- payload / profile placement checks --------------------- */\n\nfunction assertConditionPlacement(\n workflows: readonly WorkflowIR[],\n segments: readonly SegmentIR[]\n): void {\n for (const workflow of workflows) {\n if (workflow.trigger.type === 'event') {\n if (workflow.trigger.where !== undefined) {\n assertWhereTree(workflow.trigger.where, `workflow '${workflow.id}' trigger where`);\n }\n if (workflow.trigger.filter !== undefined) {\n assertProfileTree(workflow.trigger.filter, `workflow '${workflow.id}' trigger filter`);\n }\n }\n if (workflow.goal !== undefined) {\n assertProfileTree(workflow.goal.condition, `workflow '${workflow.id}' goal`);\n }\n if (workflow.exitWhen !== undefined) {\n assertProfileTree(workflow.exitWhen, `workflow '${workflow.id}' exitWhen`);\n }\n assertNodeTrees(workflow.flow, `workflow '${workflow.id}'`);\n }\n for (const segment of segments) {\n assertProfileTree(segment.condition, `segment '${segment.id}'`);\n }\n}\n\n/** A profile-context tree: payload predicates may appear only under a performed.where. */\nfunction assertProfileTree(condition: ConditionIR, owner: string): void {\n switch (condition.type) {\n case 'and':\n case 'or':\n for (const child of condition.conditions) assertProfileTree(child, owner);\n break;\n case 'not':\n assertProfileTree(condition.condition, owner);\n break;\n case 'performed':\n if (condition.where !== undefined) assertWhereTree(condition.where, owner);\n break;\n case 'payload':\n throw new Error(\n `compileBundle: ${owner} uses a payload predicate outside a where clause — payload refs are only valid inside performed({ where }) or trigger.event({ where })`\n );\n default:\n break; // property / segment\n }\n}\n\n/** A where tree: payload predicates and combinators only — no profile facts in event scope. */\nfunction assertWhereTree(condition: ConditionIR, owner: string): void {\n switch (condition.type) {\n case 'and':\n case 'or':\n for (const child of condition.conditions) assertWhereTree(child, owner);\n break;\n case 'not':\n assertWhereTree(condition.condition, owner);\n break;\n case 'payload':\n break;\n default:\n throw new Error(\n `compileBundle: ${owner} where clause may only contain payload predicates and and/or/not, found '${condition.type}'`\n );\n }\n}\n\nfunction assertNodeTrees(nodes: readonly NodeIR[], owner: string): void {\n for (const node of nodes) {\n switch (node.type) {\n case 'wait_until':\n assertProfileTree(node.condition, owner);\n if (Array.isArray(node.onTimeout)) assertNodeTrees(node.onTimeout, owner);\n break;\n case 'branch':\n for (const branchCase of node.cases) {\n assertProfileTree(branchCase.condition, owner);\n assertNodeTrees(branchCase.flow, owner);\n }\n if (node.otherwise) assertNodeTrees(node.otherwise, owner);\n break;\n case 'filter':\n assertProfileTree(node.condition, owner);\n break;\n case 'cohort':\n for (const arm of node.arms) assertNodeTrees(arm.flow, owner);\n break;\n default:\n break;\n }\n }\n}\n"],"mappings":"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"}
|
package/dist/dsl/condition.cjs
CHANGED
|
@@ -77,14 +77,25 @@ function contains(ref, value) {
|
|
|
77
77
|
function notContains(ref, value) {
|
|
78
78
|
return prop(ref, "not_contains", value, void 0, require_provenance.captureLoc(notContains));
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* @internal A duration that has to elapse — a wait's timeout, a goal's window, a performed
|
|
82
|
+
* window. Zero parses as a valid duration but means "never" or "always" depending on the node,
|
|
83
|
+
* and either way it is not what the author meant.
|
|
84
|
+
*/
|
|
85
|
+
function positiveDurationIR(value, what) {
|
|
86
|
+
const ir = require_dsl_base.durationIR(value);
|
|
87
|
+
if (ir.ms <= 0) throw new Error(`${what} must be positive, got '${value}'`);
|
|
88
|
+
return ir;
|
|
89
|
+
}
|
|
80
90
|
function performed(event, whereOrOpts, maybeOpts) {
|
|
81
91
|
const where = typeof whereOrOpts === "function" ? whereOrOpts : void 0;
|
|
82
92
|
const opts = typeof whereOrOpts === "function" ? maybeOpts : whereOrOpts;
|
|
93
|
+
if (opts?.count !== void 0 && (!Number.isInteger(opts.count) || opts.count < 1)) throw new Error(`performed(): count must be a positive integer, got ${String(opts.count)}`);
|
|
83
94
|
return cond({
|
|
84
95
|
type: "performed",
|
|
85
96
|
event: event.name,
|
|
86
97
|
...where !== void 0 ? { where: condIR(where(require_dsl_refs.payloadRefs())) } : {},
|
|
87
|
-
...opts?.within !== void 0 ? { within:
|
|
98
|
+
...opts?.within !== void 0 ? { within: positiveDurationIR(opts.within, "performed(): within") } : {},
|
|
88
99
|
...opts?.count !== void 0 ? { count: opts.count } : {}
|
|
89
100
|
}, require_provenance.captureLoc(performed));
|
|
90
101
|
}
|
|
@@ -173,6 +184,7 @@ exports.notExists = notExists;
|
|
|
173
184
|
exports.notInArray = notInArray;
|
|
174
185
|
exports.or = or;
|
|
175
186
|
exports.performed = performed;
|
|
187
|
+
exports.positiveDurationIR = positiveDurationIR;
|
|
176
188
|
exports.reachable = reachable;
|
|
177
189
|
exports.segment = segment;
|
|
178
190
|
|
|
@@ -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 *\n * (`.prefer([push.tip, 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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"}
|
|
1
|
+
{"version":3,"file":"condition.cjs","names":["PAYLOAD_PATH","captureLoc","durationIR","payloadRefs"],"sources":["../../src/dsl/condition.ts"],"sourcesContent":["import type { ConditionIR, DurationIR, 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 /** Only occurrences this recent count; must be a positive duration. */\n within?: Duration;\n /** At least this many occurrences; a positive integer (1 is the default). */\n count?: number;\n}\n\n/**\n * @internal A duration that has to elapse — a wait's timeout, a goal's window, a performed\n * window. Zero parses as a valid duration but means \"never\" or \"always\" depending on the node,\n * and either way it is not what the author meant.\n */\nexport function positiveDurationIR(value: Duration, what: string): DurationIR {\n const ir = durationIR(value);\n if (ir.ms <= 0) throw new Error(`${what} must be positive, got '${value}'`);\n return ir;\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 // count: 0 is satisfied by nobody having done anything, and a fraction is never reached\n if (opts?.count !== undefined && (!Number.isInteger(opts.count) || opts.count < 1)) {\n throw new Error(`performed(): count must be a positive integer, got ${String(opts.count)}`);\n }\n return cond(\n {\n type: 'performed',\n event: event.name,\n ...(where !== undefined ? { where: condIR(where(payloadRefs<P>())) } : {}),\n ...(opts?.within !== undefined\n ? { within: positiveDurationIR(opts.within, 'performed(): within') }\n : {}),\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([push.tip, 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/condition.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConditionIR, SourceLocIR } from "../ir.cjs";
|
|
1
|
+
import { ConditionIR, DurationIR, SourceLocIR } from "../ir.cjs";
|
|
2
2
|
import { Duration } from "./base.cjs";
|
|
3
3
|
import { EventRef, PayloadRef, PayloadRefs, UserPropertyRef } from "./refs.cjs";
|
|
4
4
|
import { Channel } from "./template.cjs";
|
|
@@ -44,9 +44,17 @@ export declare function notExists(ref: CondRef<unknown>): Condition;
|
|
|
44
44
|
export declare function contains<T extends string>(ref: CondRef<T>, value: string): Condition;
|
|
45
45
|
export declare function notContains<T extends string>(ref: CondRef<T>, value: string): Condition;
|
|
46
46
|
export interface PerformedOptions {
|
|
47
|
+
/** Only occurrences this recent count; must be a positive duration. */
|
|
47
48
|
within?: Duration;
|
|
49
|
+
/** At least this many occurrences; a positive integer (1 is the default). */
|
|
48
50
|
count?: number;
|
|
49
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* @internal A duration that has to elapse — a wait's timeout, a goal's window, a performed
|
|
54
|
+
* window. Zero parses as a valid duration but means "never" or "always" depending on the node,
|
|
55
|
+
* and either way it is not what the author meant.
|
|
56
|
+
*/
|
|
57
|
+
export declare function positiveDurationIR(value: Duration, what: string): DurationIR;
|
|
50
58
|
/** 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. */
|
|
51
59
|
export type PayloadWhere<P> = (p: PayloadRefs<P>) => Condition;
|
|
52
60
|
/**
|
|
@@ -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
|
|
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;;EAEf,SAAS;;EAET;;;;;;;wBAQc,mBAAmB,OAAO,UAAU,eAAe;;YAOvD,aAAa,MAAM,GAAG,YAAY,OAAO;;;;;;;;;;;;;;;;;;;;;;wBAuBrC,UAAU,GAAG,OAAO,SAAS,IAAI,OAAO,mBAAmB;wBAC3D,UAAU,GACxB,OAAO,SAAS,IAChB,OAAO,aAAa,IACpB,OAAO,mBACN;;;;;;;;;;;;;wBAsCa,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"}
|
package/dist/dsl/condition.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConditionIR, SourceLocIR } from "../ir.mjs";
|
|
1
|
+
import { ConditionIR, DurationIR, SourceLocIR } from "../ir.mjs";
|
|
2
2
|
import { Duration } from "./base.mjs";
|
|
3
3
|
import { EventRef, PayloadRef, PayloadRefs, UserPropertyRef } from "./refs.mjs";
|
|
4
4
|
import { Channel } from "./template.mjs";
|
|
@@ -44,9 +44,17 @@ export declare function notExists(ref: CondRef<unknown>): Condition;
|
|
|
44
44
|
export declare function contains<T extends string>(ref: CondRef<T>, value: string): Condition;
|
|
45
45
|
export declare function notContains<T extends string>(ref: CondRef<T>, value: string): Condition;
|
|
46
46
|
export interface PerformedOptions {
|
|
47
|
+
/** Only occurrences this recent count; must be a positive duration. */
|
|
47
48
|
within?: Duration;
|
|
49
|
+
/** At least this many occurrences; a positive integer (1 is the default). */
|
|
48
50
|
count?: number;
|
|
49
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* @internal A duration that has to elapse — a wait's timeout, a goal's window, a performed
|
|
54
|
+
* window. Zero parses as a valid duration but means "never" or "always" depending on the node,
|
|
55
|
+
* and either way it is not what the author meant.
|
|
56
|
+
*/
|
|
57
|
+
export declare function positiveDurationIR(value: Duration, what: string): DurationIR;
|
|
50
58
|
/** 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. */
|
|
51
59
|
export type PayloadWhere<P> = (p: PayloadRefs<P>) => Condition;
|
|
52
60
|
/**
|
|
@@ -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
|
|
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;;EAEf,SAAS;;EAET;;;;;;;wBAQc,mBAAmB,OAAO,UAAU,eAAe;;YAOvD,aAAa,MAAM,GAAG,YAAY,OAAO;;;;;;;;;;;;;;;;;;;;;;wBAuBrC,UAAU,GAAG,OAAO,SAAS,IAAI,OAAO,mBAAmB;wBAC3D,UAAU,GACxB,OAAO,SAAS,IAChB,OAAO,aAAa,IACpB,OAAO,mBACN;;;;;;;;;;;;;wBAsCa,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"}
|
package/dist/dsl/condition.mjs
CHANGED
|
@@ -76,14 +76,25 @@ function contains(ref, value) {
|
|
|
76
76
|
function notContains(ref, value) {
|
|
77
77
|
return prop(ref, "not_contains", value, void 0, captureLoc(notContains));
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* @internal A duration that has to elapse — a wait's timeout, a goal's window, a performed
|
|
81
|
+
* window. Zero parses as a valid duration but means "never" or "always" depending on the node,
|
|
82
|
+
* and either way it is not what the author meant.
|
|
83
|
+
*/
|
|
84
|
+
function positiveDurationIR(value, what) {
|
|
85
|
+
const ir = durationIR(value);
|
|
86
|
+
if (ir.ms <= 0) throw new Error(`${what} must be positive, got '${value}'`);
|
|
87
|
+
return ir;
|
|
88
|
+
}
|
|
79
89
|
function performed(event, whereOrOpts, maybeOpts) {
|
|
80
90
|
const where = typeof whereOrOpts === "function" ? whereOrOpts : void 0;
|
|
81
91
|
const opts = typeof whereOrOpts === "function" ? maybeOpts : whereOrOpts;
|
|
92
|
+
if (opts?.count !== void 0 && (!Number.isInteger(opts.count) || opts.count < 1)) throw new Error(`performed(): count must be a positive integer, got ${String(opts.count)}`);
|
|
82
93
|
return cond({
|
|
83
94
|
type: "performed",
|
|
84
95
|
event: event.name,
|
|
85
96
|
...where !== void 0 ? { where: condIR(where(payloadRefs())) } : {},
|
|
86
|
-
...opts?.within !== void 0 ? { within:
|
|
97
|
+
...opts?.within !== void 0 ? { within: positiveDurationIR(opts.within, "performed(): within") } : {},
|
|
87
98
|
...opts?.count !== void 0 ? { count: opts.count } : {}
|
|
88
99
|
}, captureLoc(performed));
|
|
89
100
|
}
|
|
@@ -153,6 +164,6 @@ function segment({ id, condition, name, description }) {
|
|
|
153
164
|
};
|
|
154
165
|
}
|
|
155
166
|
//#endregion
|
|
156
|
-
export { and, between, condIR, contains, eq, exists, gt, gte, inArray, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, performed, reachable, segment };
|
|
167
|
+
export { and, between, condIR, contains, eq, exists, gt, gte, inArray, lt, lte, ne, not, notBetween, notContains, notExists, notInArray, or, performed, positiveDurationIR, reachable, segment };
|
|
157
168
|
|
|
158
169
|
//# sourceMappingURL=condition.mjs.map
|