j-templates 8.0.19 → 8.0.21

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.
@@ -92,26 +92,24 @@ export declare function MappedScope<D, T>(data: D, callback: (data: D) => T | Pr
92
92
  */
93
93
  export declare function GateScope<T>(callback: () => T | Promise<T>, idOverride?: string): T;
94
94
  /**
95
- * Creates a computed scope that isn't registered as a dependency with the parent.
95
+ * Invokes a callback with dependency tracking suspended.
96
96
  * Use this function to read reactive data without subscribing to changes
97
97
  * to that data.
98
98
  *
99
- * Unlike GateScope, PeekScope does not register itself as a dependency, meaning
100
- * changes to the data accessed within the callback will not trigger recomputation
101
- * of the parent scope. The scope is still memoized by ID within the watch context
102
- * to avoid redundant computation during the same evaluation.
99
+ * No scope is created: the callback runs immediately on every call and nothing
100
+ * is memoized. Reactive values read inside the callback are not registered with
101
+ * the current watch context, so changes to them will not trigger recomputation
102
+ * of the parent scope. The previous watch context is restored afterwards, even
103
+ * if the callback throws.
103
104
  *
104
- * Supports async callbacks — Promise<T> is resolved and the resolved value is returned.
105
- *
106
- * Only works within a watch context (during another scope's execution).
107
- * Throws if called outside a watch context.
108
- * @template T The type of value returned by the callback (extracted from Promise if async).
109
- * @param callback The function to compute the derived value.
110
- * @param idOverride Optional custom ID for memoization when using multiple peek calls.
111
- * @returns The computed value, reusing existing scope if available.
112
- * @throws Error if called outside a watch context.
105
+ * Can be called anywhere, inside or outside a watch context. Because tracking is
106
+ * suspended, InlineScope, GateScope and MappedScope throw if called from within
107
+ * the callback. Async callbacks are not awaited; the Promise is returned as-is.
108
+ * @template T The type of value returned by the callback.
109
+ * @param callback The function to invoke without dependency tracking.
110
+ * @returns The value returned by the callback.
113
111
  */
114
- export declare function PeekScope<T>(callback: () => T | Promise<T>, idOverride?: string): T;
112
+ export declare function PeekCallback<T>(callback: () => T): T;
115
113
  export declare namespace ObservableScope {
116
114
  /**
117
115
  * Creates a new observable scope from a value function.
@@ -178,6 +176,8 @@ export declare namespace ObservableScope {
178
176
  /**
179
177
  * Subscribes to changes on a dynamic scope.
180
178
  * @template T The type of value stored in the scope.
179
+ * The callback must not cause the same scope to emit again (for example by calling
180
+ * `Update(scope)`): re-entrant emits throw.
181
181
  * @param scope The scope to watch for changes.
182
182
  * @param callback Function to invoke when the scope's value changes.
183
183
  */
@@ -268,11 +268,7 @@ function ExecuteScope(scope) {
268
268
  }
269
269
  }
270
270
  function ExecuteBasicScope(scope) {
271
- const parent = watchState;
272
- watchState = null;
273
- const result = scope.getFunction();
274
- watchState = parent;
275
- return result;
271
+ return PeekCallback(scope.getFunction);
276
272
  }
277
273
  /**
278
274
  * Creates a scope from a function, choosing between static and dynamic based on dependencies.
@@ -355,7 +351,6 @@ function MappedScopeHelper(callback, data, onUpdated, onDestroyed) {
355
351
  }
356
352
  const SCOPE_DEFAULT = Symbol("SCOPE_DEFAULT");
357
353
  const GATE_DEFAULT = Symbol("GATE_DEFAULT");
358
- const PEEK_DEFAULT = Symbol("PEEK_DEFAULT");
359
354
  /**
360
355
  * Creates an inline computed scope registered as a dependency of the parent.
361
356
  * The scope is memoized by ID and reused across reads within the same parent evaluation.
@@ -417,32 +412,32 @@ export function GateScope(callback, idOverride) {
417
412
  return GetScopeValue(scope);
418
413
  }
419
414
  /**
420
- * Creates a computed scope that isn't registered as a dependency with the parent.
415
+ * Invokes a callback with dependency tracking suspended.
421
416
  * Use this function to read reactive data without subscribing to changes
422
417
  * to that data.
423
418
  *
424
- * Unlike GateScope, PeekScope does not register itself as a dependency, meaning
425
- * changes to the data accessed within the callback will not trigger recomputation
426
- * of the parent scope. The scope is still memoized by ID within the watch context
427
- * to avoid redundant computation during the same evaluation.
419
+ * No scope is created: the callback runs immediately on every call and nothing
420
+ * is memoized. Reactive values read inside the callback are not registered with
421
+ * the current watch context, so changes to them will not trigger recomputation
422
+ * of the parent scope. The previous watch context is restored afterwards, even
423
+ * if the callback throws.
428
424
  *
429
- * Supports async callbacks — Promise<T> is resolved and the resolved value is returned.
430
- *
431
- * Only works within a watch context (during another scope's execution).
432
- * Throws if called outside a watch context.
433
- * @template T The type of value returned by the callback (extracted from Promise if async).
434
- * @param callback The function to compute the derived value.
435
- * @param idOverride Optional custom ID for memoization when using multiple peek calls.
436
- * @returns The computed value, reusing existing scope if available.
437
- * @throws Error if called outside a watch context.
425
+ * Can be called anywhere, inside or outside a watch context. Because tracking is
426
+ * suspended, InlineScope, GateScope and MappedScope throw if called from within
427
+ * the callback. Async callbacks are not awaited; the Promise is returned as-is.
428
+ * @template T The type of value returned by the callback.
429
+ * @param callback The function to invoke without dependency tracking.
430
+ * @returns The value returned by the callback.
438
431
  */
439
- export function PeekScope(callback, idOverride) {
440
- if (watchState === null) {
441
- throw new Error("peek() must be called within a watch context");
432
+ export function PeekCallback(callback) {
433
+ const parent = watchState;
434
+ watchState = null;
435
+ try {
436
+ return callback();
437
+ }
438
+ finally {
439
+ watchState = parent;
442
440
  }
443
- const id = idOverride ?? PEEK_DEFAULT;
444
- const scope = ScopeHelper(callback, id, false);
445
- return GetScopeValue(scope);
446
441
  }
447
442
  /**
448
443
  * Updates a scope's dependency emitters using efficient diffing.
@@ -621,6 +616,8 @@ export var ObservableScope;
621
616
  /**
622
617
  * Subscribes to changes on a dynamic scope.
623
618
  * @template T The type of value stored in the scope.
619
+ * The callback must not cause the same scope to emit again (for example by calling
620
+ * `Update(scope)`): re-entrant emits throw.
624
621
  * @param scope The scope to watch for changes.
625
622
  * @param callback Function to invoke when the scope's value changes.
626
623
  */
@@ -39,13 +39,13 @@
39
39
  * | Aspect | @Computed | @Scope |
40
40
  * |--------|-----------|--------|
41
41
  * | Caching | ✅ Yes | ✅ Yes |
42
- * | Object identity preserved | ✅ Yes (ApplyDiff) | ❌ No (new object) |
42
+ * | Object identity preserved | ✅ Yes (Apply) | ❌ No (new object) |
43
43
  * | Overhead | Store + diff | Single scope |
44
44
  * | Best for | Complex objects | Primitives/simple values |
45
45
  *
46
46
  * Object Identity (Key Difference):
47
47
  * ---------------------------------
48
- * @Computed: Same object reference, in-place updates via ObservableNode.ApplyDiff
48
+ * @Computed: Same object reference, in-place updates via ObservableNode.Apply
49
49
  * @Scope: New object reference on every update
50
50
  *
51
51
  * Example:
@@ -104,13 +104,16 @@ import { Injector } from "./injector";
104
104
  * @remarks
105
105
  * The @Computed decorator uses a Gated getter scope driving a persistent observable node:
106
106
  * 1. Getter scope (Gated): Re-evaluates the getter when dependencies change, batched via
107
- * the microtask queue. If the result deep-equals the previous value, no update is emitted.
107
+ * the microtask queue, and returns a clone of the result. A watcher on this scope
108
+ * applies each new clone to the node.
108
109
  * 2. ObservableNode.Apply: Diffs the fresh value against the node's current root and applies
109
- * only the changed paths in-place, preserving the node's object identity.
110
- * 3. Downstream notification: Per-property scopes are fired for each changed path, so scopes
111
- * reading sub-properties update directly. If the diff collapses to a full root replacement
112
- * (type change or removed keys), the root value gets a new identity and the Gated scope
113
- * emits, notifying top-level subscribers.
110
+ * only the changed paths in-place, preserving the node's object identity. If the result
111
+ * deep-equals the previous value, nothing is changed and nothing is emitted.
112
+ * 3. Downstream notification: Per-property emitters fire for each changed path, so scopes
113
+ * reading sub-properties update directly. The property itself is backed by a separate
114
+ * scope that reads the node's root; it emits, notifying top-level subscribers, only when
115
+ * the diff collapses to a full root replacement (type change or removed keys) and the
116
+ * root value gets a new identity.
114
117
  *
115
118
  * **Initialization**: @Computed uses lazy initialization - the scopes are created on first access:
116
119
  * ```typescript
@@ -127,7 +130,7 @@ import { Injector } from "./injector";
127
130
  * dependencies change. The key difference is what happens during re-evaluation:
128
131
  *
129
132
  * **Key benefit - Object Reuse**: The returned object maintains its identity across updates.
130
- * Only the changed properties are modified in-place using ObservableNode.ApplyDiff. This enables:
133
+ * Only the changed properties are modified in-place using ObservableNode.Apply. This enables:
131
134
  * - DOM reference preservation (no unnecessary re-renders)
132
135
  * - Existing proxy reuse (no proxy recreation overhead)
133
136
  * - Identity checks (===) remain stable across updates
@@ -156,7 +159,7 @@ import { Injector } from "./injector";
156
159
  * @see {@link Scope} for simple getter-based reactive properties (caches but new reference)
157
160
  * @see {@link ComputedAsync} for async computed properties
158
161
  * @see {@link ObservableNode.Apply} for how diffs are applied in-place to maintain object identity
159
- * @see {@link ObservableScope} for the Gated scope mechanism (batched, ===-gated updates)
162
+ * @see {@link ObservableScope} for the Gated scope mechanism (batched updates)
160
163
  * @see {@link StoreSync} for the store-backed variant used by @ComputedAsync
161
164
  */
162
165
  export declare function Computed<T extends WeakKey, K extends keyof T, D extends T[K]>(): (target: T, propertyKey: K, descriptor: PropertyDescriptor) => PropertyDescriptor;
@@ -488,7 +491,7 @@ export declare function Value(): any;
488
491
  *
489
492
  * @see {@link Computed} for cached computed properties with object reuse
490
493
  * @see {@link ComputedAsync} for sync getters with StoreAsync backend
491
- * @see {@link ObservableNode.ApplyDiff} for how @Computed maintains object identity
494
+ * @see {@link ObservableNode.Apply} for how @Computed maintains object identity
492
495
  * @see {@link ObservableScope} for the scope-based reactivity system
493
496
  * @see {@link scope} for inline computed scopes within @Scope
494
497
  */
@@ -39,13 +39,13 @@
39
39
  * | Aspect | @Computed | @Scope |
40
40
  * |--------|-----------|--------|
41
41
  * | Caching | ✅ Yes | ✅ Yes |
42
- * | Object identity preserved | ✅ Yes (ApplyDiff) | ❌ No (new object) |
42
+ * | Object identity preserved | ✅ Yes (Apply) | ❌ No (new object) |
43
43
  * | Overhead | Store + diff | Single scope |
44
44
  * | Best for | Complex objects | Primitives/simple values |
45
45
  *
46
46
  * Object Identity (Key Difference):
47
47
  * ---------------------------------
48
- * @Computed: Same object reference, in-place updates via ObservableNode.ApplyDiff
48
+ * @Computed: Same object reference, in-place updates via ObservableNode.Apply
49
49
  * @Scope: New object reference on every update
50
50
  *
51
51
  * Example:
@@ -56,7 +56,7 @@
56
56
  * console.log(list === list2); // @Computed: true, @Scope: false
57
57
  * ```
58
58
  */
59
- import { ObservableScope, } from "../Store/Tree/observableScope";
59
+ import { ObservableScope, PeekCallback, } from "../Store/Tree/observableScope";
60
60
  import { ObservableNode } from "../Store/Tree/observableNode";
61
61
  import { StoreAsync } from "../Store";
62
62
  /**
@@ -144,29 +144,43 @@ function CreateStoreScope(getter, store, defaultValue) {
144
144
  return propertyScope;
145
145
  }
146
146
  /**
147
- * Creates a Gated observable scope that exposes a getter's value through a persistent
147
+ * Creates an observable scope that exposes a getter's value through a persistent
148
148
  * observable node, preserving the value's object identity across updates.
149
149
  *
150
- * On each re-evaluation the getter's value is cloned and merged into the node in-place
151
- * via ObservableNode.Apply:
150
+ * The getter runs in a Gated scope that returns a clone of its value. A watcher on that
151
+ * scope merges each new clone into the node in-place via ObservableNode.Apply, with
152
+ * dependency tracking suspended. The returned scope reads the node's root:
152
153
  * - Content changes (same structure, changed properties) fire only the affected
153
- * per-property scopes - the top-level scope does not emit.
154
+ * per-property emitters - the returned scope does not emit.
154
155
  * - Structural changes (type change or removed keys) replace the root value with a new
155
- * identity, which the Gated scope detects and emits for.
156
- * - Deep-equal results are suppressed entirely by the Gated ===-gating.
156
+ * identity; the root's emitter fires and the returned scope emits.
157
+ * - Deep-equal results change nothing in the node, so nothing downstream is notified.
158
+ *
159
+ * Destroying the returned scope also destroys the Gated getter scope.
157
160
  *
158
161
  * @param getter Function computing the value. May read reactive dependencies.
159
- * @returns The Gated scope whose value is the observable root node.
162
+ * @returns A scope whose value is the observable node's root.
160
163
  */
161
164
  function CreateNodeScope(getter) {
162
- const observableNode = ObservableNode.Create({ root: null });
163
165
  const getterScope = ObservableScope.Gated(function () {
164
166
  const value = getter();
165
167
  const clonedValue = ObservableNode.Clone(value);
166
- ObservableNode.Apply(observableNode, { root: clonedValue });
168
+ return clonedValue;
169
+ });
170
+ const observableNode = ObservableNode.Create({ root: ObservableScope.Peek(getterScope) });
171
+ ObservableScope.Watch(getterScope, function (scope) {
172
+ PeekCallback(function () {
173
+ const value = ObservableScope.Peek(scope);
174
+ ObservableNode.Apply(observableNode, { root: value });
175
+ });
176
+ });
177
+ const nodeScope = ObservableScope.Create(function () {
167
178
  return observableNode.root;
168
179
  });
169
- return getterScope;
180
+ ObservableScope.OnDestroyed(nodeScope, function () {
181
+ ObservableScope.Destroy(getterScope);
182
+ });
183
+ return nodeScope;
170
184
  }
171
185
  /**
172
186
  * Computed decorator factory for creating synchronous computed properties with caching and object reuse.
@@ -214,13 +228,16 @@ function CreateNodeScope(getter) {
214
228
  * @remarks
215
229
  * The @Computed decorator uses a Gated getter scope driving a persistent observable node:
216
230
  * 1. Getter scope (Gated): Re-evaluates the getter when dependencies change, batched via
217
- * the microtask queue. If the result deep-equals the previous value, no update is emitted.
231
+ * the microtask queue, and returns a clone of the result. A watcher on this scope
232
+ * applies each new clone to the node.
218
233
  * 2. ObservableNode.Apply: Diffs the fresh value against the node's current root and applies
219
- * only the changed paths in-place, preserving the node's object identity.
220
- * 3. Downstream notification: Per-property scopes are fired for each changed path, so scopes
221
- * reading sub-properties update directly. If the diff collapses to a full root replacement
222
- * (type change or removed keys), the root value gets a new identity and the Gated scope
223
- * emits, notifying top-level subscribers.
234
+ * only the changed paths in-place, preserving the node's object identity. If the result
235
+ * deep-equals the previous value, nothing is changed and nothing is emitted.
236
+ * 3. Downstream notification: Per-property emitters fire for each changed path, so scopes
237
+ * reading sub-properties update directly. The property itself is backed by a separate
238
+ * scope that reads the node's root; it emits, notifying top-level subscribers, only when
239
+ * the diff collapses to a full root replacement (type change or removed keys) and the
240
+ * root value gets a new identity.
224
241
  *
225
242
  * **Initialization**: @Computed uses lazy initialization - the scopes are created on first access:
226
243
  * ```typescript
@@ -237,7 +254,7 @@ function CreateNodeScope(getter) {
237
254
  * dependencies change. The key difference is what happens during re-evaluation:
238
255
  *
239
256
  * **Key benefit - Object Reuse**: The returned object maintains its identity across updates.
240
- * Only the changed properties are modified in-place using ObservableNode.ApplyDiff. This enables:
257
+ * Only the changed properties are modified in-place using ObservableNode.Apply. This enables:
241
258
  * - DOM reference preservation (no unnecessary re-renders)
242
259
  * - Existing proxy reuse (no proxy recreation overhead)
243
260
  * - Identity checks (===) remain stable across updates
@@ -266,7 +283,7 @@ function CreateNodeScope(getter) {
266
283
  * @see {@link Scope} for simple getter-based reactive properties (caches but new reference)
267
284
  * @see {@link ComputedAsync} for async computed properties
268
285
  * @see {@link ObservableNode.Apply} for how diffs are applied in-place to maintain object identity
269
- * @see {@link ObservableScope} for the Gated scope mechanism (batched, ===-gated updates)
286
+ * @see {@link ObservableScope} for the Gated scope mechanism (batched updates)
270
287
  * @see {@link StoreSync} for the store-backed variant used by @ComputedAsync
271
288
  */
272
289
  export function Computed() {
@@ -733,7 +750,7 @@ function ValueDecorator(target, propertyKey) {
733
750
  *
734
751
  * @see {@link Computed} for cached computed properties with object reuse
735
752
  * @see {@link ComputedAsync} for sync getters with StoreAsync backend
736
- * @see {@link ObservableNode.ApplyDiff} for how @Computed maintains object identity
753
+ * @see {@link ObservableNode.Apply} for how @Computed maintains object identity
737
754
  * @see {@link ObservableScope} for the scope-based reactivity system
738
755
  * @see {@link scope} for inline computed scopes within @Scope
739
756
  */
@@ -1,5 +1,5 @@
1
1
  export type EmitterCallback<T extends readonly any[] = any[]> = (...args: T) => void;
2
- export type Emitter = [number, ...EmitterCallback[]];
2
+ export type Emitter = [[number, number], ...EmitterCallback[]];
3
3
  export declare namespace Emitter {
4
4
  function Create(): Emitter;
5
5
  function On(emitter: Emitter, callback: EmitterCallback): void;
package/Utils/emitter.js CHANGED
@@ -1,8 +1,7 @@
1
- import { RemoveNulls } from "./array";
2
1
  import { _requestIdleCallback } from "./scheduling";
3
2
  const pendingCompactEmitters = new Set();
4
3
  function Compact(emitter) {
5
- if (emitter.length === 1 || emitter[0] === 0)
4
+ if (emitter.length === 1 || emitter[0][0] === 0)
6
5
  return;
7
6
  pendingCompactEmitters.add(emitter);
8
7
  ScheduleCompact();
@@ -22,57 +21,63 @@ function PerformCompact() {
22
21
  CompactEmitter(compactEmitters[x]);
23
22
  }
24
23
  function CompactEmitter(emitter) {
25
- if (emitter[0] === 0)
24
+ if (emitter[0][0] === 0)
26
25
  return;
27
- emitter.splice(1, emitter[0]);
28
- emitter[0] = 0;
26
+ emitter.splice(1, emitter[0][0]);
27
+ emitter[0][0] = 0;
29
28
  }
30
29
  export var Emitter;
31
30
  (function (Emitter) {
32
31
  function Create() {
33
- return [0];
32
+ return [[0, -1]];
34
33
  }
35
34
  Emitter.Create = Create;
36
35
  function On(emitter, callback) {
37
36
  emitter.push(callback);
38
37
  }
39
38
  Emitter.On = On;
40
- const emitterStack = [];
41
- const modifiedDuringEmit = new Set();
39
+ // Re-entry during emit is not supported and considered a bug in client code
42
40
  function Emit(emitter, ...args) {
43
- emitterStack.push(emitter);
44
- for (let x = emitter[0] + 1; x < emitter.length; x++)
45
- emitter[x]?.(...args);
46
- emitterStack.pop();
47
- if (modifiedDuringEmit.delete(emitter)) {
48
- RemoveNulls(emitter);
49
- emitter[0] = 0;
41
+ if (emitter[0][1] !== -1)
42
+ throw new Error("Emitter.Emit: re-entry during emit is not supported");
43
+ try {
44
+ for (let x = emitter[0][1] = emitter[0][0] + 1; x < emitter.length; emitter[0][1] = ++x) {
45
+ emitter[x](...args);
46
+ }
50
47
  }
48
+ finally {
49
+ emitter[0][1] = -1;
50
+ }
51
+ if (emitter[0][0] !== 0)
52
+ Compact(emitter);
51
53
  }
52
54
  Emitter.Emit = Emit;
55
+ function RemoveFromEnd(emitter, index) {
56
+ emitter[index] = emitter[emitter.length - 1];
57
+ emitter.pop();
58
+ }
59
+ function RemoveFromBeginning(emitter, index) {
60
+ const tombstoneSize = emitter[0][0];
61
+ emitter[index] = emitter[tombstoneSize + 1];
62
+ emitter[tombstoneSize + 1] = null;
63
+ emitter[0][0]++;
64
+ }
53
65
  function Remove(emitter, callback) {
54
66
  if (emitter.length === 1)
55
67
  return;
56
- const startIndex = emitter[0] + 1;
68
+ const startIndex = emitter[0][0] + 1;
57
69
  const index = emitter.indexOf(callback, startIndex);
58
70
  if (index < 1)
59
71
  return;
60
- const emitterStackIndex = emitterStack.indexOf(emitter);
61
- if (emitterStackIndex < 0) {
62
- emitter[0]++;
63
- emitter[index] = emitter[startIndex];
64
- emitter[startIndex] = null;
65
- Compact(emitter);
66
- }
67
- else {
68
- emitter[index] = null;
69
- modifiedDuringEmit.add(emitter);
70
- }
72
+ if (index <= emitter[0][1])
73
+ RemoveFromBeginning(emitter, index);
74
+ else
75
+ RemoveFromEnd(emitter, index);
71
76
  }
72
77
  Emitter.Remove = Remove;
73
78
  function Clear(emitter) {
74
79
  emitter.length > 1 && emitter.splice(1);
75
- emitter[0] = 0;
80
+ emitter[0][0] = 0;
76
81
  }
77
82
  Emitter.Clear = Clear;
78
83
  })(Emitter || (Emitter = {}));
package/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { Component } from './Node/component';
2
- export { InlineScope as scope, GateScope as gate, PeekScope as peek, MappedScope as mapped } from './Store/Tree/observableScope';
2
+ export { InlineScope as scope, GateScope as gate, PeekCallback as peek, MappedScope as mapped } from './Store/Tree/observableScope';
3
3
  export { vNode } from './Node/vNode.types';
package/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export { Component } from './Node/component';
2
- export { InlineScope as scope, GateScope as gate, PeekScope as peek, MappedScope as mapped } from './Store/Tree/observableScope';
2
+ export { InlineScope as scope, GateScope as gate, PeekCallback as peek, MappedScope as mapped } from './Store/Tree/observableScope';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "j-templates",
3
- "version": "8.0.19",
3
+ "version": "8.0.21",
4
4
  "description": "j-templates",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/TypesInCode/jTemplates",