@signaltree/enterprise 13.1.1 → 13.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/skills/using-signaltree/SKILL.md +153 -0
- package/skills/using-signaltree/enterprise/SKILL.md +95 -0
- package/skills/using-signaltree/reference/anti-patterns.md +266 -0
- package/skills/using-signaltree/reference/core.md +398 -0
- package/skills/using-signaltree/reference/install.md +110 -0
- package/skills/using-signaltree/reference/migration-from-ngrx-signals.md +971 -0
- package/skills/using-signaltree/reference/migration-from-ngrx-store.md +396 -0
- package/skills/using-signaltree/reference/optimal-implementation.md +211 -0
- package/skills/using-signaltree/reference/orchestrating-a-migration.md +364 -0
- package/skills/using-signaltree/reference/patterns.md +1250 -0
- package/skills/using-signaltree/reference/testing.md +282 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
# Core reference: `@signaltree/core`
|
|
2
|
+
|
|
3
|
+
Authoritative reference for the surface area an agent uses day-to-day. Signatures are simplified for readability — the source under `packages/core/src/` is the tiebreaker when generics or advanced overloads matter.
|
|
4
|
+
|
|
5
|
+
## `signalTree(initialState, configOrDerived?)`
|
|
6
|
+
|
|
7
|
+
Factory that turns a plain state object into a reactive tree.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { signalTree } from '@signaltree/core';
|
|
11
|
+
|
|
12
|
+
interface AppState {
|
|
13
|
+
counter: number;
|
|
14
|
+
user: { name: string; email: string };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const tree = signalTree<AppState>({
|
|
18
|
+
counter: 0,
|
|
19
|
+
user: { name: 'Ada', email: 'ada@example.com' },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
tree.$.counter(); // read → 0
|
|
23
|
+
tree.$.user.name.set('Grace');
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Two overloads:
|
|
27
|
+
|
|
28
|
+
- `signalTree<T>(initialState: T, config?: TreeConfig)` — standard path.
|
|
29
|
+
- `signalTree<T, D>(initialState: T, derivedFactory: ($: TreeNode<T>) => D)` — attach derived state declared alongside the tree. The returned tree has `tree.$` augmented with the derived keys.
|
|
30
|
+
|
|
31
|
+
Both overloads return a **builder** whose `.with(enhancer)` chain composes enhancers in application order.
|
|
32
|
+
|
|
33
|
+
## The `$` proxy
|
|
34
|
+
|
|
35
|
+
`tree.$` is a typed proxy mirroring the shape of your state.
|
|
36
|
+
|
|
37
|
+
- Leaf access (`tree.$.counter`) returns a `WritableSignal<T>`-compatible accessor. Call it to read (`tree.$.counter()`), call `.set(v)` to assign, `.update(fn)` to update.
|
|
38
|
+
- Branch access (`tree.$.user`) returns a group accessor. You can:
|
|
39
|
+
- Dot further into it: `tree.$.user.name`.
|
|
40
|
+
- Read the whole subtree as an object: `tree.$.user()`.
|
|
41
|
+
- Replace the subtree: `tree.$.user({ name, email })` (call the branch accessor).
|
|
42
|
+
- Patch the subtree with an updater: `tree.$.user((u) => ({ ...u, name }))`.
|
|
43
|
+
- `@signaltree/callable-syntax` adds the same callable shorthand for leaves (`tree.$.counter(5)` → `.set(5)`).
|
|
44
|
+
|
|
45
|
+
**What's a leaf vs a branch?** It's a property of the *value's type*, not its position in the tree:
|
|
46
|
+
|
|
47
|
+
- **Leaves** (have `.set()` / `.update()`): primitives (`string`, `number`, `boolean`, `null`, `undefined`, `bigint`, `symbol`), arrays, `Date`, `Map`, `Set`, `Error`, `RegExp`, `Promise`, functions, and class instances.
|
|
48
|
+
- **Branches** (call form, no `.set()`): plain objects (records, interfaces, type aliases for object literals).
|
|
49
|
+
|
|
50
|
+
A property typed `currentFilter: PlantFilter` where `PlantFilter` is `{ isArchived: boolean; sortBy: string }` is a **branch**, even though it lives next to primitive siblings. Writing `tree.$.currentFilter.set(merged)` will fail with `Property 'set' does not exist on type 'NodeAccessor<PlantFilter> & TreeNode<PlantFilter>'`. The correct form is the call form: `tree.$.currentFilter(merged)` or `tree.$.currentFilter((c) => ({ ...c, ...changes }))`.
|
|
51
|
+
|
|
52
|
+
The `$` proxy is fully typed — dotting through `tree.$` gives you accurate autocomplete all the way down.
|
|
53
|
+
|
|
54
|
+
## Reads and writes
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { signalTree } from '@signaltree/core';
|
|
58
|
+
|
|
59
|
+
const tree = signalTree({
|
|
60
|
+
counter: 0,
|
|
61
|
+
user: { name: 'Ada', email: 'ada@example.com' },
|
|
62
|
+
ui: { theme: 'light' as 'light' | 'dark', sidebarOpen: false },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// read a leaf
|
|
66
|
+
const n = tree.$.counter();
|
|
67
|
+
|
|
68
|
+
// read a whole subtree
|
|
69
|
+
const user = tree.$.user();
|
|
70
|
+
|
|
71
|
+
// write a leaf
|
|
72
|
+
tree.$.counter.set(1);
|
|
73
|
+
tree.$.counter.update((n) => n + 1);
|
|
74
|
+
|
|
75
|
+
// replace a subtree (call the branch accessor with a new object)
|
|
76
|
+
tree.$.user({ name: 'Grace', email: 'grace@example.com' });
|
|
77
|
+
|
|
78
|
+
// patch a subtree immutably (call the branch accessor with an updater fn)
|
|
79
|
+
tree.$.user((u) => ({ ...u, name: 'Grace' }));
|
|
80
|
+
|
|
81
|
+
// write multiple leaves at the root by calling the tree itself with an updater
|
|
82
|
+
tree((s) => ({ ...s, counter: 99, ui: { ...s.ui, theme: 'dark' } }));
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Reads inside `computed()` and `effect()` register reactive dependencies exactly like bare signals.
|
|
86
|
+
|
|
87
|
+
**Branch and root writes auto-batch.** When you call a branch accessor or the
|
|
88
|
+
root tree with an object or updater, every child signal write that results from
|
|
89
|
+
the merge is coalesced into a single change-detection notification. This
|
|
90
|
+
replaces the `patchState(store, { a, b, c })` idiom from `@ngrx/signals` —
|
|
91
|
+
there's no need to add the `batching()` enhancer just to group a multi-field
|
|
92
|
+
update, because branch/root writes are already batched by construction. Reach
|
|
93
|
+
for `batching()` only when you want to coalesce multiple _separate_ writes
|
|
94
|
+
(different call sites) within a tick.
|
|
95
|
+
|
|
96
|
+
## Markers
|
|
97
|
+
|
|
98
|
+
Markers are typed placeholders you put in the initial state. `signalTree()` replaces them at that path with the real runtime API.
|
|
99
|
+
|
|
100
|
+
### `entityMap<E, K>(config?)`
|
|
101
|
+
|
|
102
|
+
Normalized collection with O(1) CRUD.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { signalTree, entityMap } from '@signaltree/core';
|
|
106
|
+
|
|
107
|
+
interface User {
|
|
108
|
+
id: number;
|
|
109
|
+
name: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const store = signalTree({
|
|
113
|
+
users: entityMap<User, number>(),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
store.$.users.addOne({ id: 1, name: 'Ada' });
|
|
117
|
+
store.$.users.upsertOne({ id: 1, name: 'Ada Lovelace' });
|
|
118
|
+
store.$.users.removeOne(1);
|
|
119
|
+
store.$.users.setAll([{ id: 2, name: 'Grace' }]);
|
|
120
|
+
|
|
121
|
+
const all = store.$.users.all(); // Signal<User[]> → User[]
|
|
122
|
+
const userNode = store.$.users.byId(2); // EntityNode<User> | undefined — callable accessor with per-field signals
|
|
123
|
+
const one = userNode?.(); // User | undefined
|
|
124
|
+
const nameSignal = userNode?.name; // Signal<string> | undefined (field-level)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Key options live on `EntityConfig<E, K>`: `selectId: (entity) => entity.someKey` for a custom id key (e.g., `selectId: (p) => p.sku`); `sortComparer: (a, b) => …` (v10.5+, `@ngrx/entity` parity) to keep `all()` / `ids()` in a stable sorted order (`map()` stays insertion-order); plus optional `beforeAdd` / `beforeUpdate` / `beforeRemove` lifecycle hooks.
|
|
128
|
+
|
|
129
|
+
**Full mutation surface:** `addOne`, `addMany`, `upsertOne`, `upsertMany`, `updateOne`, `updateMany(ids: K[], changes: Partial<E>)` (NOT NgRx-style `[{id, changes}]`), `updateWhere(pred, changes)`, `removeOne`, `removeMany`, `removeWhere`, `clear`, `setAll`.
|
|
130
|
+
**Full read surface (Signals — invoke with `()`):** `all`, `count`, `ids`, `map`, `has(id)`, `where(pred)`, `find(pred)`, `empty` (the `.isEmpty` alias was removed in v11).
|
|
131
|
+
**Node access:** `byId(id) → EntityNode<E> | undefined`. **Per-entity reads are body-granular** — `byId(id).field()` re-runs only when *that* entity changes (fan-out 1), not on every collection mutation.
|
|
132
|
+
**Computed slices:** `entityMap<User>().computed('active', all => all.filter(u => u.active))` materializes `store.$.users.active()` as a `Signal<User[]>` — **fully typed on `tree.$` since v13.2** (no cast; chain `.computed()` more than once and every name is typed independently). The `compute` fn receives only that collection's `E[]`, so a projection that needs other state (another collection, an external id signal) stays a normal `computed`/`.derived()`.
|
|
133
|
+
|
|
134
|
+
### `status()`
|
|
135
|
+
|
|
136
|
+
Async operation state (`NotLoaded` | `Loading` | `Loaded` | `Error`), surfaced as a signal-backed object.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { signalTree, status, LoadingState } from '@signaltree/core';
|
|
140
|
+
|
|
141
|
+
// status() defaults to Error type; pass a type parameter for a custom error shape.
|
|
142
|
+
const tree = signalTree({
|
|
143
|
+
load: status<string>(),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Setters
|
|
147
|
+
tree.$.load.setLoading();
|
|
148
|
+
tree.$.load.setLoaded();
|
|
149
|
+
tree.$.load.setError('network failure');
|
|
150
|
+
tree.$.load.setNotLoaded(); // resets to initial NotLoaded state, clears error
|
|
151
|
+
|
|
152
|
+
// Boolean reader signals — v10.3 canonical (bare names). Use these in templates and computed().
|
|
153
|
+
tree.$.load.loading(); // boolean
|
|
154
|
+
tree.$.load.loaded(); // boolean
|
|
155
|
+
tree.$.load.hasError(); // boolean
|
|
156
|
+
tree.$.load.notLoaded(); // boolean
|
|
157
|
+
// (The is-prefix aliases .isLoading/.isLoaded/.isError/.isNotLoaded were removed in v11.)
|
|
158
|
+
// — same Signal instance, both work; canonical preferred in new code.
|
|
159
|
+
|
|
160
|
+
// v10.2+ Promise-vocabulary aliases (identical semantics, no args for booleans)
|
|
161
|
+
tree.$.load.start(); // === setLoading()
|
|
162
|
+
tree.$.load.setSuccess(); // === setLoaded() — NO ARGS
|
|
163
|
+
tree.$.load.succeed(); // === setLoaded()
|
|
164
|
+
tree.$.load.fail('network failure'); // === setError('network failure')
|
|
165
|
+
|
|
166
|
+
// Raw state and error if you need them
|
|
167
|
+
tree.$.load.state(); // LoadingState (state is WritableSignal<LoadingState>)
|
|
168
|
+
tree.$.load.error(); // E | null (error is WritableSignal<E | null> — write via .error.set(e))
|
|
169
|
+
|
|
170
|
+
// When comparing raw state, always use the LoadingState enum — never string literals
|
|
171
|
+
const loading = tree.$.load.state() === LoadingState.Loading; // ✓
|
|
172
|
+
// tree.$.load.state() === 'loading' // ✗ TypeScript error
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### `stored(key, default)`
|
|
176
|
+
|
|
177
|
+
A single signal whose value is persisted to `localStorage` under `key`.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
import { signalTree, stored } from '@signaltree/core';
|
|
181
|
+
|
|
182
|
+
const tree = signalTree({
|
|
183
|
+
theme: stored<'light' | 'dark'>('app.theme', 'light'),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
tree.$.theme.set('dark'); // writes through to localStorage
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
For full-tree persistence instead of a single leaf, use the `persistence()` enhancer.
|
|
190
|
+
|
|
191
|
+
### `form(fields)`
|
|
192
|
+
|
|
193
|
+
Tree-integrated form state (values, errors, dirty/touched, validity). For Angular `FormGroup` interop, pair with `formBridge()` from `@signaltree/ng-forms`.
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
import { signalTree, form } from '@signaltree/core';
|
|
197
|
+
|
|
198
|
+
// form() takes a FormConfig — pass initial values under `initial`.
|
|
199
|
+
const tree = signalTree({
|
|
200
|
+
profile: form({
|
|
201
|
+
initial: { username: '', email: '' },
|
|
202
|
+
}),
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
See [`../ng-forms/SKILL.md`](../ng-forms/SKILL.md) for full form guidance.
|
|
207
|
+
|
|
208
|
+
### `asyncSource<T>(config)`
|
|
209
|
+
|
|
210
|
+
Load-and-expose async primitive. Materializes into an accessor with `data`, `loading`, `error` signals plus `refresh` / `set` / `update` / `reset` methods. Accepts `Observable<T>` or `Promise<T>` from the loader.
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
import { signalTree, asyncSource } from '@signaltree/core';
|
|
214
|
+
import type { Observable } from 'rxjs';
|
|
215
|
+
|
|
216
|
+
interface User { id: number; name: string; }
|
|
217
|
+
interface Report { id: string; rows: number; }
|
|
218
|
+
declare const api: {
|
|
219
|
+
list$(): Observable<User[]>;
|
|
220
|
+
generateReport$(): Observable<Report>;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const store = signalTree({
|
|
224
|
+
users: asyncSource<User[]>({
|
|
225
|
+
initial: [],
|
|
226
|
+
load: () => api.list$(), // auto-loads on materialization
|
|
227
|
+
}),
|
|
228
|
+
report: asyncSource<Report>({
|
|
229
|
+
load: () => api.generateReport$(),
|
|
230
|
+
lazy: true, // skip auto-load; caller invokes .refresh()
|
|
231
|
+
}),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
store.$.users(); // current value (User[] | undefined)
|
|
235
|
+
store.$.users.loading(); // Signal<boolean>
|
|
236
|
+
store.$.users.error(); // Signal<unknown | null>
|
|
237
|
+
store.$.users.refresh(); // reload (cancels in-flight)
|
|
238
|
+
store.$.users.set([{ id: 1, name: 'Override' }]); // manual override
|
|
239
|
+
store.$.users.reset(); // clear data/loading/error
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Auto-cleans on the surrounding `DestroyRef`. **No manual `setLoading()` / `setLoaded()` wiring.**
|
|
243
|
+
|
|
244
|
+
### `asyncQuery<TInput, TResult>(config)`
|
|
245
|
+
|
|
246
|
+
Input-driven debounced query. A writable `input` signal drives a built-in `debounce → filter → distinctUntilChanged → switchMap(query)` pipeline. Race conditions, cancellation, dedup all handled.
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import { signalTree, asyncQuery } from '@signaltree/core';
|
|
250
|
+
import type { Observable } from 'rxjs';
|
|
251
|
+
|
|
252
|
+
interface User { id: number; name: string; }
|
|
253
|
+
declare const api: { search$(q: string): Observable<User[]>; };
|
|
254
|
+
|
|
255
|
+
const store = signalTree({
|
|
256
|
+
search: asyncQuery<string, User[]>({
|
|
257
|
+
initialResult: [],
|
|
258
|
+
debounce: 300,
|
|
259
|
+
filter: (q) => q.length > 0,
|
|
260
|
+
query: (q) => api.search$(q),
|
|
261
|
+
}),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Either two-way bind in a template:
|
|
265
|
+
// <input [(ngModel)]="store.$.search.input">
|
|
266
|
+
// Or set imperatively:
|
|
267
|
+
store.$.search.input.set('alice');
|
|
268
|
+
|
|
269
|
+
store.$.search(); // current results
|
|
270
|
+
store.$.search.loading();
|
|
271
|
+
store.$.search.error();
|
|
272
|
+
store.$.search.rerun(); // rerun current input, skip dedup
|
|
273
|
+
store.$.search.reset();
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### Custom markers
|
|
277
|
+
|
|
278
|
+
`registerMarkerProcessor(processor)` extends the marker system. **Call it before any `signalTree()` that uses the custom marker.**
|
|
279
|
+
|
|
280
|
+
## Enhancer composition
|
|
281
|
+
|
|
282
|
+
`.with(enhancer)` chains enhancers in application order.
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import { signalTree, batching, timeTravel, devTools, persistence } from '@signaltree/core';
|
|
286
|
+
|
|
287
|
+
const tree = signalTree({ count: 0 })
|
|
288
|
+
.with(batching())
|
|
289
|
+
.with(
|
|
290
|
+
timeTravel({
|
|
291
|
+
// Customize the labels that show up in the DevTools action list.
|
|
292
|
+
actionNames: { update: 'x/update', set: 'x/set' },
|
|
293
|
+
})
|
|
294
|
+
)
|
|
295
|
+
.with(devTools({ treeName: 'Counter' }))
|
|
296
|
+
.with(
|
|
297
|
+
persistence({
|
|
298
|
+
key: 'counter.v1',
|
|
299
|
+
autoSave: true,
|
|
300
|
+
autoLoad: true,
|
|
301
|
+
debounceMs: 500,
|
|
302
|
+
})
|
|
303
|
+
);
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
SignalTree does **not** support per-call action names — there is no
|
|
307
|
+
`store.$.count.set(5, 'incrementClicked')` signature. The `actionNames` block
|
|
308
|
+
above (on `TimeTravelConfig`) rewrites the category labels (`update`, `set`,
|
|
309
|
+
`batch`) globally. If you need finer-grained DevTools labels, use an Angular
|
|
310
|
+
`effect()` to emit a tagged action as state mirrors the interesting change, or
|
|
311
|
+
split hot domains into separately-labeled trees with their own
|
|
312
|
+
`devTools({ treeName })`.
|
|
313
|
+
|
|
314
|
+
Built-in enhancers exported from `@signaltree/core`:
|
|
315
|
+
|
|
316
|
+
- `batching(config?: BatchingConfig)` — coalesce change-detection notifications.
|
|
317
|
+
- `timeTravel(config?)` — undo / redo history.
|
|
318
|
+
- `devTools(config?)` — Redux DevTools integration.
|
|
319
|
+
- `serialization(config?)` / `persistence(config?)` — snapshot and auto-persist.
|
|
320
|
+
|
|
321
|
+
> The `memoization` enhancer was removed in 9.0.1. Use Angular's built-in `computed()` for memoization — it provides equivalent caching at zero runtime cost.
|
|
322
|
+
|
|
323
|
+
Utilities you may need when composing enhancers yourself:
|
|
324
|
+
|
|
325
|
+
- `composeEnhancers(...enhancers)` — combine multiple enhancers into one.
|
|
326
|
+
- `createEnhancer(metadata, fn)` — author a third-party enhancer with metadata.
|
|
327
|
+
- `resolveEnhancerOrder(list)` — dependency-aware ordering.
|
|
328
|
+
- `ENHANCER_META` — symbol used by third parties to attach metadata.
|
|
329
|
+
|
|
330
|
+
## Derived state helpers
|
|
331
|
+
|
|
332
|
+
One helper for declaring derived functions in separate files without losing type inference:
|
|
333
|
+
|
|
334
|
+
- `derivedFrom<TTree>()` — returns a function that accepts `($) => derived`. Explicitly types `TTree` while inferring the return.
|
|
335
|
+
|
|
336
|
+
```ts
|
|
337
|
+
import { derivedFrom } from '@signaltree/core';
|
|
338
|
+
import type { SignalTree } from '@signaltree/core';
|
|
339
|
+
|
|
340
|
+
interface AppState {
|
|
341
|
+
counter: number;
|
|
342
|
+
}
|
|
343
|
+
type AppTree = SignalTree<AppState>;
|
|
344
|
+
|
|
345
|
+
export const counterDerived = derivedFrom<AppTree>()(($) => ({
|
|
346
|
+
doubled: $.counter() * 2,
|
|
347
|
+
}));
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Prefer plain Angular `computed()` for inline derivations. Reach for `derivedFrom` only when you need to declare derived logic in a separate file and still get typed `$`.
|
|
351
|
+
|
|
352
|
+
### `linked()` — derived-but-writable
|
|
353
|
+
|
|
354
|
+
`.derived()` values are read-only `computed`s. When you need a value derived from a source that is *also writable* (and re-derives when the source changes), return `linked()` — comparable to NgRx `withLinkedState`, wrapping Angular's `linkedSignal`. The merged path is a real `WritableSignal`, so `.set()`/`.update()` type-check.
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
import { signalTree, linked } from '@signaltree/core';
|
|
358
|
+
|
|
359
|
+
interface Option {
|
|
360
|
+
id: number;
|
|
361
|
+
label: string;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const tree = signalTree({ options: [] as Option[] }).derived(($) => ({
|
|
365
|
+
// Sticky selection: derive from options, keep the chosen item across refreshes.
|
|
366
|
+
// Annotate the computation's return type — like Angular's linkedSignal, TS
|
|
367
|
+
// can't infer it from the body when `prev.value` is referenced.
|
|
368
|
+
selected: linked({
|
|
369
|
+
source: () => $.options(),
|
|
370
|
+
computation: (opts, prev): Option | undefined =>
|
|
371
|
+
opts.find((o) => o.id === prev?.value?.id) ?? opts[0],
|
|
372
|
+
}),
|
|
373
|
+
// Simple form — re-derives when its reads change (return type inferred):
|
|
374
|
+
count: linked(() => $.options().length),
|
|
375
|
+
}));
|
|
376
|
+
|
|
377
|
+
const current: Option | undefined = tree.$.selected(); // read (derived)
|
|
378
|
+
tree.$.selected.set({ id: 9, label: 'manual' }); // write (override) — type-checks
|
|
379
|
+
tree.$.count.update((n) => n + 1); // writable too
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Use `linked()` only when you need the writable part; for pure projections use `computed()`. The `source` reads tree state, so `linked()` belongs in `.derived(...)` where `$` is available.
|
|
383
|
+
|
|
384
|
+
## Other utilities
|
|
385
|
+
|
|
386
|
+
- `equal(a, b)` / `deepEqual(a, b)` — equality helpers used internally and safe to consume.
|
|
387
|
+
- `parsePath('a.b.c')` — split a dot path into segments for dynamic access.
|
|
388
|
+
- `isNodeAccessor(x)`, `isAnySignal(x)`, `toWritableSignal(x)` — runtime type guards / coercions.
|
|
389
|
+
|
|
390
|
+
## Subpath exports
|
|
391
|
+
|
|
392
|
+
`@signaltree/core` ships additional entry points (not re-exported from the root) to keep the main bundle small:
|
|
393
|
+
|
|
394
|
+
- `@signaltree/core/edit-session` — value-level (`createEditSession`) and path-bound (`createTreeEditSession`, v10.1+) draft / undo / commit primitives.
|
|
395
|
+
- `@signaltree/core/security` — security-oriented helpers (`SecurityValidator`, `SecurityPresets`).
|
|
396
|
+
- `@signaltree/core/storage` — storage adapter primitives (`createStorageAdapter`, `createIndexedDBAdapter`).
|
|
397
|
+
|
|
398
|
+
Import from the subpath when you need these — do not expect them on the main barrel.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Install
|
|
2
|
+
|
|
3
|
+
Version and install steps derived from each package's `peerDependencies` in `packages/<pkg>/package.json` (tiebreaker: `package.json` itself).
|
|
4
|
+
|
|
5
|
+
## Required runtime
|
|
6
|
+
|
|
7
|
+
- **Angular 20, 21, or 22** — every Angular-consuming package declares
|
|
8
|
+
`@angular/core: ^20.0.0 || ^21.0.0 || ^22.0.0` in `peerDependencies`.
|
|
9
|
+
SignalTree runs on Angular 20, 21, and 22 unchanged.
|
|
10
|
+
- **TypeScript** — whatever your Angular project already pins.
|
|
11
|
+
- **Node / package manager** — your normal Angular toolchain.
|
|
12
|
+
|
|
13
|
+
Never instruct consumers to install `@signaltree/shared`, `@signaltree/types`, or `@signaltree/utils`. These are **private** packages bundled into the public `@signaltree/*` packages at build time.
|
|
14
|
+
|
|
15
|
+
## Core
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @signaltree/core
|
|
19
|
+
# yarn: yarn add @signaltree/core
|
|
20
|
+
# pnpm: pnpm add @signaltree/core # in a single-package repo
|
|
21
|
+
# pnpm add -w @signaltree/core # in a pnpm workspace, install at the workspace root
|
|
22
|
+
# pnpm --filter <pkg> add @signaltree/core # in a pnpm workspace, install in one package only
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
> **pnpm workspaces:** plain `pnpm add @signaltree/core` from the workspace root fails with `ERR_PNPM_ADDING_TO_ROOT`. Use `-w` for the workspace root, or `--filter <pkg>` to scope to one workspace package.
|
|
26
|
+
|
|
27
|
+
Required peer deps (from `packages/core/package.json`):
|
|
28
|
+
`@angular/core ^20.0.0 || ^21.0.0 || ^22.0.0`, `tslib ^2.0.0`. `@angular/compiler`, `@angular/platform-browser-dynamic`, and `zone.js` are declared optional peers.
|
|
29
|
+
|
|
30
|
+
## Optional packages
|
|
31
|
+
|
|
32
|
+
Install only what you need. Each package declares `@signaltree/core` as a peer.
|
|
33
|
+
|
|
34
|
+
### `@signaltree/ng-forms`
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @signaltree/ng-forms
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Adds `@angular/forms ^20.0.0 || ^21.0.0 || ^22.0.0` and `rxjs ^7.0.0` as peers. Read [`../ng-forms/SKILL.md`](../ng-forms/SKILL.md).
|
|
41
|
+
|
|
42
|
+
### `@signaltree/enterprise`
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install @signaltree/enterprise
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Licensed under BSL-1.1 (see the package `package.json`). Peers: `@angular/core ^20.0.0 || ^21.0.0 || ^22.0.0`, `@signaltree/core`. Read [`../enterprise/SKILL.md`](../enterprise/SKILL.md).
|
|
49
|
+
|
|
50
|
+
### `@signaltree/callable-syntax`
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm install --save-dev @signaltree/callable-syntax
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Build-time-only transform — **install as a dev dependency**. Zero bytes at runtime. Wire into your build via the Vite plugin (`@signaltree/callable-syntax/vite`) or the Webpack plugin (`@signaltree/callable-syntax/webpack`). Detailed setup lives in [`../callable-syntax/SKILL.md`](../callable-syntax/SKILL.md).
|
|
57
|
+
|
|
58
|
+
**TypeScript augmentation (≥ 9.2.0):** `@signaltree/callable-syntax` owns the `declare module '@angular/core'` augmentation that adds callable overloads to `WritableSignal<T>`. Prior to `@signaltree/core@9.2.0` the same augmentation also lived in `core` and activated globally on any import from core; that was removed because it broke type-checking in projects coexisting with `@ngrx/signals`. If you want the callable form on raw Angular signals, opt in explicitly via either:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// side-effect import in a file that's part of your app's compilation
|
|
62
|
+
import '@signaltree/callable-syntax/augmentation';
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
or in `tsconfig.json`:
|
|
66
|
+
|
|
67
|
+
```jsonc
|
|
68
|
+
{
|
|
69
|
+
"compilerOptions": {
|
|
70
|
+
"types": ["@signaltree/callable-syntax"]
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `@signaltree/guardrails`
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install --save-dev @signaltree/guardrails
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Development-only package. Its `exports` map resolves to `./dist/noop.js` under the `production` condition, so production bundles contain only no-ops. Peers: `@signaltree/core ^9.0.1`, `tslib ^2.0.0`. Read [`../guardrails/SKILL.md`](../guardrails/SKILL.md).
|
|
82
|
+
|
|
83
|
+
### `@signaltree/events`
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm install @signaltree/events zod
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**ESM-only.** Zod is a required runtime peer. Optional peers (install only when using the matching subpath): `@nestjs/common`, `bullmq`, `ioredis`, `@angular/core`, `rxjs`, `reflect-metadata`, `socket.io-client`. Read [`../events/SKILL.md`](../events/SKILL.md).
|
|
90
|
+
|
|
91
|
+
### `@signaltree/realtime`
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npm install @signaltree/realtime
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Peers: `@angular/core ^20.0.0 || ^21.0.0 || ^22.0.0`, `@signaltree/core ^9.0.0`, `tslib ^2.0.0`. `@supabase/supabase-js ^2.0.0` and `firebase` are **optional** peers — install the one that matches your backend. Read [`../realtime/SKILL.md`](../realtime/SKILL.md).
|
|
98
|
+
|
|
99
|
+
## Verifying the install
|
|
100
|
+
|
|
101
|
+
After installing, a minimal smoke test:
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { signalTree } from '@signaltree/core';
|
|
105
|
+
|
|
106
|
+
const tree = signalTree({ ok: true });
|
|
107
|
+
console.assert(tree.$.ok() === true);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
If that typechecks and runs, `@signaltree/core` is wired correctly. Repeat per optional package as you add it.
|