@hamak/ui-store-impl 0.4.6 → 0.4.16

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.
@@ -1,215 +0,0 @@
1
- /**
2
- * ReducerRegistry Tests
3
- */
4
-
5
- import { describe, test, expect, beforeEach, vi } from 'vitest';
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 vi.fn>;
12
-
13
- beforeEach(() => {
14
- onReducerChange = vi.fn(() => {});
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
- });
@@ -1,71 +0,0 @@
1
- /**
2
- * Reducer Registry Implementation
3
- */
4
-
5
- import { combineReducers, type Reducer } from 'redux';
6
- import type { IReducerRegistry, ReducerMap, ReducerRegistration } from '@hamak/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
- }
@@ -1,288 +0,0 @@
1
- /**
2
- * StoreManager Tests
3
- */
4
-
5
- import { describe, test, expect, beforeEach, vi } from 'vitest';
6
- import { StoreManager } from './store-manager';
7
- import type { Middleware, Reducer } from 'redux';
8
-
9
- describe('StoreManager', () => {
10
- let manager: StoreManager;
11
-
12
- beforeEach(() => {
13
- manager = new StoreManager();
14
- });
15
-
16
- describe('initialization', () => {
17
- test('should not be initialized by default', () => {
18
- expect(manager.isInitialized()).toBe(false);
19
- });
20
-
21
- test('should throw error when getting store before initialization', () => {
22
- expect(() => manager.getStore()).toThrow(
23
- /Store not initialized/
24
- );
25
- });
26
-
27
- test('should initialize store', () => {
28
- const store = manager.initialize();
29
-
30
- expect(manager.isInitialized()).toBe(true);
31
- expect(store).toBeDefined();
32
- expect(store.getState).toBeDefined();
33
- expect(store.dispatch).toBeDefined();
34
- });
35
-
36
- test('should throw error when initializing twice', () => {
37
- manager.initialize();
38
-
39
- expect(() => manager.initialize()).toThrow(
40
- /Store already initialized/
41
- );
42
- });
43
-
44
- test('should lock middleware registry after initialization', () => {
45
- const middleware: Middleware = () => (next) => (action) => next(action);
46
-
47
- manager.initialize();
48
-
49
- expect(() =>
50
- manager.getMiddlewareRegistry().register({
51
- id: 'test',
52
- middleware,
53
- })
54
- ).toThrow(/store already created/);
55
- });
56
- });
57
-
58
- describe('middleware registration', () => {
59
- test('should register middleware before initialization', () => {
60
- const middleware: Middleware = () => (next) => (action) => next(action);
61
-
62
- manager.getMiddlewareRegistry().register({
63
- id: 'test',
64
- middleware,
65
- priority: 100,
66
- });
67
-
68
- expect(manager.getMiddlewareRegistry().has('test')).toBe(true);
69
- });
70
-
71
- test('should apply middleware in priority order', () => {
72
- const callOrder: string[] = [];
73
-
74
- const middleware1: Middleware = () => (next) => (action: any) => {
75
- callOrder.push('middleware1');
76
- return next(action);
77
- };
78
-
79
- const middleware2: Middleware = () => (next) => (action: any) => {
80
- callOrder.push('middleware2');
81
- return next(action);
82
- };
83
-
84
- const middleware3: Middleware = () => (next) => (action: any) => {
85
- callOrder.push('middleware3');
86
- return next(action);
87
- };
88
-
89
- manager.getMiddlewareRegistry().register({
90
- id: 'low',
91
- middleware: middleware1,
92
- priority: 10,
93
- });
94
-
95
- manager.getMiddlewareRegistry().register({
96
- id: 'high',
97
- middleware: middleware2,
98
- priority: 100,
99
- });
100
-
101
- manager.getMiddlewareRegistry().register({
102
- id: 'medium',
103
- middleware: middleware3,
104
- priority: 50,
105
- });
106
-
107
- const store = manager.initialize();
108
- store.dispatch({ type: 'TEST' });
109
-
110
- expect(callOrder).toEqual(['middleware2', 'middleware3', 'middleware1']);
111
- });
112
- });
113
-
114
- describe('reducer registration', () => {
115
- test('should register reducers', () => {
116
- const reducer: Reducer = (state = {}) => state;
117
-
118
- manager.getReducerRegistry().register('test', reducer);
119
-
120
- expect(manager.getReducerRegistry().has('test')).toBe(true);
121
- });
122
-
123
- test('should combine multiple reducers', () => {
124
- const counterReducer: Reducer = (state = 0, action: any) => {
125
- if (action.type === 'INCREMENT') return state + 1;
126
- return state;
127
- };
128
-
129
- const nameReducer: Reducer = (state = 'initial', action: any) => {
130
- if (action.type === 'SET_NAME') return action.payload;
131
- return state;
132
- };
133
-
134
- manager.getReducerRegistry().register('counter', counterReducer);
135
- manager.getReducerRegistry().register('name', nameReducer);
136
-
137
- const store = manager.initialize();
138
-
139
- expect(store.getState()).toEqual({ counter: 0, name: 'initial' });
140
-
141
- store.dispatch({ type: 'INCREMENT' });
142
- expect(store.getState()).toEqual({ counter: 1, name: 'initial' });
143
-
144
- store.dispatch({ type: 'SET_NAME', payload: 'Updated' });
145
- expect(store.getState()).toEqual({ counter: 1, name: 'Updated' });
146
- });
147
-
148
- test('should support hot reducer replacement', () => {
149
- const reducer1: Reducer = (state = { version: 1 }) => state;
150
- manager.getReducerRegistry().register('feature', reducer1);
151
-
152
- const store = manager.initialize();
153
- expect(store.getState()).toEqual({ feature: { version: 1 } });
154
-
155
- // Hot replace reducer
156
- const reducer2: Reducer = (state = { version: 2 }, action: any) => {
157
- // If state is provided from previous reducer, keep it
158
- // Only use new initial state if state is undefined
159
- if (state === undefined) return { version: 2 };
160
- return state;
161
- };
162
- manager.getReducerRegistry().register('feature', reducer2, true);
163
-
164
- // The state won't automatically reset - hot replacement keeps existing state
165
- // unless we dispatch an action that changes it
166
- expect(store.getState().feature).toEqual({ version: 1 });
167
- });
168
- });
169
-
170
- describe('store operations', () => {
171
- test('should dispatch actions', () => {
172
- const reducer: Reducer = (state = 0, action: any) => {
173
- if (action.type === 'INCREMENT') return state + 1;
174
- return state;
175
- };
176
-
177
- manager.getReducerRegistry().register('counter', reducer);
178
- manager.initialize();
179
-
180
- const action = { type: 'INCREMENT' };
181
- const result = manager.dispatch(action);
182
-
183
- expect(result).toEqual(action);
184
- expect(manager.getState()).toEqual({ counter: 1 });
185
- });
186
-
187
- test('should get state', () => {
188
- const reducer: Reducer = (state = { test: 'value' }) => state;
189
-
190
- manager.getReducerRegistry().register('data', reducer);
191
- manager.initialize();
192
-
193
- expect(manager.getState()).toEqual({ data: { test: 'value' } });
194
- });
195
-
196
- test('should subscribe to changes', () => {
197
- const reducer: Reducer = (state = 0, action: any) => {
198
- if (action.type === 'INCREMENT') return state + 1;
199
- return state;
200
- };
201
-
202
- manager.getReducerRegistry().register('counter', reducer);
203
- manager.initialize();
204
-
205
- const listener = vi.fn(() => {});
206
- const unsubscribe = manager.subscribe(listener);
207
-
208
- manager.dispatch({ type: 'INCREMENT' });
209
- expect(listener).toHaveBeenCalled();
210
-
211
- listener.mockClear();
212
- unsubscribe();
213
- manager.dispatch({ type: 'INCREMENT' });
214
- expect(listener).not.toHaveBeenCalled();
215
- });
216
-
217
- test('should replace reducer', () => {
218
- const reducer1: Reducer = (state = { value: 1 }) => state;
219
- manager.getReducerRegistry().register('data', reducer1);
220
- manager.initialize();
221
-
222
- expect(manager.getState()).toEqual({ data: { value: 1 } });
223
-
224
- // Use combineReducers to create a proper reducer function
225
- const { combineReducers } = require('redux');
226
- const reducer2: Reducer = (state = { value: 2 }) => state;
227
- const combined = combineReducers({ data: reducer2 });
228
- manager.replaceReducer(combined);
229
-
230
- // State is preserved unless reducer explicitly changes it
231
- // The existing state is passed to the new reducer
232
- expect(manager.getState().data.value).toBe(1);
233
- });
234
- });
235
-
236
- describe('preloaded state', () => {
237
- test('should initialize with preloaded state', () => {
238
- const reducer: Reducer = (state = 0) => state;
239
- manager.getReducerRegistry().register('counter', reducer);
240
-
241
- const preloadedState = { counter: 42 };
242
- manager.initialize({ preloadedState });
243
-
244
- expect(manager.getState()).toEqual({ counter: 42 });
245
- });
246
- });
247
-
248
- describe('destroy', () => {
249
- test('should destroy store', () => {
250
- manager.initialize();
251
- manager.destroy();
252
-
253
- expect(manager.isInitialized()).toBe(false);
254
- expect(() => manager.getStore()).toThrow(/Store not initialized/);
255
- });
256
-
257
- test('should allow re-initialization after destroy', () => {
258
- manager.initialize();
259
- manager.destroy();
260
-
261
- // Should be able to initialize again
262
- expect(() => manager.initialize()).not.toThrow();
263
- expect(manager.isInitialized()).toBe(true);
264
- });
265
- });
266
-
267
- describe('DevTools configuration', () => {
268
- test('should accept devTools configuration', () => {
269
- // Should not throw with devTools config
270
- expect(() => manager.initialize({ devTools: false })).not.toThrow();
271
- });
272
-
273
- test('should accept enhancers', () => {
274
- const enhancer = (next: any) => (reducer: any, initialState: any) => {
275
- const store = next(reducer, initialState);
276
- return {
277
- ...store,
278
- customMethod: () => 'enhanced',
279
- };
280
- };
281
-
282
- const store = manager.initialize({ enhancers: [enhancer] }) as any;
283
-
284
- expect(store.customMethod).toBeDefined();
285
- expect(store.customMethod()).toBe('enhanced');
286
- });
287
- });
288
- });
@@ -1,125 +0,0 @@
1
- /**
2
- * Store Manager Implementation
3
- */
4
-
5
- import { createStore, applyMiddleware, compose, type Store, type Reducer } from 'redux';
6
- import type { IStoreManager, StoreConfig, RootState, AppAction } from '@hamak/ui-store-api';
7
- import { MiddlewareRegistry } from './middleware-registry';
8
- import { ReducerRegistry } from './reducer-registry';
9
-
10
- export class StoreManager implements IStoreManager {
11
- private store: Store<RootState, AppAction> | null = null;
12
- private middlewareRegistry: MiddlewareRegistry;
13
- private reducerRegistry: ReducerRegistry;
14
- private initialized = false;
15
- private config: StoreConfig | null = null;
16
-
17
- constructor() {
18
- this.middlewareRegistry = new MiddlewareRegistry();
19
- this.reducerRegistry = new ReducerRegistry((rootReducer: any) => {
20
- // Hot replacement callback
21
- if (this.store) {
22
- this.store.replaceReducer(rootReducer);
23
- }
24
- });
25
- }
26
-
27
- getMiddlewareRegistry() {
28
- return this.middlewareRegistry;
29
- }
30
-
31
- getReducerRegistry() {
32
- return this.reducerRegistry;
33
- }
34
-
35
- isInitialized(): boolean {
36
- return this.initialized;
37
- }
38
-
39
- initialize(config: StoreConfig = {}): Store<RootState, AppAction> {
40
- if (this.initialized) {
41
- throw new Error('[StoreManager] Store already initialized');
42
- }
43
-
44
- this.config = config;
45
-
46
- // Get all middleware in priority order
47
- const middleware = this.middlewareRegistry.getAll();
48
-
49
- // Lock the middleware registry
50
- this.middlewareRegistry.lock();
51
-
52
- // Get combined reducer
53
- const rootReducer = this.reducerRegistry.getCombinedReducer();
54
-
55
- // Create enhancers
56
- const enhancers = config.enhancers || [];
57
-
58
- // Setup Redux DevTools
59
- let composeEnhancers = compose;
60
- if (config.devTools !== false && typeof window !== 'undefined') {
61
- const devToolsExtension = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
62
- if (devToolsExtension) {
63
- composeEnhancers = devToolsExtension({
64
- trace: true,
65
- traceLimit: 25,
66
- });
67
- }
68
- }
69
-
70
- // Create store
71
- const enhancer = composeEnhancers(
72
- applyMiddleware(...middleware),
73
- ...enhancers
74
- ) as any;
75
-
76
- this.store = createStore(
77
- rootReducer as any,
78
- config.preloadedState,
79
- enhancer
80
- );
81
-
82
- this.initialized = true;
83
-
84
- console.log('[StoreManager] Store initialized with:', {
85
- reducers: this.reducerRegistry.getAllRegistrations().map(r => r.key),
86
- middleware: this.middlewareRegistry.getAllRegistrations().map(m => ({
87
- id: m.id,
88
- priority: m.priority,
89
- plugin: m.plugin,
90
- })),
91
- devTools: config.devTools !== false,
92
- });
93
-
94
- return this.store;
95
- }
96
-
97
- getStore(): Store<RootState, AppAction> {
98
- if (!this.store) {
99
- throw new Error('[StoreManager] Store not initialized. Call initialize() first.');
100
- }
101
- return this.store;
102
- }
103
-
104
- dispatch<A extends AppAction>(action: A): A {
105
- return this.getStore().dispatch(action);
106
- }
107
-
108
- getState<S = RootState>(): S {
109
- return this.getStore().getState() as S;
110
- }
111
-
112
- subscribe(listener: () => void): () => void {
113
- return this.getStore().subscribe(listener);
114
- }
115
-
116
- replaceReducer(nextReducer: Reducer<RootState, AppAction>): void {
117
- this.getStore().replaceReducer(nextReducer);
118
- }
119
-
120
- destroy(): void {
121
- this.store = null;
122
- this.initialized = false;
123
- console.log('[StoreManager] Store destroyed');
124
- }
125
- }