@rs-x/state-manager 0.4.4
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/LICENSE +21 -0
- package/dist/index.d.ts +683 -0
- package/dist/index.js +3219 -0
- package/package.json +56 -0
- package/readme.md +1765 -0
package/readme.md
ADDED
|
@@ -0,0 +1,1765 @@
|
|
|
1
|
+
# State-manager
|
|
2
|
+
|
|
3
|
+
The **State Manager** provides an efficient way to observe and synchronize state changes across your application.
|
|
4
|
+
|
|
5
|
+
A state item is defined by a **context** and an **index**.
|
|
6
|
+
A context can be an object, and an index can be a property name — but it is not limited to that. It can be **any value**. The context is used as an identifier to group a set of state indexes.
|
|
7
|
+
|
|
8
|
+
Examples of state indexes:
|
|
9
|
+
|
|
10
|
+
- Object property or field → index = property or field name
|
|
11
|
+
- Array item → index = numeric position
|
|
12
|
+
- Map item → index = map key
|
|
13
|
+
|
|
14
|
+
The State Manager does **not** automatically know how to detect changes for every state value data type, nor how to patch the corresponding state setter. Therefore, it relies on two services:
|
|
15
|
+
|
|
16
|
+
- A service implementing `IObjectPropertyObserverProxyPairManager`
|
|
17
|
+
Responsible for creating observers and proxying values when needed.
|
|
18
|
+
|
|
19
|
+
- A service implementing `IIndexValueAccessor`
|
|
20
|
+
Responsible for retrieving the current value.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
The **State Manager** has the followng interface:
|
|
24
|
+
```ts
|
|
25
|
+
export interface IStateManager {
|
|
26
|
+
readonly changed: Observable<IStateChange>;
|
|
27
|
+
readonly contextChanged: Observable<IContextChanged>;
|
|
28
|
+
readonly startChangeCycle: Observable<void>;
|
|
29
|
+
readonly endChangeCycle: Observable<void>;
|
|
30
|
+
|
|
31
|
+
isWatched(
|
|
32
|
+
context: unknown,
|
|
33
|
+
index: unknown,
|
|
34
|
+
mustProxify: MustProxify
|
|
35
|
+
): boolean;
|
|
36
|
+
|
|
37
|
+
watchState(
|
|
38
|
+
context: unknown,
|
|
39
|
+
index: unknown,
|
|
40
|
+
mustProxify?: MustProxify
|
|
41
|
+
): unknown;
|
|
42
|
+
|
|
43
|
+
releaseState(
|
|
44
|
+
oontext: unknown,
|
|
45
|
+
index: unknown,
|
|
46
|
+
mustProxify?: MustProxify
|
|
47
|
+
): void;
|
|
48
|
+
|
|
49
|
+
getState<T>(
|
|
50
|
+
context: unknown,
|
|
51
|
+
index: unknown
|
|
52
|
+
): T;
|
|
53
|
+
|
|
54
|
+
setState<T>(
|
|
55
|
+
context: unknown,
|
|
56
|
+
index: unknown,
|
|
57
|
+
value: T
|
|
58
|
+
): void;
|
|
59
|
+
|
|
60
|
+
clear(): void;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Members
|
|
67
|
+
|
|
68
|
+
### **changed**
|
|
69
|
+
**Type:** `Observable<IStateChange>`
|
|
70
|
+
Emits whenever a state item value changes.
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
### **contextChanged**
|
|
75
|
+
**Type:** `Observable<IContextChanged>`
|
|
76
|
+
Emits whenever an entire context is replaced.
|
|
77
|
+
This happens, for example, when a nested object is replaced.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
### **startChangeCycle**
|
|
82
|
+
**Type:** `Observable<void>`
|
|
83
|
+
Emits at the start of processing a state item change.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
### **endChangeCycle**
|
|
88
|
+
**Type:** `Observable<void>`
|
|
89
|
+
Emits at the end of processing a state item change.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
### **isWatched(context, index, mustProxify)**
|
|
94
|
+
Returns whether the state item identified by the `(context, index, mustProxify)` triplet is currently being watched.
|
|
95
|
+
|
|
96
|
+
| Parameter | Type | Description |
|
|
97
|
+
| --------------- | -------------------------- | --------------------------------------------------------------- |
|
|
98
|
+
| **context** | `unknown` | The context to which the state index belongs. |
|
|
99
|
+
| **index** | `unknown` | The index identifying the state on the given context. |
|
|
100
|
+
| **mustProxify** | `MustProxify` *(optional)* | Predicate determining whether a nested state value must be proxified. It should be the same predicate that was passed to `watchState`. |
|
|
101
|
+
|
|
102
|
+
**Returns:** `boolean`
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### **watchState(context, index, mustProxify?)**
|
|
107
|
+
Watches a state item identified by the `(context, index, mustProxify)` triplet so that its value is proxified and tracked.
|
|
108
|
+
|
|
109
|
+
| Parameter | Type | Description |
|
|
110
|
+
| --------------- | -------------------------- | --------------------------------------------------------------- |
|
|
111
|
+
| **context** | `unknown` | The state context. |
|
|
112
|
+
| **index** | `unknown` | The index identifying the state on the given context. |
|
|
113
|
+
| **mustProxify** | `MustProxify` *(optional)* | Predicate determining whether a nested state value must be proxified.|
|
|
114
|
+
|
|
115
|
+
**Returns:**
|
|
116
|
+
`unknown` — the state item value if it was already being watched; otherwise `undefined`.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### **releaseState(context, index, mustProxify?)**
|
|
121
|
+
Releases the state item identified by the `(context, index, mustProxify)` triplet.
|
|
122
|
+
Each call to `watchState` should have a corresponding `releaseState` call to ensure the state item is released when it is no longer needed.
|
|
123
|
+
|
|
124
|
+
| Parameter | Type | Description |
|
|
125
|
+
| --------------- | -------------------------- | --------------------------------------------------------------- |
|
|
126
|
+
| **context** | `unknown` | The state context. |
|
|
127
|
+
| **index** | `unknown` | The index identifying the state on the given context. |
|
|
128
|
+
| **mustProxify** | `MustProxify` *(optional)* | Predicate determining whether a nested state value must be proxified. It should be the same predicate that was passed to `watchState`. . |
|
|
129
|
+
|
|
130
|
+
**Returns:** `void`
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### **getState(context, index)**
|
|
135
|
+
Retrieves the current state item value identified by the `(context, index)` pair.
|
|
136
|
+
|
|
137
|
+
| Parameter | Type | Description |
|
|
138
|
+
| ----------- | --------- | --------------------------------------------------------------- |
|
|
139
|
+
| **context** | `unknown` | The state context. |
|
|
140
|
+
| **index** | `unknown` | The index identifying the state on the given context. |
|
|
141
|
+
|
|
142
|
+
**Returns:** `unknown`
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
### **setState(context, index, value)**
|
|
147
|
+
Sets the value of the state item identified by the `(context, index)` pair.
|
|
148
|
+
|
|
149
|
+
Unlike `watchState`, `setState` does **not** track changes. It does not patch setters or proxify values.
|
|
150
|
+
A change event is emitted on the first `setState` call and again whenever the value changes in subsequent calls.
|
|
151
|
+
|
|
152
|
+
| Parameter | Type | Description |
|
|
153
|
+
| ----------- | --------- | --------------------------------------------------------------- |
|
|
154
|
+
| **context** | `unknown` | The state context. |
|
|
155
|
+
| **index** | `unknown` | The index identifying the state on the given context. |
|
|
156
|
+
| **value** | `unknown` | The state value. |
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### **clear()**
|
|
161
|
+
Releases all registered state items.
|
|
162
|
+
|
|
163
|
+
**Returns:** `void`
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Get an instance of the State Manager
|
|
168
|
+
|
|
169
|
+
The state manager is registered as a **singleton service**.
|
|
170
|
+
You must load the module into the injection container if you went
|
|
171
|
+
to use it.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { InjectionContainer } from '@rs-x/core';
|
|
175
|
+
import { RsXStateManagerModule } from '@rs-x/state-manager';
|
|
176
|
+
|
|
177
|
+
InjectionContainer.load(RsXStateManagerModule);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
There are two ways to get an instance:
|
|
181
|
+
|
|
182
|
+
### 1. Using the injection container
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { InjectionContainer } from '@rs-x/core';
|
|
186
|
+
import { IIStateManager, RsXStateManagerInjectionTokens } from '@rs-x/state-manager';
|
|
187
|
+
|
|
188
|
+
const stateManager: IIStateManager = InjectionContainer.get(
|
|
189
|
+
RsXStateManagerInjectionTokens.IStateManager
|
|
190
|
+
);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 2. Using the `@Inject` decorator
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { Inject } from '@rs-x/core';
|
|
197
|
+
import { IIStateManager, RsXStateManagerInjectionTokens } from '@rs-x/state-manager';
|
|
198
|
+
|
|
199
|
+
export class MyClass {
|
|
200
|
+
|
|
201
|
+
constructor(
|
|
202
|
+
@Inject(RsXStateManagerInjectionTokens.IStateManager)
|
|
203
|
+
private readonly _stateManager: IIStateManager
|
|
204
|
+
) {}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Register state
|
|
211
|
+
|
|
212
|
+
There are two variants:
|
|
213
|
+
|
|
214
|
+
### Non-recursive
|
|
215
|
+
Monitors only assignment of a **new value** to the index.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { InjectionContainer, printValue } from '@rs-x/core';
|
|
219
|
+
import {
|
|
220
|
+
IStateChange,
|
|
221
|
+
IStateManager,
|
|
222
|
+
RsXStateManagerInjectionTokens,
|
|
223
|
+
RsXStateManagerModule
|
|
224
|
+
} from '@rs-x/state-manager';
|
|
225
|
+
|
|
226
|
+
// Load the state manager module into the injection container
|
|
227
|
+
InjectionContainer.load(RsXStateManagerModule);
|
|
228
|
+
|
|
229
|
+
export const run = (() => {
|
|
230
|
+
const stateManager: IStateManager = InjectionContainer.get(
|
|
231
|
+
RsXStateManagerInjectionTokens.IStateManager
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const stateContext = {
|
|
235
|
+
x: { y: 10 }
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// This will emit a change event with the initial (current) value.
|
|
239
|
+
console.log('Initial value:');
|
|
240
|
+
const changedSubsription = stateManager.changed.subscribe((change: IStateChange) => {
|
|
241
|
+
printValue(change.newValue);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
// This will emit the new value { y: 10 }
|
|
246
|
+
stateManager.watchState(stateContext, 'x');
|
|
247
|
+
|
|
248
|
+
console.log('Changed value:');
|
|
249
|
+
// This will emit the new value { y: 10 }
|
|
250
|
+
stateContext.x = {
|
|
251
|
+
y: 20
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
console.log(`Latest value:`);
|
|
255
|
+
printValue(stateManager.getState(stateContext, 'x'));
|
|
256
|
+
|
|
257
|
+
// This will emit no change because the state is not recursive.
|
|
258
|
+
console.log('\nstateContext.x.y = 30 will not emit any change:\n---\n');
|
|
259
|
+
stateContext.x.y = 30;
|
|
260
|
+
|
|
261
|
+
} finally {
|
|
262
|
+
changedSubsription.unsubscribe();
|
|
263
|
+
// Always release the state when it is no longer needed.
|
|
264
|
+
stateManager.releaseState(stateContext, 'x');
|
|
265
|
+
}
|
|
266
|
+
})();
|
|
267
|
+
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
**Output:**
|
|
271
|
+
|
|
272
|
+
```console
|
|
273
|
+
Running demo: demo/src/rs-x-state-manager/register-non-recursive-state.ts
|
|
274
|
+
Initial value:
|
|
275
|
+
{
|
|
276
|
+
y: 10
|
|
277
|
+
}
|
|
278
|
+
Changed value:
|
|
279
|
+
{
|
|
280
|
+
y: 20
|
|
281
|
+
}
|
|
282
|
+
Latest value:
|
|
283
|
+
{
|
|
284
|
+
y: 20
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
stateContext.x.y = 30 will not emit any change:
|
|
288
|
+
---
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
### Recursive
|
|
294
|
+
Monitors assignments **and** changes *inside* the value.
|
|
295
|
+
Example: if the value is an object, internal object changes are also observed. You can make a state item recursive by passing in a **mustProxify** predicate to a **watchState** call. The mustProxify will be called for every nested index. If you return true it will watch the index otherwise not.
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
import { InjectionContainer, printValue, truePredicate } from '@rs-x/core';
|
|
299
|
+
import {
|
|
300
|
+
IStateChange,
|
|
301
|
+
IStateManager,
|
|
302
|
+
RsXStateManagerInjectionTokens,
|
|
303
|
+
RsXStateManagerModule
|
|
304
|
+
} from '@rs-x/state-manager';
|
|
305
|
+
|
|
306
|
+
// Load the state manager module into the injection container
|
|
307
|
+
InjectionContainer.load(RsXStateManagerModule);
|
|
308
|
+
|
|
309
|
+
export const run = (() => {
|
|
310
|
+
const stateManager: IStateManager = InjectionContainer.get(
|
|
311
|
+
RsXStateManagerInjectionTokens.IStateManager
|
|
312
|
+
);
|
|
313
|
+
const stateContext = {
|
|
314
|
+
x: { y: 10 }
|
|
315
|
+
};
|
|
316
|
+
const changedSubscription = stateManager.changed.subscribe((change: IStateChange) => {
|
|
317
|
+
printValue(change.newValue)
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
// We register recursive state by passing
|
|
322
|
+
// a predicate as the third argument.
|
|
323
|
+
// In this case, we want to watch the entire value,
|
|
324
|
+
// so we pass a predicate that always returns true.
|
|
325
|
+
// This will emit an initial value { y: 10 }
|
|
326
|
+
console.log('Initial value:');
|
|
327
|
+
stateManager.watchState(stateContext, 'x', truePredicate);
|
|
328
|
+
|
|
329
|
+
console.log('Changed value:');
|
|
330
|
+
// This will emit the new value { y: 10 }
|
|
331
|
+
stateContext.x = {
|
|
332
|
+
y: 20
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
console.log('Changed (recursive) value:');
|
|
336
|
+
// This will emit the new value { y: 30 } because x
|
|
337
|
+
// is registered as a recursive state.
|
|
338
|
+
stateContext.x.y = 30;
|
|
339
|
+
|
|
340
|
+
console.log(`Latest value:`);
|
|
341
|
+
printValue(stateManager.getState(stateContext, 'x'));
|
|
342
|
+
|
|
343
|
+
} finally {
|
|
344
|
+
changedSubscription.unsubscribe();
|
|
345
|
+
// Always release the state when it is no longer needed.
|
|
346
|
+
stateManager.releaseState(stateContext, 'x', truePredicate);
|
|
347
|
+
}
|
|
348
|
+
})();
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**Output:**
|
|
352
|
+
|
|
353
|
+
```console
|
|
354
|
+
Initial value:
|
|
355
|
+
{ y: 10 }
|
|
356
|
+
|
|
357
|
+
Changed value:
|
|
358
|
+
{ y: 20 }
|
|
359
|
+
|
|
360
|
+
Changed (recursive) value:
|
|
361
|
+
{ y: 30 }
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
### Manually setting state
|
|
367
|
+
|
|
368
|
+
Besides that you can register a watched stated (calling `watchedState`) you can register an unwatched state using `setState`. An example for using `setState` might be an readonly property:
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
import { InjectionContainer, printValue } from '@rs-x/core';
|
|
372
|
+
import {
|
|
373
|
+
IStateChange,
|
|
374
|
+
IStateManager,
|
|
375
|
+
RsXStateManagerInjectionTokens,
|
|
376
|
+
RsXStateManagerModule
|
|
377
|
+
} from '@rs-x/state-manager';
|
|
378
|
+
|
|
379
|
+
// Load the state manager module into the injection container
|
|
380
|
+
InjectionContainer.load(RsXStateManagerModule);
|
|
381
|
+
|
|
382
|
+
export const run = (() => {
|
|
383
|
+
const stateManager: IStateManager = InjectionContainer.get(
|
|
384
|
+
RsXStateManagerInjectionTokens.IStateManager
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
class StateContext {
|
|
388
|
+
private readonly _aPlusBId = 'a+b';
|
|
389
|
+
private _a = 10;
|
|
390
|
+
private _b = 20;
|
|
391
|
+
|
|
392
|
+
constructor() {
|
|
393
|
+
this.setAPlusB();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
public dispose(): void {
|
|
397
|
+
return stateManager.releaseState(this, this._aPlusBId);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
public get aPlusB(): number {
|
|
401
|
+
return stateManager.getState(this, this._aPlusBId);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
public get a(): number {
|
|
405
|
+
return this._a;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
public set a(value: number) {
|
|
409
|
+
this._a = value;
|
|
410
|
+
this.setAPlusB();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
public get b(): number {
|
|
414
|
+
return this._b;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
public set b(value: number) {
|
|
418
|
+
this._b = value;
|
|
419
|
+
this.setAPlusB();
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private setAPlusB(): void {
|
|
423
|
+
stateManager.setState(this, this._aPlusBId, this._a + this._b)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const stateContext = new StateContext();
|
|
428
|
+
const changeSubscription = stateManager.changed.subscribe((change: IStateChange) => {
|
|
429
|
+
printValue(change.newValue);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
console.log(`Initial value for readonly property 'aPlusB':`);
|
|
434
|
+
console.log(stateContext.aPlusB);
|
|
435
|
+
|
|
436
|
+
console.log(`set 'stateContext.a' to '100' will emit a change event for readonly property 'aPlusB'`);
|
|
437
|
+
console.log(`Changed value for readonly property 'aPlusB':`);
|
|
438
|
+
stateContext.a = 100;
|
|
439
|
+
|
|
440
|
+
console.log(`set 'stateContext.b' to '200' will emit a change event for readonly property 'aPlusB'`);
|
|
441
|
+
console.log(`Changed value for readonly property 'aPlusB':`);
|
|
442
|
+
stateContext.b = 200;
|
|
443
|
+
|
|
444
|
+
} finally {
|
|
445
|
+
changeSubscription.unsubscribe();
|
|
446
|
+
// Always release the state when it is no longer needed.
|
|
447
|
+
stateContext.dispose();
|
|
448
|
+
}
|
|
449
|
+
})();
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**Output:**
|
|
453
|
+
|
|
454
|
+
```console
|
|
455
|
+
Running demo: demo/src/rs-x-state-manager/register-readonly-property.ts
|
|
456
|
+
Initial value for readonly property 'aPlusB':
|
|
457
|
+
30
|
|
458
|
+
set 'stateContext.a' to '100' will emit a change event for readonly property 'aPlusB'
|
|
459
|
+
Changed value for readonly property 'aPlusB':
|
|
460
|
+
120
|
|
461
|
+
set 'stateContext.b' to '200' will emit a change event for readonly property 'aPlusB'
|
|
462
|
+
Changed value for readonly property 'aPlusB':
|
|
463
|
+
300
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
---
|
|
467
|
+
|
|
468
|
+
### State registration is idempotent
|
|
469
|
+
|
|
470
|
+
You can register the same state item multiple times.
|
|
471
|
+
Never assume a state is already registered. Always register it if you depend on it.
|
|
472
|
+
Otherwise the state may disappear when another part of the system unregisters it. The state manager keeps track of a reference count and will release the state when it goes to zero.
|
|
473
|
+
|
|
474
|
+
When done, release the state:
|
|
475
|
+
|
|
476
|
+
```ts
|
|
477
|
+
import { InjectionContainer, printValue } from '@rs-x/core';
|
|
478
|
+
import {
|
|
479
|
+
IStateChange,
|
|
480
|
+
IStateManager,
|
|
481
|
+
RsXStateManagerInjectionTokens,
|
|
482
|
+
RsXStateManagerModule
|
|
483
|
+
} from '@rs-x/state-manager';
|
|
484
|
+
|
|
485
|
+
// Load the state manager module into the injection container
|
|
486
|
+
InjectionContainer.load(RsXStateManagerModule);
|
|
487
|
+
|
|
488
|
+
export const run = (() => {
|
|
489
|
+
const stateManager: IStateManager = InjectionContainer.get(
|
|
490
|
+
RsXStateManagerInjectionTokens.IStateManager
|
|
491
|
+
);
|
|
492
|
+
const stateContext = {
|
|
493
|
+
x: { y: 10 }
|
|
494
|
+
};
|
|
495
|
+
const changedSubscription = stateManager.changed.subscribe((change: IStateChange) => {
|
|
496
|
+
printValue(change.newValue);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
// Register is idempotent: you can register the same state multiple times.
|
|
501
|
+
// For every register call, make sure you call unregister when you're done.
|
|
502
|
+
console.log('Initial value:');
|
|
503
|
+
stateManager.watchState(stateContext, 'x');
|
|
504
|
+
stateManager.watchState(stateContext, 'x');
|
|
505
|
+
|
|
506
|
+
console.log('Changed value:');
|
|
507
|
+
stateContext.x = { y: 20 };
|
|
508
|
+
|
|
509
|
+
stateManager.releaseState(stateContext, 'x');
|
|
510
|
+
|
|
511
|
+
console.log('Changed event is still emitted after unregister because one observer remains.');
|
|
512
|
+
console.log('Changed value:');
|
|
513
|
+
stateContext.x = { y: 30 };
|
|
514
|
+
|
|
515
|
+
stateManager.releaseState(stateContext, 'x');
|
|
516
|
+
|
|
517
|
+
console.log('Changed event is no longer emitted after the last observer unregisters.');
|
|
518
|
+
console.log('Changed value:');
|
|
519
|
+
console.log('---');
|
|
520
|
+
stateContext.x = { y: 30 };
|
|
521
|
+
|
|
522
|
+
} finally {
|
|
523
|
+
changedSubscription.unsubscribe();
|
|
524
|
+
}
|
|
525
|
+
})();
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**Output:**
|
|
529
|
+
|
|
530
|
+
```console
|
|
531
|
+
Initial value:
|
|
532
|
+
{ y: 10 }
|
|
533
|
+
|
|
534
|
+
Changed value:
|
|
535
|
+
{ y: 20 }
|
|
536
|
+
|
|
537
|
+
Changed event is still emitted after unregister because one observer remains.
|
|
538
|
+
Changed value:
|
|
539
|
+
{ y: 30 }
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## Support data types
|
|
545
|
+
|
|
546
|
+
The state manager works by creating **observers** based on the data type of the registered state.
|
|
547
|
+
It uses a chain of **observer factories**, each capable of determining whether it supports a particular type.
|
|
548
|
+
The **first factory** that returns `true` is used.
|
|
549
|
+
|
|
550
|
+
You can override this factory list by providing your own custom provider service.
|
|
551
|
+
|
|
552
|
+
| Type | Index | Implementation | example |
|
|
553
|
+
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | -------------------------------- |
|
|
554
|
+
| Object | any field/property | Patching | [example](#object-propertyfield) |
|
|
555
|
+
| Date | - year, utcYear<br>- month, utcMonth<br>- date, utcDate<br>- hours, utcHours<br>- minutes, utcMinutes<br>- seconds, utcSeconds<br>- milliseconds, utcMilliseconds<br>- times | Proxy | [example](#date) |
|
|
556
|
+
| Array | number | Proxy | [example](#array) |
|
|
557
|
+
| Map | any | Proxy | [example](#map) |
|
|
558
|
+
| Set | Not indexable | Proxy | [example](#set) |
|
|
559
|
+
| Promise | Not indexable | Attach `.then` handler | [example](#promise) |
|
|
560
|
+
| Observable | Not indexable | Subscribe | [example](#observable) |
|
|
561
|
+
| Custom type | user defined | user defined | [example](#customtype) |
|
|
562
|
+
|
|
563
|
+
State item is identified by a **context**, **index** and **mustProxify** predicate for a recursive state item
|
|
564
|
+
The manager checks each observer factory to determine support based on the **context** and **index**.
|
|
565
|
+
|
|
566
|
+
Behavior:
|
|
567
|
+
|
|
568
|
+
- Both recursive and non-recursive observers monitor **assignment** of a new value.
|
|
569
|
+
- Recursive observers additionally monitor **internal changes** of the value. The nested values you want to monitor are determine by the **mustProxify** predicate.
|
|
570
|
+
|
|
571
|
+
The following example illustrates the different state types:
|
|
572
|
+
|
|
573
|
+
### Object property/field
|
|
574
|
+
**Example**
|
|
575
|
+
```ts
|
|
576
|
+
import { InjectionContainer, printValue, truePredicate } from '@rs-x/core';
|
|
577
|
+
import {
|
|
578
|
+
IStateChange,
|
|
579
|
+
IStateManager,
|
|
580
|
+
RsXStateManagerInjectionTokens,
|
|
581
|
+
RsXStateManagerModule
|
|
582
|
+
} from '@rs-x/state-manager';
|
|
583
|
+
|
|
584
|
+
// Load the state manager module into the injection container
|
|
585
|
+
InjectionContainer.load(RsXStateManagerModule);
|
|
586
|
+
|
|
587
|
+
interface INestStateConext {
|
|
588
|
+
a: number;
|
|
589
|
+
nested?: INestStateConext;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
class StateContext {
|
|
593
|
+
private _b: INestStateConext = {
|
|
594
|
+
a: 10,
|
|
595
|
+
nested: {
|
|
596
|
+
a: 20,
|
|
597
|
+
nested: {
|
|
598
|
+
a: 30,
|
|
599
|
+
nested: {
|
|
600
|
+
a: 40
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
public get b(): INestStateConext {
|
|
607
|
+
return this._b;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
public set b(value: INestStateConext) {
|
|
611
|
+
this._b = value;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export const run = (() => {
|
|
616
|
+
const stateManager: IStateManager = InjectionContainer.get(
|
|
617
|
+
RsXStateManagerInjectionTokens.IStateManager
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
const stateContext = new StateContext();
|
|
621
|
+
|
|
622
|
+
const changeSubscription = stateManager.changed.subscribe((change: IStateChange) => {
|
|
623
|
+
printValue(change.newValue);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
try {
|
|
627
|
+
// Observe property `b` recursively.
|
|
628
|
+
// Otherwise, only assigning a new value to stateContext.b would emit a change event.
|
|
629
|
+
// This will emit a change event with the initial (current) value.
|
|
630
|
+
console.log('Initial value:');
|
|
631
|
+
stateManager.watchState(stateContext, 'b', truePredicate);
|
|
632
|
+
|
|
633
|
+
console.log('\nReplacing stateContext.b.nested.nested will emit a change event');
|
|
634
|
+
console.log('Changed value:');
|
|
635
|
+
|
|
636
|
+
stateContext.b.nested.nested = {
|
|
637
|
+
a: -30,
|
|
638
|
+
nested: {
|
|
639
|
+
a: -40
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
console.log(`Latest value:`);
|
|
644
|
+
printValue(stateManager.getState(stateContext, 'b'));
|
|
645
|
+
|
|
646
|
+
} finally {
|
|
647
|
+
changeSubscription.unsubscribe();
|
|
648
|
+
// Always release the state when it is no longer needed.
|
|
649
|
+
stateManager.releaseState(stateContext, 'b', truePredicate);
|
|
650
|
+
}
|
|
651
|
+
})();
|
|
652
|
+
```
|
|
653
|
+
**Output:**
|
|
654
|
+
```console
|
|
655
|
+
Running demo: /Users/robertsanders/projects/rs-x/demo/src/rs-x-state-manager/register-property.ts
|
|
656
|
+
Initial value:
|
|
657
|
+
{
|
|
658
|
+
a: 10,
|
|
659
|
+
nested: {
|
|
660
|
+
a: 20,
|
|
661
|
+
nested: {
|
|
662
|
+
a: 30,
|
|
663
|
+
nested: {
|
|
664
|
+
a: 40
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
Replacing stateContext.b.nested.nested will emit a change event
|
|
671
|
+
Changed value:
|
|
672
|
+
{
|
|
673
|
+
a: 10,
|
|
674
|
+
nested: {
|
|
675
|
+
a: 20,
|
|
676
|
+
nested: {
|
|
677
|
+
a: -30,
|
|
678
|
+
nested: {
|
|
679
|
+
a: -40
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
Latest value:
|
|
685
|
+
{
|
|
686
|
+
a: 10,
|
|
687
|
+
nested: {
|
|
688
|
+
a: 20,
|
|
689
|
+
nested: {
|
|
690
|
+
a: -30,
|
|
691
|
+
nested: {
|
|
692
|
+
a: -40
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### Date
|
|
700
|
+
**Example**
|
|
701
|
+
```ts
|
|
702
|
+
import { InjectionContainer, truePredicate, utCDate } from '@rs-x/core';
|
|
703
|
+
import {
|
|
704
|
+
IProxyRegistry,
|
|
705
|
+
IStateChange,
|
|
706
|
+
IStateManager,
|
|
707
|
+
RsXStateManagerInjectionTokens,
|
|
708
|
+
RsXStateManagerModule
|
|
709
|
+
} from '@rs-x/state-manager';
|
|
710
|
+
|
|
711
|
+
// Load the state manager module into the injection container
|
|
712
|
+
InjectionContainer.load(RsXStateManagerModule);
|
|
713
|
+
|
|
714
|
+
function watchDate(stateManager: IStateManager) {
|
|
715
|
+
console.log('\n******************************************');
|
|
716
|
+
console.log('* Watching date');
|
|
717
|
+
console.log('******************************************\n');
|
|
718
|
+
|
|
719
|
+
const stateContext = {
|
|
720
|
+
date: utCDate(2021, 2, 5)
|
|
721
|
+
};
|
|
722
|
+
const changeSubscription = stateManager.changed.subscribe((change: IStateChange) => {
|
|
723
|
+
console.log(`${change.key}: ${(change.newValue as Date).toUTCString()}`);
|
|
724
|
+
});
|
|
725
|
+
try {
|
|
726
|
+
console.log('Initial value:');
|
|
727
|
+
stateManager.watchState(stateContext, 'date', truePredicate);
|
|
728
|
+
|
|
729
|
+
console.log('Changed value:');
|
|
730
|
+
stateContext.date.setFullYear(2023);
|
|
731
|
+
|
|
732
|
+
console.log('Set value:');
|
|
733
|
+
stateContext.date = new Date(2024, 5, 6);
|
|
734
|
+
|
|
735
|
+
console.log('Latest value:');
|
|
736
|
+
console.log(stateManager.getState<Date>(stateContext, 'date').toUTCString());
|
|
737
|
+
} finally {
|
|
738
|
+
changeSubscription.unsubscribe();
|
|
739
|
+
// Always release the state when it is no longer needed.
|
|
740
|
+
stateManager.releaseState(stateContext, 'date', truePredicate);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function watchDateProperty(stateManager: IStateManager) {
|
|
745
|
+
console.log('\n******************************************');
|
|
746
|
+
console.log('* Watching year');
|
|
747
|
+
console.log('******************************************\n');
|
|
748
|
+
const date = utCDate(2021, 2, 5);
|
|
749
|
+
const changeSubscription = stateManager.changed.subscribe((change: IStateChange) => {
|
|
750
|
+
console.log(change.newValue);
|
|
751
|
+
});
|
|
752
|
+
try {
|
|
753
|
+
// This will emit a change event with the initial (current) value.
|
|
754
|
+
console.log('Initial value:');
|
|
755
|
+
stateManager.watchState(date, 'year');
|
|
756
|
+
|
|
757
|
+
const proxyRegister: IProxyRegistry = InjectionContainer.get(RsXStateManagerInjectionTokens.IProxyRegistry);
|
|
758
|
+
const dateProxy = proxyRegister.getProxy<Date>(date);
|
|
759
|
+
console.log('Changed value:');
|
|
760
|
+
dateProxy.setFullYear(2023);
|
|
761
|
+
|
|
762
|
+
console.log('Latest value:');
|
|
763
|
+
console.log(stateManager.getState(date, 'year'));
|
|
764
|
+
|
|
765
|
+
} finally {
|
|
766
|
+
changeSubscription.unsubscribe();
|
|
767
|
+
stateManager.releaseState(date, 'year');
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
export const run = (() => {
|
|
772
|
+
const stateManager: IStateManager = InjectionContainer.get(
|
|
773
|
+
RsXStateManagerInjectionTokens.IStateManager
|
|
774
|
+
);
|
|
775
|
+
watchDate(stateManager);
|
|
776
|
+
watchDateProperty(stateManager);
|
|
777
|
+
})();
|
|
778
|
+
```
|
|
779
|
+
**Output:**
|
|
780
|
+
```console
|
|
781
|
+
Running demo: demo/src/rs-x-state-manager/register-date.ts
|
|
782
|
+
|
|
783
|
+
******************************************
|
|
784
|
+
* Watching date
|
|
785
|
+
******************************************
|
|
786
|
+
|
|
787
|
+
Initial value:
|
|
788
|
+
date: Fri, 05 Mar 2021 00:00:00 GMT
|
|
789
|
+
Changed value:
|
|
790
|
+
date: Sun, 05 Mar 2023 00:00:00 GMT
|
|
791
|
+
Set value:
|
|
792
|
+
date: Thu, 06 Jun 2024 00:00:00 GMT
|
|
793
|
+
Latest value:
|
|
794
|
+
Thu, 06 Jun 2024 00:00:00 GMT
|
|
795
|
+
|
|
796
|
+
******************************************
|
|
797
|
+
* Watching year
|
|
798
|
+
******************************************
|
|
799
|
+
|
|
800
|
+
Initial value:
|
|
801
|
+
2021
|
|
802
|
+
Changed value:
|
|
803
|
+
2023
|
|
804
|
+
Latest value:
|
|
805
|
+
2023
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
### Array
|
|
809
|
+
**Example**
|
|
810
|
+
```ts
|
|
811
|
+
import { InjectionContainer, printValue, truePredicate } from '@rs-x/core';
|
|
812
|
+
import {
|
|
813
|
+
IStateChange,
|
|
814
|
+
IStateManager,
|
|
815
|
+
RsXStateManagerInjectionTokens,
|
|
816
|
+
RsXStateManagerModule
|
|
817
|
+
} from '@rs-x/state-manager';
|
|
818
|
+
|
|
819
|
+
// Load the state manager module into the injection container
|
|
820
|
+
InjectionContainer.load(RsXStateManagerModule);
|
|
821
|
+
|
|
822
|
+
export const run = (() => {
|
|
823
|
+
const stateManager: IStateManager = InjectionContainer.get(
|
|
824
|
+
RsXStateManagerInjectionTokens.IStateManager
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
const stateContext = {
|
|
828
|
+
array: [
|
|
829
|
+
[1, 2],
|
|
830
|
+
[3, 4]
|
|
831
|
+
]
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
const changeSubscription = stateManager.changed.subscribe((change: IStateChange) => {
|
|
835
|
+
printValue(change.newValue);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
try {
|
|
839
|
+
// This will emit a change event with the initial (current) value.
|
|
840
|
+
console.log('Initial value:');
|
|
841
|
+
stateManager.watchState(stateContext, 'array', truePredicate);
|
|
842
|
+
|
|
843
|
+
console.log('Changed value:');
|
|
844
|
+
stateContext.array[1].push(5);
|
|
845
|
+
|
|
846
|
+
console.log('Latest value:');
|
|
847
|
+
printValue(stateManager.getState(stateContext, 'array'));
|
|
848
|
+
|
|
849
|
+
} finally {
|
|
850
|
+
changeSubscription.unsubscribe();
|
|
851
|
+
// Always release the state when it is no longer needed.
|
|
852
|
+
stateManager.releaseState(stateContext, 'array', truePredicate);
|
|
853
|
+
}
|
|
854
|
+
})();
|
|
855
|
+
```
|
|
856
|
+
**Output:**
|
|
857
|
+
```console
|
|
858
|
+
Running demo: /Users/robertsanders/projects/rs-x/demo/src/rs-x-state-manager/register-array.ts
|
|
859
|
+
Initial value:
|
|
860
|
+
[
|
|
861
|
+
[
|
|
862
|
+
1,
|
|
863
|
+
2
|
|
864
|
+
],
|
|
865
|
+
[
|
|
866
|
+
3,
|
|
867
|
+
4
|
|
868
|
+
]
|
|
869
|
+
]
|
|
870
|
+
Changed value:
|
|
871
|
+
[
|
|
872
|
+
[
|
|
873
|
+
1,
|
|
874
|
+
2
|
|
875
|
+
],
|
|
876
|
+
[
|
|
877
|
+
3,
|
|
878
|
+
4,
|
|
879
|
+
5
|
|
880
|
+
]
|
|
881
|
+
]
|
|
882
|
+
Latest value:
|
|
883
|
+
[
|
|
884
|
+
[
|
|
885
|
+
1,
|
|
886
|
+
2
|
|
887
|
+
],
|
|
888
|
+
[
|
|
889
|
+
3,
|
|
890
|
+
4,
|
|
891
|
+
5
|
|
892
|
+
]
|
|
893
|
+
]
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
### Map
|
|
897
|
+
**Example**
|
|
898
|
+
```ts
|
|
899
|
+
import { InjectionContainer, printValue, truePredicate } from '@rs-x/core';
|
|
900
|
+
import {
|
|
901
|
+
IStateChange,
|
|
902
|
+
IStateManager,
|
|
903
|
+
RsXStateManagerInjectionTokens,
|
|
904
|
+
RsXStateManagerModule
|
|
905
|
+
} from '@rs-x/state-manager';
|
|
906
|
+
|
|
907
|
+
// Load the state manager module into the injection container
|
|
908
|
+
InjectionContainer.load(RsXStateManagerModule);
|
|
909
|
+
|
|
910
|
+
const stateManager: IStateManager = InjectionContainer.get(
|
|
911
|
+
RsXStateManagerInjectionTokens.IStateManager
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
export const run = (() => {
|
|
915
|
+
const stateContext = {
|
|
916
|
+
map: new Map([
|
|
917
|
+
['a', [1, 2]],
|
|
918
|
+
['b', [3, 4]]
|
|
919
|
+
])
|
|
920
|
+
};
|
|
921
|
+
|
|
922
|
+
const changeSubscription = stateManager.changed.subscribe((change: IStateChange) => {
|
|
923
|
+
printValue(change.newValue);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
try {
|
|
927
|
+
// This will emit a change event with the initial (current) value.
|
|
928
|
+
console.log('Initial value:');
|
|
929
|
+
stateManager.watchState(stateContext, 'map', truePredicate);
|
|
930
|
+
|
|
931
|
+
console.log('Changed value:');
|
|
932
|
+
stateContext.map.get('b').push(5);
|
|
933
|
+
|
|
934
|
+
console.log('Latest value:');
|
|
935
|
+
printValue(stateManager.getState(stateContext, 'map'))
|
|
936
|
+
|
|
937
|
+
} finally {
|
|
938
|
+
changeSubscription.unsubscribe();
|
|
939
|
+
// Always release the state when it is no longer needed.
|
|
940
|
+
stateManager.releaseState(stateContext, 'array', truePredicate);
|
|
941
|
+
}
|
|
942
|
+
})();
|
|
943
|
+
```
|
|
944
|
+
**Output:**
|
|
945
|
+
```console
|
|
946
|
+
Running demo: /Users/robertsanders/projects/rs-x/demo/src/rs-x-state-manager/register-map.ts
|
|
947
|
+
Initial value:
|
|
948
|
+
[
|
|
949
|
+
[
|
|
950
|
+
a,
|
|
951
|
+
[
|
|
952
|
+
1,
|
|
953
|
+
2
|
|
954
|
+
]
|
|
955
|
+
],
|
|
956
|
+
[
|
|
957
|
+
b,
|
|
958
|
+
[
|
|
959
|
+
3,
|
|
960
|
+
4
|
|
961
|
+
]
|
|
962
|
+
]
|
|
963
|
+
]
|
|
964
|
+
Changed value:
|
|
965
|
+
[
|
|
966
|
+
[
|
|
967
|
+
a,
|
|
968
|
+
[
|
|
969
|
+
1,
|
|
970
|
+
2
|
|
971
|
+
]
|
|
972
|
+
],
|
|
973
|
+
[
|
|
974
|
+
b,
|
|
975
|
+
[
|
|
976
|
+
3,
|
|
977
|
+
4,
|
|
978
|
+
5
|
|
979
|
+
]
|
|
980
|
+
]
|
|
981
|
+
]
|
|
982
|
+
Latest value:
|
|
983
|
+
[
|
|
984
|
+
[
|
|
985
|
+
a,
|
|
986
|
+
[
|
|
987
|
+
1,
|
|
988
|
+
2
|
|
989
|
+
]
|
|
990
|
+
],
|
|
991
|
+
[
|
|
992
|
+
b,
|
|
993
|
+
[
|
|
994
|
+
3,
|
|
995
|
+
4,
|
|
996
|
+
5
|
|
997
|
+
]
|
|
998
|
+
]
|
|
999
|
+
]
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
### Set
|
|
1003
|
+
**Example**
|
|
1004
|
+
```ts
|
|
1005
|
+
import { InjectionContainer, printValue, truePredicate } from '@rs-x/core';
|
|
1006
|
+
import {
|
|
1007
|
+
IProxyRegistry,
|
|
1008
|
+
IStateChange,
|
|
1009
|
+
IStateManager,
|
|
1010
|
+
RsXStateManagerInjectionTokens,
|
|
1011
|
+
RsXStateManagerModule
|
|
1012
|
+
} from '@rs-x/state-manager';
|
|
1013
|
+
|
|
1014
|
+
// Load the state manager module into the injection container
|
|
1015
|
+
InjectionContainer.load(RsXStateManagerModule);
|
|
1016
|
+
|
|
1017
|
+
export const run = (() => {
|
|
1018
|
+
const stateManager: IStateManager = InjectionContainer.get(
|
|
1019
|
+
RsXStateManagerInjectionTokens.IStateManager
|
|
1020
|
+
);
|
|
1021
|
+
const item1 = [1, 2];
|
|
1022
|
+
const item2 = [3, 4];
|
|
1023
|
+
const stateContext = {
|
|
1024
|
+
set: new Set([item1, item2])
|
|
1025
|
+
};
|
|
1026
|
+
const changeSubscription = stateManager.changed.subscribe((change: IStateChange) => {
|
|
1027
|
+
printValue(change.newValue);
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
try {
|
|
1031
|
+
// This will emit a change event with the initial (current) value.
|
|
1032
|
+
console.log('Initial value:');
|
|
1033
|
+
stateManager.watchState(stateContext, 'set', truePredicate);
|
|
1034
|
+
|
|
1035
|
+
console.log('Changed value:');
|
|
1036
|
+
const proxyRegister: IProxyRegistry = InjectionContainer.get(RsXStateManagerInjectionTokens.IProxyRegistry);
|
|
1037
|
+
proxyRegister.getProxy<number[]>(item2).push(5);
|
|
1038
|
+
|
|
1039
|
+
console.log('Latest value:');
|
|
1040
|
+
printValue(stateManager.getState(stateContext, 'set'));
|
|
1041
|
+
|
|
1042
|
+
} finally {
|
|
1043
|
+
changeSubscription.unsubscribe();
|
|
1044
|
+
// Always release the state when it is no longer needed.
|
|
1045
|
+
stateManager.releaseState(stateContext, 'set', truePredicate);
|
|
1046
|
+
}
|
|
1047
|
+
})();
|
|
1048
|
+
```
|
|
1049
|
+
**Output:**
|
|
1050
|
+
```console
|
|
1051
|
+
Running demo: /Users/robertsanders/projects/rs-x/demo/src/rs-x-state-manager/register-set.ts
|
|
1052
|
+
Initial value:
|
|
1053
|
+
[
|
|
1054
|
+
[
|
|
1055
|
+
1,
|
|
1056
|
+
2
|
|
1057
|
+
],
|
|
1058
|
+
[
|
|
1059
|
+
3,
|
|
1060
|
+
4
|
|
1061
|
+
]
|
|
1062
|
+
]
|
|
1063
|
+
Changed value:
|
|
1064
|
+
[
|
|
1065
|
+
[
|
|
1066
|
+
1,
|
|
1067
|
+
2
|
|
1068
|
+
],
|
|
1069
|
+
[
|
|
1070
|
+
3,
|
|
1071
|
+
4,
|
|
1072
|
+
5
|
|
1073
|
+
]
|
|
1074
|
+
]
|
|
1075
|
+
Latest value:
|
|
1076
|
+
[
|
|
1077
|
+
[
|
|
1078
|
+
1,
|
|
1079
|
+
2
|
|
1080
|
+
],
|
|
1081
|
+
[
|
|
1082
|
+
3,
|
|
1083
|
+
4,
|
|
1084
|
+
5
|
|
1085
|
+
]
|
|
1086
|
+
]
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
### Promise
|
|
1090
|
+
**Example**
|
|
1091
|
+
```ts
|
|
1092
|
+
import { InjectionContainer, WaitForEvent } from '@rs-x/core';
|
|
1093
|
+
import {
|
|
1094
|
+
IStateChange,
|
|
1095
|
+
IStateManager,
|
|
1096
|
+
RsXStateManagerInjectionTokens,
|
|
1097
|
+
RsXStateManagerModule
|
|
1098
|
+
} from '@rs-x/state-manager';
|
|
1099
|
+
|
|
1100
|
+
// Load the state manager module into the injection container
|
|
1101
|
+
InjectionContainer.load(RsXStateManagerModule);
|
|
1102
|
+
|
|
1103
|
+
export const run = (async () => {
|
|
1104
|
+
const stateManager: IStateManager = InjectionContainer.get(
|
|
1105
|
+
RsXStateManagerInjectionTokens.IStateManager
|
|
1106
|
+
);
|
|
1107
|
+
|
|
1108
|
+
const stateContext = {
|
|
1109
|
+
promise: Promise.resolve(10)
|
|
1110
|
+
};
|
|
1111
|
+
const changeSubscription = stateManager.changed.subscribe((change: IStateChange) => {
|
|
1112
|
+
console.log(change.newValue);
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
try {
|
|
1116
|
+
await new WaitForEvent(stateManager, 'changed').wait(() => {
|
|
1117
|
+
// This will emit a change event with the initial (current) value.
|
|
1118
|
+
console.log('Initial value:');
|
|
1119
|
+
stateManager.watchState(stateContext, 'promise');
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
await new WaitForEvent(stateManager, 'changed').wait(() => {
|
|
1123
|
+
console.log('Changed value:');
|
|
1124
|
+
let resolveHandler: (value: number) => void;
|
|
1125
|
+
stateContext.promise = new Promise((resolve) => { resolveHandler = resolve; });
|
|
1126
|
+
resolveHandler(30);
|
|
1127
|
+
});
|
|
1128
|
+
|
|
1129
|
+
console.log(`Latest value: ${stateManager.getState(stateContext, 'promise')}`);
|
|
1130
|
+
} finally {
|
|
1131
|
+
changeSubscription.unsubscribe();
|
|
1132
|
+
// Always release the state when it is no longer needed.
|
|
1133
|
+
stateManager.releaseState(stateContext, 'promise');
|
|
1134
|
+
}
|
|
1135
|
+
})();
|
|
1136
|
+
```
|
|
1137
|
+
**Output:**
|
|
1138
|
+
```console
|
|
1139
|
+
Running demo: /Users/robertsanders/projects/rs-x/demo/src/rs-x-state-manager/register-promise.ts
|
|
1140
|
+
Initial value:
|
|
1141
|
+
10
|
|
1142
|
+
Changed value:
|
|
1143
|
+
30
|
|
1144
|
+
Latest value: 30
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
### Observable
|
|
1148
|
+
**Example**
|
|
1149
|
+
```ts
|
|
1150
|
+
import { InjectionContainer, WaitForEvent } from '@rs-x/core';
|
|
1151
|
+
import {
|
|
1152
|
+
IStateChange,
|
|
1153
|
+
IStateManager,
|
|
1154
|
+
RsXStateManagerInjectionTokens,
|
|
1155
|
+
RsXStateManagerModule
|
|
1156
|
+
} from '@rs-x/state-manager';
|
|
1157
|
+
import { of, Subject } from 'rxjs';
|
|
1158
|
+
|
|
1159
|
+
// Load the state manager module into the injection container
|
|
1160
|
+
InjectionContainer.load(RsXStateManagerModule);
|
|
1161
|
+
|
|
1162
|
+
export const run = (async () => {
|
|
1163
|
+
const stateManager: IStateManager = InjectionContainer.get(
|
|
1164
|
+
RsXStateManagerInjectionTokens.IStateManager
|
|
1165
|
+
);
|
|
1166
|
+
|
|
1167
|
+
const stateContext = {
|
|
1168
|
+
observable: of(10)
|
|
1169
|
+
};
|
|
1170
|
+
|
|
1171
|
+
const changeSubscription = stateManager.changed.subscribe((change: IStateChange) => {
|
|
1172
|
+
console.log(change.newValue);
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
try {
|
|
1176
|
+
// We need to wait here until the event is emitted,
|
|
1177
|
+
// otherwise the demo will exit before the change event occurs.
|
|
1178
|
+
await new WaitForEvent(stateManager, 'changed').wait(() => {
|
|
1179
|
+
// This will emit a change event with the initial (current) value.
|
|
1180
|
+
console.log('Initial value:');
|
|
1181
|
+
stateManager.watchState(stateContext, 'observable');
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
await new WaitForEvent(stateManager, 'changed').wait(() => {
|
|
1185
|
+
console.log('Changed value:');
|
|
1186
|
+
const subject = new Subject<number>();
|
|
1187
|
+
stateContext.observable = subject
|
|
1188
|
+
subject.next(30);
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
console.log(`Latest value: ${stateManager.getState(stateContext, 'observable')}`);
|
|
1192
|
+
|
|
1193
|
+
} finally {
|
|
1194
|
+
changeSubscription.unsubscribe();
|
|
1195
|
+
// Always release the state when it is no longer needed.
|
|
1196
|
+
stateManager.releaseState(stateContext, 'observable');
|
|
1197
|
+
}
|
|
1198
|
+
})();
|
|
1199
|
+
```
|
|
1200
|
+
**Output:**
|
|
1201
|
+
```console
|
|
1202
|
+
Running demo: /Users/robertsanders/projects/rs-x/demo/src/rs-x-state-manager/register-observable.ts
|
|
1203
|
+
Initial value:
|
|
1204
|
+
10
|
|
1205
|
+
Changed value:
|
|
1206
|
+
30
|
|
1207
|
+
Latest value: 30
|
|
1208
|
+
```
|
|
1209
|
+
|
|
1210
|
+
### Custom type
|
|
1211
|
+
1. Create an accessor to retrieve index values on your type.
|
|
1212
|
+
2. Create a factory to create an observer for your data type.
|
|
1213
|
+
3. Create a factory to create an observer for an index on your data instance.
|
|
1214
|
+
|
|
1215
|
+
The following example demonstrates adding support for a custom `TextDocument` class:
|
|
1216
|
+
|
|
1217
|
+
**Example**
|
|
1218
|
+
```ts
|
|
1219
|
+
import {
|
|
1220
|
+
ContainerModule,
|
|
1221
|
+
defaultIndexValueAccessorList,
|
|
1222
|
+
IDisposableOwner,
|
|
1223
|
+
IErrorLog,
|
|
1224
|
+
IGuidFactory,
|
|
1225
|
+
IIndexValueAccessor,
|
|
1226
|
+
Inject,
|
|
1227
|
+
Injectable,
|
|
1228
|
+
InjectionContainer,
|
|
1229
|
+
IPropertyChange,
|
|
1230
|
+
overrideMultiInjectServices,
|
|
1231
|
+
RsXCoreInjectionTokens,
|
|
1232
|
+
SingletonFactory,
|
|
1233
|
+
truePredicate
|
|
1234
|
+
} from '@rs-x/core';
|
|
1235
|
+
import {
|
|
1236
|
+
AbstractObserver,
|
|
1237
|
+
defaultObjectObserverProxyPairFactoryList,
|
|
1238
|
+
defaultPropertyObserverProxyPairFactoryList,
|
|
1239
|
+
IIndexObserverInfo,
|
|
1240
|
+
IndexObserverProxyPairFactory,
|
|
1241
|
+
IObjectObserverProxyPairFactory,
|
|
1242
|
+
IObjectObserverProxyPairManager,
|
|
1243
|
+
IObserverProxyPair,
|
|
1244
|
+
IPropertyInfo,
|
|
1245
|
+
IProxyRegistry,
|
|
1246
|
+
IProxyTarget,
|
|
1247
|
+
IStateChange,
|
|
1248
|
+
IStateManager,
|
|
1249
|
+
RsXStateManagerInjectionTokens,
|
|
1250
|
+
RsXStateManagerModule
|
|
1251
|
+
} from '@rs-x/state-manager';
|
|
1252
|
+
import { ReplaySubject, Subscription } from 'rxjs';
|
|
1253
|
+
|
|
1254
|
+
const MyInjectTokens = {
|
|
1255
|
+
TextDocumentObserverManager: Symbol('TextDocumentObserverManager'),
|
|
1256
|
+
TextDocumenIndexObserverManager: Symbol('TextDocumenIndexObserverManager'),
|
|
1257
|
+
TextDocumentIndexAccessor: Symbol('TextDocumentIndexAccessor'),
|
|
1258
|
+
TextDocumentObserverProxyPairFactory: Symbol('TextDocumentObserverProxyPairFactory'),
|
|
1259
|
+
TextDocumentInxdexObserverProxyPairFactory: Symbol('TextDocumentInxdexObserverProxyPairFactory')
|
|
1260
|
+
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
class IndexForTextDocumentxObserverManager
|
|
1264
|
+
extends SingletonFactory<
|
|
1265
|
+
number,
|
|
1266
|
+
IIndexObserverInfo<ITextDocumentIndex>,
|
|
1267
|
+
TextDocumentIndexObserver> {
|
|
1268
|
+
constructor(
|
|
1269
|
+
private readonly _textDocument: TextDocument,
|
|
1270
|
+
private readonly _textDocumentObserverManager: TextDocumentObserverManager,
|
|
1271
|
+
private readonly releaseOwner: () => void
|
|
1272
|
+
) {
|
|
1273
|
+
super();
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
public override getId(indexObserverInfo: IIndexObserverInfo<ITextDocumentIndex>): number {
|
|
1277
|
+
return this.createId(indexObserverInfo);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
protected override createInstance(indexObserverInfo: IIndexObserverInfo<ITextDocumentIndex>, id: number): TextDocumentIndexObserver {
|
|
1281
|
+
const textDocumentObserver = this._textDocumentObserverManager.create(this._textDocument).instance;
|
|
1282
|
+
return new TextDocumentIndexObserver(
|
|
1283
|
+
{
|
|
1284
|
+
canDispose: () => this.getReferenceCount(id) === 1,
|
|
1285
|
+
release: () => {
|
|
1286
|
+
textDocumentObserver.dispose();
|
|
1287
|
+
this.release(id);
|
|
1288
|
+
},
|
|
1289
|
+
},
|
|
1290
|
+
textDocumentObserver, indexObserverInfo.index
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
protected override createId(indexObserverInfo: IIndexObserverInfo<ITextDocumentIndex>): number {
|
|
1295
|
+
// Using Cantor pairing to create a unique id from page and line index
|
|
1296
|
+
const { pageIndex, lineIndex } = indexObserverInfo.index;
|
|
1297
|
+
return ((pageIndex + lineIndex) * (pageIndex + lineIndex + 1)) / 2 + lineIndex;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
protected override onReleased(): void {
|
|
1301
|
+
if (this.isEmpty) {
|
|
1302
|
+
this.releaseOwner();
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// We want to ensure that for the same TextDocument we always have the same observer
|
|
1308
|
+
@Injectable()
|
|
1309
|
+
class TextDocumentObserverManager extends SingletonFactory<TextDocument, TextDocument, TextDocumentObserver> {
|
|
1310
|
+
constructor(
|
|
1311
|
+
@Inject(RsXStateManagerInjectionTokens.IProxyRegistry)
|
|
1312
|
+
private readonly _proxyRegister: IProxyRegistry) {
|
|
1313
|
+
super();
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
public override getId(textDocument: TextDocument): TextDocument {
|
|
1317
|
+
return textDocument;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
protected override createInstance(textDocument: TextDocument, id: TextDocument): TextDocumentObserver {
|
|
1321
|
+
return new TextDocumentObserver(
|
|
1322
|
+
textDocument,
|
|
1323
|
+
this._proxyRegister,
|
|
1324
|
+
{
|
|
1325
|
+
canDispose: () => this.getReferenceCount(id) === 1,
|
|
1326
|
+
release: () => this.release(id)
|
|
1327
|
+
}
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
protected override createId(textDocument: TextDocument): TextDocument {
|
|
1332
|
+
return textDocument;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// We want to ensure we create only one index-manager per TextDocument
|
|
1337
|
+
@Injectable()
|
|
1338
|
+
export class TextDocumenIndexObserverManager
|
|
1339
|
+
extends SingletonFactory<
|
|
1340
|
+
TextDocument,
|
|
1341
|
+
TextDocument,
|
|
1342
|
+
IndexForTextDocumentxObserverManager
|
|
1343
|
+
> {
|
|
1344
|
+
constructor(
|
|
1345
|
+
@Inject(MyInjectTokens.TextDocumentObserverManager)
|
|
1346
|
+
private readonly _textDocumentObserverManager: TextDocumentObserverManager,
|
|
1347
|
+
) {
|
|
1348
|
+
super();
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
public override getId(textDocument: TextDocument): TextDocument {
|
|
1352
|
+
return textDocument;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
protected override createId(textDocument: TextDocument): TextDocument {
|
|
1356
|
+
return textDocument;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
protected override createInstance(
|
|
1360
|
+
textDocument: TextDocument
|
|
1361
|
+
): IndexForTextDocumentxObserverManager {
|
|
1362
|
+
|
|
1363
|
+
return new IndexForTextDocumentxObserverManager(textDocument, this._textDocumentObserverManager, () => this.release(textDocument));
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
protected override releaseInstance(
|
|
1367
|
+
indexForTextDocumentxObserverManager: IndexForTextDocumentxObserverManager
|
|
1368
|
+
): void {
|
|
1369
|
+
indexForTextDocumentxObserverManager.dispose();
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
@Injectable()
|
|
1377
|
+
export class TextDocumentIndexAccessor implements IIndexValueAccessor<TextDocument, ITextDocumentIndex> {
|
|
1378
|
+
public readonly priority: 200;
|
|
1379
|
+
|
|
1380
|
+
public hasValue(context: TextDocument, index: ITextDocumentIndex): boolean {
|
|
1381
|
+
return context.getLine(index) !== undefined;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// We don’t have any properties that can be iterated through.
|
|
1385
|
+
public getIndexes(_context: TextDocument, _index?: ITextDocumentIndex): IterableIterator<ITextDocumentIndex> {
|
|
1386
|
+
return [].values();
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// Indicate whether the value is async. For example when the value is a Promise
|
|
1390
|
+
public isAsync(_context: TextDocument, _index: ITextDocumentIndex): boolean {
|
|
1391
|
+
return false;
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Here it is the same as getValue.
|
|
1395
|
+
// For example, for a Promise accessor getValue returns the promise
|
|
1396
|
+
// and getResolvedValue returns the resolved promise value
|
|
1397
|
+
public getResolvedValue(context: TextDocument, index: ITextDocumentIndex): string {
|
|
1398
|
+
return this.getValue(context, index);
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
public getValue(context: TextDocument, index: ITextDocumentIndex): string {
|
|
1402
|
+
return context.getLine(index);
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
public setValue(context: TextDocument, index: ITextDocumentIndex, value: string): void {
|
|
1406
|
+
context.setLine(index, value);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
public applies(context: unknown, _index: ITextDocumentIndex): boolean {
|
|
1410
|
+
return context instanceof TextDocument;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
@Injectable()
|
|
1415
|
+
export class TextDocumentInxdexObserverProxyPairFactory extends IndexObserverProxyPairFactory<TextDocument, unknown> {
|
|
1416
|
+
constructor(
|
|
1417
|
+
@Inject(RsXStateManagerInjectionTokens.IObjectObserverProxyPairManager)
|
|
1418
|
+
objectObserverManager: IObjectObserverProxyPairManager,
|
|
1419
|
+
@Inject( MyInjectTokens.TextDocumenIndexObserverManager)
|
|
1420
|
+
textDocumenIndexObserverManager: TextDocumenIndexObserverManager,
|
|
1421
|
+
@Inject(RsXCoreInjectionTokens.IErrorLog)
|
|
1422
|
+
errorLog: IErrorLog,
|
|
1423
|
+
@Inject(RsXCoreInjectionTokens.IGuidFactory)
|
|
1424
|
+
guidFactory: IGuidFactory,
|
|
1425
|
+
@Inject(RsXCoreInjectionTokens.IIndexValueAccessor)
|
|
1426
|
+
indexValueAccessor: IIndexValueAccessor,
|
|
1427
|
+
@Inject(RsXStateManagerInjectionTokens.IProxyRegistry)
|
|
1428
|
+
proxyRegister: IProxyRegistry
|
|
1429
|
+
) {
|
|
1430
|
+
super(
|
|
1431
|
+
objectObserverManager,
|
|
1432
|
+
textDocumenIndexObserverManager,
|
|
1433
|
+
errorLog,
|
|
1434
|
+
guidFactory,
|
|
1435
|
+
indexValueAccessor,
|
|
1436
|
+
proxyRegister
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
public applies(object: unknown, propertyInfo: IPropertyInfo): boolean {
|
|
1441
|
+
const documentKey = propertyInfo.key as ITextDocumentIndex;
|
|
1442
|
+
return object instanceof TextDocument && documentKey?.lineIndex >= 0 && documentKey?.pageIndex >= 0;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
@Injectable()
|
|
1447
|
+
export class TextDocumentObserverProxyPairFactory implements IObjectObserverProxyPairFactory {
|
|
1448
|
+
public readonly priority = 100;
|
|
1449
|
+
|
|
1450
|
+
constructor(
|
|
1451
|
+
@Inject( MyInjectTokens.TextDocumentObserverManager)
|
|
1452
|
+
private readonly _textDocumentObserverManager: TextDocumentObserverManager) { }
|
|
1453
|
+
|
|
1454
|
+
public create(
|
|
1455
|
+
_: IDisposableOwner,
|
|
1456
|
+
proxyTarget: IProxyTarget<TextDocument>): IObserverProxyPair<TextDocument> {
|
|
1457
|
+
|
|
1458
|
+
const observer = this._textDocumentObserverManager.create(proxyTarget.target).instance;
|
|
1459
|
+
return {
|
|
1460
|
+
observer,
|
|
1461
|
+
proxy: observer.target as TextDocument,
|
|
1462
|
+
proxyTarget: proxyTarget.target,
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
public applies(object: unknown): boolean {
|
|
1467
|
+
return object instanceof TextDocument;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
interface ITextDocumentIndex {
|
|
1472
|
+
pageIndex: number;
|
|
1473
|
+
lineIndex: number;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
class TextDocument {
|
|
1477
|
+
private readonly _pages = new Map<number, Map<number, string>>();
|
|
1478
|
+
constructor(
|
|
1479
|
+
pages?: string[][],
|
|
1480
|
+
) {
|
|
1481
|
+
|
|
1482
|
+
pages?.forEach((page, pageIndex) => {
|
|
1483
|
+
const pageText = new Map<number, string>();
|
|
1484
|
+
|
|
1485
|
+
this._pages.set(pageIndex, pageText);
|
|
1486
|
+
page.forEach((lineText, lineIndex) => {
|
|
1487
|
+
pageText.set(lineIndex, lineText);
|
|
1488
|
+
});
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
public toString(): string {
|
|
1493
|
+
const pages: string[] = [];
|
|
1494
|
+
|
|
1495
|
+
// Sort pages by pageIndex
|
|
1496
|
+
const sortedPageIndexes = Array.from(this._pages.keys()).sort((a, b) => a - b);
|
|
1497
|
+
|
|
1498
|
+
for (const pageIndex of sortedPageIndexes) {
|
|
1499
|
+
const page = this._pages.get(pageIndex);
|
|
1500
|
+
if (!page) {
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// Sort lines by lineIndex
|
|
1505
|
+
const sortedLineIndexes = Array.from(page.keys()).sort((a, b) => a - b);
|
|
1506
|
+
|
|
1507
|
+
const lines = sortedLineIndexes.map(lineIndex => ` ${lineIndex}: ${page.get(lineIndex)}`);
|
|
1508
|
+
pages.push(`Page ${pageIndex}:\n${lines.join('\n')}`);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
return pages.join('\n\n');
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
public setLine(index: ITextDocumentIndex, text: string): void {
|
|
1515
|
+
const { pageIndex, lineIndex } = index;
|
|
1516
|
+
let page = this._pages.get(pageIndex);
|
|
1517
|
+
if (!page) {
|
|
1518
|
+
page = new Map();
|
|
1519
|
+
this._pages.set(pageIndex, page);
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
page.set(lineIndex, text);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
public getLine(index: ITextDocumentIndex): string {
|
|
1526
|
+
const { pageIndex, lineIndex } = index;
|
|
1527
|
+
return this._pages.get(pageIndex)?.get(lineIndex);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
class TextDocumentIndexObserver extends AbstractObserver<TextDocument, string, ITextDocumentIndex> {
|
|
1532
|
+
private readonly _changeSubscription: Subscription;
|
|
1533
|
+
|
|
1534
|
+
constructor(
|
|
1535
|
+
owner: IDisposableOwner,
|
|
1536
|
+
private readonly _observer: TextDocumentObserver,
|
|
1537
|
+
index: ITextDocumentIndex,
|
|
1538
|
+
) {
|
|
1539
|
+
super(owner, _observer.target, _observer.target.getLine(index), new ReplaySubject(), index);
|
|
1540
|
+
this._changeSubscription = _observer.changed.subscribe(this.onChange);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
protected override disposeInternal(): void {
|
|
1544
|
+
this._changeSubscription.unsubscribe();
|
|
1545
|
+
this._observer.dispose();
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
private readonly onChange = (change: IPropertyChange) => {
|
|
1549
|
+
const changeIndex = change.id as ITextDocumentIndex;
|
|
1550
|
+
if (changeIndex.lineIndex === this.id.lineIndex && changeIndex.pageIndex === this.id.pageIndex) {
|
|
1551
|
+
this.emitChange(change);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
class TextDocumentObserver extends AbstractObserver<TextDocument> {
|
|
1557
|
+
constructor(
|
|
1558
|
+
textDocument: TextDocument,
|
|
1559
|
+
private readonly _proxyRegister: IProxyRegistry,
|
|
1560
|
+
owner?: IDisposableOwner,) {
|
|
1561
|
+
super(owner, null, textDocument);
|
|
1562
|
+
|
|
1563
|
+
this.target = new Proxy(textDocument, this);
|
|
1564
|
+
|
|
1565
|
+
// Always register a proxy at the proxy registry
|
|
1566
|
+
// so we can determine if an instance is a proxy or not.
|
|
1567
|
+
this._proxyRegister.register(textDocument, this.target);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
protected override disposeInternal(): void {
|
|
1571
|
+
this._proxyRegister.unregister(this.value);
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
public get(
|
|
1575
|
+
textDocument: TextDocument,
|
|
1576
|
+
property: PropertyKey,
|
|
1577
|
+
receiver: unknown
|
|
1578
|
+
): unknown {
|
|
1579
|
+
if (property == 'setLine') {
|
|
1580
|
+
return (index: ITextDocumentIndex, text: string) => {
|
|
1581
|
+
textDocument.setLine(index, text);
|
|
1582
|
+
this.emitChange({
|
|
1583
|
+
arguments: [],
|
|
1584
|
+
id: index,
|
|
1585
|
+
target: textDocument,
|
|
1586
|
+
newValue: text,
|
|
1587
|
+
});
|
|
1588
|
+
};
|
|
1589
|
+
|
|
1590
|
+
} else {
|
|
1591
|
+
return Reflect.get(textDocument, property, receiver);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
// Load the state manager module into the injection container
|
|
1597
|
+
InjectionContainer.load(RsXStateManagerModule);
|
|
1598
|
+
|
|
1599
|
+
const MyModule = new ContainerModule((options) => {
|
|
1600
|
+
options
|
|
1601
|
+
.bind<TextDocumentObserverManager>(
|
|
1602
|
+
MyInjectTokens.TextDocumentObserverManager
|
|
1603
|
+
)
|
|
1604
|
+
.to(TextDocumentObserverManager)
|
|
1605
|
+
.inSingletonScope();
|
|
1606
|
+
|
|
1607
|
+
options
|
|
1608
|
+
.bind<TextDocumenIndexObserverManager>(
|
|
1609
|
+
MyInjectTokens.TextDocumenIndexObserverManager
|
|
1610
|
+
)
|
|
1611
|
+
.to(TextDocumenIndexObserverManager)
|
|
1612
|
+
.inSingletonScope();
|
|
1613
|
+
|
|
1614
|
+
|
|
1615
|
+
overrideMultiInjectServices(options, RsXCoreInjectionTokens.IIndexValueAccessorList, [
|
|
1616
|
+
{ target: TextDocumentIndexAccessor, token: MyInjectTokens.TextDocumentIndexAccessor },
|
|
1617
|
+
...defaultIndexValueAccessorList
|
|
1618
|
+
]);
|
|
1619
|
+
|
|
1620
|
+
overrideMultiInjectServices(options, RsXStateManagerInjectionTokens.IObjectObserverProxyPairFactoryList, [
|
|
1621
|
+
{ target: TextDocumentObserverProxyPairFactory, token: MyInjectTokens.TextDocumentObserverProxyPairFactory },
|
|
1622
|
+
...defaultObjectObserverProxyPairFactoryList
|
|
1623
|
+
]);
|
|
1624
|
+
|
|
1625
|
+
overrideMultiInjectServices(options,RsXStateManagerInjectionTokens.IPropertyObserverProxyPairFactoryList, [
|
|
1626
|
+
{ target: TextDocumentInxdexObserverProxyPairFactory, token: MyInjectTokens.TextDocumentInxdexObserverProxyPairFactory },
|
|
1627
|
+
...defaultPropertyObserverProxyPairFactoryList
|
|
1628
|
+
]);
|
|
1629
|
+
});
|
|
1630
|
+
|
|
1631
|
+
InjectionContainer.load(MyModule);
|
|
1632
|
+
|
|
1633
|
+
function testMonitorTextDocument(stateManager: IStateManager, stateContext: { myBook: TextDocument }): void {
|
|
1634
|
+
const bookSubscription = stateManager.changed.subscribe(() => {
|
|
1635
|
+
console.log(stateContext.myBook.toString());
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
// We observe the whole book
|
|
1639
|
+
// This will use TextDocumentObserverProxyPairFactory
|
|
1640
|
+
try {
|
|
1641
|
+
console.log('\n***********************************************');
|
|
1642
|
+
console.log("Start watching the whole book\n");
|
|
1643
|
+
console.log('My initial book:\n');
|
|
1644
|
+
stateManager.watchState(stateContext, 'myBook', truePredicate);
|
|
1645
|
+
|
|
1646
|
+
console.log('\nUpdate second line on the first page:\n');
|
|
1647
|
+
console.log('My book after change:\n');
|
|
1648
|
+
stateContext.myBook.setLine({ pageIndex: 0, lineIndex: 1 }, 'In a far far away land');
|
|
1649
|
+
|
|
1650
|
+
} finally {
|
|
1651
|
+
// Stop monitoring the whole book
|
|
1652
|
+
stateManager.releaseState(stateContext, 'myBook', truePredicate);
|
|
1653
|
+
bookSubscription.unsubscribe();
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function testMonitoreSpecificLineInDocument(stateManager: IStateManager, stateContext: { myBook: TextDocument }): void {
|
|
1658
|
+
const line3OnPage1Index = { pageIndex: 0, lineIndex: 2 };
|
|
1659
|
+
const lineSubscription = stateManager.changed.subscribe((change: IStateChange) => {
|
|
1660
|
+
const documentIndex = change.key as ITextDocumentIndex;
|
|
1661
|
+
console.log(`Line ${documentIndex.lineIndex + 1} on page ${documentIndex.pageIndex + 1} has changed to '${change.newValue}'`);
|
|
1662
|
+
console.log('My book after change:\n');
|
|
1663
|
+
console.log(stateContext.myBook.toString());
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
try {
|
|
1667
|
+
// Here we only watch line 3 on page 1.
|
|
1668
|
+
// Notice that the line does not have to exist yet.
|
|
1669
|
+
// The initial book does not have a line 3 on page 1.
|
|
1670
|
+
//
|
|
1671
|
+
// TextDocumentInxdexObserverProxyPairFactory is used here
|
|
1672
|
+
|
|
1673
|
+
console.log('\n***********************************************');
|
|
1674
|
+
console.log("Start watching line 3 on page 1\n");
|
|
1675
|
+
stateManager.watchState(stateContext.myBook, line3OnPage1Index);
|
|
1676
|
+
|
|
1677
|
+
const proxRegistry: IProxyRegistry = InjectionContainer.get(RsXStateManagerInjectionTokens.IProxyRegistry);
|
|
1678
|
+
const bookProxy: TextDocument = proxRegistry.getProxy(stateContext.myBook);
|
|
1679
|
+
|
|
1680
|
+
bookProxy.setLine(line3OnPage1Index, 'a prince was born');
|
|
1681
|
+
|
|
1682
|
+
console.log('\nChanging line 1 on page 1 does not emit change:');
|
|
1683
|
+
console.log('---');
|
|
1684
|
+
bookProxy.setLine({ pageIndex: 0, lineIndex: 0 }, 'a troll was born');
|
|
1685
|
+
|
|
1686
|
+
} finally {
|
|
1687
|
+
// Stop monitoring line 3 on page 1.
|
|
1688
|
+
stateManager.releaseState(stateContext.myBook, line3OnPage1Index);
|
|
1689
|
+
lineSubscription.unsubscribe();
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
export const run = (() => {
|
|
1694
|
+
const stateManager: IStateManager = InjectionContainer.get(
|
|
1695
|
+
RsXStateManagerInjectionTokens.IStateManager
|
|
1696
|
+
);
|
|
1697
|
+
const stateContext = {
|
|
1698
|
+
myBook: new TextDocument([
|
|
1699
|
+
[
|
|
1700
|
+
'Once upon a time',
|
|
1701
|
+
'bla bla'
|
|
1702
|
+
],
|
|
1703
|
+
[
|
|
1704
|
+
'bla bla',
|
|
1705
|
+
'They lived happily ever after.',
|
|
1706
|
+
'The end'
|
|
1707
|
+
]
|
|
1708
|
+
])
|
|
1709
|
+
};
|
|
1710
|
+
testMonitorTextDocument(stateManager, stateContext);
|
|
1711
|
+
testMonitoreSpecificLineInDocument(stateManager, stateContext);
|
|
1712
|
+
})();
|
|
1713
|
+
```nclude_relative ../demo/src/rs-x-state-manager/register-set.ts %}
|
|
1714
|
+
```
|
|
1715
|
+
**Output:**
|
|
1716
|
+
```console
|
|
1717
|
+
Running demo: /Users/robertsanders/projects/rs-x/demo/src/rs-x-state-manager/state-manager-customize.ts
|
|
1718
|
+
|
|
1719
|
+
***********************************************
|
|
1720
|
+
Start watching the whole book
|
|
1721
|
+
|
|
1722
|
+
My initial book:
|
|
1723
|
+
|
|
1724
|
+
Page 0:
|
|
1725
|
+
0: Once upon a time
|
|
1726
|
+
1: bla bla
|
|
1727
|
+
|
|
1728
|
+
Page 1:
|
|
1729
|
+
0: bla bla
|
|
1730
|
+
1: They lived happily ever after.
|
|
1731
|
+
2: The end
|
|
1732
|
+
|
|
1733
|
+
Update second line on the first page:
|
|
1734
|
+
|
|
1735
|
+
My book after change:
|
|
1736
|
+
|
|
1737
|
+
Page 0:
|
|
1738
|
+
0: Once upon a time
|
|
1739
|
+
1: In a far far away land
|
|
1740
|
+
|
|
1741
|
+
Page 1:
|
|
1742
|
+
0: bla bla
|
|
1743
|
+
1: They lived happily ever after.
|
|
1744
|
+
2: The end
|
|
1745
|
+
|
|
1746
|
+
***********************************************
|
|
1747
|
+
Start watching line 3 on page 1
|
|
1748
|
+
|
|
1749
|
+
Line 3 on page 1 has changed to 'a prince was born'
|
|
1750
|
+
My book after change:
|
|
1751
|
+
|
|
1752
|
+
Page 0:
|
|
1753
|
+
0: Once upon a time
|
|
1754
|
+
1: In a far far away land
|
|
1755
|
+
2: a prince was born
|
|
1756
|
+
|
|
1757
|
+
Page 1:
|
|
1758
|
+
0: bla bla
|
|
1759
|
+
1: They lived happily ever after.
|
|
1760
|
+
2: The end
|
|
1761
|
+
|
|
1762
|
+
Changing line 1 on page 1 does not emit change:
|
|
1763
|
+
---
|
|
1764
|
+
```
|
|
1765
|
+
|