@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.
- package/README.md +275 -65
- package/dist/cjs/ExampleFeature/index.js +23 -0
- package/dist/cjs/ExampleFeature/index.js.map +1 -0
- package/dist/esm/ExampleFeature/index.js +7 -0
- package/dist/esm/ExampleFeature/index.js.map +1 -0
- package/dist/types/ExampleFeature/index.d.ts +5 -0
- package/dist/types/ExampleFeature/index.d.ts.map +1 -0
- package/package.json +3 -2
- package/src/AppObject/README.md +476 -0
- package/src/DomainFactories/README.md +154 -0
- package/src/Entities/README.md +340 -0
- package/src/ExampleFeature/README.md +804 -0
- package/src/Types/README.md +549 -0
- package/src/Utilities/README.md +478 -0
- package/src/ValueObjects/README.md +552 -0
|
@@ -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
|