@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 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:
@@ -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.set(value);
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.1",
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
  }
@@ -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 type CallableWritableSignal<T> = WritableSignal<T> & {
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;