Skip to content

Commit

Permalink
refactor(mini-rx-store, signal-store, common): streamline feature sto…
Browse files Browse the repository at this point in the history
…res and component stores
  • Loading branch information
mini-rx committed Oct 10, 2024
1 parent 740a2ca commit 16ddeb1
Show file tree
Hide file tree
Showing 10 changed files with 75 additions and 128 deletions.
1 change: 1 addition & 0 deletions libs/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export {
export { createFeatureStoreReducer } from './lib/create-feature-store-reducer';
export { createComponentStoreReducer } from './lib/create-component-store-reducer';
export { generateId } from './lib/generate-id';
export { generateFeatureKey } from './lib/generate-feature-key';
export { calculateExtensions } from './lib/calculate-extensions';
export { calcNextState } from './lib/calc-next-state';
export { createReducerManager, ReducerManager } from './lib/reducer-manager';
Expand Down
6 changes: 6 additions & 0 deletions libs/common/src/lib/calculate-extensions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ComponentStoreConfig, ComponentStoreExtension } from './models';
import { sortExtensions } from './sort-extensions';
import { miniRxError } from './mini-rx-error';

function mergeComponentStoreExtensions(
globalExtensions: ComponentStoreExtension[],
Expand Down Expand Up @@ -31,5 +32,10 @@ export function calculateExtensions(
globalConfig?.extensions ?? [],
localConfig?.extensions ?? []
);
extensions.forEach((ext) => {
if (!ext.hasCsSupport) {
miniRxError(`Extension "${ext.constructor.name}" is not supported by Component Store.`);
}
});
return sortExtensions(extensions);
}
2 changes: 1 addition & 1 deletion libs/common/src/lib/create-component-store-reducer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface CounterState {
}

describe('createComponentStoreReducer', () => {
const reducer = createComponentStoreReducer<CounterState>({ counter: 1 });
const reducer = createComponentStoreReducer<CounterState>({ counter: 1 }, []);

it('should update state', () => {
const action: MiniRxAction<CounterState> = {
Expand Down
12 changes: 9 additions & 3 deletions libs/common/src/lib/create-component-store-reducer.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { Action, Reducer } from './models';
import { Action, ComponentStoreExtension, MetaReducer, Reducer } from './models';
import { isMiniRxAction } from './is-mini-rx-action';
import { calcNextState } from './calc-next-state';
import { combineMetaReducers } from './combine-meta-reducers';

export function createComponentStoreReducer<StateType>(
initialState: StateType
initialState: StateType,
extensions: ComponentStoreExtension[]
): Reducer<StateType> {
return (state: StateType = initialState, action: Action) => {
const reducer = (state: StateType = initialState, action: Action) => {
return isMiniRxAction<StateType>(action)
? calcNextState(state, action.stateOrCallback)
: state;
};

const metaReducers: MetaReducer<StateType>[] = extensions.map((ext) => ext.init());
const combinedMetaReducer = combineMetaReducers(metaReducers);
return combinedMetaReducer(reducer);
}
5 changes: 5 additions & 0 deletions libs/common/src/lib/generate-feature-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { generateId } from './generate-id';

export function generateFeatureKey(featureKey: string, multi?: boolean) {
return multi ? featureKey + '-' + generateId() : featureKey;
}
82 changes: 33 additions & 49 deletions libs/mini-rx-store/src/lib/component-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import { ComponentStoreLike } from './models';
import {
Action,
calculateExtensions,
combineMetaReducers,
componentStoreConfig,
ComponentStoreConfig,
ComponentStoreExtension,
createActionsOnQueue,
createComponentStoreReducer,
createMiniRxActionType,
createSubSink,
ExtensionId,
MetaReducer,
MiniRxAction,
miniRxError,
OperationType,
Expand All @@ -22,33 +21,28 @@ import {
import { createEffectFn } from './effect';
import { createUpdateFn } from './update';
import { createState } from './state';
import { Observable } from 'rxjs';
import { createConnectFn } from './connect';
import { createAssertState } from './assert-state';

let componentStoreConfig: ComponentStoreConfig | undefined = undefined;

const csFeatureKey = 'component-store';
const globalCsConfig = componentStoreConfig();
// Keep configureComponentStores for backwards compatibility (mini-rx-store-ng)
export function configureComponentStores(config: ComponentStoreConfig) {
if (!componentStoreConfig) {
componentStoreConfig = config;
return;
}
miniRxError('`configureComponentStores` was called multiple times.');
globalCsConfig.set(config);
}

const csFeatureKey = 'component-store';

export class ComponentStore<StateType extends object> implements ComponentStoreLike<StateType> {
private actionsOnQueue = createActionsOnQueue();
private readonly combinedMetaReducer: MetaReducer<StateType>;
private reducer: Reducer<StateType> | undefined;
private readonly hasUndoExtension: boolean = false;
private readonly extensions: ComponentStoreExtension[] = calculateExtensions(
this.config,
globalCsConfig.get()
);
private readonly hasUndoExtension: boolean = this.extensions.some(
(ext) => ext.id === ExtensionId.UNDO
);

private subSink = createSubSink();
private actionsOnQueue = createActionsOnQueue();

private _state = createState<StateType>();
private assertState = createAssertState(this.constructor.name, this._state);
state$: Observable<StateType> = this._state.select();
get state(): StateType {
this.assertState.isInitialized();
return this._state.get()!;
Expand All @@ -66,32 +60,10 @@ export class ComponentStore<StateType extends object> implements ComponentStoreL
});
};

constructor(initialState?: StateType, config?: ComponentStoreConfig) {
const extensions: ComponentStoreExtension[] = calculateExtensions(
config,
componentStoreConfig
);
const metaReducers: MetaReducer<StateType>[] = extensions.map((ext) => {
if (!ext.hasCsSupport) {
miniRxError(
`Extension "${ext.constructor.name}" is not supported by Component Store.`
);
}
return ext.init();
});
this.hasUndoExtension = extensions.some((ext) => ext.id === ExtensionId.UNDO);

this.combinedMetaReducer = combineMetaReducers(metaReducers);

this.subSink.sink = this.actionsOnQueue.actions$.subscribe((action) => {
const newState: StateType = this.reducer!(
// We are sure, there is a reducer!
this._state.get()!, // Initially undefined, but the reducer can handle undefined (by falling back to initial state)
action
);
this._state.set(newState);
});
private subSink = createSubSink();
private assertState = createAssertState(this.constructor.name, this._state);

constructor(initialState?: StateType, private config?: ComponentStoreConfig) {
if (initialState) {
this.setInitialState(initialState);
}
Expand All @@ -100,14 +72,26 @@ export class ComponentStore<StateType extends object> implements ComponentStoreL
setInitialState(initialState: StateType): void {
this.assertState.isNotInitialized();

this.reducer = this.combinedMetaReducer(createComponentStoreReducer(initialState));
const reducer: Reducer<StateType> = createComponentStoreReducer(
initialState,
this.extensions
);

this.subSink.sink = this.actionsOnQueue.actions$.subscribe((action) => {
const newState: StateType = reducer(
// We are sure, there is a reducer!
this._state.get() as StateType, // Initially undefined, but the reducer can handle undefined (by falling back to initial state)
action
);
this._state.set(newState);
});

this.actionsOnQueue.dispatch({
type: createMiniRxActionType(OperationType.INIT, csFeatureKey),
});
}

// Implementation of abstract method from BaseStore
undo(action: Action) {
undo(action: Action): void {
this.hasUndoExtension
? this.actionsOnQueue.dispatch(undo(action))
: miniRxError(`${this.constructor.name} has no UndoExtension yet.`);
Expand All @@ -119,9 +103,9 @@ export class ComponentStore<StateType extends object> implements ComponentStoreL
select = this._state.select;

destroy() {
if (this.reducer) {
if (this._state.get()) {
// Dispatch an action really just for logging via LoggerExtension
// Only dispatch if a reducer exists (if an initial state was provided or setInitialState was called)
// Only dispatch if an initial state was provided or setInitialState was called
this.actionsOnQueue.dispatch({
type: createMiniRxActionType(OperationType.DESTROY, csFeatureKey),
});
Expand Down
12 changes: 5 additions & 7 deletions libs/mini-rx-store/src/lib/feature-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
createMiniRxActionType,
createSubSink,
FeatureStoreConfig,
generateFeatureKey,
generateId,
MiniRxAction,
miniRxError,
Expand All @@ -17,7 +18,6 @@ import {
import { createEffectFn } from './effect';
import { createUpdateFn } from './update';
import { createState } from './state';
import { Observable } from 'rxjs';
import { createAssertState } from './assert-state';
import { createConnectFn } from './connect';

Expand All @@ -28,11 +28,7 @@ export class FeatureStore<StateType extends object> implements ComponentStoreLik
return this._featureKey;
}

private subSink = createSubSink();

private _state = createState<StateType>();
private assertState = createAssertState(this.constructor.name, this._state);
state$: Observable<StateType> = this._state.select();
get state(): StateType {
this.assertState.isInitialized();
return this._state.get()!;
Expand All @@ -51,13 +47,16 @@ export class FeatureStore<StateType extends object> implements ComponentStoreLik
});
};

private subSink = createSubSink();
private assertState = createAssertState(this.constructor.name, this._state);

constructor(
featureKey: string,
initialState: StateType | undefined,
config: FeatureStoreConfig = {}
) {
this.featureId = generateId();
this._featureKey = config.multi ? featureKey + '-' + generateId() : featureKey;
this._featureKey = generateFeatureKey(featureKey, config.multi);

if (initialState) {
this.setInitialState(initialState);
Expand All @@ -77,7 +76,6 @@ export class FeatureStore<StateType extends object> implements ComponentStoreLik
.subscribe((v) => this._state.set(v));
}

// Implementation of abstract method from BaseStore
undo(action: Action): void {
hasUndoExtension
? dispatch(undo(action))
Expand Down
58 changes: 4 additions & 54 deletions libs/mini-rx-store/src/lib/spec/component-store.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ComponentStore, configureComponentStores, createComponentStore } from '../component-store';
import { counterInitialState, CounterState, userState } from './_spec-helpers';
import { Observable, of, pipe, Subject } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { pipe, Subject } from 'rxjs';
import { tap } from 'rxjs/operators';
import { createComponentStateSelector, createSelector } from '../selector';
import { ExtensionId, StoreExtension, ComponentStoreExtension } from '@mini-rx/common';
import { ComponentStoreExtension, ExtensionId, StoreExtension } from '@mini-rx/common';

describe('ComponentStore', () => {
it('should initialize the store', () => {
Expand Down Expand Up @@ -44,26 +44,6 @@ describe('ComponentStore', () => {
expect(spy).toHaveBeenCalledWith({ ...userState, firstName: 'Nicolas' });
expect(spy).toHaveBeenCalledTimes(1);
});
//
// it('should update state using an Observable', () => {
// const cs = createComponentStore(counterInitialState);
//
// const counterState$: Observable<CounterState> = of(2, 3, 4, 5).pipe(
// map((v) => ({ counter: v }))
// );
//
// const subscribeCallback = jest.fn<void, [number]>();
// cs.select((state) => state.counter).subscribe(subscribeCallback);
//
// // setState with Observable
// cs.setState(counterState$);
//
// // "normal" setState
// cs.setState((state) => ({ counter: state.counter + 1 }));
// cs.setState((state) => ({ counter: state.counter + 1 }));
//
// expect(subscribeCallback.mock.calls).toEqual([[1], [2], [3], [4], [5], [6], [7]]);
// });

it('should select state with memoized selectors', () => {
const getCounterSpy = jest.fn<void, [number]>();
Expand Down Expand Up @@ -93,15 +73,6 @@ describe('ComponentStore', () => {
expect(getSquareCounterSpy.mock.calls).toEqual([[1], [2], [3], [4]]);
});

it('should select component state with the `state$` property', () => {
const spy = jest.fn();

const cs = createComponentStore(counterInitialState);
cs.state$.subscribe(spy);

expect(spy).toHaveBeenCalledWith(counterInitialState);
});

it('should dispatch an Action when updating state', () => {
const cs = createComponentStore(counterInitialState);

Expand All @@ -127,27 +98,6 @@ describe('ComponentStore', () => {
expect(spy).toHaveBeenCalledTimes(1);

spy.mockReset();

// With setState name (when passing an Observable to setState)
// cs.setState(of(1, 2).pipe(map((v) => ({ counter: v }))), 'updateCounterFromObservable');
// expect(spy.mock.calls).toEqual([
// [
// {
// type: '@mini-rx/component-store/set-state/updateCounterFromObservable',
// stateOrCallback: {
// counter: 1,
// },
// },
// ],
// [
// {
// type: '@mini-rx/component-store/set-state/updateCounterFromObservable',
// stateOrCallback: {
// counter: 2,
// },
// },
// ],
// ]);
});

it('should dispatch an Action on destroy (only if initial state has been set)', () => {
Expand Down Expand Up @@ -289,7 +239,7 @@ describe('ComponentStore', () => {
it('should throw when calling `configureComponentStores` more than once', () => {
configureComponentStores({ extensions: [] });
expect(() => configureComponentStores({ extensions: [] })).toThrowError(
'@mini-rx: `configureComponentStores` was called multiple times.'
'@mini-rx: ComponentStore config was set multiple times.'
);
});

Expand Down
Loading

0 comments on commit 16ddeb1

Please sign in to comment.