|
| 1 | +# PureMVC TypeScript MultiCore — Developer Guide |
| 2 | + |
| 3 | +This guide explains how the PureMVC MultiCore framework works in TypeScript and shows how to build an app using Facades, Proxies, Mediators, Commands, and Notifications. All examples are TypeScript and use the published NPM package. |
| 4 | + |
| 5 | +- Package: `@puremvc/puremvc-typescript-multicore-framework` |
| 6 | +- Docs: https://puremvc.org/pages/docs/TypeScript/multicore/ |
| 7 | + |
| 8 | +## Installation |
| 9 | + |
| 10 | +```sh |
| 11 | +npm install @puremvc/puremvc-typescript-multicore-framework |
| 12 | +``` |
| 13 | + |
| 14 | +## Core Concepts |
| 15 | + |
| 16 | +PureMVC implements the classic Model–View–Controller meta‑pattern. In MultiCore, each application module (a “Core”) has its own set of MVC actors, identified by a unique Multiton key. |
| 17 | + |
| 18 | +- Model: manages application data via `Proxy` instances. |
| 19 | +- View: manages presentation and event routing via `Mediator` instances. |
| 20 | +- Controller: maps `Notification` names to `Command` classes. |
| 21 | +- Facade: the single API surface that exposes Model, View, Controller for a Core. In MultiCore, Facade is a Multiton; one instance per Core key. |
| 22 | +- Notification: the event/message traveling through the system. |
| 23 | + |
| 24 | +Key properties of MultiCore: |
| 25 | +- Every Core is referenced by a unique string key. |
| 26 | +- Each `Notifier` (Proxy, Mediator, Command) is automatically associated with the correct Core when it is executed/registered; it can then call `sendNotification` to communicate within that Core. |
| 27 | + |
| 28 | +## Public API (what you usually import) |
| 29 | + |
| 30 | +```ts |
| 31 | +import { |
| 32 | + Facade, |
| 33 | + Proxy, |
| 34 | + Mediator, |
| 35 | + SimpleCommand, |
| 36 | + MacroCommand, |
| 37 | + Notification, |
| 38 | + // Types (optional) |
| 39 | + type IFacade, |
| 40 | + type IProxy, |
| 41 | + type IMediator, |
| 42 | + type ICommand, |
| 43 | + type INotification, |
| 44 | +} from '@puremvc/puremvc-typescript-multicore-framework'; |
| 45 | +``` |
| 46 | + |
| 47 | +## Multiton: Creating and Accessing a Core |
| 48 | + |
| 49 | +Each Core is identified by a string key. You never `new Facade()` directly. Instead call `Facade.getInstance(key, factory)` and supply a factory that constructs your Facade subclass. |
| 50 | + |
| 51 | +```ts |
| 52 | +const CORE_KEY = 'com.example.myapp'; |
| 53 | + |
| 54 | +// Your concrete Facade (defined below) |
| 55 | +const facade = Facade.getInstance(CORE_KEY, (key) => new AppFacade(key)); |
| 56 | + |
| 57 | +// Later, retrieve the same core anywhere: |
| 58 | +const same = Facade.getInstance(CORE_KEY, (key) => new AppFacade(key)); |
| 59 | +``` |
| 60 | + |
| 61 | +Notes: |
| 62 | +- The factory is only used the first time for a given key; subsequent calls return the existing instance. |
| 63 | +- Use different keys to run multiple, isolated cores simultaneously. |
| 64 | + |
| 65 | +## Subclassing Facade |
| 66 | + |
| 67 | +You typically subclass `Facade` to register Proxies, Mediators, and Commands at startup. |
| 68 | + |
| 69 | +```ts |
| 70 | +// AppFacade.ts |
| 71 | +import { Facade } from '@puremvc/puremvc-typescript-multicore-framework'; |
| 72 | + |
| 73 | +export class AppFacade extends Facade { |
| 74 | + // app-specific notification names |
| 75 | + public static readonly STARTUP = 'AppFacade/STARTUP'; |
| 76 | + |
| 77 | + protected initializeModel(): void { |
| 78 | + super.initializeModel(); |
| 79 | + // Register your Proxies here |
| 80 | + this.registerProxy(new UserProxy()); |
| 81 | + } |
| 82 | + |
| 83 | + protected initializeController(): void { |
| 84 | + super.initializeController(); |
| 85 | + // Map notifications to commands |
| 86 | + this.registerCommand(AppFacade.STARTUP, () => new StartupCommand()); |
| 87 | + } |
| 88 | + |
| 89 | + protected initializeView(): void { |
| 90 | + super.initializeView(); |
| 91 | + // Register Mediators here (optionally in StartupCommand) |
| 92 | + } |
| 93 | +} |
| 94 | +``` |
| 95 | + |
| 96 | +## Proxies (Model) |
| 97 | + |
| 98 | +Extend `Proxy` to manage data. Proxies are given the Core key when they are registered with the Model, after which they can send notifications. |
| 99 | + |
| 100 | +```ts |
| 101 | +// UserProxy.ts |
| 102 | +import { Proxy } from '@puremvc/puremvc-typescript-multicore-framework'; |
| 103 | + |
| 104 | +export class UserProxy extends Proxy { |
| 105 | + public static readonly NAME = 'UserProxy'; |
| 106 | + public static readonly USERS_LOADED = 'UserProxy/USERS_LOADED'; |
| 107 | + |
| 108 | + constructor() { |
| 109 | + super(UserProxy.NAME); |
| 110 | + } |
| 111 | + |
| 112 | + async loadUsers(): Promise<void> { |
| 113 | + // Simulate remote call |
| 114 | + const users = await Promise.resolve([ |
| 115 | + { id: 1, name: 'Ada' }, |
| 116 | + { id: 2, name: 'Grace' }, |
| 117 | + ]); |
| 118 | + this.data = users; |
| 119 | + this.sendNotification(UserProxy.USERS_LOADED, users); |
| 120 | + } |
| 121 | +} |
| 122 | +``` |
| 123 | + |
| 124 | +## Mediators (View) |
| 125 | + |
| 126 | +Extend `Mediator` to coordinate a view component and react to notifications. Mediators are given the Core key when registered with the View. |
| 127 | + |
| 128 | +```ts |
| 129 | +// UserListMediator.ts |
| 130 | +import { Mediator, type INotification } from '@puremvc/puremvc-typescript-multicore-framework'; |
| 131 | +import { UserProxy } from './UserProxy'; |
| 132 | + |
| 133 | +export class UserListMediator extends Mediator { |
| 134 | + public static readonly NAME = 'UserListMediator'; |
| 135 | + |
| 136 | + constructor(viewComponent: HTMLUListElement) { |
| 137 | + super(UserListMediator.NAME, viewComponent); |
| 138 | + } |
| 139 | + |
| 140 | + public override listNotificationInterests(): string[] { |
| 141 | + return [UserProxy.USERS_LOADED]; |
| 142 | + } |
| 143 | + |
| 144 | + public override handleNotification(note: INotification): void { |
| 145 | + switch (note.name) { |
| 146 | + case UserProxy.USERS_LOADED: |
| 147 | + this.render(note.body as Array<{ id: number; name: string }>); |
| 148 | + break; |
| 149 | + } |
| 150 | + } |
| 151 | + |
| 152 | + private render(users: Array<{ id: number; name: string }>): void { |
| 153 | + const ul = this.viewComponent as HTMLUListElement; |
| 154 | + ul.innerHTML = users.map((u) => `<li>${u.name}</li>`).join(''); |
| 155 | + } |
| 156 | +} |
| 157 | +``` |
| 158 | + |
| 159 | +Registering Mediators typically happens during startup (see `StartupCommand` below) or in `AppFacade.initializeView`. |
| 160 | + |
| 161 | +## Commands (Controller) |
| 162 | + |
| 163 | +Commands encapsulate application logic executed in response to notifications. They are given the Core key when executed by the Controller, after which they can access the Facade and send notifications. |
| 164 | + |
| 165 | +```ts |
| 166 | +// StartupCommand.ts |
| 167 | +import { SimpleCommand, type INotification } from '@puremvc/puremvc-typescript-multicore-framework'; |
| 168 | +import { UserProxy } from './UserProxy'; |
| 169 | +import { UserListMediator } from './UserListMediator'; |
| 170 | + |
| 171 | +export class StartupCommand extends SimpleCommand { |
| 172 | + public override execute(note: INotification): void { |
| 173 | + // Optionally, get something from the startup body |
| 174 | + const root = note.body as { userListEl: HTMLUListElement } | undefined; |
| 175 | + |
| 176 | + // Ensure Proxy exists and kick off initial load |
| 177 | + const userProxy = this.facade.retrieveProxy(UserProxy.NAME) as UserProxy | null; |
| 178 | + if (userProxy) userProxy.loadUsers(); |
| 179 | + |
| 180 | + // Register Mediator now that we have the view element |
| 181 | + if (root?.userListEl) { |
| 182 | + this.facade.registerMediator(new UserListMediator(root.userListEl)); |
| 183 | + } |
| 184 | + } |
| 185 | +} |
| 186 | +``` |
| 187 | + |
| 188 | +`MacroCommand` allows sequencing multiple `SimpleCommand`s: |
| 189 | + |
| 190 | +```ts |
| 191 | +import { MacroCommand } from '@puremvc/puremvc-typescript-multicore-framework'; |
| 192 | + |
| 193 | +export class AppStartupMacro extends MacroCommand { |
| 194 | + protected override initializeMacroCommand(): void { |
| 195 | + this.addSubCommand(() => new PrepModelCommand()); |
| 196 | + this.addSubCommand(() => new PrepViewCommand()); |
| 197 | + this.addSubCommand(() => new KickoffCommand()); |
| 198 | + } |
| 199 | +} |
| 200 | +``` |
| 201 | + |
| 202 | +## Wiring It Up (Startup) |
| 203 | + |
| 204 | +Create the Facade for your Core, register your startup mapping, and send a `STARTUP` notification. |
| 205 | + |
| 206 | +```ts |
| 207 | +// main.ts |
| 208 | +import { Facade } from '@puremvc/puremvc-typescript-multicore-framework'; |
| 209 | +import { AppFacade } from './AppFacade'; |
| 210 | + |
| 211 | +const CORE_KEY = 'com.example.myapp'; |
| 212 | +const facade = Facade.getInstance(CORE_KEY, (key) => new AppFacade(key)); |
| 213 | + |
| 214 | +// Somewhere you create or obtain your view root(s) |
| 215 | +const userListEl = document.getElementById('users') as HTMLUListElement; |
| 216 | + |
| 217 | +// Kick off the app |
| 218 | +facade.sendNotification(AppFacade.STARTUP, { userListEl }); |
| 219 | +``` |
| 220 | + |
| 221 | +Notes: |
| 222 | +- Don’t call `new AppFacade()` directly. Always use `Facade.getInstance(key, factory)`. |
| 223 | +- The Notifier caveat in MultiCore: a `Notifier` (Proxy, Mediator, Command) cannot use `sendNotification` until it has been given a multitonKey. This happens automatically when: |
| 224 | + - a Proxy is registered with the Model, |
| 225 | + - a Mediator is registered with the View, |
| 226 | + - a Command is executed by the Controller. |
| 227 | + |
| 228 | +## Communicating with Notifications |
| 229 | + |
| 230 | +Notifications are identified by string names and may carry an optional `body` and `type`. |
| 231 | + |
| 232 | +```ts |
| 233 | +// From inside any Notifier (Proxy, Mediator, Command): |
| 234 | +this.sendNotification('User/CREATE', { id: 3, name: 'Margaret' }, 'immediate'); |
| 235 | + |
| 236 | +// Handling in a Mediator subclass |
| 237 | +import { Mediator, type INotification } from '@puremvc/puremvc-typescript-multicore-framework'; |
| 238 | + |
| 239 | +class ExampleMediator extends Mediator { |
| 240 | + listNotificationInterests(): string[] { |
| 241 | + return ['User/CREATE']; |
| 242 | + } |
| 243 | + |
| 244 | + handleNotification(note: INotification): void { |
| 245 | + if (note.name === 'User/CREATE') { |
| 246 | + // note.body and note.type as needed |
| 247 | + } |
| 248 | + } |
| 249 | +} |
| 250 | +``` |
| 251 | + |
| 252 | +Guidelines: |
| 253 | +- Keep notification names unique and scoped (e.g., `'UserProxy/USERS_LOADED'`). |
| 254 | +- Co-locate names with the class that owns them (e.g., `UserProxy.USERS_LOADED`). |
| 255 | + |
| 256 | +## Multiple Cores (Modules) |
| 257 | + |
| 258 | +Run multiple isolated Cores by using different Multiton keys. Each Core has its own Facade, Model, View, Controller, and its own set of Notifiers. |
| 259 | + |
| 260 | +```ts |
| 261 | +const AdminKey = 'com.example.app/admin'; |
| 262 | +const ShopKey = 'com.example.app/shop'; |
| 263 | + |
| 264 | +const admin = Facade.getInstance(AdminKey, (k) => new AdminFacade(k)); |
| 265 | +const shop = Facade.getInstance(ShopKey, (k) => new ShopFacade(k)); |
| 266 | + |
| 267 | +admin.sendNotification(AdminFacade.STARTUP); |
| 268 | +shop.sendNotification(ShopFacade.STARTUP); |
| 269 | +``` |
| 270 | + |
| 271 | +Cross-core communication is usually achieved at a higher level (e.g., a shell module) by listening to one core and translating into another core’s notifications. For inter-module message bus patterns, see the PureMVC Pipes utility. |
| 272 | + |
| 273 | +## Shutting Down a Core |
| 274 | + |
| 275 | +Remove a Core and its MVC actors when you’re done with it: |
| 276 | + |
| 277 | +```ts |
| 278 | +import { Facade } from '@puremvc/puremvc-typescript-multicore-framework'; |
| 279 | + |
| 280 | +Facade.removeCore('com.example.myapp'); |
| 281 | +``` |
| 282 | + |
| 283 | +This removes the `Model`, `View`, `Controller`, and `Facade` instances for that key. |
| 284 | + |
| 285 | +## Testing Tips |
| 286 | + |
| 287 | +- Prefer deterministic `Notification` names; expose them as `public static readonly` on the owning class. |
| 288 | +- Query Proxies via `facade.retrieveProxy(ProxyClass.NAME)`; check `proxy.data`. |
| 289 | +- Mediators are unit-testable by directly calling `handleNotification` with a constructed `Notification`. |
| 290 | + |
| 291 | +## Reference: Common Facade Methods |
| 292 | + |
| 293 | +- `registerProxy(proxy)` / `retrieveProxy(name)` / `removeProxy(name)` / `hasProxy(name)` |
| 294 | +- `registerMediator(mediator)` / `retrieveMediator(name)` / `removeMediator(name)` / `hasMediator(name)` |
| 295 | +- `registerCommand(notificationName, factory)` / `removeCommand(notificationName)` / `hasCommand(notificationName)` |
| 296 | +- `sendNotification(name, body?, type?)` |
| 297 | +- `notifyObservers(notification)` (rarely used directly; prefer `sendNotification`) |
| 298 | + |
| 299 | +## Minimal End-to-End Example |
| 300 | + |
| 301 | +```ts |
| 302 | +import { |
| 303 | + Facade, |
| 304 | + Proxy, |
| 305 | + Mediator, |
| 306 | + SimpleCommand, |
| 307 | + type INotification, |
| 308 | +} from '@puremvc/puremvc-typescript-multicore-framework'; |
| 309 | + |
| 310 | +// 1) Proxy |
| 311 | +class CounterProxy extends Proxy { |
| 312 | + static readonly NAME = 'CounterProxy'; |
| 313 | + static readonly UPDATED = 'CounterProxy/UPDATED'; |
| 314 | + |
| 315 | + constructor() { |
| 316 | + super(CounterProxy.NAME, { value: 0 }); |
| 317 | + } |
| 318 | + |
| 319 | + increment() { |
| 320 | + this.data = { value: (this.data?.value ?? 0) + 1 }; |
| 321 | + this.sendNotification(CounterProxy.UPDATED, this.data); |
| 322 | + } |
| 323 | +} |
| 324 | + |
| 325 | +// 2) Mediator |
| 326 | +class CounterMediator extends Mediator { |
| 327 | + static readonly NAME = 'CounterMediator'; |
| 328 | + constructor(span: HTMLSpanElement) { |
| 329 | + super(CounterMediator.NAME, span); |
| 330 | + } |
| 331 | + listNotificationInterests(): string[] { |
| 332 | + return [CounterProxy.UPDATED]; |
| 333 | + } |
| 334 | + handleNotification(note: INotification): void { |
| 335 | + if (note.name === CounterProxy.UPDATED) { |
| 336 | + (this.viewComponent as HTMLSpanElement).textContent = String(note.body.value); |
| 337 | + } |
| 338 | + } |
| 339 | +} |
| 340 | + |
| 341 | +// 3) Command |
| 342 | +class StartupCommand extends SimpleCommand { |
| 343 | + execute(note: INotification): void { |
| 344 | + const { span } = note.body as { span: HTMLSpanElement }; |
| 345 | + |
| 346 | + // Register Mediator |
| 347 | + this.facade.registerMediator(new CounterMediator(span)); |
| 348 | + |
| 349 | + // Use the Proxy |
| 350 | + const proxy = this.facade.retrieveProxy(CounterProxy.NAME) as CounterProxy | null; |
| 351 | + proxy?.increment(); |
| 352 | + } |
| 353 | +} |
| 354 | + |
| 355 | +// 4) Facade |
| 356 | +class AppFacade extends Facade { |
| 357 | + static readonly STARTUP = 'App/STARTUP'; |
| 358 | + protected initializeModel(): void { |
| 359 | + super.initializeModel(); |
| 360 | + this.registerProxy(new CounterProxy()); |
| 361 | + } |
| 362 | + protected initializeController(): void { |
| 363 | + super.initializeController(); |
| 364 | + this.registerCommand(AppFacade.STARTUP, () => new StartupCommand()); |
| 365 | + } |
| 366 | +} |
| 367 | + |
| 368 | +// 5) Bootstrap |
| 369 | +const KEY = 'example.counter'; |
| 370 | +const facade = Facade.getInstance(KEY, (k) => new AppFacade(k)); |
| 371 | +const span = document.getElementById('count') as HTMLSpanElement; |
| 372 | +facade.sendNotification(AppFacade.STARTUP, { span }); |
| 373 | +``` |
| 374 | + |
| 375 | +That’s it! You now have the basic shape to build robust, modular, testable TypeScript applications with PureMVC MultiCore. |
0 commit comments