fyflow-orchestration 0.7.0 → 0.9.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 +1895 -206
- package/README.md +89 -104
- package/dist/browser/chunks/{chunk-YVOP3DIN.js → chunk-3U4UZC34.js} +2683 -449
- package/dist/browser/index.js +19 -9
- package/dist/browser/scheduler/index.js +158 -67
- package/dist/node/chunks/{chunk-YVOP3DIN.js → chunk-3U4UZC34.js} +2683 -449
- package/dist/node/index.js +19 -9
- package/dist/node/scheduler/index.js +158 -67
- 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 +827 -61
- 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/accumulator.d.ts +94 -0
- package/dist/types/src/fold/policies.d.ts +4 -5
- package/dist/types/src/fold/run-state.d.ts +275 -17
- 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 +144 -39
- 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;
|
|
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);
|
|
2216
2736
|
}
|
|
2217
|
-
|
|
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
|
-
}
|
|
2242
|
-
}
|
|
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,52 @@ 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"]];
|
|
3092
|
+
var NO_RECORDS = Object.freeze([]);
|
|
3093
|
+
var TaskLog = class {
|
|
3094
|
+
byTask = /* @__PURE__ */ new Map();
|
|
3095
|
+
visits = 0;
|
|
3096
|
+
/** `append`'s half: the record joins its task's sub-log, by reference. */
|
|
3097
|
+
push(record) {
|
|
3098
|
+
const sub = this.byTask.get(record.taskId);
|
|
3099
|
+
if (sub) sub.push(record);
|
|
3100
|
+
else this.byTask.set(record.taskId, [record]);
|
|
3101
|
+
}
|
|
3102
|
+
/** One task's records, in append order — what `effectiveOutcome` and `isSuspended` are handed. Read, never kept. */
|
|
3103
|
+
of(taskId) {
|
|
3104
|
+
const sub = this.byTask.get(taskId) ?? NO_RECORDS;
|
|
3105
|
+
this.visits += sub.length;
|
|
3106
|
+
return sub;
|
|
3107
|
+
}
|
|
3108
|
+
};
|
|
3109
|
+
var Fifo = class {
|
|
3110
|
+
items = [];
|
|
3111
|
+
head = 0;
|
|
3112
|
+
get length() {
|
|
3113
|
+
return this.items.length - this.head;
|
|
3114
|
+
}
|
|
3115
|
+
push(x) {
|
|
3116
|
+
this.items.push(x);
|
|
3117
|
+
}
|
|
3118
|
+
/** The oldest element, removed — `undefined` when empty, as `Array.prototype.shift`. */
|
|
3119
|
+
shift() {
|
|
3120
|
+
if (this.head >= this.items.length) return void 0;
|
|
3121
|
+
const x = this.items[this.head];
|
|
3122
|
+
this.items[this.head] = void 0;
|
|
3123
|
+
this.head += 1;
|
|
3124
|
+
if (this.head === this.items.length) {
|
|
3125
|
+
this.items = [];
|
|
3126
|
+
this.head = 0;
|
|
3127
|
+
}
|
|
3128
|
+
return x;
|
|
3129
|
+
}
|
|
3130
|
+
/** Every element still queued, oldest first. */
|
|
3131
|
+
*[Symbol.iterator]() {
|
|
3132
|
+
for (let i = this.head; i < this.items.length; i += 1) yield this.items[i];
|
|
3133
|
+
}
|
|
3134
|
+
};
|
|
2493
3135
|
var Driver = class {
|
|
2494
3136
|
/**
|
|
2495
3137
|
* `OR-R7` — nothing outside the inbound API writes to run state. Private, and
|
|
@@ -2507,16 +3149,31 @@ var Driver = class {
|
|
|
2507
3149
|
* channels with no shared order, so `RN-R3`'s *"read in append order"* was about
|
|
2508
3150
|
* a value no one could hold.
|
|
2509
3151
|
*
|
|
2510
|
-
* `log` above is the **record-kind subsequence of this array**,
|
|
2511
|
-
*
|
|
2512
|
-
*
|
|
2513
|
-
*
|
|
2514
|
-
*
|
|
3152
|
+
* `log` above is the **record-kind subsequence of this array**, by reference — the
|
|
3153
|
+
* same record objects, never copies; `test/driver/` asserts the two agree, so the
|
|
3154
|
+
* redundancy is checked rather than trusted. *(It was kept "only because
|
|
3155
|
+
* `effectiveOutcome` and `isSuspended` scan it on every settlement" until W195,
|
|
3156
|
+
* whose {@link TaskLog} hands each of them one task's records instead — P39 fact 5.
|
|
3157
|
+
* It stays for what still reads it WHOLE and in order: `snapshot()`, `outputs()`'s
|
|
3158
|
+
* catch-up, and the retention walk's alias check.)*
|
|
2515
3159
|
*/
|
|
2516
3160
|
inputs = [];
|
|
3161
|
+
/**
|
|
3162
|
+
* **Each task's records, by reference and in append order** — {@link TaskLog}, `RN-R13` v0.30, ADR 0121 (W195). Written in
|
|
3163
|
+
* `append` beside `log`; read by every per-task question the driver asks of its log — `advance`, `parked`, `hasParked`,
|
|
3164
|
+
* `inFlight`, `outputs()`, `effectiveOutcomeOf` — so each costs the task, never the run.
|
|
3165
|
+
*/
|
|
3166
|
+
taskLog = new TaskLog();
|
|
2517
3167
|
graph;
|
|
2518
3168
|
regions;
|
|
2519
3169
|
dispatched = /* @__PURE__ */ new Set();
|
|
3170
|
+
/**
|
|
3171
|
+
* **Every ASK this run has reached, by task id** — `RN-R37`, v0.22, W180, ADR 0109: the ids of the `asked` elements
|
|
3172
|
+
* appended, so `RS-R1`'s widened *suspended* (an `asked` element and no record after it) is one lookup rather than a
|
|
3173
|
+
* scan of the input sequence at every `resolve()`, and an ask's id is minted once per path (`ID-R1`'s guard). Ids only —
|
|
3174
|
+
* the kept INPUT lives on the element alone. `dispatched`'s twin for the one node that is never dispatched.
|
|
3175
|
+
*/
|
|
3176
|
+
asked = /* @__PURE__ */ new Set();
|
|
2520
3177
|
now;
|
|
2521
3178
|
opts;
|
|
2522
3179
|
def;
|
|
@@ -2594,6 +3251,29 @@ var Driver = class {
|
|
|
2594
3251
|
* quiescent point (`RN-R10`), not here — ADR 0046 rule 5.
|
|
2595
3252
|
*/
|
|
2596
3253
|
arrivals = /* @__PURE__ */ new Map();
|
|
3254
|
+
/**
|
|
3255
|
+
* **What a map INSTANCE keeps for `"$item"`** — `RN-R13`'s sixth term since v0.28 (`spec/run-state.md`), P38 (e),
|
|
3256
|
+
* [ADR 0118](../../doc/decisions/0118-a-map-instance-keeps-its-item-while-a-node-of-it-may-still-be-entered.md),
|
|
3257
|
+
* [W191](../../doc/work-items/w191-the-map-instances-item-as-a-reference.md). Two halves, one field:
|
|
3258
|
+
*
|
|
3259
|
+
* - **`kept`** — decided from the DOCUMENT at construction: the maps whose region holds a `"$item"` reader (a node, or
|
|
3260
|
+
* an edge of a node, whose innermost containing map is that map — `innermostMapOf`, P38 (c)), and for each whether
|
|
3261
|
+
* its received value may be the CALLER's own object (`true`: reached from the entry through gates alone, with no
|
|
3262
|
+
* `needs` — `start()` hands `enter` the value it was called with, as an entry task and an entry ask receive it). A
|
|
3263
|
+
* map no reader names keeps NOTHING, and a flow declaring no `"$item"` has `kept` empty (P38 decision 2).
|
|
3264
|
+
* - **`held`** — map id → the instance's branch path → its item. Written by the MAP ARM, where the instance opens —
|
|
3265
|
+
* 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
|
|
3266
|
+
* a caller's call) — **BY REFERENCE to the element the map received** (the driver's own: an edge-hop clone, or a
|
|
3267
|
+
* `needs` composition's per-key copy), and as its OWN copy only where `kept` says that element is the caller's
|
|
3268
|
+
* (P35 (f)'s reason for `asked.input`). Every READ clones (`JP-R11`), so no node receives the held object.
|
|
3269
|
+
* **Released when NOTHING of that instance may still be entered** — its own branch terminal (`branch.arrived`,
|
|
3270
|
+
* `branch.lost` or `branch.exited`, `JP-R5`: one per opened branch) AND no branch of a region NESTED in it still live
|
|
3271
|
+
* (`within`, `nested`, `ended` — `trackItems`): a nested join a policy fires before its last branch is terminal lets
|
|
3272
|
+
* the instance's branch arrive while that branch can still read (W191's conformity reader, C1). A suspended node (an
|
|
3273
|
+
* ask, a parked task) keeps its branch live, and so its item. And at the run-level moments
|
|
3274
|
+
* `arrivals` is cleared at — a halt, a cancel, a stop, a `cancelled` settlement — because no node is entered after.
|
|
3275
|
+
*/
|
|
3276
|
+
items;
|
|
2597
3277
|
/**
|
|
2598
3278
|
* **The one run this driver serves** — ADR 0022 rule 1, fixed at construction
|
|
2599
3279
|
* and never `null`.
|
|
@@ -2636,16 +3316,212 @@ var Driver = class {
|
|
|
2636
3316
|
* elements a consumer does not already hold ({@link Driver.delivers}).
|
|
2637
3317
|
*/
|
|
2638
3318
|
replay = null;
|
|
2639
|
-
/** The run has been
|
|
3319
|
+
/** The run has been HALTED by a refusal (`OR-R10`). Nothing dispatches for it again. */
|
|
2640
3320
|
failed = false;
|
|
2641
3321
|
/**
|
|
2642
|
-
* **A
|
|
2643
|
-
*
|
|
3322
|
+
* **A task of this run settled `cancelled`, so the run is HALTED** — `RN-R28`,
|
|
3323
|
+
* [ADR 0094](../../doc/decisions/0094-a-cancelled-task-is-a-terminal-state-that-routes-no-edge-and-ends-its-run-cancelled.md)
|
|
3324
|
+
* rule 3, W158. Every door `failed` shuts, this shuts — nothing is dispatched or routed for the run again,
|
|
3325
|
+
* a later settlement is appended and routed nowhere, and `resolve()` is refused — and it differs from `failed`
|
|
3326
|
+
* in three things only: no `halted` element is appended (the fold reads the run `cancelled` from the
|
|
3327
|
+
* settlement itself), `onRunFailure` is not called (a cancel is its owner's act, not a defect), and
|
|
3328
|
+
* `hasFailed()` stays false. **Separate rather than `failed` with a reason**, because those three are each
|
|
3329
|
+
* a reader of `failed` that must not fire.
|
|
3330
|
+
*/
|
|
3331
|
+
cancelled = false;
|
|
3332
|
+
/**
|
|
3333
|
+
* **A resume refused because its replay regenerated a HALT in the lost tail** — `RN-R23`: that driver keeps the
|
|
3334
|
+
* halted run (its `header`, so a store may take its checkpoint) and still hears NOTHING. `onEvent`'s other test,
|
|
2644
3335
|
* *started with no header*, cannot see it, because this refusal is the one that keeps its header: until W151 such a
|
|
2645
3336
|
* driver built by hand appended and delivered a later settlement and reported an unminted id (W151's conformity
|
|
2646
|
-
* reader, finding 6). A driver
|
|
3337
|
+
* reader, finding 6). A driver halted by `failRun` on a run it started is NOT deaf: `OR-R10` halts routing only.
|
|
3338
|
+
*
|
|
3339
|
+
* **And a STOPPED driver, since W174** (`RN-R33`, P34 decision 3): a stop sets it with its hold — before the
|
|
3340
|
+
* `interrupted` element, and so BEFORE it sends the first cancel — so no `task.cancelled` of the stop — nor any later settlement — is appended,
|
|
3341
|
+
* routed, delivered or reported, and the log ends with the in-flight set dispatched and unsettled for a resume to
|
|
3342
|
+
* re-dispatch. A replay feeding a stored `interrupted` element does NOT set it: the replay hears nothing anyway.
|
|
2647
3343
|
*/
|
|
2648
3344
|
deaf = false;
|
|
3345
|
+
/**
|
|
3346
|
+
* **The run's OWNER cancelled it** — `RN-R31`, v0.18, W173,
|
|
3347
|
+
* [ADR 0104](../../doc/decisions/0104-a-run-its-owner-cancels-is-cancelling-until-its-last-task-has-stopped.md). `null`
|
|
3348
|
+
* until `cancel()` is accepted; then what every LATER cancel of a task sends the scheduler — the caller's `reason` and
|
|
3349
|
+
* `graceMs`, each only if given — and, per task id, the promise the scheduler's `cancel` returned for it, which is what
|
|
3350
|
+
* `cancel()`'s own promise waits on. **A halt** (`shut()`), set BEFORE the `cancelRequested` element is appended, so a
|
|
3351
|
+
* consumer's callback on that element already sees the run held. It holds no result: one promise per task STILL
|
|
3352
|
+
* stopping — each dropped as it settles, so none once the run is `cancelled` — and the two option values.
|
|
3353
|
+
*/
|
|
3354
|
+
cancelRequest = null;
|
|
3355
|
+
/**
|
|
3356
|
+
* **The run's OWNER stopped it, to resume it elsewhere** — `RN-R33`, v0.19, W174,
|
|
3357
|
+
* [ADR 0105](../../doc/decisions/0105-a-run-its-owner-stops-goes-deaf-before-it-cancels-and-resumes-elsewhere.md). `null`
|
|
3358
|
+
* until `stop()` is accepted; then, as `cancelRequest`'s shape, what every cancel the stop sends carries and one promise
|
|
3359
|
+
* per task STILL stopping, each dropped as it settles — and the `actor`, which the element is appended with.
|
|
3360
|
+
*
|
|
3361
|
+
* **`pending` while the stop has been ACCEPTED and not yet TAKEN**: a stop called MID-STEP — from a callback on the
|
|
3362
|
+
* driver's own step — takes effect when that step ends (`stepping` back to `0`), so the step's routing is all in the log
|
|
3363
|
+
* and none of it is lost to a resume (`RN-R33`, W174). While pending it is the resolver of the promise every call made
|
|
3364
|
+
* before then waits on, `resolve()` and `cancel()` are already refused, and the hold is NOT yet shut; `null` once the
|
|
3365
|
+
* stop is taken, from which moment it IS a halt (`shut()`). `took` is the promise a second call made while pending
|
|
3366
|
+
* waits on. A function and a promise held only until the step ends — `null` both once taken, never at rest.
|
|
3367
|
+
*
|
|
3368
|
+
* **And a REPLAY sets it too**, taken, with no options and nothing stopping, where it feeds a stored `interrupted`
|
|
3369
|
+
* element (`RN-R19`): the stored run was held from that position, so the replay holds there as well, until a fed
|
|
3370
|
+
* `resumed` — or the resume itself, once the replay is over — sets it back to `null`. The stop's deafness is `deaf`'s.
|
|
3371
|
+
*/
|
|
3372
|
+
stopRequest = null;
|
|
3373
|
+
/**
|
|
3374
|
+
* **Tasks whose `dispatched` element is appended and which the scheduler has NOT been handed yet** — `RN-R29` (i)'s
|
|
3375
|
+
* window, ADR 0103's inherited hazard, W173. A scheduler's `cancel()` of an id it does not hold resolves `false` and
|
|
3376
|
+
* does nothing, so `cancel()` sends nothing for these: the hand-off site cancels each right AFTER its `addTask` —
|
|
3377
|
+
* `dispatch` for the task in progress, `resume()` for its in-flight set. Empty at rest; at most the one task being
|
|
3378
|
+
* dispatched, or a resume's in-flight set while it is handed back.
|
|
3379
|
+
*/
|
|
3380
|
+
unhanded = /* @__PURE__ */ new Set();
|
|
3381
|
+
/**
|
|
3382
|
+
* **How deep the driver is in its OWN step** — `start`, `resume`, `resolve` or a scheduler event, each counting itself
|
|
3383
|
+
* in (`RN-R31`, W173's conformity reader, D-1). `0` at rest. A consumer callback the driver calls mid-step sees a
|
|
3384
|
+
* PREFIX that can read finished while the step is about to dispatch more (`RN-R12`'s quiescent point, P34 fact 3), so
|
|
3385
|
+
* `cancel()` refuses `ended` only at rest: mid-step, *nothing in flight and nothing parked* is not *finished*.
|
|
3386
|
+
*/
|
|
3387
|
+
stepping = 0;
|
|
3388
|
+
/**
|
|
3389
|
+
* **The run's OWNER has paused it, and the pause is TAKEN** — `RN-R35`, v0.20, W175,
|
|
3390
|
+
* [ADR 0106](../../doc/decisions/0106-a-run-its-owner-pauses-routes-on-and-holds-every-dispatch-until-unpaused.md):
|
|
3391
|
+
* set when a `paused` element is appended (by `pause()`, or fed to a replay) and cleared when an `unpaused` one is —
|
|
3392
|
+
* so it is always what `RunState.paused` folds to on the driver's own sequence. **Not a halt**: it does not join
|
|
3393
|
+
* `shut()`, because a paused run still routes and still accepts a resolution (ADR 0103's consequence). It is the
|
|
3394
|
+
* dispatch hold's DEFERRED form, at the dispatch door (ADR 0103 rule 6): each dispatch is appended, and held.
|
|
3395
|
+
*/
|
|
3396
|
+
paused = false;
|
|
3397
|
+
/**
|
|
3398
|
+
* **Every dispatch the pause HELD, in dispatch order** (`RN-R35`): appended — so the fold reads each `running`, in flight
|
|
3399
|
+
* under `RN-R21` (P34 fact 3) — and NOT handed to the scheduler. An `unpause` hands each on, first held first; a
|
|
3400
|
+
* `cancel` hands each on and cancels it (ADR 0104 rule 7); a `stop` sends nothing for them, so they stay in flight in
|
|
3401
|
+
* the log for a resume to hold again. Empty unless the run is paused — or an unpause's hand-off is under way, while
|
|
3402
|
+
* which a dispatch joins the END of it, so the order holds; or a halt broke that hand-off off, after which nothing is
|
|
3403
|
+
* handed on for the run and what is left here never is. One `createTask` configuration per held task, no result.
|
|
3404
|
+
*/
|
|
3405
|
+
pauseHeld = /* @__PURE__ */ new Map();
|
|
3406
|
+
/**
|
|
3407
|
+
* **Pause and unpause calls ACCEPTED mid-step and not yet TAKEN**, in call order (`RN-R35`): a call from a callback on
|
|
3408
|
+
* the driver's own step is taken when that step ends (`stepping` back to `0`, `leaveStep()`), as a stop is (`RN-R33`) —
|
|
3409
|
+
* so its element lands BETWEEN two steps, where a replay can feed it (`RN-R19`). Empty at rest.
|
|
3410
|
+
* **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
|
|
3411
|
+
* brief: *an owner element a resume's replay feeds is TAKEN AT REST*): accepted at the call — every refusal already asked — and its resolution
|
|
3412
|
+
* appended when the step ends, in the same call order as the pauses beside it.
|
|
3413
|
+
*/
|
|
3414
|
+
pausePending = new Fifo();
|
|
3415
|
+
/**
|
|
3416
|
+
* **The highest attempt number RECORDED for each task, by task id** — `RN-R40`, v0.26, W186, P36 (f) and (g): the
|
|
3417
|
+
* number the last `attempted` element for it carried — appended, or fed by a replay. What decides that a `task.running`
|
|
3418
|
+
* repeating a number (a requeue) appends nothing, and what a resume re-hands an in-flight task with as `attemptsSpent`.
|
|
3419
|
+
* One integer per task that ever started an attempt, no result; kept for the life of the run, as `dispatched`'s ids are.
|
|
3420
|
+
*/
|
|
3421
|
+
lastAttempt = /* @__PURE__ */ new Map();
|
|
3422
|
+
/**
|
|
3423
|
+
* **`attempted` elements HEARD and not yet appended**, in the order heard (`RN-R40`): a `task.running` arrives on the
|
|
3424
|
+
* driver's OWN step when an inline pool starts the task inside `addTask`, and an element appended there would sit between
|
|
3425
|
+
* two elements of one step — where the replay, which regenerates a step whole, would refuse the checkpoint `diverged`. So
|
|
3426
|
+
* every one is appended when the step ends, BEFORE a pause, an unpause, an answer or a stop pending on that step is taken
|
|
3427
|
+
* (`leaveStep()`): the attempt started before any of them. Empty at rest.
|
|
3428
|
+
*/
|
|
3429
|
+
attemptsPending = new Fifo();
|
|
3430
|
+
/**
|
|
3431
|
+
* **The dispatch hold is SHUT: a halt is on the run, and the driver dispatches and routes nothing further for it** —
|
|
3432
|
+
* `RN-R29`,
|
|
3433
|
+
* [ADR 0103](../../doc/decisions/0103-every-halt-shares-one-dispatch-hold.md), P34
|
|
3434
|
+
* decisions 5 and 8, W172. **The ONE predicate every halt shares**, read at these doors and no others — `RN-R29`
|
|
3435
|
+
* states the two things it does not hold: before each of `resume()`'s re-dispatches (the maintainer's answer to
|
|
3436
|
+
* 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
|
|
3437
|
+
* branches), at the head of `dispatch`, after a resolution's append and again before its routing (door (b)),
|
|
3438
|
+
* after a settlement's append (`settle`), after a join's `joinSettled` append (`settleJoin`), and at `resolve()`'s
|
|
3439
|
+
* entry. A halt P34 adds — a cancel or a stop — joins this disjunction, and every door follows.
|
|
3440
|
+
*
|
|
3441
|
+
* **A METHOD over the halt flags, never a field of its own**: a second copy of *the run is halted* is a field that
|
|
3442
|
+
* can disagree with `failed` or `cancelled`, and each of those has readers this predicate must not stand in for
|
|
3443
|
+
* (`hasFailed()`, `onRunFailure`). **The replay's hold is the OTHER form and is not this** — it appends a dispatch
|
|
3444
|
+
* and defers handing it on (`RN-R19`), where a shut hold appends nothing; `dispatch` keeps both.
|
|
3445
|
+
*
|
|
3446
|
+
* **A REQUESTED cancel joins it since W173** (`RN-R31`, ADR 0104) — ADR 0103's *a halt P34 adds joins this
|
|
3447
|
+
* disjunction*: from the moment `cancel()` is accepted, every door above holds for the run. **And a STOP since W174**
|
|
3448
|
+
* (`RN-R33`, ADR 0105): from the moment the stop is TAKEN — at once when `stop()` is called at rest, and when the step
|
|
3449
|
+
* ends when it is called mid-step (`stopRequest.pending`) — and, in a replay, from a fed `interrupted` element on.
|
|
3450
|
+
*/
|
|
3451
|
+
shut() {
|
|
3452
|
+
return this.failed || this.cancelled || this.cancelRequest !== null || this.stopRequest !== null && this.stopRequest.pending === null;
|
|
3453
|
+
}
|
|
3454
|
+
/**
|
|
3455
|
+
* **What holds a resume's re-dispatch: the hold, EXCEPT a requested cancel** (ADR 0103 rule 7, ADR 0104 rule 7) — read
|
|
3456
|
+
* through `shut()` and never as a second copy of its disjunction (ADR 0103 rule 1; W173's conformity reader, F7), so a
|
|
3457
|
+
* halt W174 adds to `shut()` holds here too. **W174's stop never reaches this loop taken** (`RN-R33`): a stop is taken
|
|
3458
|
+
* at rest, and a stop called on this stack — on the `resumed` element, or in the replay's lost tail — is PENDING until
|
|
3459
|
+
* `resume()` returns, so each task is handed on here and the stop then cancels it; and the replay's own fed stop is
|
|
3460
|
+
* cleared before the loop. A request is the one halt that hands a task on: its tasks are `dispatched` and unsettled,
|
|
3461
|
+
* and only the scheduler can settle them, so each is handed on and cancelled (`handOn`).
|
|
3462
|
+
*/
|
|
3463
|
+
holdsResume() {
|
|
3464
|
+
return this.shut() && this.cancelRequest === null;
|
|
3465
|
+
}
|
|
3466
|
+
/**
|
|
3467
|
+
* **The driver's own step ends** — each of `start`, `resume`, `resolve` and a scheduler event counts itself out here.
|
|
3468
|
+
* At rest again, a stop called on that step is TAKEN (`RN-R33`, W174): the step's routing is whole in the log, and
|
|
3469
|
+
* every task it handed on is in flight for the stop to cancel. **And a pause or an unpause called on it** (`RN-R35`,
|
|
3470
|
+
* W175), each in call order and BEFORE the stop — a pause or an unpause after a stop is refused, so any pending one was
|
|
3471
|
+
* called first. Each take is a step of its own and ends here too, so the next pending call is taken after it.
|
|
3472
|
+
* **And a resolution given on the step** (`RN-R37`, W180), in the same queue and the same call order.
|
|
3473
|
+
* **And FIRST, every `attempted` element heard on the step** (`RN-R40`, W186): the attempts started before anything
|
|
3474
|
+
* taken here, and a stop taken before them would leave them unrecorded — the driver deaf — under P36 (h)'s *a stopped
|
|
3475
|
+
* attempt counts*.
|
|
3476
|
+
*/
|
|
3477
|
+
leaveStep() {
|
|
3478
|
+
this.stepping -= 1;
|
|
3479
|
+
if (this.stepping === 0 && this.attemptsPending.length > 0) this.takeAttempts();
|
|
3480
|
+
while (this.stepping === 0 && this.pausePending.length > 0) this.takePending(this.pausePending.shift());
|
|
3481
|
+
if (this.stepping === 0 && this.stopRequest !== null && this.stopRequest.pending !== null) this.takeStop();
|
|
3482
|
+
}
|
|
3483
|
+
/**
|
|
3484
|
+
* **Every `attempted` element heard and not yet appended, appended — AT REST, as ONE step of their own** (`RN-R40`,
|
|
3485
|
+
* W186): in the order heard, each BETWEEN two of the driver's steps, where a replay feeds it (`RN-R19`). A step, so a
|
|
3486
|
+
* call a consumer makes from a callback on one — a pause, a stop — is taken after the last of them. **Appended on a
|
|
3487
|
+
* halted run too**, as a settlement is: the scheduler started the attempt whatever the run's routing does. A consumer's
|
|
3488
|
+
* `onFoldInput` THROWING on one HALTS the run, reported once (`OR-R10` v0.19, W184), and the throw reaches whoever is on
|
|
3489
|
+
* the stack — a driver call's caller, or the listener boundary of a scheduler event's step; the elements still pending
|
|
3490
|
+
* behind it are appended by this step's own end, SILENTLY (the run is halted, its failure reported), and so is a throw
|
|
3491
|
+
* on any of them once the run is halted.
|
|
3492
|
+
*/
|
|
3493
|
+
takeAttempts() {
|
|
3494
|
+
this.stepping += 1;
|
|
3495
|
+
try {
|
|
3496
|
+
while (this.attemptsPending.length > 0) {
|
|
3497
|
+
const a = this.attemptsPending.shift();
|
|
3498
|
+
const silent = this.failed;
|
|
3499
|
+
try {
|
|
3500
|
+
this.append({ kind: "attempted", taskId: a.taskId, attemptNumber: a.attemptNumber });
|
|
3501
|
+
} catch (e) {
|
|
3502
|
+
if (silent) continue;
|
|
3503
|
+
this.haltOnce(e);
|
|
3504
|
+
throw e;
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
} finally {
|
|
3508
|
+
this.leaveStep();
|
|
3509
|
+
}
|
|
3510
|
+
}
|
|
3511
|
+
/**
|
|
3512
|
+
* **The ONE rule for a throw that reaches a driver call's boundary once the call is ACCEPTED** — `OR-R10` v0.19, P35 (k),
|
|
3513
|
+
* P36 (b) and (c), W184: reported ONCE through `onRunFailure` under its own class, and the run HALTED — its `halted`
|
|
3514
|
+
* element appended after the element thrown on — unless the run is already halted, whose halt was reported when it was
|
|
3515
|
+
* raised. `start()`'s treatment since W3, brought to every call: `resolve`, `resume` and `cancel` at their own element,
|
|
3516
|
+
* 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
|
|
3517
|
+
* the FIRST throw halts and a call still pending behind it is dropped by its own halted guard (the readers' C1). The
|
|
3518
|
+
* caller rethrows: the consumer's error reaches it.
|
|
3519
|
+
* A refusal the call raises BEFORE it is accepted — `RunControlRefused`, `BoundaryRefused`, `ResumeRefused`,
|
|
3520
|
+
* `MultipleRuns`, a `DataCloneError` — never reaches this: nothing was appended, and a rejected command halts nothing.
|
|
3521
|
+
*/
|
|
3522
|
+
haltOnce(e) {
|
|
3523
|
+
if (!this.failed) this.failRun(e);
|
|
3524
|
+
}
|
|
2649
3525
|
/** 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
3526
|
nodeTypes;
|
|
2651
3527
|
/** The registry's languages, normalised once (W61). Read at construction only; the driver evaluates nothing. */
|
|
@@ -2654,6 +3530,11 @@ var Driver = class {
|
|
|
2654
3530
|
workers;
|
|
2655
3531
|
/** 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
3532
|
gateTypes;
|
|
3533
|
+
/**
|
|
3534
|
+
* **Each gate's RESOLVED route set, by gate id** — ADR 0092 rule 3: `routesOf` asked once per gate at
|
|
3535
|
+
* construction, after validation passed, and read by every decision. Never the type's members.
|
|
3536
|
+
*/
|
|
3537
|
+
gateRoutes;
|
|
2657
3538
|
/**
|
|
2658
3539
|
* **Each gate's MEMORY: one per gate per RUN, the driver's, and never written down** — `RN-R26`,
|
|
2659
3540
|
* [ADR 0067](../../doc/decisions/0067-a-gate-runs-in-the-driver-its-memory-is-the-drivers-and-its-decision-is-recorded.md),
|
|
@@ -2726,25 +3607,35 @@ var Driver = class {
|
|
|
2726
3607
|
...opts.gateTypes !== void 0 ? { gateTypes: opts.gateTypes } : {}
|
|
2727
3608
|
};
|
|
2728
3609
|
const findings2 = validateDefinition(definition, registry);
|
|
2729
|
-
if (findings2.length)
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
);
|
|
3610
|
+
if (findings2.length) throw InvalidDefinition.fromFindings(definition.flowId, findings2);
|
|
3611
|
+
const gateRoutes = /* @__PURE__ */ new Map();
|
|
3612
|
+
for (const n of definition.nodes) {
|
|
3613
|
+
if (n.kind !== "gate") continue;
|
|
3614
|
+
const resolved = routesOf(this.gateTypes.get(n.type), n.settings ?? {});
|
|
3615
|
+
if ("problem" in resolved) {
|
|
3616
|
+
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}`);
|
|
3617
|
+
}
|
|
3618
|
+
gateRoutes.set(n.id, resolved);
|
|
2734
3619
|
}
|
|
3620
|
+
this.gateRoutes = gateRoutes;
|
|
2735
3621
|
this.def = definition;
|
|
2736
3622
|
this.opts = opts;
|
|
2737
3623
|
this.now = opts.now ?? (() => Date.now());
|
|
2738
3624
|
this.graph = buildGraph(definition);
|
|
2739
3625
|
this.regions = allRegions(this.graph, definition);
|
|
3626
|
+
const kept = itemMaps(definition, this.regions);
|
|
3627
|
+
this.items = { kept, within: itemWithin(kept, this.regions), held: /* @__PURE__ */ new Map(), nested: /* @__PURE__ */ new Map(), ended: /* @__PURE__ */ new Set() };
|
|
2740
3628
|
if (this.regions.length > 0 && !opts.joinPolicy) {
|
|
2741
3629
|
throw new MissingJoinPolicy(
|
|
2742
3630
|
`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
3631
|
);
|
|
2744
3632
|
}
|
|
2745
|
-
for (const name of
|
|
3633
|
+
for (const name of SUBSCRIBED_EVENTS) {
|
|
2746
3634
|
opts.scheduler.addEventListener(name, (e) => this.onEvent(name, detailOf(e)));
|
|
2747
3635
|
}
|
|
3636
|
+
for (const [name, callback] of PASS_THROUGH) {
|
|
3637
|
+
if (opts[callback] !== void 0) opts.scheduler.addEventListener(name, (e) => this.onEvent(name, detailOf(e)));
|
|
3638
|
+
}
|
|
2748
3639
|
}
|
|
2749
3640
|
/**
|
|
2750
3641
|
* Dispatch the entry node for the run this driver was constructed for.
|
|
@@ -2771,6 +3662,15 @@ var Driver = class {
|
|
|
2771
3662
|
* substitution rather than dropping it.
|
|
2772
3663
|
*/
|
|
2773
3664
|
start(payload) {
|
|
3665
|
+
this.stepping += 1;
|
|
3666
|
+
try {
|
|
3667
|
+
this.startStep(payload);
|
|
3668
|
+
} finally {
|
|
3669
|
+
this.leaveStep();
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
/** `start`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
|
|
3673
|
+
startStep(payload) {
|
|
2774
3674
|
const runId = this.runId;
|
|
2775
3675
|
if (this.started) {
|
|
2776
3676
|
throw new MultipleRuns(
|
|
@@ -2781,11 +3681,12 @@ var Driver = class {
|
|
|
2781
3681
|
for (const { node, value } of boundaryTargets(this.def, payload, (g, v) => this.boundaryEdges(g, v))) {
|
|
2782
3682
|
const n = this.graph.byId.get(node);
|
|
2783
3683
|
if (!n || n.kind !== "task") continue;
|
|
3684
|
+
if (n.transform !== void 0) continue;
|
|
2784
3685
|
const type = this.nodeTypes.get(n.type);
|
|
2785
3686
|
const worker = type?.worker === void 0 ? void 0 : this.workers.get(type.worker);
|
|
2786
3687
|
const schema = worker?.inputSchema;
|
|
2787
3688
|
if (schema === void 0) continue;
|
|
2788
|
-
const received = this.compose(n, value,
|
|
3689
|
+
const received = this.compose(n, value, EMPTY_INDICES, payload, () => value);
|
|
2789
3690
|
const fs = findings(schema, received);
|
|
2790
3691
|
if (fs.length) {
|
|
2791
3692
|
throw new BoundaryRefused(
|
|
@@ -2799,12 +3700,23 @@ var Driver = class {
|
|
|
2799
3700
|
}
|
|
2800
3701
|
}
|
|
2801
3702
|
const entryPayload = structuredClone(payload);
|
|
2802
|
-
|
|
3703
|
+
const header = { format: CHECKPOINT_FORMAT, runId, flowId: this.def.flowId, version: this.def.version, entryPayload };
|
|
3704
|
+
const onHeader = this.opts.onHeader;
|
|
3705
|
+
if (onHeader !== void 0) {
|
|
3706
|
+
this.started = true;
|
|
3707
|
+
try {
|
|
3708
|
+
onHeader(Object.freeze({ ...header, entryPayload: structuredClone(entryPayload) }));
|
|
3709
|
+
} catch (e) {
|
|
3710
|
+
this.started = false;
|
|
3711
|
+
throw e;
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
this.header = header;
|
|
2803
3715
|
this.started = true;
|
|
2804
3716
|
try {
|
|
2805
3717
|
this.enter(runId, this.def.entry, payload);
|
|
2806
3718
|
} catch (e) {
|
|
2807
|
-
this.
|
|
3719
|
+
this.haltOnce(e);
|
|
2808
3720
|
throw e;
|
|
2809
3721
|
}
|
|
2810
3722
|
}
|
|
@@ -2819,28 +3731,43 @@ var Driver = class {
|
|
|
2819
3731
|
* **In order, and each step is a rule:**
|
|
2820
3732
|
* 1. **Refuse what the header says** (`RN-R23`) — {@link resumeRefusal}.
|
|
2821
3733
|
* 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
|
|
3734
|
+
* `start()` enters it, then FEED every stored settlement, resolution and `resumed` element — and, since
|
|
3735
|
+
* 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
3736
|
* other element the replay REGENERATES.
|
|
2825
3737
|
* 3. **The PREFIX check** (`RN-R20`): each regenerated element at a position the stored events cover
|
|
2826
3738
|
* must be the stored one; the first that is not is refused as `diverged`, naming its position, and
|
|
2827
3739
|
* so is a replay that ends short. A replay that runs PAST the stored events is the stored run's
|
|
2828
3740
|
* lost tail — a checkpoint taken inside `onFoldInput` ends mid-step (ADR 0064) — and it is
|
|
2829
3741
|
* DELIVERED, because the consumer does not hold it (`RN-R24`).
|
|
2830
|
-
* 4. **A run the replay
|
|
3742
|
+
* 4. **A run the replay HALTS is refused** as `halted` (P23 decision 10), its tail delivered first.
|
|
2831
3743
|
* 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
|
|
3744
|
+
* (`RN-R21`, `RN-R22`) — and then hand each to the scheduler at the SAME task id, with `attemptsSpent` the highest
|
|
3745
|
+
* attempt recorded for it where one was (v0.26, W186); or, where the replay
|
|
3746
|
+
* ends PAUSED (a fed `paused`, no `unpaused` after it), HOLD each again for an unpause (`RN-R35`).
|
|
2833
3747
|
*
|
|
2834
3748
|
* **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
|
-
*
|
|
3749
|
+
* `resumed` may have finished in the process that is gone, and runs again here. **Not at a fresh retry budget since
|
|
3750
|
+
* W186** (`RN-R21` v0.26, ADR 0113): it is handed on with `attemptsSpent`, the highest attempt the stored run recorded
|
|
3751
|
+
* 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.
|
|
3752
|
+
* *Until W186 it ran at a FRESH budget ([Q365](../../doc/questions/q365.md) — measured: a budget of two attempts ran
|
|
3753
|
+
* three times across one kill).* A task parked at `user_action` has SETTLED and is not in flight: it stays parked,
|
|
2838
3754
|
* and `resolve()` works on it.
|
|
2839
3755
|
*
|
|
2840
3756
|
* @throws MultipleRuns — the driver has already started or resumed a run.
|
|
2841
3757
|
* @throws ResumeRefused — see {@link ResumeRefusalReason}; the driver is then spent.
|
|
3758
|
+
* @throws the consumer's own error — a callback that threw on the `resumed` element, or on an element the resume's step
|
|
3759
|
+
* appended after it: the run is HALTED and reported once, and nothing further is handed on (P36 (c), W184).
|
|
2842
3760
|
*/
|
|
2843
3761
|
resume(checkpoint) {
|
|
3762
|
+
this.stepping += 1;
|
|
3763
|
+
try {
|
|
3764
|
+
this.resumeStep(checkpoint);
|
|
3765
|
+
} finally {
|
|
3766
|
+
this.leaveStep();
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
/** `resume`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
|
|
3770
|
+
resumeStep(checkpoint) {
|
|
2844
3771
|
if (this.started) {
|
|
2845
3772
|
throw new MultipleRuns(
|
|
2846
3773
|
`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 +3796,49 @@ var Driver = class {
|
|
|
2869
3796
|
this.replay = null;
|
|
2870
3797
|
if (this.failed) {
|
|
2871
3798
|
this.deaf = true;
|
|
2872
|
-
const at = this.inputs.findIndex((e) => e.kind === "
|
|
3799
|
+
const at = this.inputs.findIndex((e) => e.kind === "halted");
|
|
2873
3800
|
throw new ResumeRefused(
|
|
2874
|
-
"
|
|
3801
|
+
"halted",
|
|
2875
3802
|
at,
|
|
2876
|
-
`ResumeRefused (
|
|
3803
|
+
`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
3804
|
);
|
|
2878
3805
|
}
|
|
3806
|
+
if (this.stopRequest !== null && this.stopRequest.pending === null) this.stopRequest = null;
|
|
2879
3807
|
const inFlight = this.inFlight();
|
|
2880
|
-
|
|
2881
|
-
|
|
3808
|
+
const holding = this.paused;
|
|
3809
|
+
for (const id of inFlight) {
|
|
3810
|
+
const config = replay.held.get(id);
|
|
3811
|
+
const spent = this.lastAttempt.get(id) ?? 0;
|
|
3812
|
+
if (config !== void 0 && spent > 0) replay.held.set(id, { ...config, attemptsSpent: spent });
|
|
3813
|
+
}
|
|
3814
|
+
if (holding) {
|
|
2882
3815
|
for (const id of inFlight) {
|
|
2883
3816
|
const config = replay.held.get(id);
|
|
2884
3817
|
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.
|
|
3818
|
+
this.pauseHeld.set(id, config);
|
|
3819
|
+
}
|
|
3820
|
+
} else {
|
|
3821
|
+
for (const id of inFlight) this.unhanded.add(id);
|
|
3822
|
+
}
|
|
3823
|
+
try {
|
|
3824
|
+
this.append({ kind: "resumed", taskIds: inFlight });
|
|
3825
|
+
for (const id of holding ? [] : inFlight) {
|
|
3826
|
+
if (this.holdsResume()) break;
|
|
3827
|
+
const config = replay.held.get(id);
|
|
3828
|
+
if (config === void 0) throw new Error(`resume: in-flight task "${id}" has no held dispatch \u2014 this is a defect in the driver`);
|
|
3829
|
+
this.handOn(id, config);
|
|
2886
3830
|
}
|
|
2887
3831
|
} catch (e) {
|
|
2888
|
-
this.
|
|
3832
|
+
this.haltOnce(e);
|
|
2889
3833
|
throw e;
|
|
3834
|
+
} finally {
|
|
3835
|
+
this.unhanded.clear();
|
|
2890
3836
|
}
|
|
2891
3837
|
}
|
|
2892
3838
|
/**
|
|
2893
3839
|
* **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
|
|
3840
|
+
* turns a refusal routing raises into a `halted` element exactly as the listener's boundary does
|
|
3841
|
+
* (`OR-R10`), so the replay regenerates a halt where the stored run had one.
|
|
2896
3842
|
*/
|
|
2897
3843
|
replayStored(stored) {
|
|
2898
3844
|
const replay = this.replay;
|
|
@@ -2906,16 +3852,35 @@ var Driver = class {
|
|
|
2906
3852
|
}
|
|
2907
3853
|
if (this.inputs.length < stored.length) throw this.diverged(this.inputs.length, "the replay ended before it");
|
|
2908
3854
|
}
|
|
2909
|
-
/** One fed element — a settlement, a resolution,
|
|
3855
|
+
/** One fed element — a settlement, a resolution, an earlier resume, a stop, a pause, an unpause or an attempt — at stored position `i`. */
|
|
2910
3856
|
feedOne(e, i) {
|
|
2911
3857
|
if (e.kind === "resumed") {
|
|
2912
3858
|
const now = this.inFlight();
|
|
2913
3859
|
if (!Array.isArray(e.taskIds) || e.taskIds.length !== now.length || e.taskIds.some((t, k) => t !== now[k])) {
|
|
2914
3860
|
throw this.diverged(i, `in flight in the replay here: ${JSON.stringify(now)}`);
|
|
2915
3861
|
}
|
|
3862
|
+
this.stopRequest = null;
|
|
2916
3863
|
this.append({ kind: "resumed", taskIds: [...e.taskIds] });
|
|
2917
3864
|
return;
|
|
2918
3865
|
}
|
|
3866
|
+
if (e.kind === "interrupted") {
|
|
3867
|
+
this.feedInterrupted(e);
|
|
3868
|
+
return;
|
|
3869
|
+
}
|
|
3870
|
+
if (e.kind === "paused" || e.kind === "unpaused") {
|
|
3871
|
+
this.paused = e.kind === "paused";
|
|
3872
|
+
this.append({ kind: e.kind, actor: e.actor });
|
|
3873
|
+
return;
|
|
3874
|
+
}
|
|
3875
|
+
if (e.kind === "attempted") {
|
|
3876
|
+
const n = e.attemptNumber;
|
|
3877
|
+
if (typeof e.taskId !== "string" || !this.dispatched.has(e.taskId)) throw this.diverged(i, "the replay never dispatched that task");
|
|
3878
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 1) throw this.diverged(i, "its attemptNumber is not a positive integer");
|
|
3879
|
+
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");
|
|
3880
|
+
this.lastAttempt.set(e.taskId, n);
|
|
3881
|
+
this.append({ kind: "attempted", taskId: e.taskId, attemptNumber: n });
|
|
3882
|
+
return;
|
|
3883
|
+
}
|
|
2919
3884
|
if (e.kind !== "outcome" && e.kind !== "resolution") return;
|
|
2920
3885
|
const record = e.record;
|
|
2921
3886
|
let parts;
|
|
@@ -2932,7 +3897,7 @@ var Driver = class {
|
|
|
2932
3897
|
this.settle(cloneRecord(record), runId, nodeId, branchPath);
|
|
2933
3898
|
return;
|
|
2934
3899
|
}
|
|
2935
|
-
if (!
|
|
3900
|
+
if (!this.parked(record.taskId)) throw this.diverged(i, "that task is not parked in the replay");
|
|
2936
3901
|
this.appendResolution(cloneRecord(record), nodeId, branchPath);
|
|
2937
3902
|
this.advance(runId, nodeId, branchPath);
|
|
2938
3903
|
}
|
|
@@ -2960,9 +3925,7 @@ var Driver = class {
|
|
|
2960
3925
|
* handing it to a worker again would run what a person was asked to decide.
|
|
2961
3926
|
*/
|
|
2962
3927
|
inFlight() {
|
|
2963
|
-
|
|
2964
|
-
for (const r of this.log) if (r.kind === "outcome") settled.add(r.taskId);
|
|
2965
|
-
return [...this.dispatched].filter((id) => !settled.has(id));
|
|
3928
|
+
return [...this.dispatched].filter((id) => !this.taskLog.of(id).some((r) => r.kind === "outcome"));
|
|
2966
3929
|
}
|
|
2967
3930
|
/**
|
|
2968
3931
|
* **Whether a callback fires for the element at `seq`** — `RN-R24`. Always, except during a replay,
|
|
@@ -2979,14 +3942,43 @@ var Driver = class {
|
|
|
2979
3942
|
* second resolution is refused like any other. A rejected command, not a run
|
|
2980
3943
|
* failure: it came from outside and a caller can retry.
|
|
2981
3944
|
*
|
|
3945
|
+
* **And an ASK's answer** — `RS-R1` v0.18, `RN-R37`, ADR 0109: an ask the run reached and nobody has answered is
|
|
3946
|
+
* suspended too, so the person's answer is this call — `'done'` with the answer as `result`, held to the ask's
|
|
3947
|
+
* `answerSchema` (`FD-R4`), or `'failed'` with an `error`, the person DECLINING, held to nothing. The answer IS the
|
|
3948
|
+
* node's result and routes as a task's settlement does.
|
|
3949
|
+
*
|
|
2982
3950
|
* @returns `false` when refused. Nothing is appended and the run is unaffected.
|
|
2983
3951
|
*/
|
|
2984
3952
|
resolve(taskId, resolvedAs, value) {
|
|
2985
|
-
|
|
3953
|
+
const midStep = this.stepping > 0;
|
|
3954
|
+
this.stepping += 1;
|
|
3955
|
+
try {
|
|
3956
|
+
return this.resolveStep(taskId, resolvedAs, value, midStep);
|
|
3957
|
+
} finally {
|
|
3958
|
+
this.leaveStep();
|
|
3959
|
+
}
|
|
3960
|
+
}
|
|
3961
|
+
/** `resolve`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
|
|
3962
|
+
resolveStep(taskId, resolvedAs, value, midStep = false) {
|
|
3963
|
+
if (!this.parked(taskId)) return false;
|
|
3964
|
+
for (const c of this.pausePending) if (c.kind === "answer" && c.record.taskId === taskId) return false;
|
|
2986
3965
|
const { runId, nodeId, branchPath } = parseTaskId(taskId);
|
|
2987
|
-
if (this.
|
|
3966
|
+
if (this.shut() || this.stopRequest !== null) return false;
|
|
2988
3967
|
if (resolvedAs === "done" && this.opts.validateBoundaries !== false) {
|
|
2989
3968
|
const n = this.graph.byId.get(nodeId);
|
|
3969
|
+
if (n?.kind === "ask") {
|
|
3970
|
+
const schema2 = n.answerSchema;
|
|
3971
|
+
const fs = schema2 === void 0 ? [] : findings(schema2, value.result ?? null);
|
|
3972
|
+
if (fs.length) {
|
|
3973
|
+
throw new BoundaryRefused(
|
|
3974
|
+
"resolve",
|
|
3975
|
+
nodeId,
|
|
3976
|
+
taskId,
|
|
3977
|
+
fs,
|
|
3978
|
+
`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.`
|
|
3979
|
+
);
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
2990
3982
|
const type = n?.kind === "task" ? this.nodeTypes.get(n.type) : void 0;
|
|
2991
3983
|
const schema = type?.worker === void 0 ? void 0 : this.workers.get(type.worker)?.outputSchema;
|
|
2992
3984
|
if (schema !== void 0) {
|
|
@@ -3002,7 +3994,7 @@ var Driver = class {
|
|
|
3002
3994
|
}
|
|
3003
3995
|
}
|
|
3004
3996
|
}
|
|
3005
|
-
|
|
3997
|
+
const record = {
|
|
3006
3998
|
kind: "resolution",
|
|
3007
3999
|
taskId,
|
|
3008
4000
|
resolvedAs,
|
|
@@ -3010,45 +4002,597 @@ var Driver = class {
|
|
|
3010
4002
|
error: resolvedAs === "failed" ? value.error ?? "resolved as failed" : null,
|
|
3011
4003
|
actor: value.actor,
|
|
3012
4004
|
observedAt: this.now()
|
|
3013
|
-
}
|
|
4005
|
+
};
|
|
4006
|
+
if (midStep) {
|
|
4007
|
+
this.pausePending.push({ kind: "answer", record, runId, nodeId, branchPath });
|
|
4008
|
+
return true;
|
|
4009
|
+
}
|
|
4010
|
+
try {
|
|
4011
|
+
this.appendResolution(record, nodeId, branchPath);
|
|
4012
|
+
} catch (e) {
|
|
4013
|
+
this.haltOnce(e);
|
|
4014
|
+
throw e;
|
|
4015
|
+
}
|
|
4016
|
+
if (this.shut()) return true;
|
|
4017
|
+
try {
|
|
4018
|
+
this.advance(runId, nodeId, branchPath);
|
|
4019
|
+
} catch (e) {
|
|
4020
|
+
this.haltOnce(e);
|
|
4021
|
+
throw e;
|
|
4022
|
+
}
|
|
4023
|
+
return true;
|
|
4024
|
+
}
|
|
4025
|
+
/**
|
|
4026
|
+
* **A resolution appended, and the region told** — the half of `resolve()` the REPLAY shares
|
|
4027
|
+
* (`RN-R19`): a stored resolution is appended as it was stored and routes exactly as the live one
|
|
4028
|
+
* did, so a resumed run's `branch.resumed` and everything after it are regenerated rather than
|
|
4029
|
+
* written by a second road.
|
|
4030
|
+
*/
|
|
4031
|
+
appendResolution(record, nodeId, branchPath) {
|
|
4032
|
+
this.append({ kind: "resolution", record });
|
|
4033
|
+
if (this.shut()) return;
|
|
4034
|
+
const region = this.regionOf(nodeId);
|
|
4035
|
+
if (region) {
|
|
4036
|
+
this.emit({
|
|
4037
|
+
type: "branch.resumed",
|
|
4038
|
+
regionId: region.fanoutId,
|
|
4039
|
+
regionPath: this.rpathOf(region, this.indicesFrom(nodeId, branchPath)),
|
|
4040
|
+
branchPath,
|
|
4041
|
+
at: nodeId,
|
|
4042
|
+
resolvedAs: record.resolvedAs
|
|
4043
|
+
});
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
/**
|
|
4047
|
+
* **Cancel the run: hold it, record the request, and ask the scheduler to cancel each task in flight** — `RN-R31`,
|
|
4048
|
+
* `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),
|
|
4049
|
+
* P34 decision 4 and stop-and-asks (b), (c), (i), (j). **Resolves once every task it cancelled has stopped**; the fold
|
|
4050
|
+
* reads the run `cancelling` until then and `cancelled` after (`RN-R30`), so a runtime's `done` settles after the last
|
|
4051
|
+
* `task.cancelled` from the fold alone.
|
|
4052
|
+
*
|
|
4053
|
+
* **In order, and each step is a rule:**
|
|
4054
|
+
* 1. **Refuse** — {@link RunControlRefused}, before anything is recorded or sent; and a `graceMs` the scheduler
|
|
4055
|
+
* would throw on is thrown here, as it would be (`TypeError`, `RangeError`).
|
|
4056
|
+
* 2. **Hold** (`RN-R29`): `cancelRequest` is set, so `shut()` is true for every door; every region's collection is
|
|
4057
|
+
* released, `failRun`'s run-level moment (ADR 0046 rule 3, `JP-R12`) — no join of this run settles again.
|
|
4058
|
+
* 3. **Record** ONE `cancelRequested` element, BEFORE the scheduler is asked for anything.
|
|
4059
|
+
* 4. **Cancel** each task in flight the scheduler holds — `RN-R21`'s set, so a PARKED task, settled already, is sent
|
|
4060
|
+
* nothing (P34 (i); the scheduler cannot end it, fact 8). A task not yet handed on is cancelled by `handOn` after
|
|
4061
|
+
* its hand-off (`RN-R29` (i)), and so is each task a PAUSE held (`RN-R35`, P34 (c)). Tasks a worker SPAWNED are
|
|
4062
|
+
* not this run's (P34 (j)). **In a `finally` around step 3** (`RN-R31` v0.21, P35 decision 4): a consumer's
|
|
4063
|
+
* `onFoldInput` that throws on the element still has every cancel sent and every held task handed on and
|
|
4064
|
+
* cancelled — and THEN the run is HALTED and reported once, and the throw reaches the caller (`RN-R31` v0.24,
|
|
4065
|
+
* `OR-R10` v0.19, P36 (b), W184).
|
|
4066
|
+
*
|
|
4067
|
+
* **A SECOND call** appends nothing and forwards its `graceMs` to each task still stopping — the scheduler's own
|
|
4068
|
+
* escalation — and resolves once those have stopped. It is never refused for the halt its own first call made.
|
|
4069
|
+
*
|
|
4070
|
+
* @throws RunControlRefused — `unsupported`, `not-started`, `halted`, `cancelled` or `ended`; nothing recorded or sent.
|
|
4071
|
+
* @throws TypeError / RangeError — `graceMs` not a number, or negative or `NaN`; nothing recorded or sent.
|
|
4072
|
+
*/
|
|
4073
|
+
cancel(options) {
|
|
4074
|
+
const cancelOf = this.opts.scheduler.cancel;
|
|
4075
|
+
if (typeof cancelOf !== "function") {
|
|
4076
|
+
throw new RunControlRefused(
|
|
4077
|
+
"cancel",
|
|
4078
|
+
"unsupported",
|
|
4079
|
+
`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.`
|
|
4080
|
+
);
|
|
4081
|
+
}
|
|
4082
|
+
const graceMs = options?.graceMs;
|
|
4083
|
+
if (graceMs !== void 0) {
|
|
4084
|
+
if (typeof graceMs !== "number") throw new TypeError(`cancel: graceMs must be a number of ms, got ${typeof graceMs} \u2014 nothing was recorded`);
|
|
4085
|
+
if (Number.isNaN(graceMs) || graceMs < 0) throw new RangeError(`cancel: graceMs must be 0 or more, got ${graceMs} \u2014 nothing was recorded`);
|
|
4086
|
+
}
|
|
4087
|
+
if (this.header === null) {
|
|
4088
|
+
throw new RunControlRefused(
|
|
4089
|
+
"cancel",
|
|
4090
|
+
"not-started",
|
|
4091
|
+
`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.`
|
|
4092
|
+
);
|
|
4093
|
+
}
|
|
4094
|
+
const req = this.cancelRequest;
|
|
4095
|
+
if (req !== null) {
|
|
4096
|
+
if (graceMs !== void 0) req.graceMs = graceMs;
|
|
4097
|
+
for (const id of this.inFlight()) if (this.handedOn(id)) this.cancelTask(id);
|
|
4098
|
+
const stopping2 = req.stopping;
|
|
4099
|
+
return Promise.resolve().then(() => Promise.all([...stopping2.values()])).then(() => void 0);
|
|
4100
|
+
}
|
|
4101
|
+
if (this.failed) {
|
|
4102
|
+
throw new RunControlRefused(
|
|
4103
|
+
"cancel",
|
|
4104
|
+
"halted",
|
|
4105
|
+
`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.`
|
|
4106
|
+
);
|
|
4107
|
+
}
|
|
4108
|
+
if (this.cancelled) {
|
|
4109
|
+
throw new RunControlRefused(
|
|
4110
|
+
"cancel",
|
|
4111
|
+
"cancelled",
|
|
4112
|
+
`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.`
|
|
4113
|
+
);
|
|
4114
|
+
}
|
|
4115
|
+
if (this.stopRequest !== null) {
|
|
4116
|
+
throw new RunControlRefused(
|
|
4117
|
+
"cancel",
|
|
4118
|
+
"interrupted",
|
|
4119
|
+
`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.`
|
|
4120
|
+
);
|
|
4121
|
+
}
|
|
4122
|
+
const inFlight = this.inFlight();
|
|
4123
|
+
if (this.stepping === 0 && inFlight.length === 0 && !this.hasParked()) {
|
|
4124
|
+
throw new RunControlRefused(
|
|
4125
|
+
"cancel",
|
|
4126
|
+
"ended",
|
|
4127
|
+
`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.`
|
|
4128
|
+
);
|
|
4129
|
+
}
|
|
4130
|
+
const reason = options.reason;
|
|
4131
|
+
const stopping = /* @__PURE__ */ new Map();
|
|
4132
|
+
this.cancelRequest = {
|
|
4133
|
+
...reason !== void 0 ? { reason } : {},
|
|
4134
|
+
...graceMs !== void 0 ? { graceMs } : {},
|
|
4135
|
+
stopping
|
|
4136
|
+
};
|
|
4137
|
+
this.arrivals.clear();
|
|
4138
|
+
this.releaseAllItems();
|
|
4139
|
+
const released = new Map(this.pauseHeld);
|
|
4140
|
+
this.pauseHeld.clear();
|
|
4141
|
+
for (const id of released.keys()) this.unhanded.add(id);
|
|
4142
|
+
try {
|
|
4143
|
+
try {
|
|
4144
|
+
this.append({ kind: "cancelRequested", actor: options.actor, reason: reason ?? null });
|
|
4145
|
+
} finally {
|
|
4146
|
+
try {
|
|
4147
|
+
for (const id of this.inFlight()) {
|
|
4148
|
+
const config = released.get(id);
|
|
4149
|
+
if (config !== void 0) this.handOn(id, config);
|
|
4150
|
+
else if (this.handedOn(id)) this.cancelTask(id);
|
|
4151
|
+
}
|
|
4152
|
+
} finally {
|
|
4153
|
+
for (const id of released.keys()) this.unhanded.delete(id);
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
} catch (e) {
|
|
4157
|
+
this.haltOnce(e);
|
|
4158
|
+
throw e;
|
|
4159
|
+
}
|
|
4160
|
+
return Promise.resolve().then(() => Promise.all([...stopping.values()])).then(() => void 0);
|
|
4161
|
+
}
|
|
4162
|
+
/**
|
|
4163
|
+
* **Stop the run so that a resume ELSEWHERE can continue it: hold it, record the stop, go DEAF, and only then ask the
|
|
4164
|
+
* scheduler to cancel each task in flight** — `RN-R33`, `spec/run-state.md` section 14,
|
|
4165
|
+
* [ADR 0105](../../doc/decisions/0105-a-run-its-owner-stops-goes-deaf-before-it-cancels-and-resumes-elsewhere.md), P34
|
|
4166
|
+
* decisions 3 and 9 and stop-and-ask (d). **Resolves once every task it cancelled has stopped**; the fold reads the run
|
|
4167
|
+
* `interrupted` from the element on (`RN-R32`), and the log ends with the cancelled tasks dispatched and UNSETTLED —
|
|
4168
|
+
* the in-flight set a resume on another runtime re-dispatches at the same ids (`RN-R21`).
|
|
4169
|
+
*
|
|
4170
|
+
* **In order, and the ORDER is the rule** (P34 decision 3):
|
|
4171
|
+
* 1. **Refuse** — {@link RunControlRefused} with `control: 'stop'`, before anything is recorded or sent: `unsupported`,
|
|
4172
|
+
* `not-started`, `halted`, `cancelled` (`RN-R28`'s, or a cancel its owner requested — P34 (c)) and `ended` (at
|
|
4173
|
+
* rest only, as for a cancel); and a `graceMs` the scheduler would throw on is thrown here. From the accepted call
|
|
4174
|
+
* on, `resolve()` and `cancel()` are refused (P34 decision 9, (c)).
|
|
4175
|
+
* 2. **Hold** (`RN-R29`): the stop is TAKEN, so `shut()` is true for every door, and every region's collection is
|
|
4176
|
+
* released.
|
|
4177
|
+
* 3. **Go DEAF**: nothing the scheduler fires for this run is heard again — from BEFORE the element, so not even a
|
|
4178
|
+
* settlement a callback on it provokes — and no `task.cancelled` of the stop is appended.
|
|
4179
|
+
* 4. **Record** ONE `interrupted` element.
|
|
4180
|
+
* 5. **Cancel** each task in flight the scheduler holds — none for a parked task (P34 fact 4), and none for a task a
|
|
4181
|
+
* PAUSE held, which the scheduler was never handed and which stays in flight for the resume (`RN-R35`).
|
|
4182
|
+
*
|
|
4183
|
+
* **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
|
|
4184
|
+
* is made from a callback on that step (`stepping`). A hold taken mid-step would drop the rest of the step's routing
|
|
4185
|
+
* — a fan-out's later branches, a settlement's successor — and a resume re-dispatches only what is IN FLIGHT, so that
|
|
4186
|
+
* work would be lost to the resumed run for good. Taken at rest, the step is whole in the log; the task whose own
|
|
4187
|
+
* `dispatched` element carried the call is handed on in the step and cancelled here (`RN-R29` (i)), and so is a resumed
|
|
4188
|
+
* run's in-flight set, handed back before `resume()` returns.
|
|
4189
|
+
*
|
|
4190
|
+
* **A SECOND call** appends nothing, forwards its `graceMs` to each task still stopping, and resolves as the first does.
|
|
4191
|
+
*
|
|
4192
|
+
* @throws RunControlRefused — `unsupported`, `not-started`, `halted`, `cancelled` or `ended`; nothing recorded or sent.
|
|
4193
|
+
* @throws TypeError / RangeError — `graceMs` not a number, or negative or `NaN`; nothing recorded or sent.
|
|
4194
|
+
*/
|
|
4195
|
+
stop(options) {
|
|
4196
|
+
if (typeof this.opts.scheduler.cancel !== "function") {
|
|
4197
|
+
throw new RunControlRefused(
|
|
4198
|
+
"stop",
|
|
4199
|
+
"unsupported",
|
|
4200
|
+
`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.`
|
|
4201
|
+
);
|
|
4202
|
+
}
|
|
4203
|
+
const graceMs = options?.graceMs;
|
|
4204
|
+
if (graceMs !== void 0) {
|
|
4205
|
+
if (typeof graceMs !== "number") throw new TypeError(`stop: graceMs must be a number of ms, got ${typeof graceMs} \u2014 nothing was recorded`);
|
|
4206
|
+
if (Number.isNaN(graceMs) || graceMs < 0) throw new RangeError(`stop: graceMs must be 0 or more, got ${graceMs} \u2014 nothing was recorded`);
|
|
4207
|
+
}
|
|
4208
|
+
if (this.header === null) {
|
|
4209
|
+
throw new RunControlRefused(
|
|
4210
|
+
"stop",
|
|
4211
|
+
"not-started",
|
|
4212
|
+
`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.`
|
|
4213
|
+
);
|
|
4214
|
+
}
|
|
4215
|
+
const req = this.stopRequest;
|
|
4216
|
+
if (req !== null) {
|
|
4217
|
+
if (graceMs !== void 0) req.graceMs = graceMs;
|
|
4218
|
+
if (req.pending === null) {
|
|
4219
|
+
for (const id of this.inFlight()) if (this.handedOn(id) && req.stopping.has(id)) this.cancelTask(id);
|
|
4220
|
+
return this.stopSettled(req);
|
|
4221
|
+
}
|
|
4222
|
+
return req.took.then(() => this.stopSettled(req));
|
|
4223
|
+
}
|
|
4224
|
+
if (this.failed) {
|
|
4225
|
+
throw new RunControlRefused(
|
|
4226
|
+
"stop",
|
|
4227
|
+
"halted",
|
|
4228
|
+
`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.`
|
|
4229
|
+
);
|
|
4230
|
+
}
|
|
4231
|
+
if (this.cancelled || this.cancelRequest !== null) {
|
|
4232
|
+
throw new RunControlRefused(
|
|
4233
|
+
"stop",
|
|
4234
|
+
"cancelled",
|
|
4235
|
+
`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.`
|
|
4236
|
+
);
|
|
4237
|
+
}
|
|
4238
|
+
if (this.stepping === 0 && this.inFlight().length === 0 && !this.hasParked()) {
|
|
4239
|
+
throw new RunControlRefused(
|
|
4240
|
+
"stop",
|
|
4241
|
+
"ended",
|
|
4242
|
+
`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.`
|
|
4243
|
+
);
|
|
4244
|
+
}
|
|
4245
|
+
const reason = options.reason;
|
|
4246
|
+
let taken;
|
|
4247
|
+
const took = new Promise((r) => {
|
|
4248
|
+
taken = r;
|
|
4249
|
+
});
|
|
4250
|
+
const accepted = {
|
|
4251
|
+
actor: options.actor,
|
|
4252
|
+
...reason !== void 0 ? { reason } : {},
|
|
4253
|
+
...graceMs !== void 0 ? { graceMs } : {},
|
|
4254
|
+
stopping: /* @__PURE__ */ new Map(),
|
|
4255
|
+
pending: taken,
|
|
4256
|
+
took
|
|
4257
|
+
};
|
|
4258
|
+
this.stopRequest = accepted;
|
|
4259
|
+
if (this.stepping === 0) this.takeStop();
|
|
4260
|
+
return took.then(() => this.stopSettled(accepted));
|
|
4261
|
+
}
|
|
4262
|
+
/**
|
|
4263
|
+
* **The stop, TAKEN** — `RN-R33` steps 2-5, at rest: from `stop()` itself, or from the end of the driver's own step
|
|
4264
|
+
* when `stop()` was called on it (each step's wrapper calls this when `stepping` returns to `0`).
|
|
4265
|
+
*/
|
|
4266
|
+
takeStop() {
|
|
4267
|
+
const req = this.stopRequest;
|
|
4268
|
+
const taken = req.pending;
|
|
4269
|
+
req.pending = null;
|
|
4270
|
+
req.took = null;
|
|
4271
|
+
this.arrivals.clear();
|
|
4272
|
+
this.releaseAllItems();
|
|
4273
|
+
this.deaf = true;
|
|
4274
|
+
const inFlight = this.inFlight().filter((id) => this.handedOn(id));
|
|
4275
|
+
const silent = this.failed;
|
|
4276
|
+
try {
|
|
4277
|
+
try {
|
|
4278
|
+
this.append({ kind: "interrupted", actor: req.actor, reason: req.reason ?? null });
|
|
4279
|
+
} finally {
|
|
4280
|
+
try {
|
|
4281
|
+
for (const id of inFlight) this.cancelTask(id);
|
|
4282
|
+
} finally {
|
|
4283
|
+
taken();
|
|
4284
|
+
}
|
|
4285
|
+
}
|
|
4286
|
+
} catch (e) {
|
|
4287
|
+
if (silent) return;
|
|
4288
|
+
this.haltOnce(e);
|
|
4289
|
+
throw e;
|
|
4290
|
+
}
|
|
4291
|
+
}
|
|
4292
|
+
/** What a stop's promise resolves on once it is taken: a microtask on, every task it sent a cancel to has stopped. */
|
|
4293
|
+
stopSettled(req) {
|
|
4294
|
+
return Promise.resolve().then(() => Promise.all([...req.stopping.values()])).then(() => void 0);
|
|
4295
|
+
}
|
|
4296
|
+
/**
|
|
4297
|
+
* **Pause the run: it routes on, and every dispatch it makes from here is appended and HELD until `unpause()`** —
|
|
4298
|
+
* `RN-R35`, `spec/run-state.md` section 15,
|
|
4299
|
+
* [ADR 0106](../../doc/decisions/0106-a-run-its-owner-pauses-routes-on-and-holds-every-dispatch-until-unpaused.md),
|
|
4300
|
+
* P34 stop-and-ask (e). Tasks already handed to the scheduler run on and settle, and their settlements route as ever; a
|
|
4301
|
+
* person's `resolve()` is accepted. **Not a halt** (`shut()` is untouched): only the scheduler's `addTask` waits. The
|
|
4302
|
+
* fold reads `RunState.paused` and no new outcome — a held task is `dispatched`, so it reads `running` (P34 fact 3).
|
|
4303
|
+
*
|
|
4304
|
+
* **In order**: (1) **refused by name** before anything is recorded — {@link RunControlRefused} with `control: 'pause'`:
|
|
4305
|
+
* `not-started`, `halted`, `cancelled` (`RN-R28`'s or a requested cancel), `interrupted` (a stop, taken or pending),
|
|
4306
|
+
* `paused` (already paused, or a pause called earlier in this step still to be taken — P34 (e): *a second pause is
|
|
4307
|
+
* refused by name*) and `ended` (nothing in flight and nothing parked, at rest only); (2) **TAKEN AT REST** — at once
|
|
4308
|
+
* 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
|
|
4309
|
+
* `paused` element is appended, BETWEEN two steps, where a resume's replay can feed it (`RN-R19`). A pause taken
|
|
4310
|
+
* mid-step would land between two elements of one step, and the replay — which regenerates a step whole — would refuse
|
|
4311
|
+
* its checkpoint `diverged` there; so the step a pause is called on finishes unpaused, its dispatches handed on.
|
|
4312
|
+
*/
|
|
4313
|
+
pause(options) {
|
|
4314
|
+
this.refuseRunHeld("pause");
|
|
4315
|
+
if (this.pauseWanted()) {
|
|
4316
|
+
throw new RunControlRefused(
|
|
4317
|
+
"pause",
|
|
4318
|
+
"paused",
|
|
4319
|
+
`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.`
|
|
4320
|
+
);
|
|
4321
|
+
}
|
|
4322
|
+
if (this.stepping === 0 && this.inFlight().length === 0 && !this.hasParked()) {
|
|
4323
|
+
throw new RunControlRefused(
|
|
4324
|
+
"pause",
|
|
4325
|
+
"ended",
|
|
4326
|
+
`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.`
|
|
4327
|
+
);
|
|
4328
|
+
}
|
|
4329
|
+
this.pausePending.push({ kind: "paused", actor: options.actor });
|
|
4330
|
+
while (this.stepping === 0 && this.pausePending.length > 0) this.takePending(this.pausePending.shift());
|
|
4331
|
+
}
|
|
4332
|
+
/**
|
|
4333
|
+
* **Unpause the run: record it, then hand every held task to the scheduler in DISPATCH ORDER** — `RN-R35`, ADR 0106,
|
|
4334
|
+
* P34 (e). Refused by name — `control: 'unpause'`: `not-started`, `halted`, `cancelled`, `interrupted` (a stopped
|
|
4335
|
+
* run's held tasks are a resume's: unpause it where it resumes) and `not-paused`. **Never `ended`**: an unpause rewrites
|
|
4336
|
+
* no outcome, so a paused run that finished may still be unpaused. Taken at rest, as a pause is: ONE `unpaused` element,
|
|
4337
|
+
* then the hand-off — each held task's `createTask` and `addTask`, first held first.
|
|
4338
|
+
*/
|
|
4339
|
+
unpause(options) {
|
|
4340
|
+
this.refuseRunHeld("unpause");
|
|
4341
|
+
if (!this.pauseWanted()) {
|
|
4342
|
+
throw new RunControlRefused(
|
|
4343
|
+
"unpause",
|
|
4344
|
+
"not-paused",
|
|
4345
|
+
`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.`
|
|
4346
|
+
);
|
|
4347
|
+
}
|
|
4348
|
+
this.pausePending.push({ kind: "unpaused", actor: options.actor });
|
|
4349
|
+
while (this.stepping === 0 && this.pausePending.length > 0) this.takePending(this.pausePending.shift());
|
|
4350
|
+
}
|
|
4351
|
+
/** The pause the owner has ASKED for: the last call still to be taken, else the pause taken (`RN-R35`). */
|
|
4352
|
+
pauseWanted() {
|
|
4353
|
+
const last = [...this.pausePending].filter((c) => c.kind !== "answer").at(-1);
|
|
4354
|
+
return last !== void 0 ? last.kind === "paused" : this.paused;
|
|
4355
|
+
}
|
|
4356
|
+
/**
|
|
4357
|
+
* 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
|
|
4358
|
+
* release: none started, or the run halted, cancelled or stopped. Thrown before anything is recorded.
|
|
4359
|
+
*/
|
|
4360
|
+
refuseRunHeld(control) {
|
|
4361
|
+
if (this.header === null) {
|
|
4362
|
+
throw new RunControlRefused(
|
|
4363
|
+
control,
|
|
4364
|
+
"not-started",
|
|
4365
|
+
`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.`
|
|
4366
|
+
);
|
|
4367
|
+
}
|
|
4368
|
+
if (this.failed) {
|
|
4369
|
+
throw new RunControlRefused(
|
|
4370
|
+
control,
|
|
4371
|
+
"halted",
|
|
4372
|
+
`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.`
|
|
4373
|
+
);
|
|
4374
|
+
}
|
|
4375
|
+
if (this.cancelled || this.cancelRequest !== null) {
|
|
4376
|
+
throw new RunControlRefused(
|
|
4377
|
+
control,
|
|
4378
|
+
"cancelled",
|
|
4379
|
+
`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.`
|
|
4380
|
+
);
|
|
4381
|
+
}
|
|
4382
|
+
if (this.stopRequest !== null) {
|
|
4383
|
+
throw new RunControlRefused(
|
|
4384
|
+
control,
|
|
4385
|
+
"interrupted",
|
|
4386
|
+
`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.`
|
|
4387
|
+
);
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
/**
|
|
4391
|
+
* **A pause or an unpause, TAKEN** — `RN-R35`, at rest: from the call itself, or from the end of the driver's own step
|
|
4392
|
+
* (`leaveStep()`). **A step of its own** (`stepping`), so a call a consumer makes from a callback on the element — a
|
|
4393
|
+
* second pause, a stop, a `resolve()` that dispatches — is taken, or routes, as on any step. **A pause** sets the flag
|
|
4394
|
+
* BEFORE its element, so a dispatch a callback on the element causes is held. **An unpause** clears it before its
|
|
4395
|
+
* element and hands the held tasks on AFTER it, in a `finally`: a consumer's `onFoldInput` that throws on the element
|
|
4396
|
+
* 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
|
|
4397
|
+
* lesson) — and then the take HALTS the run itself, reported once (`takePause`, `RN-R35` v0.24, W184), so a call pending
|
|
4398
|
+
* behind it is dropped, and the throw reaches whoever is on the stack: the caller of the call at rest or of the driver
|
|
4399
|
+
* 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
|
|
4400
|
+
* of the held queue (`dispatch`), so the order is the dispatch order still.
|
|
4401
|
+
*/
|
|
4402
|
+
takePending(c) {
|
|
4403
|
+
if (c.kind === "answer") this.takeAnswer(c);
|
|
4404
|
+
else this.takePause(c);
|
|
4405
|
+
}
|
|
4406
|
+
/**
|
|
4407
|
+
* **A resolution given mid-step, TAKEN at rest** — `RN-R37`, W180 (the conformity reader's R4; S-4 for every
|
|
4408
|
+
* resolution): a step of its own, so the resolution lands BETWEEN two steps — where a resume's replay FEEDS it
|
|
4409
|
+
* (`RN-R19`) — after the step that parked its node has completed (an ask's `asked` or a task's `user_action`
|
|
4410
|
+
* settlement, and in a region `branch.suspended`). Answered on its own `asked` element and appended
|
|
4411
|
+
* there instead, it sat between two elements of one step, and the replay — which regenerates a step whole — refused the
|
|
4412
|
+
* run's checkpoint `diverged` at it. **Dropped, recording nothing, when the step it was given on HALTED or CANCELLED the
|
|
4413
|
+
* run** (or the node is no longer parked) — `takePause`'s rule: it would be refused were it given now. It routes as
|
|
4414
|
+
* `resolve()` at rest routes: a throw — from routing, or a consumer's on the resolution's own element — HALTS the run
|
|
4415
|
+
* here, reported once, so a call pending behind it is dropped; it then reaches whoever is on the stack: a driver call's
|
|
4416
|
+
* caller, or the listener boundary of a scheduler event's step (`onEvent`, `OR-R10` v0.19, W184).
|
|
4417
|
+
*/
|
|
4418
|
+
takeAnswer(c) {
|
|
4419
|
+
if (this.failed || this.cancelled || this.cancelRequest !== null || !this.parked(c.record.taskId)) return;
|
|
4420
|
+
this.stepping += 1;
|
|
4421
|
+
try {
|
|
4422
|
+
try {
|
|
4423
|
+
this.appendResolution(c.record, c.nodeId, c.branchPath);
|
|
4424
|
+
if (this.shut()) return;
|
|
4425
|
+
this.advance(c.runId, c.nodeId, c.branchPath);
|
|
4426
|
+
} catch (e) {
|
|
4427
|
+
this.haltOnce(e);
|
|
4428
|
+
throw e;
|
|
4429
|
+
}
|
|
4430
|
+
} finally {
|
|
4431
|
+
this.leaveStep();
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
4434
|
+
takePause(p) {
|
|
4435
|
+
if (this.failed || this.cancelled || this.cancelRequest !== null) return;
|
|
4436
|
+
this.stepping += 1;
|
|
4437
|
+
try {
|
|
4438
|
+
try {
|
|
4439
|
+
if (p.kind === "paused") {
|
|
4440
|
+
this.paused = true;
|
|
4441
|
+
this.append({ kind: "paused", actor: p.actor });
|
|
4442
|
+
} else {
|
|
4443
|
+
this.paused = false;
|
|
4444
|
+
try {
|
|
4445
|
+
this.append({ kind: "unpaused", actor: p.actor });
|
|
4446
|
+
} finally {
|
|
4447
|
+
this.drainHeld();
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
} catch (e) {
|
|
4451
|
+
this.haltOnce(e);
|
|
4452
|
+
throw e;
|
|
4453
|
+
}
|
|
4454
|
+
} finally {
|
|
4455
|
+
this.leaveStep();
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
/**
|
|
4459
|
+
* **The unpause's hand-off**: each held task to the scheduler, first held first (`RN-R35`). Taken out of the hold and
|
|
4460
|
+
* marked `unhanded` before its `addTask`, as a resume's re-dispatch is (`RN-R31`), so a cancel raised inside one
|
|
4461
|
+
* hand-off sends nothing for a task not yet handed — and `handOn` then cancels it. **Held for a HALT** — checked before
|
|
4462
|
+
* each, as a resume's re-dispatch is (`RN-R29` (4), `holdsResume()`): a halt raised inside one hand-off hands nothing
|
|
4463
|
+
* further on, and what is left is never handed. A dispatch made on this stack joins the end of the queue and is handed
|
|
4464
|
+
* on in its turn (the `Map` visits what is added while it is walked). Caller-initiated, so a throw from the scheduler
|
|
4465
|
+
* is reported AND rethrown, `start()`'s and `resume()`'s treatment.
|
|
4466
|
+
*/
|
|
4467
|
+
drainHeld() {
|
|
3014
4468
|
try {
|
|
3015
|
-
|
|
4469
|
+
for (const [id, config] of this.pauseHeld) {
|
|
4470
|
+
if (this.holdsResume()) break;
|
|
4471
|
+
this.pauseHeld.delete(id);
|
|
4472
|
+
this.unhanded.add(id);
|
|
4473
|
+
this.handOn(id, config);
|
|
4474
|
+
}
|
|
3016
4475
|
} catch (e) {
|
|
3017
4476
|
this.failRun(e);
|
|
3018
4477
|
throw e;
|
|
3019
4478
|
}
|
|
3020
|
-
return true;
|
|
3021
4479
|
}
|
|
3022
4480
|
/**
|
|
3023
|
-
*
|
|
3024
|
-
* (`RN-
|
|
3025
|
-
*
|
|
3026
|
-
* written by a second road.
|
|
4481
|
+
* Whether the scheduler holds this task — dispatched, not held by a replay, past its `addTask` (`RN-R31`), and not held
|
|
4482
|
+
* by a pause (`RN-R35`, W175): a stop of a paused run cancels only what this says the scheduler holds (ADR 0105's last
|
|
4483
|
+
* consequence), and a cancel hands each held task on before it cancels it.
|
|
3027
4484
|
*/
|
|
3028
|
-
|
|
3029
|
-
this.
|
|
3030
|
-
const region = this.regionOf(nodeId);
|
|
3031
|
-
if (region) {
|
|
3032
|
-
this.emit({
|
|
3033
|
-
type: "branch.resumed",
|
|
3034
|
-
regionId: region.fanoutId,
|
|
3035
|
-
regionPath: this.rpathOf(region, this.indicesFrom(nodeId, branchPath)),
|
|
3036
|
-
branchPath,
|
|
3037
|
-
at: nodeId,
|
|
3038
|
-
resolvedAs: record.resolvedAs
|
|
3039
|
-
});
|
|
3040
|
-
}
|
|
4485
|
+
handedOn(taskId) {
|
|
4486
|
+
return this.replay === null && !this.unhanded.has(taskId) && !this.pauseHeld.has(taskId);
|
|
3041
4487
|
}
|
|
3042
4488
|
// --- the scheduler's side ----------------------------------------------------
|
|
3043
4489
|
onEvent(name, detail) {
|
|
4490
|
+
this.stepping += 1;
|
|
4491
|
+
try {
|
|
4492
|
+
this.onEventStep(name, detail);
|
|
4493
|
+
} finally {
|
|
4494
|
+
try {
|
|
4495
|
+
this.leaveStep();
|
|
4496
|
+
} catch (e) {
|
|
4497
|
+
try {
|
|
4498
|
+
if (!this.failed) this.failRun(e);
|
|
4499
|
+
} catch {
|
|
4500
|
+
}
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
/** `onEvent`'s body — wrapped so the driver knows it is on the stack of its own step (`stepping`, `RN-R31`, W173 D-1). */
|
|
4505
|
+
onEventStep(name, detail) {
|
|
3044
4506
|
if (this.deaf || this.started && this.header === null) return;
|
|
3045
4507
|
try {
|
|
3046
4508
|
this.onEventOrThrow(name, detail);
|
|
3047
4509
|
} catch (e) {
|
|
3048
|
-
|
|
4510
|
+
try {
|
|
4511
|
+
this.haltOnce(e);
|
|
4512
|
+
} catch {
|
|
4513
|
+
}
|
|
4514
|
+
}
|
|
4515
|
+
}
|
|
4516
|
+
/**
|
|
4517
|
+
* **A `task.running`: an attempt started** — `RN-R40`, W186, P36 (f). Its id and number are COPIED here, while the
|
|
4518
|
+
* listener runs (`OR-R1`). Recorded only for a task THIS run dispatched, and only when the number EXCEEDS the last one
|
|
4519
|
+
* recorded for it — a requeue repeats its number and appends nothing (the scheduler's own recipe's `max`). **Everything
|
|
4520
|
+
* else is dropped in SILENCE, and none of it is a refusal**: an id that does not parse (a plain task on a shared
|
|
4521
|
+
* scheduler — its SETTLEMENT is what `onUnmintedTask` reports, `ID-R5`), another run's (`ID-R4` tier 1), one this run
|
|
4522
|
+
* never dispatched (its settlement is `ID-R4` tier 2's `UndispatchedTask`), and a detail carrying no positive integer
|
|
4523
|
+
* `attemptNumber` (a scheduler older than the pin, or a double). An attempt start never stops a run by existing. **Queued, not
|
|
4524
|
+
* appended**: {@link Driver.takeAttempts} appends it when the step it was heard on ends — at once, on a scheduler
|
|
4525
|
+
* event's own step; at the step's end when an inline pool fires it inside the driver's `addTask`.
|
|
4526
|
+
*/
|
|
4527
|
+
heardRunning(detail) {
|
|
4528
|
+
const taskId = detail?.id;
|
|
4529
|
+
const attemptNumber = attemptNumberOf(detail);
|
|
4530
|
+
if (typeof taskId !== "string" || attemptNumber === null) return;
|
|
4531
|
+
let runId;
|
|
4532
|
+
try {
|
|
4533
|
+
runId = parseTaskId(taskId).runId;
|
|
4534
|
+
} catch (e) {
|
|
4535
|
+
if (e instanceof InvalidTaskId) return;
|
|
4536
|
+
throw e;
|
|
4537
|
+
}
|
|
4538
|
+
if (runId !== this.runId || !this.dispatched.has(taskId)) return;
|
|
4539
|
+
if (attemptNumber <= (this.lastAttempt.get(taskId) ?? 0)) return;
|
|
4540
|
+
this.lastAttempt.set(taskId, attemptNumber);
|
|
4541
|
+
this.attemptsPending.push({ taskId, attemptNumber });
|
|
4542
|
+
}
|
|
4543
|
+
/**
|
|
4544
|
+
* **A `task.message` or a `task.progress`: PASSED THROUGH to its callback** — `RN-R42`, W187, P36 (k), ADR 0114. The
|
|
4545
|
+
* task's id is the detail's `taskId` for a message — `task.message` carries no `id` (P36 fact 2) — and its `id` for a
|
|
4546
|
+
* progress report, whose detail is the task spread flat. Handed on only for a task THIS run dispatched (`ID-R4`'s two
|
|
4547
|
+
* tiers, asked SILENTLY, as {@link Driver.heardRunning} asks them): anything else is dropped and is not a refusal. Every
|
|
4548
|
+
* field is COPIED here, while the listener runs (ADR 0007 rule 1, `OR-R1`), and the copy FROZEN (`OR-R7`); a value that
|
|
4549
|
+
* cannot be cloned is handed on `undefined` — the scheduler's own treatment of an uncloneable `emit` (its §6) — so a
|
|
4550
|
+
* pass-through never stops a run by existing. **The consumer's throw is NOT caught here**: it leaves through
|
|
4551
|
+
* `onEventOrThrow` to the listener boundary, which reports it and halts the run (`onUnmintedTask`'s road, `OR-R10`).
|
|
4552
|
+
*/
|
|
4553
|
+
heardPassThrough(name, detail) {
|
|
4554
|
+
const isMessage = name === "task.message";
|
|
4555
|
+
const taskId = isMessage ? detail?.taskId : detail?.id;
|
|
4556
|
+
if (typeof taskId !== "string" || detail === void 0) return;
|
|
4557
|
+
let parts;
|
|
4558
|
+
try {
|
|
4559
|
+
parts = parseTaskId(taskId);
|
|
4560
|
+
} catch (e) {
|
|
4561
|
+
if (e instanceof InvalidTaskId) return;
|
|
4562
|
+
throw e;
|
|
4563
|
+
}
|
|
4564
|
+
if (parts.runId !== this.runId || !this.dispatched.has(taskId)) return;
|
|
4565
|
+
const attribution = {
|
|
4566
|
+
runId: parts.runId,
|
|
4567
|
+
nodeId: parts.nodeId,
|
|
4568
|
+
branchPath: parts.branchPath,
|
|
4569
|
+
taskId,
|
|
4570
|
+
flowAttempt: parts.attempt,
|
|
4571
|
+
attemptNumber: attemptNumberOf(detail)
|
|
4572
|
+
};
|
|
4573
|
+
if (isMessage) {
|
|
4574
|
+
const m = {
|
|
4575
|
+
...attribution,
|
|
4576
|
+
name: detail.name,
|
|
4577
|
+
data: frozenCopy(detail.data),
|
|
4578
|
+
seq: detail.seq,
|
|
4579
|
+
timestamp: detail.timestamp
|
|
4580
|
+
};
|
|
4581
|
+
if (detail.dropped !== void 0) m.dropped = detail.dropped;
|
|
4582
|
+
if (detail.error !== void 0) m.error = detail.error;
|
|
4583
|
+
this.opts.onTaskMessage?.(Object.freeze(m));
|
|
4584
|
+
return;
|
|
3049
4585
|
}
|
|
4586
|
+
const p = {
|
|
4587
|
+
...attribution,
|
|
4588
|
+
progress: detail.progress,
|
|
4589
|
+
message: frozenCopy(detail.message),
|
|
4590
|
+
details: frozenCopy(detail.details),
|
|
4591
|
+
timestamp: detail.timestamp
|
|
4592
|
+
};
|
|
4593
|
+
this.opts.onTaskProgress?.(Object.freeze(p));
|
|
3050
4594
|
}
|
|
3051
|
-
/** Whether this run has been
|
|
4595
|
+
/** Whether this run has been HALTED by a refusal (`OR-R10`; its outcome `halted`). */
|
|
3052
4596
|
hasFailed() {
|
|
3053
4597
|
return this.failed;
|
|
3054
4598
|
}
|
|
@@ -3057,19 +4601,36 @@ var Driver = class {
|
|
|
3057
4601
|
this.failed = true;
|
|
3058
4602
|
const err = cause instanceof Error ? cause : new Error(String(cause));
|
|
3059
4603
|
let seq = null;
|
|
4604
|
+
const report = () => {
|
|
4605
|
+
if (this.replay !== null && (seq === null || !this.delivers(seq))) return;
|
|
4606
|
+
this.opts.onRunFailure?.({
|
|
4607
|
+
runId: this.runId,
|
|
4608
|
+
kind: err.constructor?.name ?? "Error",
|
|
4609
|
+
message: err.message,
|
|
4610
|
+
cause
|
|
4611
|
+
});
|
|
4612
|
+
};
|
|
3060
4613
|
if (!already) {
|
|
3061
|
-
|
|
3062
|
-
|
|
4614
|
+
try {
|
|
4615
|
+
seq = this.append({ kind: "halted", reason: `${err.constructor?.name ?? "Error"}: ${err.message}` });
|
|
4616
|
+
} finally {
|
|
4617
|
+
this.arrivals.clear();
|
|
4618
|
+
this.releaseAllItems();
|
|
4619
|
+
report();
|
|
4620
|
+
}
|
|
4621
|
+
return;
|
|
3063
4622
|
}
|
|
3064
|
-
|
|
3065
|
-
this.opts.onRunFailure?.({
|
|
3066
|
-
runId: this.runId,
|
|
3067
|
-
kind: err.constructor?.name ?? "Error",
|
|
3068
|
-
message: err.message,
|
|
3069
|
-
cause
|
|
3070
|
-
});
|
|
4623
|
+
report();
|
|
3071
4624
|
}
|
|
3072
4625
|
onEventOrThrow(name, detail) {
|
|
4626
|
+
if (name === "task.running") {
|
|
4627
|
+
this.heardRunning(detail);
|
|
4628
|
+
return;
|
|
4629
|
+
}
|
|
4630
|
+
if (name === "task.message" || name === "task.progress") {
|
|
4631
|
+
this.heardPassThrough(name, detail);
|
|
4632
|
+
return;
|
|
4633
|
+
}
|
|
3073
4634
|
const taskId = detail?.id;
|
|
3074
4635
|
let parts;
|
|
3075
4636
|
try {
|
|
@@ -3102,7 +4663,13 @@ var Driver = class {
|
|
|
3102
4663
|
const seq = this.append({ kind: "outcome", record });
|
|
3103
4664
|
const report = this.opts.onNonconformingResult;
|
|
3104
4665
|
if (report !== void 0 && this.delivers(seq)) this.reportResultSchema(record, nodeId, report);
|
|
3105
|
-
if (this.
|
|
4666
|
+
if (this.shut()) return;
|
|
4667
|
+
if (record.terminalState === "cancelled") {
|
|
4668
|
+
this.cancelled = true;
|
|
4669
|
+
this.arrivals.clear();
|
|
4670
|
+
this.releaseAllItems();
|
|
4671
|
+
return;
|
|
4672
|
+
}
|
|
3106
4673
|
if (record.terminalState === "user_action") {
|
|
3107
4674
|
const region = this.regionOf(nodeId);
|
|
3108
4675
|
if (region) {
|
|
@@ -3176,7 +4743,7 @@ var Driver = class {
|
|
|
3176
4743
|
if (!node) return;
|
|
3177
4744
|
const itemIndices = this.indicesFrom(nodeId, branchPath);
|
|
3178
4745
|
const taskId = buildTaskId(runId, nodeId, branchIndices(this.pathOf(nodeId, itemIndices)));
|
|
3179
|
-
const outcome = effectiveOutcome(this.
|
|
4746
|
+
const outcome = effectiveOutcome(this.taskLog.of(taskId), taskId);
|
|
3180
4747
|
if (!outcome) return;
|
|
3181
4748
|
const result = selectEdge(
|
|
3182
4749
|
node,
|
|
@@ -3222,9 +4789,10 @@ var Driver = class {
|
|
|
3222
4789
|
* through within this turn; only a `task` becomes a scheduler task.
|
|
3223
4790
|
*/
|
|
3224
4791
|
enter(runId, nodeId, payload, fromBranch = "-", fromRegion, itemIndices = EMPTY_INDICES) {
|
|
4792
|
+
if (this.shut()) return;
|
|
3225
4793
|
const node = this.graph.byId.get(nodeId);
|
|
3226
4794
|
if (!node) return;
|
|
3227
|
-
const received = this.compose(node, payload, itemIndices);
|
|
4795
|
+
const received = this.compose(node, payload, itemIndices, this.runPayload(), () => this.itemOf(node.id, itemIndices));
|
|
3228
4796
|
switch (node.kind) {
|
|
3229
4797
|
case "task":
|
|
3230
4798
|
return this.dispatch(runId, node, received, itemIndices);
|
|
@@ -3297,6 +4865,12 @@ var Driver = class {
|
|
|
3297
4865
|
if (branch === void 0) return;
|
|
3298
4866
|
const idx = items.map((_v, i) => withIndex(itemIndices, nodeId, i));
|
|
3299
4867
|
const paths = idx.map((m) => this.pathOf(branch, m));
|
|
4868
|
+
const kept = this.items.kept.get(nodeId);
|
|
4869
|
+
if (kept !== void 0 && items.length > 0) {
|
|
4870
|
+
const byPath = this.items.held.get(nodeId) ?? /* @__PURE__ */ new Map();
|
|
4871
|
+
this.items.held.set(nodeId, byPath);
|
|
4872
|
+
items.forEach((v, i) => byPath.set(paths[i], kept ? cloneValue(v) : v));
|
|
4873
|
+
}
|
|
3300
4874
|
for (const p of paths) {
|
|
3301
4875
|
this.emit({
|
|
3302
4876
|
type: "branch.opened",
|
|
@@ -3372,14 +4946,14 @@ var Driver = class {
|
|
|
3372
4946
|
case "gate": {
|
|
3373
4947
|
const decided = this.decideGate(node, received, this.memoryOf(nodeId));
|
|
3374
4948
|
if ("threw" in decided) throw decided.threw;
|
|
3375
|
-
const
|
|
4949
|
+
const set = decided.set;
|
|
3376
4950
|
if (!decided.declared) {
|
|
3377
4951
|
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 ${
|
|
4952
|
+
`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
4953
|
);
|
|
3380
4954
|
}
|
|
3381
4955
|
const route = decided.route;
|
|
3382
|
-
const edge = gateEdge(node,
|
|
4956
|
+
const edge = gateEdge(node, set.defaultRoute, route);
|
|
3383
4957
|
if (edge === null) {
|
|
3384
4958
|
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
4959
|
}
|
|
@@ -3389,6 +4963,12 @@ var Driver = class {
|
|
|
3389
4963
|
this.enter(runId, edge.to, received, fromBranch, fromRegion, itemIndices);
|
|
3390
4964
|
return;
|
|
3391
4965
|
}
|
|
4966
|
+
// **An ASK PARKS, and nothing is handed to the scheduler** — `RN-R37`, v0.22,
|
|
4967
|
+
// [ADR 0109](../../doc/decisions/0109-an-ask-parks-with-nothing-dispatched-and-its-answer-is-its-result.md),
|
|
4968
|
+
// [W180](../../doc/work-items/w180-asking-and-answering-in-a-run.md), P35 (b)-(f). It replaces ADR 0108 rule 8's
|
|
4969
|
+
// interim refusal at this arm. See `ask()`.
|
|
4970
|
+
case "ask":
|
|
4971
|
+
return this.ask(runId, node, received, itemIndices);
|
|
3392
4972
|
// **A node kind this driver cannot execute REFUSES rather than returning.**
|
|
3393
4973
|
// Added by [W32](../../doc/work-items/w32-what-a-map-fanout-is.md) as a
|
|
3394
4974
|
// declared late exception, after its conformity reader measured what the
|
|
@@ -3487,8 +5067,9 @@ var Driver = class {
|
|
|
3487
5067
|
} catch (threw) {
|
|
3488
5068
|
return { threw };
|
|
3489
5069
|
}
|
|
3490
|
-
const
|
|
3491
|
-
|
|
5070
|
+
const set = this.gateRoutes.get(node.id);
|
|
5071
|
+
const declared = typeof route === "string" && set.routes.includes(route);
|
|
5072
|
+
return { set, route, declared, key: recorded };
|
|
3492
5073
|
}
|
|
3493
5074
|
/**
|
|
3494
5075
|
* **`FD-R4`'s road through an ENTRY gate: the edge the run will take, and no other** — W93, Outputs (9).
|
|
@@ -3501,7 +5082,7 @@ var Driver = class {
|
|
|
3501
5082
|
boundaryEdges(gate, value) {
|
|
3502
5083
|
const decided = this.decideGate(gate, value, /* @__PURE__ */ new Map());
|
|
3503
5084
|
if ("threw" in decided || !decided.declared) return [];
|
|
3504
|
-
const edge = gateEdge(gate, decided.
|
|
5085
|
+
const edge = gateEdge(gate, decided.set.defaultRoute, decided.route);
|
|
3505
5086
|
return edge === null ? [] : [edge];
|
|
3506
5087
|
}
|
|
3507
5088
|
/**
|
|
@@ -3509,7 +5090,7 @@ var Driver = class {
|
|
|
3509
5090
|
* [ADR 0040](../../doc/decisions/0040-what-a-node-can-reach-is-the-runs-own-outputs-and-a-node-id-names-them-relative-to-a-branch-path.md)
|
|
3510
5091
|
* rules 2 and 3, and `FD-C2`.
|
|
3511
5092
|
*
|
|
3512
|
-
* **It is a PROJECTION of the log and adds no retention** — measured at
|
|
5093
|
+
* **It is a PROJECTION of the log and adds no retention of a result** — measured at
|
|
3513
5094
|
* [P12](../../doc/plans/p12-what-a-node-receives-and-what-a-decision-sees.md)'s
|
|
3514
5095
|
* declaration and again before this was written: this driver holds
|
|
3515
5096
|
* `private readonly log: LogRecord[]` with exactly one `push` and no `splice`,
|
|
@@ -3519,14 +5100,11 @@ var Driver = class {
|
|
|
3519
5100
|
* described **today** before this method existed, and this adds **no retention**.
|
|
3520
5101
|
* *(Q19 is RESOLVED since W53 —
|
|
3521
5102
|
* [ADR 0046](../../doc/decisions/0046-a-run-retains-what-its-tasks-returned-and-a-region-releases-its-collection-when-its-join-becomes-terminal.md)
|
|
3522
|
-
* — and `RN-R13` names
|
|
5103
|
+
* — and `RN-R13` names `outputsCache` by id: it holds results **by reference only**,
|
|
3523
5104
|
* asserted by IDENTITY at ladder L2 rather than by a byte count, because a
|
|
3524
5105
|
* reachability SET cannot tell an alias from a copy. W53's readers found that
|
|
3525
5106
|
* from both ends — the rule's stated test returned a third name on a `needs`
|
|
3526
5107
|
* flow, and cloning here cost ZERO.)*
|
|
3527
|
-
* *(It adds ONE field — `outputsCache` below — which is a memo the log invalidates
|
|
3528
|
-
* and which holds nothing the log does not. This read *"zero fields"* until W41's
|
|
3529
|
-
* conformity reader read it beside the memo the cost measurement had just added.)*
|
|
3530
5108
|
*
|
|
3531
5109
|
* **`effectiveOutcome` and not the raw record**, for `spec/flow-definition.md`
|
|
3532
5110
|
* section 3 rule 4's own reason: a resumed node has two records in the log and
|
|
@@ -3534,43 +5112,97 @@ var Driver = class {
|
|
|
3534
5112
|
* suspended has no effective outcome and therefore no entry — which is what makes
|
|
3535
5113
|
* `FD-R3` fire rather than a reference quietly resolving to `undefined`.
|
|
3536
5114
|
*
|
|
3537
|
-
* **
|
|
3538
|
-
*
|
|
3539
|
-
*
|
|
3540
|
-
*
|
|
3541
|
-
* `
|
|
3542
|
-
*
|
|
5115
|
+
* **CAUGHT UP rather than rebuilt** — `RN-R13` v0.30, P39 decision 4 and fact 7,
|
|
5116
|
+
* [ADR 0121](../../doc/decisions/0121-the-driver-keeps-a-per-task-view-of-its-log.md) (W195), amending ADR 0040
|
|
5117
|
+
* rule 2's *computed rather than maintained*. Until W195 this was memoised on the log's LENGTH and rebuilt from the
|
|
5118
|
+
* WHOLE log whenever the log had grown — so a node with a `needs` entered once per settlement, which is every instance
|
|
5119
|
+
* of a `map` whose branch declares one, paid O(log) per entry and the run O(log²): the case its own memo missed by
|
|
5120
|
+
* construction. Now each call reads only the records appended since the last (`at`, an index into the append-only
|
|
5121
|
+
* `log`), and for each one asks `effectiveOutcome` of THAT TASK's records ({@link TaskLog}) — so the merge stays in
|
|
5122
|
+
* ONE place, which is ADR 0040 rule 2's own reason, and an entry is always the merge of its task's whole sub-log at
|
|
5123
|
+
* the moment of the call: nothing here can say what the log does not. **Equal to a rebuild at every read**
|
|
5124
|
+
* (`test/driver/log-view.test.ts`, at every fold input of every valid corpus flow): the same entries, a node's list
|
|
5125
|
+
* in its tasks' first-record order, each `result` the log record's own object.
|
|
5126
|
+
*
|
|
5127
|
+
* **Still LAZY, so ADR 0040 rule 1's *the default is unchanged* holds of the cost**: nothing is caught up until a
|
|
5128
|
+
* node or an edge that declares a `needs` asks, and a run that declares none never builds it.
|
|
3543
5129
|
*/
|
|
3544
5130
|
outputs() {
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
} catch {
|
|
3560
|
-
continue;
|
|
5131
|
+
const c = this.outputsCache ??= { at: 0, byNode: /* @__PURE__ */ new Map(), tasks: /* @__PURE__ */ new Map(), orders: /* @__PURE__ */ new Map() };
|
|
5132
|
+
for (; c.at < this.log.length; c.at += 1) {
|
|
5133
|
+
const r = this.log[c.at];
|
|
5134
|
+
let t = c.tasks.get(r.taskId);
|
|
5135
|
+
if (t === void 0) {
|
|
5136
|
+
let parts;
|
|
5137
|
+
try {
|
|
5138
|
+
parts = parseTaskId(r.taskId);
|
|
5139
|
+
} catch {
|
|
5140
|
+
c.tasks.set(r.taskId, null);
|
|
5141
|
+
continue;
|
|
5142
|
+
}
|
|
5143
|
+
t = { nodeId: parts.nodeId, path: parts.branchPath, order: c.tasks.size, entry: null };
|
|
5144
|
+
c.tasks.set(r.taskId, t);
|
|
3561
5145
|
}
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
else byNode.set(parts.nodeId, [{ path: parts.branchPath, result: eff.result }]);
|
|
5146
|
+
if (t === null) continue;
|
|
5147
|
+
this.place(c, t, effectiveOutcome(this.taskLog.of(r.taskId), r.taskId));
|
|
3565
5148
|
}
|
|
3566
|
-
|
|
3567
|
-
return byNode;
|
|
5149
|
+
return c.byNode;
|
|
3568
5150
|
}
|
|
3569
5151
|
/**
|
|
3570
|
-
*
|
|
3571
|
-
*
|
|
3572
|
-
*
|
|
3573
|
-
*
|
|
5152
|
+
* **One task's entry, made what its effective outcome now says** — `outputs()`'s catch-up, for a task one of whose records
|
|
5153
|
+
* was just read. An entry is `{ path, result }` with `result` the record's own object (`RN-R13`); a node's list is kept
|
|
5154
|
+
* in its tasks' FIRST-RECORD order, the order the rebuild grouped them in.
|
|
5155
|
+
*
|
|
5156
|
+
* **`!eff` — a suspended node contributes no entry.** The first record of a `user_action` task has none, and its
|
|
5157
|
+
* RESOLUTION, later, gives it one: that is the one entry that is not appended at the END of its node's list but placed
|
|
5158
|
+
* at its first record's position — reachable, and the corpus's *parked, attempted, resolved* drain reaches it. *(Whether
|
|
5159
|
+
* a suspended node's ABSENCE is ever read: no — W41's adequacy reader measured making it contribute
|
|
5160
|
+
* `{result: undefined}` cost **0**, because every shipped policy leaves a parked branch holding a token, so no join
|
|
5161
|
+
* fires while one is parked and no reader downstream of it is entered. [Q97](../../doc/questions/q97.md).)* An entry
|
|
5162
|
+
* that LOSES its effective outcome is removed — **reachable**: `ID-R4` asks only whether a settlement's id was dispatched,
|
|
5163
|
+
* so a settlement event repeated for a settled task is appended, and a `done` then a `user_action` leaves the task with
|
|
5164
|
+
* none (W195's conformity reader, C4; `test/driver/log-view.test.ts` drives it and the resolution that places it again).
|
|
5165
|
+
*/
|
|
5166
|
+
place(c, t, eff) {
|
|
5167
|
+
const list = c.byNode.get(t.nodeId);
|
|
5168
|
+
const orders = c.orders.get(t.nodeId);
|
|
5169
|
+
if (eff === null) {
|
|
5170
|
+
if (t.entry === null || list === void 0 || orders === void 0) return;
|
|
5171
|
+
const at = list.indexOf(t.entry);
|
|
5172
|
+
list.splice(at, 1);
|
|
5173
|
+
orders.splice(at, 1);
|
|
5174
|
+
t.entry = null;
|
|
5175
|
+
if (list.length === 0) {
|
|
5176
|
+
c.byNode.delete(t.nodeId);
|
|
5177
|
+
c.orders.delete(t.nodeId);
|
|
5178
|
+
}
|
|
5179
|
+
return;
|
|
5180
|
+
}
|
|
5181
|
+
const entry = { path: t.path, result: eff.result };
|
|
5182
|
+
if (t.entry !== null && list !== void 0) {
|
|
5183
|
+
list[list.indexOf(t.entry)] = entry;
|
|
5184
|
+
t.entry = entry;
|
|
5185
|
+
return;
|
|
5186
|
+
}
|
|
5187
|
+
t.entry = entry;
|
|
5188
|
+
if (list === void 0 || orders === void 0) {
|
|
5189
|
+
c.byNode.set(t.nodeId, [entry]);
|
|
5190
|
+
c.orders.set(t.nodeId, [t.order]);
|
|
5191
|
+
return;
|
|
5192
|
+
}
|
|
5193
|
+
let i = list.length;
|
|
5194
|
+
while (i > 0 && orders[i - 1] > t.order) i -= 1;
|
|
5195
|
+
list.splice(i, 0, entry);
|
|
5196
|
+
orders.splice(i, 0, t.order);
|
|
5197
|
+
}
|
|
5198
|
+
/**
|
|
5199
|
+
* The projection above, CAUGHT UP to `at` — `RN-R13` v0.30, ADR 0121 (W195). **Not run state and not a second copy of
|
|
5200
|
+
* it**: `byNode`'s entries hold each result BY REFERENCE (the log record's own object) — so it is a holder of results and
|
|
5201
|
+
* never a copy — `tasks` is one slot per task id
|
|
5202
|
+
* read — its node, its path, its first-record position and its current entry — and `orders` the same positions, per
|
|
5203
|
+
* node, parallel to its list. Everything here is the merge `effectiveOutcome` computed from the task's own records at the moment they were
|
|
5204
|
+
* read, and `at` says which records have been; so there is nothing here that could disagree with the log. `ADR 0014`'s
|
|
5205
|
+
* *the driver computes no run state* is untouched. `null` until a `needs` first asks.
|
|
3574
5206
|
*/
|
|
3575
5207
|
outputsCache = null;
|
|
3576
5208
|
/**
|
|
@@ -3585,21 +5217,32 @@ var Driver = class {
|
|
|
3585
5217
|
* the edge carried, so the composed object is the whole payload and what the
|
|
3586
5218
|
* worker gets is what the document says, character for character.
|
|
3587
5219
|
*
|
|
5220
|
+
* **`item` is a THUNK, read only for a `"$item"` key** (v0.45, P38 (e)): in a run it reads the instance's held item
|
|
5221
|
+
* (`itemOf`), at `FD-R4` it is the walk's own value — and a node declaring no `"$item"` never asks.
|
|
5222
|
+
*
|
|
3588
5223
|
* @throws UnresolvedReference — `FD-R3`.
|
|
3589
5224
|
*/
|
|
3590
|
-
compose(node, edgeValue, itemIndices) {
|
|
5225
|
+
compose(node, edgeValue, itemIndices, runPayload, item) {
|
|
3591
5226
|
const needs = node.needs;
|
|
3592
5227
|
if (!needs) return edgeValue;
|
|
3593
|
-
|
|
3594
|
-
const produced = this.outputs();
|
|
5228
|
+
let lookup = null;
|
|
3595
5229
|
const out = {};
|
|
3596
5230
|
for (const [key, ref] of Object.entries(needs)) {
|
|
3597
5231
|
const t = referenceTarget(ref);
|
|
3598
|
-
if (t.
|
|
5232
|
+
if (t.form === "self") {
|
|
3599
5233
|
out[key] = cloneValue(edgeValue);
|
|
3600
5234
|
continue;
|
|
3601
5235
|
}
|
|
3602
|
-
|
|
5236
|
+
if (t.form === "payload") {
|
|
5237
|
+
out[key] = cloneValue(runPayload);
|
|
5238
|
+
continue;
|
|
5239
|
+
}
|
|
5240
|
+
if (t.form === "item") {
|
|
5241
|
+
out[key] = cloneValue(item());
|
|
5242
|
+
continue;
|
|
5243
|
+
}
|
|
5244
|
+
lookup ??= { here: this.pathOf(node.id, itemIndices), produced: this.outputs() };
|
|
5245
|
+
const v = this.resolveReference(node.id, lookup.here, lookup.produced, key, t, "needs");
|
|
3603
5246
|
if (v !== ABSENT) out[key] = v;
|
|
3604
5247
|
}
|
|
3605
5248
|
return out;
|
|
@@ -3632,6 +5275,35 @@ var Driver = class {
|
|
|
3632
5275
|
}
|
|
3633
5276
|
return t.form === "all" ? visible.map((e) => cloneValue(e.result)) : cloneValue(visible[0].result);
|
|
3634
5277
|
}
|
|
5278
|
+
/**
|
|
5279
|
+
* **The value the run ENTERED with — the header's copy** (`RN-R15`, P38 fact 4 and (f)): taken at `start()` from the
|
|
5280
|
+
* caller's payload, or on a resume from the stored header, and held for the life of the run. What a `"$payload"`
|
|
5281
|
+
* reference composes from, in both doors; `FD-R4` hands the caller's own value instead, because it runs before the
|
|
5282
|
+
* header exists. Read only where a node or an edge composes the token — a run declaring none never clones it
|
|
5283
|
+
* (P38 decision 2). The header is set before the first `enter` in both doors, so `null` here is not a run.
|
|
5284
|
+
*/
|
|
5285
|
+
runPayload() {
|
|
5286
|
+
return this.header?.entryPayload;
|
|
5287
|
+
}
|
|
5288
|
+
/**
|
|
5289
|
+
* **The item of the map instance `nodeId` is entered in** — `"$item"`'s source in a run (v0.45, P38 (c), (e)): the
|
|
5290
|
+
* innermost map containing the node (`innermostMapOf`, the walk `FD-V49` checked), and the instance its `itemIndices`
|
|
5291
|
+
* name — the branch path that map's branch entry has in it. **By reference to the held item, uncloned**: the caller
|
|
5292
|
+
* clones, per key. Read only where a node or an edge declares `"$item"`.
|
|
5293
|
+
*
|
|
5294
|
+
* **A miss THROWS a plain `Error` naming the invariant**: a node is entered only while its instance is live, and an
|
|
5295
|
+
* instance's item is released only once nothing of it may still be entered (`trackItems`), so a reader with nothing held is a driver
|
|
5296
|
+
* defect — reported as a halt (`OR-R10`) rather than handed `undefined`, which an item may legitimately be.
|
|
5297
|
+
*/
|
|
5298
|
+
itemOf(nodeId, itemIndices) {
|
|
5299
|
+
const m = innermostMapOf(this.regions, nodeId);
|
|
5300
|
+
const path = this.pathOf(m.branches[0], itemIndices);
|
|
5301
|
+
const byPath = this.items.held.get(m.fanoutId);
|
|
5302
|
+
if (byPath === void 0 || !byPath.has(path)) {
|
|
5303
|
+
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`);
|
|
5304
|
+
}
|
|
5305
|
+
return byPath.get(path);
|
|
5306
|
+
}
|
|
3635
5307
|
/**
|
|
3636
5308
|
* **What a decision sees, beyond its node's outcome** — `FD-C3`, and
|
|
3637
5309
|
* [ADR 0041](../../doc/decisions/0041-a-decision-sees-its-nodes-outcome-and-whatever-its-own-edge-declares.md).
|
|
@@ -3642,7 +5314,10 @@ var Driver = class {
|
|
|
3642
5314
|
*
|
|
3643
5315
|
* **No `"."`, and `FD-V26` refuses it on an edge**: *what this predicate would have
|
|
3644
5316
|
* 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.
|
|
5317
|
+
* `edgeValue` parameter here and none is derivable — see the call site. **A
|
|
5318
|
+
* `"$payload"` key needs none either** (v0.44, ADR 0117): it is not derived from what
|
|
5319
|
+
* the NODE received but read from the run's header, `runPayload()` above, its own
|
|
5320
|
+
* copy per key per read.
|
|
3646
5321
|
*
|
|
3647
5322
|
* @throws UnresolvedReference — `FD-R3`, from the same expression a node's does.
|
|
3648
5323
|
*/
|
|
@@ -3654,6 +5329,17 @@ var Driver = class {
|
|
|
3654
5329
|
const out = {};
|
|
3655
5330
|
for (const [key, ref] of Object.entries(needs)) {
|
|
3656
5331
|
const t = referenceTarget(ref);
|
|
5332
|
+
if (t.form === "payload") {
|
|
5333
|
+
out[key] = cloneValue(this.runPayload());
|
|
5334
|
+
continue;
|
|
5335
|
+
}
|
|
5336
|
+
if (t.form === "item") {
|
|
5337
|
+
out[key] = cloneValue(this.itemOf(node.id, itemIndices));
|
|
5338
|
+
continue;
|
|
5339
|
+
}
|
|
5340
|
+
if (t.form === "self") {
|
|
5341
|
+
throw new Error(`node "${node.id}"'s edge declares needs[${JSON.stringify(key)}] = ${JSON.stringify(ref)}, which FD-V26 refuses on an edge`);
|
|
5342
|
+
}
|
|
3657
5343
|
const v = this.resolveReference(node.id, here, produced, key, t, "edge needs");
|
|
3658
5344
|
if (v !== ABSENT) out[key] = v;
|
|
3659
5345
|
}
|
|
@@ -3668,11 +5354,13 @@ var Driver = class {
|
|
|
3668
5354
|
* graph, and there is no longer a carried value that could disagree with it.
|
|
3669
5355
|
*/
|
|
3670
5356
|
dispatch(runId, node, payload, itemIndices = EMPTY_INDICES) {
|
|
5357
|
+
if (this.shut()) return;
|
|
3671
5358
|
const taskId = buildTaskId(runId, node.id, branchIndices(this.pathOf(node.id, itemIndices)), 0);
|
|
3672
5359
|
if (this.dispatched.has(taskId)) {
|
|
3673
5360
|
throw new DuplicateTaskId(`"${taskId}" was already dispatched in run "${runId}"`);
|
|
3674
5361
|
}
|
|
3675
5362
|
this.dispatched.add(taskId);
|
|
5363
|
+
if (this.replay === null) this.unhanded.add(taskId);
|
|
3676
5364
|
this.append({ kind: "dispatched", taskId });
|
|
3677
5365
|
const type = this.nodeTypes.get(node.type);
|
|
3678
5366
|
const retryPolicy = node.retryPolicy ?? type.retryPolicy ?? this.opts.defaultRetryPolicy;
|
|
@@ -3688,7 +5376,108 @@ var Driver = class {
|
|
|
3688
5376
|
this.replay.held.set(taskId, config);
|
|
3689
5377
|
return;
|
|
3690
5378
|
}
|
|
3691
|
-
|
|
5379
|
+
if ((this.paused || this.pauseHeld.size > 0) && !this.shut()) {
|
|
5380
|
+
this.pauseHeld.set(taskId, config);
|
|
5381
|
+
this.unhanded.delete(taskId);
|
|
5382
|
+
return;
|
|
5383
|
+
}
|
|
5384
|
+
this.handOn(taskId, config);
|
|
5385
|
+
}
|
|
5386
|
+
/**
|
|
5387
|
+
* **A branch reached an ASK: it parks, keeping what reached it, and NOTHING is handed to the scheduler** — `RN-R37`,
|
|
5388
|
+
* v0.22, [ADR 0109](../../doc/decisions/0109-an-ask-parks-with-nothing-dispatched-and-its-answer-is-its-result.md),
|
|
5389
|
+
* P35 stop-and-asks (b), (c) and (f), decision 5.
|
|
5390
|
+
*
|
|
5391
|
+
* 1. **The hold** (`RN-R29`), as `dispatch`'s: `enter` checked it at its head and `compose` ran consumer code since.
|
|
5392
|
+
* 2. **Its task id** by `spec/task-id.md`'s scheme, attempt `0` — the address a person's answer names. Minted once
|
|
5393
|
+
* per path: a second `asked` at one id is `ID-R1`'s defect and refused as one (`DuplicateTaskId`), never a
|
|
5394
|
+
* second park.
|
|
5395
|
+
* 3. **ONE `asked` element**, carrying `received` — what reached the ask, composed through its `needs` — as the
|
|
5396
|
+
* driver's OWN deep copy: at the entry `received` is the caller's own `start()` payload, and a kept input must not
|
|
5397
|
+
* move when the caller's object does (P35 (f)). No `dispatched` element, no `createTask`, no `addTask`: the ask is
|
|
5398
|
+
* never in flight (`RN-R21`), so a stop cancels nothing for it and a resume re-dispatches nothing (P35 fact 5).
|
|
5399
|
+
* 4. **In a region, `branch.suspended`** — what a parked task emits (`settle`'s `user_action` arm): the branch is
|
|
5400
|
+
* live and its join waits for the person (Q7), which is how an ask in a fan-out holds its join. AFTER the element,
|
|
5401
|
+
* as the parked task's follows its settlement, and the hold checked between them (`RN-R29`): a consumer's
|
|
5402
|
+
* callback on `asked` may halt the run, and a halted run emits nothing further. It may also ANSWER the ask: the
|
|
5403
|
+
* answer is accepted and PENDING until the step ends (reading R4), so the park completes first.
|
|
5404
|
+
*
|
|
5405
|
+
* **During a replay this is how the park is REGENERATED** (`RN-R19`): the element is compared with the stored one,
|
|
5406
|
+
* input and all (`sameData`, `RN-R20`), so a replay that composes a different input is refused `diverged` here.
|
|
5407
|
+
*/
|
|
5408
|
+
ask(runId, node, received, itemIndices) {
|
|
5409
|
+
if (this.shut()) return;
|
|
5410
|
+
const branchPath = this.pathOf(node.id, itemIndices);
|
|
5411
|
+
const taskId = buildTaskId(runId, node.id, branchIndices(branchPath), 0);
|
|
5412
|
+
if (this.asked.has(taskId) || this.dispatched.has(taskId)) {
|
|
5413
|
+
throw new DuplicateTaskId(`"${taskId}" was already asked in run "${runId}" \u2014 an ask's id is minted once per path (ID-R1)`);
|
|
5414
|
+
}
|
|
5415
|
+
this.asked.add(taskId);
|
|
5416
|
+
this.append({ kind: "asked", taskId, input: cloneValue(received) });
|
|
5417
|
+
if (this.shut()) return;
|
|
5418
|
+
const region = this.regionOf(node.id);
|
|
5419
|
+
if (region) {
|
|
5420
|
+
this.emit({
|
|
5421
|
+
type: "branch.suspended",
|
|
5422
|
+
regionId: region.fanoutId,
|
|
5423
|
+
regionPath: this.rpathOf(region, itemIndices),
|
|
5424
|
+
branchPath,
|
|
5425
|
+
at: node.id
|
|
5426
|
+
});
|
|
5427
|
+
}
|
|
5428
|
+
}
|
|
5429
|
+
/**
|
|
5430
|
+
* **`RS-R1`'s *suspended*, for one task id** — a dispatched task whose last record is a `user_action` settlement, or,
|
|
5431
|
+
* since v0.18 (`RN-R37`, ADR 0109), an ask this run reached that has no record at all. The FIVE readers of the
|
|
5432
|
+
* predicate read it here: `resolve()`, the replay's fed resolution, and the three `ended` refusals (`hasParked`).
|
|
5433
|
+
*/
|
|
5434
|
+
parked(taskId) {
|
|
5435
|
+
return isSuspended(this.taskLog.of(taskId), taskId, this.asked.has(taskId));
|
|
5436
|
+
}
|
|
5437
|
+
/**
|
|
5438
|
+
* **Whether anything in the run waits on a person** — a parked task or an unanswered ask. `cancel`, `stop` and `pause`
|
|
5439
|
+
* 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
|
|
5440
|
+
* waiting on a person, not finished (P35 fact 5 (iv)). The rest of an ask under run control needs no line of its own:
|
|
5441
|
+
* never dispatched, it is never in flight, so every road reading `inFlight()` passes it by (`RN-R38`, W181).
|
|
5442
|
+
*/
|
|
5443
|
+
hasParked() {
|
|
5444
|
+
for (const id of this.dispatched) if (isSuspended(this.taskLog.of(id), id)) return true;
|
|
5445
|
+
for (const id of this.asked) if (this.parked(id)) return true;
|
|
5446
|
+
return false;
|
|
5447
|
+
}
|
|
5448
|
+
/**
|
|
5449
|
+
* **Hand one task to the scheduler — and, if the run's owner cancelled it while the task was on its way, cancel it
|
|
5450
|
+
* at once** (`RN-R29` (i), `RN-R31`, ADR 0103's inherited hazard, W173). The ONE place `addTask` is called: by
|
|
5451
|
+
* `dispatch` for the task in progress, and by `resume()` for each task in flight. A cancel raised on this task's own
|
|
5452
|
+
* `dispatched` element — or on a resume's `resumed` element — ran before the scheduler held the id, and sent nothing
|
|
5453
|
+
* for it; this is where that task is cancelled, so it ends with a genuine `task.cancelled` rather than running.
|
|
5454
|
+
*/
|
|
5455
|
+
handOn(taskId, config) {
|
|
5456
|
+
try {
|
|
5457
|
+
this.opts.scheduler.addTask((this.opts.createTask ?? defaultCreateTask)(config));
|
|
5458
|
+
} finally {
|
|
5459
|
+
this.unhanded.delete(taskId);
|
|
5460
|
+
}
|
|
5461
|
+
if (this.cancelRequest !== null) this.cancelTask(taskId);
|
|
5462
|
+
}
|
|
5463
|
+
/**
|
|
5464
|
+
* **One task cancelled through the scheduler, with the request's options**, its promise kept for `cancel()` — or,
|
|
5465
|
+
* since W174, `stop()` — to wait on (`RN-R31`, `RN-R33`). Each option is passed only when the caller gave it, so an
|
|
5466
|
+
* absent `graceMs` is the pool's `cancelGraceMs` and not `undefined` read as a value. A cancel and a stop never both
|
|
5467
|
+
* hold — each refuses the other — so the one present is the one whose options these are.
|
|
5468
|
+
*/
|
|
5469
|
+
cancelTask(taskId) {
|
|
5470
|
+
const req = this.cancelRequest ?? this.stopRequest;
|
|
5471
|
+
const options = {};
|
|
5472
|
+
if (req.reason !== void 0) options.reason = req.reason;
|
|
5473
|
+
if (req.graceMs !== void 0) options.graceMs = req.graceMs;
|
|
5474
|
+
const p = this.opts.scheduler.cancel(taskId, options);
|
|
5475
|
+
if (!req.stopping.has(taskId)) req.stopping.set(taskId, p);
|
|
5476
|
+
const drop = () => {
|
|
5477
|
+
if (req.stopping.get(taskId) === p) req.stopping.delete(taskId);
|
|
5478
|
+
};
|
|
5479
|
+
p.then(drop, drop);
|
|
5480
|
+
return p;
|
|
3692
5481
|
}
|
|
3693
5482
|
/**
|
|
3694
5483
|
* **A node's `branchPath`, derived from the graph** — `spec/task-id.md` section
|
|
@@ -3747,6 +5536,49 @@ var Driver = class {
|
|
|
3747
5536
|
this.arrivals.get(fanoutId)?.delete(rp);
|
|
3748
5537
|
if (this.arrivals.get(fanoutId)?.size === 0) this.arrivals.delete(fanoutId);
|
|
3749
5538
|
}
|
|
5539
|
+
/**
|
|
5540
|
+
* **The release horizon of what instances keep** — ADR 0118 rule 5. A `branch.opened` of a region nested in a kept map's
|
|
5541
|
+
* instance counts one live branch there; its terminal event uncounts it. The instance's OWN terminal event releases its
|
|
5542
|
+
* item when that count is zero, and otherwise marks it ENDED, released by the nested branch that ends last. O(nesting
|
|
5543
|
+
* depth) per event, and nothing at all where no map keeps (`kept` empty). Run in a replay as in a run, so a resume
|
|
5544
|
+
* rebuilds the counts with the items.
|
|
5545
|
+
*/
|
|
5546
|
+
trackItems(e) {
|
|
5547
|
+
const it = this.items;
|
|
5548
|
+
if (it.kept.size === 0) return;
|
|
5549
|
+
const opened = e.type === "branch.opened";
|
|
5550
|
+
const terminal = isTerminalEvent(e);
|
|
5551
|
+
if (!opened && !terminal) return;
|
|
5552
|
+
for (const { map, depth } of it.within.get(e.regionId) ?? []) {
|
|
5553
|
+
const path = e.branchPath.split(".").slice(0, depth).join(".");
|
|
5554
|
+
const k = `${map} ${path}`;
|
|
5555
|
+
const n = (it.nested.get(k) ?? 0) + (opened ? 1 : -1);
|
|
5556
|
+
if (n > 0) {
|
|
5557
|
+
it.nested.set(k, n);
|
|
5558
|
+
continue;
|
|
5559
|
+
}
|
|
5560
|
+
it.nested.delete(k);
|
|
5561
|
+
if (it.ended.has(k)) this.releaseItem(map, path);
|
|
5562
|
+
}
|
|
5563
|
+
if (terminal && it.kept.has(e.regionId)) {
|
|
5564
|
+
if (it.nested.has(`${e.regionId} ${e.branchPath}`)) it.ended.add(`${e.regionId} ${e.branchPath}`);
|
|
5565
|
+
else this.releaseItem(e.regionId, e.branchPath);
|
|
5566
|
+
}
|
|
5567
|
+
}
|
|
5568
|
+
/** **One instance's item, dropped** — and the map's own key with its last instance, as `releaseArrivals` drops a region's. */
|
|
5569
|
+
releaseItem(mapId, branchPath) {
|
|
5570
|
+
this.items.ended.delete(`${mapId} ${branchPath}`);
|
|
5571
|
+
const byPath = this.items.held.get(mapId);
|
|
5572
|
+
if (byPath === void 0) return;
|
|
5573
|
+
byPath.delete(branchPath);
|
|
5574
|
+
if (byPath.size === 0) this.items.held.delete(mapId);
|
|
5575
|
+
}
|
|
5576
|
+
/** **Everything instances keep, dropped** — the run-level moments, where no node of the run is entered again. */
|
|
5577
|
+
releaseAllItems() {
|
|
5578
|
+
this.items.held.clear();
|
|
5579
|
+
this.items.nested.clear();
|
|
5580
|
+
this.items.ended.clear();
|
|
5581
|
+
}
|
|
3750
5582
|
/** The arrival map for one region, created on first arrival. */
|
|
3751
5583
|
arrivalsOf(fanoutId, rp) {
|
|
3752
5584
|
const byPath = this.arrivals.get(fanoutId) ?? /* @__PURE__ */ new Map();
|
|
@@ -3887,13 +5719,18 @@ var Driver = class {
|
|
|
3887
5719
|
*
|
|
3888
5720
|
* A record is pushed to `log` as well, by reference: `effectiveOutcome` reads
|
|
3889
5721
|
* the stored object and `OR-R8` derives rather than appends, so a second copy
|
|
3890
|
-
* here would be a second truth.
|
|
5722
|
+
* here would be a second truth. **And to its task's sub-log in {@link TaskLog}**,
|
|
5723
|
+
* by reference too (`RN-R13` v0.30, ADR 0121, W195) — here, the one place every
|
|
5724
|
+
* element passes, so the view can never lag the log. What leaves through `onFoldInput` **is** a
|
|
3891
5725
|
* copy, frozen, for the same reason `snapshot()` is: a listener holding the
|
|
3892
5726
|
* driver's own array could forge a settlement and bypass `RS-R1`.
|
|
3893
5727
|
*/
|
|
3894
5728
|
append(input) {
|
|
3895
5729
|
const seq = this.inputs.push(input) - 1;
|
|
3896
|
-
if (input.kind === "outcome" || input.kind === "resolution")
|
|
5730
|
+
if (input.kind === "outcome" || input.kind === "resolution") {
|
|
5731
|
+
this.log.push(input.record);
|
|
5732
|
+
this.taskLog.push(input.record);
|
|
5733
|
+
}
|
|
3897
5734
|
const replay = this.replay;
|
|
3898
5735
|
if (replay !== null && seq < replay.stored.length) {
|
|
3899
5736
|
const same = isFed(input) ? replay.feeding === seq && input.kind === replay.stored[seq]?.kind : sameData(input, replay.stored[seq]);
|
|
@@ -3902,12 +5739,22 @@ var Driver = class {
|
|
|
3902
5739
|
if (this.opts.onFoldInput && this.delivers(seq)) this.opts.onFoldInput(freezeInput(input), seq);
|
|
3903
5740
|
return seq;
|
|
3904
5741
|
}
|
|
5742
|
+
/**
|
|
5743
|
+
* **A stored `interrupted` element, fed** (`RN-R19`, `RN-R32`): the replay holds as the stopped run was held — the hold
|
|
5744
|
+
* BEFORE the element, as a stop is taken — and appends the element as stored. Neither `deaf` nor a cancel nor the
|
|
5745
|
+
* collections' release: those were the stopped driver's, and a replay hands a scheduler nothing.
|
|
5746
|
+
*/
|
|
5747
|
+
feedInterrupted(e) {
|
|
5748
|
+
this.stopRequest = { actor: e.actor, stopping: /* @__PURE__ */ new Map(), pending: null, took: null };
|
|
5749
|
+
this.append({ kind: "interrupted", actor: e.actor, reason: e.reason ?? null });
|
|
5750
|
+
}
|
|
3905
5751
|
emit(e) {
|
|
3906
5752
|
const policy = this.opts.joinPolicy;
|
|
3907
5753
|
if (policy && this.joinStateHas(e.regionId, e.regionPath)) {
|
|
3908
5754
|
this.joinStateSet(e.regionId, e.regionPath, policy.step(this.joinStateGet(e.regionId, e.regionPath), e));
|
|
3909
5755
|
}
|
|
3910
5756
|
const event = Object.freeze({ ...e });
|
|
5757
|
+
this.trackItems(e);
|
|
3911
5758
|
const seq = this.append({ kind: "region", event });
|
|
3912
5759
|
if (this.delivers(seq)) this.opts.onRegionEvent?.(event);
|
|
3913
5760
|
}
|
|
@@ -3978,7 +5825,7 @@ var Driver = class {
|
|
|
3978
5825
|
joinId: region.joinId,
|
|
3979
5826
|
fired: true
|
|
3980
5827
|
});
|
|
3981
|
-
if (this.
|
|
5828
|
+
if (this.shut()) return;
|
|
3982
5829
|
const collected = collectArrivals(this.arrivals.get(region.fanoutId)?.get(rp));
|
|
3983
5830
|
this.releaseArrivals(region.fanoutId, rp);
|
|
3984
5831
|
const join = this.graph.byId.get(region.joinId);
|
|
@@ -4108,7 +5955,7 @@ var Driver = class {
|
|
|
4108
5955
|
* the way in.
|
|
4109
5956
|
*/
|
|
4110
5957
|
effectiveOutcomeOf(taskId) {
|
|
4111
|
-
const o = effectiveOutcome(this.
|
|
5958
|
+
const o = effectiveOutcome(this.taskLog.of(taskId), taskId);
|
|
4112
5959
|
return o === null ? null : cloneRecord(o);
|
|
4113
5960
|
}
|
|
4114
5961
|
terminalStateOf(taskId) {
|
|
@@ -4116,134 +5963,511 @@ var Driver = class {
|
|
|
4116
5963
|
}
|
|
4117
5964
|
};
|
|
4118
5965
|
|
|
4119
|
-
// src/fold/
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
return withOutcome({
|
|
4134
|
-
...prev,
|
|
4135
|
-
runId: prev.runId ?? runId2,
|
|
4136
|
-
nodes: { ...prev.nodes, [input.taskId]: { nodeId: nodeId2, branchPath: branchPath2, state: "running", result: null, error: null } }
|
|
4137
|
-
});
|
|
5966
|
+
// src/fold/accumulator.ts
|
|
5967
|
+
var Suspended = class {
|
|
5968
|
+
items = [];
|
|
5969
|
+
alive = /* @__PURE__ */ new Map();
|
|
5970
|
+
gen = 0;
|
|
5971
|
+
count;
|
|
5972
|
+
constructor(from, count) {
|
|
5973
|
+
this.count = count;
|
|
5974
|
+
const g = ++this.gen;
|
|
5975
|
+
for (const v of from) {
|
|
5976
|
+
this.alive.set(v, g);
|
|
5977
|
+
this.items.push({ v, g });
|
|
5978
|
+
}
|
|
5979
|
+
count(from.length);
|
|
4138
5980
|
}
|
|
4139
|
-
|
|
4140
|
-
return
|
|
5981
|
+
has(v) {
|
|
5982
|
+
return this.alive.has(v);
|
|
4141
5983
|
}
|
|
4142
|
-
|
|
4143
|
-
if (
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
});
|
|
5984
|
+
add(v) {
|
|
5985
|
+
if (this.alive.has(v)) return;
|
|
5986
|
+
const g = ++this.gen;
|
|
5987
|
+
this.alive.set(v, g);
|
|
5988
|
+
this.items.push({ v, g });
|
|
4148
5989
|
}
|
|
4149
|
-
|
|
4150
|
-
|
|
5990
|
+
remove(v) {
|
|
5991
|
+
this.alive.delete(v);
|
|
4151
5992
|
}
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
5993
|
+
toArray() {
|
|
5994
|
+
this.count(this.items.length);
|
|
5995
|
+
const out = [];
|
|
5996
|
+
for (const e of this.items) if (this.alive.get(e.v) === e.g) out.push(e.v);
|
|
5997
|
+
return out;
|
|
4156
5998
|
}
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
5999
|
+
};
|
|
6000
|
+
function regionWeight(opened, joined) {
|
|
6001
|
+
const unjoined = joined !== true ? 1 : 0;
|
|
6002
|
+
return { unjoined, openedNothing: opened.length === 0 && joined !== true ? 1 : 0 };
|
|
6003
|
+
}
|
|
6004
|
+
var hasOwn2 = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
6005
|
+
function put(o, k, v) {
|
|
6006
|
+
if (k === "__proto__") Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true });
|
|
6007
|
+
else o[k] = v;
|
|
6008
|
+
}
|
|
6009
|
+
var Accumulator = class _Accumulator {
|
|
6010
|
+
runId;
|
|
6011
|
+
outcome;
|
|
6012
|
+
haltedBy;
|
|
6013
|
+
cancelRequest;
|
|
6014
|
+
interruption;
|
|
6015
|
+
paused;
|
|
6016
|
+
/** Entries written since the load, by task id, in first-write order. A task id not here reads through to the base. */
|
|
6017
|
+
nodes = /* @__PURE__ */ new Map();
|
|
6018
|
+
/** Instances touched since the load, by opener and then by the opener's branch path, in first-touch order. */
|
|
6019
|
+
regions = /* @__PURE__ */ new Map();
|
|
6020
|
+
pathEnds = [];
|
|
6021
|
+
resumes = [];
|
|
6022
|
+
gated = [];
|
|
6023
|
+
counts = null;
|
|
6024
|
+
base;
|
|
6025
|
+
counter;
|
|
6026
|
+
constructor(base, counter) {
|
|
6027
|
+
this.base = base;
|
|
6028
|
+
this.counter = counter;
|
|
6029
|
+
this.runId = base.runId;
|
|
6030
|
+
this.outcome = base.outcome;
|
|
6031
|
+
this.haltedBy = base.haltedBy;
|
|
6032
|
+
this.cancelRequest = base.cancelRequest;
|
|
6033
|
+
this.interruption = base.interruption;
|
|
6034
|
+
this.paused = base.paused;
|
|
6035
|
+
}
|
|
6036
|
+
/** An accumulator over `state`, which it reads and never mutates. `counter`, when given, is charged for its work. */
|
|
6037
|
+
static load(state, counter) {
|
|
6038
|
+
return new _Accumulator(state, counter);
|
|
6039
|
+
}
|
|
6040
|
+
tick(n = 1) {
|
|
6041
|
+
if (this.counter !== void 0) this.counter.visits += n;
|
|
6042
|
+
}
|
|
6043
|
+
// --- the reads a runtime keeps an accumulator OPEN for — W196, ADR 0122, `RN-R44` -----------------------------
|
|
6044
|
+
/**
|
|
6045
|
+
* **The outcome as the last applied input left it** — what {@link materialise} would write into `outcome`, read in
|
|
6046
|
+
* O(1). The runtime's `RunHandle.outcome()` and `done`'s predicate read this, never a materialisation (P39 prediction 4).
|
|
6047
|
+
*/
|
|
6048
|
+
outcomeNow() {
|
|
6049
|
+
return this.outcome;
|
|
4163
6050
|
}
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
6051
|
+
/**
|
|
6052
|
+
* **One task's entry, or `undefined`** — the object {@link materialise} would place at `nodes[taskId]`, read in O(1).
|
|
6053
|
+
* An OWN entry only: a task id naming an `Object.prototype` member (`toString`, `__proto__`) is a task the run never
|
|
6054
|
+
* dispatched, where `prev.nodes[taskId]` on a plain record would read the inherited member.
|
|
6055
|
+
*/
|
|
6056
|
+
entry(taskId) {
|
|
6057
|
+
if (this.nodes.has(taskId)) return this.nodes.get(taskId);
|
|
6058
|
+
return hasOwn2(this.base.nodes, taskId) ? this.base.nodes[taskId] : void 0;
|
|
6059
|
+
}
|
|
6060
|
+
// --- reads, through to the base ---------------------------------------------------------------------------
|
|
6061
|
+
/** `prev.nodes[taskId]` — the entry written since the load, else the base's own read, prototype and all. */
|
|
6062
|
+
node(taskId) {
|
|
6063
|
+
return this.nodes.has(taskId) ? this.nodes.get(taskId) : this.base.nodes[taskId];
|
|
6064
|
+
}
|
|
6065
|
+
/** `prev.regions[regionId]?.[regionPath]` — the touched instance, else the base's. */
|
|
6066
|
+
instance(regionId, regionPath) {
|
|
6067
|
+
const touched = this.regions.get(regionId)?.get(regionPath);
|
|
6068
|
+
return touched ?? this.base.regions[regionId]?.[regionPath];
|
|
6069
|
+
}
|
|
6070
|
+
/** The counts, taken from everything the state holds on first need — O(the state), once per accumulator. */
|
|
6071
|
+
tally() {
|
|
6072
|
+
if (this.counts !== null) return this.counts;
|
|
6073
|
+
const c = { byState: /* @__PURE__ */ new Map(), nodes: 0, unjoined: 0, openedNothing: 0, noEligibleEdge: 0, ends: 0, pathEnds: 0 };
|
|
6074
|
+
const baseNodes = this.base.nodes;
|
|
6075
|
+
const keys = Object.keys(baseNodes);
|
|
6076
|
+
this.tick(keys.length);
|
|
6077
|
+
for (const k of keys) if (!this.nodes.has(k)) this.countNode(c, baseNodes[k], 1);
|
|
6078
|
+
for (const n of this.nodes.values()) this.countNode(c, n, 1);
|
|
6079
|
+
this.tick(this.nodes.size);
|
|
6080
|
+
const baseRegions = this.base.regions;
|
|
6081
|
+
for (const id of Object.keys(baseRegions)) {
|
|
6082
|
+
const byPath = baseRegions[id];
|
|
6083
|
+
const touched = this.regions.get(id);
|
|
6084
|
+
for (const p of Object.keys(byPath)) {
|
|
6085
|
+
this.tick();
|
|
6086
|
+
if (touched?.has(p)) continue;
|
|
6087
|
+
const r = byPath[p];
|
|
6088
|
+
const w = regionWeight(r.opened, r.joined);
|
|
6089
|
+
c.unjoined += w.unjoined;
|
|
6090
|
+
c.openedNothing += w.openedNothing;
|
|
6091
|
+
}
|
|
6092
|
+
}
|
|
6093
|
+
for (const byPath of this.regions.values()) {
|
|
6094
|
+
for (const acc of byPath.values()) {
|
|
6095
|
+
this.tick();
|
|
6096
|
+
const w = regionWeight(acc.opened, acc.joined);
|
|
6097
|
+
c.unjoined += w.unjoined;
|
|
6098
|
+
c.openedNothing += w.openedNothing;
|
|
6099
|
+
}
|
|
6100
|
+
}
|
|
6101
|
+
for (const list of [this.base.pathEnds, this.pathEnds]) {
|
|
6102
|
+
this.tick(list.length);
|
|
6103
|
+
for (const e of list) {
|
|
6104
|
+
c.pathEnds += 1;
|
|
6105
|
+
if (e.reason === "no-eligible-edge") c.noEligibleEdge += 1;
|
|
6106
|
+
if (e.reason === "end") c.ends += 1;
|
|
6107
|
+
}
|
|
6108
|
+
}
|
|
6109
|
+
this.counts = c;
|
|
6110
|
+
return c;
|
|
6111
|
+
}
|
|
6112
|
+
countNode(c, n, by) {
|
|
6113
|
+
c.nodes += by;
|
|
6114
|
+
c.byState.set(n.state, (c.byState.get(n.state) ?? 0) + by);
|
|
6115
|
+
}
|
|
6116
|
+
stateCount(state) {
|
|
6117
|
+
return this.tally().byState.get(state) ?? 0;
|
|
6118
|
+
}
|
|
6119
|
+
// --- writes ------------------------------------------------------------------------------------------------
|
|
6120
|
+
/** `nodes[taskId] = entry`, the counts kept. An entry is REPLACED, never mutated: a materialised state may hold the old one. */
|
|
6121
|
+
setNode(taskId, entry) {
|
|
6122
|
+
if (this.counts !== null) {
|
|
6123
|
+
const own3 = this.nodes.has(taskId) || hasOwn2(this.base.nodes, taskId);
|
|
6124
|
+
if (own3) this.countNode(this.counts, this.node(taskId), -1);
|
|
6125
|
+
this.countNode(this.counts, entry, 1);
|
|
6126
|
+
}
|
|
6127
|
+
this.nodes.set(taskId, entry);
|
|
6128
|
+
this.tick();
|
|
6129
|
+
}
|
|
6130
|
+
/**
|
|
6131
|
+
* The instance at (`regionId`, `regionPath`) to write to — the touched one, else a COPY of the base's (O(its branch
|
|
6132
|
+
* paths), once), else an empty one; with its share of the region counts taken out, for {@link settleRegion} to put back.
|
|
6133
|
+
*/
|
|
6134
|
+
touchRegion(regionId, regionPath) {
|
|
6135
|
+
let byPath = this.regions.get(regionId);
|
|
6136
|
+
let acc = byPath?.get(regionPath);
|
|
6137
|
+
if (acc === void 0) {
|
|
6138
|
+
const at = this.base.regions[regionId]?.[regionPath];
|
|
6139
|
+
if (at) {
|
|
6140
|
+
this.tick(at.opened.length + Object.keys(at.settled).length);
|
|
6141
|
+
acc = {
|
|
6142
|
+
origin: at,
|
|
6143
|
+
opened: [...at.opened],
|
|
6144
|
+
openedSet: new Set(at.opened),
|
|
6145
|
+
settled: new Map(Object.entries(at.settled)),
|
|
6146
|
+
suspended: new Suspended(at.suspended, (n) => this.tick(n)),
|
|
6147
|
+
joined: at.joined
|
|
6148
|
+
};
|
|
6149
|
+
} else {
|
|
6150
|
+
acc = {
|
|
6151
|
+
origin: { opened: [], settled: {}, suspended: [], joined: null },
|
|
6152
|
+
opened: [],
|
|
6153
|
+
openedSet: /* @__PURE__ */ new Set(),
|
|
6154
|
+
settled: /* @__PURE__ */ new Map(),
|
|
6155
|
+
suspended: new Suspended([], (n) => this.tick(n)),
|
|
6156
|
+
joined: null
|
|
6157
|
+
};
|
|
6158
|
+
}
|
|
6159
|
+
if (byPath === void 0) {
|
|
6160
|
+
byPath = /* @__PURE__ */ new Map();
|
|
6161
|
+
this.regions.set(regionId, byPath);
|
|
6162
|
+
}
|
|
6163
|
+
byPath.set(regionPath, acc);
|
|
6164
|
+
const inner = this.base.regions[regionId];
|
|
6165
|
+
if (this.counts !== null && at && inner !== void 0 && hasOwn2(this.base.regions, regionId) && hasOwn2(inner, regionPath)) {
|
|
6166
|
+
this.unweigh(regionWeight(at.opened, at.joined));
|
|
6167
|
+
}
|
|
6168
|
+
} else if (this.counts !== null) {
|
|
6169
|
+
this.unweigh(regionWeight(acc.opened, acc.joined));
|
|
6170
|
+
}
|
|
6171
|
+
this.tick();
|
|
6172
|
+
return acc;
|
|
6173
|
+
}
|
|
6174
|
+
unweigh(w) {
|
|
6175
|
+
this.counts.unjoined -= w.unjoined;
|
|
6176
|
+
this.counts.openedNothing -= w.openedNothing;
|
|
6177
|
+
}
|
|
6178
|
+
/** The instance's share of the region counts put back after a write. */
|
|
6179
|
+
settleRegion(acc) {
|
|
6180
|
+
if (this.counts === null) return;
|
|
6181
|
+
const w = regionWeight(acc.opened, acc.joined);
|
|
6182
|
+
this.counts.unjoined += w.unjoined;
|
|
6183
|
+
this.counts.openedNothing += w.openedNothing;
|
|
6184
|
+
}
|
|
6185
|
+
addPathEnd(e) {
|
|
6186
|
+
this.pathEnds.push(e);
|
|
6187
|
+
this.tick();
|
|
6188
|
+
if (this.counts !== null) {
|
|
6189
|
+
this.counts.pathEnds += 1;
|
|
6190
|
+
if (e.reason === "no-eligible-edge") this.counts.noEligibleEdge += 1;
|
|
6191
|
+
if (e.reason === "end") this.counts.ends += 1;
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
// --- the arms -------------------------------------------------------------------------------------------------
|
|
6195
|
+
/**
|
|
6196
|
+
* **Apply one input.** `true` when the arm moved the state — where `step` before W194 returned a NEW object — and
|
|
6197
|
+
* `false` where it returned the state it was handed, so `step` can return that same object. Throws where
|
|
6198
|
+
* that `step` threw: a malformed task id (`parseTaskId`), a kind it does not name (`input.record` undefined) — within
|
|
6199
|
+
* the module header's stated limit on region paths.
|
|
6200
|
+
*/
|
|
6201
|
+
apply(input) {
|
|
6202
|
+
this.tick();
|
|
6203
|
+
if (input.kind === "halted" || input.kind === "stopped") {
|
|
6204
|
+
this.haltedBy = input.reason;
|
|
6205
|
+
this.outcome = "halted";
|
|
6206
|
+
return true;
|
|
6207
|
+
}
|
|
6208
|
+
if (input.kind === "cancelRequested") {
|
|
6209
|
+
if (this.cancelRequest !== null || this.outcome === "cancelled") return false;
|
|
6210
|
+
this.cancelRequest = { actor: input.actor, reason: input.reason };
|
|
6211
|
+
return this.withOutcome();
|
|
6212
|
+
}
|
|
6213
|
+
if (input.kind === "interrupted") {
|
|
6214
|
+
if (this.interruption !== null) return false;
|
|
6215
|
+
this.interruption = { actor: input.actor, reason: input.reason };
|
|
6216
|
+
return this.withOutcome();
|
|
6217
|
+
}
|
|
6218
|
+
if (input.kind === "paused" || input.kind === "unpaused") {
|
|
6219
|
+
const paused = input.kind === "paused";
|
|
6220
|
+
if (this.paused === paused) return false;
|
|
6221
|
+
this.paused = paused;
|
|
6222
|
+
return true;
|
|
6223
|
+
}
|
|
6224
|
+
if (input.kind === "region") {
|
|
6225
|
+
this.applyRegion(input.event);
|
|
6226
|
+
return this.withOutcome();
|
|
6227
|
+
}
|
|
6228
|
+
if (input.kind === "dispatched") {
|
|
6229
|
+
const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, attempt: attempt2 } = parseTaskId(input.taskId);
|
|
6230
|
+
if (this.node(input.taskId)) return false;
|
|
6231
|
+
this.runId = this.runId ?? runId2;
|
|
6232
|
+
this.setNode(input.taskId, {
|
|
6233
|
+
runId: runId2,
|
|
6234
|
+
nodeId: nodeId2,
|
|
6235
|
+
branchPath: branchPath2,
|
|
6236
|
+
flowAttempt: attempt2,
|
|
6237
|
+
attemptNumber: null,
|
|
6238
|
+
state: "running",
|
|
6239
|
+
result: null,
|
|
6240
|
+
error: null
|
|
6241
|
+
});
|
|
6242
|
+
return this.withOutcome();
|
|
6243
|
+
}
|
|
6244
|
+
if (input.kind === "asked") {
|
|
6245
|
+
const { runId: runId2, nodeId: nodeId2, branchPath: branchPath2, attempt: attempt2 } = parseTaskId(input.taskId);
|
|
6246
|
+
if (this.node(input.taskId)) return false;
|
|
6247
|
+
this.runId = this.runId ?? runId2;
|
|
6248
|
+
this.setNode(input.taskId, {
|
|
6249
|
+
runId: runId2,
|
|
6250
|
+
nodeId: nodeId2,
|
|
6251
|
+
branchPath: branchPath2,
|
|
6252
|
+
flowAttempt: attempt2,
|
|
6253
|
+
attemptNumber: null,
|
|
6254
|
+
state: "suspended",
|
|
6255
|
+
result: null,
|
|
6256
|
+
error: null,
|
|
6257
|
+
input: structuredClone(input.input)
|
|
6258
|
+
});
|
|
6259
|
+
return this.withOutcome();
|
|
6260
|
+
}
|
|
6261
|
+
if (input.kind === "pathEnded") {
|
|
6262
|
+
this.addPathEnd({ reason: input.reason, at: input.at });
|
|
6263
|
+
return this.withOutcome();
|
|
6264
|
+
}
|
|
6265
|
+
if (input.kind === "regionEntered") {
|
|
6266
|
+
if (this.instance(input.regionId, input.regionPath)) return false;
|
|
6267
|
+
const acc = this.touchRegion(input.regionId, input.regionPath);
|
|
6268
|
+
this.settleRegion(acc);
|
|
6269
|
+
return this.withOutcome();
|
|
6270
|
+
}
|
|
6271
|
+
if (input.kind === "resumed") {
|
|
6272
|
+
this.resumes.push([...input.taskIds]);
|
|
6273
|
+
this.tick(input.taskIds.length + 1);
|
|
6274
|
+
if (this.interruption !== null) {
|
|
6275
|
+
this.interruption = null;
|
|
6276
|
+
return this.withOutcome();
|
|
6277
|
+
}
|
|
6278
|
+
return true;
|
|
6279
|
+
}
|
|
6280
|
+
if (input.kind === "gated") {
|
|
6281
|
+
const d = { gateId: input.gateId, branchPath: input.branchPath, route: input.route };
|
|
6282
|
+
if (input.key !== void 0) d.key = structuredClone(input.key);
|
|
6283
|
+
this.gated.push(d);
|
|
6284
|
+
this.tick();
|
|
6285
|
+
return true;
|
|
6286
|
+
}
|
|
6287
|
+
if (input.kind === "attempted") {
|
|
6288
|
+
const node = this.node(input.taskId);
|
|
6289
|
+
if (node === void 0) return false;
|
|
6290
|
+
this.setNode(input.taskId, { ...node, attemptNumber: input.attemptNumber });
|
|
6291
|
+
return true;
|
|
6292
|
+
}
|
|
6293
|
+
if (input.kind === "joinSettled") {
|
|
6294
|
+
const acc = this.touchRegion(input.regionId, input.regionPath);
|
|
6295
|
+
acc.joined = input.fired;
|
|
6296
|
+
this.settleRegion(acc);
|
|
6297
|
+
return this.withOutcome();
|
|
6298
|
+
}
|
|
6299
|
+
const r = input.record;
|
|
6300
|
+
const { runId, nodeId, branchPath, attempt } = parseTaskId(r.taskId);
|
|
6301
|
+
const existing = this.node(r.taskId);
|
|
6302
|
+
const attemptNumber = existing?.attemptNumber ?? null;
|
|
6303
|
+
if (r.kind === "outcome") {
|
|
6304
|
+
this.setNode(r.taskId, {
|
|
6305
|
+
runId,
|
|
6306
|
+
nodeId,
|
|
6307
|
+
branchPath,
|
|
6308
|
+
flowAttempt: attempt,
|
|
6309
|
+
attemptNumber,
|
|
6310
|
+
// `user_action` is `suspended` at flow level: a resolution resumes it.
|
|
6311
|
+
state: r.terminalState === "user_action" ? "suspended" : r.terminalState,
|
|
6312
|
+
result: r.result,
|
|
6313
|
+
error: r.error
|
|
6314
|
+
});
|
|
6315
|
+
} else {
|
|
6316
|
+
this.setNode(r.taskId, {
|
|
6317
|
+
runId: existing?.runId ?? runId,
|
|
6318
|
+
nodeId: existing?.nodeId ?? nodeId,
|
|
6319
|
+
branchPath: existing?.branchPath ?? branchPath,
|
|
6320
|
+
flowAttempt: existing?.flowAttempt ?? attempt,
|
|
6321
|
+
attemptNumber,
|
|
6322
|
+
state: r.resolvedAs,
|
|
6323
|
+
result: r.result,
|
|
6324
|
+
error: r.error,
|
|
6325
|
+
...existing !== void 0 && "input" in existing ? { input: existing.input } : {}
|
|
6326
|
+
});
|
|
6327
|
+
}
|
|
6328
|
+
this.runId = this.runId ?? runId;
|
|
6329
|
+
return this.withOutcome();
|
|
6330
|
+
}
|
|
6331
|
+
applyRegion(e) {
|
|
6332
|
+
const acc = this.touchRegion(e.regionId, e.regionPath);
|
|
6333
|
+
switch (e.type) {
|
|
6334
|
+
case "branch.opened":
|
|
6335
|
+
if (!acc.openedSet.has(e.branchPath)) {
|
|
6336
|
+
acc.opened.push(e.branchPath);
|
|
6337
|
+
acc.openedSet.add(e.branchPath);
|
|
6338
|
+
}
|
|
6339
|
+
break;
|
|
6340
|
+
case "branch.arrived":
|
|
6341
|
+
case "branch.exited":
|
|
6342
|
+
case "branch.lost": {
|
|
6343
|
+
const how = e.type === "branch.arrived" ? "arrived" : e.type === "branch.exited" ? "exited" : "lost";
|
|
6344
|
+
acc.settled.set(e.branchPath, how);
|
|
6345
|
+
acc.suspended.remove(e.branchPath);
|
|
6346
|
+
break;
|
|
6347
|
+
}
|
|
6348
|
+
case "branch.suspended":
|
|
6349
|
+
acc.suspended.add(e.branchPath);
|
|
6350
|
+
break;
|
|
6351
|
+
case "branch.resumed":
|
|
6352
|
+
acc.suspended.remove(e.branchPath);
|
|
6353
|
+
break;
|
|
6354
|
+
}
|
|
6355
|
+
this.settleRegion(acc);
|
|
6356
|
+
}
|
|
6357
|
+
/**
|
|
6358
|
+
* **Section 3's precedence, read off the counts** — `RN-R7`, `RN-R8`, `RN-R10`, `RN-R28`, `RN-R30` and `RN-R32` in the
|
|
6359
|
+
* one order they have always had. Called by exactly the arms that recomputed before W194. Always `true`: an arm that
|
|
6360
|
+
* recomputes has moved the state.
|
|
6361
|
+
*/
|
|
6362
|
+
withOutcome() {
|
|
6363
|
+
this.outcome = this.precedence();
|
|
6364
|
+
return true;
|
|
6365
|
+
}
|
|
6366
|
+
precedence() {
|
|
6367
|
+
if (this.haltedBy !== null) return "halted";
|
|
6368
|
+
if (this.cancelRequest === null && this.stateCount("cancelled") > 0) return "cancelled";
|
|
6369
|
+
if (this.cancelRequest !== null) return this.stateCount("running") > 0 ? "cancelling" : "cancelled";
|
|
6370
|
+
if (this.interruption !== null) return "interrupted";
|
|
6371
|
+
if (this.stateCount("suspended") > 0) return "awaiting_action";
|
|
6372
|
+
if (this.stateCount("running") > 0) return "running";
|
|
6373
|
+
const c = this.tally();
|
|
6374
|
+
if (c.nodes === 0 && c.pathEnds === 0 && c.openedNothing === 0) return "running";
|
|
6375
|
+
if (c.unjoined > 0) return "stuck";
|
|
6376
|
+
if (c.noEligibleEdge > 0) return "failed";
|
|
6377
|
+
if (c.ends > 0) return "completed";
|
|
6378
|
+
if (this.stateCount("failed") > 0) return "failed";
|
|
6379
|
+
return "completed";
|
|
6380
|
+
}
|
|
6381
|
+
// --- materialisation ------------------------------------------------------------------------------------------
|
|
6382
|
+
/**
|
|
6383
|
+
* **One plain `RunState`**, built from the base and what was written since the load: a record or list nothing wrote is
|
|
6384
|
+
* the base's own object, and one that was written is a new one with the base's entries in their places and the new
|
|
6385
|
+
* entries after them, in first-write order — the key order a spread-per-step produced. O(what it builds).
|
|
6386
|
+
*/
|
|
6387
|
+
materialise() {
|
|
6388
|
+
const b = this.base;
|
|
6389
|
+
let nodes = b.nodes;
|
|
6390
|
+
if (this.nodes.size > 0) {
|
|
6391
|
+
nodes = { ...b.nodes };
|
|
6392
|
+
this.tick(Object.keys(b.nodes).length);
|
|
6393
|
+
for (const [k, n] of this.nodes) put(nodes, k, n);
|
|
6394
|
+
this.tick(this.nodes.size);
|
|
6395
|
+
}
|
|
6396
|
+
let regions = b.regions;
|
|
6397
|
+
if (this.regions.size > 0) {
|
|
6398
|
+
regions = { ...b.regions };
|
|
6399
|
+
this.tick(Object.keys(b.regions).length);
|
|
6400
|
+
for (const [id, byPath] of this.regions) {
|
|
6401
|
+
const inner = { ...b.regions[id] };
|
|
6402
|
+
this.tick(Object.keys(inner).length);
|
|
6403
|
+
for (const [p, acc] of byPath) put(inner, p, this.materialiseRegion(acc));
|
|
6404
|
+
put(regions, id, inner);
|
|
6405
|
+
}
|
|
6406
|
+
}
|
|
6407
|
+
return {
|
|
6408
|
+
...b,
|
|
6409
|
+
runId: this.runId,
|
|
6410
|
+
outcome: this.outcome,
|
|
6411
|
+
nodes,
|
|
6412
|
+
regions,
|
|
6413
|
+
pathEnds: this.list(b.pathEnds, this.pathEnds),
|
|
6414
|
+
haltedBy: this.haltedBy,
|
|
6415
|
+
cancelRequest: this.cancelRequest,
|
|
6416
|
+
interruption: this.interruption,
|
|
6417
|
+
paused: this.paused,
|
|
6418
|
+
resumes: this.list(b.resumes, this.resumes),
|
|
6419
|
+
gated: this.list(b.gated, this.gated)
|
|
4185
6420
|
};
|
|
4186
6421
|
}
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
}
|
|
4192
|
-
|
|
4193
|
-
|
|
6422
|
+
list(base, added) {
|
|
6423
|
+
if (added.length === 0) return base;
|
|
6424
|
+
this.tick(base.length + added.length);
|
|
6425
|
+
return [...base, ...added];
|
|
6426
|
+
}
|
|
6427
|
+
materialiseRegion(acc) {
|
|
6428
|
+
const settled = {};
|
|
6429
|
+
for (const [k, v] of acc.settled) settled[k] = v;
|
|
6430
|
+
this.tick(acc.opened.length + acc.settled.size + 1);
|
|
6431
|
+
return { ...acc.origin, opened: [...acc.opened], settled, suspended: acc.suspended.toArray(), joined: acc.joined };
|
|
6432
|
+
}
|
|
6433
|
+
};
|
|
6434
|
+
function foldWith(inputs, counter) {
|
|
6435
|
+
const acc = Accumulator.load(emptyState(), counter);
|
|
6436
|
+
for (const i of inputs) acc.apply(i);
|
|
6437
|
+
return acc.materialise();
|
|
4194
6438
|
}
|
|
4195
|
-
function
|
|
4196
|
-
|
|
6439
|
+
function stepWith(prev, input, counter) {
|
|
6440
|
+
const acc = Accumulator.load(prev, counter);
|
|
6441
|
+
return acc.apply(input) ? acc.materialise() : prev;
|
|
4197
6442
|
}
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
if (!cur.suspended.includes(e.branchPath)) cur.suspended.push(e.branchPath);
|
|
4215
|
-
break;
|
|
4216
|
-
case "branch.resumed":
|
|
4217
|
-
cur.suspended = cur.suspended.filter((b) => b !== e.branchPath);
|
|
4218
|
-
break;
|
|
4219
|
-
}
|
|
4220
|
-
return { ...prev, regions: withRegion(prev.regions, e.regionId, e.regionPath, cur) };
|
|
6443
|
+
|
|
6444
|
+
// src/fold/run-state.ts
|
|
6445
|
+
function emptyState() {
|
|
6446
|
+
return {
|
|
6447
|
+
runId: null,
|
|
6448
|
+
outcome: "running",
|
|
6449
|
+
nodes: {},
|
|
6450
|
+
regions: {},
|
|
6451
|
+
pathEnds: [],
|
|
6452
|
+
haltedBy: null,
|
|
6453
|
+
cancelRequest: null,
|
|
6454
|
+
interruption: null,
|
|
6455
|
+
paused: false,
|
|
6456
|
+
resumes: [],
|
|
6457
|
+
gated: []
|
|
6458
|
+
};
|
|
4221
6459
|
}
|
|
4222
|
-
function
|
|
4223
|
-
|
|
4224
|
-
const nodes = Object.values(s.nodes);
|
|
4225
|
-
if (nodes.some((n) => n.state === "suspended")) return { ...s, outcome: "awaiting_action" };
|
|
4226
|
-
if (nodes.some((n) => n.state === "running")) return { ...s, outcome: "running" };
|
|
4227
|
-
const openedNothing = allRegionRuns(s).some((r) => r.opened.length === 0 && r.joined !== true);
|
|
4228
|
-
if (nodes.length === 0 && s.pathEnds.length === 0 && !openedNothing) {
|
|
4229
|
-
return { ...s, outcome: "running" };
|
|
4230
|
-
}
|
|
4231
|
-
if (allRegionRuns(s).some((r) => r.joined !== true)) return { ...s, outcome: "stuck" };
|
|
4232
|
-
if (s.pathEnds.some((e) => e.reason === "no-eligible-edge")) return { ...s, outcome: "failed" };
|
|
4233
|
-
if (s.pathEnds.some((e) => e.reason === "end")) return { ...s, outcome: "completed" };
|
|
4234
|
-
if (nodes.some((n) => n.state === "failed")) return { ...s, outcome: "failed" };
|
|
4235
|
-
return { ...s, outcome: "completed" };
|
|
6460
|
+
function step(prev, input) {
|
|
6461
|
+
return stepWith(prev, input);
|
|
4236
6462
|
}
|
|
4237
6463
|
function fold(inputs) {
|
|
4238
|
-
|
|
4239
|
-
for (const i of inputs) s = step(s, i);
|
|
4240
|
-
return s;
|
|
6464
|
+
return foldWith(inputs);
|
|
4241
6465
|
}
|
|
4242
6466
|
function foldLog(log) {
|
|
4243
6467
|
return fold(log.map((r) => ({ kind: r.kind, record: r })));
|
|
4244
6468
|
}
|
|
4245
6469
|
function isTerminal(s) {
|
|
4246
|
-
return s.outcome !== "running" && s.outcome !== "awaiting_action";
|
|
6470
|
+
return s.outcome !== "running" && s.outcome !== "awaiting_action" && s.outcome !== "cancelling";
|
|
4247
6471
|
}
|
|
4248
6472
|
function nodeOf(s, taskId) {
|
|
4249
6473
|
return s.nodes[taskId] ?? null;
|
|
@@ -4329,6 +6553,7 @@ export {
|
|
|
4329
6553
|
innermostRegionOf,
|
|
4330
6554
|
branchPathOf,
|
|
4331
6555
|
regionChainOf,
|
|
6556
|
+
innermostMapOf,
|
|
4332
6557
|
sharedRegionDepth,
|
|
4333
6558
|
outputIsVisible,
|
|
4334
6559
|
branchIndexOf,
|
|
@@ -4339,6 +6564,8 @@ export {
|
|
|
4339
6564
|
canonical,
|
|
4340
6565
|
findings,
|
|
4341
6566
|
describeFindings,
|
|
6567
|
+
readKeyPath,
|
|
6568
|
+
ASK_REFUSED_KEYS,
|
|
4342
6569
|
EDGE_KEYS_DECLARED,
|
|
4343
6570
|
TASK_KEYS_DECLARED,
|
|
4344
6571
|
NODE_KEYS_DECLARED,
|
|
@@ -4353,9 +6580,9 @@ export {
|
|
|
4353
6580
|
languagesOf,
|
|
4354
6581
|
KEY_PATH_RE,
|
|
4355
6582
|
keyPathProblem,
|
|
4356
|
-
readKeyPath,
|
|
4357
6583
|
GATE_TYPE_KEYS,
|
|
4358
6584
|
gateTypeProblem,
|
|
6585
|
+
routesOf,
|
|
4359
6586
|
distinct,
|
|
4360
6587
|
BUILT_IN_GATE_TYPES,
|
|
4361
6588
|
gateTypesOf,
|
|
@@ -4393,8 +6620,10 @@ export {
|
|
|
4393
6620
|
CHECKED_WORKER_SCHEMAS,
|
|
4394
6621
|
NODE_TYPE_DECLARATION_KEYS,
|
|
4395
6622
|
POOL_SPEC_KEYS,
|
|
6623
|
+
MESSAGE_OPTION_KEYS,
|
|
4396
6624
|
DECLARED_POOL_KEYS,
|
|
4397
6625
|
INSTANCE_KEYS,
|
|
6626
|
+
POOL_INSTANCE_KEYS,
|
|
4398
6627
|
isDerivation,
|
|
4399
6628
|
mergeConfig,
|
|
4400
6629
|
nodeTypeFromDeclaration,
|
|
@@ -4410,6 +6639,7 @@ export {
|
|
|
4410
6639
|
fits,
|
|
4411
6640
|
boundaryTargets,
|
|
4412
6641
|
checkWiring,
|
|
6642
|
+
givenVerdict,
|
|
4413
6643
|
describeVerdict,
|
|
4414
6644
|
VERDICT_KINDS,
|
|
4415
6645
|
WIRING_SHAPES,
|
|
@@ -4429,6 +6659,8 @@ export {
|
|
|
4429
6659
|
eligibleEdges,
|
|
4430
6660
|
gateEdge,
|
|
4431
6661
|
selectEdge,
|
|
6662
|
+
TERMINAL_EVENTS,
|
|
6663
|
+
isTerminalEvent,
|
|
4432
6664
|
InvalidDefinition,
|
|
4433
6665
|
DuplicateTaskId,
|
|
4434
6666
|
UnresolvedMapArity,
|
|
@@ -4437,12 +6669,14 @@ export {
|
|
|
4437
6669
|
BoundaryRefused,
|
|
4438
6670
|
MultipleRuns,
|
|
4439
6671
|
NotStarted,
|
|
6672
|
+
RunControlRefused,
|
|
4440
6673
|
CHECKPOINT_FORMAT,
|
|
4441
6674
|
ResumeRefused,
|
|
4442
6675
|
resumeRefusal,
|
|
4443
6676
|
UndispatchedTask,
|
|
4444
6677
|
MissingJoinPolicy,
|
|
4445
6678
|
Driver,
|
|
6679
|
+
Accumulator,
|
|
4446
6680
|
emptyState,
|
|
4447
6681
|
step,
|
|
4448
6682
|
fold,
|