j-templates 7.0.74 → 7.0.76
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/DOM/createPropertyAssignment.js +14 -7
- package/DOM/elements.d.ts +24 -24
- package/DOM/index.d.ts +2 -0
- package/DOM/index.js +2 -2
- package/Node/vNode.js +4 -0
- package/Node/vNode.types.d.ts +2 -0
- package/Store/Diff/diffAsync.d.ts +28 -0
- package/Store/Diff/diffAsync.js +25 -0
- package/Store/Diff/diffSync.d.ts +22 -0
- package/Store/Diff/diffSync.js +22 -0
- package/Store/Diff/diffTree.d.ts +33 -0
- package/Store/Diff/diffTree.js +68 -0
- package/Store/Store/store.d.ts +38 -0
- package/Store/Store/store.js +25 -0
- package/Store/Tree/observableNode.js +5 -5
- package/Store/Tree/observableScope.js +99 -105
- package/Utils/asyncQueue.d.ts +20 -0
- package/Utils/asyncQueue.js +20 -0
- package/Utils/decorators.d.ts +674 -49
- package/Utils/decorators.js +674 -49
- package/Utils/emitter.d.ts +1 -3
- package/Utils/emitter.js +5 -23
- package/Utils/functions.d.ts +5 -0
- package/Utils/functions.js +5 -0
- package/Utils/json.d.ts +10 -0
- package/Utils/json.js +63 -1
- package/package.json +1 -1
- package/Utils/bitSet.d.ts +0 -23
- package/Utils/bitSet.js +0 -139
- package/Utils/jsonType.d.ts +0 -1
- package/Utils/jsonType.js +0 -5
- package/index.debug.d.ts +0 -0
- package/index.debug.js +0 -0
package/Utils/decorators.js
CHANGED
|
@@ -9,6 +9,52 @@
|
|
|
9
9
|
* @see StoreAsync
|
|
10
10
|
* @see StoreSync
|
|
11
11
|
* @see Component
|
|
12
|
+
*
|
|
13
|
+
* Decorator Selection Guide
|
|
14
|
+
* ==========================
|
|
15
|
+
*
|
|
16
|
+
* | Your Value Type | Use | Why |
|
|
17
|
+
* |---------------------------|---------|-------------------------------|
|
|
18
|
+
* | number, string, boolean | @Value | Lightweight, no proxy overhead|
|
|
19
|
+
* | null, undefined | @Value | Simple scope storage |
|
|
20
|
+
* | { nested: objects } | @State | Deep reactivity needed |
|
|
21
|
+
* | arrays (User[], Todo[]) | @State | Array mutations tracked |
|
|
22
|
+
* | getters only (expensive) | @Computed | Cached + object reuse via Store |
|
|
23
|
+
* | getters only (simple) | @Scope | Cached, but new reference on update |
|
|
24
|
+
* | getters only (async) | @ComputedAsync | Async with caching + object reuse |
|
|
25
|
+
* | subscribe to changes | @Watch | Calls method when scope value changes |
|
|
26
|
+
* | dependency injection | @Inject | Gets value from component injector |
|
|
27
|
+
* | cleanup on destroy | @Destroy| Calls .Destroy() on component teardown |
|
|
28
|
+
*
|
|
29
|
+
* @Computed vs @Scope (Both are getters only, both cache):
|
|
30
|
+
* ---------------------------------------------------------
|
|
31
|
+
* | Use @Computed when: | Use @Scope when: |
|
|
32
|
+
* | - Computation is expensive | - Computation is cheap |
|
|
33
|
+
* | - Need object identity (===) | - Object identity doesn't matter|
|
|
34
|
+
* | - DOM reference preservation needed | - Minimal overhead preferred |
|
|
35
|
+
* | - Array/object transformations | - String/math operations |
|
|
36
|
+
*
|
|
37
|
+
* Key Difference: Object Identity on Update
|
|
38
|
+
* -----------------------------------------
|
|
39
|
+
* | Aspect | @Computed | @Scope |
|
|
40
|
+
* |--------|-----------|--------|
|
|
41
|
+
* | Caching | ✅ Yes | ✅ Yes |
|
|
42
|
+
* | Object identity preserved | ✅ Yes (ApplyDiff) | ❌ No (new object) |
|
|
43
|
+
* | Overhead | Store + diff | Single scope |
|
|
44
|
+
* | Best for | Complex objects | Primitives/simple values |
|
|
45
|
+
*
|
|
46
|
+
* Object Identity (Key Difference):
|
|
47
|
+
* ---------------------------------
|
|
48
|
+
* @Computed: Same object reference, in-place updates via ObservableNode.ApplyDiff
|
|
49
|
+
* @Scope: New object reference on every update
|
|
50
|
+
*
|
|
51
|
+
* Example:
|
|
52
|
+
* ```typescript
|
|
53
|
+
* const list = this.items; // Reference A
|
|
54
|
+
* this.data.changed = true; // Triggers update
|
|
55
|
+
* const list2 = this.items; // @Computed: A, @Scope: B
|
|
56
|
+
* console.log(list === list2); // @Computed: true, @Scope: false
|
|
57
|
+
* ```
|
|
12
58
|
*/
|
|
13
59
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
60
|
exports.Bound = void 0;
|
|
@@ -102,21 +148,101 @@ function CreateComputedScope(getter, defaultValue, store) {
|
|
|
102
148
|
return propertyScope;
|
|
103
149
|
}
|
|
104
150
|
/**
|
|
105
|
-
* Computed decorator factory for creating synchronous computed properties.
|
|
151
|
+
* Computed decorator factory for creating synchronous computed properties with caching and object reuse.
|
|
106
152
|
* A computed property is derived from other properties and automatically updates when its dependencies change.
|
|
107
153
|
*
|
|
154
|
+
* Use @Computed for expensive computations that benefit from caching AND object identity preservation
|
|
155
|
+
* (filtering, sorting, aggregations, transformations of complex objects).
|
|
156
|
+
*
|
|
108
157
|
* @example
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
158
|
+
* ```typescript
|
|
159
|
+
* // ✅ Good: Expensive computation accessed frequently
|
|
160
|
+
* @State()
|
|
161
|
+
* items: TodoItem[] = [];
|
|
162
|
+
*
|
|
163
|
+
* @Computed([])
|
|
164
|
+
* get completedItems(): TodoItem[] {
|
|
165
|
+
* // Expensive: filters entire array, cached until items change
|
|
166
|
+
* // Result object is REUSED - only changed properties are updated
|
|
167
|
+
* return this.items.filter(item => item.completed).sort(...);
|
|
114
168
|
* }
|
|
169
|
+
* ```
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```typescript
|
|
173
|
+
* // ❌ Avoid: Cheap computation - use @Scope() instead
|
|
174
|
+
* @Value()
|
|
175
|
+
* firstName: string = "John";
|
|
176
|
+
*
|
|
177
|
+
* @Value()
|
|
178
|
+
* lastName: string = "Doe";
|
|
179
|
+
*
|
|
180
|
+
* @Computed("") // Overhead: creates StoreSync, watch cycle, diff computation
|
|
181
|
+
* get fullName(): string {
|
|
182
|
+
* return this.firstName + " " + this.lastName; // Cheap string concat
|
|
183
|
+
* }
|
|
184
|
+
*
|
|
185
|
+
* @Scope() // Better: direct scope, no store overhead
|
|
186
|
+
* get fullName(): string {
|
|
187
|
+
* return this.firstName + " " + this.lastName;
|
|
188
|
+
* }
|
|
189
|
+
* ```
|
|
115
190
|
*
|
|
116
191
|
* @param defaultValue The default value to be used if the computed property is not defined.
|
|
117
192
|
* @returns A property decorator that can be applied to a getter method.
|
|
118
193
|
* @throws Will throw an error if the property is not a getter or if it has a setter.
|
|
119
|
-
* @remarks
|
|
194
|
+
* @remarks
|
|
195
|
+
* The @Computed decorator uses a two-phase caching system with diff-based updates:
|
|
196
|
+
* 1. Getter scope: Computes value and writes to StoreSync when dependencies change
|
|
197
|
+
* 2. StoreSync: Computes diff between old and new values
|
|
198
|
+
* 3. ObservableNode.ApplyDiff: Updates the EXISTING object with only changed properties
|
|
199
|
+
* 4. Property scope: Reads the updated (but same reference) value from StoreSync
|
|
200
|
+
*
|
|
201
|
+
* **Initialization**: @Computed uses lazy initialization - the scopes are created on first access:
|
|
202
|
+
* ```typescript
|
|
203
|
+
* @Computed([])
|
|
204
|
+
* get completedItems(): TodoItem[] {
|
|
205
|
+
* return this.items.filter(i => i.completed);
|
|
206
|
+
* }
|
|
207
|
+
*
|
|
208
|
+
* // Scopes created here, on first access:
|
|
209
|
+
* const items = this.completedItems;
|
|
210
|
+
* ```
|
|
211
|
+
*
|
|
212
|
+
* **Caching**: Like @Scope, @Computed caches the computed value and only re-evaluates when
|
|
213
|
+
* dependencies change. The key difference is what happens during re-evaluation:
|
|
214
|
+
*
|
|
215
|
+
* **Key benefit - Object Reuse**: The returned object maintains its identity across updates.
|
|
216
|
+
* Only the changed properties are modified in-place using ObservableNode.ApplyDiff. This enables:
|
|
217
|
+
* - DOM reference preservation (no unnecessary re-renders)
|
|
218
|
+
* - Existing proxy reuse (no proxy recreation overhead)
|
|
219
|
+
* - Identity checks (===) remain stable across updates
|
|
220
|
+
* - Efficient propagation (only changed properties trigger downstream updates)
|
|
221
|
+
*
|
|
222
|
+
* **Example of object reuse**:
|
|
223
|
+
* ```typescript
|
|
224
|
+
* const list = this.completedItems; // Reference A
|
|
225
|
+
* this.items[0].completed = true; // Triggers re-computation
|
|
226
|
+
* const list2 = this.completedItems; // Same reference A, not a new array
|
|
227
|
+
* console.log(list === list2); // true - @Computed preserves identity
|
|
228
|
+
* ```
|
|
229
|
+
*
|
|
230
|
+
* **@Computed vs @Scope caching**:
|
|
231
|
+
* | Aspect | @Computed | @Scope |
|
|
232
|
+
* |--------|-----------|--------|
|
|
233
|
+
* | Caches value | ✅ Yes | ✅ Yes |
|
|
234
|
+
* | Re-evaluates on dep change | ✅ Yes | ✅ Yes |
|
|
235
|
+
* | Object identity preserved | ✅ Yes | ❌ No |
|
|
236
|
+
* | Returns new object on update | ❌ No | ✅ Yes |
|
|
237
|
+
*
|
|
238
|
+
* **Performance note**:
|
|
239
|
+
* - Use @Computed for expensive operations on complex objects where object reuse matters
|
|
240
|
+
* - Use @Scope() for simple computations where object identity doesn't matter
|
|
241
|
+
* - The diff computation overhead is justified when you need object reuse
|
|
242
|
+
* @see {@link Scope} for simple getter-based reactive properties (caches but new reference)
|
|
243
|
+
* @see {@link ComputedAsync} for async computed properties
|
|
244
|
+
* @see {@link ObservableNode.ApplyDiff} for how diffs are applied to maintain object identity
|
|
245
|
+
* @see {@link StoreSync} for sync store implementation
|
|
120
246
|
*/
|
|
121
247
|
function Computed(defaultValue) {
|
|
122
248
|
return function (target, propertyKey, descriptor) {
|
|
@@ -154,21 +280,83 @@ function ComputedDecorator(target, prop, descriptor, defaultValue) {
|
|
|
154
280
|
};
|
|
155
281
|
}
|
|
156
282
|
/**
|
|
157
|
-
* ComputedAsync decorator factory for creating asynchronous computed properties.
|
|
283
|
+
* ComputedAsync decorator factory for creating asynchronous computed properties with caching.
|
|
158
284
|
* A computed property is derived from other properties and automatically updates when its dependencies change.
|
|
159
285
|
*
|
|
286
|
+
* Use @ComputedAsync for expensive async operations (API calls, file reads, database queries).
|
|
287
|
+
*
|
|
160
288
|
* @example
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
289
|
+
* ```typescript
|
|
290
|
+
* // ✅ Good: Async operation with caching
|
|
291
|
+
* @Value()
|
|
292
|
+
* userId: string = "123";
|
|
293
|
+
*
|
|
294
|
+
* @ComputedAsync(null)
|
|
295
|
+
* async getUser(): Promise<User | null> {
|
|
296
|
+
* // Expensive: API call, cached until userId changes
|
|
297
|
+
* return await fetch(`/api/users/${this.userId}`);
|
|
298
|
+
* }
|
|
299
|
+
* ```
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* ```typescript
|
|
303
|
+
* // ❌ Avoid: Simple/synchronous computation - use @Computed() instead
|
|
304
|
+
* @Value()
|
|
305
|
+
* count: number = 0;
|
|
306
|
+
*
|
|
307
|
+
* @ComputedAsync(0) // Overhead: async wrapper, StoreAsync
|
|
308
|
+
* get doubled(): number {
|
|
309
|
+
* return this.count * 2; // Sync operation
|
|
310
|
+
* }
|
|
311
|
+
*
|
|
312
|
+
* @Computed(0) // Better: synchronous caching
|
|
313
|
+
* get doubled(): number {
|
|
314
|
+
* return this.count * 2;
|
|
166
315
|
* }
|
|
316
|
+
* ```
|
|
167
317
|
*
|
|
168
318
|
* @param defaultValue The default value to be used if the computed property is not defined.
|
|
169
319
|
* @returns A property decorator that can be applied to a getter method.
|
|
170
320
|
* @throws Will throw an error if the property is not a getter or if it has a setter.
|
|
171
|
-
* @remarks
|
|
321
|
+
* @remarks
|
|
322
|
+
* The @ComputedAsync decorator uses StoreAsync for caching async results. The getter must be
|
|
323
|
+
* an async function or return a Promise. While waiting for the async operation, the defaultValue
|
|
324
|
+
* is returned. When the Promise resolves, the value is cached and dependent scopes update.
|
|
325
|
+
*
|
|
326
|
+
* **Initialization**: @ComputedAsync uses lazy initialization - the scopes are created on first access:
|
|
327
|
+
* ```typescript
|
|
328
|
+
* @ComputedAsync(null)
|
|
329
|
+
* async getUser(): Promise<User> {
|
|
330
|
+
* return fetch('/api/user');
|
|
331
|
+
* }
|
|
332
|
+
*
|
|
333
|
+
* // Scopes created here, on first access:
|
|
334
|
+
* const user = await this.getUser;
|
|
335
|
+
* ```
|
|
336
|
+
*
|
|
337
|
+
* **Caching**: Like @Computed, @ComputedAsync caches the result and maintains object identity
|
|
338
|
+
* through diff-based updates. The async getter only runs when dependencies change.
|
|
339
|
+
*
|
|
340
|
+
* **Object Reuse**: Like @Computed, the resolved value maintains its identity across updates.
|
|
341
|
+
* Only changed properties are modified in-place, preserving object references.
|
|
342
|
+
*
|
|
343
|
+
* **Important**: Only use this for truly async operations. For sync computations, use @Computed()
|
|
344
|
+
* which has less overhead and provides synchronous values.
|
|
345
|
+
*
|
|
346
|
+
* **Comparison**:
|
|
347
|
+
* | Aspect | @Scope | @Computed | @ComputedAsync |
|
|
348
|
+
* |--------|--------|-----------|----------------|
|
|
349
|
+
* | Caches value | ✅ Yes | ✅ Yes | ✅ Yes |
|
|
350
|
+
* | Sync/Async | Sync | Sync | Async |
|
|
351
|
+
* | Object identity | ❌ New reference | ✅ Same reference | ✅ Same reference |
|
|
352
|
+
* | Best for | Simple sync values | Complex sync values | Async operations |
|
|
353
|
+
*
|
|
354
|
+
* **Error handling**: If the async getter throws, the error is stored in the StoreAsync.
|
|
355
|
+
* You can handle errors in the getter itself or by watching for changes and checking the value.
|
|
356
|
+
*
|
|
357
|
+
* @see {@link Computed} for synchronous computed properties with object reuse
|
|
358
|
+
* @see {@link Scope} for simple getter-based reactive properties (caches, new reference)
|
|
359
|
+
* @see {@link StoreAsync} for async store implementation
|
|
172
360
|
*/
|
|
173
361
|
function ComputedAsync(defaultValue) {
|
|
174
362
|
return function (target, propertyKey, descriptor) {
|
|
@@ -206,17 +394,62 @@ function ComputedAsyncDecorator(target, prop, descriptor, defaultValue) {
|
|
|
206
394
|
};
|
|
207
395
|
}
|
|
208
396
|
/**
|
|
209
|
-
* State decorator factory for creating
|
|
210
|
-
*
|
|
397
|
+
* State decorator factory for creating reactive properties backed by ObservableNode proxies.
|
|
398
|
+
*
|
|
399
|
+
* Use this decorator for complex data structures (objects, arrays) that need deep reactivity.
|
|
211
400
|
*
|
|
212
401
|
* @example
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
* }
|
|
402
|
+
* ```typescript
|
|
403
|
+
* // ✅ Good: Complex objects/arrays
|
|
404
|
+
* @State()
|
|
405
|
+
* user: { name: string; email: string } = { name: "", email: "" };
|
|
406
|
+
*
|
|
407
|
+
* @State()
|
|
408
|
+
* items: TodoItem[] = [];
|
|
409
|
+
* ```
|
|
410
|
+
*
|
|
411
|
+
* @example
|
|
412
|
+
* ```typescript
|
|
413
|
+
* // ❌ Avoid: Simple primitives - use @Value() instead for better performance
|
|
414
|
+
* @State()
|
|
415
|
+
* count: number = 0; // Overhead: creates proxy, leaf scopes, caches
|
|
416
|
+
*
|
|
417
|
+
* @Value()
|
|
418
|
+
* count: number = 0; // Better: simple scope with direct value storage
|
|
419
|
+
* ```
|
|
217
420
|
*
|
|
218
421
|
* @returns A property decorator that can be applied to a property.
|
|
219
|
-
* @remarks
|
|
422
|
+
* @remarks
|
|
423
|
+
* The @State decorator wraps the value in an ObservableNode proxy, enabling:
|
|
424
|
+
* - Deep reactivity for nested properties and array mutations
|
|
425
|
+
* - Automatic proxy creation for all nested objects/arrays
|
|
426
|
+
* - Fine-grained per-property dependency tracking
|
|
427
|
+
*
|
|
428
|
+
* **Initialization**: @State uses lazy initialization - the proxy is created on first access,
|
|
429
|
+
* not at construction time. This means:
|
|
430
|
+
* ```typescript
|
|
431
|
+
* @State()
|
|
432
|
+
* data: { items: string[] } = { items: [] };
|
|
433
|
+
*
|
|
434
|
+
* // Proxy created here, on first access:
|
|
435
|
+
* console.log(this.data.items);
|
|
436
|
+
* ```
|
|
437
|
+
*
|
|
438
|
+
* **Performance note**: This overhead is beneficial for complex structures but wasteful for primitives.
|
|
439
|
+
* Use @Value() for simple types (number, string, boolean) that don't need deep reactivity.
|
|
440
|
+
*
|
|
441
|
+
* **Default value handling**: The value from the class field initializer is used on first access.
|
|
442
|
+
* If no initializer is provided, the initial value is `{ root: undefined }`.
|
|
443
|
+
* ```typescript
|
|
444
|
+
* @State()
|
|
445
|
+
* data: { items: string[] }; // Initial value: { root: undefined }
|
|
446
|
+
*
|
|
447
|
+
* @State()
|
|
448
|
+
* data2: { items: string[] } = { items: [] }; // Initial value: { root: { items: [] } }
|
|
449
|
+
* ```
|
|
450
|
+
* @see {@link Value} for simple primitive values
|
|
451
|
+
* @see {@link Computed} for derived/read-only values
|
|
452
|
+
* @see {@link ObservableNode} for the proxy-based reactivity system
|
|
220
453
|
*/
|
|
221
454
|
function State() {
|
|
222
455
|
return StateDecorator;
|
|
@@ -247,17 +480,65 @@ function StateDecorator(target, propertyKey) {
|
|
|
247
480
|
};
|
|
248
481
|
}
|
|
249
482
|
/**
|
|
250
|
-
* Value decorator factory for creating
|
|
251
|
-
*
|
|
483
|
+
* Value decorator factory for creating reactive properties backed by ObservableScope.
|
|
484
|
+
*
|
|
485
|
+
* Use this decorator for simple primitive values (number, string, boolean, null, undefined).
|
|
252
486
|
*
|
|
253
487
|
* @example
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
488
|
+
* ```typescript
|
|
489
|
+
* // ✅ Good: Simple primitives
|
|
490
|
+
* @Value()
|
|
491
|
+
* count: number = 0;
|
|
492
|
+
*
|
|
493
|
+
* @Value()
|
|
494
|
+
* isLoading: boolean = false;
|
|
495
|
+
*
|
|
496
|
+
* @Value()
|
|
497
|
+
* title: string = "My App";
|
|
498
|
+
* ```
|
|
499
|
+
*
|
|
500
|
+
* @example
|
|
501
|
+
* ```typescript
|
|
502
|
+
* // ❌ Avoid: Complex objects/arrays - use @State() instead for deep reactivity
|
|
503
|
+
* @Value()
|
|
504
|
+
* user: { name: string } = { name: "" }; // Nested changes won't trigger updates
|
|
505
|
+
*
|
|
506
|
+
* @State()
|
|
507
|
+
* user: { name: string } = { name: "" }; // Better: nested changes are tracked
|
|
508
|
+
* ```
|
|
258
509
|
*
|
|
259
510
|
* @returns A property decorator that can be applied to a property.
|
|
260
|
-
* @remarks
|
|
511
|
+
* @remarks
|
|
512
|
+
* The @Value decorator creates a lightweight ObservableScope to store the value.
|
|
513
|
+
* This is more efficient than @State() for primitives because it:
|
|
514
|
+
* - Avoids proxy creation overhead
|
|
515
|
+
* - Uses direct value comparison instead of deep diffing
|
|
516
|
+
* - Stores the value directly without nested scope caches
|
|
517
|
+
*
|
|
518
|
+
* **Initialization**: @Value uses lazy initialization - the scope is created on first access:
|
|
519
|
+
* ```typescript
|
|
520
|
+
* @Value()
|
|
521
|
+
* count: number = 0;
|
|
522
|
+
*
|
|
523
|
+
* // Scope created here, on first access:
|
|
524
|
+
* console.log(this.count);
|
|
525
|
+
* ```
|
|
526
|
+
*
|
|
527
|
+
* **Limitation**: Only top-level changes trigger updates. Nested object/array mutations
|
|
528
|
+
* will not be detected. Use @State() for complex structures.
|
|
529
|
+
*
|
|
530
|
+
* **Default value handling**: The value from the class field initializer is used on first access.
|
|
531
|
+
* If no initializer is provided, the initial value is `undefined`.
|
|
532
|
+
* ```typescript
|
|
533
|
+
* @Value()
|
|
534
|
+
* count: number; // Initial value: undefined
|
|
535
|
+
*
|
|
536
|
+
* @Value()
|
|
537
|
+
* count2: number = 0; // Initial value: 0
|
|
538
|
+
* ```
|
|
539
|
+
* @see {@link State} for complex objects/arrays needing deep reactivity
|
|
540
|
+
* @see {@link Computed} for derived/read-only values
|
|
541
|
+
* @see {@link ObservableScope} for the scope-based reactivity system
|
|
261
542
|
*/
|
|
262
543
|
function Value() {
|
|
263
544
|
return ValueDecorator;
|
|
@@ -300,19 +581,110 @@ function ValueDecorator(target, propertyKey) {
|
|
|
300
581
|
};
|
|
301
582
|
}
|
|
302
583
|
/**
|
|
303
|
-
* Scope decorator factory for creating
|
|
304
|
-
* A scope property is
|
|
584
|
+
* Scope decorator factory for creating reactive getter properties without caching overhead.
|
|
585
|
+
* A scope property is derived from other properties and automatically updates when its dependencies change.
|
|
586
|
+
*
|
|
587
|
+
* Use @Scope for simple, cheap computations where object identity preservation isn't needed.
|
|
305
588
|
*
|
|
306
589
|
* @example
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
590
|
+
* ```typescript
|
|
591
|
+
* // ✅ Good: Simple, cheap computation
|
|
592
|
+
* @Value()
|
|
593
|
+
* firstName: string = "John";
|
|
594
|
+
*
|
|
595
|
+
* @Value()
|
|
596
|
+
* lastName: string = "Doe";
|
|
597
|
+
*
|
|
598
|
+
* @Scope()
|
|
599
|
+
* get fullName(): string {
|
|
600
|
+
* // Cheap: string concatenation, no caching overhead
|
|
601
|
+
* return this.firstName + " " + this.lastName;
|
|
312
602
|
* }
|
|
603
|
+
* ```
|
|
604
|
+
*
|
|
605
|
+
* @example
|
|
606
|
+
* ```typescript
|
|
607
|
+
* // ❌ Avoid: Expensive computation - use @Computed() instead
|
|
608
|
+
* @State()
|
|
609
|
+
* items: TodoItem[] = [];
|
|
610
|
+
*
|
|
611
|
+
* @Scope() // New array object on every update, no caching
|
|
612
|
+
* get completedItems(): TodoItem[] {
|
|
613
|
+
* return this.items.filter(item => item.completed).sort(...);
|
|
614
|
+
* }
|
|
615
|
+
*
|
|
616
|
+
* @Computed([]) // Better: cached + object reuse via StoreSync
|
|
617
|
+
* get completedItems(): TodoItem[] {
|
|
618
|
+
* return this.items.filter(item => item.completed).sort(...);
|
|
619
|
+
* }
|
|
620
|
+
* ```
|
|
313
621
|
*
|
|
314
622
|
* @returns A property decorator that can be applied to a getter method.
|
|
315
623
|
* @throws Will throw an error if the property is not a getter or if it has a setter.
|
|
624
|
+
* @remarks
|
|
625
|
+
* The @Scope decorator creates a lightweight ObservableScope directly from the getter.
|
|
626
|
+
* Like @Computed, it caches the computed value and only re-evaluates when dependencies change.
|
|
627
|
+
* The key difference is that @Scope returns a NEW object reference on each re-evaluation.
|
|
628
|
+
*
|
|
629
|
+
* **Initialization**: @Scope uses lazy initialization - the scope is created on first access:
|
|
630
|
+
* ```typescript
|
|
631
|
+
* @Scope()
|
|
632
|
+
* get displayName(): string {
|
|
633
|
+
* return this.firstName + " " + this.lastName;
|
|
634
|
+
* }
|
|
635
|
+
*
|
|
636
|
+
* // Scope created here, on first access:
|
|
637
|
+
* console.log(this.displayName);
|
|
638
|
+
* ```
|
|
639
|
+
*
|
|
640
|
+
* **Caching**: @Scope caches the computed value just like @Computed. The getter only runs
|
|
641
|
+
* when dependencies change, not on every access. However, each re-evaluation produces
|
|
642
|
+
* a new object reference.
|
|
643
|
+
*
|
|
644
|
+
* **Key difference from @Computed**: When dependencies change, @Scope returns a NEW object
|
|
645
|
+
* reference, while @Computed updates the EXISTING object in-place using diff-based updates.
|
|
646
|
+
*
|
|
647
|
+
* **Example of caching (shared with @Computed)**:
|
|
648
|
+
* ```typescript
|
|
649
|
+
* const result1 = this.fullName; // Computes: "John Doe"
|
|
650
|
+
* const result2 = this.fullName; // Cached: "John Doe" (same value)
|
|
651
|
+
* this.firstName = "Jane"; // Triggers re-computation
|
|
652
|
+
* const result3 = this.fullName; // Computes: "Jane Doe" (new value)
|
|
653
|
+
* ```
|
|
654
|
+
*
|
|
655
|
+
* **Example of no object reuse (differs from @Computed)**:
|
|
656
|
+
* ```typescript
|
|
657
|
+
* const list = this.completedItems; // Reference A
|
|
658
|
+
* this.items[0].completed = true; // Triggers re-computation
|
|
659
|
+
* const list2 = this.completedItems; // NEW reference B
|
|
660
|
+
* console.log(list === list2); // false - @Scope creates new object
|
|
661
|
+
* ```
|
|
662
|
+
*
|
|
663
|
+
* **When to use @Scope**:
|
|
664
|
+
* - Computation is cheap (string ops, simple math, primitive values)
|
|
665
|
+
* - Object identity doesn't matter (primitives, one-time use values)
|
|
666
|
+
* - Memory overhead should be minimal
|
|
667
|
+
* - You don't need to preserve DOM references
|
|
668
|
+
*
|
|
669
|
+
* **Use @Computed instead when**:
|
|
670
|
+
* - Computation is expensive (array transformations, complex calculations)
|
|
671
|
+
* - Value is accessed frequently without dependency changes
|
|
672
|
+
* - Object identity matters (array/object references used in templates)
|
|
673
|
+
* - You need DOM reference preservation to avoid re-renders
|
|
674
|
+
*
|
|
675
|
+
* **Performance comparison**:
|
|
676
|
+
* | Aspect | @Scope | @Computed |
|
|
677
|
+
* |--------|--------|-----------|
|
|
678
|
+
* | Caches value | ✅ Yes | ✅ Yes |
|
|
679
|
+
* | Re-evaluates on dep change | ✅ Yes | ✅ Yes |
|
|
680
|
+
* | Object identity | ❌ New reference | ✅ Same reference |
|
|
681
|
+
* | Overhead | Minimal (single scope) | Higher (Store + diff) |
|
|
682
|
+
* | Best for | Primitives, cheap ops | Complex objects, expensive ops |
|
|
683
|
+
*
|
|
684
|
+
* @see {@link Computed} for cached computed properties with object reuse
|
|
685
|
+
* @see {@link ComputedAsync} for async computed properties
|
|
686
|
+
* @see {@link ObservableNode.ApplyDiff} for how @Computed maintains object identity
|
|
687
|
+
* @see {@link ObservableScope} for the scope-based reactivity system
|
|
316
688
|
*/
|
|
317
689
|
function Scope() {
|
|
318
690
|
return ScopeDecorator;
|
|
@@ -345,6 +717,62 @@ function ScopeDecorator(target, propertyKey, descriptor) {
|
|
|
345
717
|
},
|
|
346
718
|
};
|
|
347
719
|
}
|
|
720
|
+
/**
|
|
721
|
+
* Watch decorator factory for subscribing to observable changes and invoking a handler method.
|
|
722
|
+
* The watched value is computed from a function that accesses reactive properties, and the
|
|
723
|
+
* decorated method is called whenever that value changes.
|
|
724
|
+
*
|
|
725
|
+
* Use @Watch to react to changes in computed values or state without manual subscription management.
|
|
726
|
+
*
|
|
727
|
+
* @example
|
|
728
|
+
* ```typescript
|
|
729
|
+
* class MyComponent extends Component {
|
|
730
|
+
* @Value()
|
|
731
|
+
* count: number = 0;
|
|
732
|
+
*
|
|
733
|
+
* @Watch((self) => self.count * 2)
|
|
734
|
+
* handleDoubled(value: number) {
|
|
735
|
+
* console.log('Doubled count:', value); // Called whenever count changes
|
|
736
|
+
* }
|
|
737
|
+
* }
|
|
738
|
+
* ```
|
|
739
|
+
*
|
|
740
|
+
* @example
|
|
741
|
+
* ```typescript
|
|
742
|
+
* class MyComponent extends Component {
|
|
743
|
+
* @State()
|
|
744
|
+
* user: { name: string; email: string } = { name: "", email: "" };
|
|
745
|
+
*
|
|
746
|
+
* @Watch((self) => self.user.name)
|
|
747
|
+
* onUserNameChanged(newName: string) {
|
|
748
|
+
* // Called when user.name changes
|
|
749
|
+
* this.saveToAnalytics(newName);
|
|
750
|
+
* }
|
|
751
|
+
* }
|
|
752
|
+
* ```
|
|
753
|
+
*
|
|
754
|
+
* @param scope A function that takes the component instance and returns the value to watch.
|
|
755
|
+
* The function should access reactive properties (@State, @Value, @Scope, @Computed).
|
|
756
|
+
* @returns A method decorator that subscribes to the watched value and invokes the method on changes.
|
|
757
|
+
* @throws Will throw an error if the decorated member is not a method.
|
|
758
|
+
*
|
|
759
|
+
* @remarks
|
|
760
|
+
* The @Watch decorator provides:
|
|
761
|
+
* - **Automatic subscription**: No manual ObservableScope.Watch() calls needed
|
|
762
|
+
* - **Immediate invocation**: The handler is called immediately with the initial value
|
|
763
|
+
* - **Cleanup on destroy**: Subscriptions are automatically removed when the component is destroyed
|
|
764
|
+
* - **Batched updates**: Uses greedy scopes to batch rapid changes efficiently
|
|
765
|
+
*
|
|
766
|
+
* **Lifecycle**: The watch subscription is established when the component's Bound() method is called,
|
|
767
|
+
* which happens after the component is constructed and attached to the DOM.
|
|
768
|
+
*
|
|
769
|
+
* **Performance**: Watched scopes are greedy (batched via microtask queue), so rapid changes
|
|
770
|
+
* to the watched value will only trigger one handler invocation.
|
|
771
|
+
*
|
|
772
|
+
* @see {@link Bound} for when the watch subscription is established
|
|
773
|
+
* @see {@link Destroy} for automatic cleanup on component teardown
|
|
774
|
+
* @see {@link Scope} for creating computed values to watch
|
|
775
|
+
*/
|
|
348
776
|
function Watch(scope) {
|
|
349
777
|
return function (target, propertyKey, descriptor) {
|
|
350
778
|
return WatchDecorator(target, propertyKey, descriptor, scope);
|
|
@@ -370,23 +798,77 @@ function WatchDecorator(target, propertyKey, descriptor, scopeFunction) {
|
|
|
370
798
|
boundArray.push(bindWatch);
|
|
371
799
|
}
|
|
372
800
|
/**
|
|
373
|
-
* Inject decorator factory for
|
|
374
|
-
*
|
|
801
|
+
* Inject decorator factory for dependency injection from the component's injector.
|
|
802
|
+
* Properties decorated with @Inject are lazily resolved from the component's Injector
|
|
803
|
+
* on first access, enabling loose coupling and testability.
|
|
804
|
+
*
|
|
805
|
+
* Use @Inject to access services, other components, or shared resources without direct instantiation.
|
|
806
|
+
*
|
|
807
|
+
* @example
|
|
808
|
+
* ```typescript
|
|
809
|
+
* // Basic injection
|
|
810
|
+
* class MyComponent extends Component {
|
|
811
|
+
* @Inject(LoggerService)
|
|
812
|
+
* logger: LoggerService;
|
|
813
|
+
*
|
|
814
|
+
* doWork() {
|
|
815
|
+
* this.logger.log('Working...'); // Resolved from injector on first access
|
|
816
|
+
* }
|
|
817
|
+
* }
|
|
818
|
+
* ```
|
|
375
819
|
*
|
|
376
820
|
* @example
|
|
821
|
+
* ```typescript
|
|
822
|
+
* // Injection with default value (fallback if not in injector)
|
|
377
823
|
* class MyComponent extends Component {
|
|
378
|
-
* @Inject(
|
|
379
|
-
*
|
|
824
|
+
* @Inject(ConfigService)
|
|
825
|
+
* config: ConfigService = new ConfigService({}); // Default if not injected
|
|
380
826
|
* }
|
|
827
|
+
* ```
|
|
381
828
|
*
|
|
382
|
-
*
|
|
829
|
+
* @example
|
|
830
|
+
* ```typescript
|
|
831
|
+
* // Setting injected values programmatically
|
|
383
832
|
* class MyComponent extends Component {
|
|
384
|
-
* @Inject(
|
|
385
|
-
*
|
|
833
|
+
* @Inject(ApiService)
|
|
834
|
+
* api: ApiService;
|
|
835
|
+
*
|
|
836
|
+
* constructor() {
|
|
837
|
+
* super();
|
|
838
|
+
* // Override injection for testing
|
|
839
|
+
* this.Injector.Set(ApiService, mockApiService);
|
|
840
|
+
* }
|
|
386
841
|
* }
|
|
842
|
+
* ```
|
|
387
843
|
*
|
|
388
|
-
* @param type The constructor type of the dependency to
|
|
844
|
+
* @param type The constructor type (or token) of the dependency to inject.
|
|
845
|
+
* Used as the key to look up the dependency in the injector.
|
|
389
846
|
* @returns A property decorator that can be applied to a property.
|
|
847
|
+
*
|
|
848
|
+
* @remarks
|
|
849
|
+
* The @Inject decorator provides:
|
|
850
|
+
* - **Lazy resolution**: Dependency is resolved on first access, not at construction
|
|
851
|
+
* - **Read-write access**: Both getter and setter are provided
|
|
852
|
+
* - **Injector hierarchy**: Resolves through parent injectors if not found locally
|
|
853
|
+
* - **Testability**: Easy to mock by setting values in the injector
|
|
854
|
+
*
|
|
855
|
+
* **Resolution flow**:
|
|
856
|
+
* 1. Access property → calls Injector.Get(type)
|
|
857
|
+
* 2. If found in current injector, return the value
|
|
858
|
+
* 3. If not found, traverse up parent injectors
|
|
859
|
+
* 4. If still not found, returns undefined (use default value if provided)
|
|
860
|
+
*
|
|
861
|
+
* **Setting values**: You can also set injected values directly:
|
|
862
|
+
* ```typescript
|
|
863
|
+
* this.Injector.Set(ApiService, customService);
|
|
864
|
+
* // Now @Inject(ApiService) will return customService
|
|
865
|
+
* ```
|
|
866
|
+
*
|
|
867
|
+
* **Lifecycle**: The injector is set up during component construction. Injected values
|
|
868
|
+
* can be accessed anytime after construction, but typically in lifecycle methods like Bound().
|
|
869
|
+
*
|
|
870
|
+
* @see {@link Injector} for the dependency injection system
|
|
871
|
+
* @see {@link Component.Injector} for accessing the component's injector
|
|
390
872
|
*/
|
|
391
873
|
function Inject(type) {
|
|
392
874
|
return function () {
|
|
@@ -406,18 +888,119 @@ function InjectDecorator(type) {
|
|
|
406
888
|
};
|
|
407
889
|
}
|
|
408
890
|
/**
|
|
409
|
-
* Destroy decorator factory for marking
|
|
891
|
+
* Destroy decorator factory for marking properties that need cleanup on component teardown.
|
|
892
|
+
* Properties decorated with @Destroy will have their .Destroy() method automatically called
|
|
893
|
+
* when the component is destroyed, preventing memory leaks and resource leaks.
|
|
894
|
+
*
|
|
895
|
+
* Use @Destroy for managing disposable resources like timers, subscriptions, event listeners,
|
|
896
|
+
* or any object with a cleanup method.
|
|
897
|
+
*
|
|
410
898
|
* @example
|
|
899
|
+
* ```typescript
|
|
411
900
|
* class MyComponent extends Component {
|
|
412
901
|
* @Destroy()
|
|
413
|
-
* timer: Timer;
|
|
902
|
+
* timer: Timer = new Timer();
|
|
903
|
+
*
|
|
904
|
+
* startCountdown() {
|
|
905
|
+
* this.timer.start(1000); // Automatically stopped when component destroys
|
|
906
|
+
* }
|
|
414
907
|
* }
|
|
908
|
+
* ```
|
|
909
|
+
*
|
|
910
|
+
* @example
|
|
911
|
+
* ```typescript
|
|
912
|
+
* class MyComponent extends Component {
|
|
913
|
+
* @Destroy()
|
|
914
|
+
* subscription: Subscription = someObservable.subscribe(...);
|
|
915
|
+
*
|
|
916
|
+
* // No manual unsubscribe needed - handled automatically
|
|
917
|
+
* }
|
|
918
|
+
* ```
|
|
919
|
+
*
|
|
920
|
+
* @example
|
|
921
|
+
* ```typescript
|
|
922
|
+
* class MyComponent extends Component {
|
|
923
|
+
* @Destroy()
|
|
924
|
+
* eventListener: EventListener = new EventListener();
|
|
925
|
+
*
|
|
926
|
+
* bound() {
|
|
927
|
+
* document.addEventListener('click', this.eventListener);
|
|
928
|
+
* }
|
|
929
|
+
* // Automatically cleaned up when component is destroyed
|
|
930
|
+
* }
|
|
931
|
+
* ```
|
|
932
|
+
*
|
|
933
|
+
* @returns A property decorator that marks the property for automatic cleanup.
|
|
934
|
+
*
|
|
935
|
+
* @remarks
|
|
936
|
+
* The @Destroy decorator provides:
|
|
937
|
+
* - **Automatic cleanup**: Calls .Destroy() on the property when component is destroyed
|
|
938
|
+
* - **Null-safe**: Does nothing if the property is undefined/null
|
|
939
|
+
* - **Ordered cleanup**: Runs after observable scopes are destroyed
|
|
940
|
+
*
|
|
941
|
+
* **Requirements**: The decorated property must:
|
|
942
|
+
* 1. Be an object with a .Destroy() method
|
|
943
|
+
* 2. Have the method be callable with no arguments
|
|
944
|
+
* 3. Be assigned before component destruction
|
|
945
|
+
*
|
|
946
|
+
* **Lifecycle timing**:
|
|
947
|
+
* ```typescript
|
|
948
|
+
* component.Destroy() calls:
|
|
949
|
+
* 1. ObservableScope.DestroyAll() - Cleanup reactive scopes
|
|
950
|
+
* 2. Destroy.All() - Calls .Destroy() on all @Destroy()-marked properties
|
|
951
|
+
* ```
|
|
952
|
+
*
|
|
953
|
+
* **Common use cases**:
|
|
954
|
+
* - Timers and intervals
|
|
955
|
+
* - Observable subscriptions
|
|
956
|
+
* - Event listeners
|
|
957
|
+
* - WebSocket connections
|
|
958
|
+
* - File handles (Node.js)
|
|
959
|
+
* - Canvas contexts
|
|
960
|
+
*
|
|
961
|
+
* **What happens if .Destroy() throws?**: Errors are not caught - ensure your
|
|
962
|
+
* .Destroy() methods are robust and handle edge cases gracefully.
|
|
963
|
+
*
|
|
964
|
+
* @see {@link Destroy.All} for the cleanup implementation
|
|
965
|
+
* @see {@link Component.Destroy} for component lifecycle
|
|
415
966
|
*/
|
|
416
967
|
function Destroy() {
|
|
417
968
|
return DestroyDecorator;
|
|
418
969
|
}
|
|
970
|
+
/**
|
|
971
|
+
* Bound namespace provides lifecycle hooks for component initialization.
|
|
972
|
+
* Methods in this namespace are called after a component is attached to the DOM.
|
|
973
|
+
*/
|
|
419
974
|
var Bound;
|
|
420
975
|
(function (Bound) {
|
|
976
|
+
/**
|
|
977
|
+
* Binds all @Watch-decorated methods on a component instance.
|
|
978
|
+
* Called automatically by Component.Bound() after the component is attached to the DOM.
|
|
979
|
+
*
|
|
980
|
+
* @param value The component instance to bind.
|
|
981
|
+
*
|
|
982
|
+
* @example
|
|
983
|
+
* ```typescript
|
|
984
|
+
* class MyComponent extends Component {
|
|
985
|
+
* // Bound.All(this) is called automatically in Component.Bound()
|
|
986
|
+
* public Bound() {
|
|
987
|
+
* super.Bound(); // This calls Bound.All(this)
|
|
988
|
+
* // Now @Watch handlers are active
|
|
989
|
+
* }
|
|
990
|
+
* }
|
|
991
|
+
* ```
|
|
992
|
+
*
|
|
993
|
+
* @remarks
|
|
994
|
+
* This method:
|
|
995
|
+
* - Finds all @Watch-decorated methods on the component's prototype chain
|
|
996
|
+
* - Invokes the binding function for each @Watch decorator
|
|
997
|
+
* - Establishes watch subscriptions for all decorated methods
|
|
998
|
+
*
|
|
999
|
+
* **Timing**: Called after component construction and DOM attachment, but before
|
|
1000
|
+
* any user-defined Bound() logic runs (if you override Bound()).
|
|
1001
|
+
*
|
|
1002
|
+
* **Idempotent**: Safe to call multiple times - @Watch bindings are set up once.
|
|
1003
|
+
*/
|
|
421
1004
|
function All(value) {
|
|
422
1005
|
const array = GetBoundArrayForPrototype(Object.getPrototypeOf(value), false);
|
|
423
1006
|
for (let x = 0; array && x < array.length; x++)
|
|
@@ -426,12 +1009,54 @@ var Bound;
|
|
|
426
1009
|
Bound.All = All;
|
|
427
1010
|
})(Bound || (exports.Bound = Bound = {}));
|
|
428
1011
|
/**
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
* @example
|
|
432
|
-
* Destroy.All(this);
|
|
1012
|
+
* Destroy namespace provides cleanup utilities for component teardown.
|
|
1013
|
+
* Methods in this namespace are called when a component is destroyed to prevent memory leaks.
|
|
433
1014
|
*/
|
|
434
1015
|
(function (Destroy) {
|
|
1016
|
+
/**
|
|
1017
|
+
* Destroys all observable scopes and invokes .Destroy() on all @Destroy()-marked properties.
|
|
1018
|
+
* Called automatically by Component.Destroy() during component teardown.
|
|
1019
|
+
*
|
|
1020
|
+
* @param value The component instance to destroy.
|
|
1021
|
+
*
|
|
1022
|
+
* @example
|
|
1023
|
+
* ```typescript
|
|
1024
|
+
* class MyComponent extends Component {
|
|
1025
|
+
* @Destroy()
|
|
1026
|
+
* timer: Timer = new Timer();
|
|
1027
|
+
*
|
|
1028
|
+
* // Destroy.All(this) is called automatically in Component.Destroy()
|
|
1029
|
+
* public Destroy() {
|
|
1030
|
+
* super.Destroy(); // This calls Destroy.All(this)
|
|
1031
|
+
* // timer.Destroy() has been called, scopes are destroyed
|
|
1032
|
+
* }
|
|
1033
|
+
* }
|
|
1034
|
+
* ```
|
|
1035
|
+
*
|
|
1036
|
+
* @remarks
|
|
1037
|
+
* This method performs cleanup in the following order:
|
|
1038
|
+
* 1. **ObservableScope.DestroyAll()**: Destroys all scopes created by @Value, @Scope, @Computed
|
|
1039
|
+
* 2. **@Destroy cleanup**: Calls .Destroy() on each property marked with @Destroy()
|
|
1040
|
+
*
|
|
1041
|
+
* **Timing**: Called during component destruction, after unbinding from the DOM but before
|
|
1042
|
+
* the component is fully garbage collected.
|
|
1043
|
+
*
|
|
1044
|
+
* **Idempotent**: Safe to call multiple times - destroyed scopes are marked and ignored.
|
|
1045
|
+
*
|
|
1046
|
+
* **Error handling**: If any .Destroy() method throws, the error propagates. Ensure your
|
|
1047
|
+
* cleanup methods are robust and handle edge cases gracefully.
|
|
1048
|
+
*
|
|
1049
|
+
* **What gets destroyed**:
|
|
1050
|
+
* - All @Value-decorated property scopes
|
|
1051
|
+
* - All @Scope-decorated property scopes
|
|
1052
|
+
* - All @Computed-decorated property scopes (both getter and property scopes)
|
|
1053
|
+
* - All @ComputedAsync-decorated property scopes (including StoreAsync)
|
|
1054
|
+
* - All @Destroy-marked properties (calls .Destroy() method)
|
|
1055
|
+
* - All @Watch subscriptions (via scope destruction)
|
|
1056
|
+
*
|
|
1057
|
+
* @see {@link Bound.All} for initialization counterpart
|
|
1058
|
+
* @see {@link Component.Destroy} for component lifecycle
|
|
1059
|
+
*/
|
|
435
1060
|
function All(value) {
|
|
436
1061
|
const scopeMap = scopeInstanceMap.get(value);
|
|
437
1062
|
if (scopeMap !== undefined) {
|