Skip to content

Commit 302d746

Browse files
authored
Merge pull request #4 from PureMVC/use-js-extensions-in-import-export
Use js extensions in import export
2 parents bf2cd3b + 043a7c3 commit 302d746

File tree

5 files changed

+356
-12
lines changed

5 files changed

+356
-12
lines changed

DEV_GUIDE.md

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