@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,193 @@
|
|
|
1
|
+
import { deepClone } from '../../../deep-clone.js';
|
|
2
|
+
import { deepEqual } from '../../../deep-equal.js';
|
|
3
|
+
|
|
4
|
+
class TimeTravelManager {
|
|
5
|
+
tree;
|
|
6
|
+
config;
|
|
7
|
+
restoreStateFn;
|
|
8
|
+
history = [];
|
|
9
|
+
currentIndex = -1;
|
|
10
|
+
maxHistorySize;
|
|
11
|
+
includePayload;
|
|
12
|
+
actionNames;
|
|
13
|
+
constructor(tree, config = {}, restoreStateFn) {
|
|
14
|
+
this.tree = tree;
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.restoreStateFn = restoreStateFn;
|
|
17
|
+
this.maxHistorySize = config.maxHistorySize ?? 50;
|
|
18
|
+
this.includePayload = config.includePayload ?? true;
|
|
19
|
+
this.actionNames = {
|
|
20
|
+
update: 'UPDATE',
|
|
21
|
+
set: 'SET',
|
|
22
|
+
batch: 'BATCH',
|
|
23
|
+
...config.actionNames
|
|
24
|
+
};
|
|
25
|
+
this.addEntry('INIT', this.tree());
|
|
26
|
+
}
|
|
27
|
+
addEntry(action, state, payload) {
|
|
28
|
+
if (this.currentIndex < this.history.length - 1) {
|
|
29
|
+
this.history = this.history.slice(0, this.currentIndex + 1);
|
|
30
|
+
}
|
|
31
|
+
const entry = {
|
|
32
|
+
state: deepClone(state),
|
|
33
|
+
timestamp: Date.now(),
|
|
34
|
+
action: this.actionNames[action] || action,
|
|
35
|
+
...(this.includePayload && payload !== undefined && {
|
|
36
|
+
payload
|
|
37
|
+
})
|
|
38
|
+
};
|
|
39
|
+
this.history.push(entry);
|
|
40
|
+
this.currentIndex = this.history.length - 1;
|
|
41
|
+
if (this.history.length > this.maxHistorySize) {
|
|
42
|
+
this.history.shift();
|
|
43
|
+
this.currentIndex--;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
undo() {
|
|
47
|
+
if (!this.canUndo()) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
this.currentIndex--;
|
|
51
|
+
const entry = this.history[this.currentIndex];
|
|
52
|
+
this.restoreState(entry.state);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
redo() {
|
|
56
|
+
if (!this.canRedo()) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
this.currentIndex++;
|
|
60
|
+
const entry = this.history[this.currentIndex];
|
|
61
|
+
this.restoreState(entry.state);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
getHistory() {
|
|
65
|
+
return this.history.map(entry => ({
|
|
66
|
+
...entry,
|
|
67
|
+
state: deepClone(entry.state)
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
resetHistory() {
|
|
71
|
+
const currentState = this.tree();
|
|
72
|
+
this.history = [];
|
|
73
|
+
this.currentIndex = -1;
|
|
74
|
+
this.addEntry('RESET', currentState);
|
|
75
|
+
}
|
|
76
|
+
jumpTo(index) {
|
|
77
|
+
if (index < 0 || index >= this.history.length) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
this.currentIndex = index;
|
|
81
|
+
const entry = this.history[index];
|
|
82
|
+
this.restoreState(entry.state);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
getCurrentIndex() {
|
|
86
|
+
return this.currentIndex;
|
|
87
|
+
}
|
|
88
|
+
canUndo() {
|
|
89
|
+
return this.currentIndex > 0;
|
|
90
|
+
}
|
|
91
|
+
canRedo() {
|
|
92
|
+
return this.currentIndex < this.history.length - 1;
|
|
93
|
+
}
|
|
94
|
+
restoreState(state) {
|
|
95
|
+
if (this.restoreStateFn) {
|
|
96
|
+
this.restoreStateFn(state);
|
|
97
|
+
} else {
|
|
98
|
+
this.tree(state);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function withTimeTravel(config = {}) {
|
|
103
|
+
return tree => {
|
|
104
|
+
const originalTreeCall = tree.bind(tree);
|
|
105
|
+
let isRestoring = false;
|
|
106
|
+
const timeTravelManager = new TimeTravelManager(tree, config, state => {
|
|
107
|
+
isRestoring = true;
|
|
108
|
+
try {
|
|
109
|
+
originalTreeCall(state);
|
|
110
|
+
} finally {
|
|
111
|
+
isRestoring = false;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
const enhancedTree = function (...args) {
|
|
115
|
+
if (args.length === 0) {
|
|
116
|
+
return originalTreeCall();
|
|
117
|
+
} else {
|
|
118
|
+
if (isRestoring) {
|
|
119
|
+
if (args.length === 1) {
|
|
120
|
+
const arg = args[0];
|
|
121
|
+
if (typeof arg === 'function') {
|
|
122
|
+
return originalTreeCall(arg);
|
|
123
|
+
} else {
|
|
124
|
+
return originalTreeCall(arg);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const beforeState = originalTreeCall();
|
|
130
|
+
let result;
|
|
131
|
+
if (args.length === 1) {
|
|
132
|
+
const arg = args[0];
|
|
133
|
+
if (typeof arg === 'function') {
|
|
134
|
+
result = originalTreeCall(arg);
|
|
135
|
+
} else {
|
|
136
|
+
result = originalTreeCall(arg);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const afterState = originalTreeCall();
|
|
140
|
+
const statesEqual = deepEqual(beforeState, afterState);
|
|
141
|
+
if (!statesEqual) {
|
|
142
|
+
timeTravelManager.addEntry('update', afterState);
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
Object.setPrototypeOf(enhancedTree, Object.getPrototypeOf(tree));
|
|
148
|
+
Object.assign(enhancedTree, tree);
|
|
149
|
+
if ('state' in tree) {
|
|
150
|
+
Object.defineProperty(enhancedTree, 'state', {
|
|
151
|
+
value: tree.state,
|
|
152
|
+
enumerable: false,
|
|
153
|
+
configurable: true
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if ('$' in tree) {
|
|
157
|
+
Object.defineProperty(enhancedTree, '$', {
|
|
158
|
+
value: tree['$'],
|
|
159
|
+
enumerable: false,
|
|
160
|
+
configurable: true
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
enhancedTree.undo = () => {
|
|
164
|
+
timeTravelManager.undo();
|
|
165
|
+
};
|
|
166
|
+
enhancedTree.redo = () => {
|
|
167
|
+
timeTravelManager.redo();
|
|
168
|
+
};
|
|
169
|
+
enhancedTree.getHistory = () => timeTravelManager.getHistory();
|
|
170
|
+
enhancedTree.resetHistory = () => {
|
|
171
|
+
timeTravelManager.resetHistory();
|
|
172
|
+
};
|
|
173
|
+
enhancedTree.jumpTo = index => {
|
|
174
|
+
timeTravelManager.jumpTo(index);
|
|
175
|
+
};
|
|
176
|
+
enhancedTree.canUndo = () => timeTravelManager.canUndo();
|
|
177
|
+
enhancedTree.canRedo = () => timeTravelManager.canRedo();
|
|
178
|
+
enhancedTree.getCurrentIndex = () => timeTravelManager.getCurrentIndex();
|
|
179
|
+
return Object.assign(enhancedTree, {
|
|
180
|
+
__timeTravel: timeTravelManager
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function enableTimeTravel(maxHistorySize) {
|
|
185
|
+
return withTimeTravel({
|
|
186
|
+
maxHistorySize
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function getTimeTravel(tree) {
|
|
190
|
+
return tree.__timeTravel;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export { enableTimeTravel, getTimeTravel, withTimeTravel };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { signalTree } from './lib/signal-tree.js';
|
|
2
|
+
export { ENHANCER_META, entityMap } from './lib/types.js';
|
|
3
|
+
export { composeEnhancers, createLazySignalTree, isAnySignal, isNodeAccessor, toWritableSignal } from './lib/utils.js';
|
|
4
|
+
export { getPathNotifier } from './lib/path-notifier.js';
|
|
5
|
+
export { SecurityPresets, SecurityValidator } from './lib/security/security-validator.js';
|
|
6
|
+
export { createEnhancer, resolveEnhancerOrder } from './enhancers/index.js';
|
|
7
|
+
export { flushBatchedUpdates, getBatchQueueSize, hasPendingUpdates, withBatching, withHighPerformanceBatching } from './enhancers/batching/lib/batching.js';
|
|
8
|
+
export { cleanupMemoizationCache, clearAllCaches, getGlobalCacheStats, memoize, memoizeReference, memoizeShallow, withComputedMemoization, withDeepStateMemoization, withHighFrequencyMemoization, withHighPerformanceMemoization, withLightweightMemoization, withMemoization, withSelectorMemoization, withShallowMemoization } from './enhancers/memoization/lib/memoization.js';
|
|
9
|
+
export { enableTimeTravel, getTimeTravel, withTimeTravel } from './enhancers/time-travel/lib/time-travel.js';
|
|
10
|
+
export { enableEntities, withEntities, withHighPerformanceEntities } from './enhancers/entities/lib/entities.js';
|
|
11
|
+
export { applyPersistence, applySerialization, createIndexedDBAdapter, createStorageAdapter, enableSerialization, withPersistence, withSerialization } from './enhancers/serialization/lib/serialization.js';
|
|
12
|
+
export { enableDevTools, withDevTools, withFullDevTools, withProductionDevTools } from './enhancers/devtools/lib/devtools.js';
|
|
13
|
+
export { createAsyncOperation, trackAsync } from './lib/async-helpers.js';
|
|
14
|
+
export { TREE_PRESETS, combinePresets, createDevTree, createPresetConfig, getAvailablePresets, validatePreset } from './enhancers/presets/lib/presets.js';
|
|
15
|
+
export { computedEnhancer, createComputed } from './enhancers/computed/lib/computed.js';
|
|
16
|
+
export { SIGNAL_TREE_CONSTANTS, SIGNAL_TREE_MESSAGES } from './lib/constants.js';
|
|
17
|
+
export { deepEqual, deepEqual as equal } from './deep-equal.js';
|
|
18
|
+
export { parsePath } from './parse-path.js';
|
|
19
|
+
export { isBuiltInObject } from './is-built-in-object.js';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function isBuiltInObject(value) {
|
|
2
|
+
if (value === null || value === undefined) return false;
|
|
3
|
+
if (value instanceof Date || value instanceof RegExp || typeof value === 'function' || value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet || value instanceof ArrayBuffer || value instanceof DataView || value instanceof Error || value instanceof Promise) {
|
|
4
|
+
return true;
|
|
5
|
+
}
|
|
6
|
+
if (value instanceof Int8Array || value instanceof Uint8Array || value instanceof Uint8ClampedArray || value instanceof Int16Array || value instanceof Uint16Array || value instanceof Int32Array || value instanceof Uint32Array || value instanceof Float32Array || value instanceof Float64Array || value instanceof BigInt64Array || value instanceof BigUint64Array) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
if (typeof window !== 'undefined') {
|
|
10
|
+
if (value instanceof URL || value instanceof URLSearchParams || value instanceof FormData || value instanceof Blob || typeof File !== 'undefined' && value instanceof File || typeof FileList !== 'undefined' && value instanceof FileList || typeof Headers !== 'undefined' && value instanceof Headers || typeof Request !== 'undefined' && value instanceof Request || typeof Response !== 'undefined' && value instanceof Response || typeof AbortController !== 'undefined' && value instanceof AbortController || typeof AbortSignal !== 'undefined' && value instanceof AbortSignal) {
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const NodeBuffer = globalThis === null || globalThis === void 0 ? void 0 : globalThis.Buffer;
|
|
16
|
+
if (NodeBuffer && value instanceof NodeBuffer) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
} catch (_a) {}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { isBuiltInObject };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { signal } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
function createAsyncOperation(name, operation) {
|
|
4
|
+
return async tree => {
|
|
5
|
+
if (typeof tree.batchUpdate === 'function') {
|
|
6
|
+
const pendingPatch = {
|
|
7
|
+
[`${name}_PENDING`]: true
|
|
8
|
+
};
|
|
9
|
+
tree.batchUpdate(() => pendingPatch);
|
|
10
|
+
} else if ('$' in tree) {
|
|
11
|
+
try {
|
|
12
|
+
tree['$'][`${name}_PENDING`]?.set?.(true);
|
|
13
|
+
} catch (e) {
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const result = await operation();
|
|
18
|
+
if (typeof tree.batchUpdate === 'function') {
|
|
19
|
+
const resultPatch = {
|
|
20
|
+
[`${name}_RESULT`]: result,
|
|
21
|
+
[`${name}_PENDING`]: false
|
|
22
|
+
};
|
|
23
|
+
tree.batchUpdate(() => resultPatch);
|
|
24
|
+
} else if ('$' in tree) {
|
|
25
|
+
try {
|
|
26
|
+
tree['$'][`${name}_RESULT`]?.set?.(result);
|
|
27
|
+
tree['$'][`${name}_PENDING`]?.set?.(false);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
void e;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return result;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (typeof tree.batchUpdate === 'function') {
|
|
35
|
+
const errorPatch = {
|
|
36
|
+
[`${name}_ERROR`]: error,
|
|
37
|
+
[`${name}_PENDING`]: false
|
|
38
|
+
};
|
|
39
|
+
tree.batchUpdate(() => errorPatch);
|
|
40
|
+
} else if ('$' in tree) {
|
|
41
|
+
try {
|
|
42
|
+
tree['$'][`${name}_ERROR`]?.set?.(error);
|
|
43
|
+
tree['$'][`${name}_PENDING`]?.set?.(false);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function trackAsync(operation) {
|
|
52
|
+
const pending = signal(false);
|
|
53
|
+
const error = signal(null);
|
|
54
|
+
const result = signal(null);
|
|
55
|
+
return {
|
|
56
|
+
pending: pending.asReadonly(),
|
|
57
|
+
error: error.asReadonly(),
|
|
58
|
+
result: result.asReadonly(),
|
|
59
|
+
execute: async () => {
|
|
60
|
+
pending.set(true);
|
|
61
|
+
error.set(null);
|
|
62
|
+
try {
|
|
63
|
+
const res = await operation();
|
|
64
|
+
result.set(res);
|
|
65
|
+
return res;
|
|
66
|
+
} catch (e) {
|
|
67
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
68
|
+
error.set(err);
|
|
69
|
+
throw err;
|
|
70
|
+
} finally {
|
|
71
|
+
pending.set(false);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { createAsyncOperation, trackAsync };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { DEFAULT_PATH_CACHE_SIZE } from '../constants.js';
|
|
2
|
+
|
|
3
|
+
const SIGNAL_TREE_CONSTANTS = {
|
|
4
|
+
MAX_PATH_CACHE_SIZE: DEFAULT_PATH_CACHE_SIZE,
|
|
5
|
+
LAZY_THRESHOLD: 50,
|
|
6
|
+
ESTIMATE_MAX_DEPTH: 3,
|
|
7
|
+
ESTIMATE_SAMPLE_SIZE_ARRAY: 3,
|
|
8
|
+
ESTIMATE_SAMPLE_SIZE_OBJECT: 5,
|
|
9
|
+
DEFAULT_CACHE_SIZE: 100,
|
|
10
|
+
DEFAULT_BATCH_SIZE: 10
|
|
11
|
+
};
|
|
12
|
+
const DEV_MESSAGES = {
|
|
13
|
+
NULL_OR_UNDEFINED: 'null/undefined',
|
|
14
|
+
CIRCULAR_REF: 'circular ref',
|
|
15
|
+
UPDATER_INVALID: 'updater invalid',
|
|
16
|
+
LAZY_FALLBACK: 'lazy fallback',
|
|
17
|
+
SIGNAL_CREATION_FAILED: 'signal creation failed',
|
|
18
|
+
UPDATE_PATH_NOT_FOUND: 'update path not found',
|
|
19
|
+
UPDATE_FAILED: 'update failed',
|
|
20
|
+
ROLLBACK_FAILED: 'rollback failed',
|
|
21
|
+
CLEANUP_ERROR: 'cleanup error',
|
|
22
|
+
PRESET_UNKNOWN: 'unknown preset',
|
|
23
|
+
STRATEGY_SELECTION: 'strategy select',
|
|
24
|
+
TREE_DESTROYED: 'destroyed',
|
|
25
|
+
UPDATE_TRANSACTION: 'update tx',
|
|
26
|
+
BATCH_NOT_ENABLED: 'batching disabled',
|
|
27
|
+
MEMOIZE_NOT_ENABLED: 'memoize disabled',
|
|
28
|
+
MIDDLEWARE_NOT_AVAILABLE: 'middleware missing',
|
|
29
|
+
ENTITY_HELPERS_NOT_AVAILABLE: 'entity helpers missing',
|
|
30
|
+
TIME_TRAVEL_NOT_AVAILABLE: 'time travel missing',
|
|
31
|
+
OPTIMIZE_NOT_AVAILABLE: 'optimize missing',
|
|
32
|
+
UPDATE_OPTIMIZED_NOT_AVAILABLE: 'update optimized missing',
|
|
33
|
+
CACHE_NOT_AVAILABLE: 'cache missing',
|
|
34
|
+
PERFORMANCE_NOT_ENABLED: 'performance disabled',
|
|
35
|
+
ENHANCER_ORDER_FAILED: 'enhancer order failed',
|
|
36
|
+
ENHANCER_CYCLE_DETECTED: 'enhancer cycle',
|
|
37
|
+
ENHANCER_REQUIREMENT_MISSING: 'enhancer req missing',
|
|
38
|
+
ENHANCER_PROVIDES_MISSING: 'enhancer provides missing',
|
|
39
|
+
ENHANCER_FAILED: 'enhancer failed',
|
|
40
|
+
ENHANCER_NOT_FUNCTION: 'enhancer not function',
|
|
41
|
+
EFFECT_NO_CONTEXT: 'no angular context',
|
|
42
|
+
SUBSCRIBE_NO_CONTEXT: 'no angular context'
|
|
43
|
+
};
|
|
44
|
+
const PROD_MESSAGES = (() => {
|
|
45
|
+
const out = {};
|
|
46
|
+
let i = 0;
|
|
47
|
+
for (const k of Object.keys(DEV_MESSAGES)) {
|
|
48
|
+
out[k] = String(i++);
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
})();
|
|
52
|
+
const _isProdByEnv = Boolean(typeof globalThis === 'object' && globalThis !== null && 'process' in globalThis && typeof globalThis.process === 'object' && 'env' in globalThis.process && globalThis.process.env.NODE_ENV === 'production');
|
|
53
|
+
const _isDev = typeof ngDevMode !== 'undefined' ? Boolean(ngDevMode) : !_isProdByEnv;
|
|
54
|
+
const SIGNAL_TREE_MESSAGES = Object.freeze(_isDev ? DEV_MESSAGES : PROD_MESSAGES);
|
|
55
|
+
|
|
56
|
+
export { SIGNAL_TREE_CONSTANTS, SIGNAL_TREE_MESSAGES };
|
|
@@ -0,0 +1,280 @@
|
|
|
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
|
+
return new Proxy(this, {
|
|
24
|
+
get: (target, prop) => {
|
|
25
|
+
if (typeof prop === 'string' && !isNaN(Number(prop))) {
|
|
26
|
+
return target.byId(Number(prop));
|
|
27
|
+
}
|
|
28
|
+
return target[prop];
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
byId(id) {
|
|
33
|
+
const entity = this.storage.get(id);
|
|
34
|
+
if (!entity) return undefined;
|
|
35
|
+
return this.getOrCreateNode(id, entity);
|
|
36
|
+
}
|
|
37
|
+
byIdOrFail(id) {
|
|
38
|
+
const node = this.byId(id);
|
|
39
|
+
if (!node) {
|
|
40
|
+
throw new Error(`Entity with id ${String(id)} not found`);
|
|
41
|
+
}
|
|
42
|
+
return node;
|
|
43
|
+
}
|
|
44
|
+
get all() {
|
|
45
|
+
return this.allSignal;
|
|
46
|
+
}
|
|
47
|
+
get count() {
|
|
48
|
+
return this.countSignal;
|
|
49
|
+
}
|
|
50
|
+
get ids() {
|
|
51
|
+
return this.idsSignal;
|
|
52
|
+
}
|
|
53
|
+
get map() {
|
|
54
|
+
return this.mapSignal;
|
|
55
|
+
}
|
|
56
|
+
has(id) {
|
|
57
|
+
return computed(() => this.storage.has(id));
|
|
58
|
+
}
|
|
59
|
+
get isEmpty() {
|
|
60
|
+
return computed(() => this.storage.size === 0);
|
|
61
|
+
}
|
|
62
|
+
where(predicate) {
|
|
63
|
+
return computed(() => {
|
|
64
|
+
const result = [];
|
|
65
|
+
for (const entity of this.storage.values()) {
|
|
66
|
+
if (predicate(entity)) {
|
|
67
|
+
result.push(entity);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
find(predicate) {
|
|
74
|
+
return computed(() => {
|
|
75
|
+
for (const entity of this.storage.values()) {
|
|
76
|
+
if (predicate(entity)) {
|
|
77
|
+
return entity;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
addOne(entity, opts) {
|
|
84
|
+
const id = opts?.selectId?.(entity) ?? this.selectId(entity);
|
|
85
|
+
if (this.storage.has(id)) {
|
|
86
|
+
throw new Error(`Entity with id ${String(id)} already exists`);
|
|
87
|
+
}
|
|
88
|
+
let transformedEntity = entity;
|
|
89
|
+
for (const handler of this.interceptHandlers) {
|
|
90
|
+
const ctx = {
|
|
91
|
+
block: reason => {
|
|
92
|
+
throw new Error(`Cannot add entity: ${reason || 'blocked by interceptor'}`);
|
|
93
|
+
},
|
|
94
|
+
transform: value => {
|
|
95
|
+
transformedEntity = value;
|
|
96
|
+
},
|
|
97
|
+
blocked: false,
|
|
98
|
+
blockReason: undefined
|
|
99
|
+
};
|
|
100
|
+
handler.onAdd?.(entity, ctx);
|
|
101
|
+
}
|
|
102
|
+
this.storage.set(id, transformedEntity);
|
|
103
|
+
this.nodeCache.delete(id);
|
|
104
|
+
this.updateSignals();
|
|
105
|
+
this.pathNotifier.notify(`${this.basePath}.${String(id)}`, transformedEntity, undefined);
|
|
106
|
+
for (const handler of this.tapHandlers) {
|
|
107
|
+
handler.onAdd?.(transformedEntity, id);
|
|
108
|
+
}
|
|
109
|
+
return id;
|
|
110
|
+
}
|
|
111
|
+
addMany(entities, opts) {
|
|
112
|
+
const ids = [];
|
|
113
|
+
for (const entity of entities) {
|
|
114
|
+
ids.push(this.addOne(entity, opts));
|
|
115
|
+
}
|
|
116
|
+
return ids;
|
|
117
|
+
}
|
|
118
|
+
updateOne(id, changes) {
|
|
119
|
+
const entity = this.storage.get(id);
|
|
120
|
+
if (!entity) {
|
|
121
|
+
throw new Error(`Entity with id ${String(id)} not found`);
|
|
122
|
+
}
|
|
123
|
+
const prev = entity;
|
|
124
|
+
let transformedChanges = changes;
|
|
125
|
+
for (const handler of this.interceptHandlers) {
|
|
126
|
+
const ctx = {
|
|
127
|
+
block: reason => {
|
|
128
|
+
throw new Error(`Cannot update entity: ${reason || 'blocked by interceptor'}`);
|
|
129
|
+
},
|
|
130
|
+
transform: value => {
|
|
131
|
+
transformedChanges = value;
|
|
132
|
+
},
|
|
133
|
+
blocked: false,
|
|
134
|
+
blockReason: undefined
|
|
135
|
+
};
|
|
136
|
+
handler.onUpdate?.(id, changes, ctx);
|
|
137
|
+
}
|
|
138
|
+
const finalUpdated = {
|
|
139
|
+
...entity,
|
|
140
|
+
...transformedChanges
|
|
141
|
+
};
|
|
142
|
+
this.storage.set(id, finalUpdated);
|
|
143
|
+
this.nodeCache.delete(id);
|
|
144
|
+
this.updateSignals();
|
|
145
|
+
this.pathNotifier.notify(`${this.basePath}.${String(id)}`, finalUpdated, prev);
|
|
146
|
+
for (const handler of this.tapHandlers) {
|
|
147
|
+
handler.onUpdate?.(id, transformedChanges, finalUpdated);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
updateMany(ids, changes) {
|
|
151
|
+
for (const id of ids) {
|
|
152
|
+
this.updateOne(id, changes);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
updateWhere(predicate, changes) {
|
|
156
|
+
let count = 0;
|
|
157
|
+
for (const [id, entity] of this.storage) {
|
|
158
|
+
if (predicate(entity)) {
|
|
159
|
+
this.updateOne(id, changes);
|
|
160
|
+
count++;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return count;
|
|
164
|
+
}
|
|
165
|
+
removeOne(id) {
|
|
166
|
+
const entity = this.storage.get(id);
|
|
167
|
+
if (!entity) {
|
|
168
|
+
throw new Error(`Entity with id ${String(id)} not found`);
|
|
169
|
+
}
|
|
170
|
+
for (const handler of this.interceptHandlers) {
|
|
171
|
+
const ctx = {
|
|
172
|
+
block: reason => {
|
|
173
|
+
throw new Error(`Cannot remove entity: ${reason || 'blocked by interceptor'}`);
|
|
174
|
+
},
|
|
175
|
+
transform: () => {},
|
|
176
|
+
blocked: false,
|
|
177
|
+
blockReason: undefined
|
|
178
|
+
};
|
|
179
|
+
handler.onRemove?.(id, entity, ctx);
|
|
180
|
+
}
|
|
181
|
+
this.storage.delete(id);
|
|
182
|
+
this.nodeCache.delete(id);
|
|
183
|
+
this.updateSignals();
|
|
184
|
+
this.pathNotifier.notify(`${this.basePath}.${String(id)}`, undefined, entity);
|
|
185
|
+
for (const handler of this.tapHandlers) {
|
|
186
|
+
handler.onRemove?.(id, entity);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
removeMany(ids) {
|
|
190
|
+
for (const id of ids) {
|
|
191
|
+
this.removeOne(id);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
removeWhere(predicate) {
|
|
195
|
+
const idsToRemove = [];
|
|
196
|
+
for (const [id, entity] of this.storage) {
|
|
197
|
+
if (predicate(entity)) {
|
|
198
|
+
idsToRemove.push(id);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
let count = 0;
|
|
202
|
+
for (const id of idsToRemove) {
|
|
203
|
+
this.removeOne(id);
|
|
204
|
+
count++;
|
|
205
|
+
}
|
|
206
|
+
return count;
|
|
207
|
+
}
|
|
208
|
+
upsertOne(entity, opts) {
|
|
209
|
+
const id = opts?.selectId?.(entity) ?? this.selectId(entity);
|
|
210
|
+
if (this.storage.has(id)) {
|
|
211
|
+
this.updateOne(id, entity);
|
|
212
|
+
} else {
|
|
213
|
+
this.addOne(entity, opts);
|
|
214
|
+
}
|
|
215
|
+
return id;
|
|
216
|
+
}
|
|
217
|
+
upsertMany(entities, opts) {
|
|
218
|
+
return entities.map(e => this.upsertOne(e, opts));
|
|
219
|
+
}
|
|
220
|
+
clear() {
|
|
221
|
+
this.storage.clear();
|
|
222
|
+
this.nodeCache.clear();
|
|
223
|
+
this.updateSignals();
|
|
224
|
+
}
|
|
225
|
+
removeAll() {
|
|
226
|
+
this.clear();
|
|
227
|
+
}
|
|
228
|
+
setAll(entities, opts) {
|
|
229
|
+
this.clear();
|
|
230
|
+
this.addMany(entities, opts);
|
|
231
|
+
}
|
|
232
|
+
tap(handlers) {
|
|
233
|
+
this.tapHandlers.push(handlers);
|
|
234
|
+
return () => {
|
|
235
|
+
const idx = this.tapHandlers.indexOf(handlers);
|
|
236
|
+
if (idx > -1) this.tapHandlers.splice(idx, 1);
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
intercept(handlers) {
|
|
240
|
+
this.interceptHandlers.push(handlers);
|
|
241
|
+
return () => {
|
|
242
|
+
const idx = this.interceptHandlers.indexOf(handlers);
|
|
243
|
+
if (idx > -1) this.interceptHandlers.splice(idx, 1);
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
updateSignals() {
|
|
247
|
+
const entities = Array.from(this.storage.values());
|
|
248
|
+
const ids = Array.from(this.storage.keys());
|
|
249
|
+
const map = new Map(this.storage);
|
|
250
|
+
this.allSignal.set(entities);
|
|
251
|
+
this.countSignal.set(entities.length);
|
|
252
|
+
this.idsSignal.set(ids);
|
|
253
|
+
this.mapSignal.set(map);
|
|
254
|
+
}
|
|
255
|
+
getOrCreateNode(id, entity) {
|
|
256
|
+
let node = this.nodeCache.get(id);
|
|
257
|
+
if (!node) {
|
|
258
|
+
node = this.createEntityNode(id, entity);
|
|
259
|
+
this.nodeCache.set(id, node);
|
|
260
|
+
}
|
|
261
|
+
return node;
|
|
262
|
+
}
|
|
263
|
+
createEntityNode(id, entity) {
|
|
264
|
+
const node = () => this.storage.get(id);
|
|
265
|
+
for (const key of Object.keys(entity)) {
|
|
266
|
+
Object.defineProperty(node, key, {
|
|
267
|
+
get: () => {
|
|
268
|
+
const current = this.storage.get(id);
|
|
269
|
+
const value = current?.[key];
|
|
270
|
+
return () => value;
|
|
271
|
+
},
|
|
272
|
+
enumerable: true,
|
|
273
|
+
configurable: true
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return node;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export { EntitySignalImpl };
|