From 7f5a6d60435f600fa2f10a02e5b179e37e1c7ad7 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 19 Dec 2025 17:43:48 -0500 Subject: [PATCH 1/3] Minor version 1.0.8 Add .js to filenames imported and exported throughout the library. --- docs/classes/AsyncCommand.html | 6 +++--- docs/classes/AsyncMacroCommand.html | 14 +++++++------- docs/interfaces/IAsyncCommand.html | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/classes/AsyncCommand.html b/docs/classes/AsyncCommand.html index ba2bc83..f646c3f 100644 --- a/docs/classes/AsyncCommand.html +++ b/docs/classes/AsyncCommand.html @@ -2,7 +2,7 @@

Your subclass should override the execute method where your business logic will handle the INotification.

AsyncMacroCommand

-

Hierarchy

Implements

Constructors

Hierarchy

  • SimpleCommand
    • AsyncCommand

Implements

Constructors

Properties

Accessors

facade @@ -19,7 +19,7 @@

Methods

  • Notify the parent AsyncMacroCommand that this command is complete.

    Call this method from your subclass to signify that your asynchronous command -has finished.

    Returns void

  • Fulfill the use-case initiated by the given Notification.

    +has finished.

    Returns void

  • Fulfill the use-case initiated by the given Notification.

    In the Command Pattern, an application use-case typically begins with some user action, which results in a Notification being broadcast, which is handled by business logic in the execute method of an @@ -45,4 +45,4 @@

  • Optionaltype: string

    Optional type of the notification.

Returns void

+

Returns void

diff --git a/docs/classes/AsyncMacroCommand.html b/docs/classes/AsyncMacroCommand.html index ae63a78..d07ef05 100644 --- a/docs/classes/AsyncMacroCommand.html +++ b/docs/classes/AsyncMacroCommand.html @@ -22,7 +22,7 @@ override the initializeAsyncMacroCommand method, calling addSubCommand once for each SubCommand to be executed.

AsyncCommand

-

Hierarchy

Implements

Constructors

Hierarchy

  • Notifier
    • AsyncMacroCommand

Implements

Constructors

Properties

Accessors

facade @@ -40,7 +40,7 @@ method.

If your subclass does define a constructor, be -sure to call super().

Returns AsyncMacroCommand

Properties

multitonKey: string

The Multiton Key for this app

+sure to call super().

Returns AsyncMacroCommand

Properties

multitonKey: string

The Multiton Key for this app

MULTITON_MSG: string

Message Constants

Accessors

  • get facade(): IFacade

    Return the Multiton Facade instance

    Returns IFacade

    The facade instance.

    @@ -49,11 +49,11 @@

    The SubCommands will be called in First In/First Out (FIFO) order.

    Parameters

    • factory: () => ICommand

      a factory that returns an instance that implements ICommand.

      -

    Returns void

  • Starts execution of this AsyncMacroCommand's SubCommands.

    +

Returns void

  • Starts execution of this AsyncMacroCommand's SubCommands.

    The SubCommands will be called in First In/First Out (FIFO) order.

    Parameters

    • notification: INotification

      the INotification object to be passsed to each SubCommand.

      -

    Returns void

  • Initialize the AsyncMacroCommand.

    +

Returns void

  • Initialize the AsyncMacroCommand.

    In your subclass, override this method to initialize the AsyncMacroCommand's SubCommand @@ -71,7 +71,7 @@

    Note that SubCommands may be any ICommand implementor, AsyncMacroCommands, AsyncCommands, -MacroCommands or SimpleCommands are all acceptable.

    Returns void

  • Initialize this Notifier instance.

    +MacroCommands or SimpleCommands are all acceptable.

    Returns void

  • Initialize this Notifier instance.

    This is how a Notifier gets its multitonKey. Calls to sendNotification or to access the facade will fail until after this method @@ -86,7 +86,7 @@

Returns void

  • Execute this AsyncMacroCommand's next SubCommand.

    If the next SubCommand is asynchronous, a callback is registered for -the command completion, else the next command is run.

    Returns void

  • Create and send an Notification.

    +the command completion, else the next command is run.

    Returns void

  • Create and send an Notification.

    Keeps us from having to construct new Notification instances in our implementation code.

    Parameters

    • notificationName: string

      The name of the notification to be sent.

      @@ -94,4 +94,4 @@
    • Optionaltype: string

      Optional type of the notification.

    Returns void

+

Returns void

diff --git a/docs/interfaces/IAsyncCommand.html b/docs/interfaces/IAsyncCommand.html index 0b68215..8da174e 100644 --- a/docs/interfaces/IAsyncCommand.html +++ b/docs/interfaces/IAsyncCommand.html @@ -1,5 +1,5 @@ IAsyncCommand | @puremvc/puremvc-typescript-util-async-command

Interface for an Asynchronous Command.

-
interface IAsyncCommand {
    execute(notification: INotification): void;
    initializeNotifier(key: string): void;
    sendNotification(notificationName: string, body?: any, type?: string): void;
    setOnComplete(value: () => void): void;
}

Hierarchy

  • ICommand
    • IAsyncCommand

Implemented by

Methods

interface IAsyncCommand {
    execute(notification: INotification): void;
    initializeNotifier(key: string): void;
    sendNotification(notificationName: string, body?: any, type?: string): void;
    setOnComplete(value: () => void): void;
}

Hierarchy

  • ICommand
    • IAsyncCommand

Implemented by

Methods

  • Optionaltype: string

    Optional type of the notification.

  • Returns void

    • Registers the callback for a parent AsyncMacroCommand.

      Parameters

      • value: () => void

        The AsyncMacroCommand method to call on completion

        -

      Returns void

    +

    Returns void

    From a405fd9c955b21c1f08ed5e284f910b3ef97ae35 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 19 Dec 2025 18:26:39 -0500 Subject: [PATCH 2/3] Add DEV_GUIDE.md and update README.md --- DEV_GUIDE.md | 338 +++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 + 2 files changed, 342 insertions(+) create mode 100644 DEV_GUIDE.md diff --git a/DEV_GUIDE.md b/DEV_GUIDE.md new file mode 100644 index 0000000..a6f1fe9 --- /dev/null +++ b/DEV_GUIDE.md @@ -0,0 +1,338 @@ +## PureMVC Multicore Async Command Utility - Developer Guide + +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. + +### What Problem Does This Solve? +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. + +--- + +### Key Types + +- `IAsyncCommand` — interface extending PureMVC `ICommand` with `setOnComplete(cb)`. +- `AsyncCommand` — base class for a command that may finish later; call `commandComplete()` when done. +- `AsyncMacroCommand` — orchestrates a FIFO list of sub‑commands. Supports both sync (`SimpleCommand`) and async (`AsyncCommand`/`AsyncMacroCommand`) sub‑commands. + +Imports (ESM): +```ts +import { AsyncCommand } from "./src/command/AsyncCommand.js"; +import { AsyncMacroCommand } from "./src/command/AsyncMacroCommand.js"; +// Or, if consuming from the published package: +// 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. + +--- + +### Execution Model and Lifecycle + +1. You register a macro (or simple) command with the Controller (usually via your `Facade`). +2. A notification is sent. The Controller instantiates the mapped command and calls its `execute(notification)`. +3. For `AsyncMacroCommand`: + - It stores the `notification` and calls `nextCommand()`. + - It dequeues the next sub‑command factory, creates the command, and runs it. + - If the sub‑command is async (`AsyncCommand` or `AsyncMacroCommand`), 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. +4. For `AsyncCommand`: + - Do your work in `execute(notification)`. + - When asynchronous work completes, call `this.commandComplete()`. + +If you forget to call `commandComplete()` in an `AsyncCommand`, the pipeline will pause indefinitely at that step. + +--- + +### Example 1 — A Minimal AsyncCommand using a Timer + +```ts +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); + } +} +``` + +--- + +### Example 2 — AsyncCommand with async/await + +Use `try/finally` to ensure `commandComplete()` is always called. + +```ts +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 { + 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(); + } + } +} +``` + +--- + +### Example 3 — Composing an AsyncMacroCommand + +Create a macro that runs several steps in order. Sub‑commands can be `SimpleCommand`, `AsyncCommand`, or even another `AsyncMacroCommand`. + +```ts +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: +1) log start, 2) delay, 3) fetch the user, 4) log done — each in order, waiting where needed. + +--- + +### Example 4 — Nested AsyncMacros and Mixed Sync/Async + +```ts +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. + +--- + +### Integrating with PureMVC’s Controller/Facade + +Map a notification to your macro (or command), then send the notification to trigger it. + +```ts +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. + +--- + +### Passing Data Between Steps + +All sub‑commands receive the original notification. Include whatever state they need in the notification body: + +```ts +facade.sendNotification("START_PIPELINE", { + userId: "123", + options: { warm: true }, +}); +``` + +If you must build state progressively, you can +1. Have a sub‑command send a new notification with aggregated data +2. Write to a Proxy and read from it in later steps +3. Add properties to the object passed in the `body` of the original note. + +--- + +### Error Handling Patterns + +- Handle errors inside the sub‑command; send an error notification if appropriate. +- Always call `commandComplete()` in `AsyncCommand` even on error (use `finally`). +- 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). + +--- + +### Example 5 — Multiple async operations in a single command + +```ts +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 { + 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(); + } + } +} +``` + +--- + +### Example 6 — Testing an AsyncMacroCommand with Jest + +```ts +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 call `setOnComplete` with a test callback before calling `execute`. + +--- + +### Common Pitfalls + +- Forgetting to call `commandComplete()` in an `AsyncCommand` → pipeline stalls. +- Throwing from `execute` without catching → still call `commandComplete()` in `finally`. +- Accidentally overriding `AsyncMacroCommand.execute` in your subclass → don’t; override `initializeAsyncMacroCommand()` and add sub‑commands there. +- Using relative imports without `.js` suffix in ESM builds → add the `.js` suffix for local files. + +--- + +### API Summary (from this utility) + +```ts +// 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 +} +``` + +--- + +### FAQ + +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. + +--- + +### Conclusion + +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. diff --git a/README.md b/README.md index 0bc2f16..ac3335f 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,10 @@ But that leads to a tight coupling of one command to the next, if the first must 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. +## Dev Guide +* [PureMVC TypeScript Async Command — Developer Guide](DEV_GUIDE.md) + +## Installation ```shell npm install @puremvc/puremvc-typescript-multicore-framework npm install @puremvc/puremvc-typescript-util-async-command From 043a7c35c2b34a28527ba82fbb50b62e5a83992d Mon Sep 17 00:00:00 2001 From: cliffhall Date: Fri, 19 Dec 2025 19:01:15 -0500 Subject: [PATCH 3/3] Update DEV_GUIDE.md --- DEV_GUIDE.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/DEV_GUIDE.md b/DEV_GUIDE.md index a6f1fe9..e276be1 100644 --- a/DEV_GUIDE.md +++ b/DEV_GUIDE.md @@ -13,13 +13,15 @@ You often need to execute a series of commands where one or more steps perform a - `AsyncCommand` — base class for a command that may finish later; call `commandComplete()` when done. - `AsyncMacroCommand` — orchestrates a FIFO list of sub‑commands. Supports both sync (`SimpleCommand`) and async (`AsyncCommand`/`AsyncMacroCommand`) sub‑commands. -Imports (ESM): -```ts -import { AsyncCommand } from "./src/command/AsyncCommand.js"; -import { AsyncMacroCommand } from "./src/command/AsyncMacroCommand.js"; -// Or, if consuming from the published package: -// import { AsyncCommand, AsyncMacroCommand } from "@puremvc/puremvc-typescript-util-async-command"; +### Install +``` +npm install @puremvc/puremvc-typescript-multicore-framework +npm install @puremvc/puremvc-typescript-util-async-command +``` +### Imports (ESM): +```ts +import { AsyncCommand, AsyncMacroCommand } from "@puremvc/puremvc-typescript-util-async-command"; import { SimpleCommand, INotification,