@rs-x/core 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/readme.md ADDED
@@ -0,0 +1,1184 @@
1
+ # Core
2
+
3
+ Provides shared core functionality for the RS-X project:
4
+
5
+ * [Dependency Injection](#dependency-injection)
6
+ * [Deep Clone](#deep-clone)
7
+ * [Deep Equality](#deep-equality)
8
+ * [Guid Factory](#guid-factory)
9
+ * [Index Value Accessor](#index-value-accessor)
10
+ * [Singleton factory](#singleton-factory)
11
+ * [Error Log](#error-log)
12
+ * [WaitForEvent](#waitforevent)
13
+
14
+ ## Dependency Injection
15
+ Implemented with [Inversify](https://github.com/inversify/InversifyJS).
16
+
17
+ The following aliases were added to make them consistent with the code style used throughout the project:
18
+
19
+ * `inject` renamed to `Inject`
20
+ * `multiInject` renamed to `MultiInject`
21
+ * `injectable` renamed to `Injectable`
22
+ * `unmanaged` renamed to `Unmanaged`
23
+ * `preDestroy` renamed to `PreDestroy`
24
+
25
+ In addition, the following extensions were added to Inversify:
26
+
27
+ ### Multi-Inject Service Utilities
28
+
29
+ These functions help manage **multi-injectable services** in an Inversify `Container` or `ContainerModuleLoadOptions`. They allow registering multiple implementations for a single token and overriding existing multi-inject lists.
30
+
31
+ ---
32
+
33
+ #### `registerMultiInjectServices(options, multiInjectToken, services)`
34
+
35
+ Registers multiple services under a single multi-inject token.
36
+
37
+ **Parameters:**
38
+
39
+ | Parameter | Type | Description |
40
+ | ------------------ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------- |
41
+ | `options` | `ContainerModuleLoadOptions` | The container or module options used for binding. |
42
+ | `multiInjectToken` | `symbol` | The multi-inject token that groups the services. |
43
+ | `services` | `MultiInjectService[]` | Array of service definitions to register. Each service must define a `target` (class) and optional `token` (symbol). |
44
+
45
+ **Behavior:**
46
+
47
+ - Iterates through the list of services and registers each using `registerMultiInjectService`.
48
+ - Each service is bound to the container and added to the multi-inject token.
49
+
50
+ ---
51
+
52
+ #### `registerMultiInjectService(container, target, options)`
53
+
54
+ Registers a single service under a multi-inject token.
55
+
56
+ **Parameters:**
57
+
58
+ | Parameter | Type | Description |
59
+ | ----------- | ----------------------------------------- | ------------------------------------------------------------------------------------ |
60
+ | `container` | `ContainerModuleLoadOptions \| Container` | The container or module to bind to. |
61
+ | `target` | `Newable<unknown>` | The class to bind. |
62
+ | `options` | `IMultiInjectTokens` | Object containing: `multiInjectToken` (symbol) and optional `serviceToken` (symbol). |
63
+
64
+ **Behavior:**
65
+
66
+ - Binds the class itself as a singleton.
67
+ - Optionally binds a service token to the class.
68
+ - Adds the class to the multi-inject token.
69
+
70
+ ---
71
+
72
+ #### `overrideMultiInjectServices(container, multiInjectToken, services)`
73
+
74
+ Overrides an existing multi-inject list, removing any previous bindings for the given token.
75
+
76
+ **Parameters:**
77
+
78
+ | Parameter | Type | Description |
79
+ | ------------------ | ----------------------------------------- | ----------------------------------------- |
80
+ | `container` | `Container \| ContainerModuleLoadOptions` | The container or module to bind to. |
81
+ | `multiInjectToken` | `symbol` | The multi-inject token to override. |
82
+ | `services` | `MultiInjectService[]` | Array of service definitions to register. |
83
+
84
+ **Behavior:**
85
+
86
+ - Removes all previous bindings for the given `multiInjectToken`.
87
+ - Binds each service in the list to the container as a singleton.
88
+ - Binds optional service tokens if provided.
89
+ - Ensures no duplicate classes are added to the multi-inject token.
90
+
91
+ **Usage Notes:**
92
+
93
+ - Use this function when you want to completely replace the multi-inject service list.
94
+ - Ensures that `container.getAll(multiInjectToken)` returns only the new services without duplicates.
95
+
96
+
97
+
98
+ ## Deep Clone
99
+
100
+ - Uses [structuredClone](https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone) by default
101
+ - Falls back to [Lodash `cloneDeepWith`](https://lodash.com/docs/4.17.15#cloneDeepWith) for unsupported types
102
+
103
+ ### Get an instance of the Deep clone service
104
+
105
+ The deep clone service is registered as a **singleton service**.
106
+ You must load the core module into the injection container if you want
107
+ to use it.
108
+
109
+ ```ts
110
+ import {
111
+ InjectionContainer,
112
+ RsXCoreModule
113
+ } from '@rs-x/core';
114
+
115
+ InjectionContainer.load(RsXCoreModule);
116
+ ```
117
+
118
+ There are two ways to get an instance:
119
+
120
+ 1. Using the injection container
121
+
122
+ ```ts
123
+ import {
124
+ IDeepClone,
125
+ InjectionContainer,
126
+ RsXCoreInjectionTokens
127
+ } from '@rs-x/core';
128
+
129
+ const deepClone: IDeepClone = InjectionContainer.get(
130
+ RsXCoreInjectionTokens.IDeepClone
131
+ );
132
+ ```
133
+
134
+ 2. Using the `@Inject` decorator
135
+
136
+ ```ts
137
+ import {
138
+ IDeepClone,
139
+ Inject,
140
+ RsXCoreInjectionTokens
141
+ } from '@rs-x/core';
142
+
143
+ export class MyClass {
144
+
145
+ constructor(
146
+ @Inject(RsXCoreInjectionTokens.IDeepClone)
147
+ private readonly _deepClone: IDeepClone
148
+ ) {}
149
+ }
150
+ ```
151
+
152
+ The following example shows how to use deep clone service:
153
+
154
+ ```ts
155
+ import {
156
+ IDeepClone,
157
+ InjectionContainer,
158
+ printValue,
159
+ RsXCoreInjectionTokens,
160
+ RsXCoreModule
161
+ } from '@rs-x/core';
162
+
163
+ // Load the core module into the injection container
164
+ InjectionContainer.load(RsXCoreModule);
165
+ const deepClone: IDeepClone = InjectionContainer.get(RsXCoreInjectionTokens.IDeepClone);
166
+
167
+ export const run = (() => {
168
+ const object = {
169
+ a: 10,
170
+ nested: {
171
+ b: 20
172
+ }
173
+ };
174
+ const clone = deepClone.clone(object);
175
+
176
+ console.log(`Clone is a copy of the cloned object: ${object !== clone}`)
177
+ console.log('Cloned object');
178
+ printValue(clone);
179
+ })();
180
+ ```
181
+
182
+ **Output:**
183
+ ```console
184
+ Running demo: demo/src/rs-x-core/deep-clone.ts
185
+ Clone is a copy of the cloned object: true
186
+ Cloned object
187
+ {
188
+ a: 10
189
+ nested: {
190
+ b: 20
191
+ }
192
+ }
193
+ ```
194
+
195
+ ## Deep Equality
196
+
197
+ Uses [fast-equals](https://github.com/planttheidea/fast-equals) for deep equality
198
+
199
+ ### Get an instance of the Equality Service
200
+
201
+ The equality service is registered as a **singleton service**.
202
+ You must load the core module into the injection container if you want
203
+ to use it.
204
+
205
+ ```ts
206
+ import {
207
+ InjectionContainer,
208
+ RsXCoreModule
209
+ } from '@rs-x/core';
210
+
211
+ InjectionContainer.load(RsXCoreModule);
212
+ ```
213
+
214
+ There are two ways to get an instance:
215
+
216
+ 1. Using the injection container
217
+
218
+ ```ts
219
+ import {
220
+ IEqualityService,
221
+ InjectionContainer,
222
+ RsXCoreInjectionTokens
223
+ } from '@rs-x/core';
224
+
225
+ const equalityService: IEqualityService = InjectionContainer.get(
226
+ RsXCoreInjectionTokens.IEqualityService
227
+ );
228
+ ```
229
+
230
+ 2. Using the `@Inject` decorator
231
+
232
+ ```ts
233
+ import {
234
+ IEqualityService,
235
+ Inject,
236
+ RsXCoreInjectionTokens
237
+ } from '@rs-x/core';
238
+
239
+ export class MyClass {
240
+
241
+ constructor(
242
+ @Inject(RsXCoreInjectionTokens.IEqualityService)
243
+ private readonly _equalityService: IEqualityService
244
+ ) {}
245
+ }
246
+ ```
247
+
248
+ The following example shows how to use equality service
249
+
250
+ ```ts
251
+ import {
252
+ IEqualityService,
253
+ InjectionContainer,
254
+ printValue,
255
+ RsXCoreInjectionTokens,
256
+ RsXCoreModule
257
+ } from '@rs-x/core';
258
+
259
+ // Load the core module into the injection container
260
+ InjectionContainer.load(RsXCoreModule);
261
+ const equalityService: IEqualityService = InjectionContainer.get(RsXCoreInjectionTokens.IEqualityService);
262
+
263
+ export const run = (() => {
264
+ const object1 = {
265
+ a: 10,
266
+ nested: {
267
+ b: 20
268
+ }
269
+ };
270
+ const object2 = {
271
+ a: 10,
272
+ nested: {
273
+ b: 20
274
+ }
275
+ };
276
+
277
+ printValue(object1);
278
+ console.log('is equal to')
279
+ printValue(object2);
280
+
281
+ const result = equalityService.isEqual(object1, object2);
282
+ console.log(`Result: ${result}`)
283
+ })();
284
+ ```
285
+
286
+ **Output:**
287
+ ```console
288
+ Running demo: demo/src/rs-x-core/equality-service.ts
289
+ {
290
+ a: 10
291
+ nested: {
292
+ b: 20
293
+ }
294
+ }
295
+ is equal to
296
+ {
297
+ a: 10
298
+ nested: {
299
+ b: 20
300
+ }
301
+ }
302
+ Result: true
303
+ ```
304
+
305
+ ## Guid Factory
306
+
307
+ Uses crypto.randomUUID() to create GUIDs
308
+
309
+ ### Get an instance of a Guid Factory
310
+
311
+ The guid factory is registered as a **singleton service**.
312
+ You must load the core module into the injection container if you want
313
+ to use it.
314
+
315
+ ```ts
316
+ import {
317
+ InjectionContainer,
318
+ RsXCoreModule
319
+ } from '@rs-x/core';
320
+
321
+ InjectionContainer.load(RsXCoreModule);
322
+ ```
323
+
324
+ There are two ways to get an instance:
325
+
326
+ 1. Using the injection container
327
+
328
+ ```ts
329
+ import {
330
+ IGuidFactory,
331
+ InjectionContainer,
332
+ RsXCoreInjectionTokens
333
+ } from '@rs-x/core';
334
+
335
+ const guidFactory: IGuidFactory = InjectionContainer.get(
336
+ RsXCoreInjectionTokens.IGuidFactory
337
+ );
338
+ ```
339
+
340
+ 2. Using the `@Inject` decorator
341
+
342
+ ```ts
343
+ import {
344
+ IGuidFactory,
345
+ Inject,
346
+ RsXCoreInjectionTokens
347
+ } from '@rs-x/core';
348
+
349
+ export class MyClass {
350
+
351
+ constructor(
352
+ @Inject(RsXCoreInjectionTokens.IGuidFactory)
353
+ private readonly _guidFactory: IGuidFactory
354
+ ) {}
355
+ }
356
+ ```
357
+
358
+ The following example shows how to use the guid factory
359
+
360
+ ```ts
361
+ import {
362
+ IGuidFactory,
363
+ InjectionContainer,
364
+ RsXCoreInjectionTokens,
365
+ RsXCoreModule
366
+ } from '@rs-x/core';
367
+
368
+ // Load the core module into the injection container
369
+ InjectionContainer.load(RsXCoreModule);
370
+ const guidFactory: IGuidFactory = InjectionContainer.get(RsXCoreInjectionTokens.IGuidFactory);
371
+
372
+ export const run = (() => {
373
+ const guid = guidFactory.create();
374
+ console.log(`Created guid: ${guid}`)
375
+ })();
376
+ ```
377
+
378
+ **Output:**
379
+ ```console
380
+ Running demo: demo/src/rs-x-core/guid-factory.ts
381
+ Created guid 1f64aabb-a57e-42e7-9edf-71c24773c150
382
+ ```
383
+
384
+ ## Index Value Accessor
385
+
386
+ Normalizes access to object properties, array indices, map keys, and similar index-based data structures.
387
+
388
+ ### The `IIndexValueAccessor` interface
389
+
390
+ export interface IIndexValueAccessor<TContext = unknown, TIndex = unknown> {
391
+ isAsync(
392
+ context: TContext,
393
+ index: TIndex
394
+ ): boolean;
395
+
396
+ getResolvedValue(
397
+ context: TContext,
398
+ index: TIndex
399
+ ): unknown;
400
+
401
+ hasValue(
402
+ context: TContext,
403
+ index: TIndex
404
+ ): boolean;
405
+
406
+ getValue(
407
+ context: TContext,
408
+ index: TIndex
409
+ ): unknown;
410
+
411
+ setValue(
412
+ context: TContext,
413
+ index: TIndex,
414
+ value: unknown
415
+ ): void;
416
+
417
+ getIndexes(
418
+ context: TContext,
419
+ index?: TIndex
420
+ ): IterableIterator<TIndex>;
421
+
422
+ applies(
423
+ context: unknown,
424
+ index: TIndex
425
+ ): boolean;
426
+ }
427
+
428
+ ---
429
+
430
+ ### Members
431
+
432
+ ### **priority**
433
+ **Type:** `number`
434
+ Defines the priority of the index value accessor. Higher numbers indicate higher priority when selecting which accessor to use.
435
+
436
+ ---
437
+
438
+ #### **isAsync(context, index)**
439
+
440
+ Returns `true` if accessing the given index is asynchronous, for example when it yields a `Promise` or an `Observable`.
441
+
442
+ | Parameter | Type | Description |
443
+ | ----------- | --------- | -------------------- |
444
+ | **context** | `unknown` | The index context. |
445
+ | **index** | `unknown` | The index to access. |
446
+
447
+ **Returns:** `boolean` — `true` if the index access is asynchronous; otherwise `false`.
448
+
449
+ ---
450
+
451
+ #### **getResolvedValue(context, index)**
452
+
453
+ Returns the resolved value of the index.
454
+
455
+ The resolved value differs from the raw index value when the index returns a `Promise` or an `Observable`. In such cases, the raw value is the `Promise` or `Observable`, while the resolved value is the value produced by it.
456
+
457
+ | Parameter | Type | Description |
458
+ | ----------- | --------- | -------------------- |
459
+ | **context** | `unknown` | The index context. |
460
+ | **index** | `unknown` | The index to access. |
461
+
462
+ **Returns:** `unknown` — the resolved index value.
463
+
464
+ ---
465
+
466
+ #### **hasValue(context, index)**
467
+
468
+ Returns `true` if the index has a value in the given context.
469
+
470
+ | Parameter | Type | Description |
471
+ | ----------- | --------- | -------------------- |
472
+ | **context** | `unknown` | The index context. |
473
+ | **index** | `unknown` | The index to access. |
474
+
475
+ **Returns:** `boolean` — `true` if the index has a value; otherwise `false`.
476
+
477
+ ---
478
+
479
+ #### **getValue(context, index)**
480
+
481
+ Returns the raw value of the index.
482
+
483
+ | Parameter | Type | Description |
484
+ | ----------- | --------- | -------------------- |
485
+ | **context** | `unknown` | The index context. |
486
+ | **index** | `unknown` | The index to access. |
487
+
488
+ **Returns:** `unknown` — the index value.
489
+
490
+ ---
491
+
492
+ #### **setValue(context, index, value)**
493
+
494
+ Sets the value of the index.
495
+
496
+ | Parameter | Type | Description |
497
+ | ----------- | --------- | -------------------- |
498
+ | **context** | `unknown` | The index context. |
499
+ | **index** | `unknown` | The index to access. |
500
+ | **value** | `unknown` | The new index value. |
501
+
502
+ **Returns:** `void`
503
+
504
+ ---
505
+
506
+ #### **getIndexes(context)**
507
+
508
+ Returns all indexes defined for the given context.
509
+
510
+ | Parameter | Type | Description |
511
+ | ----------- | --------- | ------------------ |
512
+ | **context** | `unknown` | The index context. |
513
+
514
+ **Returns:** `IterableIterator<TIndex>` — the supported indexes.
515
+
516
+ ---
517
+
518
+ #### **applies(context, index)**
519
+
520
+ Returns `true` if this index value accessor supports the given `(context, index)` pair.
521
+
522
+ | Parameter | Type | Description |
523
+ | ----------- | --------- | -------------------- |
524
+ | **context** | `unknown` | The index context. |
525
+ | **index** | `unknown` | The index to access. |
526
+
527
+ **Returns:** `boolean` — `true` if the `(context, index)` pair is supported.
528
+
529
+ ---
530
+
531
+ The default `IIndexValueAccessor` implementation internally uses the following list of `IIndexValueAccessor` implementations.
532
+ The accessors are evaluated in order of **priority**, with higher-priority accessors being checked first:
533
+
534
+ * **`PropertyValueAccessor`** – accesses properties or fields on an object. Priority = 7
535
+ * **`MethodAccessor`** – accesses methods on an object. Priority = 6
536
+ * **`ArrayIndexAccessor`** – accesses array items. Priority = 5
537
+ * **`MapKeyccessor`** – accesses map items. Priority = 4
538
+ * **`SetKeyAccessor`** – accesses `Set` items. Priority = 3
539
+ * **`ObservableAccessor`** – accesses the latest value emitted by an `Observable`. Priority = 2
540
+ * **`PromiseAccessor`** – accesses the resolved value of a `Promise`. Priority = 1
541
+ * **`DatePropertyAccessor`** – accesses date-related properties. Priority = 0
542
+
543
+ The default accessor attempts to find the appropriate index value accessor for a given `(context, index)` pair and delegates the operation to it.
544
+
545
+ If no suitable index value accessor can be found, an `UnsupportedException` is thrown.
546
+
547
+ ### Get an instance of the Index Value Accessor Service
548
+
549
+ The index value accessor service is registered as a **singleton service**.
550
+ You must load the core module into the injection container if you want
551
+ to use it.
552
+
553
+ ```ts
554
+ import {
555
+ InjectionContainer,
556
+ RsXCoreModule
557
+ } from '@rs-x/core';
558
+
559
+ InjectionContainer.load(RsXCoreModule);
560
+ ```
561
+
562
+ There are two ways to get an instance:
563
+
564
+ 1. Using the injection container
565
+
566
+ ```ts
567
+ import {
568
+ IIndexValueAccessor,
569
+ InjectionContainer,
570
+ RsXCoreInjectionTokens
571
+ } from '@rs-x/core';
572
+
573
+ const indexValueAccessor: IIndexValueAccessor = InjectionContainer.get(
574
+ RsXCoreInjectionTokens.IIndexValueAccessor
575
+ );
576
+ ```
577
+
578
+ 2. Using the `@Inject` decorator
579
+
580
+ ```ts
581
+ import {
582
+ IIndexValueAccessor,
583
+ Inject,
584
+ RsXCoreInjectionTokens
585
+ } from '@rs-x/core';
586
+
587
+ export class MyClass {
588
+
589
+ constructor(
590
+ @Inject(RsXCoreInjectionTokens.IIndexValueAccessor)
591
+ private readonly _indexValueAccessor: IIndexValueAccessor
592
+ ) {}
593
+ }
594
+ ```
595
+
596
+ ### Customize the supported index value accessor list
597
+
598
+ You can customize the index value accessor list by overriding it:
599
+
600
+ ```ts
601
+ import {
602
+ ArrayIndexAccessor,
603
+ ContainerModule,
604
+ IIndexValueAccessor,
605
+ InjectionContainer,
606
+ overrideMultiInjectServices,
607
+ PropertyValueAccessor,
608
+ RsXCoreInjectionTokens,
609
+ RsXCoreModule
610
+ } from '@rs-x/core';
611
+
612
+ // Load the core module into the injection container
613
+ InjectionContainer.load(RsXCoreModule);
614
+
615
+ export const MyModule = new ContainerModule((options) => {
616
+ overrideMultiInjectServices(options, RsXCoreInjectionTokens.IIndexValueAccessorList,
617
+ [
618
+ { target: PropertyValueAccessor, token: RsXCoreInjectionTokens.IPropertyValueAccessor },
619
+ { target: ArrayIndexAccessor, token: RsXCoreInjectionTokens.IArrayIndexAccessor },
620
+ ]
621
+ );
622
+ });
623
+
624
+ InjectionContainer.load(MyModule);
625
+ const indexValueAccessor: IIndexValueAccessor = InjectionContainer.get(RsXCoreInjectionTokens.IIndexValueAccessor);
626
+
627
+ export const run = (() => {
628
+ const object = {
629
+ a: 10,
630
+ array: [1, 2],
631
+ map: new Map([['x', 300]])
632
+ };
633
+ const aValue = indexValueAccessor.getValue(object, 'a');
634
+ console.log(`Value of field 'a': ${aValue} `);
635
+
636
+ const arrayValue = indexValueAccessor.getValue(object.array, 1);
637
+ console.log(`Value of 'array[1]': ${arrayValue} `);
638
+
639
+ let errrThrown = false;
640
+ try {
641
+ indexValueAccessor.getValue(object.map, 'x')
642
+ } catch {
643
+ errrThrown = true
644
+ }
645
+
646
+ console.log(`Value of 'map['x'] will throw error: ${errrThrown}`);
647
+
648
+ })();
649
+
650
+ ```
651
+
652
+ ## Singleton factory
653
+
654
+ Besides static singleton services registered via the dependency injection framework, we sometimes want to be able to create **dynamic singleton services**. These are services that are created based on dynamic data.
655
+
656
+ For example, suppose we have a service that patches a property on an object so it can emit an event whenever the property value changes. In this scenario, we want to ensure that the property is patched **only once**. The example below shows how we can use `SingletonFactory` to implement this:
657
+
658
+ ```ts
659
+ import {
660
+ IDisposable,
661
+ IDisposableOwner,
662
+ InvalidOperationException,
663
+ IPropertyChange,
664
+ IPropertyDescriptor,
665
+ PropertyDescriptorType,
666
+ SingletonFactory,
667
+ Type,
668
+ UnsupportedException
669
+ } from '@rs-x/core';
670
+ import { Observable, Subject } from 'rxjs';
671
+
672
+ interface IObserver extends IDisposable {
673
+ changed: Observable<IPropertyChange>;
674
+ }
675
+
676
+ class PropertObserver implements IObserver {
677
+ private _isDisposed = false;
678
+ private _value: unknown;
679
+ private _propertyDescriptorWithTarget: IPropertyDescriptor;
680
+ private readonly _changed = new Subject<IPropertyChange>();
681
+
682
+ constructor(
683
+ private readonly _owner: IDisposableOwner,
684
+ private readonly _target: object,
685
+ private readonly _propertyName: string,
686
+ ) {
687
+ this.patch();
688
+ }
689
+
690
+ public get changed(): Observable<IPropertyChange> {
691
+ return this._changed;
692
+ }
693
+
694
+ public dispose(): void {
695
+ if (this._isDisposed) {
696
+ return;
697
+ }
698
+
699
+ if (!this._owner?.canDispose || this._owner.canDispose()) {
700
+ const propertyName = this._propertyName as string;
701
+ const value = this._target[propertyName];
702
+ //to prevent errors if is was non configurable
703
+ delete this._target[propertyName];
704
+
705
+ if (
706
+ this._propertyDescriptorWithTarget.type !==
707
+ PropertyDescriptorType.Function
708
+ ) {
709
+ this._target[propertyName] = value
710
+ }
711
+
712
+ this._propertyDescriptorWithTarget = undefined;
713
+ }
714
+
715
+ this._owner?.release?.();
716
+ }
717
+
718
+ private patch(): void {
719
+ const descriptorWithTarget = Type.getPropertyDescriptor(
720
+ this._target,
721
+ this._propertyName
722
+ );
723
+ const descriptor = descriptorWithTarget.descriptor;
724
+ let newDescriptor: PropertyDescriptor;
725
+
726
+ if (descriptorWithTarget.type === PropertyDescriptorType.Function) {
727
+ throw new UnsupportedException('Methods are not supported')
728
+ } else if (!descriptor.get && !descriptor.set) {
729
+ newDescriptor =
730
+ this.createFieldPropertyDescriptor(descriptorWithTarget);
731
+ } else if (descriptor.set) {
732
+ newDescriptor =
733
+ this.createWritablePropertyDescriptor(descriptorWithTarget);
734
+ } else {
735
+ throw new InvalidOperationException(
736
+ `Property '${this._propertyName}' can not be watched because it is readonly`
737
+ );
738
+ }
739
+
740
+ Object.defineProperty(this._target, this._propertyName, newDescriptor);
741
+
742
+ this._propertyDescriptorWithTarget = descriptorWithTarget;
743
+ }
744
+
745
+ private emitChange(change: Partial<IPropertyChange>, id: unknown) {
746
+ this._value = change.newValue;
747
+
748
+ this._changed.next({
749
+ arguments: [],
750
+ ...change,
751
+ chain: [{ object: this._target, id: this._propertyName }],
752
+ target: this._target,
753
+ id,
754
+ });
755
+ }
756
+
757
+ private createFieldPropertyDescriptor(
758
+ descriptorWithTarget: IPropertyDescriptor
759
+ ): PropertyDescriptor {
760
+ const newDescriptor = { ...descriptorWithTarget.descriptor };
761
+
762
+ newDescriptor.get = () => this._value;
763
+ delete newDescriptor.writable;
764
+ delete newDescriptor.value;
765
+
766
+ newDescriptor.set = (value) => {
767
+ if (value !== this._value) {
768
+ this.emitChange({ newValue: value, }, this._propertyName);
769
+ }
770
+ };
771
+
772
+ this._value = this._target[this._propertyName];
773
+
774
+ return newDescriptor;
775
+ }
776
+
777
+ private createWritablePropertyDescriptor(
778
+ descriptorWithTarget: IPropertyDescriptor
779
+ ): PropertyDescriptor {
780
+ const newDescriptor = { ...descriptorWithTarget.descriptor };
781
+ const oldSetter = descriptorWithTarget.descriptor.set;
782
+ newDescriptor.set = (value) => {
783
+ const oldValue = this._target[this._propertyName];
784
+ if (value !== oldValue) {
785
+ oldSetter.call(this._target, value);
786
+ this.emitChange({ newValue: value }, this._propertyName);
787
+ }
788
+ };
789
+
790
+ this._value = this._target[this._propertyName];
791
+
792
+ return newDescriptor;
793
+ }
794
+ }
795
+
796
+ class PropertyObserverManager
797
+ extends SingletonFactory<
798
+ string,
799
+ string,
800
+ IObserver,
801
+ string
802
+ > {
803
+ constructor(
804
+ private readonly _object: object,
805
+ private readonly releaseObject: () => void
806
+ ) {
807
+ super();
808
+ }
809
+
810
+ public override getId(propertyName: string): string {
811
+ return propertyName;
812
+ }
813
+
814
+ protected override createId(propertyName: string): string {
815
+ return propertyName;
816
+ }
817
+
818
+ protected override createInstance(
819
+ propertyName: string,
820
+ id: string
821
+ ): IObserver {
822
+ return new PropertObserver(
823
+ {
824
+ canDispose: () => this.getReferenceCount(id) === 1,
825
+ release: () => this.release(id),
826
+ },
827
+ this._object,
828
+ propertyName
829
+ );
830
+ }
831
+
832
+ protected override onReleased(): void {
833
+ this.releaseObject();
834
+ }
835
+
836
+ protected override releaseInstance(observer: IObserver): void {
837
+ observer.dispose();
838
+ }
839
+ }
840
+
841
+ class ObjectPropertyObserverManager
842
+ extends SingletonFactory<object, object, PropertyObserverManager> {
843
+ constructor() { super(); }
844
+
845
+ public override getId(context: object): object {
846
+ return context;
847
+ }
848
+
849
+ protected override createId(context: object): object {
850
+ return context;
851
+ }
852
+
853
+ protected override createInstance(
854
+ context: object
855
+ ): PropertyObserverManager {
856
+ return new PropertyObserverManager(
857
+ context,
858
+ () => this.release(context)
859
+ );
860
+ }
861
+ protected override releaseInstance(propertyObserverManager: PropertyObserverManager): void {
862
+ propertyObserverManager.dispose();
863
+ }
864
+ }
865
+
866
+ class PropertyObserverFactory {
867
+ private readonly _objectPropertyObserverManager = new ObjectPropertyObserverManager();
868
+
869
+ public create(context: object, propertyName: string): IObserver {
870
+ return this._objectPropertyObserverManager
871
+ .create(context).instance
872
+ .create(propertyName).instance;
873
+ }
874
+ }
875
+
876
+ export const run = (() => {
877
+ const context = {
878
+ a: 10
879
+ };
880
+ const propertyObserverFactory = new PropertyObserverFactory();
881
+
882
+ const aObserver1 = propertyObserverFactory.create(context, 'a');
883
+ const aObserver2 = propertyObserverFactory.create(context, 'a');
884
+
885
+ const changeSubsription1 = aObserver1.changed.subscribe((change) => {
886
+ console.log('Observer 1:')
887
+ console.log(change.newValue)
888
+ });
889
+ const changeSubsription2 = aObserver1.changed.subscribe((change) => {
890
+ console.log('Observer 2:')
891
+ console.log(change.newValue)
892
+ });
893
+
894
+ console.log('You can observe the same property multiple times but only one observer will be create:');
895
+ console.log(aObserver1 === aObserver2);
896
+
897
+ console.log('Changing value to 20:')
898
+
899
+ context.a = 20;
900
+
901
+ // Dispose of the observers
902
+ aObserver1.dispose();
903
+ aObserver2.dispose();
904
+ // Unsubsribe to the changed event
905
+ changeSubsription1.unsubscribe();
906
+ changeSubsription2.unsubscribe();
907
+ })();
908
+ ```
909
+
910
+ **Output:**
911
+ ```console
912
+ Running demo: demo/src/rs-x-core/implementation-of-singleton-factory.ts
913
+ You can observe the same property multiple times but only one observer will be create:
914
+ true
915
+ Changing value to 20:
916
+ Observer 1:
917
+ 20
918
+ Observer 2:
919
+ 20
920
+ ```
921
+
922
+ In this example, we have derived two classes from `SingletonFactory`:
923
+
924
+ * **`PropertyObserverManager`** – ensures that only one `PropertyObserver` is created per property.
925
+ * **`ObjectPropertyObserverManager`** – ensures that only one `PropertyObserverManager` is created per object.
926
+
927
+ It is good practice **not to expose classes derived from `SingletonFactory`** directly, but to use them internally to keep the interface simple.
928
+
929
+ For example, we have created a class **`PropertyObserverFactory`** that internally uses `ObjectPropertyObserverManager`.
930
+
931
+ The `PropertyObserver` class implements a `dispose` method, which ensures that it is released when there are no references left.
932
+
933
+
934
+ ## Error Log
935
+
936
+ Basic logging using `console.error`
937
+
938
+ ### interface IErrorLog
939
+
940
+
941
+ ```ts
942
+ export interface IErrorLog {
943
+ readonly error: Observable<IError>;
944
+ add(error: IError): void;
945
+ clear(): void;
946
+ }
947
+ ```
948
+
949
+ ### Members
950
+
951
+ ### **error**
952
+ **Type:** `Observable<IError>`
953
+ event emitted when error is added
954
+
955
+ ---
956
+
957
+ #### **add(error)**
958
+ log a new error and emit error event.
959
+
960
+ | Parameter | Type | Description |
961
+ | --------- | -------- | ----------- |
962
+ | **error** | `IError` | error. |
963
+
964
+
965
+ **Returns:** `void`
966
+
967
+ ---
968
+
969
+ #### **clear()**
970
+ removes all logged errors
971
+
972
+ **Returns:** `void`
973
+
974
+ ---
975
+
976
+ The default implementation uses `console.error` to log an error.
977
+
978
+
979
+ ### Get an instance of the Error Log
980
+
981
+ The error log is registered as a **singleton service**.
982
+ You must load the core module into the injection container if you want
983
+ to use it.
984
+
985
+ ```ts
986
+ import {
987
+ InjectionContainer,
988
+ RsXCoreModule
989
+ } from '@rs-x/core';
990
+
991
+ InjectionContainer.load(RsXCoreModule);
992
+ ```
993
+
994
+ There are two ways to get an instance:
995
+
996
+ 1. Using the injection container
997
+
998
+ ```ts
999
+ import {
1000
+ IErrorLog,
1001
+ InjectionContainer,
1002
+ RsXCoreInjectionTokens
1003
+ } from '@rs-x/core';
1004
+
1005
+ const errorLog: IErrorLog = InjectionContainer.get(
1006
+ RsXCoreInjectionTokens.IErrorLog
1007
+ );
1008
+ ```
1009
+
1010
+ 2. Using the `@Inject` decorator
1011
+
1012
+ ```ts
1013
+ import {
1014
+ IErrorLog,
1015
+ Inject,
1016
+ RsXCoreInjectionTokens
1017
+ } from '@rs-x/core';
1018
+
1019
+ export class MyClass {
1020
+
1021
+ constructor(
1022
+ @Inject(RsXCoreInjectionTokens.IErrorLog)
1023
+ private readonly _errorLog: IErrorLog
1024
+ ) {}
1025
+ }
1026
+ ```
1027
+
1028
+ The following example shows how to use the error log
1029
+
1030
+ ```ts
1031
+ import {
1032
+ IErrorLog,
1033
+ InjectionContainer,
1034
+ printValue,
1035
+ RsXCoreInjectionTokens,
1036
+ RsXCoreModule
1037
+ } from '@rs-x/core';
1038
+
1039
+ // Load the core module into the injection container
1040
+ InjectionContainer.load(RsXCoreModule);
1041
+ const errorLog: IErrorLog = InjectionContainer.get(RsXCoreInjectionTokens.IErrorLog);
1042
+
1043
+ export const run = (() => {
1044
+ const context = {
1045
+ name: 'My error context'
1046
+ };
1047
+ const changeSubscription = errorLog.error.subscribe(e => {
1048
+ console.log('Emmitted error');
1049
+ printValue(e);
1050
+ });
1051
+
1052
+ try {
1053
+ throw new Error('Oops an error');
1054
+ } catch (e) {
1055
+ errorLog.add({
1056
+ exception: e,
1057
+ message: 'Oops',
1058
+ context,
1059
+ });
1060
+ } finally {
1061
+ changeSubscription.unsubscribe();
1062
+ }
1063
+ })();
1064
+ ```
1065
+
1066
+ **Output:**
1067
+ ```console
1068
+ Running demo: demo/src/rs-x-core/error-log.ts
1069
+ Emmitted error
1070
+ {
1071
+ exception: Error: Oops an error
1072
+ message: Oops
1073
+ context: {
1074
+ name: My error context
1075
+ }
1076
+ }
1077
+ ```
1078
+
1079
+ ## WaitForEvent
1080
+
1081
+ ### Overview
1082
+
1083
+ `WaitForEvent` is a utility class that allows you to **wait for one or more emissions from an RxJS `Observable`** exposed as a property on an object.
1084
+ It is particularly useful in **tests**, **async workflows**, and **event-driven logic**, where you need to trigger an action and then await observable events with optional constraints such as timeouts or emission counts.
1085
+
1086
+ ---
1087
+
1088
+ ### Key Features
1089
+
1090
+ - Waits for one or multiple observable emissions
1091
+ - Supports synchronous, `Promise`, or `Observable` triggers
1092
+ - Optional timeout handling
1093
+ - Ability to ignore the initial observable value. For example when the event is implemented with `BehaviorSubject` or `ReplaySubject`
1094
+
1095
+ ---
1096
+
1097
+ ### Constructor
1098
+
1099
+ ```ts
1100
+ constructor(
1101
+ target: T,
1102
+ eventName: E,
1103
+ options?: WaitOptions<T, E, R>
1104
+ )
1105
+ ```
1106
+
1107
+ | Parameter | Type | Description |
1108
+ | --------- | -------------------- | ------------------------------------------ |
1109
+ | target | T | Object containing the observable event |
1110
+ | eventName | E | Name of the observable property to wait on |
1111
+ | options | WaitOptions<T, E, R> | Optional configuration |
1112
+
1113
+
1114
+ #### `WaitOptions<T, E, R>`
1115
+
1116
+ Configuration options for waiting.
1117
+
1118
+ | Property / Option | Type | Default | Description |
1119
+ | ------------------ | --------- | ------- | ------------------------------ |
1120
+ | count | `number` | 1 | Number of events to wait for |
1121
+ | timeout | `number` | 100 | Timeout in milliseconds |
1122
+ | ignoreInitialValue | `boolean` | false | Ignore the first emitted value |
1123
+
1124
+ ---
1125
+
1126
+
1127
+ ### Methods
1128
+
1129
+ #### **wait(trigger)**
1130
+
1131
+ Waits for the observable to emit the specified number of events after running the trigger.
1132
+
1133
+ | Parameter | Type | Description |
1134
+ | --------- | ----------------------------------------------------- | -------------------------- |
1135
+ | trigger | () => void \| Promise<unknown> \| Observable<unknown> | action triggering the even |
1136
+
1137
+ **Returns:** `Promise<R | null>` resolving to the emitted value(s) or `null` if timeout occurs.
1138
+
1139
+ ---
1140
+
1141
+
1142
+ ### Example
1143
+
1144
+ ```ts
1145
+ import {
1146
+ printValue,
1147
+ WaitForEvent,
1148
+
1149
+ } from '@rs-x/core';
1150
+ import { Observable, Subject } from 'rxjs';
1151
+
1152
+ export const run = (async () => {
1153
+ class MyEventContext {
1154
+ private readonly _message = new Subject<string>();
1155
+
1156
+
1157
+ public get message(): Observable<string> {
1158
+ return this._message;
1159
+ }
1160
+
1161
+ public emitMessage(message: string): void {
1162
+ this._message.next(message);
1163
+ }
1164
+ }
1165
+
1166
+ const eventContext = new MyEventContext();
1167
+ const result = await new WaitForEvent(eventContext, 'message', { count: 2 }).wait(() => {
1168
+ eventContext.emitMessage('Hello');
1169
+ eventContext.emitMessage('hi');
1170
+ });
1171
+ console.log('Emitted events:');
1172
+ printValue(result);
1173
+ })();
1174
+ ```
1175
+
1176
+ **Output:**
1177
+ ```console
1178
+ Running demo: demo/src/rs-x-core/wait-for-event.ts
1179
+ Emitted events:
1180
+ [
1181
+ Hello,
1182
+ hi
1183
+ ]
1184
+ ```