@signaltree/core 7.1.4 → 7.1.6
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/dist/constants.js +6 -0
- package/dist/deep-equal.js +41 -0
- package/dist/enhancers/batching/batching.js +230 -0
- package/dist/enhancers/devtools/devtools.js +318 -0
- package/dist/enhancers/effects/effects.js +66 -0
- package/dist/enhancers/entities/entities.js +7 -0
- package/dist/enhancers/index.js +72 -0
- package/dist/enhancers/memoization/memoization.js +420 -0
- package/dist/enhancers/presets/lib/presets.js +27 -0
- package/dist/enhancers/serialization/constants.js +15 -0
- package/dist/enhancers/serialization/serialization.js +656 -0
- package/dist/enhancers/time-travel/time-travel.js +283 -0
- package/dist/enhancers/time-travel/utils.js +11 -0
- package/dist/enhancers/utils/copy-tree-properties.js +20 -0
- package/dist/index.js +26 -0
- package/dist/is-built-in-object.js +23 -0
- package/dist/lib/async-helpers.js +77 -0
- package/dist/lib/constants.js +56 -0
- package/dist/lib/edit-session.js +84 -0
- package/dist/lib/entity-signal.js +544 -0
- package/dist/lib/internals/batch-scope.js +8 -0
- package/dist/lib/internals/derived-types.js +6 -0
- package/dist/lib/internals/materialize-markers.js +72 -0
- package/dist/lib/internals/merge-derived.js +59 -0
- package/dist/lib/markers/derived.js +6 -0
- package/dist/lib/markers/entity-map.js +20 -0
- package/dist/lib/markers/status.js +71 -0
- package/dist/lib/markers/stored.js +111 -0
- package/dist/lib/memory/memory-manager.js +164 -0
- package/dist/lib/path-notifier.js +178 -0
- package/dist/lib/presets.js +20 -0
- package/dist/lib/security/security-validator.js +121 -0
- package/dist/lib/signal-tree.js +415 -0
- package/dist/lib/types.js +3 -0
- package/dist/lib/utils.js +264 -0
- package/dist/lru-cache.js +64 -0
- package/dist/parse-path.js +13 -0
- package/package.json +1 -1
- package/src/enhancers/batching/batching.d.ts +10 -0
- package/src/enhancers/batching/batching.types.d.ts +1 -0
- package/src/enhancers/batching/index.d.ts +1 -0
- package/src/enhancers/batching/test-setup.d.ts +3 -0
- package/src/enhancers/devtools/devtools.d.ts +68 -0
- package/src/enhancers/devtools/devtools.types.d.ts +1 -0
- package/src/enhancers/devtools/index.d.ts +1 -0
- package/src/enhancers/devtools/test-setup.d.ts +3 -0
- package/src/enhancers/effects/effects.d.ts +9 -0
- package/src/enhancers/effects/effects.types.d.ts +1 -0
- package/src/enhancers/effects/index.d.ts +1 -0
- package/src/enhancers/entities/entities.d.ts +7 -0
- package/src/enhancers/entities/entities.types.d.ts +1 -0
- package/src/enhancers/entities/index.d.ts +1 -0
- package/src/enhancers/entities/test-setup.d.ts +3 -0
- package/src/enhancers/index.d.ts +3 -0
- package/src/enhancers/memoization/index.d.ts +1 -0
- package/src/enhancers/memoization/memoization.d.ts +54 -0
- package/src/enhancers/memoization/memoization.types.d.ts +1 -0
- package/src/enhancers/memoization/test-setup.d.ts +3 -0
- package/src/enhancers/presets/index.d.ts +1 -0
- package/src/enhancers/presets/lib/presets.d.ts +8 -0
- package/src/enhancers/serialization/constants.d.ts +14 -0
- package/src/enhancers/serialization/index.d.ts +2 -0
- package/src/enhancers/serialization/serialization.d.ts +68 -0
- package/src/enhancers/serialization/test-setup.d.ts +3 -0
- package/src/enhancers/test-helpers/types-equals.d.ts +2 -0
- package/src/enhancers/time-travel/index.d.ts +1 -0
- package/src/enhancers/time-travel/test-setup.d.ts +3 -0
- package/src/enhancers/time-travel/time-travel.d.ts +10 -0
- package/src/enhancers/time-travel/time-travel.types.d.ts +1 -0
- package/src/enhancers/time-travel/utils.d.ts +2 -0
- package/src/enhancers/types.d.ts +1 -0
- package/src/enhancers/typing/helpers-types.d.ts +2 -0
- package/src/enhancers/utils/copy-tree-properties.d.ts +1 -0
- package/src/index.d.ts +26 -0
- package/src/lib/async-helpers.d.ts +8 -0
- package/src/lib/constants.d.ts +41 -0
- package/src/lib/dev-proxy.d.ts +3 -0
- package/src/lib/edit-session.d.ts +21 -0
- package/src/lib/entity-signal.d.ts +1 -0
- package/src/lib/internals/batch-scope.d.ts +3 -0
- package/src/lib/internals/builder-types.d.ts +13 -0
- package/src/lib/internals/derived-types.d.ts +19 -0
- package/src/lib/internals/materialize-markers.d.ts +5 -0
- package/src/lib/internals/merge-derived.d.ts +4 -0
- package/src/lib/markers/derived.d.ts +9 -0
- package/src/lib/markers/entity-map.d.ts +4 -0
- package/src/lib/markers/index.d.ts +3 -0
- package/src/lib/markers/status.d.ts +32 -0
- package/src/lib/markers/stored.d.ts +23 -0
- package/src/lib/memory/memory-manager.d.ts +30 -0
- package/src/lib/path-notifier.d.ts +34 -0
- package/src/lib/performance/diff-engine.d.ts +33 -0
- package/src/lib/performance/path-index.d.ts +25 -0
- package/src/lib/performance/update-engine.d.ts +32 -0
- package/src/lib/presets.d.ts +34 -0
- package/src/lib/security/security-validator.d.ts +33 -0
- package/src/lib/signal-tree.d.ts +6 -0
- package/src/lib/types.d.ts +300 -0
- package/src/lib/utils.d.ts +25 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function deepEqual(a, b) {
|
|
2
|
+
if (a === b) return true;
|
|
3
|
+
if (a == null || b == null) return a === b;
|
|
4
|
+
const typeA = typeof a;
|
|
5
|
+
const typeB = typeof b;
|
|
6
|
+
if (typeA !== typeB) return false;
|
|
7
|
+
if (typeA !== 'object') return false;
|
|
8
|
+
if (a instanceof Date && b instanceof Date) {
|
|
9
|
+
return a.getTime() === b.getTime();
|
|
10
|
+
}
|
|
11
|
+
if (a instanceof RegExp && b instanceof RegExp) {
|
|
12
|
+
return a.source === b.source && a.flags === b.flags;
|
|
13
|
+
}
|
|
14
|
+
if (a instanceof Map && b instanceof Map) {
|
|
15
|
+
if (a.size !== b.size) return false;
|
|
16
|
+
for (const [key, value] of a) {
|
|
17
|
+
if (!b.has(key) || !deepEqual(value, b.get(key))) return false;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
if (a instanceof Set && b instanceof Set) {
|
|
22
|
+
if (a.size !== b.size) return false;
|
|
23
|
+
for (const value of a) {
|
|
24
|
+
if (!b.has(value)) return false;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(a)) {
|
|
29
|
+
if (!Array.isArray(b) || a.length !== b.length) return false;
|
|
30
|
+
return a.every((item, index) => deepEqual(item, b[index]));
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(b)) return false;
|
|
33
|
+
const objA = a;
|
|
34
|
+
const objB = b;
|
|
35
|
+
const keysA = Object.keys(objA);
|
|
36
|
+
const keysB = Object.keys(objB);
|
|
37
|
+
if (keysA.length !== keysB.length) return false;
|
|
38
|
+
return keysA.every(key => key in objB && deepEqual(objA[key], objB[key]));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { deepEqual };
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { copyTreeProperties } from '../utils/copy-tree-properties.js';
|
|
2
|
+
|
|
3
|
+
function batching(config = {}) {
|
|
4
|
+
const enabled = config.enabled ?? true;
|
|
5
|
+
const notificationDelayMs = config.notificationDelayMs ?? 0;
|
|
6
|
+
return tree => {
|
|
7
|
+
if (!enabled) {
|
|
8
|
+
const passthrough = {
|
|
9
|
+
batch: fn => fn(),
|
|
10
|
+
coalesce: fn => fn(),
|
|
11
|
+
hasPendingNotifications: () => false,
|
|
12
|
+
flushNotifications: () => {}
|
|
13
|
+
};
|
|
14
|
+
const enhanced = tree;
|
|
15
|
+
Object.assign(enhanced, passthrough);
|
|
16
|
+
enhanced.batchUpdate = updater => {
|
|
17
|
+
if (typeof tree.batchUpdate === 'function') {
|
|
18
|
+
tree.batchUpdate(updater);
|
|
19
|
+
} else {
|
|
20
|
+
updater(tree());
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
return enhanced;
|
|
24
|
+
}
|
|
25
|
+
let notificationPending = false;
|
|
26
|
+
let notificationTimeoutId;
|
|
27
|
+
let inBatch = false;
|
|
28
|
+
let inCoalesce = false;
|
|
29
|
+
const coalescedUpdates = new Map();
|
|
30
|
+
const scheduleNotification = () => {
|
|
31
|
+
if (notificationPending) return;
|
|
32
|
+
notificationPending = true;
|
|
33
|
+
if (notificationDelayMs > 0) {
|
|
34
|
+
notificationTimeoutId = setTimeout(flushNotificationsInternal, notificationDelayMs);
|
|
35
|
+
} else {
|
|
36
|
+
queueMicrotask(flushNotificationsInternal);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const flushNotificationsInternal = () => {
|
|
40
|
+
if (!notificationPending) return;
|
|
41
|
+
notificationPending = false;
|
|
42
|
+
if (notificationTimeoutId !== undefined) {
|
|
43
|
+
clearTimeout(notificationTimeoutId);
|
|
44
|
+
notificationTimeoutId = undefined;
|
|
45
|
+
}
|
|
46
|
+
if (tree.__notifyChangeDetection) {
|
|
47
|
+
tree.__notifyChangeDetection();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const flushCoalescedUpdates = () => {
|
|
51
|
+
const updates = Array.from(coalescedUpdates.values());
|
|
52
|
+
coalescedUpdates.clear();
|
|
53
|
+
updates.forEach(fn => {
|
|
54
|
+
try {
|
|
55
|
+
fn();
|
|
56
|
+
} catch (e) {
|
|
57
|
+
console.error('[SignalTree] Error in coalesced update:', e);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
const wrapSignalSetters = (node, path = '') => {
|
|
62
|
+
if (!node || typeof node !== 'object') return;
|
|
63
|
+
if (typeof node.set === 'function' && !node.__batchingWrapped) {
|
|
64
|
+
const originalSet = node.set.bind(node);
|
|
65
|
+
node.set = value => {
|
|
66
|
+
if (inCoalesce) {
|
|
67
|
+
coalescedUpdates.set(path, () => originalSet(value));
|
|
68
|
+
} else {
|
|
69
|
+
originalSet(value);
|
|
70
|
+
}
|
|
71
|
+
if (!inBatch) {
|
|
72
|
+
scheduleNotification();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
node.__batchingWrapped = true;
|
|
76
|
+
}
|
|
77
|
+
if (typeof node.update === 'function' && !node.__batchingUpdateWrapped) {
|
|
78
|
+
const originalUpdate = node.update.bind(node);
|
|
79
|
+
node.update = updater => {
|
|
80
|
+
if (inCoalesce) {
|
|
81
|
+
coalescedUpdates.set(`${path}:update:${Date.now()}`, () => originalUpdate(updater));
|
|
82
|
+
} else {
|
|
83
|
+
originalUpdate(updater);
|
|
84
|
+
}
|
|
85
|
+
if (!inBatch) {
|
|
86
|
+
scheduleNotification();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
node.__batchingUpdateWrapped = true;
|
|
90
|
+
}
|
|
91
|
+
for (const key of Object.keys(node)) {
|
|
92
|
+
if (key.startsWith('_') || key === 'set' || key === 'update') continue;
|
|
93
|
+
const child = node[key];
|
|
94
|
+
if (child && typeof child === 'object') {
|
|
95
|
+
wrapSignalSetters(child, path ? `${path}.${key}` : key);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
if (tree.$) {
|
|
100
|
+
wrapSignalSetters(tree.$);
|
|
101
|
+
}
|
|
102
|
+
const batchingMethods = {
|
|
103
|
+
batch(fn) {
|
|
104
|
+
const wasBatching = inBatch;
|
|
105
|
+
inBatch = true;
|
|
106
|
+
try {
|
|
107
|
+
fn();
|
|
108
|
+
} finally {
|
|
109
|
+
inBatch = wasBatching;
|
|
110
|
+
if (!inBatch) {
|
|
111
|
+
scheduleNotification();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
coalesce(fn) {
|
|
116
|
+
const wasCoalescing = inCoalesce;
|
|
117
|
+
const wasBatching = inBatch;
|
|
118
|
+
inCoalesce = true;
|
|
119
|
+
inBatch = true;
|
|
120
|
+
try {
|
|
121
|
+
fn();
|
|
122
|
+
} finally {
|
|
123
|
+
inCoalesce = wasCoalescing;
|
|
124
|
+
inBatch = wasBatching;
|
|
125
|
+
if (!wasCoalescing) {
|
|
126
|
+
flushCoalescedUpdates();
|
|
127
|
+
}
|
|
128
|
+
if (!inBatch) {
|
|
129
|
+
scheduleNotification();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
hasPendingNotifications() {
|
|
134
|
+
return notificationPending;
|
|
135
|
+
},
|
|
136
|
+
flushNotifications() {
|
|
137
|
+
flushNotificationsInternal();
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const originalTreeCall = tree.bind(tree);
|
|
141
|
+
const enhancedTree = function (...args) {
|
|
142
|
+
if (args.length === 0) {
|
|
143
|
+
return originalTreeCall();
|
|
144
|
+
} else {
|
|
145
|
+
if (args.length === 1) {
|
|
146
|
+
const arg = args[0];
|
|
147
|
+
if (typeof arg === 'function') {
|
|
148
|
+
originalTreeCall(arg);
|
|
149
|
+
} else {
|
|
150
|
+
originalTreeCall(arg);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!inBatch) {
|
|
154
|
+
scheduleNotification();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
Object.setPrototypeOf(enhancedTree, Object.getPrototypeOf(tree));
|
|
159
|
+
Object.assign(enhancedTree, tree);
|
|
160
|
+
try {
|
|
161
|
+
copyTreeProperties(tree, enhancedTree);
|
|
162
|
+
} catch {}
|
|
163
|
+
Object.defineProperty(enhancedTree, 'with', {
|
|
164
|
+
value: function (enhancer) {
|
|
165
|
+
if (typeof enhancer !== 'function') {
|
|
166
|
+
throw new Error('Enhancer must be a function');
|
|
167
|
+
}
|
|
168
|
+
return enhancer(enhancedTree);
|
|
169
|
+
},
|
|
170
|
+
writable: false,
|
|
171
|
+
enumerable: false,
|
|
172
|
+
configurable: true
|
|
173
|
+
});
|
|
174
|
+
if ('state' in tree) {
|
|
175
|
+
Object.defineProperty(enhancedTree, 'state', {
|
|
176
|
+
value: tree.state,
|
|
177
|
+
enumerable: false,
|
|
178
|
+
configurable: true
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
if ('$' in tree) {
|
|
182
|
+
Object.defineProperty(enhancedTree, '$', {
|
|
183
|
+
value: tree.$,
|
|
184
|
+
enumerable: false,
|
|
185
|
+
configurable: true
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
Object.assign(enhancedTree, batchingMethods);
|
|
189
|
+
enhancedTree.batchUpdate = updater => {
|
|
190
|
+
enhancedTree.batch(() => {
|
|
191
|
+
const current = originalTreeCall();
|
|
192
|
+
const updates = updater(current);
|
|
193
|
+
Object.entries(updates).forEach(([key, value]) => {
|
|
194
|
+
const property = enhancedTree.state[key];
|
|
195
|
+
if (property && typeof property.set === 'function') {
|
|
196
|
+
property.set(value);
|
|
197
|
+
} else if (typeof property === 'function') {
|
|
198
|
+
property(value);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
return enhancedTree;
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function highPerformanceBatching() {
|
|
207
|
+
return batching({
|
|
208
|
+
enabled: true,
|
|
209
|
+
notificationDelayMs: 0
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
function batchingWithConfig(config = {}) {
|
|
213
|
+
return batching(config);
|
|
214
|
+
}
|
|
215
|
+
function flushBatchedUpdates() {
|
|
216
|
+
console.warn('[SignalTree] flushBatchedUpdates() is deprecated. Use tree.flushNotifications() instead.');
|
|
217
|
+
}
|
|
218
|
+
function hasPendingUpdates() {
|
|
219
|
+
console.warn('[SignalTree] hasPendingUpdates() is deprecated. Use tree.hasPendingNotifications() instead.');
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
function getBatchQueueSize() {
|
|
223
|
+
console.warn('[SignalTree] getBatchQueueSize() is deprecated. Signal writes are now synchronous.');
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
Object.assign((config = {}) => batching(config), {
|
|
227
|
+
highPerformance: highPerformanceBatching
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
export { batching, batchingWithConfig, flushBatchedUpdates, getBatchQueueSize, hasPendingUpdates, highPerformanceBatching };
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { signal } from '@angular/core';
|
|
2
|
+
import { copyTreeProperties } from '../utils/copy-tree-properties.js';
|
|
3
|
+
|
|
4
|
+
function createActivityTracker() {
|
|
5
|
+
const modules = new Map();
|
|
6
|
+
return {
|
|
7
|
+
trackMethodCall: (module, method, duration) => {
|
|
8
|
+
const existing = modules.get(module);
|
|
9
|
+
if (existing) {
|
|
10
|
+
existing.lastActivity = new Date();
|
|
11
|
+
existing.operationCount++;
|
|
12
|
+
existing.averageExecutionTime = (existing.averageExecutionTime * (existing.operationCount - 1) + duration) / existing.operationCount;
|
|
13
|
+
} else {
|
|
14
|
+
modules.set(module, {
|
|
15
|
+
name: module,
|
|
16
|
+
methods: [method],
|
|
17
|
+
addedAt: new Date(),
|
|
18
|
+
lastActivity: new Date(),
|
|
19
|
+
operationCount: 1,
|
|
20
|
+
averageExecutionTime: duration,
|
|
21
|
+
errorCount: 0
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
trackError: (module, error, context) => {
|
|
26
|
+
const existing = modules.get(module);
|
|
27
|
+
if (existing) {
|
|
28
|
+
existing.errorCount++;
|
|
29
|
+
}
|
|
30
|
+
console.error(`❌ [${module}] Error${context ? ` in ${context}` : ''}:`, error);
|
|
31
|
+
},
|
|
32
|
+
getModuleActivity: module => modules.get(module),
|
|
33
|
+
getAllModules: () => Array.from(modules.values())
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function createCompositionLogger() {
|
|
37
|
+
const logs = [];
|
|
38
|
+
const addLog = (module, type, data) => {
|
|
39
|
+
logs.push({
|
|
40
|
+
timestamp: new Date(),
|
|
41
|
+
module,
|
|
42
|
+
type,
|
|
43
|
+
data
|
|
44
|
+
});
|
|
45
|
+
if (logs.length > 1000) {
|
|
46
|
+
logs.splice(0, logs.length - 1000);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
logComposition: (modules, action) => {
|
|
51
|
+
addLog('core', 'composition', {
|
|
52
|
+
modules,
|
|
53
|
+
action
|
|
54
|
+
});
|
|
55
|
+
console.log(`🔗 Composition ${action}:`, modules.join(' → '));
|
|
56
|
+
},
|
|
57
|
+
logMethodExecution: (module, method, args, result) => {
|
|
58
|
+
addLog(module, 'method', {
|
|
59
|
+
method,
|
|
60
|
+
args,
|
|
61
|
+
result
|
|
62
|
+
});
|
|
63
|
+
console.debug(`🔧 [${module}] ${method}`, {
|
|
64
|
+
args,
|
|
65
|
+
result
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
logStateChange: (module, path, oldValue, newValue) => {
|
|
69
|
+
addLog(module, 'state', {
|
|
70
|
+
path,
|
|
71
|
+
oldValue,
|
|
72
|
+
newValue
|
|
73
|
+
});
|
|
74
|
+
console.debug(`📝 [${module}] State change at ${path}:`, {
|
|
75
|
+
from: oldValue,
|
|
76
|
+
to: newValue
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
logPerformanceWarning: (module, operation, duration, threshold) => {
|
|
80
|
+
addLog(module, 'performance', {
|
|
81
|
+
operation,
|
|
82
|
+
duration,
|
|
83
|
+
threshold
|
|
84
|
+
});
|
|
85
|
+
console.warn(`⚠️ [${module}] Slow ${operation}: ${duration.toFixed(2)}ms (threshold: ${threshold}ms)`);
|
|
86
|
+
},
|
|
87
|
+
exportLogs: () => [...logs]
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function createNoopLogger() {
|
|
91
|
+
return {
|
|
92
|
+
logComposition: () => {},
|
|
93
|
+
logMethodExecution: () => {},
|
|
94
|
+
logStateChange: () => {},
|
|
95
|
+
logPerformanceWarning: () => {},
|
|
96
|
+
exportLogs: () => []
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function createModularMetrics() {
|
|
100
|
+
const metricsSignal = signal({
|
|
101
|
+
totalUpdates: 0,
|
|
102
|
+
moduleUpdates: {},
|
|
103
|
+
modulePerformance: {},
|
|
104
|
+
compositionChain: [],
|
|
105
|
+
signalGrowth: {},
|
|
106
|
+
memoryDelta: {},
|
|
107
|
+
moduleCacheStats: {}
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
signal: metricsSignal.asReadonly(),
|
|
111
|
+
updateMetrics: updates => {
|
|
112
|
+
metricsSignal.update(current => ({
|
|
113
|
+
...current,
|
|
114
|
+
...updates
|
|
115
|
+
}));
|
|
116
|
+
},
|
|
117
|
+
trackModuleUpdate: (module, duration) => {
|
|
118
|
+
metricsSignal.update(current => ({
|
|
119
|
+
...current,
|
|
120
|
+
totalUpdates: current.totalUpdates + 1,
|
|
121
|
+
moduleUpdates: {
|
|
122
|
+
...current.moduleUpdates,
|
|
123
|
+
[module]: (current.moduleUpdates[module] || 0) + 1
|
|
124
|
+
},
|
|
125
|
+
modulePerformance: {
|
|
126
|
+
...current.modulePerformance,
|
|
127
|
+
[module]: duration
|
|
128
|
+
}
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function devTools(config = {}) {
|
|
134
|
+
const {
|
|
135
|
+
enabled = true,
|
|
136
|
+
treeName = 'SignalTree',
|
|
137
|
+
name,
|
|
138
|
+
enableBrowserDevTools = true,
|
|
139
|
+
enableLogging = true,
|
|
140
|
+
performanceThreshold = 16
|
|
141
|
+
} = config;
|
|
142
|
+
const displayName = name ?? treeName;
|
|
143
|
+
return tree => {
|
|
144
|
+
if (!enabled) {
|
|
145
|
+
const noopMethods = {
|
|
146
|
+
connectDevTools() {},
|
|
147
|
+
disconnectDevTools() {}
|
|
148
|
+
};
|
|
149
|
+
return Object.assign(tree, noopMethods);
|
|
150
|
+
}
|
|
151
|
+
const activityTracker = createActivityTracker();
|
|
152
|
+
const logger = enableLogging ? createCompositionLogger() : createNoopLogger();
|
|
153
|
+
const metrics = createModularMetrics();
|
|
154
|
+
const compositionHistory = [];
|
|
155
|
+
const activeProfiles = new Map();
|
|
156
|
+
let browserDevTools = null;
|
|
157
|
+
const initBrowserDevTools = () => {
|
|
158
|
+
if (!enableBrowserDevTools || typeof window === 'undefined' || !('__REDUX_DEVTOOLS_EXTENSION__' in window)) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const devToolsExt = window['__REDUX_DEVTOOLS_EXTENSION__'];
|
|
163
|
+
const connection = devToolsExt.connect({
|
|
164
|
+
name: displayName,
|
|
165
|
+
features: {
|
|
166
|
+
dispatch: true,
|
|
167
|
+
jump: true,
|
|
168
|
+
skip: true
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
browserDevTools = {
|
|
172
|
+
send: connection.send
|
|
173
|
+
};
|
|
174
|
+
browserDevTools.send('@@INIT', tree());
|
|
175
|
+
console.log(`🔗 Connected to Redux DevTools as "${displayName}"`);
|
|
176
|
+
} catch (e) {
|
|
177
|
+
console.warn('[SignalTree] Failed to connect to Redux DevTools:', e);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const originalTreeCall = tree.bind(tree);
|
|
181
|
+
const enhancedTree = function (...args) {
|
|
182
|
+
if (args.length === 0) {
|
|
183
|
+
return originalTreeCall();
|
|
184
|
+
}
|
|
185
|
+
const startTime = performance.now();
|
|
186
|
+
let result;
|
|
187
|
+
if (args.length === 1) {
|
|
188
|
+
const arg = args[0];
|
|
189
|
+
if (typeof arg === 'function') {
|
|
190
|
+
result = originalTreeCall(arg);
|
|
191
|
+
} else {
|
|
192
|
+
result = originalTreeCall(arg);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const duration = performance.now() - startTime;
|
|
196
|
+
const newState = originalTreeCall();
|
|
197
|
+
metrics.trackModuleUpdate('core', duration);
|
|
198
|
+
if (duration > performanceThreshold) {
|
|
199
|
+
logger.logPerformanceWarning('core', 'update', duration, performanceThreshold);
|
|
200
|
+
}
|
|
201
|
+
if (browserDevTools) {
|
|
202
|
+
browserDevTools.send('UPDATE', newState);
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
};
|
|
206
|
+
Object.setPrototypeOf(enhancedTree, Object.getPrototypeOf(tree));
|
|
207
|
+
copyTreeProperties(tree, enhancedTree);
|
|
208
|
+
Object.defineProperty(enhancedTree, 'with', {
|
|
209
|
+
value: function (enhancer) {
|
|
210
|
+
if (typeof enhancer !== 'function') {
|
|
211
|
+
throw new Error('Enhancer must be a function');
|
|
212
|
+
}
|
|
213
|
+
return enhancer(enhancedTree);
|
|
214
|
+
},
|
|
215
|
+
writable: false,
|
|
216
|
+
enumerable: false,
|
|
217
|
+
configurable: true
|
|
218
|
+
});
|
|
219
|
+
if ('state' in tree) {
|
|
220
|
+
Object.defineProperty(enhancedTree, 'state', {
|
|
221
|
+
value: tree.state,
|
|
222
|
+
enumerable: false,
|
|
223
|
+
configurable: true
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if ('$' in tree) {
|
|
227
|
+
Object.defineProperty(enhancedTree, '$', {
|
|
228
|
+
value: tree.$,
|
|
229
|
+
enumerable: false,
|
|
230
|
+
configurable: true
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
const devToolsInterface = {
|
|
234
|
+
activityTracker,
|
|
235
|
+
logger,
|
|
236
|
+
metrics: metrics.signal,
|
|
237
|
+
trackComposition: modules => {
|
|
238
|
+
compositionHistory.push({
|
|
239
|
+
timestamp: new Date(),
|
|
240
|
+
chain: [...modules]
|
|
241
|
+
});
|
|
242
|
+
metrics.updateMetrics({
|
|
243
|
+
compositionChain: modules
|
|
244
|
+
});
|
|
245
|
+
logger.logComposition(modules, 'with');
|
|
246
|
+
},
|
|
247
|
+
startModuleProfiling: module => {
|
|
248
|
+
const profileId = `${module}_${Date.now()}`;
|
|
249
|
+
activeProfiles.set(profileId, {
|
|
250
|
+
module,
|
|
251
|
+
operation: 'profile',
|
|
252
|
+
startTime: performance.now()
|
|
253
|
+
});
|
|
254
|
+
return profileId;
|
|
255
|
+
},
|
|
256
|
+
endModuleProfiling: profileId => {
|
|
257
|
+
const profile = activeProfiles.get(profileId);
|
|
258
|
+
if (profile) {
|
|
259
|
+
const duration = performance.now() - profile.startTime;
|
|
260
|
+
activityTracker.trackMethodCall(profile.module, profile.operation, duration);
|
|
261
|
+
activeProfiles.delete(profileId);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
connectDevTools: name => {
|
|
265
|
+
if (browserDevTools) {
|
|
266
|
+
browserDevTools.send('@@INIT', originalTreeCall());
|
|
267
|
+
console.log(`🔗 Connected to Redux DevTools as "${name}"`);
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
exportDebugSession: () => ({
|
|
271
|
+
metrics: metrics.signal(),
|
|
272
|
+
modules: activityTracker.getAllModules(),
|
|
273
|
+
logs: logger.exportLogs(),
|
|
274
|
+
compositionHistory: [...compositionHistory]
|
|
275
|
+
})
|
|
276
|
+
};
|
|
277
|
+
const methods = {
|
|
278
|
+
connectDevTools() {
|
|
279
|
+
initBrowserDevTools();
|
|
280
|
+
},
|
|
281
|
+
disconnectDevTools() {
|
|
282
|
+
browserDevTools = null;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
enhancedTree['__devTools'] = devToolsInterface;
|
|
286
|
+
return Object.assign(enhancedTree, methods);
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function enableDevTools(treeName = 'SignalTree') {
|
|
290
|
+
return devTools({
|
|
291
|
+
treeName,
|
|
292
|
+
enabled: true
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
function fullDevTools(treeName = 'SignalTree') {
|
|
296
|
+
return devTools({
|
|
297
|
+
treeName,
|
|
298
|
+
enabled: true,
|
|
299
|
+
enableBrowserDevTools: true,
|
|
300
|
+
enableLogging: true,
|
|
301
|
+
performanceThreshold: 10
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function productionDevTools() {
|
|
305
|
+
return devTools({
|
|
306
|
+
enabled: true,
|
|
307
|
+
enableBrowserDevTools: false,
|
|
308
|
+
enableLogging: false,
|
|
309
|
+
performanceThreshold: 50
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
Object.assign(devTools, {
|
|
313
|
+
production: productionDevTools,
|
|
314
|
+
full: fullDevTools,
|
|
315
|
+
enable: enableDevTools
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
export { devTools, enableDevTools, fullDevTools, productionDevTools };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { effect, untracked } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
function effects(config = {}) {
|
|
4
|
+
const {
|
|
5
|
+
enabled = true
|
|
6
|
+
} = config;
|
|
7
|
+
return tree => {
|
|
8
|
+
const cleanupFns = [];
|
|
9
|
+
const methods = {
|
|
10
|
+
effect(effectFn) {
|
|
11
|
+
if (!enabled) {
|
|
12
|
+
return () => {};
|
|
13
|
+
}
|
|
14
|
+
let innerCleanup;
|
|
15
|
+
const effectRef = effect(() => {
|
|
16
|
+
const state = tree();
|
|
17
|
+
if (innerCleanup) {
|
|
18
|
+
untracked(() => innerCleanup());
|
|
19
|
+
}
|
|
20
|
+
innerCleanup = untracked(() => effectFn(state));
|
|
21
|
+
});
|
|
22
|
+
const cleanup = () => {
|
|
23
|
+
if (innerCleanup) {
|
|
24
|
+
innerCleanup();
|
|
25
|
+
}
|
|
26
|
+
effectRef.destroy();
|
|
27
|
+
};
|
|
28
|
+
cleanupFns.push(cleanup);
|
|
29
|
+
return cleanup;
|
|
30
|
+
},
|
|
31
|
+
subscribe(fn) {
|
|
32
|
+
if (!enabled) {
|
|
33
|
+
return () => {};
|
|
34
|
+
}
|
|
35
|
+
const effectRef = effect(() => {
|
|
36
|
+
const state = tree();
|
|
37
|
+
untracked(() => fn(state));
|
|
38
|
+
});
|
|
39
|
+
const cleanup = () => {
|
|
40
|
+
effectRef.destroy();
|
|
41
|
+
};
|
|
42
|
+
cleanupFns.push(cleanup);
|
|
43
|
+
return cleanup;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const originalDestroy = tree.destroy?.bind(tree);
|
|
47
|
+
tree.destroy = () => {
|
|
48
|
+
cleanupFns.forEach(fn => fn());
|
|
49
|
+
cleanupFns.length = 0;
|
|
50
|
+
if (originalDestroy) {
|
|
51
|
+
originalDestroy();
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
return Object.assign(tree, methods);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function enableEffects() {
|
|
58
|
+
return effects({
|
|
59
|
+
enabled: true
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
Object.assign((config = {}) => effects(config), {
|
|
63
|
+
enable: enableEffects
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export { effects, enableEffects };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
function entities(config = {}) {
|
|
2
|
+
throw new Error('entities() has been removed. Remove `.with(entities())` from your code; v7+ auto-processes EntityMap markers.');
|
|
3
|
+
}
|
|
4
|
+
const enableEntities = entities;
|
|
5
|
+
const highPerformanceEntities = entities;
|
|
6
|
+
|
|
7
|
+
export { enableEntities, entities, highPerformanceEntities };
|