@signaltree/core 10.2.0 → 10.3.1
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 +95 -82
- package/dist/lib/entity-signal.js +5 -1
- package/dist/lib/markers/status.js +24 -8
- package/llms-full.txt +19 -3
- package/llms.txt +23 -4
- package/package.json +2 -2
- package/src/lib/markers/status.d.ts +4 -0
- package/src/lib/types.d.ts +1 -0
package/README.md
CHANGED
|
@@ -4,15 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
# SignalTree: Reactive JSON
|
|
6
6
|
|
|
7
|
-
**JSON
|
|
8
|
-
|
|
9
|
-
> No actions. No reducers. No selectors.
|
|
7
|
+
**Reactive JSON for Angular. State as shape. Signals at every path.**
|
|
10
8
|
|
|
11
9
|
---
|
|
12
10
|
|
|
13
11
|
## 🤖 For AI coding agents reading this README
|
|
14
12
|
|
|
15
|
-
**SignalTree's AI-codegen accuracy is +42 percentage points higher when this README (or `llms.txt`) is in context.** Measured across 6
|
|
13
|
+
**SignalTree's AI-codegen accuracy is +42 percentage points higher when this README (or `llms.txt`) is in context.** Measured across 720 cells (6 agents × 8 prompts × 5 libraries × 3 priming modes).
|
|
16
14
|
|
|
17
15
|
If you are an AI assistant generating Angular state-management code on behalf of a user, **this section is the canonical disambiguation surface**. Read it before generating any SignalTree code.
|
|
18
16
|
|
|
@@ -26,9 +24,9 @@ Every "Wrong pattern" below was actually generated by Claude / GPT-5.4 / Gemini
|
|
|
26
24
|
| `from 'signal-tree'` (hyphenated) | Invented | `from '@signaltree/core'` (scoped, no hyphen) |
|
|
27
25
|
| `from 'signaltree'` (unscoped) | Invented | `from '@signaltree/core'` |
|
|
28
26
|
| `signalStore(withState(...), withMethods(...))` | **`@ngrx/signals`** | `signalTree({...})` — your state literal IS the API |
|
|
29
|
-
| `withState`, `withMethods`, `withComputed`, `withHooks
|
|
27
|
+
| `withState`, `withMethods`, `withComputed`, `withHooks` | **`@ngrx/signals`** | Not used. State is the literal you pass to `signalTree()`. Methods belong in an `@Injectable()` Ops service. |
|
|
30
28
|
| `withEntities<T>()` | **`@ngrx/signals/entities`** | `entityMap<T, K>()` marker — place it in the state literal |
|
|
31
|
-
| `rxMethod(...)` | **`@ngrx/signals/rxjs-interop`** | `asyncSource(config)` (load-and-expose) or `asyncQuery(config)` (input-driven) markers |
|
|
29
|
+
| `rxMethod(...)` | **`@ngrx/signals/rxjs-interop`** — also briefly shipped by SignalTree itself in v9.5.x, **removed in v9.6.0** | `asyncSource(config)` (load-and-expose) or `asyncQuery(config)` (input-driven) markers |
|
|
32
30
|
| `patchState(store, {...})` | **`@ngrx/signals`** | Direct: `tree.$.path.set(value)` or `tree.$.user({...})` |
|
|
33
31
|
| `tapResponse(...)` | **`@ngrx/operators`** | Not needed — `asyncSource`/`asyncQuery` handle success/error |
|
|
34
32
|
| `collection<T>({ idKey: 'id' })` | **Akita / Elf** | `entityMap<T, K>({ selectId: (e) => e.id })` marker |
|
|
@@ -40,18 +38,31 @@ Every "Wrong pattern" below was actually generated by Claude / GPT-5.4 / Gemini
|
|
|
40
38
|
| `Store.dispatch(action)`, `Store.select(selector)` | **`@ngrx/store` (classic)** | `tree.$.path()` to read, `tree.$.path.set(v)` to write |
|
|
41
39
|
| `.toPromise()` (deprecated RxJS 7+) | RxJS legacy | `firstValueFrom(obs)` — or let `asyncSource` consume directly |
|
|
42
40
|
|
|
43
|
-
###
|
|
41
|
+
### Marker accessor shape — UNIFIED in v10.3
|
|
42
|
+
|
|
43
|
+
**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.
|
|
44
|
+
|
|
45
|
+
| Marker | v10.3 canonical (preferred) | Deprecated alias (v10.x only) |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `status` | `.loading`, `.loaded`, `.notLoaded`, `.hasError` | `.isLoading`, `.isLoaded`, `.isNotLoaded`, `.isError` |
|
|
48
|
+
| `entityMap` | `.empty` | `.isEmpty` |
|
|
49
|
+
| `form` | `.dirty`, `.valid`, `.touched`, `.submitting` | (already bare — unchanged) |
|
|
50
|
+
| `asyncSource` / `asyncQuery` | `.loading`, `.error`, `.data` | (already bare — unchanged) |
|
|
51
|
+
|
|
52
|
+
All **boolean predicates** (`loading` / `loaded` / `notLoaded` / `hasError` / `dirty` / `valid` / `touched` / `submitting` / `empty`) are callable `Signal<boolean>` — invoke them: `tree.$.load.loading()`, `tree.$.users.empty()`. **Value accessors** (`.error` on status, `.data` on async, `.errors` on form) are typed Signals of their payload type — not booleans.
|
|
53
|
+
|
|
54
|
+
### Status marker — method names (frequently confused)
|
|
44
55
|
|
|
45
56
|
The `status()` marker's canonical methods are **`setLoading` / `setLoaded` / `setError`**. As of **v10.2**, Promise-vocabulary aliases also work (identical semantics):
|
|
46
57
|
|
|
47
58
|
| Wrong-but-now-aliased (v10.2+) | Canonical | Equivalent? |
|
|
48
59
|
|---|---|---|
|
|
49
|
-
| `.setSuccess()` | `.setLoaded()` | Yes — alias |
|
|
60
|
+
| `.setSuccess()` (no args) | `.setLoaded()` | Yes — alias |
|
|
50
61
|
| `.start()` | `.setLoading()` | Yes — alias |
|
|
51
62
|
| `.succeed()` | `.setLoaded()` | Yes — alias |
|
|
52
63
|
| `.fail(err)` | `.setError(err)` | Yes — alias |
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
|
|
65
|
+
**Reading status:** `.loading()`, `.loaded()`, `.notLoaded()`, `.hasError()` are callable `Signal<boolean>`. `.state` is the source `WritableSignal<LoadingState>`. `.error` is `WritableSignal<E | null>` — invoke as `.error()` to read.
|
|
55
66
|
|
|
56
67
|
### Canonical async pattern — use `asyncSource`, NOT `status` + manual try/catch
|
|
57
68
|
|
|
@@ -71,7 +82,7 @@ export class UsersService {
|
|
|
71
82
|
}),
|
|
72
83
|
});
|
|
73
84
|
|
|
74
|
-
readonly users = this.tree.$.users; // .users() →
|
|
85
|
+
readonly users = this.tree.$.users; // .users() → User[] | undefined, .users.loading(), .users.error()
|
|
75
86
|
load = () => this.tree.$.users.refresh();
|
|
76
87
|
}
|
|
77
88
|
```
|
|
@@ -90,7 +101,9 @@ export class AppService {
|
|
|
90
101
|
private readonly tree = signalTree({
|
|
91
102
|
users: entityMap<User, number>(),
|
|
92
103
|
saveStatus: status(),
|
|
93
|
-
profile: form
|
|
104
|
+
profile: form<{ firstName: string; lastName: string }>({
|
|
105
|
+
initial: { firstName: '', lastName: '' },
|
|
106
|
+
}),
|
|
94
107
|
feed: asyncSource<Post[]>({ initial: [], load: () => api.feed$() }),
|
|
95
108
|
});
|
|
96
109
|
|
|
@@ -139,17 +152,15 @@ You don't model state as actions, reducers, selectors, or classes — you model
|
|
|
139
152
|
|
|
140
153
|
## Import guidance (tree-shaking)
|
|
141
154
|
|
|
142
|
-
Modern bundlers (
|
|
155
|
+
Modern bundlers (Vite, esbuild, Rollup, webpack 5+) **automatically tree-shake barrel imports** from `@signaltree/core`. Unused enhancers and markers drop out of the bundle.
|
|
143
156
|
|
|
144
157
|
```ts
|
|
145
|
-
//
|
|
158
|
+
// Import only what you use — unused symbols are tree-shaken away
|
|
146
159
|
import { signalTree, batching } from '@signaltree/core';
|
|
147
|
-
|
|
148
|
-
// ✅ Also fine: Explicit subpath (same bundle size)
|
|
149
|
-
import { signalTree } from '@signaltree/core';
|
|
150
|
-
import { batching } from '@signaltree/core/enhancers/batching';
|
|
151
160
|
```
|
|
152
161
|
|
|
162
|
+
Published subpaths (in `package.json` `exports`): `./security`, `./edit-session`, `./storage`. Enhancers are NOT a published subpath — they live in the main barrel and are tree-shaken from there.
|
|
163
|
+
|
|
153
164
|
**Measured impact** (with modern bundlers):
|
|
154
165
|
|
|
155
166
|
- Core only: ~8.5 KB gzipped
|
|
@@ -184,35 +195,36 @@ const tree = signalTree({ count: 0 });
|
|
|
184
195
|
|
|
185
196
|
This repo's ESLint rule is **disabled by default** since testing confirms effective tree-shaking with barrel imports.
|
|
186
197
|
|
|
187
|
-
### Callable
|
|
198
|
+
### Callable shape — branches natively, leaves with the build transform
|
|
188
199
|
|
|
189
|
-
|
|
200
|
+
**Branches are natively callable** for reads AND writes at runtime — no transform required:
|
|
190
201
|
|
|
191
202
|
```typescript
|
|
192
|
-
//
|
|
193
|
-
tree.$.name
|
|
194
|
-
|
|
203
|
+
tree.$.user(); // Read the user subtree
|
|
204
|
+
tree.$.user({ name: 'Jane' }); // Deep-merge partial update at runtime
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Leaves are Angular signals** — callable as getters, but writes go through `.set()` / `.update()`. The `@signaltree/callable-syntax` build-time transform extends the branch's call-with-arg shape down to leaf writes, so call-sites read uniformly from root to leaf:
|
|
195
208
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
tree.$.
|
|
209
|
+
```typescript
|
|
210
|
+
// With @signaltree/callable-syntax transform installed:
|
|
211
|
+
tree.$.name('Jane'); // compiles to tree.$.name.set('Jane')
|
|
212
|
+
tree.$.count((n) => n + 1); // compiles to tree.$.count.update((n) => n + 1)
|
|
199
213
|
|
|
200
|
-
//
|
|
201
|
-
const name = tree.$.name();
|
|
214
|
+
// Without the transform — leaf reads work, leaf writes use .set() / .update():
|
|
215
|
+
const name = tree.$.name();
|
|
216
|
+
tree.$.name.set('Jane');
|
|
217
|
+
tree.$.count.update((n) => n + 1);
|
|
202
218
|
```
|
|
203
219
|
|
|
204
220
|
**Key Points:**
|
|
205
221
|
|
|
206
|
-
- **Zero runtime overhead**:
|
|
207
|
-
- **
|
|
208
|
-
- **
|
|
209
|
-
- **
|
|
210
|
-
|
|
211
|
-
**Function-valued leaves:**
|
|
212
|
-
When a leaf stores a function as its value, use direct `.set(fn)` to assign. Callable `sig(fn)` is treated as an updater.
|
|
222
|
+
- **Zero runtime overhead**: branch callables are a native part of the proxy; the leaf-write transform compiles away before production
|
|
223
|
+
- **Optional**: `@signaltree/callable-syntax` is only needed for leaf-write sugar — `.set()` / `.update()` always work
|
|
224
|
+
- **Type-safe**: full TypeScript support via module augmentation
|
|
225
|
+
- **Configure `rootIdentifiers`**: the transform's default is `['tree']`; if your variable is named `store`/`state`, add it to the plugin options or the rewrite is skipped
|
|
213
226
|
|
|
214
|
-
**
|
|
215
|
-
Install `@signaltree/callable-syntax` and configure your build tool to apply the transform. Without the transform, use `.set/.update` directly.
|
|
227
|
+
**Function-valued leaves:** when a leaf stores a function as its value, use direct `.set(fn)` to assign. Callable `sig(fn)` is treated as an updater.
|
|
216
228
|
|
|
217
229
|
### Measuring performance and size
|
|
218
230
|
|
|
@@ -269,7 +281,7 @@ export function createUserTree() {
|
|
|
269
281
|
// ✅ Correct: Derived from multiple signals
|
|
270
282
|
const selectedUser = computed(() => {
|
|
271
283
|
const id = $.selected.userId();
|
|
272
|
-
return id ? $.users.byId(id)() : null;
|
|
284
|
+
return id ? $.users.byId(id)?.() ?? null : null;
|
|
273
285
|
});
|
|
274
286
|
|
|
275
287
|
// ❌ Wrong: Wrapping an existing signal
|
|
@@ -280,12 +292,12 @@ const selectedUserId = computed(() => $.selected.userId()); // Unnecessary!
|
|
|
280
292
|
|
|
281
293
|
```typescript
|
|
282
294
|
// ✅ SignalTree-native
|
|
283
|
-
const user = $.users.byId(123)(); //
|
|
295
|
+
const user = $.users.byId(123)?.(); // EntityNode → User | undefined
|
|
284
296
|
const allUsers = $.users.all; // Get all
|
|
285
297
|
$.users.setAll(usersFromApi); // Replace all
|
|
286
298
|
|
|
287
|
-
//
|
|
288
|
-
const user = entityMap()[123];
|
|
299
|
+
// (NgRx Signal Store equivalent — for context, not SignalTree syntax)
|
|
300
|
+
// const user = usersStore.entityMap()[123];
|
|
289
301
|
```
|
|
290
302
|
|
|
291
303
|
### Notification Batching
|
|
@@ -608,9 +620,9 @@ const tree = signalTree<AppState>({
|
|
|
608
620
|
},
|
|
609
621
|
});
|
|
610
622
|
|
|
611
|
-
// Complex updates with type safety
|
|
612
|
-
//
|
|
613
|
-
//
|
|
623
|
+
// Complex updates with type safety — the root accessor itself is callable
|
|
624
|
+
// (no @signaltree/callable-syntax transform required for the root). Pass a
|
|
625
|
+
// partial object or an updater function. For leaf writes, use .set() / .update().
|
|
614
626
|
tree((state) => ({
|
|
615
627
|
auth: {
|
|
616
628
|
...state.auth,
|
|
@@ -709,7 +721,7 @@ const activeUsers = computed(() => tree.$.users().filter((user) => user.active))
|
|
|
709
721
|
|
|
710
722
|
### 4) Manual async state management
|
|
711
723
|
|
|
712
|
-
Core provides basic state updates. For
|
|
724
|
+
Core provides basic state updates. For canonical async patterns, use the **`asyncSource` and `asyncQuery` markers** (see the async section). The patterns below show the manual style for cases the markers don't cover (multi-stage orchestration, conditional pipelines):
|
|
713
725
|
|
|
714
726
|
```typescript
|
|
715
727
|
const tree = signalTree({
|
|
@@ -770,8 +782,8 @@ All enhancers are exported directly from `@signaltree/core`:
|
|
|
770
782
|
|
|
771
783
|
**Data Management:**
|
|
772
784
|
|
|
773
|
-
- `
|
|
774
|
-
- `
|
|
785
|
+
- `asyncSource(config)` marker - Load-and-expose async state (canonical, v9.5+)
|
|
786
|
+
- `asyncQuery(config)` marker - Input-driven debounced query state (canonical, v9.5+)
|
|
775
787
|
- `serialization()` - State persistence and SSR support
|
|
776
788
|
- `persistence()` - Auto-save to localStorage/IndexedDB
|
|
777
789
|
|
|
@@ -838,7 +850,10 @@ tree.$.products.addOne(newProduct);
|
|
|
838
850
|
tree.$.products.setAll(productsFromApi);
|
|
839
851
|
|
|
840
852
|
// Entity queries
|
|
841
|
-
|
|
853
|
+
// Reactive: returns Signal<Product[]> that tracks the predicate
|
|
854
|
+
const electronics = tree.$.products.where((p) => p.category === 'electronics');
|
|
855
|
+
// One-shot non-reactive read: call .all() then filter
|
|
856
|
+
const electronicsSnapshot = tree.$.products.all().filter((p) => p.category === 'electronics');
|
|
842
857
|
```
|
|
843
858
|
|
|
844
859
|
**Full-Stack Application:**
|
|
@@ -897,7 +912,7 @@ const tree = signalTree(state)
|
|
|
897
912
|
SignalTree Core includes all enhancer functionality built-in. No separate packages needed:
|
|
898
913
|
|
|
899
914
|
```typescript
|
|
900
|
-
import { signalTree, entityMap
|
|
915
|
+
import { signalTree, entityMap } from '@signaltree/core';
|
|
901
916
|
|
|
902
917
|
// Without entityMap - use manual array updates
|
|
903
918
|
const basic = signalTree({ users: [] as User[] });
|
|
@@ -909,7 +924,7 @@ const enhanced = signalTree({
|
|
|
909
924
|
});
|
|
910
925
|
|
|
911
926
|
enhanced.$.users.addOne(newUser); // ✅ Advanced CRUD operations
|
|
912
|
-
enhanced.$.users.byId(123)(); // ✅ O(1) lookups
|
|
927
|
+
enhanced.$.users.byId(123)?.(); // ✅ O(1) lookups (undefined if missing)
|
|
913
928
|
enhanced.$.users.all; // ✅ Get all as array
|
|
914
929
|
```
|
|
915
930
|
|
|
@@ -1248,17 +1263,26 @@ const tree = signalTree({
|
|
|
1248
1263
|
tree.$.users.loadStatus.state(); // Signal<LoadingState>
|
|
1249
1264
|
tree.$.users.loadStatus.error(); // Signal<ApiError | null>
|
|
1250
1265
|
|
|
1251
|
-
// Convenience signals
|
|
1252
|
-
tree.$.users.loadStatus.
|
|
1253
|
-
tree.$.users.loadStatus.
|
|
1254
|
-
tree.$.users.loadStatus.
|
|
1255
|
-
tree.$.users.loadStatus.
|
|
1266
|
+
// Convenience signals (v10.3 canonical — bare names)
|
|
1267
|
+
tree.$.users.loadStatus.notLoaded(); // Signal<boolean>
|
|
1268
|
+
tree.$.users.loadStatus.loading(); // Signal<boolean>
|
|
1269
|
+
tree.$.users.loadStatus.loaded(); // Signal<boolean>
|
|
1270
|
+
tree.$.users.loadStatus.hasError(); // Signal<boolean>
|
|
1271
|
+
// Deprecated aliases (work through v10.x, removed v11):
|
|
1272
|
+
// .isNotLoaded(), .isLoading(), .isLoaded(), .isError()
|
|
1256
1273
|
|
|
1257
|
-
// Update methods
|
|
1274
|
+
// Update methods (canonical)
|
|
1258
1275
|
tree.$.users.loadStatus.setLoading();
|
|
1259
1276
|
tree.$.users.loadStatus.setLoaded();
|
|
1260
1277
|
tree.$.users.loadStatus.setError({ code: 404, message: 'Not found' });
|
|
1261
|
-
tree.$.users.loadStatus.
|
|
1278
|
+
tree.$.users.loadStatus.setNotLoaded();
|
|
1279
|
+
tree.$.users.loadStatus.reset(); // alias for setNotLoaded
|
|
1280
|
+
|
|
1281
|
+
// v10.2+ Promise-vocabulary aliases (identical semantics, no args)
|
|
1282
|
+
tree.$.users.loadStatus.start(); // === setLoading()
|
|
1283
|
+
tree.$.users.loadStatus.setSuccess(); // === setLoaded() — NO ARGS
|
|
1284
|
+
tree.$.users.loadStatus.succeed(); // === setLoaded()
|
|
1285
|
+
tree.$.users.loadStatus.fail(err); // === setError(err)
|
|
1262
1286
|
|
|
1263
1287
|
// LoadingState enum
|
|
1264
1288
|
LoadingState.NotLoaded; // 'not-loaded'
|
|
@@ -1533,23 +1557,12 @@ const tree = signalTree({
|
|
|
1533
1557
|
async function handleSubmit() {
|
|
1534
1558
|
const contactForm = tree.$.contact;
|
|
1535
1559
|
|
|
1536
|
-
//
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
if (!isValid) return;
|
|
1541
|
-
|
|
1542
|
-
// Set submitting state
|
|
1543
|
-
contactForm.setSubmitting(true);
|
|
1544
|
-
|
|
1545
|
-
try {
|
|
1546
|
-
await api.submit(contactForm());
|
|
1560
|
+
// .submit() handles touchAll / validate / submitting toggling / error trapping
|
|
1561
|
+
// internally. Pass a handler that does the actual network call.
|
|
1562
|
+
await contactForm.submit(async (values) => {
|
|
1563
|
+
await api.submit(values);
|
|
1547
1564
|
contactForm.reset();
|
|
1548
|
-
}
|
|
1549
|
-
// Handle error
|
|
1550
|
-
} finally {
|
|
1551
|
-
contactForm.setSubmitting(false);
|
|
1552
|
-
}
|
|
1565
|
+
});
|
|
1553
1566
|
}
|
|
1554
1567
|
```
|
|
1555
1568
|
|
|
@@ -1702,7 +1715,7 @@ const filteredProducts = computed(() => {
|
|
|
1702
1715
|
### Data Management Composition
|
|
1703
1716
|
|
|
1704
1717
|
```typescript
|
|
1705
|
-
import { signalTree, entityMap
|
|
1718
|
+
import { signalTree, entityMap } from '@signaltree/core';
|
|
1706
1719
|
|
|
1707
1720
|
// entityMap() markers self-register — entity operations available immediately
|
|
1708
1721
|
const tree = signalTree({
|
|
@@ -1714,7 +1727,7 @@ const tree = signalTree({
|
|
|
1714
1727
|
// Advanced entity operations via tree.$ accessor
|
|
1715
1728
|
tree.$.users.addOne(newUser);
|
|
1716
1729
|
tree.$.users.where((u) => u.active);
|
|
1717
|
-
tree.$.users.updateMany([
|
|
1730
|
+
tree.$.users.updateMany(['1', '2', '3'], { status: 'active' }); // ids + shared changes
|
|
1718
1731
|
|
|
1719
1732
|
// Entity helpers work with nested structures
|
|
1720
1733
|
// Example: deeply nested entities in a domain-driven design pattern
|
|
@@ -1790,7 +1803,7 @@ const tree = signalTree({
|
|
|
1790
1803
|
async function fetchUser(id: string) {
|
|
1791
1804
|
return await api.getUser(id);
|
|
1792
1805
|
}
|
|
1793
|
-
tree.$.app.data.users.byId(userId)(); // O(1) lookup
|
|
1806
|
+
tree.$.app.data.users.byId(userId)?.(); // O(1) lookup (undefined if missing)
|
|
1794
1807
|
tree.undo(); // Time travel
|
|
1795
1808
|
tree.save(); // Persistence
|
|
1796
1809
|
```
|
|
@@ -1854,7 +1867,7 @@ In Redux DevTools you will see a single instance named `"MyApp SignalTree"` with
|
|
|
1854
1867
|
### Production-Ready Composition
|
|
1855
1868
|
|
|
1856
1869
|
```typescript
|
|
1857
|
-
import { signalTree, batching,
|
|
1870
|
+
import { signalTree, batching, serialization } from '@signaltree/core';
|
|
1858
1871
|
|
|
1859
1872
|
// Production build (no dev tools)
|
|
1860
1873
|
const tree = signalTree(initialState)
|
|
@@ -1917,7 +1930,7 @@ const tree = signalTree(state);
|
|
|
1917
1930
|
const tree2 = tree.with(batching());
|
|
1918
1931
|
|
|
1919
1932
|
// Phase 3: Add async for API integration
|
|
1920
|
-
// withAsync removed —
|
|
1933
|
+
// withAsync removed in v9 — use the asyncSource() / asyncQuery() markers (v10.x canonical) for load-and-expose and input-driven flows.
|
|
1921
1934
|
|
|
1922
1935
|
// Each phase is fully functional and production-ready
|
|
1923
1936
|
```
|
|
@@ -2205,7 +2218,7 @@ tree.destroy(); // Cleanup resources
|
|
|
2205
2218
|
|
|
2206
2219
|
// Entity helpers (entityMap() self-registers — no enhancer needed)
|
|
2207
2220
|
tree.$.users.addOne(user); // Add single entity
|
|
2208
|
-
tree.$.users.byId(id)();
|
|
2221
|
+
tree.$.users.byId(id)?.(); // O(1) cursor unwrap (undefined if missing)
|
|
2209
2222
|
tree.$.users.byId(id)?.name(); // Read single field (computed signal)
|
|
2210
2223
|
tree.$.users.byId(id)?.name.set('Bob'); // Write single field (throws if entity removed)
|
|
2211
2224
|
tree.$.users.byId(id)?.name.update(fn); // Updater on single field
|
|
@@ -2267,7 +2280,7 @@ Consider enhancers when you need:
|
|
|
2267
2280
|
|
|
2268
2281
|
- ⚡ Performance optimization (batching)
|
|
2269
2282
|
- 🐛 Advanced debugging (devTools, timeTravel)
|
|
2270
|
-
- 📦 Entity management (
|
|
2283
|
+
- 📦 Entity management (`entityMap` marker)
|
|
2271
2284
|
|
|
2272
2285
|
Consider separate packages when you need:
|
|
2273
2286
|
|
|
@@ -2453,8 +2466,8 @@ npm install @signaltree/core
|
|
|
2453
2466
|
# Everything is available from @signaltree/core:
|
|
2454
2467
|
import {
|
|
2455
2468
|
signalTree,
|
|
2469
|
+
entityMap,
|
|
2456
2470
|
batching,
|
|
2457
|
-
entities,
|
|
2458
2471
|
devTools,
|
|
2459
2472
|
timeTravel,
|
|
2460
2473
|
serialization
|
|
@@ -2784,7 +2797,7 @@ export default {
|
|
|
2784
2797
|
**Start with just `@signaltree/core`** - it includes comprehensive enhancers for most applications:
|
|
2785
2798
|
|
|
2786
2799
|
- Performance optimization (batching)
|
|
2787
|
-
- Data management (
|
|
2800
|
+
- Data management (`entityMap` marker, `asyncSource` / `asyncQuery` markers)
|
|
2788
2801
|
- Development tools (devtools, time-travel)
|
|
2789
2802
|
- State persistence (serialization)
|
|
2790
2803
|
|
|
@@ -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
|
|
30
|
-
let
|
|
31
|
-
let
|
|
32
|
-
let
|
|
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
|
|
53
|
+
return getNotLoaded();
|
|
38
54
|
},
|
|
39
55
|
get isLoading() {
|
|
40
|
-
return
|
|
56
|
+
return getLoading();
|
|
41
57
|
},
|
|
42
58
|
get isLoaded() {
|
|
43
|
-
return
|
|
59
|
+
return getLoaded();
|
|
44
60
|
},
|
|
45
61
|
get isError() {
|
|
46
|
-
return
|
|
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) | `.
|
|
534
|
-
| `.error` (bare property) | `.error()`
|
|
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
|
|
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) |
|
|
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) | **`.
|
|
134
|
-
| `.error` (property) | **`.error()`** (
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signaltree/core",
|
|
3
|
-
"version": "10.
|
|
4
|
-
"description": "Reactive JSON for Angular.
|
|
3
|
+
"version": "10.3.1",
|
|
4
|
+
"description": "Reactive JSON for Angular. State as shape. Signals at every path.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -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>;
|
package/src/lib/types.d.ts
CHANGED
|
@@ -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[]>;
|