1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

Add State Provider Framework (#6640)

* Add StateDefinition

Add a class for encapsulation information about state
this will often be for a domain but creations of this will
exist outside of a specific domain, hence just the name State.

* Add KeyDefinition

This adds a type that extends state definition into another sub-key
and forces creators to define the data that will be stored and how
to read the data that they expect to be stored.

* Add key-builders helper functions

Adds to function to help building keys for both keys scoped
to a specific user and for keys scoped to global storage.

Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>

* Add updates$ stream to existing storageServices

Original commit by Matt: 823d9546fe
Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>

* Add fromChromeEvent helper

Create a helper that creats an Observable from a chrome event
and removes the listener when the subscription is completed.

* Implement `updates$` property for chrome storage

Use fromChromeEvent to create an observable from chrome
event and map that into our expected shape.

* Add GlobalState Abstractions

* Add UserState Abstractions

* Add Default Implementations of User/Global state

Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>

* Add Barrel File for state

Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>

* Fix ChromeStorageServices

* Rework fromChromeEvent

Rework fromChromeEvent so we have to lie to TS less and
remove unneeded generics. I did this by caring less about
the function and more about the parameters only.

Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>

* Fix UserStateProvider Test

* Add Inner Mock & Assert Calls

* Update Tests to use new keys

Use different key format

* Prefer returns over mutations in update

* Update Tests

* Address PR Feedback

* Be stricter with userId parameter

* Add Better Way To Determine if it was a remove

* Fix Web & Browser Storage Services

* Fix Desktop & CLI Storage Services

* Fix Test Storage Service

* Use createKey Helper

* Prefer implement to extending

* Determine storage location in providers

* Export default providers publicly

* Fix user state tests

* Name tests

* Fix CLI

* Prefer Implement In Chrome Storage

* Remove Secure Storage Option

Also throw an exception for subscribes to the secure storage observable.

* Update apps/browser/src/platform/browser/from-chrome-event.ts

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* Enforce state module barrel file

* Fix Linting Error

* Allow state module import from other modules

* Globally Unregister fromChromeEvent Listeners

Changed fromChromeEvent to add its listeners through the BrowserApi, so that
they will be unregistered when safari closes.

* Test default global state

* Use Proper Casing in Parameter

* Address Feedback

* Update libs/common/src/platform/state/key-definition.ts

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* Add `buildCacheKey` Method

* Fix lint errors

* Add Comment

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>

* Use Generic in callback parameter

* Refactor Out DerivedStateDefinition

* Persist Listener Return Type

* Add Ticket Link

---------

Co-authored-by: Matt Gibson <MGibson1@users.noreply.github.com>
Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
This commit is contained in:
Justin Baur
2023-11-09 17:06:42 -05:00
committed by GitHub
parent 801141f90e
commit e1b5b83723
36 changed files with 1352 additions and 68 deletions

View File

@@ -0,0 +1,102 @@
import { Jsonify, Opaque } from "type-fest";
import { UserId } from "../../types/guid";
import { Utils } from "../misc/utils";
import { StateDefinition } from "./state-definition";
/**
* KeyDefinitions describe the precise location to store data for a given piece of state.
* The StateDefinition is used to describe the domain of the state, and the KeyDefinition
* sub-divides that domain into specific keys.
*/
export class KeyDefinition<T> {
/**
* Creates a new instance of a KeyDefinition
* @param stateDefinition The state definition for which this key belongs to.
* @param key The name of the key, this should be unique per domain
* @param deserializer A function to use to safely convert your type from json to your expected type.
*/
constructor(
readonly stateDefinition: StateDefinition,
readonly key: string,
readonly deserializer: (jsonValue: Jsonify<T>) => T
) {}
/**
* Creates a {@link KeyDefinition} for state that is an array.
* @param stateDefinition The state definition to be added to the KeyDefinition
* @param key The key to be added to the KeyDefinition
* @param deserializer The deserializer for the element of the array in your state.
* @returns A {@link KeyDefinition} that contains a serializer that will run the provided deserializer for each
* element of an array **unless that array is null in which case it will return an empty list.**
*/
static array<T>(
stateDefinition: StateDefinition,
key: string,
deserializer: (jsonValue: Jsonify<T>) => T
) {
return new KeyDefinition<T[]>(stateDefinition, key, (jsonValue) => {
return jsonValue?.map((v) => deserializer(v)) ?? [];
});
}
/**
* Creates a {@link KeyDefinition} for state that is a record.
* @param stateDefinition The state definition to be added to the KeyDefinition
* @param key The key to be added to the KeyDefinition
* @param deserializer The deserializer for the value part of a record.
* @returns A {@link KeyDefinition} that contains a serializer that will run the provided deserializer for each
* value in a record and returns every key as a string **unless that record is null in which case it will return an record.**
*/
static record<T>(
stateDefinition: StateDefinition,
key: string,
deserializer: (jsonValue: Jsonify<T>) => T
) {
return new KeyDefinition<Record<string, T>>(stateDefinition, key, (jsonValue) => {
const output: Record<string, T> = {};
if (jsonValue == null) {
return output;
}
for (const key in jsonValue) {
output[key] = deserializer((jsonValue as Record<string, Jsonify<T>>)[key]);
}
return output;
});
}
/**
*
* @returns
*/
buildCacheKey(): string {
return `${this.stateDefinition.storageLocation}_${this.stateDefinition.name}_${this.key}`;
}
}
export type StorageKey = Opaque<string, "StorageKey">;
/**
* Creates a {@link StorageKey} that points to the data at the given key definition for the specified user.
* @param userId The userId of the user you want the key to be for.
* @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 userKeyBuilder(userId: UserId, keyDefinition: KeyDefinition<unknown>): StorageKey {
if (!Utils.isGuid(userId)) {
throw new Error("You cannot build a user key without a valid UserId");
}
return `user_${userId}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}` as StorageKey;
}
/**
* 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;
}