@expressive/steps 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,4 +55,4 @@ const { orderId } = await order; // runs run(), typed by its return
55
55
 
56
56
  A failed `courier.book` unwinds newest first: `refund`, then `release`, then every taken-back step's fields restored.
57
57
 
58
- Reference: [docs/](docs/README.md), and the `steps` page of the Expressive skill ([skills/steps.md](../../skills/steps.md)).
58
+ Reference: [docs/](docs/README.md), and the package's agent skill ([skills/expressive-steps](skills/expressive-steps/SKILL.md)).
package/dist/index.d.ts CHANGED
@@ -2,8 +2,14 @@ import { Model } from "@expressive/mvc";
2
2
 
3
3
  //#region src/steps.d.ts
4
4
  declare namespace Steps {
5
+ /**
6
+ * Where the instance stands - reactive through `then.status`.
7
+ *
8
+ * `idle` - no run, or the last ended cleanly. `running`. `waiting` - parked on an open wait.
9
+ * `stopped`, `aborted`, `failed` - how the last run ended; held until the next one starts.
10
+ */
5
11
  type Status = 'idle' | 'running' | 'waiting' | 'stopped' | 'aborted' | 'failed';
6
- /** What `run` returns - the run's result. */
12
+ /** The run's result - what `run` resolves with; types `x.run()` and `await x`. */
7
13
  type Result<T> = T extends {
8
14
  run(...args: any[]): infer R;
9
15
  } ? Awaited<R> : unknown;
@@ -21,12 +27,18 @@ declare namespace Steps {
21
27
  /** Caught by `.catch()`: a step given the error (its key), `then` for a function, or `true` for none. */
22
28
  rescue?: string | true;
23
29
  }
24
- /** What a step call inside a step returns - inert, not awaitable. */
30
+ /** What calling a step inside a step returns - inert, not awaitable; scheduled once the caller settles. */
25
31
  type Token = Readonly<Node>;
32
+ /**
33
+ * A committed step, as `after` receives it - keys and arguments, no values.
34
+ *
35
+ * `id` is `key#n`, e.g. `pay#1`. The Runner keeps no journal - collect entries in `after`.
36
+ */
26
37
  interface Entry {
27
38
  id: string;
28
39
  key: string;
29
40
  args: unknown[];
41
+ /** What the step scheduled. */
30
42
  next: Node[];
31
43
  /** Fields the step wrote, collections changed in place included. */
32
44
  wrote: string[];
@@ -49,6 +61,7 @@ declare namespace Steps {
49
61
  /** An `end` the runtime added - the plan drained without one. */
50
62
  implicit?: true;
51
63
  }
64
+ /** An open wait, as listed by `then.waiting`. */
52
65
  interface Wait {
53
66
  id: string;
54
67
  /** Key of the step which opened the wait. */
@@ -60,23 +73,41 @@ declare namespace Steps {
60
73
  /** When a duration wait closes, in epoch milliseconds. */
61
74
  until?: number;
62
75
  }
63
- /** A `then.on` handler: a function, given the error, or nothing on cancellation; or for a durable run, a step method uncalled - given the same - or steps called for it. */
76
+ /**
77
+ * A `then.on` handler.
78
+ *
79
+ * A function - given the error, or nothing on a cancellation; process-local. For a durable run:
80
+ * a step method uncalled - given the same, recorded - or steps called for it, an array a group.
81
+ */
64
82
  type Handler = {
65
83
  handler(reason?: unknown): unknown;
66
84
  }['handler'] | Promise<void> | readonly Promise<void>[];
67
- /** Handlers for the outcomes of a step's subtree - `this.then.on({ ... })`. */
85
+ /**
86
+ * Handlers for the outcomes of a step and everything it schedules.
87
+ *
88
+ * Per step they fire `catch`, `abort`, `reset`, `close`; across steps deepest first, newest first.
89
+ * Handlers run before fields are restored and see the step's partial writes; keys they write are kept.
90
+ */
68
91
  interface On {
69
- /** The step is taken back - a failure leaving its subtree, `abort()`, a lost race: reset its effects. Fields are restored after it. */
92
+ /**
93
+ * The step is taken back - reset its effects.
94
+ *
95
+ * A failure leaving its subtree, `abort()`, a lost race; not `stop()`. Fields are restored after it.
96
+ */
70
97
  reset?: Handler;
71
98
  /**
72
- * A failure below, no `.catch` closer to it took it - for recovery. Returning recovers - the failed part
73
- * of the subtree unwinds, the step's own effects stay, steps it schedules replace the rest; throwing
74
- * passes it on.
99
+ * A failure below that no closer `.catch` took - return to recover, throw to pass it on.
100
+ *
101
+ * Recovering unwinds the failed part of the subtree; the step's own effects stay and its `reset`
102
+ * does not run; steps it schedules replace the rest. One per step; runs before a `.catch` on the
103
+ * step's call. `then.retry()` replays the step.
75
104
  */
76
105
  catch?: Handler;
77
106
  /**
78
- * The subtree ended without completing, before `reset` - given the error of a failure no `catch`
79
- * recovered; nothing for a cancellation (`abort()`, `stop()`, a lost race). `then.status` tells which.
107
+ * The subtree ended without completing - `commit()`'s counterpart.
108
+ *
109
+ * Given the error of a failure no `catch` recovered; nothing for a cancellation (`abort()`, `stop()`,
110
+ * a lost race) - `then.status` tells which. Skips a clean end, a recovered failure and `then.end()`.
80
111
  */
81
112
  abort?: Handler;
82
113
  /** The subtree ended, any way - a finally over it, last. */
@@ -86,6 +117,7 @@ declare namespace Steps {
86
117
  type Aborted = InstanceType<typeof Steps.Aborted>;
87
118
  /** Why a run or subtree ended: `stop()`. */
88
119
  type Stopped = InstanceType<typeof Steps.Stopped>;
120
+ /** A plain copy of the controller's reactive fields - `then.get()`. */
89
121
  interface Snapshot {
90
122
  status: Status;
91
123
  active: string | undefined;
@@ -96,8 +128,14 @@ declare namespace Steps {
96
128
  }
97
129
  interface Controller<T extends Model = Model> {
98
130
  /**
99
- * Inside a step: park this point in the plan for a duration - units summed. The next step in
100
- * sequence runs once it passes.
131
+ * Inside a step: park the plan for a duration - units summed.
132
+ *
133
+ * A wait nothing answers, journaled `wait#n` with `args: [ms]`. The next step in sequence runs once
134
+ * it passes - backoff, timeouts, nudges.
135
+ *
136
+ * @example
137
+ * this.then({ sec: 30 });
138
+ * this.retryUpload();
101
139
  */
102
140
  <This>(this: This, duration: {
103
141
  ms?: number;
@@ -107,123 +145,228 @@ declare namespace Steps {
107
145
  day?: number;
108
146
  }): Promise<void>;
109
147
  /**
110
- * Inside a step: call `each` per item - a function issuing a step, or a step itself -
111
- * and run the tokens as one parallel group.
148
+ * Inside a step: one parallel group of `each`, called per item.
149
+ *
150
+ * `each` is a function issuing a step, or a step itself. The next step in sequence runs once every
151
+ * member, and all it schedules, is done.
152
+ *
153
+ * @example
154
+ * this.then(this.files, this.upload);
155
+ * this.then(this.sites, (site) => this.scan(site, 'deep'));
112
156
  */
113
157
  <I, This>(this: This, items: Iterable<I>, each: (item: I, index: number) => unknown): Promise<void>;
114
158
  /**
115
- * Inside a step: park this point in the plan until a call to one of these steps answers it - from
116
- * outside, or from a step called beside them (see below). The next step in sequence runs once the
117
- * answer, and all it schedules, is done.
159
+ * Inside a step: park the plan until one of these steps is called.
160
+ *
161
+ * Status `waiting`. Answering steps read `undefined` from outside unless an open wait accepts them.
162
+ * An outside call answers the wait whose opening step's first argument `===` its own, else the
163
+ * oldest; its Promise resolves when that answer's subtree is done. A call no wait accepts throws.
164
+ *
165
+ * @example
166
+ * this.then(this.approve, this.reject);
118
167
  */
119
168
  <This>(this: This, ...steps: ((...args: any[]) => unknown)[]): Promise<void>;
120
169
  /**
121
- * Inside a step: run these tokens as one parallel group at this point in the plan.
122
- * The next step in sequence runs once every member, and all it schedules, is done.
170
+ * Inside a step: run these tokens as one parallel group.
171
+ *
172
+ * The next step in sequence runs once every member, and all it schedules, is done. Beside steps
173
+ * uncalled, each called step is a racer, started when the plan reaches the wait: the first answer
174
+ * wins and keeps all it did; losers' signals abort, what they scheduled drops, and each losing
175
+ * branch unwinds - resets run, fields restored.
123
176
  *
124
- * Beside steps uncalled - `this.then(this.allow, this.deny, this.timeout(call))` - each called step
125
- * is a racer, started when the plan reaches the wait. Once a call answers, the racers which did not
126
- * make it lose: signals aborted, what they scheduled dropped, gates released; a step in flight
127
- * settles, then each losing branch unwinds - resets run, fields restored. The winner keeps all it did.
177
+ * @example
178
+ * this.then(this.ship(order), this.receipt(order));
179
+ * this.then(this.allow, this.deny, this.timeout(call));
128
180
  */
129
181
  <This>(this: This, ...steps: unknown[]): Promise<void>;
130
182
  /** The instance this controller runs. */
131
183
  readonly self: T;
184
+ /** Where the instance stands - see {@link Steps.Status}. */
132
185
  readonly status: Steps.Status;
133
186
  /** Key of a step in flight - the latest invoked, with a group in flight. */
134
187
  readonly active: string | undefined;
135
188
  /** Arguments of each `run()` waiting for the active run to finish. */
136
189
  readonly queue: readonly unknown[][];
137
- /** Open waits, oldest first. */
190
+ /** Open waits, oldest first - durations included. */
138
191
  readonly waiting: readonly Steps.Wait[];
139
192
  /** Runs finished - ended, failed, stopped or aborted. */
140
193
  readonly runs: number;
194
+ /** What failed the last run; cleared when the next starts. */
141
195
  readonly error: unknown;
142
196
  /** A plain snapshot of status, active, queue, waiting, runs and error. */
143
197
  get(): Steps.Snapshot;
144
198
  /**
145
- * End the run - inside a step, where the plan reaches it; from outside, now. Pending steps are
146
- * discarded and steps in flight settle first; `run`, awaiting the plan, then returns the result.
199
+ * End the run early, keeping everything.
200
+ *
201
+ * Inside a step, where the plan reaches it; from outside, now. Steps in flight settle, what they
202
+ * schedule drops, only `close` fires; `run`'s tail then builds the result from fields. Takes no
203
+ * value - write why it ended to a field first. To halt now, `stop()` or `abort()`.
204
+ *
205
+ * @example
206
+ * if (this.found) this.then.end();
207
+ * else this.next();
147
208
  */
148
209
  end(): void;
149
210
  /**
150
- * Inside a step: a gate at this point in the plan - queue FIFO for one of `max` slots; what this
151
- * step schedules after it runs holding the slot, until done. A function taking `$` - the instance's
152
- * tracking proxy - is re-read when anything it read through `$` changes, other Models included; one
153
- * taking nothing, when a slot frees and when the instance updates. `0` pauses. `key` shares the
154
- * queue: an object across groups and instances, any other value across this instance. Default: the
155
- * enclosing group's, else the instance's.
211
+ * Inside a step: a gate - only `max` at once; what the step schedules after it holds a slot until done.
212
+ *
213
+ * Queues FIFO. `max` as a function of `$`, the tracking proxy, is re-read live; `0` pauses. `key`
214
+ * shares the queue: an object across groups and instances (`only(1, this.manager)` is a mutex), any
215
+ * other value across this instance. Default: the enclosing group's, else the instance's. A delay
216
+ * before queueing is a duration ahead of it.
217
+ *
218
+ * @example
219
+ * this.then.only(($) => $.concurrency);
220
+ * this.upload(file);
156
221
  */
157
222
  only(max: number | (($: T) => number), key?: unknown): void;
158
223
  /**
159
- * Halt, keeping what was done: discard pending steps and queued runs; in-flight steps settle and
160
- * commit. `reason` - the `cause` of the `'Stopped'` error the run and queue reject with, and its
161
- * message when a string.
224
+ * Halt, keeping what was done - status `stopped`.
225
+ *
226
+ * Pending steps and queued runs drop; in-flight steps settle and commit. The run, the queue and
227
+ * in-flight signals reject with a `Steps.Stopped` - `reason` as its `cause`, and its message when a string.
228
+ * `set(null)` on the instance stops too.
162
229
  */
163
230
  stop(reason?: unknown): void;
164
231
  /**
165
- * Halt and unwind, as a failure would - status `'aborted'`; the run's resets run, newest first, and
166
- * fields are restored. The run and queue reject with a `Steps.Aborted`, `reason` as for `stop`.
167
- * A reset which throws fails the run: its error, given the `Steps.Aborted` as `cause`.
232
+ * Halt and unwind, as a failure would - status `aborted`.
233
+ *
234
+ * The run's resets run, newest first, and fields are restored. The run and queue reject with a
235
+ * `Steps.Aborted`, `reason` as for `stop`. A reset which throws fails the run: its error, given the
236
+ * `Steps.Aborted` as `cause`.
168
237
  */
169
238
  abort(reason?: unknown): void;
170
- /** Inside a step: a point of no return - the step is not caught, restored or undone, and the unwind stops here. */
239
+ /**
240
+ * Inside a step: a point of no return.
241
+ *
242
+ * The step is not caught, retried, restored or undone; an unwind stops here and what came before stands.
243
+ */
171
244
  commit(): void;
172
- /** Inside a step: handlers for the outcomes of this step and all it schedules, deepest step first. */
245
+ /**
246
+ * Inside a step: handlers for the outcomes of this step and all it schedules - see {@link Steps.On}.
247
+ *
248
+ * Register right after the effect it undoes. An unknown key throws.
249
+ *
250
+ * @example
251
+ * const hold = await inventory.hold(this.cart.items);
252
+ *
253
+ * this.then.on({ reset: () => inventory.release(hold) });
254
+ * this.pay();
255
+ */
173
256
  on(handlers: Steps.On): void;
174
257
  /** Inside a `.catch` handler: restore the caught branch's fields now; writes after it stand. */
175
258
  restore(): void;
176
259
  /**
177
- * Inside a step: its `AbortSignal` - aborted on `stop()` or `abort()`, an outside `end`, a failure
178
- * elsewhere, a race lost, and once the step settles. A step rejecting with its reason is not a failure.
260
+ * Inside a step: its `AbortSignal` - pass it to the call the step awaits.
261
+ *
262
+ * Aborted on `stop()` or `abort()`, an outside `end`, a failure elsewhere, a race lost, and once the
263
+ * step settles. A step rejecting with its reason is not a failure.
264
+ *
265
+ * @example
266
+ * this.page = await fetch(this.url, { signal: this.then.signal });
179
267
  */
180
268
  readonly signal: AbortSignal;
181
- /** Inside a step: how many times `then.retry()` replayed it - 0 on a first run. In a handler, the failed one's. */
269
+ /** Inside a step: how many times `then.retry()` replayed it - `0` on a first run. In a handler, the failed one's. */
182
270
  readonly retried: number;
183
271
  /**
184
- * Inside a `.catch` handler: schedule what the catch is attached to again - the failed step, or the
185
- * whole group - with its recorded args, still caught by the same handler. The handler holds the policy:
186
- * `if (this.then.retried >= 3) throw error; this.then.retry();`. Backoff belongs in the retried step.
272
+ * Inside a `.catch` handler: schedule what the catch is attached to again, under the same handler.
273
+ *
274
+ * The failed step or the whole group, with its recorded args. The handler holds the policy; backoff
275
+ * belongs in the retried step. Throws outside a handler.
276
+ *
277
+ * @example
278
+ * this.scan(site).catch((error) => {
279
+ * if (this.then.retried >= 3) throw error;
280
+ * this.then.retry();
281
+ * });
187
282
  */
188
283
  retry(): void;
189
284
  }
190
285
  interface Thenable<T extends Model = Model> {
191
- /** Idle, calls `run()` - rejecting if it takes arguments; then settles once the active run and every queued one have, with the last one's result. */
286
+ /**
287
+ * Join the active run - or, idle, call `run()`; settles with the last queued run's result.
288
+ *
289
+ * Rejects if idle and `run` takes arguments - give an optional one a default.
290
+ */
192
291
  <R1 = Steps.Result<T>, R2 = never>(onfulfilled?: ((value: Steps.Result<T>) => R1 | PromiseLike<R1>) | null, onrejected?: ((reason: unknown) => R2 | PromiseLike<R2>) | null): Promise<R1 | R2>;
193
292
  then: Steps.Runner<T>;
194
293
  }
195
- /**
196
- * The controller, and a thenable - so the instance with it as `then`. Awaiting it joins the
197
- * active run; idle, it calls `run()`, or rejects if `run` takes arguments.
198
- */
294
+ /** The controller, and a thenable - so the instance, with it as `then`, is awaitable. */
199
295
  type Runner<T extends Model = Model> = Controller<T> & Thenable<T>;
200
296
  }
201
- /**
202
- * A runnable Model: its methods are steps, `run()` starts a run and awaiting the instance joins
203
- * it, resolving with what `run` returns. The members below are the contract - each optional.
204
- */
205
297
  declare class Aborted extends Error {
206
298
  name: string;
207
299
  }
208
300
  declare class Stopped extends Error {
209
301
  name: string;
210
302
  }
303
+ /**
304
+ * A runnable Model: every method is a step, a step schedules the next by calling it.
305
+ *
306
+ * Fields carry data between steps. Each step is atomic and registers its own reset; a failure unwinds
307
+ * what committed. `run()` starts a run; awaiting the instance joins it. In-process only.
308
+ *
309
+ * @example
310
+ * class Checkout extends Steps {
311
+ * cart!: Cart;
312
+ * charge?: Charge = undefined;
313
+ *
314
+ * async run() {
315
+ * this.pay();
316
+ * await this;
317
+ * return this.charge!.id;
318
+ * }
319
+ *
320
+ * protected async pay() {
321
+ * this.charge = await stripe.charge(this.cart.total);
322
+ * this.then.on({ reset: () => stripe.refund(this.charge!.id) });
323
+ * }
324
+ * }
325
+ *
326
+ * const id = await Checkout.new({ cart });
327
+ */
211
328
  declare class Steps extends Model {
212
329
  /** Why a run or subtree ended: `abort()`, or a lost race. */
213
330
  static readonly Aborted: typeof Aborted;
214
331
  /** Why a run or subtree ended: `stop()`. */
215
332
  static readonly Stopped: typeof Stopped;
216
- /** The controller - status, journal, queue, waits; awaiting the instance awaits it. */
333
+ /**
334
+ * The controller - plan tokens inside a step, status and control from outside.
335
+ *
336
+ * Its fields are reactive through the instance: `x.get(($) => $.then.status)`, or read in a render.
337
+ * Enumerable - `{ ...x.then }` snapshots; not in `instance.get()`. Awaiting the instance awaits it.
338
+ */
217
339
  readonly then: Steps.Runner<this>;
218
340
  constructor(...args: Model.Args);
219
341
  /**
220
- * The entry, `async`: starts a run, or queues one while a run is active. Its return is the run's result.
221
- * It takes arguments and calls steps; `await this` inside it waits for the run, then it returns.
342
+ * The entry - starts a run, or queues one while a run is active; settles with what it returns.
343
+ *
344
+ * Must be `async`; the only step callable from outside. Before `await this`: field writes and step
345
+ * calls only. After it, the tail: read fields, return or throw - step calls throw. Without `await this`
346
+ * it settles at once. `return await this` is a TS circularity - return from fields. Inside a step,
347
+ * `this.run()` throws.
348
+ *
349
+ * @example
350
+ * async run(url: string) {
351
+ * this.fetch(url);
352
+ * await this;
353
+ * return this.rows;
354
+ * }
222
355
  */
223
356
  run?(...args: any[]): Promise<unknown>;
224
- /** Before each step, once per step. */
357
+ /** Hook: before each step. Synchronous, the raw instance as `this`; writes are unjournaled. */
225
358
  protected before?(step: string, args: unknown[]): void;
226
- /** After each step commits, once per step. */
359
+ /**
360
+ * Hook: after each step commits, given its entry - the only journal there is.
361
+ *
362
+ * Synchronous, the raw instance as `this`; writes are unjournaled. A try a catch took, and the failure
363
+ * ending the run, arrive with `entry.error`; a called `end` arrives too, an implicit one does not.
364
+ *
365
+ * @example
366
+ * protected after(step: string, entry: Steps.Entry) {
367
+ * this.log.push(entry);
368
+ * }
369
+ */
227
370
  protected after?(step: string, entry: Steps.Entry): void;
228
371
  }
229
372
  //#endregion
package/dist/index.js CHANGED
@@ -135,16 +135,37 @@ function install(self) {
135
135
  listener(self, (key) => void (key === REFRESH && wire()));
136
136
  self.get(null, () => control.stop());
137
137
  }
138
- /**
139
- * A runnable Model: its methods are steps, `run()` starts a run and awaiting the instance joins
140
- * it, resolving with what `run` returns. The members below are the contract - each optional.
141
- */
142
138
  var Aborted = class extends Error {
143
139
  name = "Aborted";
144
140
  };
145
141
  var Stopped = class extends Error {
146
142
  name = "Stopped";
147
143
  };
144
+ /**
145
+ * A runnable Model: every method is a step, a step schedules the next by calling it.
146
+ *
147
+ * Fields carry data between steps. Each step is atomic and registers its own reset; a failure unwinds
148
+ * what committed. `run()` starts a run; awaiting the instance joins it. In-process only.
149
+ *
150
+ * @example
151
+ * class Checkout extends Steps {
152
+ * cart!: Cart;
153
+ * charge?: Charge = undefined;
154
+ *
155
+ * async run() {
156
+ * this.pay();
157
+ * await this;
158
+ * return this.charge!.id;
159
+ * }
160
+ *
161
+ * protected async pay() {
162
+ * this.charge = await stripe.charge(this.cart.total);
163
+ * this.then.on({ reset: () => stripe.refund(this.charge!.id) });
164
+ * }
165
+ * }
166
+ *
167
+ * const id = await Checkout.new({ cart });
168
+ */
148
169
  var Steps = class extends Model {
149
170
  /** Why a run or subtree ended: `abort()`, or a lost race. */
150
171
  static Aborted = Aborted;