Skip to content

Commit

Permalink
simplify webview API client resolving, #559
Browse files Browse the repository at this point in the history
  • Loading branch information
vladimiry committed May 3, 2023
1 parent 7e33af9 commit 624cda5
Show file tree
Hide file tree
Showing 8 changed files with 212 additions and 298 deletions.
1 change: 0 additions & 1 deletion scripts/finalize-browser-window-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ const appDir = process.env.NODE_ENV === "development" ? "./app-dev" : "./app";
((): void => {
const filePatterns = [
`const moduleFactory = yield compiler.compileModuleAsync(DbViewModule);`,
`const apiClient = yield resolvePrimaryWebViewApiClient();`,
`customCssKey = yield webView.insertCSS(customCSS);`,
] as const;
const fileLocation = path.join(appDir, WEB_PROTOCOL_DIR, `./browser-window/_accounts.mjs`);
Expand Down
4 changes: 1 addition & 3 deletions src/e2e/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ test("general actions: app start, master password setup, add accounts", async ()
await initAppWithTestContext({initial: true}, async ({workflow}) => {
await workflow.saveScreenshot(); // screenshot with user agent clearly displayed at this point
await workflow.login({setup: true, savePassword: false});
for (const entryUrlValue of PROTON_API_ENTRY_URLS) {
await workflow.addAccount({entryUrlValue});
}
await workflow.addAccount({entryUrlValue: PROTON_API_ENTRY_URLS[0]});
await workflow.logout();
});
});
Expand Down
12 changes: 3 additions & 9 deletions src/web/browser-window/app/_accounts/account-view.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,14 +335,9 @@ export class AccountViewComponent extends NgChangesObservableComponent implement
// eslint-disable-next-line @typescript-eslint/unbound-method
this.logger.info(nameof(AccountViewComponent.prototype.onPrimaryViewLoadedOnce));

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const resolvePrimaryWebViewApiClient = async () => firstValueFrom(
this.electronService
.primaryWebViewClient({webView: primaryWebView, ...await this.resolveAccountIndex()}, {pingTimeoutMs: 7000}),
);
const primaryWebViewClient = this.electronService.primaryWebViewClient({webView: primaryWebView});
const resolveLiveProtonClientSession = async (): Promise<ProtonClientSession> => {
const apiClient = await resolvePrimaryWebViewApiClient();
const value = await apiClient("resolveLiveProtonClientSession")(await this.resolveAccountIndex());
const value = await primaryWebViewClient("resolveLiveProtonClientSession")(await this.resolveAccountIndex());
if (!value) {
throw new Error(`Failed to resolve "proton client session" object`);
}
Expand Down Expand Up @@ -432,8 +427,7 @@ export class AccountViewComponent extends NgChangesObservableComponent implement
const [clientSession, sessionStoragePatch] = await Promise.all([
resolveLiveProtonClientSession(),
(async () => {
const apiClient = await resolvePrimaryWebViewApiClient();
return apiClient("resolvedLiveSessionStoragePatch")(await this.resolveAccountIndex());
return primaryWebViewClient("resolvedLiveSessionStoragePatch")(await this.resolveAccountIndex());
})(),
]);
// TODO if "src$" has been set before, consider only refreshing the client session without full page reload
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,9 @@ export class AccountsCalendarNsEffects {
logger.info("setup");

return merge(
this.api.calendarWebViewClient({webView, accountIndex}, {finishPromise}).pipe(
mergeMap((webViewClient) => {
return from(
webViewClient("notification")({accountIndex}),
);
}),
from(
this.api.calendarWebViewClient({webView}, {finishPromise})("notification")({accountIndex}),
).pipe(
withLatestFrom(
this.store.pipe(
select(AccountsSelectors.ACCOUNTS.pickAccount({login})),
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {Actions, createEffect} from "@ngrx/effects";
import {concatMap, delay, finalize, mergeMap, take} from "rxjs/operators";
import {concatMap, finalize, mergeMap, take} from "rxjs/operators";
import {EMPTY, from, merge, NEVER, Observable, of} from "rxjs";
import {Injectable} from "@angular/core";
import {select, Store} from "@ngrx/store";

import {ACCOUNTS_ACTIONS} from "src/web/browser-window/app/store/actions";
import {AccountsSelectors} from "src/web/browser-window/app/store/selectors";
import {AccountsService} from "src/web/browser-window/app/_accounts/accounts.service";
import {consumeMemoryRateLimiter, curryFunctionMembers} from "src/shared/util";
import {asyncDelay, consumeMemoryRateLimiter, curryFunctionMembers} from "src/shared/util";
import {ElectronService} from "src/web/browser-window/app/_core/electron.service";
import {getWebLogger} from "src/web/browser-window/util";
import {ofType} from "src/shared/util/ngrx-of-type";
Expand Down Expand Up @@ -66,15 +66,11 @@ export class AccountsLoginFormSubmittingEffects {
case "login": {
const onlyFillLoginAction = (): Observable<import("@ngrx/store").Action> => {
logger.info("fillLogin");

return merge(
of(this.accountsService.buildLoginDelaysResetAction({login})),
this.api.primaryWebViewClient({webView, accountIndex}, {pingTimeoutMs: 7008}).pipe(
mergeMap((webViewClient) => {
return from(
webViewClient("fillLogin")({login, accountIndex}),
);
}),
from(
this.api.primaryWebViewClient({webView})("fillLogin")({login, accountIndex}),
).pipe(
mergeMap(() => of(ACCOUNTS_ACTIONS.Patch({login, patch: {loginFilledOnce: true}}))),
),
);
Expand All @@ -87,22 +83,20 @@ export class AccountsLoginFormSubmittingEffects {
of(this.accountsService.buildLoginDelaysResetAction({login})),
of(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {password: true}})),
resetNotificationsState$,
this.api.primaryWebViewClient({webView, accountIndex}, {pingTimeoutMs: 7009}).pipe(
delay(
account.loggedInOnce
? ONE_SECOND_MS
: 0,
),
mergeMap((webViewClient) => {
from(
account.loggedInOnce ? asyncDelay(ONE_SECOND_MS) : Promise.resolve(),
).pipe(
mergeMap(() => {
return from(
webViewClient("login")({login, password, accountIndex}),
this.api.primaryWebViewClient({webView})("login")({login, password, accountIndex}),
).pipe(
mergeMap(() => EMPTY),
finalize(() => this.store.dispatch(ACCOUNTS_ACTIONS.PatchProgress({
login,
patch: {password: false}
}))),
);
}),
mergeMap(() => EMPTY),
finalize(() => this.store.dispatch(ACCOUNTS_ACTIONS.PatchProgress({
login,
patch: {password: false}
}))),
),
);

Expand Down Expand Up @@ -167,12 +161,9 @@ export class AccountsLoginFormSubmittingEffects {
const action$ = merge(
of(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {twoFactorCode: true}})),
resetNotificationsState$,
this.api.primaryWebViewClient({webView, accountIndex}, {pingTimeoutMs: 7010}).pipe(
mergeMap((webViewClient) => {
return from(
webViewClient("login2fa")({secret, accountIndex}),
);
}),
from(
this.api.primaryWebViewClient({webView})("login2fa")({secret, accountIndex}),
).pipe(
mergeMap(() => EMPTY),
finalize(() => this.store.dispatch(ACCOUNTS_ACTIONS.PatchProgress({
login,
Expand All @@ -199,13 +190,9 @@ export class AccountsLoginFormSubmittingEffects {
const action$ = merge(
of(ACCOUNTS_ACTIONS.PatchProgress({login, patch: {mailPassword: true}})),
resetNotificationsState$,
// TODO TS: resolve "webViewClient" calling "this.api.webViewClient" as normally
of(__ELECTRON_EXPOSURE__.buildIpcPrimaryWebViewClient(webView)).pipe(
mergeMap((webViewClient) => {
return from(
webViewClient("unlock")({mailPassword, accountIndex}),
);
}),
from(
this.api.primaryWebViewClient({webView})("unlock")({mailPassword, accountIndex}),
).pipe(
mergeMap(() => EMPTY),
finalize(() => this.store.dispatch(ACCOUNTS_ACTIONS.PatchProgress({
login,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@ export class AccountsPrimaryNsEffects {
logger.info("setup");

return merge(
this.api.primaryWebViewClient({webView, accountIndex}, {finishPromise, pingTimeoutMs: 7011}).pipe(
mergeMap((webViewClient) => {
return from(
webViewClient("notification")({login, entryApiUrl, apiEndpointOriginSS, accountIndex}),
);
from(
this.api.primaryWebViewClient({webView}, {finishPromise})("notification")({
login,
entryApiUrl,
apiEndpointOriginSS,
accountIndex
}),
).pipe(
withLatestFrom(
this.store.pipe(
select(AccountsSelectors.ACCOUNTS.pickAccount({login})),
Expand Down Expand Up @@ -77,17 +79,16 @@ export class AccountsPrimaryNsEffects {
filter(({payload: {key}}) => key.login === login),
mergeMap(({payload}) => {
// TODO live attachments export: fire error if offline or not signed-in into the account
return this.api.primaryWebViewClient({webView, accountIndex}, {finishPromise, pingTimeoutMs: 7012}).pipe(
mergeMap((webViewClient) => {
return from(
webViewClient("exportMailAttachments", {timeoutMs: payload.timeoutMs})({
uuid: payload.uuid,
mailPk: payload.mailPk,
login: payload.key.login,
accountIndex,
}),
);
return from(
this.api.primaryWebViewClient({webView}, {finishPromise})(
"exportMailAttachments", {timeoutMs: payload.timeoutMs},
)({
uuid: payload.uuid,
mailPk: payload.mailPk,
login: payload.key.login,
accountIndex,
}),
).pipe(
catchError((error) => {
return from(
this.api.ipcMainClient()("dbExportMailAttachmentsNotification")({
Expand Down
71 changes: 11 additions & 60 deletions src/web/browser-window/app/_core/electron.service.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,21 @@
import {concatMap, delay, mergeMap, retryWhen, switchMap, take} from "rxjs/operators";
import {createIpcMainApiService} from "electron-rpc-api";
import {defer, Observable, of, race, Subscription, throwError, timer} from "rxjs";
import {Injectable, NgZone} from "@angular/core";
import {mergeMap} from "rxjs/operators";
import type {OnDestroy} from "@angular/core";
import {select, Store} from "@ngrx/store";
import {Subscription} from "rxjs";

import {DEFAULT_API_CALL_TIMEOUT, ONE_SECOND_MS} from "src/shared/const";
import {DEFAULT_API_CALL_TIMEOUT} from "src/shared/const";
import {getWebLogger} from "src/web/browser-window/util";
import {OptionsSelectors} from "src/web/browser-window/app/store/selectors";
import {PROTON_CALENDAR_IPC_WEBVIEW_API_DEFINITION} from "src/shared/api/webview/calendar";
import {State} from "src/web/browser-window/app/store/reducers/options";
import {WebAccountIndexProp} from "src/web/browser-window/app/model";

type SuperCallOptions = Required<Exclude<Parameters<ReturnType<(typeof createIpcMainApiService)>["client"]>[0], undefined>>["options"];

type LimitedCallOptions = Partial<Pick<SuperCallOptions, "timeoutMs" | "finishPromise" | "serialization">>;

const logger = getWebLogger(__filename);

export class WebviewPingFailedError extends Error {
constructor(message: string) {
super(message);
if (Error.captureStackTrace) {
Error.captureStackTrace(this, WebviewPingFailedError);
}
this.name = nameof(WebviewPingFailedError);
}
}

@Injectable()
export class ElectronService implements OnDestroy {
private defaultApiCallTimeoutMs = DEFAULT_API_CALL_TIMEOUT;
Expand Down Expand Up @@ -56,44 +44,30 @@ export class ElectronService implements OnDestroy {
if (this.defaultApiCallTimeoutMs === value) {
return;
}
logger.info(`changing "defaultApiCallTimeoutMs" from ${this.defaultApiCallTimeoutMs} to ${value}`);
logger.info(`changing timeout from ${this.defaultApiCallTimeoutMs} to ${value}`);
this.defaultApiCallTimeoutMs = value;
}),
);
}

primaryWebViewClient(
{webView, accountIndex}: { webView: Electron.WebviewTag } & WebAccountIndexProp,
options?: LimitedCallOptions & { pingTimeoutMs?: number },
): Observable<ReturnType<typeof __ELECTRON_EXPOSURE__.buildIpcPrimaryWebViewClient>> {
const client = __ELECTRON_EXPOSURE__.buildIpcPrimaryWebViewClient(
{webView}: { webView: Electron.WebviewTag },
options?: LimitedCallOptions,
): ReturnType<typeof __ELECTRON_EXPOSURE__.buildIpcPrimaryWebViewClient> {
return __ELECTRON_EXPOSURE__.buildIpcPrimaryWebViewClient(
webView,
{options: this.buildApiCallOptions(options)},
);
return this.store.pipe(select(OptionsSelectors.CONFIG.timeouts)).pipe(
take(1),
switchMap(({webViewApiPing: timeoutMs}) => {
return this.raceWebViewClient({client, accountIndex}, options?.pingTimeoutMs ?? timeoutMs);
}),
mergeMap(() => of(client)),
);
}

calendarWebViewClient(
{webView, accountIndex}: { webView: Electron.WebviewTag } & WebAccountIndexProp,
{webView}: { webView: Electron.WebviewTag },
options?: LimitedCallOptions,
): Observable<ReturnType<typeof __ELECTRON_EXPOSURE__.buildIpcCalendarWebViewClient>> {
const client = __ELECTRON_EXPOSURE__.buildIpcCalendarWebViewClient(
): ReturnType<typeof __ELECTRON_EXPOSURE__.buildIpcCalendarWebViewClient> {
return __ELECTRON_EXPOSURE__.buildIpcCalendarWebViewClient(
webView,
{options: this.buildApiCallOptions(options)},
);

// TODO consider removing "ping" API or pinging once per "webView", keeping state in WeakMap<WebView, ...>?
return this.store.pipe(
select(OptionsSelectors.CONFIG.timeouts),
switchMap(({webViewApiPing: timeoutMs}) => this.raceWebViewClient({client, accountIndex}, timeoutMs)),
concatMap(() => of(client)),
);
}

ipcMainClient(options?: LimitedCallOptions): ReturnType<typeof __ELECTRON_EXPOSURE__.buildIpcMainClient> {
Expand All @@ -106,29 +80,6 @@ export class ElectronService implements OnDestroy {
this.subscription.unsubscribe();
}

// TODO TS: simplify method signature
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
private raceWebViewClient<T extends ReturnType<import("pubsub-to-rpc-api")
.Model
.CreateServiceReturn<Pick<(typeof PROTON_CALENDAR_IPC_WEBVIEW_API_DEFINITION), "ping">,
[import("electron").IpcMessageEvent]>["caller"]>>(
{client, accountIndex}: { client: T } & WebAccountIndexProp,
timeoutMs: number,
) {
return race(
defer(async () => client("ping", {timeoutMs: ONE_SECOND_MS})({accountIndex})).pipe(
retryWhen((errors) => errors.pipe(delay(ONE_SECOND_MS))),
),
timer(timeoutMs).pipe(
concatMap(() => {
return throwError(() => {
return new WebviewPingFailedError(`Failed to ping the "webview" backend service (timeout: ${timeoutMs}ms).`);
});
}),
),
);
}

private buildApiCallOptions(options: LimitedCallOptions = {}): SuperCallOptions {
return {
logger,
Expand Down

0 comments on commit 624cda5

Please sign in to comment.