Skip to content

Commit

Permalink
Merged PR 75612: NativeApp Cancel/Progress for iOS
Browse files Browse the repository at this point in the history
#### Allow get progress event on the frontend for native app (ios/desktop)
```ts
  const downloadToken = await NativeApp.startDownloadBriefcase(requestContext, testProjectId, testIModelId, IModelVersion.latest(),
    (_progress: ProgressInfo) => {
     // handle progress
  });
```
#### If online flag is set to false Request throws ResponseError instantly avoiding retries.
```ts
  if (!RequestGlobalOptions.online) {
    throw new ResponseError(503, "Service unavailable");
  }

```

Related work items: #280275, #290361
  • Loading branch information
khanaffan committed Mar 24, 2020
1 parent 06a4987 commit f9683a8
Show file tree
Hide file tree
Showing 18 changed files with 200 additions and 56 deletions.
2 changes: 1 addition & 1 deletion common/api/imodeljs-clients-backend.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class IOSAzureFileHandler implements FileHandler {
// (undocumented)
agent: any;
basename(filePath: string): string;
downloadFile(requestContext: AuthorizedClientRequestContext, downloadUrl: string, downloadToPathname: string): Promise<void>;
downloadFile(requestContext: AuthorizedClientRequestContext, downloadUrl: string, downloadToPathname: string, _fileSize?: number, progressCallback?: ProgressCallback, cancelRequest?: CancelRequest): Promise<void>;
exists(filePath: string): boolean;
getFileSize(filePath: string): number;
isDirectory(filePath: string): boolean;
Expand Down
2 changes: 2 additions & 0 deletions common/api/imodeljs-clients.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1611,6 +1611,8 @@ export class RequestGlobalOptions {
// (undocumented)
static maxRetries: number;
// (undocumented)
static online: boolean;
// (undocumented)
static timeout: RequestTimeoutOptions;
}

Expand Down
8 changes: 3 additions & 5 deletions common/api/imodeljs-common.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,12 +518,8 @@ export interface BRepPrimitive {

// @internal
export interface BriefcaseProps extends IModelTokenProps {
// (undocumented)
downloading?: boolean;
// (undocumented)
fileSize?: number;
// (undocumented)
isOpen?: boolean;
}

export { BriefcaseStatus }
Expand Down Expand Up @@ -1839,6 +1835,8 @@ export namespace Events {
const // (undocumented)
onMemoryWarning = "onMemoryWarning";
const // (undocumented)
onBriefcaseDownloadProgress = "download-progress";
const // (undocumented)
onInternetConnectivityChanged = "onInternetConnectivityChanged";
}
}
Expand Down Expand Up @@ -3724,7 +3722,7 @@ export abstract class NativeAppRpcInterface extends RpcInterface {
log(_timestamp: number, _level: LogLevel, _category: string, _message: string, _metaData?: any): Promise<void>;
openBriefcase(_iModelToken: IModelTokenProps): Promise<IModelProps>;
overrideInternetConnectivity(_overriddenBy: OverriddenBy, _status?: InternetConnectivityStatus): Promise<void>;
startDownloadBriefcase(_iModelToken: IModelTokenProps): Promise<IModelTokenProps>;
startDownloadBriefcase(_iModelToken: IModelTokenProps, _reportProgress: boolean): Promise<IModelTokenProps>;
// (undocumented)
storageGet(_storageId: string, _key: string): Promise<StorageValue | undefined>;
// (undocumented)
Expand Down
18 changes: 14 additions & 4 deletions common/api/imodeljs-frontend.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ import { PolylineData } from '@bentley/imodeljs-common';
import { PolylineEdgeArgs } from '@bentley/imodeljs-common';
import { PolylineFlags } from '@bentley/imodeljs-common';
import { PolylineTypeFlags } from '@bentley/imodeljs-common';
import { ProgressCallback } from '@bentley/imodeljs-clients';
import { PropertyDescription } from '@bentley/ui-abstract';
import { QParams2d } from '@bentley/imodeljs-common';
import { QParams3d } from '@bentley/imodeljs-common';
Expand Down Expand Up @@ -2100,6 +2101,15 @@ export abstract class DisplayStyleState extends ElementState implements DisplayS
get wantShadows(): boolean;
}

// @internal
export class DownloadBriefcaseToken {
constructor(iModelToken: IModelToken, stopProgressEvents: () => void);
// (undocumented)
iModelToken: IModelToken;
// (undocumented)
stopProgressEvents: () => void;
}

// @alpha
export interface DrawClipOptions {
color?: ColorDef;
Expand Down Expand Up @@ -5308,7 +5318,7 @@ export enum ModifyElementSource {
// @internal
export class NativeApp {
// (undocumented)
static cancelDownloadBriefcase(requestContext: AuthorizedClientRequestContext, iModelToken: IModelTokenProps): Promise<boolean>;
static cancelDownloadBriefcase(requestContext: AuthorizedClientRequestContext, downloadBriefcaseToken: DownloadBriefcaseToken): Promise<boolean>;
// (undocumented)
static checkInternetConnectivity(): Promise<InternetConnectivityStatus>;
static closeStorage(storage: Storage, deleteId: boolean): Promise<void>;
Expand All @@ -5317,7 +5327,7 @@ export class NativeApp {
// (undocumented)
static downloadBriefcase(requestContext: AuthorizedClientRequestContext, contextId: string, iModelId: string, version?: IModelVersion): Promise<IModelTokenProps>;
// (undocumented)
static finishDownloadBriefcase(requestContext: AuthorizedClientRequestContext, iModelToken: IModelTokenProps): Promise<void>;
static finishDownloadBriefcase(requestContext: AuthorizedClientRequestContext, downloadBriefcaseToken: DownloadBriefcaseToken): Promise<void>;
static getBriefcases(): Promise<BriefcaseProps[]>;
static getStorageNames(): Promise<string[]>;
// (undocumented)
Expand All @@ -5328,11 +5338,11 @@ export class NativeApp {
static openBriefcase(requestContext: AuthorizedClientRequestContext, iModelToken: IModelTokenProps): Promise<IModelConnection>;
static openStorage(name: string): Promise<Storage>;
// (undocumented)
static overrideInternetConnectivity(status?: InternetConnectivityStatus): Promise<void>;
static overrideInternetConnectivity(status: InternetConnectivityStatus): Promise<void>;
// (undocumented)
static shutdown(): Promise<void>;
// (undocumented)
static startDownloadBriefcase(requestContext: AuthorizedClientRequestContext, contextId: string, iModelId: string, version?: IModelVersion): Promise<IModelTokenProps>;
static startDownloadBriefcase(requestContext: AuthorizedClientRequestContext, contextId: string, iModelId: string, version?: IModelVersion, progress?: ProgressCallback): Promise<DownloadBriefcaseToken>;
// (undocumented)
static startup(opts?: IModelAppOptions): Promise<void>;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@bentley/imodeljs-backend",
"comment": "Support for progress/cancel from ios",
"type": "none"
}
],
"packageName": "@bentley/imodeljs-backend",
"email": "[email protected]"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@bentley/imodeljs-clients-backend",
"comment": "Support for progress/cancel from ios",
"type": "none"
}
],
"packageName": "@bentley/imodeljs-clients-backend",
"email": "[email protected]"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@bentley/imodeljs-clients",
"comment": "If explicitly mark offline request should fail instantly",
"type": "none"
}
],
"packageName": "@bentley/imodeljs-clients",
"email": "[email protected]"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@bentley/imodeljs-common",
"comment": "Support for progress/cancel from ios",
"type": "none"
}
],
"packageName": "@bentley/imodeljs-common",
"email": "[email protected]"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@bentley/imodeljs-frontend",
"comment": "Support for progress/cancel from ios",
"type": "none"
}
],
"packageName": "@bentley/imodeljs-frontend",
"email": "[email protected]"
}
1 change: 0 additions & 1 deletion core/backend/src/BriefcaseManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,6 @@ export class BriefcaseManager {
iModelId: briefcase.iModelId,
changeSetId: briefcase.currentChangeSetId,
openMode: briefcase.openParams.openMode,
isOpen: briefcase.isOpen,
fileSize: fz,
});
}
Expand Down
2 changes: 2 additions & 0 deletions core/backend/src/NativeAppBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { EventSinkManager, EmitStrategy } from "./EventSink";
import * as path from "path";
import * as os from "os";
import { IModelJsFs } from "./IModelJsFs";
import { RequestGlobalOptions } from "@bentley/imodeljs-clients";

/**
* Used by desktop/mobile native application
Expand Down Expand Up @@ -106,6 +107,7 @@ export class NativeAppBackend {
public static overrideInternetConnectivity(_overridenBy: OverriddenBy, status?: InternetConnectivityStatus): void {
if (this._reachability !== status) {
this._reachability = status;
RequestGlobalOptions.online = this._reachability === InternetConnectivityStatus.Online;
this.onInternetConnectivityChanged.raiseEvent();
}
}
Expand Down
20 changes: 14 additions & 6 deletions core/backend/src/rpc-impl/NativeAppRpcImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ import {
BriefcaseProps,
StorageValue,
IModelError,
Events,
} from "@bentley/imodeljs-common";
import { EventSinkManager } from "../EventSink";
import { EventSinkManager, EmitStrategy } from "../EventSink";
import { cancelTileContentRequests } from "./IModelTileRpcImpl";
import { NativeAppBackend } from "../NativeAppBackend";
import { Config, AuthorizedClientRequestContext } from "@bentley/imodeljs-clients";
import { Logger, LogLevel, ClientRequestContext, DbResult, assert } from "@bentley/bentleyjs-core";
import { Config, AuthorizedClientRequestContext, ProgressInfo } from "@bentley/imodeljs-clients";
import { Logger, LogLevel, ClientRequestContext, DbResult } from "@bentley/bentleyjs-core";
import { BriefcaseDb, OpenParams } from "../IModelDb";
import { BriefcaseManager, KeepBriefcase } from "../BriefcaseManager";
import { NativeAppStorage } from "../NativeAppStorage";
Expand Down Expand Up @@ -119,13 +120,20 @@ export class NativeAppRpcImpl extends RpcInterface implements NativeAppRpcInterf
* @returns briefcase id of the briefcase that is downloaded.
* @note this api can be call only in connected mode where internet is available.
*/
public async startDownloadBriefcase(tokenProps: IModelTokenProps): Promise<IModelTokenProps> {
public async startDownloadBriefcase(tokenProps: IModelTokenProps, reportProgress: boolean): Promise<IModelTokenProps> {
const requestContext = ClientRequestContext.current as AuthorizedClientRequestContext;

BriefcaseManager.initializeBriefcaseCacheFromDisk();

const iModelToken = IModelToken.fromJSON(tokenProps);
const openParams: OpenParams = OpenParams.pullOnly();
if (reportProgress) {
openParams.downloadProgress = (progress: ProgressInfo) => {
EventSinkManager.global.emit(
Events.NativeApp.namespace,
Events.NativeApp.onBriefcaseDownloadProgress + "-" + tokenProps.iModelId,
{ progress }, { strategy: EmitStrategy.PurgeOlderEvents });
};
}
const iModelVersion = IModelVersion.asOfChangeSet(iModelToken.changeSetId!);
const db = await BriefcaseDb.startDownloadBriefcase(requestContext, iModelToken.contextId!, iModelToken.iModelId!, openParams, iModelVersion);
return db.toJSON();
Expand Down Expand Up @@ -171,7 +179,7 @@ export class NativeAppRpcImpl extends RpcInterface implements NativeAppRpcInterf
if (briefcases.length === 0) {
throw new IModelError(DbResult.BE_SQLITE_ERROR_FileNotFound, "Briefcase not found with requested iModelId/changesetId/openMode");
}
assert(briefcases.length === 1);

Object.assign(iModelToken, { key: briefcases[0].key });
}
const db = await BriefcaseDb.openBriefcase(requestContext, iModelToken);
Expand Down
21 changes: 18 additions & 3 deletions core/clients-backend/src/imodelhub/IOSAzureFileHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
/** @packageDocumentation
* @module iModelHub
*/
import { Logger } from "@bentley/bentleyjs-core";
import { ArgumentCheck, AuthorizedClientRequestContext, FileHandler, request, RequestOptions } from "@bentley/imodeljs-clients";
import { Logger, BriefcaseStatus } from "@bentley/bentleyjs-core";
import { ArgumentCheck, AuthorizedClientRequestContext, FileHandler, request, RequestOptions, ProgressCallback, UserCancelledError, CancelRequest } from "@bentley/imodeljs-clients";
import * as fs from "fs";
import * as path from "path";
import { ClientsBackendLoggerCategory } from "../ClientsBackendLoggerCategory";
Expand Down Expand Up @@ -48,7 +48,7 @@ export class IOSAzureFileHandler implements FileHandler {
* @throws [[IModelHubClientError]] with [IModelHubStatus.UndefinedArgumentError]($bentley) if one of the arguments is undefined or empty.
*/

public async downloadFile(requestContext: AuthorizedClientRequestContext, downloadUrl: string, downloadToPathname: string): Promise<void> {
public async downloadFile(requestContext: AuthorizedClientRequestContext, downloadUrl: string, downloadToPathname: string, _fileSize?: number, progressCallback?: ProgressCallback, cancelRequest?: CancelRequest): Promise<void> {
requestContext.enter();
if (!IOSAzureFileHandler._isMobile) {
Logger.logError(loggerCategory, "Expecting this code to run on a mobile device");
Expand All @@ -68,6 +68,9 @@ export class IOSAzureFileHandler implements FileHandler {
const xhr = new XMLHttpRequest();
xhr.open("GET", downloadUrl);
xhr.onload = () => {
if (cancelRequest !== undefined) {
cancelRequest.cancel = () => false;
}
if (xhr.status >= 200 && xhr.status < 300) {
resolve();
} else {
Expand All @@ -77,6 +80,18 @@ export class IOSAzureFileHandler implements FileHandler {
xhr.onerror = () => {
reject();
};
if (progressCallback) {
(xhr as any).onprogress = (doneBytes: number, totalBytes: number) => {
progressCallback({ loaded: doneBytes, total: totalBytes, percent: Number(((doneBytes / totalBytes) * 100).toFixed(2)) });
};
}
if (cancelRequest !== undefined) {
cancelRequest.cancel = () => {
(xhr as any).cancel();
reject(new UserCancelledError(BriefcaseStatus.DownloadCancelled, "User cancelled download", Logger.logWarning));
return true;
};
}
// iOS implementation knows about this method
(xhr as any).downloadFile(downloadToPathname);
});
Expand Down
10 changes: 9 additions & 1 deletion core/clients/src/Request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ export class RequestGlobalOptions {
deadline: 25000,
response: 10000,
};
// Assume application is online or offline. This hint skip retry/timeout
public static online: boolean = true;
}

/** Error object that's thrown/rejected if the Request fails due to a network error, or if the status is *not* in the range of 200-299 (inclusive)
Expand Down Expand Up @@ -277,8 +279,14 @@ export async function request(requestContext: ClientRequestContext, url: string,
} else {
proxyUrl = url;
}

if (!RequestGlobalOptions.online) {
throw new ResponseError(503, "Service unavailable");
}

let sareq: sarequest.SuperAgentRequest = sarequest(options.method, proxyUrl);
const retries = typeof options.retries === "undefined" ? RequestGlobalOptions.maxRetries : options.retries;
let sareq: sarequest.SuperAgentRequest = sarequest(options.method, proxyUrl).retry(retries, options.retryCallback);
sareq = sareq.retry(retries, options.retryCallback);

if (Logger.isEnabled(loggerCategory, LogLevel.Trace))
sareq = sareq.use(logRequest);
Expand Down
6 changes: 3 additions & 3 deletions core/common/src/rpc/NativeAppRpcInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export namespace Events {
export namespace NativeApp {
export const namespace = "NativeApp";
export const onMemoryWarning = "onMemoryWarning";
export const onBriefcaseDownloadProgress = "download-progress";
export const onInternetConnectivityChanged = "onInternetConnectivityChanged";
}
}
Expand All @@ -42,8 +43,6 @@ export namespace Events {
* @internal
*/
export interface BriefcaseProps extends IModelTokenProps {
downloading?: boolean;
isOpen?: boolean;
fileSize?: number;
}

Expand Down Expand Up @@ -127,9 +126,10 @@ export abstract class NativeAppRpcInterface extends RpcInterface {
/**
* Starts download of a briefcase. The call require internet connection and must have valid token.
* @param _iModelToken IModel context information.
* @param _reportProgress Report progress to frontend
* @returns IModelTokenProps which allow to create IModelConnection.
*/
public async startDownloadBriefcase(_iModelToken: IModelTokenProps): Promise<IModelTokenProps> { return this.forward(arguments); }
public async startDownloadBriefcase(_iModelToken: IModelTokenProps, _reportProgress: boolean): Promise<IModelTokenProps> { return this.forward(arguments); }

/**
* Finishes download of a briefcase. The call require internet connection and must have valid token.
Expand Down
2 changes: 1 addition & 1 deletion core/common/src/rpc/mobile/MobileRpcProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class MobileRpcProtocol extends RpcProtocol {
public socket: WebSocket = (undefined as any);
public requests: Map<string, MobileRpcRequest> = new Map();
private _pending: MobileRpcChunks[] = [];
private _capacity: number = 1;
private _capacity: number = Number.MAX_SAFE_INTEGER;
private _sendInterval: number | undefined = undefined;
private _sendIntervalHandler = () => this.trySend();
public readonly requestType = MobileRpcRequest;
Expand Down
Loading

0 comments on commit f9683a8

Please sign in to comment.