@vived/core 2.0.0 → 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/AppObject/AppObjectEntityRepo.js +16 -2
- package/dist/cjs/AppObject/AppObjectEntityRepo.js.map +1 -1
- package/dist/cjs/ExampleFeature/Entities/ExampleRepo.js +3 -4
- package/dist/cjs/ExampleFeature/Entities/ExampleRepo.js.map +1 -1
- package/dist/cjs/ExampleFeature/index.js +23 -0
- package/dist/cjs/ExampleFeature/index.js.map +1 -0
- package/dist/esm/AppObject/AppObjectEntityRepo.js +16 -2
- package/dist/esm/AppObject/AppObjectEntityRepo.js.map +1 -1
- package/dist/esm/ExampleFeature/Entities/ExampleRepo.js +3 -4
- package/dist/esm/ExampleFeature/Entities/ExampleRepo.js.map +1 -1
- package/dist/esm/ExampleFeature/index.js +7 -0
- package/dist/esm/ExampleFeature/index.js.map +1 -0
- package/dist/types/AppObject/AppObjectEntityRepo.d.ts +9 -1
- package/dist/types/AppObject/AppObjectEntityRepo.d.ts.map +1 -1
- package/dist/types/ExampleFeature/Entities/ExampleRepo.d.ts +1 -1
- package/dist/types/ExampleFeature/Entities/ExampleRepo.d.ts.map +1 -1
- 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,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
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Entities
|
|
2
|
+
|
|
3
|
+
The Entities folder provides foundational classes for building reactive, observable data models. It includes the Observer pattern implementation and a suite of memoized wrapper classes for various data types.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This module solves two key problems in reactive application development:
|
|
8
|
+
|
|
9
|
+
1. **Change Notification** - Notify observers when data changes (Observer pattern)
|
|
10
|
+
2. **Performance Optimization** - Only trigger callbacks when values actually change (memoization)
|
|
11
|
+
|
|
12
|
+
## Core Components
|
|
13
|
+
|
|
14
|
+
### Observer Pattern
|
|
15
|
+
|
|
16
|
+
#### ObservableEntity
|
|
17
|
+
|
|
18
|
+
Abstract base class implementing the Observer pattern. Extend this to make any entity observable.
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { ObservableEntity } from "@vived/core";
|
|
22
|
+
|
|
23
|
+
export class MyEntity extends ObservableEntity
|
|
24
|
+
{
|
|
25
|
+
private _name: string = "";
|
|
26
|
+
|
|
27
|
+
get name(): string
|
|
28
|
+
{
|
|
29
|
+
return this._name;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
set name(value: string)
|
|
33
|
+
{
|
|
34
|
+
this._name = value;
|
|
35
|
+
this.notify(); // Notify all observers
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Usage
|
|
40
|
+
const entity = new MyEntity();
|
|
41
|
+
entity.addObserver(() => console.log("Entity changed!"));
|
|
42
|
+
entity.name = "New Name"; // Logs: "Entity changed!"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
#### ObserverList
|
|
46
|
+
|
|
47
|
+
Generic implementation of the Observer pattern with typed messages. Use this when you need to pass data to observers.
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { ObserverList } from "@vived/core";
|
|
51
|
+
|
|
52
|
+
const observers = new ObserverList<string>();
|
|
53
|
+
|
|
54
|
+
observers.add((message) => console.log(`Received: ${message}`));
|
|
55
|
+
observers.notify("Hello World"); // Logs: "Received: Hello World"
|
|
56
|
+
|
|
57
|
+
// Check observer count
|
|
58
|
+
console.log(observers.length); // 1
|
|
59
|
+
|
|
60
|
+
// Clear all observers
|
|
61
|
+
observers.clear();
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Memoized Wrappers
|
|
65
|
+
|
|
66
|
+
Memoized classes wrap values and only trigger callbacks when the value actually changes. This prevents unnecessary re-renders and computations.
|
|
67
|
+
|
|
68
|
+
#### MemoizedBoolean
|
|
69
|
+
|
|
70
|
+
Wrapper for boolean values with change detection.
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { MemoizedBoolean } from "@vived/core";
|
|
74
|
+
|
|
75
|
+
const isVisible = new MemoizedBoolean(
|
|
76
|
+
false,
|
|
77
|
+
() => console.log("Visibility changed")
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
isVisible.val = true; // Logs: "Visibility changed"
|
|
81
|
+
isVisible.val = true; // No callback - value didn't change
|
|
82
|
+
isVisible.val = false; // Logs: "Visibility changed"
|
|
83
|
+
|
|
84
|
+
// Set without triggering callback
|
|
85
|
+
isVisible.setValQuietly(true);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### MemoizedNumber
|
|
89
|
+
|
|
90
|
+
Wrapper for numeric values with change detection.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { MemoizedNumber } from "@vived/core";
|
|
94
|
+
|
|
95
|
+
const count = new MemoizedNumber(
|
|
96
|
+
0,
|
|
97
|
+
() => console.log("Count changed")
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
count.val = 5; // Logs: "Count changed"
|
|
101
|
+
count.val = 5; // No callback - value didn't change
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### MemoizedString
|
|
105
|
+
|
|
106
|
+
Wrapper for string values with change detection.
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { MemoizedString } from "@vived/core";
|
|
110
|
+
|
|
111
|
+
const title = new MemoizedString(
|
|
112
|
+
"Untitled",
|
|
113
|
+
() => console.log("Title changed")
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
title.val = "New Title"; // Logs: "Title changed"
|
|
117
|
+
title.val = "New Title"; // No callback - value didn't change
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### MemoizedAngle
|
|
121
|
+
|
|
122
|
+
Wrapper for Angle value objects with change detection.
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { MemoizedAngle, Angle } from "@vived/core";
|
|
126
|
+
|
|
127
|
+
const rotation = new MemoizedAngle(
|
|
128
|
+
Angle.FromDegrees(0),
|
|
129
|
+
() => console.log("Rotation changed")
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
rotation.val = Angle.FromDegrees(45);
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### MemoizedVector2
|
|
136
|
+
|
|
137
|
+
Wrapper for 2D vector value objects with change detection.
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
import { MemoizedVector2, Vector2 } from "@vived/core";
|
|
141
|
+
|
|
142
|
+
const position = new MemoizedVector2(
|
|
143
|
+
Vector2.Zero(),
|
|
144
|
+
() => console.log("Position changed")
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
position.val = new Vector2(10, 20);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### MemoizedVector3
|
|
151
|
+
|
|
152
|
+
Wrapper for 3D vector value objects with change detection.
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
import { MemoizedVector3, Vector3 } from "@vived/core";
|
|
156
|
+
|
|
157
|
+
const position = new MemoizedVector3(
|
|
158
|
+
Vector3.Zero(),
|
|
159
|
+
() => console.log("Position changed")
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
position.val = new Vector3(10, 20, 30);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### MemoizedQuaternion
|
|
166
|
+
|
|
167
|
+
Wrapper for quaternion rotation value objects with change detection.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { MemoizedQuaternion, Quaternion } from "@vived/core";
|
|
171
|
+
|
|
172
|
+
const rotation = new MemoizedQuaternion(
|
|
173
|
+
Quaternion.Identity(),
|
|
174
|
+
() => console.log("Rotation changed")
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
rotation.val = Quaternion.FromEuler(0, Math.PI / 2, 0);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
#### MemoizedColor
|
|
181
|
+
|
|
182
|
+
Wrapper for color value objects with change detection.
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
import { MemoizedColor, Color } from "@vived/core";
|
|
186
|
+
|
|
187
|
+
const backgroundColor = new MemoizedColor(
|
|
188
|
+
Color.White(),
|
|
189
|
+
() => console.log("Color changed")
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
backgroundColor.val = Color.FromRGB(255, 0, 0);
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Range-Constrained Values
|
|
196
|
+
|
|
197
|
+
#### RangedNumber
|
|
198
|
+
|
|
199
|
+
Numeric value that is automatically clamped to a specified range.
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
import { RangedNumber } from "@vived/core";
|
|
203
|
+
|
|
204
|
+
const volume = new RangedNumber(
|
|
205
|
+
{
|
|
206
|
+
min: 0,
|
|
207
|
+
max: 100,
|
|
208
|
+
initialValue: 50
|
|
209
|
+
},
|
|
210
|
+
() => console.log("Volume changed")
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
volume.val = 75; // Sets to 75
|
|
214
|
+
volume.val = 150; // Clamped to 100
|
|
215
|
+
volume.val = -10; // Clamped to 0
|
|
216
|
+
|
|
217
|
+
console.log(volume.minValue); // 0
|
|
218
|
+
console.log(volume.maxValue); // 100
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Common Patterns
|
|
222
|
+
|
|
223
|
+
### Building Reactive Entities
|
|
224
|
+
|
|
225
|
+
Combine memoized values within an observable entity:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { ObservableEntity, MemoizedString, MemoizedNumber } from "@vived/core";
|
|
229
|
+
|
|
230
|
+
export class User extends ObservableEntity
|
|
231
|
+
{
|
|
232
|
+
readonly name: MemoizedString;
|
|
233
|
+
readonly age: MemoizedNumber;
|
|
234
|
+
|
|
235
|
+
constructor(name: string, age: number)
|
|
236
|
+
{
|
|
237
|
+
super();
|
|
238
|
+
this.name = new MemoizedString(name, () => this.notify());
|
|
239
|
+
this.age = new MemoizedNumber(age, () => this.notify());
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Usage
|
|
244
|
+
const user = new User("John", 30);
|
|
245
|
+
user.addObserver(() => console.log("User updated"));
|
|
246
|
+
|
|
247
|
+
user.name.val = "Jane"; // Triggers observer
|
|
248
|
+
user.age.val = 31; // Triggers observer
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Silent Updates
|
|
252
|
+
|
|
253
|
+
Use `setValQuietly()` when you need to update values without triggering side effects:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
const count = new MemoizedNumber(0, () => updateUI());
|
|
257
|
+
|
|
258
|
+
// Update without triggering UI update
|
|
259
|
+
count.setValQuietly(10);
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Managing Observers
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
const entity = new MyEntity();
|
|
266
|
+
|
|
267
|
+
// Add observer
|
|
268
|
+
const observer = () => console.log("Changed");
|
|
269
|
+
entity.addObserver(observer);
|
|
270
|
+
|
|
271
|
+
// Remove specific observer
|
|
272
|
+
entity.removeObserver(observer);
|
|
273
|
+
|
|
274
|
+
// Using ObserverList directly
|
|
275
|
+
const observers = new ObserverList<void>();
|
|
276
|
+
observers.add(observer);
|
|
277
|
+
observers.remove(observer);
|
|
278
|
+
observers.clear(); // Remove all
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Design Principles
|
|
282
|
+
|
|
283
|
+
### Memoization
|
|
284
|
+
|
|
285
|
+
All memoized classes compare values before triggering callbacks:
|
|
286
|
+
- **Primitives** (boolean, number, string) use strict equality (`===`)
|
|
287
|
+
- **Value Objects** (Vector, Color, etc.) use type-specific equality methods
|
|
288
|
+
- This prevents unnecessary callback execution and improves performance
|
|
289
|
+
|
|
290
|
+
### Immutability Considerations
|
|
291
|
+
|
|
292
|
+
Value objects (Vector3, Color, etc.) should be treated as immutable. Always create new instances rather than mutating existing ones:
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
// Good
|
|
296
|
+
position.val = new Vector3(position.val.x + 1, position.val.y, position.val.z);
|
|
297
|
+
|
|
298
|
+
// Bad - mutation won't trigger change detection
|
|
299
|
+
position.val.x += 1;
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Testing
|
|
303
|
+
|
|
304
|
+
All memoized and observable classes include comprehensive test coverage demonstrating:
|
|
305
|
+
- Initial value storage
|
|
306
|
+
- Change detection
|
|
307
|
+
- Callback triggering
|
|
308
|
+
- Silent updates
|
|
309
|
+
- Range clamping (for RangedNumber)
|
|
310
|
+
|
|
311
|
+
Example test pattern:
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
it("only triggers callback when value changes", () => {
|
|
315
|
+
const callback = jest.fn();
|
|
316
|
+
const memoized = new MemoizedNumber(5, callback);
|
|
317
|
+
|
|
318
|
+
memoized.val = 5; // No change
|
|
319
|
+
expect(callback).not.toBeCalled();
|
|
320
|
+
|
|
321
|
+
memoized.val = 10; // Changed
|
|
322
|
+
expect(callback).toBeCalledTimes(1);
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Use Cases
|
|
327
|
+
|
|
328
|
+
- **State Management** - Track application state with automatic change notifications
|
|
329
|
+
- **View Models** - Build presentation models that notify views when data changes
|
|
330
|
+
- **Form Inputs** - Validate and constrain user input with RangedNumber
|
|
331
|
+
- **Animation** - Track animated values and trigger updates only when they change
|
|
332
|
+
- **Configuration** - Manage settings with type-safe, observable properties
|
|
333
|
+
- **Game Objects** - Track entity properties (position, rotation, color) with change detection
|
|
334
|
+
|
|
335
|
+
## Performance Benefits
|
|
336
|
+
|
|
337
|
+
- **Reduced Re-renders** - UI updates only when values actually change
|
|
338
|
+
- **Efficient Validation** - Range constraints applied automatically
|
|
339
|
+
- **Lazy Computation** - Callbacks only fire when necessary
|
|
340
|
+
- **Memory Efficient** - Lightweight wrappers with minimal overhead
|