Skip to content

Commit a405fd9

Browse files
committed
Add DEV_GUIDE.md and update README.md
1 parent 7f5a6d6 commit a405fd9

File tree

2 files changed

+342
-0
lines changed

2 files changed

+342
-0
lines changed

DEV_GUIDE.md

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
## PureMVC Multicore Async Command Utility - Developer Guide
2+
3+
This guide explains how the Async Command utility works and how to use it in a PureMVC Typescript Multicore app. You’ll learn the execution model and see practical, copy‑pasteable examples in Typescript.
4+
5+
### What Problem Does This Solve?
6+
You often need to execute a series of commands where one or more steps perform asynchronous work (fetch data, wait for timers, write to storage, etc.). Orchestration using notifications alone couples commands together. `AsyncCommand` and `AsyncMacroCommand` let you compose a pipeline where each step can be synchronous or asynchronous, and the next step runs only after the current one completes.
7+
8+
---
9+
10+
### Key Types
11+
12+
- `IAsyncCommand` — interface extending PureMVC `ICommand` with `setOnComplete(cb)`.
13+
- `AsyncCommand` — base class for a command that may finish later; call `commandComplete()` when done.
14+
- `AsyncMacroCommand` — orchestrates a FIFO list of sub‑commands. Supports both sync (`SimpleCommand`) and async (`AsyncCommand`/`AsyncMacroCommand`) sub‑commands.
15+
16+
Imports (ESM):
17+
```ts
18+
import { AsyncCommand } from "./src/command/AsyncCommand.js";
19+
import { AsyncMacroCommand } from "./src/command/AsyncMacroCommand.js";
20+
// Or, if consuming from the published package:
21+
// import { AsyncCommand, AsyncMacroCommand } from "@puremvc/puremvc-typescript-util-async-command";
22+
23+
import {
24+
SimpleCommand,
25+
INotification,
26+
Facade,
27+
ICommand,
28+
} from "@puremvc/puremvc-typescript-multicore-framework";
29+
```
30+
31+
Note: This repo uses ESM; local relative imports include the `.js` suffix.
32+
33+
---
34+
35+
### Execution Model and Lifecycle
36+
37+
1. You register a macro (or simple) command with the Controller (usually via your `Facade`).
38+
2. A notification is sent. The Controller instantiates the mapped command and calls its `execute(notification)`.
39+
3. For `AsyncMacroCommand`:
40+
- It stores the `notification` and calls `nextCommand()`.
41+
- It dequeues the next sub‑command factory, creates the command, and runs it.
42+
- If the sub‑command is async (`AsyncCommand` or `AsyncMacroCommand`), the macro waits until that sub‑command calls its completion callback.
43+
- When the queue is empty, the macro calls its own completion callback (if part of a parent macro) and clears references.
44+
4. For `AsyncCommand`:
45+
- Do your work in `execute(notification)`.
46+
- When asynchronous work completes, call `this.commandComplete()`.
47+
48+
If you forget to call `commandComplete()` in an `AsyncCommand`, the pipeline will pause indefinitely at that step.
49+
50+
---
51+
52+
### Example 1 — A Minimal AsyncCommand using a Timer
53+
54+
```ts
55+
import { AsyncCommand } from "@puremvc/puremvc-typescript-util-async-command";
56+
import { INotification } from "@puremvc/puremvc-typescript-multicore-framework";
57+
58+
export class DelayCommand extends AsyncCommand {
59+
public execute(note: INotification): void {
60+
const ms = (note.body as { delayMs: number }).delayMs;
61+
62+
setTimeout(() => {
63+
// Do something after the delay, then signal completion
64+
this.commandComplete();
65+
}, ms);
66+
}
67+
}
68+
```
69+
70+
---
71+
72+
### Example 2 — AsyncCommand with async/await
73+
74+
Use `try/finally` to ensure `commandComplete()` is always called.
75+
76+
```ts
77+
import { AsyncCommand } from "@puremvc/puremvc-typescript-util-async-command";
78+
import { INotification } from "@puremvc/puremvc-typescript-multicore-framework";
79+
80+
export class FetchUserCommand extends AsyncCommand {
81+
public async execute(note: INotification): Promise<void> {
82+
try {
83+
const { userId } = note.body as { userId: string };
84+
const res = await fetch(`/api/users/${userId}`);
85+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
86+
const user = await res.json();
87+
88+
// Optionally send another notification with the result
89+
this.sendNotification("USER_FETCHED", { user });
90+
} catch (err) {
91+
this.sendNotification("USER_FETCH_FAILED", { error: String(err) });
92+
} finally {
93+
this.commandComplete();
94+
}
95+
}
96+
}
97+
```
98+
99+
---
100+
101+
### Example 3 — Composing an AsyncMacroCommand
102+
103+
Create a macro that runs several steps in order. Sub‑commands can be `SimpleCommand`, `AsyncCommand`, or even another `AsyncMacroCommand`.
104+
105+
```ts
106+
import { AsyncMacroCommand } from "@puremvc/puremvc-typescript-util-async-command";
107+
import { SimpleCommand, INotification } from "@puremvc/puremvc-typescript-multicore-framework";
108+
import { DelayCommand } from "./DelayCommand.js";
109+
import { FetchUserCommand } from "./FetchUserCommand.js";
110+
111+
class LogStartCommand extends SimpleCommand {
112+
public execute(note: INotification): void {
113+
console.log("Pipeline starting", note.body);
114+
}
115+
}
116+
117+
class LogDoneCommand extends SimpleCommand {
118+
public execute(): void {
119+
console.log("Pipeline complete");
120+
}
121+
}
122+
123+
export class LoadUserPipeline extends AsyncMacroCommand {
124+
protected initializeAsyncMacroCommand(): void {
125+
this.addSubCommand(() => new LogStartCommand());
126+
this.addSubCommand(() => new DelayCommand());
127+
this.addSubCommand(() => new FetchUserCommand());
128+
this.addSubCommand(() => new LogDoneCommand());
129+
}
130+
}
131+
```
132+
133+
When the macro executes, it will:
134+
1) log start, 2) delay, 3) fetch the user, 4) log done — each in order, waiting where needed.
135+
136+
---
137+
138+
### Example 4 — Nested AsyncMacros and Mixed Sync/Async
139+
140+
```ts
141+
import { AsyncMacroCommand } from "@puremvc/puremvc-typescript-util-async-command";
142+
import { SimpleCommand } from "@puremvc/puremvc-typescript-multicore-framework";
143+
144+
class InitSyncCommand extends SimpleCommand { /* ... */ }
145+
class LoadAssetsMacro extends AsyncMacroCommand { /* addSubCommand(() => new AsyncStep()) ... */ }
146+
class WarmupServicesMacro extends AsyncMacroCommand { /* ... */ }
147+
148+
export class AppStartupMacro extends AsyncMacroCommand {
149+
protected initializeAsyncMacroCommand(): void {
150+
this.addSubCommand(() => new InitSyncCommand());
151+
this.addSubCommand(() => new LoadAssetsMacro());
152+
this.addSubCommand(() => new WarmupServicesMacro());
153+
}
154+
}
155+
```
156+
157+
`AppStartupMacro` will wait for each nested macro to complete before moving on.
158+
159+
---
160+
161+
### Integrating with PureMVC’s Controller/Facade
162+
163+
Map a notification to your macro (or command), then send the notification to trigger it.
164+
165+
```ts
166+
import { Facade } from "@puremvc/puremvc-typescript-multicore-framework";
167+
import { LoadUserPipeline } from "./LoadUserPipeline.js";
168+
169+
export const NOTE_LOAD_USER = "NOTE_LOAD_USER" as const;
170+
171+
export class AppFacade extends Facade {
172+
public static getInstance(key: string): AppFacade {
173+
if (!this.instanceMap[key]) this.instanceMap[key] = new AppFacade(key);
174+
return this.instanceMap[key] as AppFacade;
175+
}
176+
177+
protected initializeController(): void {
178+
super.initializeController();
179+
this.controller.registerCommand(NOTE_LOAD_USER, LoadUserPipeline);
180+
}
181+
}
182+
183+
// Somewhere in your view/mediator/proxy:
184+
const facade = AppFacade.getInstance("CoreA");
185+
facade.sendNotification(NOTE_LOAD_USER, { userId: "123", delayMs: 250 });
186+
```
187+
188+
Notes:
189+
- The same `INotification` (name, body, type) is passed to each sub‑command in the macro.
190+
- A sub‑command may send additional notifications as needed, but the pipeline sequencing is independent of those notifications.
191+
192+
---
193+
194+
### Passing Data Between Steps
195+
196+
All sub‑commands receive the original notification. Include whatever state they need in the notification body:
197+
198+
```ts
199+
facade.sendNotification("START_PIPELINE", {
200+
userId: "123",
201+
options: { warm: true },
202+
});
203+
```
204+
205+
If you must build state progressively, you can
206+
1. Have a sub‑command send a new notification with aggregated data
207+
2. Write to a Proxy and read from it in later steps
208+
3. Add properties to the object passed in the `body` of the original note.
209+
210+
---
211+
212+
### Error Handling Patterns
213+
214+
- Handle errors inside the sub‑command; send an error notification if appropriate.
215+
- Always call `commandComplete()` in `AsyncCommand` even on error (use `finally`).
216+
- For macros, consider terminal error policies: either continue to next step, or have a step send a specific notification that leads to aborting the flow (e.g., by not scheduling additional work).
217+
218+
---
219+
220+
### Example 5 — Multiple async operations in a single command
221+
222+
```ts
223+
import { AsyncCommand } from "@puremvc/puremvc-typescript-util-async-command";
224+
import { INotification } from "@puremvc/puremvc-typescript-multicore-framework";
225+
226+
export class LoadUserAndPostsCommand extends AsyncCommand {
227+
public async execute(note: INotification): Promise<void> {
228+
try {
229+
const { userId } = note.body as { userId: string };
230+
const [userRes, postsRes] = await Promise.all([
231+
fetch(`/api/users/${userId}`),
232+
fetch(`/api/users/${userId}/posts`),
233+
]);
234+
const [user, posts] = await Promise.all([userRes.json(), postsRes.json()]);
235+
this.sendNotification("USER_AND_POSTS_LOADED", { user, posts });
236+
} finally {
237+
this.commandComplete();
238+
}
239+
}
240+
}
241+
```
242+
243+
---
244+
245+
### Example 6 — Testing an AsyncMacroCommand with Jest
246+
247+
```ts
248+
import { Facade } from "@puremvc/puremvc-typescript-multicore-framework";
249+
import { LoadUserPipeline } from "../src/LoadUserPipeline.js";
250+
251+
const NOTE = "TEST_LOAD_USER";
252+
253+
class TestFacade extends Facade {
254+
protected initializeController(): void {
255+
super.initializeController();
256+
this.controller.registerCommand(NOTE, LoadUserPipeline);
257+
}
258+
}
259+
260+
test("pipeline completes and emits USER_FETCHED", async () => {
261+
const facade = TestFacade.getInstance("TestCore");
262+
263+
const events: string[] = [];
264+
facade.registerMediator({
265+
getMediatorName: () => "SpyMediator",
266+
listNotificationInterests: () => ["USER_FETCHED"],
267+
handleNotification: n => events.push(n.getName()),
268+
onRegister: () => {},
269+
onRemove: () => {},
270+
} as any);
271+
272+
facade.sendNotification(NOTE, { userId: "1", delayMs: 0 });
273+
274+
// Wait for async queue to flush — in real tests, prefer explicit promises
275+
await new Promise(r => setTimeout(r, 50));
276+
277+
expect(events).toContain("USER_FETCHED");
278+
});
279+
```
280+
281+
Tips:
282+
- Prefer exposing deterministic hooks (e.g., a Proxy state) and awaiting on explicit signals in tests.
283+
- If you test a single `AsyncCommand`, you can instantiate it directly and call `setOnComplete` with a test callback before calling `execute`.
284+
285+
---
286+
287+
### Common Pitfalls
288+
289+
- Forgetting to call `commandComplete()` in an `AsyncCommand` → pipeline stalls.
290+
- Throwing from `execute` without catching → still call `commandComplete()` in `finally`.
291+
- Accidentally overriding `AsyncMacroCommand.execute` in your subclass → don’t; override `initializeAsyncMacroCommand()` and add sub‑commands there.
292+
- Using relative imports without `.js` suffix in ESM builds → add the `.js` suffix for local files.
293+
294+
---
295+
296+
### API Summary (from this utility)
297+
298+
```ts
299+
// Signatures (ambient declarations for reference)
300+
301+
// IAsyncCommand
302+
declare interface IAsyncCommand extends ICommand {
303+
setOnComplete(value: () => void): void;
304+
}
305+
306+
// AsyncCommand
307+
declare class AsyncCommand extends SimpleCommand implements IAsyncCommand {
308+
public setOnComplete(value: () => void): void;
309+
protected commandComplete(): void; // call when your async work is done
310+
}
311+
312+
// AsyncMacroCommand
313+
declare class AsyncMacroCommand implements IAsyncCommand {
314+
protected initializeAsyncMacroCommand(): void; // override to add sub-commands
315+
protected addSubCommand(factory: () => ICommand): void; // FIFO
316+
public setOnComplete(value: () => void): void;
317+
public execute(note: INotification): void; // starts the pipeline
318+
}
319+
```
320+
321+
---
322+
323+
### FAQ
324+
325+
Q: Can a sub‑command send notifications while the macro is running?
326+
A: Yes. Notifications are independent of sequencing. The macro only advances when an async sub‑command signals completion or when a sync sub‑command returns from `execute`.
327+
328+
Q: Can I pass different data to each sub‑command?
329+
A: All sub‑commands receive the same `INotification`. If you need evolving state, use a Proxy or send additional notifications.
330+
331+
Q: Can I short‑circuit the pipeline?
332+
A: The macro runs through its queue. A sub‑command may choose to send a notification that results in different application flow (e.g., not scheduling the next macro), but there’s no built‑in “cancel remaining steps” API. You can model cancellation by designing a sub‑command that clears or ignores follow‑up work.
333+
334+
---
335+
336+
### Conclusion
337+
338+
Use `AsyncCommand` for steps that complete later, and `AsyncMacroCommand` to compose them into deterministic pipelines. This utility keeps command‑to‑command coupling low, while preserving the PureMVC notification model and Controller mappings.

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ But that leads to a tight coupling of one command to the next, if the first must
77

88
With the `AsyncCommand` and `AsyncMacroCommand` you could dynamically create a pipeline of commands to be executed sequentially, each of which may have multiple async tasks to complete. None need know anything about the others.
99

10+
## Dev Guide
11+
* [PureMVC TypeScript Async Command — Developer Guide](DEV_GUIDE.md)
12+
13+
## Installation
1014
```shell
1115
npm install @puremvc/puremvc-typescript-multicore-framework
1216
npm install @puremvc/puremvc-typescript-util-async-command

0 commit comments

Comments
 (0)