@signaltree/core 10.3.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.
Files changed (2) hide show
  1. package/README.md +83 -83
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -4,15 +4,13 @@
4
4
 
5
5
  # SignalTree: Reactive JSON
6
6
 
7
- **JSON branches, reactive leaves.**
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 frontier models × 8 prompts × 5 libraries × 720 cells.
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`, `withProps` | **`@ngrx/signals`** | Not used. State is the literal you pass to `signalTree()`. Methods belong in an `@Injectable()` Ops service. |
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 |
@@ -48,10 +46,10 @@ Every "Wrong pattern" below was actually generated by Claude / GPT-5.4 / Gemini
48
46
  |---|---|---|
49
47
  | `status` | `.loading`, `.loaded`, `.notLoaded`, `.hasError` | `.isLoading`, `.isLoaded`, `.isNotLoaded`, `.isError` |
50
48
  | `entityMap` | `.empty` | `.isEmpty` |
51
- | `form` | `.dirty`, `.valid`, `.touched`, `.pristine` | (already bare — unchanged) |
49
+ | `form` | `.dirty`, `.valid`, `.touched`, `.submitting` | (already bare — unchanged) |
52
50
  | `asyncSource` / `asyncQuery` | `.loading`, `.error`, `.data` | (already bare — unchanged) |
53
51
 
54
- All predicates are **callable `Signal<boolean>`** — invoke them: `tree.$.load.loading()`, `tree.$.users.empty()`.
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.
55
53
 
56
54
  ### Status marker — method names (frequently confused)
57
55
 
@@ -59,12 +57,12 @@ The `status()` marker's canonical methods are **`setLoading` / `setLoaded` / `se
59
57
 
60
58
  | Wrong-but-now-aliased (v10.2+) | Canonical | Equivalent? |
61
59
  |---|---|---|
62
- | `.setSuccess()` | `.setLoaded()` | Yes — alias |
60
+ | `.setSuccess()` (no args) | `.setLoaded()` | Yes — alias |
63
61
  | `.start()` | `.setLoading()` | Yes — alias |
64
62
  | `.succeed()` | `.setLoaded()` | Yes — alias |
65
63
  | `.fail(err)` | `.setError(err)` | Yes — alias |
66
- | `.loading` (bare property — read-only) | call as signal: `.loading()` | Yes — callable Signal |
67
- | `.error` (bare property — read-only) | call as signal: `.error()` | Yesreturns error value |
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.
68
66
 
69
67
  ### Canonical async pattern — use `asyncSource`, NOT `status` + manual try/catch
70
68
 
@@ -84,7 +82,7 @@ export class UsersService {
84
82
  }),
85
83
  });
86
84
 
87
- readonly users = this.tree.$.users; // .users() → data, .users.loading(), .users.error()
85
+ readonly users = this.tree.$.users; // .users() → User[] | undefined, .users.loading(), .users.error()
88
86
  load = () => this.tree.$.users.refresh();
89
87
  }
90
88
  ```
@@ -103,7 +101,9 @@ export class AppService {
103
101
  private readonly tree = signalTree({
104
102
  users: entityMap<User, number>(),
105
103
  saveStatus: status(),
106
- profile: form({ firstName: '', lastName: '' }),
104
+ profile: form<{ firstName: string; lastName: string }>({
105
+ initial: { firstName: '', lastName: '' },
106
+ }),
107
107
  feed: asyncSource<Post[]>({ initial: [], load: () => api.feed$() }),
108
108
  });
109
109
 
@@ -152,17 +152,15 @@ You don't model state as actions, reducers, selectors, or classes — you model
152
152
 
153
153
  ## Import guidance (tree-shaking)
154
154
 
155
- Modern bundlers (webpack 5+, esbuild, Rollup, Vite) **automatically tree-shake barrel imports** from `@signaltree/core`. Both import styles produce identical bundle sizes:
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.
156
156
 
157
157
  ```ts
158
- // Recommended: Simple and clean
158
+ // Import only what you use — unused symbols are tree-shaken away
159
159
  import { signalTree, batching } from '@signaltree/core';
160
-
161
- // ✅ Also fine: Explicit subpath (same bundle size)
162
- import { signalTree } from '@signaltree/core';
163
- import { batching } from '@signaltree/core/enhancers/batching';
164
160
  ```
165
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
+
166
164
  **Measured impact** (with modern bundlers):
167
165
 
168
166
  - Core only: ~8.5 KB gzipped
@@ -197,35 +195,36 @@ const tree = signalTree({ count: 0 });
197
195
 
198
196
  This repo's ESLint rule is **disabled by default** since testing confirms effective tree-shaking with barrel imports.
199
197
 
200
- ### Callable leaf signals (DX sugar only)
198
+ ### Callable shape branches natively, leaves with the build transform
201
199
 
202
- SignalTree provides TypeScript support for callable syntax on leaf signals as developer experience sugar:
200
+ **Branches are natively callable** for reads AND writes at runtime no transform required:
203
201
 
204
202
  ```typescript
205
- // TypeScript accepts this syntax (with proper tooling):
206
- tree.$.name('Jane'); // Set value
207
- tree.$.count((n) => n + 1); // Update with function
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:
208
208
 
209
- // At build time, transforms convert to:
210
- tree.$.name.set('Jane'); // Direct Angular signal API
211
- tree.$.count.update((n) => n + 1); // Direct Angular signal API
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)
212
213
 
213
- // Reading always works directly:
214
- const name = tree.$.name(); // No transform needed
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);
215
218
  ```
216
219
 
217
220
  **Key Points:**
218
221
 
219
- - **Zero runtime overhead**: No Proxy wrappers or runtime hooks
220
- - **Build-time only**: AST transform converts callable syntax to direct `.set/.update` calls
221
- - **Optional**: Use `@signaltree/callable-syntax` transform or stick with direct `.set/.update`
222
- - **Type-safe**: Full TypeScript support via module augmentation
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
223
226
 
224
- **Function-valued leaves:**
225
- When a leaf stores a function as its value, use direct `.set(fn)` to assign. Callable `sig(fn)` is treated as an updater.
226
-
227
- **Setup:**
228
- 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.
229
228
 
230
229
  ### Measuring performance and size
231
230
 
@@ -282,7 +281,7 @@ export function createUserTree() {
282
281
  // ✅ Correct: Derived from multiple signals
283
282
  const selectedUser = computed(() => {
284
283
  const id = $.selected.userId();
285
- return id ? $.users.byId(id)() : null;
284
+ return id ? $.users.byId(id)?.() ?? null : null;
286
285
  });
287
286
 
288
287
  // ❌ Wrong: Wrapping an existing signal
@@ -293,12 +292,12 @@ const selectedUserId = computed(() => $.selected.userId()); // Unnecessary!
293
292
 
294
293
  ```typescript
295
294
  // ✅ SignalTree-native
296
- const user = $.users.byId(123)(); // O(1) lookup
295
+ const user = $.users.byId(123)?.(); // EntityNode → User | undefined
297
296
  const allUsers = $.users.all; // Get all
298
297
  $.users.setAll(usersFromApi); // Replace all
299
298
 
300
- // NgRx-style (avoid)
301
- const user = entityMap()[123]; // Requires intermediate object
299
+ // (NgRx Signal Store equivalent — for context, not SignalTree syntax)
300
+ // const user = usersStore.entityMap()[123];
302
301
  ```
303
302
 
304
303
  ### Notification Batching
@@ -621,9 +620,9 @@ const tree = signalTree<AppState>({
621
620
  },
622
621
  });
623
622
 
624
- // Complex updates with type safety
625
- // Requires @signaltree/callable-syntax. Without the transform, use
626
- // tree.set((state) => ({ ... })) or leaf .set() / .update() calls instead.
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().
627
626
  tree((state) => ({
628
627
  auth: {
629
628
  ...state.auth,
@@ -722,7 +721,7 @@ const activeUsers = computed(() => tree.$.users().filter((user) => user.active))
722
721
 
723
722
  ### 4) Manual async state management
724
723
 
725
- Core provides basic state updates. For advanced async helpers, use the built-in async helpers (`createAsyncOperation`, `trackAsync`):
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):
726
725
 
727
726
  ```typescript
728
727
  const tree = signalTree({
@@ -783,8 +782,8 @@ All enhancers are exported directly from `@signaltree/core`:
783
782
 
784
783
  **Data Management:**
785
784
 
786
- - `createAsyncOperation()` - Async operation management with loading/error states
787
- - `trackAsync()` - Track async operations in your state
785
+ - `asyncSource(config)` marker - Load-and-expose async state (canonical, v9.5+)
786
+ - `asyncQuery(config)` marker - Input-driven debounced query state (canonical, v9.5+)
788
787
  - `serialization()` - State persistence and SSR support
789
788
  - `persistence()` - Auto-save to localStorage/IndexedDB
790
789
 
@@ -851,7 +850,10 @@ tree.$.products.addOne(newProduct);
851
850
  tree.$.products.setAll(productsFromApi);
852
851
 
853
852
  // Entity queries
854
- const electronics = tree.$.products.all.filter((p) => p.category === 'electronics');
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');
855
857
  ```
856
858
 
857
859
  **Full-Stack Application:**
@@ -910,7 +912,7 @@ const tree = signalTree(state)
910
912
  SignalTree Core includes all enhancer functionality built-in. No separate packages needed:
911
913
 
912
914
  ```typescript
913
- import { signalTree, entityMap, entities } from '@signaltree/core';
915
+ import { signalTree, entityMap } from '@signaltree/core';
914
916
 
915
917
  // Without entityMap - use manual array updates
916
918
  const basic = signalTree({ users: [] as User[] });
@@ -922,7 +924,7 @@ const enhanced = signalTree({
922
924
  });
923
925
 
924
926
  enhanced.$.users.addOne(newUser); // ✅ Advanced CRUD operations
925
- enhanced.$.users.byId(123)(); // ✅ O(1) lookups
927
+ enhanced.$.users.byId(123)?.(); // ✅ O(1) lookups (undefined if missing)
926
928
  enhanced.$.users.all; // ✅ Get all as array
927
929
  ```
928
930
 
@@ -1261,17 +1263,26 @@ const tree = signalTree({
1261
1263
  tree.$.users.loadStatus.state(); // Signal<LoadingState>
1262
1264
  tree.$.users.loadStatus.error(); // Signal<ApiError | null>
1263
1265
 
1264
- // Convenience signals
1265
- tree.$.users.loadStatus.isNotLoaded(); // Signal<boolean>
1266
- tree.$.users.loadStatus.isLoading(); // Signal<boolean>
1267
- tree.$.users.loadStatus.isLoaded(); // Signal<boolean>
1268
- tree.$.users.loadStatus.isError(); // Signal<boolean>
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()
1269
1273
 
1270
- // Update methods
1274
+ // Update methods (canonical)
1271
1275
  tree.$.users.loadStatus.setLoading();
1272
1276
  tree.$.users.loadStatus.setLoaded();
1273
1277
  tree.$.users.loadStatus.setError({ code: 404, message: 'Not found' });
1274
- tree.$.users.loadStatus.reset();
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)
1275
1286
 
1276
1287
  // LoadingState enum
1277
1288
  LoadingState.NotLoaded; // 'not-loaded'
@@ -1546,23 +1557,12 @@ const tree = signalTree({
1546
1557
  async function handleSubmit() {
1547
1558
  const contactForm = tree.$.contact;
1548
1559
 
1549
- // Validate all fields first
1550
- contactForm.touchAll();
1551
- const isValid = await contactForm.validate();
1552
-
1553
- if (!isValid) return;
1554
-
1555
- // Set submitting state
1556
- contactForm.setSubmitting(true);
1557
-
1558
- try {
1559
- 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);
1560
1564
  contactForm.reset();
1561
- } catch (error) {
1562
- // Handle error
1563
- } finally {
1564
- contactForm.setSubmitting(false);
1565
- }
1565
+ });
1566
1566
  }
1567
1567
  ```
1568
1568
 
@@ -1715,7 +1715,7 @@ const filteredProducts = computed(() => {
1715
1715
  ### Data Management Composition
1716
1716
 
1717
1717
  ```typescript
1718
- import { signalTree, entityMap, entities } from '@signaltree/core';
1718
+ import { signalTree, entityMap } from '@signaltree/core';
1719
1719
 
1720
1720
  // entityMap() markers self-register — entity operations available immediately
1721
1721
  const tree = signalTree({
@@ -1727,7 +1727,7 @@ const tree = signalTree({
1727
1727
  // Advanced entity operations via tree.$ accessor
1728
1728
  tree.$.users.addOne(newUser);
1729
1729
  tree.$.users.where((u) => u.active);
1730
- tree.$.users.updateMany([{ id: '1', changes: { status: 'active' } }]);
1730
+ tree.$.users.updateMany(['1', '2', '3'], { status: 'active' }); // ids + shared changes
1731
1731
 
1732
1732
  // Entity helpers work with nested structures
1733
1733
  // Example: deeply nested entities in a domain-driven design pattern
@@ -1803,7 +1803,7 @@ const tree = signalTree({
1803
1803
  async function fetchUser(id: string) {
1804
1804
  return await api.getUser(id);
1805
1805
  }
1806
- tree.$.app.data.users.byId(userId)(); // O(1) lookup
1806
+ tree.$.app.data.users.byId(userId)?.(); // O(1) lookup (undefined if missing)
1807
1807
  tree.undo(); // Time travel
1808
1808
  tree.save(); // Persistence
1809
1809
  ```
@@ -1867,7 +1867,7 @@ In Redux DevTools you will see a single instance named `"MyApp SignalTree"` with
1867
1867
  ### Production-Ready Composition
1868
1868
 
1869
1869
  ```typescript
1870
- import { signalTree, batching, entities, serialization } from '@signaltree/core';
1870
+ import { signalTree, batching, serialization } from '@signaltree/core';
1871
1871
 
1872
1872
  // Production build (no dev tools)
1873
1873
  const tree = signalTree(initialState)
@@ -1930,7 +1930,7 @@ const tree = signalTree(state);
1930
1930
  const tree2 = tree.with(batching());
1931
1931
 
1932
1932
  // Phase 3: Add async for API integration
1933
- // withAsync removed — no explicit async enhancer; use async helpers instead
1933
+ // withAsync removed in v9 use the asyncSource() / asyncQuery() markers (v10.x canonical) for load-and-expose and input-driven flows.
1934
1934
 
1935
1935
  // Each phase is fully functional and production-ready
1936
1936
  ```
@@ -2218,7 +2218,7 @@ tree.destroy(); // Cleanup resources
2218
2218
 
2219
2219
  // Entity helpers (entityMap() self-registers — no enhancer needed)
2220
2220
  tree.$.users.addOne(user); // Add single entity
2221
- tree.$.users.byId(id)(); // O(1) lookup by ID (read whole entity)
2221
+ tree.$.users.byId(id)?.(); // O(1) cursor unwrap (undefined if missing)
2222
2222
  tree.$.users.byId(id)?.name(); // Read single field (computed signal)
2223
2223
  tree.$.users.byId(id)?.name.set('Bob'); // Write single field (throws if entity removed)
2224
2224
  tree.$.users.byId(id)?.name.update(fn); // Updater on single field
@@ -2280,7 +2280,7 @@ Consider enhancers when you need:
2280
2280
 
2281
2281
  - ⚡ Performance optimization (batching)
2282
2282
  - 🐛 Advanced debugging (devTools, timeTravel)
2283
- - 📦 Entity management (entities)
2283
+ - 📦 Entity management (`entityMap` marker)
2284
2284
 
2285
2285
  Consider separate packages when you need:
2286
2286
 
@@ -2466,8 +2466,8 @@ npm install @signaltree/core
2466
2466
  # Everything is available from @signaltree/core:
2467
2467
  import {
2468
2468
  signalTree,
2469
+ entityMap,
2469
2470
  batching,
2470
- entities,
2471
2471
  devTools,
2472
2472
  timeTravel,
2473
2473
  serialization
@@ -2797,7 +2797,7 @@ export default {
2797
2797
  **Start with just `@signaltree/core`** - it includes comprehensive enhancers for most applications:
2798
2798
 
2799
2799
  - Performance optimization (batching)
2800
- - Data management (entities, async operations)
2800
+ - Data management (`entityMap` marker, `asyncSource` / `asyncQuery` markers)
2801
2801
  - Development tools (devtools, time-travel)
2802
2802
  - State persistence (serialization)
2803
2803
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@signaltree/core",
3
- "version": "10.3.0",
4
- "description": "Reactive JSON for Angular. JSON branches, reactive leaves. No actions. No reducers. No selectors.",
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,