@signaltree/core 6.2.4 → 6.4.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 +40 -0
- package/dist/enhancers/time-travel/time-travel.js +38 -2
- package/dist/index.js +1 -0
- package/dist/lib/entity-signal.js +12 -2
- package/dist/lib/internals/merge-derived.js +59 -0
- package/dist/lib/markers/derived.js +12 -0
- package/dist/lib/path-notifier.js +72 -0
- package/dist/lib/signal-tree.js +111 -2
- package/package.json +1 -1
- package/src/index.d.ts +3 -0
- package/src/lib/internals/builder-types.d.ts +13 -0
- package/src/lib/internals/derived-types.d.ts +10 -0
- package/src/lib/internals/merge-derived.d.ts +4 -0
- package/src/lib/markers/derived.d.ts +10 -0
- package/src/lib/markers/index.d.ts +1 -0
- package/src/lib/signal-tree.d.ts +5 -2
package/README.md
CHANGED
|
@@ -148,6 +148,46 @@ $.users.setAll(usersFromApi); // Replace all
|
|
|
148
148
|
const user = entityMap()[123]; // Requires intermediate object
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
+
### Notification Batching
|
|
152
|
+
|
|
153
|
+
SignalTree automatically batches _notification delivery_ to subscribers and change detection to the end of the current microtask. This prevents render thrashing when multiple values are updated together and preserves immediate read-after-write semantics (values update synchronously, notifications are deferred).
|
|
154
|
+
|
|
155
|
+
**Example**
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// Multiple updates in the same microtask are coalesced into a single notification
|
|
159
|
+
tree.$.form.name.set('Alice');
|
|
160
|
+
tree.$.form.email.set('alice@example.com');
|
|
161
|
+
tree.$.form.submitted.set(true);
|
|
162
|
+
// → Subscribers are notified once at the end of the microtask with final values
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Testing**
|
|
166
|
+
|
|
167
|
+
When tests need synchronous notification delivery, use `flushSync()`:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { getPathNotifier } from '@signaltree/core';
|
|
171
|
+
|
|
172
|
+
it('updates state', () => {
|
|
173
|
+
tree.$.count.set(5);
|
|
174
|
+
getPathNotifier().flushSync();
|
|
175
|
+
expect(subscriber).toHaveBeenCalledWith(5, 0);
|
|
176
|
+
});
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Alternatively, await a microtask (`await Promise.resolve()`) to allow the automatic flush to occur.
|
|
180
|
+
|
|
181
|
+
**Opting out**
|
|
182
|
+
|
|
183
|
+
To disable automatic microtask batching for a specific tree instance:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
const tree = signalTree(initialState, { batching: false });
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Use this only for rare cases that truly require synchronous notifications (most apps should keep batching enabled).
|
|
190
|
+
|
|
151
191
|
## Quick start
|
|
152
192
|
|
|
153
193
|
### Installation
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { snapshotState } from '../../lib/utils.js';
|
|
2
|
-
import {
|
|
2
|
+
import { deepEqual, deepClone } from './utils.js';
|
|
3
3
|
|
|
4
4
|
class TimeTravelManager {
|
|
5
5
|
tree;
|
|
@@ -24,7 +24,7 @@ class TimeTravelManager {
|
|
|
24
24
|
};
|
|
25
25
|
this.addEntry('INIT', this.tree());
|
|
26
26
|
}
|
|
27
|
-
addEntry(action, state, payload) {
|
|
27
|
+
addEntry(action, state, payload, provisional = false) {
|
|
28
28
|
if (this.currentIndex < this.history.length - 1) {
|
|
29
29
|
this.history = this.history.slice(0, this.currentIndex + 1);
|
|
30
30
|
}
|
|
@@ -43,6 +43,12 @@ class TimeTravelManager {
|
|
|
43
43
|
payload
|
|
44
44
|
})
|
|
45
45
|
};
|
|
46
|
+
if (provisional) entry.__provisional = true;
|
|
47
|
+
const last = this.history[this.history.length - 1];
|
|
48
|
+
if (last && deepEqual(last.state, entry.state)) {
|
|
49
|
+
if (last.__provisional) delete last.__provisional;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
46
52
|
this.history.push(entry);
|
|
47
53
|
this.currentIndex = this.history.length - 1;
|
|
48
54
|
if (this.history.length > this.maxHistorySize) {
|
|
@@ -59,6 +65,20 @@ class TimeTravelManager {
|
|
|
59
65
|
this.restoreState(entry.state);
|
|
60
66
|
return true;
|
|
61
67
|
}
|
|
68
|
+
finalizeProvisional(state) {
|
|
69
|
+
const last = this.history[this.history.length - 1];
|
|
70
|
+
if (last && last.__provisional) {
|
|
71
|
+
if (deepEqual(last.state, state)) {
|
|
72
|
+
delete last.__provisional;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
last.state = deepClone(state);
|
|
76
|
+
last.timestamp = Date.now();
|
|
77
|
+
delete last.__provisional;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.addEntry('update', state);
|
|
81
|
+
}
|
|
62
82
|
redo() {
|
|
63
83
|
if (!this.canRedo()) {
|
|
64
84
|
return false;
|
|
@@ -143,6 +163,22 @@ function timeTravel(config = {}) {
|
|
|
143
163
|
isRestoring = false;
|
|
144
164
|
}
|
|
145
165
|
});
|
|
166
|
+
try {
|
|
167
|
+
const req = globalThis['require'];
|
|
168
|
+
if (typeof req === 'function') {
|
|
169
|
+
const {
|
|
170
|
+
getPathNotifier
|
|
171
|
+
} = req('../../lib/path-notifier');
|
|
172
|
+
const notifier = getPathNotifier();
|
|
173
|
+
if (notifier && typeof notifier.onFlush === 'function') {
|
|
174
|
+
notifier.onFlush(() => {
|
|
175
|
+
if (isRestoring) return;
|
|
176
|
+
const afterState = originalTreeCall();
|
|
177
|
+
timeTravelManager.addEntry('batch', afterState);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch {}
|
|
146
182
|
const enhancedTree = function (...args) {
|
|
147
183
|
if (args.length === 0) {
|
|
148
184
|
return originalTreeCall();
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { signalTree } from './lib/signal-tree.js';
|
|
2
2
|
export { ENHANCER_META, entityMap } from './lib/types.js';
|
|
3
|
+
export { derived, isDerivedMarker } from './lib/markers/derived.js';
|
|
3
4
|
export { composeEnhancers, createLazySignalTree, isAnySignal, isNodeAccessor, toWritableSignal } from './lib/utils.js';
|
|
4
5
|
export { createEditSession } from './lib/edit-session.js';
|
|
5
6
|
export { getPathNotifier } from './lib/path-notifier.js';
|
|
@@ -42,6 +42,8 @@ function createEntitySignal(config, pathNotifier, basePath) {
|
|
|
42
42
|
}
|
|
43
43
|
return node;
|
|
44
44
|
}
|
|
45
|
+
const whereCache = new WeakMap();
|
|
46
|
+
const findCache = new WeakMap();
|
|
45
47
|
const api = {
|
|
46
48
|
byId(id) {
|
|
47
49
|
const entity = storage.get(id);
|
|
@@ -74,10 +76,18 @@ function createEntitySignal(config, pathNotifier, basePath) {
|
|
|
74
76
|
return computed(() => countSignal() === 0);
|
|
75
77
|
},
|
|
76
78
|
where(predicate) {
|
|
77
|
-
|
|
79
|
+
const cached = whereCache.get(predicate);
|
|
80
|
+
if (cached) return cached;
|
|
81
|
+
const s = computed(() => allSignal().filter(predicate));
|
|
82
|
+
whereCache.set(predicate, s);
|
|
83
|
+
return s;
|
|
78
84
|
},
|
|
79
85
|
find(predicate) {
|
|
80
|
-
|
|
86
|
+
const cached = findCache.get(predicate);
|
|
87
|
+
if (cached) return cached;
|
|
88
|
+
const s = computed(() => allSignal().find(predicate));
|
|
89
|
+
findCache.set(predicate, s);
|
|
90
|
+
return s;
|
|
81
91
|
},
|
|
82
92
|
addOne(entity, opts) {
|
|
83
93
|
const id = opts?.selectId?.(entity) ?? selectId(entity);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { computed, isSignal } from '@angular/core';
|
|
2
|
+
import { isDerivedMarker } from '../markers/derived.js';
|
|
3
|
+
|
|
4
|
+
function isSignalLike(value) {
|
|
5
|
+
return isSignal(value);
|
|
6
|
+
}
|
|
7
|
+
function ensurePathAndGetTarget($, path) {
|
|
8
|
+
if (!path) return $;
|
|
9
|
+
const parts = path.split('.');
|
|
10
|
+
let current = $;
|
|
11
|
+
for (const part of parts) {
|
|
12
|
+
if (!(part in current)) {
|
|
13
|
+
current[part] = {};
|
|
14
|
+
}
|
|
15
|
+
current = current[part];
|
|
16
|
+
}
|
|
17
|
+
return current;
|
|
18
|
+
}
|
|
19
|
+
function mergeDerivedState($, derivedDef, path = '') {
|
|
20
|
+
if (!derivedDef || typeof derivedDef !== 'object') {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
for (const [key, value] of Object.entries(derivedDef)) {
|
|
24
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
25
|
+
if (isDerivedMarker(value)) {
|
|
26
|
+
const target = ensurePathAndGetTarget($, path);
|
|
27
|
+
if (key in target && isSignalLike(target[key])) {
|
|
28
|
+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
|
29
|
+
console.warn(`SignalTree: Derived "${currentPath}" overwrites source signal. ` + `Consider using a different key to avoid confusion.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
target[key] = computed(value.factory);
|
|
33
|
+
} else if (isSignalLike(value)) {
|
|
34
|
+
const target = ensurePathAndGetTarget($, path);
|
|
35
|
+
if (key in target && isSignalLike(target[key])) {
|
|
36
|
+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
|
|
37
|
+
console.warn(`SignalTree: Derived signal "${currentPath}" overwrites source signal. ` + `Consider using a different key to avoid confusion.`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
target[key] = value;
|
|
41
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
42
|
+
const target = ensurePathAndGetTarget($, path);
|
|
43
|
+
if (!(key in target)) {
|
|
44
|
+
target[key] = {};
|
|
45
|
+
} else if (isSignalLike(target[key])) {
|
|
46
|
+
throw new Error(`SignalTree: Cannot merge derived object into "${currentPath}" ` + `because source is a signal. Either make source an object or use a different key.`);
|
|
47
|
+
}
|
|
48
|
+
mergeDerivedState($, value, currentPath);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function applyDerivedFactories($, factories) {
|
|
53
|
+
for (const factory of factories) {
|
|
54
|
+
const derivedDef = factory($);
|
|
55
|
+
mergeDerivedState($, derivedDef);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { applyDerivedFactories, mergeDerivedState };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const DERIVED_MARKER = Symbol.for('signaltree:derived');
|
|
2
|
+
function derived(factory) {
|
|
3
|
+
return {
|
|
4
|
+
[DERIVED_MARKER]: true,
|
|
5
|
+
factory
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function isDerivedMarker(value) {
|
|
9
|
+
return value !== null && typeof value === 'object' && DERIVED_MARKER in value && value[DERIVED_MARKER] === true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { derived, isDerivedMarker };
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
class PathNotifier {
|
|
2
2
|
subscribers = new Map();
|
|
3
3
|
interceptors = new Map();
|
|
4
|
+
batchingEnabled = true;
|
|
5
|
+
pendingFlush = false;
|
|
6
|
+
pending = new Map();
|
|
7
|
+
firstValues = new Map();
|
|
8
|
+
flushCallbacks = new Set();
|
|
9
|
+
constructor(options) {
|
|
10
|
+
if (options && options.batching === false) this.batchingEnabled = false;
|
|
11
|
+
}
|
|
12
|
+
setBatchingEnabled(enabled) {
|
|
13
|
+
this.batchingEnabled = enabled;
|
|
14
|
+
}
|
|
15
|
+
isBatchingEnabled() {
|
|
16
|
+
return this.batchingEnabled;
|
|
17
|
+
}
|
|
4
18
|
subscribe(pattern, handler) {
|
|
5
19
|
if (!this.subscribers.has(pattern)) {
|
|
6
20
|
this.subscribers.set(pattern, new Set());
|
|
@@ -34,6 +48,26 @@ class PathNotifier {
|
|
|
34
48
|
};
|
|
35
49
|
}
|
|
36
50
|
notify(path, value, prev) {
|
|
51
|
+
if (!this.batchingEnabled) {
|
|
52
|
+
return this._runNotify(path, value, prev);
|
|
53
|
+
}
|
|
54
|
+
if (!this.pending.has(path)) {
|
|
55
|
+
this.firstValues.set(path, prev);
|
|
56
|
+
}
|
|
57
|
+
this.pending.set(path, {
|
|
58
|
+
newValue: value,
|
|
59
|
+
oldValue: this.firstValues.get(path)
|
|
60
|
+
});
|
|
61
|
+
if (!this.pendingFlush) {
|
|
62
|
+
this.pendingFlush = true;
|
|
63
|
+
queueMicrotask(() => this.flush());
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
blocked: false,
|
|
67
|
+
value
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
_runNotify(path, value, prev) {
|
|
37
71
|
let blocked = false;
|
|
38
72
|
let transformed = value;
|
|
39
73
|
for (const [pattern, interceptorSet] of this.interceptors) {
|
|
@@ -67,6 +101,41 @@ class PathNotifier {
|
|
|
67
101
|
value: transformed
|
|
68
102
|
};
|
|
69
103
|
}
|
|
104
|
+
flush() {
|
|
105
|
+
const toNotify = new Map(this.pending);
|
|
106
|
+
this.pending.clear();
|
|
107
|
+
this.firstValues.clear();
|
|
108
|
+
this.pendingFlush = false;
|
|
109
|
+
for (const [path, {
|
|
110
|
+
newValue,
|
|
111
|
+
oldValue
|
|
112
|
+
}] of toNotify) {
|
|
113
|
+
if (newValue === oldValue) continue;
|
|
114
|
+
const res = this._runNotify(path, newValue, oldValue);
|
|
115
|
+
if (res.blocked) ;
|
|
116
|
+
}
|
|
117
|
+
for (const cb of Array.from(this.flushCallbacks)) {
|
|
118
|
+
try {
|
|
119
|
+
cb();
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
flushSync() {
|
|
124
|
+
while (this.pending.size > 0 || this.pendingFlush) {
|
|
125
|
+
if (this.pendingFlush && this.pending.size === 0) {
|
|
126
|
+
this.pendingFlush = false;
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
this.flush();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
onFlush(callback) {
|
|
133
|
+
this.flushCallbacks.add(callback);
|
|
134
|
+
return () => this.flushCallbacks.delete(callback);
|
|
135
|
+
}
|
|
136
|
+
hasPending() {
|
|
137
|
+
return this.pending.size > 0;
|
|
138
|
+
}
|
|
70
139
|
matches(pattern, path) {
|
|
71
140
|
if (pattern === '**') return true;
|
|
72
141
|
if (pattern === path) return true;
|
|
@@ -79,6 +148,9 @@ class PathNotifier {
|
|
|
79
148
|
clear() {
|
|
80
149
|
this.subscribers.clear();
|
|
81
150
|
this.interceptors.clear();
|
|
151
|
+
this.pending.clear();
|
|
152
|
+
this.firstValues.clear();
|
|
153
|
+
this.pendingFlush = false;
|
|
82
154
|
}
|
|
83
155
|
getSubscriberCount() {
|
|
84
156
|
let count = 0;
|
package/dist/lib/signal-tree.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { signal, isSignal } from '@angular/core';
|
|
2
2
|
import { SIGNAL_TREE_MESSAGES, SIGNAL_TREE_CONSTANTS } from './constants.js';
|
|
3
|
+
import { applyDerivedFactories } from './internals/merge-derived.js';
|
|
3
4
|
import { SignalMemoryManager } from './memory/memory-manager.js';
|
|
5
|
+
import { getPathNotifier } from './path-notifier.js';
|
|
4
6
|
import { SecurityValidator } from './security/security-validator.js';
|
|
5
7
|
import { createLazySignalTree, unwrap } from './utils.js';
|
|
6
8
|
import { deepEqual } from '../deep-equal.js';
|
|
@@ -172,6 +174,9 @@ function create(initialState, config) {
|
|
|
172
174
|
const useLazy = shouldUseLazy(initialState, config, estimatedSize);
|
|
173
175
|
let signalState;
|
|
174
176
|
let memoryManager;
|
|
177
|
+
try {
|
|
178
|
+
getPathNotifier().setBatchingEnabled(Boolean(config.batchUpdates !== false));
|
|
179
|
+
} catch {}
|
|
175
180
|
if (useLazy && typeof initialState === 'object') {
|
|
176
181
|
try {
|
|
177
182
|
memoryManager = new SignalMemoryManager();
|
|
@@ -271,8 +276,112 @@ function create(initialState, config) {
|
|
|
271
276
|
}
|
|
272
277
|
return tree;
|
|
273
278
|
}
|
|
274
|
-
function signalTree(initialState,
|
|
275
|
-
|
|
279
|
+
function signalTree(initialState, configOrDerived) {
|
|
280
|
+
const isFactory = typeof configOrDerived === 'function';
|
|
281
|
+
const config = isFactory ? {} : configOrDerived ?? {};
|
|
282
|
+
const baseTree = create(initialState, config);
|
|
283
|
+
const builder = createBuilder(baseTree);
|
|
284
|
+
if (isFactory) {
|
|
285
|
+
return builder.derived(configOrDerived);
|
|
286
|
+
}
|
|
287
|
+
return builder;
|
|
288
|
+
}
|
|
289
|
+
function createBuilder(baseTree) {
|
|
290
|
+
const derivedQueue = [];
|
|
291
|
+
let isFinalized = false;
|
|
292
|
+
const finalize = () => {
|
|
293
|
+
if (isFinalized) return;
|
|
294
|
+
isFinalized = true;
|
|
295
|
+
if (derivedQueue.length > 0) {
|
|
296
|
+
applyDerivedFactories(baseTree.$, derivedQueue);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
const builder = function (arg) {
|
|
300
|
+
if (arguments.length === 0) {
|
|
301
|
+
return baseTree();
|
|
302
|
+
}
|
|
303
|
+
return baseTree(arg);
|
|
304
|
+
};
|
|
305
|
+
builder[NODE_ACCESSOR_SYMBOL] = true;
|
|
306
|
+
Object.defineProperty(builder, 'state', {
|
|
307
|
+
get() {
|
|
308
|
+
finalize();
|
|
309
|
+
return baseTree.state;
|
|
310
|
+
},
|
|
311
|
+
enumerable: false,
|
|
312
|
+
configurable: true
|
|
313
|
+
});
|
|
314
|
+
Object.defineProperty(builder, '$', {
|
|
315
|
+
get() {
|
|
316
|
+
finalize();
|
|
317
|
+
return baseTree.$;
|
|
318
|
+
},
|
|
319
|
+
enumerable: false,
|
|
320
|
+
configurable: true
|
|
321
|
+
});
|
|
322
|
+
Object.defineProperty(builder, 'with', {
|
|
323
|
+
value: function (enhancer) {
|
|
324
|
+
const enhanced = baseTree.with(enhancer);
|
|
325
|
+
const newBuilder = createBuilder(enhanced);
|
|
326
|
+
for (const key of Object.keys(enhanced)) {
|
|
327
|
+
if (key !== '$' && key !== 'state' && key !== 'with' && key !== 'bind' && key !== 'destroy' && key !== 'derived') {
|
|
328
|
+
try {
|
|
329
|
+
newBuilder[key] = enhanced[key];
|
|
330
|
+
} catch {}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
for (const factory of derivedQueue) {
|
|
334
|
+
newBuilder.derived(factory);
|
|
335
|
+
}
|
|
336
|
+
return newBuilder;
|
|
337
|
+
},
|
|
338
|
+
enumerable: false,
|
|
339
|
+
writable: false,
|
|
340
|
+
configurable: true
|
|
341
|
+
});
|
|
342
|
+
if (typeof baseTree.bind === 'function') {
|
|
343
|
+
Object.defineProperty(builder, 'bind', {
|
|
344
|
+
value: baseTree.bind.bind(baseTree),
|
|
345
|
+
enumerable: false,
|
|
346
|
+
writable: false,
|
|
347
|
+
configurable: true
|
|
348
|
+
});
|
|
349
|
+
} else {
|
|
350
|
+
Object.defineProperty(builder, 'bind', {
|
|
351
|
+
value: () => builder,
|
|
352
|
+
enumerable: false,
|
|
353
|
+
writable: false,
|
|
354
|
+
configurable: true
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (typeof baseTree.destroy === 'function') {
|
|
358
|
+
Object.defineProperty(builder, 'destroy', {
|
|
359
|
+
value: baseTree.destroy.bind(baseTree),
|
|
360
|
+
enumerable: false,
|
|
361
|
+
writable: true,
|
|
362
|
+
configurable: true
|
|
363
|
+
});
|
|
364
|
+
} else {
|
|
365
|
+
Object.defineProperty(builder, 'destroy', {
|
|
366
|
+
value: () => {},
|
|
367
|
+
enumerable: false,
|
|
368
|
+
writable: true,
|
|
369
|
+
configurable: true
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
Object.defineProperty(builder, 'derived', {
|
|
373
|
+
value: function (factory) {
|
|
374
|
+
if (isFinalized) {
|
|
375
|
+
throw new Error('SignalTree: Cannot add derived() after tree.$ has been accessed. ' + 'Chain all .derived() calls before accessing $.');
|
|
376
|
+
}
|
|
377
|
+
derivedQueue.push(factory);
|
|
378
|
+
return builder;
|
|
379
|
+
},
|
|
380
|
+
enumerable: false,
|
|
381
|
+
writable: false,
|
|
382
|
+
configurable: true
|
|
383
|
+
});
|
|
384
|
+
return builder;
|
|
276
385
|
}
|
|
277
386
|
|
|
278
387
|
export { isNodeAccessor, signalTree };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signaltree/core",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.0",
|
|
4
4
|
"description": "Lightweight, type-safe signal-based state management for Angular. Core package providing hierarchical signal trees, basic entity management, and async actions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
package/src/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export { signalTree } from './lib/signal-tree';
|
|
2
2
|
export type { ISignalTree, SignalTree, SignalTreeBase, FullSignalTree, ProdSignalTree, TreeNode, CallableWritableSignal, AccessibleNode, NodeAccessor, Primitive, NotFn, TreeConfig, TreePreset, Enhancer, EnhancerMeta, EnhancerWithMeta, EntitySignal, EntityMapMarker, EntityConfig, MutationOptions, AddOptions, AddManyOptions, TimeTravelEntry, TimeTravelMethods, } from './lib/types';
|
|
3
3
|
export { entityMap } from './lib/types';
|
|
4
|
+
export type { ProcessDerived, DeepMergeTree, DerivedFactory, } from './lib/internals/derived-types';
|
|
5
|
+
export type { SignalTreeBuilder } from './lib/internals/builder-types';
|
|
6
|
+
export { derived, isDerivedMarker, type DerivedMarker, type DerivedType, } from './lib/markers/derived';
|
|
4
7
|
export { equal, deepEqual, isNodeAccessor, isAnySignal, toWritableSignal, parsePath, composeEnhancers, isBuiltInObject, createLazySignalTree, } from './lib/utils';
|
|
5
8
|
export { createEditSession, type EditSession, type UndoRedoHistory, } from './lib/edit-session';
|
|
6
9
|
export { getPathNotifier } from './lib/path-notifier';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ProcessDerived } from './derived-types';
|
|
2
|
+
import type { ISignalTree, TreeNode } from '../types';
|
|
3
|
+
export interface SignalTreeBuilder<TSource, TAccum = TreeNode<TSource>> {
|
|
4
|
+
(): TSource;
|
|
5
|
+
(value: TSource): void;
|
|
6
|
+
(updater: (current: TSource) => TSource): void;
|
|
7
|
+
readonly $: TAccum;
|
|
8
|
+
readonly state: TAccum;
|
|
9
|
+
with<TAdded>(enhancer: (tree: ISignalTree<TSource>) => ISignalTree<TSource> & TAdded): SignalTreeBuilder<TSource, TAccum> & TAdded;
|
|
10
|
+
bind(thisArg?: unknown): (value?: TSource) => TSource | void;
|
|
11
|
+
destroy(): void;
|
|
12
|
+
derived<TDerived extends object>(factory: ($: TAccum) => TDerived): SignalTreeBuilder<TSource, TAccum & ProcessDerived<TDerived>>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Signal } from '@angular/core';
|
|
2
|
+
import type { DerivedMarker } from '../markers/derived';
|
|
3
|
+
import type { TreeNode } from '../types';
|
|
4
|
+
export type ProcessDerived<T> = T extends DerivedMarker<infer R> ? Signal<R> : T extends Signal<infer S> ? Signal<S> : T extends object ? {
|
|
5
|
+
[P in keyof T]: ProcessDerived<T[P]>;
|
|
6
|
+
} : never;
|
|
7
|
+
export type DeepMergeTree<TSource, TDerived> = {
|
|
8
|
+
[K in keyof TSource | keyof TDerived]: K extends keyof TSource ? K extends keyof TDerived ? TSource[K] extends object ? TDerived[K] extends object ? TDerived[K] extends DerivedMarker<infer R> ? Signal<R> : TSource[K] & DeepMergeTree<TSource[K], ProcessDerived<TDerived[K]>> : TSource[K] : ProcessDerived<TDerived[K]> : TSource[K] : K extends keyof TDerived ? ProcessDerived<TDerived[K]> : never;
|
|
9
|
+
};
|
|
10
|
+
export type DerivedFactory<TSource, TDerived> = ($: TreeNode<TSource>) => TDerived;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare const DERIVED_MARKER: unique symbol;
|
|
2
|
+
export interface DerivedMarker<T> {
|
|
3
|
+
readonly [DERIVED_MARKER]: true;
|
|
4
|
+
readonly factory: () => T;
|
|
5
|
+
readonly __type?: T;
|
|
6
|
+
}
|
|
7
|
+
export type DerivedType<T> = T extends DerivedMarker<infer R> ? R : never;
|
|
8
|
+
export declare function derived<T>(factory: () => T): DerivedMarker<T>;
|
|
9
|
+
export declare function isDerivedMarker(value: unknown): value is DerivedMarker<unknown>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { derived, isDerivedMarker, getDerivedMarkerSymbol, type DerivedMarker, type DerivedType, } from './derived';
|
package/src/lib/signal-tree.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { SignalTreeBuilder } from './internals/builder-types';
|
|
2
|
+
import { ProcessDerived } from './internals/derived-types';
|
|
3
|
+
import type { TreeNode, TreeConfig, NodeAccessor } from './types';
|
|
2
4
|
export declare function isNodeAccessor(value: unknown): value is NodeAccessor<unknown>;
|
|
3
|
-
export declare function signalTree<T extends object>(initialState: T,
|
|
5
|
+
export declare function signalTree<T extends object, TDerived extends object>(initialState: T, derivedFactory: ($: TreeNode<T>) => TDerived): SignalTreeBuilder<T, TreeNode<T> & ProcessDerived<TDerived>>;
|
|
6
|
+
export declare function signalTree<T extends object>(initialState: T, config?: TreeConfig): SignalTreeBuilder<T, TreeNode<T>>;
|