j-templates 7.0.84 → 7.0.86

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/Node/vNode.js CHANGED
@@ -174,7 +174,7 @@ function CreateChildrenScope(vnode, children, data = DefaultData) {
174
174
  if ((0, functions_1.IsAsync)(data)) {
175
175
  const asyncData = data;
176
176
  data = function () {
177
- return (0, observableScope_1.CalcScope)(async function () {
177
+ return (0, observableScope_1.InlineScope)(async function () {
178
178
  return asyncData();
179
179
  });
180
180
  };
@@ -35,8 +35,8 @@ interface IDynamicObservableScope<T> {
35
35
  emitters: (Emitter | null)[];
36
36
  /** Emitter for notifying when the scope is destroyed */
37
37
  onDestroyed: Emitter | null;
38
- /** Map of nested calc scopes created during this scope's execution */
39
- calcScopes: {
38
+ /** Map of nested scopes created during this scope's execution */
39
+ scopes: {
40
40
  [id: string]: IObservableScope<unknown> | null;
41
41
  } | null;
42
42
  }
@@ -46,21 +46,62 @@ interface IDynamicObservableScope<T> {
46
46
  */
47
47
  export type IObservableScope<T> = IStaticObservableScope<T> | IDynamicObservableScope<T>;
48
48
  /**
49
- * Creates a computed scope that acts as a gatekeeper for parent scope emissions.
50
- * If this scope's value doesn't change (=== comparison) it won't emit.
49
+ * Creates an inline computed scope registered as a dependency of the parent.
50
+ * The scope is memoized by ID and reused across reads within the same parent evaluation.
51
+ * Emits on every recomputation — no === gating.
51
52
  *
52
- * Useful for optimizing expensive operations driven by key values (e.g., sort keys).
53
- * Scopes are memoized by ID and reused across multiple reads within a single parent scope evaluation.
54
- * Defaults to "default" ID - provide custom IDs when using multiple calc scopes.
53
+ * Supports async callbacks Promise<T> is resolved and the resolved value is emitted.
55
54
  *
56
55
  * Only works within a watch context (during another scope's execution).
56
+ * Throws if called outside a watch context.
57
+ * Creates non-greedy scopes that emit immediately without === gating.
58
+ * @template T The type of value returned by the callback (extracted from Promise if async).
59
+ * @param callback The function to compute the derived value.
60
+ * @param idOverride Optional custom ID for memoization when using multiple scope calls.
61
+ * @returns The computed value, reusing existing scope if available.
62
+ * @throws Error if called outside a watch context.
63
+ */
64
+ export declare function InlineScope<T>(callback: () => T | Promise<T>, idOverride?: string): T;
65
+ /**
66
+ * Creates a gatekeeper scope that only emits when the value actually changes (===).
67
+ * Prevents unnecessary downstream re-evaluations when the derived value stays the same.
68
+ *
69
+ * Useful for optimizing expensive operations driven by frequently-changing parent state.
70
+ * Scopes are memoized by ID and reused across reads within the same parent evaluation.
71
+ *
72
+ * Supports async callbacks — Promise<T> is resolved and the resolved value is gated.
73
+ *
74
+ * Only works within a watch context (during another scope's execution).
75
+ * Throws if called outside a watch context.
57
76
  * Always creates greedy scopes that batch updates via microtask queue.
58
- * @template T The type of value returned by the callback.
77
+ * @template T The type of value returned by the callback (extracted from Promise if async).
78
+ * @param callback The function to compute the derived value.
79
+ * @param idOverride Optional custom ID for memoization when using multiple gate calls.
80
+ * @returns The computed value, reusing existing scope if available.
81
+ * @throws Error if called outside a watch context.
82
+ */
83
+ export declare function GateScope<T>(callback: () => T | Promise<T>, idOverride?: string): T;
84
+ /**
85
+ * Creates a computed scope that isn't registered as a dependency with the parent.
86
+ * Use this function to read reactive data without subscribing to changes
87
+ * to that data.
88
+ *
89
+ * Unlike GateScope, PeekScope does not register itself as a dependency, meaning
90
+ * changes to the data accessed within the callback will not trigger recomputation
91
+ * of the parent scope. The scope is still memoized by ID within the watch context
92
+ * to avoid redundant computation during the same evaluation.
93
+ *
94
+ * Supports async callbacks — Promise<T> is resolved and the resolved value is returned.
95
+ *
96
+ * Only works within a watch context (during another scope's execution).
97
+ * Throws if called outside a watch context.
98
+ * @template T The type of value returned by the callback (extracted from Promise if async).
59
99
  * @param callback The function to compute the derived value.
60
- * @param idOverride Optional custom ID for memoization when using multiple calc scopes.
100
+ * @param idOverride Optional custom ID for memoization when using multiple peek calls.
61
101
  * @returns The computed value, reusing existing scope if available.
102
+ * @throws Error if called outside a watch context.
62
103
  */
63
- export declare function CalcScope<T>(callback: () => T, idOverride?: string): T;
104
+ export declare function PeekScope<T>(callback: () => T | Promise<T>, idOverride?: string): T;
64
105
  export declare namespace ObservableScope {
65
106
  /**
66
107
  * Creates a new observable scope from a value function.
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ObservableScope = void 0;
4
- exports.CalcScope = CalcScope;
4
+ exports.InlineScope = InlineScope;
5
+ exports.GateScope = GateScope;
6
+ exports.PeekScope = PeekScope;
5
7
  const emitter_1 = require("../../Utils/emitter");
6
8
  const functions_1 = require("../../Utils/functions");
7
9
  /**
@@ -26,7 +28,7 @@ function CreateDynamicScope(getFunction, greedy, value) {
26
28
  emitter: emitter_1.Emitter.Create(),
27
29
  emitters: null,
28
30
  onDestroyed: null,
29
- calcScopes: null,
31
+ scopes: null,
30
32
  };
31
33
  scope.setCallback = OnSet.bind(scope);
32
34
  return scope;
@@ -189,7 +191,7 @@ let watchState = null;
189
191
  * Executes a callback while tracking all scope and emitter dependencies.
190
192
  * Creates a watch context that records what was accessed during execution.
191
193
  * @param callback The function to execute while tracking dependencies.
192
- * @param currentCalc Optional map of existing calc scopes to reuse.
194
+ * @param currentCalc Optional map of existing inline scopes to reuse.
193
195
  * @returns The watch state containing tracked dependencies and result.
194
196
  */
195
197
  function WatchFunction(callback, currentCalc, initialEmitters) {
@@ -220,17 +222,21 @@ function WatchFunction(callback, currentCalc, initialEmitters) {
220
222
  */
221
223
  function ExecuteScope(scope) {
222
224
  scope.dirty = false;
223
- const state = WatchFunction(scope.getFunction, scope.calcScopes, scope.emitters);
225
+ const state = WatchFunction(scope.getFunction, scope.scopes, scope.emitters);
224
226
  UpdateEmitters(scope, state);
225
227
  const calcScopes = state.currentCalc;
226
- scope.calcScopes = state.nextCalc;
228
+ scope.scopes = state.nextCalc;
227
229
  for (const key in calcScopes)
228
230
  DestroyScope(calcScopes[key]);
229
- if (scope.async)
230
- state.value.then(function (result) {
231
+ if (scope.async) {
232
+ const promise = state.value;
233
+ promise.then(function (result) {
234
+ if (state.value !== promise)
235
+ return;
231
236
  scope.value = result;
232
237
  emitter_1.Emitter.Emit(scope.emitter, scope);
233
238
  });
239
+ }
234
240
  else
235
241
  scope.value = state.value;
236
242
  }
@@ -247,48 +253,112 @@ function ExecuteFunction(callback, greedy, allowStatic) {
247
253
  const state = WatchFunction(callback, null, null);
248
254
  if (!allowStatic || async || state.emitters !== null) {
249
255
  const scope = CreateDynamicScope(callback, greedy, async ? null : state.value);
250
- scope.calcScopes = state.nextCalc;
256
+ scope.scopes = state.nextCalc;
251
257
  UpdateEmitters(scope, state);
252
- if (async)
253
- state.value.then(function (result) {
258
+ if (async) {
259
+ const promise = state.value;
260
+ promise.then(function (result) {
261
+ if (state.value !== promise)
262
+ return;
254
263
  scope.value = result;
255
264
  emitter_1.Emitter.Emit(scope.emitter, scope);
256
265
  });
266
+ }
257
267
  return scope;
258
268
  }
259
269
  const value = state.value;
260
270
  return CreateStaticScope(value);
261
271
  }
272
+ function ScopeHelper(callback, id, greedy) {
273
+ const nextScopes = (watchState.nextCalc ??= {});
274
+ if (nextScopes[id])
275
+ return nextScopes[id];
276
+ const currentScopes = watchState.currentCalc;
277
+ nextScopes[id] =
278
+ currentScopes?.[id] ?? ExecuteFunction(callback, greedy, true);
279
+ if (currentScopes?.[id])
280
+ delete currentScopes[id];
281
+ return nextScopes[id];
282
+ }
262
283
  /**
263
- * Creates a computed scope that acts as a gatekeeper for parent scope emissions.
264
- * If this scope's value doesn't change (=== comparison) it won't emit.
284
+ * Creates an inline computed scope registered as a dependency of the parent.
285
+ * The scope is memoized by ID and reused across reads within the same parent evaluation.
286
+ * Emits on every recomputation — no === gating.
287
+ *
288
+ * Supports async callbacks — Promise<T> is resolved and the resolved value is emitted.
289
+ *
290
+ * Only works within a watch context (during another scope's execution).
291
+ * Throws if called outside a watch context.
292
+ * Creates non-greedy scopes that emit immediately without === gating.
293
+ * @template T The type of value returned by the callback (extracted from Promise if async).
294
+ * @param callback The function to compute the derived value.
295
+ * @param idOverride Optional custom ID for memoization when using multiple scope calls.
296
+ * @returns The computed value, reusing existing scope if available.
297
+ * @throws Error if called outside a watch context.
298
+ */
299
+ function InlineScope(callback, idOverride) {
300
+ if (watchState === null) {
301
+ throw new Error("scope() must be called within a watch context");
302
+ }
303
+ const id = idOverride ?? "scope_default";
304
+ const scope = ScopeHelper(callback, id, false);
305
+ RegisterScope(scope);
306
+ return GetScopeValue(scope);
307
+ }
308
+ /**
309
+ * Creates a gatekeeper scope that only emits when the value actually changes (===).
310
+ * Prevents unnecessary downstream re-evaluations when the derived value stays the same.
311
+ *
312
+ * Useful for optimizing expensive operations driven by frequently-changing parent state.
313
+ * Scopes are memoized by ID and reused across reads within the same parent evaluation.
265
314
  *
266
- * Useful for optimizing expensive operations driven by key values (e.g., sort keys).
267
- * Scopes are memoized by ID and reused across multiple reads within a single parent scope evaluation.
268
- * Defaults to "default" ID - provide custom IDs when using multiple calc scopes.
315
+ * Supports async callbacks Promise<T> is resolved and the resolved value is gated.
269
316
  *
270
317
  * Only works within a watch context (during another scope's execution).
318
+ * Throws if called outside a watch context.
271
319
  * Always creates greedy scopes that batch updates via microtask queue.
272
- * @template T The type of value returned by the callback.
320
+ * @template T The type of value returned by the callback (extracted from Promise if async).
273
321
  * @param callback The function to compute the derived value.
274
- * @param idOverride Optional custom ID for memoization when using multiple calc scopes.
322
+ * @param idOverride Optional custom ID for memoization when using multiple gate calls.
275
323
  * @returns The computed value, reusing existing scope if available.
324
+ * @throws Error if called outside a watch context.
276
325
  */
277
- function CalcScope(callback, idOverride) {
278
- if (watchState === null)
279
- return callback();
280
- const nextScopes = (watchState.nextCalc ??= {});
281
- const id = idOverride ?? "default";
282
- if (nextScopes[id]) {
283
- RegisterScope(nextScopes[id]);
284
- return GetScopeValue(nextScopes[id]);
326
+ function GateScope(callback, idOverride) {
327
+ if (watchState === null) {
328
+ throw new Error("gate() must be called within a watch context");
285
329
  }
286
- const currentScopes = watchState.currentCalc;
287
- nextScopes[id] = currentScopes?.[id] ?? ExecuteFunction(callback, true, true);
288
- if (currentScopes?.[id])
289
- delete currentScopes[id];
290
- RegisterScope(nextScopes[id]);
291
- return GetScopeValue(nextScopes[id]);
330
+ const id = idOverride ?? "gate_default";
331
+ const scope = ScopeHelper(callback, id, true);
332
+ RegisterScope(scope);
333
+ return GetScopeValue(scope);
334
+ }
335
+ /**
336
+ * Creates a computed scope that isn't registered as a dependency with the parent.
337
+ * Use this function to read reactive data without subscribing to changes
338
+ * to that data.
339
+ *
340
+ * Unlike GateScope, PeekScope does not register itself as a dependency, meaning
341
+ * changes to the data accessed within the callback will not trigger recomputation
342
+ * of the parent scope. The scope is still memoized by ID within the watch context
343
+ * to avoid redundant computation during the same evaluation.
344
+ *
345
+ * Supports async callbacks — Promise<T> is resolved and the resolved value is returned.
346
+ *
347
+ * Only works within a watch context (during another scope's execution).
348
+ * Throws if called outside a watch context.
349
+ * @template T The type of value returned by the callback (extracted from Promise if async).
350
+ * @param callback The function to compute the derived value.
351
+ * @param idOverride Optional custom ID for memoization when using multiple peek calls.
352
+ * @returns The computed value, reusing existing scope if available.
353
+ * @throws Error if called outside a watch context.
354
+ */
355
+ function PeekScope(callback, idOverride) {
356
+ if (watchState === null) {
357
+ throw new Error("peek() must be called within a watch context");
358
+ }
359
+ const id = idOverride ?? "peek_default";
360
+ const scope = ScopeHelper(callback, id, false);
361
+ return GetScopeValue(scope);
292
362
  }
293
363
  /**
294
364
  * Updates a scope's dependency emitters using efficient diffing.
@@ -350,12 +420,12 @@ function DestroyScope(scope) {
350
420
  if (!scope || scope.type === "static")
351
421
  return;
352
422
  emitter_1.Emitter.Clear(scope.emitter);
353
- for (const key in scope.calcScopes)
354
- DestroyScope(scope.calcScopes[key]);
423
+ for (const key in scope.scopes)
424
+ DestroyScope(scope.scopes[key]);
355
425
  for (let x = 0; x < scope.emitters.length; x++)
356
426
  emitter_1.Emitter.Remove(scope.emitters[x], scope.setCallback);
357
427
  scope.value = undefined;
358
- scope.calcScopes = null;
428
+ scope.scopes = null;
359
429
  scope.emitters = null;
360
430
  scope.emitter = null;
361
431
  scope.getFunction = null;
@@ -21,7 +21,7 @@
21
21
  * | getters only (expensive) | @Computed | Cached + object reuse via Store |
22
22
  * | getters only (simple) | @Scope | Cached, but new reference on update |
23
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
+ * | async functions (direct scope) | ObservableScope.Create(async) | Direct async, new reference, initial null |
25
25
  * | subscribe to changes | @Watch | Calls method when scope value changes |
26
26
  * | dependency injection | @Inject | Gets value from component injector |
27
27
  * | cleanup on destroy | @Destroy| Calls .Destroy() on component teardown |
@@ -233,7 +233,7 @@ export declare function Computed<T extends WeakKey, K extends keyof T, D extends
233
233
  * Both use synchronous getters and provide object identity preservation.
234
234
  *
235
235
  * **Comparison**:
236
- * | Aspect | @Scope | @Computed | @ComputedAsync | calc(async) + @Scope |
236
+ * | Aspect | @Scope | @Computed | @ComputedAsync | scope(async) + @Scope |
237
237
  * |--------|--------|-----------|----------------|----------------------|
238
238
  * | Caches value | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes (memoized) |
239
239
  * | Getter type | Sync | Sync | Sync | Async supported |
@@ -461,15 +461,15 @@ export declare function Value(): any;
461
461
  * - Object identity matters (array/object references used in templates)
462
462
  * - You need DOM reference preservation to avoid re-renders
463
463
  *
464
- * **Async pattern with @Scope**: Use `calc(async () => ...)` for async operations:
464
+ * **Async pattern with @Scope**: Use `scope(async () => ...)` for async operations:
465
465
  * ```typescript
466
466
  * @Scope()
467
467
  * get CurrentUser() {
468
- * return calc(async () => fetchUser(`/api/user/${this.userId}`));
468
+ * return scope(async () => fetchUser(`/api/user/${this.userId}`));
469
469
  * }
470
470
  * ```
471
- * - `calc` memoizes async operations with ID-based caching
472
- * - Returns Promise initially, resolves when complete
471
+ * - `scope` creates an inline computed scope that resolves the async operation
472
+ * - Returns the resolved value (not a Promise)
473
473
  * - Automatically batches updates via microtask queue
474
474
  * - New reference on each update (no object reuse)
475
475
  *
@@ -486,7 +486,7 @@ export declare function Value(): any;
486
486
  * @see {@link ComputedAsync} for sync getters with StoreAsync backend
487
487
  * @see {@link ObservableNode.ApplyDiff} for how @Computed maintains object identity
488
488
  * @see {@link ObservableScope} for the scope-based reactivity system
489
- * @see {@link calc} for memoized async operations within @Scope
489
+ * @see {@link scope} for inline computed scopes within @Scope
490
490
  */
491
491
  export declare function Scope(): typeof ScopeDecorator;
492
492
  /**
@@ -22,7 +22,7 @@
22
22
  * | getters only (expensive) | @Computed | Cached + object reuse via Store |
23
23
  * | getters only (simple) | @Scope | Cached, but new reference on update |
24
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
+ * | async functions (direct scope) | ObservableScope.Create(async) | Direct async, new reference, initial null |
26
26
  * | subscribe to changes | @Watch | Calls method when scope value changes |
27
27
  * | dependency injection | @Inject | Gets value from component injector |
28
28
  * | cleanup on destroy | @Destroy| Calls .Destroy() on component teardown |
@@ -136,10 +136,14 @@ function GetDestroyArrayForPrototype(prototype, create = true) {
136
136
  function CreateComputedScope(getter, store, defaultValue) {
137
137
  const getterScope = observableScope_1.ObservableScope.Create(getter, true);
138
138
  observableScope_1.ObservableScope.Watch(getterScope, (scope) => {
139
- const data = observableScope_1.ObservableScope.Value(scope);
139
+ const data = observableScope_1.ObservableScope.Peek(scope);
140
140
  store.Write(data, "root");
141
141
  });
142
- const propertyScope = observableScope_1.ObservableScope.Create(() => store.Get("root", defaultValue));
142
+ const data = observableScope_1.ObservableScope.Peek(getterScope);
143
+ store.Write(data, "root");
144
+ const propertyScope = observableScope_1.ObservableScope.Create(() => {
145
+ return store.Get("root", defaultValue);
146
+ });
143
147
  observableScope_1.ObservableScope.OnDestroyed(propertyScope, function () {
144
148
  observableScope_1.ObservableScope.Destroy(getterScope);
145
149
  if (store instanceof Store_1.StoreAsync)
@@ -356,7 +360,7 @@ function ComputedDecorator(target, prop, descriptor) {
356
360
  * Both use synchronous getters and provide object identity preservation.
357
361
  *
358
362
  * **Comparison**:
359
- * | Aspect | @Scope | @Computed | @ComputedAsync | calc(async) + @Scope |
363
+ * | Aspect | @Scope | @Computed | @ComputedAsync | scope(async) + @Scope |
360
364
  * |--------|--------|-----------|----------------|----------------------|
361
365
  * | Caches value | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes (memoized) |
362
366
  * | Getter type | Sync | Sync | Sync | Async supported |
@@ -684,15 +688,15 @@ function ValueDecorator(target, propertyKey) {
684
688
  * - Object identity matters (array/object references used in templates)
685
689
  * - You need DOM reference preservation to avoid re-renders
686
690
  *
687
- * **Async pattern with @Scope**: Use `calc(async () => ...)` for async operations:
691
+ * **Async pattern with @Scope**: Use `scope(async () => ...)` for async operations:
688
692
  * ```typescript
689
693
  * @Scope()
690
694
  * get CurrentUser() {
691
- * return calc(async () => fetchUser(`/api/user/${this.userId}`));
695
+ * return scope(async () => fetchUser(`/api/user/${this.userId}`));
692
696
  * }
693
697
  * ```
694
- * - `calc` memoizes async operations with ID-based caching
695
- * - Returns Promise initially, resolves when complete
698
+ * - `scope` creates an inline computed scope that resolves the async operation
699
+ * - Returns the resolved value (not a Promise)
696
700
  * - Automatically batches updates via microtask queue
697
701
  * - New reference on each update (no object reuse)
698
702
  *
@@ -709,7 +713,7 @@ function ValueDecorator(target, propertyKey) {
709
713
  * @see {@link ComputedAsync} for sync getters with StoreAsync backend
710
714
  * @see {@link ObservableNode.ApplyDiff} for how @Computed maintains object identity
711
715
  * @see {@link ObservableScope} for the scope-based reactivity system
712
- * @see {@link calc} for memoized async operations within @Scope
716
+ * @see {@link scope} for inline computed scopes within @Scope
713
717
  */
714
718
  function Scope() {
715
719
  return ScopeDecorator;
package/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { Component } from './Node/component';
2
- export { CalcScope as calc } from './Store/Tree/observableScope';
2
+ export { InlineScope as scope, GateScope as gate, PeekScope as peek } from './Store/Tree/observableScope';
package/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.calc = exports.Component = void 0;
3
+ exports.peek = exports.gate = exports.scope = exports.Component = void 0;
4
4
  var component_1 = require("./Node/component");
5
5
  Object.defineProperty(exports, "Component", { enumerable: true, get: function () { return component_1.Component; } });
6
6
  var observableScope_1 = require("./Store/Tree/observableScope");
7
- Object.defineProperty(exports, "calc", { enumerable: true, get: function () { return observableScope_1.CalcScope; } });
7
+ Object.defineProperty(exports, "scope", { enumerable: true, get: function () { return observableScope_1.InlineScope; } });
8
+ Object.defineProperty(exports, "gate", { enumerable: true, get: function () { return observableScope_1.GateScope; } });
9
+ Object.defineProperty(exports, "peek", { enumerable: true, get: function () { return observableScope_1.PeekScope; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "j-templates",
3
- "version": "7.0.84",
3
+ "version": "7.0.86",
4
4
  "description": "j-templates",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/TypesInCode/jTemplates",