@signaltree/core 10.2.0 → 10.3.0

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/README.md CHANGED
@@ -40,7 +40,20 @@ Every "Wrong pattern" below was actually generated by Claude / GPT-5.4 / Gemini
40
40
  | `Store.dispatch(action)`, `Store.select(selector)` | **`@ngrx/store` (classic)** | `tree.$.path()` to read, `tree.$.path.set(v)` to write |
41
41
  | `.toPromise()` (deprecated RxJS 7+) | RxJS legacy | `firstValueFrom(obs)` — or let `asyncSource` consume directly |
42
42
 
43
- ### Status markerexact method names (frequently confused)
43
+ ### Marker accessor shape UNIFIED in v10.3
44
+
45
+ **As of v10.3, predicate accessors are bare-named everywhere** — matching `FormControl.dirty` / `.valid` and Angular signals conventions. The old `is`-prefix names on `status` and `entityMap.isEmpty` are kept as **deprecated aliases through v10.x** (return the same Signal instance, no double cost). Removal in v11.0.
46
+
47
+ | Marker | v10.3 canonical (preferred) | Deprecated alias (v10.x only) |
48
+ |---|---|---|
49
+ | `status` | `.loading`, `.loaded`, `.notLoaded`, `.hasError` | `.isLoading`, `.isLoaded`, `.isNotLoaded`, `.isError` |
50
+ | `entityMap` | `.empty` | `.isEmpty` |
51
+ | `form` | `.dirty`, `.valid`, `.touched`, `.pristine` | (already bare — unchanged) |
52
+ | `asyncSource` / `asyncQuery` | `.loading`, `.error`, `.data` | (already bare — unchanged) |
53
+
54
+ All predicates are **callable `Signal<boolean>`** — invoke them: `tree.$.load.loading()`, `tree.$.users.empty()`.
55
+
56
+ ### Status marker — method names (frequently confused)
44
57
 
45
58
  The `status()` marker's canonical methods are **`setLoading` / `setLoaded` / `setError`**. As of **v10.2**, Promise-vocabulary aliases also work (identical semantics):
46
59
 
@@ -50,8 +63,8 @@ The `status()` marker's canonical methods are **`setLoading` / `setLoaded` / `se
50
63
  | `.start()` | `.setLoading()` | Yes — alias |
51
64
  | `.succeed()` | `.setLoaded()` | Yes — alias |
52
65
  | `.fail(err)` | `.setError(err)` | Yes — alias |
53
- | `.loading` (bare property) | `.isLoading()` (call as signal) | **Nomust call** |
54
- | `.error` (bare property) | `.error()` (call as signal) | **Nomust call** |
66
+ | `.loading` (bare property — read-only) | call as signal: `.loading()` | Yescallable Signal |
67
+ | `.error` (bare property — read-only) | call as signal: `.error()` | Yesreturns error value |
55
68
 
56
69
  ### Canonical async pattern — use `asyncSource`, NOT `status` + manual try/catch
57
70
 
@@ -7,6 +7,7 @@ function createEntitySignal(config, pathNotifier, basePath) {
7
7
  const idsSignal = signal([]);
8
8
  const mapSignal = signal(new Map());
9
9
  const nodeCache = new Map();
10
+ let cachedEmpty = null;
10
11
  const selectId = config.selectId ?? (entity => entity['id']);
11
12
  const tapHandlers = [];
12
13
  const interceptHandlers = [];
@@ -98,8 +99,11 @@ function createEntitySignal(config, pathNotifier, basePath) {
98
99
  has(id) {
99
100
  return computed(() => mapSignal().has(id));
100
101
  },
102
+ get empty() {
103
+ return cachedEmpty ??= computed(() => countSignal() === 0);
104
+ },
101
105
  get isEmpty() {
102
- return computed(() => countSignal() === 0);
106
+ return cachedEmpty ??= computed(() => countSignal() === 0);
103
107
  },
104
108
  where(predicate) {
105
109
  const cached = whereCache.get(predicate);
@@ -26,24 +26,40 @@ function isStatusMarker(value) {
26
26
  function createStatusSignal(marker) {
27
27
  const stateSignal = signal(marker.initialState);
28
28
  const errorSignal = signal(null);
29
- let _isNotLoaded = null;
30
- let _isLoading = null;
31
- let _isLoaded = null;
32
- let _isError = null;
29
+ let _notLoaded = null;
30
+ let _loading = null;
31
+ let _loaded = null;
32
+ let _hasError = null;
33
+ const getNotLoaded = () => _notLoaded ??= computed(() => stateSignal() === LoadingState.NotLoaded);
34
+ const getLoading = () => _loading ??= computed(() => stateSignal() === LoadingState.Loading);
35
+ const getLoaded = () => _loaded ??= computed(() => stateSignal() === LoadingState.Loaded);
36
+ const getHasError = () => _hasError ??= computed(() => stateSignal() === LoadingState.Error);
33
37
  return {
34
38
  state: stateSignal,
35
39
  error: errorSignal,
40
+ get notLoaded() {
41
+ return getNotLoaded();
42
+ },
43
+ get loading() {
44
+ return getLoading();
45
+ },
46
+ get loaded() {
47
+ return getLoaded();
48
+ },
49
+ get hasError() {
50
+ return getHasError();
51
+ },
36
52
  get isNotLoaded() {
37
- return _isNotLoaded ??= computed(() => stateSignal() === LoadingState.NotLoaded);
53
+ return getNotLoaded();
38
54
  },
39
55
  get isLoading() {
40
- return _isLoading ??= computed(() => stateSignal() === LoadingState.Loading);
56
+ return getLoading();
41
57
  },
42
58
  get isLoaded() {
43
- return _isLoaded ??= computed(() => stateSignal() === LoadingState.Loaded);
59
+ return getLoaded();
44
60
  },
45
61
  get isError() {
46
- return _isError ??= computed(() => stateSignal() === LoadingState.Error);
62
+ return getHasError();
47
63
  },
48
64
  setNotLoaded() {
49
65
  stateSignal.set(LoadingState.NotLoaded);
package/llms-full.txt CHANGED
@@ -520,6 +520,23 @@ This is the pattern enforced in production migrations. The `$` access stays read
520
520
  | `Store.dispatch(action)`, `Store.select(selector)` | **`@ngrx/store` (classic NgRx)** | Direct tree access: `tree.$.path()` to read, `tree.$.path.set(v)` to write |
521
521
  | `.toPromise()` on Observables (deprecated RxJS 7+) | RxJS legacy | `firstValueFrom(observable)` — or let `asyncSource` consume the Observable directly |
522
522
 
523
+ ### Marker accessor shape — UNIFIED in v10.3 (bare callable signals)
524
+
525
+ **v10.3 aligns predicate names across all markers** to match `FormControl` / Angular signal conventions. Bare names (`loading`, `loaded`, `empty`, etc.) are now canonical everywhere. The old `is`-prefix names on `status` and `entityMap.isEmpty` are deprecated aliases through v10.x — they return the same Signal instance as the canonical names. Removal in v11.0.
526
+
527
+ | Marker | v10.3 canonical predicate | Deprecated alias (v10.x only) |
528
+ |---|---|---|
529
+ | `status` | `.loading` | `.isLoading` |
530
+ | `status` | `.loaded` | `.isLoaded` |
531
+ | `status` | `.notLoaded` | `.isNotLoaded` |
532
+ | `status` | `.hasError` | `.isError` |
533
+ | `entityMap` | `.empty` | `.isEmpty` |
534
+ | `form` | `.dirty`, `.valid`, `.touched`, `.pristine` | (already bare — unchanged) |
535
+ | `asyncSource` | `.loading`, `.error`, `.data` | (already bare — unchanged) |
536
+ | `asyncQuery` | `.loading`, `.error`, `.data` | (already bare — unchanged) |
537
+
538
+ All predicates are callable `Signal<boolean>` — invoke them: `tree.$.load.loading()`. Both the canonical and deprecated alias return the **same Signal instance**, so there's no double computed cost.
539
+
523
540
  ### Status marker — exact method names (frequently confused)
524
541
 
525
542
  The `status()` marker's canonical methods are **`setLoading` / `setLoaded` / `setError`**, NOT Promise-vocabulary names (`setSuccess`, `start`, `succeed`, `fail`). However, **as of v10.2 the Promise-vocabulary aliases also work** — they delegate to the canonical methods with identical semantics. Use either; canonical is preferred in new code for searchability.
@@ -530,9 +547,8 @@ The `status()` marker's canonical methods are **`setLoading` / `setLoaded` / `se
530
547
  | `.start()` | `.setLoading()` | Yes — alias |
531
548
  | `.succeed()` | `.setLoaded()` | Yes — alias |
532
549
  | `.fail(err)` | `.setError(err)` | Yes — alias |
533
- | `.loading` (bare property) | `.isLoading()` (callable signal) | No — must call as signal |
534
- | `.error` (bare property) | `.error()` (callable signal) | No must call as signal |
535
- | `.success` (bare property) | `.isLoaded()` (callable signal) | No — must call as signal |
550
+ | `.loading` (bare property — read-only) | call as signal: `.loading()` | Yes (callable signal) |
551
+ | `.error` (bare property — read-only) | call as signal: `.error()` | Yes (returns error value E \| null) |
536
552
 
537
553
  ### Async patterns — prefer `asyncSource` / `asyncQuery` over manual `status` + try/catch
538
554
 
package/llms.txt CHANGED
@@ -120,18 +120,37 @@ This table catches the most common cross-library hallucinations. **Every "Wrong"
120
120
  | `.upsert(user)` on entity collections | **Akita** | `.upsertOne(user)` (singular suffix) |
121
121
  | `BehaviorSubject<T>`, `.next(v)`, `.asObservable()` | **RxJS classic** | A plain leaf in the `signalTree()` literal — no Observable wrapping needed |
122
122
 
123
+ ### Marker accessor shape — UNIFIED in v10.3 (bare callable signals)
124
+
125
+ **As of v10.3, every marker uses the same accessor shape: bare-named callable signals (matching `FormControl.dirty` / `.valid` and Angular signals conventions).** The `is`-prefix names that used to appear on `status` and `entityMap.isEmpty` are kept as deprecated aliases through v10.x and will be removed in v11.0.
126
+
127
+ **Cross-marker predicate naming (v10.3 canonical):**
128
+
129
+ | Marker | Predicate signal (v10.3) | Old name (deprecated alias, v10.x only) |
130
+ |---|---|---|
131
+ | `status` | `.loading` | `.isLoading` |
132
+ | `status` | `.loaded` | `.isLoaded` |
133
+ | `status` | `.notLoaded` | `.isNotLoaded` |
134
+ | `status` | `.hasError` | `.isError` |
135
+ | `entityMap` | `.empty` | `.isEmpty` |
136
+ | `form` | `.dirty`, `.valid`, `.touched`, `.pristine` | (already bare — unchanged) |
137
+ | `asyncSource` | `.loading`, `.error`, `.data` | (already bare — unchanged) |
138
+ | `asyncQuery` | `.loading`, `.error`, `.data` | (already bare — unchanged) |
139
+
140
+ All predicates are **callable Signals** — invoke them: `tree.$.load.loading()`, `tree.$.users.empty()`. Both `.loading` and `.isLoading` return the same underlying Signal instance; no double allocation.
141
+
123
142
  ### Status marker — exact method names (frequently confused)
124
143
 
125
- The `status()` marker uses **`setLoading` / `setLoaded` / `setError`**, NOT Promise-vocabulary names. **Aliases ship as of v10.2** so the most common wrong-names also work, but the canonical names are:
144
+ The `status()` marker's canonical methods are **`setLoading` / `setLoaded` / `setError` / `setNotLoaded` / `reset`**. Promise-vocabulary aliases also work as of v10.2 (identical semantics):
126
145
 
127
- | Wrong (Promise-vocab guess) | Correct (canonical) | Notes |
146
+ | Wrong (Promise-vocab guess) | Canonical | Notes |
128
147
  |---|---|---|
129
148
  | `.setSuccess()` | **`.setLoaded()`** | Alias `.setSuccess` works in v10.2+ |
130
149
  | `.start()` | **`.setLoading()`** | Alias `.start` works in v10.2+ |
131
150
  | `.succeed()` | **`.setLoaded()`** | Alias `.succeed` works in v10.2+ |
132
151
  | `.fail(err)` | **`.setError(err)`** | Alias `.fail` works in v10.2+ |
133
- | `.loading` (property) | **`.isLoading()`** (callable signal invoke it) |
134
- | `.error` (property) | **`.error()`** (callable signal invoke it) |
152
+ | `.loading` (bare property, can't be assigned) | **`.loading()`** (call as signal) | Read-only derived signal |
153
+ | `.error` (bare property, can't be assigned) | **`.error()`** (call as signal) | Read-only error value |
135
154
 
136
155
  ### Async pattern — prefer `asyncSource` / `asyncQuery` over `status` + manual try/catch
137
156
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaltree/core",
3
- "version": "10.2.0",
3
+ "version": "10.3.0",
4
4
  "description": "Reactive JSON for Angular. JSON branches, reactive leaves. No actions. No reducers. No selectors.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,6 +17,10 @@ export interface StatusMarker<E = Error> {
17
17
  export interface StatusSignal<E = Error> {
18
18
  state: WritableSignal<LoadingState>;
19
19
  error: WritableSignal<E | null>;
20
+ notLoaded: Signal<boolean>;
21
+ loading: Signal<boolean>;
22
+ loaded: Signal<boolean>;
23
+ hasError: Signal<boolean>;
20
24
  isNotLoaded: Signal<boolean>;
21
25
  isLoading: Signal<boolean>;
22
26
  isLoaded: Signal<boolean>;
@@ -180,6 +180,7 @@ export interface EntitySignal<E, K extends string | number = string> {
180
180
  readonly count: Signal<number>;
181
181
  readonly ids: Signal<K[]>;
182
182
  has(id: K): Signal<boolean>;
183
+ readonly empty: Signal<boolean>;
183
184
  readonly isEmpty: Signal<boolean>;
184
185
  readonly map: Signal<ReadonlyMap<K, E>>;
185
186
  where(predicate: (entity: E) => boolean): Signal<E[]>;