@v4fire/client
Version:
V4Fire client core library
420 lines (331 loc) • 9.7 kB
text/typescript
/*!
* V4Fire Client Core
* https://github.com/V4Fire/Client
*
* Released under the MIT license
* https://github.com/V4Fire/Client/blob/master/LICENSE
*/
import symbolGenerator from 'core/symbol';
import Friend from 'super/i-block/modules/friend';
import type bVirtualScroll from 'base/b-virtual-scroll/b-virtual-scroll';
import type ChunkRender from 'base/b-virtual-scroll/modules/chunk-render';
import { isAsyncClearError } from 'base/b-virtual-scroll/modules/helpers';
import type { RemoteData, DataState, LastLoadedChunk } from 'base/b-virtual-scroll/interface';
export const
$$ = symbolGenerator();
export default class ChunkRequest extends Friend {
override readonly C!: bVirtualScroll;
/**
* Current page
*/
page: number = 1;
/**
* Total amount of elements being loaded
*/
total: number = 0;
/**
* All loaded data
*/
data: unknown[] = [];
/**
* Last loaded data chunk that was processed with `dbConverter`
*
* @deprecated
* @see [[ScrollRequest.lastLoadedChunk]]
*/
lastLoadedData: unknown[] = [];
/**
* Last loaded data chunk
*/
lastLoadedChunk: LastLoadedChunk = {
normalized: [],
raw: undefined
};
/**
* True if all requests for additional data has been requested
*/
isDone: boolean = false;
/**
* True if the last request returned an empty array or undefined
*/
isLastEmpty: boolean = false;
/**
* Contains data that pending to be rendered
*/
pendingData: object[] = [];
/**
* A buffer to accumulate data from the main request and all additional requests.
* Sometimes a data provider can't provide the whole batch of data in one request,
* so you need to emit some extra requests till the data batch is filled.
*/
currentAccumulatedData: CanUndef<unknown[]> = undefined;
/**
* Contains `currentAccumulatedData` from previous requests cycle
*/
previousDataStore: CanUndef<unknown> = undefined;
/** @see [[ChunkRequest.previousDataStore]] */
get previousData(): CanUndef<unknown> {
return this.previousDataStore;
}
/**
* @emits dataChange(v: unknown)
* @see [[ChunkRequest.previousDataStore]]
*/
set previousData(v: unknown) {
this.previousDataStore = v;
this.ctx.emit('dataChange', v);
}
/**
* API for scroll rendering
*/
protected get chunkRender(): ChunkRender {
return this.ctx.chunkRender;
}
/**
* Resets the current state
*/
reset(): void {
this.total = 0;
this.page = 1;
// tslint:disable-next-line: deprecation
this.lastLoadedData = [];
this.data = [];
this.lastLoadedChunk = {raw: undefined, normalized: []};
this.pendingData = [];
this.isDone = false;
this.isLastEmpty = false;
this.currentAccumulatedData = undefined;
this.previousDataStore = undefined;
this.async.clearTimeout({label: $$.waitForInitCalls});
this.async.cancelRequest({label: $$.request});
}
/**
* Reloads the last request
*/
reloadLast(): void {
this.isDone = false;
this.isLastEmpty = false;
this.chunkRender.setRefVisibility('retry', false);
this.try().catch(stderr);
}
/**
* Initializes the request module
*/
async init(): Promise<void> {
await this.async.sleep(15, {label: $$.waitForInitCalls});
const
{chunkSize, dataProvider} = this.ctx;
this.pendingData = [...this.lastLoadedChunk.normalized];
if (this.pendingData.length < chunkSize && dataProvider != null && !this.isDone) {
this.currentAccumulatedData = this.ctx.db?.data;
}
await this.try(false);
if (
this.ctx.localState !== 'error' &&
this.pendingData.length === 0 &&
this.chunkRender.itemsCount === 0 &&
this.isDone
) {
this.chunkRender.setRefVisibility('empty', true);
}
this.chunkRender.tryShowRenderNextSlot();
if (this.previousData === undefined && Array.isArray(this.ctx.db?.data)) {
this.previousData = this.ctx.db!.data;
}
this.ctx.localState = 'ready';
}
/**
* Tries to request additional data
*
* @param [initialCall]
*
* @emits `dbChange(data:` [[RemoteData]]`)`
* @emits `chunkLoading(page: number)`
*/
try(initialCall: boolean = true): Promise<CanUndef<RemoteData>> {
const
{ctx, chunkRender} = this,
{chunkSize, dataProvider} = ctx;
const
resolved = Promise.resolve(undefined);
const additionParams = {
lastLoadedChunk: {
...this.lastLoadedChunk,
normalized: this.lastLoadedChunk.normalized
}
};
if (this.pendingData.length > 0) {
if (dataProvider == null) {
chunkRender.initItems(this.pendingData.splice(0, chunkSize));
chunkRender.render();
if (this.pendingData.length === 0) {
this.emitDone();
}
return resolved;
}
if (this.pendingData.length >= chunkSize) {
chunkRender.initItems(this.pendingData.splice(0, chunkSize));
chunkRender.render();
if (this.isDone && this.pendingData.length === 0) {
this.emitDone();
}
return resolved;
}
}
const updateCurrentData = () => {
if (this.currentAccumulatedData != null) {
this.previousData = this.currentAccumulatedData;
this.currentAccumulatedData = undefined;
}
};
const shouldRequest = ctx.loadStrategy === 'scroll' ?
ctx.shouldMakeRequest(ctx.getDataStateSnapshot(additionParams, this, chunkRender)) :
true;
if (this.isDone) {
updateCurrentData();
this.onRequestsDone();
return resolved;
}
const cantRequest = () => this.isDone ||
!shouldRequest ||
ctx.dataProvider == null ||
ctx.mods.progress === 'true';
if (cantRequest()) {
return resolved;
}
if (initialCall) {
this.currentAccumulatedData = undefined;
}
chunkRender.setLoadersVisibility(true);
chunkRender.setRefVisibility('renderNext', false);
ctx.emit('chunkLoading', this.page);
return this.load()
.then((v) => {
if (Object.size(v?.data) === 0) {
this.isLastEmpty = true;
this.shouldStopRequest(this.ctx.getDataStateSnapshot({
lastLoadedData: [],
lastLoadedChunk: {
raw: undefined,
normalized: []
}
}, this, chunkRender));
chunkRender.setLoadersVisibility(false);
updateCurrentData();
return;
}
const
data = (<RemoteData>v).data!;
this.page++;
this.isLastEmpty = false;
this.data = this.data.concat(data);
this.pendingData = this.pendingData.concat(data);
this.currentAccumulatedData = Array.concat(this.currentAccumulatedData ?? [], data);
ctx.emit('dbChange', {...v, data: this.data});
this.shouldStopRequest(this.ctx.getCurrentDataState());
if (this.pendingData.length < ctx.chunkSize) {
return this.try(false);
}
this.previousData = this.currentAccumulatedData;
this.currentAccumulatedData = undefined;
chunkRender.setLoadersVisibility(false);
if (!this.isDone) {
chunkRender.initItems(this.pendingData.splice(0, chunkSize));
chunkRender.render();
}
if (!this.isDone || this.pendingData.length > 0) {
chunkRender.setRefVisibility('renderNext', true);
}
}).catch((err) => {
if (isAsyncClearError(err)) {
return;
}
stderr(err);
return undefined;
});
}
/**
* Checks for the possibility of stopping data requests
* @param params
*/
shouldStopRequest(params: DataState): boolean {
const {ctx} = this;
this.isDone = ctx.shouldStopRequest(params);
if (this.isDone) {
this.onRequestsDone();
}
return this.isDone;
}
/**
* Sets `isDone` to `true` and fires `onRequestDone` handler
*/
protected emitDone(): void {
this.isDone = true;
this.onRequestsDone();
}
/**
* Loads additional data
* @emits `chunkLoaded(lastLoadedChunk:` [[LastLoadedChunk]]`)`
*/
protected load(): Promise<CanUndef<RemoteData>> {
const {
ctx,
chunkRender
} = this;
void ctx.setMod('progress', true);
const
defaultRequestParams = ctx.getDefaultRequestParams('get'),
params = <CanUndef<Dictionary>>(defaultRequestParams ?? [])[0];
Object.assign(params, ctx.requestQuery?.(ctx.getCurrentDataState())?.get);
return ctx.async.request(ctx.getData(this.component, params), {label: $$.request})
.then((data) => {
this.ctx.localState = 'ready';
void ctx.removeMod('progress', true);
this.lastLoadedChunk.raw = data;
const
converted = data != null ? ctx.convertDataToDB<RemoteData>(<object>data) : undefined;
this.lastLoadedChunk.normalized = Object.size(converted?.data) <= 0 ?
this.lastLoadedChunk.normalized = [] :
this.lastLoadedChunk.normalized = converted!.data!;
ctx.emit('chunkLoaded', this.lastLoadedChunk, this.page);
return converted;
})
.catch((err) => {
void ctx.removeMod('progress', true);
if (isAsyncClearError(err)) {
return Promise.reject(err);
}
chunkRender.setRefVisibility('retry', true);
chunkRender.setRefVisibility('renderNext', false);
this.ctx.onRequestError(err, this.ctx.reloadLast.bind(this.ctx));
stderr(err);
this.lastLoadedChunk.raw = [];
this.lastLoadedChunk.normalized = [];
return undefined;
});
}
/**
* Handler: all requests are done
*/
protected onRequestsDone(): void {
const
{ctx, chunkRender, async: $a} = this,
{chunkSize} = ctx;
if (this.pendingData.length > 0) {
chunkRender.initItems(this.pendingData.splice(0, chunkSize));
chunkRender.render();
}
if (this.pendingData.length === 0) {
chunkRender.setRefVisibility('done', true);
chunkRender.setRefVisibility('renderNext', false);
}
$a.wait(() => ctx.localState === 'ready', {label: $$.requestDoneWaitForReady})
.then(() => {
if (this.pendingData.length === 0) {
chunkRender.setRefVisibility('done', true);
}
chunkRender.setLoadersVisibility(false);
})
.catch(stderr);
}
}