j-templates 7.0.97 → 7.0.99

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/SYNTAX_PRIMER.md CHANGED
@@ -1,6 +1,6 @@
1
- # j-templates Syntax Primer — v3
1
+ # j-templates Syntax Primer — v4
2
2
 
3
- Complete reference for the **j-templates** framework syntax. This documents **j-templates v7.0.94** (see `package.json`). For pattern-oriented guides, see `docs/patterns/`; for step-by-step tutorials, see `docs/tutorials/`.
3
+ Complete reference for the **j-templates** framework syntax. This documents **j-templates v7.0.98** (see `package.json`). For pattern-oriented guides, see `docs/patterns/`; for step-by-step tutorials, see `docs/tutorials/`.
4
4
 
5
5
  > **Core concepts:** Components define UI via `Template()`. State decorators (`@Value`, `@State`, `@Computed`) enable reactivity. DOM functions (`div()`, `button()`) create virtual nodes. No compile step, minimal dependencies.
6
6
 
@@ -30,6 +30,27 @@ This one fact drives every design decision in this framework:
30
30
 
31
31
  **The core loop:** `@Value` → `Template()` → `Component.ToFunction` → `Component.Attach`. Everything else is a refinement.
32
32
 
33
+ ### ⚠️ The Destructuring Trap
34
+
35
+ If you've written React, you have a strong instinct to destructure state at the top of a component function — `const { count } = this.state` — because in React the whole component re-renders anyway, so hoisting reads costs nothing.
36
+
37
+ **In j-templates this instinct is actively harmful.** There is no whole-component re-render on state change — only the specific children function that reads a value re-runs. Hoisting a read to the top of `Template()` forces the *entire* component to behave like React: a full rebuild on every change, defeating the framework's fine-grained reactivity.
38
+
39
+ ```typescript
40
+ // ❌ React instinct — do NOT do this in j-templates
41
+ Template() {
42
+ const { count } = this.state; // hoisted read = full-component rebuild
43
+ return div({}, () => `Count: ${count}`);
44
+ }
45
+
46
+ // ✅ j-templates idiom — read inside the function that uses it
47
+ Template() {
48
+ return div({}, () => `Count: ${this.count}`);
49
+ }
50
+ ```
51
+
52
+ **If you find yourself writing `const x = this.something;` before a `return div(...)` in `Template()` — stop. That line is the bug.** This applies equally to `@Scope`/`@Computed` getters and to `this.Data` in components. This rule is referenced throughout the doc as **the Destructuring Trap**.
53
+
33
54
  ---
34
55
 
35
56
  ## Cheat Sheet
@@ -64,7 +85,8 @@ Component.Register("my-comp", MyComponent); // Web Component
64
85
 
65
86
  **The 3 conditional patterns**
66
87
  ```typescript
67
- // 1. Nested children function — isolated scope, use when an "else" branch is needed
88
+ // 1. Nested children function — isolated scope, use when an "else" branch is needed.
89
+ // Note: the "else" branch must return a vNode (text(() => "")), never null/undefined.
68
90
  div({}, () => this.isLoading ? div({}, () => "Loading") : text(() => ""));
69
91
 
70
92
  // 2. data: boolean — falsy renders nothing, truthy renders child (no "else")
@@ -90,7 +112,7 @@ fragment({ data: () => this.isLoading }, () => div({}, () => "Loading"));
90
112
  **The 3 golden rules**
91
113
  1. Pass arrays as `data:` — the framework iterates. Don't call `.map()` inside children.
92
114
  2. Wrap children in functions for separate reactive scopes.
93
- 3. Read scopes at the point of use (inside children functions / `data:` bindings), never at the top of `Template()`.
115
+ 3. Avoid the Destructuring Trap — read scopes at the point of use (inside children functions / `data:` bindings), never at the top of `Template()`.
94
116
 
95
117
  ---
96
118
 
@@ -331,7 +353,7 @@ npm install j-templates
331
353
 
332
354
  **File naming:** Components: kebab-case (`todo-list.ts`). Services: kebab-case + `-service` suffix. Exports: lowercase matching filename.
333
355
 
334
- **Public entry points** (verified against `src/index.ts`, `src/DOM/index.ts`, `src/Utils/index.ts`, `src/Store/index.ts`):
356
+ **Public entry points:**
335
357
 
336
358
  | Import path | Exports |
337
359
  |-------------|---------|
@@ -340,7 +362,7 @@ npm install j-templates
340
362
  | `j-templates/Utils` | `Value`, `State`, `Computed`, `ComputedAsync`, `Scope`, `Watch`, `Inject`, `Destroy`, `Bound`, `Animation`, `AnimationType`, `IDestroyable` |
341
363
  | `j-templates/Store` | `StoreSync`, `StoreAsync`, `ObservableScope`, `ObservableNode` |
342
364
 
343
- > ⚠️ **`Injector` is NOT exported from any public entry point.** It is defined in `src/Utils/injector.ts` but never re-exported. Use `@Inject` and `this.Injector` on components instead of importing `Injector` directly.
365
+ > ⚠️ **`Injector` is NOT exported from any public entry point.** It exists internally but is never re-exported. Use `@Inject` and `this.Injector` on components instead of importing `Injector` directly.
344
366
 
345
367
  ### DOM Functions
346
368
 
@@ -368,7 +390,7 @@ Text node: `text`
368
390
 
369
391
  Fragment: `fragment` (no DOM node — children reconcile into the real ancestor)
370
392
 
371
- No SVG-specific elements are exported (the `svgElements` module is commented out in `src/DOM/index.ts`). Use `Component.ToFunction` with a namespace for custom SVG components. Note that the `svg` element function itself creates an **HTML-namespace** `<svg>` element (no SVG namespace), so it is not suitable for inline SVG rendering — use a namespaced `Component.ToFunction` instead.
393
+ No SVG element functions are exported. Use `Component.ToFunction` with a namespace for custom SVG components. Note that the `svg` element function itself creates an **HTML-namespace** `<svg>` element (no SVG namespace), so it is not suitable for inline SVG rendering — use a namespaced `Component.ToFunction` instead.
372
394
 
373
395
  ---
374
396
 
@@ -570,12 +592,14 @@ Template() {
570
592
  | `true` | Wraps as `[true]`, renders child once |
571
593
  | `false` / `null` / `undefined` | Returns `[]`, no children rendered |
572
594
 
573
- > ⚠️ **Falsy edge case:** the wrapping logic is `if (!result) return [];` (see `ToArray` in `src/Node/vNode.ts`). So **`0`, `""`, and `NaN` also collapse to `[]`** not just `false`/`null`/`undefined`. Only *truthy* non-array values are wrapped as `[value]`.
595
+ > ⚠️ **Falsy edge case:** the falsy-collapse rule applies broadly **`0`, `""`, and `NaN` also collapse to `[]`**, not just `false`/`null`/`undefined`. Only *truthy* non-array values are wrapped as `[value]`.
574
596
 
575
597
  **Also accepts** `Promise<T>` and `Promise<T[]>` for async data. While a Promise is pending, the scope evaluates to `null` (falsy), so the element renders nothing until resolved — there is no built-in placeholder; implement one yourself if needed.
576
598
 
577
599
  The `false`/`true` behavior makes `data:` a clean conditional rendering mechanism. **The child function is invoked with the truthy value as its data argument** — e.g. `data: () => this.isLoading` calls the child with `true` when loading. It renders its child once when true and nothing when false, with its own isolated reactive scope.
578
600
 
601
+ **Reordering:** when the array itself changes order (e.g. `.sort()`, `.reverse()`) without changing which object references it contains, each item's existing DOM node and per-item scope move to the new position rather than being destroyed and recreated. Only genuinely new or removed references trigger create/destroy.
602
+
579
603
  #### Components
580
604
 
581
605
  Components receive the raw return value as `this.Data` — no iteration, no wrapping, no `false`/`null` short-circuit.
@@ -687,8 +711,8 @@ Template() {
687
711
  }
688
712
  ```
689
713
 
690
- 7. **Read `@Scope` getters at the point of use** reading a `@Scope` at the top of `Template()` registers it as a dependency of the entire Template. Reading it inside a children function or `data:` binding keeps the subscription scoped to that subtree.
691
- 8. **Same applies to `this.Data` in components** reading `this.Data` at the top of `Template()` subscribes the entire component to the parent's data scope. Any parent data change rebuilds the entire Template. Read `this.Data` inside children functions, `props:` functions, or `data:` bindings to scope reactivity to specific DOM subtrees.
714
+ 7. **Avoid the Destructuring Trap — read `@Scope` getters at the point of use.** See Mental Model. Reading a `@Scope` at the top of `Template()` registers it as a dependency of the entire Template. Reading it inside a children function or `data:` binding keeps the subscription scoped to that subtree.
715
+ 8. **Same applies to `this.Data` in components.** Reading `this.Data` at the top of `Template()` subscribes the entire component to the parent's data scope. Any parent data change rebuilds the entire Template. Read `this.Data` inside children functions, `props:` functions, or `data:` bindings to scope reactivity to specific DOM subtrees.
692
716
 
693
717
  ```typescript
694
718
  // Anti-pattern — this.Data read at top of Template()
@@ -760,25 +784,13 @@ div({}, () => [span({ data: () => this.value1 }, (v) => v), span({ data: () => t
760
784
 
761
785
  > **🔑 Key Insight:** The framework does **not** diff vNode trees. When a scope emits, the children function re-runs and produces new vNodes. DOM is patched from old to new. Optimization comes from minimizing *how often* scopes emit, not making the re-run cheap.
762
786
 
763
- When a reactive scope emits, the framework:
764
-
765
- 1. Re-runs the children function that read the scope, producing a new vNode tree.
766
- 2. Patches the DOM from the old vNode tree to the new vNode tree.
767
-
768
- The framework does **not** diff two vNode trees against each other. There is no vNode-to-vNode reconciliation — no keyed diffing, no positional matching of old vNodes to new vNodes. The "surgical" aspect comes from scoping — only the children functions that subscribed to the changed scope re-run. Everything else is untouched.
769
-
770
- **What actually happens at the DOM level:** when a children function re-runs, it produces a fresh array of vNodes. Each vNode maps to a DOM node, and `reconcileChildren` (in `src/DOM/domNodeConfig.ts`) reconciles the element's real DOM children against that list. Reuse is purely by **reference identity**:
771
- - A vNode that is the *same object* as before (which is what per-item `MappedScope` reuse produces) keeps its existing DOM node — no rebuild.
772
- - A *new* vNode object creates a *new* DOM node (`createNode`); the old node is removed.
773
- - Text nodes are special-cased: if the incoming child is a string and the current child is a text node, the text node is **reused and its value updated** (`setText`) rather than replaced.
774
-
775
- So "no vNode diffing" is precise: there is no keyed reconciliation and no vNode-to-vNode matching. But there *is* a cheap DOM-node reconciliation that reuses nodes by `===` reference. This is exactly why `@Computed` (same reference across updates) avoids DOM churn while `@Scope` (new reference) forces node recreation, and why per-item identity reuse is the framework's only mechanism for stable DOM.
787
+ When a reactive scope emits, only the children functions that read it re-run, producing new vNodes for that subtree — everything else is untouched. DOM nodes are reused when the new vNode is the same object reference as the old one (this is why `@Computed`'s identity preservation matters — see below); a new reference always creates a new DOM node. Text content is updated in place rather than replaced.
776
788
 
777
789
  This means:
778
790
  - A scope read at the top of `Template()` rebuilds the entire component vNode tree.
779
791
  - A scope read inside a children function rebuilds only that subtree.
780
792
  - A scope read inside a `data:` binding rebuilds only that iteration's vNode.
781
- - Per-item scopes are reused when the same data object reference reappears (identity-based, not key-based).
793
+ - Per-item scopes are reused when the same data object reference reappears (identity-based, not key-based); reordering an array of existing references moves nodes rather than recreating them.
782
794
 
783
795
  The optimization goal is minimizing **how often** children functions re-run through fine-grained scopes, not making the re-run itself cheap.
784
796
 
@@ -860,11 +872,11 @@ Cached, preserves object identity via `ApplyDiff`. No default value parameter. F
860
872
 
861
873
  `@Computed` uses `ApplyDiff` to merge changes into the existing object reference. Downstream consumers using `===` comparison (like `gate()` or `data:` bindings) only see a change when actual structure differs, not when the getter re-runs. Use `@Computed` when returning composite objects consumed by multiple UI regions. Prefer per-region `@Scope` when you need granular updates.
862
874
 
863
- > **`@Computed` returns a copy, not the source reference.** The getter's result is copied into a new reactive object whose identity is preserved across updates via `ApplyDiff`. It is a distinct object from whatever the getter read. Consequently, changes to the *source* object do not fire on the copy — the copy only re-evaluates when its own source dependencies change. If you need downstream consumers to observe mutations to an existing reactive object directly, use `@Scope` (which passes the existing reference through) instead.
875
+ > **`@Computed` returns a copy, not the source reference.** The getter's result is copied into a new reactive object whose identity is preserved across updates. It is a distinct object from whatever the getter read. Consequently, changes to the *source* object do not fire on the copy — the copy only re-evaluates when its own source dependencies change. If you need downstream consumers to observe mutations to an existing reactive object directly, use `@Scope` (which passes the existing reference through) instead.
864
876
 
865
- > **Recompute timing.** `@Computed`'s `StoreSync` is created lazily on first read; after that it recomputes eagerly on emit (pulling from the source) so it can `ApplyDiff`. This contrasts with `@Scope`, which re-evaluates lazily on the next read.
877
+ > **Recompute timing.** `@Computed`'s `StoreSync` backend is created lazily on first read; after that it recomputes eagerly on emit (pulling from the source). This contrasts with `@Scope`, which re-evaluates lazily on the next read.
866
878
 
867
- **Dependency tracking note:** `@Computed` (via `StoreSync`) registers dependencies based on what properties the getter accesses through the proxy. If the getter returns `this.tasks` without iterating or reading individual item properties, per-item mutations (e.g., `task.completed = true`) won't trigger re-evaluation. The getter must touch every reactive property it intends to track — `.filter()`, `.map()`, `.reduce()`, and manual property reads all register deps. Returning the array reference alone only tracks array-level mutations (push, splice, reassignment).
879
+ **Dependency tracking note:** `@Computed` registers dependencies based on what properties the getter accesses through the proxy. If the getter returns `this.tasks` without iterating or reading individual item properties, per-item mutations (e.g., `task.completed = true`) won't trigger re-evaluation. The getter must touch every reactive property it intends to track — `.filter()`, `.map()`, `.reduce()`, and manual property reads all register deps. Returning the array reference alone only tracks array-level mutations (push, splice, reassignment).
868
880
 
869
881
  **When NOT to use:** for cheap ops or array filter/sort of existing refs — use `@Scope()` or a plain getter (identity already preserved, less overhead).
870
882
 
@@ -895,7 +907,7 @@ syncFromParent(newFilter: FilterType): void {
895
907
  }
896
908
  ```
897
909
 
898
- Fires immediately with initial value when `Bound()` runs, then on each change. Subscription auto-cleaned on `Destroy()`. Uses greedy (batched) scope (`ObservableScope.Greedy`).
910
+ Fires immediately with initial value when `Bound()` runs, then on each change. Subscription auto-cleaned on `Destroy()`. Uses a greedy (batched) scope (`ObservableScope.Gated`) — multiple synchronous changes to the watched value within the same tick are debounced into a single callback invocation, firing once with the latest value.
899
911
 
900
912
  **When NOT to use:** for values you only read in `Template()` — reading a scope there is already reactive; `@Watch` is for side effects (syncing state, logging, triggering external calls).
901
913
 
@@ -998,7 +1010,7 @@ Need derived data?
998
1010
  - Each region independent -> `@Scope()` per region
999
1011
 
1000
1012
  Where to read a scope in Template()?
1001
- - Top of `Template()` -> subscribes entire Template (avoid unless unavoidable)
1013
+ - Avoid the Destructuring Trap (see Mental Model): don't hoist reads to the top of `Template()`.
1002
1014
  - Inside children function -> subscribes only that subtree (preferred)
1003
1015
  - Inside `data:` binding -> subscribes only that iteration (preferred)
1004
1016
 
@@ -1011,9 +1023,9 @@ Need conditional rendering?
1011
1023
 
1012
1024
  ## Inline Computed Scopes: scope(), gate(), peek(), mapped()
1013
1025
 
1014
- Four functions for creating memoized computed scopes inline within a watch context (template functions, `@Scope` getters, etc.). All accept `() => T | Promise<T>` — async callbacks are resolved and the resolved value is emitted. They live in `src/Store/Tree/observableScope.ts` and are re-exported from the root (`src/index.ts`).
1026
+ Four functions for creating memoized computed scopes inline within a watch context (template functions, `@Scope` getters, etc.). All accept `() => T | Promise<T>` — async callbacks are resolved and the resolved value is emitted.
1015
1027
 
1016
- **Only works within a watch context** throws if called outside (e.g. `scope() must be called within a watch context`).
1028
+ **Watch context** includes children functions, `data:`/`props:`/`attrs:` functions, `@Scope`/`@Computed`/`@ComputedAsync` getters, and `@Watch` callbacks. It does **not** include `on:` event handlers, which run outside reactive evaluation. Calling `scope()`/`gate()`/`peek()`/`mapped()` outside a watch context throws (e.g. `scope() must be called within a watch context`).
1017
1029
 
1018
1030
  ### scope() — Full Reactivity
1019
1031
 
@@ -1122,7 +1134,7 @@ const name = peek(() => this.Data.name, "name");
1122
1134
 
1123
1135
  ### mapped() — Per-Item Scopes (Advanced)
1124
1136
 
1125
- `mapped(data, callback, onUpdated?, onDestroyed?)` creates a per-item scope for a single data value. This is the mechanism `data:` uses internally. Only needed for advanced manual per-item scoping.
1137
+ `mapped(data, callback, onUpdated?, onDestroyed?)` creates a per-item scope for a single data value. This is the mechanism `data:` uses internally — array iteration is effectively `array.map(item => mapped(item, callback))`, one `mapped()` call per item, keyed by object identity. Only needed for advanced manual per-item scoping.
1126
1138
 
1127
1139
  ```typescript
1128
1140
  import { mapped } from "j-templates";
@@ -1130,7 +1142,7 @@ import { mapped } from "j-templates";
1130
1142
  mapped(data, (d) => /* ... */, (lastValue, scope) => /* onUpdated */, (lastValue) => /* onDestroyed */);
1131
1143
  ```
1132
1144
 
1133
- Signature (see `MappedScope` in `src/Store/Tree/observableScope.ts`):
1145
+ Signature:
1134
1146
 
1135
1147
  ```typescript
1136
1148
  function mapped<D, T>(
@@ -1287,7 +1299,7 @@ Both stores share the same API and flattening model, but differ fundamentally in
1287
1299
 
1288
1300
  **StoreSync** computes diffs synchronously on the main thread. Writes are immediate and consistent — a value written is readable in the same tick. It has no worker overhead, no serialisation constraints, and no `Destroy()` requirement. Use StoreSync for the vast majority of application state: user data, UI state, app config, form data, and any dataset where diff computation is not a bottleneck. `@Computed` uses `StoreSync` internally.
1289
1301
 
1290
- **StoreAsync** offloads all diff computation to a dedicated Web Worker via a serialised message queue. The worker maintains its own shadow copy of the store state and computes minimal diffs off the main thread, returning only the changed paths. This prevents large dataset operations from blocking rendering or input. Use StoreAsync when diffing genuinely large or deeply nested datasets — message feeds, large tables, real-time data — where synchronous diffing would cause frame drops. `@ComputedAsync` uses `StoreAsync` internally.
1302
+ **StoreAsync** offloads all diff computation to a dedicated Web Worker via a serialised message queue, computing minimal diffs off the main thread so large dataset operations don't block rendering or input. Use StoreAsync when diffing genuinely large or deeply nested datasets — message feeds, large tables, real-time data. `@ComputedAsync` uses `StoreAsync` internally.
1291
1303
 
1292
1304
  **If you are unsure which to use, start with StoreSync.** StoreAsync introduces meaningful constraints (see below) that are only worth accepting when the dataset size justifies off-thread diffing.
1293
1305
 
@@ -1295,7 +1307,7 @@ Both stores share the same API and flattening model, but differ fundamentally in
1295
1307
  |--------|-----------|------------|
1296
1308
  | Diff execution | Main thread, synchronous | Web Worker, asynchronous |
1297
1309
  | Write consistency | Immediate — readable same tick | Eventual — must `await` before reading |
1298
- | `keyFunc` constraint | None — can close over outer scope | **Must be self-contained** — serialised via `.toString()` and `eval`'d in worker |
1310
+ | `keyFunc` constraint | None — can close over outer scope | **Must be self-contained** — serialised and executed in worker |
1299
1311
  | Data constraint | Any JS value | **JSON-serialisable only** — no class instances, methods, `Date`, `Map`, `Set`, circular refs |
1300
1312
  | `Destroy()` required | No | Yes — terminates worker and queue |
1301
1313
  | Best for | Most app state | Large / real-time datasets |
@@ -1303,11 +1315,11 @@ Both stores share the same API and flattening model, but differ fundamentally in
1303
1315
 
1304
1316
  ### StoreAsync Constraints
1305
1317
 
1306
- StoreAsync's worker is bootstrapped by serialising `keyFunc` and the diff engine as strings and executing them inside a Blob URL. This has two hard constraints:
1318
+ StoreAsync's worker is bootstrapped by serialising `keyFunc` and the diff engine and executing them in a worker context. This has two hard constraints:
1307
1319
 
1308
1320
  **1. `keyFunc` must be self-contained — no closed-over variables.**
1309
1321
 
1310
- The function is serialised via `.toString()` and `eval`'d in the worker context. Any variable from the outer scope will be undefined inside the worker.
1322
+ The function is serialised and evaluated in the worker context. Any variable from the outer scope will be undefined inside the worker.
1311
1323
 
1312
1324
  ```typescript
1313
1325
  // ❌ Breaks at runtime — prefix is not accessible in the worker
@@ -1417,12 +1429,15 @@ If an object has no `id` property (or `keyFunc` returns `undefined`), it is stor
1417
1429
 
1418
1430
  ```typescript
1419
1431
  namespace ObservableScope {
1420
- Create<T>(valueFunction: { (): T | Promise<T> }, greedy?: boolean, force?: boolean): IObservableScope<T>;
1432
+ Create<T>(valueFunction: { (): T | Promise<T> }): IObservableScope<T>; // Non-greedy scope
1433
+ Gated<T>(valueFunction: { (): T | Promise<T> }): IObservableScope<T>; // Greedy scope (batches via microtask)
1434
+ Basic<T>(valueFunction: { (): T }): IBasicObservableScope<T>; // Direct-value scope; no dep tracking, no cache — Update() to emit
1421
1435
  Value<T>(scope: IObservableScope<T>): T; // Get value + register dependency
1422
1436
  Peek<T>(scope: IObservableScope<T>): T; // Get value without registering dependency
1423
1437
  Touch<T>(scope: IObservableScope<T>): void; // Register as dependency without reading value
1424
1438
  Watch<T>(scope: IObservableScope<T>, callback: EmitterCallback<[IObservableScope<T>]>): void;
1425
1439
  Unwatch<T>(scope: IObservableScope<T>, callback: EmitterCallback<[IObservableScope<T>]>): void;
1440
+ OnUpdated<T>(scope: IObservableScope<T>, callback: (lastValue: T, scope: IObservableScope<T>) => void): void;
1426
1441
  OnDestroyed(scope: IObservableScope<unknown>, callback: EmitterCallback): void;
1427
1442
  Update(scope: IObservableScope<any>): void; // Mark dirty, triggers recomputation
1428
1443
  Register(emitter: Emitter): void;
@@ -1433,6 +1448,12 @@ namespace ObservableScope {
1433
1448
 
1434
1449
  **Async limitation:** Dependencies are only captured synchronously. Read all reactive values before the first `await`. Reactive reads after `await` are not tracked.
1435
1450
 
1451
+ **Static scope edge case:** `Create` returns a *static* scope when its valueFunction reads no
1452
+ reactive dependencies (no `@Value`/`@State`/other scope reads). Static scopes do **not** emit when
1453
+ passed to `Update` — reactivity silently breaks with no error. If you need a manually-updatable
1454
+ scope whose value doesn't derive from reactive state, use `Basic` instead, which always emits on
1455
+ `Update`.
1456
+
1436
1457
  ### Service Patterns
1437
1458
 
1438
1459
  ```typescript
@@ -1448,9 +1469,11 @@ class DataService implements IDestroyable {
1448
1469
  }
1449
1470
 
1450
1471
  // Reactive counter via shared service
1472
+ // NOTE: _count is a plain field (not reactive), so Create() would yield a static scope that
1473
+ // ignores Update(). Use Basic() for manually-updatable scopes.
1451
1474
  class CounterService implements IDestroyable {
1452
1475
  private _count = 0;
1453
- private countScope = ObservableScope.Create(() => this._count, false, true);
1476
+ private countScope = ObservableScope.Basic(() => this._count);
1454
1477
  get count() { return ObservableScope.Value(this.countScope); }
1455
1478
  increment() { this._count++; ObservableScope.Update(this.countScope); }
1456
1479
  Destroy(): void { ObservableScope.Destroy(this.countScope); }
@@ -1465,16 +1488,28 @@ class CounterService implements IDestroyable {
1465
1488
  namespace ObservableNode {
1466
1489
  Create<T>(value: T): T; // Wrap in reactive proxy
1467
1490
  Unwrap<T>(value: T): T; // Get raw value from proxy
1468
- Touch(value: unknown, prop?: string | number): void; // Manually trigger change
1469
- ApplyDiff(rootNode: any, diffResult: JsonDiffResult): void; // Apply diff in-place (@Computed uses this)
1491
+ Clone<T>(value: T): T; // Strip proxies into plain data (mutates plain objects in place)
1492
+ Update(value: unknown, prop?: string | number): void; // Manually trigger change on a node/property
1493
+ Apply(rootNode: any, value: any): void; // Merge a full value in-place, preserving identity (public)
1494
+ ApplyDiff(rootNode: any, diffResult: JsonDiffResult): void; // Apply diff in-place (internal — used by Store/@Computed)
1470
1495
  CreateFactory(alias?: (value: any) => any | undefined): <T>(value: T) => T; // Factory with aliasing
1471
1496
  }
1472
1497
  ```
1473
1498
 
1499
+ **`Apply` vs `ApplyDiff`:** `Apply(rootNode, value)` merges a full replacement value into an observable node in-place, preserving the node's reference (so `===` checks and DOM reuse stay stable). Use this when you want to update an observable node in place — assigning a property directly on an observable node does **not** generate a diff. `ApplyDiff` is internal-only (used by `StoreSync`/`StoreAsync`/`@Computed`) — ignore it.
1500
+
1474
1501
  **Array operations on ObservableNode proxies:** `push`, `pop`, `shift`, `unshift`, `splice`, `sort`, `reverse` — all trigger reactive updates.
1475
1502
 
1476
1503
  ---
1477
1504
 
1505
+ ## Testing
1506
+
1507
+ The standard setup is **Vitest + JSDOM**, which provides browser-like globals (`document`, DOM node classes, etc.) so components can be attached and inspected without a real browser.
1508
+
1509
+ Set **`SYNC_SCHEDULING=true`** to make reactive updates apply synchronously — a state mutation is reflected in the DOM immediately, with no `flush()`/`await` step needed before asserting. This is the default for standard test runs. Omit it only when a test specifically needs to verify real async or batching timing (e.g. confirming `@Watch` debounces multiple synchronous writes into one call, or that an async scope resolves correctly across a microtask boundary) — with `SYNC_SCHEDULING` on, that behavior collapses and can't be observed.
1510
+
1511
+ ---
1512
+
1478
1513
  ## Traps & Gotchas
1479
1514
 
1480
1515
  These are the subtle behaviors that cause the most bugs. Read this before writing components.
@@ -1485,19 +1520,22 @@ These are the subtle behaviors that cause the most bugs. Read this before writin
1485
1520
  4. **`data:` boolean renders the element, not nothing.** A falsy `data:` value removes the element's *children*, but the element itself stays in the DOM. A styled container (padding/background/border) will still show as an empty box. To remove an element entirely, use a nested children function or `gate()`.
1486
1521
  5. **`@ComputedAsync` is a *sync* getter.** The "Async" refers to the `StoreAsync` backend, not the getter signature. For real async, use `@Scope() + scope(async)` or `ObservableScope.Create(async)`.
1487
1522
  6. **`gate()` is incompatible with `@Scope`.** `@Scope` returns a new reference every update, so `gate()`'s `===` always sees a change. Use `@Computed()` for reference stability.
1488
- 7. **Per-item scope reuse is identity-based, not key-based.** The same data object reference reuses its scope; a new reference creates a new scope.
1489
- 8. **`@Watch` fires immediately on `Bound()`** with the initial value — not just on changes. Missing `super.Bound()` means `@Watch` never fires.
1523
+ 7. **Per-item scope reuse is identity-based, not key-based.** The same data object reference reuses its scope; a new reference creates a new scope. Reordering an array of existing references moves the DOM node and scope to the new position rather than recreating them.
1524
+ 8. **`@Watch` fires immediately on `Bound()`** with the initial value — not just on changes. Missing `super.Bound()` means `@Watch` never fires. Multiple synchronous changes in the same tick are debounced into a single callback.
1490
1525
  9. **`@State` arrays support direct mutation** (`push`, `splice`, item property writes) because they're proxies. Plain arrays require reassignment.
1491
1526
  10. **`@Computed` only tracks what the getter touches.** Returning `this.tasks` without reading item properties won't re-trigger on per-item mutations. Touch every property you track.
1492
- 11. **`scope()`/`gate()`/`peek()`/`mapped()` throw outside a watch context.** They must be called inside a template function, `@Scope` getter, or other watch context.
1527
+ 11. **`scope()`/`gate()`/`peek()`/`mapped()` throw outside a watch context.** They must be called inside a children function, `data:`/`props:`/`attrs:` function, `@Scope`/`@Computed`/`@ComputedAsync` getter, or `@Watch` callback — not inside `on:` event handlers.
1493
1528
  12. **`Injector` is not publicly exported.** Use `@Inject` and `this.Injector` on components.
1494
1529
  13. **StoreAsync data must be JSON-serialisable** and `keyFunc` must be self-contained (no closed-over variables). Always `await` StoreAsync writes before reading.
1495
1530
  14. **Two-way binding needs reactive props** (`props: () => ({ value })`). A static `props: { value }` object causes input focus loss.
1496
- 15. **Reading a scope at the top of `Template()` subscribes the whole component.** Read scopes inside children functions or `data:` bindings for fine-grained updates.
1531
+ 15. **The Destructuring Trap.** Reading a scope or `this.Data` at the top of `Template()` subscribes the whole component — the React instinct to hoist state reads is backwards here. Read scopes inside children functions or `data:` bindings for fine-grained updates. See Mental Model.
1497
1532
  16. **`scope()`/`gate()`/`peek()` ID collisions are per-scope.** Multiple calls to the same helper in one watch context without IDs silently resolve to the first scope. Provide distinct IDs when calling the same helper more than once in a single ObservableScope definition.
1498
1533
  17. **`IsAsync` only detects the `async` keyword.** A function that *returns* a Promise but is not declared `async` (e.g. `() => fetch(...)`) is treated as synchronous — the scope stores the Promise as its value instead of resolving it. Always write `async () => ...` for async scopes.
1499
1534
  18. **`fragment()` has no DOM node.** It cannot be attached directly (wrap it in a real element) and a falsy `data:` value renders *nothing* — no empty wrapper box. Its children reconcile into the nearest real ancestor.
1500
- 19. **`@State`/`ObservableNode` only deep-tracks plain objects and arrays.** `JsonType` classifies values by prototype; class instances, `Date`, `Map`, `Set`, and other non-plain objects are treated as opaque primitives — nested mutations won't be tracked. Use plain objects/arrays for reactive state.
1535
+ 19. **`@State`/`ObservableNode` only deep-tracks plain objects and arrays.** Class instances, `Date`, `Map`, `Set`, and other non-plain objects are treated as opaque primitives — nested mutations won't be tracked. Use plain objects/arrays for reactive state.
1536
+ 20. **`ObservableScope.Create` with no reactive deps yields a static scope.** If the valueFunction reads no `@Value`/`@State`/other scope, `Create` returns a static scope that silently ignores `ObservableScope.Update` — reactivity breaks with no error. Use `ObservableScope.Basic` for manually-updatable scopes whose value doesn't derive from reactive state.
1537
+ 21. **No error boundaries.** An exception thrown in a children function, `props:`/`attrs:` function, or getter propagates uncaught — there's no per-subtree isolation. Guard risky logic with your own `try`/`catch`.
1538
+ 22. **Children functions can't return `null`/`undefined`.** For conditional presence, use the element's own `data:` (falsy → no children) or `fragment({ data: () => condition }, ...)` for no wrapper. A ternary's "else" branch must return a vNode (`text(() => "")`), not `null`.
1501
1539
 
1502
1540
  ---
1503
1541
 
@@ -1512,7 +1550,7 @@ These are the subtle behaviors that cause the most bugs. Read this before writin
1512
1550
  | Single `@Scope` for multiple independent UI regions | One `@Scope` per region |
1513
1551
  | `gate()` wrapping `@Scope` getter | Use `@Computed` or remove `gate()` |
1514
1552
  | `data:` binding inside helper function called from `Template()` | Inline in `Template()` with `@Scope` data source |
1515
- | `@Scope` read at top of `Template()` | Read inside children function or `data:` binding |
1553
+ | Destructuring/hoisting a scope read at top of `Template()` | Read inside children function or `data:` binding — see Mental Model |
1516
1554
  | `this.Data` read at top of `Template()` | Read inside children function, `props:` function, or `data:` binding |
1517
1555
  | Render callback for items needing state/events | Use dedicated component |
1518
1556
  | Child `@Value` not synced with parent `Data` | Use `@Watch((self) => self.Data.prop)` |
@@ -1522,6 +1560,7 @@ These are the subtle behaviors that cause the most bugs. Read this before writin
1522
1560
  | Assuming framework diffs vNode trees | It doesn't — optimize by minimizing scope emission frequency |
1523
1561
  | `@State()` on class instances / `Date` / `Map` / `Set` | Use plain objects/arrays — non-plain objects are treated as primitives (no deep reactivity) |
1524
1562
  | Promise-returning arrow without `async` keyword in an async scope | Use `async () => ...` — `IsAsync` only detects `async` functions |
1563
+ | Children function returning `null`/`undefined` | Return `text(() => "")`, or move the condition into `data:`/`fragment()` |
1525
1564
 
1526
1565
  ---
1527
1566
 
@@ -1535,7 +1574,7 @@ These are the subtle behaviors that cause the most bugs. Read this before writin
1535
1574
  | `@Watch` never fires | Missing `super.Bound()` | Call in `Bound()` method |
1536
1575
  | Memory leak | Missing cleanup | `@Destroy()` + `super.Destroy()` |
1537
1576
  | Input loses focus | Static `props` object | Use `props: () => ({ value })` |
1538
- | Entire Template re-runs on small change | `@Scope` read at top of `Template()` | Read scope inside children function or `data:` binding |
1577
+ | Entire Template re-runs on small change | Destructuring Trap — scope read at top of `Template()` | Read scope inside children function or `data:` binding |
1539
1578
  | Entire section re-renders on small change | Single `@Scope` feeds multiple regions | Split into per-region `@Scope` getters |
1540
1579
  | `gate()` doesn't prevent re-renders | Wrapping `@Scope` getter (always new ref) | Read `@Scope` directly or use `@Computed` |
1541
1580
  | `data:` binding re-renders every time | Element created in helper, not `Template()` | Inline element in `Template()` |
@@ -1543,19 +1582,7 @@ These are the subtle behaviors that cause the most bugs. Read this before writin
1543
1582
  | Conditional re-renders when sibling list updates | Condition and list share same children function scope | Isolate condition into nested children function, `data:` boolean, or `gate()` |
1544
1583
  | Expensive Template re-runs on every change | Large vNode subtree subscribed to frequently-changing scope | Split into smaller scopes to reduce rebuild surface |
1545
1584
  | Async scope resolves to a Promise instead of a value | Callback not declared `async` | Use `async () => ...` so `IsAsync` detects it |
1546
-
1547
- ### Internal Mechanics
1548
-
1549
- - **No vNode Diffing:** The framework does not diff vNode trees. When a scope emits, the children function re-runs, producing new vNodes. The DOM is patched from old to new. Per-item scopes are reused when the same data object reference reappears (identity-based, not key-based).
1550
- - **DOM-node reconciliation by reference:** `reconcileChildren` reuses a DOM node only when the vNode object reference is identical (per-item reuse); a new vNode creates a new DOM node. Text nodes are reused and updated in place (`setText`) rather than replaced. There is no keyed or positional vNode matching.
1551
- - **Fragments reconcile into the ancestor:** `fragment()` has no DOM node; its children are patched into the nearest real ancestor via `reconcileRange` (a range-based sibling reconciliation). Nested fragments flatten into that same ancestor.
1552
- - **Centralized scheduling:** all async callbacks (`requestAnimationFrame`, `queueMicrotask`, `setTimeout`, `requestIdleCallback`) route through `src/Utils/scheduling.ts`. Setting `SYNC_SCHEDULING=true` makes every callback run synchronously — the default vitest project uses this for deterministic tests, while the `*-test-async.ts` project runs without it.
1553
- - **Object Identity:** `@Computed` uses `ApplyDiff` to merge changes into existing references, preventing DOM subtree recreation.
1554
- - **StoreAsync Constraints:** Uses Web Workers for diffing; data must be JSON-serializable (no methods or circular references).
1555
- - **`gate()` as Circuit Breaker:** Prevents reactivity propagation when result is unchanged (`===`). Ineffective with `@Scope` (always new ref).
1556
- - **Scope Types:** `static` (fixed, zero overhead), `basic` (used by `@Value`), `dynamic` (tracks deps, caches, emits on change), `greedy` (batched via microtask queue — used for watch callbacks, async).
1557
- - **Lazy Initialization:** Scopes created on first access, not construction.
1558
- - **Memory Management:** All scopes tracked via WeakMap. `Destroy()` calls `ObservableScope.DestroyAll()` + `@Destroy` properties auto-cleaned.
1585
+ | Uncaught exception crashes a render | No error boundaries exist | Add `try`/`catch` inside the risky function |
1559
1586
 
1560
1587
  ---
1561
1588
 
@@ -1566,11 +1593,13 @@ These are the subtle behaviors that cause the most bugs. Read this before writin
1566
1593
  | **vNode** | Virtual node — the internal representation of a DOM element or text node |
1567
1594
  | **Scope** | A reactive unit that tracks dependencies, caches a value, and emits on change |
1568
1595
  | **Static scope** | A scope with a fixed value — no dependency tracking, zero overhead |
1569
- | **Basic scope** | A lightweight scope used by `@Value` — direct value storage, no proxy |
1596
+ | **Basic scope** | A lightweight scope used by `@Value` — stores a value directly, with no dependency tracking or caching; `ObservableScope.Update` must be called to emit |
1570
1597
  | **Dynamic scope** | A scope with a getter function — tracks dependencies, re-evaluates on change |
1571
1598
  | **Greedy scope** | A dynamic scope that batches updates via microtask queue (used for `@Watch`, async) |
1572
1599
  | **Children function** | The function passed as the second argument to a DOM function (e.g., `div({}, () => ...)`) |
1573
- | **ApplyDiff** | Deep merge that preserves object identityused by `@Computed` to update existing references in-place |
1600
+ | **Watch context** | Code executed during evaluation of an `ObservableScope` children functions, `data:`/`props:`/`attrs:` functions, `@Scope`/`@Computed`/`@ComputedAsync` getters, `@Watch` callbacks. Excludes `on:` handlers. |
1601
+ | **The Destructuring Trap** | The React-primed instinct to hoist a state/scope read to the top of a component function; in j-templates this subscribes the whole `Template()` instead of a subtree. |
1602
+ | **ApplyDiff** | Deep merge that preserves object identity — used internally by `@Computed` to update existing references in-place |
1574
1603
  | **ObservableNode** | A reactive proxy wrapper around objects/arrays that tracks property-level mutations |
1575
1604
  | **Injector** | Scoped dependency injection container with parent-chain resolution (not publicly exported) |
1576
1605
  | **keyFunc** | A function passed to Store that extracts an ID from objects for automatic flattening |
@@ -1579,7 +1608,7 @@ These are the subtle behaviors that cause the most bugs. Read this before writin
1579
1608
 
1580
1609
  ## References
1581
1610
 
1582
- - **Source of truth:** `src/` (this primer documents `j-templates` v7.0.94).
1611
+ - **Source of truth:** `src/` (this primer documents `j-templates` v7.0.98).
1583
1612
  - **Pattern guides:** `docs/patterns/01-components.md`, `docs/patterns/02-reactivity.md`, `docs/patterns/03-templates-and-data.md`, `docs/patterns/04-dependency-injection.md`.
1584
1613
  - **Tutorials:** `docs/tutorials/` (01-getting-started through 08-building-complete-app).
1585
1614
  - **Worked example:** `examples/smart-tasks/src/` (the Smart Tasks app used above).
@@ -8,6 +8,28 @@ export declare namespace ObservableNode {
8
8
  * @returns The unwrapped raw value without proxy wrappers.
9
9
  */
10
10
  function Unwrap<T>(value: T): T;
11
+ /**
12
+ * Produces a plain, non-reactive version of a value by deep-cloning any observable nodes it
13
+ * contains into plain objects/arrays. Used internally by `@Computed` to strip proxies from a
14
+ * computed value before it is written to the store.
15
+ *
16
+ * Behavior depends on the input:
17
+ * - If `value` is an observable node, returns a NEW plain deep copy (top-level identity is not
18
+ * preserved; nested nodes become plain objects/arrays).
19
+ * - If `value` is a plain object/array, it is MUTATED in place — nested observable nodes are
20
+ * replaced with plain copies — and the SAME reference is returned (not a copy).
21
+ * - Primitives and non-plain objects (Date, Map, Set, class instances) are passed through by
22
+ * reference, unchanged.
23
+ *
24
+ * **Dependency tracking side effect:** Cloning an observable node reads every property through
25
+ * the proxy's getters, so any reactive scope that contains the clone call (e.g. the `@Computed`
26
+ * getter scope) registers a dependency on each nested property. This is what lets `@Computed`
27
+ * observe and react to modifications of nested properties, not just top-level reassignment.
28
+ *
29
+ * @template T The type of value to clone.
30
+ * @param value The observable node or value containing observable nodes to convert to plain data.
31
+ * @returns A plain, non-reactive version of the value.
32
+ */
11
33
  function Clone<T>(value: T): T;
12
34
  /**
13
35
  * Creates an observable node from a plain value.
@@ -23,8 +45,26 @@ export declare namespace ObservableNode {
23
45
  * @param value The observable node to touch.
24
46
  * @param prop Optional property name or index to touch a specific nested property.
25
47
  */
26
- function Update(value: unknown, prop?: string | number | symbol): void;
27
- function Apply(rootNode: any, update: any): void;
48
+ function Update<T>(value: T, prop?: keyof T): void;
49
+ /**
50
+ * Merges a new value into an observable node in-place, preserving the node's object identity.
51
+ * Computes the diff between the node's current value and the provided value, then applies only
52
+ * the changed paths to the existing node. This is the public alternative to `ApplyDiff` for
53
+ * cases where you want to update an observable node while keeping the same reference (so
54
+ * downstream `===` comparisons and DOM reuse remain stable).
55
+ *
56
+ * Unlike assigning a property directly on an observable node (which does not generate a diff),
57
+ * `Apply` reconciles the full value: properties present in `value` are updated, and properties
58
+ * missing from `value` are removed from the target object.
59
+ *
60
+ * @param rootNode The observable node to update in-place.
61
+ * @param value The full replacement value to merge into the node. Properties missing from this
62
+ * value are removed from the target object.
63
+ * @throws If the JSON type of `value` differs from the node's current type (e.g. object → array,
64
+ * or a primitive root), the node's type cannot be changed.
65
+ * @remarks No-op when `value` deep-equals the node's current value (empty diff).
66
+ */
67
+ function Apply(rootNode: any, value: any): void;
28
68
  /**
29
69
  * Applies a JSON diff result to an observable node, efficiently updating only changed properties.
30
70
  * Optimizes nested object updates by computing paths incrementally and touching modified properties.
@@ -42,13 +42,6 @@ function ownKeys(value) {
42
42
  function ownKeysArray(value) {
43
43
  return Object.keys(value[NODE_VALUE]);
44
44
  }
45
- function TouchValue(value, prop = OBJECT_SCOPE) {
46
- const wrapper = wrapperCache.get(value);
47
- if (wrapper) {
48
- const scope = wrapper[prop] ?? wrapper[OBJECT_SCOPE];
49
- observableScope_1.ObservableScope.Touch(scope);
50
- }
51
- }
52
45
  function UnwrapProxy(value, type = (0, json_2.JsonType)(value)) {
53
46
  if (type === "value")
54
47
  return value;
@@ -294,6 +287,28 @@ var ObservableNode;
294
287
  return UnwrapProxy(value);
295
288
  }
296
289
  ObservableNode.Unwrap = Unwrap;
290
+ /**
291
+ * Produces a plain, non-reactive version of a value by deep-cloning any observable nodes it
292
+ * contains into plain objects/arrays. Used internally by `@Computed` to strip proxies from a
293
+ * computed value before it is written to the store.
294
+ *
295
+ * Behavior depends on the input:
296
+ * - If `value` is an observable node, returns a NEW plain deep copy (top-level identity is not
297
+ * preserved; nested nodes become plain objects/arrays).
298
+ * - If `value` is a plain object/array, it is MUTATED in place — nested observable nodes are
299
+ * replaced with plain copies — and the SAME reference is returned (not a copy).
300
+ * - Primitives and non-plain objects (Date, Map, Set, class instances) are passed through by
301
+ * reference, unchanged.
302
+ *
303
+ * **Dependency tracking side effect:** Cloning an observable node reads every property through
304
+ * the proxy's getters, so any reactive scope that contains the clone call (e.g. the `@Computed`
305
+ * getter scope) registers a dependency on each nested property. This is what lets `@Computed`
306
+ * observe and react to modifications of nested properties, not just top-level reassignment.
307
+ *
308
+ * @template T The type of value to clone.
309
+ * @param value The observable node or value containing observable nodes to convert to plain data.
310
+ * @returns A plain, non-reactive version of the value.
311
+ */
297
312
  function Clone(value) {
298
313
  return CloneProxy(value);
299
314
  }
@@ -323,9 +338,27 @@ var ObservableNode;
323
338
  }
324
339
  }
325
340
  ObservableNode.Update = Update;
326
- function Apply(rootNode, update) {
341
+ /**
342
+ * Merges a new value into an observable node in-place, preserving the node's object identity.
343
+ * Computes the diff between the node's current value and the provided value, then applies only
344
+ * the changed paths to the existing node. This is the public alternative to `ApplyDiff` for
345
+ * cases where you want to update an observable node while keeping the same reference (so
346
+ * downstream `===` comparisons and DOM reuse remain stable).
347
+ *
348
+ * Unlike assigning a property directly on an observable node (which does not generate a diff),
349
+ * `Apply` reconciles the full value: properties present in `value` are updated, and properties
350
+ * missing from `value` are removed from the target object.
351
+ *
352
+ * @param rootNode The observable node to update in-place.
353
+ * @param value The full replacement value to merge into the node. Properties missing from this
354
+ * value are removed from the target object.
355
+ * @throws If the JSON type of `value` differs from the node's current type (e.g. object → array,
356
+ * or a primitive root), the node's type cannot be changed.
357
+ * @remarks No-op when `value` deep-equals the node's current value (empty diff).
358
+ */
359
+ function Apply(rootNode, value) {
327
360
  const root = rootNode[NODE_VALUE];
328
- const diff = (0, json_1.JsonDiff)(update, root);
361
+ const diff = (0, json_1.JsonDiff)(value, root);
329
362
  ApplyDiff(rootNode, diff);
330
363
  }
331
364
  ObservableNode.Apply = Apply;
@@ -115,6 +115,9 @@ export declare function PeekScope<T>(callback: () => T | Promise<T>, idOverride?
115
115
  export declare namespace ObservableScope {
116
116
  /**
117
117
  * Creates a new observable scope from a value function.
118
+ * The scope auto-tracks the reactive dependencies the valueFunction reads. If it reads no
119
+ * reactive dependencies, `Create` returns a static scope that does not emit on `Update` — use
120
+ * `Basic` for a manually-updatable scope whose value doesn't derive from reactive state.
118
121
  * @template T The type of value returned by the function.
119
122
  * @param valueFunction Function that returns the scope's value. Can be async.
120
123
  * @returns A new observable scope.
@@ -122,9 +125,26 @@ export declare namespace ObservableScope {
122
125
  function Create<T>(valueFunction: {
123
126
  (): T | Promise<T>;
124
127
  }): IObservableScope<T>;
128
+ /**
129
+ * Creates a greedy observable scope that batches updates via the microtask queue.
130
+ * Unlike `Create`, a greedy scope does not emit immediately on change — updates are coalesced
131
+ * and emitted on the next microtask. Used by `@Watch` for batched side effects.
132
+ * @template T The type of value returned by the function.
133
+ * @param valueFunction Function that returns the scope's value. Can be async.
134
+ * @returns A new greedy observable scope.
135
+ */
125
136
  function Gated<T>(valueFunction: {
126
137
  (): T | Promise<T>;
127
138
  }): IObservableScope<T>;
139
+ /**
140
+ * Creates a lightweight basic scope that stores a value directly without a proxy.
141
+ * Used by `@Value` for primitives. Basic scopes do not automatically track dependencies —
142
+ * after creation, `ObservableScope.Update` must be called for the scope to emit. They also do
143
+ * not cache a value internally: the valueFunction is invoked on every read.
144
+ * @template T The type of value stored in the scope.
145
+ * @param valueFunction Function that returns the scope's value.
146
+ * @returns A new basic observable scope.
147
+ */
128
148
  function Basic<T>(valueFunction: {
129
149
  (): T;
130
150
  }): IBasicObservableScope<T>;
@@ -167,6 +187,13 @@ export declare namespace ObservableScope {
167
187
  * @param callback The callback function to remove.
168
188
  */
169
189
  function Unwatch<T>(scope: IObservableScope<T>, callback: (scope: IObservableScope<T>) => void): void;
190
+ /**
191
+ * Registers a callback to be invoked when the scope's value is updated.
192
+ * Only applies to dynamic scopes; no-op for static scopes.
193
+ * @template T The type of value stored in the scope.
194
+ * @param scope The scope to monitor for updates.
195
+ * @param callback Function invoked with the last value and the scope on each update.
196
+ */
170
197
  function OnUpdated<T>(scope: IObservableScope<T>, callback: {
171
198
  (lastValue: T, scope: IObservableScope<T>): void;
172
199
  }): void;
@@ -180,6 +207,8 @@ export declare namespace ObservableScope {
180
207
  }): void;
181
208
  /**
182
209
  * Marks a scope as dirty, triggering recomputation on next access or batch.
210
+ * No-op for static scopes (created when a `Create` valueFunction reads no reactive
211
+ * dependencies) — they never emit. Use `Basic` for scopes you drive manually with `Update`.
183
212
  * @param scope The scope to mark for update.
184
213
  */
185
214
  function Update(scope: IObservableScope<any> | IBasicObservableScope<any>): void;
@@ -538,6 +538,9 @@ var ObservableScope;
538
538
  (function (ObservableScope) {
539
539
  /**
540
540
  * Creates a new observable scope from a value function.
541
+ * The scope auto-tracks the reactive dependencies the valueFunction reads. If it reads no
542
+ * reactive dependencies, `Create` returns a static scope that does not emit on `Update` — use
543
+ * `Basic` for a manually-updatable scope whose value doesn't derive from reactive state.
541
544
  * @template T The type of value returned by the function.
542
545
  * @param valueFunction Function that returns the scope's value. Can be async.
543
546
  * @returns A new observable scope.
@@ -546,10 +549,27 @@ var ObservableScope;
546
549
  return ExecuteFunction(valueFunction, false);
547
550
  }
548
551
  ObservableScope.Create = Create;
552
+ /**
553
+ * Creates a greedy observable scope that batches updates via the microtask queue.
554
+ * Unlike `Create`, a greedy scope does not emit immediately on change — updates are coalesced
555
+ * and emitted on the next microtask. Used by `@Watch` for batched side effects.
556
+ * @template T The type of value returned by the function.
557
+ * @param valueFunction Function that returns the scope's value. Can be async.
558
+ * @returns A new greedy observable scope.
559
+ */
549
560
  function Gated(valueFunction) {
550
561
  return ExecuteFunction(valueFunction, true);
551
562
  }
552
563
  ObservableScope.Gated = Gated;
564
+ /**
565
+ * Creates a lightweight basic scope that stores a value directly without a proxy.
566
+ * Used by `@Value` for primitives. Basic scopes do not automatically track dependencies —
567
+ * after creation, `ObservableScope.Update` must be called for the scope to emit. They also do
568
+ * not cache a value internally: the valueFunction is invoked on every read.
569
+ * @template T The type of value stored in the scope.
570
+ * @param valueFunction Function that returns the scope's value.
571
+ * @returns A new basic observable scope.
572
+ */
553
573
  function Basic(valueFunction) {
554
574
  return CreateBasicScope(valueFunction);
555
575
  }
@@ -620,6 +640,13 @@ var ObservableScope;
620
640
  emitter_1.Emitter.Remove(scope.emitter, callback);
621
641
  }
622
642
  ObservableScope.Unwatch = Unwatch;
643
+ /**
644
+ * Registers a callback to be invoked when the scope's value is updated.
645
+ * Only applies to dynamic scopes; no-op for static scopes.
646
+ * @template T The type of value stored in the scope.
647
+ * @param scope The scope to monitor for updates.
648
+ * @param callback Function invoked with the last value and the scope on each update.
649
+ */
623
650
  function OnUpdated(scope, callback) {
624
651
  if (scope.type !== "dynamic")
625
652
  return;
@@ -639,6 +666,8 @@ var ObservableScope;
639
666
  ObservableScope.OnDestroyed = OnDestroyed;
640
667
  /**
641
668
  * Marks a scope as dirty, triggering recomputation on next access or batch.
669
+ * No-op for static scopes (created when a `Create` valueFunction reads no reactive
670
+ * dependencies) — they never emit. Use `Basic` for scopes you drive manually with `Update`.
642
671
  * @param scope The scope to mark for update.
643
672
  */
644
673
  function Update(scope) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "j-templates",
3
- "version": "7.0.97",
3
+ "version": "7.0.99",
4
4
  "description": "j-templates",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/TypesInCode/jTemplates",