@signal-tree/angular 15.1.1 → 15.1.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/README.md +103 -2
- package/dist/index.js +1 -1
- package/dist/lib/construction-input.js +1 -0
- package/dist/lib/define-store.js +1 -1
- package/package.json +2 -2
- package/src/index.d.ts +4 -2
- package/src/lib/construction-input.d.ts +9 -0
- package/src/lib/define-store.d.ts +20 -9
package/README.md
CHANGED
|
@@ -23,8 +23,82 @@ the complete SignalTree facade for Angular applications: import `signalTree`,
|
|
|
23
23
|
markers, enhancers, and types from this package rather than mixing kernel
|
|
24
24
|
imports into Angular application code.
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
## Application-store ownership
|
|
27
|
+
|
|
28
|
+
Use `defineStore(() => signalTree(...))` for an Angular-owned application store.
|
|
29
|
+
`signalTree` selects Angular reactivity and constructs the tree; `defineStore`
|
|
30
|
+
creates its injectable token and binds destruction to the providing injector.
|
|
31
|
+
The factory runs in injection context and must return a fresh owned object or
|
|
32
|
+
function, not a primitive or a tree borrowed from another owner. Cleanup errors
|
|
33
|
+
are reported with Angular's default `ErrorHandler` so later store cleanup can
|
|
34
|
+
still run. This does not resolve the application's custom handler: that handler
|
|
35
|
+
may itself depend on a store, and the owning injector is already destroyed when
|
|
36
|
+
teardown runs.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { inject } from '@angular/core';
|
|
40
|
+
import { defineStore, signalTree } from '@signal-tree/angular';
|
|
41
|
+
|
|
42
|
+
export const SettingsStore = defineStore(() => signalTree({ theme: 'light' }), { providedIn: 'root' });
|
|
43
|
+
|
|
44
|
+
// In an Angular injection context:
|
|
45
|
+
const settings = inject(SettingsStore);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Omit `providedIn` and add the token to a component's `providers` for a separate
|
|
49
|
+
store per component. A consumer borrowing an injected store does not destroy
|
|
50
|
+
it. Do not wrap a borrowed tree in another `defineStore`: that registers a
|
|
51
|
+
second owner. Root/request injectors must themselves be destroyed by the
|
|
52
|
+
application or SSR host at the end of their lifetime.
|
|
53
|
+
|
|
54
|
+
### Readonly state and operations share one owner
|
|
55
|
+
|
|
56
|
+
`expose: 'readonly'` narrows the token for **all** consumers, including an Ops
|
|
57
|
+
service. It does not create a separate writable injection path. When components
|
|
58
|
+
need readonly state and operations need writes, keep the owner token internal
|
|
59
|
+
and expose its readonly `$` through a non-owning Angular provider:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { inject, Injectable, InjectionToken, type Provider } from '@angular/core';
|
|
63
|
+
import { asReadonly, defineStore, signalTree } from '@signal-tree/angular';
|
|
64
|
+
|
|
65
|
+
const CounterTree = defineStore(() => signalTree({ count: 0 }));
|
|
66
|
+
|
|
67
|
+
@Injectable()
|
|
68
|
+
export class CounterOps {
|
|
69
|
+
private readonly tree = inject(CounterTree);
|
|
70
|
+
readonly state = asReadonly(this.tree).$;
|
|
71
|
+
|
|
72
|
+
increment(): void {
|
|
73
|
+
this.tree.$.count.update((count) => count + 1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const COUNTER_STATE = new InjectionToken<CounterOps['state']>('CounterState');
|
|
78
|
+
|
|
79
|
+
export function provideCounterStore(): Provider[] {
|
|
80
|
+
return [CounterTree, CounterOps, { provide: COUNTER_STATE, useFactory: () => inject(CounterOps).state }];
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Register `provideCounterStore()` at application bootstrap for one application
|
|
85
|
+
owner, or in a component's `providers` for independent local owners. Register
|
|
86
|
+
Ops and the reader together with the tree at the intended scope. Components use
|
|
87
|
+
`inject(COUNTER_STATE).count()` and `inject(CounterOps).increment()`; only
|
|
88
|
+
`CounterTree` owns teardown. The reader exposes neither writers nor `destroy()`.
|
|
89
|
+
Readonly remains a compile-time boundary, not runtime access control.
|
|
90
|
+
|
|
91
|
+
For stores where every injected consumer should be readonly, the existing
|
|
92
|
+
`expose: 'readonly'` option is sufficient. Prefer an inferred config literal or
|
|
93
|
+
`{ expose: 'readonly' } satisfies DefineStoreConfig`; widening to
|
|
94
|
+
`DefineStoreConfig` erases the information required to choose the readonly
|
|
95
|
+
return type.
|
|
96
|
+
|
|
97
|
+
## Tree construction
|
|
98
|
+
|
|
99
|
+
Inside the store factory, declare state, enhancers, and derived recipes together.
|
|
100
|
+
The following standalone example uses explicit cleanup; this lower-level form
|
|
101
|
+
also serves tests and intentionally manual lifetimes:
|
|
28
102
|
|
|
29
103
|
```ts
|
|
30
104
|
import { asReadonly, batching, entityMap, signalTree } from '@signal-tree/angular';
|
|
@@ -53,6 +127,7 @@ const tree = signalTree(
|
|
|
53
127
|
|
|
54
128
|
const reader = asReadonly(tree);
|
|
55
129
|
reader.$.selectedName();
|
|
130
|
+
tree.destroy(); // End the explicitly owned standalone example.
|
|
56
131
|
```
|
|
57
132
|
|
|
58
133
|
There is one construction grammar: state, enhancers, and one derived factory are
|
|
@@ -81,3 +156,29 @@ const profileModel = toWritableSignal(tree.$.profile, injector, {
|
|
|
81
156
|
Application components
|
|
82
157
|
should normally receive a read-only `$` plus explicit operation services for
|
|
83
158
|
writes and asynchronous work.
|
|
159
|
+
|
|
160
|
+
## Initial values and external reactivity
|
|
161
|
+
|
|
162
|
+
Pass initial values to `signalTree()`, not existing Angular signals. Direct
|
|
163
|
+
signals (including readonly and computed signals) at the root or a nested branch
|
|
164
|
+
are rejected with a path-specific error, and known signal types are rejected by
|
|
165
|
+
TypeScript. SignalTree creates its own native signals so it owns their writes.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import { signal, untracked } from '@angular/core';
|
|
169
|
+
import { leaf, signalTree } from '@signal-tree/angular';
|
|
170
|
+
|
|
171
|
+
const existing = signal(1);
|
|
172
|
+
const tree = signalTree({ count: leaf(untracked(existing)) });
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
This takes an independent snapshot; later writes to `existing` do not update
|
|
176
|
+
`tree`. For an object snapshot, copy the value too if you need independent object
|
|
177
|
+
identity. `leaf(existing)` explicitly stores the signal itself as data. Reading
|
|
178
|
+
that leaf returns the original signal; its inner writes remain outside tree
|
|
179
|
+
transactions and restoration.
|
|
180
|
+
|
|
181
|
+
Validation stops at explicit `leaf(...)`, marker definitions, arrays and built-in
|
|
182
|
+
terminal values. Their contents remain data, not separately owned tree locations.
|
|
183
|
+
Ordinary functions remain valid callable data. These checks apply to initial
|
|
184
|
+
construction, not arbitrary later writes or foreign reactivity from other libraries.
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ANGULAR_OBSERVATION_ADAPTER}from"./lib/observation-adapter.js";import{createSignalTreeFactory}from"@signal-tree/kernel/adapter";import{asReadonly as asReadonly$1}from"@signal-tree/kernel";export*from"@signal-tree/kernel";import{defineStore}from"./lib/define-store.js";import{toWritableSignal}from"./lib/to-writable-signal.js";const
|
|
1
|
+
import{ANGULAR_OBSERVATION_ADAPTER}from"./lib/observation-adapter.js";import{createSignalTreeFactory}from"@signal-tree/kernel/adapter";import{asReadonly as asReadonly$1}from"@signal-tree/kernel";export*from"@signal-tree/kernel";import{prepareConstructionInput}from"./lib/construction-input.js";import{defineStore}from"./lib/define-store.js";import{toWritableSignal}from"./lib/to-writable-signal.js";const createAngularTree=createSignalTreeFactory(ANGULAR_OBSERVATION_ADAPTER);const signalTree=(initialState,config)=>createAngularTree(prepareConstructionInput(initialState),config);const asReadonly=asReadonly$1;export{asReadonly,defineStore,signalTree,toWritableSignal};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{isSignal}from"@angular/core";import{isConstructionBranch}from"@signal-tree/kernel/adapter";function prepareConstructionInput(initialState){const branches=new WeakMap;const visit=(value,path)=>{if(isSignal(value)){throw new TypeError(`SignalTree: Angular signal at ${path}. Pass an initial value or leaf(untracked(existing)) for an independent snapshot. Use leaf(existing) only to store the signal as data; its writes remain external to the tree.`)}if(!isConstructionBranch(value))return value;const previous=branches.get(value);if(previous)return previous;const prepared=Object.create(null);branches.set(value,prepared);for(const key of Object.getOwnPropertySymbols(value)){const descriptor=Object.getOwnPropertyDescriptor(value,key);if(descriptor)Object.defineProperty(prepared,key,descriptor)}for(const[key,child]of Object.entries(value)){prepared[key]=key==="__proto__"?child:visit(child,`${path}[${JSON.stringify(key)}]`)}return prepared};return visit(initialState,"$")}export{prepareConstructionInput};
|
package/dist/lib/define-store.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{__decorate,__metadata}from"tslib";import{Injectable,inject,DestroyRef}from"@angular/core";function defineStore(factory,config={}){let SignalTreeStore=class SignalTreeStore{constructor(){const tree=factory();inject(DestroyRef).onDestroy(()=>{try{tree.destroy
|
|
1
|
+
import{__decorate,__metadata}from"tslib";import{Injectable,inject,DestroyRef,ErrorHandler}from"@angular/core";function defineStore(factory,config={}){let SignalTreeStore=class SignalTreeStore{constructor(){const tree=factory();if(tree===null||typeof tree!=="object"&&typeof tree!=="function"){throw new TypeError("[SignalTree] defineStore factory must return an object or function.")}inject(DestroyRef).onDestroy(()=>{try{const destroy=tree.destroy;if(typeof destroy==="function")destroy.call(tree)}catch(error){new ErrorHandler().handleError(error)}});return tree}};SignalTreeStore=__decorate([Injectable({providedIn:config.providedIn??null}),__metadata("design:paramtypes",[])],SignalTreeStore);return SignalTreeStore}export{defineStore};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signal-tree/angular",
|
|
3
|
-
"version": "15.1.
|
|
3
|
+
"version": "15.1.3",
|
|
4
4
|
"description": "Angular realization for SignalTree.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"llms.txt"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@signal-tree/kernel": "15.1.
|
|
28
|
+
"@signal-tree/kernel": "15.1.3"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@angular/core": "^20.0.0 || ^21.0.0 || ^22.0.0",
|
package/src/index.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/** Angular observation plus the complete SignalTree application surface. */
|
|
2
2
|
import './lib/carrier.js';
|
|
3
|
-
import type { AccessibleNodeOf, EntityNodeOf, EntitySignalOf, EntitySignalWithSlicesOf, ISignalTreeOf, LeafOf, ReadonlyStoreOf, ReadonlyViewOf,
|
|
4
|
-
|
|
3
|
+
import type { AccessibleNodeOf, EntityNodeOf, EntitySignalOf, EntitySignalWithSlicesOf, ISignalTreeOf, LeafOf, ReadonlyStoreOf, ReadonlyViewOf, TreeNodeOf } from '@signal-tree/kernel/adapter';
|
|
4
|
+
import { type FrameworkSignalTreeFactory } from './lib/construction-input.js';
|
|
5
|
+
/** Construct native Angular leaves from values; external signals require leaf(). */
|
|
6
|
+
export declare const signalTree: FrameworkSignalTreeFactory;
|
|
5
7
|
export type TreeNode<T> = TreeNodeOf<T, 'angular'>;
|
|
6
8
|
export type WritableLeaf<T> = LeafOf<T, 'angular'>;
|
|
7
9
|
export type AccessibleNode<T> = AccessibleNodeOf<T, 'angular'>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Signal } from '@angular/core';
|
|
2
|
+
import type { SignalTreeFactoryOf } from '@signal-tree/kernel/adapter';
|
|
3
|
+
export type FrameworkSignalTreeFactory = SignalTreeFactoryOf<'angular', Signal<unknown>>;
|
|
4
|
+
/**
|
|
5
|
+
* Read each branch property once, so accessor-backed definitions cannot change
|
|
6
|
+
* between validation and construction. Terminal data retains its identity.
|
|
7
|
+
* No reactive primitive is read, subscribed to, or adopted here.
|
|
8
|
+
*/
|
|
9
|
+
export declare function prepareConstructionInput(initialState: object): object;
|
|
@@ -5,9 +5,9 @@ import type { ISignalTreeOf, ReadonlyStoreOf } from '@signal-tree/kernel/adapter
|
|
|
5
5
|
*/
|
|
6
6
|
export interface DefineStoreConfig {
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* provide it locally via a component/route `providers` array.
|
|
8
|
+
* `'root'` provides an application singleton. `'platform'` shares the store
|
|
9
|
+
* across applications on that platform; do not use it for request-local state.
|
|
10
|
+
* Omit/`null` to provide it locally via a component/route `providers` array.
|
|
11
11
|
*/
|
|
12
12
|
providedIn?: 'root' | 'platform' | null;
|
|
13
13
|
/**
|
|
@@ -21,9 +21,10 @@ export interface DefineStoreConfig {
|
|
|
21
21
|
* it does not protect against a deliberate `as any` bypass — it protects
|
|
22
22
|
* the common case where an AI agent or developer reaches for a
|
|
23
23
|
* `.set()`/mutator that simply isn't offered on the injected type. Use it
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* when every consumer of this token should receive a readonly tree. An Ops
|
|
25
|
+
* service injecting this same token is also readonly. For separate readers
|
|
26
|
+
* and writers, own one writable tree and expose a non-owning readonly `$`
|
|
27
|
+
* token; see the Angular package README's ownership example.
|
|
27
28
|
*
|
|
28
29
|
* Only accepted when the factory returns a real tree
|
|
29
30
|
* (`signalTree(...)`-shaped); combining it with
|
|
@@ -36,14 +37,24 @@ export interface DefineStoreConfig {
|
|
|
36
37
|
* idiomatic Angular DI pattern for a tree, comparable to NgRx SignalStore's
|
|
37
38
|
* `signalStore()`.
|
|
38
39
|
*
|
|
39
|
-
* `inject(MyStore)` resolves to the **real tree** —
|
|
40
|
+
* `inject(MyStore)` resolves to the **real tree** — with `$` and any
|
|
40
41
|
* configured enhancer methods — not a wrapper. The tree's
|
|
41
42
|
* `destroy()` is tied to the host injector's lifecycle via `DestroyRef`, so a
|
|
42
43
|
* component-provided store tears down with the component and a root store with
|
|
43
44
|
* the app.
|
|
44
45
|
*
|
|
45
46
|
* The factory runs inside Angular's injection context, so it may call `inject()`
|
|
46
|
-
* (e.g. to read other services).
|
|
47
|
+
* (e.g. to read other services). Return a fresh owned object or function for
|
|
48
|
+
* each providing injector, never a tree borrowed from another owner. The
|
|
49
|
+
* factory selects the realization: import `signalTree` from this Angular
|
|
50
|
+
* facade, not the neutral kernel. Use ordinary DI aliases for borrowed views;
|
|
51
|
+
* another `defineStore` would register another destruction owner.
|
|
52
|
+
*
|
|
53
|
+
* Primitive factory results are rejected. If the returned object has a
|
|
54
|
+
* `destroy()` method, failures are reported with Angular's default `ErrorHandler`,
|
|
55
|
+
* not resolved from DI: an application handler may itself depend on this store,
|
|
56
|
+
* and the owning injector is already destroyed when cleanup runs. Reporting
|
|
57
|
+
* allows sibling cleanup to continue. SignalTree destruction is idempotent.
|
|
47
58
|
*
|
|
48
59
|
* @example
|
|
49
60
|
* ```ts
|
|
@@ -90,6 +101,6 @@ export declare function defineStore<T, A>(factory: () => ISignalTreeOf<T, 'angul
|
|
|
90
101
|
* literal (or `as const`). Deliberate: this cliff is what makes
|
|
91
102
|
* `expose: 'readonly'` misuse a compile error instead of a silent no-op.
|
|
92
103
|
*/
|
|
93
|
-
export declare function defineStore<R>(factory: () => R, config?: DefineStoreConfig & {
|
|
104
|
+
export declare function defineStore<R extends object>(factory: () => R, config?: DefineStoreConfig & {
|
|
94
105
|
expose?: undefined;
|
|
95
106
|
}): Type<R>;
|