@v4fire/client
Version:
V4Fire client core library
610 lines (488 loc) • 12.4 kB
text/typescript
/*!
* V4Fire Client Core
* https://github.com/V4Fire/Client
*
* Released under the MIT license
* https://github.com/V4Fire/Client/blob/master/LICENSE
*/
/**
* [[include:super/i-block/modules/async-render/README.md]]
* @packageDocumentation
*/
import Range from 'core/range';
import SyncPromise from 'core/promise/sync';
//#if runtime has component/async-render
import { queue, restart, deferRestart } from 'core/render';
//#endif
import type iBlock from 'super/i-block/i-block';
import type { ComponentElement } from 'super/i-block/i-block';
import Friend from 'super/i-block/modules/friend';
import type { TaskParams, TaskDesc } from 'super/i-block/modules/async-render/interface';
export * from 'super/i-block/modules/async-render/interface';
/**
* Class provides API to render chunks of a component template asynchronously
*/
export default class AsyncRender extends Friend {
//#if runtime has component/async-render
constructor(component: iBlock) {
super(component);
this.meta.hooks.beforeUpdate.push({
fn: () => this.async.clearAll({
group: 'asyncComponents'
})
});
}
/**
* Restarts the `asyncRender` daemon to force rendering
*/
forceRender(): void {
restart();
this.localEmitter.emit('forceRender');
}
/**
* Restarts the `asyncRender` daemon to force rendering
* (runs on the next tick)
*/
deferForceRender(): void {
deferRestart();
this.localEmitter.emit('forceRender');
}
/**
* Returns a function that returns a promise that will be resolved after firing the `forceRender` event.
* The method can take an element name as the first parameter. This element will be dropped before resolving.
*
* Notice, the initial rendering of a component is mean the same as `forceRender`.
* The method is useful to re-render a non-regular component (functional or flyweight)
* without touching the parent state.
*
* @param elementToDrop - element to drop before resolving the promise
* (if it passed as a function, it would be executed)
*
* @example
* ```
* < button @click = asyncRender.forceRender()
* Re-render the component
*
* < .&__wrapper
* < template v-for = el in asyncRender.iterate(true, {filter: asyncRender.waitForceRender('content')})
* < .&__content
* {{ Math.random() }}
* ```
*/
waitForceRender(
elementToDrop?: string | ((ctx: this['component']) => CanPromise<CanUndef<string | Element>>)
): () => CanPromise<boolean> {
return () => {
const
canImmediateRender = this.lfc.isBeforeCreate() || this.hook === 'beforeMount';
if (canImmediateRender) {
return true;
}
return this.localEmitter.promisifyOnce('forceRender').then(async () => {
if (elementToDrop != null) {
let
el;
if (Object.isFunction(elementToDrop)) {
el = await elementToDrop(this.ctx);
} else {
el = elementToDrop;
}
if (Object.isString(el)) {
this.block?.element(el)?.remove();
} else {
el?.remove();
}
}
return true;
});
};
}
/**
* Creates an asynchronous render stream from the specified value.
* This method helps to optimize the rendering of a component by splitting big render tasks into little.
*
* @param value
* @param [sliceOrOpts] - elements per chunk, `[start position, elements per chunk]` or additional options
* @param [opts] - additional options
*
* @emits `localEmitter.asyncRenderChunkComplete(e: TaskParams & TaskDesc)`
* @emits `localEmitter.asyncRenderComplete(e: TaskParams & TaskDesc)`
*
* @example
* ```
* /// Asynchronous rendering of components, only five elements per chunk
* < template v-for = el in asyncRender.iterate(largeList, 5)
* < my-component :data = el
* ```
*/
iterate(
value: unknown,
sliceOrOpts: number | [number?, number?] | TaskParams = 1,
opts: TaskParams = {}
): unknown[] {
if (value == null) {
return [];
}
if (Object.isPlainObject(sliceOrOpts)) {
opts = sliceOrOpts;
sliceOrOpts = [];
}
const
{filter} = opts;
let
iterable = this.getIterable(value, filter != null);
let
startPos,
perChunk;
if (Object.isArray(sliceOrOpts)) {
startPos = sliceOrOpts[0];
perChunk = sliceOrOpts[1];
} else {
perChunk = sliceOrOpts;
}
startPos ??= 0;
perChunk ??= 1;
const
firstRender = <unknown[]>[],
untreatedEls = <unknown[]>[],
srcIsPromise = Object.isPromise(iterable);
let
iterator: Iterator<unknown>,
lastSyncEl: IteratorResult<unknown>;
let
syncI = 0,
syncTotal = 0;
if (!srcIsPromise) {
iterator = iterable[Symbol.iterator]();
// eslint-disable-next-line no-multi-assign
for (let o = iterator, el = lastSyncEl = o.next(); !el.done; el = o.next(), syncI++) {
if (startPos > 0) {
startPos--;
continue;
}
const
val = el.value;
let
valIsPromise = Object.isPromise(val),
canRender = !valIsPromise;
if (canRender && filter != null) {
canRender = filter.call(this.component, val, syncI, {
iterable,
i: syncI,
total: syncTotal
});
if (Object.isPromise(canRender)) {
valIsPromise = true;
canRender = false;
} else if (!Object.isTruly(canRender)) {
canRender = false;
}
}
if (canRender) {
syncTotal++;
firstRender.push(val);
} else if (valIsPromise) {
untreatedEls.push(val);
}
if (syncTotal >= perChunk || valIsPromise) {
break;
}
}
}
const
BREAK = {};
firstRender[this.asyncLabel] = async (cb) => {
const {
async: $a,
localEmitter
} = this;
const
weight = opts.weight ?? 1,
newIterator = createIterator();
let
i = 0,
total = syncTotal,
chunkTotal = 0,
chunkI = 0,
awaiting = 0;
let
group = 'asyncComponents',
renderBuffer = <unknown[]>[];
let
lastTask,
lastEvent;
for (let o = newIterator, el = o.next(); !el.done; el = o.next()) {
if (opts.group != null) {
group = `asyncComponents:${opts.group}:${chunkI}`;
}
let
val = el.value;
const
valIsPromise = Object.isPromise(val);
if (valIsPromise) {
try {
// eslint-disable-next-line require-atomic-updates
val = await $a.promise(<Promise<unknown>>val, {group});
if (val === BREAK) {
break;
}
} catch (err) {
if (err?.type === 'clearAsync' && err.reason === 'group' && err.link.group === group) {
break;
}
stderr(err);
continue;
}
}
const resolveTask = (filter?: boolean) => {
if (filter === false) {
return;
}
total++;
chunkTotal++;
renderBuffer.push(val);
lastTask = () => {
lastTask = null;
awaiting++;
const task = () => {
const desc: TaskDesc = {
async: $a,
renderGroup: group
};
cb(renderBuffer, desc, (els: Node[]) => {
chunkI++;
chunkTotal = 0;
renderBuffer = [];
awaiting--;
lastEvent = {...opts, ...desc};
localEmitter.emit('asyncRenderChunkComplete', lastEvent);
$a.worker(() => {
const destroyEl = (el: CanUndef<ComponentElement | Node>) => {
if (el == null) {
return;
}
if (el[this.asyncLabel] != null) {
delete el[this.asyncLabel];
$a.worker(() => destroyEl(el), {group});
} else {
const
els = el instanceof Element ? Array.from(el.querySelectorAll('.i-block-helper')) : [];
if (opts.destructor?.(el, els) !== true) {
this.destroy(el, els);
}
}
};
for (let i = 0; i < els.length; i++) {
destroyEl(els[i]);
}
}, {group});
});
};
return this.createTask(task, {group, weight});
};
if (!valIsPromise && chunkTotal < perChunk) {
return;
}
return lastTask();
};
try {
if (filter != null) {
const needRender = filter.call(this.ctx, val, i, {
iterable,
i: syncI + i + 1,
chunk: chunkI,
total
});
if (Object.isPromise(needRender)) {
await $a.promise(needRender, {group})
.then((res) => resolveTask(res === undefined || Object.isTruly(res)));
} else {
const
res = resolveTask(Object.isTruly(needRender));
if (res != null) {
await res;
}
}
} else {
const
res = resolveTask();
if (res != null) {
await res;
}
}
} catch (err) {
if (err?.type === 'clearAsync' && err.link.group === group) {
break;
}
stderr(err);
continue;
}
i++;
}
if (lastTask != null) {
awaiting++;
const
res = lastTask();
if (res != null) {
await res;
}
}
if (awaiting <= 0) {
localEmitter.emit('asyncRenderComplete', lastEvent);
} else {
const id = localEmitter.on('asyncRenderChunkComplete', () => {
if (awaiting <= 0) {
localEmitter.emit('asyncRenderComplete', lastEvent);
localEmitter.off(id);
}
});
}
function createIterator() {
if (srcIsPromise) {
const next = () => {
if (Object.isPromise(iterable)) {
return {
done: false,
value: iterable
.then((v) => {
iterable = v;
iterator = v[Symbol.iterator]();
const
el = iterator.next();
if (el.done) {
return BREAK;
}
return el.value;
})
.catch((err) => {
stderr(err);
return BREAK;
})
};
}
return iterator.next();
};
return {next};
}
let
i = 0;
const next = () => {
if (untreatedEls.length === 0 && lastSyncEl.done) {
return lastSyncEl;
}
if (i < untreatedEls.length) {
return {
value: untreatedEls[i++],
done: false
};
}
return iterator.next();
};
return {next};
}
};
return firstRender;
}
/**
* Returns an iterable object based on the passed value
*
* @param obj
* @param [hasFilter] - true if the passed object will be filtered
*/
protected getIterable(obj: unknown, hasFilter?: boolean): CanPromise<Iterable<unknown>> {
if (obj == null) {
return [];
}
if (obj === true) {
if (hasFilter) {
return new Range(0, Infinity);
}
return [];
}
if (obj === false) {
if (hasFilter) {
return new Range(0, -Infinity);
}
return [];
}
if (Object.isNumber(obj)) {
return new Range(0, [obj]);
}
if (Object.isArray(obj)) {
return obj;
}
if (Object.isString(obj)) {
return obj.letters();
}
if (Object.isPromise(obj)) {
return obj.then(this.getIterable.bind(this));
}
if (typeof obj === 'object') {
if (Object.isIterable(obj)) {
return obj;
}
return Object.entries(obj);
}
return [obj];
}
/**
* Removes the given element from the DOM tree and destroys all tied components
*
* @param el
* @param [childComponentEls] - list of child component nodes
*/
protected destroy(el: Node, childComponentEls: Element[] = []): void {
el.parentNode?.removeChild(el);
for (let i = 0; i < childComponentEls.length; i++) {
const
el = childComponentEls[i];
try {
(<ComponentElement<iBlock>>el).component?.unsafe.$destroy();
} catch (err) {
stderr(err);
}
}
try {
(<ComponentElement<iBlock>>el).component?.unsafe.$destroy();
} catch (err) {
stderr(err);
}
}
/**
* Creates a render task by the specified parameters
*
* @param taskFn
* @param [params]
*/
protected createTask(taskFn: AnyFunction, params: TaskParams = {}): Promise<void> {
const
{async: $a} = this;
const
group = params.group ?? 'asyncComponents';
return new SyncPromise<void>((resolve, reject) => {
const task = {
weight: params.weight,
fn: $a.proxy(() => {
const cb = () => {
taskFn();
resolve();
return true;
};
if (params.useRAF) {
return $a.animationFrame({group}).then(cb);
}
return cb();
}, {
group,
single: false,
onClear: (err) => {
queue.delete(task);
reject(err);
}
})
};
queue.add(task);
});
}
//#endif
}