@itwin/core-frontend
Version:
iTwin.js frontend components
297 lines • 14 kB
JavaScript
"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