mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
[PM-6404] Add UserKeyDefinition (#8052)
* Add `UserKeyDefinition` * Fix Deserialization Helpers * Fix KeyDefinition * Move `ClearEvent` * Address PR Feedback * Feedback
This commit is contained in:
@@ -4,7 +4,7 @@ import {
|
|||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
ObservableStorageService,
|
ObservableStorageService,
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
import { KeyDefinition } from "@bitwarden/common/platform/state";
|
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||||
/* eslint-disable import/no-restricted-paths -- Needed to extend class & in platform owned code */
|
/* eslint-disable import/no-restricted-paths -- Needed to extend class & in platform owned code */
|
||||||
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
|
import { DefaultActiveUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-active-user-state.provider";
|
||||||
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
|
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
|
||||||
@@ -20,7 +20,7 @@ export class WebActiveUserStateProvider extends DefaultActiveUserStateProvider {
|
|||||||
super(accountService, memoryStorage, sessionStorage);
|
super(accountService, memoryStorage, sessionStorage);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override getLocationString(keyDefinition: KeyDefinition<unknown>): string {
|
protected override getLocationString(keyDefinition: UserKeyDefinition<unknown>): string {
|
||||||
return (
|
return (
|
||||||
keyDefinition.stateDefinition.storageLocationOverrides["web"] ??
|
keyDefinition.stateDefinition.storageLocationOverrides["web"] ??
|
||||||
keyDefinition.stateDefinition.defaultStorageLocation
|
keyDefinition.stateDefinition.defaultStorageLocation
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
ObservableStorageService,
|
ObservableStorageService,
|
||||||
} from "@bitwarden/common/platform/abstractions/storage.service";
|
} from "@bitwarden/common/platform/abstractions/storage.service";
|
||||||
import { KeyDefinition } from "@bitwarden/common/platform/state";
|
import { UserKeyDefinition } from "@bitwarden/common/platform/state";
|
||||||
/* eslint-disable import/no-restricted-paths -- Needed to extend service & and in platform owned file */
|
/* eslint-disable import/no-restricted-paths -- Needed to extend service & and in platform owned file */
|
||||||
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
|
import { DefaultSingleUserStateProvider } from "@bitwarden/common/platform/state/implementations/default-single-user-state.provider";
|
||||||
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
|
import { StateDefinition } from "@bitwarden/common/platform/state/state-definition";
|
||||||
@@ -18,7 +18,7 @@ export class WebSingleUserStateProvider extends DefaultSingleUserStateProvider {
|
|||||||
super(memoryStorageService, sessionStorageService);
|
super(memoryStorageService, sessionStorageService);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override getLocationString(keyDefinition: KeyDefinition<unknown>): string {
|
protected override getLocationString(keyDefinition: UserKeyDefinition<unknown>): string {
|
||||||
return (
|
return (
|
||||||
keyDefinition.stateDefinition.storageLocationOverrides["web"] ??
|
keyDefinition.stateDefinition.storageLocationOverrides["web"] ??
|
||||||
keyDefinition.stateDefinition.defaultStorageLocation
|
keyDefinition.stateDefinition.defaultStorageLocation
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
DerivedState,
|
DerivedState,
|
||||||
DeriveDefinition,
|
DeriveDefinition,
|
||||||
DerivedStateProvider,
|
DerivedStateProvider,
|
||||||
|
UserKeyDefinition,
|
||||||
} from "../src/platform/state";
|
} from "../src/platform/state";
|
||||||
import { UserId } from "../src/types/guid";
|
import { UserId } from "../src/types/guid";
|
||||||
import { DerivedStateDependencies } from "../src/types/state";
|
import { DerivedStateDependencies } from "../src/types/state";
|
||||||
@@ -67,8 +68,14 @@ export class FakeSingleUserStateProvider implements SingleUserStateProvider {
|
|||||||
mock = mock<SingleUserStateProvider>();
|
mock = mock<SingleUserStateProvider>();
|
||||||
establishedMocks: Map<string, FakeSingleUserState<unknown>> = new Map();
|
establishedMocks: Map<string, FakeSingleUserState<unknown>> = new Map();
|
||||||
states: Map<string, SingleUserState<unknown>> = new Map();
|
states: Map<string, SingleUserState<unknown>> = new Map();
|
||||||
get<T>(userId: UserId, keyDefinition: KeyDefinition<T>): SingleUserState<T> {
|
get<T>(
|
||||||
|
userId: UserId,
|
||||||
|
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
|
||||||
|
): SingleUserState<T> {
|
||||||
this.mock.get(userId, keyDefinition);
|
this.mock.get(userId, keyDefinition);
|
||||||
|
if (keyDefinition instanceof KeyDefinition) {
|
||||||
|
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
|
||||||
|
}
|
||||||
let result = this.states.get(`${keyDefinition.fullName}_${userId}`);
|
let result = this.states.get(`${keyDefinition.fullName}_${userId}`);
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
@@ -108,7 +115,10 @@ export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
|
|||||||
this.activeUserId$ = accountService.activeAccountSubject.asObservable().pipe(map((a) => a.id));
|
this.activeUserId$ = accountService.activeAccountSubject.asObservable().pipe(map((a) => a.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
get<T>(keyDefinition: KeyDefinition<T>): ActiveUserState<T> {
|
get<T>(keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>): ActiveUserState<T> {
|
||||||
|
if (keyDefinition instanceof KeyDefinition) {
|
||||||
|
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
|
||||||
|
}
|
||||||
let result = this.states.get(keyDefinition.fullName);
|
let result = this.states.get(keyDefinition.fullName);
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
@@ -150,7 +160,7 @@ export class FakeStateProvider implements StateProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setUserState<T>(
|
async setUserState<T>(
|
||||||
keyDefinition: KeyDefinition<T>,
|
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
|
||||||
value: T,
|
value: T,
|
||||||
userId?: UserId,
|
userId?: UserId,
|
||||||
): Promise<[UserId, T]> {
|
): Promise<[UserId, T]> {
|
||||||
@@ -162,7 +172,7 @@ export class FakeStateProvider implements StateProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getActive<T>(keyDefinition: KeyDefinition<T>): ActiveUserState<T> {
|
getActive<T>(keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>): ActiveUserState<T> {
|
||||||
return this.activeUser.get(keyDefinition);
|
return this.activeUser.get(keyDefinition);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +180,10 @@ export class FakeStateProvider implements StateProvider {
|
|||||||
return this.global.get(keyDefinition);
|
return this.global.get(keyDefinition);
|
||||||
}
|
}
|
||||||
|
|
||||||
getUser<T>(userId: UserId, keyDefinition: KeyDefinition<T>): SingleUserState<T> {
|
getUser<T>(
|
||||||
|
userId: UserId,
|
||||||
|
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
|
||||||
|
): SingleUserState<T> {
|
||||||
return this.singleUser.get(userId, keyDefinition);
|
return this.singleUser.get(userId, keyDefinition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
ActiveUserState,
|
ActiveUserState,
|
||||||
KeyDefinition,
|
KeyDefinition,
|
||||||
DeriveDefinition,
|
DeriveDefinition,
|
||||||
|
UserKeyDefinition,
|
||||||
} from "../src/platform/state";
|
} from "../src/platform/state";
|
||||||
// eslint-disable-next-line import/no-restricted-paths -- using unexposed options for clean typing in test class
|
// eslint-disable-next-line import/no-restricted-paths -- using unexposed options for clean typing in test class
|
||||||
import { StateUpdateOptions } from "../src/platform/state/state-update-options";
|
import { StateUpdateOptions } from "../src/platform/state/state-update-options";
|
||||||
@@ -131,7 +132,7 @@ export class FakeSingleUserState<T> implements SingleUserState<T> {
|
|||||||
|
|
||||||
/** Tracks update values resolved by `FakeState.update` */
|
/** Tracks update values resolved by `FakeState.update` */
|
||||||
nextMock = jest.fn<void, [T]>();
|
nextMock = jest.fn<void, [T]>();
|
||||||
private _keyDefinition: KeyDefinition<T> | null = null;
|
private _keyDefinition: UserKeyDefinition<T> | null = null;
|
||||||
get keyDefinition() {
|
get keyDefinition() {
|
||||||
if (this._keyDefinition == null) {
|
if (this._keyDefinition == null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -140,7 +141,7 @@ export class FakeSingleUserState<T> implements SingleUserState<T> {
|
|||||||
}
|
}
|
||||||
return this._keyDefinition;
|
return this._keyDefinition;
|
||||||
}
|
}
|
||||||
set keyDefinition(value: KeyDefinition<T>) {
|
set keyDefinition(value: UserKeyDefinition<T>) {
|
||||||
this._keyDefinition = value;
|
this._keyDefinition = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,7 +196,7 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
/** Tracks update values resolved by `FakeState.update` */
|
/** Tracks update values resolved by `FakeState.update` */
|
||||||
nextMock = jest.fn<void, [[UserId, T]]>();
|
nextMock = jest.fn<void, [[UserId, T]]>();
|
||||||
|
|
||||||
private _keyDefinition: KeyDefinition<T> | null = null;
|
private _keyDefinition: UserKeyDefinition<T> | null = null;
|
||||||
get keyDefinition() {
|
get keyDefinition() {
|
||||||
if (this._keyDefinition == null) {
|
if (this._keyDefinition == null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -204,7 +205,7 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
}
|
}
|
||||||
return this._keyDefinition;
|
return this._keyDefinition;
|
||||||
}
|
}
|
||||||
set keyDefinition(value: KeyDefinition<T>) {
|
set keyDefinition(value: UserKeyDefinition<T>) {
|
||||||
this._keyDefinition = value;
|
this._keyDefinition = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
libs/common/src/platform/state/deserialization-helpers.ts
Normal file
38
libs/common/src/platform/state/deserialization-helpers.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param elementDeserializer
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function array<T>(
|
||||||
|
elementDeserializer: (element: Jsonify<T>) => T,
|
||||||
|
): (array: Jsonify<T[]>) => T[] {
|
||||||
|
return (array) => {
|
||||||
|
if (array == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array.map((element) => elementDeserializer(element));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param valueDeserializer
|
||||||
|
*/
|
||||||
|
export function record<T, TKey extends string = string>(
|
||||||
|
valueDeserializer: (value: Jsonify<T>) => T,
|
||||||
|
): (record: Jsonify<Record<TKey, T>>) => Record<TKey, T> {
|
||||||
|
return (jsonValue: Jsonify<Record<TKey, T> | null>) => {
|
||||||
|
if (jsonValue == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const output: Record<string, T> = {};
|
||||||
|
for (const key in jsonValue) {
|
||||||
|
output[key] = valueDeserializer((jsonValue as Record<string, Jsonify<T>>)[key]);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from "../../abstractions/storage.service";
|
} from "../../abstractions/storage.service";
|
||||||
import { KeyDefinition } from "../key-definition";
|
import { KeyDefinition } from "../key-definition";
|
||||||
import { StateDefinition } from "../state-definition";
|
import { StateDefinition } from "../state-definition";
|
||||||
|
import { UserKeyDefinition, isUserKeyDefinition } from "../user-key-definition";
|
||||||
import { ActiveUserState } from "../user-state";
|
import { ActiveUserState } from "../user-state";
|
||||||
import { ActiveUserStateProvider } from "../user-state.provider";
|
import { ActiveUserStateProvider } from "../user-state.provider";
|
||||||
|
|
||||||
@@ -27,7 +28,10 @@ export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
|
|||||||
this.activeUserId$ = this.accountService.activeAccount$.pipe(map((account) => account?.id));
|
this.activeUserId$ = this.accountService.activeAccount$.pipe(map((account) => account?.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
get<T>(keyDefinition: KeyDefinition<T>): ActiveUserState<T> {
|
get<T>(keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>): ActiveUserState<T> {
|
||||||
|
if (!isUserKeyDefinition(keyDefinition)) {
|
||||||
|
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
|
||||||
|
}
|
||||||
const cacheKey = this.buildCacheKey(keyDefinition);
|
const cacheKey = this.buildCacheKey(keyDefinition);
|
||||||
const existingUserState = this.cache[cacheKey];
|
const existingUserState = this.cache[cacheKey];
|
||||||
if (existingUserState != null) {
|
if (existingUserState != null) {
|
||||||
@@ -41,11 +45,11 @@ export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
|
|||||||
return newUserState;
|
return newUserState;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildCacheKey(keyDefinition: KeyDefinition<unknown>) {
|
private buildCacheKey(keyDefinition: UserKeyDefinition<unknown>) {
|
||||||
return `${this.getLocationString(keyDefinition)}_${keyDefinition.fullName}`;
|
return `${this.getLocationString(keyDefinition)}_${keyDefinition.fullName}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected buildActiveUserState<T>(keyDefinition: KeyDefinition<T>): ActiveUserState<T> {
|
protected buildActiveUserState<T>(keyDefinition: UserKeyDefinition<T>): ActiveUserState<T> {
|
||||||
return new DefaultActiveUserState<T>(
|
return new DefaultActiveUserState<T>(
|
||||||
keyDefinition,
|
keyDefinition,
|
||||||
this.accountService,
|
this.accountService,
|
||||||
@@ -53,7 +57,7 @@ export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getLocationString(keyDefinition: KeyDefinition<unknown>): string {
|
protected getLocationString(keyDefinition: UserKeyDefinition<unknown>): string {
|
||||||
return keyDefinition.stateDefinition.defaultStorageLocation;
|
return keyDefinition.stateDefinition.defaultStorageLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import { FakeStorageService } from "../../../../spec/fake-storage.service";
|
|||||||
import { AccountInfo, AccountService } from "../../../auth/abstractions/account.service";
|
import { AccountInfo, AccountService } from "../../../auth/abstractions/account.service";
|
||||||
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { KeyDefinition, userKeyBuilder } from "../key-definition";
|
|
||||||
import { StateDefinition } from "../state-definition";
|
import { StateDefinition } from "../state-definition";
|
||||||
|
import { UserKeyDefinition } from "../user-key-definition";
|
||||||
|
|
||||||
import { DefaultActiveUserState } from "./default-active-user-state";
|
import { DefaultActiveUserState } from "./default-active-user-state";
|
||||||
|
|
||||||
@@ -33,9 +33,10 @@ class TestState {
|
|||||||
|
|
||||||
const testStateDefinition = new StateDefinition("fake", "disk");
|
const testStateDefinition = new StateDefinition("fake", "disk");
|
||||||
const cleanupDelayMs = 15;
|
const cleanupDelayMs = 15;
|
||||||
const testKeyDefinition = new KeyDefinition<TestState>(testStateDefinition, "fake", {
|
const testKeyDefinition = new UserKeyDefinition<TestState>(testStateDefinition, "fake", {
|
||||||
deserializer: TestState.fromJSON,
|
deserializer: TestState.fromJSON,
|
||||||
cleanupDelayMs,
|
cleanupDelayMs,
|
||||||
|
clearOn: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("DefaultActiveUserState", () => {
|
describe("DefaultActiveUserState", () => {
|
||||||
@@ -592,7 +593,7 @@ describe("DefaultActiveUserState", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await changeActiveUser("1");
|
await changeActiveUser("1");
|
||||||
userKey = userKeyBuilder(userId, testKeyDefinition);
|
userKey = testKeyDefinition.buildKey(userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
function assertClean() {
|
function assertClean() {
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import {
|
|||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
ObservableStorageService,
|
ObservableStorageService,
|
||||||
} from "../../abstractions/storage.service";
|
} from "../../abstractions/storage.service";
|
||||||
import { KeyDefinition, userKeyBuilder } from "../key-definition";
|
|
||||||
import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options";
|
import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options";
|
||||||
|
import { UserKeyDefinition } from "../user-key-definition";
|
||||||
import { ActiveUserState, CombinedState, activeMarker } from "../user-state";
|
import { ActiveUserState, CombinedState, activeMarker } from "../user-state";
|
||||||
|
|
||||||
import { getStoredValue } from "./util";
|
import { getStoredValue } from "./util";
|
||||||
@@ -39,7 +39,7 @@ export class DefaultActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
state$: Observable<T>;
|
state$: Observable<T>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected keyDefinition: KeyDefinition<T>,
|
protected keyDefinition: UserKeyDefinition<T>,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private chosenStorageLocation: AbstractStorageService & ObservableStorageService,
|
private chosenStorageLocation: AbstractStorageService & ObservableStorageService,
|
||||||
) {
|
) {
|
||||||
@@ -61,7 +61,7 @@ export class DefaultActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
return FAKE;
|
return FAKE;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullKey = userKeyBuilder(userId, this.keyDefinition);
|
const fullKey = this.keyDefinition.buildKey(userId);
|
||||||
const data = await getStoredValue(
|
const data = await getStoredValue(
|
||||||
fullKey,
|
fullKey,
|
||||||
this.chosenStorageLocation,
|
this.chosenStorageLocation,
|
||||||
@@ -80,7 +80,7 @@ export class DefaultActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
// Null userId is already taken care of through the userChange observable above
|
// Null userId is already taken care of through the userChange observable above
|
||||||
filter((u) => u != null),
|
filter((u) => u != null),
|
||||||
// Take the userId and build the fullKey that we can now create
|
// Take the userId and build the fullKey that we can now create
|
||||||
map((userId) => [userId, userKeyBuilder(userId, this.keyDefinition)] as const),
|
map((userId) => [userId, this.keyDefinition.buildKey(userId)] as const),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Filter to only storage updates that pertain to our key
|
// Filter to only storage updates that pertain to our key
|
||||||
@@ -168,7 +168,7 @@ export class DefaultActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
throw new Error("No active user at this time.");
|
throw new Error("No active user at this time.");
|
||||||
}
|
}
|
||||||
const fullKey = userKeyBuilder(userId, this.keyDefinition);
|
const fullKey = this.keyDefinition.buildKey(userId);
|
||||||
return [
|
return [
|
||||||
userId,
|
userId,
|
||||||
fullKey,
|
fullKey,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
} from "../../abstractions/storage.service";
|
} from "../../abstractions/storage.service";
|
||||||
import { KeyDefinition } from "../key-definition";
|
import { KeyDefinition } from "../key-definition";
|
||||||
import { StateDefinition } from "../state-definition";
|
import { StateDefinition } from "../state-definition";
|
||||||
|
import { UserKeyDefinition, isUserKeyDefinition } from "../user-key-definition";
|
||||||
import { SingleUserState } from "../user-state";
|
import { SingleUserState } from "../user-state";
|
||||||
import { SingleUserStateProvider } from "../user-state.provider";
|
import { SingleUserStateProvider } from "../user-state.provider";
|
||||||
|
|
||||||
@@ -19,7 +20,13 @@ export class DefaultSingleUserStateProvider implements SingleUserStateProvider {
|
|||||||
protected readonly diskStorage: AbstractStorageService & ObservableStorageService,
|
protected readonly diskStorage: AbstractStorageService & ObservableStorageService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
get<T>(userId: UserId, keyDefinition: KeyDefinition<T>): SingleUserState<T> {
|
get<T>(
|
||||||
|
userId: UserId,
|
||||||
|
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
|
||||||
|
): SingleUserState<T> {
|
||||||
|
if (!isUserKeyDefinition(keyDefinition)) {
|
||||||
|
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
|
||||||
|
}
|
||||||
const cacheKey = this.buildCacheKey(userId, keyDefinition);
|
const cacheKey = this.buildCacheKey(userId, keyDefinition);
|
||||||
const existingUserState = this.cache[cacheKey];
|
const existingUserState = this.cache[cacheKey];
|
||||||
if (existingUserState != null) {
|
if (existingUserState != null) {
|
||||||
@@ -33,13 +40,13 @@ export class DefaultSingleUserStateProvider implements SingleUserStateProvider {
|
|||||||
return newUserState;
|
return newUserState;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildCacheKey(userId: UserId, keyDefinition: KeyDefinition<unknown>) {
|
private buildCacheKey(userId: UserId, keyDefinition: UserKeyDefinition<unknown>) {
|
||||||
return `${this.getLocationString(keyDefinition)}_${keyDefinition.fullName}_${userId}`;
|
return `${this.getLocationString(keyDefinition)}_${keyDefinition.fullName}_${userId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected buildSingleUserState<T>(
|
protected buildSingleUserState<T>(
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
keyDefinition: KeyDefinition<T>,
|
keyDefinition: UserKeyDefinition<T>,
|
||||||
): SingleUserState<T> {
|
): SingleUserState<T> {
|
||||||
return new DefaultSingleUserState<T>(
|
return new DefaultSingleUserState<T>(
|
||||||
userId,
|
userId,
|
||||||
@@ -48,7 +55,7 @@ export class DefaultSingleUserStateProvider implements SingleUserStateProvider {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getLocationString(keyDefinition: KeyDefinition<unknown>): string {
|
protected getLocationString(keyDefinition: UserKeyDefinition<unknown>): string {
|
||||||
return keyDefinition.stateDefinition.defaultStorageLocation;
|
return keyDefinition.stateDefinition.defaultStorageLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import { trackEmissions, awaitAsync } from "../../../../spec";
|
|||||||
import { FakeStorageService } from "../../../../spec/fake-storage.service";
|
import { FakeStorageService } from "../../../../spec/fake-storage.service";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { Utils } from "../../misc/utils";
|
import { Utils } from "../../misc/utils";
|
||||||
import { KeyDefinition, userKeyBuilder } from "../key-definition";
|
|
||||||
import { StateDefinition } from "../state-definition";
|
import { StateDefinition } from "../state-definition";
|
||||||
|
import { UserKeyDefinition } from "../user-key-definition";
|
||||||
|
|
||||||
import { DefaultSingleUserState } from "./default-single-user-state";
|
import { DefaultSingleUserState } from "./default-single-user-state";
|
||||||
|
|
||||||
@@ -31,12 +31,13 @@ class TestState {
|
|||||||
|
|
||||||
const testStateDefinition = new StateDefinition("fake", "disk");
|
const testStateDefinition = new StateDefinition("fake", "disk");
|
||||||
const cleanupDelayMs = 10;
|
const cleanupDelayMs = 10;
|
||||||
const testKeyDefinition = new KeyDefinition<TestState>(testStateDefinition, "fake", {
|
const testKeyDefinition = new UserKeyDefinition<TestState>(testStateDefinition, "fake", {
|
||||||
deserializer: TestState.fromJSON,
|
deserializer: TestState.fromJSON,
|
||||||
cleanupDelayMs,
|
cleanupDelayMs,
|
||||||
|
clearOn: [],
|
||||||
});
|
});
|
||||||
const userId = Utils.newGuid() as UserId;
|
const userId = Utils.newGuid() as UserId;
|
||||||
const userKey = userKeyBuilder(userId, testKeyDefinition);
|
const userKey = testKeyDefinition.buildKey(userId);
|
||||||
|
|
||||||
describe("DefaultSingleUserState", () => {
|
describe("DefaultSingleUserState", () => {
|
||||||
let diskStorageService: FakeStorageService;
|
let diskStorageService: FakeStorageService;
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import {
|
|||||||
AbstractStorageService,
|
AbstractStorageService,
|
||||||
ObservableStorageService,
|
ObservableStorageService,
|
||||||
} from "../../abstractions/storage.service";
|
} from "../../abstractions/storage.service";
|
||||||
import { KeyDefinition, userKeyBuilder } from "../key-definition";
|
|
||||||
import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options";
|
import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options";
|
||||||
|
import { UserKeyDefinition } from "../user-key-definition";
|
||||||
import { CombinedState, SingleUserState } from "../user-state";
|
import { CombinedState, SingleUserState } from "../user-state";
|
||||||
|
|
||||||
import { getStoredValue } from "./util";
|
import { getStoredValue } from "./util";
|
||||||
@@ -33,10 +33,10 @@ export class DefaultSingleUserState<T> implements SingleUserState<T> {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly userId: UserId,
|
readonly userId: UserId,
|
||||||
private keyDefinition: KeyDefinition<T>,
|
private keyDefinition: UserKeyDefinition<T>,
|
||||||
private chosenLocation: AbstractStorageService & ObservableStorageService,
|
private chosenLocation: AbstractStorageService & ObservableStorageService,
|
||||||
) {
|
) {
|
||||||
this.storageKey = userKeyBuilder(this.userId, this.keyDefinition);
|
this.storageKey = this.keyDefinition.buildKey(this.userId);
|
||||||
const initialStorageGet$ = defer(() => {
|
const initialStorageGet$ = defer(() => {
|
||||||
return getStoredValue(this.storageKey, this.chosenLocation, this.keyDefinition.deserializer);
|
return getStoredValue(this.storageKey, this.chosenLocation, this.keyDefinition.deserializer);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { DerivedStateProvider } from "../derived-state.provider";
|
|||||||
import { GlobalStateProvider } from "../global-state.provider";
|
import { GlobalStateProvider } from "../global-state.provider";
|
||||||
import { KeyDefinition } from "../key-definition";
|
import { KeyDefinition } from "../key-definition";
|
||||||
import { StateProvider } from "../state.provider";
|
import { StateProvider } from "../state.provider";
|
||||||
|
import { UserKeyDefinition } from "../user-key-definition";
|
||||||
import { ActiveUserStateProvider, SingleUserStateProvider } from "../user-state.provider";
|
import { ActiveUserStateProvider, SingleUserStateProvider } from "../user-state.provider";
|
||||||
|
|
||||||
export class DefaultStateProvider implements StateProvider {
|
export class DefaultStateProvider implements StateProvider {
|
||||||
@@ -21,7 +22,10 @@ export class DefaultStateProvider implements StateProvider {
|
|||||||
this.activeUserId$ = this.activeUserStateProvider.activeUserId$;
|
this.activeUserId$ = this.activeUserStateProvider.activeUserId$;
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserState$<T>(keyDefinition: KeyDefinition<T>, userId?: UserId): Observable<T> {
|
getUserState$<T>(
|
||||||
|
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
|
||||||
|
userId?: UserId,
|
||||||
|
): Observable<T> {
|
||||||
if (userId) {
|
if (userId) {
|
||||||
return this.getUser<T>(userId, keyDefinition).state$;
|
return this.getUser<T>(userId, keyDefinition).state$;
|
||||||
} else {
|
} else {
|
||||||
@@ -33,7 +37,7 @@ export class DefaultStateProvider implements StateProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setUserState<T>(
|
async setUserState<T>(
|
||||||
keyDefinition: KeyDefinition<T>,
|
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
|
||||||
value: T,
|
value: T,
|
||||||
userId?: UserId,
|
userId?: UserId,
|
||||||
): Promise<[UserId, T]> {
|
): Promise<[UserId, T]> {
|
||||||
|
|||||||
@@ -7,5 +7,6 @@ export { GlobalStateProvider } from "./global-state.provider";
|
|||||||
export { ActiveUserState, SingleUserState } from "./user-state";
|
export { ActiveUserState, SingleUserState } from "./user-state";
|
||||||
export { ActiveUserStateProvider, SingleUserStateProvider } from "./user-state.provider";
|
export { ActiveUserStateProvider, SingleUserStateProvider } from "./user-state.provider";
|
||||||
export { KeyDefinition } from "./key-definition";
|
export { KeyDefinition } from "./key-definition";
|
||||||
|
export { UserKeyDefinition } from "./user-key-definition";
|
||||||
|
|
||||||
export * from "./state-definitions";
|
export * from "./state-definitions";
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { UserId } from "../../types/guid";
|
|
||||||
import { StorageKey } from "../../types/state";
|
import { StorageKey } from "../../types/state";
|
||||||
import { Utils } from "../misc/utils";
|
|
||||||
|
|
||||||
|
import { array, record } from "./deserialization-helpers";
|
||||||
import { StateDefinition } from "./state-definition";
|
import { StateDefinition } from "./state-definition";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A set of options for customizing the behavior of a {@link KeyDefinition}
|
* A set of options for customizing the behavior of a {@link KeyDefinition}
|
||||||
*/
|
*/
|
||||||
type KeyDefinitionOptions<T> = {
|
export type KeyDefinitionOptions<T> = {
|
||||||
/**
|
/**
|
||||||
* A function to use to safely convert your type from json to your expected type.
|
* A function to use to safely convert your type from json to your expected type.
|
||||||
*
|
*
|
||||||
@@ -78,8 +77,7 @@ export class KeyDefinition<T> {
|
|||||||
* @param key The key to be added to the KeyDefinition
|
* @param key The key to be added to the KeyDefinition
|
||||||
* @param options The options to customize the final {@link KeyDefinition}.
|
* @param options The options to customize the final {@link KeyDefinition}.
|
||||||
* @returns A {@link KeyDefinition} initialized for arrays, the options run
|
* @returns A {@link KeyDefinition} initialized for arrays, the options run
|
||||||
* the deserializer on the provided options for each element of an array
|
* the deserializer on the provided options for each element of an array.
|
||||||
* **unless that array is null, in which case it will return an empty list.**
|
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
@@ -96,12 +94,7 @@ export class KeyDefinition<T> {
|
|||||||
) {
|
) {
|
||||||
return new KeyDefinition<T[]>(stateDefinition, key, {
|
return new KeyDefinition<T[]>(stateDefinition, key, {
|
||||||
...options,
|
...options,
|
||||||
deserializer: (jsonValue) => {
|
deserializer: array((e) => options.deserializer(e)),
|
||||||
if (jsonValue == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return jsonValue.map((v) => options.deserializer(v));
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +104,7 @@ export class KeyDefinition<T> {
|
|||||||
* @param key The key to be added to the KeyDefinition
|
* @param key The key to be added to the KeyDefinition
|
||||||
* @param options The options to customize the final {@link KeyDefinition}.
|
* @param options The options to customize the final {@link KeyDefinition}.
|
||||||
* @returns A {@link KeyDefinition} that contains a serializer that will run the provided deserializer for each
|
* @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.**
|
* value in a record and returns every key as a string.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
@@ -128,17 +121,7 @@ export class KeyDefinition<T> {
|
|||||||
) {
|
) {
|
||||||
return new KeyDefinition<Record<TKey, T>>(stateDefinition, key, {
|
return new KeyDefinition<Record<TKey, T>>(stateDefinition, key, {
|
||||||
...options,
|
...options,
|
||||||
deserializer: (jsonValue) => {
|
deserializer: record((v) => options.deserializer(v)),
|
||||||
if (jsonValue == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const output: Record<string, T> = {};
|
|
||||||
for (const key in jsonValue) {
|
|
||||||
output[key] = options.deserializer((jsonValue as Record<string, Jsonify<T>>)[key]);
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,24 +129,11 @@ export class KeyDefinition<T> {
|
|||||||
return `${this.stateDefinition.name}_${this.key}`;
|
return `${this.stateDefinition.name}_${this.key}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private get errorKeyName() {
|
protected get errorKeyName() {
|
||||||
return `${this.stateDefinition.name} > ${this.key}`;
|
return `${this.stateDefinition.name} > ${this.key}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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}
|
* Creates a {@link StorageKey}
|
||||||
* @param keyDefinition The key definition of which data the key should point to.
|
* @param keyDefinition The key definition of which data the key should point to.
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { GlobalState } from "./global-state";
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- used in docs
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- used in docs
|
||||||
import { GlobalStateProvider } from "./global-state.provider";
|
import { GlobalStateProvider } from "./global-state.provider";
|
||||||
import { KeyDefinition } from "./key-definition";
|
import { KeyDefinition } from "./key-definition";
|
||||||
|
import { UserKeyDefinition } from "./user-key-definition";
|
||||||
import { ActiveUserState, SingleUserState } from "./user-state";
|
import { ActiveUserState, SingleUserState } from "./user-state";
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- used in docs
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- used in docs
|
||||||
import { ActiveUserStateProvider, SingleUserStateProvider } from "./user-state.provider";
|
import { ActiveUserStateProvider, SingleUserStateProvider } from "./user-state.provider";
|
||||||
@@ -29,22 +30,72 @@ export abstract class StateProvider {
|
|||||||
* @param userId - The userId for which you want the state for. If not provided, the state for the currently active user will be returned.
|
* @param userId - The userId for which you want the state for. If not provided, the state for the currently active user will be returned.
|
||||||
*/
|
*/
|
||||||
getUserState$: <T>(keyDefinition: KeyDefinition<T>, userId?: UserId) => Observable<T>;
|
getUserState$: <T>(keyDefinition: KeyDefinition<T>, userId?: UserId) => Observable<T>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the state for a given key and userId.
|
* Sets the state for a given key and userId.
|
||||||
*
|
*
|
||||||
|
* @overload
|
||||||
* @param keyDefinition - The key definition for the state you want to set.
|
* @param keyDefinition - The key definition for the state you want to set.
|
||||||
* @param value - The value to set the state to.
|
* @param value - The value to set the state to.
|
||||||
* @param userId - The userId for which you want to set the state for. If not provided, the state for the currently active user will be set.
|
* @param userId - The userId for which you want to set the state for. If not provided, the state for the currently active user will be set.
|
||||||
*/
|
*/
|
||||||
setUserState: <T>(
|
abstract setUserState<T>(
|
||||||
|
keyDefinition: UserKeyDefinition<T>,
|
||||||
|
value: T,
|
||||||
|
userId?: UserId,
|
||||||
|
): Promise<[UserId, T]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the state for a given key and userId.
|
||||||
|
*
|
||||||
|
* **NOTE** Consider converting your {@link KeyDefinition} to a {@link UserKeyDefinition} for additional features.
|
||||||
|
*
|
||||||
|
* @overload
|
||||||
|
* @param keyDefinition - The key definition for the state you want to set.
|
||||||
|
* @param value - The value to set the state to.
|
||||||
|
* @param userId - The userId for which you want to set the state for. If not provided, the state for the currently active user will be set.
|
||||||
|
*/
|
||||||
|
abstract setUserState<T>(
|
||||||
keyDefinition: KeyDefinition<T>,
|
keyDefinition: KeyDefinition<T>,
|
||||||
value: T,
|
value: T,
|
||||||
userId?: UserId,
|
userId?: UserId,
|
||||||
) => Promise<[UserId, T]>;
|
): Promise<[UserId, T]>;
|
||||||
|
|
||||||
|
abstract setUserState<T>(
|
||||||
|
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
|
||||||
|
value: T,
|
||||||
|
userId?: UserId,
|
||||||
|
): Promise<[UserId, T]>;
|
||||||
|
|
||||||
/** @see{@link ActiveUserStateProvider.get} */
|
/** @see{@link ActiveUserStateProvider.get} */
|
||||||
getActive: <T>(keyDefinition: KeyDefinition<T>) => ActiveUserState<T>;
|
abstract getActive<T>(keyDefinition: UserKeyDefinition<T>): ActiveUserState<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see{@link ActiveUserStateProvider.get}
|
||||||
|
*
|
||||||
|
* **NOTE** Consider converting your {@link KeyDefinition} to a {@link UserKeyDefinition} for additional features.
|
||||||
|
*/
|
||||||
|
abstract getActive<T>(keyDefinition: KeyDefinition<T>): ActiveUserState<T>;
|
||||||
|
|
||||||
|
/** @see{@link ActiveUserStateProvider.get} */
|
||||||
|
abstract getActive<T>(keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>): ActiveUserState<T>;
|
||||||
|
|
||||||
/** @see{@link SingleUserStateProvider.get} */
|
/** @see{@link SingleUserStateProvider.get} */
|
||||||
getUser: <T>(userId: UserId, keyDefinition: KeyDefinition<T>) => SingleUserState<T>;
|
abstract getUser<T>(userId: UserId, keyDefinition: UserKeyDefinition<T>): SingleUserState<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see{@link SingleUserStateProvider.get}
|
||||||
|
*
|
||||||
|
* **NOTE** Consider converting your {@link KeyDefinition} to a {@link UserKeyDefinition} for additional features.
|
||||||
|
*/
|
||||||
|
abstract getUser<T>(userId: UserId, keyDefinition: KeyDefinition<T>): SingleUserState<T>;
|
||||||
|
|
||||||
|
/** @see{@link SingleUserStateProvider.get} */
|
||||||
|
abstract getUser<T>(
|
||||||
|
userId: UserId,
|
||||||
|
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
|
||||||
|
): SingleUserState<T>;
|
||||||
|
|
||||||
/** @see{@link GlobalStateProvider.get} */
|
/** @see{@link GlobalStateProvider.get} */
|
||||||
getGlobal: <T>(keyDefinition: KeyDefinition<T>) => GlobalState<T>;
|
getGlobal: <T>(keyDefinition: KeyDefinition<T>) => GlobalState<T>;
|
||||||
getDerived: <TFrom, TTo, TDeps extends DerivedStateDependencies>(
|
getDerived: <TFrom, TTo, TDeps extends DerivedStateDependencies>(
|
||||||
|
|||||||
149
libs/common/src/platform/state/user-key-definition.ts
Normal file
149
libs/common/src/platform/state/user-key-definition.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { UserId } from "../../types/guid";
|
||||||
|
import { StorageKey } from "../../types/state";
|
||||||
|
import { Utils } from "../misc/utils";
|
||||||
|
|
||||||
|
import { array, record } from "./deserialization-helpers";
|
||||||
|
import { KeyDefinition, KeyDefinitionOptions } from "./key-definition";
|
||||||
|
import { StateDefinition } from "./state-definition";
|
||||||
|
|
||||||
|
type ClearEvent = "lock" | "logout";
|
||||||
|
|
||||||
|
type UserKeyDefinitionOptions<T> = KeyDefinitionOptions<T> & {
|
||||||
|
clearOn: ClearEvent[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const USER_KEY_DEFINITION_MARKER: unique symbol = Symbol("UserKeyDefinition");
|
||||||
|
|
||||||
|
export function isUserKeyDefinition<T>(
|
||||||
|
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
|
||||||
|
): keyDefinition is UserKeyDefinition<T> {
|
||||||
|
return (
|
||||||
|
USER_KEY_DEFINITION_MARKER in keyDefinition &&
|
||||||
|
keyDefinition[USER_KEY_DEFINITION_MARKER] === true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserKeyDefinition<T> {
|
||||||
|
readonly [USER_KEY_DEFINITION_MARKER] = true;
|
||||||
|
/**
|
||||||
|
* A unique array of events that the state stored at this key should be cleared on.
|
||||||
|
*/
|
||||||
|
readonly clearOn: ClearEvent[];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly stateDefinition: StateDefinition,
|
||||||
|
readonly key: string,
|
||||||
|
private readonly options: UserKeyDefinitionOptions<T>,
|
||||||
|
) {
|
||||||
|
if (options.deserializer == null) {
|
||||||
|
throw new Error(`'deserializer' is a required property on key ${this.errorKeyName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.cleanupDelayMs <= 0) {
|
||||||
|
throw new Error(
|
||||||
|
`'cleanupDelayMs' must be greater than 0. Value of ${options.cleanupDelayMs} passed to key ${this.errorKeyName} `,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out repeat values
|
||||||
|
this.clearOn = Array.from(new Set(options.clearOn));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the deserializer configured for this {@link KeyDefinition}
|
||||||
|
*/
|
||||||
|
get deserializer() {
|
||||||
|
return this.options.deserializer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of milliseconds to wait before cleaning up the state after the last subscriber has unsubscribed.
|
||||||
|
*/
|
||||||
|
get cleanupDelayMs() {
|
||||||
|
return this.options.cleanupDelayMs < 0 ? 0 : this.options.cleanupDelayMs ?? 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param keyDefinition
|
||||||
|
* @returns
|
||||||
|
*
|
||||||
|
* @deprecated You should not use this to convert, just create a {@link UserKeyDefinition}
|
||||||
|
*/
|
||||||
|
static fromBaseKeyDefinition<T>(keyDefinition: KeyDefinition<T>) {
|
||||||
|
return new UserKeyDefinition<T>(keyDefinition.stateDefinition, keyDefinition.key, {
|
||||||
|
...keyDefinition["options"],
|
||||||
|
clearOn: [], // Default to not clearing
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link UserKeyDefinition} for state that is an array.
|
||||||
|
* @param stateDefinition The state definition to be added to the UserKeyDefinition
|
||||||
|
* @param key The key to be added to the KeyDefinition
|
||||||
|
* @param options The options to customize the final {@link UserKeyDefinition}.
|
||||||
|
* @returns A {@link UserKeyDefinition} initialized for arrays, the options run
|
||||||
|
* the deserializer on the provided options for each element of an array
|
||||||
|
* **unless that array is null, in which case it will return an empty list.**
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const MY_KEY = UserKeyDefinition.array<MyArrayElement>(MY_STATE, "key", {
|
||||||
|
* deserializer: (myJsonElement) => convertToElement(myJsonElement),
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
static array<T>(
|
||||||
|
stateDefinition: StateDefinition,
|
||||||
|
key: string,
|
||||||
|
// We have them provide options for the element of the array, depending on future options we add, this could get a little weird.
|
||||||
|
options: UserKeyDefinitionOptions<T>,
|
||||||
|
) {
|
||||||
|
return new UserKeyDefinition<T[]>(stateDefinition, key, {
|
||||||
|
...options,
|
||||||
|
deserializer: array((e) => options.deserializer(e)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link UserKeyDefinition} for state that is a record.
|
||||||
|
* @param stateDefinition The state definition to be added to the UserKeyDefinition
|
||||||
|
* @param key The key to be added to the KeyDefinition
|
||||||
|
* @param options The options to customize the final {@link UserKeyDefinition}.
|
||||||
|
* @returns A {@link UserKeyDefinition} 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.**
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const MY_KEY = UserKeyDefinition.record<MyRecordValue>(MY_STATE, "key", {
|
||||||
|
* deserializer: (myJsonValue) => convertToValue(myJsonValue),
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
static record<T, TKey extends string = string>(
|
||||||
|
stateDefinition: StateDefinition,
|
||||||
|
key: string,
|
||||||
|
// We have them provide options for the value of the record, depending on future options we add, this could get a little weird.
|
||||||
|
options: UserKeyDefinitionOptions<T>, // The array helper forces an initialValue of an empty record
|
||||||
|
) {
|
||||||
|
return new UserKeyDefinition<Record<TKey, T>>(stateDefinition, key, {
|
||||||
|
...options,
|
||||||
|
deserializer: record((v) => options.deserializer(v)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get fullName() {
|
||||||
|
return `${this.stateDefinition.name}_${this.key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
buildKey(userId: UserId) {
|
||||||
|
if (!Utils.isGuid(userId)) {
|
||||||
|
throw new Error("You cannot build a user key without a valid UserId");
|
||||||
|
}
|
||||||
|
return `user_${userId}_${this.stateDefinition.name}_${this.key}` as StorageKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get errorKeyName() {
|
||||||
|
return `${this.stateDefinition.name} > ${this.key}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { Observable } from "rxjs";
|
|||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
|
|
||||||
import { KeyDefinition } from "./key-definition";
|
import { KeyDefinition } from "./key-definition";
|
||||||
|
import { UserKeyDefinition } from "./user-key-definition";
|
||||||
import { ActiveUserState, SingleUserState } from "./user-state";
|
import { ActiveUserState, SingleUserState } from "./user-state";
|
||||||
|
|
||||||
/** A provider for getting an implementation of state scoped to a given key and userId */
|
/** A provider for getting an implementation of state scoped to a given key and userId */
|
||||||
@@ -10,10 +11,25 @@ export abstract class SingleUserStateProvider {
|
|||||||
/**
|
/**
|
||||||
* Gets a {@link SingleUserState} scoped to the given {@link KeyDefinition} and {@link UserId}
|
* Gets a {@link SingleUserState} scoped to the given {@link KeyDefinition} and {@link UserId}
|
||||||
*
|
*
|
||||||
|
* **NOTE** Consider converting your {@link KeyDefinition} to a {@link UserKeyDefinition} for additional features.
|
||||||
|
*
|
||||||
* @param userId - The {@link UserId} for which you want the user state for.
|
* @param userId - The {@link UserId} for which you want the user state for.
|
||||||
* @param keyDefinition - The {@link KeyDefinition} for which you want the user state for.
|
* @param keyDefinition - The {@link KeyDefinition} for which you want the user state for.
|
||||||
*/
|
*/
|
||||||
get: <T>(userId: UserId, keyDefinition: KeyDefinition<T>) => SingleUserState<T>;
|
abstract get<T>(userId: UserId, keyDefinition: KeyDefinition<T>): SingleUserState<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a {@link SingleUserState} scoped to the given {@link UserKeyDefinition} and {@link UserId}
|
||||||
|
*
|
||||||
|
* @param userId - The {@link UserId} for which you want the user state for.
|
||||||
|
* @param userKeyDefinition - The {@link UserKeyDefinition} for which you want the user state for.
|
||||||
|
*/
|
||||||
|
abstract get<T>(userId: UserId, userKeyDefinition: UserKeyDefinition<T>): SingleUserState<T>;
|
||||||
|
|
||||||
|
abstract get<T>(
|
||||||
|
userId: UserId,
|
||||||
|
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
|
||||||
|
): SingleUserState<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A provider for getting an implementation of state scoped to a given key, but always pointing
|
/** A provider for getting an implementation of state scoped to a given key, but always pointing
|
||||||
@@ -24,11 +40,24 @@ export abstract class ActiveUserStateProvider {
|
|||||||
* Convenience re-emission of active user ID from {@link AccountService.activeAccount$}
|
* Convenience re-emission of active user ID from {@link AccountService.activeAccount$}
|
||||||
*/
|
*/
|
||||||
activeUserId$: Observable<UserId | undefined>;
|
activeUserId$: Observable<UserId | undefined>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a {@link ActiveUserState} scoped to the given {@link KeyDefinition}, but updates when active user changes such
|
* Gets a {@link ActiveUserState} scoped to the given {@link KeyDefinition}, but updates when active user changes such
|
||||||
* that the emitted values always represents the state for the currently active user.
|
* that the emitted values always represents the state for the currently active user.
|
||||||
*
|
*
|
||||||
|
* @param keyDefinition - The {@link UserKeyDefinition} for which you want the user state for.
|
||||||
|
*/
|
||||||
|
abstract get<T>(userKeyDefinition: UserKeyDefinition<T>): ActiveUserState<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a {@link ActiveUserState} scoped to the given {@link KeyDefinition}, but updates when active user changes such
|
||||||
|
* that the emitted values always represents the state for the currently active user.
|
||||||
|
*
|
||||||
|
* **NOTE** Consider converting your {@link KeyDefinition} to a {@link UserKeyDefinition} for additional features.
|
||||||
|
*
|
||||||
* @param keyDefinition - The {@link KeyDefinition} for which you want the user state for.
|
* @param keyDefinition - The {@link KeyDefinition} for which you want the user state for.
|
||||||
*/
|
*/
|
||||||
get: <T>(keyDefinition: KeyDefinition<T>) => ActiveUserState<T>;
|
abstract get<T>(keyDefinition: KeyDefinition<T>): ActiveUserState<T>;
|
||||||
|
|
||||||
|
abstract get<T>(keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>): ActiveUserState<T>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user