UNPKG

vscroll

Version:
487 lines 17.9 kB
import { Reactive } from './reactive'; import { AdapterPropName, AdapterPropType, EMPTY_ITEM, getDefaultAdapterProps, methodPausedResult, methodPreResult, reactiveConfigStorage } from './adapter/props'; import { wantedUtils } from './adapter/wanted'; import { Direction } from '../inputs/index'; import { AdapterProcess, ProcessStatus } from '../processes/index'; const ADAPTER_PROPS_STUB = getDefaultAdapterProps(); const ALLOWED_METHODS_WHEN_PAUSED = ADAPTER_PROPS_STUB.filter(v => !!v.allowedWhenPaused).map(v => v.name); const _has = (obj, prop) => !!obj && typeof obj === 'object' && Object.prototype.hasOwnProperty.call(obj, prop); const convertAppendArgs = (prepend, options, eof) => { let result = options; if (!_has(options, 'items')) { const items = !Array.isArray(options) ? [options] : options; result = prepend ? { items, bof: eof } : { items, eof: eof }; } return result; }; const convertRemoveArgs = (options) => { if (!(_has(options, 'predicate') || _has(options, 'indexes'))) { const predicate = options; options = { predicate }; } return options; }; export class Adapter { get workflow() { return this.getWorkflow(); } get reloadCount() { return this.reloadCounter; } get reloadId() { return this.id + '.' + this.reloadCounter; } getPromisifiedMethod(method, args) { return new Promise(resolve => { if (this.relax$) { this.relax$.once(value => resolve(value)); } method.apply(this, args); }); } getWorkflowRunnerMethod(method, name) { return (...args) => { var _a, _b, _c, _d; if (!this.relax$) { (_b = (_a = this.logger) === null || _a === void 0 ? void 0 : _a.log) === null || _b === void 0 ? void 0 : _b.call(_a, () => 'scroller is not initialized: ' + name + ' method is ignored'); return Promise.resolve(methodPreResult); } if (this.paused && !ALLOWED_METHODS_WHEN_PAUSED.includes(name)) { (_d = (_c = this.logger) === null || _c === void 0 ? void 0 : _c.log) === null || _d === void 0 ? void 0 : _d.call(_c, () => 'scroller is paused: ' + name + ' method is ignored'); return Promise.resolve(methodPausedResult); } return this.getPromisifiedMethod(method, args); }; } constructor(context, getWorkflow, logger) { this.source = {}; // for Reactive props this.box = {}; // for Scalars over Reactive props this.demand = {}; // for Scalars on demand this.setFirstOrLastVisible = (_) => { }; this.getWorkflow = getWorkflow; this.logger = logger; this.relax$ = null; this.relaxRun = null; this.reloadCounter = 0; const contextId = (context === null || context === void 0 ? void 0 : context.id) || -1; // public context (if exists) should provide access to Reactive props config by id const reactivePropsStore = (context && reactiveConfigStorage.get(context.id)) || {}; // the Adapter initialization should not trigger "wanted" props setting; // after the initialization is completed, "wanted" functionality must be unblocked wantedUtils.setBlock(true, contextId); // make array of the original values from public context if present const adapterProps = context ? ADAPTER_PROPS_STUB.map(prop => { let value = context[prop.name]; // if context is augmented, we need to replace external reactive props with inner ones if (context.augmented) { const reactiveProp = reactivePropsStore[prop.name]; if (reactiveProp) { value = reactiveProp.default; // boolean doesn't matter here } } return Object.assign(Object.assign({}, prop), { value }); }) : getDefaultAdapterProps(); // restore default reactive props if they were configured Object.entries(reactivePropsStore).forEach(([key, value]) => { const prop = adapterProps.find(({ name }) => name === key); if (prop && value) { prop.value = value.default; } }); // Scalar permanent props adapterProps .filter(({ type, permanent }) => type === AdapterPropType.Scalar && permanent) .forEach(({ name, value }) => Object.defineProperty(this, name, { configurable: true, get: () => value })); // Reactive props: store original values in "source" container, to avoid extra .get() calls on scalar twins set adapterProps .filter(prop => prop.type === AdapterPropType.Reactive) .forEach(({ name, value }) => { this.source[name] = value; Object.defineProperty(this, name, { configurable: true, get: () => this.source[name] }); }); // for "wanted" props that can be explicitly requested for the first time after the Adapter initialization, // an implicit calculation of the initial value is required; // so this method should be called when accessing the "wanted" props through one of the following getters const processWanted = (prop) => { if (wantedUtils.setBox(prop, contextId)) { const firstPropList = [AdapterPropName.firstVisible, AdapterPropName.firstVisible$]; const lastPropList = [AdapterPropName.lastVisible, AdapterPropName.lastVisible$]; if (firstPropList.some(n => n === prop.name)) { this.setFirstOrLastVisible({ first: true }); } else if (lastPropList.some(n => n === prop.name)) { this.setFirstOrLastVisible({ last: true }); } } }; // Scalar props that have Reactive twins // 1) reactive props (from "source") should be triggered on set // 2) scalars should use "box" container on get // 3) "wanted" scalars should also run wanted-related logic on get adapterProps .filter(prop => prop.type === AdapterPropType.Scalar && !!prop.reactive) .forEach((prop) => { const { name, value, reactive } = prop; this.box[name] = value; Object.defineProperty(this, name, { configurable: true, set: (newValue) => { if (newValue !== this.box[name]) { this.box[name] = newValue; this.source[reactive].set(newValue); // need to emit new value through the configured reactive prop if present const reactiveProp = reactivePropsStore[reactive]; if (reactiveProp) { reactiveProp.emit(reactiveProp.source, newValue); } } }, get: () => { processWanted(prop); return this.box[name]; } }); }); // Scalar props on-demand // these scalars should use "demand" container // setting defaults should be overridden on init() adapterProps .filter(prop => prop.type === AdapterPropType.Scalar && prop.onDemand) .forEach(({ name, value }) => { this.demand[name] = value; Object.defineProperty(this, name, { configurable: true, get: () => this.demand[name] }); }); if (!context) { return; } // Adapter public context augmentation adapterProps.forEach((prop) => { const { name, type, permanent } = prop; let value = this[name]; if (type === AdapterPropType.Function) { value = value.bind(this); } else if (type === AdapterPropType.WorkflowRunner) { value = this.getWorkflowRunnerMethod(value, name); } else if (type === AdapterPropType.Reactive && reactivePropsStore[name]) { value = context[name]; } else if (name === AdapterPropName.augmented) { value = true; } const nonPermanentScalar = !permanent && type === AdapterPropType.Scalar; Object.defineProperty(context, name, { configurable: true, get: () => { processWanted(prop); // consider accessing "wanted" Reactive props if (nonPermanentScalar) { return this[name]; // non-permanent Scalars should be taken in runtime } return value; // other props (Reactive/Functions/WorkflowRunners) can be defined once } }); }); this.externalContext = context; wantedUtils.setBlock(false, contextId); } initialize({ buffer, state, viewport, logger, adapterRun$, getWorkflow }) { // buffer Object.defineProperty(this.demand, AdapterPropName.itemsCount, { get: () => buffer.getVisibleItemsCount() }); Object.defineProperty(this.demand, AdapterPropName.bufferInfo, { get: () => ({ firstIndex: buffer.firstIndex, lastIndex: buffer.lastIndex, minIndex: buffer.minIndex, maxIndex: buffer.maxIndex, absMinIndex: buffer.absMinIndex, absMaxIndex: buffer.absMaxIndex, defaultSize: buffer.defaultSize }) }); this.bof = buffer.bof.get(); buffer.bof.on(bof => (this.bof = bof)); this.eof = buffer.eof.get(); buffer.eof.on(eof => (this.eof = eof)); // state Object.defineProperty(this.demand, AdapterPropName.packageInfo, { get: () => state.packageInfo }); this.loopPending = state.cycle.innerLoop.busy.get(); state.cycle.innerLoop.busy.on(busy => (this.loopPending = busy)); this.isLoading = state.cycle.busy.get(); state.cycle.busy.on(busy => (this.isLoading = busy)); this.paused = state.paused.get(); state.paused.on(paused => (this.paused = paused)); //viewport this.setFirstOrLastVisible = ({ first, last, workflow }) => { var _a, _b, _c; if ((!first && !last) || ((_a = workflow === null || workflow === void 0 ? void 0 : workflow.call) === null || _a === void 0 ? void 0 : _a.interrupted)) { return; } const token = first ? AdapterPropName.firstVisible : AdapterPropName.lastVisible; if (!((_c = wantedUtils.getBox((_b = this.externalContext) === null || _b === void 0 ? void 0 : _b.id)) === null || _c === void 0 ? void 0 : _c[token])) { return; } if (buffer.items.some(({ element }) => !element)) { logger.log('skipping first/lastVisible set because not all buffered items are rendered at this moment'); return; } const direction = first ? Direction.backward : Direction.forward; const { item } = viewport.getEdgeVisibleItem(buffer.items, direction); if (!item || item.element !== this[token].element) { this[token] = (item ? item.get() : EMPTY_ITEM); } }; // logger this.logger = logger; // self-pending subscription; set up only on the very first init if (adapterRun$) { if (!this.relax$) { this.relax$ = new Reactive(); } const relax$ = this.relax$; adapterRun$.on(({ status, payload }) => { let unSubRelax = () => { }; if (status === ProcessStatus.start) { unSubRelax = this.isLoading$.on(value => { if (!value) { unSubRelax(); relax$.set({ success: true, immediate: false, details: null }); } }); } else if (status === ProcessStatus.done || status === ProcessStatus.error) { unSubRelax(); relax$.set({ success: status !== ProcessStatus.error, immediate: true, details: status === ProcessStatus.error && payload ? String(payload.error) : null }); } }); } // workflow getter if (getWorkflow) { this.getWorkflow = getWorkflow; } // init this.init = true; } dispose() { if (this.relax$) { this.relax$.dispose(); } if (this.externalContext) { this.resetContext(); } Object.getOwnPropertyNames(this).forEach(prop => { delete this[prop]; }); this.disposed = true; } resetContext() { var _a; const reactiveStore = reactiveConfigStorage.get((_a = this.externalContext) === null || _a === void 0 ? void 0 : _a.id); ADAPTER_PROPS_STUB.forEach(({ type, permanent, name, value }) => { // assign initial values to non-reactive non-permanent props if (type !== AdapterPropType.Reactive && !permanent) { Object.defineProperty(this.externalContext, name, { configurable: true, get: () => value }); } // reset reactive props if (type === AdapterPropType.Reactive && reactiveStore) { const property = reactiveStore[name]; if (property) { property.default.reset(); property.emit(property.source, property.default.get()); } } }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any reset(options) { this.reloadCounter++; this.logger.logAdapterMethod('reset', options, ` of ${this.reloadId}`); this.workflow.call({ process: AdapterProcess.reset, status: ProcessStatus.start, payload: { options } }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any reload(options) { this.reloadCounter++; this.logger.logAdapterMethod('reload', options, ` of ${this.reloadId}`); this.workflow.call({ process: AdapterProcess.reload, status: ProcessStatus.start, payload: { options } }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any append(_options, eof) { const options = convertAppendArgs(false, _options, eof); // support old signature this.logger.logAdapterMethod('append', [options.items, options.eof]); this.workflow.call({ process: AdapterProcess.append, status: ProcessStatus.start, payload: { options } }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any prepend(_options, bof) { const options = convertAppendArgs(true, _options, bof); // support old signature this.logger.logAdapterMethod('prepend', [options.items, options.bof]); this.workflow.call({ process: AdapterProcess.prepend, status: ProcessStatus.start, payload: { options } }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any check() { this.logger.logAdapterMethod('check'); this.workflow.call({ process: AdapterProcess.check, status: ProcessStatus.start }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any remove(options) { options = convertRemoveArgs(options); // support old signature this.logger.logAdapterMethod('remove', options); this.workflow.call({ process: AdapterProcess.remove, status: ProcessStatus.start, payload: { options } }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any clip(options) { this.logger.logAdapterMethod('clip', options); this.workflow.call({ process: AdapterProcess.clip, status: ProcessStatus.start, payload: { options } }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any insert(options) { this.logger.logAdapterMethod('insert', options); this.workflow.call({ process: AdapterProcess.insert, status: ProcessStatus.start, payload: { options } }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any replace(options) { this.logger.logAdapterMethod('replace', options); this.workflow.call({ process: AdapterProcess.replace, status: ProcessStatus.start, payload: { options } }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any update(options) { this.logger.logAdapterMethod('update', options); this.workflow.call({ process: AdapterProcess.update, status: ProcessStatus.start, payload: { options } }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any pause() { this.logger.logAdapterMethod('pause'); this.workflow.call({ process: AdapterProcess.pause, status: ProcessStatus.start }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any resume() { this.logger.logAdapterMethod('resume'); this.workflow.call({ process: AdapterProcess.pause, status: ProcessStatus.start, payload: { options: { resume: true } } }); } // eslint-disable-next-line @typescript-eslint/no-explicit-any fix(options) { this.logger.logAdapterMethod('fix', options); this.workflow.call({ process: AdapterProcess.fix, status: ProcessStatus.start, payload: { options } }); } relaxUnchained(callback, reloadId) { const runCallback = () => typeof callback === 'function' && reloadId === this.reloadId && callback(); if (!this.isLoading) { runCallback(); } return new Promise(resolve => { if (!this.isLoading) { resolve(true); return; } this.isLoading$.once(() => { runCallback(); resolve(false); }); }).then(immediate => { var _a, _b; if (this.disposed) { return { immediate, success: false, details: 'Adapter was disposed' }; } const success = reloadId === this.reloadId; (_b = (_a = this.logger) === null || _a === void 0 ? void 0 : _a.log) === null || _b === void 0 ? void 0 : _b.call(_a, () => !success ? `relax promise cancelled due to ${reloadId} != ${this.reloadId}` : void 0); return { immediate, success, details: !success ? 'Interrupted by reload or reset' : null }; }); } relax(callback) { const reloadId = this.reloadId; this.logger.logAdapterMethod('relax', callback, ` of ${reloadId}`); if (!this.init) { return Promise.resolve(methodPreResult); } return (this.relaxRun = this.relaxRun ? this.relaxRun.then(() => this.relaxUnchained(callback, reloadId)) : this.relaxUnchained(callback, reloadId).then(result => { this.relaxRun = null; return result; })); } showLog() { this.logger.logAdapterMethod('showLog'); this.logger.logForce(); } } //# sourceMappingURL=adapter.js.map