@expressive/steps 0.0.0-stage → 0.1.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/README.md +57 -2
- package/dist/index.d.ts +231 -0
- package/dist/index.js +1662 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -4
package/README.md
CHANGED
|
@@ -1,3 +1,58 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @expressive/steps
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Multi-step processes as Expressive Models. Extend `Steps` and every method is a step: a step schedules the next by calling it, fields carry data between them. `run()` takes the arguments, schedules, waits for the run with `await this`, and returns the result. Each step is atomic and registers its own reset, so a failure unwinds what committed. Runs repeat, queue and park on waits answered from outside; status is reactive on `this.then`.
|
|
4
|
+
|
|
5
|
+
In-process only for now - no durable persistence.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @expressive/steps @expressive/mvc
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Steps } from '@expressive/steps';
|
|
13
|
+
|
|
14
|
+
class SubmitOrder extends Steps {
|
|
15
|
+
cart!: Cart;
|
|
16
|
+
charge?: Charge = undefined;
|
|
17
|
+
tracking = '';
|
|
18
|
+
|
|
19
|
+
async run() {
|
|
20
|
+
this.reserve();
|
|
21
|
+
|
|
22
|
+
await this;
|
|
23
|
+
|
|
24
|
+
return { orderId: this.charge!.id, tracking: this.tracking };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
protected async reserve() {
|
|
28
|
+
const hold = await inventory.hold(this.cart.items);
|
|
29
|
+
|
|
30
|
+
this.then.on({ reset: () => inventory.release(hold) });
|
|
31
|
+
this.pay();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected async pay() {
|
|
35
|
+
this.charge = await stripe.charge(this.cart.total);
|
|
36
|
+
this.then.on({ reset: () => stripe.refund(this.charge!.id) });
|
|
37
|
+
|
|
38
|
+
this.then(this.ship(this.charge.id), this.receipt(this.charge.receiptUrl));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
protected async ship(chargeId: string) {
|
|
42
|
+
this.tracking = (await courier.book(this.cart, chargeId)).id;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
protected async receipt(url: string) {
|
|
46
|
+
await mail.send(this.cart.email, url);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const order = SubmitOrder.new({ cart });
|
|
51
|
+
|
|
52
|
+
order.then.status; // 'idle' - reactive, like any field
|
|
53
|
+
const { orderId } = await order; // runs run(), typed by its return
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
A failed `courier.book` unwinds newest first: `refund`, then `release`, then every taken-back step's fields restored.
|
|
57
|
+
|
|
58
|
+
Reference: [docs/](docs/README.md), and the `steps` page of the Expressive skill ([skills/steps.md](../../skills/steps.md)).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { Model } from "@expressive/mvc";
|
|
2
|
+
|
|
3
|
+
//#region src/steps.d.ts
|
|
4
|
+
declare namespace Steps {
|
|
5
|
+
type Status = 'idle' | 'running' | 'waiting' | 'stopped' | 'aborted' | 'failed';
|
|
6
|
+
/** What `run` returns - the run's result. */
|
|
7
|
+
type Result<T> = T extends {
|
|
8
|
+
run(...args: any[]): infer R;
|
|
9
|
+
} ? Awaited<R> : unknown;
|
|
10
|
+
/** A planned item: a step, a parallel group (`group`), or a wait (`answers`). */
|
|
11
|
+
interface Node {
|
|
12
|
+
id: string;
|
|
13
|
+
key: string;
|
|
14
|
+
/** Arguments - a duration wait's milliseconds. */
|
|
15
|
+
args: unknown[];
|
|
16
|
+
group?: Node[];
|
|
17
|
+
/** Keys of the steps which may answer this wait - none for a duration. */
|
|
18
|
+
answers?: string[];
|
|
19
|
+
/** Steps raced against the answers - cancelled once one answers. */
|
|
20
|
+
race?: Node;
|
|
21
|
+
/** Caught by `.catch()`: a step given the error (its key), `then` for a function, or `true` for none. */
|
|
22
|
+
rescue?: string | true;
|
|
23
|
+
}
|
|
24
|
+
/** What a step call inside a step returns - inert, not awaitable. */
|
|
25
|
+
type Token = Readonly<Node>;
|
|
26
|
+
interface Entry {
|
|
27
|
+
id: string;
|
|
28
|
+
key: string;
|
|
29
|
+
args: unknown[];
|
|
30
|
+
next: Node[];
|
|
31
|
+
/** Fields the step wrote, collections changed in place included. */
|
|
32
|
+
wrote: string[];
|
|
33
|
+
/** Handlers the step registered with `then.on()`, by outcome, in order: a step method given uncalled or steps called for one (by key), or `then` for a function. */
|
|
34
|
+
on?: { [K in keyof On]?: Node[] };
|
|
35
|
+
/** Taken back by an unwind - resets run, fields restored; `'failed'` when a handler threw. */
|
|
36
|
+
unwound?: true | 'failed';
|
|
37
|
+
/** Id of the wait this step answered. */
|
|
38
|
+
wake?: string;
|
|
39
|
+
/** How many times `then.retry()` replayed the step, when past its first run. */
|
|
40
|
+
retried?: number;
|
|
41
|
+
/** What the step threw - a caught failure, or one which kept its writes. */
|
|
42
|
+
error?: unknown;
|
|
43
|
+
/** Collections changed in a way no restore can undo - the unwind stops here. */
|
|
44
|
+
irreversible?: string[];
|
|
45
|
+
/** The step called `then.commit()` - not retried, restored or undone; the unwind stops here. */
|
|
46
|
+
committed?: true;
|
|
47
|
+
/** The unwind stopped at this entry; it and every entry before it stay applied. */
|
|
48
|
+
halted?: true;
|
|
49
|
+
/** An `end` the runtime added - the plan drained without one. */
|
|
50
|
+
implicit?: true;
|
|
51
|
+
}
|
|
52
|
+
interface Wait {
|
|
53
|
+
id: string;
|
|
54
|
+
/** Key of the step which opened the wait. */
|
|
55
|
+
step: string;
|
|
56
|
+
/** Arguments of the step which opened the wait. */
|
|
57
|
+
args: unknown[];
|
|
58
|
+
/** Keys of the steps which may answer it. */
|
|
59
|
+
answers: string[];
|
|
60
|
+
/** When a duration wait closes, in epoch milliseconds. */
|
|
61
|
+
until?: number;
|
|
62
|
+
}
|
|
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. */
|
|
64
|
+
type Handler = {
|
|
65
|
+
handler(reason?: unknown): unknown;
|
|
66
|
+
}['handler'] | Promise<void> | readonly Promise<void>[];
|
|
67
|
+
/** Handlers for the outcomes of a step's subtree - `this.then.on({ ... })`. */
|
|
68
|
+
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. */
|
|
70
|
+
reset?: Handler;
|
|
71
|
+
/**
|
|
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.
|
|
75
|
+
*/
|
|
76
|
+
catch?: Handler;
|
|
77
|
+
/**
|
|
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.
|
|
80
|
+
*/
|
|
81
|
+
abort?: Handler;
|
|
82
|
+
/** The subtree ended, any way - a finally over it, last. */
|
|
83
|
+
close?: Handler;
|
|
84
|
+
}
|
|
85
|
+
/** Why a run or subtree ended: `abort()`, or a lost race. */
|
|
86
|
+
type Aborted = InstanceType<typeof Steps.Aborted>;
|
|
87
|
+
/** Why a run or subtree ended: `stop()`. */
|
|
88
|
+
type Stopped = InstanceType<typeof Steps.Stopped>;
|
|
89
|
+
interface Snapshot {
|
|
90
|
+
status: Status;
|
|
91
|
+
active: string | undefined;
|
|
92
|
+
queue: unknown[][];
|
|
93
|
+
waiting: Wait[];
|
|
94
|
+
runs: number;
|
|
95
|
+
error: unknown;
|
|
96
|
+
}
|
|
97
|
+
interface Controller<T extends Model = Model> {
|
|
98
|
+
/**
|
|
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.
|
|
101
|
+
*/
|
|
102
|
+
<This>(this: This, duration: {
|
|
103
|
+
ms?: number;
|
|
104
|
+
sec?: number;
|
|
105
|
+
min?: number;
|
|
106
|
+
hr?: number;
|
|
107
|
+
day?: number;
|
|
108
|
+
}): Promise<void>;
|
|
109
|
+
/**
|
|
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.
|
|
112
|
+
*/
|
|
113
|
+
<I, This>(this: This, items: Iterable<I>, each: (item: I, index: number) => unknown): Promise<void>;
|
|
114
|
+
/**
|
|
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.
|
|
118
|
+
*/
|
|
119
|
+
<This>(this: This, ...steps: ((...args: any[]) => unknown)[]): Promise<void>;
|
|
120
|
+
/**
|
|
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.
|
|
123
|
+
*
|
|
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.
|
|
128
|
+
*/
|
|
129
|
+
<This>(this: This, ...steps: unknown[]): Promise<void>;
|
|
130
|
+
/** The instance this controller runs. */
|
|
131
|
+
readonly self: T;
|
|
132
|
+
readonly status: Steps.Status;
|
|
133
|
+
/** Key of a step in flight - the latest invoked, with a group in flight. */
|
|
134
|
+
readonly active: string | undefined;
|
|
135
|
+
/** Arguments of each `run()` waiting for the active run to finish. */
|
|
136
|
+
readonly queue: readonly unknown[][];
|
|
137
|
+
/** Open waits, oldest first. */
|
|
138
|
+
readonly waiting: readonly Steps.Wait[];
|
|
139
|
+
/** Runs finished - ended, failed, stopped or aborted. */
|
|
140
|
+
readonly runs: number;
|
|
141
|
+
readonly error: unknown;
|
|
142
|
+
/** A plain snapshot of status, active, queue, waiting, runs and error. */
|
|
143
|
+
get(): Steps.Snapshot;
|
|
144
|
+
/**
|
|
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.
|
|
147
|
+
*/
|
|
148
|
+
end(): void;
|
|
149
|
+
/**
|
|
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.
|
|
156
|
+
*/
|
|
157
|
+
only(max: number | (($: T) => number), key?: unknown): void;
|
|
158
|
+
/**
|
|
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.
|
|
162
|
+
*/
|
|
163
|
+
stop(reason?: unknown): void;
|
|
164
|
+
/**
|
|
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`.
|
|
168
|
+
*/
|
|
169
|
+
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. */
|
|
171
|
+
commit(): void;
|
|
172
|
+
/** Inside a step: handlers for the outcomes of this step and all it schedules, deepest step first. */
|
|
173
|
+
on(handlers: Steps.On): void;
|
|
174
|
+
/** Inside a `.catch` handler: restore the caught branch's fields now; writes after it stand. */
|
|
175
|
+
restore(): void;
|
|
176
|
+
/**
|
|
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.
|
|
179
|
+
*/
|
|
180
|
+
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. */
|
|
182
|
+
readonly retried: number;
|
|
183
|
+
/**
|
|
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.
|
|
187
|
+
*/
|
|
188
|
+
retry(): void;
|
|
189
|
+
}
|
|
190
|
+
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. */
|
|
192
|
+
<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
|
+
then: Steps.Runner<T>;
|
|
194
|
+
}
|
|
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
|
+
*/
|
|
199
|
+
type Runner<T extends Model = Model> = Controller<T> & Thenable<T>;
|
|
200
|
+
}
|
|
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
|
+
declare class Aborted extends Error {
|
|
206
|
+
name: string;
|
|
207
|
+
}
|
|
208
|
+
declare class Stopped extends Error {
|
|
209
|
+
name: string;
|
|
210
|
+
}
|
|
211
|
+
declare class Steps extends Model {
|
|
212
|
+
/** Why a run or subtree ended: `abort()`, or a lost race. */
|
|
213
|
+
static readonly Aborted: typeof Aborted;
|
|
214
|
+
/** Why a run or subtree ended: `stop()`. */
|
|
215
|
+
static readonly Stopped: typeof Stopped;
|
|
216
|
+
/** The controller - status, journal, queue, waits; awaiting the instance awaits it. */
|
|
217
|
+
readonly then: Steps.Runner<this>;
|
|
218
|
+
constructor(...args: Model.Args);
|
|
219
|
+
/**
|
|
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.
|
|
222
|
+
*/
|
|
223
|
+
run?(...args: any[]): Promise<unknown>;
|
|
224
|
+
/** Before each step, once per step. */
|
|
225
|
+
protected before?(step: string, args: unknown[]): void;
|
|
226
|
+
/** After each step commits, once per step. */
|
|
227
|
+
protected after?(step: string, entry: Steps.Entry): void;
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
export { Steps };
|
|
231
|
+
//# sourceMappingURL=index.d.ts.map
|