@sigi/core
Version:
Sigi core library
478 lines (467 loc) • 19 kB
JavaScript
'use strict';
var immer = require('immer');
var rxjs = require('rxjs');
var operators = require('rxjs/operators');
var di = require('@sigi/di');
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
const hmrEnabled = process.env.NODE_ENV === 'development' &&
((typeof module !== 'undefined' && typeof module.hot === 'object') ||
('hot' in ({ url: (typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.js', document.baseURI).href)) }) && undefined !== null));
let hmrInstanceCache;
if (hmrEnabled) {
hmrInstanceCache = new Map();
}
const IS_PROD = process.env.NODE_ENV === 'production';
const EFFECT_DECORATOR_SYMBOL = IS_PROD ? 'E' : 'EFFECT_DECORATOR_SYMBOL';
const REDUCER_DECORATOR_SYMBOL = IS_PROD ? 'R' : 'REDUCER_DECORATOR_SYMBOL';
const IMMER_REDUCER_DECORATOR_SYMBOL = IS_PROD ? 'IR' : 'IMMER_REDUCER_DECORATOR_SYMBOL';
const DEFINE_ACTION_DECORATOR_SYMBOL = IS_PROD ? 'D' : 'DEFINE_ACTION_DECORATOR_SYMBOL';
const INIT_ACTION_TYPE_SYMBOL = IS_PROD ? 'IA' : 'INIT_ACTION_TYPE_SYMBOL';
const NOOP_ACTION_TYPE_SYMBOL = IS_PROD ? 'N' : 'NOOP_ACTION_TYPE_SYMBOL';
const TERMINATE_ACTION_TYPE_SYMBOL = IS_PROD ? 'T' : 'TERMINATE_ACTION_TYPE_SYMBOL';
const RESET_ACTION_TYPE_SYMBOL = IS_PROD ? 'RST' : 'RESET_ACTION_TYPE_SYMBOL';
const RETRY_ACTION_TYPE_SYMBOL = IS_PROD ? 'RT' : 'RETRY_ACTION_TYPE_SYMBOL';
const SSR_ACTION_META_SYMBOL = IS_PROD ? 'SA' : 'SSR_ACTION_META_SYMBOL';
const ACTION_TO_SKIP_SYMBOL = IS_PROD ? 'RS' : 'ACTION_TO_SKIP_SYMBOL';
const GLOBAL_KEY_SYMBOL = 'SIGI_STATE';
const RETRY_KEY_SYMBOL = 'SIGI_RETRY';
const actionEnum = {
DefineAction: DEFINE_ACTION_DECORATOR_SYMBOL,
Reducer: REDUCER_DECORATOR_SYMBOL,
ImmerReducer: IMMER_REDUCER_DECORATOR_SYMBOL,
Effect: EFFECT_DECORATOR_SYMBOL,
};
const metadataFactory = (key) => {
const get = (prototype, defaultValue) => {
const meta = Reflect.getMetadata(key, prototype);
if (!meta || !Array.isArray(meta)) {
return defaultValue;
}
return meta;
};
return {
get,
add: (prototype, meta) => {
const stored = get(prototype);
if (!stored) {
Reflect.defineMetadata(key, [meta], prototype);
}
else {
stored.push(meta);
}
},
};
};
function getDecoratedActions(prototype, type, defaultValue) {
const { get } = metadataFactory(actionEnum[type]);
return get(prototype, defaultValue);
}
function createActionDecorator(type) {
return () => (prototype, propertyKey, descriptor) => {
if (typeof prototype === 'function' || !propertyKey) {
throw new Error(`${type} can only be used to decorate properties.`);
}
const { add } = metadataFactory(actionEnum[type]);
add(prototype, propertyKey);
return descriptor;
};
}
const { get: getSSREffectMeta, add: addSSREffectMeta } = metadataFactory(SSR_ACTION_META_SYMBOL);
const { get: getActionsToSkip, add: addActionToSkip } = metadataFactory(ACTION_TO_SKIP_SYMBOL);
exports.logStoreAction = (_action) => { };
const replaceLogger = (logger) => (exports.logStoreAction = logger);
class Store {
get state() {
return this.internalState;
}
get ready() {
return this.isReady;
}
constructor(name, reducer = rxjs.identity, epic = () => rxjs.NEVER) {
this.state$ = new rxjs.ReplaySubject(1);
this.action$ = new rxjs.Subject();
this.isReady = false;
this.actionSub = new rxjs.Subscription();
this.initAction = {
type: INIT_ACTION_TYPE_SYMBOL,
payload: null,
store: this,
};
this.name = name;
this.reducer = reducer;
this.epic$ = new rxjs.BehaviorSubject(epic);
}
setup(defaultState) {
this.internalState = defaultState;
this.state$.next(defaultState);
this.subscribeAction();
this.log(this.initAction);
this.isReady = true;
}
addEpic(combineEpic) {
const { epic$ } = this;
const prevEpic = epic$.getValue();
epic$.next(combineEpic((action$) => {
let output$;
if (action$ instanceof rxjs.Subject) {
output$ = prevEpic(action$);
}
else {
output$ = prevEpic(action$.pipe(operators.share()));
}
return output$.pipe(operators.takeUntil(this.action$.pipe(operators.last(null, null))));
}));
return () => {
this.epic$.next(prevEpic);
};
}
dispatch(action) {
if (action.type === NOOP_ACTION_TYPE_SYMBOL) {
return;
}
if (action.store !== this) {
action.store.dispatch(action);
return;
}
const prevState = this.internalState;
const newState = this.reducer(prevState, action);
if (newState !== prevState) {
if (process.env.NODE_ENV !== 'production' && newState === undefined) {
console.warn(`${action.type} produced an undefined state, you may forget to return new State in `);
}
this.internalState = newState;
this.state$.next(newState);
}
this.log(action);
this.action$.next(action);
}
log(action) {
if (action.type !== TERMINATE_ACTION_TYPE_SYMBOL) {
exports.logStoreAction(action);
}
}
dispose() {
this.actionSub.unsubscribe();
this.action$.complete();
this.state$.complete();
this.epic$.complete();
}
subscribeAction() {
this.actionSub = this.epic$
.pipe(operators.switchMap((epic) => epic(this.action$).pipe(operators.takeUntil(this.action$.pipe(operators.last(null, null))))))
.subscribe({
next: (action) => {
try {
this.dispatch(action);
}
catch (e) {
if (process.env.NODE_ENV === 'development') {
console.error(e);
}
this.action$.error(e);
}
},
error: (e) => {
if (!this.action$.closed) {
this.action$.error(e);
}
},
});
}
}
const _globalThis = typeof globalThis === 'undefined'
? typeof window === 'undefined'
? global
: window
: globalThis;
const DEFAULT_STATE_KEY = 'defaultState';
class EffectModule {
get state$() {
return this.store.state$;
}
get action$() {
return this.store.action$;
}
get state() {
return this.store.state;
}
constructor() {
this.actionStreams = {};
this.retryActionsCreator = {};
this.actionNames = [];
this.restoredFromSSR = false;
this.createNoopAction = () => {
return this.noop();
};
this.terminate = () => {
return { type: TERMINATE_ACTION_TYPE_SYMBOL, payload: null, store: this.store };
};
this.reset = () => {
return { type: RESET_ACTION_TYPE_SYMBOL, payload: null, store: this.store };
};
this.moduleName = Object.getPrototypeOf(this).moduleName;
const reducer = this.combineReducers();
const definedActions = this.combineDefineActions();
const epic = this.combineEffects();
this.store = new Store(this.moduleName, reducer, epic);
for (const name of definedActions) {
this[name] = this.store.action$.pipe(operators.filter(({ type }) => type === name), operators.map(({ payload }) => payload));
}
this.actions = {
reset: this.reset,
terminate: this.terminate,
noop: this.noop,
};
this.dispatchers = {
reset: () => {
this.store.dispatch(this.reset());
},
terminate: () => {
this.store.dispatch(this.terminate());
},
noop: () => {
this.store.dispatch(this.noop());
},
};
for (const name of this.actionNames) {
const actionCreator = (payload) => ({ type: name, payload, store: this.store });
this.actions[name] = actionCreator;
this.dispatchers[name] = (payload) => {
this.store.dispatch(actionCreator(payload));
};
this.actionStreams[name] = this.store.action$.pipe(operators.filter(({ type }) => type === name), operators.map(({ payload }) => payload));
}
if (typeof Proxy !== 'undefined') {
const context = this;
return new Proxy(this, {
defineProperty(target, p, attr) {
if (p === DEFAULT_STATE_KEY) {
if (attr.set) {
const rawSetter = attr.set;
attr.set = function (value) {
context.internalDefaultState = value;
if (!context.store.ready) {
context.store.setup(context.getDefaultState());
context.actionsToRetry = new Set(_globalThis[RETRY_KEY_SYMBOL]?.[this.moduleName] || []);
context.actionsToSkip = new Set(context.restoredFromSSR
?
getActionsToSkip(context.constructor.prototype) || []
: []);
}
return rawSetter.call(this, value);
};
}
else if ('value' in attr) {
context.internalDefaultState = attr.value;
if (!context.store.ready) {
context.store.setup(context.getDefaultState());
context.actionsToRetry = new Set(_globalThis[RETRY_KEY_SYMBOL]?.[context.moduleName] || []);
context.actionsToSkip = new Set(context.restoredFromSSR
?
getActionsToSkip(context.constructor.prototype) || []
: []);
}
}
}
return Reflect.defineProperty(target, p, attr);
},
set(target, p, value, receiver) {
if (p === DEFAULT_STATE_KEY) {
context.internalDefaultState = value;
if (!context.store.ready) {
context.store.setup(context.getDefaultState());
context.actionsToRetry = new Set(_globalThis[RETRY_KEY_SYMBOL]?.[context.moduleName] || []);
context.actionsToSkip = new Set(context.restoredFromSSR
?
getActionsToSkip(context.constructor.prototype) || []
: []);
}
}
return Reflect.set(target, p, value, receiver);
},
});
}
else {
Object.defineProperty(this, DEFAULT_STATE_KEY, {
set: (value) => {
this.internalDefaultState = value;
if (!this.store.ready) {
this.store.setup(this.getDefaultState());
this.actionsToRetry = new Set(_globalThis[RETRY_KEY_SYMBOL]?.[this.moduleName] || []);
this.actionsToSkip = new Set(this.restoredFromSSR ? getActionsToSkip(this.constructor.prototype) || [] : []);
}
},
get: () => {
return this.getDefaultState();
},
});
}
}
getActions() {
return this.actions;
}
getAction$() {
return this.actionStreams;
}
retryOnClient() {
return this.retryActionsCreator;
}
noop() {
return { type: NOOP_ACTION_TYPE_SYMBOL, payload: null, store: this.store };
}
getDefaultState() {
return this.tryReadHmrState() ?? this.tryReadSSRState() ?? this.internalDefaultState;
}
tryReadSSRState() {
const ssrCache = _globalThis[GLOBAL_KEY_SYMBOL];
if (ssrCache?.[this.moduleName]) {
this.restoredFromSSR = true;
return ssrCache[this.moduleName];
}
}
tryReadHmrState() {
if (hmrEnabled) {
const hmrCache = hmrInstanceCache.get(this.moduleName);
if (hmrCache) {
const cachedState = hmrCache.state;
hmrCache.dispose();
return cachedState;
}
}
}
combineEffects() {
const effectKeys = getDecoratedActions(this.constructor.prototype, 'Effect');
if (!effectKeys || effectKeys.length === 0) {
return (action$) => action$.pipe(operators.ignoreElements());
}
this.actionNames.push(...effectKeys);
return (action$) => {
return rxjs.merge(...effectKeys.map((name) => {
const effect = this[name];
const payload$ = action$.pipe(operators.filter(({ type }) => type === name), operators.filter((_, index) => {
const skipCount = !this.actionsToRetry.has(name) && this.actionsToSkip?.has(name) ? 1 : 0;
return skipCount <= index;
}), operators.map(({ payload }) => payload));
this.retryActionsCreator[name] = () => ({
type: RETRY_ACTION_TYPE_SYMBOL,
payload: {
module: this,
name,
},
store: this.store,
});
return effect.call(this, payload$);
}));
};
}
combineReducers() {
const reducerKeys = getDecoratedActions(this.constructor.prototype, 'Reducer', []);
const immerReducerKeys = getDecoratedActions(this.constructor.prototype, 'ImmerReducer', []);
this.actionNames.push(...reducerKeys, ...immerReducerKeys);
const immerReducers = immerReducerKeys.reduce((acc, property) => {
acc[property] = this[property].bind(this);
return acc;
}, {});
const reducers = reducerKeys.reduce((acc, property) => {
acc[property] = this[property].bind(this);
return acc;
}, {});
return (prevState, action) => {
const { type } = action;
if (type === RESET_ACTION_TYPE_SYMBOL) {
return this.getDefaultState();
}
else {
if (reducers[type]) {
return reducers[type](prevState, action.payload);
}
else if (immerReducers[type]) {
return immer.produce(prevState, (draft) => immerReducers[type](draft, action.payload));
}
}
return prevState;
};
}
combineDefineActions() {
const defineActionKeys = getDecoratedActions(this.constructor.prototype, 'DefineAction', []);
this.actionNames.push(...defineActionKeys);
return defineActionKeys;
}
}
const DefineAction = createActionDecorator('DefineAction');
const ImmerReducer = createActionDecorator('ImmerReducer');
const Reducer = createActionDecorator('Reducer');
const Effect = (options) => {
const effectDecorator = createActionDecorator('Effect');
if (options && (options.ssr || options.payloadGetter)) {
const { payloadGetter, skipFirstClientDispatch } = {
payloadGetter: undefined,
skipFirstClientDispatch: true,
...options,
};
return (target, propertyKey, descriptor) => {
addSSREffectMeta(target, { action: propertyKey, payloadGetter });
if (skipFirstClientDispatch) {
addActionToSkip(target, propertyKey);
}
return Effect()(target, propertyKey, descriptor);
};
}
return effectDecorator();
};
const configSets = new Set();
const Module = (name) => {
if (typeof name !== 'string') {
throw new TypeError('Module name should be string');
}
if (configSets.has(name)) {
if (hmrEnabled) {
console.warn(`Duplicated Module name found: \`${name}\`. this warning may caused by two reasons:
1. You defined two modules with the same name passed. If so, you should check your definitions and avoid it.
2. We detected your code is running with HMR environment. If so, you can safely ignore this warning.`);
}
else {
throw new Error(`Duplicated Module name: ${name}`);
}
}
else {
configSets.add(name);
}
return (target) => {
target.prototype.moduleName = name;
return di.Injectable()(target);
};
};
if (hmrEnabled) {
Module.removeModule = (name, instance) => {
configSets.delete(name);
hmrInstanceCache.set(name, instance);
};
}
exports.ACTION_TO_SKIP_SYMBOL = ACTION_TO_SKIP_SYMBOL;
exports.DEFINE_ACTION_DECORATOR_SYMBOL = DEFINE_ACTION_DECORATOR_SYMBOL;
exports.DefineAction = DefineAction;
exports.EFFECT_DECORATOR_SYMBOL = EFFECT_DECORATOR_SYMBOL;
exports.Effect = Effect;
exports.EffectModule = EffectModule;
exports.GLOBAL_KEY_SYMBOL = GLOBAL_KEY_SYMBOL;
exports.IMMER_REDUCER_DECORATOR_SYMBOL = IMMER_REDUCER_DECORATOR_SYMBOL;
exports.INIT_ACTION_TYPE_SYMBOL = INIT_ACTION_TYPE_SYMBOL;
exports.ImmerReducer = ImmerReducer;
exports.Module = Module;
exports.NOOP_ACTION_TYPE_SYMBOL = NOOP_ACTION_TYPE_SYMBOL;
exports.REDUCER_DECORATOR_SYMBOL = REDUCER_DECORATOR_SYMBOL;
exports.RESET_ACTION_TYPE_SYMBOL = RESET_ACTION_TYPE_SYMBOL;
exports.RETRY_ACTION_TYPE_SYMBOL = RETRY_ACTION_TYPE_SYMBOL;
exports.RETRY_KEY_SYMBOL = RETRY_KEY_SYMBOL;
exports.Reducer = Reducer;
exports.SSR_ACTION_META_SYMBOL = SSR_ACTION_META_SYMBOL;
exports.TERMINATE_ACTION_TYPE_SYMBOL = TERMINATE_ACTION_TYPE_SYMBOL;
exports.addActionToSkip = addActionToSkip;
exports.addSSREffectMeta = addSSREffectMeta;
exports.createActionDecorator = createActionDecorator;
exports.getActionsToSkip = getActionsToSkip;
exports.getDecoratedActions = getDecoratedActions;
exports.getSSREffectMeta = getSSREffectMeta;
exports.replaceLogger = replaceLogger;
//# sourceMappingURL=index.js.map