UNPKG

@o3r/core

Version:
522 lines (504 loc) • 20.5 kB
import { map, filter, EMPTY, from, of, isObservable, identity, BehaviorSubject, merge, observeOn, animationFrameScheduler } from 'rxjs'; import { tap, startWith, pairwise, switchMap, catchError, filter as filter$1, delay, finalize, mergeMap, bufferCount, concatMap, scan } from 'rxjs/operators'; import { v4 } from 'uuid'; /* eslint-disable @typescript-eslint/naming-convention -- exception for `BuildTimeProperties` */ /** * Library build time default properties */ const DEFAULT_BUILD_PROPERTIES = { DEBUG_MODE: true, APP_BASE_HREF: '.', APP_VERSION: '0.0.0', DEFAULT_LOC_BUNDLE_NAME: '', DEVTOOL_HISTORY_SIZE: 20, ENABLE_GHOSTING: false, ENABLE_WEBSTORAGE: true, ENVIRONMENT: 'dev', LOCALIZATION_BUNDLES_OUTPUT: 'localizations/', USE_MOCKS: false }; /** * Private field where Otter component information are stored */ const otterComponentInfoPropertyName = '__otter-info__'; /** * Decorates an Angular component to provide Otter information * @param info Information to define the Otter component * @returns the component with the information */ // eslint-disable-next-line @typescript-eslint/naming-convention -- decorator should be PascalCase function O3rComponent(info) { return (constructor) => { const componentName = constructor.name; constructor.prototype[otterComponentInfoPropertyName] = { ...info, componentName }; return constructor; }; } /** * Compute the name of the component with the library's name to generate unique component identifier used in metadata and different modules * @param componentName Name of the component to get the configuration * @param libraryName Name of the library the component is coming from */ function computeItemIdentifier(componentName, libraryName) { return (libraryName ? libraryName + '#' : '') + componentName; } /** Type of a message exchanged within the Otter Framework */ const otterMessageType = 'otter'; /** Target of a message that should be handled by the application */ const applicationMessageTarget = 'app'; /** * Determine if a message should be handle by the application * @param message Message to analyze */ const isToAppOtterMessage = (message) => { return message?.to === applicationMessageTarget; }; /** * Determine if a message is emitted by an Otter tool * @param message Message to analyze */ const isOtterMessage = (message) => { return message?.type === otterMessageType; }; /** * Send an Otter Message * @param dataType Type of the message * @param content content of the message * @param preStringify determine if the message should JSON.stringify before being send (will use the default mechanism otherwise) */ const sendOtterMessage = (dataType, content, preStringify = true) => { const message = { type: otterMessageType, content: { ...content, dataType } }; return window.postMessage(preStringify ? JSON.stringify(message) : message, '*'); }; /** * Filter the Otter message that should be handle by the application and returns it content * @param predicate condition to filter the message * @returns content of the message */ /** * Operator to get only Otter messages that match the predicate * @param predicate */ function filterMessageContent(predicate) { return (source$) => { const obs = source$.pipe(map((event) => { const data = event.data; return typeof data === 'string' ? JSON.parse(data) : data; }), filter(isOtterMessage), filter(isToAppOtterMessage), map((message) => message.content)); if (predicate) { return obs.pipe(filter(predicate)); } return obs; }; } const asyncStoreItemAdapter = { addRequest: (item, requestId) => { return { ...item, requestIds: [...item.requestIds, requestId], isFailure: item.requestIds.length === 0 ? false : item.isFailure, isPending: true }; }, failRequest: (item, requestId) => { const requestIds = requestId ? item.requestIds.filter((id) => id !== requestId) : item.requestIds; return { ...item, isFailure: true, isPending: requestIds.length > 0, requestIds }; }, resolveRequest: (item, requestId) => { const requestIds = requestId ? item.requestIds.filter((id) => id !== requestId) : item.requestIds; return { ...item, requestIds, isPending: requestIds.length > 0 }; }, initialize: (entityItem) => { return { ...entityItem, requestIds: [] }; }, extractAsyncStoreItem: (entityItem) => { return { requestIds: [...entityItem.requestIds], isFailure: entityItem.isFailure, isPending: entityItem.isPending }; }, clearAsyncStoreItem: (entityItem) => { const { isPending, isFailure, ...newResponse } = { ...entityItem, requestIds: [] }; return newResponse; }, merge: (...items) => { return items.reduce((mergedItem, item) => item ? { requestIds: [...mergedItem.requestIds, ...item.requestIds], isFailure: mergedItem.isFailure || item.isFailure, isPending: mergedItem.isPending || item.isPending } : mergedItem, asyncStoreItemAdapter.initialize({})); }, entityStatusAddRequest: (status, subResource, requestId) => { const currentSubStatus = status[subResource] || asyncStoreItemAdapter.initialize({}); return { ...status, [subResource]: asyncStoreItemAdapter.addRequest(currentSubStatus, requestId) }; }, entityStatusResolveRequest: (status, subResource, requestId) => { const currentSubStatus = status[subResource]; return { ...status, [subResource]: currentSubStatus ? asyncStoreItemAdapter.resolveRequest(currentSubStatus, requestId) : { requestIds: [] } }; }, entityStatusFailRequest: (status, subResource, requestId) => { const currentSubStatus = status[subResource]; return { ...status, [subResource]: currentSubStatus ? asyncStoreItemAdapter.failRequest(currentSubStatus, requestId) : { requestIds: [], isFailure: true } }; }, entityStatusResetFailure: (status, subResource) => { const currentSubStatus = status[subResource]; return { ...status, [subResource]: currentSubStatus ? asyncStoreItemAdapter.resetFailureStatus(currentSubStatus) : { requestIds: [] } }; }, resetFailureStatus: (entityItem) => { return { ...entityItem, isFailure: false }; }, setLoadingStatus: (entityItem) => { return { ...entityItem, isPending: true }; } }; /** * Create an Asynchronous Request Entity Adapter * @param adapter Entity Adapter */ function createEntityAsyncRequestAdapter(adapter) { const addRequestOne = (state, id, requestId) => { const currentEntity = typeof id !== 'undefined' && id !== null && state.entities[id]; if (currentEntity) { const changes = asyncStoreItemAdapter.addRequest(asyncStoreItemAdapter.extractAsyncStoreItem(currentEntity), requestId); return adapter.updateOne({ id, changes }, state); } return asyncStoreItemAdapter.addRequest(state, requestId); }; const addRequestMany = (state, ids, requestId) => adapter.updateMany(ids.filter((id) => !!state.entities[id]).map((id) => ({ id, changes: asyncStoreItemAdapter.addRequest(asyncStoreItemAdapter.extractAsyncStoreItem(state.entities[id]), requestId) })), state); const resolveRequestOne = (state, entity, requestId, idProperty = 'id') => { const currentEntity = state.entities[entity[idProperty]]; const newEntity = currentEntity ? asyncStoreItemAdapter.resolveRequest({ ...entity, ...asyncStoreItemAdapter.extractAsyncStoreItem(currentEntity) }, requestId) : asyncStoreItemAdapter.initialize(entity); state = asyncStoreItemAdapter.resolveRequest(state, requestId); return adapter.upsertOne(newEntity, state); }; const resolveRequestMany = (state, entities, requestId, idProperty = 'id') => adapter.updateMany(entities.filter((entity) => !!state.entities[entity[idProperty]]).map((entity) => { const model = { ...entity, ...asyncStoreItemAdapter.extractAsyncStoreItem(state.entities[entity[idProperty]]) }; return { id: entity[idProperty], changes: asyncStoreItemAdapter.resolveRequest(model, requestId) }; }), state); const failRequestMany = (state, ids = [], requestId) => { if (ids.length > 0 && !ids.some((id) => state.entities[id] === undefined)) { return adapter.updateMany(ids.map((id) => ({ id, changes: asyncStoreItemAdapter.failRequest(asyncStoreItemAdapter.extractAsyncStoreItem(state.entities[id]), requestId) })), state); } return asyncStoreItemAdapter.failRequest(state, requestId); }; return { ...adapter, failRequestMany, addRequestOne, addRequestMany, resolveRequestOne, resolveRequestMany }; } /** * Determine if the action is an AsyncRequest action * @param action Redux Action */ function isCallAction(action) { if (!action) { return false; } return !!action.call && action.call instanceof Promise; } /** * Determine if the action is an AsyncRequest action with a Request ID * @param action Redux Action */ function isIdentifiedCallAction(action) { return isCallAction(action) && typeof action.requestId !== 'undefined'; } /** * Determine if the given item implements the AsyncRequest interface * @param item */ function isAsyncRequest(item) { return typeof item.requestId !== 'undefined'; } /** * Determine if the given parameter is a Promise * @param object */ const isPromise = (object) => object && typeof object === 'object' && typeof object.then !== 'undefined'; /** * Custom operator to use instead of SwitchMap with effects based on FromApi actions. * It makes sure to emit an action when the inner subscription is unsubscribed in order to keep the store up-to-date with pending information. * @param successHandler function that returns the action to emit in case the FromApi call is a success * @param errorHandler function that returns the action to emit in case the FromApi call fails * @param cancelRequestActionFactory function that returns the action to emit in case the FromApi action is 'cancelled' because a new action was received by the switchMap */ function fromApiEffectSwitchMap(successHandler, errorHandler, cancelRequestActionFactory) { const pendingRequestIdsContext = {}; return (source$) => source$.pipe(tap((action) => { if (isIdentifiedCallAction(action)) { pendingRequestIdsContext[action.requestId] = true; } }), startWith(undefined), pairwise(), switchMap(([previousAction, action]) => { if (!action) { return EMPTY; } const isPreviousActionStillRunning = isIdentifiedCallAction(previousAction) && pendingRequestIdsContext[previousAction.requestId]; const cleanStack = () => { if (isIdentifiedCallAction(action)) { delete pendingRequestIdsContext[action.requestId]; } }; return from(action.call).pipe(tap(cleanStack), switchMap((result) => { const success = successHandler(result, action); return isObservable(success) ? success : (isPromise(success) ? success : of(success)); }), catchError((error) => { cleanStack(); return errorHandler?.(error, action) || EMPTY; }), isPreviousActionStillRunning && cancelRequestActionFactory ? startWith(cancelRequestActionFactory({ requestId: previousAction.requestId }, action)) : identity); })); } /** * Same as {@link fromApiEffectSwitchMap}, instead one inner subscription is kept by id. * @param successHandler * @param errorHandler * @param cancelRequestActionFactory * @param cleanUpTimer */ function fromApiEffectSwitchMapById(successHandler, errorHandler, cancelRequestActionFactory, cleanUpTimer) { const innerSourcesById = {}; return (source$) => { return source$.pipe(filter$1((action) => { if (!isIdentifiedCallAction(action) || !action.id) { return false; } if (isIdentifiedCallAction(action) && innerSourcesById[action.id]) { innerSourcesById[action.id][0].next(action); return false; } return true; }), switchMap((action) => { const newIdSubject = new BehaviorSubject(action); const newId$ = newIdSubject.pipe(fromApiEffectSwitchMap(successHandler, errorHandler, cancelRequestActionFactory)); innerSourcesById[action.id] = [newIdSubject, newId$]; if (cleanUpTimer !== undefined) { newIdSubject.pipe(switchMap((myAction) => from(myAction.call).pipe(delay(cleanUpTimer), finalize(() => { delete innerSourcesById[myAction.id]; newIdSubject.complete(); })))).subscribe(); } const streams = Object.values(innerSourcesById).map(([_, obs]) => obs); return merge(...streams); })); }; } /** * Returns a creator that makes sure that requestId is defined in the action's properties by generating one * if needed. */ const asyncProps = () => { return (props) => isAsyncRequest(props) ? props : { ...props, requestId: v4() }; }; /** * Serializer for asynchronous store. * @param state State of an asynchronous store to serialize * @returns a plain json object to pass to json.stringify */ function asyncSerializer(state) { return asyncStoreItemAdapter.clearAsyncStoreItem(state); } /** * Serializer for asynchronous entity store. * @param state State of an asynchronous entity store to serialize * @returns a plain json object to pass to json.stringify */ function asyncEntitySerializer(state) { const entities = state.ids.reduce((entitiesAcc, entityId) => { entitiesAcc[entityId] = asyncStoreItemAdapter.clearAsyncStoreItem(state.entities[entityId]); return entitiesAcc; }, {}); return { ...asyncStoreItemAdapter.clearAsyncStoreItem(state), entities }; } /** * Serializer for asynchronous entity store with status. * @param state State of an asynchronous entity store with status to serialize * @returns a plain json object to pass to json.stringify */ function asyncEntityWithStatusSerializer(state) { const entities = state.ids.reduce((entitiesAcc, entityId) => { entitiesAcc[entityId] = { ...asyncStoreItemAdapter.clearAsyncStoreItem(state.entities[entityId]), status: {} }; return entitiesAcc; }, {}); return { ...asyncStoreItemAdapter.clearAsyncStoreItem(state), entities }; } class StateSerializer { constructor(serializer) { this.reviver = (_, value) => value; this.serialize = serializer.serialize || this.serialize; this.deserialize = serializer.deserialize || this.deserialize; this.reviver = serializer.reviver || this.reviver; this.replacer = serializer.replacer || this.replacer; this.initialState = serializer.initialState || this.initialState; } } /** * Pad number * @param val * @param digits */ function padNumber(val, digits = 2) { const str = `${val}`; return '0'.repeat(Math.max(0, digits - str.length)) + str; } /** * Returns TRUE if bootstrap config environment is production FALSE otherwise * @param dataset * @returns TRUE if bootstrap config environment is production FALSE otherwise */ function isProductionEnvironment(dataset) { const bootstrapConfig = dataset.bootstrapconfig && JSON.parse(dataset.bootstrapconfig); return bootstrapConfig?.environment === 'prod'; } const defaultConstruct = (data) => data; const isDate = (data) => data instanceof Date && !Number.isNaN(data); /** * Check if an object is not an array or a date * @param obj * @param additionalMappers */ function isObject(obj, additionalMappers) { return obj instanceof Object && !Array.isArray(obj) && !additionalMappers?.some((mapper) => mapper.condition(obj)) && !isDate(obj); } /** * Return a new reference of the given object * @param obj * @param additionalMappers */ function immutablePrimitive(obj, additionalMappers) { if (Array.isArray(obj)) { return obj.slice(); } const matchingPrimitive = additionalMappers?.find((mapper) => mapper.condition(obj)); const resolvedType = matchingPrimitive && ((matchingPrimitive.construct || defaultConstruct)(obj)); if (resolvedType !== undefined) { return resolvedType; } if (isDate(obj)) { return new Date(obj); } else if (obj instanceof Object) { return deepFill(obj, obj, additionalMappers); } else { return obj; } } /** * Deep fill of base object using source * It will do a deep merge of the objects, overriding arrays * All properties not present in source, but present in base, will remain * @param base * @param source * @param additionalMappers Map of conditions of type mapper */ function deepFill(base, source, additionalMappers) { if (typeof source === 'undefined') { return deepFill(base, base, additionalMappers); } if (!isObject(base, additionalMappers)) { return immutablePrimitive(typeof source === 'undefined' ? base : source, additionalMappers); } const newObj = { ...base }; for (const key in base) { if (source[key] === null) { newObj[key] = immutablePrimitive(null, additionalMappers); } else if (key in source && typeof base[key] === typeof source[key]) { const keyOfSource = source[key]; newObj[key] = typeof keyOfSource === 'undefined' ? immutablePrimitive(base[key], additionalMappers) : deepFill(base[key], keyOfSource, additionalMappers); } else { newObj[key] = immutablePrimitive(base[key], additionalMappers); } } // getting keys present in source and not present in base for (const key in source) { if (!(key in newObj)) { newObj[key] = immutablePrimitive(source[key], additionalMappers); } } return newObj; } /** * Buffers and emits data for lazy/progressive rendering of big lists * That could solve issues with long-running tasks when trying to render an array * of similar components. * @param delayMs Delay between data emits * @param concurrency Amount of elements that should be emitted at once */ function lazyArray(delayMs = 0, concurrency = 2) { let isFirstEmission = true; return (source$) => { return source$.pipe(mergeMap((items) => { if (!isFirstEmission) { return of(items); } const items$ = from(items); return items$.pipe(bufferCount(concurrency), concatMap((value, index) => { return of(value).pipe(observeOn(animationFrameScheduler), delay(index * delayMs)); }), scan((acc, steps) => { return [...acc, ...steps]; }, []), tap((scannedItems) => { const scanDidComplete = scannedItems.length === items.length; if (scanDidComplete) { isFirstEmission = false; } })); })); }; } /** * Generated bundle index. Do not edit. */ export { DEFAULT_BUILD_PROPERTIES, O3rComponent, StateSerializer, applicationMessageTarget, asyncEntitySerializer, asyncEntityWithStatusSerializer, asyncProps, asyncSerializer, asyncStoreItemAdapter, computeItemIdentifier, createEntityAsyncRequestAdapter, deepFill, filterMessageContent, fromApiEffectSwitchMap, fromApiEffectSwitchMapById, immutablePrimitive, isAsyncRequest, isCallAction, isIdentifiedCallAction, isObject, isOtterMessage, isProductionEnvironment, isToAppOtterMessage, lazyArray, otterComponentInfoPropertyName, otterMessageType, padNumber, sendOtterMessage }; //# sourceMappingURL=o3r-core.mjs.map