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.
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.
IAsyncCommand— interface extending PureMVCICommandwithsetOnComplete(cb).AsyncCommand— base class for a command that may finish later; callcommandComplete()when done.AsyncMacroCommand— orchestrates a FIFO list of sub‑commands. Supports both sync (SimpleCommand) and async (AsyncCommand/AsyncMacroCommand) sub‑commands.
npm install @puremvc/puremvc-typescript-multicore-framework
npm install @puremvc/puremvc-typescript-util-async-command
import { AsyncCommand, AsyncMacroCommand } from "@puremvc/puremvc-typescript-util-async-command";
import {
SimpleCommand,
INotification,
Facade,
ICommand,
} from "@puremvc/puremvc-typescript-multicore-framework";Note: This repo uses ESM; local relative imports include the .js suffix.
- You register a macro (or simple) command with the Controller (usually via your
Facade). - A notification is sent. The Controller instantiates the mapped command and calls its
execute(notification). - For
AsyncMacroCommand:- It stores the
notificationand callsnextCommand(). - It dequeues the next sub‑command factory, creates the command, and runs it.
- If the sub‑command is async (
AsyncCommandorAsyncMacroCommand), the macro waits until that sub‑command calls its completion callback. - When the queue is empty, the macro calls its own completion callback (if part of a parent macro) and clears references.
- It stores the
- For
AsyncCommand:- Do your work in
execute(notification). - When asynchronous work completes, call
this.commandComplete().
- Do your work in
If you forget to call commandComplete() in an AsyncCommand, the pipeline will pause indefinitely at that step.
import { AsyncCommand } from "@puremvc/puremvc-typescript-util-async-command";
import { INotification } from "@puremvc/puremvc-typescript-multicore-framework";
export class DelayCommand extends AsyncCommand {
public execute(note: INotification): void {
const ms = (note.body as { delayMs: number }).delayMs;
setTimeout(() => {
// Do something after the delay, then signal completion
this.commandComplete();
}, ms);
}
}Use try/finally to ensure commandComplete() is always called.
import { AsyncCommand } from "@puremvc/puremvc-typescript-util-async-command";
import { INotification } from "@puremvc/puremvc-typescript-multicore-framework";
export class FetchUserCommand extends AsyncCommand {
public async execute(note: INotification): Promise<void> {
try {
const { userId } = note.body as { userId: string };
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const user = await res.json();
// Optionally send another notification with the result
this.sendNotification("USER_FETCHED", { user });
} catch (err) {
this.sendNotification("USER_FETCH_FAILED", { error: String(err) });
} finally {
this.commandComplete();
}
}
}Create a macro that runs several steps in order. Sub‑commands can be SimpleCommand, AsyncCommand, or even another AsyncMacroCommand.
import { AsyncMacroCommand } from "@puremvc/puremvc-typescript-util-async-command";
import { SimpleCommand, INotification } from "@puremvc/puremvc-typescript-multicore-framework";
import { DelayCommand } from "./DelayCommand.js";
import { FetchUserCommand } from "./FetchUserCommand.js";
class LogStartCommand extends SimpleCommand {
public execute(note: INotification): void {
console.log("Pipeline starting", note.body);
}
}
class LogDoneCommand extends SimpleCommand {
public execute(): void {
console.log("Pipeline complete");
}
}
export class LoadUserPipeline extends AsyncMacroCommand {
protected initializeAsyncMacroCommand(): void {
this.addSubCommand(() => new LogStartCommand());
this.addSubCommand(() => new DelayCommand());
this.addSubCommand(() => new FetchUserCommand());
this.addSubCommand(() => new LogDoneCommand());
}
}When the macro executes, it will:
- log start, 2) delay, 3) fetch the user, 4) log done — each in order, waiting where needed.
import { AsyncMacroCommand } from "@puremvc/puremvc-typescript-util-async-command";
import { SimpleCommand } from "@puremvc/puremvc-typescript-multicore-framework";
class InitSyncCommand extends SimpleCommand { /* ... */ }
class LoadAssetsMacro extends AsyncMacroCommand { /* addSubCommand(() => new AsyncStep()) ... */ }
class WarmupServicesMacro extends AsyncMacroCommand { /* ... */ }
export class AppStartupMacro extends AsyncMacroCommand {
protected initializeAsyncMacroCommand(): void {
this.addSubCommand(() => new InitSyncCommand());
this.addSubCommand(() => new LoadAssetsMacro());
this.addSubCommand(() => new WarmupServicesMacro());
}
}AppStartupMacro will wait for each nested macro to complete before moving on.
Map a notification to your macro (or command), then send the notification to trigger it.
import { Facade } from "@puremvc/puremvc-typescript-multicore-framework";
import { LoadUserPipeline } from "./LoadUserPipeline.js";
export const NOTE_LOAD_USER = "NOTE_LOAD_USER" as const;
export class AppFacade extends Facade {
public static getInstance(key: string): AppFacade {
if (!this.instanceMap[key]) this.instanceMap[key] = new AppFacade(key);
return this.instanceMap[key] as AppFacade;
}
protected initializeController(): void {
super.initializeController();
this.controller.registerCommand(NOTE_LOAD_USER, LoadUserPipeline);
}
}
// Somewhere in your view/mediator/proxy:
const facade = AppFacade.getInstance("CoreA");
facade.sendNotification(NOTE_LOAD_USER, { userId: "123", delayMs: 250 });Notes:
- The same
INotification(name, body, type) is passed to each sub‑command in the macro. - A sub‑command may send additional notifications as needed, but the pipeline sequencing is independent of those notifications.
All sub‑commands receive the original notification. Include whatever state they need in the notification body:
facade.sendNotification("START_PIPELINE", {
userId: "123",
options: { warm: true },
});If you must build state progressively, you can
- Have a sub‑command send a new notification with aggregated data
- Write to a Proxy and read from it in later steps
- Add properties to the object passed in the
bodyof the original note.
- Handle errors inside the sub‑command; send an error notification if appropriate.
- Always call
commandComplete()inAsyncCommandeven on error (usefinally). - 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).
import { AsyncCommand } from "@puremvc/puremvc-typescript-util-async-command";
import { INotification } from "@puremvc/puremvc-typescript-multicore-framework";
export class LoadUserAndPostsCommand extends AsyncCommand {
public async execute(note: INotification): Promise<void> {
try {
const { userId } = note.body as { userId: string };
const [userRes, postsRes] = await Promise.all([
fetch(`/api/users/${userId}`),
fetch(`/api/users/${userId}/posts`),
]);
const [user, posts] = await Promise.all([userRes.json(), postsRes.json()]);
this.sendNotification("USER_AND_POSTS_LOADED", { user, posts });
} finally {
this.commandComplete();
}
}
}import { Facade } from "@puremvc/puremvc-typescript-multicore-framework";
import { LoadUserPipeline } from "../src/LoadUserPipeline.js";
const NOTE = "TEST_LOAD_USER";
class TestFacade extends Facade {
protected initializeController(): void {
super.initializeController();
this.controller.registerCommand(NOTE, LoadUserPipeline);
}
}
test("pipeline completes and emits USER_FETCHED", async () => {
const facade = TestFacade.getInstance("TestCore");
const events: string[] = [];
facade.registerMediator({
getMediatorName: () => "SpyMediator",
listNotificationInterests: () => ["USER_FETCHED"],
handleNotification: n => events.push(n.getName()),
onRegister: () => {},
onRemove: () => {},
} as any);
facade.sendNotification(NOTE, { userId: "1", delayMs: 0 });
// Wait for async queue to flush — in real tests, prefer explicit promises
await new Promise(r => setTimeout(r, 50));
expect(events).toContain("USER_FETCHED");
});Tips:
- Prefer exposing deterministic hooks (e.g., a Proxy state) and awaiting on explicit signals in tests.
- If you test a single
AsyncCommand, you can instantiate it directly and callsetOnCompletewith a test callback before callingexecute.
- Forgetting to call
commandComplete()in anAsyncCommand→ pipeline stalls. - Throwing from
executewithout catching → still callcommandComplete()infinally. - Accidentally overriding
AsyncMacroCommand.executein your subclass → don’t; overrideinitializeAsyncMacroCommand()and add sub‑commands there. - Using relative imports without
.jssuffix in ESM builds → add the.jssuffix for local files.
// Signatures (ambient declarations for reference)
// IAsyncCommand
declare interface IAsyncCommand extends ICommand {
setOnComplete(value: () => void): void;
}
// AsyncCommand
declare class AsyncCommand extends SimpleCommand implements IAsyncCommand {
public setOnComplete(value: () => void): void;
protected commandComplete(): void; // call when your async work is done
}
// AsyncMacroCommand
declare class AsyncMacroCommand implements IAsyncCommand {
protected initializeAsyncMacroCommand(): void; // override to add sub-commands
protected addSubCommand(factory: () => ICommand): void; // FIFO
public setOnComplete(value: () => void): void;
public execute(note: INotification): void; // starts the pipeline
}Q: Can a sub‑command send notifications while the macro is running?
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.
Q: Can I pass different data to each sub‑command?
A: All sub‑commands receive the same INotification. If you need evolving state, use a Proxy or send additional notifications.
Q: Can I short‑circuit the pipeline? 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.
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.