@signal-tree/angular 15.1.2 → 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 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
- Angular applications should construct state through this package, not through
27
- the neutral kernel package:
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
@@ -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?.()}catch{}});return tree}};SignalTreeStore=__decorate([Injectable({providedIn:config.providedIn??null}),__metadata("design:paramtypes",[])],SignalTreeStore);return SignalTreeStore}export{defineStore};
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.2",
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.2"
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",
@@ -5,9 +5,9 @@ import type { ISignalTreeOf, ReadonlyStoreOf } from '@signal-tree/kernel/adapter
5
5
  */
6
6
  export interface DefineStoreConfig {
7
7
  /**
8
- * Where to provide the store. `'root'`/`'platform'` make it an app-wide
9
- * singleton (like `@Injectable({ providedIn: 'root' })`); omit/`null` to
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
- * for components that should only read the store, pairing it with a
25
- * separate `@Injectable` Ops service for writes (see "Production
26
- * architecture" in the root README).
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** — callable, with `$` and any
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>;