@signaltree/core 5.1.1 → 5.1.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 +116 -55
- package/dist/constants.js +6 -0
- package/dist/deep-clone.js +80 -0
- package/dist/deep-equal.js +41 -0
- package/dist/enhancers/batching/lib/batching.js +161 -0
- package/dist/enhancers/computed/lib/computed.js +21 -0
- package/dist/enhancers/devtools/lib/devtools.js +321 -0
- package/dist/enhancers/entities/lib/entities.js +93 -0
- package/dist/enhancers/index.js +72 -0
- package/dist/enhancers/memoization/lib/memoization.js +410 -0
- package/dist/enhancers/presets/lib/presets.js +87 -0
- package/dist/enhancers/serialization/constants.js +15 -0
- package/dist/enhancers/serialization/lib/serialization.js +662 -0
- package/dist/enhancers/time-travel/lib/time-travel.js +193 -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 +280 -0
- package/dist/lib/memory/memory-manager.js +164 -0
- package/dist/lib/path-notifier.js +106 -0
- package/dist/lib/performance/diff-engine.js +156 -0
- package/dist/lib/performance/path-index.js +156 -0
- package/dist/lib/performance/update-engine.js +188 -0
- package/dist/lib/security/security-validator.js +121 -0
- package/dist/lib/signal-tree.js +626 -0
- package/dist/lib/types.js +9 -0
- package/dist/lib/utils.js +261 -0
- package/dist/lru-cache.js +64 -0
- package/dist/parse-path.js +13 -0
- package/package.json +1 -1
- package/src/async-helpers.d.ts +8 -0
- package/src/batching.d.ts +16 -0
- package/src/computed.d.ts +12 -0
- package/src/constants.d.ts +14 -0
- package/src/devtools.d.ts +77 -0
- package/src/diff-engine.d.ts +33 -0
- package/src/enhancers/batching/index.d.ts +1 -0
- package/src/enhancers/batching/lib/batching.d.ts +16 -0
- package/src/enhancers/batching/test-setup.d.ts +3 -0
- package/src/enhancers/computed/index.d.ts +1 -0
- package/src/enhancers/computed/lib/computed.d.ts +12 -0
- package/src/enhancers/devtools/index.d.ts +1 -0
- package/src/enhancers/devtools/lib/devtools.d.ts +77 -0
- package/src/enhancers/devtools/test-setup.d.ts +3 -0
- package/src/enhancers/entities/index.d.ts +1 -0
- package/src/enhancers/entities/lib/entities.d.ts +20 -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/lib/memoization.d.ts +65 -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 +11 -0
- package/src/enhancers/presets/test-setup.d.ts +3 -0
- package/src/enhancers/serialization/constants.d.ts +14 -0
- package/src/enhancers/serialization/index.d.ts +2 -0
- package/src/enhancers/serialization/lib/serialization.d.ts +59 -0
- package/src/enhancers/serialization/test-setup.d.ts +3 -0
- package/src/enhancers/time-travel/index.d.ts +1 -0
- package/src/enhancers/time-travel/lib/time-travel.d.ts +36 -0
- package/src/enhancers/time-travel/lib/utils.d.ts +1 -0
- package/src/enhancers/time-travel/test-setup.d.ts +3 -0
- package/src/enhancers/types.d.ts +74 -0
- package/src/entities.d.ts +20 -0
- package/src/entity-signal.d.ts +1 -0
- package/src/index.d.ts +18 -0
- package/src/lib/async-helpers.d.ts +8 -0
- package/src/lib/constants.d.ts +41 -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/security/security-validator.d.ts +33 -0
- package/src/lib/signal-tree.d.ts +8 -0
- package/src/lib/types.d.ts +278 -0
- package/src/lib/utils.d.ts +28 -0
- package/src/memoization.d.ts +65 -0
- package/src/memory-manager.d.ts +30 -0
- package/src/path-index.d.ts +25 -0
- package/src/path-notifier.d.ts +12 -0
- package/src/presets.d.ts +11 -0
- package/src/security-validator.d.ts +33 -0
- package/src/serialization.d.ts +59 -0
- package/src/signal-tree.d.ts +8 -0
- package/src/test-setup.d.ts +3 -0
- package/src/time-travel.d.ts +36 -0
- package/src/types.d.ts +278 -0
- package/src/update-engine.d.ts +32 -0
- package/src/utils.d.ts +1 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { isSignal } from '@angular/core';
|
|
2
|
+
import { DiffEngine, ChangeType } from './diff-engine.js';
|
|
3
|
+
import { PathIndex } from './path-index.js';
|
|
4
|
+
|
|
5
|
+
class OptimizedUpdateEngine {
|
|
6
|
+
pathIndex;
|
|
7
|
+
diffEngine;
|
|
8
|
+
constructor(tree) {
|
|
9
|
+
this.pathIndex = new PathIndex();
|
|
10
|
+
this.diffEngine = new DiffEngine();
|
|
11
|
+
this.pathIndex.buildFromTree(tree);
|
|
12
|
+
}
|
|
13
|
+
update(tree, updates, options = {}) {
|
|
14
|
+
const startTime = performance.now();
|
|
15
|
+
const diffOptions = {};
|
|
16
|
+
if (options.maxDepth !== undefined) diffOptions.maxDepth = options.maxDepth;
|
|
17
|
+
if (options.ignoreArrayOrder !== undefined) diffOptions.ignoreArrayOrder = options.ignoreArrayOrder;
|
|
18
|
+
if (options.equalityFn !== undefined) diffOptions.equalityFn = options.equalityFn;
|
|
19
|
+
const diff = this.diffEngine.diff(tree, updates, diffOptions);
|
|
20
|
+
if (diff.changes.length === 0) {
|
|
21
|
+
return {
|
|
22
|
+
changed: false,
|
|
23
|
+
duration: performance.now() - startTime,
|
|
24
|
+
changedPaths: []
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const patches = this.createPatches(diff.changes);
|
|
28
|
+
const sortedPatches = this.sortPatches(patches);
|
|
29
|
+
const result = options.batch ? this.batchApplyPatches(tree, sortedPatches, options.batchSize) : this.applyPatches(tree, sortedPatches);
|
|
30
|
+
const duration = performance.now() - startTime;
|
|
31
|
+
return {
|
|
32
|
+
changed: true,
|
|
33
|
+
duration,
|
|
34
|
+
changedPaths: result.appliedPaths,
|
|
35
|
+
stats: {
|
|
36
|
+
totalPaths: diff.changes.length,
|
|
37
|
+
optimizedPaths: patches.length,
|
|
38
|
+
batchedUpdates: result.batchCount
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
rebuildIndex(tree) {
|
|
43
|
+
this.pathIndex.clear();
|
|
44
|
+
this.pathIndex.buildFromTree(tree);
|
|
45
|
+
}
|
|
46
|
+
getIndexStats() {
|
|
47
|
+
return this.pathIndex.getStats();
|
|
48
|
+
}
|
|
49
|
+
createPatches(changes) {
|
|
50
|
+
const patches = [];
|
|
51
|
+
const processedPaths = new Set();
|
|
52
|
+
for (const change of changes) {
|
|
53
|
+
const pathStr = change.path.join('.');
|
|
54
|
+
let skipPath = false;
|
|
55
|
+
for (const processed of processedPaths) {
|
|
56
|
+
if (pathStr.startsWith(processed + '.')) {
|
|
57
|
+
skipPath = true;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (skipPath) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const patch = this.createPatch(change);
|
|
65
|
+
patches.push(patch);
|
|
66
|
+
processedPaths.add(pathStr);
|
|
67
|
+
if (change.type === ChangeType.REPLACE && typeof change.value === 'object') {
|
|
68
|
+
processedPaths.add(pathStr);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return patches;
|
|
72
|
+
}
|
|
73
|
+
createPatch(change) {
|
|
74
|
+
return {
|
|
75
|
+
type: change.type,
|
|
76
|
+
path: change.path,
|
|
77
|
+
value: change.value,
|
|
78
|
+
oldValue: change.oldValue,
|
|
79
|
+
priority: this.calculatePriority(change),
|
|
80
|
+
signal: this.pathIndex.get(change.path)
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
calculatePriority(change) {
|
|
84
|
+
let priority = 0;
|
|
85
|
+
priority += (10 - change.path.length) * 10;
|
|
86
|
+
if (change.path.some(p => typeof p === 'number')) {
|
|
87
|
+
priority -= 20;
|
|
88
|
+
}
|
|
89
|
+
if (change.type === ChangeType.REPLACE) {
|
|
90
|
+
priority += 30;
|
|
91
|
+
}
|
|
92
|
+
return priority;
|
|
93
|
+
}
|
|
94
|
+
sortPatches(patches) {
|
|
95
|
+
return patches.sort((a, b) => {
|
|
96
|
+
if (a.priority !== b.priority) {
|
|
97
|
+
return b.priority - a.priority;
|
|
98
|
+
}
|
|
99
|
+
return a.path.length - b.path.length;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
applyPatches(tree, patches) {
|
|
103
|
+
const appliedPaths = [];
|
|
104
|
+
let updateCount = 0;
|
|
105
|
+
for (const patch of patches) {
|
|
106
|
+
if (this.applyPatch(patch, tree)) {
|
|
107
|
+
appliedPaths.push(patch.path.join('.'));
|
|
108
|
+
updateCount++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
appliedPaths,
|
|
113
|
+
updateCount,
|
|
114
|
+
batchCount: 1
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
batchApplyPatches(tree, patches, batchSize = 50) {
|
|
118
|
+
const batches = [];
|
|
119
|
+
for (let i = 0; i < patches.length; i += batchSize) {
|
|
120
|
+
batches.push(patches.slice(i, i + batchSize));
|
|
121
|
+
}
|
|
122
|
+
const appliedPaths = [];
|
|
123
|
+
let updateCount = 0;
|
|
124
|
+
for (const currentBatch of batches) {
|
|
125
|
+
for (const patch of currentBatch) {
|
|
126
|
+
if (this.applyPatch(patch, tree)) {
|
|
127
|
+
appliedPaths.push(patch.path.join('.'));
|
|
128
|
+
updateCount++;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
appliedPaths,
|
|
134
|
+
updateCount,
|
|
135
|
+
batchCount: batches.length
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
applyPatch(patch, tree) {
|
|
139
|
+
try {
|
|
140
|
+
if (patch.signal && isSignal(patch.signal) && 'set' in patch.signal) {
|
|
141
|
+
const currentValue = patch.signal();
|
|
142
|
+
if (this.isEqual(currentValue, patch.value)) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
patch.signal.set(patch.value);
|
|
146
|
+
if (patch.type === ChangeType.ADD && patch.value !== undefined) {
|
|
147
|
+
this.pathIndex.set(patch.path, patch.signal);
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
let current = tree;
|
|
152
|
+
for (let i = 0; i < patch.path.length - 1; i++) {
|
|
153
|
+
const key = patch.path[i];
|
|
154
|
+
current = current[key];
|
|
155
|
+
if (!current || typeof current !== 'object') {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const lastKey = patch.path[patch.path.length - 1];
|
|
160
|
+
if (this.isEqual(current[lastKey], patch.value)) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
current[lastKey] = patch.value;
|
|
164
|
+
return true;
|
|
165
|
+
} catch (error) {
|
|
166
|
+
console.error(`Failed to apply patch at ${patch.path.join('.')}:`, error);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
isEqual(a, b) {
|
|
171
|
+
if (a === b) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
if (typeof a !== typeof b) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
if (typeof a !== 'object' || a === null || b === null) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
182
|
+
} catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export { OptimizedUpdateEngine };
|
|
@@ -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 };
|