@evoke-build/evoke 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/core.wasm CHANGED
Binary file
package/dist/project.d.ts CHANGED
@@ -20,7 +20,8 @@ export interface DecideOptions {
20
20
  tags?: string[] | undefined;
21
21
  /** This reflex alone is offered: how a weave decides a fragment, or a step with a value written into its words. */
22
22
  only?: string | undefined;
23
- /** Aborts the adapter call; `decide` rejects with the signal's reason. */
23
+ /** Aborts the adapter call; `decide` rejects with the signal's reason. A weave's run it cancels: every body
24
+ * ended, every step that did not finish `skipped · cancelled`, the rejection's reason carrying the record. */
24
25
  signal?: AbortSignal | undefined;
25
26
  }
26
27
  export interface RunOptions {
@@ -73,7 +74,8 @@ export interface WovenStep<R = AnyReflexes> {
73
74
  }
74
75
  /** A request planned and run: the plan; the whole's status — the worst step's, as the exit codes rank them, or
75
76
  * why nothing ran: refused by the verdict, declined or unanswered at what the plan asked first; per step what
76
- * became of it, none when nothing ran. */
77
+ * became of it, none when nothing ran. A weave the signal cancelled rejects with the signal's reason and carries
78
+ * this record on it as `woven`: the steps that finished as they ended, every other one `skipped · cancelled`. */
77
79
  export interface Woven<R = AnyReflexes> {
78
80
  plan: W.Weave;
79
81
  status: W.Status;
package/dist/project.js CHANGED
@@ -380,9 +380,13 @@ function make(ground, invoked) {
380
380
  return "filled";
381
381
  }
382
382
  /** The plan run: stage by stage, a stage's rounds each taken to the point of running in the words' order, then
383
- * their bodies together; a step decided again with its bound values in its words when the run asks for it. */
383
+ * their bodies together; a step decided again with its bound values in its words when the run asks for it. The
384
+ * signal aborted mid-run is a cancel: every body's group ended, the rounds under way and every round after
385
+ * reported `skipped · cancelled`, and the rejection — the signal's reason — carrying the record once it is
386
+ * whole. */
384
387
  async function executed(woven, options, traces) {
385
388
  const invoked = `weave(${JSON.stringify(shown(woven.input))})`;
389
+ const { signal } = options;
386
390
  const progress = { decided: [], handled: [] };
387
391
  const rounds = new Map();
388
392
  const record = (handling, decision, became) => {
@@ -397,7 +401,7 @@ function make(ground, invoked) {
397
401
  const running = call("weave.execute", { plan, ...gate, weave: woven, progress }, invoked);
398
402
  if (running.type === "done") {
399
403
  const { executed } = running;
400
- return {
404
+ const whole = {
401
405
  plan: woven,
402
406
  status: executed.worst,
403
407
  steps: executed.steps.map(step => ({
@@ -408,10 +412,21 @@ function make(ground, invoked) {
408
412
  rounds: rounds.get(step.step) ?? [],
409
413
  })),
410
414
  };
415
+ if (signal?.aborted)
416
+ throw carrying(signal.reason, whole);
417
+ return whole;
411
418
  }
412
419
  const { todo } = running;
413
420
  if (todo.type === "decide") {
414
- progress.decided.push([todo.asked, await decided(todo.asked, options, traces)]);
421
+ try {
422
+ progress.decided.push([todo.asked, await decided(todo.asked, options, traces)]);
423
+ }
424
+ catch (error) {
425
+ // The signal aborted the decision's call: the round it was for never starts.
426
+ if (!signal?.aborted || error !== signal.reason)
427
+ throw error;
428
+ progress.handled.push({ step: todo.step, round: todo.round, ...CANCELLED });
429
+ }
415
430
  continue;
416
431
  }
417
432
  const bodies = [];
@@ -421,12 +436,17 @@ function make(ground, invoked) {
421
436
  const own = step?.reflex === undefined || !handling.bound?.length ? undefined : traces.get(key({ text: handling.input, only: step.reflex }));
422
437
  const trace = own ?? (step === undefined ? undefined : traces.get(key(askedFor(step, options.tags ?? [])))) ?? [];
423
438
  const decision = lined(handling.decision, { input: handling.input, plan: plan.digest, trace });
439
+ // Nothing starts after the signal: a round handed after it is cancelled without a question asked.
440
+ if (signal?.aborted) {
441
+ record(handling, decision, CANCELLED);
442
+ continue;
443
+ }
424
444
  const readied = await ready(decision, told(options, { step: handling.step, round: handling.round }));
425
445
  if (!readied.ready) {
426
446
  record(handling, readied.decision, { status: readied.status, why: readied.why });
427
447
  continue;
428
448
  }
429
- bodies.push(ran(readied.decision, options.signal).then(became => record(handling, readied.decision, became)));
449
+ bodies.push(ran(readied.decision, signal).then(became => record(handling, readied.decision, became)));
430
450
  }
431
451
  await Promise.all(bodies);
432
452
  }
@@ -491,12 +511,15 @@ function make(ground, invoked) {
491
511
  const options = signal === undefined ? {} : { signal };
492
512
  return decision.outcome === "confirm" ? project.run(decision, { ...options, confirmed: true }) : project.run(decision, options);
493
513
  }
494
- /** One body run for a weave: what it returned, or its failure as the step's own outcome, never the weave's. */
514
+ /** One body run for a weave: what it returned, or its failure as the step's own outcome, never the weave's; a
515
+ * body the signal ended is the round cancelled. */
495
516
  async function ran(decision, signal) {
496
517
  try {
497
518
  return { status: "ran", result: await bodied(decision, signal) };
498
519
  }
499
520
  catch (error) {
521
+ if (signal?.aborted && error === signal.reason)
522
+ return CANCELLED;
500
523
  if (error instanceof FailureError) {
501
524
  return { status: "failed", why: { type: "said", message: error.why === undefined ? error.what : `${error.what}: ${error.why}` } };
502
525
  }
@@ -571,6 +594,16 @@ function make(ground, invoked) {
571
594
  }
572
595
  return project;
573
596
  }
597
+ /** A round the signal cancelled, as its step and the record read it. */
598
+ const CANCELLED = { status: "skipped", why: { type: "cancelled" } };
599
+ /** The signal's reason with the record on it as `woven`, when the reason is something that can carry one: what
600
+ * a cancelled `weave` rejects with, so the caller's own test for a cancel holds and the steps are there to read. */
601
+ function carrying(reason, woven) {
602
+ if (reason !== null && (typeof reason === "object" || typeof reason === "function") && Object.isExtensible(reason)) {
603
+ Object.assign(reason, { woven });
604
+ }
605
+ return reason;
606
+ }
574
607
  /** Whether two asks want the same things for the same reasons. */
575
608
  function same(after, before) {
576
609
  return JSON.stringify(after) === JSON.stringify(before);
package/dist/types.d.ts CHANGED
@@ -754,6 +754,10 @@ export type Cap = {
754
754
  } | {
755
755
  type: "two_things";
756
756
  contender: Contender;
757
+ }
758
+ /** A weave merged a part that matched nothing on its own back into these words: never run unasked. */
759
+ | {
760
+ type: "merged";
757
761
  };
758
762
  /** The confirm prompt: `evoke`'s own line, then the manifest's template filled in. */
759
763
  export interface Prompt {
@@ -971,6 +975,10 @@ export type WeaveWhy = {
971
975
  } | {
972
976
  type: "read_as";
973
977
  reflex: LocalName;
978
+ }
979
+ /** The weave was cancelled: the round the host ended, and every step that had not finished by then. */
980
+ | {
981
+ type: "cancelled";
974
982
  } | {
975
983
  type: "said";
976
984
  message: string;
@@ -991,6 +999,8 @@ export interface Progress {
991
999
  /** What the run needs a host to do next. */
992
1000
  export type Todo = {
993
1001
  type: "decide";
1002
+ step: number;
1003
+ round: number;
994
1004
  asked: Asked;
995
1005
  } | {
996
1006
  type: "handle";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoke-build/evoke",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "You invoke a function; you evoke a reflex. The SDK: load a project of reflexes, decide with Jev, run.",
5
5
  "keywords": [
6
6
  "jev",
@@ -2,11 +2,12 @@
2
2
  // and its default export called with (args, { input, config, signal }), then one result line on stdout:
3
3
  // { text, data? } or { error, refused? } — `refused` when the error is a refusal by Node's permission model or the
4
4
  // kernel: what was refused, a permission or a syscall, and the path, so the host names the declaration's key. It
5
- // exits when stdin closes, before or during a body, so its life is bounded by its parent's. The body's console
6
- // and stdout go to stderr, as do the frames of an error it throws — the frames alone, without the message the
7
- // host reports, and without the frames inside Node itself; an error thrown from a callback ends the body as a
8
- // rejection does. SIGTERM and the deadline abort `signal`; a body that has not settled a second later is
9
- // abandoned. The SDK ships this same file.
5
+ // exits when stdin closes before a body, and during one aborts the body's `signal` first, so its life is bounded
6
+ // by its parent's and the body still hears the end. The body's console and stdout go to stderr, as do the frames
7
+ // of an error it throws — the frames alone, without the message the host reports, and without the frames inside
8
+ // Node itself; an error thrown from a callback ends the body as a rejection does. SIGTERM, the deadline and
9
+ // stdin's end abort `signal`; a body that has not settled a second later is abandoned. The SDK ships this same
10
+ // file.
10
11
  import { writeSync } from "node:fs";
11
12
  import module from "node:module";
12
13
  import { pathToFileURL } from "node:url";
@@ -23,6 +24,12 @@ try {
23
24
  // Nothing to warm.
24
25
  }
25
26
  const controller = new AbortController();
27
+ // The body's signal aborted with the reason, and the loader gone a grace later whatever the body does: with a
28
+ // line for the host when it is still there to read one, silently when it is not.
29
+ const stop = (why, report) => {
30
+ if (!controller.signal.aborted) controller.abort(new Error(why));
31
+ setTimeout(() => (report ? fail(why) : process.exit(1)), GRACE);
32
+ };
26
33
  // The whole line, however long: `process.stdout` below puts the pipe in non-blocking mode, so one write may take
27
34
  // part of it, and the next may be told to wait a moment.
28
35
  const out = (value) => {
@@ -56,7 +63,8 @@ process.stdin.on("data", (chunk) => {
56
63
  run(buffered.slice(0, end));
57
64
  }
58
65
  });
59
- process.stdin.on("end", () => process.exit(started ? 1 : 0));
66
+ // The host is gone: before a body, nothing to do; during one, the body hears it and the loader follows.
67
+ process.stdin.on("end", () => (started ? stop("the host is gone", false) : process.exit(0)));
60
68
 
61
69
  async function run(line) {
62
70
  let envelope;
@@ -66,13 +74,9 @@ async function run(line) {
66
74
  return fail(`the envelope is not JSON: ${error.message}`);
67
75
  }
68
76
  const { run, args, input, config, deadline } = envelope;
69
- const stop = (why) => {
70
- if (!controller.signal.aborted) controller.abort(new Error(why));
71
- setTimeout(() => fail(why), GRACE);
72
- };
73
- process.on("SIGTERM", () => stop("terminated"));
77
+ process.on("SIGTERM", () => stop("terminated", true));
74
78
  process.on("uncaughtException", thrown);
75
- const timer = setTimeout(() => stop(`timed out after ${deadline} ms`), deadline);
79
+ const timer = setTimeout(() => stop(`timed out after ${deadline} ms`, true), deadline);
76
80
  try {
77
81
  const module = await import(pathToFileURL(run).href);
78
82
  if (module.default === undefined) throw new Error(`${run} has no default export`);