/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { isArray } from 'vs/base/common/types'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { canceled } from 'vs/base/common/errors'; import { range } from 'vs/base/common/arrays'; /** * A Pager is a stateless abstraction over a paged collection. */ export interface IPager { firstPage: T[]; total: number; pageSize: number; getPage(pageIndex: number, cancellationToken: CancellationToken): Promise; } interface IPage { isResolved: boolean; promise: Promise | null; cts: CancellationTokenSource | null; promiseIndexes: Set; elements: T[]; } function createPage(elements?: T[]): IPage { return { isResolved: !!elements, promise: null, cts: null, promiseIndexes: new Set(), elements: elements || [] }; } /** * A PagedModel is a stateful model over an abstracted paged collection. */ export interface IPagedModel { length: number; isResolved(index: number): boolean; get(index: number): T; resolve(index: number, cancellationToken: CancellationToken): Promise; } export function singlePagePager(elements: T[]): IPager { return { firstPage: elements, total: elements.length, pageSize: elements.length, getPage: (pageIndex: number, cancellationToken: CancellationToken): Promise => { return Promise.resolve(elements); } }; } export class PagedModel implements IPagedModel { private pager: IPager; private pages: IPage[] = []; get length(): number { return this.pager.total; } constructor(arg: IPager | T[]) { this.pager = isArray(arg) ? singlePagePager(arg) : arg; const totalPages = Math.ceil(this.pager.total / this.pager.pageSize); this.pages = [ createPage(this.pager.firstPage.slice()), ...range(totalPages - 1).map(() => createPage()) ]; } isResolved(index: number): boolean { const pageIndex = Math.floor(index / this.pager.pageSize); const page = this.pages[pageIndex]; return !!page.isResolved; } get(index: number): T { const pageIndex = Math.floor(index / this.pager.pageSize); const indexInPage = index % this.pager.pageSize; const page = this.pages[pageIndex]; return page.elements[indexInPage]; } resolve(index: number, cancellationToken: CancellationToken): Promise { if (cancellationToken.isCancellationRequested) { return Promise.reject(canceled()); } const pageIndex = Math.floor(index / this.pager.pageSize); const indexInPage = index % this.pager.pageSize; const page = this.pages[pageIndex]; if (page.isResolved) { return Promise.resolve(page.elements[indexInPage]); } if (!page.promise) { page.cts = new CancellationTokenSource(); page.promise = this.pager.getPage(pageIndex, page.cts.token) .then(elements => { page.elements = elements; page.isResolved = true; page.promise = null; page.cts = null; }, err => { page.isResolved = false; page.promise = null; page.cts = null; return Promise.reject(err); }); } cancellationToken.onCancellationRequested(() => { if (!page.cts) { return; } page.promiseIndexes.delete(index); if (page.promiseIndexes.size === 0) { page.cts.cancel(); } }); page.promiseIndexes.add(index); return page.promise.then(() => page.elements[indexInPage]); } } export class DelayedPagedModel implements IPagedModel { get length(): number { return this.model.length; } constructor(private model: IPagedModel, private timeout: number = 500) { } isResolved(index: number): boolean { return this.model.isResolved(index); } get(index: number): T { return this.model.get(index); } resolve(index: number, cancellationToken: CancellationToken): Promise { return new Promise((c, e) => { if (cancellationToken.isCancellationRequested) { return e(canceled()); } const timer = setTimeout(() => { if (cancellationToken.isCancellationRequested) { return e(canceled()); } timeoutCancellation.dispose(); this.model.resolve(index, cancellationToken).then(c, e); }, this.timeout); const timeoutCancellation = cancellationToken.onCancellationRequested(() => { clearTimeout(timer); timeoutCancellation.dispose(); e(canceled()); }); }); } } /** * Similar to array.map, `mapPager` lets you map the elements of an * abstract paged collection to another type. */ export function mapPager(pager: IPager, fn: (t: T) => R): IPager { return { firstPage: pager.firstPage.map(fn), total: pager.total, pageSize: pager.pageSize, getPage: (pageIndex, token) => pager.getPage(pageIndex, token).then(r => r.map(fn)) }; }