UNPKG

@itwin/core-frontend

Version:
297 lines • 14 kB
"use strict"; /*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ Object.defineProperty(exports, "__esModule", { value: true }); exports.SubCategoriesCache = void 0; const core_bentley_1 = require("@itwin/core-bentley"); const core_common_1 = require("@itwin/core-common"); const invalidCategoryIdEntry = new Set(); /** A cache of information about the subcategories contained within an [[IModelConnection]]. It is populated on demand. * @internal */ class SubCategoriesCache { _byCategoryId = new Map(); _appearances = new Map(); _imodel; _missingAtTimeOfPreload; constructor(imodel) { this._imodel = imodel; } /** Get the Ids of all subcategories belonging to the category with the specified Id, or undefined if no such information is present. */ getSubCategories(categoryId) { return this._byCategoryId.get(categoryId); } /** Get the base appearance of the subcategory with the specified Id, or undefined if no such information is present. */ getSubCategoryAppearance(subCategoryId) { return this._appearances.get(subCategoryId.toString()); } /** Request that the subcategory information for all of the specified categories is loaded. * If all such information has already been loaded, returns undefined. * Otherwise, dispatches an asynchronous request to load those categories which are not already loaded and returns a cancellable request object * containing the corresponding promise and the set of categories still to be loaded. */ load(categoryIds) { const missing = this.getMissing(categoryIds); if (undefined === missing) return undefined; const request = new SubCategoriesCache.Request(missing, this._imodel); const promise = request.dispatch().then((result) => { if (undefined !== result) this.processResults(result, missing); return !request.wasCanceled; }); return { missingCategoryIds: missing, promise, cancel: () => request.cancel(), }; } /** Load all subcategories that come from used spatial categories of the iModel into the cache. */ async loadAllUsedSpatialSubCategories() { try { const results = await this._imodel.queryAllUsedSpatialSubCategories(); if (undefined !== results) { this.processResults(results, new Set(), false); } } catch { // In case of a truncated response, gracefully handle the error and exit. } } /** Given categoryIds, return which of these are not cached. */ getMissing(categoryIds) { let missing; for (const catId of core_bentley_1.Id64.iterable(categoryIds)) { if (undefined === this._byCategoryId.get(catId)) { if (undefined === missing) missing = new Set(); missing.add(catId); } } return missing; } clear() { this._byCategoryId.clear(); this._appearances.clear(); } onIModelConnectionClose() { this.clear(); } static createSubCategoryAppearance(json) { let props; if ("string" === typeof json && 0 < json.length) props = JSON.parse(json); return new core_common_1.SubCategoryAppearance(props); } processResults(result, missing, override = true) { for (const row of result) { this.add(row.parentId, row.id, SubCategoriesCache.createSubCategoryAppearance(row.appearance), override); } // Ensure that any category Ids which returned no results (e.g., non-existent category, invalid Id, etc) are still recorded so they are not repeatedly re-requested for (const id of missing) if (undefined === this._byCategoryId.get(id)) this._byCategoryId.set(id, invalidCategoryIdEntry); } /** Exposed strictly for tests. * @internal */ add(categoryId, subCategoryId, appearance, override) { let set = this._byCategoryId.get(categoryId); if (undefined === set) this._byCategoryId.set(categoryId, set = new Set()); set.add(subCategoryId); if (override || !this._appearances.has(subCategoryId)) this._appearances.set(subCategoryId, appearance); } async getCategoryInfo(inputCategoryIds) { // Eliminate duplicates... const categoryIds = new Set(typeof inputCategoryIds === "string" ? [inputCategoryIds] : inputCategoryIds); const req = this.load(categoryIds); if (req) await req.promise; const map = new Map(); for (const categoryId of categoryIds) { const subCategoryIds = this._byCategoryId.get(categoryId); if (!subCategoryIds) continue; const subCategories = this.mapSubCategoryInfos(categoryId, subCategoryIds); map.set(categoryId, { id: categoryId, subCategories }); } return map; } async getSubCategoryInfo(categoryId, inputSubCategoryIds) { // Eliminate duplicates... const subCategoryIds = new Set(typeof inputSubCategoryIds === "string" ? [inputSubCategoryIds] : inputSubCategoryIds); const req = this.load(categoryId); if (req) await req.promise; return this.mapSubCategoryInfos(categoryId, subCategoryIds); } mapSubCategoryInfos(categoryId, subCategoryIds) { const map = new Map(); for (const id of subCategoryIds) { const appearance = this._appearances.get(id); if (appearance) map.set(id, { id, categoryId, appearance }); } return map; } } exports.SubCategoriesCache = SubCategoriesCache; /** This namespace and the types within it are exported strictly for use in tests. * @internal */ (function (SubCategoriesCache) { class Request { _imodel; _categoryIds = []; _result = []; _canceled = false; _curCategoryIdsIndex = 0; get wasCanceled() { return this._canceled || this._imodel.isClosed; } constructor(categoryIds, imodel, maxCategoriesPerQuery = 2500) { this._imodel = imodel; const catIds = [...categoryIds]; core_bentley_1.OrderedId64Iterable.sortArray(catIds); // sort categories, so that given the same set of categoryIds we will always create the same batches. while (catIds.length !== 0) { const end = (catIds.length > maxCategoriesPerQuery) ? maxCategoriesPerQuery : catIds.length; const compressedIds = core_bentley_1.CompressedId64Set.compressArray(catIds.splice(0, end)); this._categoryIds.push(compressedIds); } } cancel() { this._canceled = true; } async dispatch() { if (this.wasCanceled || this._curCategoryIdsIndex >= this._categoryIds.length) // handle case of empty category Id set... return undefined; try { const catIds = this._categoryIds[this._curCategoryIdsIndex]; const result = await this._imodel.querySubCategories(catIds); this._result.push(...result); if (this.wasCanceled) return undefined; } catch { // ###TODO: detect cases in which retry is warranted // Note that currently, if we succeed in obtaining some pages of results and fail to retrieve another page, we will end up processing the // incomplete results. Since we're not retrying, that's the best we can do. } // Finished with current batch of categoryIds. Dispatch the next batch if one exists. if (++this._curCategoryIdsIndex < this._categoryIds.length) { if (this.wasCanceled) return undefined; else return this.dispatch(); } // Even if we were canceled, we've retrieved all the rows. Might as well process them to prevent another request for some of the same rows from being enqueued. return this._result; } } SubCategoriesCache.Request = Request; class QueueEntry { categoryIds; funcs; constructor(categoryIds, func) { this.categoryIds = categoryIds; this.funcs = [func]; } } SubCategoriesCache.QueueEntry = QueueEntry; /** A "queue" of SubCategoriesRequests, which consists of between 0 and 2 entries. Each entry specifies the set of category IDs to be loaded and a list of functions to be executed * when loading is completed. This is used to enforce ordering of operations upon subcategories despite the need to asynchronously load them. It incidentally also provides an * opportunity to reduce the number of backend requests by batching consecutive requests. * Chiefly used by [[Viewport]]. * @internal */ class Queue { /* NB: Members marked protected for use in tests only. */ _current; _next; _request; _disposed = false; /** Push a request onto the queue. The requested categories will be loaded if necessary, and then * the supplied function will be invoked. Any previously-pushed requests are guaranteed to be processed before this one. */ push(cache, categoryIds, func) { if (this._disposed) return; else if (undefined === this._current) this.pushCurrent(cache, categoryIds, func); else this.pushNext(categoryIds, func); } /** Cancel all requests and empty the queue. */ [Symbol.dispose]() { if (undefined !== this._request) { (0, core_bentley_1.assert)(undefined !== this._current); this._request.cancel(); this._request = undefined; } this._current = this._next = undefined; this._disposed = true; } get isEmpty() { return undefined === this._current && undefined === this._next; } pushCurrent(cache, categoryIds, func) { (0, core_bentley_1.assert)(undefined === this._next); (0, core_bentley_1.assert)(undefined === this._current); (0, core_bentley_1.assert)(undefined === this._request); this._request = cache.load(categoryIds); if (undefined === this._request) { // All requested categories are already loaded. func(); return; } else { // We need to load the requested categories before invoking the function. this.processCurrent(cache, new QueueEntry(core_bentley_1.Id64.toIdSet(categoryIds, true), func)); } } processCurrent(cache, entry) { (0, core_bentley_1.assert)(undefined !== this._request); (0, core_bentley_1.assert)(undefined === this._current); (0, core_bentley_1.assert)(undefined === this._next); this._current = entry; this._request.promise.then((completed) => { if (this._disposed) return; // Invoke all the functions which were awaiting this set of IModelConnection.Categories. (0, core_bentley_1.assert)(undefined !== this._current); if (completed) for (const func of this._current.funcs) func(); this._request = undefined; this._current = undefined; // If we have more requests, process them. const next = this._next; this._next = undefined; if (undefined !== next) { this._request = cache.load(next.categoryIds); if (undefined === this._request) { // All categories loaded. for (const func of next.funcs) func(); } else { // We need to load the requested categories before invoking the pending functions. this.processCurrent(cache, next); } } }); } pushNext(categoryIds, func) { (0, core_bentley_1.assert)(undefined !== this._current); (0, core_bentley_1.assert)(undefined !== this._request); if (undefined === this._next) { // We have a request currently in process and none pending. // We could potentially determine that this request doesn't require any categories that are not already loaded or being loaded by the current request. // But we will find that out (synchronously) when current request completes, unless more requests come in. Probably not worth it. this._next = new QueueEntry(core_bentley_1.Id64.toIdSet(categoryIds, true), func); } else { // We have a request currently in process, and one or more pending. Append this one to the pending. this._next.funcs.push(func); for (const categoryId of core_bentley_1.Id64.iterable(categoryIds)) this._next.categoryIds.add(categoryId); } } } SubCategoriesCache.Queue = Queue; })(SubCategoriesCache || (exports.SubCategoriesCache = SubCategoriesCache = {})); //# sourceMappingURL=SubCategoriesCache.js.map