fyflow-orchestration 0.12.0 → 0.14.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/AGENTS.md +239 -21
- package/README.md +1 -1
- package/dist/browser/chunks/{chunk-4JFQX7DL.js → chunk-5KUYTCXI.js} +177 -30
- package/dist/browser/index.js +12 -4
- package/dist/browser/scheduler/index.js +17 -6
- package/dist/node/chunks/{chunk-4JFQX7DL.js → chunk-5KUYTCXI.js} +177 -30
- package/dist/node/index.js +12 -4
- package/dist/node/scheduler/index.js +17 -6
- package/dist/types/src/definition/condition.d.ts +7 -4
- package/dist/types/src/definition/registry.d.ts +18 -10
- package/dist/types/src/definition/types.d.ts +56 -12
- package/dist/types/src/definition/validate.d.ts +3 -2
- package/dist/types/src/scheduler/runtime.d.ts +14 -10
- package/package.json +3 -3
|
@@ -402,7 +402,13 @@ function evalLeaf(scope, p, unknownOp) {
|
|
|
402
402
|
else if (typeof v === "string") v = v.length;
|
|
403
403
|
else return false;
|
|
404
404
|
}
|
|
405
|
-
|
|
405
|
+
let w;
|
|
406
|
+
if (p.valueFrom !== void 0) {
|
|
407
|
+
w = readKeyPath(scope, p.valueFrom);
|
|
408
|
+
if (w === void 0 || typeof w === "object" && w !== null) return false;
|
|
409
|
+
} else {
|
|
410
|
+
w = p.value;
|
|
411
|
+
}
|
|
406
412
|
switch (p.op) {
|
|
407
413
|
// **Type-STRICT, and it is a departure measured rather than assumed.** Under JS comparison
|
|
408
414
|
// `{confidence: "0.4"} < 0.5` is true and every `typeof`-guarded predicate in this tree is false;
|
|
@@ -755,8 +761,9 @@ function gateTypeCollisions(x) {
|
|
|
755
761
|
return names.filter((n) => Object.prototype.hasOwnProperty.call(BUILT_IN_GATE_TYPES, n)).sort();
|
|
756
762
|
}
|
|
757
763
|
var PREDICATE_NAME_KEYS = ["predicate"];
|
|
758
|
-
var PREDICATE_LEAF_KEYS = ["path", "op", "value", "default", "length"];
|
|
759
|
-
var PREDICATE_LEAF_REQUIRED = ["path", "op"
|
|
764
|
+
var PREDICATE_LEAF_KEYS = ["path", "op", "value", "valueFrom", "default", "length"];
|
|
765
|
+
var PREDICATE_LEAF_REQUIRED = ["path", "op"];
|
|
766
|
+
var RIGHT_SIDE_KEYS = ["value", "valueFrom"];
|
|
760
767
|
var PREDICATE_GROUP_KEYS = ["all", "any"];
|
|
761
768
|
function predicateShapeOf(o) {
|
|
762
769
|
const marks = [];
|
|
@@ -783,29 +790,53 @@ var GATE_SCOPE = {
|
|
|
783
790
|
foreignRoot: () => 'which an if gate cannot see \u2014 its one root is "input", the value that reached the gate; a gate has no outcome and no edge needs, and any other first segment would resolve to nothing in silence',
|
|
784
791
|
rootAlone: () => "and names no key \u2014 a gate's path names a key INSIDE the value that reached it, as every gate's key path does; a value that must be compared whole is wrapped by the task before the gate"
|
|
785
792
|
};
|
|
786
|
-
function
|
|
787
|
-
const
|
|
788
|
-
if (extra.length) {
|
|
789
|
-
return { rule: "FD-V42", message: `${where} carries ${extra.map((k) => JSON.stringify(k)).join(", ")}, which a comparison does not declare; it is { ${PREDICATE_LEAF_KEYS.join(", ")} }` };
|
|
790
|
-
}
|
|
791
|
-
for (const k of PREDICATE_LEAF_REQUIRED) {
|
|
792
|
-
if (!(k in o)) return { rule: "FD-V42", message: `${where} is missing required key "${k}"; a comparison is { ${PREDICATE_LEAF_KEYS.join(", ")} } with ${PREDICATE_LEAF_REQUIRED.join(", ")} required` };
|
|
793
|
-
}
|
|
794
|
-
const path = o.path;
|
|
793
|
+
function comparisonPathProblem(o, key, where, scope) {
|
|
794
|
+
const path = o[key];
|
|
795
795
|
if (typeof path !== "string" || !KEY_PATH_RE.test(path)) {
|
|
796
|
-
return { rule: "FD-V43", message: `${where}
|
|
796
|
+
return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} is not a key path \u2014 ${KEY_PATH_WORDS}` };
|
|
797
797
|
}
|
|
798
|
-
const
|
|
798
|
+
const segments = path.split(".");
|
|
799
|
+
const first = segments[0];
|
|
799
800
|
const bracket = first.indexOf("[");
|
|
800
801
|
const root = bracket === -1 ? first : first.slice(0, bracket);
|
|
801
802
|
if (!scope.roots.includes(root)) {
|
|
802
|
-
return { rule: "FD-V43", message: `${where}
|
|
803
|
+
return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} starts at ${JSON.stringify(root)}, ${scope.foreignRoot(root)}` };
|
|
803
804
|
}
|
|
804
805
|
if (bracket !== -1) {
|
|
805
|
-
return { rule: "FD-V43", message: `${where}
|
|
806
|
+
return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} indexes its root ${JSON.stringify(root)} \u2014 a path names a KEY inside its root first, as a gate's key path names a key inside the value that reached it; an index follows a key (${root}.items[0])` };
|
|
806
807
|
}
|
|
807
|
-
if (
|
|
808
|
-
return { rule: "FD-V43", message: `${where}
|
|
808
|
+
if (segments.length < 2) {
|
|
809
|
+
return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} is a root alone ${scope.rootAlone(root)}` };
|
|
810
|
+
}
|
|
811
|
+
if (root === "needs" && scope.needsDeclared !== void 0) {
|
|
812
|
+
const seg = segments[1];
|
|
813
|
+
const at = seg.indexOf("[");
|
|
814
|
+
const name = at === -1 ? seg : seg.slice(0, at);
|
|
815
|
+
if (!scope.needsDeclared.includes(name)) {
|
|
816
|
+
const declared = scope.needsDeclared.length === 0 ? "declares no `needs`, so its `ctx.needs` is {}" : `declares needs ${scope.needsDeclared.map((k2) => JSON.stringify(k2)).join(", ")}`;
|
|
817
|
+
return { rule: "FD-V43", message: `${where} ${key} ${JSON.stringify(path)} reads needs key ${JSON.stringify(name)}, and this edge ${declared} \u2014 the key would be absent for ever and the comparison no match for ever; declare it in the edge's needs` };
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
function leafProblem(o, where, scope = EDGE_SCOPE) {
|
|
823
|
+
const extra = Object.keys(o).filter((k) => !PREDICATE_LEAF_KEYS.includes(k));
|
|
824
|
+
if (extra.length) {
|
|
825
|
+
return { rule: "FD-V42", message: `${where} carries ${extra.map((k) => JSON.stringify(k)).join(", ")}, which a comparison does not declare; it is { ${PREDICATE_LEAF_KEYS.join(", ")} }` };
|
|
826
|
+
}
|
|
827
|
+
for (const k of PREDICATE_LEAF_REQUIRED) {
|
|
828
|
+
if (!(k in o)) return { rule: "FD-V42", message: `${where} is missing required key "${k}"; a comparison is { ${PREDICATE_LEAF_KEYS.join(", ")} } with ${PREDICATE_LEAF_REQUIRED.join(", ")} required and exactly one of ${RIGHT_SIDE_KEYS.map((k2) => JSON.stringify(k2)).join(" or ")}` };
|
|
829
|
+
}
|
|
830
|
+
const sides = RIGHT_SIDE_KEYS.filter((k) => k in o);
|
|
831
|
+
if (sides.length !== 1) {
|
|
832
|
+
return { rule: "FD-V42", message: `${where} carries ${sides.length === 0 ? "neither" : "both"} "value" and "valueFrom"; a comparison compares against exactly one \u2014 a literal "value", or "valueFrom", a key path over the same context` };
|
|
833
|
+
}
|
|
834
|
+
const pathProblem = comparisonPathProblem(o, "path", where, scope);
|
|
835
|
+
if (pathProblem !== null) return pathProblem;
|
|
836
|
+
const fromValue = "valueFrom" in o;
|
|
837
|
+
if (fromValue) {
|
|
838
|
+
const fromProblem = comparisonPathProblem(o, "valueFrom", where, scope);
|
|
839
|
+
if (fromProblem !== null) return fromProblem;
|
|
809
840
|
}
|
|
810
841
|
const op = o.op;
|
|
811
842
|
if (typeof op !== "string" || !PREDICATE_OPS.includes(op)) {
|
|
@@ -815,6 +846,12 @@ function leafProblem(o, where, scope = EDGE_SCOPE) {
|
|
|
815
846
|
return { rule: "FD-V44", message: `${where} length is ${JSON.stringify(o.length)}; it is exactly true or absent \u2014 there is no "compare the value, not its length" to ask for` };
|
|
816
847
|
}
|
|
817
848
|
const takesLength = o.length === true;
|
|
849
|
+
if (fromValue) {
|
|
850
|
+
if (takesLength && STRING_OPS.has(op)) {
|
|
851
|
+
return { rule: "FD-V44", message: `${where} takes a length and uses "${op}"; a length is a number and has no prefix` };
|
|
852
|
+
}
|
|
853
|
+
return defaultProblem(o, where, takesLength);
|
|
854
|
+
}
|
|
818
855
|
if (!isScalar(o.value)) {
|
|
819
856
|
return { rule: "FD-V44", message: `${where} value is ${scalarName(o.value)}; a comparison compares against a SCALAR \u2014 null, a boolean, a finite number or a string` };
|
|
820
857
|
}
|
|
@@ -830,6 +867,9 @@ function leafProblem(o, where, scope = EDGE_SCOPE) {
|
|
|
830
867
|
if (takesLength && STRING_OPS.has(op)) {
|
|
831
868
|
return { rule: "FD-V44", message: `${where} takes a length and uses "${op}"; a length is a number and has no prefix` };
|
|
832
869
|
}
|
|
870
|
+
return defaultProblem(o, where, takesLength);
|
|
871
|
+
}
|
|
872
|
+
function defaultProblem(o, where, takesLength) {
|
|
833
873
|
if ("default" in o) {
|
|
834
874
|
const d = o.default;
|
|
835
875
|
const okDefault = Array.isArray(d) ? d.every(isScalar) : isScalar(d);
|
|
@@ -845,7 +885,8 @@ function leafProblem(o, where, scope = EDGE_SCOPE) {
|
|
|
845
885
|
}
|
|
846
886
|
return null;
|
|
847
887
|
}
|
|
848
|
-
function predicateProblem(v) {
|
|
888
|
+
function predicateProblem(v, edgeNeeds) {
|
|
889
|
+
const scope = edgeNeeds === void 0 ? EDGE_SCOPE : { ...EDGE_SCOPE, needsDeclared: edgeNeeds };
|
|
849
890
|
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
850
891
|
return {
|
|
851
892
|
rule: "FD-V42",
|
|
@@ -866,8 +907,8 @@ function predicateProblem(v) {
|
|
|
866
907
|
if (typeof o.predicate !== "string") return { rule: "FD-V42", message: `when predicate is ${scalarName(o.predicate)}; it is the name of a registered predicate` };
|
|
867
908
|
return null;
|
|
868
909
|
}
|
|
869
|
-
if (shape === "leaf") return leafProblem(o, "when");
|
|
870
|
-
return groupProblem(o, "when",
|
|
910
|
+
if (shape === "leaf") return leafProblem(o, "when", scope);
|
|
911
|
+
return groupProblem(o, "when", scope);
|
|
871
912
|
}
|
|
872
913
|
function groupProblem(o, label, scope) {
|
|
873
914
|
const extra = Object.keys(o).filter((k) => !PREDICATE_GROUP_KEYS.includes(k));
|
|
@@ -1360,7 +1401,9 @@ function validateDefinition(doc, registry) {
|
|
|
1360
1401
|
}
|
|
1361
1402
|
}
|
|
1362
1403
|
if (ed?.when !== void 0) {
|
|
1363
|
-
const
|
|
1404
|
+
const nv = ed.needs;
|
|
1405
|
+
const edgeNeeds = nv === void 0 ? [] : typeof nv === "object" && nv !== null && !Array.isArray(nv) && Object.keys(nv).length > 0 ? Object.keys(nv) : void 0;
|
|
1406
|
+
const problem = predicateProblem(ed.when, edgeNeeds);
|
|
1364
1407
|
if (problem) add(problem.rule, at, problem.message);
|
|
1365
1408
|
else for (const name of predicateNamesIn(ed.when)) {
|
|
1366
1409
|
if (!preds.has(name)) add("FD-V4", at, `when names "${name}", which is not a registered predicate`);
|
|
@@ -1641,11 +1684,38 @@ var GateTypeRefused = class extends RegistryError {
|
|
|
1641
1684
|
var BUILT_IN_GROUP_TYPES = {
|
|
1642
1685
|
ConcurrentLimitGroup: { configSchema: { type: "object", required: ["limit"], properties: { limit: { type: "integer", minimum: 1 } }, additionalProperties: false } },
|
|
1643
1686
|
RateLimitGroup: { configSchema: { type: "object", required: ["windows"], properties: { windows: RATE_WINDOWS() }, additionalProperties: false } },
|
|
1644
|
-
KeyedRateLimitGroup: { configSchema: { type: "object", required: ["windows"], properties: { windows: RATE_WINDOWS() }, additionalProperties: false } }
|
|
1687
|
+
KeyedRateLimitGroup: { configSchema: { type: "object", required: ["windows"], properties: { windows: RATE_WINDOWS() }, additionalProperties: false } },
|
|
1688
|
+
// v0.56: `units` a count (the units `0`…`n-1`) or a list of names — `type` may be a list in the subset (P46 fact 7), and
|
|
1689
|
+
// `minimum` reads a number and `minItems` / `items` an array, so each keyword holds its own form. Distinct: beside it.
|
|
1690
|
+
UnitGroup: { configSchema: { type: "object", required: ["units"], properties: { units: { type: ["integer", "array"], minimum: 1, minItems: 1, items: { type: "string", minLength: 1 } } }, additionalProperties: false } },
|
|
1691
|
+
// v0.56: `limits` an object of per-key limits; each value a positive integer is beside it (no schema-valued
|
|
1692
|
+
// `additionalProperties` in the subset).
|
|
1693
|
+
KeyedConcurrentLimitGroup: { configSchema: { type: "object", required: ["limit"], properties: { limit: { type: "integer", minimum: 1 }, limits: { type: "object" } }, additionalProperties: false } }
|
|
1645
1694
|
};
|
|
1646
1695
|
function RATE_WINDOWS() {
|
|
1647
1696
|
return { type: "array", minItems: 1, items: { type: "object", required: ["limit", "windowMs"], properties: { limit: { type: "integer", minimum: 1 }, windowMs: { type: "integer", minimum: 1 } }, additionalProperties: false } };
|
|
1648
1697
|
}
|
|
1698
|
+
var isPositiveInteger = (v) => typeof v === "number" && Number.isInteger(v) && v >= 1;
|
|
1699
|
+
function builtInConfigProblem(type, config) {
|
|
1700
|
+
if (type === "UnitGroup") {
|
|
1701
|
+
const units = config.units;
|
|
1702
|
+
if (Array.isArray(units)) {
|
|
1703
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1704
|
+
for (const u of units) {
|
|
1705
|
+
if (seen.has(u)) return `$.units lists unit ${JSON.stringify(u)} twice \u2014 a UnitGroup's units are distinct`;
|
|
1706
|
+
seen.add(u);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
} else if (type === "KeyedConcurrentLimitGroup") {
|
|
1710
|
+
const limits = config.limits;
|
|
1711
|
+
if (limits !== void 0) {
|
|
1712
|
+
for (const [k, v] of Object.entries(limits)) {
|
|
1713
|
+
if (!isPositiveInteger(v)) return `$.limits[${q(k)}] must be a positive integer \u2014 a per-key limit, as \`limit\` is \u2014 got ${shown(v)}`;
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return null;
|
|
1718
|
+
}
|
|
1649
1719
|
var DOCUMENT_KEYS = ["workers", "groups", "nodeTypes", "pools", "predicates", "languages"];
|
|
1650
1720
|
var WORKER_KEYS = ["url", "configSchema", "settingsSchema", "inputSchema", "outputSchema"];
|
|
1651
1721
|
var CHECKED_WORKER_SCHEMAS = ["configSchema", "settingsSchema"];
|
|
@@ -1685,9 +1755,12 @@ function mergePool(base, over) {
|
|
|
1685
1755
|
}
|
|
1686
1756
|
function copyResident(r) {
|
|
1687
1757
|
if (Array.isArray(r)) return [...r];
|
|
1688
|
-
if (isPlainObject2(r)) return
|
|
1758
|
+
if (isPlainObject2(r)) return Object.fromEntries(Object.entries(r).map(([k, v]) => [k, isPlainObject2(v) ? copyRequest(v) : v]));
|
|
1689
1759
|
return r;
|
|
1690
1760
|
}
|
|
1761
|
+
function copyRequest(v) {
|
|
1762
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, Array.isArray(x) ? [...x] : x]));
|
|
1763
|
+
}
|
|
1691
1764
|
function nodeTypeFromDeclaration(decl, over) {
|
|
1692
1765
|
const declaredPool = mergePool(decl.pool ?? {}, over);
|
|
1693
1766
|
const def = {
|
|
@@ -1732,7 +1805,10 @@ function poolSpecProblem(v) {
|
|
|
1732
1805
|
if (r.some((g) => typeof g !== "string" || g.length === 0)) return `PoolSpec.residentGroups must be an array of group keys or a { key: cost } record, got ${JSON.stringify(r)}`;
|
|
1733
1806
|
if (new Set(r).size !== r.length) return "PoolSpec.residentGroups repeats a key";
|
|
1734
1807
|
} else if (isPlainObject2(r)) {
|
|
1735
|
-
for (const [k, c] of Object.entries(r))
|
|
1808
|
+
for (const [k, c] of Object.entries(r)) {
|
|
1809
|
+
const p = residentValueProblem(c);
|
|
1810
|
+
if (p !== null) return `PoolSpec.residentGroups[${q(k)}] ${p}`;
|
|
1811
|
+
}
|
|
1736
1812
|
} else return `PoolSpec.residentGroups must be an array of group keys or a { key: cost } record, got ${JSON.stringify(r)}`;
|
|
1737
1813
|
}
|
|
1738
1814
|
if (v.cancelGraceMs !== void 0 && !(typeof v.cancelGraceMs === "number" && v.cancelGraceMs >= 0)) {
|
|
@@ -1753,6 +1829,27 @@ function poolSpecProblem(v) {
|
|
|
1753
1829
|
}
|
|
1754
1830
|
return null;
|
|
1755
1831
|
}
|
|
1832
|
+
var RESIDENT_REQUEST_KEYS = ["cost", "from", "prefer"];
|
|
1833
|
+
function residentValueProblem(c) {
|
|
1834
|
+
if (typeof c === "number") return isPositiveInteger(c) ? null : `must be a cost > 0, an INTEGER (the scheduler's rule), or a request { ${RESIDENT_REQUEST_KEYS.map((k) => `${k}?`).join(", ")} }, got ${shown(c)}`;
|
|
1835
|
+
if (!isPlainObject2(c) || !isPlainPrototype(c)) {
|
|
1836
|
+
return `must be a cost (a positive integer) or a request { ${RESIDENT_REQUEST_KEYS.map((k) => `${k}?`).join(", ")} } \u2014 a plain object \u2014 got ${describeNonPlain(c)}`;
|
|
1837
|
+
}
|
|
1838
|
+
for (const k of Object.keys(c)) if (!RESIDENT_REQUEST_KEYS.includes(k)) return `is a request carrying undeclared key ${q(k)}; it declares ${RESIDENT_REQUEST_KEYS.join(", ")}`;
|
|
1839
|
+
if (ownKey(c, "cost") && !isPositiveInteger(c.cost)) return `.cost must be a positive integer, got ${shown(c.cost)}`;
|
|
1840
|
+
for (const f of ["from", "prefer"]) {
|
|
1841
|
+
if (!ownKey(c, f)) continue;
|
|
1842
|
+
const list = c[f];
|
|
1843
|
+
if (!Array.isArray(list) || list.length === 0) return `.${f} must be a non-empty list of unit ids, got ${shown(list)}`;
|
|
1844
|
+
for (const u of list) {
|
|
1845
|
+
if (!(typeof u === "string" && u.length > 0 || typeof u === "number" && Number.isInteger(u) && u >= 0)) {
|
|
1846
|
+
return `.${f} holds ${shown(u)} \u2014 a unit id is a non-empty string, or an integer >= 0 for a UnitGroup declared by count`;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
if (new Set(list).size !== list.length) return `.${f} repeats a unit id`;
|
|
1850
|
+
}
|
|
1851
|
+
return null;
|
|
1852
|
+
}
|
|
1756
1853
|
function isPlainPrototype(o) {
|
|
1757
1854
|
const proto = Object.getPrototypeOf(o);
|
|
1758
1855
|
return proto === Object.prototype || proto === null;
|
|
@@ -1783,6 +1880,45 @@ function groupsNamedBy(spec) {
|
|
|
1783
1880
|
function residentKeysOf(spec) {
|
|
1784
1881
|
return spec.residentGroups === void 0 ? [] : Array.isArray(spec.residentGroups) ? [...spec.residentGroups] : Object.keys(spec.residentGroups);
|
|
1785
1882
|
}
|
|
1883
|
+
function residentEntriesOf(spec) {
|
|
1884
|
+
const r = spec.residentGroups;
|
|
1885
|
+
return r === void 0 ? [] : Array.isArray(r) ? r.map((g) => [g, 1]) : Object.entries(r);
|
|
1886
|
+
}
|
|
1887
|
+
var KEYED_BUILT_INS = {
|
|
1888
|
+
KeyedRateLimitGroup: 'the scheduler resolves no key for a resident group, so every worker holding it is counted in ONE "" bucket, in silence',
|
|
1889
|
+
KeyedConcurrentLimitGroup: "the scheduler REJECTS a KeyedConcurrentLimitGroup in residentGroups when it is constructed: a worker holds its units across tasks, and so across keys, with no key to hold them under"
|
|
1890
|
+
};
|
|
1891
|
+
function residentAgainstGroupProblem(group, value) {
|
|
1892
|
+
if (!hasOwn(BUILT_IN_GROUP_TYPES, group.type)) return null;
|
|
1893
|
+
const request = isPlainObject2(value) ? value : void 0;
|
|
1894
|
+
const cost = typeof value === "number" ? value : request?.cost ?? 1;
|
|
1895
|
+
const named = `group ${q(group.key)}, of the built-in type ${q(group.type)}`;
|
|
1896
|
+
if (group.type === "UnitGroup") {
|
|
1897
|
+
const declared = group.config.units;
|
|
1898
|
+
const units = typeof declared === "number" ? Array.from({ length: declared }, (_, i) => i) : [...declared];
|
|
1899
|
+
const has = new Set(units);
|
|
1900
|
+
for (const f of ["from", "prefer"]) {
|
|
1901
|
+
for (const u of request?.[f] ?? []) {
|
|
1902
|
+
if (!has.has(u)) {
|
|
1903
|
+
return `${f} names unit ${shown(u)}, which ${named} does not have \u2014 its units are ${units.map((x) => shown(x)).join(", ")}` + (typeof declared === "number" ? " (declared by count: integers, never strings)" : "");
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
if (request?.from !== void 0 && cost > request.from.length) {
|
|
1908
|
+
return `a cost of ${cost} from ${request.from.length} candidate unit(s) in from can never be granted by ${named}`;
|
|
1909
|
+
}
|
|
1910
|
+
if (cost > units.length) return `a cost of ${cost} is above the ${units.length} unit(s) of ${named} \u2014 a worker of this pool could never start`;
|
|
1911
|
+
return null;
|
|
1912
|
+
}
|
|
1913
|
+
if (request !== void 0 && (ownKey(request, "from") || ownKey(request, "prefer"))) {
|
|
1914
|
+
return `a request naming ${["from", "prefer"].filter((f) => ownKey(request, f)).join(" and ")} on ${named}, which takes no request \u2014 from and prefer are a UnitGroup's (or a code group type's with a checkRequest); { cost } alone is a cost, for any group`;
|
|
1915
|
+
}
|
|
1916
|
+
const limit = group.type === "ConcurrentLimitGroup" ? group.config.limit : group.type === "RateLimitGroup" ? Math.min(...group.config.windows.map((w) => w.limit)) : void 0;
|
|
1917
|
+
if (limit !== void 0 && cost > limit) {
|
|
1918
|
+
return `a cost of ${cost} is above the limit of ${limit} of ${named}${group.type === "RateLimitGroup" ? " (its smallest window's)" : ""} \u2014 a worker of this pool could never start`;
|
|
1919
|
+
}
|
|
1920
|
+
return null;
|
|
1921
|
+
}
|
|
1786
1922
|
function poolOptionsOf(spec, inline, workerUrl, languages) {
|
|
1787
1923
|
const { config, groups, ...rest } = spec;
|
|
1788
1924
|
return {
|
|
@@ -1870,7 +2006,7 @@ function derivePool(key, typeName, def, worker, config, groupKeys, refuse, pool)
|
|
|
1870
2006
|
}
|
|
1871
2007
|
const resident = spec.residentGroups === void 0 ? 0 : Array.isArray(spec.residentGroups) ? spec.residentGroups.length : Object.keys(spec.residentGroups).length;
|
|
1872
2008
|
if (resident > 0 && spec.idleTimeout === 0) {
|
|
1873
|
-
throw new DerivationRefused(key, `${who}: residentGroups beside idleTimeout: 0 \u2014 a worker that never idles never releases a resident group, and a second pool contending for it waits for ever (measured
|
|
2009
|
+
throw new DerivationRefused(key, `${who}: residentGroups beside idleTimeout: 0 \u2014 a worker that never idles never releases a resident group, and a second pool contending for it waits for ever (measured at P18 fact 6 and re-measured at P46 fact 6); give the pool a positive idleTimeout`);
|
|
1874
2010
|
}
|
|
1875
2011
|
return { key, type: typeName, definition: def, worker, config, spec, ...pool !== void 0 ? { pool: copyPool(pool) } : {} };
|
|
1876
2012
|
}
|
|
@@ -1981,6 +2117,10 @@ function resolveRegistry(doc, code = {}) {
|
|
|
1981
2117
|
const fs = findings(gt.configSchema, inst.config ?? {});
|
|
1982
2118
|
if (fs.length) throw new GroupConfigRefused(key, `group ${q(key)}: config fails ${q(inst.type)}'s configSchema \u2014 ${describeFindings(fs)}`);
|
|
1983
2119
|
}
|
|
2120
|
+
if (hasOwn(BUILT_IN_GROUP_TYPES, inst.type)) {
|
|
2121
|
+
const p = builtInConfigProblem(inst.type, inst.config);
|
|
2122
|
+
if (p !== null) throw new GroupConfigRefused(key, `group ${q(key)}: config fails ${q(inst.type)}'s rule beside its configSchema \u2014 ${p}`);
|
|
2123
|
+
}
|
|
1984
2124
|
groups.set(key, { key, type: inst.type, groupType: gt, config: inst.config });
|
|
1985
2125
|
}
|
|
1986
2126
|
const groupKeys = new Set(groups.keys());
|
|
@@ -2019,10 +2159,16 @@ function resolveRegistry(doc, code = {}) {
|
|
|
2019
2159
|
pools.set(key, derivePool(key, inst.type, def, def.worker === void 0 ? void 0 : workers.get(def.worker), inst.config, groupKeys, ConfigRefused, inst.pool));
|
|
2020
2160
|
}
|
|
2021
2161
|
for (const p of pools.values()) {
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
const
|
|
2025
|
-
|
|
2162
|
+
const who = p.key === p.type ? `node type ${q(p.type)}'s default instance` : `pool ${q(p.key)} (an instance of ${q(p.type)})`;
|
|
2163
|
+
for (const [g, value] of residentEntriesOf(p.spec)) {
|
|
2164
|
+
const group = groups.get(g);
|
|
2165
|
+
if (group === void 0) continue;
|
|
2166
|
+
const keyed = KEYED_BUILT_INS[group.type];
|
|
2167
|
+
if (hasOwn(KEYED_BUILT_INS, group.type) && keyed !== void 0) {
|
|
2168
|
+
throw new DerivationRefused(p.key, `${who}: the derived pool holds group ${q(g)}, of the built-in type ${q(group.type)}, in residentGroups \u2014 ${keyed} (Q619). Name ${q(g)} in groups, where each task is counted under its limitKey (FD-R6), or hold a ConcurrentLimitGroup or RateLimitGroup resident (or a UnitGroup)`);
|
|
2169
|
+
}
|
|
2170
|
+
const problem = residentAgainstGroupProblem(group, value);
|
|
2171
|
+
if (problem !== null) throw new DerivationRefused(p.key, `${who}: residentGroups[${q(g)}] \u2014 ${problem}`);
|
|
2026
2172
|
}
|
|
2027
2173
|
}
|
|
2028
2174
|
const predicateNames = doc.predicates ?? [];
|
|
@@ -7028,6 +7174,7 @@ export {
|
|
|
7028
7174
|
canonical,
|
|
7029
7175
|
findings,
|
|
7030
7176
|
describeFindings,
|
|
7177
|
+
keyPathSteps,
|
|
7031
7178
|
readKeyPath,
|
|
7032
7179
|
ID_RE_COPY,
|
|
7033
7180
|
KINDS_COPY,
|
package/dist/browser/index.js
CHANGED
|
@@ -111,6 +111,7 @@ import {
|
|
|
111
111
|
isTerminal,
|
|
112
112
|
isTerminalEvent,
|
|
113
113
|
keyPathProblem,
|
|
114
|
+
keyPathSteps,
|
|
114
115
|
languagesOf,
|
|
115
116
|
lastRecordFor,
|
|
116
117
|
liveToken,
|
|
@@ -151,7 +152,7 @@ import {
|
|
|
151
152
|
visibilityKey,
|
|
152
153
|
witness,
|
|
153
154
|
workersOf
|
|
154
|
-
} from "./chunks/chunk-
|
|
155
|
+
} from "./chunks/chunk-5KUYTCXI.js";
|
|
155
156
|
|
|
156
157
|
// src/definition/definition-schema.ts
|
|
157
158
|
var DRAFT_2020_12 = "https://json-schema.org/draft/2020-12/schema";
|
|
@@ -237,7 +238,7 @@ var KIND_DESCRIPTIONS = {
|
|
|
237
238
|
var EDGE_DESCRIPTIONS = {
|
|
238
239
|
to: "The id of the node this edge leads to (FD-V2, validateDefinition's).",
|
|
239
240
|
on: 'The terminal states this edge is eligible for: a non-empty list of distinct "done" / "failed" (FD-V18). Absent: ["done"]. "user_action" suspends rather than routes.',
|
|
240
|
-
when: "The decision: a registered predicate NAME { predicate }, a declared COMPARISON { path, op, value, default?, length? }, or a flat GROUP { all } or { any } of comparisons (FD-V42-FD-V45). Absent: the edge is the default for its state.",
|
|
241
|
+
when: "The decision: a registered predicate NAME { predicate }, a declared COMPARISON { path, op, value or valueFrom, default?, length? }, or a flat GROUP { all } or { any } of comparisons (FD-V42-FD-V45). Absent: the edge is the default for its state.",
|
|
241
242
|
needs: "What this edge's predicate sees beyond its node's outcome \u2014 `needs`' grammar minus \".\" (FD-V26).",
|
|
242
243
|
route: "On a gate's edge, which of the gate type's routes it carries; absent: the type's DEFAULT route (FD-V41). Refused on every other kind's edge."
|
|
243
244
|
};
|
|
@@ -251,9 +252,10 @@ var TRANSFORM_DESCRIPTIONS = {
|
|
|
251
252
|
};
|
|
252
253
|
var PREDICATE_DESCRIPTIONS = {
|
|
253
254
|
predicate: "A registered predicate's name (FD-V4, validateDefinition's).",
|
|
254
|
-
path: `A key path over the decision's context, starting at ${PREDICATE_ROOTS.map((r) => `"${r}"`).join(" or ")} and naming a key beyond it; a key past the root may be followed by array indexes [n], the root never (FD-V43).`,
|
|
255
|
+
path: `A key path over the decision's context, starting at ${PREDICATE_ROOTS.map((r) => `"${r}"`).join(" or ")} and naming a key beyond it; a key past the root may be followed by array indexes [n], the root never; a "needs" key the edge declares (FD-V43).`,
|
|
255
256
|
op: `One of ${PREDICATE_OPS.join(", ")} (FD-V44). The ordering operators compare numbers and "startsWith" a string \u2014 a comparison between kinds is no match, never a coercion.`,
|
|
256
|
-
value: "The scalar compared against: null, a boolean, a number or a string (FD-V44).",
|
|
257
|
+
value: "The scalar compared against: null, a boolean, a number or a string (FD-V44). Exactly one of value and valueFrom (FD-V42).",
|
|
258
|
+
valueFrom: `A key path over the same context, compared against as a literal would be \u2014 the same roots as path, ${PREDICATE_ROOTS.map((r) => `"${r}"`).join(" or ")}, and a "needs" key the edge declares (FD-V43). Exactly one of value and valueFrom (FD-V42). Where it does not resolve, or resolves to an object or an array, the comparison is no match for every operator.`,
|
|
257
259
|
default: "What is compared where the path does not resolve or resolves to null: a scalar, or \u2014 with `length` \u2014 an array or a string (FD-V44).",
|
|
258
260
|
length: "Exactly true: compare the LENGTH of the array or string, against a number (FD-V44).",
|
|
259
261
|
all: "A flat group: a non-empty list of comparisons that must all hold (FD-V45).",
|
|
@@ -438,6 +440,9 @@ function leafSchema(id) {
|
|
|
438
440
|
path: { type: "string", pattern: path },
|
|
439
441
|
op: { enum: [...PREDICATE_OPS] },
|
|
440
442
|
value: scalar(),
|
|
443
|
+
// v0.55 (ADR 0140): the RIGHT side as a key path — `path`'s own pattern, so the two keys cannot be held to different
|
|
444
|
+
// grammars; (g1)'s *a `needs` key the edge declares* needs the edge's own `needs` and is the validator's alone.
|
|
445
|
+
valueFrom: { type: "string", pattern: path },
|
|
441
446
|
default: { anyOf: [scalar(), { type: "array", items: scalar() }] },
|
|
442
447
|
length: { const: true }
|
|
443
448
|
};
|
|
@@ -445,6 +450,8 @@ function leafSchema(id) {
|
|
|
445
450
|
description: "A declared comparison over the decision's context.",
|
|
446
451
|
type: "object",
|
|
447
452
|
required: [...PREDICATE_LEAF_REQUIRED],
|
|
453
|
+
// **Exactly one of `value` / `valueFrom`** (`FD-V42`, v0.55): both satisfy both branches and neither satisfies none.
|
|
454
|
+
oneOf: [{ required: ["value"] }, { required: ["valueFrom"] }],
|
|
448
455
|
properties: Object.fromEntries(PREDICATE_LEAF_KEYS.map((k) => {
|
|
449
456
|
const s = keySchemas[k];
|
|
450
457
|
if (s === void 0) throw new Error(`definitionSchema: a comparison declares the key "${k}" and no schema is written for it`);
|
|
@@ -942,6 +949,7 @@ export {
|
|
|
942
949
|
isTerminal,
|
|
943
950
|
isTerminalEvent,
|
|
944
951
|
keyPathProblem,
|
|
952
|
+
keyPathSteps,
|
|
945
953
|
languagesOf,
|
|
946
954
|
lastRecordFor,
|
|
947
955
|
liveToken,
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
retryPolicyProblem,
|
|
27
27
|
validateDefinition,
|
|
28
28
|
workersOf
|
|
29
|
-
} from "../chunks/chunk-
|
|
29
|
+
} from "../chunks/chunk-5KUYTCXI.js";
|
|
30
30
|
|
|
31
31
|
// src/scheduler/bootstrap.ts
|
|
32
32
|
import { WorkerManager } from "fyflow-scheduler";
|
|
@@ -83,7 +83,7 @@ function bootstrap(registry, groups = {}, options = {}) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// src/scheduler/runtime.ts
|
|
86
|
-
import { ConcurrentLimitGroup, FyflowScheduler, KeyedRateLimitGroup, RateLimitGroup } from "fyflow-scheduler";
|
|
86
|
+
import { ConcurrentLimitGroup, FyflowScheduler, KeyedConcurrentLimitGroup, KeyedRateLimitGroup, RateLimitGroup, UnitGroup } from "fyflow-scheduler";
|
|
87
87
|
var RuntimeError = class extends Error {
|
|
88
88
|
/** The entry this refusal is about — a worker's key, a group's key, a type's name. `RegistryError`'s field, one layer out. */
|
|
89
89
|
at;
|
|
@@ -107,8 +107,18 @@ var KEYED_RULE = "FD-R6";
|
|
|
107
107
|
var BUILT_IN_GROUP_FACTORIES = {
|
|
108
108
|
ConcurrentLimitGroup: (config, key) => new ConcurrentLimitGroup(config.limit, key),
|
|
109
109
|
RateLimitGroup: (config, key) => new RateLimitGroup(config.windows, key),
|
|
110
|
-
KeyedRateLimitGroup: (config, key) => new KeyedRateLimitGroup(config.windows, { id: key })
|
|
110
|
+
KeyedRateLimitGroup: (config, key) => new KeyedRateLimitGroup(config.windows, { id: key }),
|
|
111
|
+
UnitGroup: (config, key) => {
|
|
112
|
+
const units = config.units;
|
|
113
|
+
return new UnitGroup(typeof units === "number" ? units : [...units], { id: key });
|
|
114
|
+
},
|
|
115
|
+
KeyedConcurrentLimitGroup: (config, key) => {
|
|
116
|
+
const { limit, limits } = config;
|
|
117
|
+
const own = new Map(Object.entries(limits ?? {}));
|
|
118
|
+
return new KeyedConcurrentLimitGroup(limit, { id: key, limitFor: (k) => own.get(k) ?? limit });
|
|
119
|
+
}
|
|
111
120
|
};
|
|
121
|
+
var KEYED_BUILT_IN_GROUP_TYPES = ["KeyedRateLimitGroup", "KeyedConcurrentLimitGroup"];
|
|
112
122
|
var GROUP_MEMBERS = [
|
|
113
123
|
{ name: "canRun", kind: "function", why: "the scheduler calls it before every admission; a group without it throws on the first task" },
|
|
114
124
|
{ name: "onStart", kind: "function", why: "the scheduler calls it unguarded at admission, though the interface marks it optional; a group without it throws on the first task (measured on 0.5.2)" },
|
|
@@ -210,8 +220,8 @@ function createRuntime(document, code = {}, options = {}) {
|
|
|
210
220
|
pools: poolsOf(registry.pools),
|
|
211
221
|
...registry.gateTypes !== void 0 ? { gateTypes: registry.gateTypes } : {}
|
|
212
222
|
};
|
|
213
|
-
const keyedByBuiltIn = /* @__PURE__ */ new
|
|
214
|
-
for (const [key, g] of resolved.groups) if (g.type
|
|
223
|
+
const keyedByBuiltIn = /* @__PURE__ */ new Map();
|
|
224
|
+
for (const [key, g] of resolved.groups) if (KEYED_BUILT_IN_GROUP_TYPES.includes(g.type)) keyedByBuiltIn.set(key, g.type);
|
|
215
225
|
const keyedFindings = (flow) => {
|
|
216
226
|
const nodes = flow?.nodes;
|
|
217
227
|
if (keyedByBuiltIn.size === 0 || typeof nodes?.[Symbol.iterator] !== "function") return [];
|
|
@@ -226,10 +236,11 @@ function createRuntime(document, code = {}, options = {}) {
|
|
|
226
236
|
const reached = [.../* @__PURE__ */ new Set([...fromPool, ...fromNode])].filter((g) => keyedByBuiltIn.has(g)).sort();
|
|
227
237
|
if (reached.length === 0) continue;
|
|
228
238
|
const at = typeof n.id === "string" ? n.id : null;
|
|
239
|
+
const types = [...new Set(reached.map((g) => keyedByBuiltIn.get(g)))].sort().join(" and ");
|
|
229
240
|
out.push({
|
|
230
241
|
rule: KEYED_RULE,
|
|
231
242
|
at,
|
|
232
|
-
detail: `node ${at === null ? "<unnamed>" : q(at)} reaches keyed group${reached.length > 1 ? "s" : ""} ${reached.map(q).join(", ")}, built by the built-in
|
|
243
|
+
detail: `node ${at === null ? "<unnamed>" : q(at)} reaches keyed group${reached.length > 1 ? "s" : ""} ${reached.map(q).join(", ")}, built by the built-in ${types} factory with no keyFrom \u2014 so every task of it needs a limitKey, which nothing sets unless start is given a createTask, and without one the scheduler's addTask throws "Missing limit key". Two roads work: pass start a createTask that sets limitKey on the task it builds, or declare the group with a code group type (code.groupTypes) whose create() builds a ${types} with a keyFrom that derives the key. A key derived from the flow itself is Q248, deferred`
|
|
233
244
|
});
|
|
234
245
|
}
|
|
235
246
|
return out;
|