Skip to content

Commit

Permalink
Presentation: Add functions that return async iterators for paged res…
Browse files Browse the repository at this point in the history
…ponses (#6472)

Co-authored-by: yato333 <yato333@users.noreply.github.com>
Co-authored-by: Grigas <35135765+grigasp@users.noreply.github.com>
  • Loading branch information
3 people authored Mar 4, 2024
1 parent ec2d5b0 commit d1b9f65
Show file tree
Hide file tree
Showing 57 changed files with 1,748 additions and 1,039 deletions.
2 changes: 2 additions & 0 deletions common/api/presentation-common.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1683,6 +1683,8 @@ export class LocalizationHelper {
// (undocumented)
getLocalizedLabelDefinitions(labelDefinitions: LabelDefinition[]): LabelDefinition[];
// (undocumented)
getLocalizedNode(node: Node_2): Node_2;
// (undocumented)
getLocalizedNodePathElement(npe: NodePathElement): NodePathElement;
// (undocumented)
getLocalizedNodes(nodes: Node_2[]): Node_2[];
Expand Down
60 changes: 47 additions & 13 deletions common/api/presentation-frontend.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { IDisposable } from '@itwin/core-bentley';
import { IModelConnection } from '@itwin/core-frontend';
import { InstanceKey } from '@itwin/presentation-common';
import { InternetConnectivityStatus } from '@itwin/core-common';
import { Item } from '@itwin/presentation-common';
import { Key } from '@itwin/presentation-common';
import { Keys } from '@itwin/presentation-common';
import { KeySet } from '@itwin/presentation-common';
Expand All @@ -44,7 +45,6 @@ import { NodeKey } from '@itwin/presentation-common';
import { NodePathElement } from '@itwin/presentation-common';
import { Paged } from '@itwin/presentation-common';
import { PagedResponse } from '@itwin/presentation-common';
import { PageOptions } from '@itwin/presentation-common';
import { RegisteredRuleset } from '@itwin/presentation-common';
import { RpcRequestsHandler } from '@itwin/presentation-common';
import { Ruleset } from '@itwin/presentation-common';
Expand Down Expand Up @@ -78,9 +78,6 @@ export class BrowserLocalFavoritePropertiesStorage implements IFavoritePropertie
savePropertiesOrder(orderInfos: FavoritePropertiesOrderInfo[], iTwinId: string | undefined, imodelId: string): Promise<void>;
}

// @internal (undocumented)
export const buildPagedArrayResponse: <TItem>(requestedPage: PageOptions | undefined, getter: (page: Required<PageOptions>, requestIndex: number) => Promise<PagedResponse<TItem>>) => Promise<PagedResponse<TItem>>;

// @beta
export function consoleDiagnosticsHandler(diagnostics: ClientDiagnostics): void;

Expand Down Expand Up @@ -165,9 +162,18 @@ export enum FavoritePropertiesScope {
ITwin = 1
}

// @public
export type GetContentRequestOptions = ContentRequestOptions<IModelConnection, Descriptor | DescriptorOverrides, KeySet, RulesetVariable> & ClientDiagnosticsAttribute;

// @public
export type GetDistinctValuesRequestOptions = DistinctValuesRequestOptions<IModelConnection, Descriptor | DescriptorOverrides, KeySet, RulesetVariable> & ClientDiagnosticsAttribute;

// @internal (undocumented)
export const getFieldInfos: (field: Field) => Set<PropertyFullName>;

// @public
export type GetNodesRequestOptions = HierarchyRequestOptions<IModelConnection, NodeKey, RulesetVariable> & ClientDiagnosticsAttribute;

// @public @deprecated
export function getScopeId(scope: SelectionScope | string | undefined): string;

Expand Down Expand Up @@ -240,6 +246,11 @@ export interface ISelectionProvider {
selectionChange: SelectionChangeEvent;
}

// @public
export type MultipleValuesRequestOptions = Paged<{
maxParallelRequests?: number;
}>;

// @internal (undocumented)
export class NoopFavoritePropertiesStorage implements IFavoritePropertiesStorage {
// (undocumented)
Expand Down Expand Up @@ -314,32 +325,55 @@ export class PresentationManager implements IDisposable {
dispose(): void;
// @internal
ensureIModelInitialized(_: IModelConnection): Promise<void>;
getContent(requestOptions: Paged<ContentRequestOptions<IModelConnection, Descriptor | DescriptorOverrides, KeySet, RulesetVariable>> & ClientDiagnosticsAttribute): Promise<Content | undefined>;
getContentAndSize(requestOptions: Paged<ContentRequestOptions<IModelConnection, Descriptor | DescriptorOverrides, KeySet, RulesetVariable>> & ClientDiagnosticsAttribute): Promise<{
// @deprecated
getContent(requestOptions: GetContentRequestOptions & MultipleValuesRequestOptions): Promise<Content | undefined>;
// @deprecated
getContentAndSize(requestOptions: GetContentRequestOptions & MultipleValuesRequestOptions): Promise<{
content: Content;
size: number;
} | undefined>;
getContentDescriptor(requestOptions: ContentDescriptorRequestOptions<IModelConnection, KeySet, RulesetVariable> & ClientDiagnosticsAttribute): Promise<Descriptor | undefined>;
getContentInstanceKeys(requestOptions: ContentInstanceKeysRequestOptions<IModelConnection, KeySet, RulesetVariable> & ClientDiagnosticsAttribute): Promise<{
getContentInstanceKeys(requestOptions: ContentInstanceKeysRequestOptions<IModelConnection, KeySet, RulesetVariable> & ClientDiagnosticsAttribute & MultipleValuesRequestOptions): Promise<{
total: number;
items: () => AsyncGenerator<InstanceKey>;
}>;
getContentSetSize(requestOptions: ContentRequestOptions<IModelConnection, Descriptor | DescriptorOverrides, KeySet, RulesetVariable> & ClientDiagnosticsAttribute): Promise<number>;
getContentIterator(requestOptions: GetContentRequestOptions & MultipleValuesRequestOptions): Promise<{
descriptor: Descriptor;
total: number;
items: AsyncIterableIterator<Item>;
} | undefined>;
getContentSetSize(requestOptions: GetContentRequestOptions): Promise<number>;
getContentSources(requestOptions: ContentSourcesRequestOptions<IModelConnection> & ClientDiagnosticsAttribute): Promise<SelectClassInfo[]>;
getDisplayLabelDefinition(requestOptions: DisplayLabelRequestOptions<IModelConnection, InstanceKey> & ClientDiagnosticsAttribute): Promise<LabelDefinition>;
getDisplayLabelDefinitions(requestOptions: DisplayLabelsRequestOptions<IModelConnection, InstanceKey> & ClientDiagnosticsAttribute): Promise<LabelDefinition[]>;
// @deprecated
getDisplayLabelDefinitions(requestOptions: DisplayLabelsRequestOptions<IModelConnection, InstanceKey> & ClientDiagnosticsAttribute & MultipleValuesRequestOptions): Promise<LabelDefinition[]>;
getDisplayLabelDefinitionsIterator(requestOptions: DisplayLabelsRequestOptions<IModelConnection, InstanceKey> & ClientDiagnosticsAttribute & MultipleValuesRequestOptions): Promise<{
total: number;
items: AsyncIterableIterator<LabelDefinition>;
}>;
getDistinctValuesIterator(requestOptions: GetDistinctValuesRequestOptions & MultipleValuesRequestOptions): Promise<{
total: number;
items: AsyncIterableIterator<DisplayValueGroup>;
}>;
getElementProperties(requestOptions: SingleElementPropertiesRequestOptions<IModelConnection> & ClientDiagnosticsAttribute): Promise<ElementProperties | undefined>;
getFilteredNodePaths(requestOptions: FilterByTextHierarchyRequestOptions<IModelConnection, RulesetVariable> & ClientDiagnosticsAttribute): Promise<NodePathElement[]>;
getNodePaths(requestOptions: FilterByInstancePathsHierarchyRequestOptions<IModelConnection, RulesetVariable> & ClientDiagnosticsAttribute): Promise<NodePathElement[]>;
getNodes(requestOptions: Paged<HierarchyRequestOptions<IModelConnection, NodeKey, RulesetVariable>> & ClientDiagnosticsAttribute): Promise<Node_2[]>;
getNodesAndCount(requestOptions: Paged<HierarchyRequestOptions<IModelConnection, NodeKey, RulesetVariable>> & ClientDiagnosticsAttribute): Promise<{
// @deprecated
getNodes(requestOptions: GetNodesRequestOptions & MultipleValuesRequestOptions): Promise<Node_2[]>;
// @deprecated
getNodesAndCount(requestOptions: GetNodesRequestOptions & MultipleValuesRequestOptions): Promise<{
count: number;
nodes: Node_2[];
}>;
getNodesCount(requestOptions: HierarchyRequestOptions<IModelConnection, NodeKey, RulesetVariable> & ClientDiagnosticsAttribute): Promise<number>;
getNodesCount(requestOptions: GetNodesRequestOptions): Promise<number>;
// @beta
getNodesDescriptor(requestOptions: HierarchyLevelDescriptorRequestOptions<IModelConnection, NodeKey, RulesetVariable> & ClientDiagnosticsAttribute): Promise<Descriptor | undefined>;
getPagedDistinctValues(requestOptions: DistinctValuesRequestOptions<IModelConnection, Descriptor | DescriptorOverrides, KeySet, RulesetVariable> & ClientDiagnosticsAttribute): Promise<PagedResponse<DisplayValueGroup>>;
getNodesIterator(requestOptions: GetNodesRequestOptions & MultipleValuesRequestOptions): Promise<{
total: number;
items: AsyncIterableIterator<Node_2>;
}>;
// @deprecated
getPagedDistinctValues(requestOptions: GetDistinctValuesRequestOptions & MultipleValuesRequestOptions): Promise<PagedResponse<DisplayValueGroup>>;
// @internal (undocumented)
get ipcRequestsHandler(): IpcRequestsHandler | undefined;
// @alpha
Expand Down
4 changes: 3 additions & 1 deletion common/api/summary/presentation-frontend.exports.csv
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
sep=;
Release Tag;API Item
internal;BrowserLocalFavoritePropertiesStorage
internal;buildPagedArrayResponse:
beta;consoleDiagnosticsHandler(diagnostics: ClientDiagnostics): void
beta;createCombinedDiagnosticsHandler(handlers: ClientDiagnosticsHandler[]): (diagnostics: ClientDiagnostics) => void
public;createFavoritePropertiesStorage(type: DefaultFavoritePropertiesStorageTypes): IFavoritePropertiesStorage
Expand All @@ -15,7 +14,10 @@ public;FavoritePropertiesManager
public;FavoritePropertiesManagerProps
public;FavoritePropertiesOrderInfo
public;FavoritePropertiesScope
public;GetContentRequestOptions = ContentRequestOptions
public;GetDistinctValuesRequestOptions = DistinctValuesRequestOptions
internal;getFieldInfos: (field: Field) => Set
public;GetNodesRequestOptions = HierarchyRequestOptions
public;getScopeId(scope: SelectionScope | string | undefined): string
deprecated;getScopeId(scope: SelectionScope | string | undefined): string
internal;HILITE_RULESET: Ruleset
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/presentation-common",
"comment": "",
"type": "none"
}
],
"packageName": "@itwin/presentation-common"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/presentation-frontend",
"comment": "Add `getNodesIterator`, `getDistinctValuesIterator`, `getContentIterator` and `getDisplayLabelDefinitionIterator` methods to `PresentationManager` which return async iterators and load data using multiple parallel requests.",
"type": "none"
}
],
"packageName": "@itwin/presentation-frontend"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/presentation-frontend",
"comment": "Deprecate `getNodes`, `getNodesAndCount`, `getContent`, `getContentAndSize`, `getPagedDistinctValues` and `getDisplayLabelDefinitions` in favor of the alternatives which return iterators.",
"type": "none"
}
],
"packageName": "@itwin/presentation-frontend"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/rpcinterface-full-stack-tests",
"comment": "",
"type": "none"
}
],
"packageName": "@itwin/rpcinterface-full-stack-tests"
}
35 changes: 35 additions & 0 deletions docs/changehistory/NextVersion.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Table of contents:
- [Seafloor terrain](#seafloor-terrain)
- [Electron 29 support](#electron-29-support)
- [Editor](#editor)
- [Presentation](#presentation)
- [Deprecation of async array results in favor of async iterators](#deprecation-of-async-array-results-in-favor-of-async-iterators)

## Display

Expand Down Expand Up @@ -47,3 +49,36 @@ Changes to @beta [CreateElementWithDynamicsTool]($editor-frontend) class:

- [CreateElementWithDynamicsTool.doCreateElement]($editor-frontend) no longer takes an optional [ElementGeometryBuilderParams]($common) as it will be set on the supplied [GeometricElementProps]($common).

## Presentation

### Deprecation of async array results in favor of async iterators

`PresentationManager` contains a number of methods to retrieve sets of results like nodes, content, etc. All of these methods have been deprecated in favor of new ones that return an async iterator instead of an array:

- Use `getContentIterator` instead of `getContent` and `getContentAndSize`.
- Use `getDisplayLabelDefinitionsIterator` instead of `getDisplayLabelDefinitions`.
- Use `getDistinctValuesIterator` instead of `getPagedDistinctValues`.
- Use `getNodesIterator` instead of `getNodes` and `getNodesAndCount`.

All of the above methods, including the deprecated ones, received ability to load large sets of results concurrently. Previously, when requesting a large set (page size > 1000), the result was created by sending a number of requests sequentially by requesting the first page, then the second, and so on, until the whole requested set was retrieved. Now, we send a request for the first page to determine total number of items and backend's page size limit, together with the first page of results, and then other requests are made all at once. At attribute `maxParallelRequests` was added to these methods to control how many parallel requests should be sent at a time.

While performance-wise deprecated methods should be in line with the newly added async iterator ones, the latter have two advantages:

1. Caller can start iterating over results as soon as we receive the first page, instead of having to wait for the whole set.
2. The iterator version stops sending requests as soon as the caller stops iterating over the async iterator.

> Example: Showing display labels for a large set of elements in a React component.
>
> The deprecated approach would be to use `PresentationManager.getDisplayLabelDefinitions` to retrieve labels of all elements. Because the API has to load all the data before returning it to the widget, user has to wait a long time to start seeing the results. Moreover, if he decides to close the widget (the component is unmounted), the `getDisplayLabelDefinitions` keeps building the array by sending requests to the backend - there's no way to cancel that.
>
> The new approach is to use `PresentationManager.getDisplayLabelDefinitionsIterator` to get the labels. The labels get streamed to the called as soon as the first results' page is retrieved, so user gets to see initial results quickly, while additional pages keep loading in the background. In addition, if the component is unmounted, iteration can be stopped, thus cancelling all further requests to the backend:
>
> ```ts
> const { items } = await manager.getDisplayLabelDefinitionsIterator(requestProps);
> for await (const label of items) {
> if (isComponentUnmounted) {
> break;
> }
> // update component's model to render the loaded label
> }
> ```
11 changes: 11 additions & 0 deletions full-stack-tests/presentation/src/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,14 @@ export async function waitFor<T>(check: () => Promise<T> | T, timeout?: number):
} while (timer.current.milliseconds < timeout);
throw lastError;
}

/**
* Collects items of an async iterable to an array.
*/
export async function collect<T>(iter: AsyncIterable<T>): Promise<T[]> {
const result = new Array<T>();
for await (const item of iter) {
result.push(item);
}
return result;
}
Loading

0 comments on commit d1b9f65

Please sign in to comment.