@mini-rx/signal-store
Version:
Signal Store for Angular
566 lines (545 loc) • 24 kB
JavaScript
import * as i0 from '@angular/core';
import { computed, signal, assertInInjectionContext, inject, Injector, effect, untracked, DestroyRef, EnvironmentInjector, isSignal, InjectionToken, NgModule, Inject, Optional, makeEnvironmentProviders, ENVIRONMENT_INITIALIZER } from '@angular/core';
import { isKey, createStore, createRegisterEffectFn, createSubSink, defaultEffectsErrorHandler, createMiniRxActionType, createUpdateFn, generateId, generateFeatureKey, createFeatureStoreReducer, undo, miniRxError, componentStoreConfig, calculateExtensions, createActionsOnQueue, componentStoreFeatureKey, createComponentStoreReducer, AbstractReduxDevtoolsExtension, miniRxNameSpace, Actions, hasEffectMetaData } from '@mini-rx/common';
export { Actions, ImmutableStateExtension, LoggerExtension, StoreExtension, UndoExtension, createRxEffect, mapResponse, tapResponse, undo } from '@mini-rx/common';
import { Subject, isObservable } from 'rxjs';
// Credits go to NgRx
// Typings are taken directly from NgRx with small modifications:
// https://github.com/ngrx/platform/blob/8.6.0/modules/store/src/selector.ts
// The MIT License (MIT)
//
// Copyright (c) 2017 Brandon Roberts, Mike Ryan, Victor Savkin, Rob Wormald
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
const SIGNAL_SELECTOR_KEY = '@mini-rx/signalSelector';
function createSelector(...args) {
const selectors = args.slice(0, args.length - 1);
const projector = args[args.length - 1];
const selector = (state) => {
const signalsFromSelectors = selectors.map((fn) => {
return fn(state); // Pass the state Signal
});
// Return computed Signal which recalculates when one of the `signalsFromSelectors` notifies about changes
return computed(() => {
const results = signalsFromSelectors.map((aSignal) => aSignal());
return projector(...results);
});
};
return addSignalSelectorKey(selector);
}
function createFeatureStateSelector(featureKey) {
let selector;
if (featureKey) {
selector = createSelector(addSignalSelectorKey((state) => state), (state) => state[featureKey]);
}
else {
selector = (state) => state; // Do not memoize: when used with FeatureStore there is a new state object created for every `setState`
}
return addSignalSelectorKey(selector);
}
function createComponentStateSelector() {
return addSignalSelectorKey((state) => state);
}
// Exported for testing
function addSignalSelectorKey(s) {
Object.defineProperty(s, SIGNAL_SELECTOR_KEY, {
value: true,
});
return s;
}
function isSignalSelector(v) {
return Object.hasOwn(v, SIGNAL_SELECTOR_KEY);
}
function createSelectFn(state) {
function select(mapFnOrKey) {
if (!mapFnOrKey) {
return state;
}
if (isSignalSelector(mapFnOrKey)) {
return mapFnOrKey(state);
}
return computed(() => {
const rawState = state();
return isKey(rawState, mapFnOrKey) ? rawState[mapFnOrKey] : mapFnOrKey(state());
});
}
return select;
}
function createSelectableSignal(state) {
return {
select: createSelectFn(state),
get: () => {
return state();
},
};
}
function createSelectableWritableSignal(state) {
return {
select: createSelectFn(state),
get: () => {
return state();
},
set: (v) => {
state.set(v);
},
};
}
const storeCore = createStore(createSelectableWritableSignal(signal({})));
const rxEffect = createRegisterEffectFn(storeCore.dispatch);
class Store {
constructor(config) {
this.dispatch = storeCore.dispatch;
this.select = storeCore.appState.select;
storeCore.configureStore(config);
}
}
// Credits go to Angular
// Copied from with small modifications: https://github.com/angular/angular/blob/16.2.10/packages/core/rxjs-interop/src/to_observable.ts
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
// Reimplemented `toObservable` from Angular
// Use Subject instead of ReplaySubject (which is more lightweight and sufficient for internal use in MiniRx)
function miniRxToObservable(source, options) {
!options?.injector && assertInInjectionContext(miniRxToObservable);
const injector = options?.injector ?? inject(Injector);
const subject = new Subject();
const watcher = effect(() => {
let value;
try {
value = source();
}
catch (err) {
untracked(() => subject.error(err));
return;
}
untracked(() => subject.next(value));
}, { injector, manualCleanup: true });
injector.get(DestroyRef).onDestroy(() => {
watcher.destroy();
subject.complete();
});
return subject.asObservable();
}
function createSignalStoreSubSink() {
const subSink = createSubSink();
inject(DestroyRef).onDestroy(subSink.unsubscribe);
return {
set sink(sub) {
subSink.sink = sub;
},
};
}
function createRxEffectFn() {
const subSink = createSignalStoreSubSink();
const injector = inject(EnvironmentInjector);
function rxEffect(effectFn) {
const subject = new Subject();
const effect$ = effectFn(subject);
subSink.sink = effect$.pipe(defaultEffectsErrorHandler).subscribe();
return ((observableOrValue) => {
// If we detect a Signal: convert Signal to Observable
observableOrValue = isSignal(observableOrValue)
? miniRxToObservable(observableOrValue, { injector })
: observableOrValue;
isObservable(observableOrValue)
? (subSink.sink = observableOrValue.subscribe((v) => subject.next(v)))
: subject.next(observableOrValue);
});
}
return rxEffect;
}
function createConnectFn(updateStateCallback) {
const subSink = createSignalStoreSubSink();
const injector = inject(EnvironmentInjector);
return (dict) => {
const keys = Object.keys(dict);
keys.forEach((key) => {
const observableOrSignal = dict[key];
const obs$ = isSignal(observableOrSignal)
? miniRxToObservable(observableOrSignal, { injector })
: observableOrSignal;
subSink.sink = obs$.subscribe((v) => {
updateStateCallback({
[key]: v,
}, "connection" /* OperationType.CONNECTION */, key);
});
});
};
}
class FeatureStore {
get featureKey() {
return this._featureKey;
}
get state() {
return untracked(() => this._state());
}
constructor(featureKey, initialState, config = {}) {
this._state = storeCore.appState.select((state) => state[this.featureKey]);
this.updateState = (stateOrCallback, operationType, name) => {
return storeCore.dispatch({
type: createMiniRxActionType(operationType, this.featureKey, name),
stateOrCallback,
featureId: this.featureId,
});
};
this.setState = createUpdateFn(this.updateState);
this.connect = createConnectFn(this.updateState);
this.rxEffect = createRxEffectFn();
this.select = createSelectableSignal(this._state).select;
this.featureId = generateId();
this._featureKey = generateFeatureKey(featureKey, config.multi);
storeCore.addFeature(this._featureKey, createFeatureStoreReducer(this.featureId, initialState));
inject(DestroyRef).onDestroy(() => this.destroy());
}
undo(action) {
storeCore.hasUndoExtension
? storeCore.dispatch(undo(action))
: miniRxError('UndoExtension is not initialized.');
}
destroy() {
storeCore.removeFeature(this._featureKey);
}
}
function createFeatureStore(featureKey, initialState, config = {}) {
return new FeatureStore(featureKey, initialState, config);
}
const globalCsConfig = componentStoreConfig();
class ComponentStore {
get state() {
return untracked(() => this._state.get());
}
constructor(initialState, config) {
this.initialState = initialState;
this.config = config;
this.extensions = calculateExtensions(this.config, globalCsConfig.get());
this.hasUndoExtension = this.extensions.some((ext) => ext.id === 1 /* ExtensionId.UNDO */);
this.actionsOnQueue = createActionsOnQueue();
this._state = createSelectableWritableSignal(signal(this.initialState));
this.updateState = (stateOrCallback, operationType, name) => {
return this.actionsOnQueue.dispatch({
type: createMiniRxActionType(operationType, componentStoreFeatureKey, name),
stateOrCallback,
});
};
this.setState = createUpdateFn(this.updateState);
this.connect = createConnectFn(this.updateState);
this.rxEffect = createRxEffectFn();
this.select = this._state.select;
inject(DestroyRef).onDestroy(() => this.destroy());
const reducer = createComponentStoreReducer(initialState, this.extensions);
const subSink = createSignalStoreSubSink();
subSink.sink = this.actionsOnQueue.actions$.subscribe((action) => {
const newState = reducer(this.state, action);
this._state.set(newState);
});
this.actionsOnQueue.dispatch({
type: createMiniRxActionType("init" /* OperationType.INIT */, componentStoreFeatureKey),
});
}
undo(action) {
this.hasUndoExtension
? this.actionsOnQueue.dispatch(undo(action))
: miniRxError(`${this.constructor.name} has no UndoExtension yet.`);
}
destroy() {
// Dispatch an action really just for logging via LoggerExtension
this.actionsOnQueue.dispatch({
type: createMiniRxActionType("destroy" /* OperationType.DESTROY */, componentStoreFeatureKey),
});
}
}
function createComponentStore(initialState, config) {
return new ComponentStore(initialState, config);
}
class ReduxDevtoolsExtension extends AbstractReduxDevtoolsExtension {
get actions$() {
return storeCore.actions$;
}
readState() {
return storeCore.appState.get();
}
updateState(state) {
storeCore.appState.set(state);
}
}
// Injection tokens are part of the bundle, therefore keep the strings as short as possible (they just have to be unique!)
const STORE_CONFIG = new InjectionToken(`${miniRxNameSpace}/1`);
const FEATURE_NAMES = new InjectionToken(`${miniRxNameSpace}/2`);
const FEATURE_REDUCERS = new InjectionToken(`${miniRxNameSpace}/3`);
const FEATURE_CONFIGS = new InjectionToken(`${miniRxNameSpace}/4`);
const OBJECTS_WITH_EFFECTS = new InjectionToken(`${miniRxNameSpace}/5`);
const STORE_PROVIDER = new InjectionToken(`${miniRxNameSpace}/6`);
const FEATURE_PROVIDER = new InjectionToken(`${miniRxNameSpace}/7`);
const EFFECTS_PROVIDER = new InjectionToken(`${miniRxNameSpace}/8`);
const COMPONENT_STORE_CONFIG_PROVIDER = new InjectionToken(`${miniRxNameSpace}/9`);
function storeFactory$1(config) {
return new Store(config);
}
class StoreRootModule {
constructor() {
this.store = inject(Store); // Make sure store is initialized also if it is NOT injected in other services/components
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: StoreRootModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.3.9", ngImport: i0, type: StoreRootModule }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: StoreRootModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: StoreRootModule, decorators: [{
type: NgModule
}] });
class StoreFeatureModule {
constructor() {
const storeRootModule = inject(StoreRootModule); // Prevent feature states to be initialized before root state
const featureNames = inject(FEATURE_NAMES);
const reducers = inject(FEATURE_REDUCERS);
const configs = inject(FEATURE_CONFIGS);
featureNames.forEach((featureName, index) => {
storeCore.addFeature(featureName, reducers[index], configs[index]);
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: StoreFeatureModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.3.9", ngImport: i0, type: StoreFeatureModule }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: StoreFeatureModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: StoreFeatureModule, decorators: [{
type: NgModule
}], ctorParameters: () => [] });
class StoreModule {
static forRoot(config) {
return {
ngModule: StoreRootModule,
providers: [
{ provide: STORE_CONFIG, useValue: config },
{
provide: Store,
useFactory: storeFactory$1,
deps: [STORE_CONFIG],
},
{
provide: Actions,
useValue: storeCore.actions$,
},
],
};
}
static forFeature(featureName, reducer, config) {
return {
ngModule: StoreFeatureModule,
providers: [
{ provide: FEATURE_NAMES, multi: true, useValue: featureName },
{ provide: FEATURE_REDUCERS, multi: true, useValue: reducer },
{ provide: FEATURE_CONFIGS, multi: true, useValue: config },
],
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: StoreModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.3.9", ngImport: i0, type: StoreModule }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: StoreModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: StoreModule, decorators: [{
type: NgModule
}] });
// Credits go to Marko Stanimirović
// Copied from with small modifications: https://github.com/markostanimirovic/juliette/blob/1.2.0/projects/juliette-ng/src/lib/effects.mapper.ts
const fromClassesWithEffectsToClassProviders = (injectionToken, classesWithEffects) => classesWithEffects.map((classWithEffects) => ({
provide: injectionToken,
useClass: classWithEffects,
multi: true,
}));
const fromObjectsWithEffectsToEffects = (objectsWithEffects) => objectsWithEffects.reduce((acc, objectWithEffects) => {
const effectsFromCurrentObject = Object.getOwnPropertyNames(objectWithEffects).reduce((acc, prop) => {
const effect = objectWithEffects[prop];
if (hasEffectMetaData(effect)) {
acc.push(effect);
}
return acc;
}, []);
return [...acc, ...effectsFromCurrentObject];
}, []);
// Credits go to Marko Stanimirović
// Copied from with small modifications: https://github.com/markostanimirovic/juliette/blob/1.2.0/projects/juliette-ng/src/lib/effects.module.ts
// MIT License
//
// Copyright (c) 2020 Marko Stanimirović
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
class EffectsModule {
constructor(objectsWithEffects,
// Make sure effects can select state from store, also if EffectsModule is registered before Store.forFeature
storeRootModule, storeFeatureModule) {
const effects = fromObjectsWithEffectsToEffects(objectsWithEffects);
effects.forEach((effect) => {
rxEffect(effect);
});
}
static register(classesWithEffects) {
return {
ngModule: EffectsModule,
providers: [
...fromClassesWithEffectsToClassProviders(OBJECTS_WITH_EFFECTS, classesWithEffects),
],
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: EffectsModule, deps: [{ token: OBJECTS_WITH_EFFECTS }, { token: StoreRootModule, optional: true }, { token: StoreFeatureModule, optional: true }], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.3.9", ngImport: i0, type: EffectsModule }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: EffectsModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: EffectsModule, decorators: [{
type: NgModule
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [OBJECTS_WITH_EFFECTS]
}] }, { type: StoreRootModule, decorators: [{
type: Optional
}] }, { type: StoreFeatureModule, decorators: [{
type: Optional
}] }] });
class ComponentStoreModule {
static forRoot(config) {
globalCsConfig.set(config);
return {
ngModule: ComponentStoreModule,
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ComponentStoreModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.3.9", ngImport: i0, type: ComponentStoreModule }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ComponentStoreModule }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ComponentStoreModule, decorators: [{
type: NgModule
}] });
// Store
function storeFactory(config) {
return new Store(config);
}
function rootStoreProviderFactory() {
inject(Store);
inject(Actions);
}
function provideStore(config) {
return makeEnvironmentProviders([
{ provide: STORE_CONFIG, useValue: config },
{
provide: Store,
useFactory: storeFactory,
deps: [STORE_CONFIG],
},
{
provide: Actions,
useValue: storeCore.actions$,
},
{ provide: STORE_PROVIDER, useFactory: rootStoreProviderFactory },
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useFactory() {
return () => inject(STORE_PROVIDER);
},
},
]);
}
// Feature
function featureProviderFactory() {
const featureNames = inject(FEATURE_NAMES);
const reducers = inject(FEATURE_REDUCERS);
const configs = inject(FEATURE_CONFIGS);
featureNames.forEach((featureName, index) => {
storeCore.addFeature(featureName, reducers[index], configs[index]);
});
}
function provideFeature(featureName, reducer, config) {
return makeEnvironmentProviders([
{ provide: FEATURE_NAMES, multi: true, useValue: featureName },
{ provide: FEATURE_REDUCERS, multi: true, useValue: reducer },
{ provide: FEATURE_CONFIGS, multi: true, useValue: config },
{ provide: FEATURE_PROVIDER, useFactory: featureProviderFactory },
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useFactory() {
return () => inject(FEATURE_PROVIDER);
},
},
]);
}
// Effects
function effectsProviderFactory() {
const objectsWithEffects = inject(OBJECTS_WITH_EFFECTS);
const effects = fromObjectsWithEffectsToEffects(objectsWithEffects);
effects.forEach((effect) => {
rxEffect(effect);
});
}
function provideEffects(...classesWithEffects) {
return makeEnvironmentProviders([
...fromClassesWithEffectsToClassProviders(OBJECTS_WITH_EFFECTS, classesWithEffects),
{ provide: EFFECTS_PROVIDER, useFactory: effectsProviderFactory },
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useFactory() {
return () => inject(EFFECTS_PROVIDER);
},
},
]);
}
// Component Store config
function provideComponentStoreConfig(config) {
return makeEnvironmentProviders([
{
provide: COMPONENT_STORE_CONFIG_PROVIDER,
useFactory: () => globalCsConfig.set(config),
},
{
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useFactory() {
return () => inject(COMPONENT_STORE_CONFIG_PROVIDER);
},
},
]);
}
/*
* Public API Surface of Signal Store
*/
/**
* Generated bundle index. Do not edit.
*/
export { ComponentStore, ComponentStoreModule, EffectsModule, FeatureStore, ReduxDevtoolsExtension, Store, StoreFeatureModule, StoreModule, StoreRootModule, createComponentStateSelector, createComponentStore, createFeatureStateSelector, createFeatureStore, createSelector, provideComponentStoreConfig, provideEffects, provideFeature, provideStore };
//# sourceMappingURL=mini-rx-signal-store.mjs.map