@signaltree/core 6.0.0 → 6.0.3
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 +76 -76
- package/dist/constants.js +6 -0
- package/dist/deep-equal.js +41 -0
- package/dist/enhancers/batching/batching.js +189 -0
- package/dist/enhancers/devtools/devtools.js +306 -0
- package/dist/enhancers/effects/effects.js +66 -0
- package/dist/enhancers/entities/entities.js +51 -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 +231 -0
- package/dist/enhancers/time-travel/utils.js +11 -0
- package/dist/index.js +19 -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/entity-signal.js +283 -0
- package/dist/lib/memory/memory-manager.js +164 -0
- package/dist/lib/path-notifier.js +106 -0
- package/dist/lib/presets.js +21 -0
- package/dist/lib/security/security-validator.js +121 -0
- package/dist/lib/signal-tree.js +277 -0
- package/dist/lib/types.js +9 -0
- package/dist/lib/utils.js +299 -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 +11 -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 +11 -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/index.d.ts +17 -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/entity-signal.d.ts +1 -0
- package/src/lib/memory/memory-manager.d.ts +30 -0
- package/src/lib/path-notifier.d.ts +4 -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 +3 -0
- package/src/lib/types.d.ts +301 -0
- package/src/lib/utils.d.ts +32 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
class SignalMemoryManager {
|
|
2
|
+
cache = new Map();
|
|
3
|
+
registry = null;
|
|
4
|
+
config;
|
|
5
|
+
stats = {
|
|
6
|
+
cleanedUpSignals: 0,
|
|
7
|
+
peakCachedSignals: 0,
|
|
8
|
+
manualDisposes: 0
|
|
9
|
+
};
|
|
10
|
+
constructor(config = {}) {
|
|
11
|
+
this.config = {
|
|
12
|
+
enableAutoCleanup: config.enableAutoCleanup ?? true,
|
|
13
|
+
debugMode: config.debugMode ?? false,
|
|
14
|
+
onCleanup: config.onCleanup ?? (() => {})
|
|
15
|
+
};
|
|
16
|
+
if (this.config.enableAutoCleanup && typeof FinalizationRegistry !== 'undefined') {
|
|
17
|
+
this.registry = new FinalizationRegistry(path => {
|
|
18
|
+
this.handleCleanup(path);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (this.config.debugMode) {
|
|
22
|
+
console.log('[SignalMemoryManager] Initialized', {
|
|
23
|
+
autoCleanup: this.config.enableAutoCleanup,
|
|
24
|
+
hasRegistry: !!this.registry
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
cacheSignal(path, signal) {
|
|
29
|
+
const ref = new WeakRef(signal);
|
|
30
|
+
const entry = {
|
|
31
|
+
ref,
|
|
32
|
+
path,
|
|
33
|
+
cachedAt: Date.now()
|
|
34
|
+
};
|
|
35
|
+
this.cache.set(path, entry);
|
|
36
|
+
if (this.registry) {
|
|
37
|
+
this.registry.register(signal, path, signal);
|
|
38
|
+
}
|
|
39
|
+
const currentSize = this.cache.size;
|
|
40
|
+
if (currentSize > this.stats.peakCachedSignals) {
|
|
41
|
+
this.stats.peakCachedSignals = currentSize;
|
|
42
|
+
}
|
|
43
|
+
if (this.config.debugMode) {
|
|
44
|
+
console.log(`[SignalMemoryManager] Cached signal: ${path}`, {
|
|
45
|
+
cacheSize: currentSize,
|
|
46
|
+
peak: this.stats.peakCachedSignals
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
getSignal(path) {
|
|
51
|
+
const entry = this.cache.get(path);
|
|
52
|
+
if (!entry) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const signal = entry.ref.deref();
|
|
56
|
+
if (!signal) {
|
|
57
|
+
this.cache.delete(path);
|
|
58
|
+
if (this.config.debugMode) {
|
|
59
|
+
console.log(`[SignalMemoryManager] Signal GC'd: ${path}`);
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return signal;
|
|
64
|
+
}
|
|
65
|
+
hasSignal(path) {
|
|
66
|
+
return this.cache.has(path);
|
|
67
|
+
}
|
|
68
|
+
removeSignal(path) {
|
|
69
|
+
const entry = this.cache.get(path);
|
|
70
|
+
if (!entry) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
const signal = entry.ref.deref();
|
|
74
|
+
if (signal && this.registry) {
|
|
75
|
+
this.registry.unregister(signal);
|
|
76
|
+
}
|
|
77
|
+
this.cache.delete(path);
|
|
78
|
+
if (this.config.debugMode) {
|
|
79
|
+
console.log(`[SignalMemoryManager] Removed signal: ${path}`);
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
handleCleanup(path) {
|
|
84
|
+
this.cache.delete(path);
|
|
85
|
+
this.stats.cleanedUpSignals++;
|
|
86
|
+
const currentStats = this.getStats();
|
|
87
|
+
if (this.config.debugMode) {
|
|
88
|
+
console.log(`[SignalMemoryManager] Auto cleanup: ${path}`, currentStats);
|
|
89
|
+
}
|
|
90
|
+
this.config.onCleanup(path, currentStats);
|
|
91
|
+
}
|
|
92
|
+
getStats() {
|
|
93
|
+
let validSignals = 0;
|
|
94
|
+
for (const [path, entry] of this.cache.entries()) {
|
|
95
|
+
if (entry.ref.deref()) {
|
|
96
|
+
validSignals++;
|
|
97
|
+
} else {
|
|
98
|
+
this.cache.delete(path);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const estimatedMemoryBytes = validSignals * 100;
|
|
102
|
+
return {
|
|
103
|
+
cachedSignals: validSignals,
|
|
104
|
+
cleanedUpSignals: this.stats.cleanedUpSignals,
|
|
105
|
+
peakCachedSignals: this.stats.peakCachedSignals,
|
|
106
|
+
manualDisposes: this.stats.manualDisposes,
|
|
107
|
+
estimatedMemoryBytes
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
dispose() {
|
|
111
|
+
if (this.config.debugMode) {
|
|
112
|
+
console.log('[SignalMemoryManager] Disposing', {
|
|
113
|
+
cachedSignals: this.cache.size
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (this.registry) {
|
|
117
|
+
for (const entry of this.cache.values()) {
|
|
118
|
+
const signal = entry.ref.deref();
|
|
119
|
+
if (signal) {
|
|
120
|
+
this.registry.unregister(signal);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
this.cache.clear();
|
|
125
|
+
this.stats.manualDisposes++;
|
|
126
|
+
if (this.config.debugMode) {
|
|
127
|
+
console.log('[SignalMemoryManager] Disposed', this.getStats());
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
getCachedPaths() {
|
|
131
|
+
const paths = [];
|
|
132
|
+
for (const [path, entry] of this.cache.entries()) {
|
|
133
|
+
if (entry.ref.deref()) {
|
|
134
|
+
paths.push(path);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return paths;
|
|
138
|
+
}
|
|
139
|
+
clearStale() {
|
|
140
|
+
let removed = 0;
|
|
141
|
+
for (const [path, entry] of this.cache.entries()) {
|
|
142
|
+
if (!entry.ref.deref()) {
|
|
143
|
+
this.cache.delete(path);
|
|
144
|
+
removed++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (this.config.debugMode && removed > 0) {
|
|
148
|
+
console.log(`[SignalMemoryManager] Cleared ${removed} stale entries`);
|
|
149
|
+
}
|
|
150
|
+
return removed;
|
|
151
|
+
}
|
|
152
|
+
resetStats() {
|
|
153
|
+
this.stats = {
|
|
154
|
+
cleanedUpSignals: 0,
|
|
155
|
+
peakCachedSignals: 0,
|
|
156
|
+
manualDisposes: 0
|
|
157
|
+
};
|
|
158
|
+
if (this.config.debugMode) {
|
|
159
|
+
console.log('[SignalMemoryManager] Stats reset');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export { SignalMemoryManager };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
class PathNotifier {
|
|
2
|
+
subscribers = new Map();
|
|
3
|
+
interceptors = new Map();
|
|
4
|
+
subscribe(pattern, handler) {
|
|
5
|
+
if (!this.subscribers.has(pattern)) {
|
|
6
|
+
this.subscribers.set(pattern, new Set());
|
|
7
|
+
}
|
|
8
|
+
const handlers = this.subscribers.get(pattern);
|
|
9
|
+
if (!handlers) {
|
|
10
|
+
return () => {};
|
|
11
|
+
}
|
|
12
|
+
handlers.add(handler);
|
|
13
|
+
return () => {
|
|
14
|
+
handlers.delete(handler);
|
|
15
|
+
if (handlers.size === 0) {
|
|
16
|
+
this.subscribers.delete(pattern);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
intercept(pattern, interceptor) {
|
|
21
|
+
if (!this.interceptors.has(pattern)) {
|
|
22
|
+
this.interceptors.set(pattern, new Set());
|
|
23
|
+
}
|
|
24
|
+
const interceptors = this.interceptors.get(pattern);
|
|
25
|
+
if (!interceptors) {
|
|
26
|
+
return () => {};
|
|
27
|
+
}
|
|
28
|
+
interceptors.add(interceptor);
|
|
29
|
+
return () => {
|
|
30
|
+
interceptors.delete(interceptor);
|
|
31
|
+
if (interceptors.size === 0) {
|
|
32
|
+
this.interceptors.delete(pattern);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
notify(path, value, prev) {
|
|
37
|
+
let blocked = false;
|
|
38
|
+
let transformed = value;
|
|
39
|
+
for (const [pattern, interceptorSet] of this.interceptors) {
|
|
40
|
+
if (this.matches(pattern, path)) {
|
|
41
|
+
for (const interceptor of interceptorSet) {
|
|
42
|
+
const result = interceptor(transformed, prev, path);
|
|
43
|
+
if (result.block) {
|
|
44
|
+
blocked = true;
|
|
45
|
+
}
|
|
46
|
+
if (result.transform !== undefined) {
|
|
47
|
+
transformed = result.transform;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (blocked) {
|
|
53
|
+
return {
|
|
54
|
+
blocked: true,
|
|
55
|
+
value: prev
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
for (const [pattern, handlers] of this.subscribers) {
|
|
59
|
+
if (this.matches(pattern, path)) {
|
|
60
|
+
for (const handler of handlers) {
|
|
61
|
+
handler(transformed, prev, path);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
blocked: false,
|
|
67
|
+
value: transformed
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
matches(pattern, path) {
|
|
71
|
+
if (pattern === '**') return true;
|
|
72
|
+
if (pattern === path) return true;
|
|
73
|
+
if (pattern.endsWith('.*')) {
|
|
74
|
+
const prefix = pattern.slice(0, -2);
|
|
75
|
+
return path.startsWith(prefix + '.');
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
clear() {
|
|
80
|
+
this.subscribers.clear();
|
|
81
|
+
this.interceptors.clear();
|
|
82
|
+
}
|
|
83
|
+
getSubscriberCount() {
|
|
84
|
+
let count = 0;
|
|
85
|
+
for (const handlers of this.subscribers.values()) {
|
|
86
|
+
count += handlers.size;
|
|
87
|
+
}
|
|
88
|
+
return count;
|
|
89
|
+
}
|
|
90
|
+
getInterceptorCount() {
|
|
91
|
+
let count = 0;
|
|
92
|
+
for (const interceptors of this.interceptors.values()) {
|
|
93
|
+
count += interceptors.size;
|
|
94
|
+
}
|
|
95
|
+
return count;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
let globalPathNotifier = null;
|
|
99
|
+
function getPathNotifier() {
|
|
100
|
+
if (!globalPathNotifier) {
|
|
101
|
+
globalPathNotifier = new PathNotifier();
|
|
102
|
+
}
|
|
103
|
+
return globalPathNotifier;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { PathNotifier, getPathNotifier };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { signalTree } from './signal-tree.js';
|
|
2
|
+
import { effects } from '../enhancers/effects/effects.js';
|
|
3
|
+
import { batching } from '../enhancers/batching/batching.js';
|
|
4
|
+
import { memoization } from '../enhancers/memoization/memoization.js';
|
|
5
|
+
import { entities } from '../enhancers/entities/entities.js';
|
|
6
|
+
import { timeTravel } from '../enhancers/time-travel/time-travel.js';
|
|
7
|
+
import { devTools } from '../enhancers/devtools/devtools.js';
|
|
8
|
+
|
|
9
|
+
function createDevTree(initialState, config = {}) {
|
|
10
|
+
if (arguments.length === 0) {
|
|
11
|
+
const enhancer = tree => tree.with(effects()).with(batching()).with(memoization()).with(entities()).with(timeTravel()).with(devTools());
|
|
12
|
+
return {
|
|
13
|
+
enhancer
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const base = signalTree(initialState, config);
|
|
17
|
+
const enhanced = base.with(effects()).with(batching(config.batching)).with(memoization(config.memoization)).with(entities()).with(timeTravel(config.timeTravel)).with(devTools(config.devTools));
|
|
18
|
+
return enhanced;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { createDevTree };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const DANGEROUS_KEYS = ['__proto__', 'constructor', 'prototype'];
|
|
2
|
+
const HTML_TAG_PATTERN = /<[^>]*>/g;
|
|
3
|
+
const DANGEROUS_HTML_PATTERNS = [/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, /on\w+\s*=/gi, /javascript:/gi, /<iframe\b/gi, /<object\b/gi, /<embed\b/gi];
|
|
4
|
+
class SecurityValidator {
|
|
5
|
+
config;
|
|
6
|
+
dangerousKeys;
|
|
7
|
+
constructor(config = {}) {
|
|
8
|
+
this.config = {
|
|
9
|
+
preventPrototypePollution: config.preventPrototypePollution ?? true,
|
|
10
|
+
preventXSS: config.preventXSS ?? false,
|
|
11
|
+
preventFunctions: config.preventFunctions ?? true,
|
|
12
|
+
customDangerousKeys: config.customDangerousKeys ?? [],
|
|
13
|
+
onSecurityEvent: config.onSecurityEvent ?? (() => {}),
|
|
14
|
+
sanitizationMode: config.sanitizationMode ?? 'strict'
|
|
15
|
+
};
|
|
16
|
+
this.dangerousKeys = new Set([...DANGEROUS_KEYS, ...this.config.customDangerousKeys]);
|
|
17
|
+
}
|
|
18
|
+
validateKey(key) {
|
|
19
|
+
if (!this.config.preventPrototypePollution) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (this.dangerousKeys.has(key)) {
|
|
23
|
+
const event = {
|
|
24
|
+
type: 'dangerous-key-blocked',
|
|
25
|
+
key,
|
|
26
|
+
reason: `Dangerous key "${key}" blocked to prevent prototype pollution`,
|
|
27
|
+
timestamp: Date.now()
|
|
28
|
+
};
|
|
29
|
+
this.config.onSecurityEvent(event);
|
|
30
|
+
throw new Error(`[SignalTree Security] Dangerous key "${key}" is not allowed. ` + `This key can lead to prototype pollution attacks. ` + `Blocked keys: ${Array.from(this.dangerousKeys).join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
validateValue(value) {
|
|
34
|
+
if (this.config.preventFunctions && typeof value === 'function') {
|
|
35
|
+
const event = {
|
|
36
|
+
type: 'function-value-blocked',
|
|
37
|
+
value,
|
|
38
|
+
reason: 'Function values are not allowed - state must be serializable',
|
|
39
|
+
timestamp: Date.now()
|
|
40
|
+
};
|
|
41
|
+
this.config.onSecurityEvent(event);
|
|
42
|
+
throw new Error(`[SignalTree Security] Function values are not allowed in state trees. ` + `Functions cannot be serialized, breaking features like time-travel, ` + `persistence, debugging, and SSR. ` + `\n\nTo fix this:` + `\n - Store function references outside the tree` + `\n - Use method names (strings) and a function registry` + `\n - Use computed signals for derived values` + `\n\nBlocked value: ${value.toString().substring(0, 100)}...`);
|
|
43
|
+
}
|
|
44
|
+
if (!this.config.preventXSS || typeof value !== 'string') {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
let hasDangerousPattern = false;
|
|
48
|
+
for (const pattern of DANGEROUS_HTML_PATTERNS) {
|
|
49
|
+
pattern.lastIndex = 0;
|
|
50
|
+
if (pattern.test(value)) {
|
|
51
|
+
hasDangerousPattern = true;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (hasDangerousPattern) {
|
|
56
|
+
const event = {
|
|
57
|
+
type: 'xss-attempt-blocked',
|
|
58
|
+
value,
|
|
59
|
+
reason: 'Dangerous HTML pattern detected and sanitized',
|
|
60
|
+
timestamp: Date.now()
|
|
61
|
+
};
|
|
62
|
+
this.config.onSecurityEvent(event);
|
|
63
|
+
return this.sanitize(value);
|
|
64
|
+
}
|
|
65
|
+
return this.sanitize(value);
|
|
66
|
+
}
|
|
67
|
+
sanitize(value) {
|
|
68
|
+
if (this.config.sanitizationMode === 'strict') {
|
|
69
|
+
let sanitized = value;
|
|
70
|
+
for (const pattern of DANGEROUS_HTML_PATTERNS) {
|
|
71
|
+
pattern.lastIndex = 0;
|
|
72
|
+
sanitized = sanitized.replace(pattern, '');
|
|
73
|
+
}
|
|
74
|
+
return sanitized.replace(HTML_TAG_PATTERN, '');
|
|
75
|
+
} else {
|
|
76
|
+
let sanitized = value;
|
|
77
|
+
for (const pattern of DANGEROUS_HTML_PATTERNS) {
|
|
78
|
+
pattern.lastIndex = 0;
|
|
79
|
+
sanitized = sanitized.replace(pattern, '');
|
|
80
|
+
}
|
|
81
|
+
return sanitized;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
validateKeyValue(key, value) {
|
|
85
|
+
this.validateKey(key);
|
|
86
|
+
return this.validateValue(value);
|
|
87
|
+
}
|
|
88
|
+
isDangerousKey(key) {
|
|
89
|
+
return this.dangerousKeys.has(key);
|
|
90
|
+
}
|
|
91
|
+
getConfig() {
|
|
92
|
+
return {
|
|
93
|
+
...this.config
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const SecurityPresets = {
|
|
98
|
+
strict: () => new SecurityValidator({
|
|
99
|
+
preventPrototypePollution: true,
|
|
100
|
+
preventXSS: true,
|
|
101
|
+
preventFunctions: true,
|
|
102
|
+
sanitizationMode: 'strict'
|
|
103
|
+
}),
|
|
104
|
+
standard: () => new SecurityValidator({
|
|
105
|
+
preventPrototypePollution: true,
|
|
106
|
+
preventXSS: false,
|
|
107
|
+
preventFunctions: true
|
|
108
|
+
}),
|
|
109
|
+
permissive: () => new SecurityValidator({
|
|
110
|
+
preventPrototypePollution: true,
|
|
111
|
+
preventXSS: false,
|
|
112
|
+
preventFunctions: false
|
|
113
|
+
}),
|
|
114
|
+
disabled: () => new SecurityValidator({
|
|
115
|
+
preventPrototypePollution: false,
|
|
116
|
+
preventXSS: false,
|
|
117
|
+
preventFunctions: false
|
|
118
|
+
})
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export { SecurityPresets, SecurityValidator };
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { signal, isSignal } from '@angular/core';
|
|
2
|
+
import { SIGNAL_TREE_MESSAGES, SIGNAL_TREE_CONSTANTS } from './constants.js';
|
|
3
|
+
import { SignalMemoryManager } from './memory/memory-manager.js';
|
|
4
|
+
import { SecurityValidator } from './security/security-validator.js';
|
|
5
|
+
import { createLazySignalTree, unwrap } from './utils.js';
|
|
6
|
+
import { deepEqual } from '../deep-equal.js';
|
|
7
|
+
import { isBuiltInObject } from '../is-built-in-object.js';
|
|
8
|
+
|
|
9
|
+
const NODE_ACCESSOR_SYMBOL = Symbol.for('SignalTree:NodeAccessor');
|
|
10
|
+
function isNodeAccessor(value) {
|
|
11
|
+
return typeof value === 'function' && value[NODE_ACCESSOR_SYMBOL] === true;
|
|
12
|
+
}
|
|
13
|
+
function isEntityMapMarker(value) {
|
|
14
|
+
return Boolean(value && typeof value === 'object' && value['__isEntityMap'] === true);
|
|
15
|
+
}
|
|
16
|
+
function createEqualityFn(useShallowComparison) {
|
|
17
|
+
return useShallowComparison ? Object.is : deepEqual;
|
|
18
|
+
}
|
|
19
|
+
function estimateObjectSize(obj, maxDepth = SIGNAL_TREE_CONSTANTS.ESTIMATE_MAX_DEPTH, currentDepth = 0) {
|
|
20
|
+
if (currentDepth >= maxDepth) return 1;
|
|
21
|
+
if (obj === null || obj === undefined) return 0;
|
|
22
|
+
if (typeof obj !== 'object') return 1;
|
|
23
|
+
let size = 0;
|
|
24
|
+
try {
|
|
25
|
+
if (Array.isArray(obj)) {
|
|
26
|
+
size = obj.length;
|
|
27
|
+
const sampleSize = Math.min(SIGNAL_TREE_CONSTANTS.ESTIMATE_SAMPLE_SIZE_ARRAY, obj.length);
|
|
28
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
29
|
+
size += estimateObjectSize(obj[i], maxDepth, currentDepth + 1) * 0.1;
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
const keys = Object.keys(obj);
|
|
33
|
+
size = keys.length;
|
|
34
|
+
const sampleSize = Math.min(SIGNAL_TREE_CONSTANTS.ESTIMATE_SAMPLE_SIZE_OBJECT, keys.length);
|
|
35
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
36
|
+
const value = obj[keys[i]];
|
|
37
|
+
size += estimateObjectSize(value, maxDepth, currentDepth + 1) * 0.5;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
return Math.floor(size);
|
|
44
|
+
}
|
|
45
|
+
function shouldUseLazy(obj, config, precomputedSize) {
|
|
46
|
+
if (config.useLazySignals !== undefined) return config.useLazySignals;
|
|
47
|
+
if (config.debugMode || config.enableDevTools) return false;
|
|
48
|
+
const estimatedSize = precomputedSize ?? estimateObjectSize(obj);
|
|
49
|
+
return estimatedSize > SIGNAL_TREE_CONSTANTS.LAZY_THRESHOLD;
|
|
50
|
+
}
|
|
51
|
+
function validateTree(obj, config) {
|
|
52
|
+
if (!config.security) return;
|
|
53
|
+
const validator = new SecurityValidator(config.security);
|
|
54
|
+
function validate(value, path) {
|
|
55
|
+
if (value === null || value === undefined) return;
|
|
56
|
+
if (typeof value !== 'object') {
|
|
57
|
+
validator.validateValue(value);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (isBuiltInObject(value)) return;
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
value.forEach((item, i) => validate(item, [...path, String(i)]));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const key of Object.keys(value)) {
|
|
66
|
+
try {
|
|
67
|
+
validator.validateKey(key);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
throw new Error(`${error.message}\nPath: ${[...path, key].join('.')}`);
|
|
70
|
+
}
|
|
71
|
+
const val = value[key];
|
|
72
|
+
try {
|
|
73
|
+
validator.validateValue(val);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new Error(`${error.message}\nPath: ${[...path, key].join('.')}`);
|
|
76
|
+
}
|
|
77
|
+
validate(val, [...path, key]);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
validate(obj, []);
|
|
81
|
+
}
|
|
82
|
+
function makeNodeAccessor(store) {
|
|
83
|
+
const accessor = function (arg) {
|
|
84
|
+
if (arguments.length === 0) {
|
|
85
|
+
return unwrap(store);
|
|
86
|
+
}
|
|
87
|
+
if (typeof arg === 'function') {
|
|
88
|
+
const updater = arg;
|
|
89
|
+
const current = unwrap(store);
|
|
90
|
+
recursiveUpdate(store, updater(current));
|
|
91
|
+
} else {
|
|
92
|
+
recursiveUpdate(store, arg);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
accessor[NODE_ACCESSOR_SYMBOL] = true;
|
|
96
|
+
for (const key of Object.keys(store)) {
|
|
97
|
+
Object.defineProperty(accessor, key, {
|
|
98
|
+
value: store[key],
|
|
99
|
+
enumerable: true,
|
|
100
|
+
configurable: true
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return accessor;
|
|
104
|
+
}
|
|
105
|
+
function recursiveUpdate(target, updates) {
|
|
106
|
+
if (!updates || typeof updates !== 'object') return;
|
|
107
|
+
const targetObj = isNodeAccessor(target) ? target : target;
|
|
108
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
109
|
+
const prop = targetObj[key];
|
|
110
|
+
if (prop === undefined) continue;
|
|
111
|
+
if (isSignal(prop) && 'set' in prop) {
|
|
112
|
+
prop.set(value);
|
|
113
|
+
} else if (isNodeAccessor(prop)) {
|
|
114
|
+
if (value && typeof value === 'object') {
|
|
115
|
+
recursiveUpdate(prop, value);
|
|
116
|
+
} else {
|
|
117
|
+
prop(value);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function createSignalStore(obj, equalityFn) {
|
|
123
|
+
if (obj === null || obj === undefined || typeof obj !== 'object') {
|
|
124
|
+
return signal(obj, {
|
|
125
|
+
equal: equalityFn
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (Array.isArray(obj)) {
|
|
129
|
+
return signal(obj, {
|
|
130
|
+
equal: equalityFn
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (isBuiltInObject(obj)) {
|
|
134
|
+
return signal(obj, {
|
|
135
|
+
equal: equalityFn
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const store = {};
|
|
139
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
140
|
+
if (isEntityMapMarker(value)) {
|
|
141
|
+
store[key] = value;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (isSignal(value)) {
|
|
145
|
+
store[key] = value;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (value === null || value === undefined || typeof value !== 'object') {
|
|
149
|
+
store[key] = signal(value, {
|
|
150
|
+
equal: equalityFn
|
|
151
|
+
});
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (Array.isArray(value) || isBuiltInObject(value)) {
|
|
155
|
+
store[key] = signal(value, {
|
|
156
|
+
equal: equalityFn
|
|
157
|
+
});
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const nested = createSignalStore(value, equalityFn);
|
|
161
|
+
store[key] = makeNodeAccessor(nested);
|
|
162
|
+
}
|
|
163
|
+
return store;
|
|
164
|
+
}
|
|
165
|
+
function create(initialState, config) {
|
|
166
|
+
if (initialState === null || initialState === undefined) {
|
|
167
|
+
throw new Error(SIGNAL_TREE_MESSAGES.NULL_OR_UNDEFINED);
|
|
168
|
+
}
|
|
169
|
+
validateTree(initialState, config);
|
|
170
|
+
const equalityFn = createEqualityFn(config.useShallowComparison ?? false);
|
|
171
|
+
const estimatedSize = estimateObjectSize(initialState);
|
|
172
|
+
const useLazy = shouldUseLazy(initialState, config, estimatedSize);
|
|
173
|
+
let signalState;
|
|
174
|
+
let memoryManager;
|
|
175
|
+
if (useLazy && typeof initialState === 'object') {
|
|
176
|
+
try {
|
|
177
|
+
memoryManager = new SignalMemoryManager();
|
|
178
|
+
signalState = createLazySignalTree(initialState, equalityFn, '', memoryManager);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.warn(SIGNAL_TREE_MESSAGES.LAZY_FALLBACK, error);
|
|
181
|
+
signalState = createSignalStore(initialState, equalityFn);
|
|
182
|
+
memoryManager = undefined;
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
signalState = createSignalStore(initialState, equalityFn);
|
|
186
|
+
}
|
|
187
|
+
const tree = function (arg) {
|
|
188
|
+
if (arguments.length === 0) {
|
|
189
|
+
return unwrap(signalState);
|
|
190
|
+
}
|
|
191
|
+
if (typeof arg === 'function') {
|
|
192
|
+
const updater = arg;
|
|
193
|
+
const current = unwrap(signalState);
|
|
194
|
+
recursiveUpdate(signalState, updater(current));
|
|
195
|
+
} else {
|
|
196
|
+
recursiveUpdate(signalState, arg);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
tree[NODE_ACCESSOR_SYMBOL] = true;
|
|
200
|
+
Object.defineProperty(tree, 'state', {
|
|
201
|
+
value: signalState,
|
|
202
|
+
enumerable: false,
|
|
203
|
+
writable: false
|
|
204
|
+
});
|
|
205
|
+
Object.defineProperty(tree, '$', {
|
|
206
|
+
value: signalState,
|
|
207
|
+
enumerable: false,
|
|
208
|
+
writable: false
|
|
209
|
+
});
|
|
210
|
+
Object.defineProperty(tree, 'with', {
|
|
211
|
+
value: function (enhancer) {
|
|
212
|
+
if (typeof enhancer !== 'function') {
|
|
213
|
+
throw new Error('Enhancer must be a function');
|
|
214
|
+
}
|
|
215
|
+
return enhancer(tree);
|
|
216
|
+
},
|
|
217
|
+
enumerable: false,
|
|
218
|
+
writable: false
|
|
219
|
+
});
|
|
220
|
+
Object.defineProperty(tree, 'bind', {
|
|
221
|
+
value: function (thisArg) {
|
|
222
|
+
return Function.prototype.bind.call(tree, thisArg);
|
|
223
|
+
},
|
|
224
|
+
enumerable: false,
|
|
225
|
+
writable: true,
|
|
226
|
+
configurable: true
|
|
227
|
+
});
|
|
228
|
+
Object.defineProperty(tree, 'destroy', {
|
|
229
|
+
value: function () {
|
|
230
|
+
if (memoryManager) {
|
|
231
|
+
memoryManager.dispose();
|
|
232
|
+
}
|
|
233
|
+
if (config.debugMode) {
|
|
234
|
+
console.log(SIGNAL_TREE_MESSAGES.TREE_DESTROYED);
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
enumerable: false,
|
|
238
|
+
writable: true,
|
|
239
|
+
configurable: true
|
|
240
|
+
});
|
|
241
|
+
Object.defineProperty(tree, 'clearCache', {
|
|
242
|
+
value: () => {},
|
|
243
|
+
enumerable: false,
|
|
244
|
+
writable: true,
|
|
245
|
+
configurable: true
|
|
246
|
+
});
|
|
247
|
+
Object.defineProperty(tree, 'batchUpdate', {
|
|
248
|
+
value: function (arg) {
|
|
249
|
+
if (arguments.length === 0) return;
|
|
250
|
+
if (typeof arg === 'function') {
|
|
251
|
+
const updater = arg;
|
|
252
|
+
const current = unwrap(signalState);
|
|
253
|
+
recursiveUpdate(signalState, updater(current));
|
|
254
|
+
} else {
|
|
255
|
+
recursiveUpdate(signalState, arg);
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
enumerable: false,
|
|
259
|
+
writable: true,
|
|
260
|
+
configurable: true
|
|
261
|
+
});
|
|
262
|
+
for (const key of Object.keys(signalState)) {
|
|
263
|
+
if (!(key in tree)) {
|
|
264
|
+
Object.defineProperty(tree, key, {
|
|
265
|
+
value: signalState[key],
|
|
266
|
+
enumerable: true,
|
|
267
|
+
configurable: true
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return tree;
|
|
272
|
+
}
|
|
273
|
+
function signalTree(initialState, config = {}) {
|
|
274
|
+
return create(initialState, config);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export { isNodeAccessor, signalTree };
|