@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.
Files changed (3) hide show
  1. package/llms-full.txt +37 -25
  2. package/llms.txt +10 -10
  3. 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, // default — microtask-level notification batching
42
- equalityFn: Object.is, // custom signal equality
43
- treeName: 'AppStore', // for devtools labeling
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 }); // replace a whole branch
63
- store({ user: { name: 'Dave', age: 40 }, settings: { theme: 'dark' } }); // replace full state
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(); // tears down all enhancer resources in reverse order
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); // Signal<User | undefined>
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(); // Signal<ApiError | null>
134
- store.$.users.loading.isLoading(); // Signal<boolean>
135
- store.$.users.loading.isLoaded();
136
- store.$.users.loading.isError();
137
- store.$.users.loading.isNotLoaded();
138
-
139
- // Mutate
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)` (from `@signaltree/ng-forms` integration)
178
+ ### `form<T>(config: FormConfig<T>)`
170
179
 
171
- Tree-integrated form with validation, wizard, and persistence. Materializes into a form signal exposing field state, validation status, and submit/reset helpers. See `@signaltree/ng-forms` documentation for full config schema.
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.push({ id: 1 });
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`, `.pristine` | (already bare — unchanged) |
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 (NOT tree-bound that's planned for v10.1). For write encapsulation use `@signaltree/events` or a service facade. |
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. | Real methods: `.setLoading()`, `.setLoaded()`, `.setError(err)`, `.setNotLoaded()`, `.reset()`. There is no `.setSuccess()`. |
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. Turn a plain object into a tree of signals — no actions, no reducers, no selectors. Markers and derived state attach at any depth in the tree.
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' (auto-loaded from localStorage)
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
- - **Replace full state:** `tree(newState)`
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()` tears down all resources in reverse enhancer order; `tree.destroyed()` is a signal; `tree.registerCleanup(fn)` for custom hooks
99
- - **Edit sessions:** `createEditSession(initial)` from `@signaltree/core/edit-session` — wraps any value with `applyChanges` / `undo` / `redo` / `reset` / `setOriginal` for form-wizard and draft-and-cancel workflows. Independent of the tree; bridge by syncing `session.modified()` ↔ tree leaves when appropriate.
100
- - **Async:** `asyncSource(config)` for load-and-expose, `asyncQuery(config)` for input-driven debounced queries. Both attach at any tree path and auto-expose `data`/`loading`/`error`/`refresh`. 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).
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. **Every "Wrong" pattern below was observed in a recent reproducible benchmark of frontier AI models (Claude, GPT-5.4, Gemini, Perplexity) asked to generate SignalTree code.** None of these patterns are or ever have been part of SignalTree.
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`, `.pristine` | (already bare — unchanged) |
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signaltree/core",
3
- "version": "10.3.1",
3
+ "version": "10.3.3",
4
4
  "description": "Reactive JSON for Angular. State as shape. Signals at every path.",
5
5
  "license": "MIT",
6
6
  "type": "module",