@signaltree/core 10.3.1 → 10.3.3
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/llms-full.txt +37 -25
- package/llms.txt +10 -10
- package/package.json +1 -1
package/llms-full.txt
CHANGED
|
@@ -38,9 +38,9 @@ const store = signalTree({
|
|
|
38
38
|
|
|
39
39
|
// With config:
|
|
40
40
|
const store2 = signalTree(initialState, {
|
|
41
|
-
batchUpdates: true,
|
|
42
|
-
|
|
43
|
-
treeName: 'AppStore',
|
|
41
|
+
batchUpdates: true, // default — microtask-level notification batching
|
|
42
|
+
useShallowComparison: true, // switch leaf signal equality from deepEqual to Object.is
|
|
43
|
+
treeName: 'AppStore', // for devtools labeling
|
|
44
44
|
});
|
|
45
45
|
```
|
|
46
46
|
|
|
@@ -59,14 +59,15 @@ Both `store.$` and `store.state` point to the same TreeNode. Use whichever reads
|
|
|
59
59
|
```typescript
|
|
60
60
|
store.$.user.name.set('Bob');
|
|
61
61
|
store.$.user.age.update((n) => n + 1);
|
|
62
|
-
store.$.user({ name: 'Carol', age: 25 }); //
|
|
63
|
-
store({ user: { name: 'Dave', age: 40 }, settings: { theme: 'dark' } }); //
|
|
62
|
+
store.$.user({ name: 'Carol', age: 25 }); // partial deep-merge update; sibling keys preserved
|
|
63
|
+
store({ user: { name: 'Dave', age: 40 }, settings: { theme: 'dark' } }); // partial deep-merge of root; keys not in payload preserved
|
|
64
|
+
store(); // no-arg call returns the current snapshot
|
|
64
65
|
```
|
|
65
66
|
|
|
66
67
|
### Lifecycle
|
|
67
68
|
|
|
68
69
|
```typescript
|
|
69
|
-
store.destroy(); //
|
|
70
|
+
store.destroy(); // runs registered cleanup hooks in registration order
|
|
70
71
|
store.destroyed(); // Signal<boolean>
|
|
71
72
|
store.registerCleanup(() => ws.close()); // custom hook called during destroy
|
|
72
73
|
```
|
|
@@ -95,7 +96,7 @@ store.$.users.setAll(users);
|
|
|
95
96
|
store.$.users.upsertOne(user);
|
|
96
97
|
store.$.users.upsertMany(users);
|
|
97
98
|
store.$.users.updateOne(id, changes);
|
|
98
|
-
store.$.users.updateMany([{ id, changes
|
|
99
|
+
store.$.users.updateMany([id1, id2, id3], { active: false }); // shared Partial<E> applied to every id — NOT NgRx-style [{id, changes}]
|
|
99
100
|
store.$.users.updateWhere(pred, changes);
|
|
100
101
|
store.$.users.removeOne(id);
|
|
101
102
|
store.$.users.removeMany(ids);
|
|
@@ -104,7 +105,7 @@ store.$.users.clear();
|
|
|
104
105
|
|
|
105
106
|
// Queries (all return signals)
|
|
106
107
|
store.$.users.all(); // Signal<User[]>
|
|
107
|
-
store.$.users.byId(id); //
|
|
108
|
+
store.$.users.byId(id); // EntityNode<User> | undefined — invoke: .byId(id)?.() → User | undefined
|
|
108
109
|
store.$.users.count(); // Signal<number>
|
|
109
110
|
store.$.users.has(id); // Signal<boolean>
|
|
110
111
|
store.$.users.ids(); // Signal<number[]>
|
|
@@ -128,20 +129,28 @@ const store = signalTree({
|
|
|
128
129
|
},
|
|
129
130
|
});
|
|
130
131
|
|
|
131
|
-
// Read
|
|
132
|
-
store.$.users.loading.state(); // Signal<LoadingState>
|
|
133
|
-
store.$.users.loading.error(); //
|
|
134
|
-
store.$.users.loading.
|
|
135
|
-
store.$.users.loading.
|
|
136
|
-
store.$.users.loading.
|
|
137
|
-
store.$.users.loading.
|
|
138
|
-
|
|
139
|
-
//
|
|
132
|
+
// Read — v10.3 canonical (bare names)
|
|
133
|
+
store.$.users.loading.state(); // LoadingState (calling Signal<LoadingState>)
|
|
134
|
+
store.$.users.loading.error(); // ApiError | null
|
|
135
|
+
store.$.users.loading.loading(); // boolean
|
|
136
|
+
store.$.users.loading.loaded(); // boolean
|
|
137
|
+
store.$.users.loading.hasError(); // boolean
|
|
138
|
+
store.$.users.loading.notLoaded(); // boolean
|
|
139
|
+
// Deprecated through v10.x, removed v11: .isLoading, .isLoaded, .isError, .isNotLoaded
|
|
140
|
+
// (same Signal instance — both work; canonical preferred in new code)
|
|
141
|
+
|
|
142
|
+
// Mutate — canonical methods
|
|
140
143
|
store.$.users.loading.setLoading();
|
|
141
144
|
store.$.users.loading.setLoaded();
|
|
142
145
|
store.$.users.loading.setError(err);
|
|
143
146
|
store.$.users.loading.setNotLoaded();
|
|
144
147
|
store.$.users.loading.reset();
|
|
148
|
+
|
|
149
|
+
// v10.2+ Promise-vocabulary aliases (identical semantics, no args)
|
|
150
|
+
store.$.users.loading.start(); // === setLoading()
|
|
151
|
+
store.$.users.loading.setSuccess(); // === setLoaded() — NO ARGS
|
|
152
|
+
store.$.users.loading.succeed(); // === setLoaded()
|
|
153
|
+
store.$.users.loading.fail(err); // === setError(err)
|
|
145
154
|
```
|
|
146
155
|
|
|
147
156
|
### `stored(key, defaultValue, options?)`
|
|
@@ -166,9 +175,9 @@ store.$.settings.theme.reload(); // re-read from localStorage
|
|
|
166
175
|
|
|
167
176
|
Supports versioning and migration functions via the third argument. Use for individual per-leaf persistence; use the `persistence()` enhancer for tree-wide persistence with storage adapters.
|
|
168
177
|
|
|
169
|
-
### `form<T>(config)`
|
|
178
|
+
### `form<T>(config: FormConfig<T>)`
|
|
170
179
|
|
|
171
|
-
Tree-integrated form
|
|
180
|
+
Tree-integrated form marker, **exported from `@signaltree/core`** (not from `@signaltree/ng-forms`). Materializes into a form signal exposing field state, validation status, and submit/reset helpers. Requires `{ initial: T, validators?, asyncValidators?, wizard? }`. `@signaltree/ng-forms` is a separate **bridge** that binds these markers to Angular `FormGroup` — useful but not required.
|
|
172
181
|
|
|
173
182
|
### Custom markers via `registerMarkerProcessor(spec)`
|
|
174
183
|
|
|
@@ -278,7 +287,7 @@ const store = signalTree({ count: 0, items: [] })
|
|
|
278
287
|
|
|
279
288
|
store.batch(() => {
|
|
280
289
|
store.$.count.set(10);
|
|
281
|
-
store.$.items.
|
|
290
|
+
store.$.items.update((arr) => [...arr, { id: 1 }]); // .push() doesn't exist — arrays live in a WritableSignal
|
|
282
291
|
});
|
|
283
292
|
|
|
284
293
|
store.undo();
|
|
@@ -303,6 +312,8 @@ store.$.count((n) => n + 1); // → store.$.count.update((n) => n + 1
|
|
|
303
312
|
|
|
304
313
|
**This is not a runtime proxy.** The transform runs at build time and disappears in production — there is no wrapper function, no `Proxy` object, no runtime overhead. Install as a dev dependency, register the Vite or Webpack plugin, and the transform compiles away.
|
|
305
314
|
|
|
315
|
+
> **Configure `rootIdentifiers`**: the plugin's default is `['tree']`. If your tree variable is named `store` or `state` (common in service facades), pass `{ rootIdentifiers: ['tree', 'store', 'state'] }` to the plugin options — variables not in this list are silently skipped.
|
|
316
|
+
|
|
306
317
|
---
|
|
307
318
|
|
|
308
319
|
## Subpath imports
|
|
@@ -310,12 +321,13 @@ store.$.count((n) => n + 1); // → store.$.count.update((n) => n + 1
|
|
|
310
321
|
Specialized APIs live in subpaths to keep the main barrel small:
|
|
311
322
|
|
|
312
323
|
```typescript
|
|
313
|
-
import { TREE_PRESETS, createDevTree, createProdTree } from '@signaltree/core/presets';
|
|
314
324
|
import { SecurityValidator, SecurityPresets } from '@signaltree/core/security';
|
|
315
|
-
import { createEditSession } from '@signaltree/core/edit-session';
|
|
325
|
+
import { createEditSession, createTreeEditSession } from '@signaltree/core/edit-session';
|
|
316
326
|
import { createStorageAdapter, createIndexedDBAdapter } from '@signaltree/core/storage';
|
|
317
327
|
```
|
|
318
328
|
|
|
329
|
+
The only published subpaths in `@signaltree/core` are `./security`, `./edit-session`, and `./storage`. The main barrel (`@signaltree/core`) re-exports everything; modern bundlers tree-shake unused symbols regardless.
|
|
330
|
+
|
|
319
331
|
### Async — `asyncSource` and `asyncQuery` markers (canonical, v9.5+)
|
|
320
332
|
|
|
321
333
|
Async state belongs **at the tree path it describes**. Two markers cover the two main async patterns and compose with the rest of the marker family:
|
|
@@ -531,7 +543,7 @@ This is the pattern enforced in production migrations. The `$` access stays read
|
|
|
531
543
|
| `status` | `.notLoaded` | `.isNotLoaded` |
|
|
532
544
|
| `status` | `.hasError` | `.isError` |
|
|
533
545
|
| `entityMap` | `.empty` | `.isEmpty` |
|
|
534
|
-
| `form` | `.dirty`, `.valid`, `.touched`, `.
|
|
546
|
+
| `form` | `.dirty`, `.valid`, `.touched`, `.submitting` | (already bare — unchanged) |
|
|
535
547
|
| `asyncSource` | `.loading`, `.error`, `.data` | (already bare — unchanged) |
|
|
536
548
|
| `asyncQuery` | `.loading`, `.error`, `.data` | (already bare — unchanged) |
|
|
537
549
|
|
|
@@ -591,7 +603,7 @@ For **input-driven queries** (debounced search, filtered fetch), use `asyncQuery
|
|
|
591
603
|
| "Markers must live at the tree root." | Comparison to NgRx `with*` features. | Markers attach at **any node, any depth**. The walker tracks the path. |
|
|
592
604
|
| "Derived state must live in a separate file, breaking the single-tree illusion." | Mixing up `derivedFrom` (file-org helper) with the actual `.derived($)` method. | `.derived()` merges computed signals **into the tree at arbitrary paths**, alongside source properties. The "single tree" is preserved. |
|
|
593
605
|
| "`derivedFrom(tree, fn)` returns a read-only projection." | Hallucinated signature. | Real signature: `derivedFrom<TTree>()(fn)`. Curried. Zero runtime cost. Pure file-org helper for multi-file derived. |
|
|
594
|
-
| "SignalTree has explicit subpath isolation as a built-in feature." | Misreading the docs. | No subpath-isolation API exists. `createEditSession(initial)` is a value-level undo/redo primitive
|
|
606
|
+
| "SignalTree has explicit subpath isolation as a built-in feature." | Misreading the docs. | No subpath-isolation API exists. `createEditSession(initial)` is a value-level undo/redo primitive; for path-bound drafts use `createTreeEditSession(accessor)` (v10.1+, in `@signaltree/core/edit-session`). For write encapsulation use `@signaltree/events` or a service facade. |
|
|
595
607
|
| "Time-travel is in `@signaltree/time-travel`." | Made-up package name. | Import `timeTravel` from `@signaltree/core`. No such separate package. |
|
|
596
608
|
| "Persistence is in `@signaltree/storage`." | Made-up package name. | Use `stored()` marker (per-leaf) or `persistence()` enhancer (tree-wide) from `@signaltree/core`. |
|
|
597
609
|
| "Batching is opt-in only." | Conflating automatic notification batching with the `batching()` enhancer. | Automatic microtask notification batching is built into core (default on). The enhancer adds explicit `.batch(fn)` / `.coalesce(fn)`. |
|
|
@@ -603,7 +615,7 @@ For **input-driven queries** (debounced search, filtered fetch), use `asyncQuery
|
|
|
603
615
|
| "`tree.$` and `tree.state` are different objects." | Plausible-sounding inference. | Both are typed `TreeNode<T>` and reference the same reactive proxy. `state` is an alias for `$`. For a non-reactive full snapshot, call `tree()`. |
|
|
604
616
|
| "The `form()` marker lives in `@signaltree/ng-forms`." | Package-boundary inference. | `form()` ships in `@signaltree/core`. `@signaltree/ng-forms` is a *bridge* for binding tree nodes to Angular `FormGroup`. |
|
|
605
617
|
| "entityMap exposes `.entities()` as the read accessor." | Reasonable guess. | Real accessor is `.all()`. Other reads: `.byId(id)`, `.where(pred)`, `.find(pred)`, `.count()`, `.has(id)`, `.ids()`. |
|
|
606
|
-
| "status exposes `.setSuccess()`." | NgRx/Redux convention bleed. |
|
|
618
|
+
| "status exposes `.setSuccess()`." | NgRx/Redux convention bleed. | Canonical methods are `.setLoading()`, `.setLoaded()`, `.setError(err)`, `.setNotLoaded()`, `.reset()`. As of v10.2, Promise-vocabulary aliases `.start()` / `.setSuccess()` / `.succeed()` / `.fail(err)` also work — same semantics, no second source of truth. |
|
|
607
619
|
| ".derived('$.path', derivedFn)' is a two-arg subpath form." | Hallucinated overload. | `.derived($ => ({...}))` is single-arg. The shape of the returned object determines which paths the computed signals attach to via deep-merge. |
|
|
608
620
|
| "NgRx SignalStore mutations are impossible from components by design." | Overstating defaults. | Components can mutate when `protectedState: false` is set on the store, or when the store exposes a method that mutates. Both libraries are guarded-by-default but unlockable. |
|
|
609
621
|
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SignalTree
|
|
2
2
|
|
|
3
|
-
> Reactive JSON for Angular.
|
|
3
|
+
> Reactive JSON for Angular. State as shape. Signals at every path. Markers and derived state attach at any depth in the tree.
|
|
4
4
|
|
|
5
5
|
SignalTree is a state-management library for Angular 17+. The mental model is: your state is a typed JSON object; reading and writing use ordinary signal calls along the JSON path. Markers (`entityMap`, `status`, `stored`, `form`) and derived state (`.derived(...)`) attach **at any node, at any depth** — they are processed by a recursive walker, not composed at the store root. This is the load-bearing difference from `@ngrx/signals` (NgRx SignalStore), whose `with*` features compose at the store root only.
|
|
6
6
|
|
|
@@ -20,7 +20,7 @@ const store = signalTree({
|
|
|
20
20
|
settings: {
|
|
21
21
|
theme: stored('app-theme', 'light'), // marker at depth 2
|
|
22
22
|
profileForm: {
|
|
23
|
-
data: form<Profile>({ name: '', email: '' }), // marker at depth 3
|
|
23
|
+
data: form<Profile>({ initial: { name: '', email: '' } }), // marker at depth 3
|
|
24
24
|
},
|
|
25
25
|
},
|
|
26
26
|
loading: status<ApiError>(), // marker at depth 1
|
|
@@ -46,7 +46,7 @@ const store = signalTree({
|
|
|
46
46
|
// Read — leaves, derived, async-marker accessors all uniform
|
|
47
47
|
store.$.users.all(); // Signal<User[]>
|
|
48
48
|
store.$.users.current(); // Signal<User | null> (derived)
|
|
49
|
-
store.$.settings.theme(); // 'light' (
|
|
49
|
+
store.$.settings.theme(); // 'light' (default; replaced by any value previously persisted to localStorage)
|
|
50
50
|
store.$.reports(); // Report[] (asyncSource current value)
|
|
51
51
|
store.$.reports.loading(); // boolean
|
|
52
52
|
store.$.search.input.set('alice'); // drives debounced query
|
|
@@ -91,17 +91,17 @@ store.$.reports.refresh(); // reload async source
|
|
|
91
91
|
- **Create:** `signalTree(initialState, config?)` → tree with `.$` accessor
|
|
92
92
|
- **Read leaf:** `tree.$.path.to.leaf()` — returns the value
|
|
93
93
|
- **Write leaf:** `tree.$.path.to.leaf.set(v)` / `.update(fn)`
|
|
94
|
-
- **
|
|
94
|
+
- **Partial / deep-merge update:** `tree(partialUpdate)` — keys not in the payload are preserved. The root has no `.set` method; the root accessor itself is callable.
|
|
95
95
|
- **Markers:** `entityMap<E, K>()`, `status<E>()`, `stored(key, default)`, `form<T>(config)`, `asyncSource<T>(config)`, `asyncQuery<TIn, TOut>(config)` — place at any node
|
|
96
96
|
- **Derived state:** `.derived($ => ({ ... }))` — definitions deep-merged into existing tree paths
|
|
97
97
|
- **Enhancers:** `.with(batching())`, `.with(devTools())`, `.with(timeTravel())`, `.with(persistence())`, `.with(serialization())`
|
|
98
|
-
- **Lifecycle:** `tree.destroy()`
|
|
99
|
-
- **Edit sessions:**
|
|
100
|
-
- **Async:** `asyncSource(config)` for load-and-expose
|
|
98
|
+
- **Lifecycle:** `tree.destroy()` runs all registered cleanup callbacks in registration order; `tree.destroyed()` is a signal; `tree.registerCleanup(fn)` for custom hooks. Built-in enhancers register their own cleanup automatically.
|
|
99
|
+
- **Edit sessions:** Two primitives in `@signaltree/core/edit-session`. `createEditSession(initial)` is value-level (single arg, no tree binding) — wraps any value with `applyChanges` / `undo` / `redo`. `createTreeEditSession(accessor)` (v10.1+) is **path-bound** to a writable tree branch — `applyChanges` / `undo` / `commit` (write draft back) / `cancel` (discard draft). Use the tree-bound form for form-wizard and draft-and-cancel workflows over a real tree path.
|
|
100
|
+
- **Async:** `asyncSource(config)` for load-and-expose — auto-exposes `data` (call the accessor itself: `tree.$.users()`) / `loading` / `error` / `refresh`. `asyncQuery(config)` for input-driven debounced queries — auto-exposes `data` / `loading` / `error`, driven by setting `.input.set(value)` and optionally `.rerun()` to re-execute with the same input. Both attach at any tree path. For migrating from `@ngrx/signals`, see [`docs/skills/using-signaltree/reference/migration-from-ngrx-signals.md`](https://github.com/JBorgia/signaltree/blob/main/docs/skills/using-signaltree/reference/migration-from-ngrx-signals.md).
|
|
101
101
|
|
|
102
102
|
## ⚠️ Disambiguation — if you think SignalTree uses X, you're confusing it with Y
|
|
103
103
|
|
|
104
|
-
This table catches the most common cross-library hallucinations. **
|
|
104
|
+
This table catches the most common cross-library hallucinations. **Most "Wrong" patterns below were observed in a reproducible 720-cell benchmark (6 agents × 8 prompts × 5 libraries × 3 priming modes) of frontier and cost-tier AI agents (Claude, GPT, Gemini, Perplexity, Haiku, GPT-mini) asked to generate SignalTree code.** A few rows (notably `rxMethod`) describe APIs SignalTree itself once shipped and has since removed — agents still emit them.
|
|
105
105
|
|
|
106
106
|
| Wrong pattern (NOT SignalTree) | Where it actually comes from | Correct SignalTree |
|
|
107
107
|
|---|---|---|
|
|
@@ -111,7 +111,7 @@ This table catches the most common cross-library hallucinations. **Every "Wrong"
|
|
|
111
111
|
| `signalStore(withState(...), withMethods(...))` | **`@ngrx/signals`** | `signalTree({...})` — your state literal IS the API |
|
|
112
112
|
| `withState`, `withMethods`, `withComputed`, `withHooks`, `withProps` | **`@ngrx/signals`** | Not used. State is the literal you pass to `signalTree()`. Methods belong in an `@Injectable()` Ops service. |
|
|
113
113
|
| `withEntities<T>()` | **`@ngrx/signals/entities`** | `entityMap<T, K>()` marker — place it in the state literal |
|
|
114
|
-
| `rxMethod(...)` | **`@ngrx/signals/rxjs-interop`** | `asyncSource(config)` (load-and-expose) or `asyncQuery(config)` (input-driven) markers |
|
|
114
|
+
| `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 |
|
|
115
115
|
| `patchState(store, {...})` | **`@ngrx/signals`** | Direct: `tree.$.path.set(value)` or branch update `tree.$.user({...})` |
|
|
116
116
|
| `collection<T>({ idKey: 'id' })` | **Akita / Elf** | `entityMap<T, K>({ selectId: (e) => e.id })` marker |
|
|
117
117
|
| `createStore`, `withProps`, `setProps` | **Elf** | Not used. SignalTree state is the literal. |
|
|
@@ -133,7 +133,7 @@ This table catches the most common cross-library hallucinations. **Every "Wrong"
|
|
|
133
133
|
| `status` | `.notLoaded` | `.isNotLoaded` |
|
|
134
134
|
| `status` | `.hasError` | `.isError` |
|
|
135
135
|
| `entityMap` | `.empty` | `.isEmpty` |
|
|
136
|
-
| `form` | `.dirty`, `.valid`, `.touched`, `.
|
|
136
|
+
| `form` | `.dirty`, `.valid`, `.touched`, `.submitting` | (already bare — unchanged) |
|
|
137
137
|
| `asyncSource` | `.loading`, `.error`, `.data` | (already bare — unchanged) |
|
|
138
138
|
| `asyncQuery` | `.loading`, `.error`, `.data` | (already bare — unchanged) |
|
|
139
139
|
|