@signaltree/core 7.1.3 → 7.1.4
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 +28 -13
- package/package.json +2 -2
- package/dist/constants.js +0 -6
- package/dist/deep-equal.js +0 -41
- package/dist/enhancers/batching/batching.js +0 -230
- package/dist/enhancers/devtools/devtools.js +0 -318
- package/dist/enhancers/effects/effects.js +0 -66
- package/dist/enhancers/entities/entities.js +0 -7
- package/dist/enhancers/index.js +0 -72
- package/dist/enhancers/memoization/memoization.js +0 -420
- package/dist/enhancers/presets/lib/presets.js +0 -27
- package/dist/enhancers/serialization/constants.js +0 -15
- package/dist/enhancers/serialization/serialization.js +0 -656
- package/dist/enhancers/time-travel/time-travel.js +0 -283
- package/dist/enhancers/time-travel/utils.js +0 -11
- package/dist/enhancers/utils/copy-tree-properties.js +0 -20
- package/dist/index.js +0 -26
- package/dist/is-built-in-object.js +0 -23
- package/dist/lib/async-helpers.js +0 -77
- package/dist/lib/constants.js +0 -56
- package/dist/lib/edit-session.js +0 -84
- package/dist/lib/entity-signal.js +0 -544
- package/dist/lib/internals/batch-scope.js +0 -8
- package/dist/lib/internals/derived-types.js +0 -5
- package/dist/lib/internals/materialize-markers.js +0 -72
- package/dist/lib/internals/merge-derived.js +0 -59
- package/dist/lib/markers/derived.js +0 -6
- package/dist/lib/markers/entity-map.js +0 -20
- package/dist/lib/markers/status.js +0 -71
- package/dist/lib/markers/stored.js +0 -111
- package/dist/lib/memory/memory-manager.js +0 -164
- package/dist/lib/path-notifier.js +0 -178
- package/dist/lib/presets.js +0 -20
- package/dist/lib/security/security-validator.js +0 -121
- package/dist/lib/signal-tree.js +0 -415
- package/dist/lib/types.js +0 -3
- package/dist/lib/utils.js +0 -264
- package/dist/lru-cache.js +0 -64
- package/dist/parse-path.js +0 -13
- package/src/enhancers/batching/batching.d.ts +0 -10
- package/src/enhancers/batching/batching.types.d.ts +0 -1
- package/src/enhancers/batching/index.d.ts +0 -1
- package/src/enhancers/batching/test-setup.d.ts +0 -3
- package/src/enhancers/devtools/devtools.d.ts +0 -68
- package/src/enhancers/devtools/devtools.types.d.ts +0 -1
- package/src/enhancers/devtools/index.d.ts +0 -1
- package/src/enhancers/devtools/test-setup.d.ts +0 -3
- package/src/enhancers/effects/effects.d.ts +0 -9
- package/src/enhancers/effects/effects.types.d.ts +0 -1
- package/src/enhancers/effects/index.d.ts +0 -1
- package/src/enhancers/entities/entities.d.ts +0 -7
- package/src/enhancers/entities/entities.types.d.ts +0 -1
- package/src/enhancers/entities/index.d.ts +0 -1
- package/src/enhancers/entities/test-setup.d.ts +0 -3
- package/src/enhancers/index.d.ts +0 -3
- package/src/enhancers/memoization/index.d.ts +0 -1
- package/src/enhancers/memoization/memoization.d.ts +0 -54
- package/src/enhancers/memoization/memoization.types.d.ts +0 -1
- package/src/enhancers/memoization/test-setup.d.ts +0 -3
- package/src/enhancers/presets/index.d.ts +0 -1
- package/src/enhancers/presets/lib/presets.d.ts +0 -8
- package/src/enhancers/serialization/constants.d.ts +0 -14
- package/src/enhancers/serialization/index.d.ts +0 -2
- package/src/enhancers/serialization/serialization.d.ts +0 -68
- package/src/enhancers/serialization/test-setup.d.ts +0 -3
- package/src/enhancers/test-helpers/types-equals.d.ts +0 -2
- package/src/enhancers/time-travel/index.d.ts +0 -1
- package/src/enhancers/time-travel/test-setup.d.ts +0 -3
- package/src/enhancers/time-travel/time-travel.d.ts +0 -10
- package/src/enhancers/time-travel/time-travel.types.d.ts +0 -1
- package/src/enhancers/time-travel/utils.d.ts +0 -2
- package/src/enhancers/types.d.ts +0 -1
- package/src/enhancers/typing/helpers-types.d.ts +0 -2
- package/src/enhancers/utils/copy-tree-properties.d.ts +0 -1
- package/src/index.d.ts +0 -26
- package/src/lib/async-helpers.d.ts +0 -8
- package/src/lib/constants.d.ts +0 -41
- package/src/lib/dev-proxy.d.ts +0 -3
- package/src/lib/edit-session.d.ts +0 -21
- package/src/lib/entity-signal.d.ts +0 -1
- package/src/lib/internals/batch-scope.d.ts +0 -3
- package/src/lib/internals/builder-types.d.ts +0 -13
- package/src/lib/internals/derived-types.d.ts +0 -18
- package/src/lib/internals/materialize-markers.d.ts +0 -5
- package/src/lib/internals/merge-derived.d.ts +0 -4
- package/src/lib/markers/derived.d.ts +0 -9
- package/src/lib/markers/entity-map.d.ts +0 -4
- package/src/lib/markers/index.d.ts +0 -3
- package/src/lib/markers/status.d.ts +0 -32
- package/src/lib/markers/stored.d.ts +0 -23
- package/src/lib/memory/memory-manager.d.ts +0 -30
- package/src/lib/path-notifier.d.ts +0 -34
- package/src/lib/performance/diff-engine.d.ts +0 -33
- package/src/lib/performance/path-index.d.ts +0 -25
- package/src/lib/performance/update-engine.d.ts +0 -32
- package/src/lib/presets.d.ts +0 -34
- package/src/lib/security/security-validator.d.ts +0 -33
- package/src/lib/signal-tree.d.ts +0 -6
- package/src/lib/types.d.ts +0 -300
- package/src/lib/utils.d.ts +0 -25
package/README.md
CHANGED
|
@@ -2,19 +2,34 @@
|
|
|
2
2
|
<img src="https://raw.githubusercontent.com/JBorgia/signaltree/main/apps/demo/public/signaltree.svg" alt="SignalTree Logo" width="60" height="60" />
|
|
3
3
|
</div>
|
|
4
4
|
|
|
5
|
-
# SignalTree
|
|
5
|
+
# SignalTree: Reactive JSON
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**JSON branches, reactive leaves.**
|
|
8
|
+
|
|
9
|
+
> No actions. No reducers. No selectors.
|
|
8
10
|
|
|
9
11
|
## What is @signaltree/core?
|
|
10
12
|
|
|
11
|
-
SignalTree
|
|
13
|
+
SignalTree treats application state as **reactive JSON** — a typed, dot-notation interface to plain JSON-like objects with fine-grained reactivity layered transparently on top.
|
|
14
|
+
|
|
15
|
+
You don't model state as actions, reducers, selectors, or classes — you model it as **data**.
|
|
16
|
+
|
|
17
|
+
### Core Philosophy
|
|
18
|
+
|
|
19
|
+
| Principle | What It Means |
|
|
20
|
+
| ------------------------ | ---------------------------------------------------------------------------- |
|
|
21
|
+
| **State is Data** | Your state shape looks like JSON. No ceremony, no abstractions. |
|
|
22
|
+
| **Dot-Notation Access** | `tree.$.user.profile.name()` — fully type-safe, IDE-discoverable |
|
|
23
|
+
| **Invisible Reactivity** | You think in data paths, not subscriptions. Reactivity emerges naturally. |
|
|
24
|
+
| **Lazy by Design** | Signals created only where accessed. Types do heavy lifting at compile time. |
|
|
25
|
+
|
|
26
|
+
### Technical Features
|
|
12
27
|
|
|
13
28
|
- Recursive typing with deep nesting and accurate type inference
|
|
14
29
|
- Fast operations with sub‑millisecond measurements at 5–20+ levels
|
|
15
30
|
- Strong TypeScript safety across nested structures
|
|
16
31
|
- Memory efficiency via structural sharing and lazy signals
|
|
17
|
-
- Small API surface with
|
|
32
|
+
- Small API surface with minimal runtime overhead
|
|
18
33
|
- Compact bundle size suited for production
|
|
19
34
|
|
|
20
35
|
## Import guidance (tree-shaking)
|
|
@@ -936,7 +951,7 @@ For larger applications, you may want to organize derived tiers into separate fi
|
|
|
936
951
|
|
|
937
952
|
SignalTree provides two utilities for external derived functions:
|
|
938
953
|
|
|
939
|
-
- **`
|
|
954
|
+
- **`derivedFrom<TTree>()`** - Curried helper function that provides type context for your derived function
|
|
940
955
|
- **`WithDerived<TTree, TDerivedFn>`** - Type utility to build intermediate tree types
|
|
941
956
|
|
|
942
957
|
```typescript
|
|
@@ -967,11 +982,11 @@ export function createAppTree() {
|
|
|
967
982
|
```typescript
|
|
968
983
|
// derived/tier-entity-resolution.ts
|
|
969
984
|
import { computed } from '@angular/core';
|
|
970
|
-
import {
|
|
985
|
+
import { derivedFrom } from '@signaltree/core';
|
|
971
986
|
import type { AppTreeBase } from '../app-tree';
|
|
972
987
|
|
|
973
|
-
//
|
|
974
|
-
export const entityResolutionDerived =
|
|
988
|
+
// derivedFrom provides the type context for $ via curried syntax
|
|
989
|
+
export const entityResolutionDerived = derivedFrom<AppTreeBase>()(($) => ({
|
|
975
990
|
selectedUser: computed(() => {
|
|
976
991
|
const id = $.selectedUserId();
|
|
977
992
|
return id != null ? $.users.byId(id)?.() ?? null : null;
|
|
@@ -982,11 +997,11 @@ export const entityResolutionDerived = externalDerived<AppTreeBase>()(($) => ({
|
|
|
982
997
|
```typescript
|
|
983
998
|
// derived/tier-complex-logic.ts
|
|
984
999
|
import { computed } from '@angular/core';
|
|
985
|
-
import {
|
|
1000
|
+
import { derivedFrom } from '@signaltree/core';
|
|
986
1001
|
import type { AppTreeWithTier1 } from '../app-tree';
|
|
987
1002
|
|
|
988
1003
|
// This tier has access to $.selectedUser from Tier 1
|
|
989
|
-
export const complexLogicDerived =
|
|
1004
|
+
export const complexLogicDerived = derivedFrom<AppTreeWithTier1>()(($) => ({
|
|
990
1005
|
isAdmin: computed(() => $.selectedUser()?.role === 'admin'),
|
|
991
1006
|
displayName: computed(() => {
|
|
992
1007
|
const user = $.selectedUser();
|
|
@@ -1006,13 +1021,13 @@ export function myDerived($) {
|
|
|
1006
1021
|
return { foo: computed(() => $.bar()) }; // Error: $ has no properties
|
|
1007
1022
|
}
|
|
1008
1023
|
|
|
1009
|
-
// ✅
|
|
1010
|
-
export const myDerived =
|
|
1024
|
+
// ✅ derivedFrom provides the type context (curried syntax)
|
|
1025
|
+
export const myDerived = derivedFrom<AppTreeBase>()(($) => ({
|
|
1011
1026
|
foo: computed(() => $.bar()), // $ is properly typed
|
|
1012
1027
|
}));
|
|
1013
1028
|
```
|
|
1014
1029
|
|
|
1015
|
-
**Key point**: `
|
|
1030
|
+
**Key point**: `derivedFrom` is **only needed for functions defined in separate files**. Inline functions automatically inherit types from the chain. Note the curried syntax: `derivedFrom<TreeType>()(fn)` - this allows TypeScript to infer the return type while you specify the tree type.
|
|
1016
1031
|
|
|
1017
1032
|
## Error handling examples
|
|
1018
1033
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@signaltree/core",
|
|
3
|
-
"version": "7.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "7.1.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",
|
|
7
7
|
"sideEffects": false,
|
package/dist/constants.js
DELETED
package/dist/deep-equal.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
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 };
|
|
@@ -1,230 +0,0 @@
|
|
|
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 };
|
|
@@ -1,318 +0,0 @@
|
|
|
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 };
|