@vived/core 2.0.1 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,476 @@
1
+ # App Object Architecture
2
+
3
+ > This document describes the `AppObject` architecture and its component ecosystem. The low-level `AppObjectController` base class file itself is not detailed here, but controller *behavior* and flow are now documented for completeness.
4
+
5
+ ## Overview
6
+ The architecture centers around the concept of an **App Object** – a composable, observable container of domain-specific components. Each `AppObject` aggregates behavior and state through **components** that follow clear separation-of-responsibility categories:
7
+
8
+ - Entity: stateful, observable model logic
9
+ - Presentation Manager (PM): derives and emits immutable view models
10
+ - Use Case (UC): encapsulates application operations / workflows
11
+ - View: renders or binds presentation output to a UI or rendering substrate
12
+ - (Other) Arbitrary/custom components categorized as `UNKNOWN`
13
+
14
+ The system encourages:
15
+ - Decoupling via **component lookup** and **repository-level singleton resolution**
16
+ - Reactive propagation via observer lists on entities, PMs, and AppObjects
17
+ - Composability: AppObjects can be extended at runtime by adding/replacing components
18
+ - Testability: Small, focused classes with explicit contracts
19
+
20
+ ## Core Building Blocks
21
+
22
+ ### AppObject (`AppObject.ts`)
23
+ An abstract observable node that:
24
+ - Owns a unique `id`
25
+ - Registers itself in an `AppObjectRepo`
26
+ - Manages a map of `type -> AppObjectComponent`
27
+ - Notifies observers when its component set changes (e.g., add/remove/replace)
28
+ - Handles lifecycle: `dispose()` removes itself from the repo and disposes all components
29
+
30
+ Creation is performed via `makeAppObject(id, repo)` which returns a concrete internal implementation.
31
+
32
+ ### AppObjectComponent (`AppObjectComponent.ts`)
33
+ The base class for all components. Responsibilities:
34
+ - Auto–attaches to its parent `AppObject` on construction
35
+ - Exposes `componentType` (categorical enum) and `type` (unique string identifier)
36
+ - Provides cached retrieval helpers:
37
+ - `getCachedLocalComponent(type)` – same AppObject
38
+ - `getCachedSingleton(type)` – repo-level singleton component
39
+ - `getSingleton(type, logLevel)` – non‑cached lookup with log control
40
+ - Supports disposal: removing itself if still attached
41
+ - Provides uniform logging APIs proxied to the repository
42
+
43
+ Design Notes:
44
+ - Caching avoids repeated map traversal and singleton scans
45
+ - Logging includes composite key `AppObjectID/ComponentTypeString`
46
+
47
+ ### AppObjectEntity (`AppObjectEntity.ts`)
48
+ Specialized component for **domain/application state**. Provides:
49
+ - Change observers via `addChangeObserver` / `notifyOnChange`
50
+ - Disposal observers via `addOnDisposeObserver`
51
+ - Automatic registration of the parent `AppObject`'s `notify` method as a change observer so AppObject-level observers react to entity changes
52
+
53
+ Patterns enabled:
54
+ - PM subscribes to one or more Entities to derive view state
55
+ - Repository-level aggregation via `AppObjectEntityRepo`
56
+
57
+ ### AppObjectSingletonEntity (`AppObjectSingletonEntity.ts`)
58
+ A specialized entity that automatically registers itself as a singleton. Extends `AppObjectEntity` with:
59
+ - Automatic singleton registration via `appObjects.registerSingleton(this)` in constructor
60
+ - Automatic singleton unregistration via `appObjects.unregisterSingleton(this.type)` on disposal
61
+ - Ensures only one instance of this entity type exists across the entire application
62
+
63
+ Use this when you need globally unique entities (e.g., application-wide configuration, user session state).
64
+
65
+ ### AppObjectEntityRepo (`AppObjectEntityRepo.ts`)
66
+ A stateful collection component (itself an `AppObjectEntity`) that manages many entity instances keyed by `AppObject.id`:
67
+ - Add/remove operations attach/detach change observers on each entity to bubble aggregate change notifications
68
+ - Emits addition/removal notifications through dedicated observer lists
69
+ - Factory pattern support:
70
+ - `create(id?: string)` – creates a new entity via `entityFactory` and adds it to the repository (auto-generates ID if not provided)
71
+ - `entityFactory(appObject: AppObject)` – abstract method that derived classes override to provide custom entity creation logic
72
+ - Query surface:
73
+ - `has(id)` – checks if an entity exists for the given ID
74
+ - `getById(id)` – retrieves an entity by its ID
75
+ - `getOrCreate(id)` – gets an existing entity by ID, or creates it if it doesn't exist
76
+ - `getAll()` – returns all entities in the repository
77
+ - Mutation operations:
78
+ - `add(entity)` – adds an entity to the repository
79
+ - `removeById(id)` – removes a single entity by its ID
80
+ - `deleteAll()` – removes all entities from the repository
81
+ - Legacy methods (deprecated):
82
+ - `hasForAppObject(id)` – use `has(id)` instead
83
+ - `getForAppObject(id)` – use `getById(id)` instead
84
+ - `removeForAppObject(id)` – use `removeById(id)` instead
85
+
86
+ **Factory Pattern Usage:**
87
+ Derived repositories should override `entityFactory` to provide entity creation logic:
88
+ ```ts
89
+ class PlayerRepo extends AppObjectEntityRepo<PlayerEntity> {
90
+ static type = "playerRepo";
91
+
92
+ constructor(appObject: AppObject) {
93
+ super(appObject, PlayerRepo.type);
94
+ }
95
+
96
+ entityFactory(appObject: AppObject): PlayerEntity {
97
+ return new PlayerEntity(appObject);
98
+ }
99
+ }
100
+
101
+ // Usage
102
+ const repo = new PlayerRepo(repoAppObject);
103
+ const player1 = repo.create("player-1"); // Create with specific ID
104
+ const player2 = repo.create(); // Create with auto-generated ID
105
+ const player3 = repo.getOrCreate("player-1"); // Get existing player1 or create if missing
106
+ ```
107
+
108
+ This enables higher-level coordination (e.g., multi-selection, batch processing, dashboards) with cohesive reactivity.
109
+
110
+ ### AppObjectSingletonEntityRepo (`AppObjectSingletonEntityRepo.ts`)
111
+ Generic singleton repository for managing entity collections. Extends `AppObjectEntityRepo<T>` with:
112
+ - Automatic singleton registration and unregistration (same pattern as `AppObjectSingletonEntity`)
113
+ - Type-safe entity collection management across the entire application
114
+ - Use when you need a single, centralized collection (e.g., all players, all items, all tasks)
115
+
116
+ Example:
117
+ ```ts
118
+ class PlayerRepo extends AppObjectSingletonEntityRepo<PlayerEntity> {
119
+ static type = "playerRepo";
120
+ constructor(appObject: AppObject) {
121
+ super(appObject, PlayerRepo.type);
122
+ }
123
+ }
124
+ ```
125
+
126
+ ### AppObjectPM (`AppObjectPM.ts`)
127
+ The Presentation Manager (Presentation Model / MVVM mediator). Responsibilities:
128
+ - Maintains last emitted view model (`lastVM`)
129
+ - Optionally provides a `defaultVM` for initial state before any view model is generated
130
+ - Compares new vs prior view model via abstract `vmsAreEqual(a,b)` to suppress redundant updates
131
+ - Manages a list of view observers (`addView` / `removeView`)
132
+ - Provides `doUpdateView(vm)` to push updates only when meaningfully changed
133
+ - Supports automatic entity observation via `observeEntity(entity)` for reactive view model updates
134
+ - Implements lazy evaluation: `formVM()` is only called when views are registered
135
+ - Provides `onViewAdded()` lifecycle hook that is called whenever a view is added
136
+ - Automatically cleans up entity observers on disposal
137
+
138
+ **New Reactive Pattern (Preferred):**
139
+ Derived PMs can now use automatic entity observation:
140
+ 1. Call `observeEntity(entity)` in the constructor to register entities
141
+ 2. Override `formVM()` to generate view models from entity state
142
+ 3. When observed entities change, `formVM()` is automatically called (only if views are registered)
143
+ 4. Optionally override `onViewAdded()` to react when views are added (e.g., start animations, log analytics)
144
+ 5. Entity observers are automatically cleaned up on disposal
145
+
146
+ **Backward Compatibility:**
147
+ All existing PM implementations continue to work without modification. The new features are optional enhancements that provide:
148
+ - Automatic view model regeneration on entity changes
149
+ - Default view model support for initial state
150
+ - Simplified entity observation with automatic cleanup
151
+ - Lifecycle hooks for reacting to view additions
152
+
153
+ This isolates transformation logic and prevents UI churn.
154
+
155
+ ### AppObjectSingletonPM (`AppObjectSingletonPM.ts`)
156
+ Abstract singleton presentation manager that extends `AppObjectPM<T>` with:
157
+ - Automatic singleton registration and unregistration
158
+ - Ensures only one PM instance of this type exists across the application
159
+ - Inherits all automatic entity observation and view model caching features
160
+ - Use for global UI state (e.g., application theme, notification center, global progress indicator)
161
+
162
+ Example:
163
+ ```ts
164
+ class GlobalThemePM extends AppObjectSingletonPM<ThemeVM> {
165
+ static type = "globalThemePM";
166
+
167
+ constructor(appObject: AppObject) {
168
+ super(appObject, GlobalThemePM.type);
169
+ const settings = this.getCachedSingleton<SettingsEntity>(SettingsEntity.type);
170
+ if (settings) this.observeEntity(settings);
171
+ }
172
+
173
+ vmsAreEqual(a: ThemeVM, b: ThemeVM): boolean {
174
+ return a.mode === b.mode && a.primaryColor === b.primaryColor;
175
+ }
176
+
177
+ formVM(): void {
178
+ // Transform settings into theme view model
179
+ }
180
+ }
181
+ ```
182
+
183
+ ### AppObjectUC (`AppObjectUC.ts`)
184
+ A semantic base for **application operations** (e.g., workflows, transactional steps). It adds categorical identity (`componentType = UC`) but intentionally stays minimal so concrete subclasses can:
185
+ - Orchestrate entities, PMs, and repositories
186
+ - Enforce validation and domain rules
187
+ - Trigger PM updates or entity mutations
188
+
189
+ ### AppObjectSingletonUC (`AppObjectSingletonUC.ts`)
190
+ Singleton use case component that extends `AppObjectUC` with:
191
+ - Automatic singleton registration and unregistration
192
+ - Ensures only one UC instance of this type exists across the application
193
+ - Use for application-wide orchestration logic (e.g., global purchase manager, authentication coordinator)
194
+
195
+ Example:
196
+ ```ts
197
+ class AuthenticationUC extends AppObjectSingletonUC {
198
+ static type = "authenticationUC";
199
+
200
+ constructor(appObject: AppObject) {
201
+ super(appObject, AuthenticationUC.type);
202
+ }
203
+
204
+ login(username: string, password: string): Promise<boolean> {
205
+ // Application-wide authentication logic
206
+ }
207
+
208
+ logout(): void {
209
+ // Clean up session, notify entities, etc.
210
+ }
211
+ }
212
+ ```
213
+
214
+ ### AppObjectView (`AppObjectView.ts`)
215
+ A rendering/binding endpoint:
216
+ - Categorized as `VIEW`
217
+ - Typically subscribes to one or more PMs (manually, via the PM's `addView` API)
218
+ - Focused purely on projecting view models to a target (DOM, WebGL scene graph, canvas, etc.)
219
+
220
+ ### getSingletonComponent (`getSingletonComponent.ts`)
221
+ A convenience helper:
222
+ ```ts
223
+ const camera = getSingletonComponent<CameraPM>(CameraPM.type, repo);
224
+ ```
225
+ - Wraps `repo.getSingleton(type)` to simplify imports and generics at call sites
226
+
227
+ ### printAppObjectDetails (`printAppObjectDetails.ts`)
228
+ Debugging utility that enumerates components attached to a given AppObject ID.
229
+
230
+ ## Repository Layer
231
+ `AppObjectRepo`:
232
+ - Tracks all AppObjects (`id -> AppObject`)
233
+ - Aggregates reactivity: registers itself as observer of each AppObject; its own observers can react to structural changes
234
+ - Maintains explicit singleton registry (Map of `type -> component`)
235
+ - `registerSingleton(component)` – explicitly register a component as singleton
236
+ - `unregisterSingleton(type)` – remove a singleton registration (called automatically by singleton components on disposal)
237
+ - `hasSingleton(type)` – check if a singleton exists (checks registry first, then falls back to scanning for single instance)
238
+ - `getSingleton(type)` – retrieve a singleton component
239
+ - Fallback heuristic: if not explicitly registered, scan all components of that type
240
+ - Warns on zero or multiple matches; caches first valid resolution
241
+ - Query Helpers:
242
+ - `getAllAppObjectsWithComponent(type)`
243
+ - `getAllComponents(type)`
244
+ - `getAppObjectComponent(appObjectID, type)`
245
+ - Logging hub consumed by components
246
+
247
+ ### Singleton Component Pattern
248
+ The framework provides four singleton base classes that automatically handle registration/unregistration:
249
+ - `AppObjectSingletonEntity` – singleton entities
250
+ - `AppObjectSingletonEntityRepo<T>` – singleton entity repositories
251
+ - `AppObjectSingletonPM<T>` – singleton presentation managers
252
+ - `AppObjectSingletonUC` – singleton use cases
253
+
254
+ All singleton components:
255
+ 1. Call `this.appObjects.registerSingleton(this)` in their constructor
256
+ 2. Call `this.appObjects.unregisterSingleton(this.type)` in their `dispose()` method
257
+ 3. Can be retrieved via `repo.getSingleton(type)` or `component.getCachedSingleton(type)`
258
+
259
+ This pattern ensures singleton lifecycle management is handled consistently across all component types.
260
+
261
+ ## Typical User → UI → Domain Flow
262
+
263
+ 1. **Controller Trigger**
264
+ A user action (click, input, gesture, hotkey, network event) calls a small *controller function*. Controllers are intentionally plain functions (see `ExampleFeature/Controllers/*.ts`). They:
265
+ - Accept raw UI parameters (strings, numbers, ids)
266
+ - Locate the appropriate Use Case (by `id` for per-object UCs or via static singleton accessors for global UCs)
267
+ - Guard against missing UCs and submit warnings through the repo
268
+ - Invoke a single semantic method on the UC
269
+
270
+ 2. **Use Case Mutation**
271
+ The Use Case applies business rules and mutates one or more **Entities** (and occasionally invokes other UCs). Entities are considered part of the inner domain and remain decoupled from presentation concerns.
272
+
273
+ 3. **Entity Notification**
274
+ Entities call `notifyOnChange()` after state mutation. Their change observers (PMs, the parent `AppObject`, repositories, etc.) are invoked.
275
+
276
+ 4. **Presentation Derivation**
277
+ PMs receiving the change recompute an immutable **View Model**. If `vmsAreEqual(last, next)` is false, `doUpdateView(nextVM)` broadcasts the new model.
278
+
279
+ 5. **View Update**
280
+ Views (or framework-side adapters) receive the new View Model and update rendering / UI bindings. React hooks, canvas redraws, WebGL scene updates, etc., occur here.
281
+
282
+ Controller Functions remain deliberately slim: *resolve UC → call UC → handle missing cases*. This keeps UI code declarative and reduces duplication of lookup logic.
283
+
284
+ ### Adapters (Domain Boundary Helpers)
285
+ Adapters (see `ExampleFeature/Adapters/*.ts`) standardize subscription mechanics between UI frameworks (React hooks, etc.) and PMs:
286
+ - Provide a `defaultVM` for initial render
287
+ - Encapsulate `subscribe` / `unsubscribe` logic
288
+ - Support both per-object (`PmAdapter`) and singleton (`SingletonPmAdapter`) patterns
289
+
290
+ ## Dependency Direction (Clean Architecture Constraints)
291
+
292
+ Strict layering reduces coupling and prevents presentation concerns from leaking inward:
293
+
294
+ | Layer | May Depend On | Must NOT Depend On | Notes |
295
+ |-------|----------------|--------------------|-------|
296
+ | Entities (incl. Repos & Value Objects) | Other Entities, Value Objects | UCs, PMs, Controllers, Adapters | Repositories are treated as Entities (state holders) |
297
+ | Use Cases (UCs) | Entities, other UCs | PMs | They orchestrate but never shape view models directly |
298
+ | PMs | Entities, UCs | Other PMs (allowed but avoided), Controllers | No current need for PM→PM dependency encountered |
299
+ | Controllers | UCs, (optionally) Repos for lookup | Entities, PM internals | Pure boundary functions; translate UI intent to UC invocation |
300
+ | Adapters | PMs (subscription), Repos (to resolve PM) | UCs, Entities (direct mutation) | Provide view-model streaming into UI layer |
301
+ | Views | PMs (via adapters or direct subscription) | UCs, Entities | Rendering only |
302
+
303
+ Additional Rules:
304
+ - Repos are considered data/state layer; treat them like Entities for dependency purposes.
305
+ - Singletons do not relax dependency direction—acquire them only where the layer already allows the dependency.
306
+ - Logging via repo methods is allowed anywhere because it does not introduce upward coupling.
307
+
308
+ Violation Signals:
309
+ - An Entity importing a UC or PM
310
+ - A UC importing a PM
311
+ - A PM invoking controller logic
312
+ - Adapters mutating Entity state directly
313
+
314
+ Refactor Strategy on Violation:
315
+ 1. Push mutation inward (Controller → UC → Entity)
316
+ 2. Introduce a new UC if orchestration spans multiple existing UCs
317
+ 3. Decompose a PM if it begins coordinating workflow rather than projecting state
318
+
319
+ ## Data & Reactive Flow
320
+ 1. Entity mutation occurs (e.g., property set in a subclass) → calls `notifyOnChange()`
321
+ 2. Entity notifies its observers (including its parent AppObject and any PMs)
322
+ 3. AppObject notifies its observers (repository + any external listeners)
323
+ 4. PM recalculates view model; if changed (`vmsAreEqual` is false), it calls `doUpdateView(vm)`
324
+ 5. Views previously registered with the PM receive the new view model and re-render
325
+
326
+ ```
327
+ Entity --(notifyOnChange)--> PM --(doUpdateView)--> View
328
+ | ^ |
329
+ +----> AppObject --(notify)----+ |
330
+ | |
331
+ v |
332
+ AppObjectRepo (optional higher-level observers)
333
+ ```
334
+
335
+ ## Lifecycle Summary
336
+ - Construct `AppObject` via `makeAppObject(id, repo)` → auto-added to repo
337
+ - Construct components with `(appObject, type)` → auto-attached
338
+ - Replace component: adding another of same `type` disposes previous instance
339
+ - Dispose entity / component: removes self from AppObject, clears observers
340
+ - Dispose AppObject: disposes all components, removes itself from repo
341
+
342
+ ## Extension Guidelines
343
+ When adding a new component type:
344
+ 1. Define a static string identifier (e.g., `export const MyFeatureEntityType = "MyFeatureEntity";`)
345
+ 2. Choose the appropriate base class:
346
+ - For **regular components**: `AppObjectEntity`, `AppObjectPM`, `AppObjectUC`, or `AppObjectView`
347
+ - For **singleton components**: `AppObjectSingletonEntity`, `AppObjectSingletonEntityRepo<T>`, `AppObjectSingletonPM<T>`, or `AppObjectSingletonUC`
348
+ 3. Invoke `super(appObject, MyFeatureEntityType);` in constructor
349
+ 4. For PMs: implement `vmsAreEqual` (and optionally `formVM()` with `observeEntity()` for automatic updates)
350
+ 5. For singleton components: No additional registration needed—handled automatically by the base class
351
+ 6. Use cached getters for performance when repeatedly accessing collaborating components
352
+
353
+ ### Choosing Between Regular and Singleton Components
354
+ Use **singleton components** when:
355
+ - Only one instance should exist across the entire application
356
+ - The component represents global application state or behavior
357
+ - Examples: user session, app configuration, global theme, authentication manager
358
+
359
+ Use **regular components** when:
360
+ - Multiple instances may exist (one per AppObject)
361
+ - The component represents per-object state or behavior
362
+ - Examples: player state, item properties, entity-specific UI
363
+
364
+ ### Choosing Component Categories
365
+ - Put durable, observable state in an Entity
366
+ - Put pure transformation / derivation logic in a PM
367
+ - Put orchestration / cross-entity logic in a UC
368
+ - Put rendering / binding logic in a View
369
+ - Avoid mixing responsibilities—compose instead
370
+
371
+ ## Example Composition (Pseudo-Code)
372
+ ```ts
373
+ // Create repo & object
374
+ const repo = makeAppObjectRepo();
375
+ const playerAO = makeAppObject("player-1", repo);
376
+
377
+ // Entity
378
+ class PlayerState extends AppObjectEntity {
379
+ static type = "PlayerState";
380
+ health = 100;
381
+ constructor(ao: AppObject) { super(ao, PlayerState.type); }
382
+ damage(amount: number) { this.health = Math.max(0, this.health - amount); this.notifyOnChange(); }
383
+ }
384
+
385
+ // PM using new automatic entity observation (v1.7+)
386
+ class PlayerHUDPM extends AppObjectPM<{ healthPercent: number }> {
387
+ static type = "PlayerHUDPM";
388
+ readonly defaultVM = { healthPercent: 1.0 }; // Initial full health
389
+
390
+ constructor(ao: AppObject) {
391
+ super(ao, PlayerHUDPM.type);
392
+ const state = ao.getComponent<PlayerState>(PlayerState.type);
393
+ if (state) {
394
+ this.observeEntity(state); // Automatic observation
395
+ }
396
+ }
397
+
398
+ vmsAreEqual(a, b) { return a.healthPercent === b.healthPercent; }
399
+
400
+ // Automatically called when observed entities change (if views are registered)
401
+ formVM(): void {
402
+ const state = this.getCachedLocalComponent<PlayerState>(PlayerState.type);
403
+ if (state) {
404
+ this.doUpdateView({ healthPercent: state.health / 100 });
405
+ }
406
+ }
407
+ }
408
+
409
+ // View (simplified)
410
+ class ConsoleHUDView extends AppObjectView {
411
+ static type = "ConsoleHUDView";
412
+ constructor(ao: AppObject, hudPM: PlayerHUDPM) {
413
+ super(ao, ConsoleHUDView.type);
414
+ hudPM.addView(vm => console.log("HP:", vm.healthPercent));
415
+ }
416
+ }
417
+
418
+ new PlayerState(playerAO);
419
+ const hudPM = new PlayerHUDPM(playerAO);
420
+ new ConsoleHUDView(playerAO, hudPM);
421
+
422
+ // Trigger - PM automatically updates views when entity changes
423
+ const ps = playerAO.getComponent<PlayerState>(PlayerState.type);
424
+ ps?.damage(10); // PM automatically calls formVM() and updates views
425
+ ```
426
+
427
+ **Legacy Pattern (Pre-v1.7, still supported):**
428
+ ```ts
429
+ class PlayerHUDPM extends AppObjectPM<{ healthPercent: number }> {
430
+ static type = "PlayerHUDPM";
431
+ constructor(ao: AppObject) { super(ao, PlayerHUDPM.type); }
432
+ vmsAreEqual(a, b) { return a.healthPercent === b.healthPercent; }
433
+
434
+ // Manual update method
435
+ update() {
436
+ const state = this.getCachedLocalComponent<PlayerState>(PlayerState.type);
437
+ if (!state) return;
438
+ this.doUpdateView({ healthPercent: state.health / 100 });
439
+ }
440
+ }
441
+
442
+ // Manual trigger required
443
+ ps?.damage(10);
444
+ hudPM.update(); // Must manually call update
445
+ ```
446
+
447
+ ## Logging & Diagnostics
448
+ - Components call `log|warn|error` → forwarded via repo (current implementation prints to console)
449
+ - `printAppObjectDetails(id, repo)` lists attached component types
450
+ - Replacing a component logs a warning
451
+
452
+ ## Performance Considerations
453
+ - Component caches avoid redundant map lookups and singleton scans
454
+ - PM equality check prevents spurious view updates
455
+ - Repository singleton cache resolves dynamic discovery only once per type
456
+
457
+ ## Error Handling & Warnings
458
+ - Missing singleton lookup emits a warning (not fatal)
459
+ - Multiple candidates for a supposed singleton: first is used + warning
460
+ - Replacing a component of same type logs a warning and disposes old instance
461
+
462
+ ## Glossary
463
+ - AppObject: A composable, observable unit of application composition
464
+ - Component: A behavior/state module attached to an AppObject
465
+ - Entity: Stateful, observable component storing domain data
466
+ - PM (Presentation Manager): Transforms Entities into view models for Views
467
+ - UC (Use Case): Encapsulates business workflow logic
468
+ - View: Renders or binds presentation logic to UI / output medium
469
+ - Singleton Component: A component intended to exist once across the repo, retrievable via `getSingleton`
470
+
471
+ ## Future Enhancements (Ideas)
472
+ - Stronger typing for component `type` identifiers (string literal unions)
473
+ - Async disposal hooks for resources (e.g., WebGL buffers)
474
+ - Built-in metrics / instrumentation hooks
475
+ - Dev tooling: Graph generation of AppObjects and dependencies
476
+
@@ -0,0 +1,154 @@
1
+ # DomainFactories
2
+
3
+ The DomainFactories feature provides a structured approach for organizing and initializing application domains. It enforces a multi-phase setup sequence that ensures dependencies are properly resolved across different architectural layers.
4
+
5
+ ## Overview
6
+
7
+ A domain factory is responsible for setting up all components within a specific domain or feature of your application. The setup is orchestrated through a four-phase process:
8
+
9
+ 1. **Entities** - Data models and repositories
10
+ 2. **Use Cases (UCs)** - Business logic that operates on entities
11
+ 3. **Presentation Managers (PMs)** - View models that transform entity data
12
+ 4. **Final Setup** - Any remaining initialization after all components are ready
13
+
14
+ This phased approach ensures that components can depend on earlier phases being complete, both within a domain and across different domains.
15
+
16
+ ## Core Components
17
+
18
+ ### DomainFactory
19
+
20
+ Abstract base class that all domain factories must extend. Each concrete factory must implement four setup methods:
21
+
22
+ - `setupEntities()` - Initialize data models and repositories
23
+ - `setupUCs()` - Initialize business logic components
24
+ - `setupPMs()` - Initialize presentation layer components
25
+ - `finalSetup()` - Perform final initialization
26
+
27
+ Domain factories automatically register themselves with the `DomainFactoryRepo` upon construction.
28
+
29
+ ### DomainFactoryRepo
30
+
31
+ Singleton repository that manages all domain factories and orchestrates their initialization. It provides:
32
+
33
+ - **Phase coordination** - Executes setup phases across all factories in the correct order
34
+ - **Factory lookup** - Retrieve factories by name using `getByName()`
35
+ - **Global access** - Singleton accessible via `DomainFactoryRepo.get(appObjects)`
36
+
37
+ ## Usage
38
+
39
+ ### Creating a Domain Factory
40
+
41
+ ```typescript
42
+ import { DomainFactory } from "@vived/core";
43
+ import { AppObject } from "@vived/core";
44
+
45
+ export class MyFeatureDomainFactory extends DomainFactory
46
+ {
47
+ readonly factoryName = "MyFeatureDomainFactory";
48
+
49
+ setupEntities(): void
50
+ {
51
+ // Create and configure entities and repositories
52
+ // Example: new MyEntityRepo(this.appObjects.getOrCreate("MyEntityRepo"));
53
+ }
54
+
55
+ setupUCs(): void
56
+ {
57
+ // Create and configure use cases
58
+ // Example: new MyUseCase(this.appObjects.getOrCreate("MyUseCase"));
59
+ }
60
+
61
+ setupPMs(): void
62
+ {
63
+ // Create and configure presentation managers
64
+ // Example: new MyPM(this.appObjects.getOrCreate("MyPM"));
65
+ }
66
+
67
+ finalSetup(): void
68
+ {
69
+ // Perform any final initialization
70
+ // Example: Subscribe to events, configure cross-domain dependencies
71
+ }
72
+
73
+ constructor(appObject: AppObject)
74
+ {
75
+ super(appObject);
76
+ }
77
+ }
78
+ ```
79
+
80
+ ### Initializing the Domain Layer
81
+
82
+ ```typescript
83
+ import { makeAppObjectRepo, makeDomainFactoryRepo } from "@vived/core";
84
+ import { MyFeatureDomainFactory } from "./MyFeatureDomainFactory";
85
+ import { AnotherFeatureDomainFactory } from "./AnotherFeatureDomainFactory";
86
+
87
+ // Create the application object repository
88
+ const appObjects = makeAppObjectRepo();
89
+
90
+ // Create the domain factory repository
91
+ const domainFactoryRepo = makeDomainFactoryRepo(appObjects);
92
+
93
+ // Instantiate your domain factories (they auto-register)
94
+ new MyFeatureDomainFactory(appObjects.getOrCreate("MyFeatureDomainFactory"));
95
+ new AnotherFeatureDomainFactory(appObjects.getOrCreate("AnotherFeatureDomainFactory"));
96
+
97
+ // Execute the complete domain setup in proper sequence
98
+ domainFactoryRepo.setupDomain();
99
+ ```
100
+
101
+ ### Accessing a Domain Factory
102
+
103
+ ```typescript
104
+ // Retrieve a specific domain factory by name
105
+ const myFactory = domainFactoryRepo.getByName("MyFeatureDomainFactory");
106
+
107
+ // Access the singleton repo from anywhere
108
+ const repo = DomainFactoryRepo.get(appObjects);
109
+ ```
110
+
111
+ ## Setup Sequence
112
+
113
+ When `setupDomain()` is called, the repository executes phases in this order:
114
+
115
+ 1. All factories execute `setupEntities()`
116
+ 2. All factories execute `setupUCs()`
117
+ 3. All factories execute `setupPMs()`
118
+ 4. All factories execute `finalSetup()`
119
+
120
+ This ensures that if Factory B's use cases depend on Factory A's entities, those entities will already be initialized.
121
+
122
+ ## Testing
123
+
124
+ The `MockDomainFactory` class provides a test double with Jest mock functions for all setup methods:
125
+
126
+ ```typescript
127
+ import { MockDomainFactory } from "@vived/core";
128
+ import { makeAppObjectRepo } from "@vived/core";
129
+
130
+ const appObjects = makeAppObjectRepo();
131
+ const mockFactory = new MockDomainFactory(appObjects.getOrCreate("test"));
132
+
133
+ // Verify setup methods are called
134
+ expect(mockFactory.setupEntities).toHaveBeenCalled();
135
+
136
+ // Check call order
137
+ expect(mockFactory.setupEntities).toHaveBeenCalledBefore(mockFactory.setupUCs);
138
+ ```
139
+
140
+ ## Best Practices
141
+
142
+ - **Keep factories focused** - Each factory should manage a single domain or feature
143
+ - **Use meaningful names** - Set `factoryName` to something descriptive for easy retrieval
144
+ - **Respect phase boundaries** - Don't access components from later phases during earlier phases
145
+ - **Avoid cross-phase dependencies** - Entities shouldn't depend on UCs, UCs shouldn't depend on PMs
146
+ - **Use finalSetup sparingly** - Most initialization should happen in the appropriate phase
147
+
148
+ ## Architecture Benefits
149
+
150
+ - **Dependency management** - Clear initialization order prevents "component not found" errors
151
+ - **Modularity** - Each domain is self-contained in its own factory
152
+ - **Testability** - Mock factories enable isolated testing of the orchestration logic
153
+ - **Scalability** - Easy to add new domains without modifying existing code
154
+ - **Maintainability** - Clear structure makes it obvious where components should be initialized