UNPKG

@activejs/core

Version:

Pragmatic, Reactive State Management for JavaScript Apps

1,551 lines (1,535 loc) 139 kB
import { Observable, Subject, BehaviorSubject, merge } from 'rxjs'; import { map, distinctUntilChanged } from 'rxjs/operators'; /* MIT License Copyright (c) 2020 Ankit Singh <dabalyan@hotmail.com> 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. This license applies to the complete mono-repo, unless specified otherwise. */ // ____________________________ Common Events ____________________________ // // _______________________________________________________________________ // /** * An event that gets emitted on successful replay by using the `replay` method. * @event * @category Common */ class EventReplay { /** * @param value The current value that got replayed. */ constructor(value) { this.value = value; } } // _________________________ Common Units Events _________________________ // // _______________________________________________________________________ // /** * An event that gets emitted on successful dispatch by using the Units' `dispatch` method. * @event * @category Common Units */ class EventUnitDispatch { /** * @param value The value that was passed to the dispatch method. * @param options The options that were passed to the dispatch method. */ constructor(value, options) { this.value = value; this.options = options; } } /** * All the reasons for why a Unit dispatch might fail. */ var DispatchFailReason; (function (DispatchFailReason) { /** * The first reason, if the Unit is frozen. */ DispatchFailReason["FROZEN_UNIT"] = "FROZEN_UNIT"; /** * The second reason, if the dispatched value is invalid. */ DispatchFailReason["INVALID_VALUE"] = "INVALID_VALUE"; /** * The third reason, if {@link UnitConfig.customDispatchCheck} returns a `falsy` value. */ DispatchFailReason["CUSTOM_DISPATCH_CHECK"] = "CUSTOM_DISPATCH_CHECK"; /** * The fourth reason, if {@link UnitConfig.distinctDispatchCheck} is not `false` and the dispatched value is same as current value. */ DispatchFailReason["DISTINCT_CHECK"] = "DISTINCT_CHECK"; })(DispatchFailReason || (DispatchFailReason = {})); /** * An event that gets emitted on failed dispatch using the Units' `dispatch` method. * @event * @category Common Units */ class EventUnitDispatchFail { /** * @param value The value that was passed to the dispatch method. * @param reason The reason for why the dispatch failed. * @param options The options that were passed to the dispatch method. */ constructor(value, reason, options) { this.value = value; this.reason = reason; this.options = options; } } /** * An event that gets emitted on successful unmute using the Units' `unmute` method. * @event * @category Common Units */ class EventUnitUnmute { } /** * An event that gets emitted when a Unit gets frozen. * @event * @category Common Units */ class EventUnitFreeze { } /** * An event that gets emitted when a Unit gets unfrozen after being frozen. * @event * @category Common Units */ class EventUnitUnfreeze { } /** * An event that gets emitted on successful cache-navigation, * using the Units' several cache-navigation methods like `goBack`, `goForward`, `jump`, etc. * @event * @category Common Units */ class EventUnitJump { /** * @param steps The number of steps jumped represented as a number, * positive for forward navigation and negative for backwards. * @param newCacheIndex The new `cacheIndex` of the emitted value. */ constructor(steps, newCacheIndex) { this.steps = steps; this.newCacheIndex = newCacheIndex; } } /** * An event that gets emitted on successful execution of Units' `clearCache` method. * @event * @category Common Units */ class EventUnitClearCache { /** * @param options The options that were directly or indirectly passed to the `clearCache` method. */ constructor(options) { this.options = options; } } /** * An event that gets emitted on successful execution of Units' `clearValue` method. * @event * @category Common Units */ class EventUnitClearValue { } /** * An event that gets emitted on successful execution of Units' `clear` method. * @event * @category Common Units */ class EventUnitClear { /** * @param options The options that were passed for the implicitly called `clearCache` method. */ constructor(options) { this.options = options; } } /** * An event that gets emitted on successful execution of Units' `resetValue` method. * @event * @category Common Units */ class EventUnitResetValue { } /** * An event that gets emitted on successful execution of Units' `reset` method. * @event * @category Common Units */ class EventUnitReset { /** * @param options The options that were passed for the implicitly called `clearCache` method. */ constructor(options) { this.options = options; } } /** * An event that gets emitted on successful execution of Units' `clearPersistentValue` method. * @event * @category Common Units */ class EventUnitClearPersistedValue { } // ____________________________ DictUnit Events ____________________________ // // _________________________________________________________________________ // /** * An event that gets emitted on successful execution of DictUnit's `set` method. * @event * @category DictUnit */ class EventDictUnitSet { /** * @param key The name of the property that was passed to the `set` method. * @param value The value of the property that was passed to the `set` method. */ constructor(key, value) { this.key = key; this.value = value; } } /** * An event that gets emitted on successful execution of DictUnit's `assign` method. * @event * @category DictUnit */ class EventDictUnitAssign { /** * @param sources The source objects that were passed to the `assign` method. * @param newProps The new properties that finally got added to the DictUnit's value. */ constructor(sources, newProps) { this.sources = sources; this.newProps = newProps; } } /** * An event that gets emitted on successful execution of DictUnit's `delete` or `deleteIf` method. * @event * @category DictUnit */ class EventDictUnitDelete { /** * @param deletedProps The properties that were deleted by the `delete` or `deleteIf` method. */ constructor(deletedProps) { this.deletedProps = deletedProps; } } // ____________________________ ListUnit Events ____________________________ // // _________________________________________________________________________ // /** * An event that gets emitted on successful execution of ListUnit's `set` method. * @event * @category ListUnit */ class EventListUnitSet { /** * @param index The index for the item passed to the `set` method. * @param item The item passed to the `set` method. */ constructor(index, item) { this.index = index; this.item = item; } } /** * An event that gets emitted on successful execution of ListUnit's `pop` method. * @event * @category ListUnit */ class EventListUnitPop { /** * @param item The item that got popped from the ListUnit's value. */ constructor(item) { this.item = item; } } /** * An event that gets emitted on successful execution of ListUnit's `push` method. * @event * @category ListUnit */ class EventListUnitPush { /** * @param items The items that were passed to the `push` method. */ constructor(items) { this.items = items; } } /** * An event that gets emitted on successful execution of ListUnit's `shift` method. * @event * @category ListUnit */ class EventListUnitShift { /** * @param item The item that got shifted out. */ constructor(item) { this.item = item; } } /** * An event that gets emitted on successful execution of ListUnit's `unshift` method. * @event * @category ListUnit */ class EventListUnitUnshift { /** * @param items The items that were passed to the `unshift` method. */ constructor(items) { this.items = items; } } /** * An event that gets emitted on successful execution of ListUnit's `delete` or `deleteIf` method. * @event * @category ListUnit */ class EventListUnitDelete { /** * @param indices The indices that were passed to the `delete` method explicitly, * or implicitly by `deleteIf` method. * @param deletedItems The items that got deleted from the ListUnit's value. */ constructor(indices, deletedItems) { this.indices = indices; this.deletedItems = deletedItems; } } /** * An event that gets emitted on successful execution of ListUnit's `remove` or `removeIf` method. * @event * @category ListUnit */ class EventListUnitRemove { /** * @param indices The indices that were passed to the `remove` method explicitly, * or implicitly by `removeIf` method. * @param removedItems The items that got removed from the ListUnit's value. */ constructor(indices, removedItems) { this.indices = indices; this.removedItems = removedItems; } } /** * An event that gets emitted on successful execution of ListUnit's `splice` or `insert` method. * @event * @category ListUnit */ class EventListUnitSplice { /** * @param start The zero-based location that was passed to the `splice` method. * @param deleteCount The number of items that were to be removed. * @param removedItems The items that got removed. * @param addedItems The items that were passed as `items` to be added. */ constructor(start, deleteCount, removedItems, addedItems) { this.start = start; this.deleteCount = deleteCount; this.removedItems = removedItems; this.addedItems = addedItems; } } /** * An event that gets emitted on successful execution of ListUnit's `fill` method. * @event * @category ListUnit */ class EventListUnitFill { /** * @param item The item that was passed to the `fill` method. * @param start The starting position where the filling started. * @param end The last position where the filling stopped. */ constructor(item, start, end) { this.item = item; this.start = start; this.end = end; } } /** * An event that gets emitted on successful execution of ListUnit's `copyWithin` method. * @event * @category ListUnit */ class EventListUnitCopyWithin { /** * @param target The target position from where the copied section starts replacing. * @param start The starting position of the copied section. * @param end The ending position of the copied section. */ constructor(target, start, end) { this.target = target; this.start = start; this.end = end; } } /** * An event that gets emitted on successful execution of ListUnit's `reverse` method. * @event * @category ListUnit */ class EventListUnitReverse { } /** * An event that gets emitted on successful execution of ListUnit's `sort` method. * @event * @category ListUnit */ class EventListUnitSort { } /** * @internal please do not use. */ const NOOP = () => { }; /** * @internal please do not use. */ const IteratorSymbol = (typeof Symbol === 'function' && Symbol.iterator) || /* istanbul ignore next */ '@@iterator'; /** * @internal please do not use. */ function isValidId(id) { return typeof id === 'string' && !!id.trim().length; } /** * @internal please do not use. */ function isDict(o) { return Object.prototype.toString.call(o) === '[object Object]'; } /** * @internal please do not use. */ function isObject(o) { return o != null && typeof o === 'object'; } /** * @internal please do not use. */ function isValidKey(key) { return typeof key === 'string' || typeof key === 'number'; } /** * @internal please do not use. */ function isValidIndex(i) { const a = []; a[i] = 1; return !!a.length && a[i] === 1; } /** * @internal please do not use. */ function normalizeIndex(index, arrLength) { return index < 0 ? (index < -arrLength ? 0 : arrLength + index) : index; } /** * @internal please do not use. */ function sanitizeIndices(indices, arrLength) { const sanitizedIndices = []; indices.forEach(index => { index = normalizeIndex(index, arrLength); if (index < arrLength && isValidIndex(index)) { sanitizedIndices.push(index); } }); return deDuplicate(sanitizedIndices); } /** * @internal please do not use. */ function isNumber(n) { return typeof n === 'number' && !isNaN(n); } /** * @internal please do not use. */ // tslint:disable-next-line:ban-types function isFunction(fn) { return typeof fn === 'function'; } /** * @internal please do not use. */ /*export function isNativeFn(fn: any): fn is () => any { return /{\s*?\[native code]\s*?}/.test('' + fn); }*/ /** * Creates a clone of the provided value.\ * All the primitives are returned as is, since they are immutable.\ * Non-primitives that this function can clone are array and object-literal.\ * Other non-primitives are returned as is. * * This function is also used internally by ActiveJS. * * @param o The value to be cloned. * @returns A clone of the provided value. * * @category Global */ function deepCopy(o) { if (o == null || typeof o !== 'object') { return o; } if (Array.isArray(o)) { return o.map(v => deepCopy(v)); } if (isDict(o)) { return Object.keys(o).reduce((newO, k) => { newO[k] = deepCopy(o[k]); return newO; }, {}); } return o; } /** * @internal please do not use. */ function deepFreeze(o) { if (!isObject(o)) { return o; } if (Array.isArray(o)) { o.forEach(v => deepFreeze(v)); } else if (isDict(o)) { Object.keys(o).forEach(k => deepFreeze(o[k])); } try { return Object.freeze(o); } catch (e) { return o; } } /** * @internal please do not use. */ function deDuplicate(arr) { if (typeof Set === 'function') { return [...new Set(arr)]; } else { return arr.filter((x, i) => arr.indexOf(x) === i); } } /** * @internal please do not use. */ function isSerializable(o) { if (o == null || typeof o === 'string' || typeof o === 'boolean' || typeof o === 'number') { return [true]; } /*if (typeof o === 'number') { return o === Infinity || o === -Infinity ? [false, o] : [true]; }*/ if (Array.isArray(o)) { let foundPositive; o.find(v => { const testResult = isSerializable(v); if (testResult[0] === false) { foundPositive = testResult; return true; } }); return foundPositive || [true]; } else if (o.constructor === Object) { let foundPositive; Object.keys(o).find(k => { const testResult = isSerializable(o[k]); if (testResult[0] === false) { foundPositive = testResult; return true; } }); return foundPositive || [true]; } return [false, o]; } /** * @internal please do not use. */ function findIndex(array, predicate, fromIndex) { let i = isValidIndex(fromIndex) ? Math.max(0, Math.min(fromIndex, array.length - 1)) : 0; while (i < array.length) { if (predicate(array[i], i, array)) { return i; } ++i; } return -1; } /** * @internal please do not use. */ function findIndexBackwards(array, predicate, fromIndex) { let i = isValidIndex(fromIndex) ? Math.max(0, Math.min(fromIndex, array.length - 1)) : array.length - 1; while (i > -1) { if (predicate(array[i], i, array)) { return i; } --i; } return -1; } /** * @internal please do not use. */ function debounce(func, waitTime, callMode) { if (!isNumber(waitTime)) { waitTime = 200; } if (!['START', 'END', 'BOTH'].includes(callMode)) { callMode = 'END'; } let timeout; return function (...args) { const context = this; const later = () => { timeout = null; if (callMode !== 'START') { return func.apply(context, args); } }; const callNow = callMode !== 'END' && !timeout; clearTimeout(timeout); timeout = setTimeout(later, waitTime); if (callNow) { return func.apply(context, args); } }; } /** * @internal please do not use. */ function makeNonEnumerable(o) { if (o == null || typeof o !== 'object') { return; } Object.keys(o).forEach(key => { Object.defineProperty(o, key, { enumerable: false, }); }); } /** * @internal please do not use. */ function generateAsyncSystemIds(systemId, queryConfig, dataConfig, errorConfig, pendingConfig) { var _a, _b, _c, _d; const ids = Object.assign(Object.assign(Object.assign(Object.assign({}, ((queryConfig === null || queryConfig === void 0 ? void 0 : queryConfig.hasOwnProperty('id')) && { queryUnitId: queryConfig.id })), ((dataConfig === null || dataConfig === void 0 ? void 0 : dataConfig.hasOwnProperty('id')) && { dataUnitId: dataConfig.id })), ((errorConfig === null || errorConfig === void 0 ? void 0 : errorConfig.hasOwnProperty('id')) && { errorUnitId: errorConfig.id })), ((pendingConfig === null || pendingConfig === void 0 ? void 0 : pendingConfig.hasOwnProperty('id')) && { pendingUnitId: pendingConfig.id })); if (isValidId(systemId)) { ids.queryUnitId = (_a = ids.queryUnitId) !== null && _a !== void 0 ? _a : systemId + '_QUERY'; ids.dataUnitId = (_b = ids.dataUnitId) !== null && _b !== void 0 ? _b : systemId + '_DATA'; ids.errorUnitId = (_c = ids.errorUnitId) !== null && _c !== void 0 ? _c : systemId + '_ERROR'; ids.pendingUnitId = (_d = ids.pendingUnitId) !== null && _d !== void 0 ? _d : systemId + '_PENDING'; } return ids; } /** * @internal please do not use. */ function plucker(o, path) { const length = Array.isArray(path) ? path.length : 0; for (let i = 0; i < length; i++) { if (o == null) { return undefined; } o = Object.prototype.hasOwnProperty.call(o, path[i]) ? o[path[i]] : undefined; } return o; } /** * @internal please do not use. */ function hashCode(str) { // tslint:disable:no-bitwise let hash = 0; const length = typeof str === 'string' ? str.length : 0; if (length === 0) { return String(hash); } for (let i = 0; i < length; i++) { hash = (hash << 5) - hash + str.charCodeAt(i); hash |= 0; // Convert to 32bit integer } hash = hash >>> 0; return Number(hash).toString(32).toUpperCase(); // tslint:enable:no-bitwise } /** * @internal please do not use. */ function stackTrace() { try { throw new Error(); } catch (error) { return error.stacktrace || error.stack; } } /** * @internal please do not use. */ function getLocationId(source) { if (source == null || typeof source !== 'object') { return ''; } source = source.constructor.name; const errorTrace = stackTrace(); const locationMatch = errorTrace.match(new RegExp( // get two lines beyond ActiveJS scope https://regexr.com/5eb9g `new ${source}\\b.+\\n(?:[\\s\\S]+new (?:AsyncSystem|Cluster)\\b.+\\n)?((?:.+\\n?){1,2})`)); return hashCode((locationMatch === null || locationMatch === void 0 ? void 0 : locationMatch[1]) || errorTrace); } /** * @internal please do not use. */ const UniqueIdsAndLocationIdMap = {}; /** * @internal please do not use. */ const FrozenObj = Object.freeze({}); /** * The Global configuration for all ActiveJS constructs, Units, Systems, Action and Cluster. * * See {@link https://docs.activejs.dev/guides/configuration} for more details. * * @category 4. Utility */ class Configuration { // tslint:enable:variable-name /** * The default Storage API being used for storing the values of persistent Units. * * @default `localStorage` */ static get storage() { return Configuration._storage || localStorage; } /** * Global ActiveJS environment configurations options. */ static get ENVIRONMENT() { if (this.isDevMode()) { return Configuration._ENVIRONMENT; } return Object.freeze({}); } /** * Configuration options applied to all the Actions. {@link Action} */ static get ACTION() { return Configuration._ACTION; } /** * Configuration options applied to all the Clusters. {@link Cluster} */ static get CLUSTER() { return Configuration._CLUSTER; } /** * Configuration options applied to all the Units. {@link Unit} */ static get UNITS() { return Configuration._UNITS; } /** * Configuration options applied to all the BoolUnits. {@link BoolUnit} */ static get BOOL_UNIT() { return Configuration._BOOL_UNIT; } /** * Configuration options applied to all the NumUnits. {@link NumUnit} */ static get NUM_UNIT() { return Configuration._NUM_UNIT; } /** * Configuration options applied to all the StringUnits. {@link StringUnit} */ static get STRING_UNIT() { return Configuration._STRING_UNIT; } /** * Configuration options applied to all the ListUnits. {@link ListUnit} */ static get LIST_UNIT() { return Configuration._LIST_UNIT; } /** * Configuration options applied to all the DictUnits. {@link DictUnit} */ static get DICT_UNIT() { return Configuration._DICT_UNIT; } /** * Configuration options applied to all the GenericUnits. {@link GenericUnit} */ static get GENERIC_UNIT() { return Configuration._GENERIC_UNIT; } /** * Configuration options applied to all the AsyncSystems. {@link AsyncSystem} */ static get ASYNC_SYSTEM() { return Configuration._ASYNC_SYSTEM; } /** * Sets and overrides default configurations. \ * These configurations can still be overridden at the time of instantiation. * * It should only be called once in the whole app. * Calling it second time doesn't merge the new configuration, * it simply rewrites it. * * However, the defaults are not affected by this behavior, \ * they have to be overridden specifically every time. * * @param config The configuration options. */ static set(config) { const { storage, ENVIRONMENT, ACTION, UNITS, CLUSTER, BOOL_UNIT, NUM_UNIT, STRING_UNIT, LIST_UNIT, DICT_UNIT, GENERIC_UNIT, ASYNC_SYSTEM, } = Object.assign({}, config); Configuration._storage = storage; Configuration._ENVIRONMENT = Object.freeze(Object.assign({}, ENVIRONMENT)); Configuration._ACTION = Object.freeze(Object.assign({}, ACTION)); Configuration._CLUSTER = Object.freeze(Object.assign({}, CLUSTER)); Configuration._UNITS = Object.freeze(Object.assign({}, UNITS)); Configuration._BOOL_UNIT = Object.freeze(Object.assign({}, BOOL_UNIT)); Configuration._NUM_UNIT = Object.freeze(Object.assign({}, NUM_UNIT)); Configuration._STRING_UNIT = Object.freeze(Object.assign({}, STRING_UNIT)); Configuration._LIST_UNIT = Object.freeze(Object.assign({}, LIST_UNIT)); Configuration._DICT_UNIT = Object.freeze(Object.assign({}, DICT_UNIT)); Configuration._GENERIC_UNIT = Object.freeze(Object.assign({}, GENERIC_UNIT)); Configuration._ASYNC_SYSTEM = Object.freeze(Object.assign(Object.assign({}, ASYNC_SYSTEM), { UNITS: Object.assign({}, ASYNC_SYSTEM === null || ASYNC_SYSTEM === void 0 ? void 0 : ASYNC_SYSTEM.UNITS), QUERY_UNIT: Object.assign({}, ASYNC_SYSTEM === null || ASYNC_SYSTEM === void 0 ? void 0 : ASYNC_SYSTEM.QUERY_UNIT), DATA_UNIT: Object.assign({}, ASYNC_SYSTEM === null || ASYNC_SYSTEM === void 0 ? void 0 : ASYNC_SYSTEM.DATA_UNIT), ERROR_UNIT: Object.assign({}, ASYNC_SYSTEM === null || ASYNC_SYSTEM === void 0 ? void 0 : ASYNC_SYSTEM.ERROR_UNIT), PENDING_UNIT: Object.assign({}, ASYNC_SYSTEM === null || ASYNC_SYSTEM === void 0 ? void 0 : ASYNC_SYSTEM.PENDING_UNIT) })); if (Configuration.ENVIRONMENT.checkUniqueId === true) { Object.keys(UniqueIdsAndLocationIdMap).forEach(k => { delete UniqueIdsAndLocationIdMap[k]; }); } } /** * Resets all global configurations to their default/empty state. \ * It doesn't affect any currently existing instances, it's only applicable to the instances created after this. */ static reset() { Configuration.set(null); } /** * It sets the {@link ENVIRONMENT} to it's default configuration, \ * i.e: it turns off all the checks declared in {@link ENVIRONMENT} and \ * also, disables the extra logs. */ static enableProdMode() { this._isDevMode = false; } /** * To check whether the production mode has been enabled or not. */ static isDevMode() { return this._isDevMode; } } // tslint:disable:variable-name /** * @internal please do not use. */ Configuration._isDevMode = true; /** * @internal please do not use. */ Configuration._ENVIRONMENT = FrozenObj; /** * @internal please do not use. */ Configuration._ACTION = FrozenObj; /** * @internal please do not use. */ Configuration._CLUSTER = FrozenObj; /** * @internal please do not use. */ Configuration._UNITS = FrozenObj; /** * @internal please do not use. */ Configuration._BOOL_UNIT = FrozenObj; /** * @internal please do not use. */ Configuration._NUM_UNIT = FrozenObj; /** * @internal please do not use. */ Configuration._STRING_UNIT = FrozenObj; /** * @internal please do not use. */ Configuration._LIST_UNIT = FrozenObj; /** * @internal please do not use. */ Configuration._DICT_UNIT = FrozenObj; /** * @internal please do not use. */ Configuration._GENERIC_UNIT = FrozenObj; /** * @internal please do not use. */ Configuration._ASYNC_SYSTEM = FrozenObj; /** * This is the most basic building block of ActiveJS. * * Base serves as the base for all fundamental ActiveJS constructs: Units, Systems, Actions and Clusters. * * This is an internal construct, normally you'd never have to use this class directly. * However, if you're just reading the documentation, or want to learn more about how ActiveJS works, * or want to extend this class to build something on your own, please continue. * * It extends RxJS `Observable`. * Source of this Observable is either a BehaviorSubject or Subject depending on the {@link BaseConfig.replay} flag. * By default, it's a BehaviourSubject for the Units, Systems and Clusters; and a Subject for Actions. * * There's another non-replaying Observable named `future$` to observe the value, whose source is always a simple Subject, * This Subject also serves as the source for default extended Observable when {@link BaseConfig.replay} is `false`. * * In simple terms Base is an elaborate RxJS Subject like construct, with custom features like replay on demand. * Although that's just one aspect of Base. * * It also implements `Object.prototype` methods * to make working with the stored value a bit easier and efficient. * * Other than that, It also provides functionalities like: On-demand Observable custom-event, to listen to events like manual replay. * * @category 2. Abstract */ class Base extends Observable { // tslint:enable:variable-name constructor(config) { super(); /** * @internal please do not use. */ this.futureSubject = new Subject(); /** * An Observable to observe future values, * unlike the default Observable it doesn't replay when subscribed to, * rather it waits for the next value. */ this.future$ = this.futureSubject.asObservable(); /** * @internal please do not use. */ this._emitCount = 0; this.config = Object.assign({}, config); if (this.config.id !== undefined) { if (!isValidId(this.config.id)) { throw new TypeError(`Invalid id provided, expected a non-empty string, got ${String(this.config.id)}`); } if (Configuration.ENVIRONMENT.checkUniqueId === true) { const locationId = getLocationId(this); if (UniqueIdsAndLocationIdMap[this.config.id] != null && UniqueIdsAndLocationIdMap[this.config.id] !== locationId) { throw new TypeError(`Duplicate id "${this.config.id}" detected by "checkUniqueId" check, consider assigning a unique id.`); } UniqueIdsAndLocationIdMap[this.config.id] = locationId; } } else if (this.config.persistent === true) { throw new TypeError(`An id is required for persistence to work.`); } if (this.config.replay === false) { this.sourceSubject = this.futureSubject; this.source = this.future$; } else { this.sourceSubject = new BehaviorSubject(undefined); this.source = this.sourceSubject.asObservable(); } this.setupEvents(); Object.freeze(this.config); } /** * A counter to keep track of how many times has a Unit, System, Action or Cluster emitted. * @returns Number of times a Unit, System, Action or Cluster has emitted. */ get emitCount() { return this._emitCount; } /** * Current value. * Used internally, for operations that don't mutate the value. * * @internal please do not use. * * @category Access Value */ rawValue() { return this.value(); } /** * Creates a new Observable using the default Observable as source. * Use this to conceal other aspects of a Unit, System, Action or Cluster except the Observable part. * * @returns An Observable with the value of a Unit, System, Action or Cluster. * * @category Common */ asObservable() { const observable = new Observable(); observable.source = this.source; return observable; } /** * To manually re-emit the last emitted value again. * * @triggers {@link EventReplay} * @category Common */ replay() { var _a; this.emit(this.emittedValue); if ((_a = this.eventsSubject) === null || _a === void 0 ? void 0 : _a.observers.length) { this.eventsSubject.next(new EventReplay(this.emittedValue)); } } /** * Converts the value to JSON string, using `JSON.stringify`. * * @category Common */ toJsonString() { return JSON.stringify(this.rawValue()); } /** * Returns the {@link rawValue}, * JavaScript automatically invokes it when encountering an object where a primitive value is expected. * * @alias {@link rawValue} * This method is not intended to be used by developers, use {@link rawValue} instead. * It only exists to be used by JavaScript implicitly. * * @hidden * @category Customised Object.prototype */ valueOf() { return this.rawValue(); } /** * Returns the {@link rawValue}. * JavaScript automatically invokes it when `JSON.stringify` is invoked on an object. * * @alias {@link rawValue} * This method is not intended to be used by developers, use {@link rawValue} instead. * It only exists to be used by `JSON.stringify` implicitly. * * @hidden * @category Common */ toJSON() { return this.rawValue(); } /** * @internal please do not use. */ emit(value = this.value()) { ++this._emitCount; this.emittedValue = value; if (this.sourceSubject !== this.futureSubject) { this.sourceSubject.next(value); } this.futureSubject.next(value); } /** * @internal please do not use. */ setupEvents() { Object.defineProperty(this, 'events$', { get() { if (this._events) { return this._events; } this.eventsSubject = new Subject(); this._events = this.eventsSubject.asObservable(); Object.defineProperty(this, 'eventsSubject', { enumerable: false }); Object.defineProperty(this, '_events', { enumerable: false }); return this._events; }, enumerable: false, }); } } /** * @internal please do not use. */ const MethodsNotToImplement = [ ...Object.getOwnPropertyNames(Observable.prototype), ...Object.getOwnPropertyNames(Base.prototype), ]; Object.getOwnPropertyNames(Object.prototype).forEach(method => { if (!isFunction(Object.prototype[method]) || MethodsNotToImplement.includes(method)) { return; } Object.defineProperty(Base.prototype, method, { value(...args) { return this.rawValue()[method](...args); }, }); }); /** * A wrapper on RxJS Subscription. * * Stream is a simple construct that adds the ability to re-subscribe to the provided Observable. * Given an Observable, it immediately gets subscribed, and the subscription instance is saved as Stream's property. * The Stream keeps the reference to the provided Observable, and uses it for re-subscription when asked. * * See {@link https://docs.activejs.dev/utilities/stream} for more details. * * @category 4. Utility */ class Stream { /** * Given an Observable, subscribes to it immediately, and saves the Subscription instance. * * @param observable The Observable to be used for subscription. */ constructor(observable) { this.observable = observable; if (!(observable instanceof Observable)) { throw new TypeError('Expected an Observable, got ' + observable); } this.subscribe(); makeNonEnumerable(this); } /** * The current Subscription instance. * It can be `undefined` if there's no active Subscription. */ get subscription() { return this._subscription; } /** * Indicates whether the Stream is active or not. \ * i.e.: Whether the provided Observable is subscribed or not. */ get isSubscribed() { return !!this._subscription; } /** * Subscribe to the provided Observable, and save the Subscription instance. * * @returns Subscription instance returned by {@link Observable.subscribe}. * * @category Common */ subscribe() { this._subscription = this.observable.subscribe({ error: () => this.unsubscribe() }); return this.subscription; } /** * Unsubscribes from current subscription instance using {@link Subscription.unsubscribe}. \ * It also deletes the Subscription instance. * * @category Custom Stream */ unsubscribe() { if (this.subscription) { this.subscription.unsubscribe(); delete this._subscription; } } /** * Unsubscribes and then immediately subscribes again, * also, replaces the {@link subscription} instance with the new Subscription instance. * * @returns Subscription instance returned by subscribing to the provided Observable {@link Observable.subscribe}. * * @category Custom Stream */ resubscribe() { this.unsubscribe(); return this.subscribe(); } } /** * @internal * The prefix string for ids of persistent Units, applied when saving/retrieving value to/from persistent storage, * This is intended to avoid conflicts with other items in the storage that do not belong to ActiveJS, * and to identify items in the storage that belong to ActiveJS for features like {@link clearPersistentStorage} to work as intended. */ const KeyPrefix = '_AJS_UNIT_'; /** * To clear persisted values of persistent Units from storage. * * Note: It does not clear the value of Units, only the persisted value is cleared. * * See {@link https://docs.activejs.dev/guides/persistence} for more details. * * @param storage The Storage from where the Units' persisted values need to be removed. \ * {@link Configuration.storage} is used as storage by default. \ * You can pass a reference to whichever storage you want to clean up. * @category Global */ function clearPersistentStorage(storage = Configuration.storage) { Object.keys(storage).forEach(key => { if (key.startsWith(KeyPrefix)) { storage.removeItem(key); } }); } /** * @internal please do not use. */ function save(key, value, storage = Configuration.storage) { let jsonString; try { // wrap the value to later easily determine whether any value has been // saved to storage or not. // eg: If storage.get('item') is null, it can mean many things, // but {value: null} can only mean that the value is null. jsonString = JSON.stringify({ value }); } catch (e) { /* istanbul ignore next */ jsonString = JSON.stringify({ value: String(value) }); } storage.setItem(KeyPrefix + key, jsonString); } /** * @internal please do not use. */ function retrieve(key, storage = Configuration.storage) { const raw = storage.getItem(KeyPrefix + key); try { return JSON.parse(raw); } catch (e) { /* istanbul ignore next */ return null; } } /** * @internal please do not use. */ function remove(key, storage = Configuration.storage) { storage.removeItem(KeyPrefix + key); } /** * @internal please do not use. */ function logInfo(...messages) { const { logLevel } = Configuration.ENVIRONMENT; if (isNumber(logLevel) && logLevel >= 2 /* INFO */) { // tslint:disable-next-line:no-console return console.info.bind(console, ...messages); } return NOOP; } /** * @internal please do not use. */ function logWarn(...messages) { const { logLevel } = Configuration.ENVIRONMENT; if (isNumber(logLevel) && logLevel >= 1 /* WARN */) { // tslint:disable-next-line:no-console return console.warn.bind(console, ...messages); } return NOOP; } function checkAsyncSystemConfig(config) { if (config) { if (config.clearDataOnError === true && config.clearDataOnQuery === true) { return logWarn(`When "clearDataOnQuery" is set to true, "clearDataOnError" stops working, as only one of them can work at a time\n Consider only setting one at a time.`); } if (config.clearErrorOnData === true && config.clearErrorOnQuery === true) { return logWarn(`When "clearErrorOnQuery" is set to true, "clearErrorOnData" stops working, as only one of them can work at a time\n Consider only setting one at a time.`); } } return NOOP; } function checkSerializability(o) { const [serializable, nonSerializableVal] = isSerializable(o); if (serializable === false) { throw new TypeError(`Non-serializable value ${String(nonSerializableVal)} of type ${nonSerializableVal.constructor.name}:${typeof nonSerializableVal} detected by "checkSerializability" check. Consider a serializable alternative.`); } } function checkClusterItems(items) { if (!isDict(items) || !Object.values(items).some(item => item instanceof Base)) { throw new TypeError(`No ActiveJS construct provided; expected at least one Unit, System, Action or Cluster; got ${String(items)}`); } } function checkPath(path) { if (!path.length) { throw new TypeError(`Expected at least one key`); } const invalidKeyIndex = path.findIndex(key => typeof key !== 'string' && typeof key !== 'number'); if (invalidKeyIndex > -1) { const invalidKey = path[invalidKeyIndex]; throw new TypeError(`Expected numbers and strings, but got ${invalidKey} of type ${typeof invalidKey}`); } } /** * UnitBase serves as the base for all the ActiveJS Units: GenericUnit, BoolUnit, ListUnit, etc. * It extends {@link Base}. * * This is an internal construct, normally you'd never have to use this class directly. * However, if you're just reading the documentation, or want to learn more about how ActiveJS works, * or want to extend this class to build something on your own, please continue. * * UnitBase creates the foundation of all the ActiveJS Units. * It implements the features like: * - dispatching, clearing and resetting the value * - caching the dispatched values * - navigating through the cached values, by methods like goBack, goForward, jump etc. * - observable events to listen to including but not limited to the above mentioned actions * - freezing/unfreezing the Unit * - muting/unmuting the Unit * - persisting and retrieving the value to/from persistent-storage * - debouncing the dispatch * - resetting the Unit * - etc. * * @category 2. Abstract */ class UnitBase extends Base { // tslint:enable:variable-name /** * @internal please do not use. */ constructor(config) { super(Object.assign(Object.assign({}, Configuration.UNITS), config)); /** * @internal please do not use. */ this._isFrozen = false; /** * @internal please do not use. */ this._isMuted = false; /** * @internal please do not use. */ this._cachedValues = []; /** * @internal please do not use. */ this._cacheIndex = 0; /** * @internal please do not use. */ this._initialValue = undefined; const { cacheSize, initialValue, dispatchDebounce, dispatchDebounceMode, persistent, } = this.config; this.cacheSize = isNumber(cacheSize) ? Math.max(1, cacheSize) : 2; // min 1, default 2 if (persistent === true) { this.restoreValueFromPersistentStorage(initialValue); } else { this.checkSerializabilityMaybe(initialValue); this.dispatchInitialValue(this.deepCopyMaybe(initialValue)); } if (dispatchDebounce === true || isNumber(dispatchDebounce)) { this.dispatchMiddleware = debounce(this.dispatchMiddleware.bind(this), dispatchDebounce, dispatchDebounceMode); } } /** * Indicates whether the Unit is frozen or not. * See {@link freeze} for more details. * * Note: It's not the same as [Object.isFrozen](https://cutt.ly/WyFdzPD). */ get isFrozen() { return this._isFrozen; } /** * Indicates whether the Unit is muted or not. * See {@link mute} for more details. */ get isMuted() { return this._isMuted; } /** * Indicates whether the value is undefined or not. * * It should be preferred if the Unit is configured to be immutable, as it doesn't create a copy. */ get isEmpty() { return this.rawValue() === this.defaultValue(); } /** * Count of all the cached values. */ get cachedValuesCount() { return this._cachedValues.length; } /** * Index of the current {@link value} in the {@link UnitBase.cachedValues} */ get cacheIndex() { return this._cacheIndex; } /** * The initialValue provided on instantiation. * Creates a copy if the Unit is configured to be immutable. * * @category Access Value */ initialValue() { return this.deepCopyMaybe(this.initialValueRaw()); } /** * Current value of the Unit. * Creates a copy if the Unit is configured to be immutable. * * @default * BoolUnit: `false` \ * NumUnit: `0` \ * StringUnit: `''` \ * ListUnit: `[]` \ * DictUnit: `{}` \ * GenericUnit: `undefined` * * @category Access Value */ value() { return this.deepCopyMaybe(this.rawValue()); // apply the fallback and kill reference } /** * If the Unit has a non-primitive value, * use it to get access to the current {@link value}, without creating a deep-copy. * * This can come in handy if the Unit is configured to be immutable, and you want to perform a non-mutating action * without creating a deep-copy of the value. * * @category Access Value */ rawValue() { return this.applyFallbackValue(this._value); } /** * All the cached values. * Creates a copy if the Unit is configured to be immutable. * * @category Access Value */ cachedValues() { return this._cachedValues.map(value => this.deepCopyMaybe(value)); } /** * @internal please do not use. */ initialValueRaw() { return this.applyFallbackValue(this._initialValue); } /** * @internal please do not use. */ defaultValue() { return undefined; } /** * A helper method that creates a stream by subscribing to the Observable returned by the param `observableProducer` callback. * * Ideally the callback function creates an Observable by applying `Observable.pipe`. * * Just know that you should catch the error in a sub-pipe (ie: do not let it propagate to the main-pipe), otherwise * as usual the stream will stop working, and will not react on any further emissions. * * @param observableProducer A callback function that should return an Observable. * * @category Common */ createStream(observableProducer) { const observable = observableProducer(this); return new Stream(observable); } /** * Given a value, this function determines whether it should be dispatched or not. \ * The dispatch is denied in following circumstances: * - If the Unit is frozen. {@link isFrozen} * - If {@link UnitConfig.distinctDispatchCheck} is set to `true`, and the new-value === current-value, * - If {@link UnitConfig.customDispatchCheck} returns a `falsy` value. * * If the Unit is not frozen, you can bypass other dispatch-checks by passing param `force = true`. * * This function is used internally, when a value is dispatched {@link dispatch}. \ * Even initialValue {@link UnitConfig.initialValue} dispatch has to pass this check. * * You can also use it to check if the value will be dispatched or not before dispatching it. * * @param value The value to be dispatched. * @param force Whether dispatch-checks should be bypassed or not. * @returns A boolean indicating whether the param `value` would pass the dispatch-checks if dispatched. * * @category Common Units */ wouldDispatch(value, force = false) { if (this.isFrozen) { return false; } if (force === true) { return true; } if (typeof this.config.customDispatchCheck === 'function' && !this.config.customDispatchCheck(this.rawValue(), value)) { return false; } return this.distinctCheck(value); } /** * Method to dispatch new value by passing the value directly, or \ * by passing a value-producer-function that produces the value using the current {@link value}. * * Given a value, it either gets dispatched if it's allowed by {@link wouldDispatch}, \ * or it gets ignored if not allowed. * * If the Unit is not configured to be immutable, then \ * the value-producer-function (param `valueOrProducer`) should not mutate the current {@link value}, \ * which is provided as an argument to the function. * * If you mutate the value, then the cached-value might also get mutated, \ * as the cached-value is saved by reference, which can result in unpredictable state. * * @param valueOrProducer A new-value, or a pure function that produces a new-value. * @param options Dispatch options. * @returns `true` if value got dispatched, otherwise `false`. * If {@link UnitConfig.dispatchDebounce} is enabled, then it'll return `undefined`. * * @triggers {@link EventUnitDispatch}, or {@link EventUnitDispatchFail}, depending on the success of dispatch. * @category Common Action/Units