@hamak/ui-store-impl 0.1.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/.turbo/turbo-build.log +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +3 -0
- package/dist/core/middleware-registry.d.ts +21 -0
- package/dist/core/middleware-registry.d.ts.map +1 -0
- package/dist/core/middleware-registry.js +50 -0
- package/dist/core/reducer-registry.d.ts +18 -0
- package/dist/core/reducer-registry.d.ts.map +1 -0
- package/dist/core/reducer-registry.js +54 -0
- package/dist/core/store-manager.d.ts +26 -0
- package/dist/core/store-manager.d.ts.map +1 -0
- package/dist/core/store-manager.js +91 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/middleware/event-bridge-middleware.d.ts +7 -0
- package/dist/middleware/event-bridge-middleware.d.ts.map +1 -0
- package/dist/middleware/event-bridge-middleware.js +19 -0
- package/dist/middleware/index.d.ts +6 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +5 -0
- package/dist/middleware/logger-middleware.d.ts +7 -0
- package/dist/middleware/logger-middleware.d.ts.map +1 -0
- package/dist/middleware/logger-middleware.js +18 -0
- package/dist/plugin/index.d.ts +5 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +4 -0
- package/dist/plugin/store-plugin-factory.d.ts +22 -0
- package/dist/plugin/store-plugin-factory.d.ts.map +1 -0
- package/dist/plugin/store-plugin-factory.js +81 -0
- package/package.json +43 -0
- package/src/core/index.ts +3 -0
- package/src/core/middleware-registry.test.ts +247 -0
- package/src/core/middleware-registry.ts +64 -0
- package/src/core/reducer-registry.test.ts +215 -0
- package/src/core/reducer-registry.ts +71 -0
- package/src/core/store-manager.test.ts +288 -0
- package/src/core/store-manager.ts +125 -0
- package/src/index.ts +8 -0
- package/src/middleware/event-bridge-middleware.test.ts +131 -0
- package/src/middleware/event-bridge-middleware.ts +26 -0
- package/src/middleware/index.ts +6 -0
- package/src/middleware/logger-middleware.test.ts +129 -0
- package/src/middleware/logger-middleware.ts +25 -0
- package/src/plugin/index.ts +5 -0
- package/src/plugin/store-plugin-factory.ts +124 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MiddlewareRegistry Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
6
|
+
import { MiddlewareRegistry } from './middleware-registry';
|
|
7
|
+
import type { Middleware } from 'redux';
|
|
8
|
+
import type { MiddlewareRegistration } from '@amk/ui-store-api';
|
|
9
|
+
|
|
10
|
+
describe('MiddlewareRegistry', () => {
|
|
11
|
+
let registry: MiddlewareRegistry;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
registry = new MiddlewareRegistry();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('register', () => {
|
|
18
|
+
test('should register middleware', () => {
|
|
19
|
+
const middleware: Middleware = () => (next) => (action) => next(action);
|
|
20
|
+
const registration: MiddlewareRegistration = {
|
|
21
|
+
id: 'test-middleware',
|
|
22
|
+
middleware,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
registry.register(registration);
|
|
26
|
+
|
|
27
|
+
expect(registry.has('test-middleware')).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('should register middleware with default priority of 0', () => {
|
|
31
|
+
const middleware: Middleware = () => (next) => (action) => next(action);
|
|
32
|
+
const registration: MiddlewareRegistration = {
|
|
33
|
+
id: 'test-middleware',
|
|
34
|
+
middleware,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
registry.register(registration);
|
|
38
|
+
const info = registry.getInfo('test-middleware');
|
|
39
|
+
|
|
40
|
+
expect(info?.priority).toBe(0);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('should register middleware with custom priority', () => {
|
|
44
|
+
const middleware: Middleware = () => (next) => (action) => next(action);
|
|
45
|
+
const registration: MiddlewareRegistration = {
|
|
46
|
+
id: 'test-middleware',
|
|
47
|
+
middleware,
|
|
48
|
+
priority: 100,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
registry.register(registration);
|
|
52
|
+
const info = registry.getInfo('test-middleware');
|
|
53
|
+
|
|
54
|
+
expect(info?.priority).toBe(100);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('should throw error when registering after lock', () => {
|
|
58
|
+
const middleware: Middleware = () => (next) => (action) => next(action);
|
|
59
|
+
const registration: MiddlewareRegistration = {
|
|
60
|
+
id: 'test-middleware',
|
|
61
|
+
middleware,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
registry.lock();
|
|
65
|
+
|
|
66
|
+
expect(() => registry.register(registration)).toThrow(
|
|
67
|
+
/Cannot register middleware.*store already created/
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('should replace existing middleware with same id', () => {
|
|
72
|
+
const middleware1: Middleware = () => (next) => (action) => next(action);
|
|
73
|
+
const middleware2: Middleware = () => (next) => (action) => next(action);
|
|
74
|
+
|
|
75
|
+
registry.register({
|
|
76
|
+
id: 'test-middleware',
|
|
77
|
+
middleware: middleware1,
|
|
78
|
+
priority: 10,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
registry.register({
|
|
82
|
+
id: 'test-middleware',
|
|
83
|
+
middleware: middleware2,
|
|
84
|
+
priority: 20,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const info = registry.getInfo('test-middleware');
|
|
88
|
+
expect(info?.priority).toBe(20);
|
|
89
|
+
expect(info?.middleware).toBe(middleware2);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('unregister', () => {
|
|
94
|
+
test('should unregister middleware', () => {
|
|
95
|
+
const middleware: Middleware = () => (next) => (action) => next(action);
|
|
96
|
+
registry.register({
|
|
97
|
+
id: 'test-middleware',
|
|
98
|
+
middleware,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
registry.unregister('test-middleware');
|
|
102
|
+
|
|
103
|
+
expect(registry.has('test-middleware')).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('should not throw when unregistering non-existent middleware', () => {
|
|
107
|
+
expect(() => registry.unregister('non-existent')).not.toThrow();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('should throw error when unregistering after lock', () => {
|
|
111
|
+
const middleware: Middleware = () => (next) => (action) => next(action);
|
|
112
|
+
registry.register({
|
|
113
|
+
id: 'test-middleware',
|
|
114
|
+
middleware,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
registry.lock();
|
|
118
|
+
|
|
119
|
+
expect(() => registry.unregister('test-middleware')).toThrow(
|
|
120
|
+
/Cannot unregister middleware after store creation/
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('getAll', () => {
|
|
126
|
+
test('should return empty array when no middleware registered', () => {
|
|
127
|
+
expect(registry.getAll()).toEqual([]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('should return all middleware in priority order (highest first)', () => {
|
|
131
|
+
const middleware1: Middleware = () => (next) => (action) => next(action);
|
|
132
|
+
const middleware2: Middleware = () => (next) => (action) => next(action);
|
|
133
|
+
const middleware3: Middleware = () => (next) => (action) => next(action);
|
|
134
|
+
|
|
135
|
+
registry.register({ id: 'low', middleware: middleware1, priority: 10 });
|
|
136
|
+
registry.register({ id: 'high', middleware: middleware2, priority: 100 });
|
|
137
|
+
registry.register({ id: 'medium', middleware: middleware3, priority: 50 });
|
|
138
|
+
|
|
139
|
+
const all = registry.getAll();
|
|
140
|
+
|
|
141
|
+
expect(all).toEqual([middleware2, middleware3, middleware1]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('should handle middleware with same priority', () => {
|
|
145
|
+
const middleware1: Middleware = () => (next) => (action) => next(action);
|
|
146
|
+
const middleware2: Middleware = () => (next) => (action) => next(action);
|
|
147
|
+
|
|
148
|
+
registry.register({ id: 'first', middleware: middleware1, priority: 10 });
|
|
149
|
+
registry.register({ id: 'second', middleware: middleware2, priority: 10 });
|
|
150
|
+
|
|
151
|
+
const all = registry.getAll();
|
|
152
|
+
|
|
153
|
+
expect(all.length).toBe(2);
|
|
154
|
+
expect(all).toContain(middleware1);
|
|
155
|
+
expect(all).toContain(middleware2);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe('has', () => {
|
|
160
|
+
test('should return false for non-existent middleware', () => {
|
|
161
|
+
expect(registry.has('non-existent')).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('should return true for registered middleware', () => {
|
|
165
|
+
const middleware: Middleware = () => (next) => (action) => next(action);
|
|
166
|
+
registry.register({ id: 'test', middleware });
|
|
167
|
+
|
|
168
|
+
expect(registry.has('test')).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('getInfo', () => {
|
|
173
|
+
test('should return undefined for non-existent middleware', () => {
|
|
174
|
+
expect(registry.getInfo('non-existent')).toBeUndefined();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('should return registration info for existing middleware', () => {
|
|
178
|
+
const middleware: Middleware = () => (next) => (action) => next(action);
|
|
179
|
+
const registration: MiddlewareRegistration = {
|
|
180
|
+
id: 'test',
|
|
181
|
+
middleware,
|
|
182
|
+
priority: 100,
|
|
183
|
+
plugin: 'test-plugin',
|
|
184
|
+
description: 'Test middleware',
|
|
185
|
+
optional: true,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
registry.register(registration);
|
|
189
|
+
const info = registry.getInfo('test');
|
|
190
|
+
|
|
191
|
+
expect(info).toEqual(registration);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('getAllRegistrations', () => {
|
|
196
|
+
test('should return empty array when no middleware registered', () => {
|
|
197
|
+
expect(registry.getAllRegistrations()).toEqual([]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('should return all registrations', () => {
|
|
201
|
+
const middleware1: Middleware = () => (next) => (action) => next(action);
|
|
202
|
+
const middleware2: Middleware = () => (next) => (action) => next(action);
|
|
203
|
+
|
|
204
|
+
registry.register({ id: 'first', middleware: middleware1 });
|
|
205
|
+
registry.register({ id: 'second', middleware: middleware2 });
|
|
206
|
+
|
|
207
|
+
const registrations = registry.getAllRegistrations();
|
|
208
|
+
|
|
209
|
+
expect(registrations.length).toBe(2);
|
|
210
|
+
expect(registrations.map((r) => r.id)).toContain('first');
|
|
211
|
+
expect(registrations.map((r) => r.id)).toContain('second');
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('lock', () => {
|
|
216
|
+
test('should prevent further registration', () => {
|
|
217
|
+
const middleware: Middleware = () => (next) => (action) => next(action);
|
|
218
|
+
|
|
219
|
+
registry.lock();
|
|
220
|
+
|
|
221
|
+
expect(() =>
|
|
222
|
+
registry.register({ id: 'test', middleware })
|
|
223
|
+
).toThrow();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('should prevent further unregistration', () => {
|
|
227
|
+
const middleware: Middleware = () => (next) => (action) => next(action);
|
|
228
|
+
registry.register({ id: 'test', middleware });
|
|
229
|
+
|
|
230
|
+
registry.lock();
|
|
231
|
+
|
|
232
|
+
expect(() => registry.unregister('test')).toThrow();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test('should not affect read operations', () => {
|
|
236
|
+
const middleware: Middleware = () => (next) => (action) => next(action);
|
|
237
|
+
registry.register({ id: 'test', middleware });
|
|
238
|
+
|
|
239
|
+
registry.lock();
|
|
240
|
+
|
|
241
|
+
expect(registry.has('test')).toBe(true);
|
|
242
|
+
expect(registry.getInfo('test')).toBeDefined();
|
|
243
|
+
expect(registry.getAll().length).toBe(1);
|
|
244
|
+
expect(registry.getAllRegistrations().length).toBe(1);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware Registry Implementation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Middleware } from 'redux';
|
|
6
|
+
import type { IMiddlewareRegistry, MiddlewareRegistration } from '@amk/ui-store-api';
|
|
7
|
+
|
|
8
|
+
export class MiddlewareRegistry implements IMiddlewareRegistry {
|
|
9
|
+
private registrations = new Map<string, MiddlewareRegistration>();
|
|
10
|
+
private locked = false;
|
|
11
|
+
|
|
12
|
+
register(registration: MiddlewareRegistration): void {
|
|
13
|
+
if (this.locked) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`Cannot register middleware "${registration.id}" - store already created. ` +
|
|
16
|
+
`Register middleware during plugin initialization phase.`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (this.registrations.has(registration.id)) {
|
|
21
|
+
console.warn(`[MiddlewareRegistry] Middleware "${registration.id}" already registered, overwriting.`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.registrations.set(registration.id, {
|
|
25
|
+
priority: 0,
|
|
26
|
+
...registration,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
unregister(id: string): void {
|
|
31
|
+
if (this.locked) {
|
|
32
|
+
throw new Error(`Cannot unregister middleware after store creation`);
|
|
33
|
+
}
|
|
34
|
+
this.registrations.delete(id);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getAll(): Middleware[] {
|
|
38
|
+
const sorted = Array.from(this.registrations.values())
|
|
39
|
+
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
40
|
+
|
|
41
|
+
return sorted.map(reg => reg.middleware);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
has(id: string): boolean {
|
|
45
|
+
return this.registrations.has(id);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
getInfo(id: string): MiddlewareRegistration | undefined {
|
|
49
|
+
return this.registrations.get(id);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getAllRegistrations(): MiddlewareRegistration[] {
|
|
53
|
+
return Array.from(this.registrations.values())
|
|
54
|
+
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Lock the registry (called when store is created)
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
lock(): void {
|
|
62
|
+
this.locked = true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReducerRegistry Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, beforeEach, mock } from 'bun:test';
|
|
6
|
+
import { ReducerRegistry } from './reducer-registry';
|
|
7
|
+
import type { Reducer } from 'redux';
|
|
8
|
+
|
|
9
|
+
describe('ReducerRegistry', () => {
|
|
10
|
+
let registry: ReducerRegistry;
|
|
11
|
+
let onReducerChange: ReturnType<typeof mock>;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
onReducerChange = mock(() => {});
|
|
15
|
+
registry = new ReducerRegistry(onReducerChange);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('register', () => {
|
|
19
|
+
test('should register a reducer', () => {
|
|
20
|
+
const reducer: Reducer = (state = {}) => state;
|
|
21
|
+
|
|
22
|
+
registry.register('test', reducer);
|
|
23
|
+
|
|
24
|
+
expect(registry.has('test')).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('should call onReducerChange when reducer is registered', () => {
|
|
28
|
+
const reducer: Reducer = (state = {}) => state;
|
|
29
|
+
|
|
30
|
+
registry.register('test', reducer);
|
|
31
|
+
|
|
32
|
+
expect(onReducerChange).toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('should replace existing reducer when replace flag is true', () => {
|
|
36
|
+
const reducer1: Reducer = (state = { value: 1 }) => state;
|
|
37
|
+
const reducer2: Reducer = (state = { value: 2 }) => state;
|
|
38
|
+
|
|
39
|
+
registry.register('test', reducer1);
|
|
40
|
+
registry.register('test', reducer2, true);
|
|
41
|
+
|
|
42
|
+
expect(registry.has('test')).toBe(true);
|
|
43
|
+
expect(onReducerChange).toHaveBeenCalledTimes(2);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should warn when replacing without replace flag', () => {
|
|
47
|
+
const reducer1: Reducer = (state = {}) => state;
|
|
48
|
+
const reducer2: Reducer = (state = {}) => state;
|
|
49
|
+
|
|
50
|
+
registry.register('test', reducer1);
|
|
51
|
+
|
|
52
|
+
// Should not replace - just warn
|
|
53
|
+
registry.register('test', reducer2, false);
|
|
54
|
+
|
|
55
|
+
// Reducer should still be the first one
|
|
56
|
+
const info = registry.getInfo('test');
|
|
57
|
+
expect(info?.reducer).toBe(reducer1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('should not call onReducerChange when no callback provided', () => {
|
|
61
|
+
const registryWithoutCallback = new ReducerRegistry();
|
|
62
|
+
const reducer: Reducer = (state = {}) => state;
|
|
63
|
+
|
|
64
|
+
expect(() => registryWithoutCallback.register('test', reducer)).not.toThrow();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('unregister', () => {
|
|
69
|
+
test('should unregister a reducer', () => {
|
|
70
|
+
const reducer: Reducer = (state = {}) => state;
|
|
71
|
+
registry.register('test', reducer);
|
|
72
|
+
|
|
73
|
+
registry.unregister('test');
|
|
74
|
+
|
|
75
|
+
expect(registry.has('test')).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('should call onReducerChange when reducer is unregistered', () => {
|
|
79
|
+
const reducer: Reducer = (state = {}) => state;
|
|
80
|
+
registry.register('test', reducer);
|
|
81
|
+
onReducerChange.mockClear();
|
|
82
|
+
|
|
83
|
+
registry.unregister('test');
|
|
84
|
+
|
|
85
|
+
expect(onReducerChange).toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('should not throw when unregistering non-existent reducer', () => {
|
|
89
|
+
expect(() => registry.unregister('non-existent')).not.toThrow();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('has', () => {
|
|
94
|
+
test('should return false for non-existent reducer', () => {
|
|
95
|
+
expect(registry.has('non-existent')).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('should return true for registered reducer', () => {
|
|
99
|
+
const reducer: Reducer = (state = {}) => state;
|
|
100
|
+
registry.register('test', reducer);
|
|
101
|
+
|
|
102
|
+
expect(registry.has('test')).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('getInfo', () => {
|
|
107
|
+
test('should return undefined for non-existent reducer', () => {
|
|
108
|
+
expect(registry.getInfo('non-existent')).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('should return registration info for existing key', () => {
|
|
112
|
+
const reducer: Reducer = (state = {}) => state;
|
|
113
|
+
registry.register('test', reducer);
|
|
114
|
+
|
|
115
|
+
const info = registry.getInfo('test');
|
|
116
|
+
expect(info?.reducer).toBe(reducer);
|
|
117
|
+
expect(info?.key).toBe('test');
|
|
118
|
+
expect(info?.registeredAt).toBeInstanceOf(Date);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('getAll', () => {
|
|
123
|
+
test('should return empty object when no reducers registered', () => {
|
|
124
|
+
expect(registry.getAll()).toEqual({});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('should return all reducers as an object', () => {
|
|
128
|
+
const reducer1: Reducer = (state = {}) => state;
|
|
129
|
+
const reducer2: Reducer = (state = {}) => state;
|
|
130
|
+
|
|
131
|
+
registry.register('first', reducer1);
|
|
132
|
+
registry.register('second', reducer2);
|
|
133
|
+
|
|
134
|
+
const all = registry.getAll();
|
|
135
|
+
|
|
136
|
+
expect(all).toEqual({
|
|
137
|
+
first: reducer1,
|
|
138
|
+
second: reducer2,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe('getAllRegistrations', () => {
|
|
144
|
+
test('should return empty array when no reducers registered', () => {
|
|
145
|
+
expect(registry.getAllRegistrations()).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('should return all registrations with metadata', () => {
|
|
149
|
+
const reducer1: Reducer = (state = {}) => state;
|
|
150
|
+
const reducer2: Reducer = (state = {}) => state;
|
|
151
|
+
|
|
152
|
+
registry.register('first', reducer1);
|
|
153
|
+
registry.register('second', reducer2);
|
|
154
|
+
|
|
155
|
+
const registrations = registry.getAllRegistrations();
|
|
156
|
+
|
|
157
|
+
expect(registrations.length).toBe(2);
|
|
158
|
+
expect(registrations[0]).toHaveProperty('key');
|
|
159
|
+
expect(registrations[0]).toHaveProperty('reducer');
|
|
160
|
+
expect(registrations[0]).toHaveProperty('registeredAt');
|
|
161
|
+
expect(registrations[0].registeredAt).toBeInstanceOf(Date);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('getCombinedReducer', () => {
|
|
166
|
+
test('should return identity reducer when no reducers registered', () => {
|
|
167
|
+
const combined = registry.getCombinedReducer();
|
|
168
|
+
const state = { test: 'value' };
|
|
169
|
+
|
|
170
|
+
expect(combined(state, { type: 'TEST' })).toEqual(state);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('should combine multiple reducers', () => {
|
|
174
|
+
const counterReducer: Reducer = (state = 0, action: any) => {
|
|
175
|
+
if (action.type === 'INCREMENT') return state + 1;
|
|
176
|
+
return state;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const nameReducer: Reducer = (state = '', action: any) => {
|
|
180
|
+
if (action.type === 'SET_NAME') return action.payload;
|
|
181
|
+
return state;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
registry.register('counter', counterReducer);
|
|
185
|
+
registry.register('name', nameReducer);
|
|
186
|
+
|
|
187
|
+
const combined = registry.getCombinedReducer();
|
|
188
|
+
let state = combined(undefined, { type: '@@INIT' });
|
|
189
|
+
|
|
190
|
+
expect(state).toEqual({ counter: 0, name: '' });
|
|
191
|
+
|
|
192
|
+
state = combined(state, { type: 'INCREMENT' });
|
|
193
|
+
expect(state).toEqual({ counter: 1, name: '' });
|
|
194
|
+
|
|
195
|
+
state = combined(state, { type: 'SET_NAME', payload: 'Test' });
|
|
196
|
+
expect(state).toEqual({ counter: 1, name: 'Test' });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('should update combined reducer when new reducer is registered', () => {
|
|
200
|
+
const reducer1: Reducer = (state = 1) => state;
|
|
201
|
+
registry.register('first', reducer1);
|
|
202
|
+
|
|
203
|
+
const combined1 = registry.getCombinedReducer();
|
|
204
|
+
const state1 = combined1(undefined, { type: '@@INIT' });
|
|
205
|
+
expect(state1).toEqual({ first: 1 });
|
|
206
|
+
|
|
207
|
+
const reducer2: Reducer = (state = 2) => state;
|
|
208
|
+
registry.register('second', reducer2);
|
|
209
|
+
|
|
210
|
+
const combined2 = registry.getCombinedReducer();
|
|
211
|
+
const state2 = combined2(undefined, { type: '@@INIT' });
|
|
212
|
+
expect(state2).toEqual({ first: 1, second: 2 });
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reducer Registry Implementation
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { combineReducers, type Reducer } from 'redux';
|
|
6
|
+
import type { IReducerRegistry, ReducerMap, ReducerRegistration } from '@amk/ui-store-api';
|
|
7
|
+
|
|
8
|
+
export class ReducerRegistry implements IReducerRegistry {
|
|
9
|
+
private reducers = new Map<string, ReducerRegistration>();
|
|
10
|
+
private onReducerChange?: (rootReducer: Reducer) => void;
|
|
11
|
+
|
|
12
|
+
constructor(onReducerChange?: (rootReducer: Reducer) => void) {
|
|
13
|
+
this.onReducerChange = onReducerChange;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
register(key: string, reducer: Reducer, replace = false): void {
|
|
17
|
+
if (!replace && this.reducers.has(key)) {
|
|
18
|
+
console.warn(`[ReducerRegistry] Reducer "${key}" already registered. Use replace=true to override.`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.reducers.set(key, {
|
|
23
|
+
key,
|
|
24
|
+
reducer,
|
|
25
|
+
registeredAt: new Date(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Notify about reducer change for hot replacement
|
|
29
|
+
if (this.onReducerChange) {
|
|
30
|
+
this.onReducerChange(this.getCombinedReducer());
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
unregister(key: string): void {
|
|
35
|
+
this.reducers.delete(key);
|
|
36
|
+
|
|
37
|
+
if (this.onReducerChange) {
|
|
38
|
+
this.onReducerChange(this.getCombinedReducer());
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
getAll(): ReducerMap {
|
|
43
|
+
const map: ReducerMap = {};
|
|
44
|
+
this.reducers.forEach((registration, key) => {
|
|
45
|
+
map[key] = registration.reducer;
|
|
46
|
+
});
|
|
47
|
+
return map;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getCombinedReducer(): Reducer {
|
|
51
|
+
const reducerMap = this.getAll();
|
|
52
|
+
|
|
53
|
+
if (Object.keys(reducerMap).length === 0) {
|
|
54
|
+
return (state = {}) => state;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return combineReducers(reducerMap);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
has(key: string): boolean {
|
|
61
|
+
return this.reducers.has(key);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getInfo(key: string): ReducerRegistration | undefined {
|
|
65
|
+
return this.reducers.get(key);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getAllRegistrations(): ReducerRegistration[] {
|
|
69
|
+
return Array.from(this.reducers.values());
|
|
70
|
+
}
|
|
71
|
+
}
|