|
|
|
/*
|
|
|
|
* https://github.com/morethanwords/tweb
|
|
|
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
|
|
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
|
|
|
*/
|
|
|
|
|
|
|
|
import { throttle } from "../helpers/schedulers";
|
|
|
|
import { logger, LogTypes } from "../lib/logger";
|
|
|
|
import VisibilityIntersector, { OnVisibilityChange } from "./visibilityIntersector";
|
|
|
|
import { findAndSpliceAll } from "../helpers/array";
|
|
|
|
|
|
|
|
type LazyLoadElementBase = {
|
|
|
|
load: () => Promise<any>
|
|
|
|
};
|
|
|
|
|
|
|
|
type LazyLoadElement = Omit<LazyLoadElementBase, 'load'> & {
|
|
|
|
load: (target?: HTMLElement) => Promise<any>,
|
|
|
|
div: HTMLElement
|
|
|
|
wasSeen?: boolean,
|
|
|
|
};
|
|
|
|
|
|
|
|
const PARALLEL_LIMIT = 8;
|
|
|
|
|
|
|
|
export class LazyLoadQueueBase {
|
|
|
|
public queueId = 0;
|
|
|
|
protected queue: Array<LazyLoadElementBase> = [];
|
|
|
|
protected inProcess: Set<LazyLoadElementBase> = new Set();
|
|
|
|
|
|
|
|
protected lockPromise: Promise<void> = null;
|
|
|
|
protected unlockResolve: () => void = null;
|
|
|
|
|
|
|
|
protected log = logger('LL', LogTypes.Error);
|
|
|
|
protected processQueue: () => void;
|
|
|
|
|
|
|
|
constructor(protected parallelLimit = PARALLEL_LIMIT) {
|
|
|
|
this.processQueue = throttle(() => this._processQueue(), 20, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
public clear() {
|
|
|
|
this.inProcess.clear(); // ацтеки забьются, будет плохо
|
|
|
|
|
|
|
|
this.queue.length = 0;
|
|
|
|
// unreachable code
|
|
|
|
/* for(let item of this.inProcess) {
|
|
|
|
this.lazyLoadMedia.push(item);
|
|
|
|
} */
|
|
|
|
}
|
|
|
|
|
|
|
|
public lock() {
|
|
|
|
if(this.lockPromise) return;
|
|
|
|
|
|
|
|
//const perf = performance.now();
|
|
|
|
this.lockPromise = new Promise((resolve, reject) => {
|
|
|
|
this.unlockResolve = resolve;
|
|
|
|
});
|
|
|
|
|
|
|
|
/* if(DEBUG) {
|
|
|
|
this.lockPromise.then(() => {
|
|
|
|
this.log('was locked for:', performance.now() - perf);
|
|
|
|
});
|
|
|
|
} */
|
|
|
|
}
|
|
|
|
|
|
|
|
public unlock() {
|
|
|
|
if(!this.unlockResolve) return;
|
|
|
|
|
|
|
|
this.unlockResolve();
|
|
|
|
this.unlockResolve = this.lockPromise = null;
|
|
|
|
|
|
|
|
this.processQueue();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async processItem(item: LazyLoadElementBase) {
|
|
|
|
if(this.lockPromise) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.inProcess.add(item);
|
|
|
|
|
|
|
|
/* if(DEBUG) {
|
|
|
|
this.log('will load media', this.lockPromise, item);
|
|
|
|
} */
|
|
|
|
|
|
|
|
try {
|
|
|
|
//await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
|
|
//await new Promise((resolve, reject) => window.requestAnimationFrame(() => window.requestAnimationFrame(resolve)));
|
|
|
|
//await item.load(item.div);
|
|
|
|
await this.loadItem(item);
|
|
|
|
} catch(err) {
|
|
|
|
if(!['NO_ENTRY_FOUND', 'STORAGE_OFFLINE'].includes(err)) {
|
|
|
|
this.log.error('loadMediaQueue error:', err/* , item */);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.inProcess.delete(item);
|
|
|
|
|
|
|
|
/* if(DEBUG) {
|
|
|
|
this.log('loaded media', item);
|
|
|
|
} */
|
|
|
|
|
|
|
|
this.processQueue();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected loadItem(item: LazyLoadElementBase) {
|
|
|
|
return item.load();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected getItem() {
|
|
|
|
return this.queue.shift();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected addElement(method: 'push' | 'unshift', el: LazyLoadElementBase) {
|
|
|
|
this.queue[method](el);
|
|
|
|
this.processQueue();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected _processQueue(item?: LazyLoadElementBase) {
|
|
|
|
if(!this.queue.length || this.lockPromise || (this.parallelLimit > 0 && this.inProcess.size >= this.parallelLimit)) return;
|
|
|
|
|
|
|
|
//console.log('_processQueue start');
|
|
|
|
let added = 0;
|
|
|
|
do {
|
|
|
|
if(item) {
|
|
|
|
this.queue.findAndSplice(i => i === item);
|
|
|
|
} else {
|
|
|
|
item = this.getItem();
|
|
|
|
}
|
|
|
|
|
|
|
|
if(item) {
|
|
|
|
this.processItem(item);
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
item = null;
|
|
|
|
++added;
|
|
|
|
} while(this.inProcess.size < this.parallelLimit && this.queue.length);
|
|
|
|
//console.log('_processQueue end, added', added, this.queue.length);
|
|
|
|
}
|
|
|
|
|
|
|
|
public push(el: LazyLoadElementBase) {
|
|
|
|
this.addElement('push', el);
|
|
|
|
}
|
|
|
|
|
|
|
|
public unshift(el: LazyLoadElementBase) {
|
|
|
|
this.addElement('unshift', el);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class LazyLoadQueueIntersector extends LazyLoadQueueBase {
|
|
|
|
protected queue: Array<LazyLoadElement> = [];
|
|
|
|
protected inProcess: Set<LazyLoadElement> = new Set();
|
|
|
|
|
|
|
|
public intersector: VisibilityIntersector;
|
|
|
|
protected intersectorTimeout: number;
|
|
|
|
|
|
|
|
constructor(protected parallelLimit = PARALLEL_LIMIT) {
|
|
|
|
super(parallelLimit);
|
|
|
|
}
|
|
|
|
|
|
|
|
public lock() {
|
|
|
|
super.lock();
|
|
|
|
this.intersector.lock();
|
|
|
|
}
|
|
|
|
|
|
|
|
public unlock() {
|
|
|
|
super.unlock();
|
|
|
|
this.intersector.unlock();
|
|
|
|
}
|
|
|
|
|
|
|
|
public unlockAndRefresh() {
|
|
|
|
super.unlock();
|
|
|
|
this.intersector.unlockAndRefresh();
|
|
|
|
}
|
|
|
|
|
|
|
|
public clear() {
|
|
|
|
super.clear();
|
|
|
|
this.intersector.disconnect();
|
|
|
|
}
|
|
|
|
|
|
|
|
public refresh() {
|
|
|
|
this.intersector.refresh();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected loadItem(item: LazyLoadElement) {
|
|
|
|
return item.load(item.div);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected addElement(method: 'push' | 'unshift', el: LazyLoadElement) {
|
|
|
|
const item = this.queue.find(i => i.div === el.div && i.load === el.load);
|
|
|
|
if(item) {
|
|
|
|
return false;
|
|
|
|
} else {
|
|
|
|
for(const item of this.inProcess) {
|
|
|
|
if(item.div === el.div && item.load === el.load) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.queue[method](el);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected setProcessQueueTimeout() {
|
|
|
|
if(!this.intersectorTimeout) {
|
|
|
|
this.intersectorTimeout = window.setTimeout(() => {
|
|
|
|
this.intersectorTimeout = 0;
|
|
|
|
this.processQueue();
|
|
|
|
}, 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public push(el: LazyLoadElement) {
|
|
|
|
super.push(el);
|
|
|
|
}
|
|
|
|
|
|
|
|
public unshift(el: LazyLoadElement) {
|
|
|
|
super.unshift(el);
|
|
|
|
}
|
|
|
|
|
|
|
|
public unobserve(el: HTMLElement) {
|
|
|
|
findAndSpliceAll(this.queue, (i) => i.div === el);
|
|
|
|
|
|
|
|
this.intersector.unobserve(el);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default class LazyLoadQueue extends LazyLoadQueueIntersector {
|
|
|
|
constructor(protected parallelLimit = PARALLEL_LIMIT) {
|
|
|
|
super(parallelLimit);
|
|
|
|
|
|
|
|
this.intersector = new VisibilityIntersector(this.onVisibilityChange);
|
|
|
|
}
|
|
|
|
|
|
|
|
private onVisibilityChange = (target: HTMLElement, visible: boolean) => {
|
|
|
|
if(visible) {
|
|
|
|
/* if(DEBUG) {
|
|
|
|
this.log('isIntersecting', target);
|
|
|
|
} */
|
|
|
|
|
|
|
|
// need for set element first if scrolled
|
|
|
|
findAndSpliceAll(this.queue, (i) => i.div === target).forEach(item => {
|
|
|
|
item.wasSeen = true;
|
|
|
|
this.queue.unshift(item);
|
|
|
|
//this.processQueue(item);
|
|
|
|
});
|
|
|
|
|
|
|
|
this.setProcessQueueTimeout();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
protected getItem() {
|
|
|
|
return this.queue.findAndSplice(item => item.wasSeen);
|
|
|
|
}
|
|
|
|
|
|
|
|
public async processItem(item: LazyLoadElement) {
|
|
|
|
await super.processItem(item);
|
|
|
|
this.intersector.unobserve(item.div);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected addElement(method: 'push' | 'unshift', el: LazyLoadElement) {
|
|
|
|
const inserted = super.addElement(method, el);
|
|
|
|
|
|
|
|
if(!inserted) return false;
|
|
|
|
|
|
|
|
this.intersector.observe(el.div);
|
|
|
|
/* if(el.wasSeen) {
|
|
|
|
this.processQueue(el);
|
|
|
|
} else */if(!el.hasOwnProperty('wasSeen')) {
|
|
|
|
el.wasSeen = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class LazyLoadQueueRepeat extends LazyLoadQueueIntersector {
|
|
|
|
private _queue: Map<HTMLElement, LazyLoadElement> = new Map();
|
|
|
|
|
|
|
|
constructor(protected parallelLimit = PARALLEL_LIMIT, protected onVisibilityChange?: OnVisibilityChange) {
|
|
|
|
super(parallelLimit);
|
|
|
|
|
|
|
|
this.intersector = new VisibilityIntersector((target, visible) => {
|
|
|
|
const spliced = findAndSpliceAll(this.queue, (i) => i.div === target);
|
|
|
|
if(visible) {
|
|
|
|
const items = spliced.length ? spliced : [this._queue.get(target)];
|
|
|
|
items.forEach(item => {
|
|
|
|
this.queue.unshift(item || this._queue.get(target));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
this.onVisibilityChange && this.onVisibilityChange(target, visible);
|
|
|
|
this.setProcessQueueTimeout();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public clear() {
|
|
|
|
super.clear();
|
|
|
|
this._queue.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
/* public async processItem(item: LazyLoadElement) {
|
|
|
|
//await super.processItem(item);
|
|
|
|
await LazyLoadQueueBase.prototype.processItem.call(this, item);
|
|
|
|
|
|
|
|
if(this.lazyLoadMedia.length) {
|
|
|
|
this.processQueue();
|
|
|
|
}
|
|
|
|
} */
|
|
|
|
|
|
|
|
public observe(el: LazyLoadElement) {
|
|
|
|
this._queue.set(el.div, el);
|
|
|
|
this.intersector.observe(el.div);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class LazyLoadQueueRepeat2 extends LazyLoadQueueIntersector {
|
|
|
|
constructor(protected parallelLimit = PARALLEL_LIMIT, protected onVisibilityChange?: OnVisibilityChange) {
|
|
|
|
super(parallelLimit);
|
|
|
|
|
|
|
|
this.intersector = new VisibilityIntersector((target, visible) => {
|
|
|
|
const spliced = findAndSpliceAll(this.queue, (i) => i.div === target);
|
|
|
|
if(visible && spliced.length) {
|
|
|
|
spliced.forEach(item => {
|
|
|
|
this.queue.unshift(item);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
this.onVisibilityChange && this.onVisibilityChange(target, visible);
|
|
|
|
this.setProcessQueueTimeout();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
public observe(el: HTMLElement) {
|
|
|
|
this.intersector.observe(el);
|
|
|
|
}
|
|
|
|
}
|