@signaltree/core 4.0.12 → 4.0.15
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 +312 -0
- package/dist/enhancers/batching/index.d.ts +1 -0
- package/dist/enhancers/batching/index.js +1 -0
- package/dist/enhancers/batching/jest.config.d.ts +15 -0
- package/dist/enhancers/batching/jest.config.js +21 -0
- package/dist/enhancers/batching/lib/batching.d.ts +16 -0
- package/dist/enhancers/batching/lib/batching.js +155 -0
- package/dist/enhancers/batching/test-setup.d.ts +1 -0
- package/dist/enhancers/batching/test-setup.js +5 -0
- package/dist/enhancers/computed/index.d.ts +1 -0
- package/dist/enhancers/computed/index.js +1 -0
- package/dist/enhancers/computed/jest.config.d.ts +15 -0
- package/dist/enhancers/computed/jest.config.js +21 -0
- package/dist/enhancers/computed/lib/computed.d.ts +12 -0
- package/dist/enhancers/computed/lib/computed.js +19 -0
- package/dist/enhancers/devtools/index.d.ts +1 -0
- package/dist/enhancers/devtools/index.js +1 -0
- package/dist/enhancers/devtools/jest.config.d.ts +15 -0
- package/dist/enhancers/devtools/jest.config.js +21 -0
- package/dist/enhancers/devtools/lib/devtools.d.ts +77 -0
- package/dist/enhancers/devtools/lib/devtools.js +278 -0
- package/dist/enhancers/devtools/test-setup.d.ts +1 -0
- package/dist/enhancers/devtools/test-setup.js +5 -0
- package/dist/enhancers/entities/index.d.ts +1 -0
- package/dist/enhancers/entities/index.js +1 -0
- package/dist/enhancers/entities/jest.config.d.ts +15 -0
- package/dist/enhancers/entities/jest.config.js +21 -0
- package/dist/enhancers/entities/lib/entities.d.ts +22 -0
- package/dist/enhancers/entities/lib/entities.js +110 -0
- package/dist/enhancers/entities/test-setup.d.ts +1 -0
- package/dist/enhancers/entities/test-setup.js +5 -0
- package/dist/enhancers/index.d.ts +3 -0
- package/dist/enhancers/index.js +84 -0
- package/dist/enhancers/memoization/index.d.ts +1 -0
- package/dist/enhancers/memoization/index.js +1 -0
- package/dist/enhancers/memoization/jest.config.d.ts +15 -0
- package/dist/enhancers/memoization/jest.config.js +21 -0
- package/dist/enhancers/memoization/lib/memoization.d.ts +65 -0
- package/dist/enhancers/memoization/lib/memoization.js +391 -0
- package/dist/enhancers/memoization/test-setup.d.ts +1 -0
- package/dist/enhancers/memoization/test-setup.js +5 -0
- package/dist/enhancers/middleware/index.d.ts +2 -0
- package/dist/enhancers/middleware/index.js +2 -0
- package/dist/enhancers/middleware/jest.config.d.ts +15 -0
- package/dist/enhancers/middleware/jest.config.js +21 -0
- package/dist/enhancers/middleware/lib/async-helpers.d.ts +8 -0
- package/dist/enhancers/middleware/lib/async-helpers.js +85 -0
- package/dist/enhancers/middleware/lib/middleware.d.ts +11 -0
- package/dist/enhancers/middleware/lib/middleware.js +179 -0
- package/dist/enhancers/middleware/test-setup.d.ts +1 -0
- package/dist/enhancers/middleware/test-setup.js +5 -0
- package/dist/enhancers/presets/index.d.ts +1 -0
- package/dist/enhancers/presets/index.js +1 -0
- package/dist/enhancers/presets/jest.config.d.ts +15 -0
- package/dist/enhancers/presets/jest.config.js +21 -0
- package/dist/enhancers/presets/lib/presets.d.ts +11 -0
- package/dist/enhancers/presets/lib/presets.js +77 -0
- package/dist/enhancers/presets/test-setup.d.ts +1 -0
- package/dist/enhancers/presets/test-setup.js +5 -0
- package/dist/enhancers/serialization/constants.d.ts +14 -0
- package/dist/enhancers/serialization/constants.js +14 -0
- package/dist/enhancers/serialization/index.d.ts +2 -0
- package/dist/enhancers/serialization/index.js +2 -0
- package/dist/enhancers/serialization/jest.config.d.ts +15 -0
- package/dist/enhancers/serialization/jest.config.js +21 -0
- package/dist/enhancers/serialization/lib/serialization.d.ts +59 -0
- package/dist/enhancers/serialization/lib/serialization.js +668 -0
- package/dist/enhancers/serialization/test-setup.d.ts +1 -0
- package/dist/enhancers/serialization/test-setup.js +5 -0
- package/dist/enhancers/time-travel/index.d.ts +1 -0
- package/dist/enhancers/time-travel/index.js +1 -0
- package/dist/enhancers/time-travel/jest.config.d.ts +15 -0
- package/dist/enhancers/time-travel/jest.config.js +21 -0
- package/dist/enhancers/time-travel/lib/time-travel.d.ts +36 -0
- package/dist/enhancers/time-travel/lib/time-travel.js +192 -0
- package/dist/enhancers/time-travel/lib/utils.d.ts +1 -0
- package/dist/enhancers/time-travel/lib/utils.js +1 -0
- package/dist/enhancers/time-travel/test-setup.d.ts +1 -0
- package/dist/enhancers/time-travel/test-setup.js +5 -0
- package/dist/enhancers/types.d.ts +105 -0
- package/dist/enhancers/types.js +0 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +16 -0
- package/dist/lib/constants.d.ts +42 -0
- package/dist/lib/constants.js +61 -0
- package/dist/lib/memory/memory-manager.d.ts +30 -0
- package/dist/lib/memory/memory-manager.js +166 -0
- package/dist/lib/performance/diff-engine.d.ts +33 -0
- package/dist/lib/performance/diff-engine.js +156 -0
- package/dist/lib/performance/path-index.d.ts +25 -0
- package/dist/lib/performance/path-index.js +154 -0
- package/dist/lib/performance/update-engine.d.ts +32 -0
- package/dist/lib/performance/update-engine.js +193 -0
- package/dist/lib/security/security-validator.d.ts +33 -0
- package/dist/lib/security/security-validator.js +139 -0
- package/dist/lib/signal-tree.d.ts +8 -0
- package/dist/lib/signal-tree.js +665 -0
- package/dist/lib/types.d.ts +164 -0
- package/dist/lib/types.js +9 -0
- package/dist/lib/utils.d.ts +27 -0
- package/dist/lib/utils.js +286 -0
- package/package.json +2 -11
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
displayName: 'memoization',
|
|
3
|
+
preset: '../../../jest.preset.js',
|
|
4
|
+
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
|
5
|
+
coverageDirectory: '../../../coverage/packages/core/enhancers/memoization',
|
|
6
|
+
transform: {
|
|
7
|
+
'^.+\\.(ts|mjs|js|html)$': [
|
|
8
|
+
'jest-preset-angular',
|
|
9
|
+
{
|
|
10
|
+
tsconfig: '<rootDir>/tsconfig.spec.json',
|
|
11
|
+
stringifyContentPathRegex: '\\.(html|svg)$',
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
|
16
|
+
snapshotSerializers: [
|
|
17
|
+
'jest-preset-angular/build/serializers/no-ng-attributes',
|
|
18
|
+
'jest-preset-angular/build/serializers/ng-snapshot',
|
|
19
|
+
'jest-preset-angular/build/serializers/html-comment',
|
|
20
|
+
],
|
|
21
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { SignalTree } from '../../../lib/types';
|
|
2
|
+
export interface MemoizedSignalTree<T> extends SignalTree<T> {
|
|
3
|
+
memoizedUpdate: (updater: (current: T) => Partial<T>, cacheKey?: string) => void;
|
|
4
|
+
clearMemoCache: (key?: string) => void;
|
|
5
|
+
getCacheStats: () => {
|
|
6
|
+
size: number;
|
|
7
|
+
hitRate: number;
|
|
8
|
+
totalHits: number;
|
|
9
|
+
totalMisses: number;
|
|
10
|
+
keys: string[];
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare function cleanupMemoizationCache(): void;
|
|
14
|
+
export interface MemoizationConfig {
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
maxCacheSize?: number;
|
|
17
|
+
ttl?: number;
|
|
18
|
+
equality?: 'deep' | 'shallow' | 'reference';
|
|
19
|
+
enableLRU?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function memoize<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn, keyFn?: (...args: TArgs) => string, config?: MemoizationConfig): (...args: TArgs) => TReturn;
|
|
22
|
+
export declare function memoizeShallow<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn, keyFn?: (...args: TArgs) => string): (...args: TArgs) => TReturn;
|
|
23
|
+
export declare function memoizeReference<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => TReturn, keyFn?: (...args: TArgs) => string): (...args: TArgs) => TReturn;
|
|
24
|
+
export declare const MEMOIZATION_PRESETS: {
|
|
25
|
+
readonly selector: {
|
|
26
|
+
readonly equality: "reference";
|
|
27
|
+
readonly maxCacheSize: 10;
|
|
28
|
+
readonly enableLRU: false;
|
|
29
|
+
readonly ttl: undefined;
|
|
30
|
+
};
|
|
31
|
+
readonly computed: {
|
|
32
|
+
readonly equality: "shallow";
|
|
33
|
+
readonly maxCacheSize: 100;
|
|
34
|
+
readonly enableLRU: false;
|
|
35
|
+
readonly ttl: undefined;
|
|
36
|
+
};
|
|
37
|
+
readonly deepState: {
|
|
38
|
+
readonly equality: "deep";
|
|
39
|
+
readonly maxCacheSize: 1000;
|
|
40
|
+
readonly enableLRU: true;
|
|
41
|
+
readonly ttl: number;
|
|
42
|
+
};
|
|
43
|
+
readonly highFrequency: {
|
|
44
|
+
readonly equality: "reference";
|
|
45
|
+
readonly maxCacheSize: 5;
|
|
46
|
+
readonly enableLRU: false;
|
|
47
|
+
readonly ttl: undefined;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
export declare function withSelectorMemoization<T>(): (tree: SignalTree<T>) => MemoizedSignalTree<T>;
|
|
51
|
+
export declare function withComputedMemoization<T>(): (tree: SignalTree<T>) => MemoizedSignalTree<T>;
|
|
52
|
+
export declare function withDeepStateMemoization<T>(): (tree: SignalTree<T>) => MemoizedSignalTree<T>;
|
|
53
|
+
export declare function withHighFrequencyMemoization<T>(): (tree: SignalTree<T>) => MemoizedSignalTree<T>;
|
|
54
|
+
export declare function withMemoization<T>(config?: MemoizationConfig): (tree: SignalTree<T>) => MemoizedSignalTree<T>;
|
|
55
|
+
export declare function enableMemoization<T>(): (tree: SignalTree<T>) => MemoizedSignalTree<T>;
|
|
56
|
+
export declare function withHighPerformanceMemoization<T>(): (tree: SignalTree<T>) => MemoizedSignalTree<T>;
|
|
57
|
+
export declare function withLightweightMemoization<T>(): (tree: SignalTree<T>) => MemoizedSignalTree<T>;
|
|
58
|
+
export declare function withShallowMemoization<T>(): (tree: SignalTree<T>) => MemoizedSignalTree<T>;
|
|
59
|
+
export declare function clearAllCaches(): void;
|
|
60
|
+
export declare function getGlobalCacheStats(): {
|
|
61
|
+
treeCount: number;
|
|
62
|
+
totalSize: number;
|
|
63
|
+
totalHits: number;
|
|
64
|
+
averageCacheSize: number;
|
|
65
|
+
};
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { deepEqual, LRUCache } from '@signaltree/shared';
|
|
2
|
+
import { isNodeAccessor } from '../../../lib/utils';
|
|
3
|
+
const MAX_CACHE_SIZE = 1000;
|
|
4
|
+
const DEFAULT_TTL = 5 * 60 * 1000;
|
|
5
|
+
const memoizationCache = new Map();
|
|
6
|
+
function createMemoCacheStore(maxSize, enableLRU) {
|
|
7
|
+
if (enableLRU) {
|
|
8
|
+
const cache = new LRUCache(maxSize);
|
|
9
|
+
const shadow = new Map();
|
|
10
|
+
const pruneShadow = () => {
|
|
11
|
+
while (shadow.size > cache.size()) {
|
|
12
|
+
const oldestKey = shadow.keys().next().value;
|
|
13
|
+
if (oldestKey === undefined) {
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
shadow.delete(oldestKey);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
return {
|
|
20
|
+
get: (key) => {
|
|
21
|
+
const value = cache.get(key);
|
|
22
|
+
if (value !== undefined) {
|
|
23
|
+
shadow.set(key, value);
|
|
24
|
+
}
|
|
25
|
+
else if (shadow.has(key)) {
|
|
26
|
+
shadow.delete(key);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
},
|
|
30
|
+
set: (key, value) => {
|
|
31
|
+
cache.set(key, value);
|
|
32
|
+
shadow.set(key, value);
|
|
33
|
+
pruneShadow();
|
|
34
|
+
},
|
|
35
|
+
delete: (key) => {
|
|
36
|
+
cache.delete(key);
|
|
37
|
+
shadow.delete(key);
|
|
38
|
+
},
|
|
39
|
+
clear: () => {
|
|
40
|
+
cache.clear();
|
|
41
|
+
shadow.clear();
|
|
42
|
+
},
|
|
43
|
+
size: () => shadow.size,
|
|
44
|
+
forEach: (callback) => {
|
|
45
|
+
shadow.forEach((value, key) => callback(value, key));
|
|
46
|
+
},
|
|
47
|
+
keys: () => shadow.keys(),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const store = new Map();
|
|
51
|
+
return {
|
|
52
|
+
get: (key) => store.get(key),
|
|
53
|
+
set: (key, value) => {
|
|
54
|
+
store.set(key, value);
|
|
55
|
+
},
|
|
56
|
+
delete: (key) => store.delete(key),
|
|
57
|
+
clear: () => store.clear(),
|
|
58
|
+
size: () => store.size,
|
|
59
|
+
forEach: (callback) => {
|
|
60
|
+
store.forEach((value, key) => callback(value, key));
|
|
61
|
+
},
|
|
62
|
+
keys: () => store.keys(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function getCleanupInterval(tree) {
|
|
66
|
+
return tree._memoCleanupInterval;
|
|
67
|
+
}
|
|
68
|
+
function setCleanupInterval(tree, interval) {
|
|
69
|
+
if (interval === undefined) {
|
|
70
|
+
delete tree._memoCleanupInterval;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
tree._memoCleanupInterval = interval;
|
|
74
|
+
}
|
|
75
|
+
function clearCleanupInterval(tree) {
|
|
76
|
+
const interval = getCleanupInterval(tree);
|
|
77
|
+
if (!interval) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
clearInterval(interval);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
}
|
|
85
|
+
setCleanupInterval(tree);
|
|
86
|
+
}
|
|
87
|
+
function resetMemoizationCaches() {
|
|
88
|
+
memoizationCache.forEach((cache, tree) => {
|
|
89
|
+
cache.clear();
|
|
90
|
+
clearCleanupInterval(tree);
|
|
91
|
+
});
|
|
92
|
+
memoizationCache.clear();
|
|
93
|
+
}
|
|
94
|
+
export function cleanupMemoizationCache() {
|
|
95
|
+
resetMemoizationCaches();
|
|
96
|
+
}
|
|
97
|
+
function shallowEqual(a, b) {
|
|
98
|
+
if (a === b)
|
|
99
|
+
return true;
|
|
100
|
+
if (a == null || b == null)
|
|
101
|
+
return false;
|
|
102
|
+
if (typeof a !== typeof b)
|
|
103
|
+
return false;
|
|
104
|
+
if (typeof a === 'object' && typeof b === 'object') {
|
|
105
|
+
const objA = a;
|
|
106
|
+
const objB = b;
|
|
107
|
+
let countA = 0;
|
|
108
|
+
for (const key in objA) {
|
|
109
|
+
if (!Object.prototype.hasOwnProperty.call(objA, key))
|
|
110
|
+
continue;
|
|
111
|
+
countA++;
|
|
112
|
+
if (!(key in objB) || objA[key] !== objB[key])
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
let countB = 0;
|
|
116
|
+
for (const key in objB) {
|
|
117
|
+
if (Object.prototype.hasOwnProperty.call(objB, key))
|
|
118
|
+
countB++;
|
|
119
|
+
}
|
|
120
|
+
return countA === countB;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
function generateCacheKey(fn, args) {
|
|
125
|
+
try {
|
|
126
|
+
return `${fn.name || 'anonymous'}_${JSON.stringify(args)}`;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return `${fn.name || 'anonymous'}_${args.length}`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function getEqualityFn(strategy) {
|
|
133
|
+
switch (strategy) {
|
|
134
|
+
case 'shallow':
|
|
135
|
+
return shallowEqual;
|
|
136
|
+
case 'reference':
|
|
137
|
+
return (a, b) => a === b;
|
|
138
|
+
case 'deep':
|
|
139
|
+
default:
|
|
140
|
+
return deepEqual;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
export function memoize(fn, keyFn, config = {}) {
|
|
144
|
+
const maxSize = config.maxCacheSize ?? MAX_CACHE_SIZE;
|
|
145
|
+
const ttl = config.ttl ?? DEFAULT_TTL;
|
|
146
|
+
const equality = getEqualityFn(config.equality ?? 'shallow');
|
|
147
|
+
const enableLRU = config.enableLRU ?? false;
|
|
148
|
+
const cache = createMemoCacheStore(maxSize, enableLRU);
|
|
149
|
+
const cleanExpiredEntries = () => {
|
|
150
|
+
if (!ttl)
|
|
151
|
+
return;
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
cache.forEach((entry, key) => {
|
|
154
|
+
if (entry.timestamp && now - entry.timestamp > ttl) {
|
|
155
|
+
cache.delete(key);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
return (...args) => {
|
|
160
|
+
if (ttl && Math.random() < 0.01) {
|
|
161
|
+
cleanExpiredEntries();
|
|
162
|
+
}
|
|
163
|
+
const key = keyFn
|
|
164
|
+
? keyFn(...args)
|
|
165
|
+
: generateCacheKey(fn, args);
|
|
166
|
+
const cached = cache.get(key);
|
|
167
|
+
if (cached && (keyFn || equality(cached.deps, args))) {
|
|
168
|
+
if (enableLRU) {
|
|
169
|
+
cached.hitCount += 1;
|
|
170
|
+
}
|
|
171
|
+
return cached.value;
|
|
172
|
+
}
|
|
173
|
+
const result = fn(...args);
|
|
174
|
+
cache.set(key, {
|
|
175
|
+
value: result,
|
|
176
|
+
deps: args,
|
|
177
|
+
timestamp: Date.now(),
|
|
178
|
+
hitCount: 1,
|
|
179
|
+
});
|
|
180
|
+
return result;
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export function memoizeShallow(fn, keyFn) {
|
|
184
|
+
return memoize(fn, keyFn, {
|
|
185
|
+
equality: 'shallow',
|
|
186
|
+
enableLRU: false,
|
|
187
|
+
ttl: undefined,
|
|
188
|
+
maxCacheSize: 100,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
export function memoizeReference(fn, keyFn) {
|
|
192
|
+
return memoize(fn, keyFn, {
|
|
193
|
+
equality: 'reference',
|
|
194
|
+
enableLRU: false,
|
|
195
|
+
ttl: undefined,
|
|
196
|
+
maxCacheSize: 50,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
export const MEMOIZATION_PRESETS = {
|
|
200
|
+
selector: {
|
|
201
|
+
equality: 'reference',
|
|
202
|
+
maxCacheSize: 10,
|
|
203
|
+
enableLRU: false,
|
|
204
|
+
ttl: undefined,
|
|
205
|
+
},
|
|
206
|
+
computed: {
|
|
207
|
+
equality: 'shallow',
|
|
208
|
+
maxCacheSize: 100,
|
|
209
|
+
enableLRU: false,
|
|
210
|
+
ttl: undefined,
|
|
211
|
+
},
|
|
212
|
+
deepState: {
|
|
213
|
+
equality: 'deep',
|
|
214
|
+
maxCacheSize: 1000,
|
|
215
|
+
enableLRU: true,
|
|
216
|
+
ttl: 5 * 60 * 1000,
|
|
217
|
+
},
|
|
218
|
+
highFrequency: {
|
|
219
|
+
equality: 'reference',
|
|
220
|
+
maxCacheSize: 5,
|
|
221
|
+
enableLRU: false,
|
|
222
|
+
ttl: undefined,
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
export function withSelectorMemoization() {
|
|
226
|
+
return withMemoization(MEMOIZATION_PRESETS.selector);
|
|
227
|
+
}
|
|
228
|
+
export function withComputedMemoization() {
|
|
229
|
+
return withMemoization(MEMOIZATION_PRESETS.computed);
|
|
230
|
+
}
|
|
231
|
+
export function withDeepStateMemoization() {
|
|
232
|
+
return withMemoization(MEMOIZATION_PRESETS.deepState);
|
|
233
|
+
}
|
|
234
|
+
export function withHighFrequencyMemoization() {
|
|
235
|
+
return withMemoization(MEMOIZATION_PRESETS.highFrequency);
|
|
236
|
+
}
|
|
237
|
+
export function withMemoization(config = {}) {
|
|
238
|
+
const { enabled = true, maxCacheSize = 1000, ttl, equality = 'deep', enableLRU = true, } = config;
|
|
239
|
+
return (tree) => {
|
|
240
|
+
const originalTreeCall = tree.bind(tree);
|
|
241
|
+
const applyUpdateResult = (result) => {
|
|
242
|
+
Object.entries(result).forEach(([propKey, value]) => {
|
|
243
|
+
const property = tree.state[propKey];
|
|
244
|
+
if (property && 'set' in property) {
|
|
245
|
+
property.set(value);
|
|
246
|
+
}
|
|
247
|
+
else if (isNodeAccessor(property)) {
|
|
248
|
+
property(value);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
if (!enabled) {
|
|
253
|
+
const memoTree = tree;
|
|
254
|
+
memoTree.memoizedUpdate = (updater) => {
|
|
255
|
+
const currentState = originalTreeCall();
|
|
256
|
+
const result = updater(currentState);
|
|
257
|
+
applyUpdateResult(result);
|
|
258
|
+
};
|
|
259
|
+
memoTree.clearMemoCache = () => {
|
|
260
|
+
};
|
|
261
|
+
memoTree.getCacheStats = () => ({
|
|
262
|
+
size: 0,
|
|
263
|
+
hitRate: 0,
|
|
264
|
+
totalHits: 0,
|
|
265
|
+
totalMisses: 0,
|
|
266
|
+
keys: [],
|
|
267
|
+
});
|
|
268
|
+
return memoTree;
|
|
269
|
+
}
|
|
270
|
+
const cache = createMemoCacheStore(maxCacheSize, enableLRU);
|
|
271
|
+
memoizationCache.set(tree, cache);
|
|
272
|
+
const equalityFn = getEqualityFn(equality);
|
|
273
|
+
tree.memoizedUpdate = (updater, cacheKey) => {
|
|
274
|
+
const currentState = originalTreeCall();
|
|
275
|
+
const key = cacheKey ||
|
|
276
|
+
generateCacheKey(updater, [
|
|
277
|
+
currentState,
|
|
278
|
+
]);
|
|
279
|
+
const cached = cache.get(key);
|
|
280
|
+
if (cached && equalityFn(cached.deps, [currentState])) {
|
|
281
|
+
const cachedUpdate = cached.value;
|
|
282
|
+
applyUpdateResult(cachedUpdate);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const result = updater(currentState);
|
|
286
|
+
cache.set(key, {
|
|
287
|
+
value: result,
|
|
288
|
+
deps: [currentState],
|
|
289
|
+
timestamp: Date.now(),
|
|
290
|
+
hitCount: 1,
|
|
291
|
+
});
|
|
292
|
+
applyUpdateResult(result);
|
|
293
|
+
};
|
|
294
|
+
tree.clearMemoCache = (key) => {
|
|
295
|
+
if (key) {
|
|
296
|
+
cache.delete(key);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
cache.clear();
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
tree.getCacheStats = () => {
|
|
303
|
+
let totalHits = 0;
|
|
304
|
+
let totalMisses = 0;
|
|
305
|
+
cache.forEach((entry) => {
|
|
306
|
+
totalHits += entry.hitCount || 0;
|
|
307
|
+
totalMisses += Math.floor((entry.hitCount || 0) / 2);
|
|
308
|
+
});
|
|
309
|
+
const hitRate = totalHits + totalMisses > 0 ? totalHits / (totalHits + totalMisses) : 0;
|
|
310
|
+
return {
|
|
311
|
+
size: cache.size(),
|
|
312
|
+
hitRate,
|
|
313
|
+
totalHits,
|
|
314
|
+
totalMisses,
|
|
315
|
+
keys: Array.from(cache.keys()),
|
|
316
|
+
};
|
|
317
|
+
};
|
|
318
|
+
const maybeInterval = getCleanupInterval(tree);
|
|
319
|
+
if (maybeInterval) {
|
|
320
|
+
const origClear = tree.clearMemoCache.bind(tree);
|
|
321
|
+
tree.clearMemoCache = (key) => {
|
|
322
|
+
origClear(key);
|
|
323
|
+
clearCleanupInterval(tree);
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
if (ttl) {
|
|
327
|
+
const cleanup = () => {
|
|
328
|
+
const now = Date.now();
|
|
329
|
+
cache.forEach((entry, key) => {
|
|
330
|
+
if (entry.timestamp && now - entry.timestamp > ttl) {
|
|
331
|
+
cache.delete(key);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
};
|
|
335
|
+
const intervalId = setInterval(cleanup, ttl);
|
|
336
|
+
setCleanupInterval(tree, intervalId);
|
|
337
|
+
}
|
|
338
|
+
return tree;
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
export function enableMemoization() {
|
|
342
|
+
return withMemoization({ enabled: true });
|
|
343
|
+
}
|
|
344
|
+
export function withHighPerformanceMemoization() {
|
|
345
|
+
return withMemoization({
|
|
346
|
+
enabled: true,
|
|
347
|
+
maxCacheSize: 10000,
|
|
348
|
+
ttl: 300000,
|
|
349
|
+
equality: 'shallow',
|
|
350
|
+
enableLRU: true,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
export function withLightweightMemoization() {
|
|
354
|
+
return withMemoization({
|
|
355
|
+
enabled: true,
|
|
356
|
+
maxCacheSize: 100,
|
|
357
|
+
ttl: undefined,
|
|
358
|
+
equality: 'reference',
|
|
359
|
+
enableLRU: false,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
export function withShallowMemoization() {
|
|
363
|
+
return withMemoization({
|
|
364
|
+
enabled: true,
|
|
365
|
+
maxCacheSize: 1000,
|
|
366
|
+
ttl: 60000,
|
|
367
|
+
equality: 'shallow',
|
|
368
|
+
enableLRU: true,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
export function clearAllCaches() {
|
|
372
|
+
resetMemoizationCaches();
|
|
373
|
+
}
|
|
374
|
+
export function getGlobalCacheStats() {
|
|
375
|
+
let totalSize = 0;
|
|
376
|
+
let totalHits = 0;
|
|
377
|
+
let treeCount = 0;
|
|
378
|
+
memoizationCache.forEach((cache) => {
|
|
379
|
+
treeCount++;
|
|
380
|
+
totalSize += cache.size();
|
|
381
|
+
cache.forEach((entry) => {
|
|
382
|
+
totalHits += entry.hitCount || 0;
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
return {
|
|
386
|
+
treeCount,
|
|
387
|
+
totalSize,
|
|
388
|
+
totalHits,
|
|
389
|
+
averageCacheSize: treeCount > 0 ? totalSize / treeCount : 0,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
displayName: string;
|
|
3
|
+
preset: string;
|
|
4
|
+
setupFilesAfterEnv: string[];
|
|
5
|
+
coverageDirectory: string;
|
|
6
|
+
transform: {
|
|
7
|
+
'^.+\\.(ts|mjs|js|html)$': (string | {
|
|
8
|
+
tsconfig: string;
|
|
9
|
+
stringifyContentPathRegex: string;
|
|
10
|
+
})[];
|
|
11
|
+
};
|
|
12
|
+
transformIgnorePatterns: string[];
|
|
13
|
+
snapshotSerializers: string[];
|
|
14
|
+
};
|
|
15
|
+
export default _default;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
displayName: 'middleware',
|
|
3
|
+
preset: '../../../jest.preset.js',
|
|
4
|
+
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
|
5
|
+
coverageDirectory: '../../../coverage/packages/core/enhancers/middleware',
|
|
6
|
+
transform: {
|
|
7
|
+
'^.+\\.(ts|mjs|js|html)$': [
|
|
8
|
+
'jest-preset-angular',
|
|
9
|
+
{
|
|
10
|
+
tsconfig: '<rootDir>/tsconfig.spec.json',
|
|
11
|
+
stringifyContentPathRegex: '\\.(html|svg)$',
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
|
16
|
+
snapshotSerializers: [
|
|
17
|
+
'jest-preset-angular/build/serializers/no-ng-attributes',
|
|
18
|
+
'jest-preset-angular/build/serializers/ng-snapshot',
|
|
19
|
+
'jest-preset-angular/build/serializers/html-comment',
|
|
20
|
+
],
|
|
21
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SignalTree } from '../../../lib/types';
|
|
2
|
+
export declare function createAsyncOperation<T, TResult>(name: string, operation: () => Promise<TResult>): (tree: SignalTree<T>) => Promise<TResult>;
|
|
3
|
+
export declare function trackAsync<T>(operation: () => Promise<T>): {
|
|
4
|
+
pending: import("@angular/core").Signal<boolean>;
|
|
5
|
+
error: import("@angular/core").Signal<Error | null>;
|
|
6
|
+
result: import("@angular/core").Signal<T | null>;
|
|
7
|
+
execute: () => Promise<T>;
|
|
8
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { signal } from '@angular/core';
|
|
2
|
+
export function createAsyncOperation(name, operation) {
|
|
3
|
+
return async (tree) => {
|
|
4
|
+
if (typeof tree.batchUpdate === 'function') {
|
|
5
|
+
const pendingPatch = {
|
|
6
|
+
[`${name}_PENDING`]: true,
|
|
7
|
+
};
|
|
8
|
+
tree.batchUpdate(() => pendingPatch);
|
|
9
|
+
}
|
|
10
|
+
else if ('$' in tree) {
|
|
11
|
+
try {
|
|
12
|
+
tree['$'][`${name}_PENDING`]?.set?.(true);
|
|
13
|
+
}
|
|
14
|
+
catch (e) {
|
|
15
|
+
void e;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const result = await operation();
|
|
20
|
+
if (typeof tree.batchUpdate === 'function') {
|
|
21
|
+
const resultPatch = {
|
|
22
|
+
[`${name}_RESULT`]: result,
|
|
23
|
+
[`${name}_PENDING`]: false,
|
|
24
|
+
};
|
|
25
|
+
tree.batchUpdate(() => resultPatch);
|
|
26
|
+
}
|
|
27
|
+
else if ('$' in tree) {
|
|
28
|
+
try {
|
|
29
|
+
tree['$'][`${name}_RESULT`]?.set?.(result);
|
|
30
|
+
tree['$'][`${name}_PENDING`]?.set?.(false);
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
void e;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (typeof tree.batchUpdate === 'function') {
|
|
40
|
+
const errorPatch = {
|
|
41
|
+
[`${name}_ERROR`]: error,
|
|
42
|
+
[`${name}_PENDING`]: false,
|
|
43
|
+
};
|
|
44
|
+
tree.batchUpdate(() => errorPatch);
|
|
45
|
+
}
|
|
46
|
+
else if ('$' in tree) {
|
|
47
|
+
try {
|
|
48
|
+
tree['$'][`${name}_ERROR`]?.set?.(error);
|
|
49
|
+
tree['$'][`${name}_PENDING`]?.set?.(false);
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
void e;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function trackAsync(operation) {
|
|
60
|
+
const pending = signal(false);
|
|
61
|
+
const error = signal(null);
|
|
62
|
+
const result = signal(null);
|
|
63
|
+
return {
|
|
64
|
+
pending: pending.asReadonly(),
|
|
65
|
+
error: error.asReadonly(),
|
|
66
|
+
result: result.asReadonly(),
|
|
67
|
+
execute: async () => {
|
|
68
|
+
pending.set(true);
|
|
69
|
+
error.set(null);
|
|
70
|
+
try {
|
|
71
|
+
const res = await operation();
|
|
72
|
+
result.set(res);
|
|
73
|
+
return res;
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
77
|
+
error.set(err);
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
pending.set(false);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Middleware, SignalTree } from '../../../lib/types';
|
|
2
|
+
export declare function withMiddleware<T>(middlewares?: Middleware<T>[]): (tree: SignalTree<T>) => SignalTree<T>;
|
|
3
|
+
export declare function createLoggingMiddleware<T>(treeName: string): Middleware<T>;
|
|
4
|
+
export declare function createPerformanceMiddleware<T>(): Middleware<T>;
|
|
5
|
+
export declare function createValidationMiddleware<T>(validator: (state: T) => string | null): Middleware<T>;
|
|
6
|
+
export declare function createPersistenceMiddleware<T>(config: {
|
|
7
|
+
key: string;
|
|
8
|
+
storage?: Storage;
|
|
9
|
+
debounceMs?: number;
|
|
10
|
+
actions?: string[];
|
|
11
|
+
}): Middleware<T>;
|