UNPKG

@mini-rx/signal-store

Version:
566 lines (545 loc) 24 kB
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