@signaltree/core 4.2.0 → 5.0.0
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 +9 -18
- package/dist/enhancers/entities/lib/entities.js +59 -105
- package/dist/enhancers/memoization/lib/memoization.js +20 -0
- package/dist/enhancers/presets/lib/presets.js +1 -2
- package/dist/enhancers/serialization/lib/serialization.js +24 -20
- package/dist/index.js +2 -3
- package/dist/lib/entity-signal.js +280 -0
- package/dist/lib/path-notifier.js +106 -0
- package/dist/lib/signal-tree.js +12 -12
- package/dist/lib/types.js +7 -1
- package/package.json +1 -1
- package/src/enhancers/entities/lib/entities.d.ts +14 -16
- package/src/enhancers/presets/lib/presets.d.ts +1 -1
- package/src/enhancers/types.d.ts +0 -31
- package/src/index.d.ts +3 -3
- package/src/{enhancers/middleware/lib → lib}/async-helpers.d.ts +1 -1
- package/src/lib/entity-signal.d.ts +1 -0
- package/src/lib/path-notifier.d.ts +4 -0
- package/src/lib/types.d.ts +127 -7
- package/dist/enhancers/middleware/lib/middleware.js +0 -156
- package/src/enhancers/middleware/index.d.ts +0 -2
- package/src/enhancers/middleware/jest.config.d.ts +0 -15
- package/src/enhancers/middleware/lib/middleware.d.ts +0 -11
- package/src/enhancers/middleware/test-setup.d.ts +0 -1
- /package/dist/{enhancers/middleware/lib → lib}/async-helpers.js +0 -0
package/src/lib/types.d.ts
CHANGED
|
@@ -90,8 +90,6 @@ export type SignalTree<T> = NodeAccessor<T> & {
|
|
|
90
90
|
};
|
|
91
91
|
};
|
|
92
92
|
getMetrics(): PerformanceMetrics;
|
|
93
|
-
addTap(middleware: Middleware<T>): void;
|
|
94
|
-
removeTap(id: string): void;
|
|
95
93
|
entities<E extends {
|
|
96
94
|
id: string | number;
|
|
97
95
|
}>(entityKey?: keyof T): EntityHelpers<E>;
|
|
@@ -120,11 +118,6 @@ export interface TreeConfig {
|
|
|
120
118
|
useStructuralSharing?: boolean;
|
|
121
119
|
security?: SecurityValidatorConfig;
|
|
122
120
|
}
|
|
123
|
-
export interface Middleware<T> {
|
|
124
|
-
id: string;
|
|
125
|
-
before?: (action: string, payload: unknown, state: T) => boolean;
|
|
126
|
-
after?: (action: string, payload: unknown, state: T, newState: T) => void;
|
|
127
|
-
}
|
|
128
121
|
export interface PerformanceMetrics {
|
|
129
122
|
updates: number;
|
|
130
123
|
computations: number;
|
|
@@ -132,6 +125,83 @@ export interface PerformanceMetrics {
|
|
|
132
125
|
cacheMisses: number;
|
|
133
126
|
averageUpdateTime: number;
|
|
134
127
|
}
|
|
128
|
+
export interface EntityConfig<E, K extends string | number = string> {
|
|
129
|
+
selectId?: (entity: E) => K;
|
|
130
|
+
hooks?: {
|
|
131
|
+
beforeAdd?: (entity: E) => E | false;
|
|
132
|
+
beforeUpdate?: (id: K, changes: Partial<E>) => Partial<E> | false;
|
|
133
|
+
beforeRemove?: (id: K, entity: E) => boolean;
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export interface EntityMapMarker<E, K extends string | number> {
|
|
137
|
+
readonly __entityType?: E;
|
|
138
|
+
readonly __keyType?: K;
|
|
139
|
+
readonly __isEntityMap?: true;
|
|
140
|
+
readonly __entityMapConfig?: EntityConfig<E, K>;
|
|
141
|
+
}
|
|
142
|
+
export declare function entityMap<E, K extends string | number = E extends {
|
|
143
|
+
id: infer I extends string | number;
|
|
144
|
+
} ? I : string>(config?: EntityConfig<E, K>): EntityMapMarker<E, K>;
|
|
145
|
+
export interface MutationOptions {
|
|
146
|
+
onError?: (error: Error) => void;
|
|
147
|
+
}
|
|
148
|
+
export interface AddOptions<E, K> extends MutationOptions {
|
|
149
|
+
selectId?: (entity: E) => K;
|
|
150
|
+
}
|
|
151
|
+
export interface AddManyOptions<E, K> extends AddOptions<E, K> {
|
|
152
|
+
mode?: 'strict' | 'skip' | 'overwrite';
|
|
153
|
+
}
|
|
154
|
+
export interface TapHandlers<E, K extends string | number> {
|
|
155
|
+
onAdd?: (entity: E, id: K) => void;
|
|
156
|
+
onUpdate?: (id: K, changes: Partial<E>, entity: E) => void;
|
|
157
|
+
onRemove?: (id: K, entity: E) => void;
|
|
158
|
+
onChange?: () => void;
|
|
159
|
+
}
|
|
160
|
+
export interface InterceptContext<T> {
|
|
161
|
+
block(reason?: string): void;
|
|
162
|
+
transform(value: T): void;
|
|
163
|
+
readonly blocked: boolean;
|
|
164
|
+
readonly blockReason: string | undefined;
|
|
165
|
+
}
|
|
166
|
+
export interface InterceptHandlers<E, K extends string | number> {
|
|
167
|
+
onAdd?: (entity: E, ctx: InterceptContext<E>) => void | Promise<void>;
|
|
168
|
+
onUpdate?: (id: K, changes: Partial<E>, ctx: InterceptContext<Partial<E>>) => void | Promise<void>;
|
|
169
|
+
onRemove?: (id: K, entity: E, ctx: InterceptContext<void>) => void | Promise<void>;
|
|
170
|
+
}
|
|
171
|
+
export type EntityNode<E> = {
|
|
172
|
+
(): E;
|
|
173
|
+
(value: E): void;
|
|
174
|
+
(updater: (current: E) => E): void;
|
|
175
|
+
} & {
|
|
176
|
+
[P in keyof E]: E[P] extends object ? E[P] extends readonly unknown[] ? CallableWritableSignal<E[P]> : EntityNode<E[P]> : CallableWritableSignal<E[P]>;
|
|
177
|
+
};
|
|
178
|
+
export interface EntitySignal<E, K extends string | number = string> {
|
|
179
|
+
byId(id: K): EntityNode<E> | undefined;
|
|
180
|
+
byIdOrFail(id: K): EntityNode<E>;
|
|
181
|
+
all(): Signal<E[]>;
|
|
182
|
+
count(): Signal<number>;
|
|
183
|
+
ids(): Signal<K[]>;
|
|
184
|
+
has(id: K): Signal<boolean>;
|
|
185
|
+
isEmpty(): Signal<boolean>;
|
|
186
|
+
map(): Signal<ReadonlyMap<K, E>>;
|
|
187
|
+
where(predicate: (entity: E) => boolean): Signal<E[]>;
|
|
188
|
+
find(predicate: (entity: E) => boolean): Signal<E | undefined>;
|
|
189
|
+
addOne(entity: E, opts?: AddOptions<E, K>): K;
|
|
190
|
+
addMany(entities: E[], opts?: AddManyOptions<E, K>): K[];
|
|
191
|
+
updateOne(id: K, changes: Partial<E>, opts?: MutationOptions): void;
|
|
192
|
+
updateMany(ids: K[], changes: Partial<E>, opts?: MutationOptions): void;
|
|
193
|
+
updateWhere(predicate: (entity: E) => boolean, changes: Partial<E>): number;
|
|
194
|
+
upsertOne(entity: E, opts?: AddOptions<E, K>): K;
|
|
195
|
+
upsertMany(entities: E[], opts?: AddOptions<E, K>): K[];
|
|
196
|
+
removeOne(id: K, opts?: MutationOptions): void;
|
|
197
|
+
removeMany(ids: K[], opts?: MutationOptions): void;
|
|
198
|
+
removeWhere(predicate: (entity: E) => boolean): number;
|
|
199
|
+
clear(): void;
|
|
200
|
+
removeAll(): void;
|
|
201
|
+
setAll(entities: E[], opts?: AddOptions<E, K>): void;
|
|
202
|
+
tap(handlers: TapHandlers<E, K>): () => void;
|
|
203
|
+
intercept(handlers: InterceptHandlers<E, K>): () => void;
|
|
204
|
+
}
|
|
135
205
|
export interface EntityHelpers<E extends {
|
|
136
206
|
id: string | number;
|
|
137
207
|
}> {
|
|
@@ -146,6 +216,56 @@ export interface EntityHelpers<E extends {
|
|
|
146
216
|
selectTotal(): Signal<number>;
|
|
147
217
|
clear(): void;
|
|
148
218
|
}
|
|
219
|
+
export interface LoggingConfig {
|
|
220
|
+
name?: string;
|
|
221
|
+
filter?: (path: string) => boolean;
|
|
222
|
+
collapsed?: boolean;
|
|
223
|
+
onLog?: (entry: LogEntry) => void;
|
|
224
|
+
}
|
|
225
|
+
export interface LogEntry {
|
|
226
|
+
path: string;
|
|
227
|
+
prev: unknown;
|
|
228
|
+
value: unknown;
|
|
229
|
+
timestamp: number;
|
|
230
|
+
}
|
|
231
|
+
export interface ValidationConfig<T> {
|
|
232
|
+
validators: Array<{
|
|
233
|
+
match: (path: string) => boolean;
|
|
234
|
+
validate: (value: unknown, path: string) => void | never;
|
|
235
|
+
}>;
|
|
236
|
+
onError?: (error: Error, path: string) => void;
|
|
237
|
+
}
|
|
238
|
+
export interface PersistenceConfig {
|
|
239
|
+
key: string;
|
|
240
|
+
storage?: Storage;
|
|
241
|
+
debounceMs?: number;
|
|
242
|
+
filter?: (path: string) => boolean;
|
|
243
|
+
serialize?: (state: unknown) => string;
|
|
244
|
+
deserialize?: (json: string) => unknown;
|
|
245
|
+
}
|
|
246
|
+
export interface DevToolsConfig {
|
|
247
|
+
name?: string;
|
|
248
|
+
maxAge?: number;
|
|
249
|
+
features?: {
|
|
250
|
+
jump?: boolean;
|
|
251
|
+
skip?: boolean;
|
|
252
|
+
reorder?: boolean;
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
export type EntityType<T> = T extends EntitySignal<infer E, any> ? E : never;
|
|
256
|
+
export type EntityKeyType<T> = T extends EntitySignal<unknown, infer K> ? K : never;
|
|
257
|
+
export type IsEntityMap<T> = T extends EntityMapMarker<unknown, any> ? true : false;
|
|
258
|
+
export type EntityAwareTreeNode<T> = {
|
|
259
|
+
[K in keyof T]: T[K] extends EntityMapMarker<infer E, infer Key> ? EntitySignal<E, Key> : T[K] extends object ? EntityAwareTreeNode<T[K]> : CallableWritableSignal<T[K]>;
|
|
260
|
+
};
|
|
261
|
+
export type PathHandler = (value: unknown, prev: unknown, path: string) => void;
|
|
262
|
+
export type PathInterceptor = (ctx: {
|
|
263
|
+
path: string;
|
|
264
|
+
value: unknown;
|
|
265
|
+
prev: unknown;
|
|
266
|
+
blocked: boolean;
|
|
267
|
+
blockReason?: string;
|
|
268
|
+
}, next: () => void) => void | Promise<void>;
|
|
149
269
|
export interface TimeTravelEntry<T> {
|
|
150
270
|
action: string;
|
|
151
271
|
timestamp: number;
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
const middlewareMap = new WeakMap();
|
|
2
|
-
function withMiddleware(middlewares = []) {
|
|
3
|
-
return tree => {
|
|
4
|
-
middlewareMap.set(tree, [...middlewares]);
|
|
5
|
-
const originalTreeCall = tree.bind(tree);
|
|
6
|
-
const enhancedTree = function (...args) {
|
|
7
|
-
if (args.length === 0) {
|
|
8
|
-
return originalTreeCall();
|
|
9
|
-
} else {
|
|
10
|
-
const action = 'UPDATE';
|
|
11
|
-
const currentState = originalTreeCall();
|
|
12
|
-
const treeMiddlewares = middlewareMap.get(tree) || [];
|
|
13
|
-
let updateResult;
|
|
14
|
-
if (args.length === 1) {
|
|
15
|
-
const arg = args[0];
|
|
16
|
-
if (typeof arg === 'function') {
|
|
17
|
-
updateResult = arg(currentState);
|
|
18
|
-
} else {
|
|
19
|
-
updateResult = arg;
|
|
20
|
-
}
|
|
21
|
-
} else {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
for (const middleware of treeMiddlewares) {
|
|
25
|
-
if (middleware.before && !middleware.before(action, updateResult, currentState)) {
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
let isNoMutation = true;
|
|
30
|
-
if (updateResult && typeof updateResult === 'object') {
|
|
31
|
-
for (const k in updateResult) {
|
|
32
|
-
if (!Object.prototype.hasOwnProperty.call(updateResult, k)) continue;
|
|
33
|
-
const nextVal = updateResult[k];
|
|
34
|
-
const prevVal = currentState[k];
|
|
35
|
-
if (nextVal !== prevVal) {
|
|
36
|
-
isNoMutation = false;
|
|
37
|
-
break;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
if (isNoMutation) {
|
|
42
|
-
for (const middleware of treeMiddlewares) {
|
|
43
|
-
if (middleware.after) {
|
|
44
|
-
middleware.after(action, updateResult, currentState, currentState);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
const previousState = currentState;
|
|
50
|
-
if (args.length === 1) {
|
|
51
|
-
const arg = args[0];
|
|
52
|
-
if (typeof arg === 'function') {
|
|
53
|
-
originalTreeCall(arg);
|
|
54
|
-
} else {
|
|
55
|
-
originalTreeCall(arg);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
const newState = originalTreeCall();
|
|
59
|
-
for (const middleware of treeMiddlewares) {
|
|
60
|
-
if (middleware.after) {
|
|
61
|
-
middleware.after(action, updateResult, previousState, newState);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
Object.setPrototypeOf(enhancedTree, Object.getPrototypeOf(tree));
|
|
67
|
-
Object.assign(enhancedTree, tree);
|
|
68
|
-
if ('state' in tree) {
|
|
69
|
-
Object.defineProperty(enhancedTree, 'state', {
|
|
70
|
-
value: tree.state,
|
|
71
|
-
enumerable: false,
|
|
72
|
-
configurable: true
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
if ('$' in tree) {
|
|
76
|
-
Object.defineProperty(enhancedTree, '$', {
|
|
77
|
-
value: tree['$'],
|
|
78
|
-
enumerable: false,
|
|
79
|
-
configurable: true
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
enhancedTree.addTap = middleware => {
|
|
83
|
-
const treeMiddlewares = middlewareMap.get(tree) || [];
|
|
84
|
-
const existingIndex = treeMiddlewares.findIndex(m => m.id === middleware.id);
|
|
85
|
-
if (existingIndex >= 0) {
|
|
86
|
-
treeMiddlewares[existingIndex] = middleware;
|
|
87
|
-
} else {
|
|
88
|
-
treeMiddlewares.push(middleware);
|
|
89
|
-
}
|
|
90
|
-
middlewareMap.set(tree, treeMiddlewares);
|
|
91
|
-
};
|
|
92
|
-
enhancedTree.removeTap = id => {
|
|
93
|
-
const treeMiddlewares = middlewareMap.get(tree) || [];
|
|
94
|
-
const filtered = treeMiddlewares.filter(m => m.id !== id);
|
|
95
|
-
middlewareMap.set(tree, filtered);
|
|
96
|
-
};
|
|
97
|
-
const originalBatchUpdate = tree.batchUpdate;
|
|
98
|
-
if (originalBatchUpdate) {
|
|
99
|
-
enhancedTree.batchUpdate = updater => {
|
|
100
|
-
const action = 'BATCH_UPDATE';
|
|
101
|
-
const currentState = originalTreeCall();
|
|
102
|
-
const updateResult = updater(currentState);
|
|
103
|
-
const treeMiddlewares = middlewareMap.get(tree) || [];
|
|
104
|
-
for (const middleware of treeMiddlewares) {
|
|
105
|
-
if (middleware.before && !middleware.before(action, updateResult, currentState)) {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
const previousState = currentState;
|
|
110
|
-
originalBatchUpdate.call(tree, updater);
|
|
111
|
-
const newState = originalTreeCall();
|
|
112
|
-
for (const middleware of treeMiddlewares) {
|
|
113
|
-
if (middleware.after) {
|
|
114
|
-
middleware.after(action, updateResult, previousState, newState);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
const originalDestroy = tree.destroy;
|
|
120
|
-
enhancedTree.destroy = () => {
|
|
121
|
-
middlewareMap.delete(tree);
|
|
122
|
-
if (originalDestroy) {
|
|
123
|
-
originalDestroy.call(tree);
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
return enhancedTree;
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
function createLoggingMiddleware(treeName) {
|
|
130
|
-
return {
|
|
131
|
-
id: 'logging',
|
|
132
|
-
before: (action, payload, state) => {
|
|
133
|
-
console.group(`🏪 ${treeName}: ${action}`);
|
|
134
|
-
console.log('Previous state:', state);
|
|
135
|
-
console.log('Payload:', typeof payload === 'function' ? 'Function' : payload);
|
|
136
|
-
return true;
|
|
137
|
-
},
|
|
138
|
-
after: (action, payload, state, newState) => {
|
|
139
|
-
console.log('New state:', newState);
|
|
140
|
-
console.groupEnd();
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
function createValidationMiddleware(validator) {
|
|
145
|
-
return {
|
|
146
|
-
id: 'validation',
|
|
147
|
-
after: (action, payload, state, newState) => {
|
|
148
|
-
const error = validator(newState);
|
|
149
|
-
if (error) {
|
|
150
|
-
console.error(`Validation failed after ${action}:`, error);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export { createLoggingMiddleware, createValidationMiddleware, withMiddleware };
|
|
@@ -1,15 +0,0 @@
|
|
|
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;
|
|
@@ -1,11 +0,0 @@
|
|
|
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>;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
File without changes
|