1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-06 00:13:28 +00:00

Resolve state <-> state-test-utils circular dependency (#16093)

* Resolve state <-> state-test-utils circular dependency

* Fix type errors
This commit is contained in:
Justin Baur
2025-08-25 12:38:28 -04:00
committed by GitHub
parent 777b92660a
commit 5f7f1d1924
90 changed files with 543 additions and 500 deletions

1
.github/CODEOWNERS vendored
View File

@@ -101,6 +101,7 @@ libs/guid @bitwarden/team-platform-dev
libs/client-type @bitwarden/team-platform-dev
libs/core-test-utils @bitwarden/team-platform-dev
libs/state @bitwarden/team-platform-dev
libs/state-internal @bitwarden/team-platform-dev
libs/state-test-utils @bitwarden/team-platform-dev
# Web utils used across app and connectors
apps/web/src/utils/ @bitwarden/team-platform-dev

View File

@@ -143,23 +143,6 @@ import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/defau
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
import { SystemService } from "@bitwarden/common/platform/services/system.service";
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
import {
ActiveUserStateProvider,
DefaultStateService,
DerivedStateProvider,
GlobalStateProvider,
SingleUserStateProvider,
StateEventRunnerService,
StateProvider,
} from "@bitwarden/common/platform/state";
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/inline-derived-state";
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
/* eslint-enable import/no-restricted-paths */
import { PrimarySecondaryStorageService } from "@bitwarden/common/platform/storage/primary-secondary-storage.service";
import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service";
import { SyncService } from "@bitwarden/common/platform/sync";
@@ -232,6 +215,24 @@ import {
KeyService as KeyServiceAbstraction,
} from "@bitwarden/key-management";
import { BackgroundSyncService } from "@bitwarden/platform/background-sync";
import {
ActiveUserStateProvider,
DerivedStateProvider,
GlobalStateProvider,
SingleUserStateProvider,
StateEventRunnerService,
StateProvider,
} from "@bitwarden/state";
import {
DefaultActiveUserStateProvider,
DefaultGlobalStateProvider,
DefaultSingleUserStateProvider,
DefaultStateEventRegistrarService,
DefaultStateEventRunnerService,
DefaultStateProvider,
DefaultStateService,
InlineDerivedStateProvider,
} from "@bitwarden/state-internal";
import {
IndividualVaultExportService,
IndividualVaultExportServiceAbstraction,
@@ -569,12 +570,12 @@ export default class MainBackground {
this.logService,
);
const stateEventRegistrarService = new StateEventRegistrarService(
const stateEventRegistrarService = new DefaultStateEventRegistrarService(
this.globalStateProvider,
storageServiceProvider,
);
this.stateEventRunnerService = new StateEventRunnerService(
this.stateEventRunnerService = new DefaultStateEventRunnerService(
this.globalStateProvider,
storageServiceProvider,
);

View File

@@ -1,14 +1,14 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage specifically for browser backgrounds
import { MemoryStorageService } from "@bitwarden/common/platform/state/storage/memory-storage.service";
import { SerializedMemoryStorageService } from "@bitwarden/storage-core";
import { BrowserApi } from "../browser/browser-api";
import { MemoryStoragePortMessage } from "./port-messages";
import { portName } from "./port-name";
export class BackgroundMemoryStorageService extends MemoryStorageService {
export class BackgroundMemoryStorageService extends SerializedMemoryStorageService {
private _ports: chrome.runtime.Port[] = [];
constructor() {

View File

@@ -1,13 +1,10 @@
import {
AbstractStorageService,
ClientLocations,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import {
PossibleLocation,
StorageServiceProvider,
} from "@bitwarden/common/platform/services/storage-service.provider";
// eslint-disable-next-line import/no-restricted-paths
import { ClientLocations } from "@bitwarden/common/platform/state/state-definition";
} from "@bitwarden/storage-core";
export class BrowserStorageServiceProvider extends StorageServiceProvider {
constructor(

View File

@@ -104,13 +104,6 @@ import { ContainerService } from "@bitwarden/common/platform/services/container.
import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory";
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import {
DerivedStateProvider,
GlobalStateProvider,
StateProvider,
} from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- Used for dependency injection
import { InlineDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/inline-derived-state";
import { PrimarySecondaryStorageService } from "@bitwarden/common/platform/storage/primary-secondary-storage.service";
import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service";
import { SyncService } from "@bitwarden/common/platform/sync";
@@ -140,6 +133,8 @@ import {
KeyService,
} from "@bitwarden/key-management";
import { LockComponentService } from "@bitwarden/key-management-ui";
import { DerivedStateProvider, GlobalStateProvider, StateProvider } from "@bitwarden/state";
import { InlineDerivedStateProvider } from "@bitwarden/state-internal";
import {
DefaultSshImportPromptService,
PasswordRepromptService,

View File

@@ -107,25 +107,6 @@ import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/defau
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
import {
ActiveUserStateProvider,
DefaultStateService,
DerivedStateProvider,
GlobalStateProvider,
SingleUserStateProvider,
StateEventRunnerService,
StateProvider,
StateService,
} from "@bitwarden/common/platform/state";
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally these should not be accessed */
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
/* eslint-enable import/no-restricted-paths */
import { SyncService } from "@bitwarden/common/platform/sync";
// eslint-disable-next-line no-restricted-imports -- Needed for service construction
import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal";
@@ -172,6 +153,26 @@ import {
DefaultBiometricStateService,
} from "@bitwarden/key-management";
import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service";
import {
ActiveUserStateProvider,
DerivedStateProvider,
GlobalStateProvider,
SingleUserStateProvider,
StateEventRunnerService,
StateProvider,
StateService,
} from "@bitwarden/state";
import {
DefaultActiveUserStateProvider,
DefaultDerivedStateProvider,
DefaultGlobalStateProvider,
DefaultSingleUserStateProvider,
DefaultStateEventRegistrarService,
DefaultStateEventRunnerService,
DefaultStateProvider,
DefaultStateService,
} from "@bitwarden/state-internal";
import { SerializedMemoryStorageService } from "@bitwarden/storage-core";
import {
IndividualVaultExportService,
IndividualVaultExportServiceAbstraction,
@@ -209,7 +210,7 @@ export class ServiceContainer {
storageService: LowdbStorageService;
secureStorageService: NodeEnvSecureStorageService;
memoryStorageService: MemoryStorageService;
memoryStorageForStateProviders: MemoryStorageServiceForStateProviders;
memoryStorageForStateProviders: SerializedMemoryStorageService;
migrationRunner: MigrationRunner;
i18nService: I18nService;
platformUtilsService: CliPlatformUtilsService;
@@ -339,7 +340,7 @@ export class ServiceContainer {
);
this.memoryStorageService = new MemoryStorageService();
this.memoryStorageForStateProviders = new MemoryStorageServiceForStateProviders();
this.memoryStorageForStateProviders = new SerializedMemoryStorageService();
const storageServiceProvider = new StorageServiceProvider(
this.storageService,
@@ -351,12 +352,12 @@ export class ServiceContainer {
this.logService,
);
const stateEventRegistrarService = new StateEventRegistrarService(
const stateEventRegistrarService = new DefaultStateEventRegistrarService(
this.globalStateProvider,
storageServiceProvider,
);
this.stateEventRunnerService = new StateEventRunnerService(
this.stateEventRunnerService = new DefaultStateEventRunnerService(
this.globalStateProvider,
storageServiceProvider,
);

View File

@@ -91,8 +91,6 @@ import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/no
import { NoopSdkLoadService } from "@bitwarden/common/platform/services/sdk/noop-sdk-load.service";
import { SystemService } from "@bitwarden/common/platform/services/system.service";
import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DialogService, ToastService } from "@bitwarden/components";
@@ -105,6 +103,7 @@ import {
BiometricsService,
} from "@bitwarden/key-management";
import { LockComponentService } from "@bitwarden/key-management-ui";
import { SerializedMemoryStorageService } from "@bitwarden/storage-core";
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
import { DesktopLoginApprovalDialogComponentService } from "../../auth/login/desktop-login-approval-dialog-component.service";
@@ -234,7 +233,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({ provide: MEMORY_STORAGE, useClass: MemoryStorageService, deps: [] }),
safeProvider({
provide: OBSERVABLE_MEMORY_STORAGE,
useClass: MemoryStorageServiceForStateProviders,
useClass: SerializedMemoryStorageService,
deps: [],
}),
safeProvider({ provide: OBSERVABLE_DISK_STORAGE, useExisting: AbstractStorageService }),

View File

@@ -21,18 +21,17 @@ import { DefaultEnvironmentService } from "@bitwarden/common/platform/services/d
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
import { MigrationBuilderService } from "@bitwarden/common/platform/services/migration-builder.service";
import { MigrationRunner } from "@bitwarden/common/platform/services/migration-runner";
/* eslint-disable import/no-restricted-paths -- We need the implementation to inject, but generally this should not be accessed */
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
/* eslint-enable import/no-restricted-paths */
import { DefaultBiometricStateService } from "@bitwarden/key-management";
import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service";
import {
DefaultActiveUserStateProvider,
DefaultDerivedStateProvider,
DefaultGlobalStateProvider,
DefaultSingleUserStateProvider,
DefaultStateEventRegistrarService,
DefaultStateProvider,
} from "@bitwarden/state-internal";
import { SerializedMemoryStorageService, StorageServiceProvider } from "@bitwarden/storage-core";
import { MainDesktopAutotypeService } from "./autofill/main/main-desktop-autotype.service";
import { MainSshAgentService } from "./autofill/main/main-ssh-agent.service";
@@ -66,7 +65,7 @@ export class Main {
i18nService: I18nMainService;
storageService: ElectronStorageService;
memoryStorageService: MemoryStorageService;
memoryStorageForStateProviders: MemoryStorageServiceForStateProviders;
memoryStorageForStateProviders: SerializedMemoryStorageService;
messagingService: MessageSender;
environmentService: DefaultEnvironmentService;
desktopCredentialStorageListener: DesktopCredentialStorageListener;
@@ -134,7 +133,7 @@ export class Main {
const storageDefaults: any = {};
this.storageService = new ElectronStorageService(app.getPath("userData"), storageDefaults);
this.memoryStorageService = new MemoryStorageService();
this.memoryStorageForStateProviders = new MemoryStorageServiceForStateProviders();
this.memoryStorageForStateProviders = new SerializedMemoryStorageService();
const storageServiceProvider = new StorageServiceProvider(
this.storageService,
this.memoryStorageForStateProviders,
@@ -150,7 +149,7 @@ export class Main {
this.mainCryptoFunctionService = new NodeCryptoFunctionService();
const stateEventRegistrarService = new StateEventRegistrarService(
const stateEventRegistrarService = new DefaultStateEventRegistrarService(
globalStateProvider,
storageServiceProvider,
);

View File

@@ -93,10 +93,7 @@ import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
import { NoopSdkLoadService } from "@bitwarden/common/platform/services/sdk/noop-sdk-load.service";
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
/* eslint-disable import/no-restricted-paths -- Implementation for memory storage */
import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state";
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
/* eslint-enable import/no-restricted-paths -- Implementation for memory storage */
import { WindowStorageService } from "@bitwarden/common/platform/storage/window-storage.service";
import {
DefaultThemeStateService,
@@ -110,6 +107,7 @@ import {
BiometricsService,
} from "@bitwarden/key-management";
import { LockComponentService } from "@bitwarden/key-management-ui";
import { SerializedMemoryStorageService } from "@bitwarden/storage-core";
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
import { WebOrganizationInviteService } from "@bitwarden/web-vault/app/auth/core/services/organization-invite/web-organization-invite.service";
@@ -186,7 +184,7 @@ const safeProviders: SafeProvider[] = [
}),
safeProvider({
provide: OBSERVABLE_MEMORY_STORAGE,
useClass: MemoryStorageServiceForStateProviders,
useClass: SerializedMemoryStorageService,
deps: [],
}),
safeProvider({

View File

@@ -2,14 +2,11 @@ import { mock } from "jest-mock-extended";
import {
AbstractStorageService,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import { PossibleLocation } from "@bitwarden/common/platform/services/storage-service.provider";
import {
ClientLocations,
ObservableStorageService,
PossibleLocation,
StorageLocation,
// eslint-disable-next-line import/no-restricted-paths
} from "@bitwarden/common/platform/state/state-definition";
} from "@bitwarden/storage-core";
import { WebStorageServiceProvider } from "./web-storage-service.provider";

View File

@@ -1,15 +1,10 @@
import {
AbstractStorageService,
ClientLocations,
ObservableStorageService,
} from "@bitwarden/common/platform/abstractions/storage.service";
import {
PossibleLocation,
StorageServiceProvider,
} from "@bitwarden/common/platform/services/storage-service.provider";
import {
ClientLocations,
// eslint-disable-next-line import/no-restricted-paths
} from "@bitwarden/common/platform/state/state-definition";
} from "@bitwarden/storage-core";
export class WebStorageServiceProvider extends StorageServiceProvider {
constructor(

View File

@@ -230,24 +230,6 @@ import { DefaultSdkService } from "@bitwarden/common/platform/services/sdk/defau
import { StorageServiceProvider } from "@bitwarden/common/platform/services/storage-service.provider";
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
import { ValidationService } from "@bitwarden/common/platform/services/validation.service";
import {
ActiveUserAccessor,
ActiveUserStateProvider,
DefaultStateService,
DerivedStateProvider,
GlobalStateProvider,
SingleUserStateProvider,
StateProvider,
} from "@bitwarden/common/platform/state";
/* eslint-disable import/no-restricted-paths -- We need the implementations to inject, but generally these should not be accessed */
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
import { DefaultDerivedStateProvider } from "@bitwarden/common/platform/state/implementations/default-derived-state.provider";
import { DefaultGlobalStateProvider } from "@bitwarden/common/platform/state/implementations/default-global-state.provider";
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
import { DefaultStateProvider } from "@bitwarden/common/platform/state/implementations/default-state.provider";
import { StateEventRegistrarService } from "@bitwarden/common/platform/state/state-event-registrar.service";
import { StateEventRunnerService } from "@bitwarden/common/platform/state/state-event-runner.service";
/* eslint-enable import/no-restricted-paths */
import { SyncService } from "@bitwarden/common/platform/sync";
// eslint-disable-next-line no-restricted-imports -- Needed for DI
import { DefaultSyncService } from "@bitwarden/common/platform/sync/internal";
@@ -329,6 +311,26 @@ import {
UserAsymmetricKeysRegenerationApiService,
UserAsymmetricKeysRegenerationService,
} from "@bitwarden/key-management";
import {
ActiveUserStateProvider,
DerivedStateProvider,
GlobalStateProvider,
SingleUserStateProvider,
StateEventRegistrarService,
StateEventRunnerService,
StateProvider,
} from "@bitwarden/state";
import {
ActiveUserAccessor,
DefaultActiveUserStateProvider,
DefaultDerivedStateProvider,
DefaultGlobalStateProvider,
DefaultSingleUserStateProvider,
DefaultStateEventRegistrarService,
DefaultStateEventRunnerService,
DefaultStateProvider,
DefaultStateService,
} from "@bitwarden/state-internal";
import { SafeInjectionToken } from "@bitwarden/ui-common";
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
// eslint-disable-next-line no-restricted-imports
@@ -1246,12 +1248,12 @@ const safeProviders: SafeProvider[] = [
}),
safeProvider({
provide: StateEventRegistrarService,
useClass: StateEventRegistrarService,
useClass: DefaultStateEventRegistrarService,
deps: [GlobalStateProvider, StorageServiceProvider],
}),
safeProvider({
provide: StateEventRunnerService,
useClass: StateEventRunnerService,
useClass: DefaultStateEventRunnerService,
deps: [GlobalStateProvider, StorageServiceProvider],
}),
safeProvider({

View File

@@ -1 +0,0 @@
export { DeriveDefinition } from "@bitwarden/state";

View File

@@ -1 +0,0 @@
export { DerivedStateProvider } from "@bitwarden/state";

View File

@@ -1 +0,0 @@
export { DerivedState } from "@bitwarden/state";

View File

@@ -1 +0,0 @@
export { GlobalStateProvider } from "@bitwarden/state";

View File

@@ -1 +0,0 @@
export { GlobalState } from "@bitwarden/state";

View File

@@ -1,36 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable, distinctUntilChanged } from "rxjs";
import { UserId } from "../../../types/guid";
import { ActiveUserAccessor } from "../active-user.accessor";
import { UserKeyDefinition } from "../user-key-definition";
import { ActiveUserState } from "../user-state";
import { ActiveUserStateProvider, SingleUserStateProvider } from "../user-state.provider";
import { DefaultActiveUserState } from "./default-active-user-state";
export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
activeUserId$: Observable<UserId | undefined>;
constructor(
private readonly activeAccountAccessor: ActiveUserAccessor,
private readonly singleUserStateProvider: SingleUserStateProvider,
) {
this.activeUserId$ = this.activeAccountAccessor.activeUserId$.pipe(
// To avoid going to storage when we don't need to, only get updates when there is a true change.
distinctUntilChanged((a, b) => (a == null || b == null ? a == b : a === b)), // Treat null and undefined as equal
);
}
get<T>(keyDefinition: UserKeyDefinition<T>): ActiveUserState<T> {
// All other providers cache the creation of their corresponding `State` objects, this instance
// doesn't need to do that since it calls `SingleUserStateProvider` it will go through their caching
// layer, because of that, the creation of this instance is quite simple and not worth caching.
return new DefaultActiveUserState(
keyDefinition,
this.activeUserId$,
this.singleUserStateProvider,
);
}
}

View File

@@ -1 +0,0 @@
export { DefaultActiveUserState } from "@bitwarden/state";

View File

@@ -1 +0,0 @@
export { DefaultDerivedStateProvider } from "@bitwarden/state";

View File

@@ -1 +0,0 @@
export { DefaultDerivedState } from "@bitwarden/state";

View File

@@ -1 +0,0 @@
export { DefaultGlobalStateProvider } from "@bitwarden/state";

View File

@@ -1 +0,0 @@
export { DefaultGlobalState } from "@bitwarden/state";

View File

@@ -1 +0,0 @@
export { DefaultSingleUserStateProvider } from "@bitwarden/state";

View File

@@ -1 +0,0 @@
export { DefaultSingleUserState } from "@bitwarden/state";

View File

@@ -1 +0,0 @@
export { DefaultStateProvider } from "@bitwarden/state";

View File

@@ -1 +0,0 @@
export { InlineDerivedState, InlineDerivedStateProvider } from "@bitwarden/state";

View File

@@ -1 +0,0 @@
export { StateBase } from "@bitwarden/state";

View File

@@ -1 +1,6 @@
import { StateUpdateOptions as RequiredStateUpdateOptions } from "@bitwarden/state";
export * from "@bitwarden/state";
export { ActiveUserAccessor } from "@bitwarden/state-internal";
export type StateUpdateOptions<T, TCombine> = Partial<RequiredStateUpdateOptions<T, TCombine>>;

View File

@@ -1 +0,0 @@
export { KeyDefinition, KeyDefinitionOptions } from "@bitwarden/state";

View File

@@ -1,4 +1 @@
export { StateDefinition } from "@bitwarden/state";
// To be removed once references are updated to point to @bitwarden/storage-core
export { StorageLocation, ClientLocations } from "@bitwarden/storage-core";

View File

@@ -1,6 +0,0 @@
export {
StateEventRegistrarService,
StateEventInfo,
STATE_LOCK_EVENT,
STATE_LOGOUT_EVENT,
} from "@bitwarden/state";

View File

@@ -1 +0,0 @@
export { StateEventRunnerService } from "@bitwarden/state";

View File

@@ -1 +0,0 @@
export { StateProvider } from "@bitwarden/state";

View File

@@ -1 +0,0 @@
export { SerializedMemoryStorageService as MemoryStorageService } from "@bitwarden/storage-core";

View File

@@ -1 +0,0 @@
export { UserKeyDefinition, UserKeyDefinitionOptions } from "@bitwarden/state";

View File

@@ -1 +0,0 @@
export { ActiveUserStateProvider, SingleUserStateProvider } from "@bitwarden/state";

View File

@@ -1 +0,0 @@
export { ActiveUserState, SingleUserState, CombinedState } from "@bitwarden/state";

View File

@@ -0,0 +1,5 @@
# state-internal
Owned by: platform
The internal parts of @bitwarden/state that should not be used by other teams.

View File

@@ -0,0 +1,3 @@
import baseConfig from "../../eslint.config.mjs";
export default [...baseConfig];

View File

@@ -0,0 +1,10 @@
module.exports = {
displayName: "state-internal",
preset: "../../jest.preset.js",
testEnvironment: "node",
transform: {
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
},
moduleFileExtensions: ["ts", "js", "html"],
coverageDirectory: "../../coverage/libs/state-internal",
};

View File

@@ -0,0 +1,11 @@
{
"name": "@bitwarden/state-internal",
"version": "0.0.1",
"description": "The internal parts of @bitwarden/state that should not be used by other teams.",
"private": true,
"type": "commonjs",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"license": "GPL-3.0",
"author": "platform"
}

View File

@@ -0,0 +1,33 @@
{
"name": "state-internal",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/state-internal/src",
"projectType": "library",
"tags": [],
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/state-internal",
"main": "libs/state-internal/src/index.ts",
"tsConfig": "libs/state-internal/tsconfig.lib.json",
"assets": ["libs/state-internal/*.md"]
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["libs/state-internal/**/*.ts"]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/state-internal/jest.config.js"
}
}
}
}

View File

@@ -5,6 +5,12 @@
import { Observable, of } from "rxjs";
import { awaitAsync, trackEmissions } from "@bitwarden/core-test-utils";
import {
DeriveDefinition,
KeyDefinition,
StateDefinition,
UserKeyDefinition,
} from "@bitwarden/state";
import {
FakeActiveUserAccessor,
FakeActiveUserStateProvider,
@@ -14,11 +20,6 @@ import {
} from "@bitwarden/state-test-utils";
import { UserId } from "@bitwarden/user-core";
import { DeriveDefinition } from "../derive-definition";
import { KeyDefinition } from "../key-definition";
import { StateDefinition } from "../state-definition";
import { UserKeyDefinition } from "../user-key-definition";
import { DefaultStateProvider } from "./default-state.provider";
describe("DefaultStateProvider", () => {

View File

@@ -2,13 +2,15 @@
// @ts-strict-ignore
import { Observable, distinctUntilChanged } from "rxjs";
import {
ActiveUserState,
ActiveUserStateProvider,
SingleUserStateProvider,
UserKeyDefinition,
} from "@bitwarden/state";
import { UserId } from "@bitwarden/user-core";
import { ActiveUserAccessor } from "../active-user.accessor";
import { UserKeyDefinition } from "../user-key-definition";
import { ActiveUserState } from "../user-state";
import { ActiveUserStateProvider, SingleUserStateProvider } from "../user-state.provider";
import { ActiveUserAccessor } from "./active-user.accessor";
import { DefaultActiveUserState } from "./default-active-user-state";
export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {

View File

@@ -8,14 +8,11 @@ import { Jsonify } from "type-fest";
import { awaitAsync, trackEmissions } from "@bitwarden/core-test-utils";
import { LogService } from "@bitwarden/logging";
import { StateDefinition, StateEventRegistrarService, UserKeyDefinition } from "@bitwarden/state";
import { StorageServiceProvider } from "@bitwarden/storage-core";
import { FakeStorageService } from "@bitwarden/storage-test-utils";
import { UserId } from "@bitwarden/user-core";
import { StateDefinition } from "../state-definition";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { UserKeyDefinition } from "../user-key-definition";
import { DefaultActiveUserState } from "./default-active-user-state";
import { DefaultSingleUserStateProvider } from "./default-single-user-state.provider";

View File

@@ -2,13 +2,16 @@
// @ts-strict-ignore
import { Observable, map, switchMap, firstValueFrom, timeout, throwError, NEVER } from "rxjs";
import {
activeMarker,
ActiveUserState,
CombinedState,
SingleUserStateProvider,
StateUpdateOptions,
UserKeyDefinition,
} from "@bitwarden/state";
import { UserId } from "@bitwarden/user-core";
import { StateUpdateOptions } from "../state-update-options";
import { UserKeyDefinition } from "../user-key-definition";
import { ActiveUserState, CombinedState, activeMarker } from "../user-state";
import { SingleUserStateProvider } from "../user-state.provider";
export class DefaultActiveUserState<T> implements ActiveUserState<T> {
[activeMarker]: true;
combinedState$: Observable<CombinedState<T>>;
@@ -33,7 +36,7 @@ export class DefaultActiveUserState<T> implements ActiveUserState<T> {
async update<TCombine>(
configureState: (state: T, dependency: TCombine) => T,
options: StateUpdateOptions<T, TCombine> = {},
options: Partial<StateUpdateOptions<T, TCombine>> = {},
): Promise<[UserId, T]> {
const userId = await firstValueFrom(
this.activeUserId$.pipe(

View File

@@ -1,9 +1,11 @@
import { Observable } from "rxjs";
import { DerivedStateDependencies } from "../../types/state";
import { DeriveDefinition } from "../derive-definition";
import { DerivedState } from "../derived-state";
import { DerivedStateProvider } from "../derived-state.provider";
import {
DeriveDefinition,
DerivedState,
DerivedStateDependencies,
DerivedStateProvider,
} from "@bitwarden/state";
import { DefaultDerivedState } from "./default-derived-state";

View File

@@ -5,9 +5,7 @@
import { Subject, firstValueFrom } from "rxjs";
import { awaitAsync, trackEmissions } from "@bitwarden/core-test-utils";
import { DeriveDefinition } from "../derive-definition";
import { StateDefinition } from "../state-definition";
import { DeriveDefinition, StateDefinition } from "@bitwarden/state";
import { DefaultDerivedState } from "./default-derived-state";
import { DefaultDerivedStateProvider } from "./default-derived-state.provider";

View File

@@ -1,8 +1,6 @@
import { Observable, ReplaySubject, Subject, concatMap, merge, share, timer } from "rxjs";
import { DerivedStateDependencies } from "../../types/state";
import { DeriveDefinition } from "../derive-definition";
import { DerivedState } from "../derived-state";
import { DeriveDefinition, DerivedState, DerivedStateDependencies } from "@bitwarden/state";
/**
* Default derived state

View File

@@ -1,12 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { LogService } from "@bitwarden/logging";
import { GlobalState, GlobalStateProvider, KeyDefinition } from "@bitwarden/state";
import { StorageServiceProvider } from "@bitwarden/storage-core";
import { GlobalState } from "../global-state";
import { GlobalStateProvider } from "../global-state.provider";
import { KeyDefinition } from "../key-definition";
import { DefaultGlobalState } from "./default-global-state";
export class DefaultGlobalStateProvider implements GlobalStateProvider {

View File

@@ -9,12 +9,11 @@ import { Jsonify } from "type-fest";
import { trackEmissions, awaitAsync } from "@bitwarden/core-test-utils";
import { LogService } from "@bitwarden/logging";
import { KeyDefinition, StateDefinition } from "@bitwarden/state";
import { FakeStorageService } from "@bitwarden/storage-test-utils";
import { KeyDefinition, globalKeyBuilder } from "../key-definition";
import { StateDefinition } from "../state-definition";
import { DefaultGlobalState } from "./default-global-state";
import { globalKeyBuilder } from "./util";
class TestState {
date: Date;

View File

@@ -1,10 +1,9 @@
import { LogService } from "@bitwarden/logging";
import { GlobalState, KeyDefinition } from "@bitwarden/state";
import { AbstractStorageService, ObservableStorageService } from "@bitwarden/storage-core";
import { GlobalState } from "../global-state";
import { KeyDefinition, globalKeyBuilder } from "../key-definition";
import { StateBase } from "./state-base";
import { globalKeyBuilder } from "./util";
export class DefaultGlobalState<T>
extends StateBase<T, KeyDefinition<T>>

View File

@@ -1,14 +1,15 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { LogService } from "@bitwarden/logging";
import {
SingleUserState,
SingleUserStateProvider,
StateEventRegistrarService,
UserKeyDefinition,
} from "@bitwarden/state";
import { StorageServiceProvider } from "@bitwarden/storage-core";
import { UserId } from "@bitwarden/user-core";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { UserKeyDefinition } from "../user-key-definition";
import { SingleUserState } from "../user-state";
import { SingleUserStateProvider } from "../user-state.provider";
import { DefaultSingleUserState } from "./default-single-user-state";
export class DefaultSingleUserStateProvider implements SingleUserStateProvider {

View File

@@ -10,13 +10,10 @@ import { Jsonify } from "type-fest";
import { trackEmissions, awaitAsync } from "@bitwarden/core-test-utils";
import { newGuid } from "@bitwarden/guid";
import { LogService } from "@bitwarden/logging";
import { StateDefinition, StateEventRegistrarService, UserKeyDefinition } from "@bitwarden/state";
import { FakeStorageService } from "@bitwarden/storage-test-utils";
import { UserId } from "@bitwarden/user-core";
import { StateDefinition } from "../state-definition";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { UserKeyDefinition } from "../user-key-definition";
import { DefaultSingleUserState } from "./default-single-user-state";
class TestState {

View File

@@ -1,13 +1,15 @@
import { Observable, combineLatest, of } from "rxjs";
import { LogService } from "@bitwarden/logging";
import {
CombinedState,
SingleUserState,
StateEventRegistrarService,
UserKeyDefinition,
} from "@bitwarden/state";
import { AbstractStorageService, ObservableStorageService } from "@bitwarden/storage-core";
import { UserId } from "@bitwarden/user-core";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { UserKeyDefinition } from "../user-key-definition";
import { CombinedState, SingleUserState } from "../user-state";
import { StateBase } from "./state-base";
export class DefaultSingleUserState<T>

View File

@@ -1,5 +1,6 @@
import { mock } from "jest-mock-extended";
import { StateDefinition, UserKeyDefinition } from "@bitwarden/state";
import { FakeGlobalStateProvider } from "@bitwarden/state-test-utils";
import {
AbstractStorageService,
@@ -7,16 +8,17 @@ import {
StorageServiceProvider,
} from "@bitwarden/storage-core";
import { StateDefinition } from "./state-definition";
import { STATE_LOCK_EVENT, StateEventRegistrarService } from "./state-event-registrar.service";
import { UserKeyDefinition } from "./user-key-definition";
import {
DefaultStateEventRegistrarService,
STATE_LOCK_EVENT,
} from "./default-state-event-registrar.service";
describe("StateEventRegistrarService", () => {
const globalStateProvider = new FakeGlobalStateProvider();
const lockState = globalStateProvider.getFake(STATE_LOCK_EVENT);
const storageServiceProvider = mock<StorageServiceProvider>();
const sut = new StateEventRegistrarService(globalStateProvider, storageServiceProvider);
const sut = new DefaultStateEventRegistrarService(globalStateProvider, storageServiceProvider);
describe("registerEvents", () => {
const fakeKeyDefinition = new UserKeyDefinition<boolean>(

View File

@@ -0,0 +1,78 @@
import {
CLEAR_EVENT_DISK,
ClearEvent,
GlobalState,
GlobalStateProvider,
KeyDefinition,
UserKeyDefinition,
} from "@bitwarden/state";
import { PossibleLocation, StorageServiceProvider } from "@bitwarden/storage-core";
export type StateEventInfo = {
state: string;
key: string;
location: PossibleLocation;
};
export const STATE_LOCK_EVENT = KeyDefinition.array<StateEventInfo>(CLEAR_EVENT_DISK, "lock", {
deserializer: (e) => e,
});
export const STATE_LOGOUT_EVENT = KeyDefinition.array<StateEventInfo>(CLEAR_EVENT_DISK, "logout", {
deserializer: (e) => e,
});
export class DefaultStateEventRegistrarService {
private readonly stateEventStateMap: { [Prop in ClearEvent]: GlobalState<StateEventInfo[]> };
constructor(
globalStateProvider: GlobalStateProvider,
private storageServiceProvider: StorageServiceProvider,
) {
this.stateEventStateMap = {
lock: globalStateProvider.get(STATE_LOCK_EVENT),
logout: globalStateProvider.get(STATE_LOGOUT_EVENT),
};
}
async registerEvents(keyDefinition: UserKeyDefinition<unknown>) {
for (const clearEvent of keyDefinition.clearOn) {
const eventState = this.stateEventStateMap[clearEvent];
// Determine the storage location for this
const [storageLocation] = this.storageServiceProvider.get(
keyDefinition.stateDefinition.defaultStorageLocation,
keyDefinition.stateDefinition.storageLocationOverrides,
);
const newEvent: StateEventInfo = {
state: keyDefinition.stateDefinition.name,
key: keyDefinition.key,
location: storageLocation,
};
// Only update the event state if the existing list doesn't have a matching entry
await eventState.update(
(existingTickets) => {
existingTickets ??= [];
existingTickets.push(newEvent);
return existingTickets;
},
{
shouldUpdate: (currentTickets) => {
return (
// If the current tickets are null, then it will for sure be added
currentTickets == null ||
// If an existing match couldn't be found, we also need to add one
currentTickets.findIndex(
(e) =>
e.state === newEvent.state &&
e.key === newEvent.key &&
e.location === newEvent.location,
) === -1
);
},
},
);
}
}
}

View File

@@ -8,16 +8,16 @@ import {
} from "@bitwarden/storage-core";
import { UserId } from "@bitwarden/user-core";
import { STATE_LOCK_EVENT } from "./state-event-registrar.service";
import { StateEventRunnerService } from "./state-event-runner.service";
import { STATE_LOCK_EVENT } from "./default-state-event-registrar.service";
import { DefaultStateEventRunnerService } from "./default-state-event-runner.service";
describe("EventRunnerService", () => {
describe("DefaultStateEventRunnerService", () => {
const fakeGlobalStateProvider = new FakeGlobalStateProvider();
const lockState = fakeGlobalStateProvider.getFake(STATE_LOCK_EVENT);
const storageServiceProvider = mock<StorageServiceProvider>();
const sut = new StateEventRunnerService(fakeGlobalStateProvider, storageServiceProvider);
const sut = new DefaultStateEventRunnerService(fakeGlobalStateProvider, storageServiceProvider);
describe("handleEvent", () => {
it("does nothing if there are no events in state", async () => {

View File

@@ -0,0 +1,89 @@
import { firstValueFrom } from "rxjs";
import {
ClearEvent,
GlobalState,
GlobalStateProvider,
StateDefinition,
StateEventRunnerService,
UserKeyDefinition,
} from "@bitwarden/state";
import { StorageLocation, StorageServiceProvider } from "@bitwarden/storage-core";
import { UserId } from "@bitwarden/user-core";
import {
STATE_LOCK_EVENT,
STATE_LOGOUT_EVENT,
StateEventInfo,
} from "./default-state-event-registrar.service";
export class DefaultStateEventRunnerService implements StateEventRunnerService {
private readonly stateEventMap: { [Prop in ClearEvent]: GlobalState<StateEventInfo[]> };
constructor(
globalStateProvider: GlobalStateProvider,
private storageServiceProvider: StorageServiceProvider,
) {
this.stateEventMap = {
lock: globalStateProvider.get(STATE_LOCK_EVENT),
logout: globalStateProvider.get(STATE_LOGOUT_EVENT),
};
}
async handleEvent(event: ClearEvent, userId: UserId) {
let tickets = await firstValueFrom(this.stateEventMap[event].state$);
tickets ??= [];
const failures: string[] = [];
for (const ticket of tickets) {
try {
const [, service] = this.storageServiceProvider.get(
ticket.location,
{}, // The storage location is already the computed storage location for this client
);
const ticketStorageKey = this.storageKeyFor(userId, ticket);
// Evaluate current value so we can avoid writing to state if we don't need to
const currentValue = await service.get(ticketStorageKey);
if (currentValue != null) {
await service.remove(ticketStorageKey);
}
} catch (err: unknown) {
let errorMessage = "Unknown Error";
if (
err != null &&
typeof err === "object" &&
"message" in err &&
typeof err.message === "string"
) {
errorMessage = err.message;
}
failures.push(
`${errorMessage} in ${ticket.state} > ${ticket.key} located ${ticket.location}`,
);
}
}
if (failures.length > 0) {
// Throw aggregated error
throw new Error(
`One or more errors occurred while handling event '${event}' for user ${userId}.\n${failures.join("\n")}`,
);
}
}
private storageKeyFor(userId: UserId, ticket: StateEventInfo) {
const userKey = new UserKeyDefinition<unknown>(
new StateDefinition(ticket.state, ticket.location as unknown as StorageLocation),
ticket.key,
{
deserializer: (v) => v,
clearOn: [],
},
);
return userKey.buildKey(userId);
}
}

View File

@@ -5,6 +5,12 @@
import { Observable, of } from "rxjs";
import { awaitAsync, trackEmissions } from "@bitwarden/core-test-utils";
import {
DeriveDefinition,
KeyDefinition,
StateDefinition,
UserKeyDefinition,
} from "@bitwarden/state";
import {
FakeActiveUserAccessor,
FakeActiveUserStateProvider,
@@ -14,11 +20,6 @@ import {
} from "@bitwarden/state-test-utils";
import { UserId } from "@bitwarden/user-core";
import { DeriveDefinition } from "../derive-definition";
import { KeyDefinition } from "../key-definition";
import { StateDefinition } from "../state-definition";
import { UserKeyDefinition } from "../user-key-definition";
import { DefaultStateProvider } from "./default-state.provider";
describe("DefaultStateProvider", () => {

View File

@@ -2,17 +2,19 @@
// @ts-strict-ignore
import { Observable, filter, of, switchMap, take } from "rxjs";
import {
ActiveUserStateProvider,
DeriveDefinition,
DerivedState,
DerivedStateDependencies,
DerivedStateProvider,
GlobalStateProvider,
SingleUserStateProvider,
StateProvider,
UserKeyDefinition,
} from "@bitwarden/state";
import { UserId } from "@bitwarden/user-core";
import { DerivedStateDependencies } from "../../types/state";
import { DeriveDefinition } from "../derive-definition";
import { DerivedState } from "../derived-state";
import { DerivedStateProvider } from "../derived-state.provider";
import { GlobalStateProvider } from "../global-state.provider";
import { StateProvider } from "../state.provider";
import { UserKeyDefinition } from "../user-key-definition";
import { ActiveUserStateProvider, SingleUserStateProvider } from "../user-state.provider";
export class DefaultStateProvider implements StateProvider {
activeUserId$: Observable<UserId>;
constructor(

View File

@@ -10,3 +10,7 @@ export * from "./default-state.provider";
export * from "./inline-derived-state";
export * from "./state-base";
export * from "./util";
export { ActiveUserAccessor } from "./active-user.accessor";
export { DefaultStateService } from "./legacy/default-state.service";
export { DefaultStateEventRegistrarService } from "./default-state-event-registrar.service";
export { DefaultStateEventRunnerService } from "./default-state-event-runner.service";

View File

@@ -1,7 +1,6 @@
import { Subject, firstValueFrom } from "rxjs";
import { DeriveDefinition } from "../derive-definition";
import { StateDefinition } from "../state-definition";
import { DeriveDefinition, StateDefinition } from "@bitwarden/state";
import { InlineDerivedState } from "./inline-derived-state";

View File

@@ -1,9 +1,11 @@
import { Observable, concatMap } from "rxjs";
import { DerivedStateDependencies } from "../../types/state";
import { DeriveDefinition } from "../derive-definition";
import { DerivedState } from "../derived-state";
import { DerivedStateProvider } from "../derived-state.provider";
import {
DeriveDefinition,
DerivedState,
DerivedStateDependencies,
DerivedStateProvider,
} from "@bitwarden/state";
export class InlineDerivedStateProvider implements DerivedStateProvider {
get<TFrom, TTo, TDeps extends DerivedStateDependencies>(

View File

@@ -1,12 +1,12 @@
import { firstValueFrom } from "rxjs";
import { RequiredUserId, StateService } from "@bitwarden/state";
import { StorageService } from "@bitwarden/storage-core";
import { UserId } from "@bitwarden/user-core";
import { ActiveUserAccessor } from "../core";
import { ActiveUserAccessor } from "../active-user.accessor";
import { GlobalState } from "./global-state";
import { RequiredUserId, StateService } from "./state.service";
const keys = {
global: "global",

View File

@@ -1,16 +1,17 @@
import { mock } from "jest-mock-extended";
import { LogService } from "@bitwarden/logging";
import {
KeyDefinition,
StateDefinition,
StateEventRegistrarService,
UserKeyDefinition,
} from "@bitwarden/state";
import { FakeActiveUserAccessor } from "@bitwarden/state-test-utils";
import { StorageServiceProvider } from "@bitwarden/storage-core";
import { FakeStorageService } from "@bitwarden/storage-test-utils";
import { UserId } from "@bitwarden/user-core";
import { KeyDefinition } from "../key-definition";
import { StateDefinition } from "../state-definition";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { UserKeyDefinition } from "../user-key-definition";
import { DefaultActiveUserState } from "./default-active-user-state";
import { DefaultActiveUserStateProvider } from "./default-active-user-state.provider";
import { DefaultGlobalState } from "./default-global-state";

View File

@@ -16,13 +16,10 @@ import {
import { Jsonify } from "type-fest";
import { LogService } from "@bitwarden/logging";
import { DebugOptions, StateUpdateOptions, StorageKey } from "@bitwarden/state";
import { AbstractStorageService, ObservableStorageService } from "@bitwarden/storage-core";
import { StorageKey } from "../../types/state";
import { DebugOptions } from "../key-definition";
import { populateOptionsWithDefault, StateUpdateOptions } from "../state-update-options";
import { getStoredValue } from "./util";
import { getStoredValue, populateOptionsWithDefault } from "./util";
// The parts of a KeyDefinition this class cares about to make it work
type KeyDefinitionRequirements<T> = {
@@ -85,15 +82,15 @@ export abstract class StateBase<T, KeyDef extends KeyDefinitionRequirements<T>>
async update<TCombine>(
configureState: (state: T | null, dependency: TCombine) => T | null,
options: StateUpdateOptions<T, TCombine> = {},
options: Partial<StateUpdateOptions<T, TCombine>> = {},
): Promise<T | null> {
options = populateOptionsWithDefault(options);
const normalizedOptions = populateOptionsWithDefault(options);
if (this.updatePromise != null) {
await this.updatePromise;
}
try {
this.updatePromise = this.internalUpdate(configureState, options);
this.updatePromise = this.internalUpdate(configureState, normalizedOptions);
return await this.updatePromise;
} finally {
this.updatePromise = null;

View File

@@ -0,0 +1,8 @@
import * as lib from "./index";
describe("state-internal", () => {
// This test will fail until something is exported from index.ts
it("should work", () => {
expect(lib).toBeDefined();
});
});

View File

@@ -0,0 +1,38 @@
import { Jsonify } from "type-fest";
import { KeyDefinition, StateUpdateOptions, StorageKey } from "@bitwarden/state";
import { AbstractStorageService } from "@bitwarden/storage-core";
export async function getStoredValue<T>(
key: string,
storage: AbstractStorageService,
deserializer: (jsonValue: Jsonify<T>) => T | null,
) {
if (storage.valuesRequireDeserialization) {
const jsonValue = await storage.get<Jsonify<T>>(key);
return deserializer(jsonValue);
} else {
const value = await storage.get<T>(key);
return value ?? null;
}
}
/**
* Creates a {@link StorageKey}
* @param keyDefinition The key definition of which data the key should point to.
* @returns A key that is ready to be used in a storage service to get data.
*/
export function globalKeyBuilder(keyDefinition: KeyDefinition<unknown>): StorageKey {
return `global_${keyDefinition.stateDefinition.name}_${keyDefinition.key}` as StorageKey;
}
export function populateOptionsWithDefault<T, TCombine>(
options: Partial<StateUpdateOptions<T, TCombine>>,
): StateUpdateOptions<T, TCombine> {
const { combineLatestWith = null, shouldUpdate = () => true, msTimeout = 1000 } = options;
return {
combineLatestWith: combineLatestWith,
shouldUpdate: shouldUpdate,
msTimeout: msTimeout,
};
}

View File

@@ -0,0 +1,6 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": ["src/**/*.ts", "src/**/*.js"],
"exclude": ["**/build", "**/dist"]
}

View File

@@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.base.json",
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.js", "src/**/*.spec.ts"]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "node10",
"types": ["jest", "node"]
},
"include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"]
}

View File

@@ -17,22 +17,22 @@ import {
DeriveDefinition,
DerivedStateProvider,
UserKeyDefinition,
ActiveUserAccessor,
} from "@bitwarden/state";
import { UserId } from "@bitwarden/user-core";
import {
FakeActiveUserState,
FakeDerivedState,
FakeGlobalState,
FakeSingleUserState,
} from "@bitwarden/state-test-utils";
import { UserId } from "@bitwarden/user-core";
} from "./fake-state";
export interface MinimalAccountService {
activeUserId: UserId | null;
activeAccount$: Observable<{ id: UserId } | null>;
}
export class FakeActiveUserAccessor implements MinimalAccountService, ActiveUserAccessor {
export class FakeActiveUserAccessor implements MinimalAccountService {
private _subject: BehaviorSubject<UserId | null>;
constructor(startingUser: UserId | null) {

View File

@@ -1,11 +0,0 @@
import { Observable } from "rxjs";
import { UserId } from "@bitwarden/user-core";
export abstract class ActiveUserAccessor {
/**
* Returns a stream of the current active user for the application. The stream either emits the user id for that account
* or returns null if there is no current active user.
*/
abstract activeUserId$: Observable<UserId | null>;
}

View File

@@ -19,7 +19,7 @@ export interface GlobalState<T> {
*/
update: <TCombine>(
configureState: (state: T | null, dependency: TCombine) => T | null,
options?: StateUpdateOptions<T, TCombine>,
options?: Partial<StateUpdateOptions<T, TCombine>>,
) => Promise<T | null>;
/**

View File

@@ -1,17 +0,0 @@
import { Jsonify } from "type-fest";
import { AbstractStorageService } from "@bitwarden/storage-core";
export async function getStoredValue<T>(
key: string,
storage: AbstractStorageService,
deserializer: (jsonValue: Jsonify<T>) => T | null,
) {
if (storage.valuesRequireDeserialization) {
const jsonValue = await storage.get<Jsonify<T>>(key);
return deserializer(jsonValue);
} else {
const value = await storage.get<T>(key);
return value ?? null;
}
}

View File

@@ -6,14 +6,12 @@ export { StateProvider } from "./state.provider";
export { GlobalStateProvider } from "./global-state.provider";
export { ActiveUserState, SingleUserState, CombinedState } from "./user-state";
export { ActiveUserStateProvider, SingleUserStateProvider } from "./user-state.provider";
export { KeyDefinition, KeyDefinitionOptions } from "./key-definition";
export { KeyDefinition, KeyDefinitionOptions, DebugOptions } from "./key-definition";
export { StateUpdateOptions } from "./state-update-options";
export { UserKeyDefinitionOptions, UserKeyDefinition } from "./user-key-definition";
export { UserKeyDefinitionOptions, UserKeyDefinition, ClearEvent } from "./user-key-definition";
export { StateEventRunnerService } from "./state-event-runner.service";
export { activeMarker } from "./user-state";
export { StateDefinition } from "./state-definition";
export { ActiveUserAccessor } from "./active-user.accessor";
export * from "./state-definitions";
export * from "./implementations";
export * from "./state-event-registrar.service";

View File

@@ -4,8 +4,6 @@ import { Jsonify } from "type-fest";
import { array, record } from "@bitwarden/serialization";
import { StorageKey } from "../types/state";
import { StateDefinition } from "./state-definition";
export type DebugOptions = {
@@ -172,12 +170,3 @@ export class KeyDefinition<T> {
return `${this.stateDefinition.name} > ${this.key}`;
}
}
/**
* Creates a {@link StorageKey}
* @param keyDefinition The key definition of which data the key should point to.
* @returns A key that is ready to be used in a storage service to get data.
*/
export function globalKeyBuilder(keyDefinition: KeyDefinition<unknown>): StorageKey {
return `global_${keyDefinition.stateDefinition.name}_${keyDefinition.key}` as StorageKey;
}

View File

@@ -1,76 +1,5 @@
import { PossibleLocation, StorageServiceProvider } from "@bitwarden/storage-core";
import { UserKeyDefinition } from "./user-key-definition";
import { GlobalState } from "./global-state";
import { GlobalStateProvider } from "./global-state.provider";
import { KeyDefinition } from "./key-definition";
import { CLEAR_EVENT_DISK } from "./state-definitions";
import { ClearEvent, UserKeyDefinition } from "./user-key-definition";
export type StateEventInfo = {
state: string;
key: string;
location: PossibleLocation;
};
export const STATE_LOCK_EVENT = KeyDefinition.array<StateEventInfo>(CLEAR_EVENT_DISK, "lock", {
deserializer: (e) => e,
});
export const STATE_LOGOUT_EVENT = KeyDefinition.array<StateEventInfo>(CLEAR_EVENT_DISK, "logout", {
deserializer: (e) => e,
});
export class StateEventRegistrarService {
private readonly stateEventStateMap: { [Prop in ClearEvent]: GlobalState<StateEventInfo[]> };
constructor(
globalStateProvider: GlobalStateProvider,
private storageServiceProvider: StorageServiceProvider,
) {
this.stateEventStateMap = {
lock: globalStateProvider.get(STATE_LOCK_EVENT),
logout: globalStateProvider.get(STATE_LOGOUT_EVENT),
};
}
async registerEvents(keyDefinition: UserKeyDefinition<unknown>) {
for (const clearEvent of keyDefinition.clearOn) {
const eventState = this.stateEventStateMap[clearEvent];
// Determine the storage location for this
const [storageLocation] = this.storageServiceProvider.get(
keyDefinition.stateDefinition.defaultStorageLocation,
keyDefinition.stateDefinition.storageLocationOverrides,
);
const newEvent: StateEventInfo = {
state: keyDefinition.stateDefinition.name,
key: keyDefinition.key,
location: storageLocation,
};
// Only update the event state if the existing list doesn't have a matching entry
await eventState.update(
(existingTickets) => {
existingTickets ??= [];
existingTickets.push(newEvent);
return existingTickets;
},
{
shouldUpdate: (currentTickets) => {
return (
// If the current tickets are null, then it will for sure be added
currentTickets == null ||
// If an existing match couldn't be found, we also need to add one
currentTickets.findIndex(
(e) =>
e.state === newEvent.state &&
e.key === newEvent.key &&
e.location === newEvent.location,
) === -1
);
},
},
);
}
}
export abstract class StateEventRegistrarService {
abstract registerEvents(keyDefinition: UserKeyDefinition<unknown>): Promise<void>;
}

View File

@@ -1,82 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom } from "rxjs";
import { StorageServiceProvider, StorageLocation } from "@bitwarden/storage-core";
import { UserId } from "@bitwarden/user-core";
import { GlobalState } from "./global-state";
import { GlobalStateProvider } from "./global-state.provider";
import { StateDefinition } from "./state-definition";
import {
STATE_LOCK_EVENT,
STATE_LOGOUT_EVENT,
StateEventInfo,
} from "./state-event-registrar.service";
import { ClearEvent, UserKeyDefinition } from "./user-key-definition";
import { ClearEvent } from "./user-key-definition";
export class StateEventRunnerService {
private readonly stateEventMap: { [Prop in ClearEvent]: GlobalState<StateEventInfo[]> };
constructor(
globalStateProvider: GlobalStateProvider,
private storageServiceProvider: StorageServiceProvider,
) {
this.stateEventMap = {
lock: globalStateProvider.get(STATE_LOCK_EVENT),
logout: globalStateProvider.get(STATE_LOGOUT_EVENT),
};
}
async handleEvent(event: ClearEvent, userId: UserId) {
let tickets = await firstValueFrom(this.stateEventMap[event].state$);
tickets ??= [];
const failures: string[] = [];
for (const ticket of tickets) {
try {
const [, service] = this.storageServiceProvider.get(
ticket.location,
{}, // The storage location is already the computed storage location for this client
);
const ticketStorageKey = this.storageKeyFor(userId, ticket);
// Evaluate current value so we can avoid writing to state if we don't need to
const currentValue = await service.get(ticketStorageKey);
if (currentValue != null) {
await service.remove(ticketStorageKey);
}
} catch (err: unknown) {
let errorMessage = "Unknown Error";
if (typeof err === "object" && "message" in err && typeof err.message === "string") {
errorMessage = err.message;
}
failures.push(
`${errorMessage} in ${ticket.state} > ${ticket.key} located ${ticket.location}`,
);
}
}
if (failures.length > 0) {
// Throw aggregated error
throw new Error(
`One or more errors occurred while handling event '${event}' for user ${userId}.\n${failures.join("\n")}`,
);
}
}
private storageKeyFor(userId: UserId, ticket: StateEventInfo) {
const userKey = new UserKeyDefinition<unknown>(
new StateDefinition(ticket.state, ticket.location as unknown as StorageLocation),
ticket.key,
{
deserializer: (v) => v,
clearOn: [],
},
);
return userKey.buildKey(userId);
}
export abstract class StateEventRunnerService {
abstract handleEvent(event: ClearEvent, userId: UserId): Promise<void>;
}

View File

@@ -2,27 +2,8 @@
// @ts-strict-ignore
import { Observable } from "rxjs";
export const DEFAULT_OPTIONS = {
shouldUpdate: () => true,
combineLatestWith: null as Observable<unknown>,
msTimeout: 1000,
export type StateUpdateOptions<T, TCombine> = {
readonly shouldUpdate: (state: T, dependency: TCombine) => boolean;
readonly combineLatestWith: Observable<TCombine> | null;
readonly msTimeout: number;
};
type DefinitelyTypedDefault<T, TCombine> = Omit<
typeof DEFAULT_OPTIONS,
"shouldUpdate" | "combineLatestWith"
> & {
shouldUpdate: (state: T, dependency: TCombine) => boolean;
combineLatestWith?: Observable<TCombine>;
};
export type StateUpdateOptions<T, TCombine> = Partial<DefinitelyTypedDefault<T, TCombine>>;
export function populateOptionsWithDefault<T, TCombine>(
options: StateUpdateOptions<T, TCombine>,
): StateUpdateOptions<T, TCombine> {
return {
...(DEFAULT_OPTIONS as StateUpdateOptions<T, TCombine>),
...options,
};
}

View File

@@ -39,7 +39,7 @@ export interface ActiveUserState<T> extends UserState<T> {
*/
readonly update: <TCombine>(
configureState: (state: T | null, dependencies: TCombine) => T | null,
options?: StateUpdateOptions<T, TCombine>,
options?: Partial<StateUpdateOptions<T, TCombine>>,
) => Promise<[UserId, T | null]>;
}
@@ -59,6 +59,6 @@ export interface SingleUserState<T> extends UserState<T> {
*/
readonly update: <TCombine>(
configureState: (state: T | null, dependencies: TCombine) => T | null,
options?: StateUpdateOptions<T, TCombine>,
options?: Partial<StateUpdateOptions<T, TCombine>>,
) => Promise<T | null>;
}

View File

@@ -1,2 +1 @@
export { StateService } from "./state.service";
export { DefaultStateService } from "./default-state.service";
export { StateService, RequiredUserId } from "./state.service";

8
package-lock.json generated
View File

@@ -402,6 +402,10 @@
"version": "0.0.1",
"license": "GPL-3.0"
},
"libs/state-internal": {
"version": "0.0.1",
"license": "GPL-3.0"
},
"libs/state-test-utils": {
"name": "@bitwarden/state-test-utils",
"version": "0.0.1",
@@ -4709,6 +4713,10 @@
"resolved": "libs/state",
"link": true
},
"node_modules/@bitwarden/state-internal": {
"resolved": "libs/state-internal",
"link": true
},
"node_modules/@bitwarden/state-test-utils": {
"resolved": "libs/state-test-utils",
"link": true

View File

@@ -52,6 +52,7 @@
"@bitwarden/send-ui": ["./libs/tools/send/send-ui/src"],
"@bitwarden/serialization": ["libs/serialization/src/index.ts"],
"@bitwarden/state": ["libs/state/src/index.ts"],
"@bitwarden/state-internal": ["libs/state-internal/src/index.ts"],
"@bitwarden/state-test-utils": ["libs/state-test-utils/src/index.ts"],
"@bitwarden/storage-core": ["libs/storage-core/src/index.ts"],
"@bitwarden/storage-test-utils": ["libs/storage-test-utils/src/index.ts"],