j-templates 7.0.81 → 7.0.82
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/Utils/decorators.d.ts +102 -76
- package/Utils/decorators.js +107 -82
- package/package.json +1 -1
package/Utils/decorators.d.ts
CHANGED
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
* | arrays (User[], Todo[]) | @State | Array mutations tracked |
|
|
21
21
|
* | getters only (expensive) | @Computed | Cached + object reuse via Store |
|
|
22
22
|
* | getters only (simple) | @Scope | Cached, but new reference on update |
|
|
23
|
-
* | getters only (
|
|
23
|
+
* | getters only (sync, StoreAsync) | @ComputedAsync | Sync getter, StoreAsync caching + object reuse |
|
|
24
|
+
* | async functions (direct scope) | ObservableScope.Create(async) | Direct async, new reference, initial null |
|
|
24
25
|
* | subscribe to changes | @Watch | Calls method when scope value changes |
|
|
25
26
|
* | dependency injection | @Inject | Gets value from component injector |
|
|
26
27
|
* | cleanup on destroy | @Destroy| Calls .Destroy() on component teardown |
|
|
@@ -70,7 +71,7 @@ import { Injector } from "./injector";
|
|
|
70
71
|
* @State()
|
|
71
72
|
* items: TodoItem[] = [];
|
|
72
73
|
*
|
|
73
|
-
* @Computed(
|
|
74
|
+
* @Computed()
|
|
74
75
|
* get completedItems(): TodoItem[] {
|
|
75
76
|
* // Expensive: filters entire array, cached until items change
|
|
76
77
|
* // Result object is REUSED - only changed properties are updated
|
|
@@ -87,7 +88,7 @@ import { Injector } from "./injector";
|
|
|
87
88
|
* @Value()
|
|
88
89
|
* lastName: string = "Doe";
|
|
89
90
|
*
|
|
90
|
-
* @Computed(
|
|
91
|
+
* @Computed() // Overhead: creates StoreSync, watch cycle, diff computation
|
|
91
92
|
* get fullName(): string {
|
|
92
93
|
* return this.firstName + " " + this.lastName; // Cheap string concat
|
|
93
94
|
* }
|
|
@@ -110,7 +111,7 @@ import { Injector } from "./injector";
|
|
|
110
111
|
*
|
|
111
112
|
* **Initialization**: @Computed uses lazy initialization - the scopes are created on first access:
|
|
112
113
|
* ```typescript
|
|
113
|
-
* @Computed(
|
|
114
|
+
* @Computed()
|
|
114
115
|
* get completedItems(): TodoItem[] {
|
|
115
116
|
* return this.items.filter(i => i.completed);
|
|
116
117
|
* }
|
|
@@ -154,40 +155,53 @@ import { Injector } from "./injector";
|
|
|
154
155
|
* @see {@link ObservableNode.ApplyDiff} for how diffs are applied to maintain object identity
|
|
155
156
|
* @see {@link StoreSync} for sync store implementation
|
|
156
157
|
*/
|
|
157
|
-
export declare function Computed<T extends WeakKey, K extends keyof T,
|
|
158
|
+
export declare function Computed<T extends WeakKey, K extends keyof T, D extends T[K]>(): (target: T, propertyKey: K, descriptor: PropertyDescriptor) => PropertyDescriptor;
|
|
158
159
|
/**
|
|
159
|
-
* ComputedAsync decorator factory for creating
|
|
160
|
+
* ComputedAsync decorator factory for creating synchronous computed properties with StoreAsync caching.
|
|
160
161
|
* A computed property is derived from other properties and automatically updates when its dependencies change.
|
|
161
162
|
*
|
|
162
|
-
*
|
|
163
|
+
* IMPORTANT: Despite the name, @ComputedAsync expects a SYNC getter, NOT an async function.
|
|
164
|
+
* The "Async" refers to the internal StoreAsync caching mechanism, not the getter signature.
|
|
165
|
+
*
|
|
166
|
+
* Use @ComputedAsync when you need synchronous caching with StoreAsync backend.
|
|
167
|
+
* For direct async operations (no object reuse), use ObservableScope.Create(async () => ...) instead.
|
|
163
168
|
*
|
|
164
169
|
* @example
|
|
165
170
|
* ```typescript
|
|
166
|
-
* // ✅ Good:
|
|
171
|
+
* // ✅ Good: Synchronous getter with StoreAsync caching
|
|
167
172
|
* @Value()
|
|
168
173
|
* userId: string = "123";
|
|
169
174
|
*
|
|
170
175
|
* @ComputedAsync(null)
|
|
171
|
-
*
|
|
172
|
-
* //
|
|
173
|
-
* return
|
|
176
|
+
* get userData(): User | null {
|
|
177
|
+
* // Sync getter - returns value directly
|
|
178
|
+
* return getUserSync(this.userId);
|
|
174
179
|
* }
|
|
175
180
|
* ```
|
|
176
181
|
*
|
|
177
182
|
* @example
|
|
178
183
|
* ```typescript
|
|
179
|
-
* // ❌
|
|
184
|
+
* // ❌ WRONG - Do NOT use async keyword or return Promise
|
|
180
185
|
* @Value()
|
|
181
|
-
*
|
|
186
|
+
* userId: string = "123";
|
|
182
187
|
*
|
|
183
|
-
* @ComputedAsync(
|
|
184
|
-
*
|
|
185
|
-
* return this.
|
|
188
|
+
* @ComputedAsync(null)
|
|
189
|
+
* async getUser(): Promise<User> { // ERROR: getter must be synchronous!
|
|
190
|
+
* return await fetch(`/api/users/${this.userId}`);
|
|
186
191
|
* }
|
|
187
192
|
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
193
|
+
* // ✅ CORRECT - Use ObservableScope.Create for direct async operations
|
|
194
|
+
* import { ObservableScope } from "j-templates/Store";
|
|
195
|
+
*
|
|
196
|
+
* @Value()
|
|
197
|
+
* userId: string = "123";
|
|
198
|
+
*
|
|
199
|
+
* private userDataScope = ObservableScope.Create(async () => {
|
|
200
|
+
* return await fetch(`/api/users/${this.userId}`); // Async supported here!
|
|
201
|
+
* });
|
|
202
|
+
*
|
|
203
|
+
* get userData(): User | null {
|
|
204
|
+
* return ObservableScope.Value(this.userDataScope); // null while loading
|
|
191
205
|
* }
|
|
192
206
|
* ```
|
|
193
207
|
*
|
|
@@ -195,44 +209,43 @@ export declare function Computed<T extends WeakKey, K extends keyof T, V extends
|
|
|
195
209
|
* @returns A property decorator that can be applied to a getter method.
|
|
196
210
|
* @throws Will throw an error if the property is not a getter or if it has a setter.
|
|
197
211
|
* @remarks
|
|
198
|
-
* The @ComputedAsync decorator uses StoreAsync for caching
|
|
199
|
-
*
|
|
200
|
-
* is returned. When the Promise resolves, the value is cached and dependent scopes update.
|
|
212
|
+
* The @ComputedAsync decorator uses StoreAsync for caching. The getter must be a synchronous function.
|
|
213
|
+
* Unlike @Computed which uses StoreSync, @ComputedAsync uses StoreAsync internally for caching.
|
|
201
214
|
*
|
|
202
215
|
* **Initialization**: @ComputedAsync uses lazy initialization - the scopes are created on first access:
|
|
203
216
|
* ```typescript
|
|
204
217
|
* @ComputedAsync(null)
|
|
205
|
-
*
|
|
206
|
-
* return
|
|
218
|
+
* get userData(): User | null {
|
|
219
|
+
* return getUserSync(this.userId);
|
|
207
220
|
* }
|
|
208
221
|
*
|
|
209
|
-
* //
|
|
210
|
-
* const user =
|
|
222
|
+
* // Scope created here, on first access:
|
|
223
|
+
* const user = this.userData;
|
|
211
224
|
* ```
|
|
212
225
|
*
|
|
213
226
|
* **Caching**: Like @Computed, @ComputedAsync caches the result and maintains object identity
|
|
214
|
-
* through diff-based updates. The
|
|
227
|
+
* through diff-based updates. The getter only runs when dependencies change.
|
|
215
228
|
*
|
|
216
|
-
* **Object Reuse**: Like @Computed, the
|
|
229
|
+
* **Object Reuse**: Like @Computed, the result maintains its identity across updates.
|
|
217
230
|
* Only changed properties are modified in-place, preserving object references.
|
|
218
231
|
*
|
|
219
|
-
* **
|
|
220
|
-
*
|
|
232
|
+
* **Key difference from @Computed**: Uses StoreAsync instead of StoreSync for caching.
|
|
233
|
+
* Both use synchronous getters and provide object identity preservation.
|
|
221
234
|
*
|
|
222
235
|
* **Comparison**:
|
|
223
|
-
* | Aspect | @Scope | @Computed | @ComputedAsync |
|
|
224
|
-
*
|
|
225
|
-
* | Caches value | ✅ Yes | ✅ Yes | ✅ Yes |
|
|
226
|
-
* | Sync
|
|
227
|
-
* | Object identity | ❌ New
|
|
228
|
-
* |
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* @see {@link Computed} for synchronous computed properties with object reuse
|
|
236
|
+
* | Aspect | @Scope | @Computed | @ComputedAsync | calc(async) + @Scope |
|
|
237
|
+
* |--------|--------|-----------|----------------|----------------------|
|
|
238
|
+
* | Caches value | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes (memoized) |
|
|
239
|
+
* | Getter type | Sync | Sync | Sync | Async supported |
|
|
240
|
+
* | Object identity | ❌ New | ✅ Same | ✅ Same | ❌ New |
|
|
241
|
+
* | Store backend | Single scope | StoreSync | StoreAsync | Direct scope |
|
|
242
|
+
* | Initial value | First access | First access | defaultValue | Promise |
|
|
243
|
+
* | Best for | Simple sync, async | Complex sync | StoreAsync sync | Component async ops |
|
|
244
|
+
*
|
|
245
|
+
* @see {@link Computed} for synchronous computed properties with StoreSync caching
|
|
234
246
|
* @see {@link Scope} for simple getter-based reactive properties (caches, new reference)
|
|
235
247
|
* @see {@link StoreAsync} for async store implementation
|
|
248
|
+
* @see {@link ObservableScope.Create} for async function support
|
|
236
249
|
*/
|
|
237
250
|
export declare function ComputedAsync<T extends WeakKey, K extends keyof T, V extends T[K]>(defaultValue: V): (target: T, propertyKey: K, descriptor: PropertyDescriptor) => PropertyDescriptor;
|
|
238
251
|
/**
|
|
@@ -389,7 +402,7 @@ export declare function Value(): any;
|
|
|
389
402
|
* return this.items.filter(item => item.completed).sort(...);
|
|
390
403
|
* }
|
|
391
404
|
*
|
|
392
|
-
* @Computed(
|
|
405
|
+
* @Computed() // Better: cached + object reuse via StoreSync
|
|
393
406
|
* get completedItems(): TodoItem[] {
|
|
394
407
|
* return this.items.filter(item => item.completed).sort(...);
|
|
395
408
|
* }
|
|
@@ -448,6 +461,18 @@ export declare function Value(): any;
|
|
|
448
461
|
* - Object identity matters (array/object references used in templates)
|
|
449
462
|
* - You need DOM reference preservation to avoid re-renders
|
|
450
463
|
*
|
|
464
|
+
* **Async pattern with @Scope**: Use `calc(async () => ...)` for async operations:
|
|
465
|
+
* ```typescript
|
|
466
|
+
* @Scope()
|
|
467
|
+
* get CurrentUser() {
|
|
468
|
+
* return calc(async () => fetchUser(`/api/user/${this.userId}`));
|
|
469
|
+
* }
|
|
470
|
+
* ```
|
|
471
|
+
* - `calc` memoizes async operations with ID-based caching
|
|
472
|
+
* - Returns Promise initially, resolves when complete
|
|
473
|
+
* - Automatically batches updates via microtask queue
|
|
474
|
+
* - New reference on each update (no object reuse)
|
|
475
|
+
*
|
|
451
476
|
* **Performance comparison**:
|
|
452
477
|
* | Aspect | @Scope | @Computed |
|
|
453
478
|
* |--------|--------|-----------|
|
|
@@ -455,12 +480,13 @@ export declare function Value(): any;
|
|
|
455
480
|
* | Re-evaluates on dep change | ✅ Yes | ✅ Yes |
|
|
456
481
|
* | Object identity | ❌ New reference | ✅ Same reference |
|
|
457
482
|
* | Overhead | Minimal (single scope) | Higher (Store + diff) |
|
|
458
|
-
* | Best for | Primitives, cheap ops | Complex objects, expensive ops |
|
|
483
|
+
* | Best for | Primitives, cheap ops, async ops | Complex objects, expensive ops |
|
|
459
484
|
*
|
|
460
485
|
* @see {@link Computed} for cached computed properties with object reuse
|
|
461
|
-
* @see {@link ComputedAsync} for
|
|
486
|
+
* @see {@link ComputedAsync} for sync getters with StoreAsync backend
|
|
462
487
|
* @see {@link ObservableNode.ApplyDiff} for how @Computed maintains object identity
|
|
463
488
|
* @see {@link ObservableScope} for the scope-based reactivity system
|
|
489
|
+
* @see {@link calc} for memoized async operations within @Scope
|
|
464
490
|
*/
|
|
465
491
|
export declare function Scope(): typeof ScopeDecorator;
|
|
466
492
|
/**
|
|
@@ -739,38 +765,38 @@ export declare namespace Destroy {
|
|
|
739
765
|
* @Destroy()
|
|
740
766
|
* timer: Timer = new Timer();
|
|
741
767
|
*
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
768
|
+
* // Destroy.All(this) is called automatically in Component.Destroy()
|
|
769
|
+
* public Destroy() {
|
|
770
|
+
* super.Destroy(); // This calls Destroy.All(this)
|
|
771
|
+
* // timer.Destroy() has been called, scopes are destroyed
|
|
772
|
+
* }
|
|
773
|
+
* }
|
|
774
|
+
* ```
|
|
775
|
+
*
|
|
776
|
+
* @remarks
|
|
777
|
+
* This method performs cleanup in the following order:
|
|
778
|
+
* 1. **ObservableScope.DestroyAll()**: Destroys all scopes created by @Value, @Scope, @Computed
|
|
779
|
+
* 2. **@Destroy cleanup**: Calls .Destroy() on each property marked with @Destroy()
|
|
780
|
+
*
|
|
781
|
+
* **Timing**: Called during component destruction, after unbinding from the DOM but before
|
|
782
|
+
* the component is fully garbage collected.
|
|
783
|
+
*
|
|
784
|
+
* **Idempotent**: Safe to call multiple times - destroyed scopes are marked and ignored.
|
|
785
|
+
*
|
|
786
|
+
* **Error handling**: If any .Destroy() method throws, the error propagates. Ensure your
|
|
787
|
+
* cleanup methods are robust and handle edge cases gracefully.
|
|
788
|
+
*
|
|
789
|
+
* **What gets destroyed**:
|
|
790
|
+
* - All @Value-decorated property scopes
|
|
791
|
+
* - All @Scope-decorated property scopes
|
|
792
|
+
* - All @Computed-decorated property scopes (both getter and property scopes)
|
|
793
|
+
* - All @ComputedAsync-decorated property scopes (including StoreAsync)
|
|
794
|
+
* - All @Destroy-marked properties (calls .Destroy() method)
|
|
795
|
+
* - All @Watch subscriptions (via scope destruction)
|
|
796
|
+
*
|
|
797
|
+
* @see {@link Bound.All} for initialization counterpart
|
|
798
|
+
* @see {@link Component.Destroy} for component lifecycle
|
|
799
|
+
*/
|
|
774
800
|
function All<T extends WeakKey>(value: T): void;
|
|
775
801
|
}
|
|
776
802
|
declare function DestroyDecorator<T extends Record<K, IDestroyable>, K extends string>(target: T, propertyKey: K): any;
|
package/Utils/decorators.js
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
* | arrays (User[], Todo[]) | @State | Array mutations tracked |
|
|
22
22
|
* | getters only (expensive) | @Computed | Cached + object reuse via Store |
|
|
23
23
|
* | getters only (simple) | @Scope | Cached, but new reference on update |
|
|
24
|
-
* | getters only (
|
|
24
|
+
* | getters only (sync, StoreAsync) | @ComputedAsync | Sync getter, StoreAsync caching + object reuse |
|
|
25
|
+
* | async functions (direct scope) | ObservableScope.Create(async) | Direct async, new reference, initial null |
|
|
25
26
|
* | subscribe to changes | @Watch | Calls method when scope value changes |
|
|
26
27
|
* | dependency injection | @Inject | Gets value from component injector |
|
|
27
28
|
* | cleanup on destroy | @Destroy| Calls .Destroy() on component teardown |
|
|
@@ -132,13 +133,12 @@ function GetDestroyArrayForPrototype(prototype, create = true) {
|
|
|
132
133
|
}
|
|
133
134
|
return array;
|
|
134
135
|
}
|
|
135
|
-
function CreateComputedScope(getter,
|
|
136
|
+
function CreateComputedScope(getter, store, defaultValue) {
|
|
136
137
|
const getterScope = observableScope_1.ObservableScope.Create(getter, true);
|
|
137
138
|
observableScope_1.ObservableScope.Watch(getterScope, (scope) => {
|
|
138
139
|
const data = observableScope_1.ObservableScope.Value(scope);
|
|
139
140
|
store.Write(data, "root");
|
|
140
141
|
});
|
|
141
|
-
// ObservableScope.Init(getterScope);
|
|
142
142
|
const propertyScope = observableScope_1.ObservableScope.Create(() => store.Get("root", defaultValue));
|
|
143
143
|
observableScope_1.ObservableScope.OnDestroyed(propertyScope, function () {
|
|
144
144
|
observableScope_1.ObservableScope.Destroy(getterScope);
|
|
@@ -160,7 +160,7 @@ function CreateComputedScope(getter, defaultValue, store) {
|
|
|
160
160
|
* @State()
|
|
161
161
|
* items: TodoItem[] = [];
|
|
162
162
|
*
|
|
163
|
-
* @Computed(
|
|
163
|
+
* @Computed()
|
|
164
164
|
* get completedItems(): TodoItem[] {
|
|
165
165
|
* // Expensive: filters entire array, cached until items change
|
|
166
166
|
* // Result object is REUSED - only changed properties are updated
|
|
@@ -177,7 +177,7 @@ function CreateComputedScope(getter, defaultValue, store) {
|
|
|
177
177
|
* @Value()
|
|
178
178
|
* lastName: string = "Doe";
|
|
179
179
|
*
|
|
180
|
-
* @Computed(
|
|
180
|
+
* @Computed() // Overhead: creates StoreSync, watch cycle, diff computation
|
|
181
181
|
* get fullName(): string {
|
|
182
182
|
* return this.firstName + " " + this.lastName; // Cheap string concat
|
|
183
183
|
* }
|
|
@@ -200,7 +200,7 @@ function CreateComputedScope(getter, defaultValue, store) {
|
|
|
200
200
|
*
|
|
201
201
|
* **Initialization**: @Computed uses lazy initialization - the scopes are created on first access:
|
|
202
202
|
* ```typescript
|
|
203
|
-
* @Computed(
|
|
203
|
+
* @Computed()
|
|
204
204
|
* get completedItems(): TodoItem[] {
|
|
205
205
|
* return this.items.filter(i => i.completed);
|
|
206
206
|
* }
|
|
@@ -244,9 +244,9 @@ function CreateComputedScope(getter, defaultValue, store) {
|
|
|
244
244
|
* @see {@link ObservableNode.ApplyDiff} for how diffs are applied to maintain object identity
|
|
245
245
|
* @see {@link StoreSync} for sync store implementation
|
|
246
246
|
*/
|
|
247
|
-
function Computed(
|
|
247
|
+
function Computed() {
|
|
248
248
|
return function (target, propertyKey, descriptor) {
|
|
249
|
-
return ComputedDecorator(target, propertyKey, descriptor
|
|
249
|
+
return ComputedDecorator(target, propertyKey, descriptor);
|
|
250
250
|
};
|
|
251
251
|
}
|
|
252
252
|
/**
|
|
@@ -259,7 +259,7 @@ function Computed(defaultValue) {
|
|
|
259
259
|
* @returns A property descriptor that replaces the original descriptor with a computed implementation.
|
|
260
260
|
* @throws Will throw an error if the property is not a getter or if it has a setter.
|
|
261
261
|
*/
|
|
262
|
-
function ComputedDecorator(target, prop, descriptor
|
|
262
|
+
function ComputedDecorator(target, prop, descriptor) {
|
|
263
263
|
const propertyKey = prop;
|
|
264
264
|
if (!(descriptor && descriptor.get))
|
|
265
265
|
throw "Computed decorator requires a getter";
|
|
@@ -272,7 +272,7 @@ function ComputedDecorator(target, prop, descriptor, defaultValue) {
|
|
|
272
272
|
get: function () {
|
|
273
273
|
const scopeMap = GetScopeMapForInstance(this);
|
|
274
274
|
if (scopeMap[propertyKey] === undefined) {
|
|
275
|
-
const propertyScope = CreateComputedScope(getter.bind(this),
|
|
275
|
+
const propertyScope = CreateComputedScope(getter.bind(this), new Store_1.StoreSync());
|
|
276
276
|
scopeMap[propertyKey] = [propertyScope, undefined];
|
|
277
277
|
}
|
|
278
278
|
return observableScope_1.ObservableScope.Value(scopeMap[propertyKey][0]);
|
|
@@ -280,38 +280,51 @@ function ComputedDecorator(target, prop, descriptor, defaultValue) {
|
|
|
280
280
|
};
|
|
281
281
|
}
|
|
282
282
|
/**
|
|
283
|
-
* ComputedAsync decorator factory for creating
|
|
283
|
+
* ComputedAsync decorator factory for creating synchronous computed properties with StoreAsync caching.
|
|
284
284
|
* A computed property is derived from other properties and automatically updates when its dependencies change.
|
|
285
285
|
*
|
|
286
|
-
*
|
|
286
|
+
* IMPORTANT: Despite the name, @ComputedAsync expects a SYNC getter, NOT an async function.
|
|
287
|
+
* The "Async" refers to the internal StoreAsync caching mechanism, not the getter signature.
|
|
288
|
+
*
|
|
289
|
+
* Use @ComputedAsync when you need synchronous caching with StoreAsync backend.
|
|
290
|
+
* For direct async operations (no object reuse), use ObservableScope.Create(async () => ...) instead.
|
|
287
291
|
*
|
|
288
292
|
* @example
|
|
289
293
|
* ```typescript
|
|
290
|
-
* // ✅ Good:
|
|
294
|
+
* // ✅ Good: Synchronous getter with StoreAsync caching
|
|
291
295
|
* @Value()
|
|
292
296
|
* userId: string = "123";
|
|
293
297
|
*
|
|
294
298
|
* @ComputedAsync(null)
|
|
295
|
-
*
|
|
296
|
-
* //
|
|
297
|
-
* return
|
|
299
|
+
* get userData(): User | null {
|
|
300
|
+
* // Sync getter - returns value directly
|
|
301
|
+
* return getUserSync(this.userId);
|
|
298
302
|
* }
|
|
299
303
|
* ```
|
|
300
304
|
*
|
|
301
305
|
* @example
|
|
302
306
|
* ```typescript
|
|
303
|
-
* // ❌
|
|
307
|
+
* // ❌ WRONG - Do NOT use async keyword or return Promise
|
|
304
308
|
* @Value()
|
|
305
|
-
*
|
|
309
|
+
* userId: string = "123";
|
|
306
310
|
*
|
|
307
|
-
* @ComputedAsync(
|
|
308
|
-
*
|
|
309
|
-
* return this.
|
|
311
|
+
* @ComputedAsync(null)
|
|
312
|
+
* async getUser(): Promise<User> { // ERROR: getter must be synchronous!
|
|
313
|
+
* return await fetch(`/api/users/${this.userId}`);
|
|
310
314
|
* }
|
|
311
315
|
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
316
|
+
* // ✅ CORRECT - Use ObservableScope.Create for direct async operations
|
|
317
|
+
* import { ObservableScope } from "j-templates/Store";
|
|
318
|
+
*
|
|
319
|
+
* @Value()
|
|
320
|
+
* userId: string = "123";
|
|
321
|
+
*
|
|
322
|
+
* private userDataScope = ObservableScope.Create(async () => {
|
|
323
|
+
* return await fetch(`/api/users/${this.userId}`); // Async supported here!
|
|
324
|
+
* });
|
|
325
|
+
*
|
|
326
|
+
* get userData(): User | null {
|
|
327
|
+
* return ObservableScope.Value(this.userDataScope); // null while loading
|
|
315
328
|
* }
|
|
316
329
|
* ```
|
|
317
330
|
*
|
|
@@ -319,44 +332,43 @@ function ComputedDecorator(target, prop, descriptor, defaultValue) {
|
|
|
319
332
|
* @returns A property decorator that can be applied to a getter method.
|
|
320
333
|
* @throws Will throw an error if the property is not a getter or if it has a setter.
|
|
321
334
|
* @remarks
|
|
322
|
-
* The @ComputedAsync decorator uses StoreAsync for caching
|
|
323
|
-
*
|
|
324
|
-
* is returned. When the Promise resolves, the value is cached and dependent scopes update.
|
|
335
|
+
* The @ComputedAsync decorator uses StoreAsync for caching. The getter must be a synchronous function.
|
|
336
|
+
* Unlike @Computed which uses StoreSync, @ComputedAsync uses StoreAsync internally for caching.
|
|
325
337
|
*
|
|
326
338
|
* **Initialization**: @ComputedAsync uses lazy initialization - the scopes are created on first access:
|
|
327
339
|
* ```typescript
|
|
328
340
|
* @ComputedAsync(null)
|
|
329
|
-
*
|
|
330
|
-
* return
|
|
341
|
+
* get userData(): User | null {
|
|
342
|
+
* return getUserSync(this.userId);
|
|
331
343
|
* }
|
|
332
344
|
*
|
|
333
|
-
* //
|
|
334
|
-
* const user =
|
|
345
|
+
* // Scope created here, on first access:
|
|
346
|
+
* const user = this.userData;
|
|
335
347
|
* ```
|
|
336
348
|
*
|
|
337
349
|
* **Caching**: Like @Computed, @ComputedAsync caches the result and maintains object identity
|
|
338
|
-
* through diff-based updates. The
|
|
350
|
+
* through diff-based updates. The getter only runs when dependencies change.
|
|
339
351
|
*
|
|
340
|
-
* **Object Reuse**: Like @Computed, the
|
|
352
|
+
* **Object Reuse**: Like @Computed, the result maintains its identity across updates.
|
|
341
353
|
* Only changed properties are modified in-place, preserving object references.
|
|
342
354
|
*
|
|
343
|
-
* **
|
|
344
|
-
*
|
|
355
|
+
* **Key difference from @Computed**: Uses StoreAsync instead of StoreSync for caching.
|
|
356
|
+
* Both use synchronous getters and provide object identity preservation.
|
|
345
357
|
*
|
|
346
358
|
* **Comparison**:
|
|
347
|
-
* | Aspect | @Scope | @Computed | @ComputedAsync |
|
|
348
|
-
*
|
|
349
|
-
* | Caches value | ✅ Yes | ✅ Yes | ✅ Yes |
|
|
350
|
-
* | Sync
|
|
351
|
-
* | Object identity | ❌ New
|
|
352
|
-
* |
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
* @see {@link Computed} for synchronous computed properties with object reuse
|
|
359
|
+
* | Aspect | @Scope | @Computed | @ComputedAsync | calc(async) + @Scope |
|
|
360
|
+
* |--------|--------|-----------|----------------|----------------------|
|
|
361
|
+
* | Caches value | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes (memoized) |
|
|
362
|
+
* | Getter type | Sync | Sync | Sync | Async supported |
|
|
363
|
+
* | Object identity | ❌ New | ✅ Same | ✅ Same | ❌ New |
|
|
364
|
+
* | Store backend | Single scope | StoreSync | StoreAsync | Direct scope |
|
|
365
|
+
* | Initial value | First access | First access | defaultValue | Promise |
|
|
366
|
+
* | Best for | Simple sync, async | Complex sync | StoreAsync sync | Component async ops |
|
|
367
|
+
*
|
|
368
|
+
* @see {@link Computed} for synchronous computed properties with StoreSync caching
|
|
358
369
|
* @see {@link Scope} for simple getter-based reactive properties (caches, new reference)
|
|
359
370
|
* @see {@link StoreAsync} for async store implementation
|
|
371
|
+
* @see {@link ObservableScope.Create} for async function support
|
|
360
372
|
*/
|
|
361
373
|
function ComputedAsync(defaultValue) {
|
|
362
374
|
return function (target, propertyKey, descriptor) {
|
|
@@ -386,7 +398,7 @@ function ComputedAsyncDecorator(target, prop, descriptor, defaultValue) {
|
|
|
386
398
|
get: function () {
|
|
387
399
|
const scopeMap = GetScopeMapForInstance(this);
|
|
388
400
|
if (scopeMap[propertyKey] === undefined) {
|
|
389
|
-
const propertyScope = CreateComputedScope(getter.bind(this),
|
|
401
|
+
const propertyScope = CreateComputedScope(getter.bind(this), new Store_1.StoreAsync(), defaultValue);
|
|
390
402
|
scopeMap[propertyKey] = [propertyScope, undefined];
|
|
391
403
|
}
|
|
392
404
|
return observableScope_1.ObservableScope.Value(scopeMap[propertyKey][0]);
|
|
@@ -613,7 +625,7 @@ function ValueDecorator(target, propertyKey) {
|
|
|
613
625
|
* return this.items.filter(item => item.completed).sort(...);
|
|
614
626
|
* }
|
|
615
627
|
*
|
|
616
|
-
* @Computed(
|
|
628
|
+
* @Computed() // Better: cached + object reuse via StoreSync
|
|
617
629
|
* get completedItems(): TodoItem[] {
|
|
618
630
|
* return this.items.filter(item => item.completed).sort(...);
|
|
619
631
|
* }
|
|
@@ -672,6 +684,18 @@ function ValueDecorator(target, propertyKey) {
|
|
|
672
684
|
* - Object identity matters (array/object references used in templates)
|
|
673
685
|
* - You need DOM reference preservation to avoid re-renders
|
|
674
686
|
*
|
|
687
|
+
* **Async pattern with @Scope**: Use `calc(async () => ...)` for async operations:
|
|
688
|
+
* ```typescript
|
|
689
|
+
* @Scope()
|
|
690
|
+
* get CurrentUser() {
|
|
691
|
+
* return calc(async () => fetchUser(`/api/user/${this.userId}`));
|
|
692
|
+
* }
|
|
693
|
+
* ```
|
|
694
|
+
* - `calc` memoizes async operations with ID-based caching
|
|
695
|
+
* - Returns Promise initially, resolves when complete
|
|
696
|
+
* - Automatically batches updates via microtask queue
|
|
697
|
+
* - New reference on each update (no object reuse)
|
|
698
|
+
*
|
|
675
699
|
* **Performance comparison**:
|
|
676
700
|
* | Aspect | @Scope | @Computed |
|
|
677
701
|
* |--------|--------|-----------|
|
|
@@ -679,12 +703,13 @@ function ValueDecorator(target, propertyKey) {
|
|
|
679
703
|
* | Re-evaluates on dep change | ✅ Yes | ✅ Yes |
|
|
680
704
|
* | Object identity | ❌ New reference | ✅ Same reference |
|
|
681
705
|
* | Overhead | Minimal (single scope) | Higher (Store + diff) |
|
|
682
|
-
* | Best for | Primitives, cheap ops | Complex objects, expensive ops |
|
|
706
|
+
* | Best for | Primitives, cheap ops, async ops | Complex objects, expensive ops |
|
|
683
707
|
*
|
|
684
708
|
* @see {@link Computed} for cached computed properties with object reuse
|
|
685
|
-
* @see {@link ComputedAsync} for
|
|
709
|
+
* @see {@link ComputedAsync} for sync getters with StoreAsync backend
|
|
686
710
|
* @see {@link ObservableNode.ApplyDiff} for how @Computed maintains object identity
|
|
687
711
|
* @see {@link ObservableScope} for the scope-based reactivity system
|
|
712
|
+
* @see {@link calc} for memoized async operations within @Scope
|
|
688
713
|
*/
|
|
689
714
|
function Scope() {
|
|
690
715
|
return ScopeDecorator;
|
|
@@ -1025,38 +1050,38 @@ var Bound;
|
|
|
1025
1050
|
* @Destroy()
|
|
1026
1051
|
* timer: Timer = new Timer();
|
|
1027
1052
|
*
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1053
|
+
* // Destroy.All(this) is called automatically in Component.Destroy()
|
|
1054
|
+
* public Destroy() {
|
|
1055
|
+
* super.Destroy(); // This calls Destroy.All(this)
|
|
1056
|
+
* // timer.Destroy() has been called, scopes are destroyed
|
|
1057
|
+
* }
|
|
1058
|
+
* }
|
|
1059
|
+
* ```
|
|
1060
|
+
*
|
|
1061
|
+
* @remarks
|
|
1062
|
+
* This method performs cleanup in the following order:
|
|
1063
|
+
* 1. **ObservableScope.DestroyAll()**: Destroys all scopes created by @Value, @Scope, @Computed
|
|
1064
|
+
* 2. **@Destroy cleanup**: Calls .Destroy() on each property marked with @Destroy()
|
|
1065
|
+
*
|
|
1066
|
+
* **Timing**: Called during component destruction, after unbinding from the DOM but before
|
|
1067
|
+
* the component is fully garbage collected.
|
|
1068
|
+
*
|
|
1069
|
+
* **Idempotent**: Safe to call multiple times - destroyed scopes are marked and ignored.
|
|
1070
|
+
*
|
|
1071
|
+
* **Error handling**: If any .Destroy() method throws, the error propagates. Ensure your
|
|
1072
|
+
* cleanup methods are robust and handle edge cases gracefully.
|
|
1073
|
+
*
|
|
1074
|
+
* **What gets destroyed**:
|
|
1075
|
+
* - All @Value-decorated property scopes
|
|
1076
|
+
* - All @Scope-decorated property scopes
|
|
1077
|
+
* - All @Computed-decorated property scopes (both getter and property scopes)
|
|
1078
|
+
* - All @ComputedAsync-decorated property scopes (including StoreAsync)
|
|
1079
|
+
* - All @Destroy-marked properties (calls .Destroy() method)
|
|
1080
|
+
* - All @Watch subscriptions (via scope destruction)
|
|
1081
|
+
*
|
|
1082
|
+
* @see {@link Bound.All} for initialization counterpart
|
|
1083
|
+
* @see {@link Component.Destroy} for component lifecycle
|
|
1084
|
+
*/
|
|
1060
1085
|
function All(value) {
|
|
1061
1086
|
const scopeMap = scopeInstanceMap.get(value);
|
|
1062
1087
|
if (scopeMap !== undefined) {
|