1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-02-28 03:21:40 +01:00

Improve SDK direct function usage (#13353)

* feat: initalize WASM/SDK directly after load

* fix: default sdk service trying to set log level

* feat: wait for sdk to load in sdk service

* fix: add required disposable polyfills

* feat: update sdk version

* feat: replace rc-specific workaround with global polyfill

* fix: sdk service tests
This commit is contained in:
Andreas Coroiu 2025-02-26 09:08:42 +01:00 committed by GitHub
parent 44d50a70c2
commit ce5a5e3649
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 98 additions and 31 deletions

View File

@ -1,5 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import "core-js/proposals/explicit-resource-management";
import { filter, firstValueFrom, map, merge, Subject, timeout } from "rxjs"; import { filter, firstValueFrom, map, merge, Subject, timeout } from "rxjs";
import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common"; import { CollectionService, DefaultCollectionService } from "@bitwarden/admin-console/common";
@ -1290,7 +1292,7 @@ export default class MainBackground {
} }
this.containerService.attachToGlobal(self); this.containerService.attachToGlobal(self);
await this.sdkLoadService.load(); await this.sdkLoadService.loadAndInit();
// Only the "true" background should run migrations // Only the "true" background should run migrations
await this.stateService.init({ runMigrations: true }); await this.stateService.init({ runMigrations: true });

View File

@ -60,8 +60,10 @@ async function importModule(): Promise<GlobalWithWasmInit["initSdk"]> {
return (globalThis as GlobalWithWasmInit).initSdk; return (globalThis as GlobalWithWasmInit).initSdk;
} }
export class BrowserSdkLoadService implements SdkLoadService { export class BrowserSdkLoadService extends SdkLoadService {
constructor(readonly logService: LogService) {} constructor(readonly logService: LogService) {
super();
}
async load(): Promise<void> { async load(): Promise<void> {
const startTime = performance.now(); const startTime = performance.now();

View File

@ -1,2 +1,3 @@
import "core-js/stable"; import "core-js/stable";
import "core-js/proposals/explicit-resource-management";
import "zone.js"; import "zone.js";

View File

@ -32,7 +32,7 @@ export class InitService {
init() { init() {
return async () => { return async () => {
await this.sdkLoadService.load(); await this.sdkLoadService.loadAndInit();
await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations await this.stateService.init({ runMigrations: false }); // Browser background is responsible for migrations
await this.i18nService.init(); await this.i18nService.init();
this.twoFactorService.init(); this.twoFactorService.init();

View File

@ -1,3 +1,5 @@
import "core-js/proposals/explicit-resource-management";
import { program } from "commander"; import { program } from "commander";
import { OssServeConfigurator } from "./oss-serve-configurator"; import { OssServeConfigurator } from "./oss-serve-configurator";

View File

@ -1,7 +1,7 @@
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service"; import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import * as sdk from "@bitwarden/sdk-internal"; import * as sdk from "@bitwarden/sdk-internal";
export class CliSdkLoadService implements SdkLoadService { export class CliSdkLoadService extends SdkLoadService {
async load(): Promise<void> { async load(): Promise<void> {
const module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm"); const module = await import("@bitwarden/sdk-internal/bitwarden_wasm_internal_bg.wasm");
(sdk as any).init(module); (sdk as any).init(module);

View File

@ -867,7 +867,7 @@ export class ServiceContainer {
return; return;
} }
await this.sdkLoadService.load(); await this.sdkLoadService.loadAndInit();
await this.storageService.init(); await this.storageService.init();
await this.stateService.init(); await this.stateService.init();
this.containerService.attachToGlobal(global); this.containerService.attachToGlobal(global);

View File

@ -1,3 +1,5 @@
import "core-js/proposals/explicit-resource-management";
import { enableProdMode } from "@angular/core"; import { enableProdMode } from "@angular/core";
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic"; import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";

View File

@ -54,7 +54,7 @@ export class InitService {
init() { init() {
return async () => { return async () => {
await this.sdkLoadService.load(); await this.sdkLoadService.loadAndInit();
await this.sshAgentService.init(); await this.sshAgentService.init();
this.nativeMessagingService.init(); this.nativeMessagingService.init();
await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process

View File

@ -1,5 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import "core-js/proposals/explicit-resource-management";
import * as path from "path"; import * as path from "path";
import { app } from "electron"; import { app } from "electron";

View File

@ -42,7 +42,7 @@ export class InitService {
init() { init() {
return async () => { return async () => {
await this.sdkLoadService.load(); await this.sdkLoadService.loadAndInit();
await this.stateService.init(); await this.stateService.init();
const activeAccount = await firstValueFrom(this.accountService.activeAccount$); const activeAccount = await firstValueFrom(this.accountService.activeAccount$);

View File

@ -18,7 +18,7 @@ const supported = (() => {
return false; return false;
})(); })();
export class WebSdkLoadService implements SdkLoadService { export class WebSdkLoadService extends SdkLoadService {
async load(): Promise<void> { async load(): Promise<void> {
let module: any; let module: any;
if (supported) { if (supported) {

View File

@ -1,4 +1,5 @@
import "core-js/stable"; import "core-js/stable";
import "core-js/proposals/explicit-resource-management";
import "zone.js"; import "zone.js";
if (process.env.NODE_ENV === "production") { if (process.env.NODE_ENV === "production") {

View File

@ -1,3 +1,52 @@
export abstract class SdkLoadService { import { init_sdk } from "@bitwarden/sdk-internal";
abstract load(): Promise<void>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- used in docs
import type { SdkService } from "./sdk.service";
export class SdkLoadFailedError extends Error {
constructor(error: unknown) {
super(`SDK loading failed: ${error}`);
}
}
export abstract class SdkLoadService {
private static markAsReady: () => void;
private static markAsFailed: (error: unknown) => void;
/**
* This promise is resolved when the SDK is ready to be used. Use it when your code might run early and/or is not able to use DI.
* Beware that WASM always requires a load step which makes it tricky to use functions and classes directly, it is therefore recommended
* to use the SDK through the {@link SdkService}. Only use this promise in advanced scenarios!
*
* @example
* ```typescript
* import { pureFunction } from "@bitwarden/sdk-internal";
*
* async function myFunction() {
* await SdkLoadService.Ready;
* pureFunction();
* }
* ```
*/
static readonly Ready = new Promise<void>((resolve, reject) => {
SdkLoadService.markAsReady = resolve;
SdkLoadService.markAsFailed = (error: unknown) => reject(new SdkLoadFailedError(error));
});
/**
* Load WASM and initalize SDK-JS integrations such as logging.
* This method should be called once at the start of the application.
* Raw functions and classes from the SDK can be used after this method resolves.
*/
async loadAndInit(): Promise<void> {
try {
await this.load();
init_sdk();
SdkLoadService.markAsReady();
} catch (error) {
SdkLoadService.markAsFailed(error);
}
}
protected abstract load(): Promise<void>;
} }

View File

@ -1,11 +1,3 @@
// Temporary workaround for Symbol.dispose
// remove when https://github.com/jestjs/jest/issues/14874 is resolved and *released*
const disposeSymbol: unique symbol = Symbol("Symbol.dispose");
const asyncDisposeSymbol: unique symbol = Symbol("Symbol.asyncDispose");
(Symbol as any).asyncDispose ??= asyncDisposeSymbol as unknown as SymbolConstructor["asyncDispose"];
(Symbol as any).dispose ??= disposeSymbol as unknown as SymbolConstructor["dispose"];
// Import needs to be after the workaround
import { Rc } from "./rc"; import { Rc } from "./rc";
export class FreeableTestValue { export class FreeableTestValue {

View File

@ -8,7 +8,7 @@ import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service";
* *
* **Warning**: This requires WASM support and will fail if the environment does not support it. * **Warning**: This requires WASM support and will fail if the environment does not support it.
*/ */
export class DefaultSdkLoadService implements SdkLoadService { export class DefaultSdkLoadService extends SdkLoadService {
async load(): Promise<void> { async load(): Promise<void> {
(sdk as any).init(bitwardenModule); (sdk as any).init(bitwardenModule);
} }

View File

@ -11,6 +11,7 @@ import { UserKey } from "../../../types/key";
import { Environment, EnvironmentService } from "../../abstractions/environment.service"; import { Environment, EnvironmentService } from "../../abstractions/environment.service";
import { PlatformUtilsService } from "../../abstractions/platform-utils.service"; import { PlatformUtilsService } from "../../abstractions/platform-utils.service";
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service";
import { UserNotLoggedInError } from "../../abstractions/sdk/sdk.service"; import { UserNotLoggedInError } from "../../abstractions/sdk/sdk.service";
import { Rc } from "../../misc/reference-counting/rc"; import { Rc } from "../../misc/reference-counting/rc";
import { EncryptedString } from "../../models/domain/enc-string"; import { EncryptedString } from "../../models/domain/enc-string";
@ -18,6 +19,13 @@ import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
import { DefaultSdkService } from "./default-sdk.service"; import { DefaultSdkService } from "./default-sdk.service";
class TestSdkLoadService extends SdkLoadService {
protected override load(): Promise<void> {
// Simulate successfull WASM load
return Promise.resolve();
}
}
describe("DefaultSdkService", () => { describe("DefaultSdkService", () => {
describe("userClient$", () => { describe("userClient$", () => {
let sdkClientFactory!: MockProxy<SdkClientFactory>; let sdkClientFactory!: MockProxy<SdkClientFactory>;
@ -28,7 +36,9 @@ describe("DefaultSdkService", () => {
let keyService!: MockProxy<KeyService>; let keyService!: MockProxy<KeyService>;
let service!: DefaultSdkService; let service!: DefaultSdkService;
beforeEach(() => { beforeEach(async () => {
await new TestSdkLoadService().loadAndInit();
sdkClientFactory = mock<SdkClientFactory>(); sdkClientFactory = mock<SdkClientFactory>();
environmentService = mock<EnvironmentService>(); environmentService = mock<EnvironmentService>();
platformUtilsService = mock<PlatformUtilsService>(); platformUtilsService = mock<PlatformUtilsService>();

View File

@ -18,7 +18,6 @@ import { KeyService, KdfConfigService, KdfConfig, KdfType } from "@bitwarden/key
import { import {
BitwardenClient, BitwardenClient,
ClientSettings, ClientSettings,
LogLevel,
DeviceType as SdkDeviceType, DeviceType as SdkDeviceType,
} from "@bitwarden/sdk-internal"; } from "@bitwarden/sdk-internal";
@ -30,6 +29,7 @@ import { UserKey } from "../../../types/key";
import { Environment, EnvironmentService } from "../../abstractions/environment.service"; import { Environment, EnvironmentService } from "../../abstractions/environment.service";
import { PlatformUtilsService } from "../../abstractions/platform-utils.service"; import { PlatformUtilsService } from "../../abstractions/platform-utils.service";
import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory"; import { SdkClientFactory } from "../../abstractions/sdk/sdk-client-factory";
import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service";
import { SdkService, UserNotLoggedInError } from "../../abstractions/sdk/sdk.service"; import { SdkService, UserNotLoggedInError } from "../../abstractions/sdk/sdk.service";
import { compareValues } from "../../misc/compare-values"; import { compareValues } from "../../misc/compare-values";
import { Rc } from "../../misc/reference-counting/rc"; import { Rc } from "../../misc/reference-counting/rc";
@ -47,8 +47,9 @@ export class DefaultSdkService implements SdkService {
client$ = this.environmentService.environment$.pipe( client$ = this.environmentService.environment$.pipe(
concatMap(async (env) => { concatMap(async (env) => {
await SdkLoadService.Ready;
const settings = this.toSettings(env); const settings = this.toSettings(env);
return await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info); return await this.sdkClientFactory.createSdkClient(settings);
}), }),
shareReplay({ refCount: true, bufferSize: 1 }), shareReplay({ refCount: true, bufferSize: 1 }),
); );
@ -135,6 +136,7 @@ export class DefaultSdkService implements SdkService {
privateKey$, privateKey$,
userKey$, userKey$,
orgKeys$, orgKeys$,
SdkLoadService.Ready, // Makes sure we wait (once) for the SDK to be loaded
]).pipe( ]).pipe(
// switchMap is required to allow the clean-up logic to be executed when `combineLatest` emits a new value. // switchMap is required to allow the clean-up logic to be executed when `combineLatest` emits a new value.
switchMap(([env, account, kdfParams, privateKey, userKey, orgKeys]) => { switchMap(([env, account, kdfParams, privateKey, userKey, orgKeys]) => {
@ -146,7 +148,7 @@ export class DefaultSdkService implements SdkService {
} }
const settings = this.toSettings(env); const settings = this.toSettings(env);
const client = await this.sdkClientFactory.createSdkClient(settings, LogLevel.Info); const client = await this.sdkClientFactory.createSdkClient(settings);
await this.initializeClient(client, account, kdfParams, privateKey, userKey, orgKeys); await this.initializeClient(client, account, kdfParams, privateKey, userKey, orgKeys);

View File

@ -2,6 +2,6 @@ import { SdkLoadService } from "../../abstractions/sdk/sdk-load.service";
export class NoopSdkLoadService extends SdkLoadService { export class NoopSdkLoadService extends SdkLoadService {
async load() { async load() {
return; throw new Error("SDK not available in this environment");
} }
} }

View File

@ -1,3 +1,5 @@
import "core-js/proposals/explicit-resource-management";
import { webcrypto } from "crypto"; import { webcrypto } from "crypto";
import { addCustomMatchers } from "./spec"; import { addCustomMatchers } from "./spec";

8
package-lock.json generated
View File

@ -24,7 +24,7 @@
"@angular/platform-browser": "18.2.13", "@angular/platform-browser": "18.2.13",
"@angular/platform-browser-dynamic": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13",
"@angular/router": "18.2.13", "@angular/router": "18.2.13",
"@bitwarden/sdk-internal": "0.2.0-main.105", "@bitwarden/sdk-internal": "0.2.0-main.107",
"@electron/fuses": "1.8.0", "@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5", "@emotion/css": "11.13.5",
"@koa/multer": "3.0.2", "@koa/multer": "3.0.2",
@ -4651,9 +4651,9 @@
"link": true "link": true
}, },
"node_modules/@bitwarden/sdk-internal": { "node_modules/@bitwarden/sdk-internal": {
"version": "0.2.0-main.105", "version": "0.2.0-main.107",
"resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.105.tgz", "resolved": "https://registry.npmjs.org/@bitwarden/sdk-internal/-/sdk-internal-0.2.0-main.107.tgz",
"integrity": "sha512-MaQFJbuKTCbN9oZC/+opYVeegaNNJpiUv9/zx+gu8KxWmX0hyEkNPtHKxBjDt3kLLz69CudDtUxEgqOfcDsYAw==", "integrity": "sha512-xpOF6NAS0/em3jFBv4FI1ASy1Nuc7I1v41TVmG56wS+80y+NH1RnfGjp+a+XiO7Xxh3jssrxmjzihJjWQQA0rg==",
"license": "GPL-3.0" "license": "GPL-3.0"
}, },
"node_modules/@bitwarden/send-ui": { "node_modules/@bitwarden/send-ui": {

View File

@ -154,7 +154,7 @@
"@angular/platform-browser": "18.2.13", "@angular/platform-browser": "18.2.13",
"@angular/platform-browser-dynamic": "18.2.13", "@angular/platform-browser-dynamic": "18.2.13",
"@angular/router": "18.2.13", "@angular/router": "18.2.13",
"@bitwarden/sdk-internal": "0.2.0-main.105", "@bitwarden/sdk-internal": "0.2.0-main.107",
"@electron/fuses": "1.8.0", "@electron/fuses": "1.8.0",
"@emotion/css": "11.13.5", "@emotion/css": "11.13.5",
"@koa/multer": "3.0.2", "@koa/multer": "3.0.2",

View File

@ -6,7 +6,7 @@
"noImplicitAny": true, "noImplicitAny": true,
"target": "ES2016", "target": "ES2016",
"module": "ES2020", "module": "ES2020",
"lib": ["es5", "es6", "es7", "dom", "ES2021"], "lib": ["es5", "es6", "es7", "dom", "ES2021", "ESNext.Disposable"],
"sourceMap": true, "sourceMap": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"experimentalDecorators": true, "experimentalDecorators": true,