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.
@@ -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 (async) | @ComputedAsync | Async with caching + object reuse |
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("") // Overhead: creates StoreSync, watch cycle, diff computation
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, V extends T[K]>(defaultValue: V): (target: T, propertyKey: K, descriptor: PropertyDescriptor) => PropertyDescriptor;
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 asynchronous computed properties with caching.
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
- * Use @ComputedAsync for expensive async operations (API calls, file reads, database queries).
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: Async operation with caching
171
+ * // ✅ Good: Synchronous getter with StoreAsync caching
167
172
  * @Value()
168
173
  * userId: string = "123";
169
174
  *
170
175
  * @ComputedAsync(null)
171
- * async getUser(): Promise<User | null> {
172
- * // Expensive: API call, cached until userId changes
173
- * return await fetch(`/api/users/${this.userId}`);
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
- * // ❌ Avoid: Simple/synchronous computation - use @Computed() instead
184
+ * // ❌ WRONG - Do NOT use async keyword or return Promise
180
185
  * @Value()
181
- * count: number = 0;
186
+ * userId: string = "123";
182
187
  *
183
- * @ComputedAsync(0) // Overhead: async wrapper, StoreAsync
184
- * get doubled(): number {
185
- * return this.count * 2; // Sync operation
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
- * @Computed(0) // Better: synchronous caching
189
- * get doubled(): number {
190
- * return this.count * 2;
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 async results. The getter must be
199
- * an async function or return a Promise. While waiting for the async operation, the defaultValue
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
- * async getUser(): Promise<User> {
206
- * return fetch('/api/user');
218
+ * get userData(): User | null {
219
+ * return getUserSync(this.userId);
207
220
  * }
208
221
  *
209
- * // Scopes created here, on first access:
210
- * const user = await this.getUser;
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 async getter only runs when dependencies change.
227
+ * through diff-based updates. The getter only runs when dependencies change.
215
228
  *
216
- * **Object Reuse**: Like @Computed, the resolved value maintains its identity across updates.
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
- * **Important**: Only use this for truly async operations. For sync computations, use @Computed()
220
- * which has less overhead and provides synchronous values.
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/Async | Sync | Sync | Async |
227
- * | Object identity | ❌ New reference | ✅ Same reference | ✅ Same reference |
228
- * | Best for | Simple sync values | Complex sync values | Async operations |
229
- *
230
- * **Error handling**: If the async getter throws, the error is stored in the StoreAsync.
231
- * You can handle errors in the getter itself or by watching for changes and checking the value.
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([]) // Better: cached + object reuse via StoreSync
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 async computed properties
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
- * // Destroy.All(this) is called automatically in Component.Destroy()
743
- * public Destroy() {
744
- * super.Destroy(); // This calls Destroy.All(this)
745
- * // timer.Destroy() has been called, scopes are destroyed
746
- * }
747
- * }
748
- * ```
749
- *
750
- * @remarks
751
- * This method performs cleanup in the following order:
752
- * 1. **ObservableScope.DestroyAll()**: Destroys all scopes created by @Value, @Scope, @Computed
753
- * 2. **@Destroy cleanup**: Calls .Destroy() on each property marked with @Destroy()
754
- *
755
- * **Timing**: Called during component destruction, after unbinding from the DOM but before
756
- * the component is fully garbage collected.
757
- *
758
- * **Idempotent**: Safe to call multiple times - destroyed scopes are marked and ignored.
759
- *
760
- * **Error handling**: If any .Destroy() method throws, the error propagates. Ensure your
761
- * cleanup methods are robust and handle edge cases gracefully.
762
- *
763
- * **What gets destroyed**:
764
- * - All @Value-decorated property scopes
765
- * - All @Scope-decorated property scopes
766
- * - All @Computed-decorated property scopes (both getter and property scopes)
767
- * - All @ComputedAsync-decorated property scopes (including StoreAsync)
768
- * - All @Destroy-marked properties (calls .Destroy() method)
769
- * - All @Watch subscriptions (via scope destruction)
770
- *
771
- * @see {@link Bound.All} for initialization counterpart
772
- * @see {@link Component.Destroy} for component lifecycle
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;
@@ -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 (async) | @ComputedAsync | Async with caching + object reuse |
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, defaultValue, store) {
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("") // Overhead: creates StoreSync, watch cycle, diff computation
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(defaultValue) {
247
+ function Computed() {
248
248
  return function (target, propertyKey, descriptor) {
249
- return ComputedDecorator(target, propertyKey, descriptor, defaultValue);
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, defaultValue) {
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), undefined, new Store_1.StoreSync());
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 asynchronous computed properties with caching.
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
- * Use @ComputedAsync for expensive async operations (API calls, file reads, database queries).
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: Async operation with caching
294
+ * // ✅ Good: Synchronous getter with StoreAsync caching
291
295
  * @Value()
292
296
  * userId: string = "123";
293
297
  *
294
298
  * @ComputedAsync(null)
295
- * async getUser(): Promise<User | null> {
296
- * // Expensive: API call, cached until userId changes
297
- * return await fetch(`/api/users/${this.userId}`);
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
- * // ❌ Avoid: Simple/synchronous computation - use @Computed() instead
307
+ * // ❌ WRONG - Do NOT use async keyword or return Promise
304
308
  * @Value()
305
- * count: number = 0;
309
+ * userId: string = "123";
306
310
  *
307
- * @ComputedAsync(0) // Overhead: async wrapper, StoreAsync
308
- * get doubled(): number {
309
- * return this.count * 2; // Sync operation
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
- * @Computed(0) // Better: synchronous caching
313
- * get doubled(): number {
314
- * return this.count * 2;
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 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.
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
- * async getUser(): Promise<User> {
330
- * return fetch('/api/user');
341
+ * get userData(): User | null {
342
+ * return getUserSync(this.userId);
331
343
  * }
332
344
  *
333
- * // Scopes created here, on first access:
334
- * const user = await this.getUser;
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 async getter only runs when dependencies change.
350
+ * through diff-based updates. The getter only runs when dependencies change.
339
351
  *
340
- * **Object Reuse**: Like @Computed, the resolved value maintains its identity across updates.
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
- * **Important**: Only use this for truly async operations. For sync computations, use @Computed()
344
- * which has less overhead and provides synchronous values.
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/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
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), defaultValue, new Store_1.StoreAsync());
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([]) // Better: cached + object reuse via StoreSync
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 async computed properties
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
- * // 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
- */
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "j-templates",
3
- "version": "7.0.81",
3
+ "version": "7.0.82",
4
4
  "description": "j-templates",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/TypesInCode/jTemplates",