@signaltree/core 9.0.1 → 9.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/README.md +21 -0
- package/dist/lib/signal-tree.js +49 -4
- package/package.json +5 -4
- package/src/lib/types.d.ts +4 -8
package/README.md
CHANGED
|
@@ -2064,6 +2064,7 @@ tree.unwrap(); // Get plain object
|
|
|
2064
2064
|
|
|
2065
2065
|
// Tree operations
|
|
2066
2066
|
tree.update(updater); // Update entire tree
|
|
2067
|
+
tree.updateAndReport(updater); // Update + return changed leaf paths (9.1+)
|
|
2067
2068
|
tree.effect(fn); // Create reactive effects
|
|
2068
2069
|
tree.subscribe(fn); // Manual subscriptions
|
|
2069
2070
|
tree.destroy(); // Cleanup resources
|
|
@@ -2075,6 +2076,26 @@ tree.destroy(); // Cleanup resources
|
|
|
2075
2076
|
// tree.$.users.selectBy(pred); // Filtered signal
|
|
2076
2077
|
```
|
|
2077
2078
|
|
|
2079
|
+
### updateAndReport (9.1+)
|
|
2080
|
+
|
|
2081
|
+
Like `update`, but returns the dot-paths of every leaf signal whose value
|
|
2082
|
+
actually changed. Useful for diff logging, audit trails, optimistic-update
|
|
2083
|
+
rollback, and selective re-syncing to a server.
|
|
2084
|
+
|
|
2085
|
+
```typescript
|
|
2086
|
+
const tree = signalTree({ user: { name: 'Ada', age: 36 }, count: 0 });
|
|
2087
|
+
|
|
2088
|
+
const changed = tree.updateAndReport({
|
|
2089
|
+
user: { name: 'Ada', age: 37 }, // age changes, name is ref-equal
|
|
2090
|
+
count: 0, // ref-equal, skipped
|
|
2091
|
+
});
|
|
2092
|
+
// changed === ['user.age']
|
|
2093
|
+
```
|
|
2094
|
+
|
|
2095
|
+
Reference-equal values are skipped automatically by the underlying
|
|
2096
|
+
`update` path, so passing a server-returned partial that mostly matches
|
|
2097
|
+
local state is cheap (`O(changed)` instead of `O(N)`).
|
|
2098
|
+
|
|
2078
2099
|
## Extending with enhancers
|
|
2079
2100
|
|
|
2080
2101
|
SignalTree Core includes all enhancers built-in:
|
package/dist/lib/signal-tree.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { signal, isSignal } from '@angular/core';
|
|
1
|
+
import { signal, isSignal, untracked } from '@angular/core';
|
|
2
2
|
import { SIGNAL_TREE_MESSAGES, SIGNAL_TREE_CONSTANTS } from './constants.js';
|
|
3
3
|
import { batchScope } from './internals/batch-scope.js';
|
|
4
4
|
import { isRegisteredMarker, materializeMarkers } from './internals/materialize-markers.js';
|
|
@@ -114,19 +114,25 @@ function makeNodeAccessor(store) {
|
|
|
114
114
|
}
|
|
115
115
|
return accessor;
|
|
116
116
|
}
|
|
117
|
-
function recursiveUpdate(target, updates) {
|
|
117
|
+
function recursiveUpdate(target, updates, out, pathPrefix = '') {
|
|
118
118
|
if (!updates || typeof updates !== 'object') return;
|
|
119
119
|
const targetObj = isNodeAccessor(target) ? target : target;
|
|
120
120
|
for (const [key, value] of Object.entries(updates)) {
|
|
121
121
|
const prop = targetObj[key];
|
|
122
122
|
if (prop === undefined) continue;
|
|
123
|
+
const childPath = pathPrefix ? `${pathPrefix}.${key}` : key;
|
|
123
124
|
if (isSignal(prop) && 'set' in prop) {
|
|
124
|
-
prop
|
|
125
|
+
const sig = prop;
|
|
126
|
+
const current = untracked(() => sig());
|
|
127
|
+
if (current === value) continue;
|
|
128
|
+
sig.set(value);
|
|
129
|
+
if (out) out.push(childPath);
|
|
125
130
|
} else if (isNodeAccessor(prop)) {
|
|
126
131
|
if (value && typeof value === 'object') {
|
|
127
|
-
recursiveUpdate(prop, value);
|
|
132
|
+
recursiveUpdate(prop, value, out, childPath);
|
|
128
133
|
} else {
|
|
129
134
|
prop(value);
|
|
135
|
+
if (out) out.push(childPath);
|
|
130
136
|
}
|
|
131
137
|
}
|
|
132
138
|
}
|
|
@@ -333,6 +339,25 @@ function create(initialState, config) {
|
|
|
333
339
|
writable: true,
|
|
334
340
|
configurable: true
|
|
335
341
|
});
|
|
342
|
+
Object.defineProperty(tree, 'updateAndReport', {
|
|
343
|
+
value: function (arg) {
|
|
344
|
+
if (arguments.length === 0) return [];
|
|
345
|
+
const out = [];
|
|
346
|
+
if (typeof arg === 'function') {
|
|
347
|
+
const updater = arg;
|
|
348
|
+
const current = unwrap(signalState);
|
|
349
|
+
batchScope(() => recursiveUpdate(signalState, updater(current), out));
|
|
350
|
+
} else if (typeof arg === 'object' && arg !== null && !Array.isArray(arg)) {
|
|
351
|
+
batchScope(() => recursiveUpdate(signalState, arg, out));
|
|
352
|
+
} else {
|
|
353
|
+
recursiveUpdate(signalState, arg, out);
|
|
354
|
+
}
|
|
355
|
+
return out;
|
|
356
|
+
},
|
|
357
|
+
enumerable: false,
|
|
358
|
+
writable: true,
|
|
359
|
+
configurable: true
|
|
360
|
+
});
|
|
336
361
|
for (const key of Object.keys(signalState)) {
|
|
337
362
|
if (!(key in tree)) {
|
|
338
363
|
Object.defineProperty(tree, key, {
|
|
@@ -453,6 +478,26 @@ function createBuilder(baseTree) {
|
|
|
453
478
|
configurable: true
|
|
454
479
|
});
|
|
455
480
|
}
|
|
481
|
+
Object.defineProperty(builder, 'updateAndReport', {
|
|
482
|
+
value: function (arg) {
|
|
483
|
+
finalize();
|
|
484
|
+
const fn = baseTree['updateAndReport'];
|
|
485
|
+
return fn ? fn.call(baseTree, arg) : [];
|
|
486
|
+
},
|
|
487
|
+
enumerable: false,
|
|
488
|
+
writable: false,
|
|
489
|
+
configurable: true
|
|
490
|
+
});
|
|
491
|
+
Object.defineProperty(builder, 'batchUpdate', {
|
|
492
|
+
value: function (arg) {
|
|
493
|
+
finalize();
|
|
494
|
+
const fn = baseTree['batchUpdate'];
|
|
495
|
+
if (fn) fn.call(baseTree, arg);
|
|
496
|
+
},
|
|
497
|
+
enumerable: false,
|
|
498
|
+
writable: true,
|
|
499
|
+
configurable: true
|
|
500
|
+
});
|
|
456
501
|
Object.defineProperty(builder, 'derived', {
|
|
457
502
|
value: function (factory) {
|
|
458
503
|
if (isFinalized) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signaltree/core",
|
|
3
|
-
"version": "9.0
|
|
3
|
+
"version": "9.2.0",
|
|
4
4
|
"description": "Reactive JSON for Angular. JSON branches, reactive leaves. No actions. No reducers. No selectors.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"./package.json": "./package.json"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@angular/core": "^20.0.0",
|
|
36
|
-
"@angular/compiler": "^20.0.0",
|
|
37
|
-
"@angular/platform-browser-dynamic": "^20.0.0",
|
|
35
|
+
"@angular/core": "^20.0.0 || ^21.0.0",
|
|
36
|
+
"@angular/compiler": "^20.0.0 || ^21.0.0",
|
|
37
|
+
"@angular/platform-browser-dynamic": "^20.0.0 || ^21.0.0",
|
|
38
38
|
"zone.js": "^0.15.0",
|
|
39
39
|
"tslib": "^2.0.0"
|
|
40
40
|
},
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"files": [
|
|
56
56
|
"dist/**/*.js",
|
|
57
57
|
"src/**/*.d.ts",
|
|
58
|
+
"skills/**/*",
|
|
58
59
|
"README.md"
|
|
59
60
|
]
|
|
60
61
|
}
|
package/src/lib/types.d.ts
CHANGED
|
@@ -16,12 +16,6 @@ export interface TimeTravelConfig {
|
|
|
16
16
|
}
|
|
17
17
|
export type Primitive = string | number | boolean | null | undefined | bigint | symbol;
|
|
18
18
|
export type NotFn<T> = T extends (...args: unknown[]) => unknown ? never : T;
|
|
19
|
-
declare module '@angular/core' {
|
|
20
|
-
interface WritableSignal<T> {
|
|
21
|
-
(value: NotFn<T>): void;
|
|
22
|
-
(updater: (current: T) => T): void;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
19
|
export interface NodeAccessor<T> {
|
|
26
20
|
(): T;
|
|
27
21
|
(value: Partial<T>): void;
|
|
@@ -38,6 +32,7 @@ export interface ISignalTree<T> extends NodeAccessor<T> {
|
|
|
38
32
|
destroy(): void;
|
|
39
33
|
readonly destroyed: Signal<boolean>;
|
|
40
34
|
registerCleanup(fn: EnhancerCleanup): void;
|
|
35
|
+
updateAndReport(updates: Partial<T> | ((current: T) => Partial<T>)): string[];
|
|
41
36
|
}
|
|
42
37
|
export type EnhancerCleanup = () => void;
|
|
43
38
|
export interface EffectsMethods<T> {
|
|
@@ -271,10 +266,11 @@ export type PathInterceptor = (ctx: {
|
|
|
271
266
|
blocked: boolean;
|
|
272
267
|
blockReason?: string;
|
|
273
268
|
}, next: () => void) => void | Promise<void>;
|
|
274
|
-
export
|
|
269
|
+
export interface CallableWritableSignal<T> extends WritableSignal<T> {
|
|
270
|
+
(): T;
|
|
275
271
|
(value: NotFn<T>): void;
|
|
276
272
|
(updater: (current: T) => T): void;
|
|
277
|
-
}
|
|
273
|
+
}
|
|
278
274
|
export type AccessibleNode<T> = NodeAccessor<T> & TreeNode<T>;
|
|
279
275
|
export declare const ENHANCER_META: unique symbol;
|
|
280
276
|
export type Enhancer<TAdded> = (tree: ISignalTree<any>) => ISignalTree<any> & TAdded;
|