fyflow-orchestration 0.7.0 → 0.8.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 +1800 -200
- package/README.md +88 -104
- package/dist/browser/chunks/{chunk-YVOP3DIN.js → chunk-EUU44PHD.js} +2104 -283
- package/dist/browser/index.js +19 -9
- package/dist/browser/scheduler/index.js +101 -58
- package/dist/node/chunks/{chunk-YVOP3DIN.js → chunk-EUU44PHD.js} +2104 -283
- package/dist/node/index.js +19 -9
- package/dist/node/scheduler/index.js +101 -58
- package/dist/types/src/definition/assist.d.ts +29 -5
- package/dist/types/src/definition/condition.d.ts +49 -0
- package/dist/types/src/definition/regions.d.ts +12 -1
- package/dist/types/src/definition/registry.d.ts +52 -10
- package/dist/types/src/definition/schema.d.ts +20 -8
- package/dist/types/src/definition/types.d.ts +152 -23
- package/dist/types/src/definition/validate.d.ts +68 -20
- package/dist/types/src/definition/wiring.d.ts +58 -10
- package/dist/types/src/definition/witness.d.ts +12 -4
- package/dist/types/src/driver/driver.d.ts +772 -40
- package/dist/types/src/driver/listener.d.ts +7 -1
- package/dist/types/src/driver/log.d.ts +20 -5
- package/dist/types/src/driver/routing.d.ts +10 -5
- package/dist/types/src/fold/policies.d.ts +4 -5
- package/dist/types/src/fold/run-state.d.ts +245 -14
- package/dist/types/src/scheduler/bootstrap.d.ts +18 -26
- package/dist/types/src/scheduler/envelope-worker.d.ts +6 -5
- package/dist/types/src/scheduler/index.d.ts +2 -2
- package/dist/types/src/scheduler/runtime.d.ts +105 -32
- package/package.json +3 -3
|
@@ -119,6 +119,14 @@ function regionChainOf(regions, nodeId) {
|
|
|
119
119
|
}
|
|
120
120
|
return chain.reverse();
|
|
121
121
|
}
|
|
122
|
+
function innermostMapOf(regions, nodeId) {
|
|
123
|
+
const chain = regionChainOf(regions, nodeId);
|
|
124
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
125
|
+
const r = regions.find((x) => x.fanoutId === chain[i]);
|
|
126
|
+
if (r?.opensWith === "map") return r;
|
|
127
|
+
}
|
|
128
|
+
return void 0;
|
|
129
|
+
}
|
|
122
130
|
function sharedRegionDepth(regions, a, b) {
|
|
123
131
|
const ca = regionChainOf(regions, a);
|
|
124
132
|
const cb = regionChainOf(regions, b);
|
|
@@ -223,7 +231,11 @@ var SCHEMA_KEYWORDS = [
|
|
|
223
231
|
"maxLength",
|
|
224
232
|
"items",
|
|
225
233
|
"minItems",
|
|
226
|
-
"examples"
|
|
234
|
+
"examples",
|
|
235
|
+
"title",
|
|
236
|
+
"description",
|
|
237
|
+
"readOnly",
|
|
238
|
+
"writeOnly"
|
|
227
239
|
];
|
|
228
240
|
var SCHEMA_TYPES = ["object", "array", "string", "number", "integer", "boolean", "null"];
|
|
229
241
|
var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -265,6 +277,12 @@ function schemaProblem(schema, at = "$") {
|
|
|
265
277
|
for (const k of ["minLength", "maxLength", "minItems"]) {
|
|
266
278
|
if (s[k] !== void 0 && !isNonNegInt(s[k])) return `${at}.${k}: must be an integer >= 0, got ${JSON.stringify(s[k])}`;
|
|
267
279
|
}
|
|
280
|
+
for (const k of ["title", "description"]) {
|
|
281
|
+
if (s[k] !== void 0 && typeof s[k] !== "string") return `${at}.${k}: must be a string \u2014 an annotation that constrains nothing (ADR 0098) \u2014 got ${JSON.stringify(s[k])}`;
|
|
282
|
+
}
|
|
283
|
+
for (const k of ["readOnly", "writeOnly"]) {
|
|
284
|
+
if (s[k] !== void 0 && typeof s[k] !== "boolean") return `${at}.${k}: must be true or false \u2014 an annotation that constrains nothing (ADR 0098) \u2014 got ${JSON.stringify(s[k])}`;
|
|
285
|
+
}
|
|
268
286
|
if (s.items !== void 0) {
|
|
269
287
|
if (Array.isArray(s.items)) return `${at}.items: the subset admits one schema for every item, not a tuple`;
|
|
270
288
|
const p = schemaProblem(s.items, `${at}.items`);
|
|
@@ -341,9 +359,63 @@ function describeFindings(fs) {
|
|
|
341
359
|
return fs.map((f) => `${f.path} ${f.says}`).join("; ");
|
|
342
360
|
}
|
|
343
361
|
|
|
362
|
+
// src/definition/condition.ts
|
|
363
|
+
function readKeyPath(value, path) {
|
|
364
|
+
let v = value;
|
|
365
|
+
for (const seg of path.split(".")) {
|
|
366
|
+
if (typeof v !== "object" || v === null) return void 0;
|
|
367
|
+
const proto = Object.getPrototypeOf(v);
|
|
368
|
+
if (proto !== Object.prototype && proto !== null) return void 0;
|
|
369
|
+
if (!Object.prototype.hasOwnProperty.call(v, seg)) return void 0;
|
|
370
|
+
v = v[seg];
|
|
371
|
+
}
|
|
372
|
+
return v;
|
|
373
|
+
}
|
|
374
|
+
function evalLeaf(scope, p, unknownOp) {
|
|
375
|
+
let v = readKeyPath(scope, p.path);
|
|
376
|
+
if (v === void 0 && !("default" in p)) return false;
|
|
377
|
+
if ((v === void 0 || v === null) && "default" in p) v = p.default;
|
|
378
|
+
if (p.length === true) {
|
|
379
|
+
if (Array.isArray(v)) v = v.length;
|
|
380
|
+
else if (typeof v === "string") v = v.length;
|
|
381
|
+
else return false;
|
|
382
|
+
}
|
|
383
|
+
const w = p.value;
|
|
384
|
+
switch (p.op) {
|
|
385
|
+
// **Type-STRICT, and it is a departure measured rather than assumed.** Under JS comparison
|
|
386
|
+
// `{confidence: "0.4"} < 0.5` is true and every `typeof`-guarded predicate in this tree is false;
|
|
387
|
+
// measured over 1800 contexts, a type-strict form reproduces those three EXACTLY and differs from
|
|
388
|
+
// `resume.test.ts`'s unguarded `isHigh` on `{score: "9"}`, where the JS body coerces. That is
|
|
389
|
+
// `FD-V30`'s own argument — *a string `maxRetries` retries by coercion* — one rule over.
|
|
390
|
+
case "eq":
|
|
391
|
+
return v === w;
|
|
392
|
+
case "ne":
|
|
393
|
+
return v !== w;
|
|
394
|
+
case "lt":
|
|
395
|
+
return typeof v === "number" && typeof w === "number" && v < w;
|
|
396
|
+
case "lte":
|
|
397
|
+
return typeof v === "number" && typeof w === "number" && v <= w;
|
|
398
|
+
case "gt":
|
|
399
|
+
return typeof v === "number" && typeof w === "number" && v > w;
|
|
400
|
+
case "gte":
|
|
401
|
+
return typeof v === "number" && typeof w === "number" && v >= w;
|
|
402
|
+
case "startsWith":
|
|
403
|
+
return typeof v === "string" && typeof w === "string" && v.startsWith(w);
|
|
404
|
+
default: {
|
|
405
|
+
const never = p.op;
|
|
406
|
+
throw unknownOp(`operator "${String(never)}" is not in the subset; FD-V44 should have caught this at instantiation`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function evaluateDeclared(when, scope, unknownOp) {
|
|
411
|
+
if ("all" in when && Array.isArray(when.all)) return when.all.every((m) => evalLeaf(scope, m, unknownOp));
|
|
412
|
+
if ("any" in when && Array.isArray(when.any)) return when.any.some((m) => evalLeaf(scope, m, unknownOp));
|
|
413
|
+
return evalLeaf(scope, when, unknownOp);
|
|
414
|
+
}
|
|
415
|
+
|
|
344
416
|
// src/definition/validate.ts
|
|
345
417
|
var ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
346
|
-
var KINDS = ["task", "fanout", "map", "join", "end", "gate"];
|
|
418
|
+
var KINDS = ["task", "fanout", "map", "join", "end", "gate", "ask"];
|
|
347
419
|
var ROUTABLE = ["done", "failed"];
|
|
348
420
|
var DOC_KEYS = ["flowId", "version", "entry", "nodes"];
|
|
349
421
|
var NODE_KEYS = {
|
|
@@ -365,8 +437,14 @@ var NODE_KEYS = {
|
|
|
365
437
|
end: { req: ["id", "kind", "outcome"], opt: [] },
|
|
366
438
|
// v0.23 (W92, ADR 0066): a gate names a GATE type and carries settings, as a task names a node type — and
|
|
367
439
|
// nothing else a task carries. `needs` is absent on purpose: a gate hands on what reached it, unchanged.
|
|
368
|
-
gate: { req: ["id", "kind", "type", "edges"], opt: ["settings"] }
|
|
440
|
+
gate: { req: ["id", "kind", "type", "edges"], opt: ["settings"] },
|
|
441
|
+
// v0.42 (W179, ADR 0108, P35 stop-and-ask (a)): an ASK waits for a person. It routes like a task (`edges`,
|
|
442
|
+
// `FD-V7`), receives what its `needs` composes, carries opaque `settings` a UI reads and an `answerSchema` a
|
|
443
|
+
// `done` answer is held to — and NONE of the keys a task's worker reads: those six are `FD-V47`'s
|
|
444
|
+
// (`ASK_REFUSED_KEYS`), refused there by name, so `FD-V17` does not report them a second time.
|
|
445
|
+
ask: { req: ["id", "kind", "edges"], opt: ["settings", "answerSchema", "needs"] }
|
|
369
446
|
};
|
|
447
|
+
var ASK_REFUSED_KEYS = ["type", "pool", "optional", "retryPolicy", "workerGroups", "transform"];
|
|
370
448
|
var EDGE_KEYS = { req: ["to"], opt: ["on", "when", "needs", "route"] };
|
|
371
449
|
var EDGE_KEYS_DECLARED = [...EDGE_KEYS.req, ...EDGE_KEYS.opt];
|
|
372
450
|
var TASK_KEYS_DECLARED = [...NODE_KEYS.task.req, ...NODE_KEYS.task.opt];
|
|
@@ -434,51 +512,110 @@ function keyPathProblem(v) {
|
|
|
434
512
|
if (KEY_PATH_RE.test(v)) return null;
|
|
435
513
|
return `settings.key ${JSON.stringify(v)} is not a key path \u2014 dot-separated object keys, each [A-Za-z0-9_-]{1,64}; no array index, no wildcard, no expression (a key that must be computed is computed in the task before the gate)`;
|
|
436
514
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
515
|
+
var GATE_TYPE_KEYS = ["routes", "defaultRoute", "settingsSchema", "keyResolvedRoutes", "routesFor", "settingsProblem", "fn"];
|
|
516
|
+
var STATIC_ROUTE_KEYS = ["routes", "defaultRoute", "keyResolvedRoutes"];
|
|
517
|
+
var ROUTE_SET_KEYS = STATIC_ROUTE_KEYS;
|
|
518
|
+
var GATE_TYPE_OPTIONAL = /* @__PURE__ */ new Set(["settingsSchema", "keyResolvedRoutes", "settingsProblem"]);
|
|
519
|
+
var GATE_TYPE_SHAPE = `{ ${GATE_TYPE_KEYS.map((k) => GATE_TYPE_OPTIONAL.has(k) ? `${k}?` : k).join(", ")} } with its routes declared STATICALLY by routes and defaultRoute (keyResolvedRoutes beside them) or DERIVED by routesFor, exactly one of the two`;
|
|
520
|
+
function describeValue(v) {
|
|
521
|
+
try {
|
|
522
|
+
const j = JSON.stringify(v);
|
|
523
|
+
if (j !== void 0) return j;
|
|
524
|
+
} catch {
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
return typeof v === "bigint" ? `${v}n` : String(v);
|
|
528
|
+
} catch {
|
|
529
|
+
return `an unprintable ${typeof v}`;
|
|
445
530
|
}
|
|
446
|
-
return v;
|
|
447
531
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
if (!Array.isArray(routes) || routes.length === 0) return `routes must be a non-empty array of route names, got ${JSON.stringify(routes)}`;
|
|
532
|
+
function describeThrown(e) {
|
|
533
|
+
try {
|
|
534
|
+
if (e instanceof Error) return String(e.message);
|
|
535
|
+
} catch {
|
|
536
|
+
}
|
|
537
|
+
return describeValue(e);
|
|
538
|
+
}
|
|
539
|
+
function routeSetProblem(routes, defaultRoute, keyResolvedRoutes) {
|
|
540
|
+
if (!Array.isArray(routes) || routes.length === 0) return `routes must be a non-empty array of route names, got ${describeValue(routes)}`;
|
|
458
541
|
for (const r of routes) {
|
|
459
|
-
if (typeof r !== "string" || !ID_RE.test(r)) return `route ${
|
|
542
|
+
if (typeof r !== "string" || !ID_RE.test(r)) return `route ${describeValue(r)} is not [A-Za-z0-9_-]{1,64}`;
|
|
460
543
|
}
|
|
461
544
|
if (new Set(routes).size !== routes.length) return "routes repeats a route";
|
|
462
|
-
if (typeof
|
|
463
|
-
return `defaultRoute ${
|
|
545
|
+
if (typeof defaultRoute !== "string" || !routes.includes(defaultRoute)) {
|
|
546
|
+
return `defaultRoute ${describeValue(defaultRoute)} is not one of its routes (${routes.join(", ")}) \u2014 an edge with no route would carry a route the type never returns`;
|
|
464
547
|
}
|
|
465
|
-
if (
|
|
466
|
-
const
|
|
467
|
-
if (
|
|
468
|
-
}
|
|
469
|
-
if (o.keyResolvedRoutes !== void 0) {
|
|
470
|
-
const kr = o.keyResolvedRoutes;
|
|
471
|
-
if (!Array.isArray(kr)) return `keyResolvedRoutes must be an array of route names, got ${JSON.stringify(kr)}`;
|
|
548
|
+
if (keyResolvedRoutes !== void 0) {
|
|
549
|
+
const kr = keyResolvedRoutes;
|
|
550
|
+
if (!Array.isArray(kr)) return `keyResolvedRoutes must be an array of route names, got ${describeValue(kr)}`;
|
|
472
551
|
for (const r of kr) {
|
|
473
552
|
if (typeof r !== "string" || !routes.includes(r)) {
|
|
474
|
-
return `keyResolvedRoutes names ${
|
|
553
|
+
return `keyResolvedRoutes names ${describeValue(r)}, which is not one of its routes (${routes.join(", ")}) \u2014 a guarantee on a route the type never returns applies to nothing`;
|
|
475
554
|
}
|
|
476
555
|
}
|
|
477
556
|
if (new Set(kr).size !== kr.length) return "keyResolvedRoutes repeats a route";
|
|
478
557
|
}
|
|
558
|
+
return null;
|
|
559
|
+
}
|
|
560
|
+
function gateTypeProblem(t) {
|
|
561
|
+
if (typeof t !== "object" || t === null || Array.isArray(t)) return `a gate type is an object ${GATE_TYPE_SHAPE}, got ${describeValue(t)}`;
|
|
562
|
+
const o = t;
|
|
563
|
+
const extra = Object.keys(o).filter((k) => !GATE_TYPE_KEYS.includes(k));
|
|
564
|
+
if (extra.length) return `it carries ${extra.map((k) => describeValue(k)).join(", ")}, which a gate type does not declare; it is ${GATE_TYPE_SHAPE}`;
|
|
565
|
+
const staticKeys = STATIC_ROUTE_KEYS.filter((k) => o[k] !== void 0);
|
|
566
|
+
const derived = o.routesFor !== void 0;
|
|
567
|
+
if (derived && staticKeys.length) {
|
|
568
|
+
return `it carries routesFor AND ${staticKeys.join(", ")} \u2014 a gate type declares its routes statically or derives them from its gate's settings, exactly one of the two`;
|
|
569
|
+
}
|
|
570
|
+
if (!derived && staticKeys.length === 0) {
|
|
571
|
+
return "it declares its routes neither statically (routes, defaultRoute) nor by routesFor \u2014 a gate type declares exactly one of the two";
|
|
572
|
+
}
|
|
573
|
+
if (derived) {
|
|
574
|
+
if (typeof o.routesFor !== "function") return "routesFor must be a function (settings) => { routes, defaultRoute, keyResolvedRoutes? }";
|
|
575
|
+
} else {
|
|
576
|
+
const p = routeSetProblem(o.routes, o.defaultRoute, o.keyResolvedRoutes);
|
|
577
|
+
if (p !== null) return p;
|
|
578
|
+
}
|
|
579
|
+
if (o.settingsSchema !== void 0) {
|
|
580
|
+
const p = schemaProblem(o.settingsSchema);
|
|
581
|
+
if (p !== null) return `its settingsSchema is not in the built-in subset \u2014 ${p}`;
|
|
582
|
+
}
|
|
583
|
+
if (o.settingsProblem !== void 0 && typeof o.settingsProblem !== "function") return "settingsProblem must be a function (settings) => string | null";
|
|
479
584
|
if (typeof o.fn !== "function") return "fn must be a function (ctx) => route";
|
|
480
585
|
return null;
|
|
481
586
|
}
|
|
587
|
+
function routesOf(type, settings) {
|
|
588
|
+
let set;
|
|
589
|
+
if (typeof type.routesFor === "function") {
|
|
590
|
+
if (typeof settings !== "object" || settings === null || Array.isArray(settings)) {
|
|
591
|
+
return { problem: `routesFor is asked over settings that are a plain object, got ${describeValue(settings)}` };
|
|
592
|
+
}
|
|
593
|
+
let copy;
|
|
594
|
+
try {
|
|
595
|
+
copy = structuredClone(settings);
|
|
596
|
+
} catch (e) {
|
|
597
|
+
return { problem: `the gate's settings cannot be copied for routesFor \u2014 ${describeThrown(e)}` };
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
set = type.routesFor(copy);
|
|
601
|
+
} catch (e) {
|
|
602
|
+
return { problem: `routesFor threw: ${describeThrown(e)}` };
|
|
603
|
+
}
|
|
604
|
+
if (typeof set !== "object" || set === null || Array.isArray(set)) {
|
|
605
|
+
return { problem: `routesFor returned ${describeValue(set)}, which is not { routes, defaultRoute, keyResolvedRoutes? }` };
|
|
606
|
+
}
|
|
607
|
+
const extra = Object.keys(set).filter((k) => !ROUTE_SET_KEYS.includes(k));
|
|
608
|
+
if (extra.length) return { problem: `routesFor returned ${extra.map((k) => describeValue(k)).join(", ")}, which a route set does not declare; it is { routes, defaultRoute, keyResolvedRoutes? }` };
|
|
609
|
+
} else {
|
|
610
|
+
set = type;
|
|
611
|
+
}
|
|
612
|
+
const s = set;
|
|
613
|
+
const p = routeSetProblem(s.routes, s.defaultRoute, s.keyResolvedRoutes);
|
|
614
|
+
if (p !== null) return { problem: typeof type.routesFor === "function" ? `routesFor returned a malformed route set: ${p}` : p };
|
|
615
|
+
const routes = Object.freeze([...s.routes]);
|
|
616
|
+
const defaultRoute = s.defaultRoute;
|
|
617
|
+
return s.keyResolvedRoutes === void 0 ? Object.freeze({ routes, defaultRoute }) : Object.freeze({ routes, defaultRoute, keyResolvedRoutes: Object.freeze([...s.keyResolvedRoutes]) });
|
|
618
|
+
}
|
|
482
619
|
var distinct = Object.freeze({
|
|
483
620
|
routes: Object.freeze(["first", "duplicate", "nokey"]),
|
|
484
621
|
defaultRoute: "first",
|
|
@@ -502,7 +639,79 @@ var distinct = Object.freeze({
|
|
|
502
639
|
return "first";
|
|
503
640
|
}
|
|
504
641
|
});
|
|
505
|
-
var
|
|
642
|
+
var UNKNOWN_OP = (message) => new Error(message);
|
|
643
|
+
var ifGate = Object.freeze({
|
|
644
|
+
routes: Object.freeze(["then", "else"]),
|
|
645
|
+
defaultRoute: "else",
|
|
646
|
+
// The KEYS only: the grammar is not the subset's to state (P32 fact 3), and `when`'s TYPE is left to
|
|
647
|
+
// `settingsProblem` so a bare string gets the grammar's own message rather than the schema's.
|
|
648
|
+
settingsSchema: Object.freeze({
|
|
649
|
+
type: "object",
|
|
650
|
+
required: ["when"],
|
|
651
|
+
properties: { when: {} },
|
|
652
|
+
additionalProperties: false
|
|
653
|
+
}),
|
|
654
|
+
settingsProblem(settings) {
|
|
655
|
+
const p = gateConditionProblem(settings.when);
|
|
656
|
+
return p === null ? null : `${p.message} (${p.rule}'s grammar, which an if gate's condition is held to)`;
|
|
657
|
+
},
|
|
658
|
+
fn(ctx) {
|
|
659
|
+
return evaluateDeclared(ctx.settings.when, { input: ctx.input }, UNKNOWN_OP) ? "then" : "else";
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
var switchGate = Object.freeze({
|
|
663
|
+
settingsSchema: Object.freeze({
|
|
664
|
+
type: "object",
|
|
665
|
+
required: ["key", "cases"],
|
|
666
|
+
properties: { key: { type: "string", minLength: 1 }, cases: { type: "object" } },
|
|
667
|
+
additionalProperties: false
|
|
668
|
+
}),
|
|
669
|
+
settingsProblem(settings) {
|
|
670
|
+
const cases = settings.cases;
|
|
671
|
+
const seen = [];
|
|
672
|
+
if (Object.keys(cases).length === 0) return 'cases is empty \u2014 a switch with no case routes every value to default, which is "always default" written as a question';
|
|
673
|
+
for (const name of Object.keys(cases)) {
|
|
674
|
+
if (!ID_RE.test(name)) return `cases names the case ${JSON.stringify(name)}, which is not [A-Za-z0-9_-]{1,64} \u2014 a case name is a ROUTE name (FD-V16's charset)`;
|
|
675
|
+
if (name === "default") return 'cases names a case "default", which is the route a key matching no case takes \u2014 the name is reserved';
|
|
676
|
+
const v = cases[name];
|
|
677
|
+
if (Array.isArray(v) && v.length === 0) return `cases.${name} is an empty array; a case names a scalar or a non-empty array of scalars \u2014 a case no value can reach is a route no run can take`;
|
|
678
|
+
for (const x of Array.isArray(v) ? v : [v]) {
|
|
679
|
+
if (!isScalar(x)) {
|
|
680
|
+
return `cases.${name} ${Array.isArray(v) ? "holds" : "is"} ${scalarName(x)}; a case names a SCALAR \u2014 null, a boolean, a finite number or a string \u2014 or a non-empty array of them`;
|
|
681
|
+
}
|
|
682
|
+
const other = seen.find((s) => s.value === x);
|
|
683
|
+
if (other !== void 0 && other.name === name) {
|
|
684
|
+
return `cases.${name} repeats the value ${caseValue(x)} \u2014 a case names each of its values once`;
|
|
685
|
+
}
|
|
686
|
+
if (other !== void 0) {
|
|
687
|
+
if (!Object.is(other.value, x)) {
|
|
688
|
+
return `the values ${caseValue(other.value)} in case "${other.name}" and ${caseValue(x)} in case "${name}" are one value by === \u2014 a value takes one route`;
|
|
689
|
+
}
|
|
690
|
+
return `the value ${caseValue(x)} is named by two cases, "${other.name}" and "${name}" \u2014 a value takes one route (matching is ===, so "1" and 1 are two values)`;
|
|
691
|
+
}
|
|
692
|
+
seen.push({ value: x, name });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return null;
|
|
696
|
+
},
|
|
697
|
+
routesFor(settings) {
|
|
698
|
+
const cases = settings.cases;
|
|
699
|
+
if (typeof cases !== "object" || cases === null || Array.isArray(cases)) throw new Error(`cases is ${scalarName(cases)}, not an object of case names`);
|
|
700
|
+
const names = Object.keys(cases);
|
|
701
|
+
return { routes: [...names, "default"], defaultRoute: "default", keyResolvedRoutes: names };
|
|
702
|
+
},
|
|
703
|
+
fn(ctx) {
|
|
704
|
+
const key = ctx.key;
|
|
705
|
+
if (key === void 0 || typeof key === "object" && key !== null) return "default";
|
|
706
|
+
const cases = ctx.settings.cases;
|
|
707
|
+
for (const name of Object.keys(cases)) {
|
|
708
|
+
const v = cases[name];
|
|
709
|
+
if (Array.isArray(v) ? v.some((x) => x === key) : v === key) return name;
|
|
710
|
+
}
|
|
711
|
+
return "default";
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
var BUILT_IN_GATE_TYPES = Object.freeze({ distinct, if: ifGate, switch: switchGate });
|
|
506
715
|
function gateTypesOf(x) {
|
|
507
716
|
const out = new Map(Object.entries(BUILT_IN_GATE_TYPES));
|
|
508
717
|
const own3 = x === void 0 ? [] : x instanceof Map ? [...x] : Object.entries(x);
|
|
@@ -527,11 +736,23 @@ function predicateShapeOf(o) {
|
|
|
527
736
|
if (marks.length > 1) return "ambiguous";
|
|
528
737
|
return marks[0];
|
|
529
738
|
}
|
|
739
|
+
var caseValue = (v) => Object.is(v, -0) ? "-0" : JSON.stringify(v);
|
|
530
740
|
var isScalar = (v) => v === null || typeof v === "boolean" || typeof v === "string" || typeof v === "number" && Number.isFinite(v);
|
|
531
741
|
var scalarName = (v) => v === null ? "null" : Array.isArray(v) ? "an array" : typeof v === "number" && !Number.isFinite(v) ? String(v) : typeof v;
|
|
532
742
|
var NUMERIC_OPS = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
|
|
533
743
|
var STRING_OPS = /* @__PURE__ */ new Set(["startsWith"]);
|
|
534
|
-
|
|
744
|
+
var EDGE_SCOPE = {
|
|
745
|
+
roots: PREDICATE_ROOTS,
|
|
746
|
+
foreignRoot: () => `which a decision cannot see \u2014 a predicate's roots are ${PREDICATE_ROOTS.map((r) => JSON.stringify(r)).join(" and ")}, and any other first segment would resolve to nothing in silence`,
|
|
747
|
+
rootAlone: (root) => `and names no value \u2014 ${JSON.stringify(root)} is a container, so a comparison against it could only ever be no match`
|
|
748
|
+
};
|
|
749
|
+
var GATE_CONDITION_ROOTS = ["input"];
|
|
750
|
+
var GATE_SCOPE = {
|
|
751
|
+
roots: GATE_CONDITION_ROOTS,
|
|
752
|
+
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',
|
|
753
|
+
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"
|
|
754
|
+
};
|
|
755
|
+
function leafProblem(o, where, scope = EDGE_SCOPE) {
|
|
535
756
|
const extra = Object.keys(o).filter((k) => !PREDICATE_LEAF_KEYS.includes(k));
|
|
536
757
|
if (extra.length) {
|
|
537
758
|
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(", ")} }` };
|
|
@@ -544,11 +765,11 @@ function leafProblem(o, where) {
|
|
|
544
765
|
return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} is not a key path \u2014 dot-separated object keys, each [A-Za-z0-9_-]{1,64}; no array index, no wildcard, no expression` };
|
|
545
766
|
}
|
|
546
767
|
const root = path.split(".")[0];
|
|
547
|
-
if (!
|
|
548
|
-
return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} starts at ${JSON.stringify(root)},
|
|
768
|
+
if (!scope.roots.includes(root)) {
|
|
769
|
+
return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} starts at ${JSON.stringify(root)}, ${scope.foreignRoot(root)}` };
|
|
549
770
|
}
|
|
550
771
|
if (path.split(".").length < 2) {
|
|
551
|
-
return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} is a root alone
|
|
772
|
+
return { rule: "FD-V43", message: `${where} path ${JSON.stringify(path)} is a root alone ${scope.rootAlone(root)}` };
|
|
552
773
|
}
|
|
553
774
|
const op = o.op;
|
|
554
775
|
if (typeof op !== "string" || !PREDICATE_OPS.includes(op)) {
|
|
@@ -604,23 +825,26 @@ function predicateProblem(v) {
|
|
|
604
825
|
return { rule: "FD-V42", message: `when carries ${Object.keys(o).map((k) => JSON.stringify(k)).join(", ")} and matches more than one shape \u2014 a name, a comparison and a group are told apart by which keys are present, so an object may mark only one` };
|
|
605
826
|
}
|
|
606
827
|
if (shape === "name") {
|
|
607
|
-
const
|
|
608
|
-
if (
|
|
828
|
+
const extra = Object.keys(o).filter((k) => !PREDICATE_NAME_KEYS.includes(k));
|
|
829
|
+
if (extra.length) return { rule: "FD-V42", message: `when names a predicate and also carries ${extra.map((k) => JSON.stringify(k)).join(", ")}; the name form is { predicate } and nothing else` };
|
|
609
830
|
if (typeof o.predicate !== "string") return { rule: "FD-V42", message: `when predicate is ${scalarName(o.predicate)}; it is the name of a registered predicate` };
|
|
610
831
|
return null;
|
|
611
832
|
}
|
|
612
833
|
if (shape === "leaf") return leafProblem(o, "when");
|
|
834
|
+
return groupProblem(o, "when", EDGE_SCOPE);
|
|
835
|
+
}
|
|
836
|
+
function groupProblem(o, label, scope) {
|
|
613
837
|
const extra = Object.keys(o).filter((k) => !PREDICATE_GROUP_KEYS.includes(k));
|
|
614
|
-
if (extra.length) return { rule: "FD-V42", message:
|
|
615
|
-
if ("all" in o && "any" in o) return { rule: "FD-V45", message:
|
|
838
|
+
if (extra.length) return { rule: "FD-V42", message: `${label} is a group and also carries ${extra.map((k) => JSON.stringify(k)).join(", ")}; a group is { all } or { any } and nothing else` };
|
|
839
|
+
if ("all" in o && "any" in o) return { rule: "FD-V45", message: `${label} carries both "all" and "any"; a group is one or the other, because a document that says both says neither` };
|
|
616
840
|
const which = "all" in o ? "all" : "any";
|
|
617
841
|
const members = o[which];
|
|
618
842
|
if (!Array.isArray(members) || members.length === 0) {
|
|
619
|
-
return { rule: "FD-V45", message:
|
|
843
|
+
return { rule: "FD-V45", message: `${label} ${which} is ${Array.isArray(members) ? "empty" : scalarName(members)}; it is a non-empty array of comparisons \u2014 an empty group is "always true" or "always false" written as a question` };
|
|
620
844
|
}
|
|
621
845
|
for (let i = 0; i < members.length; i++) {
|
|
622
846
|
const m = members[i];
|
|
623
|
-
const at =
|
|
847
|
+
const at = `${label} ${which}[${i}]`;
|
|
624
848
|
if (typeof m !== "object" || m === null || Array.isArray(m)) {
|
|
625
849
|
return { rule: "FD-V45", message: `${at} is ${scalarName(m)}; a group's members are comparisons` };
|
|
626
850
|
}
|
|
@@ -634,11 +858,30 @@ function predicateProblem(v) {
|
|
|
634
858
|
if (ms === null || ms === "ambiguous") {
|
|
635
859
|
return { rule: "FD-V45", message: `${at} carries ${Object.keys(m).map((k) => JSON.stringify(k)).join(", ") || "no keys"} and is not a comparison` };
|
|
636
860
|
}
|
|
637
|
-
const p = leafProblem(m, at);
|
|
861
|
+
const p = leafProblem(m, at, scope);
|
|
638
862
|
if (p) return p;
|
|
639
863
|
}
|
|
640
864
|
return null;
|
|
641
865
|
}
|
|
866
|
+
function gateConditionProblem(v) {
|
|
867
|
+
const label = "settings.when";
|
|
868
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
869
|
+
return { rule: "FD-V42", message: `${label} is ${scalarName(v)}; it is an object \u2014 a comparison { ${PREDICATE_LEAF_KEYS.join(", ")} } or a flat group { all } or { any }, over the root "input"` };
|
|
870
|
+
}
|
|
871
|
+
const o = v;
|
|
872
|
+
const shape = predicateShapeOf(o);
|
|
873
|
+
if (shape === null) {
|
|
874
|
+
return { rule: "FD-V42", message: `${label} carries ${Object.keys(o).map((k) => JSON.stringify(k)).join(", ") || "no keys"} and is neither a comparison { ${PREDICATE_LEAF_KEYS.join(", ")} } nor a flat group { all } or { any }` };
|
|
875
|
+
}
|
|
876
|
+
if (shape === "ambiguous") {
|
|
877
|
+
return { rule: "FD-V42", message: `${label} carries ${Object.keys(o).map((k) => JSON.stringify(k)).join(", ")} and matches more than one shape \u2014 a comparison and a group are told apart by which keys are present, so an object may mark only one` };
|
|
878
|
+
}
|
|
879
|
+
if (shape === "name") {
|
|
880
|
+
return { rule: "FD-V42", message: `${label} names a registered predicate, which an if gate cannot ask \u2014 a predicate is handed a decision's outcome and its edge's needs, and a gate has neither. Write a comparison over "input", or register a gate type of your own` };
|
|
881
|
+
}
|
|
882
|
+
if (shape === "leaf") return leafProblem(o, label, GATE_SCOPE);
|
|
883
|
+
return groupProblem(o, label, GATE_SCOPE);
|
|
884
|
+
}
|
|
642
885
|
function predicateNamesIn(v) {
|
|
643
886
|
if (typeof v !== "object" || v === null || Array.isArray(v)) return [];
|
|
644
887
|
const o = v;
|
|
@@ -707,6 +950,8 @@ function aimedReferences(n) {
|
|
|
707
950
|
}
|
|
708
951
|
function referenceTarget(ref) {
|
|
709
952
|
if (ref === ".") return { form: "self", node: null, conditional: false };
|
|
953
|
+
if (ref === PAYLOAD_TOKEN) return { form: "payload", node: null, conditional: false };
|
|
954
|
+
if (ref === ITEM_TOKEN) return { form: "item", node: null, conditional: false };
|
|
710
955
|
if (typeof ref === "string") return aimed(ref, "one");
|
|
711
956
|
if (Array.isArray(ref) && ref.length === 1 && typeof ref[0] === "string") {
|
|
712
957
|
return aimed(ref[0], "all");
|
|
@@ -718,6 +963,20 @@ function aimed(s, form) {
|
|
|
718
963
|
const node = conditional ? s.slice(0, -1) : s;
|
|
719
964
|
return ID_RE.test(node) ? { form, node, conditional } : null;
|
|
720
965
|
}
|
|
966
|
+
var PAYLOAD_TOKEN = "$payload";
|
|
967
|
+
var ITEM_TOKEN = "$item";
|
|
968
|
+
function dollarProblem(ref) {
|
|
969
|
+
const one = Array.isArray(ref) && ref.length === 1 ? ref[0] : ref;
|
|
970
|
+
if (typeof one !== "string" || !one.startsWith("$")) return null;
|
|
971
|
+
const bare = one.endsWith("?") ? one.slice(0, -1) : one;
|
|
972
|
+
const known = bare === PAYLOAD_TOKEN || bare === ITEM_TOKEN;
|
|
973
|
+
if (known && Array.isArray(ref)) {
|
|
974
|
+
return `${JSON.stringify(ref)} is an array form, and there is ONE ${bare === PAYLOAD_TOKEN ? "run payload" : "item per map instance"}: write ${JSON.stringify(bare)}`;
|
|
975
|
+
}
|
|
976
|
+
if (known) return `${JSON.stringify(one)} carries "?", and ${JSON.stringify(bare)} is always there: write ${JSON.stringify(bare)} without "?"`;
|
|
977
|
+
return `${JSON.stringify(ref)} is not a reference: the "$" prefix is reserved, and its references are "$payload" (the run's payload) and "$item" (a map instance's item)`;
|
|
978
|
+
}
|
|
979
|
+
var itemOutside = (site, k, nodeId) => `${site}[${JSON.stringify(k)}] is "$item", and "${nodeId}" is not inside a map instance: name the item where a map's branch receives it ("."), or move this node inside the map`;
|
|
721
980
|
function validateDefinition(doc, registry) {
|
|
722
981
|
const f = [];
|
|
723
982
|
const add = (rule, at, detail) => f.push({ rule, at, detail });
|
|
@@ -764,7 +1023,7 @@ function validateDefinition(doc, registry) {
|
|
|
764
1023
|
}
|
|
765
1024
|
if (nd?.kind === void 0) continue;
|
|
766
1025
|
if (typeof nd.kind !== "string" || !KINDS.includes(nd.kind)) {
|
|
767
|
-
add("FD-V24", where, `node kind ${JSON.stringify(nd.kind)} is not task|fanout|map|join|end|gate`);
|
|
1026
|
+
add("FD-V24", where, `node kind ${JSON.stringify(nd.kind)} is not task|fanout|map|join|end|gate|ask`);
|
|
768
1027
|
continue;
|
|
769
1028
|
}
|
|
770
1029
|
const spec = NODE_KEYS[nd.kind];
|
|
@@ -775,7 +1034,11 @@ function validateDefinition(doc, registry) {
|
|
|
775
1034
|
}
|
|
776
1035
|
for (const k of Object.keys(nd)) {
|
|
777
1036
|
if (!spec.req.includes(k) && !spec.opt.includes(k)) {
|
|
778
|
-
|
|
1037
|
+
if (nd.kind === "ask" && ASK_REFUSED_KEYS.includes(k)) {
|
|
1038
|
+
add("FD-V47", where, k === "transform" ? 'ask node carries "transform", which only a task declares \u2014 a transform is evaluated in a node type\'s worker wrapper (FD-C6) and an ask runs no worker, so nothing would evaluate it; what reaches an ask is composed by its `needs` (P35 decision 6)' : `ask node carries "${k}", which only a task declares \u2014 it is read by a task's worker or its scheduler task, and an ask runs no worker and is never dispatched, so nothing would read it`);
|
|
1039
|
+
} else {
|
|
1040
|
+
add("FD-V17", where, `${nd.kind} node carries undeclared key "${k}"`);
|
|
1041
|
+
}
|
|
779
1042
|
}
|
|
780
1043
|
}
|
|
781
1044
|
if (nd.kind === "task") {
|
|
@@ -865,21 +1128,63 @@ function validateDefinition(doc, registry) {
|
|
|
865
1128
|
}
|
|
866
1129
|
}
|
|
867
1130
|
const settings = nd.settings;
|
|
1131
|
+
let settingsRefused = false;
|
|
868
1132
|
if (settings !== void 0 && (typeof settings !== "object" || settings === null || Array.isArray(settings))) {
|
|
869
|
-
add("FD-V40", where, `settings must be a plain object, got ${
|
|
1133
|
+
add("FD-V40", where, `settings must be a plain object, got ${describeValue(settings)}`);
|
|
1134
|
+
settingsRefused = true;
|
|
870
1135
|
} else {
|
|
871
1136
|
const s = settings ?? {};
|
|
872
1137
|
if (Object.prototype.hasOwnProperty.call(s, "key")) {
|
|
873
1138
|
const kp = keyPathProblem(s.key);
|
|
874
|
-
if (kp !== null)
|
|
1139
|
+
if (kp !== null) {
|
|
1140
|
+
add("FD-V40", where, kp);
|
|
1141
|
+
settingsRefused = true;
|
|
1142
|
+
}
|
|
875
1143
|
}
|
|
1144
|
+
let schemaRefused = false;
|
|
876
1145
|
if (usable?.settingsSchema !== void 0) {
|
|
877
1146
|
const fs = findings(usable.settingsSchema, s);
|
|
878
|
-
if (fs.length)
|
|
1147
|
+
if (fs.length) {
|
|
1148
|
+
add("FD-V40", where, `settings ${describeValue(s)} fail gate type "${typeName}"'s settingsSchema: ${describeFindings(fs)}`);
|
|
1149
|
+
schemaRefused = true;
|
|
1150
|
+
settingsRefused = true;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
let uncopyable = false;
|
|
1154
|
+
if (usable !== void 0 && (usable.settingsProblem !== void 0 || typeof usable.routesFor === "function")) {
|
|
1155
|
+
try {
|
|
1156
|
+
structuredClone(s);
|
|
1157
|
+
} catch (e) {
|
|
1158
|
+
add("FD-V40", where, `settings cannot be copied for gate type "${typeName}" \u2014 they must survive structuredClone, as a JSON document's do: ${describeThrown(e)}`);
|
|
1159
|
+
uncopyable = true;
|
|
1160
|
+
settingsRefused = true;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
if (usable?.settingsProblem !== void 0 && !schemaRefused && !uncopyable) {
|
|
1164
|
+
let said;
|
|
1165
|
+
let threw = false;
|
|
1166
|
+
const copy = structuredClone(s);
|
|
1167
|
+
try {
|
|
1168
|
+
said = usable.settingsProblem(copy);
|
|
1169
|
+
} catch (e) {
|
|
1170
|
+
threw = true;
|
|
1171
|
+
add("FD-V39", where, `type "${typeName}"'s settingsProblem threw: ${describeThrown(e)}`);
|
|
1172
|
+
}
|
|
1173
|
+
if (!threw && said !== null) {
|
|
1174
|
+
if (typeof said === "string") {
|
|
1175
|
+
add("FD-V40", where, `settings ${describeValue(s)} fail gate type "${typeName}"'s settingsProblem: ` + (said === "" ? "it returned an EMPTY message \u2014 a refusal with no reason given (null is the pass)" : said));
|
|
1176
|
+
} else add("FD-V39", where, `type "${typeName}"'s settingsProblem returned ${describeValue(said)}, which is neither a message nor null`);
|
|
1177
|
+
}
|
|
1178
|
+
if (threw || said !== null) settingsRefused = true;
|
|
879
1179
|
}
|
|
880
1180
|
}
|
|
881
|
-
|
|
882
|
-
|
|
1181
|
+
let routeSet;
|
|
1182
|
+
if (usable !== void 0 && !(typeof usable.routesFor === "function" && settingsRefused)) {
|
|
1183
|
+
routeSet = routesOf(usable, settings ?? {});
|
|
1184
|
+
if ("problem" in routeSet) add("FD-V39", where, `type "${typeName}" resolves no route set for this gate: ${routeSet.problem}`);
|
|
1185
|
+
}
|
|
1186
|
+
if (routeSet !== void 0 && !("problem" in routeSet) && Array.isArray(nd.edges)) {
|
|
1187
|
+
const gate = routeSet;
|
|
883
1188
|
const count = new Map(gate.routes.map((r) => [r, 0]));
|
|
884
1189
|
nd.edges.forEach((e, i) => {
|
|
885
1190
|
if (e === null || typeof e !== "object") return;
|
|
@@ -887,7 +1192,7 @@ function validateDefinition(doc, registry) {
|
|
|
887
1192
|
if (r !== void 0 && typeof r !== "string") return;
|
|
888
1193
|
const route = r === void 0 ? gate.defaultRoute : r;
|
|
889
1194
|
if (!count.has(route)) {
|
|
890
|
-
add("FD-V41", `${where}#edge${i}`, `route "${route}" is not
|
|
1195
|
+
add("FD-V41", `${where}#edge${i}`, `route "${route}" is not one of this gate's routes \u2014 the routes gate type "${typeName}" declares for it are ${gate.routes.map((x) => `"${x}"`).join(", ")}`);
|
|
891
1196
|
return;
|
|
892
1197
|
}
|
|
893
1198
|
count.set(route, count.get(route) + 1);
|
|
@@ -895,13 +1200,23 @@ function validateDefinition(doc, registry) {
|
|
|
895
1200
|
for (const [route, n2] of count) {
|
|
896
1201
|
const how = route === gate.defaultRoute ? " (the default route \u2014 an edge with no `route` carries it)" : "";
|
|
897
1202
|
if (n2 === 0) {
|
|
898
|
-
add("FD-V41", where, `route "${route}"${how} of gate type "${typeName}" has no edge; every
|
|
1203
|
+
add("FD-V41", where, `route "${route}"${how} of this gate (gate type "${typeName}") has no edge; every route of the gate is wired exactly once \u2014 a drop is an edge to an \`end\` node, because a route left unwired would be dropped in silence the day the type grows one`);
|
|
899
1204
|
} else if (n2 > 1) {
|
|
900
|
-
add("FD-V41", where, `route "${route}"${how} of gate type "${typeName}" is wired by ${n2} edges; exactly one`);
|
|
1205
|
+
add("FD-V41", where, `route "${route}"${how} of this gate (gate type "${typeName}") is wired by ${n2} edges; exactly one`);
|
|
901
1206
|
}
|
|
902
1207
|
}
|
|
903
1208
|
}
|
|
904
1209
|
}
|
|
1210
|
+
if (nd.kind === "ask") {
|
|
1211
|
+
const s = nd.settings;
|
|
1212
|
+
if (s !== void 0 && (typeof s !== "object" || s === null || Array.isArray(s))) {
|
|
1213
|
+
add("FD-V21", where, `ask settings must be a plain object, got ${describeValue(s)}`);
|
|
1214
|
+
}
|
|
1215
|
+
if (nd.answerSchema !== void 0) {
|
|
1216
|
+
const problem = schemaProblem(nd.answerSchema);
|
|
1217
|
+
if (problem !== null) add("FD-V48", where, `answerSchema is not in the built-in schema subset \u2014 ${problem}`);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
905
1220
|
if (nd.kind === "end") {
|
|
906
1221
|
if (nd.edges !== void 0) add("FD-V8", where, "end node has edges");
|
|
907
1222
|
if (nd.outcome !== void 0 && typeof nd.outcome !== "string") {
|
|
@@ -936,8 +1251,11 @@ function validateDefinition(doc, registry) {
|
|
|
936
1251
|
for (const [k, ref] of Object.entries(nv)) {
|
|
937
1252
|
if (!ID_RE.test(k)) add("FD-V26", where, `needs key ${JSON.stringify(k)} is not [A-Za-z0-9_-]{1,64}`);
|
|
938
1253
|
const target = referenceTarget(ref);
|
|
939
|
-
|
|
940
|
-
|
|
1254
|
+
const dollar = target === null ? dollarProblem(ref) : null;
|
|
1255
|
+
if (dollar !== null) {
|
|
1256
|
+
add("FD-V26", where, `needs[${JSON.stringify(k)}]: ${dollar}`);
|
|
1257
|
+
} else if (target === null) {
|
|
1258
|
+
add("FD-V26", where, `needs[${JSON.stringify(k)}] is ${JSON.stringify(ref)}, which is not "." , "$payload", "$item", a node id with an optional "?" suffix, or a one-element array holding one`);
|
|
941
1259
|
}
|
|
942
1260
|
}
|
|
943
1261
|
}
|
|
@@ -999,9 +1317,12 @@ function validateDefinition(doc, registry) {
|
|
|
999
1317
|
for (const [k, ref] of Object.entries(nv)) {
|
|
1000
1318
|
if (!ID_RE.test(k)) add("FD-V26", at, `edge needs key ${JSON.stringify(k)} is not [A-Za-z0-9_-]{1,64}`);
|
|
1001
1319
|
const target = referenceTarget(ref);
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1320
|
+
const dollar = target === null ? dollarProblem(ref) : null;
|
|
1321
|
+
if (dollar !== null) {
|
|
1322
|
+
add("FD-V26", at, `edge needs[${JSON.stringify(k)}]: ${dollar}`);
|
|
1323
|
+
} else if (target === null) {
|
|
1324
|
+
add("FD-V26", at, `edge needs[${JSON.stringify(k)}] is ${JSON.stringify(ref)}, which is not "$payload", "$item", a node id with an optional "?" suffix or a one-element array holding one`);
|
|
1325
|
+
} else if (target.form === "self") {
|
|
1005
1326
|
add("FD-V26", at, `edge needs[${JSON.stringify(k)}] is ".", which an EDGE may not use: what a predicate would have received anyway is its node's own outcome, and \`ctx.outcome\` already carries it`);
|
|
1006
1327
|
}
|
|
1007
1328
|
}
|
|
@@ -1046,18 +1367,18 @@ function validateDefinition(doc, registry) {
|
|
|
1046
1367
|
}
|
|
1047
1368
|
for (const n of nodes) {
|
|
1048
1369
|
if (typeof n?.id !== "string") continue;
|
|
1049
|
-
if (n.kind === "task") {
|
|
1370
|
+
if (n.kind === "task" || n.kind === "ask") {
|
|
1050
1371
|
const edges = (Array.isArray(n.edges) ? n.edges : []).filter(
|
|
1051
1372
|
(e) => e !== null && typeof e === "object"
|
|
1052
1373
|
);
|
|
1053
1374
|
if (edges.length === 0) {
|
|
1054
|
-
add("FD-V7", n.id,
|
|
1375
|
+
add("FD-V7", n.id, `${n.kind} node has no edges`);
|
|
1055
1376
|
continue;
|
|
1056
1377
|
}
|
|
1057
1378
|
const onOf2 = (e) => Array.isArray(e?.on) && e.on.length ? e.on : ["done"];
|
|
1058
1379
|
const states = /* @__PURE__ */ new Set();
|
|
1059
1380
|
for (const e of edges) for (const s of onOf2(e)) states.add(s);
|
|
1060
|
-
if (!states.has("done")) add("FD-V7", n.id,
|
|
1381
|
+
if (!states.has("done")) add("FD-V7", n.id, `no edge of this ${n.kind} node is eligible for "done"`);
|
|
1061
1382
|
for (const t of states) {
|
|
1062
1383
|
const eligible = edges.filter((e) => onOf2(e).includes(t));
|
|
1063
1384
|
const bare = eligible.filter((e) => e.when === void 0);
|
|
@@ -1077,8 +1398,8 @@ function validateDefinition(doc, registry) {
|
|
|
1077
1398
|
}
|
|
1078
1399
|
}
|
|
1079
1400
|
for (const m of maps) {
|
|
1080
|
-
if (typeof m.branch === "string" && g.byId.has(m.branch) && kindOf2(m.branch) !== "task") {
|
|
1081
|
-
add("FD-V25", m.id, `branch "${m.branch}" is a ${kindOf2(m.branch)}, not a task`);
|
|
1401
|
+
if (typeof m.branch === "string" && g.byId.has(m.branch) && kindOf2(m.branch) !== "task" && kindOf2(m.branch) !== "ask") {
|
|
1402
|
+
add("FD-V25", m.id, `branch "${m.branch}" is a ${kindOf2(m.branch)}, not a task or an ask`);
|
|
1082
1403
|
}
|
|
1083
1404
|
}
|
|
1084
1405
|
for (const fan of fanouts) {
|
|
@@ -1086,8 +1407,8 @@ function validateDefinition(doc, registry) {
|
|
|
1086
1407
|
if (b.length < 2) add("FD-V9", fan.id, `fanout has ${b.length} branches, needs at least 2`);
|
|
1087
1408
|
if (new Set(b).size !== b.length) add("FD-V9", fan.id, "fanout repeats a branch entry");
|
|
1088
1409
|
for (const x of b) {
|
|
1089
|
-
if (g.byId.has(x) && kindOf2(x) !== "task") {
|
|
1090
|
-
add("FD-V9", fan.id, `branch entry "${x}" is a ${kindOf2(x)}, not a task`);
|
|
1410
|
+
if (g.byId.has(x) && kindOf2(x) !== "task" && kindOf2(x) !== "ask") {
|
|
1411
|
+
add("FD-V9", fan.id, `branch entry "${x}" is a ${kindOf2(x)}, not a task or an ask`);
|
|
1091
1412
|
}
|
|
1092
1413
|
}
|
|
1093
1414
|
}
|
|
@@ -1158,14 +1479,19 @@ function validateDefinition(doc, registry) {
|
|
|
1158
1479
|
if (typeof n !== "object" || n === null || typeof n.id !== "string") continue;
|
|
1159
1480
|
for (const [k, ref, site, at] of aimedReferences(n)) {
|
|
1160
1481
|
const t = referenceTarget(ref);
|
|
1161
|
-
if (t === null
|
|
1482
|
+
if (t === null) continue;
|
|
1483
|
+
if (t.form === "item") {
|
|
1484
|
+
if (innermostMapOf(regions, n.id) === void 0) add("FD-V49", at, itemOutside(site, k, n.id));
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
if (t.form === "self" || t.form === "payload") continue;
|
|
1162
1488
|
const target = t.node;
|
|
1163
1489
|
if (!g.byId.has(target)) {
|
|
1164
1490
|
add("FD-V27", at, `${site}[${JSON.stringify(k)}] names "${target}", which is not a node`);
|
|
1165
1491
|
continue;
|
|
1166
1492
|
}
|
|
1167
|
-
if (kindOf2(target) !== "task") {
|
|
1168
|
-
add("FD-V27", at, `${site}[${JSON.stringify(k)}] names "${target}", a ${kindOf2(target)}; only a task produces an output`);
|
|
1493
|
+
if (kindOf2(target) !== "task" && kindOf2(target) !== "ask") {
|
|
1494
|
+
add("FD-V27", at, `${site}[${JSON.stringify(k)}] names "${target}", a ${kindOf2(target)}; only a task or an ask produces an output`);
|
|
1169
1495
|
continue;
|
|
1170
1496
|
}
|
|
1171
1497
|
if (!reachFrom(g, target).has(n.id)) {
|
|
@@ -1250,9 +1576,12 @@ var DOCUMENT_KEYS = ["workers", "groups", "nodeTypes", "pools", "predicates", "l
|
|
|
1250
1576
|
var WORKER_KEYS = ["url", "configSchema", "settingsSchema", "inputSchema", "outputSchema"];
|
|
1251
1577
|
var CHECKED_WORKER_SCHEMAS = ["configSchema", "settingsSchema"];
|
|
1252
1578
|
var NODE_TYPE_DECLARATION_KEYS = ["worker", "inline", "config", "pool", "groups", "retryPolicy", "settings"];
|
|
1253
|
-
var POOL_SPEC_KEYS = ["config", "maxThreads", "maxConcurrentTasks", "idleTimeout", "groups", "residentGroups"];
|
|
1579
|
+
var POOL_SPEC_KEYS = ["config", "maxThreads", "maxConcurrentTasks", "idleTimeout", "groups", "residentGroups", "cancelGraceMs", "messages"];
|
|
1580
|
+
var MESSAGE_OPTION_KEYS = ["flushIntervalMs", "maxBuffered"];
|
|
1254
1581
|
var DECLARED_POOL_KEYS = POOL_SPEC_KEYS.filter((k) => k !== "config" && k !== "groups");
|
|
1255
1582
|
var INSTANCE_KEYS = ["type", "config"];
|
|
1583
|
+
var POOL_INSTANCE_KEYS = [...INSTANCE_KEYS, "pool"];
|
|
1584
|
+
var MAX_TIMER_MS = 2 ** 31 - 1;
|
|
1256
1585
|
var isPlainObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1257
1586
|
var hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
1258
1587
|
var isBuiltInNodeType = (name) => hasOwn(BUILT_IN_NODE_TYPES, name);
|
|
@@ -1270,8 +1599,23 @@ function mergeConfig(base, over) {
|
|
|
1270
1599
|
if (isPlainObject2(base) && isPlainObject2(over)) return { ...base, ...over };
|
|
1271
1600
|
return over;
|
|
1272
1601
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
1602
|
+
var DECLARATIONS = /* @__PURE__ */ new WeakMap();
|
|
1603
|
+
function declarationOf(t) {
|
|
1604
|
+
return isDerivation(t) ? DECLARATIONS.get(t) : t;
|
|
1605
|
+
}
|
|
1606
|
+
function mergePool(base, over) {
|
|
1607
|
+
if (over === void 0) return base;
|
|
1608
|
+
const out = { ...base };
|
|
1609
|
+
for (const [k, v] of Object.entries(over)) if (v !== void 0) out[k] = v;
|
|
1610
|
+
return out;
|
|
1611
|
+
}
|
|
1612
|
+
function copyResident(r) {
|
|
1613
|
+
if (Array.isArray(r)) return [...r];
|
|
1614
|
+
if (isPlainObject2(r)) return { ...r };
|
|
1615
|
+
return r;
|
|
1616
|
+
}
|
|
1617
|
+
function nodeTypeFromDeclaration(decl, over) {
|
|
1618
|
+
const declaredPool = mergePool(decl.pool ?? {}, over);
|
|
1275
1619
|
const def = {
|
|
1276
1620
|
worker: decl.worker,
|
|
1277
1621
|
...decl.inline !== void 0 ? { inline: decl.inline } : {},
|
|
@@ -1283,10 +1627,18 @@ function nodeTypeFromDeclaration(decl) {
|
|
|
1283
1627
|
return {
|
|
1284
1628
|
...merged !== void 0 ? { config: merged } : {},
|
|
1285
1629
|
...declaredPool,
|
|
1630
|
+
// **Every declared pool OBJECT is copied, never aliased** — `messages` and `groups` since W159, and
|
|
1631
|
+
// `residentGroups` since v0.37 (P37 decision 8, ADR 0099 rule 3; it was handed on AS DECLARED until then,
|
|
1632
|
+
// W159's conformity F3), so no two pools' specs share an object and none shares one with the document. A
|
|
1633
|
+
// CLASS derivation's output is handed on as the class returned it — harmless, since the scheduler spreads
|
|
1634
|
+
// `messages` into a fresh object (W159's adequacy F7) and reads `residentGroups` without writing it.
|
|
1635
|
+
...declaredPool.messages !== void 0 ? { messages: { ...declaredPool.messages } } : {},
|
|
1636
|
+
...declaredPool.residentGroups !== void 0 ? { residentGroups: copyResident(declaredPool.residentGroups) } : {},
|
|
1286
1637
|
...decl.groups !== void 0 ? { groups: [...decl.groups] } : {}
|
|
1287
1638
|
};
|
|
1288
1639
|
}
|
|
1289
1640
|
};
|
|
1641
|
+
DECLARATIONS.set(def, over === void 0 ? decl : { ...decl, pool: declaredPool });
|
|
1290
1642
|
return def;
|
|
1291
1643
|
}
|
|
1292
1644
|
function poolSpecProblem(v) {
|
|
@@ -1309,11 +1661,53 @@ function poolSpecProblem(v) {
|
|
|
1309
1661
|
for (const [k, c] of Object.entries(r)) if (!(typeof c === "number" && Number.isFinite(c) && c > 0)) return `PoolSpec.residentGroups[${q(k)}] must be a cost > 0, got ${JSON.stringify(c)}`;
|
|
1310
1662
|
} else return `PoolSpec.residentGroups must be an array of group keys or a { key: cost } record, got ${JSON.stringify(r)}`;
|
|
1311
1663
|
}
|
|
1664
|
+
if (v.cancelGraceMs !== void 0 && !(typeof v.cancelGraceMs === "number" && v.cancelGraceMs >= 0)) {
|
|
1665
|
+
return `PoolSpec.cancelGraceMs must be a number >= 0 (Infinity: never force), got ${shown(v.cancelGraceMs)}`;
|
|
1666
|
+
}
|
|
1667
|
+
if (v.messages !== void 0) {
|
|
1668
|
+
const m = v.messages;
|
|
1669
|
+
if (!isPlainObject2(m) || !isPlainPrototype(m)) {
|
|
1670
|
+
return `PoolSpec.messages must be a plain object of ${MESSAGE_OPTION_KEYS.join(", ")} \u2014 its prototype Object.prototype or null \u2014 got ${describeNonPlain(m)}`;
|
|
1671
|
+
}
|
|
1672
|
+
for (const k of Object.keys(m)) if (!MESSAGE_OPTION_KEYS.includes(k)) return `PoolSpec.messages carries undeclared key ${q(k)}; it declares ${MESSAGE_OPTION_KEYS.join(", ")}`;
|
|
1673
|
+
if (ownKey(m, "flushIntervalMs") && !(typeof m.flushIntervalMs === "number" && Number.isFinite(m.flushIntervalMs) && m.flushIntervalMs > 0 && m.flushIntervalMs <= MAX_TIMER_MS)) {
|
|
1674
|
+
return `PoolSpec.messages.flushIntervalMs must be a finite number > 0 and at most ${MAX_TIMER_MS} (2^31-1, the longest delay a timer holds \u2014 the scheduler arms setTimeout with it, and a longer one fires at once), got ${shown(m.flushIntervalMs)}`;
|
|
1675
|
+
}
|
|
1676
|
+
if (ownKey(m, "maxBuffered") && !(typeof m.maxBuffered === "number" && Number.isInteger(m.maxBuffered) && m.maxBuffered >= 1)) {
|
|
1677
|
+
return `PoolSpec.messages.maxBuffered must be an integer >= 1, got ${shown(m.maxBuffered)}`;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1312
1680
|
return null;
|
|
1313
1681
|
}
|
|
1682
|
+
function isPlainPrototype(o) {
|
|
1683
|
+
const proto = Object.getPrototypeOf(o);
|
|
1684
|
+
return proto === Object.prototype || proto === null;
|
|
1685
|
+
}
|
|
1686
|
+
function ownKey(o, k) {
|
|
1687
|
+
return Object.prototype.propertyIsEnumerable.call(o, k);
|
|
1688
|
+
}
|
|
1689
|
+
function describeNonPlain(v) {
|
|
1690
|
+
if (Array.isArray(v)) return `an array ${shown(v)}`;
|
|
1691
|
+
if (typeof v === "object" && v !== null) {
|
|
1692
|
+
const proto = Object.getPrototypeOf(v);
|
|
1693
|
+
const name = proto?.constructor?.name;
|
|
1694
|
+
return `an object whose prototype is not Object.prototype${name ? ` (a ${name})` : ""}`;
|
|
1695
|
+
}
|
|
1696
|
+
return shown(v);
|
|
1697
|
+
}
|
|
1698
|
+
function shown(v) {
|
|
1699
|
+
if (typeof v === "number" || v === void 0) return String(v);
|
|
1700
|
+
try {
|
|
1701
|
+
return JSON.stringify(v) ?? String(v);
|
|
1702
|
+
} catch {
|
|
1703
|
+
return typeof v;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1314
1706
|
function groupsNamedBy(spec) {
|
|
1315
|
-
|
|
1316
|
-
|
|
1707
|
+
return [.../* @__PURE__ */ new Set([...spec.groups ?? [], ...residentKeysOf(spec)])];
|
|
1708
|
+
}
|
|
1709
|
+
function residentKeysOf(spec) {
|
|
1710
|
+
return spec.residentGroups === void 0 ? [] : Array.isArray(spec.residentGroups) ? [...spec.residentGroups] : Object.keys(spec.residentGroups);
|
|
1317
1711
|
}
|
|
1318
1712
|
function poolOptionsOf(spec, inline, workerUrl, languages) {
|
|
1319
1713
|
const { config, groups, ...rest } = spec;
|
|
@@ -1371,15 +1765,21 @@ function checkNodeTypeShape(name, t, builtIn = false) {
|
|
|
1371
1765
|
}
|
|
1372
1766
|
if (o.settings !== void 0 && !isPlainObject2(o.settings)) throw new DerivationRefused(at, `node type ${q(at)}.settings must be a plain object of defaults, got ${JSON.stringify(o.settings)}`);
|
|
1373
1767
|
}
|
|
1374
|
-
function derivePool(key, typeName, def, worker, config, groupKeys, refuse) {
|
|
1768
|
+
function derivePool(key, typeName, def, worker, config, groupKeys, refuse, pool) {
|
|
1375
1769
|
const who = key === typeName ? `node type ${q(typeName)}'s default instance` : `pool ${q(key)} (an instance of ${q(typeName)})`;
|
|
1376
1770
|
if (def.configSchema !== void 0) {
|
|
1377
1771
|
const fs = findings(def.configSchema, config ?? {});
|
|
1378
1772
|
if (fs.length) throw new refuse(key, `${who}: config fails the type's configSchema \u2014 ${describeFindings(fs)}`);
|
|
1379
1773
|
}
|
|
1774
|
+
let derivation = def;
|
|
1775
|
+
if (pool !== void 0) {
|
|
1776
|
+
const declaration = declarationOf(def);
|
|
1777
|
+
if (declaration === void 0) throw new Error(`derivePool: ${who} carries pool options and its type is a class \u2014 the registry's shape check should have refused it`);
|
|
1778
|
+
derivation = nodeTypeFromDeclaration(declaration, pool);
|
|
1779
|
+
}
|
|
1380
1780
|
let spec;
|
|
1381
1781
|
try {
|
|
1382
|
-
spec =
|
|
1782
|
+
spec = derivation.pool(config, key);
|
|
1383
1783
|
} catch (e) {
|
|
1384
1784
|
throw new DerivationRefused(key, `${who}: the derivation pool() THREW: ${e instanceof Error ? e.message : String(e)}`);
|
|
1385
1785
|
}
|
|
@@ -1398,7 +1798,13 @@ function derivePool(key, typeName, def, worker, config, groupKeys, refuse) {
|
|
|
1398
1798
|
if (resident > 0 && spec.idleTimeout === 0) {
|
|
1399
1799
|
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 on 0.5.2, P18 fact 6); give the pool a positive idleTimeout`);
|
|
1400
1800
|
}
|
|
1401
|
-
return { key, type: typeName, definition: def, worker, config, spec };
|
|
1801
|
+
return { key, type: typeName, definition: def, worker, config, spec, ...pool !== void 0 ? { pool: copyPool(pool) } : {} };
|
|
1802
|
+
}
|
|
1803
|
+
function copyPool(p) {
|
|
1804
|
+
const out = mergePool({}, p);
|
|
1805
|
+
if (isPlainObject2(out.messages)) out.messages = { ...out.messages };
|
|
1806
|
+
if (out.residentGroups !== void 0) out.residentGroups = copyResident(out.residentGroups);
|
|
1807
|
+
return out;
|
|
1402
1808
|
}
|
|
1403
1809
|
function poolTypeNames(consumer) {
|
|
1404
1810
|
const own3 = [...consumer].filter((n) => !Object.prototype.hasOwnProperty.call(BUILT_IN_NODE_TYPES, n)).sort();
|
|
@@ -1442,11 +1848,21 @@ function checkNodeTypesShape(rawNodeTypes, workers) {
|
|
|
1442
1848
|
function checkPoolsShape(rawPools, types) {
|
|
1443
1849
|
for (const [key, p] of poolsOf(rawPools)) {
|
|
1444
1850
|
if (!isPlainObject2(p)) throw new UndeclaredKey(key, `pool ${q(key)} is not an object`);
|
|
1445
|
-
noUndeclared("pool", key, p,
|
|
1851
|
+
noUndeclared("pool", key, p, POOL_INSTANCE_KEYS);
|
|
1446
1852
|
if (types.has(key)) throw new NameCollision(key, `pool ${q(key)} is named as node type ${q(key)} is \u2014 a type's name is its DEFAULT instance's key, so a named instance may not take it` + (isBuiltInNodeType(key) ? ` \u2014 and ${q(key)} is a BUILT-IN node type, so that name is the library's and no registry document declares it` : ""));
|
|
1447
1853
|
if (typeof p?.type !== "string" || !types.has(p.type)) {
|
|
1448
1854
|
throw new PoolTypeMissing(key, `pool ${q(key)} names type ${JSON.stringify(p?.type)}, which is not a node type` + poolTypeNames([...types.keys()]));
|
|
1449
1855
|
}
|
|
1856
|
+
const own3 = p.pool;
|
|
1857
|
+
if (own3 !== void 0) {
|
|
1858
|
+
if (declarationOf(types.get(p.type)) === void 0) {
|
|
1859
|
+
throw new UndeclaredKey(key, `pool ${q(key)} is an instance of CLASS type ${q(p.type)} and carries "pool" \u2014 pool options on an instance are a DOCUMENT type's; a class derives its pool from the instance's config and key, so the knob belongs in its configSchema (ADR 0099)`);
|
|
1860
|
+
}
|
|
1861
|
+
if (!isPlainObject2(own3)) throw new DerivationRefused(key, `pool ${q(key)}: its own pool options must be an object, got ${JSON.stringify(own3)}`);
|
|
1862
|
+
noUndeclared("pool instance pool", key, own3, DECLARED_POOL_KEYS);
|
|
1863
|
+
const problem = poolSpecProblem(own3);
|
|
1864
|
+
if (problem !== null) throw new DerivationRefused(key, `pool ${q(key)}.pool: ${problem}`);
|
|
1865
|
+
}
|
|
1450
1866
|
}
|
|
1451
1867
|
}
|
|
1452
1868
|
function checkGateTypes(x) {
|
|
@@ -1526,7 +1942,14 @@ function resolveRegistry(doc, code = {}) {
|
|
|
1526
1942
|
const builtIn = builtInEntry === void 0 ? void 0 : isDerivation(builtInEntry) ? builtInEntry : nodeTypeFromDeclaration(builtInEntry);
|
|
1527
1943
|
const def = (typeof inst.type === "string" ? nodeTypes.get(inst.type) : void 0) ?? builtIn;
|
|
1528
1944
|
if (def === void 0) throw new Error(`resolveRegistry: pool ${q(key)} reached derivation with no type \u2014 checkRegistryShape should have refused it`);
|
|
1529
|
-
pools.set(key, derivePool(key, inst.type, def, def.worker === void 0 ? void 0 : workers.get(def.worker), inst.config, groupKeys, ConfigRefused));
|
|
1945
|
+
pools.set(key, derivePool(key, inst.type, def, def.worker === void 0 ? void 0 : workers.get(def.worker), inst.config, groupKeys, ConfigRefused, inst.pool));
|
|
1946
|
+
}
|
|
1947
|
+
for (const p of pools.values()) {
|
|
1948
|
+
for (const g of residentKeysOf(p.spec)) {
|
|
1949
|
+
if (groups.get(g)?.type !== "KeyedRateLimitGroup") continue;
|
|
1950
|
+
const who = p.key === p.type ? `node type ${q(p.type)}'s default instance` : `pool ${q(p.key)} (an instance of ${q(p.type)})`;
|
|
1951
|
+
throw new DerivationRefused(p.key, `${who}: the derived pool holds group ${q(g)}, of the built-in type "KeyedRateLimitGroup", in residentGroups \u2014 the scheduler resolves no key for a resident group, so every worker holding it is counted in ONE "" bucket, in silence (Q619). Name ${q(g)} in groups, where each task is counted under its limitKey (FD-R6), or hold a ConcurrentLimitGroup or RateLimitGroup resident`);
|
|
1952
|
+
}
|
|
1530
1953
|
}
|
|
1531
1954
|
const predicateNames = doc.predicates ?? [];
|
|
1532
1955
|
if (!Array.isArray(predicateNames) || predicateNames.some((p) => typeof p !== "string")) throw new UndeclaredKey("predicates", `registry document's predicates must be an array of names, got ${JSON.stringify(predicateNames)}`);
|
|
@@ -1672,6 +2095,7 @@ function entryWalk(flow, payload, sampled, choose) {
|
|
|
1672
2095
|
out.push({ node: id, value });
|
|
1673
2096
|
return;
|
|
1674
2097
|
}
|
|
2098
|
+
if (n.kind === "ask") return;
|
|
1675
2099
|
seen.add(id);
|
|
1676
2100
|
if (n.kind === "fanout") for (const b of n.branches) walk(b, value);
|
|
1677
2101
|
if (n.kind === "gate") for (const e of choose ? choose(n, value) : n.edges ?? []) walk(e.to, value);
|
|
@@ -1680,7 +2104,10 @@ function entryWalk(flow, payload, sampled, choose) {
|
|
|
1680
2104
|
walk(n.branch, void 0);
|
|
1681
2105
|
return;
|
|
1682
2106
|
}
|
|
1683
|
-
const composed = n.needs === void 0 ? value : Object.fromEntries(Object.entries(n.needs).
|
|
2107
|
+
const composed = n.needs === void 0 ? value : Object.fromEntries(Object.entries(n.needs).flatMap(([k, r]) => {
|
|
2108
|
+
const form = referenceTarget(r)?.form;
|
|
2109
|
+
return form === "self" ? [[k, value]] : form === "payload" ? [[k, payload]] : [];
|
|
2110
|
+
}));
|
|
1684
2111
|
const arr = composed?.[n.over];
|
|
1685
2112
|
if (Array.isArray(arr)) for (const v of arr) walk(n.branch, v);
|
|
1686
2113
|
}
|
|
@@ -1697,6 +2124,13 @@ var Wiring = class {
|
|
|
1697
2124
|
workers;
|
|
1698
2125
|
types;
|
|
1699
2126
|
gateTypes;
|
|
2127
|
+
/**
|
|
2128
|
+
* **Each gate's RESOLVED route set, by gate id — or nothing** (ADR 0092 rule 3): `routesOf` over the gate's
|
|
2129
|
+
* type and settings, asked once here. This check does not run the validator first, so an unknown type and a
|
|
2130
|
+
* `routesOf` problem (it is total: a malformed set, a throwing `routesFor`) both leave the gate with NO set — no default
|
|
2131
|
+
* route and no guarantee, the arcs reported as they are, which is what an unknown type always got.
|
|
2132
|
+
*/
|
|
2133
|
+
gateRoutes = /* @__PURE__ */ new Map();
|
|
1700
2134
|
// Explicit fields, never parameter properties: Node's type stripping refuses those (`RT-R9`, `S4`),
|
|
1701
2135
|
// which is how P18's own sample failed to load (W67's declaration step).
|
|
1702
2136
|
constructor(flow, workers, types, gateTypes) {
|
|
@@ -1708,9 +2142,12 @@ var Wiring = class {
|
|
|
1708
2142
|
this.regions = allRegions(this.graph, flow);
|
|
1709
2143
|
const add = (to, from, kind, route) => this.inbound.set(to, [...this.inbound.get(to) ?? [], { from, kind, route }]);
|
|
1710
2144
|
for (const n of flow.nodes) {
|
|
1711
|
-
if (n.kind === "task" || n.kind === "join") for (const e of n.edges ?? []) add(e.to, n.id, n.kind === "
|
|
2145
|
+
if (n.kind === "task" || n.kind === "ask" || n.kind === "join") for (const e of n.edges ?? []) add(e.to, n.id, n.kind === "join" ? "join" : "edge");
|
|
1712
2146
|
if (n.kind === "gate") {
|
|
1713
|
-
const
|
|
2147
|
+
const type = this.gateTypes.get(n.type);
|
|
2148
|
+
const resolved = type === void 0 ? void 0 : routesOf(type, n.settings ?? {});
|
|
2149
|
+
if (resolved !== void 0 && !("problem" in resolved)) this.gateRoutes.set(n.id, resolved);
|
|
2150
|
+
const dflt = this.gateRoutes.get(n.id)?.defaultRoute;
|
|
1714
2151
|
for (const e of n.edges ?? []) add(e.to, n.id, "gate", e.route ?? dflt);
|
|
1715
2152
|
}
|
|
1716
2153
|
if (n.kind === "fanout") for (const b of n.branches) add(b, n.id, "branch");
|
|
@@ -1813,8 +2250,16 @@ var Wiring = class {
|
|
|
1813
2250
|
if (s === void 0) return { why: `"${nodeId}" (type "${n.type}", worker "${t.worker}") declares no ${kind}` };
|
|
1814
2251
|
return { schema: s };
|
|
1815
2252
|
}
|
|
1816
|
-
/** What a task PRODUCES, as an arrival on the arc it sends. */
|
|
2253
|
+
/** What a task — or an ask — PRODUCES, as an arrival on the arc it sends. */
|
|
1817
2254
|
produced(taskId) {
|
|
2255
|
+
const ask = this.byId.get(taskId);
|
|
2256
|
+
if (ask?.kind === "ask") {
|
|
2257
|
+
if (ask.answerSchema === void 0) return { unchecked: `"${taskId}" is an ask and declares no answerSchema, which is what would hold the person's answer` };
|
|
2258
|
+
const malformed = schemaProblem(ask.answerSchema);
|
|
2259
|
+
if (malformed !== null) return { unchecked: `"${taskId}" is an ask whose answerSchema is not in the built-in subset (FD-V48) \u2014 ${malformed}` };
|
|
2260
|
+
const a = sampleOf(ask.answerSchema);
|
|
2261
|
+
return { schema: ask.answerSchema, sample: { value: a.value, source: a.source } };
|
|
2262
|
+
}
|
|
1818
2263
|
const { schema, why } = this.schemaOf(taskId, "outputSchema");
|
|
1819
2264
|
if (schema === void 0) return { unchecked: why };
|
|
1820
2265
|
const s = sampleOf(schema);
|
|
@@ -1833,7 +2278,7 @@ var Wiring = class {
|
|
|
1833
2278
|
else if (kind === "gate") {
|
|
1834
2279
|
const gate = this.byId.get(from);
|
|
1835
2280
|
const key = this.keyOf(gate);
|
|
1836
|
-
const resolved = key !== "" && route !== void 0 && (this.
|
|
2281
|
+
const resolved = key !== "" && route !== void 0 && (this.gateRoutes.get(gate.id)?.keyResolvedRoutes ?? []).includes(route);
|
|
1837
2282
|
for (const a of this.arrivals(from)) {
|
|
1838
2283
|
if (!resolved || a.schema === void 0) {
|
|
1839
2284
|
raw.push({ ...a });
|
|
@@ -1849,7 +2294,7 @@ var Wiring = class {
|
|
|
1849
2294
|
} else raw.push(this.record(this.byId.get(from)));
|
|
1850
2295
|
}
|
|
1851
2296
|
const needs = node?.needs;
|
|
1852
|
-
const out = needs ? raw.map((a) => this.composed(a, needs)) : raw;
|
|
2297
|
+
const out = needs ? raw.map((a) => this.composed(a, needs, nodeId)) : raw;
|
|
1853
2298
|
this.memo.set(nodeId, out);
|
|
1854
2299
|
return out;
|
|
1855
2300
|
}
|
|
@@ -1874,7 +2319,7 @@ var Wiring = class {
|
|
|
1874
2319
|
/** The node(s) of one branch whose edge reaches the region's join — the value the branch carries in. */
|
|
1875
2320
|
arrivingAt(region, branch) {
|
|
1876
2321
|
const members = region.branchOf.get(branch) ?? /* @__PURE__ */ new Set();
|
|
1877
|
-
return [...members].map((id) => this.byId.get(id)).filter((n) => (n.kind === "task" || n.kind === "join" || n.kind === "gate") && (n.edges ?? []).some((e) => e.to === region.joinId));
|
|
2322
|
+
return [...members].map((id) => this.byId.get(id)).filter((n) => (n.kind === "task" || n.kind === "ask" || n.kind === "join" || n.kind === "gate") && (n.edges ?? []).some((e) => e.to === region.joinId));
|
|
1878
2323
|
}
|
|
1879
2324
|
/** `JP-R12`'s record for the region a join closes — exact for a fan-out, keyed by minted paths for a map. */
|
|
1880
2325
|
record(j) {
|
|
@@ -1922,8 +2367,24 @@ var Wiring = class {
|
|
|
1922
2367
|
sample: { value: sample, source, arity }
|
|
1923
2368
|
};
|
|
1924
2369
|
}
|
|
2370
|
+
/**
|
|
2371
|
+
* **What a `"$item"` key composes from: the enclosing map's ITEM arrival** — P38 (i), v0.45. The map is the innermost
|
|
2372
|
+
* one containing `nodeId` (`innermostMapOf`, P38 (c)), and its item is `item()` over the map's OWN arrival — the
|
|
2373
|
+
* `items` sub-schema of what it receives under `over`, with the per-element sample. **One arrival or none at all**: a
|
|
2374
|
+
* map entered from two producers has no single item shape, and an ENTRY map has no arrival (the run's payload, which no
|
|
2375
|
+
* schema declares) — both `unchecked`, never a guessed `fits`.
|
|
2376
|
+
*/
|
|
2377
|
+
itemArrival(nodeId) {
|
|
2378
|
+
const m = innermostMapOf(this.regions, nodeId);
|
|
2379
|
+
if (m === void 0) return { unchecked: `"${nodeId}" is inside no map instance (FD-V49)` };
|
|
2380
|
+
const at = this.arrivals(m.fanoutId);
|
|
2381
|
+
if (at.length !== 1) {
|
|
2382
|
+
return { unchecked: `map "${m.fanoutId}" has ${at.length === 0 ? "no arrival \u2014 the run's payload, which no schema declares" : `${at.length} arrivals`}, so no single item shape` };
|
|
2383
|
+
}
|
|
2384
|
+
return this.item(at[0], this.byId.get(m.fanoutId));
|
|
2385
|
+
}
|
|
1925
2386
|
/** `FD-C1`'s table over one arrival: the object `needs` describes, schema and sample alike. */
|
|
1926
|
-
composed(a, needs) {
|
|
2387
|
+
composed(a, needs, nodeId) {
|
|
1927
2388
|
const base = { shape: "needs", producer: a.producer, ...a.through ? { through: a.through } : {} };
|
|
1928
2389
|
if (a.refused !== void 0) return { ...base, refused: a.refused };
|
|
1929
2390
|
const props = {};
|
|
@@ -1932,11 +2393,27 @@ var Wiring = class {
|
|
|
1932
2393
|
let source = "example";
|
|
1933
2394
|
const arity = a.sample?.arity;
|
|
1934
2395
|
const dot = [];
|
|
2396
|
+
const itemKeys = [];
|
|
2397
|
+
let itemElements;
|
|
1935
2398
|
for (const [key, ref] of Object.entries(needs)) {
|
|
1936
2399
|
const t = referenceTarget(ref);
|
|
1937
2400
|
if (t === null) return { ...base, unchecked: `needs.${key} is not a reference` };
|
|
1938
2401
|
let schema2, value, src = "example";
|
|
1939
|
-
if (t.
|
|
2402
|
+
if (t.form === "payload") return { ...base, unchecked: `needs.${key} is "$payload" \u2014 the run's payload, which no schema declares` };
|
|
2403
|
+
if (t.form === "item") {
|
|
2404
|
+
const it = this.itemArrival(nodeId);
|
|
2405
|
+
if (it.schema === void 0) return { ...base, unchecked: `needs.${key} is "$item", and ${it.unchecked ?? it.refused}` };
|
|
2406
|
+
schema2 = it.schema;
|
|
2407
|
+
src = it.sample?.source ?? "witness";
|
|
2408
|
+
if (it.perItem && it.sample !== void 0) {
|
|
2409
|
+
itemElements = it.sample.value;
|
|
2410
|
+
itemKeys.push(key);
|
|
2411
|
+
} else {
|
|
2412
|
+
const w = sampleOf(it.schema);
|
|
2413
|
+
value = w?.value;
|
|
2414
|
+
src = w?.source ?? "witness";
|
|
2415
|
+
}
|
|
2416
|
+
} else if (t.form === "self") {
|
|
1940
2417
|
if (a.schema === void 0) return { ...base, unchecked: a.unchecked ?? a.refused ?? `needs.${key} is "." and the edge's value has no schema` };
|
|
1941
2418
|
schema2 = a.schema;
|
|
1942
2419
|
value = a.sample?.value;
|
|
@@ -1959,7 +2436,16 @@ var Wiring = class {
|
|
|
1959
2436
|
const elements = a.sample.value;
|
|
1960
2437
|
const value = elements.map((el) => {
|
|
1961
2438
|
const s = { ...sample };
|
|
1962
|
-
for (const k of dot) s[k] = el;
|
|
2439
|
+
for (const k of [...dot, ...itemKeys]) s[k] = el;
|
|
2440
|
+
return s;
|
|
2441
|
+
});
|
|
2442
|
+
return { ...base, perItem: true, schema, sample: { value, source, arity: elements.length } };
|
|
2443
|
+
}
|
|
2444
|
+
if (itemKeys.length > 0 && itemElements !== void 0) {
|
|
2445
|
+
const elements = itemElements;
|
|
2446
|
+
const value = elements.map((el) => {
|
|
2447
|
+
const s = { ...sample };
|
|
2448
|
+
for (const k of itemKeys) s[k] = el;
|
|
1963
2449
|
return s;
|
|
1964
2450
|
});
|
|
1965
2451
|
return { ...base, perItem: true, schema, sample: { value, source, arity: elements.length } };
|
|
@@ -1976,8 +2462,7 @@ async function checkWiring(registry, flow, options = {}) {
|
|
|
1976
2462
|
for (const node of flow.nodes) {
|
|
1977
2463
|
if (node.kind !== "task") continue;
|
|
1978
2464
|
const type = types.get(node.type);
|
|
1979
|
-
const consumer
|
|
1980
|
-
const consumerWhy = !type ? `type "${node.type}" of "${node.id}" is not in the registry` : type.worker === void 0 ? `"${node.id}" is of type "${node.type}", which only TRANSFORMS \u2014 it names no worker, so it declares no inputSchema` : !workers.get(type.worker) ? `worker "${type.worker}" of type "${node.type}" is not in the registry` : consumer === void 0 ? `"${node.id}" (type "${node.type}", worker "${type.worker}") declares no inputSchema` : void 0;
|
|
2465
|
+
const { consumer, consumerWhy } = consumerOf(node, type, workers);
|
|
1981
2466
|
for (const a of w.arrivals(node.id)) {
|
|
1982
2467
|
const v = { consumer: node.id, shape: a.shape, producer: a.producer, ...a.through ? { through: a.through } : {}, kind: "unchecked", why: "" };
|
|
1983
2468
|
if (a.refused !== void 0) {
|
|
@@ -2009,57 +2494,8 @@ async function checkWiring(registry, flow, options = {}) {
|
|
|
2009
2494
|
continue;
|
|
2010
2495
|
}
|
|
2011
2496
|
const sample = { source: a.sample.source, ...a.sample.arity !== void 0 ? { arity: a.sample.arity } : {}, value: a.sample.value };
|
|
2012
|
-
const adapter = languages.get(node.transform.lang);
|
|
2013
|
-
if (adapter === void 0) {
|
|
2014
|
-
v.why = `language "${node.transform.lang}" is not in the registry`;
|
|
2015
|
-
v.sample = sample;
|
|
2016
|
-
verdicts.push(v);
|
|
2017
|
-
continue;
|
|
2018
|
-
}
|
|
2019
2497
|
const inputs = a.perItem ? a.sample.value : [a.sample.value];
|
|
2020
|
-
|
|
2021
|
-
v.why = `the sample holds no item to run "${node.id}"'s transform on (arity 0)`;
|
|
2022
|
-
v.sample = sample;
|
|
2023
|
-
verdicts.push(v);
|
|
2024
|
-
continue;
|
|
2025
|
-
}
|
|
2026
|
-
const settings = effectiveSettings(node, type);
|
|
2027
|
-
let results;
|
|
2028
|
-
try {
|
|
2029
|
-
let compile;
|
|
2030
|
-
try {
|
|
2031
|
-
const mod = await import(adapter.worker);
|
|
2032
|
-
if (typeof mod.default === "function") compile = mod.default;
|
|
2033
|
-
} catch (e) {
|
|
2034
|
-
if (options.workerHalf?.[node.transform.lang] === void 0) throw e;
|
|
2035
|
-
}
|
|
2036
|
-
compile ??= options.workerHalf?.[node.transform.lang];
|
|
2037
|
-
if (typeof compile !== "function") throw new Error(`the worker half of language "${node.transform.lang}" has no default export, and none was handed in`);
|
|
2038
|
-
const c = compile(node.transform.expr);
|
|
2039
|
-
results = [];
|
|
2040
|
-
for (const input of inputs) results.push(await c.evaluate(input, settings));
|
|
2041
|
-
} catch (e) {
|
|
2042
|
-
v.kind = "transform-fails";
|
|
2043
|
-
v.why = `the transform threw on the ${sample.source}: ${e instanceof Error ? e.message : String(e)}`;
|
|
2044
|
-
v.sample = { ...sample, result: e instanceof Error ? e.message : String(e) };
|
|
2045
|
-
verdicts.push(v);
|
|
2046
|
-
continue;
|
|
2047
|
-
}
|
|
2048
|
-
sample.result = a.perItem ? results : results[0];
|
|
2049
|
-
v.sample = sample;
|
|
2050
|
-
if (consumer === void 0) {
|
|
2051
|
-
v.why = consumerWhy + " \u2014 the transform ran and nothing holds its result";
|
|
2052
|
-
verdicts.push(v);
|
|
2053
|
-
continue;
|
|
2054
|
-
}
|
|
2055
|
-
const bad = results.map((r, i) => [i, findings(consumer, r)]).find(([, f]) => f.length > 0);
|
|
2056
|
-
if (bad) {
|
|
2057
|
-
v.kind = "transform-fails";
|
|
2058
|
-
v.why = `the transform's result on the ${sample.source}${a.perItem ? ` (item ${bad[0]})` : ""} fails the consumer's inputSchema \u2014 ${describeFindings(bad[1])}`;
|
|
2059
|
-
} else {
|
|
2060
|
-
v.kind = "transform-passes";
|
|
2061
|
-
v.why = `the sample passed \u2014 the ${sample.source}${sample.arity !== void 0 ? ` at arity ${sample.arity}` : ""}, ${inputs.length} evaluation${inputs.length === 1 ? "" : "s"}, every result accepted by the consumer's inputSchema`;
|
|
2062
|
-
}
|
|
2498
|
+
await judgeTransform(v, node, node.transform, type, consumer, consumerWhy, languages, sample, inputs, a.perItem === true, options);
|
|
2063
2499
|
verdicts.push(v);
|
|
2064
2500
|
}
|
|
2065
2501
|
}
|
|
@@ -2067,13 +2503,84 @@ async function checkWiring(registry, flow, options = {}) {
|
|
|
2067
2503
|
for (const v of verdicts) counts[v.kind]++;
|
|
2068
2504
|
return { flowId: flow.flowId, verdicts, counts, entry: entryTasks(flow) };
|
|
2069
2505
|
}
|
|
2506
|
+
function consumerOf(node, type, workers) {
|
|
2507
|
+
const consumer = type?.worker === void 0 ? void 0 : workers.get(type.worker)?.inputSchema;
|
|
2508
|
+
const consumerWhy = !type ? `type "${node.type}" of "${node.id}" is not in the registry` : type.worker === void 0 ? `"${node.id}" is of type "${node.type}", which only TRANSFORMS \u2014 it names no worker, so it declares no inputSchema` : !workers.get(type.worker) ? `worker "${type.worker}" of type "${node.type}" is not in the registry` : consumer === void 0 ? `"${node.id}" (type "${node.type}", worker "${type.worker}") declares no inputSchema` : void 0;
|
|
2509
|
+
return { consumer, consumerWhy };
|
|
2510
|
+
}
|
|
2511
|
+
var sourceWord = (s) => s === "given" ? "given input" : s;
|
|
2512
|
+
async function judgeTransform(v, node, transform, type, consumer, consumerWhy, languages, sample, inputs, perItem, options) {
|
|
2513
|
+
const adapter = languages.get(transform.lang);
|
|
2514
|
+
if (adapter === void 0) {
|
|
2515
|
+
v.why = `language "${transform.lang}" is not in the registry`;
|
|
2516
|
+
v.sample = sample;
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
if (inputs.length === 0) {
|
|
2520
|
+
v.why = `the sample holds no item to run "${node.id}"'s transform on (arity 0)`;
|
|
2521
|
+
v.sample = sample;
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
const settings = effectiveSettings(node, type);
|
|
2525
|
+
let results;
|
|
2526
|
+
try {
|
|
2527
|
+
let compile;
|
|
2528
|
+
try {
|
|
2529
|
+
const mod = await import(adapter.worker);
|
|
2530
|
+
if (typeof mod.default === "function") compile = mod.default;
|
|
2531
|
+
} catch (e) {
|
|
2532
|
+
if (options.workerHalf?.[transform.lang] === void 0) throw e;
|
|
2533
|
+
}
|
|
2534
|
+
compile ??= options.workerHalf?.[transform.lang];
|
|
2535
|
+
if (typeof compile !== "function") throw new Error(`the worker half of language "${transform.lang}" has no default export, and none was handed in`);
|
|
2536
|
+
const c = compile(transform.expr);
|
|
2537
|
+
results = [];
|
|
2538
|
+
for (const input of inputs) results.push(await c.evaluate(input, settings));
|
|
2539
|
+
} catch (e) {
|
|
2540
|
+
v.kind = "transform-fails";
|
|
2541
|
+
v.why = `the transform threw on the ${sourceWord(sample.source)}: ${e instanceof Error ? e.message : String(e)}`;
|
|
2542
|
+
v.sample = { ...sample, result: e instanceof Error ? e.message : String(e) };
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
sample.result = perItem ? results : results[0];
|
|
2546
|
+
v.sample = sample;
|
|
2547
|
+
if (consumer === void 0) {
|
|
2548
|
+
v.why = consumerWhy + " \u2014 the transform ran and nothing holds its result";
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
const bad = results.map((r, i) => [i, findings(consumer, r)]).find(([, f]) => f.length > 0);
|
|
2552
|
+
if (bad) {
|
|
2553
|
+
v.kind = "transform-fails";
|
|
2554
|
+
v.why = `the transform's result on the ${sourceWord(sample.source)}${perItem ? ` (item ${bad[0]})` : ""} fails the consumer's inputSchema \u2014 ${describeFindings(bad[1])}`;
|
|
2555
|
+
} else {
|
|
2556
|
+
v.kind = "transform-passes";
|
|
2557
|
+
v.why = `the sample passed \u2014 the ${sourceWord(sample.source)}${sample.arity !== void 0 ? ` at arity ${sample.arity}` : ""}, ${inputs.length} evaluation${inputs.length === 1 ? "" : "s"}, every result accepted by the consumer's inputSchema`;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
async function givenVerdict(registry, flow, nodeId, input, options = {}) {
|
|
2561
|
+
if (input === void 0) return (await checkWiring(registry, flow, options)).verdicts.filter((v2) => v2.consumer === nodeId);
|
|
2562
|
+
const node = flow.nodes.find((n) => n.id === nodeId);
|
|
2563
|
+
if (node === void 0 || node.kind !== "task") return [];
|
|
2564
|
+
const workers = workersOf(registry.workers);
|
|
2565
|
+
const type = nodeTypesOf(registry.nodeTypes).get(node.type);
|
|
2566
|
+
const { consumer, consumerWhy } = consumerOf(node, type, workers);
|
|
2567
|
+
const v = { consumer: node.id, shape: "given", kind: "unchecked", why: "" };
|
|
2568
|
+
const sample = { source: "given", value: input };
|
|
2569
|
+
if (node.transform === void 0) {
|
|
2570
|
+
v.why = `"${node.id}" has no transform to run the given input through, so nothing is evaluated \u2014 holding one value to the inputSchema is \`findings\`'s question${consumerWhy !== void 0 ? `; and ${consumerWhy}` : ""}`;
|
|
2571
|
+
v.sample = sample;
|
|
2572
|
+
return [v];
|
|
2573
|
+
}
|
|
2574
|
+
await judgeTransform(v, node, node.transform, type, consumer, consumerWhy, languagesOf(registry.languages), sample, [input], false, options);
|
|
2575
|
+
return [v];
|
|
2576
|
+
}
|
|
2070
2577
|
function describeVerdict(v) {
|
|
2071
|
-
const road = v.shape === "edge" ? `${v.producer} -> ${v.consumer}` : v.shape === "branch" ? `${v.producer} -> (${v.through}) -> ${v.consumer}` : v.shape === "item" ? `${v.producer} -> (${v.through}, one item) -> ${v.consumer}` : v.shape === "record" ? `${v.producer} (the record) -> ${v.consumer}` : `${v.producer} -> needs -> ${v.consumer}`;
|
|
2072
|
-
const s = v.sample ? ` [on the ${v.sample.source}${v.sample.arity !== void 0 ? `, arity ${v.sample.arity}` : ""}]` : "";
|
|
2578
|
+
const road = v.shape === "given" ? `(given) -> ${v.consumer}` : v.shape === "edge" ? `${v.producer} -> ${v.consumer}` : v.shape === "branch" ? `${v.producer} -> (${v.through}) -> ${v.consumer}` : v.shape === "item" ? `${v.producer} -> (${v.through}, one item) -> ${v.consumer}` : v.shape === "record" ? `${v.producer} (the record) -> ${v.consumer}` : `${v.producer} -> needs -> ${v.consumer}`;
|
|
2579
|
+
const s = v.sample ? ` [on the ${sourceWord(v.sample.source)}${v.sample.arity !== void 0 ? `, arity ${v.sample.arity}` : ""}]` : "";
|
|
2073
2580
|
return `${v.kind.padEnd(16)} ${road}: ${v.why}${s}`;
|
|
2074
2581
|
}
|
|
2075
2582
|
var VERDICT_KINDS = ["fits", "no", "transform-passes", "transform-fails", "unchecked"];
|
|
2076
|
-
var WIRING_SHAPES = ["edge", "branch", "item", "record", "needs"];
|
|
2583
|
+
var WIRING_SHAPES = ["edge", "branch", "item", "record", "needs", "given"];
|
|
2077
2584
|
|
|
2078
2585
|
// src/driver/ids.ts
|
|
2079
2586
|
var SEGMENT = /^[A-Za-z0-9_-]{1,64}$/;
|
|
@@ -2132,9 +2639,10 @@ function lastRecordFor(log, taskId) {
|
|
|
2132
2639
|
}
|
|
2133
2640
|
return void 0;
|
|
2134
2641
|
}
|
|
2135
|
-
function isSuspended(log, taskId) {
|
|
2642
|
+
function isSuspended(log, taskId, asked = false) {
|
|
2136
2643
|
const last = lastRecordFor(log, taskId);
|
|
2137
|
-
|
|
2644
|
+
if (last === void 0) return asked;
|
|
2645
|
+
return last.kind === "outcome" && last.terminalState === "user_action";
|
|
2138
2646
|
}
|
|
2139
2647
|
function effectiveOutcome(log, taskId) {
|
|
2140
2648
|
let settlement = null;
|
|
@@ -2144,7 +2652,22 @@ function effectiveOutcome(log, taskId) {
|
|
|
2144
2652
|
if (r.kind === "outcome") settlement = r;
|
|
2145
2653
|
else resolution = r;
|
|
2146
2654
|
}
|
|
2147
|
-
if (!settlement)
|
|
2655
|
+
if (!settlement) {
|
|
2656
|
+
if (!resolution) return null;
|
|
2657
|
+
return {
|
|
2658
|
+
kind: "outcome",
|
|
2659
|
+
taskId,
|
|
2660
|
+
terminalState: resolution.resolvedAs,
|
|
2661
|
+
result: resolution.result,
|
|
2662
|
+
error: resolution.error,
|
|
2663
|
+
attempts: 0,
|
|
2664
|
+
schedulerTimestamp: null,
|
|
2665
|
+
observedAt: resolution.observedAt,
|
|
2666
|
+
startTime: null,
|
|
2667
|
+
endTime: null,
|
|
2668
|
+
executionTime: null
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2148
2671
|
if (!resolution) return settlement.terminalState === "user_action" ? null : settlement;
|
|
2149
2672
|
return {
|
|
2150
2673
|
...settlement,
|
|
@@ -2165,16 +2688,15 @@ var UncloneableResult = class extends Error {
|
|
|
2165
2688
|
this.name = "UncloneableResult";
|
|
2166
2689
|
}
|
|
2167
2690
|
};
|
|
2168
|
-
var TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "user_action"]);
|
|
2691
|
+
var TERMINAL = /* @__PURE__ */ new Set(["done", "failed", "user_action", "cancelled"]);
|
|
2692
|
+
var SETTLEMENT = /* @__PURE__ */ new Set(["task.completed", "task.failed", "task.user_action", "task.cancelled"]);
|
|
2169
2693
|
var num = (x) => typeof x === "number" ? x : null;
|
|
2170
2694
|
function normalizeEvent(eventName, detail, now) {
|
|
2171
|
-
if (eventName === "task.running" || eventName === "task.progress") return null;
|
|
2695
|
+
if (eventName === "task.running" || eventName === "task.progress" || eventName === "task.cancelling") return null;
|
|
2172
2696
|
const state = detail?.state;
|
|
2173
2697
|
if (typeof state !== "string" || !TERMINAL.has(state)) return null;
|
|
2174
2698
|
if (eventName === "task.failed" && state === "user_action") return null;
|
|
2175
|
-
if (
|
|
2176
|
-
return null;
|
|
2177
|
-
}
|
|
2699
|
+
if (!SETTLEMENT.has(eventName)) return null;
|
|
2178
2700
|
const terminalState = state;
|
|
2179
2701
|
let result = null;
|
|
2180
2702
|
if (terminalState === "done") {
|
|
@@ -2205,55 +2727,18 @@ function normalizeEvent(eventName, detail, now) {
|
|
|
2205
2727
|
// src/driver/routing.ts
|
|
2206
2728
|
var NoMatchingEdge = class extends Error {
|
|
2207
2729
|
};
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
if (
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
else if (typeof v === "string") v = v.length;
|
|
2215
|
-
else return false;
|
|
2216
|
-
}
|
|
2217
|
-
const w = p.value;
|
|
2218
|
-
switch (p.op) {
|
|
2219
|
-
// **Type-STRICT, and it is a departure measured rather than assumed.** Under JS comparison
|
|
2220
|
-
// `{confidence: "0.4"} < 0.5` is true and every `typeof`-guarded predicate in this tree is false;
|
|
2221
|
-
// measured over 1800 contexts, a type-strict form reproduces those three EXACTLY and differs from
|
|
2222
|
-
// `resume.test.ts`'s unguarded `isHigh` on `{score: "9"}`, where the JS body coerces. That is
|
|
2223
|
-
// `FD-V30`'s own argument — *a string `maxRetries` retries by coercion* — one rule over.
|
|
2224
|
-
case "eq":
|
|
2225
|
-
return v === w;
|
|
2226
|
-
case "ne":
|
|
2227
|
-
return v !== w;
|
|
2228
|
-
case "lt":
|
|
2229
|
-
return typeof v === "number" && typeof w === "number" && v < w;
|
|
2230
|
-
case "lte":
|
|
2231
|
-
return typeof v === "number" && typeof w === "number" && v <= w;
|
|
2232
|
-
case "gt":
|
|
2233
|
-
return typeof v === "number" && typeof w === "number" && v > w;
|
|
2234
|
-
case "gte":
|
|
2235
|
-
return typeof v === "number" && typeof w === "number" && v >= w;
|
|
2236
|
-
case "startsWith":
|
|
2237
|
-
return typeof v === "string" && typeof w === "string" && v.startsWith(w);
|
|
2238
|
-
default: {
|
|
2239
|
-
const never = p.op;
|
|
2240
|
-
throw new NoMatchingEdge(`operator "${String(never)}" is not in the subset; FD-V44 should have caught this at instantiation`);
|
|
2241
|
-
}
|
|
2730
|
+
var UNKNOWN_OP2 = (message) => new NoMatchingEdge(message);
|
|
2731
|
+
function evaluateWhen(when, ctx, predicates) {
|
|
2732
|
+
if ("predicate" in when && typeof when.predicate === "string") {
|
|
2733
|
+
const p = predicates.get(when.predicate);
|
|
2734
|
+
if (!p) throw new NoMatchingEdge(`predicate "${when.predicate}" is not registered; FD-V4 should have caught this at instantiation`);
|
|
2735
|
+
return p(ctx);
|
|
2242
2736
|
}
|
|
2243
|
-
|
|
2244
|
-
function evaluateWhen(when, ctx, predicates) {
|
|
2245
|
-
if ("predicate" in when && typeof when.predicate === "string") {
|
|
2246
|
-
const p = predicates.get(when.predicate);
|
|
2247
|
-
if (!p) throw new NoMatchingEdge(`predicate "${when.predicate}" is not registered; FD-V4 should have caught this at instantiation`);
|
|
2248
|
-
return p(ctx);
|
|
2249
|
-
}
|
|
2250
|
-
if ("all" in when && Array.isArray(when.all)) return when.all.every((m) => evalLeaf(ctx, m));
|
|
2251
|
-
if ("any" in when && Array.isArray(when.any)) return when.any.some((m) => evalLeaf(ctx, m));
|
|
2252
|
-
return evalLeaf(ctx, when);
|
|
2737
|
+
return evaluateDeclared(when, ctx, UNKNOWN_OP2);
|
|
2253
2738
|
}
|
|
2254
2739
|
var onOf = (e) => Array.isArray(e.on) && e.on.length ? e.on : ["done"];
|
|
2255
2740
|
function eligibleEdges(node, terminalState) {
|
|
2256
|
-
if (node.kind !== "task" && node.kind !== "join") return [];
|
|
2741
|
+
if (node.kind !== "task" && node.kind !== "join" && node.kind !== "ask") return [];
|
|
2257
2742
|
const edges = Array.isArray(node.edges) ? node.edges : [];
|
|
2258
2743
|
return edges.filter((e) => onOf(e).includes(terminalState));
|
|
2259
2744
|
}
|
|
@@ -2273,9 +2758,15 @@ function selectEdge(node, runId, terminalState, contextFor, predicates) {
|
|
|
2273
2758
|
);
|
|
2274
2759
|
}
|
|
2275
2760
|
|
|
2761
|
+
// src/driver/events.ts
|
|
2762
|
+
var TERMINAL_EVENTS = ["branch.arrived", "branch.exited", "branch.lost"];
|
|
2763
|
+
function isTerminalEvent(e) {
|
|
2764
|
+
return TERMINAL_EVENTS.includes(e.type);
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2276
2767
|
// src/driver/driver.ts
|
|
2277
2768
|
import { FyflowTask } from "fyflow-scheduler";
|
|
2278
|
-
var InvalidDefinition = class extends Error {
|
|
2769
|
+
var InvalidDefinition = class _InvalidDefinition extends Error {
|
|
2279
2770
|
/**
|
|
2280
2771
|
* The rule ids, on the error rather than only in its text, so a caller can act
|
|
2281
2772
|
* on them.
|
|
@@ -2291,6 +2782,25 @@ var InvalidDefinition = class extends Error {
|
|
|
2291
2782
|
super(message);
|
|
2292
2783
|
this.rules = rules;
|
|
2293
2784
|
}
|
|
2785
|
+
/**
|
|
2786
|
+
* **A definition's findings as the error, built in ONE place** — P34 fact 12, W169. `new Driver` throws it for what
|
|
2787
|
+
* `validateDefinition` found, and `createRuntime`'s `start` and `resume` throw it for what `runtime.check` found (ADR 0096
|
|
2788
|
+
* rule 5), so the two roads cannot drift apart: `rules` is `violatedRules(findings)` — sorted, each once — and the
|
|
2789
|
+
* message is `definition "<flowId>" is invalid: ` then every finding as `<rule> at <at>: <detail>`, `at` reading
|
|
2790
|
+
* `<document>` where a finding has none, joined by `; `. *(Until W169 the two roads each built it inline, the same text
|
|
2791
|
+
* twice.)* `at` is printed as given — a node id, an edge site (`id#edgeN`), or `<document>` for `null`.
|
|
2792
|
+
*
|
|
2793
|
+
* **Its precondition is at least one finding**, and an EMPTY list is refused with a `RangeError` (the conformity reader's
|
|
2794
|
+
* C3): an `InvalidDefinition` naming no defect would read *"is invalid: "* and name nothing. Both callers throw only
|
|
2795
|
+
* when they found something.
|
|
2796
|
+
*/
|
|
2797
|
+
static fromFindings(flowId, findings2) {
|
|
2798
|
+
if (findings2.length === 0) throw new RangeError(`InvalidDefinition.fromFindings: definition "${flowId}" has no finding \u2014 an invalid definition names at least one`);
|
|
2799
|
+
return new _InvalidDefinition(
|
|
2800
|
+
violatedRules(findings2),
|
|
2801
|
+
`definition "${flowId}" is invalid: ` + findings2.map((f) => `${f.rule} at ${f.at ?? "<document>"}: ${f.detail}`).join("; ")
|
|
2802
|
+
);
|
|
2803
|
+
}
|
|
2294
2804
|
};
|
|
2295
2805
|
var DuplicateTaskId = class extends Error {
|
|
2296
2806
|
constructor(message, options) {
|
|
@@ -2319,9 +2829,9 @@ var UndeclaredRoute = class extends Error {
|
|
|
2319
2829
|
var BoundaryRefused = class extends Error {
|
|
2320
2830
|
/** Which boundary refused: the run's payload at `start`, or a person's value at `resolve`. */
|
|
2321
2831
|
boundary;
|
|
2322
|
-
/** The task node whose worker's schema refused the value. */
|
|
2832
|
+
/** The task node whose worker's schema refused the value — or, at `resolve`, the ASK whose `answerSchema` did (W180). */
|
|
2323
2833
|
nodeId;
|
|
2324
|
-
/** The suspended task
|
|
2834
|
+
/** The suspended task — or parked ask — at `resolve`; `start` has dispatched nothing yet. */
|
|
2325
2835
|
taskId;
|
|
2326
2836
|
/** Every way the value failed the schema — `findings`' own list, one line each in `message`. */
|
|
2327
2837
|
findings;
|
|
@@ -2346,10 +2856,20 @@ var NotStarted = class extends Error {
|
|
|
2346
2856
|
this.name = "NotStarted";
|
|
2347
2857
|
}
|
|
2348
2858
|
};
|
|
2859
|
+
var RunControlRefused = class extends Error {
|
|
2860
|
+
control;
|
|
2861
|
+
reason;
|
|
2862
|
+
constructor(control, reason, message) {
|
|
2863
|
+
super(message);
|
|
2864
|
+
this.name = "RunControlRefused";
|
|
2865
|
+
this.control = control;
|
|
2866
|
+
this.reason = reason;
|
|
2867
|
+
}
|
|
2868
|
+
};
|
|
2349
2869
|
var CHECKPOINT_FORMAT = 1;
|
|
2350
2870
|
var ResumeRefused = class extends Error {
|
|
2351
2871
|
reason;
|
|
2352
|
-
/** The position `
|
|
2872
|
+
/** The position `halted`, `cancelled` and `diverged` name; `null` for the three a header decides. */
|
|
2353
2873
|
index;
|
|
2354
2874
|
constructor(reason, index, message) {
|
|
2355
2875
|
super(message);
|
|
@@ -2396,12 +2916,29 @@ function resumeRefusal(definition, checkpoint, runId) {
|
|
|
2396
2916
|
`ResumeRefused (flow): the checkpoint is of flow ${JSON.stringify(h.flowId)} version ${JSON.stringify(h.version)}, and this driver runs "${definition.flowId}" version ${definition.version}. There is no migration (P23 decision 7): a checkpoint resumes against the definition that produced it, and a changed flow is a new version.`
|
|
2397
2917
|
);
|
|
2398
2918
|
}
|
|
2399
|
-
const at = cp.events.findIndex((e) =>
|
|
2919
|
+
const at = cp.events.findIndex((e) => {
|
|
2920
|
+
const k = e?.kind;
|
|
2921
|
+
return k === "halted" || k === "stopped";
|
|
2922
|
+
});
|
|
2400
2923
|
if (at >= 0) {
|
|
2924
|
+
const e = cp.events[at];
|
|
2401
2925
|
return new ResumeRefused(
|
|
2402
|
-
"
|
|
2926
|
+
"halted",
|
|
2403
2927
|
at,
|
|
2404
|
-
`ResumeRefused (
|
|
2928
|
+
`ResumeRefused (halted): the stored run was HALTED by a refusal \u2014 its element at position ${at} is \`${String(e.kind)}\`${e.kind === "stopped" ? " (the name 0.7.0 wrote for `halted`, ADR 0102)" : ""} (${JSON.stringify(e.reason)}). A halted run is not resumable (P23 decision 10): the refusal that halted it would halt it again, and the one class whose cause never reached the log \u2014 a result OR-R6 refused \u2014 cannot be regenerated at all.`
|
|
2929
|
+
);
|
|
2930
|
+
}
|
|
2931
|
+
const c = cp.events.findIndex((e) => {
|
|
2932
|
+
const x = e;
|
|
2933
|
+
return x?.kind === "cancelRequested" || x?.kind === "outcome" && x.record?.terminalState === "cancelled";
|
|
2934
|
+
});
|
|
2935
|
+
if (c >= 0) {
|
|
2936
|
+
const e = cp.events[c];
|
|
2937
|
+
const what = e?.kind === "cancelRequested" ? `the request of ${JSON.stringify(e.actor)} to cancel it (\`cancelRequested\`)` : `the settlement of ${JSON.stringify(e?.record?.taskId)} as \`cancelled\``;
|
|
2938
|
+
return new ResumeRefused(
|
|
2939
|
+
"cancelled",
|
|
2940
|
+
c,
|
|
2941
|
+
`ResumeRefused (cancelled): the stored run was CANCELLED \u2014 its element at position ${c} is ${what}. A cancelled run is not resumable (ADR 0094 rule 5, ADR 0104): a cancel is its owner\`s act, and a resume would hand the scheduler again what the owner stopped.`
|
|
2405
2942
|
);
|
|
2406
2943
|
}
|
|
2407
2944
|
return null;
|
|
@@ -2409,14 +2946,42 @@ function resumeRefusal(definition, checkpoint, runId) {
|
|
|
2409
2946
|
function sameData(a, b) {
|
|
2410
2947
|
if (Object.is(a, b)) return true;
|
|
2411
2948
|
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
2412
|
-
|
|
2949
|
+
const tag = Object.prototype.toString.call(a);
|
|
2950
|
+
if (tag !== Object.prototype.toString.call(b)) return false;
|
|
2951
|
+
if (Array.isArray(a)) {
|
|
2952
|
+
const y = b;
|
|
2953
|
+
return a.length === y.length && a.every((v, i) => sameData(v, y[i]));
|
|
2954
|
+
}
|
|
2955
|
+
if (a instanceof Map || a instanceof Set) {
|
|
2956
|
+
const x = [...a.entries()], y = [...b.entries()];
|
|
2957
|
+
return x.length === y.length && x.every((e, i) => sameData(e[0], y[i][0]) && sameData(e[1], y[i][1]));
|
|
2958
|
+
}
|
|
2959
|
+
if (a instanceof Date) return Object.is(a.getTime(), b.getTime());
|
|
2960
|
+
if (a instanceof RegExp) return String(a) === String(b);
|
|
2961
|
+
if (a instanceof Error) return a.name === b.name && a.message === b.message;
|
|
2962
|
+
if (ArrayBuffer.isView(a) || a instanceof ArrayBuffer) {
|
|
2963
|
+
const bytes = (v) => v instanceof ArrayBuffer ? new Uint8Array(v) : new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
|
|
2964
|
+
const x = bytes(a), y = bytes(b);
|
|
2965
|
+
return x.length === y.length && x.every((v, i) => v === y[i]);
|
|
2966
|
+
}
|
|
2413
2967
|
const ra = a, rb = b;
|
|
2414
2968
|
const ka = Object.keys(ra).filter((k) => ra[k] !== void 0);
|
|
2415
2969
|
const kb = Object.keys(rb).filter((k) => rb[k] !== void 0);
|
|
2416
2970
|
return ka.length === kb.length && ka.every((k) => Object.hasOwn(rb, k) && sameData(ra[k], rb[k]));
|
|
2417
2971
|
}
|
|
2418
2972
|
function isFed(e) {
|
|
2419
|
-
return e.kind === "outcome" || e.kind === "resolution" || e.kind === "resumed";
|
|
2973
|
+
return e.kind === "outcome" || e.kind === "resolution" || e.kind === "resumed" || e.kind === "interrupted" || e.kind === "paused" || e.kind === "unpaused" || e.kind === "attempted";
|
|
2974
|
+
}
|
|
2975
|
+
function frozenCopy(v) {
|
|
2976
|
+
try {
|
|
2977
|
+
return Object.freeze(cloneValue(v));
|
|
2978
|
+
} catch {
|
|
2979
|
+
return void 0;
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
function attemptNumberOf(detail) {
|
|
2983
|
+
const n = detail?.attemptNumber;
|
|
2984
|
+
return typeof n === "number" && Number.isInteger(n) && n >= 1 ? n : null;
|
|
2420
2985
|
}
|
|
2421
2986
|
function describeElement(e) {
|
|
2422
2987
|
if (e === void 0) return "nothing";
|
|
@@ -2460,6 +3025,38 @@ function withIndex(base, mapId, index) {
|
|
|
2460
3025
|
next.set(mapId, index);
|
|
2461
3026
|
return next;
|
|
2462
3027
|
}
|
|
3028
|
+
function itemMaps(def, regions) {
|
|
3029
|
+
const kept = /* @__PURE__ */ new Map();
|
|
3030
|
+
for (const n of def.nodes) {
|
|
3031
|
+
const own3 = Object.values(n.needs ?? {});
|
|
3032
|
+
const onEdges = (n.edges ?? []).flatMap((e) => Object.values(e.needs ?? {}));
|
|
3033
|
+
if (![...own3, ...onEdges].some((r) => referenceTarget(r)?.form === "item")) continue;
|
|
3034
|
+
const m = innermostMapOf(regions, n.id);
|
|
3035
|
+
if (m !== void 0) kept.set(m.fanoutId, false);
|
|
3036
|
+
}
|
|
3037
|
+
const byId = new Map(def.nodes.map((n) => [n.id, n]));
|
|
3038
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3039
|
+
const walk = (id) => {
|
|
3040
|
+
const n = byId.get(id);
|
|
3041
|
+
if (n === void 0 || seen.has(id)) return;
|
|
3042
|
+
seen.add(id);
|
|
3043
|
+
if (n.kind === "map" && n.needs === void 0 && kept.has(id)) kept.set(id, true);
|
|
3044
|
+
if (n.kind === "gate") for (const e of n.edges ?? []) walk(e.to);
|
|
3045
|
+
};
|
|
3046
|
+
walk(def.entry);
|
|
3047
|
+
return kept;
|
|
3048
|
+
}
|
|
3049
|
+
function itemWithin(kept, regions) {
|
|
3050
|
+
const out = /* @__PURE__ */ new Map();
|
|
3051
|
+
for (const id of kept.keys()) {
|
|
3052
|
+
const m = regions.find((r) => r.fanoutId === id);
|
|
3053
|
+
const branch = m?.branches[0];
|
|
3054
|
+
if (m === void 0 || branch === void 0) continue;
|
|
3055
|
+
const depth = regionChainOf(regions, branch).length;
|
|
3056
|
+
for (const r of regions) if (r !== m && m.members.has(r.fanoutId)) out.set(r.fanoutId, [...out.get(r.fanoutId) ?? [], { map: id, depth }]);
|
|
3057
|
+
}
|
|
3058
|
+
return out;
|
|
3059
|
+
}
|
|
2463
3060
|
function describeRoute(v) {
|
|
2464
3061
|
if (typeof v === "string") return JSON.stringify(v);
|
|
2465
3062
|
return `${describeShape(v)} (${String(v)}) \u2014 not a route name`;
|
|
@@ -2476,7 +3073,7 @@ function cloneRecord(r) {
|
|
|
2476
3073
|
}
|
|
2477
3074
|
function freezeInput(i) {
|
|
2478
3075
|
return Object.freeze(
|
|
2479
|
-
i.kind === "outcome" || i.kind === "resolution" ? { ...i, record: Object.freeze(cloneRecord(i.record)) } : i.kind === "region" ? { ...i, event: Object.freeze({ ...i.event }) } : i.kind === "resumed" ? { ...i, taskIds: Object.freeze([...i.taskIds]) } : i.kind === "gated" && i.key !== void 0 ? { ...i, key: Object.freeze(cloneValue(i.key)) } : { ...i }
|
|
3076
|
+
i.kind === "outcome" || i.kind === "resolution" ? { ...i, record: Object.freeze(cloneRecord(i.record)) } : i.kind === "region" ? { ...i, event: Object.freeze({ ...i.event }) } : i.kind === "resumed" ? { ...i, taskIds: Object.freeze([...i.taskIds]) } : i.kind === "gated" && i.key !== void 0 ? { ...i, key: Object.freeze(cloneValue(i.key)) } : i.kind === "asked" ? { ...i, input: Object.freeze(cloneValue(i.input)) } : { ...i }
|
|
2480
3077
|
);
|
|
2481
3078
|
}
|
|
2482
3079
|
var MissingJoinPolicy = class extends Error {
|
|
@@ -2489,7 +3086,9 @@ function detailOf(e) {
|
|
|
2489
3086
|
return "detail" in e ? e.detail : void 0;
|
|
2490
3087
|
}
|
|
2491
3088
|
var defaultCreateTask = (config) => new FyflowTask(config);
|
|
2492
|
-
var SETTLEMENT_EVENTS = ["task.completed", "task.failed", "task.user_action"];
|
|
3089
|
+
var SETTLEMENT_EVENTS = ["task.completed", "task.failed", "task.user_action", "task.cancelled"];
|
|
3090
|
+
var SUBSCRIBED_EVENTS = [...SETTLEMENT_EVENTS, "task.running"];
|
|
3091
|
+
var PASS_THROUGH = [["task.message", "onTaskMessage"], ["task.progress", "onTaskProgress"]];
|
|
2493
3092
|
var Driver = class {
|
|
2494
3093
|
/**
|
|
2495
3094
|
* `OR-R7` — nothing outside the inbound API writes to run state. Private, and
|
|
@@ -2517,6 +3116,13 @@ var Driver = class {
|
|
|
2517
3116
|
graph;
|
|
2518
3117
|
regions;
|
|
2519
3118
|
dispatched = /* @__PURE__ */ new Set();
|
|
3119
|
+
/**
|
|
3120
|
+
* **Every ASK this run has reached, by task id** — `RN-R37`, v0.22, W180, ADR 0109: the ids of the `asked` elements
|
|
3121
|
+
* appended, so `RS-R1`'s widened *suspended* (an `asked` element and no record after it) is one lookup rather than a
|
|
3122
|
+
* scan of the input sequence at every `resolve()`, and an ask's id is minted once per path (`ID-R1`'s guard). Ids only —
|
|
3123
|
+
* the kept INPUT lives on the element alone. `dispatched`'s twin for the one node that is never dispatched.
|
|
3124
|
+
*/
|
|
3125
|
+
asked = /* @__PURE__ */ new Set();
|
|
2520
3126
|
now;
|
|
2521
3127
|
opts;
|
|
2522
3128
|
def;
|
|
@@ -2594,6 +3200,29 @@ var Driver = class {
|
|
|
2594
3200
|
* quiescent point (`RN-R10`), not here — ADR 0046 rule 5.
|
|
2595
3201
|
*/
|
|
2596
3202
|
arrivals = /* @__PURE__ */ new Map();
|
|
3203
|
+
/**
|
|
3204
|
+
* **What a map INSTANCE keeps for `"$item"`** — `RN-R13`'s sixth term since v0.28 (`spec/run-state.md`), P38 (e),
|
|
3205
|
+
* [ADR 0118](../../doc/decisions/0118-a-map-instance-keeps-its-item-while-a-node-of-it-may-still-be-entered.md),
|
|
3206
|
+
* [W191](../../doc/work-items/w191-the-map-instances-item-as-a-reference.md). Two halves, one field:
|
|
3207
|
+
*
|
|
3208
|
+
* - **`kept`** — decided from the DOCUMENT at construction: the maps whose region holds a `"$item"` reader (a node, or
|
|
3209
|
+
* an edge of a node, whose innermost containing map is that map — `innermostMapOf`, P38 (c)), and for each whether
|
|
3210
|
+
* its received value may be the CALLER's own object (`true`: reached from the entry through gates alone, with no
|
|
3211
|
+
* `needs` — `start()` hands `enter` the value it was called with, as an entry task and an entry ask receive it). A
|
|
3212
|
+
* map no reader names keeps NOTHING, and a flow declaring no `"$item"` has `kept` empty (P38 decision 2).
|
|
3213
|
+
* - **`held`** — map id → the instance's branch path → its item. Written by the MAP ARM, where the instance opens —
|
|
3214
|
+
* by a run and by a resume's replay alike, so a replay REBUILDS it (`RN-R19`; P38's brief: *taken at rest*, never by
|
|
3215
|
+
* a caller's call) — **BY REFERENCE to the element the map received** (the driver's own: an edge-hop clone, or a
|
|
3216
|
+
* `needs` composition's per-key copy), and as its OWN copy only where `kept` says that element is the caller's
|
|
3217
|
+
* (P35 (f)'s reason for `asked.input`). Every READ clones (`JP-R11`), so no node receives the held object.
|
|
3218
|
+
* **Released when NOTHING of that instance may still be entered** — its own branch terminal (`branch.arrived`,
|
|
3219
|
+
* `branch.lost` or `branch.exited`, `JP-R5`: one per opened branch) AND no branch of a region NESTED in it still live
|
|
3220
|
+
* (`within`, `nested`, `ended` — `trackItems`): a nested join a policy fires before its last branch is terminal lets
|
|
3221
|
+
* the instance's branch arrive while that branch can still read (W191's conformity reader, C1). A suspended node (an
|
|
3222
|
+
* ask, a parked task) keeps its branch live, and so its item. And at the run-level moments
|
|
3223
|
+
* `arrivals` is cleared at — a halt, a cancel, a stop, a `cancelled` settlement — because no node is entered after.
|
|
3224
|
+
*/
|
|
3225
|
+
items;
|
|
2597
3226
|
/**
|
|
2598
3227
|
* **The one run this driver serves** — ADR 0022 rule 1, fixed at construction
|
|
2599
3228
|
* and never `null`.
|
|
@@ -2636,16 +3265,212 @@ var Driver = class {
|
|
|
2636
3265
|
* elements a consumer does not already hold ({@link Driver.delivers}).
|
|
2637
3266
|
*/
|
|
2638
3267
|
replay = null;
|
|
2639
|
-
/** The run has been
|
|
3268
|
+
/** The run has been HALTED by a refusal (`OR-R10`). Nothing dispatches for it again. */
|
|
2640
3269
|
failed = false;
|
|
2641
3270
|
/**
|
|
2642
|
-
* **A
|
|
2643
|
-
*
|
|
3271
|
+
* **A task of this run settled `cancelled`, so the run is HALTED** — `RN-R28`,
|
|
3272
|
+
* [ADR 0094](../../doc/decisions/0094-a-cancelled-task-is-a-terminal-state-that-routes-no-edge-and-ends-its-run-cancelled.md)
|
|
3273
|
+
* rule 3, W158. Every door `failed` shuts, this shuts — nothing is dispatched or routed for the run again,
|
|
3274
|
+
* a later settlement is appended and routed nowhere, and `resolve()` is refused — and it differs from `failed`
|
|
3275
|
+
* in three things only: no `halted` element is appended (the fold reads the run `cancelled` from the
|
|
3276
|
+
* settlement itself), `onRunFailure` is not called (a cancel is its owner's act, not a defect), and
|
|
3277
|
+
* `hasFailed()` stays false. **Separate rather than `failed` with a reason**, because those three are each
|
|
3278
|
+
* a reader of `failed` that must not fire.
|
|
3279
|
+
*/
|
|
3280
|
+
cancelled = false;
|
|
3281
|
+
/**
|
|
3282
|
+
* **A resume refused because its replay regenerated a HALT in the lost tail** — `RN-R23`: that driver keeps the
|
|
3283
|
+
* halted run (its `header`, so a store may take its checkpoint) and still hears NOTHING. `onEvent`'s other test,
|
|
2644
3284
|
* *started with no header*, cannot see it, because this refusal is the one that keeps its header: until W151 such a
|
|
2645
3285
|
* driver built by hand appended and delivered a later settlement and reported an unminted id (W151's conformity
|
|
2646
|
-
* reader, finding 6). A driver
|
|
3286
|
+
* reader, finding 6). A driver halted by `failRun` on a run it started is NOT deaf: `OR-R10` halts routing only.
|
|
3287
|
+
*
|
|
3288
|
+
* **And a STOPPED driver, since W174** (`RN-R33`, P34 decision 3): a stop sets it with its hold — before the
|
|
3289
|
+
* `interrupted` element, and so BEFORE it sends the first cancel — so no `task.cancelled` of the stop — nor any later settlement — is appended,
|
|
3290
|
+
* routed, delivered or reported, and the log ends with the in-flight set dispatched and unsettled for a resume to
|
|
3291
|
+
* re-dispatch. A replay feeding a stored `interrupted` element does NOT set it: the replay hears nothing anyway.
|
|
2647
3292
|
*/
|
|
2648
3293
|
deaf = false;
|
|
3294
|
+
/**
|
|
3295
|
+
* **The run's OWNER cancelled it** — `RN-R31`, v0.18, W173,
|
|
3296
|
+
* [ADR 0104](../../doc/decisions/0104-a-run-its-owner-cancels-is-cancelling-until-its-last-task-has-stopped.md). `null`
|
|
3297
|
+
* until `cancel()` is accepted; then what every LATER cancel of a task sends the scheduler — the caller's `reason` and
|
|
3298
|
+
* `graceMs`, each only if given — and, per task id, the promise the scheduler's `cancel` returned for it, which is what
|
|
3299
|
+
* `cancel()`'s own promise waits on. **A halt** (`shut()`), set BEFORE the `cancelRequested` element is appended, so a
|
|
3300
|
+
* consumer's callback on that element already sees the run held. It holds no result: one promise per task STILL
|
|
3301
|
+
* stopping — each dropped as it settles, so none once the run is `cancelled` — and the two option values.
|
|
3302
|
+
*/
|
|
3303
|
+
cancelRequest = null;
|
|
3304
|
+
/**
|
|
3305
|
+
* **The run's OWNER stopped it, to resume it elsewhere** — `RN-R33`, v0.19, W174,
|
|
3306
|
+
* [ADR 0105](../../doc/decisions/0105-a-run-its-owner-stops-goes-deaf-before-it-cancels-and-resumes-elsewhere.md). `null`
|
|
3307
|
+
* until `stop()` is accepted; then, as `cancelRequest`'s shape, what every cancel the stop sends carries and one promise
|
|
3308
|
+
* per task STILL stopping, each dropped as it settles — and the `actor`, which the element is appended with.
|
|
3309
|
+
*
|
|
3310
|
+
* **`pending` while the stop has been ACCEPTED and not yet TAKEN**: a stop called MID-STEP — from a callback on the
|
|
3311
|
+
* driver's own step — takes effect when that step ends (`stepping` back to `0`), so the step's routing is all in the log
|
|
3312
|
+
* and none of it is lost to a resume (`RN-R33`, W174). While pending it is the resolver of the promise every call made
|
|
3313
|
+
* before then waits on, `resolve()` and `cancel()` are already refused, and the hold is NOT yet shut; `null` once the
|
|
3314
|
+
* stop is taken, from which moment it IS a halt (`shut()`). `took` is the promise a second call made while pending
|
|
3315
|
+
* waits on. A function and a promise held only until the step ends — `null` both once taken, never at rest.
|
|
3316
|
+
*
|
|
3317
|
+
* **And a REPLAY sets it too**, taken, with no options and nothing stopping, where it feeds a stored `interrupted`
|
|
3318
|
+
* element (`RN-R19`): the stored run was held from that position, so the replay holds there as well, until a fed
|
|
3319
|
+
* `resumed` — or the resume itself, once the replay is over — sets it back to `null`. The stop's deafness is `deaf`'s.
|
|
3320
|
+
*/
|
|
3321
|
+
stopRequest = null;
|
|
3322
|
+
/**
|
|
3323
|
+
* **Tasks whose `dispatched` element is appended and which the scheduler has NOT been handed yet** — `RN-R29` (i)'s
|
|
3324
|
+
* window, ADR 0103's inherited hazard, W173. A scheduler's `cancel()` of an id it does not hold resolves `false` and
|
|
3325
|
+
* does nothing, so `cancel()` sends nothing for these: the hand-off site cancels each right AFTER its `addTask` —
|
|
3326
|
+
* `dispatch` for the task in progress, `resume()` for its in-flight set. Empty at rest; at most the one task being
|
|
3327
|
+
* dispatched, or a resume's in-flight set while it is handed back.
|
|
3328
|
+
*/
|
|
3329
|
+
unhanded = /* @__PURE__ */ new Set();
|
|
3330
|
+
/**
|
|
3331
|
+
* **How deep the driver is in its OWN step** — `start`, `resume`, `resolve` or a scheduler event, each counting itself
|
|
3332
|
+
* in (`RN-R31`, W173's conformity reader, D-1). `0` at rest. A consumer callback the driver calls mid-step sees a
|
|
3333
|
+
* PREFIX that can read finished while the step is about to dispatch more (`RN-R12`'s quiescent point, P34 fact 3), so
|
|
3334
|
+
* `cancel()` refuses `ended` only at rest: mid-step, *nothing in flight and nothing parked* is not *finished*.
|
|
3335
|
+
*/
|
|
3336
|
+
stepping = 0;
|
|
3337
|
+
/**
|
|
3338
|
+
* **The run's OWNER has paused it, and the pause is TAKEN** — `RN-R35`, v0.20, W175,
|
|
3339
|
+
* [ADR 0106](../../doc/decisions/0106-a-run-its-owner-pauses-routes-on-and-holds-every-dispatch-until-unpaused.md):
|
|
3340
|
+
* set when a `paused` element is appended (by `pause()`, or fed to a replay) and cleared when an `unpaused` one is —
|
|
3341
|
+
* so it is always what `RunState.paused` folds to on the driver's own sequence. **Not a halt**: it does not join
|
|
3342
|
+
* `shut()`, because a paused run still routes and still accepts a resolution (ADR 0103's consequence). It is the
|
|
3343
|
+
* dispatch hold's DEFERRED form, at the dispatch door (ADR 0103 rule 6): each dispatch is appended, and held.
|
|
3344
|
+
*/
|
|
3345
|
+
paused = false;
|
|
3346
|
+
/**
|
|
3347
|
+
* **Every dispatch the pause HELD, in dispatch order** (`RN-R35`): appended — so the fold reads each `running`, in flight
|
|
3348
|
+
* under `RN-R21` (P34 fact 3) — and NOT handed to the scheduler. An `unpause` hands each on, first held first; a
|
|
3349
|
+
* `cancel` hands each on and cancels it (ADR 0104 rule 7); a `stop` sends nothing for them, so they stay in flight in
|
|
3350
|
+
* the log for a resume to hold again. Empty unless the run is paused — or an unpause's hand-off is under way, while
|
|
3351
|
+
* which a dispatch joins the END of it, so the order holds; or a halt broke that hand-off off, after which nothing is
|
|
3352
|
+
* handed on for the run and what is left here never is. One `createTask` configuration per held task, no result.
|
|
3353
|
+
*/
|
|
3354
|
+
pauseHeld = /* @__PURE__ */ new Map();
|
|
3355
|
+
/**
|
|
3356
|
+
* **Pause and unpause calls ACCEPTED mid-step and not yet TAKEN**, in call order (`RN-R35`): a call from a callback on
|
|
3357
|
+
* the driver's own step is taken when that step ends (`stepping` back to `0`, `leaveStep()`), as a stop is (`RN-R33`) —
|
|
3358
|
+
* so its element lands BETWEEN two steps, where a replay can feed it (`RN-R19`). Empty at rest.
|
|
3359
|
+
* **And, since W180, a RESOLUTION given mid-step** — an ask's answer or a parked task's (`RN-R37`, ADR 0109 rule 5, P35's
|
|
3360
|
+
* brief: *an owner element a resume's replay feeds is TAKEN AT REST*): accepted at the call — every refusal already asked — and its resolution
|
|
3361
|
+
* appended when the step ends, in the same call order as the pauses beside it.
|
|
3362
|
+
*/
|
|
3363
|
+
pausePending = [];
|
|
3364
|
+
/**
|
|
3365
|
+
* **The highest attempt number RECORDED for each task, by task id** — `RN-R40`, v0.26, W186, P36 (f) and (g): the
|
|
3366
|
+
* number the last `attempted` element for it carried — appended, or fed by a replay. What decides that a `task.running`
|
|
3367
|
+
* repeating a number (a requeue) appends nothing, and what a resume re-hands an in-flight task with as `attemptsSpent`.
|
|
3368
|
+
* One integer per task that ever started an attempt, no result; kept for the life of the run, as `dispatched`'s ids are.
|
|
3369
|
+
*/
|
|
3370
|
+
lastAttempt = /* @__PURE__ */ new Map();
|
|
3371
|
+
/**
|
|
3372
|
+
* **`attempted` elements HEARD and not yet appended**, in the order heard (`RN-R40`): a `task.running` arrives on the
|
|
3373
|
+
* driver's OWN step when an inline pool starts the task inside `addTask`, and an element appended there would sit between
|
|
3374
|
+
* two elements of one step — where the replay, which regenerates a step whole, would refuse the checkpoint `diverged`. So
|
|
3375
|
+
* every one is appended when the step ends, BEFORE a pause, an unpause, an answer or a stop pending on that step is taken
|
|
3376
|
+
* (`leaveStep()`): the attempt started before any of them. Empty at rest.
|
|
3377
|
+
*/
|
|
3378
|
+
attemptsPending = [];
|
|
3379
|
+
/**
|
|
3380
|
+
* **The dispatch hold is SHUT: a halt is on the run, and the driver dispatches and routes nothing further for it** —
|
|
3381
|
+
* `RN-R29`,
|
|
3382
|
+
* [ADR 0103](../../doc/decisions/0103-every-halt-shares-one-dispatch-hold.md), P34
|
|
3383
|
+
* decisions 5 and 8, W172. **The ONE predicate every halt shares**, read at these doors and no others — `RN-R29`
|
|
3384
|
+
* states the two things it does not hold: before each of `resume()`'s re-dispatches (the maintainer's answer to
|
|
3385
|
+
* W172's S-1), at the head of `enter` (door (a) of [Q605](../../doc/questions/q605.md) — a fan-out's or a map's remaining
|
|
3386
|
+
* branches), at the head of `dispatch`, after a resolution's append and again before its routing (door (b)),
|
|
3387
|
+
* after a settlement's append (`settle`), after a join's `joinSettled` append (`settleJoin`), and at `resolve()`'s
|
|
3388
|
+
* entry. A halt P34 adds — a cancel or a stop — joins this disjunction, and every door follows.
|
|
3389
|
+
*
|
|
3390
|
+
* **A METHOD over the halt flags, never a field of its own**: a second copy of *the run is halted* is a field that
|
|
3391
|
+
* can disagree with `failed` or `cancelled`, and each of those has readers this predicate must not stand in for
|
|
3392
|
+
* (`hasFailed()`, `onRunFailure`). **The replay's hold is the OTHER form and is not this** — it appends a dispatch
|
|
3393
|
+
* and defers handing it on (`RN-R19`), where a shut hold appends nothing; `dispatch` keeps both.
|
|
3394
|
+
*
|
|
3395
|
+
* **A REQUESTED cancel joins it since W173** (`RN-R31`, ADR 0104) — ADR 0103's *a halt P34 adds joins this
|
|
3396
|
+
* disjunction*: from the moment `cancel()` is accepted, every door above holds for the run. **And a STOP since W174**
|
|
3397
|
+
* (`RN-R33`, ADR 0105): from the moment the stop is TAKEN — at once when `stop()` is called at rest, and when the step
|
|
3398
|
+
* ends when it is called mid-step (`stopRequest.pending`) — and, in a replay, from a fed `interrupted` element on.
|
|
3399
|
+
*/
|
|
3400
|
+
shut() {
|
|
3401
|
+
return this.failed || this.cancelled || this.cancelRequest !== null || this.stopRequest !== null && this.stopRequest.pending === null;
|
|
3402
|
+
}
|
|
3403
|
+
/**
|
|
3404
|
+
* **What holds a resume's re-dispatch: the hold, EXCEPT a requested cancel** (ADR 0103 rule 7, ADR 0104 rule 7) — read
|
|
3405
|
+
* through `shut()` and never as a second copy of its disjunction (ADR 0103 rule 1; W173's conformity reader, F7), so a
|
|
3406
|
+
* halt W174 adds to `shut()` holds here too. **W174's stop never reaches this loop taken** (`RN-R33`): a stop is taken
|
|
3407
|
+
* at rest, and a stop called on this stack — on the `resumed` element, or in the replay's lost tail — is PENDING until
|
|
3408
|
+
* `resume()` returns, so each task is handed on here and the stop then cancels it; and the replay's own fed stop is
|
|
3409
|
+
* cleared before the loop. A request is the one halt that hands a task on: its tasks are `dispatched` and unsettled,
|
|
3410
|
+
* and only the scheduler can settle them, so each is handed on and cancelled (`handOn`).
|
|
3411
|
+
*/
|
|
3412
|
+
holdsResume() {
|
|
3413
|
+
return this.shut() && this.cancelRequest === null;
|
|
3414
|
+
}
|
|
3415
|
+
/**
|
|
3416
|
+
* **The driver's own step ends** — each of `start`, `resume`, `resolve` and a scheduler event counts itself out here.
|
|
3417
|
+
* At rest again, a stop called on that step is TAKEN (`RN-R33`, W174): the step's routing is whole in the log, and
|
|
3418
|
+
* every task it handed on is in flight for the stop to cancel. **And a pause or an unpause called on it** (`RN-R35`,
|
|
3419
|
+
* W175), each in call order and BEFORE the stop — a pause or an unpause after a stop is refused, so any pending one was
|
|
3420
|
+
* called first. Each take is a step of its own and ends here too, so the next pending call is taken after it.
|
|
3421
|
+
* **And a resolution given on the step** (`RN-R37`, W180), in the same queue and the same call order.
|
|
3422
|
+
* **And FIRST, every `attempted` element heard on the step** (`RN-R40`, W186): the attempts started before anything
|
|
3423
|
+
* taken here, and a stop taken before them would leave them unrecorded — the driver deaf — under P36 (h)'s *a stopped
|
|
3424
|
+
* attempt counts*.
|
|
3425
|
+
*/
|
|
3426
|
+
leaveStep() {
|
|
3427
|
+
this.stepping -= 1;
|
|
3428
|
+
if (this.stepping === 0 && this.attemptsPending.length > 0) this.takeAttempts();
|
|
3429
|
+
while (this.stepping === 0 && this.pausePending.length > 0) this.takePending(this.pausePending.shift());
|
|
3430
|
+
if (this.stepping === 0 && this.stopRequest !== null && this.stopRequest.pending !== null) this.takeStop();
|
|
3431
|
+
}
|
|
3432
|
+
/**
|
|
3433
|
+
* **Every `attempted` element heard and not yet appended, appended — AT REST, as ONE step of their own** (`RN-R40`,
|
|
3434
|
+
* W186): in the order heard, each BETWEEN two of the driver's steps, where a replay feeds it (`RN-R19`). A step, so a
|
|
3435
|
+
* call a consumer makes from a callback on one — a pause, a stop — is taken after the last of them. **Appended on a
|
|
3436
|
+
* halted run too**, as a settlement is: the scheduler started the attempt whatever the run's routing does. A consumer's
|
|
3437
|
+
* `onFoldInput` THROWING on one HALTS the run, reported once (`OR-R10` v0.19, W184), and the throw reaches whoever is on
|
|
3438
|
+
* the stack — a driver call's caller, or the listener boundary of a scheduler event's step; the elements still pending
|
|
3439
|
+
* behind it are appended by this step's own end, SILENTLY (the run is halted, its failure reported), and so is a throw
|
|
3440
|
+
* on any of them once the run is halted.
|
|
3441
|
+
*/
|
|
3442
|
+
takeAttempts() {
|
|
3443
|
+
this.stepping += 1;
|
|
3444
|
+
try {
|
|
3445
|
+
while (this.attemptsPending.length > 0) {
|
|
3446
|
+
const a = this.attemptsPending.shift();
|
|
3447
|
+
const silent = this.failed;
|
|
3448
|
+
try {
|
|
3449
|
+
this.append({ kind: "attempted", taskId: a.taskId, attemptNumber: a.attemptNumber });
|
|
3450
|
+
} catch (e) {
|
|
3451
|
+
if (silent) continue;
|
|
3452
|
+
this.haltOnce(e);
|
|
3453
|
+
throw e;
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
} finally {
|
|
3457
|
+
this.leaveStep();
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
/**
|
|
3461
|
+
* **The ONE rule for a throw that reaches a driver call's boundary once the call is ACCEPTED** — `OR-R10` v0.19, P35 (k),
|
|
3462
|
+
* P36 (b) and (c), W184: reported ONCE through `onRunFailure` under its own class, and the run HALTED — its `halted`
|
|
3463
|
+
* element appended after the element thrown on — unless the run is already halted, whose halt was reported when it was
|
|
3464
|
+
* raised. `start()`'s treatment since W3, brought to every call: `resolve`, `resume` and `cancel` at their own element,
|
|
3465
|
+
* and EVERY take — a stop's, a pause's, an unpause's, an answer's — at its own element, at rest or at a step's end, so
|
|
3466
|
+
* the FIRST throw halts and a call still pending behind it is dropped by its own halted guard (the readers' C1). The
|
|
3467
|
+
* caller rethrows: the consumer's error reaches it.
|
|
3468
|
+
* A refusal the call raises BEFORE it is accepted — `RunControlRefused`, `BoundaryRefused`, `ResumeRefused`,
|
|
3469
|
+
* `MultipleRuns`, a `DataCloneError` — never reaches this: nothing was appended, and a rejected command halts nothing.
|
|
3470
|
+
*/
|
|
3471
|
+
haltOnce(e) {
|
|
3472
|
+
if (!this.failed) this.failRun(e);
|
|
3473
|
+
}
|
|
2649
3474
|
/** The registry's node types, normalised once (W59; entries the document or the class form since W65). Read at dispatch for the type's retry default and its settings defaults. */
|
|
2650
3475
|
nodeTypes;
|
|
2651
3476
|
/** The registry's languages, normalised once (W61). Read at construction only; the driver evaluates nothing. */
|
|
@@ -2654,6 +3479,11 @@ var Driver = class {
|
|
|
2654
3479
|
workers;
|
|
2655
3480
|
/** The gate types — the built-ins and the registry's — normalised once (W93). Read where a branch enters a gate, and at `start()` for an ENTRY gate's `FD-R4` target. */
|
|
2656
3481
|
gateTypes;
|
|
3482
|
+
/**
|
|
3483
|
+
* **Each gate's RESOLVED route set, by gate id** — ADR 0092 rule 3: `routesOf` asked once per gate at
|
|
3484
|
+
* construction, after validation passed, and read by every decision. Never the type's members.
|
|
3485
|
+
*/
|
|
3486
|
+
gateRoutes;
|
|
2657
3487
|
/**
|
|
2658
3488
|
* **Each gate's MEMORY: one per gate per RUN, the driver's, and never written down** — `RN-R26`,
|
|
2659
3489
|
* [ADR 0067](../../doc/decisions/0067-a-gate-runs-in-the-driver-its-memory-is-the-drivers-and-its-decision-is-recorded.md),
|
|
@@ -2726,25 +3556,35 @@ var Driver = class {
|
|
|
2726
3556
|
...opts.gateTypes !== void 0 ? { gateTypes: opts.gateTypes } : {}
|
|
2727
3557
|
};
|
|
2728
3558
|
const findings2 = validateDefinition(definition, registry);
|
|
2729
|
-
if (findings2.length)
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
);
|
|
3559
|
+
if (findings2.length) throw InvalidDefinition.fromFindings(definition.flowId, findings2);
|
|
3560
|
+
const gateRoutes = /* @__PURE__ */ new Map();
|
|
3561
|
+
for (const n of definition.nodes) {
|
|
3562
|
+
if (n.kind !== "gate") continue;
|
|
3563
|
+
const resolved = routesOf(this.gateTypes.get(n.type), n.settings ?? {});
|
|
3564
|
+
if ("problem" in resolved) {
|
|
3565
|
+
throw new InvalidDefinition(["FD-V39"], `gate "${n.id}"'s type "${n.type}" resolved no route set at construction, after validation accepted one \u2014 FD-V39: ${resolved.problem}`);
|
|
3566
|
+
}
|
|
3567
|
+
gateRoutes.set(n.id, resolved);
|
|
2734
3568
|
}
|
|
3569
|
+
this.gateRoutes = gateRoutes;
|
|
2735
3570
|
this.def = definition;
|
|
2736
3571
|
this.opts = opts;
|
|
2737
3572
|
this.now = opts.now ?? (() => Date.now());
|
|
2738
3573
|
this.graph = buildGraph(definition);
|
|
2739
3574
|
this.regions = allRegions(this.graph, definition);
|
|
3575
|
+
const kept = itemMaps(definition, this.regions);
|
|
3576
|
+
this.items = { kept, within: itemWithin(kept, this.regions), held: /* @__PURE__ */ new Map(), nested: /* @__PURE__ */ new Map(), ended: /* @__PURE__ */ new Set() };
|
|
2740
3577
|
if (this.regions.length > 0 && !opts.joinPolicy) {
|
|
2741
3578
|
throw new MissingJoinPolicy(
|
|
2742
3579
|
`definition "${definition.flowId}" has ${this.regions.length} fan-out region(s) and no joinPolicy. Without one every branch would arrive and the join would never fire, which is indistinguishable from the case P1 exists to measure.`
|
|
2743
3580
|
);
|
|
2744
3581
|
}
|
|
2745
|
-
for (const name of
|
|
3582
|
+
for (const name of SUBSCRIBED_EVENTS) {
|
|
2746
3583
|
opts.scheduler.addEventListener(name, (e) => this.onEvent(name, detailOf(e)));
|
|
2747
3584
|
}
|
|
3585
|
+
for (const [name, callback] of PASS_THROUGH) {
|
|
3586
|
+
if (opts[callback] !== void 0) opts.scheduler.addEventListener(name, (e) => this.onEvent(name, detailOf(e)));
|
|
3587
|
+
}
|
|
2748
3588
|
}
|
|
2749
3589
|
/**
|
|
2750
3590
|
* Dispatch the entry node for the run this driver was constructed for.
|
|
@@ -2771,6 +3611,15 @@ var Driver = class {
|
|
|
2771
3611
|
* substitution rather than dropping it.
|
|
2772
3612
|
*/
|
|
2773
3613
|
start(payload) {
|
|
3614
|
+
this.stepping += 1;
|
|
3615
|
+
try {
|
|
3616
|
+
this.startStep(payload);
|
|
3617
|
+
} finally {
|
|
3618
|
+
this.leaveStep();
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
/** `start`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
|
|
3622
|
+
startStep(payload) {
|
|
2774
3623
|
const runId = this.runId;
|
|
2775
3624
|
if (this.started) {
|
|
2776
3625
|
throw new MultipleRuns(
|
|
@@ -2781,11 +3630,12 @@ var Driver = class {
|
|
|
2781
3630
|
for (const { node, value } of boundaryTargets(this.def, payload, (g, v) => this.boundaryEdges(g, v))) {
|
|
2782
3631
|
const n = this.graph.byId.get(node);
|
|
2783
3632
|
if (!n || n.kind !== "task") continue;
|
|
3633
|
+
if (n.transform !== void 0) continue;
|
|
2784
3634
|
const type = this.nodeTypes.get(n.type);
|
|
2785
3635
|
const worker = type?.worker === void 0 ? void 0 : this.workers.get(type.worker);
|
|
2786
3636
|
const schema = worker?.inputSchema;
|
|
2787
3637
|
if (schema === void 0) continue;
|
|
2788
|
-
const received = this.compose(n, value,
|
|
3638
|
+
const received = this.compose(n, value, EMPTY_INDICES, payload, () => value);
|
|
2789
3639
|
const fs = findings(schema, received);
|
|
2790
3640
|
if (fs.length) {
|
|
2791
3641
|
throw new BoundaryRefused(
|
|
@@ -2799,12 +3649,23 @@ var Driver = class {
|
|
|
2799
3649
|
}
|
|
2800
3650
|
}
|
|
2801
3651
|
const entryPayload = structuredClone(payload);
|
|
2802
|
-
|
|
3652
|
+
const header = { format: CHECKPOINT_FORMAT, runId, flowId: this.def.flowId, version: this.def.version, entryPayload };
|
|
3653
|
+
const onHeader = this.opts.onHeader;
|
|
3654
|
+
if (onHeader !== void 0) {
|
|
3655
|
+
this.started = true;
|
|
3656
|
+
try {
|
|
3657
|
+
onHeader(Object.freeze({ ...header, entryPayload: structuredClone(entryPayload) }));
|
|
3658
|
+
} catch (e) {
|
|
3659
|
+
this.started = false;
|
|
3660
|
+
throw e;
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
this.header = header;
|
|
2803
3664
|
this.started = true;
|
|
2804
3665
|
try {
|
|
2805
3666
|
this.enter(runId, this.def.entry, payload);
|
|
2806
3667
|
} catch (e) {
|
|
2807
|
-
this.
|
|
3668
|
+
this.haltOnce(e);
|
|
2808
3669
|
throw e;
|
|
2809
3670
|
}
|
|
2810
3671
|
}
|
|
@@ -2819,28 +3680,43 @@ var Driver = class {
|
|
|
2819
3680
|
* **In order, and each step is a rule:**
|
|
2820
3681
|
* 1. **Refuse what the header says** (`RN-R23`) — {@link resumeRefusal}.
|
|
2821
3682
|
* 2. **REPLAY** (`RN-R19`): enter the entry node with a copy of `header.entryPayload` exactly as
|
|
2822
|
-
* `start()` enters it, then FEED every stored settlement, resolution and `resumed` element
|
|
2823
|
-
* own position through the routing the listener and `resolve()` use — every dispatch HELD. Every
|
|
3683
|
+
* `start()` enters it, then FEED every stored settlement, resolution and `resumed` element — and, since
|
|
3684
|
+
* v0.19 and v0.20, every `interrupted`, `paused` and `unpaused` one, and since v0.26 every `attempted` one — at its own position through the routing the listener and `resolve()` use — every dispatch HELD. Every
|
|
2824
3685
|
* other element the replay REGENERATES.
|
|
2825
3686
|
* 3. **The PREFIX check** (`RN-R20`): each regenerated element at a position the stored events cover
|
|
2826
3687
|
* must be the stored one; the first that is not is refused as `diverged`, naming its position, and
|
|
2827
3688
|
* so is a replay that ends short. A replay that runs PAST the stored events is the stored run's
|
|
2828
3689
|
* lost tail — a checkpoint taken inside `onFoldInput` ends mid-step (ADR 0064) — and it is
|
|
2829
3690
|
* DELIVERED, because the consumer does not hold it (`RN-R24`).
|
|
2830
|
-
* 4. **A run the replay
|
|
3691
|
+
* 4. **A run the replay HALTS is refused** as `halted` (P23 decision 10), its tail delivered first.
|
|
2831
3692
|
* 5. **Append `resumed`** naming the IN-FLIGHT set — dispatched and never settled, in dispatch order
|
|
2832
|
-
* (`RN-R21`, `RN-R22`) — and then hand each to the scheduler at the SAME task id
|
|
3693
|
+
* (`RN-R21`, `RN-R22`) — and then hand each to the scheduler at the SAME task id, with `attemptsSpent` the highest
|
|
3694
|
+
* attempt recorded for it where one was (v0.26, W186); or, where the replay
|
|
3695
|
+
* ends PAUSED (a fed `paused`, no `unpaused` after it), HOLD each again for an unpause (`RN-R35`).
|
|
2833
3696
|
*
|
|
2834
3697
|
* **At-least-once, and that is the contract rather than a defect** (P23 decision 6): a task named in
|
|
2835
|
-
* `resumed` may have finished in the process that is gone, and runs again here
|
|
2836
|
-
*
|
|
2837
|
-
*
|
|
3698
|
+
* `resumed` may have finished in the process that is gone, and runs again here. **Not at a fresh retry budget since
|
|
3699
|
+
* W186** (`RN-R21` v0.26, ADR 0113): it is handed on with `attemptsSpent`, the highest attempt the stored run recorded
|
|
3700
|
+
* for it — the attempt in flight at the kill or the stop counted as spent — so a spent budget runs ONCE more and no more.
|
|
3701
|
+
* *Until W186 it ran at a FRESH budget ([Q365](../../doc/questions/q365.md) — measured: a budget of two attempts ran
|
|
3702
|
+
* three times across one kill).* A task parked at `user_action` has SETTLED and is not in flight: it stays parked,
|
|
2838
3703
|
* and `resolve()` works on it.
|
|
2839
3704
|
*
|
|
2840
3705
|
* @throws MultipleRuns — the driver has already started or resumed a run.
|
|
2841
3706
|
* @throws ResumeRefused — see {@link ResumeRefusalReason}; the driver is then spent.
|
|
3707
|
+
* @throws the consumer's own error — a callback that threw on the `resumed` element, or on an element the resume's step
|
|
3708
|
+
* appended after it: the run is HALTED and reported once, and nothing further is handed on (P36 (c), W184).
|
|
2842
3709
|
*/
|
|
2843
3710
|
resume(checkpoint) {
|
|
3711
|
+
this.stepping += 1;
|
|
3712
|
+
try {
|
|
3713
|
+
this.resumeStep(checkpoint);
|
|
3714
|
+
} finally {
|
|
3715
|
+
this.leaveStep();
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
/** `resume`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
|
|
3719
|
+
resumeStep(checkpoint) {
|
|
2844
3720
|
if (this.started) {
|
|
2845
3721
|
throw new MultipleRuns(
|
|
2846
3722
|
`this driver already started or resumed run "${this.runId}", and resume() is the other door into the SAME run. ID-R3: a Driver serves exactly one run, entered once \u2014 construct a fresh Driver for the checkpoint (runtime.resume does).`
|
|
@@ -2869,30 +3745,49 @@ var Driver = class {
|
|
|
2869
3745
|
this.replay = null;
|
|
2870
3746
|
if (this.failed) {
|
|
2871
3747
|
this.deaf = true;
|
|
2872
|
-
const at = this.inputs.findIndex((e) => e.kind === "
|
|
3748
|
+
const at = this.inputs.findIndex((e) => e.kind === "halted");
|
|
2873
3749
|
throw new ResumeRefused(
|
|
2874
|
-
"
|
|
3750
|
+
"halted",
|
|
2875
3751
|
at,
|
|
2876
|
-
`ResumeRefused (
|
|
3752
|
+
`ResumeRefused (halted): replaying run "${this.runId}" regenerated its HALT at position ${at} \u2014 past the ${stored.length} stored element(s), in the tail the store had lost: ${describeElement(this.inputs[at])}. A halted run is not resumable (P23 decision 10); the tail, \`halted\` included, has been delivered to onFoldInput so a store can hold it.`
|
|
2877
3753
|
);
|
|
2878
3754
|
}
|
|
3755
|
+
if (this.stopRequest !== null && this.stopRequest.pending === null) this.stopRequest = null;
|
|
2879
3756
|
const inFlight = this.inFlight();
|
|
2880
|
-
|
|
2881
|
-
|
|
3757
|
+
const holding = this.paused;
|
|
3758
|
+
for (const id of inFlight) {
|
|
3759
|
+
const config = replay.held.get(id);
|
|
3760
|
+
const spent = this.lastAttempt.get(id) ?? 0;
|
|
3761
|
+
if (config !== void 0 && spent > 0) replay.held.set(id, { ...config, attemptsSpent: spent });
|
|
3762
|
+
}
|
|
3763
|
+
if (holding) {
|
|
2882
3764
|
for (const id of inFlight) {
|
|
2883
3765
|
const config = replay.held.get(id);
|
|
2884
3766
|
if (config === void 0) throw new Error(`resume: in-flight task "${id}" has no held dispatch \u2014 this is a defect in the driver`);
|
|
2885
|
-
this.
|
|
3767
|
+
this.pauseHeld.set(id, config);
|
|
3768
|
+
}
|
|
3769
|
+
} else {
|
|
3770
|
+
for (const id of inFlight) this.unhanded.add(id);
|
|
3771
|
+
}
|
|
3772
|
+
try {
|
|
3773
|
+
this.append({ kind: "resumed", taskIds: inFlight });
|
|
3774
|
+
for (const id of holding ? [] : inFlight) {
|
|
3775
|
+
if (this.holdsResume()) break;
|
|
3776
|
+
const config = replay.held.get(id);
|
|
3777
|
+
if (config === void 0) throw new Error(`resume: in-flight task "${id}" has no held dispatch \u2014 this is a defect in the driver`);
|
|
3778
|
+
this.handOn(id, config);
|
|
2886
3779
|
}
|
|
2887
3780
|
} catch (e) {
|
|
2888
|
-
this.
|
|
3781
|
+
this.haltOnce(e);
|
|
2889
3782
|
throw e;
|
|
3783
|
+
} finally {
|
|
3784
|
+
this.unhanded.clear();
|
|
2890
3785
|
}
|
|
2891
3786
|
}
|
|
2892
3787
|
/**
|
|
2893
3788
|
* **The replay** — `RN-R19` and `RN-R20`. Every fed element goes through {@link Driver.feed}, which
|
|
2894
|
-
* turns a refusal routing raises into a `
|
|
2895
|
-
* (`OR-R10`), so the replay regenerates a
|
|
3789
|
+
* turns a refusal routing raises into a `halted` element exactly as the listener's boundary does
|
|
3790
|
+
* (`OR-R10`), so the replay regenerates a halt where the stored run had one.
|
|
2896
3791
|
*/
|
|
2897
3792
|
replayStored(stored) {
|
|
2898
3793
|
const replay = this.replay;
|
|
@@ -2906,16 +3801,35 @@ var Driver = class {
|
|
|
2906
3801
|
}
|
|
2907
3802
|
if (this.inputs.length < stored.length) throw this.diverged(this.inputs.length, "the replay ended before it");
|
|
2908
3803
|
}
|
|
2909
|
-
/** One fed element — a settlement, a resolution,
|
|
3804
|
+
/** One fed element — a settlement, a resolution, an earlier resume, a stop, a pause, an unpause or an attempt — at stored position `i`. */
|
|
2910
3805
|
feedOne(e, i) {
|
|
2911
3806
|
if (e.kind === "resumed") {
|
|
2912
3807
|
const now = this.inFlight();
|
|
2913
3808
|
if (!Array.isArray(e.taskIds) || e.taskIds.length !== now.length || e.taskIds.some((t, k) => t !== now[k])) {
|
|
2914
3809
|
throw this.diverged(i, `in flight in the replay here: ${JSON.stringify(now)}`);
|
|
2915
3810
|
}
|
|
3811
|
+
this.stopRequest = null;
|
|
2916
3812
|
this.append({ kind: "resumed", taskIds: [...e.taskIds] });
|
|
2917
3813
|
return;
|
|
2918
3814
|
}
|
|
3815
|
+
if (e.kind === "interrupted") {
|
|
3816
|
+
this.feedInterrupted(e);
|
|
3817
|
+
return;
|
|
3818
|
+
}
|
|
3819
|
+
if (e.kind === "paused" || e.kind === "unpaused") {
|
|
3820
|
+
this.paused = e.kind === "paused";
|
|
3821
|
+
this.append({ kind: e.kind, actor: e.actor });
|
|
3822
|
+
return;
|
|
3823
|
+
}
|
|
3824
|
+
if (e.kind === "attempted") {
|
|
3825
|
+
const n = e.attemptNumber;
|
|
3826
|
+
if (typeof e.taskId !== "string" || !this.dispatched.has(e.taskId)) throw this.diverged(i, "the replay never dispatched that task");
|
|
3827
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 1) throw this.diverged(i, "its attemptNumber is not a positive integer");
|
|
3828
|
+
if (n <= (this.lastAttempt.get(e.taskId) ?? 0)) throw this.diverged(i, "its attemptNumber does not exceed the one stored before it for that task");
|
|
3829
|
+
this.lastAttempt.set(e.taskId, n);
|
|
3830
|
+
this.append({ kind: "attempted", taskId: e.taskId, attemptNumber: n });
|
|
3831
|
+
return;
|
|
3832
|
+
}
|
|
2919
3833
|
if (e.kind !== "outcome" && e.kind !== "resolution") return;
|
|
2920
3834
|
const record = e.record;
|
|
2921
3835
|
let parts;
|
|
@@ -2932,7 +3846,7 @@ var Driver = class {
|
|
|
2932
3846
|
this.settle(cloneRecord(record), runId, nodeId, branchPath);
|
|
2933
3847
|
return;
|
|
2934
3848
|
}
|
|
2935
|
-
if (!
|
|
3849
|
+
if (!this.parked(record.taskId)) throw this.diverged(i, "that task is not parked in the replay");
|
|
2936
3850
|
this.appendResolution(cloneRecord(record), nodeId, branchPath);
|
|
2937
3851
|
this.advance(runId, nodeId, branchPath);
|
|
2938
3852
|
}
|
|
@@ -2979,14 +3893,43 @@ var Driver = class {
|
|
|
2979
3893
|
* second resolution is refused like any other. A rejected command, not a run
|
|
2980
3894
|
* failure: it came from outside and a caller can retry.
|
|
2981
3895
|
*
|
|
3896
|
+
* **And an ASK's answer** — `RS-R1` v0.18, `RN-R37`, ADR 0109: an ask the run reached and nobody has answered is
|
|
3897
|
+
* suspended too, so the person's answer is this call — `'done'` with the answer as `result`, held to the ask's
|
|
3898
|
+
* `answerSchema` (`FD-R4`), or `'failed'` with an `error`, the person DECLINING, held to nothing. The answer IS the
|
|
3899
|
+
* node's result and routes as a task's settlement does.
|
|
3900
|
+
*
|
|
2982
3901
|
* @returns `false` when refused. Nothing is appended and the run is unaffected.
|
|
2983
3902
|
*/
|
|
2984
3903
|
resolve(taskId, resolvedAs, value) {
|
|
2985
|
-
|
|
3904
|
+
const midStep = this.stepping > 0;
|
|
3905
|
+
this.stepping += 1;
|
|
3906
|
+
try {
|
|
3907
|
+
return this.resolveStep(taskId, resolvedAs, value, midStep);
|
|
3908
|
+
} finally {
|
|
3909
|
+
this.leaveStep();
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
/** `resolve`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
|
|
3913
|
+
resolveStep(taskId, resolvedAs, value, midStep = false) {
|
|
3914
|
+
if (!this.parked(taskId)) return false;
|
|
3915
|
+
if (this.pausePending.some((c) => c.kind === "answer" && c.record.taskId === taskId)) return false;
|
|
2986
3916
|
const { runId, nodeId, branchPath } = parseTaskId(taskId);
|
|
2987
|
-
if (this.
|
|
3917
|
+
if (this.shut() || this.stopRequest !== null) return false;
|
|
2988
3918
|
if (resolvedAs === "done" && this.opts.validateBoundaries !== false) {
|
|
2989
3919
|
const n = this.graph.byId.get(nodeId);
|
|
3920
|
+
if (n?.kind === "ask") {
|
|
3921
|
+
const schema2 = n.answerSchema;
|
|
3922
|
+
const fs = schema2 === void 0 ? [] : findings(schema2, value.result ?? null);
|
|
3923
|
+
if (fs.length) {
|
|
3924
|
+
throw new BoundaryRefused(
|
|
3925
|
+
"resolve",
|
|
3926
|
+
nodeId,
|
|
3927
|
+
taskId,
|
|
3928
|
+
fs,
|
|
3929
|
+
`the answer to "${taskId}" (ask "${nodeId}") does not satisfy the ask's answerSchema \u2014 ${describeFindings(fs)}. FD-R4: a person\`s answer to an ask is held to the schema the ask declares; nothing was appended and the ask is still parked, so answer it again with a conforming value. \`validateBoundaries: false\` turns the check off.`
|
|
3930
|
+
);
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
2990
3933
|
const type = n?.kind === "task" ? this.nodeTypes.get(n.type) : void 0;
|
|
2991
3934
|
const schema = type?.worker === void 0 ? void 0 : this.workers.get(type.worker)?.outputSchema;
|
|
2992
3935
|
if (schema !== void 0) {
|
|
@@ -3002,7 +3945,7 @@ var Driver = class {
|
|
|
3002
3945
|
}
|
|
3003
3946
|
}
|
|
3004
3947
|
}
|
|
3005
|
-
|
|
3948
|
+
const record = {
|
|
3006
3949
|
kind: "resolution",
|
|
3007
3950
|
taskId,
|
|
3008
3951
|
resolvedAs,
|
|
@@ -3010,11 +3953,22 @@ var Driver = class {
|
|
|
3010
3953
|
error: resolvedAs === "failed" ? value.error ?? "resolved as failed" : null,
|
|
3011
3954
|
actor: value.actor,
|
|
3012
3955
|
observedAt: this.now()
|
|
3013
|
-
}
|
|
3956
|
+
};
|
|
3957
|
+
if (midStep) {
|
|
3958
|
+
this.pausePending.push({ kind: "answer", record, runId, nodeId, branchPath });
|
|
3959
|
+
return true;
|
|
3960
|
+
}
|
|
3961
|
+
try {
|
|
3962
|
+
this.appendResolution(record, nodeId, branchPath);
|
|
3963
|
+
} catch (e) {
|
|
3964
|
+
this.haltOnce(e);
|
|
3965
|
+
throw e;
|
|
3966
|
+
}
|
|
3967
|
+
if (this.shut()) return true;
|
|
3014
3968
|
try {
|
|
3015
3969
|
this.advance(runId, nodeId, branchPath);
|
|
3016
3970
|
} catch (e) {
|
|
3017
|
-
this.
|
|
3971
|
+
this.haltOnce(e);
|
|
3018
3972
|
throw e;
|
|
3019
3973
|
}
|
|
3020
3974
|
return true;
|
|
@@ -3027,6 +3981,7 @@ var Driver = class {
|
|
|
3027
3981
|
*/
|
|
3028
3982
|
appendResolution(record, nodeId, branchPath) {
|
|
3029
3983
|
this.append({ kind: "resolution", record });
|
|
3984
|
+
if (this.shut()) return;
|
|
3030
3985
|
const region = this.regionOf(nodeId);
|
|
3031
3986
|
if (region) {
|
|
3032
3987
|
this.emit({
|
|
@@ -3039,16 +3994,556 @@ var Driver = class {
|
|
|
3039
3994
|
});
|
|
3040
3995
|
}
|
|
3041
3996
|
}
|
|
3997
|
+
/**
|
|
3998
|
+
* **Cancel the run: hold it, record the request, and ask the scheduler to cancel each task in flight** — `RN-R31`,
|
|
3999
|
+
* `spec/run-state.md` section 13, [ADR 0104](../../doc/decisions/0104-a-run-its-owner-cancels-is-cancelling-until-its-last-task-has-stopped.md),
|
|
4000
|
+
* P34 decision 4 and stop-and-asks (b), (c), (i), (j). **Resolves once every task it cancelled has stopped**; the fold
|
|
4001
|
+
* reads the run `cancelling` until then and `cancelled` after (`RN-R30`), so a runtime's `done` settles after the last
|
|
4002
|
+
* `task.cancelled` from the fold alone.
|
|
4003
|
+
*
|
|
4004
|
+
* **In order, and each step is a rule:**
|
|
4005
|
+
* 1. **Refuse** — {@link RunControlRefused}, before anything is recorded or sent; and a `graceMs` the scheduler
|
|
4006
|
+
* would throw on is thrown here, as it would be (`TypeError`, `RangeError`).
|
|
4007
|
+
* 2. **Hold** (`RN-R29`): `cancelRequest` is set, so `shut()` is true for every door; every region's collection is
|
|
4008
|
+
* released, `failRun`'s run-level moment (ADR 0046 rule 3, `JP-R12`) — no join of this run settles again.
|
|
4009
|
+
* 3. **Record** ONE `cancelRequested` element, BEFORE the scheduler is asked for anything.
|
|
4010
|
+
* 4. **Cancel** each task in flight the scheduler holds — `RN-R21`'s set, so a PARKED task, settled already, is sent
|
|
4011
|
+
* nothing (P34 (i); the scheduler cannot end it, fact 8). A task not yet handed on is cancelled by `handOn` after
|
|
4012
|
+
* its hand-off (`RN-R29` (i)), and so is each task a PAUSE held (`RN-R35`, P34 (c)). Tasks a worker SPAWNED are
|
|
4013
|
+
* not this run's (P34 (j)). **In a `finally` around step 3** (`RN-R31` v0.21, P35 decision 4): a consumer's
|
|
4014
|
+
* `onFoldInput` that throws on the element still has every cancel sent and every held task handed on and
|
|
4015
|
+
* cancelled — and THEN the run is HALTED and reported once, and the throw reaches the caller (`RN-R31` v0.24,
|
|
4016
|
+
* `OR-R10` v0.19, P36 (b), W184).
|
|
4017
|
+
*
|
|
4018
|
+
* **A SECOND call** appends nothing and forwards its `graceMs` to each task still stopping — the scheduler's own
|
|
4019
|
+
* escalation — and resolves once those have stopped. It is never refused for the halt its own first call made.
|
|
4020
|
+
*
|
|
4021
|
+
* @throws RunControlRefused — `unsupported`, `not-started`, `halted`, `cancelled` or `ended`; nothing recorded or sent.
|
|
4022
|
+
* @throws TypeError / RangeError — `graceMs` not a number, or negative or `NaN`; nothing recorded or sent.
|
|
4023
|
+
*/
|
|
4024
|
+
cancel(options) {
|
|
4025
|
+
const cancelOf = this.opts.scheduler.cancel;
|
|
4026
|
+
if (typeof cancelOf !== "function") {
|
|
4027
|
+
throw new RunControlRefused(
|
|
4028
|
+
"cancel",
|
|
4029
|
+
"unsupported",
|
|
4030
|
+
`RunControlRefused (unsupported): run "${this.runId}"'s scheduler has no cancel(taskId, options) \u2014 SchedulerLike's optional third member (P34 (b), ADR 0104). A FyflowScheduler has one, and a runtime\`s per-run view forwards it; a scheduler double must declare it to cancel a run. Nothing was recorded and nothing was sent.`
|
|
4031
|
+
);
|
|
4032
|
+
}
|
|
4033
|
+
const graceMs = options?.graceMs;
|
|
4034
|
+
if (graceMs !== void 0) {
|
|
4035
|
+
if (typeof graceMs !== "number") throw new TypeError(`cancel: graceMs must be a number of ms, got ${typeof graceMs} \u2014 nothing was recorded`);
|
|
4036
|
+
if (Number.isNaN(graceMs) || graceMs < 0) throw new RangeError(`cancel: graceMs must be 0 or more, got ${graceMs} \u2014 nothing was recorded`);
|
|
4037
|
+
}
|
|
4038
|
+
if (this.header === null) {
|
|
4039
|
+
throw new RunControlRefused(
|
|
4040
|
+
"cancel",
|
|
4041
|
+
"not-started",
|
|
4042
|
+
`RunControlRefused (not-started): driver for run "${this.runId}" holds no run to cancel \u2014 it has not started, or its resume was refused and it is spent (RN-R23). Nothing was recorded and nothing was sent.`
|
|
4043
|
+
);
|
|
4044
|
+
}
|
|
4045
|
+
const req = this.cancelRequest;
|
|
4046
|
+
if (req !== null) {
|
|
4047
|
+
if (graceMs !== void 0) req.graceMs = graceMs;
|
|
4048
|
+
for (const id of this.inFlight()) if (this.handedOn(id)) this.cancelTask(id);
|
|
4049
|
+
const stopping2 = req.stopping;
|
|
4050
|
+
return Promise.resolve().then(() => Promise.all([...stopping2.values()])).then(() => void 0);
|
|
4051
|
+
}
|
|
4052
|
+
if (this.failed) {
|
|
4053
|
+
throw new RunControlRefused(
|
|
4054
|
+
"cancel",
|
|
4055
|
+
"halted",
|
|
4056
|
+
`RunControlRefused (halted): run "${this.runId}" was HALTED by a refusal (OR-R10) \u2014 it routes nothing again, and its outcome is halted. Nothing was recorded and nothing was sent.`
|
|
4057
|
+
);
|
|
4058
|
+
}
|
|
4059
|
+
if (this.cancelled) {
|
|
4060
|
+
throw new RunControlRefused(
|
|
4061
|
+
"cancel",
|
|
4062
|
+
"cancelled",
|
|
4063
|
+
`RunControlRefused (cancelled): run "${this.runId}" was already ended CANCELLED by a task the scheduler cancelled on its own (RN-R28). Cancel a task it still runs by its id. Nothing was recorded and nothing was sent.`
|
|
4064
|
+
);
|
|
4065
|
+
}
|
|
4066
|
+
if (this.stopRequest !== null) {
|
|
4067
|
+
throw new RunControlRefused(
|
|
4068
|
+
"cancel",
|
|
4069
|
+
"interrupted",
|
|
4070
|
+
`RunControlRefused (interrupted): run "${this.runId}" was STOPPED by its owner (RN-R33) \u2014 this driver is deaf to it, and its in-flight tasks are a resume\`s to run again. Resume its checkpoint on another runtime and cancel it there. Nothing was recorded and nothing was sent.`
|
|
4071
|
+
);
|
|
4072
|
+
}
|
|
4073
|
+
const inFlight = this.inFlight();
|
|
4074
|
+
if (this.stepping === 0 && inFlight.length === 0 && !this.hasParked()) {
|
|
4075
|
+
throw new RunControlRefused(
|
|
4076
|
+
"cancel",
|
|
4077
|
+
"ended",
|
|
4078
|
+
`RunControlRefused (ended): run "${this.runId}" has nothing in flight and nothing parked on a person \u2014 it has finished, and a cancel would rewrite its outcome. Nothing was recorded and nothing was sent.`
|
|
4079
|
+
);
|
|
4080
|
+
}
|
|
4081
|
+
const reason = options.reason;
|
|
4082
|
+
const stopping = /* @__PURE__ */ new Map();
|
|
4083
|
+
this.cancelRequest = {
|
|
4084
|
+
...reason !== void 0 ? { reason } : {},
|
|
4085
|
+
...graceMs !== void 0 ? { graceMs } : {},
|
|
4086
|
+
stopping
|
|
4087
|
+
};
|
|
4088
|
+
this.arrivals.clear();
|
|
4089
|
+
this.releaseAllItems();
|
|
4090
|
+
const released = new Map(this.pauseHeld);
|
|
4091
|
+
this.pauseHeld.clear();
|
|
4092
|
+
for (const id of released.keys()) this.unhanded.add(id);
|
|
4093
|
+
try {
|
|
4094
|
+
try {
|
|
4095
|
+
this.append({ kind: "cancelRequested", actor: options.actor, reason: reason ?? null });
|
|
4096
|
+
} finally {
|
|
4097
|
+
try {
|
|
4098
|
+
for (const id of this.inFlight()) {
|
|
4099
|
+
const config = released.get(id);
|
|
4100
|
+
if (config !== void 0) this.handOn(id, config);
|
|
4101
|
+
else if (this.handedOn(id)) this.cancelTask(id);
|
|
4102
|
+
}
|
|
4103
|
+
} finally {
|
|
4104
|
+
for (const id of released.keys()) this.unhanded.delete(id);
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
} catch (e) {
|
|
4108
|
+
this.haltOnce(e);
|
|
4109
|
+
throw e;
|
|
4110
|
+
}
|
|
4111
|
+
return Promise.resolve().then(() => Promise.all([...stopping.values()])).then(() => void 0);
|
|
4112
|
+
}
|
|
4113
|
+
/**
|
|
4114
|
+
* **Stop the run so that a resume ELSEWHERE can continue it: hold it, record the stop, go DEAF, and only then ask the
|
|
4115
|
+
* scheduler to cancel each task in flight** — `RN-R33`, `spec/run-state.md` section 14,
|
|
4116
|
+
* [ADR 0105](../../doc/decisions/0105-a-run-its-owner-stops-goes-deaf-before-it-cancels-and-resumes-elsewhere.md), P34
|
|
4117
|
+
* decisions 3 and 9 and stop-and-ask (d). **Resolves once every task it cancelled has stopped**; the fold reads the run
|
|
4118
|
+
* `interrupted` from the element on (`RN-R32`), and the log ends with the cancelled tasks dispatched and UNSETTLED —
|
|
4119
|
+
* the in-flight set a resume on another runtime re-dispatches at the same ids (`RN-R21`).
|
|
4120
|
+
*
|
|
4121
|
+
* **In order, and the ORDER is the rule** (P34 decision 3):
|
|
4122
|
+
* 1. **Refuse** — {@link RunControlRefused} with `control: 'stop'`, before anything is recorded or sent: `unsupported`,
|
|
4123
|
+
* `not-started`, `halted`, `cancelled` (`RN-R28`'s, or a cancel its owner requested — P34 (c)) and `ended` (at
|
|
4124
|
+
* rest only, as for a cancel); and a `graceMs` the scheduler would throw on is thrown here. From the accepted call
|
|
4125
|
+
* on, `resolve()` and `cancel()` are refused (P34 decision 9, (c)).
|
|
4126
|
+
* 2. **Hold** (`RN-R29`): the stop is TAKEN, so `shut()` is true for every door, and every region's collection is
|
|
4127
|
+
* released.
|
|
4128
|
+
* 3. **Go DEAF**: nothing the scheduler fires for this run is heard again — from BEFORE the element, so not even a
|
|
4129
|
+
* settlement a callback on it provokes — and no `task.cancelled` of the stop is appended.
|
|
4130
|
+
* 4. **Record** ONE `interrupted` element.
|
|
4131
|
+
* 5. **Cancel** each task in flight the scheduler holds — none for a parked task (P34 fact 4), and none for a task a
|
|
4132
|
+
* PAUSE held, which the scheduler was never handed and which stays in flight for the resume (`RN-R35`).
|
|
4133
|
+
*
|
|
4134
|
+
* **Steps 2-5 are taken at REST** — at once when the call is made at rest, and when the driver's own step ends when it
|
|
4135
|
+
* is made from a callback on that step (`stepping`). A hold taken mid-step would drop the rest of the step's routing
|
|
4136
|
+
* — a fan-out's later branches, a settlement's successor — and a resume re-dispatches only what is IN FLIGHT, so that
|
|
4137
|
+
* work would be lost to the resumed run for good. Taken at rest, the step is whole in the log; the task whose own
|
|
4138
|
+
* `dispatched` element carried the call is handed on in the step and cancelled here (`RN-R29` (i)), and so is a resumed
|
|
4139
|
+
* run's in-flight set, handed back before `resume()` returns.
|
|
4140
|
+
*
|
|
4141
|
+
* **A SECOND call** appends nothing, forwards its `graceMs` to each task still stopping, and resolves as the first does.
|
|
4142
|
+
*
|
|
4143
|
+
* @throws RunControlRefused — `unsupported`, `not-started`, `halted`, `cancelled` or `ended`; nothing recorded or sent.
|
|
4144
|
+
* @throws TypeError / RangeError — `graceMs` not a number, or negative or `NaN`; nothing recorded or sent.
|
|
4145
|
+
*/
|
|
4146
|
+
stop(options) {
|
|
4147
|
+
if (typeof this.opts.scheduler.cancel !== "function") {
|
|
4148
|
+
throw new RunControlRefused(
|
|
4149
|
+
"stop",
|
|
4150
|
+
"unsupported",
|
|
4151
|
+
`RunControlRefused (unsupported): run "${this.runId}"'s scheduler has no cancel(taskId, options) \u2014 SchedulerLike's optional third member (P34 (b), ADR 0105). A stop cancels each task in flight, so a scheduler double must declare it to stop a run. Nothing was recorded and nothing was sent.`
|
|
4152
|
+
);
|
|
4153
|
+
}
|
|
4154
|
+
const graceMs = options?.graceMs;
|
|
4155
|
+
if (graceMs !== void 0) {
|
|
4156
|
+
if (typeof graceMs !== "number") throw new TypeError(`stop: graceMs must be a number of ms, got ${typeof graceMs} \u2014 nothing was recorded`);
|
|
4157
|
+
if (Number.isNaN(graceMs) || graceMs < 0) throw new RangeError(`stop: graceMs must be 0 or more, got ${graceMs} \u2014 nothing was recorded`);
|
|
4158
|
+
}
|
|
4159
|
+
if (this.header === null) {
|
|
4160
|
+
throw new RunControlRefused(
|
|
4161
|
+
"stop",
|
|
4162
|
+
"not-started",
|
|
4163
|
+
`RunControlRefused (not-started): driver for run "${this.runId}" holds no run to stop \u2014 it has not started, or its resume was refused and it is spent (RN-R23). Nothing was recorded and nothing was sent.`
|
|
4164
|
+
);
|
|
4165
|
+
}
|
|
4166
|
+
const req = this.stopRequest;
|
|
4167
|
+
if (req !== null) {
|
|
4168
|
+
if (graceMs !== void 0) req.graceMs = graceMs;
|
|
4169
|
+
if (req.pending === null) {
|
|
4170
|
+
for (const id of this.inFlight()) if (this.handedOn(id) && req.stopping.has(id)) this.cancelTask(id);
|
|
4171
|
+
return this.stopSettled(req);
|
|
4172
|
+
}
|
|
4173
|
+
return req.took.then(() => this.stopSettled(req));
|
|
4174
|
+
}
|
|
4175
|
+
if (this.failed) {
|
|
4176
|
+
throw new RunControlRefused(
|
|
4177
|
+
"stop",
|
|
4178
|
+
"halted",
|
|
4179
|
+
`RunControlRefused (halted): run "${this.runId}" was HALTED by a refusal (OR-R10) \u2014 a halted run is not resumable (RN-R23), so there is nothing to stop for a resume elsewhere. Nothing was recorded and nothing was sent.`
|
|
4180
|
+
);
|
|
4181
|
+
}
|
|
4182
|
+
if (this.cancelled || this.cancelRequest !== null) {
|
|
4183
|
+
throw new RunControlRefused(
|
|
4184
|
+
"stop",
|
|
4185
|
+
"cancelled",
|
|
4186
|
+
`RunControlRefused (cancelled): run "${this.runId}" was CANCELLED${this.cancelRequest !== null ? " by its owner (RN-R31)" : " by a task the scheduler cancelled on its own (RN-R28)"} \u2014 a cancelled run is not resumable, so it cannot be stopped to resume it elsewhere (P34 (c)). Nothing was recorded and nothing was sent.`
|
|
4187
|
+
);
|
|
4188
|
+
}
|
|
4189
|
+
if (this.stepping === 0 && this.inFlight().length === 0 && !this.hasParked()) {
|
|
4190
|
+
throw new RunControlRefused(
|
|
4191
|
+
"stop",
|
|
4192
|
+
"ended",
|
|
4193
|
+
`RunControlRefused (ended): run "${this.runId}" has nothing in flight and nothing parked on a person \u2014 it has finished, and there is nothing to resume elsewhere. Nothing was recorded and nothing was sent.`
|
|
4194
|
+
);
|
|
4195
|
+
}
|
|
4196
|
+
const reason = options.reason;
|
|
4197
|
+
let taken;
|
|
4198
|
+
const took = new Promise((r) => {
|
|
4199
|
+
taken = r;
|
|
4200
|
+
});
|
|
4201
|
+
const accepted = {
|
|
4202
|
+
actor: options.actor,
|
|
4203
|
+
...reason !== void 0 ? { reason } : {},
|
|
4204
|
+
...graceMs !== void 0 ? { graceMs } : {},
|
|
4205
|
+
stopping: /* @__PURE__ */ new Map(),
|
|
4206
|
+
pending: taken,
|
|
4207
|
+
took
|
|
4208
|
+
};
|
|
4209
|
+
this.stopRequest = accepted;
|
|
4210
|
+
if (this.stepping === 0) this.takeStop();
|
|
4211
|
+
return took.then(() => this.stopSettled(accepted));
|
|
4212
|
+
}
|
|
4213
|
+
/**
|
|
4214
|
+
* **The stop, TAKEN** — `RN-R33` steps 2-5, at rest: from `stop()` itself, or from the end of the driver's own step
|
|
4215
|
+
* when `stop()` was called on it (each step's wrapper calls this when `stepping` returns to `0`).
|
|
4216
|
+
*/
|
|
4217
|
+
takeStop() {
|
|
4218
|
+
const req = this.stopRequest;
|
|
4219
|
+
const taken = req.pending;
|
|
4220
|
+
req.pending = null;
|
|
4221
|
+
req.took = null;
|
|
4222
|
+
this.arrivals.clear();
|
|
4223
|
+
this.releaseAllItems();
|
|
4224
|
+
this.deaf = true;
|
|
4225
|
+
const inFlight = this.inFlight().filter((id) => this.handedOn(id));
|
|
4226
|
+
const silent = this.failed;
|
|
4227
|
+
try {
|
|
4228
|
+
try {
|
|
4229
|
+
this.append({ kind: "interrupted", actor: req.actor, reason: req.reason ?? null });
|
|
4230
|
+
} finally {
|
|
4231
|
+
try {
|
|
4232
|
+
for (const id of inFlight) this.cancelTask(id);
|
|
4233
|
+
} finally {
|
|
4234
|
+
taken();
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
} catch (e) {
|
|
4238
|
+
if (silent) return;
|
|
4239
|
+
this.haltOnce(e);
|
|
4240
|
+
throw e;
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
/** What a stop's promise resolves on once it is taken: a microtask on, every task it sent a cancel to has stopped. */
|
|
4244
|
+
stopSettled(req) {
|
|
4245
|
+
return Promise.resolve().then(() => Promise.all([...req.stopping.values()])).then(() => void 0);
|
|
4246
|
+
}
|
|
4247
|
+
/**
|
|
4248
|
+
* **Pause the run: it routes on, and every dispatch it makes from here is appended and HELD until `unpause()`** —
|
|
4249
|
+
* `RN-R35`, `spec/run-state.md` section 15,
|
|
4250
|
+
* [ADR 0106](../../doc/decisions/0106-a-run-its-owner-pauses-routes-on-and-holds-every-dispatch-until-unpaused.md),
|
|
4251
|
+
* P34 stop-and-ask (e). Tasks already handed to the scheduler run on and settle, and their settlements route as ever; a
|
|
4252
|
+
* person's `resolve()` is accepted. **Not a halt** (`shut()` is untouched): only the scheduler's `addTask` waits. The
|
|
4253
|
+
* fold reads `RunState.paused` and no new outcome — a held task is `dispatched`, so it reads `running` (P34 fact 3).
|
|
4254
|
+
*
|
|
4255
|
+
* **In order**: (1) **refused by name** before anything is recorded — {@link RunControlRefused} with `control: 'pause'`:
|
|
4256
|
+
* `not-started`, `halted`, `cancelled` (`RN-R28`'s or a requested cancel), `interrupted` (a stop, taken or pending),
|
|
4257
|
+
* `paused` (already paused, or a pause called earlier in this step still to be taken — P34 (e): *a second pause is
|
|
4258
|
+
* refused by name*) and `ended` (nothing in flight and nothing parked, at rest only); (2) **TAKEN AT REST** — at once
|
|
4259
|
+
* when called at rest, and when the driver's own step ends when called from a callback on it: the run is paused and ONE
|
|
4260
|
+
* `paused` element is appended, BETWEEN two steps, where a resume's replay can feed it (`RN-R19`). A pause taken
|
|
4261
|
+
* mid-step would land between two elements of one step, and the replay — which regenerates a step whole — would refuse
|
|
4262
|
+
* its checkpoint `diverged` there; so the step a pause is called on finishes unpaused, its dispatches handed on.
|
|
4263
|
+
*/
|
|
4264
|
+
pause(options) {
|
|
4265
|
+
this.refuseRunHeld("pause");
|
|
4266
|
+
if (this.pauseWanted()) {
|
|
4267
|
+
throw new RunControlRefused(
|
|
4268
|
+
"pause",
|
|
4269
|
+
"paused",
|
|
4270
|
+
`RunControlRefused (paused): run "${this.runId}" is already paused${this.pausePending.some((c) => c.kind !== "answer") ? " \u2014 by a call earlier in this step, taken when it ends" : ""} (RN-R35, P34 (e)). Unpause it first. Nothing was recorded.`
|
|
4271
|
+
);
|
|
4272
|
+
}
|
|
4273
|
+
if (this.stepping === 0 && this.inFlight().length === 0 && !this.hasParked()) {
|
|
4274
|
+
throw new RunControlRefused(
|
|
4275
|
+
"pause",
|
|
4276
|
+
"ended",
|
|
4277
|
+
`RunControlRefused (ended): run "${this.runId}" has nothing in flight and nothing parked on a person \u2014 it has finished, and there is nothing left to hold. Nothing was recorded.`
|
|
4278
|
+
);
|
|
4279
|
+
}
|
|
4280
|
+
this.pausePending.push({ kind: "paused", actor: options.actor });
|
|
4281
|
+
while (this.stepping === 0 && this.pausePending.length > 0) this.takePending(this.pausePending.shift());
|
|
4282
|
+
}
|
|
4283
|
+
/**
|
|
4284
|
+
* **Unpause the run: record it, then hand every held task to the scheduler in DISPATCH ORDER** — `RN-R35`, ADR 0106,
|
|
4285
|
+
* P34 (e). Refused by name — `control: 'unpause'`: `not-started`, `halted`, `cancelled`, `interrupted` (a stopped
|
|
4286
|
+
* run's held tasks are a resume's: unpause it where it resumes) and `not-paused`. **Never `ended`**: an unpause rewrites
|
|
4287
|
+
* no outcome, so a paused run that finished may still be unpaused. Taken at rest, as a pause is: ONE `unpaused` element,
|
|
4288
|
+
* then the hand-off — each held task's `createTask` and `addTask`, first held first.
|
|
4289
|
+
*/
|
|
4290
|
+
unpause(options) {
|
|
4291
|
+
this.refuseRunHeld("unpause");
|
|
4292
|
+
if (!this.pauseWanted()) {
|
|
4293
|
+
throw new RunControlRefused(
|
|
4294
|
+
"unpause",
|
|
4295
|
+
"not-paused",
|
|
4296
|
+
`RunControlRefused (not-paused): run "${this.runId}" is not paused${this.pausePending.some((c) => c.kind !== "answer") ? " \u2014 an unpause earlier in this step already lifts it" : ""} (RN-R35, P34 (e)). Nothing was recorded and nothing was handed on.`
|
|
4297
|
+
);
|
|
4298
|
+
}
|
|
4299
|
+
this.pausePending.push({ kind: "unpaused", actor: options.actor });
|
|
4300
|
+
while (this.stepping === 0 && this.pausePending.length > 0) this.takePending(this.pausePending.shift());
|
|
4301
|
+
}
|
|
4302
|
+
/** The pause the owner has ASKED for: the last call still to be taken, else the pause taken (`RN-R35`). */
|
|
4303
|
+
pauseWanted() {
|
|
4304
|
+
const last = this.pausePending.filter((c) => c.kind !== "answer").at(-1);
|
|
4305
|
+
return last !== void 0 ? last.kind === "paused" : this.paused;
|
|
4306
|
+
}
|
|
4307
|
+
/**
|
|
4308
|
+
* The refusals a pause and an unpause share, in `RN-R35`'s order — each a run that is not this driver's to hold or
|
|
4309
|
+
* release: none started, or the run halted, cancelled or stopped. Thrown before anything is recorded.
|
|
4310
|
+
*/
|
|
4311
|
+
refuseRunHeld(control) {
|
|
4312
|
+
if (this.header === null) {
|
|
4313
|
+
throw new RunControlRefused(
|
|
4314
|
+
control,
|
|
4315
|
+
"not-started",
|
|
4316
|
+
`RunControlRefused (not-started): driver for run "${this.runId}" holds no run to ${control} \u2014 it has not started, or its resume was refused and it is spent (RN-R23). Nothing was recorded.`
|
|
4317
|
+
);
|
|
4318
|
+
}
|
|
4319
|
+
if (this.failed) {
|
|
4320
|
+
throw new RunControlRefused(
|
|
4321
|
+
control,
|
|
4322
|
+
"halted",
|
|
4323
|
+
`RunControlRefused (halted): run "${this.runId}" was HALTED by a refusal (OR-R10) \u2014 it dispatches nothing again, so there is nothing to ${control === "pause" ? "hold" : "hand on"}. Nothing was recorded.`
|
|
4324
|
+
);
|
|
4325
|
+
}
|
|
4326
|
+
if (this.cancelled || this.cancelRequest !== null) {
|
|
4327
|
+
throw new RunControlRefused(
|
|
4328
|
+
control,
|
|
4329
|
+
"cancelled",
|
|
4330
|
+
`RunControlRefused (cancelled): run "${this.runId}" was CANCELLED${this.cancelRequest !== null ? " by its owner (RN-R31)" : " by a task the scheduler cancelled on its own (RN-R28)"} \u2014 it dispatches nothing again, so there is nothing to ${control === "pause" ? "hold" : "hand on"}. Nothing was recorded.`
|
|
4331
|
+
);
|
|
4332
|
+
}
|
|
4333
|
+
if (this.stopRequest !== null) {
|
|
4334
|
+
throw new RunControlRefused(
|
|
4335
|
+
control,
|
|
4336
|
+
"interrupted",
|
|
4337
|
+
`RunControlRefused (interrupted): run "${this.runId}" was STOPPED by its owner (RN-R33) \u2014 its in-flight tasks, held ones included, are a resume\`s to run. Resume its checkpoint on another runtime and ${control} it there. Nothing was recorded.`
|
|
4338
|
+
);
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
/**
|
|
4342
|
+
* **A pause or an unpause, TAKEN** — `RN-R35`, at rest: from the call itself, or from the end of the driver's own step
|
|
4343
|
+
* (`leaveStep()`). **A step of its own** (`stepping`), so a call a consumer makes from a callback on the element — a
|
|
4344
|
+
* second pause, a stop, a `resolve()` that dispatches — is taken, or routes, as on any step. **A pause** sets the flag
|
|
4345
|
+
* BEFORE its element, so a dispatch a callback on the element causes is held. **An unpause** clears it before its
|
|
4346
|
+
* element and hands the held tasks on AFTER it, in a `finally`: a consumer's `onFoldInput` that throws on the element
|
|
4347
|
+
* still has the element in the log, and the run it says is unpaused is (W174's conformity reader's finding 5, the stop's
|
|
4348
|
+
* lesson) — and then the take HALTS the run itself, reported once (`takePause`, `RN-R35` v0.24, W184), so a call pending
|
|
4349
|
+
* behind it is dropped, and the throw reaches whoever is on the stack: the caller of the call at rest or of the driver
|
|
4350
|
+
* call mid-step, or, on a SCHEDULER EVENT's step, the listener boundary (`onEvent`, P35 (j)). A dispatch a callback on the element causes joins the end
|
|
4351
|
+
* of the held queue (`dispatch`), so the order is the dispatch order still.
|
|
4352
|
+
*/
|
|
4353
|
+
takePending(c) {
|
|
4354
|
+
if (c.kind === "answer") this.takeAnswer(c);
|
|
4355
|
+
else this.takePause(c);
|
|
4356
|
+
}
|
|
4357
|
+
/**
|
|
4358
|
+
* **A resolution given mid-step, TAKEN at rest** — `RN-R37`, W180 (the conformity reader's R4; S-4 for every
|
|
4359
|
+
* resolution): a step of its own, so the resolution lands BETWEEN two steps — where a resume's replay FEEDS it
|
|
4360
|
+
* (`RN-R19`) — after the step that parked its node has completed (an ask's `asked` or a task's `user_action`
|
|
4361
|
+
* settlement, and in a region `branch.suspended`). Answered on its own `asked` element and appended
|
|
4362
|
+
* there instead, it sat between two elements of one step, and the replay — which regenerates a step whole — refused the
|
|
4363
|
+
* run's checkpoint `diverged` at it. **Dropped, recording nothing, when the step it was given on HALTED or CANCELLED the
|
|
4364
|
+
* run** (or the node is no longer parked) — `takePause`'s rule: it would be refused were it given now. It routes as
|
|
4365
|
+
* `resolve()` at rest routes: a throw — from routing, or a consumer's on the resolution's own element — HALTS the run
|
|
4366
|
+
* here, reported once, so a call pending behind it is dropped; it then reaches whoever is on the stack: a driver call's
|
|
4367
|
+
* caller, or the listener boundary of a scheduler event's step (`onEvent`, `OR-R10` v0.19, W184).
|
|
4368
|
+
*/
|
|
4369
|
+
takeAnswer(c) {
|
|
4370
|
+
if (this.failed || this.cancelled || this.cancelRequest !== null || !this.parked(c.record.taskId)) return;
|
|
4371
|
+
this.stepping += 1;
|
|
4372
|
+
try {
|
|
4373
|
+
try {
|
|
4374
|
+
this.appendResolution(c.record, c.nodeId, c.branchPath);
|
|
4375
|
+
if (this.shut()) return;
|
|
4376
|
+
this.advance(c.runId, c.nodeId, c.branchPath);
|
|
4377
|
+
} catch (e) {
|
|
4378
|
+
this.haltOnce(e);
|
|
4379
|
+
throw e;
|
|
4380
|
+
}
|
|
4381
|
+
} finally {
|
|
4382
|
+
this.leaveStep();
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
takePause(p) {
|
|
4386
|
+
if (this.failed || this.cancelled || this.cancelRequest !== null) return;
|
|
4387
|
+
this.stepping += 1;
|
|
4388
|
+
try {
|
|
4389
|
+
try {
|
|
4390
|
+
if (p.kind === "paused") {
|
|
4391
|
+
this.paused = true;
|
|
4392
|
+
this.append({ kind: "paused", actor: p.actor });
|
|
4393
|
+
} else {
|
|
4394
|
+
this.paused = false;
|
|
4395
|
+
try {
|
|
4396
|
+
this.append({ kind: "unpaused", actor: p.actor });
|
|
4397
|
+
} finally {
|
|
4398
|
+
this.drainHeld();
|
|
4399
|
+
}
|
|
4400
|
+
}
|
|
4401
|
+
} catch (e) {
|
|
4402
|
+
this.haltOnce(e);
|
|
4403
|
+
throw e;
|
|
4404
|
+
}
|
|
4405
|
+
} finally {
|
|
4406
|
+
this.leaveStep();
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
/**
|
|
4410
|
+
* **The unpause's hand-off**: each held task to the scheduler, first held first (`RN-R35`). Taken out of the hold and
|
|
4411
|
+
* marked `unhanded` before its `addTask`, as a resume's re-dispatch is (`RN-R31`), so a cancel raised inside one
|
|
4412
|
+
* hand-off sends nothing for a task not yet handed — and `handOn` then cancels it. **Held for a HALT** — checked before
|
|
4413
|
+
* each, as a resume's re-dispatch is (`RN-R29` (4), `holdsResume()`): a halt raised inside one hand-off hands nothing
|
|
4414
|
+
* further on, and what is left is never handed. A dispatch made on this stack joins the end of the queue and is handed
|
|
4415
|
+
* on in its turn (the `Map` visits what is added while it is walked). Caller-initiated, so a throw from the scheduler
|
|
4416
|
+
* is reported AND rethrown, `start()`'s and `resume()`'s treatment.
|
|
4417
|
+
*/
|
|
4418
|
+
drainHeld() {
|
|
4419
|
+
try {
|
|
4420
|
+
for (const [id, config] of this.pauseHeld) {
|
|
4421
|
+
if (this.holdsResume()) break;
|
|
4422
|
+
this.pauseHeld.delete(id);
|
|
4423
|
+
this.unhanded.add(id);
|
|
4424
|
+
this.handOn(id, config);
|
|
4425
|
+
}
|
|
4426
|
+
} catch (e) {
|
|
4427
|
+
this.failRun(e);
|
|
4428
|
+
throw e;
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
/**
|
|
4432
|
+
* Whether the scheduler holds this task — dispatched, not held by a replay, past its `addTask` (`RN-R31`), and not held
|
|
4433
|
+
* by a pause (`RN-R35`, W175): a stop of a paused run cancels only what this says the scheduler holds (ADR 0105's last
|
|
4434
|
+
* consequence), and a cancel hands each held task on before it cancels it.
|
|
4435
|
+
*/
|
|
4436
|
+
handedOn(taskId) {
|
|
4437
|
+
return this.replay === null && !this.unhanded.has(taskId) && !this.pauseHeld.has(taskId);
|
|
4438
|
+
}
|
|
3042
4439
|
// --- the scheduler's side ----------------------------------------------------
|
|
3043
4440
|
onEvent(name, detail) {
|
|
4441
|
+
this.stepping += 1;
|
|
4442
|
+
try {
|
|
4443
|
+
this.onEventStep(name, detail);
|
|
4444
|
+
} finally {
|
|
4445
|
+
try {
|
|
4446
|
+
this.leaveStep();
|
|
4447
|
+
} catch (e) {
|
|
4448
|
+
try {
|
|
4449
|
+
if (!this.failed) this.failRun(e);
|
|
4450
|
+
} catch {
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
}
|
|
4455
|
+
/** `onEvent`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
|
|
4456
|
+
onEventStep(name, detail) {
|
|
3044
4457
|
if (this.deaf || this.started && this.header === null) return;
|
|
3045
4458
|
try {
|
|
3046
4459
|
this.onEventOrThrow(name, detail);
|
|
3047
4460
|
} catch (e) {
|
|
3048
|
-
|
|
4461
|
+
try {
|
|
4462
|
+
this.haltOnce(e);
|
|
4463
|
+
} catch {
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
/**
|
|
4468
|
+
* **A `task.running`: an attempt started** — `RN-R40`, W186, P36 (f). Its id and number are COPIED here, while the
|
|
4469
|
+
* listener runs (`OR-R1`). Recorded only for a task THIS run dispatched, and only when the number EXCEEDS the last one
|
|
4470
|
+
* recorded for it — a requeue repeats its number and appends nothing (the scheduler's own recipe's `max`). **Everything
|
|
4471
|
+
* else is dropped in SILENCE, and none of it is a refusal**: an id that does not parse (a plain task on a shared
|
|
4472
|
+
* scheduler — its SETTLEMENT is what `onUnmintedTask` reports, `ID-R5`), another run's (`ID-R4` tier 1), one this run
|
|
4473
|
+
* never dispatched (its settlement is `ID-R4` tier 2's `UndispatchedTask`), and a detail carrying no positive integer
|
|
4474
|
+
* `attemptNumber` (a scheduler older than the pin, or a double). An attempt start never stops a run by existing. **Queued, not
|
|
4475
|
+
* appended**: {@link Driver.takeAttempts} appends it when the step it was heard on ends — at once, on a scheduler
|
|
4476
|
+
* event's own step; at the step's end when an inline pool fires it inside the driver's `addTask`.
|
|
4477
|
+
*/
|
|
4478
|
+
heardRunning(detail) {
|
|
4479
|
+
const taskId = detail?.id;
|
|
4480
|
+
const attemptNumber = attemptNumberOf(detail);
|
|
4481
|
+
if (typeof taskId !== "string" || attemptNumber === null) return;
|
|
4482
|
+
let runId;
|
|
4483
|
+
try {
|
|
4484
|
+
runId = parseTaskId(taskId).runId;
|
|
4485
|
+
} catch (e) {
|
|
4486
|
+
if (e instanceof InvalidTaskId) return;
|
|
4487
|
+
throw e;
|
|
3049
4488
|
}
|
|
4489
|
+
if (runId !== this.runId || !this.dispatched.has(taskId)) return;
|
|
4490
|
+
if (attemptNumber <= (this.lastAttempt.get(taskId) ?? 0)) return;
|
|
4491
|
+
this.lastAttempt.set(taskId, attemptNumber);
|
|
4492
|
+
this.attemptsPending.push({ taskId, attemptNumber });
|
|
3050
4493
|
}
|
|
3051
|
-
/**
|
|
4494
|
+
/**
|
|
4495
|
+
* **A `task.message` or a `task.progress`: PASSED THROUGH to its callback** — `RN-R42`, W187, P36 (k), ADR 0114. The
|
|
4496
|
+
* task's id is the detail's `taskId` for a message — `task.message` carries no `id` (P36 fact 2) — and its `id` for a
|
|
4497
|
+
* progress report, whose detail is the task spread flat. Handed on only for a task THIS run dispatched (`ID-R4`'s two
|
|
4498
|
+
* tiers, asked SILENTLY, as {@link Driver.heardRunning} asks them): anything else is dropped and is not a refusal. Every
|
|
4499
|
+
* field is COPIED here, while the listener runs (ADR 0007 rule 1, `OR-R1`), and the copy FROZEN (`OR-R7`); a value that
|
|
4500
|
+
* cannot be cloned is handed on `undefined` — the scheduler's own treatment of an uncloneable `emit` (its §6) — so a
|
|
4501
|
+
* pass-through never stops a run by existing. **The consumer's throw is NOT caught here**: it leaves through
|
|
4502
|
+
* `onEventOrThrow` to the listener boundary, which reports it and halts the run (`onUnmintedTask`'s road, `OR-R10`).
|
|
4503
|
+
*/
|
|
4504
|
+
heardPassThrough(name, detail) {
|
|
4505
|
+
const isMessage = name === "task.message";
|
|
4506
|
+
const taskId = isMessage ? detail?.taskId : detail?.id;
|
|
4507
|
+
if (typeof taskId !== "string" || detail === void 0) return;
|
|
4508
|
+
let parts;
|
|
4509
|
+
try {
|
|
4510
|
+
parts = parseTaskId(taskId);
|
|
4511
|
+
} catch (e) {
|
|
4512
|
+
if (e instanceof InvalidTaskId) return;
|
|
4513
|
+
throw e;
|
|
4514
|
+
}
|
|
4515
|
+
if (parts.runId !== this.runId || !this.dispatched.has(taskId)) return;
|
|
4516
|
+
const attribution = {
|
|
4517
|
+
runId: parts.runId,
|
|
4518
|
+
nodeId: parts.nodeId,
|
|
4519
|
+
branchPath: parts.branchPath,
|
|
4520
|
+
taskId,
|
|
4521
|
+
flowAttempt: parts.attempt,
|
|
4522
|
+
attemptNumber: attemptNumberOf(detail)
|
|
4523
|
+
};
|
|
4524
|
+
if (isMessage) {
|
|
4525
|
+
const m = {
|
|
4526
|
+
...attribution,
|
|
4527
|
+
name: detail.name,
|
|
4528
|
+
data: frozenCopy(detail.data),
|
|
4529
|
+
seq: detail.seq,
|
|
4530
|
+
timestamp: detail.timestamp
|
|
4531
|
+
};
|
|
4532
|
+
if (detail.dropped !== void 0) m.dropped = detail.dropped;
|
|
4533
|
+
if (detail.error !== void 0) m.error = detail.error;
|
|
4534
|
+
this.opts.onTaskMessage?.(Object.freeze(m));
|
|
4535
|
+
return;
|
|
4536
|
+
}
|
|
4537
|
+
const p = {
|
|
4538
|
+
...attribution,
|
|
4539
|
+
progress: detail.progress,
|
|
4540
|
+
message: frozenCopy(detail.message),
|
|
4541
|
+
details: frozenCopy(detail.details),
|
|
4542
|
+
timestamp: detail.timestamp
|
|
4543
|
+
};
|
|
4544
|
+
this.opts.onTaskProgress?.(Object.freeze(p));
|
|
4545
|
+
}
|
|
4546
|
+
/** Whether this run has been HALTED by a refusal (`OR-R10`; its outcome `halted`). */
|
|
3052
4547
|
hasFailed() {
|
|
3053
4548
|
return this.failed;
|
|
3054
4549
|
}
|
|
@@ -3057,19 +4552,36 @@ var Driver = class {
|
|
|
3057
4552
|
this.failed = true;
|
|
3058
4553
|
const err = cause instanceof Error ? cause : new Error(String(cause));
|
|
3059
4554
|
let seq = null;
|
|
4555
|
+
const report = () => {
|
|
4556
|
+
if (this.replay !== null && (seq === null || !this.delivers(seq))) return;
|
|
4557
|
+
this.opts.onRunFailure?.({
|
|
4558
|
+
runId: this.runId,
|
|
4559
|
+
kind: err.constructor?.name ?? "Error",
|
|
4560
|
+
message: err.message,
|
|
4561
|
+
cause
|
|
4562
|
+
});
|
|
4563
|
+
};
|
|
3060
4564
|
if (!already) {
|
|
3061
|
-
|
|
3062
|
-
|
|
4565
|
+
try {
|
|
4566
|
+
seq = this.append({ kind: "halted", reason: `${err.constructor?.name ?? "Error"}: ${err.message}` });
|
|
4567
|
+
} finally {
|
|
4568
|
+
this.arrivals.clear();
|
|
4569
|
+
this.releaseAllItems();
|
|
4570
|
+
report();
|
|
4571
|
+
}
|
|
4572
|
+
return;
|
|
3063
4573
|
}
|
|
3064
|
-
|
|
3065
|
-
this.opts.onRunFailure?.({
|
|
3066
|
-
runId: this.runId,
|
|
3067
|
-
kind: err.constructor?.name ?? "Error",
|
|
3068
|
-
message: err.message,
|
|
3069
|
-
cause
|
|
3070
|
-
});
|
|
4574
|
+
report();
|
|
3071
4575
|
}
|
|
3072
4576
|
onEventOrThrow(name, detail) {
|
|
4577
|
+
if (name === "task.running") {
|
|
4578
|
+
this.heardRunning(detail);
|
|
4579
|
+
return;
|
|
4580
|
+
}
|
|
4581
|
+
if (name === "task.message" || name === "task.progress") {
|
|
4582
|
+
this.heardPassThrough(name, detail);
|
|
4583
|
+
return;
|
|
4584
|
+
}
|
|
3073
4585
|
const taskId = detail?.id;
|
|
3074
4586
|
let parts;
|
|
3075
4587
|
try {
|
|
@@ -3102,7 +4614,13 @@ var Driver = class {
|
|
|
3102
4614
|
const seq = this.append({ kind: "outcome", record });
|
|
3103
4615
|
const report = this.opts.onNonconformingResult;
|
|
3104
4616
|
if (report !== void 0 && this.delivers(seq)) this.reportResultSchema(record, nodeId, report);
|
|
3105
|
-
if (this.
|
|
4617
|
+
if (this.shut()) return;
|
|
4618
|
+
if (record.terminalState === "cancelled") {
|
|
4619
|
+
this.cancelled = true;
|
|
4620
|
+
this.arrivals.clear();
|
|
4621
|
+
this.releaseAllItems();
|
|
4622
|
+
return;
|
|
4623
|
+
}
|
|
3106
4624
|
if (record.terminalState === "user_action") {
|
|
3107
4625
|
const region = this.regionOf(nodeId);
|
|
3108
4626
|
if (region) {
|
|
@@ -3222,9 +4740,10 @@ var Driver = class {
|
|
|
3222
4740
|
* through within this turn; only a `task` becomes a scheduler task.
|
|
3223
4741
|
*/
|
|
3224
4742
|
enter(runId, nodeId, payload, fromBranch = "-", fromRegion, itemIndices = EMPTY_INDICES) {
|
|
4743
|
+
if (this.shut()) return;
|
|
3225
4744
|
const node = this.graph.byId.get(nodeId);
|
|
3226
4745
|
if (!node) return;
|
|
3227
|
-
const received = this.compose(node, payload, itemIndices);
|
|
4746
|
+
const received = this.compose(node, payload, itemIndices, this.runPayload(), () => this.itemOf(node.id, itemIndices));
|
|
3228
4747
|
switch (node.kind) {
|
|
3229
4748
|
case "task":
|
|
3230
4749
|
return this.dispatch(runId, node, received, itemIndices);
|
|
@@ -3297,6 +4816,12 @@ var Driver = class {
|
|
|
3297
4816
|
if (branch === void 0) return;
|
|
3298
4817
|
const idx = items.map((_v, i) => withIndex(itemIndices, nodeId, i));
|
|
3299
4818
|
const paths = idx.map((m) => this.pathOf(branch, m));
|
|
4819
|
+
const kept = this.items.kept.get(nodeId);
|
|
4820
|
+
if (kept !== void 0 && items.length > 0) {
|
|
4821
|
+
const byPath = this.items.held.get(nodeId) ?? /* @__PURE__ */ new Map();
|
|
4822
|
+
this.items.held.set(nodeId, byPath);
|
|
4823
|
+
items.forEach((v, i) => byPath.set(paths[i], kept ? cloneValue(v) : v));
|
|
4824
|
+
}
|
|
3300
4825
|
for (const p of paths) {
|
|
3301
4826
|
this.emit({
|
|
3302
4827
|
type: "branch.opened",
|
|
@@ -3372,14 +4897,14 @@ var Driver = class {
|
|
|
3372
4897
|
case "gate": {
|
|
3373
4898
|
const decided = this.decideGate(node, received, this.memoryOf(nodeId));
|
|
3374
4899
|
if ("threw" in decided) throw decided.threw;
|
|
3375
|
-
const
|
|
4900
|
+
const set = decided.set;
|
|
3376
4901
|
if (!decided.declared) {
|
|
3377
4902
|
throw new UndeclaredRoute(
|
|
3378
|
-
`FD-R5 \u2014 run "${runId}" reached gate "${nodeId}" and its type "${node.type}" returned ${describeRoute(decided.route)}, which it does not declare; its routes are ${
|
|
4903
|
+
`FD-R5 \u2014 run "${runId}" reached gate "${nodeId}" and its type "${node.type}" returned ${describeRoute(decided.route)}, which it does not declare; its routes are ${set.routes.map((r) => JSON.stringify(r)).join(", ")}. A route only running the type reveals cannot be refused at load (FD-V41 wires every DECLARED route), so the run halts here rather than routing nowhere. The type is the CODE registered under that name, not the document.`
|
|
3379
4904
|
);
|
|
3380
4905
|
}
|
|
3381
4906
|
const route = decided.route;
|
|
3382
|
-
const edge = gateEdge(node,
|
|
4907
|
+
const edge = gateEdge(node, set.defaultRoute, route);
|
|
3383
4908
|
if (edge === null) {
|
|
3384
4909
|
throw new InvalidDefinition(["FD-V41"], `gate "${nodeId}" has no edge for route ${JSON.stringify(route)} \u2014 FD-V41 should have refused this definition at construction`);
|
|
3385
4910
|
}
|
|
@@ -3389,6 +4914,12 @@ var Driver = class {
|
|
|
3389
4914
|
this.enter(runId, edge.to, received, fromBranch, fromRegion, itemIndices);
|
|
3390
4915
|
return;
|
|
3391
4916
|
}
|
|
4917
|
+
// **An ASK PARKS, and nothing is handed to the scheduler** — `RN-R37`, v0.22,
|
|
4918
|
+
// [ADR 0109](../../doc/decisions/0109-an-ask-parks-with-nothing-dispatched-and-its-answer-is-its-result.md),
|
|
4919
|
+
// [W180](../../doc/work-items/w180-asking-and-answering-in-a-run.md), P35 (b)-(f). It replaces ADR 0108 rule 8's
|
|
4920
|
+
// interim refusal at this arm. See `ask()`.
|
|
4921
|
+
case "ask":
|
|
4922
|
+
return this.ask(runId, node, received, itemIndices);
|
|
3392
4923
|
// **A node kind this driver cannot execute REFUSES rather than returning.**
|
|
3393
4924
|
// Added by [W32](../../doc/work-items/w32-what-a-map-fanout-is.md) as a
|
|
3394
4925
|
// declared late exception, after its conformity reader measured what the
|
|
@@ -3487,8 +5018,9 @@ var Driver = class {
|
|
|
3487
5018
|
} catch (threw) {
|
|
3488
5019
|
return { threw };
|
|
3489
5020
|
}
|
|
3490
|
-
const
|
|
3491
|
-
|
|
5021
|
+
const set = this.gateRoutes.get(node.id);
|
|
5022
|
+
const declared = typeof route === "string" && set.routes.includes(route);
|
|
5023
|
+
return { set, route, declared, key: recorded };
|
|
3492
5024
|
}
|
|
3493
5025
|
/**
|
|
3494
5026
|
* **`FD-R4`'s road through an ENTRY gate: the edge the run will take, and no other** — W93, Outputs (9).
|
|
@@ -3501,7 +5033,7 @@ var Driver = class {
|
|
|
3501
5033
|
boundaryEdges(gate, value) {
|
|
3502
5034
|
const decided = this.decideGate(gate, value, /* @__PURE__ */ new Map());
|
|
3503
5035
|
if ("threw" in decided || !decided.declared) return [];
|
|
3504
|
-
const edge = gateEdge(gate, decided.
|
|
5036
|
+
const edge = gateEdge(gate, decided.set.defaultRoute, decided.route);
|
|
3505
5037
|
return edge === null ? [] : [edge];
|
|
3506
5038
|
}
|
|
3507
5039
|
/**
|
|
@@ -3585,21 +5117,32 @@ var Driver = class {
|
|
|
3585
5117
|
* the edge carried, so the composed object is the whole payload and what the
|
|
3586
5118
|
* worker gets is what the document says, character for character.
|
|
3587
5119
|
*
|
|
5120
|
+
* **`item` is a THUNK, read only for a `"$item"` key** (v0.45, P38 (e)): in a run it reads the instance's held item
|
|
5121
|
+
* (`itemOf`), at `FD-R4` it is the walk's own value — and a node declaring no `"$item"` never asks.
|
|
5122
|
+
*
|
|
3588
5123
|
* @throws UnresolvedReference — `FD-R3`.
|
|
3589
5124
|
*/
|
|
3590
|
-
compose(node, edgeValue, itemIndices) {
|
|
5125
|
+
compose(node, edgeValue, itemIndices, runPayload, item) {
|
|
3591
5126
|
const needs = node.needs;
|
|
3592
5127
|
if (!needs) return edgeValue;
|
|
3593
|
-
|
|
3594
|
-
const produced = this.outputs();
|
|
5128
|
+
let lookup = null;
|
|
3595
5129
|
const out = {};
|
|
3596
5130
|
for (const [key, ref] of Object.entries(needs)) {
|
|
3597
5131
|
const t = referenceTarget(ref);
|
|
3598
|
-
if (t.
|
|
5132
|
+
if (t.form === "self") {
|
|
3599
5133
|
out[key] = cloneValue(edgeValue);
|
|
3600
5134
|
continue;
|
|
3601
5135
|
}
|
|
3602
|
-
|
|
5136
|
+
if (t.form === "payload") {
|
|
5137
|
+
out[key] = cloneValue(runPayload);
|
|
5138
|
+
continue;
|
|
5139
|
+
}
|
|
5140
|
+
if (t.form === "item") {
|
|
5141
|
+
out[key] = cloneValue(item());
|
|
5142
|
+
continue;
|
|
5143
|
+
}
|
|
5144
|
+
lookup ??= { here: this.pathOf(node.id, itemIndices), produced: this.outputs() };
|
|
5145
|
+
const v = this.resolveReference(node.id, lookup.here, lookup.produced, key, t, "needs");
|
|
3603
5146
|
if (v !== ABSENT) out[key] = v;
|
|
3604
5147
|
}
|
|
3605
5148
|
return out;
|
|
@@ -3632,6 +5175,35 @@ var Driver = class {
|
|
|
3632
5175
|
}
|
|
3633
5176
|
return t.form === "all" ? visible.map((e) => cloneValue(e.result)) : cloneValue(visible[0].result);
|
|
3634
5177
|
}
|
|
5178
|
+
/**
|
|
5179
|
+
* **The value the run ENTERED with — the header's copy** (`RN-R15`, P38 fact 4 and (f)): taken at `start()` from the
|
|
5180
|
+
* caller's payload, or on a resume from the stored header, and held for the life of the run. What a `"$payload"`
|
|
5181
|
+
* reference composes from, in both doors; `FD-R4` hands the caller's own value instead, because it runs before the
|
|
5182
|
+
* header exists. Read only where a node or an edge composes the token — a run declaring none never clones it
|
|
5183
|
+
* (P38 decision 2). The header is set before the first `enter` in both doors, so `null` here is not a run.
|
|
5184
|
+
*/
|
|
5185
|
+
runPayload() {
|
|
5186
|
+
return this.header?.entryPayload;
|
|
5187
|
+
}
|
|
5188
|
+
/**
|
|
5189
|
+
* **The item of the map instance `nodeId` is entered in** — `"$item"`'s source in a run (v0.45, P38 (c), (e)): the
|
|
5190
|
+
* innermost map containing the node (`innermostMapOf`, the walk `FD-V49` checked), and the instance its `itemIndices`
|
|
5191
|
+
* name — the branch path that map's branch entry has in it. **By reference to the held item, uncloned**: the caller
|
|
5192
|
+
* clones, per key. Read only where a node or an edge declares `"$item"`.
|
|
5193
|
+
*
|
|
5194
|
+
* **A miss THROWS a plain `Error` naming the invariant**: a node is entered only while its instance is live, and an
|
|
5195
|
+
* instance's item is released only once nothing of it may still be entered (`trackItems`), so a reader with nothing held is a driver
|
|
5196
|
+
* defect — reported as a halt (`OR-R10`) rather than handed `undefined`, which an item may legitimately be.
|
|
5197
|
+
*/
|
|
5198
|
+
itemOf(nodeId, itemIndices) {
|
|
5199
|
+
const m = innermostMapOf(this.regions, nodeId);
|
|
5200
|
+
const path = this.pathOf(m.branches[0], itemIndices);
|
|
5201
|
+
const byPath = this.items.held.get(m.fanoutId);
|
|
5202
|
+
if (byPath === void 0 || !byPath.has(path)) {
|
|
5203
|
+
throw new Error(`node "${nodeId}" reads "$item" of map "${m.fanoutId}" instance "${path}", and that instance holds no item \u2014 an instance keeps its item until nothing of it may still be entered (RN-R13, ADR 0118); this is a defect in the driver`);
|
|
5204
|
+
}
|
|
5205
|
+
return byPath.get(path);
|
|
5206
|
+
}
|
|
3635
5207
|
/**
|
|
3636
5208
|
* **What a decision sees, beyond its node's outcome** — `FD-C3`, and
|
|
3637
5209
|
* [ADR 0041](../../doc/decisions/0041-a-decision-sees-its-nodes-outcome-and-whatever-its-own-edge-declares.md).
|
|
@@ -3642,7 +5214,10 @@ var Driver = class {
|
|
|
3642
5214
|
*
|
|
3643
5215
|
* **No `"."`, and `FD-V26` refuses it on an edge**: *what this predicate would have
|
|
3644
5216
|
* received anyway* is `ctx.outcome`, which it already has. So there is no
|
|
3645
|
-
* `edgeValue` parameter here and none is derivable — see the call site.
|
|
5217
|
+
* `edgeValue` parameter here and none is derivable — see the call site. **A
|
|
5218
|
+
* `"$payload"` key needs none either** (v0.44, ADR 0117): it is not derived from what
|
|
5219
|
+
* the NODE received but read from the run's header, `runPayload()` above, its own
|
|
5220
|
+
* copy per key per read.
|
|
3646
5221
|
*
|
|
3647
5222
|
* @throws UnresolvedReference — `FD-R3`, from the same expression a node's does.
|
|
3648
5223
|
*/
|
|
@@ -3654,6 +5229,17 @@ var Driver = class {
|
|
|
3654
5229
|
const out = {};
|
|
3655
5230
|
for (const [key, ref] of Object.entries(needs)) {
|
|
3656
5231
|
const t = referenceTarget(ref);
|
|
5232
|
+
if (t.form === "payload") {
|
|
5233
|
+
out[key] = cloneValue(this.runPayload());
|
|
5234
|
+
continue;
|
|
5235
|
+
}
|
|
5236
|
+
if (t.form === "item") {
|
|
5237
|
+
out[key] = cloneValue(this.itemOf(node.id, itemIndices));
|
|
5238
|
+
continue;
|
|
5239
|
+
}
|
|
5240
|
+
if (t.form === "self") {
|
|
5241
|
+
throw new Error(`node "${node.id}"'s edge declares needs[${JSON.stringify(key)}] = ${JSON.stringify(ref)}, which FD-V26 refuses on an edge`);
|
|
5242
|
+
}
|
|
3657
5243
|
const v = this.resolveReference(node.id, here, produced, key, t, "edge needs");
|
|
3658
5244
|
if (v !== ABSENT) out[key] = v;
|
|
3659
5245
|
}
|
|
@@ -3668,11 +5254,13 @@ var Driver = class {
|
|
|
3668
5254
|
* graph, and there is no longer a carried value that could disagree with it.
|
|
3669
5255
|
*/
|
|
3670
5256
|
dispatch(runId, node, payload, itemIndices = EMPTY_INDICES) {
|
|
5257
|
+
if (this.shut()) return;
|
|
3671
5258
|
const taskId = buildTaskId(runId, node.id, branchIndices(this.pathOf(node.id, itemIndices)), 0);
|
|
3672
5259
|
if (this.dispatched.has(taskId)) {
|
|
3673
5260
|
throw new DuplicateTaskId(`"${taskId}" was already dispatched in run "${runId}"`);
|
|
3674
5261
|
}
|
|
3675
5262
|
this.dispatched.add(taskId);
|
|
5263
|
+
if (this.replay === null) this.unhanded.add(taskId);
|
|
3676
5264
|
this.append({ kind: "dispatched", taskId });
|
|
3677
5265
|
const type = this.nodeTypes.get(node.type);
|
|
3678
5266
|
const retryPolicy = node.retryPolicy ?? type.retryPolicy ?? this.opts.defaultRetryPolicy;
|
|
@@ -3688,7 +5276,108 @@ var Driver = class {
|
|
|
3688
5276
|
this.replay.held.set(taskId, config);
|
|
3689
5277
|
return;
|
|
3690
5278
|
}
|
|
3691
|
-
|
|
5279
|
+
if ((this.paused || this.pauseHeld.size > 0) && !this.shut()) {
|
|
5280
|
+
this.pauseHeld.set(taskId, config);
|
|
5281
|
+
this.unhanded.delete(taskId);
|
|
5282
|
+
return;
|
|
5283
|
+
}
|
|
5284
|
+
this.handOn(taskId, config);
|
|
5285
|
+
}
|
|
5286
|
+
/**
|
|
5287
|
+
* **A branch reached an ASK: it parks, keeping what reached it, and NOTHING is handed to the scheduler** — `RN-R37`,
|
|
5288
|
+
* v0.22, [ADR 0109](../../doc/decisions/0109-an-ask-parks-with-nothing-dispatched-and-its-answer-is-its-result.md),
|
|
5289
|
+
* P35 stop-and-asks (b), (c) and (f), decision 5.
|
|
5290
|
+
*
|
|
5291
|
+
* 1. **The hold** (`RN-R29`), as `dispatch`'s: `enter` checked it at its head and `compose` ran consumer code since.
|
|
5292
|
+
* 2. **Its task id** by `spec/task-id.md`'s scheme, attempt `0` — the address a person's answer names. Minted once
|
|
5293
|
+
* per path: a second `asked` at one id is `ID-R1`'s defect and refused as one (`DuplicateTaskId`), never a
|
|
5294
|
+
* second park.
|
|
5295
|
+
* 3. **ONE `asked` element**, carrying `received` — what reached the ask, composed through its `needs` — as the
|
|
5296
|
+
* driver's OWN deep copy: at the entry `received` is the caller's own `start()` payload, and a kept input must not
|
|
5297
|
+
* move when the caller's object does (P35 (f)). No `dispatched` element, no `createTask`, no `addTask`: the ask is
|
|
5298
|
+
* never in flight (`RN-R21`), so a stop cancels nothing for it and a resume re-dispatches nothing (P35 fact 5).
|
|
5299
|
+
* 4. **In a region, `branch.suspended`** — what a parked task emits (`settle`'s `user_action` arm): the branch is
|
|
5300
|
+
* live and its join waits for the person (Q7), which is how an ask in a fan-out holds its join. AFTER the element,
|
|
5301
|
+
* as the parked task's follows its settlement, and the hold checked between them (`RN-R29`): a consumer's
|
|
5302
|
+
* callback on `asked` may halt the run, and a halted run emits nothing further. It may also ANSWER the ask: the
|
|
5303
|
+
* answer is accepted and PENDING until the step ends (reading R4), so the park completes first.
|
|
5304
|
+
*
|
|
5305
|
+
* **During a replay this is how the park is REGENERATED** (`RN-R19`): the element is compared with the stored one,
|
|
5306
|
+
* input and all (`sameData`, `RN-R20`), so a replay that composes a different input is refused `diverged` here.
|
|
5307
|
+
*/
|
|
5308
|
+
ask(runId, node, received, itemIndices) {
|
|
5309
|
+
if (this.shut()) return;
|
|
5310
|
+
const branchPath = this.pathOf(node.id, itemIndices);
|
|
5311
|
+
const taskId = buildTaskId(runId, node.id, branchIndices(branchPath), 0);
|
|
5312
|
+
if (this.asked.has(taskId) || this.dispatched.has(taskId)) {
|
|
5313
|
+
throw new DuplicateTaskId(`"${taskId}" was already asked in run "${runId}" \u2014 an ask's id is minted once per path (ID-R1)`);
|
|
5314
|
+
}
|
|
5315
|
+
this.asked.add(taskId);
|
|
5316
|
+
this.append({ kind: "asked", taskId, input: cloneValue(received) });
|
|
5317
|
+
if (this.shut()) return;
|
|
5318
|
+
const region = this.regionOf(node.id);
|
|
5319
|
+
if (region) {
|
|
5320
|
+
this.emit({
|
|
5321
|
+
type: "branch.suspended",
|
|
5322
|
+
regionId: region.fanoutId,
|
|
5323
|
+
regionPath: this.rpathOf(region, itemIndices),
|
|
5324
|
+
branchPath,
|
|
5325
|
+
at: node.id
|
|
5326
|
+
});
|
|
5327
|
+
}
|
|
5328
|
+
}
|
|
5329
|
+
/**
|
|
5330
|
+
* **`RS-R1`'s *suspended*, for one task id** — a dispatched task whose last record is a `user_action` settlement, or,
|
|
5331
|
+
* since v0.18 (`RN-R37`, ADR 0109), an ask this run reached that has no record at all. The FIVE readers of the
|
|
5332
|
+
* predicate read it here: `resolve()`, the replay's fed resolution, and the three `ended` refusals (`hasParked`).
|
|
5333
|
+
*/
|
|
5334
|
+
parked(taskId) {
|
|
5335
|
+
return isSuspended(this.log, taskId, this.asked.has(taskId));
|
|
5336
|
+
}
|
|
5337
|
+
/**
|
|
5338
|
+
* **Whether anything in the run waits on a person** — a parked task or an unanswered ask. `cancel`, `stop` and `pause`
|
|
5339
|
+
* refuse a run `ended` only when nothing is in flight and THIS is false: a run whose only live node is a parked ask is
|
|
5340
|
+
* waiting on a person, not finished (P35 fact 5 (iv)). The rest of an ask under run control needs no line of its own:
|
|
5341
|
+
* never dispatched, it is never in flight, so every road reading `inFlight()` passes it by (`RN-R38`, W181).
|
|
5342
|
+
*/
|
|
5343
|
+
hasParked() {
|
|
5344
|
+
for (const id of this.dispatched) if (isSuspended(this.log, id)) return true;
|
|
5345
|
+
for (const id of this.asked) if (this.parked(id)) return true;
|
|
5346
|
+
return false;
|
|
5347
|
+
}
|
|
5348
|
+
/**
|
|
5349
|
+
* **Hand one task to the scheduler — and, if the run's owner cancelled it while the task was on its way, cancel it
|
|
5350
|
+
* at once** (`RN-R29` (i), `RN-R31`, ADR 0103's inherited hazard, W173). The ONE place `addTask` is called: by
|
|
5351
|
+
* `dispatch` for the task in progress, and by `resume()` for each task in flight. A cancel raised on this task's own
|
|
5352
|
+
* `dispatched` element — or on a resume's `resumed` element — ran before the scheduler held the id, and sent nothing
|
|
5353
|
+
* for it; this is where that task is cancelled, so it ends with a genuine `task.cancelled` rather than running.
|
|
5354
|
+
*/
|
|
5355
|
+
handOn(taskId, config) {
|
|
5356
|
+
try {
|
|
5357
|
+
this.opts.scheduler.addTask((this.opts.createTask ?? defaultCreateTask)(config));
|
|
5358
|
+
} finally {
|
|
5359
|
+
this.unhanded.delete(taskId);
|
|
5360
|
+
}
|
|
5361
|
+
if (this.cancelRequest !== null) this.cancelTask(taskId);
|
|
5362
|
+
}
|
|
5363
|
+
/**
|
|
5364
|
+
* **One task cancelled through the scheduler, with the request's options**, its promise kept for `cancel()` — or,
|
|
5365
|
+
* since W174, `stop()` — to wait on (`RN-R31`, `RN-R33`). Each option is passed only when the caller gave it, so an
|
|
5366
|
+
* absent `graceMs` is the pool's `cancelGraceMs` and not `undefined` read as a value. A cancel and a stop never both
|
|
5367
|
+
* hold — each refuses the other — so the one present is the one whose options these are.
|
|
5368
|
+
*/
|
|
5369
|
+
cancelTask(taskId) {
|
|
5370
|
+
const req = this.cancelRequest ?? this.stopRequest;
|
|
5371
|
+
const options = {};
|
|
5372
|
+
if (req.reason !== void 0) options.reason = req.reason;
|
|
5373
|
+
if (req.graceMs !== void 0) options.graceMs = req.graceMs;
|
|
5374
|
+
const p = this.opts.scheduler.cancel(taskId, options);
|
|
5375
|
+
if (!req.stopping.has(taskId)) req.stopping.set(taskId, p);
|
|
5376
|
+
const drop = () => {
|
|
5377
|
+
if (req.stopping.get(taskId) === p) req.stopping.delete(taskId);
|
|
5378
|
+
};
|
|
5379
|
+
p.then(drop, drop);
|
|
5380
|
+
return p;
|
|
3692
5381
|
}
|
|
3693
5382
|
/**
|
|
3694
5383
|
* **A node's `branchPath`, derived from the graph** — `spec/task-id.md` section
|
|
@@ -3747,6 +5436,49 @@ var Driver = class {
|
|
|
3747
5436
|
this.arrivals.get(fanoutId)?.delete(rp);
|
|
3748
5437
|
if (this.arrivals.get(fanoutId)?.size === 0) this.arrivals.delete(fanoutId);
|
|
3749
5438
|
}
|
|
5439
|
+
/**
|
|
5440
|
+
* **The release horizon of what instances keep** — ADR 0118 rule 5. A `branch.opened` of a region nested in a kept map's
|
|
5441
|
+
* instance counts one live branch there; its terminal event uncounts it. The instance's OWN terminal event releases its
|
|
5442
|
+
* item when that count is zero, and otherwise marks it ENDED, released by the nested branch that ends last. O(nesting
|
|
5443
|
+
* depth) per event, and nothing at all where no map keeps (`kept` empty). Run in a replay as in a run, so a resume
|
|
5444
|
+
* rebuilds the counts with the items.
|
|
5445
|
+
*/
|
|
5446
|
+
trackItems(e) {
|
|
5447
|
+
const it = this.items;
|
|
5448
|
+
if (it.kept.size === 0) return;
|
|
5449
|
+
const opened = e.type === "branch.opened";
|
|
5450
|
+
const terminal = isTerminalEvent(e);
|
|
5451
|
+
if (!opened && !terminal) return;
|
|
5452
|
+
for (const { map, depth } of it.within.get(e.regionId) ?? []) {
|
|
5453
|
+
const path = e.branchPath.split(".").slice(0, depth).join(".");
|
|
5454
|
+
const k = `${map} ${path}`;
|
|
5455
|
+
const n = (it.nested.get(k) ?? 0) + (opened ? 1 : -1);
|
|
5456
|
+
if (n > 0) {
|
|
5457
|
+
it.nested.set(k, n);
|
|
5458
|
+
continue;
|
|
5459
|
+
}
|
|
5460
|
+
it.nested.delete(k);
|
|
5461
|
+
if (it.ended.has(k)) this.releaseItem(map, path);
|
|
5462
|
+
}
|
|
5463
|
+
if (terminal && it.kept.has(e.regionId)) {
|
|
5464
|
+
if (it.nested.has(`${e.regionId} ${e.branchPath}`)) it.ended.add(`${e.regionId} ${e.branchPath}`);
|
|
5465
|
+
else this.releaseItem(e.regionId, e.branchPath);
|
|
5466
|
+
}
|
|
5467
|
+
}
|
|
5468
|
+
/** **One instance's item, dropped** — and the map's own key with its last instance, as `releaseArrivals` drops a region's. */
|
|
5469
|
+
releaseItem(mapId, branchPath) {
|
|
5470
|
+
this.items.ended.delete(`${mapId} ${branchPath}`);
|
|
5471
|
+
const byPath = this.items.held.get(mapId);
|
|
5472
|
+
if (byPath === void 0) return;
|
|
5473
|
+
byPath.delete(branchPath);
|
|
5474
|
+
if (byPath.size === 0) this.items.held.delete(mapId);
|
|
5475
|
+
}
|
|
5476
|
+
/** **Everything instances keep, dropped** — the run-level moments, where no node of the run is entered again. */
|
|
5477
|
+
releaseAllItems() {
|
|
5478
|
+
this.items.held.clear();
|
|
5479
|
+
this.items.nested.clear();
|
|
5480
|
+
this.items.ended.clear();
|
|
5481
|
+
}
|
|
3750
5482
|
/** The arrival map for one region, created on first arrival. */
|
|
3751
5483
|
arrivalsOf(fanoutId, rp) {
|
|
3752
5484
|
const byPath = this.arrivals.get(fanoutId) ?? /* @__PURE__ */ new Map();
|
|
@@ -3902,12 +5634,22 @@ var Driver = class {
|
|
|
3902
5634
|
if (this.opts.onFoldInput && this.delivers(seq)) this.opts.onFoldInput(freezeInput(input), seq);
|
|
3903
5635
|
return seq;
|
|
3904
5636
|
}
|
|
5637
|
+
/**
|
|
5638
|
+
* **A stored `interrupted` element, fed** (`RN-R19`, `RN-R32`): the replay holds as the stopped run was held — the hold
|
|
5639
|
+
* BEFORE the element, as a stop is taken — and appends the element as stored. Neither `deaf` nor a cancel nor the
|
|
5640
|
+
* collections' release: those were the stopped driver's, and a replay hands a scheduler nothing.
|
|
5641
|
+
*/
|
|
5642
|
+
feedInterrupted(e) {
|
|
5643
|
+
this.stopRequest = { actor: e.actor, stopping: /* @__PURE__ */ new Map(), pending: null, took: null };
|
|
5644
|
+
this.append({ kind: "interrupted", actor: e.actor, reason: e.reason ?? null });
|
|
5645
|
+
}
|
|
3905
5646
|
emit(e) {
|
|
3906
5647
|
const policy = this.opts.joinPolicy;
|
|
3907
5648
|
if (policy && this.joinStateHas(e.regionId, e.regionPath)) {
|
|
3908
5649
|
this.joinStateSet(e.regionId, e.regionPath, policy.step(this.joinStateGet(e.regionId, e.regionPath), e));
|
|
3909
5650
|
}
|
|
3910
5651
|
const event = Object.freeze({ ...e });
|
|
5652
|
+
this.trackItems(e);
|
|
3911
5653
|
const seq = this.append({ kind: "region", event });
|
|
3912
5654
|
if (this.delivers(seq)) this.opts.onRegionEvent?.(event);
|
|
3913
5655
|
}
|
|
@@ -3978,7 +5720,7 @@ var Driver = class {
|
|
|
3978
5720
|
joinId: region.joinId,
|
|
3979
5721
|
fired: true
|
|
3980
5722
|
});
|
|
3981
|
-
if (this.
|
|
5723
|
+
if (this.shut()) return;
|
|
3982
5724
|
const collected = collectArrivals(this.arrivals.get(region.fanoutId)?.get(rp));
|
|
3983
5725
|
this.releaseArrivals(region.fanoutId, rp);
|
|
3984
5726
|
const join = this.graph.byId.get(region.joinId);
|
|
@@ -4118,22 +5860,72 @@ var Driver = class {
|
|
|
4118
5860
|
|
|
4119
5861
|
// src/fold/run-state.ts
|
|
4120
5862
|
function emptyState() {
|
|
4121
|
-
return {
|
|
5863
|
+
return {
|
|
5864
|
+
runId: null,
|
|
5865
|
+
outcome: "running",
|
|
5866
|
+
nodes: {},
|
|
5867
|
+
regions: {},
|
|
5868
|
+
pathEnds: [],
|
|
5869
|
+
haltedBy: null,
|
|
5870
|
+
cancelRequest: null,
|
|
5871
|
+
interruption: null,
|
|
5872
|
+
paused: false,
|
|
5873
|
+
resumes: [],
|
|
5874
|
+
gated: []
|
|
5875
|
+
};
|
|
4122
5876
|
}
|
|
4123
5877
|
function step(prev, input) {
|
|
4124
|
-
if (input.kind === "stopped") {
|
|
4125
|
-
return { ...prev,
|
|
5878
|
+
if (input.kind === "halted" || input.kind === "stopped") {
|
|
5879
|
+
return { ...prev, haltedBy: input.reason, outcome: "halted" };
|
|
5880
|
+
}
|
|
5881
|
+
if (input.kind === "cancelRequested") {
|
|
5882
|
+
if (prev.cancelRequest !== null || prev.outcome === "cancelled") return prev;
|
|
5883
|
+
return withOutcome({ ...prev, cancelRequest: { actor: input.actor, reason: input.reason } });
|
|
5884
|
+
}
|
|
5885
|
+
if (input.kind === "interrupted") {
|
|
5886
|
+
if (prev.interruption !== null) return prev;
|
|
5887
|
+
return withOutcome({ ...prev, interruption: { actor: input.actor, reason: input.reason } });
|
|
5888
|
+
}
|
|
5889
|
+
if (input.kind === "paused" || input.kind === "unpaused") {
|
|
5890
|
+
const paused = input.kind === "paused";
|
|
5891
|
+
if (prev.paused === paused) return prev;
|
|
5892
|
+
return { ...prev, paused };
|
|
4126
5893
|
}
|
|
4127
5894
|
if (input.kind === "region") {
|
|
4128
5895
|
return withOutcome(applyRegion(prev, input.event));
|
|
4129
5896
|
}
|
|
4130
5897
|
if (input.kind === "dispatched") {
|
|
4131
|
-
const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2 } = parseTaskId(input.taskId);
|
|
5898
|
+
const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, attempt: attempt2 } = parseTaskId(input.taskId);
|
|
4132
5899
|
if (prev.nodes[input.taskId]) return prev;
|
|
4133
5900
|
return withOutcome({
|
|
4134
5901
|
...prev,
|
|
4135
5902
|
runId: prev.runId ?? runId2,
|
|
4136
|
-
nodes: {
|
|
5903
|
+
nodes: {
|
|
5904
|
+
...prev.nodes,
|
|
5905
|
+
[input.taskId]: { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, flowAttempt: attempt2, attemptNumber: null, state: "running", result: null, error: null }
|
|
5906
|
+
}
|
|
5907
|
+
});
|
|
5908
|
+
}
|
|
5909
|
+
if (input.kind === "asked") {
|
|
5910
|
+
const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, attempt: attempt2 } = parseTaskId(input.taskId);
|
|
5911
|
+
if (prev.nodes[input.taskId]) return prev;
|
|
5912
|
+
return withOutcome({
|
|
5913
|
+
...prev,
|
|
5914
|
+
runId: prev.runId ?? runId2,
|
|
5915
|
+
nodes: {
|
|
5916
|
+
...prev.nodes,
|
|
5917
|
+
[input.taskId]: {
|
|
5918
|
+
runId: runId2,
|
|
5919
|
+
nodeId: nodeId2,
|
|
5920
|
+
branchPath: branchPath2,
|
|
5921
|
+
flowAttempt: attempt2,
|
|
5922
|
+
attemptNumber: null,
|
|
5923
|
+
state: "suspended",
|
|
5924
|
+
result: null,
|
|
5925
|
+
error: null,
|
|
5926
|
+
input: structuredClone(input.input)
|
|
5927
|
+
}
|
|
5928
|
+
}
|
|
4137
5929
|
});
|
|
4138
5930
|
}
|
|
4139
5931
|
if (input.kind === "pathEnded") {
|
|
@@ -4147,13 +5939,20 @@ function step(prev, input) {
|
|
|
4147
5939
|
});
|
|
4148
5940
|
}
|
|
4149
5941
|
if (input.kind === "resumed") {
|
|
4150
|
-
|
|
5942
|
+
const resumes = [...prev.resumes, [...input.taskIds]];
|
|
5943
|
+
if (prev.interruption !== null) return withOutcome({ ...prev, interruption: null, resumes });
|
|
5944
|
+
return { ...prev, resumes };
|
|
4151
5945
|
}
|
|
4152
5946
|
if (input.kind === "gated") {
|
|
4153
5947
|
const d = { gateId: input.gateId, branchPath: input.branchPath, route: input.route };
|
|
4154
5948
|
if (input.key !== void 0) d.key = structuredClone(input.key);
|
|
4155
5949
|
return { ...prev, gated: [...prev.gated, d] };
|
|
4156
5950
|
}
|
|
5951
|
+
if (input.kind === "attempted") {
|
|
5952
|
+
const node = prev.nodes[input.taskId];
|
|
5953
|
+
if (node === void 0) return prev;
|
|
5954
|
+
return { ...prev, nodes: { ...prev.nodes, [input.taskId]: { ...node, attemptNumber: input.attemptNumber } } };
|
|
5955
|
+
}
|
|
4157
5956
|
if (input.kind === "joinSettled") {
|
|
4158
5957
|
const cur = prev.regions[input.regionId]?.[input.regionPath] ?? emptyRegion();
|
|
4159
5958
|
return withOutcome({
|
|
@@ -4162,13 +5961,17 @@ function step(prev, input) {
|
|
|
4162
5961
|
});
|
|
4163
5962
|
}
|
|
4164
5963
|
const r = input.record;
|
|
4165
|
-
const { runId, nodeId, branchPath } = parseTaskId(r.taskId);
|
|
5964
|
+
const { runId, nodeId, branchPath, attempt } = parseTaskId(r.taskId);
|
|
4166
5965
|
const nodes = { ...prev.nodes };
|
|
4167
5966
|
const existing = nodes[r.taskId];
|
|
5967
|
+
const attemptNumber = existing?.attemptNumber ?? null;
|
|
4168
5968
|
if (r.kind === "outcome") {
|
|
4169
5969
|
nodes[r.taskId] = {
|
|
5970
|
+
runId,
|
|
4170
5971
|
nodeId,
|
|
4171
5972
|
branchPath,
|
|
5973
|
+
flowAttempt: attempt,
|
|
5974
|
+
attemptNumber,
|
|
4172
5975
|
// `user_action` is `suspended` at flow level: a resolution resumes it, and
|
|
4173
5976
|
// calling it terminal is what makes a design drop the fifth state.
|
|
4174
5977
|
state: r.terminalState === "user_action" ? "suspended" : r.terminalState,
|
|
@@ -4177,11 +5980,15 @@ function step(prev, input) {
|
|
|
4177
5980
|
};
|
|
4178
5981
|
} else {
|
|
4179
5982
|
nodes[r.taskId] = {
|
|
5983
|
+
runId: existing?.runId ?? runId,
|
|
4180
5984
|
nodeId: existing?.nodeId ?? nodeId,
|
|
4181
5985
|
branchPath: existing?.branchPath ?? branchPath,
|
|
5986
|
+
flowAttempt: existing?.flowAttempt ?? attempt,
|
|
5987
|
+
attemptNumber,
|
|
4182
5988
|
state: r.resolvedAs,
|
|
4183
5989
|
result: r.result,
|
|
4184
|
-
error: r.error
|
|
5990
|
+
error: r.error,
|
|
5991
|
+
...existing !== void 0 && "input" in existing ? { input: existing.input } : {}
|
|
4185
5992
|
};
|
|
4186
5993
|
}
|
|
4187
5994
|
return withOutcome({ ...prev, runId: prev.runId ?? runId, nodes });
|
|
@@ -4220,8 +6027,13 @@ function applyRegion(prev, e) {
|
|
|
4220
6027
|
return { ...prev, regions: withRegion(prev.regions, e.regionId, e.regionPath, cur) };
|
|
4221
6028
|
}
|
|
4222
6029
|
function withOutcome(s) {
|
|
4223
|
-
if (s.
|
|
6030
|
+
if (s.haltedBy !== null) return { ...s, outcome: "halted" };
|
|
4224
6031
|
const nodes = Object.values(s.nodes);
|
|
6032
|
+
if (s.cancelRequest === null && nodes.some((n) => n.state === "cancelled")) return { ...s, outcome: "cancelled" };
|
|
6033
|
+
if (s.cancelRequest !== null) {
|
|
6034
|
+
return { ...s, outcome: nodes.some((n) => n.state === "running") ? "cancelling" : "cancelled" };
|
|
6035
|
+
}
|
|
6036
|
+
if (s.interruption !== null) return { ...s, outcome: "interrupted" };
|
|
4225
6037
|
if (nodes.some((n) => n.state === "suspended")) return { ...s, outcome: "awaiting_action" };
|
|
4226
6038
|
if (nodes.some((n) => n.state === "running")) return { ...s, outcome: "running" };
|
|
4227
6039
|
const openedNothing = allRegionRuns(s).some((r) => r.opened.length === 0 && r.joined !== true);
|
|
@@ -4243,7 +6055,7 @@ function foldLog(log) {
|
|
|
4243
6055
|
return fold(log.map((r) => ({ kind: r.kind, record: r })));
|
|
4244
6056
|
}
|
|
4245
6057
|
function isTerminal(s) {
|
|
4246
|
-
return s.outcome !== "running" && s.outcome !== "awaiting_action";
|
|
6058
|
+
return s.outcome !== "running" && s.outcome !== "awaiting_action" && s.outcome !== "cancelling";
|
|
4247
6059
|
}
|
|
4248
6060
|
function nodeOf(s, taskId) {
|
|
4249
6061
|
return s.nodes[taskId] ?? null;
|
|
@@ -4329,6 +6141,7 @@ export {
|
|
|
4329
6141
|
innermostRegionOf,
|
|
4330
6142
|
branchPathOf,
|
|
4331
6143
|
regionChainOf,
|
|
6144
|
+
innermostMapOf,
|
|
4332
6145
|
sharedRegionDepth,
|
|
4333
6146
|
outputIsVisible,
|
|
4334
6147
|
branchIndexOf,
|
|
@@ -4339,6 +6152,8 @@ export {
|
|
|
4339
6152
|
canonical,
|
|
4340
6153
|
findings,
|
|
4341
6154
|
describeFindings,
|
|
6155
|
+
readKeyPath,
|
|
6156
|
+
ASK_REFUSED_KEYS,
|
|
4342
6157
|
EDGE_KEYS_DECLARED,
|
|
4343
6158
|
TASK_KEYS_DECLARED,
|
|
4344
6159
|
NODE_KEYS_DECLARED,
|
|
@@ -4353,9 +6168,9 @@ export {
|
|
|
4353
6168
|
languagesOf,
|
|
4354
6169
|
KEY_PATH_RE,
|
|
4355
6170
|
keyPathProblem,
|
|
4356
|
-
readKeyPath,
|
|
4357
6171
|
GATE_TYPE_KEYS,
|
|
4358
6172
|
gateTypeProblem,
|
|
6173
|
+
routesOf,
|
|
4359
6174
|
distinct,
|
|
4360
6175
|
BUILT_IN_GATE_TYPES,
|
|
4361
6176
|
gateTypesOf,
|
|
@@ -4393,8 +6208,10 @@ export {
|
|
|
4393
6208
|
CHECKED_WORKER_SCHEMAS,
|
|
4394
6209
|
NODE_TYPE_DECLARATION_KEYS,
|
|
4395
6210
|
POOL_SPEC_KEYS,
|
|
6211
|
+
MESSAGE_OPTION_KEYS,
|
|
4396
6212
|
DECLARED_POOL_KEYS,
|
|
4397
6213
|
INSTANCE_KEYS,
|
|
6214
|
+
POOL_INSTANCE_KEYS,
|
|
4398
6215
|
isDerivation,
|
|
4399
6216
|
mergeConfig,
|
|
4400
6217
|
nodeTypeFromDeclaration,
|
|
@@ -4410,6 +6227,7 @@ export {
|
|
|
4410
6227
|
fits,
|
|
4411
6228
|
boundaryTargets,
|
|
4412
6229
|
checkWiring,
|
|
6230
|
+
givenVerdict,
|
|
4413
6231
|
describeVerdict,
|
|
4414
6232
|
VERDICT_KINDS,
|
|
4415
6233
|
WIRING_SHAPES,
|
|
@@ -4429,6 +6247,8 @@ export {
|
|
|
4429
6247
|
eligibleEdges,
|
|
4430
6248
|
gateEdge,
|
|
4431
6249
|
selectEdge,
|
|
6250
|
+
TERMINAL_EVENTS,
|
|
6251
|
+
isTerminalEvent,
|
|
4432
6252
|
InvalidDefinition,
|
|
4433
6253
|
DuplicateTaskId,
|
|
4434
6254
|
UnresolvedMapArity,
|
|
@@ -4437,6 +6257,7 @@ export {
|
|
|
4437
6257
|
BoundaryRefused,
|
|
4438
6258
|
MultipleRuns,
|
|
4439
6259
|
NotStarted,
|
|
6260
|
+
RunControlRefused,
|
|
4440
6261
|
CHECKPOINT_FORMAT,
|
|
4441
6262
|
ResumeRefused,
|
|
4442
6263
|
resumeRefusal,
|