@signaltree/core 6.2.1 → 6.2.2

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.
Files changed (82) hide show
  1. package/dist/constants.js +6 -0
  2. package/dist/deep-equal.js +41 -0
  3. package/dist/enhancers/batching/batching.js +230 -0
  4. package/dist/enhancers/devtools/devtools.js +318 -0
  5. package/dist/enhancers/effects/effects.js +66 -0
  6. package/dist/enhancers/entities/entities.js +51 -0
  7. package/dist/enhancers/index.js +72 -0
  8. package/dist/enhancers/memoization/memoization.js +420 -0
  9. package/dist/enhancers/presets/lib/presets.js +27 -0
  10. package/dist/enhancers/serialization/constants.js +15 -0
  11. package/dist/enhancers/serialization/serialization.js +656 -0
  12. package/dist/enhancers/time-travel/time-travel.js +247 -0
  13. package/dist/enhancers/time-travel/utils.js +11 -0
  14. package/dist/enhancers/utils/copy-tree-properties.js +20 -0
  15. package/dist/index.js +20 -0
  16. package/dist/is-built-in-object.js +23 -0
  17. package/dist/lib/async-helpers.js +77 -0
  18. package/dist/lib/constants.js +56 -0
  19. package/dist/lib/edit-session.js +84 -0
  20. package/dist/lib/entity-signal.js +283 -0
  21. package/dist/lib/memory/memory-manager.js +164 -0
  22. package/dist/lib/path-notifier.js +106 -0
  23. package/dist/lib/presets.js +21 -0
  24. package/dist/lib/security/security-validator.js +121 -0
  25. package/dist/lib/signal-tree.js +278 -0
  26. package/dist/lib/types.js +9 -0
  27. package/dist/lib/utils.js +264 -0
  28. package/dist/lru-cache.js +64 -0
  29. package/dist/parse-path.js +13 -0
  30. package/package.json +2 -2
  31. package/src/enhancers/batching/batching.d.ts +10 -0
  32. package/src/enhancers/batching/batching.types.d.ts +1 -0
  33. package/src/enhancers/batching/index.d.ts +1 -0
  34. package/src/enhancers/batching/test-setup.d.ts +3 -0
  35. package/src/enhancers/devtools/devtools.d.ts +68 -0
  36. package/src/enhancers/devtools/devtools.types.d.ts +1 -0
  37. package/src/enhancers/devtools/index.d.ts +1 -0
  38. package/src/enhancers/devtools/test-setup.d.ts +3 -0
  39. package/src/enhancers/effects/effects.d.ts +9 -0
  40. package/src/enhancers/effects/effects.types.d.ts +1 -0
  41. package/src/enhancers/effects/index.d.ts +1 -0
  42. package/src/enhancers/entities/entities.d.ts +11 -0
  43. package/src/enhancers/entities/entities.types.d.ts +1 -0
  44. package/src/enhancers/entities/index.d.ts +1 -0
  45. package/src/enhancers/entities/test-setup.d.ts +3 -0
  46. package/src/enhancers/index.d.ts +3 -0
  47. package/src/enhancers/memoization/index.d.ts +1 -0
  48. package/src/enhancers/memoization/memoization.d.ts +54 -0
  49. package/src/enhancers/memoization/memoization.types.d.ts +1 -0
  50. package/src/enhancers/memoization/test-setup.d.ts +3 -0
  51. package/src/enhancers/presets/index.d.ts +1 -0
  52. package/src/enhancers/presets/lib/presets.d.ts +8 -0
  53. package/src/enhancers/serialization/constants.d.ts +14 -0
  54. package/src/enhancers/serialization/index.d.ts +2 -0
  55. package/src/enhancers/serialization/serialization.d.ts +68 -0
  56. package/src/enhancers/serialization/test-setup.d.ts +3 -0
  57. package/src/enhancers/test-helpers/types-equals.d.ts +2 -0
  58. package/src/enhancers/time-travel/index.d.ts +1 -0
  59. package/src/enhancers/time-travel/test-setup.d.ts +3 -0
  60. package/src/enhancers/time-travel/time-travel.d.ts +10 -0
  61. package/src/enhancers/time-travel/time-travel.types.d.ts +1 -0
  62. package/src/enhancers/time-travel/utils.d.ts +2 -0
  63. package/src/enhancers/types.d.ts +1 -0
  64. package/src/enhancers/typing/helpers-types.d.ts +2 -0
  65. package/src/enhancers/utils/copy-tree-properties.d.ts +1 -0
  66. package/src/index.d.ts +19 -0
  67. package/src/lib/async-helpers.d.ts +8 -0
  68. package/src/lib/constants.d.ts +41 -0
  69. package/src/lib/dev-proxy.d.ts +3 -0
  70. package/src/lib/edit-session.d.ts +21 -0
  71. package/src/lib/entity-signal.d.ts +1 -0
  72. package/src/lib/memory/memory-manager.d.ts +30 -0
  73. package/src/lib/path-notifier.d.ts +4 -0
  74. package/src/lib/performance/diff-engine.d.ts +33 -0
  75. package/src/lib/performance/path-index.d.ts +25 -0
  76. package/src/lib/performance/update-engine.d.ts +32 -0
  77. package/src/lib/presets.d.ts +34 -0
  78. package/src/lib/security/security-validator.d.ts +33 -0
  79. package/src/lib/signal-tree.d.ts +3 -0
  80. package/src/lib/types.d.ts +301 -0
  81. package/src/lib/utils.d.ts +32 -0
  82. package/LICENSE +0 -54
@@ -0,0 +1,283 @@
1
+ import { signal, computed } from '@angular/core';
2
+
3
+ class EntitySignalImpl {
4
+ pathNotifier;
5
+ basePath;
6
+ storage = new Map();
7
+ allSignal;
8
+ countSignal;
9
+ idsSignal;
10
+ mapSignal;
11
+ nodeCache = new Map();
12
+ selectId;
13
+ tapHandlers = [];
14
+ interceptHandlers = [];
15
+ constructor(config, pathNotifier, basePath) {
16
+ this.pathNotifier = pathNotifier;
17
+ this.basePath = basePath;
18
+ this.selectId = config.selectId ?? (entity => entity['id']);
19
+ this.allSignal = signal([]);
20
+ this.countSignal = signal(0);
21
+ this.idsSignal = signal([]);
22
+ this.mapSignal = signal(new Map());
23
+ }
24
+ byId(id) {
25
+ const entity = this.storage.get(id);
26
+ if (!entity) return undefined;
27
+ return this.getOrCreateNode(id, entity);
28
+ }
29
+ byIdOrFail(id) {
30
+ const node = this.byId(id);
31
+ if (!node) {
32
+ throw new Error(`Entity with id ${String(id)} not found`);
33
+ }
34
+ return node;
35
+ }
36
+ get all() {
37
+ return this.allSignal;
38
+ }
39
+ get count() {
40
+ return this.countSignal;
41
+ }
42
+ get ids() {
43
+ return this.idsSignal;
44
+ }
45
+ get map() {
46
+ return this.mapSignal;
47
+ }
48
+ has(id) {
49
+ return computed(() => this.storage.has(id));
50
+ }
51
+ get isEmpty() {
52
+ return computed(() => this.storage.size === 0);
53
+ }
54
+ where(predicate) {
55
+ return computed(() => {
56
+ const result = [];
57
+ for (const entity of this.storage.values()) {
58
+ if (predicate(entity)) {
59
+ result.push(entity);
60
+ }
61
+ }
62
+ return result;
63
+ });
64
+ }
65
+ find(predicate) {
66
+ return computed(() => {
67
+ for (const entity of this.storage.values()) {
68
+ if (predicate(entity)) {
69
+ return entity;
70
+ }
71
+ }
72
+ return undefined;
73
+ });
74
+ }
75
+ addOne(entity, opts) {
76
+ const id = opts?.selectId?.(entity) ?? this.selectId(entity);
77
+ if (this.storage.has(id)) {
78
+ throw new Error(`Entity with id ${String(id)} already exists`);
79
+ }
80
+ let transformedEntity = entity;
81
+ for (const handler of this.interceptHandlers) {
82
+ const ctx = {
83
+ block: reason => {
84
+ throw new Error(`Cannot add entity: ${reason || 'blocked by interceptor'}`);
85
+ },
86
+ transform: value => {
87
+ transformedEntity = value;
88
+ },
89
+ blocked: false,
90
+ blockReason: undefined
91
+ };
92
+ handler.onAdd?.(entity, ctx);
93
+ }
94
+ this.storage.set(id, transformedEntity);
95
+ this.nodeCache.delete(id);
96
+ this.updateSignals();
97
+ this.pathNotifier.notify(`${this.basePath}.${String(id)}`, transformedEntity, undefined);
98
+ for (const handler of this.tapHandlers) {
99
+ handler.onAdd?.(transformedEntity, id);
100
+ }
101
+ return id;
102
+ }
103
+ addMany(entities, opts) {
104
+ const ids = [];
105
+ for (const entity of entities) {
106
+ ids.push(this.addOne(entity, opts));
107
+ }
108
+ return ids;
109
+ }
110
+ updateOne(id, changes) {
111
+ const entity = this.storage.get(id);
112
+ if (!entity) {
113
+ throw new Error(`Entity with id ${String(id)} not found`);
114
+ }
115
+ const prev = entity;
116
+ let transformedChanges = changes;
117
+ for (const handler of this.interceptHandlers) {
118
+ const ctx = {
119
+ block: reason => {
120
+ throw new Error(`Cannot update entity: ${reason || 'blocked by interceptor'}`);
121
+ },
122
+ transform: value => {
123
+ transformedChanges = value;
124
+ },
125
+ blocked: false,
126
+ blockReason: undefined
127
+ };
128
+ handler.onUpdate?.(id, changes, ctx);
129
+ }
130
+ const finalUpdated = {
131
+ ...entity,
132
+ ...transformedChanges
133
+ };
134
+ this.storage.set(id, finalUpdated);
135
+ this.nodeCache.delete(id);
136
+ this.updateSignals();
137
+ this.pathNotifier.notify(`${this.basePath}.${String(id)}`, finalUpdated, prev);
138
+ for (const handler of this.tapHandlers) {
139
+ handler.onUpdate?.(id, transformedChanges, finalUpdated);
140
+ }
141
+ }
142
+ updateMany(ids, changes) {
143
+ for (const id of ids) {
144
+ this.updateOne(id, changes);
145
+ }
146
+ }
147
+ updateWhere(predicate, changes) {
148
+ let count = 0;
149
+ for (const [id, entity] of this.storage) {
150
+ if (predicate(entity)) {
151
+ this.updateOne(id, changes);
152
+ count++;
153
+ }
154
+ }
155
+ return count;
156
+ }
157
+ removeOne(id) {
158
+ const entity = this.storage.get(id);
159
+ if (!entity) {
160
+ throw new Error(`Entity with id ${String(id)} not found`);
161
+ }
162
+ for (const handler of this.interceptHandlers) {
163
+ const ctx = {
164
+ block: reason => {
165
+ throw new Error(`Cannot remove entity: ${reason || 'blocked by interceptor'}`);
166
+ },
167
+ transform: () => {},
168
+ blocked: false,
169
+ blockReason: undefined
170
+ };
171
+ handler.onRemove?.(id, entity, ctx);
172
+ }
173
+ this.storage.delete(id);
174
+ this.nodeCache.delete(id);
175
+ this.updateSignals();
176
+ this.pathNotifier.notify(`${this.basePath}.${String(id)}`, undefined, entity);
177
+ for (const handler of this.tapHandlers) {
178
+ handler.onRemove?.(id, entity);
179
+ }
180
+ }
181
+ removeMany(ids) {
182
+ for (const id of ids) {
183
+ this.removeOne(id);
184
+ }
185
+ }
186
+ removeWhere(predicate) {
187
+ const idsToRemove = [];
188
+ for (const [id, entity] of this.storage) {
189
+ if (predicate(entity)) {
190
+ idsToRemove.push(id);
191
+ }
192
+ }
193
+ let count = 0;
194
+ for (const id of idsToRemove) {
195
+ this.removeOne(id);
196
+ count++;
197
+ }
198
+ return count;
199
+ }
200
+ upsertOne(entity, opts) {
201
+ const id = opts?.selectId?.(entity) ?? this.selectId(entity);
202
+ if (this.storage.has(id)) {
203
+ this.updateOne(id, entity);
204
+ } else {
205
+ this.addOne(entity, opts);
206
+ }
207
+ return id;
208
+ }
209
+ upsertMany(entities, opts) {
210
+ return entities.map(e => this.upsertOne(e, opts));
211
+ }
212
+ clear() {
213
+ this.storage.clear();
214
+ this.nodeCache.clear();
215
+ this.updateSignals();
216
+ }
217
+ removeAll() {
218
+ this.clear();
219
+ }
220
+ setAll(entities, opts) {
221
+ this.clear();
222
+ this.addMany(entities, opts);
223
+ }
224
+ tap(handlers) {
225
+ this.tapHandlers.push(handlers);
226
+ return () => {
227
+ const idx = this.tapHandlers.indexOf(handlers);
228
+ if (idx > -1) this.tapHandlers.splice(idx, 1);
229
+ };
230
+ }
231
+ intercept(handlers) {
232
+ this.interceptHandlers.push(handlers);
233
+ return () => {
234
+ const idx = this.interceptHandlers.indexOf(handlers);
235
+ if (idx > -1) this.interceptHandlers.splice(idx, 1);
236
+ };
237
+ }
238
+ updateSignals() {
239
+ const entities = Array.from(this.storage.values());
240
+ const ids = Array.from(this.storage.keys());
241
+ const map = new Map(this.storage);
242
+ this.allSignal.set(entities);
243
+ this.countSignal.set(entities.length);
244
+ this.idsSignal.set(ids);
245
+ this.mapSignal.set(map);
246
+ }
247
+ getOrCreateNode(id, entity) {
248
+ let node = this.nodeCache.get(id);
249
+ if (!node) {
250
+ node = this.createEntityNode(id, entity);
251
+ this.nodeCache.set(id, node);
252
+ }
253
+ return node;
254
+ }
255
+ createEntityNode(id, entity) {
256
+ const node = () => this.storage.get(id);
257
+ for (const key of Object.keys(entity)) {
258
+ Object.defineProperty(node, key, {
259
+ get: () => {
260
+ const current = this.storage.get(id);
261
+ const value = current?.[key];
262
+ return () => value;
263
+ },
264
+ enumerable: true,
265
+ configurable: true
266
+ });
267
+ }
268
+ return node;
269
+ }
270
+ }
271
+ function createEntitySignal(config, pathNotifier, basePath) {
272
+ const impl = new EntitySignalImpl(config, pathNotifier, basePath);
273
+ return new Proxy(impl, {
274
+ get: (target, prop) => {
275
+ if (typeof prop === 'string' && !isNaN(Number(prop))) {
276
+ return target.byId(Number(prop));
277
+ }
278
+ return target[prop];
279
+ }
280
+ });
281
+ }
282
+
283
+ export { EntitySignalImpl, createEntitySignal };
@@ -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 };