1
0
mirror of https://github.com/bitwarden/browser.git synced 2024-09-14 02:08:50 +02:00

Require lifetime specification of user-scoped data (#8669)

* Require lifetime specification of user-scoped data

* Decouple tests for different classes

This coupling assumed constant interfaces with classes that isn't a guarantee and requires significant acrobatics to make types work, now that key definitions are not a consistent base.

* Fix types
This commit is contained in:
Matt Gibson 2024-06-07 03:33:26 -04:00 committed by GitHub
parent 9f10569e9c
commit 96d4312b82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 185 additions and 296 deletions

View File

@ -15,8 +15,6 @@ import {
DerivedStateProvider,
UserKeyDefinition,
} from "../src/platform/state";
// eslint-disable-next-line import/no-restricted-paths -- Needed to type check similarly to the real state providers
import { isUserKeyDefinition } from "../src/platform/state/user-key-definition";
import { UserId } from "../src/types/guid";
import { DerivedStateDependencies } from "../src/types/state";
@ -71,37 +69,28 @@ export class FakeSingleUserStateProvider implements SingleUserStateProvider {
mock = mock<SingleUserStateProvider>();
establishedMocks: Map<string, FakeSingleUserState<unknown>> = new Map();
states: Map<string, SingleUserState<unknown>> = new Map();
get<T>(
userId: UserId,
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
): SingleUserState<T> {
this.mock.get(userId, keyDefinition);
if (keyDefinition instanceof KeyDefinition) {
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
}
const cacheKey = `${keyDefinition.fullName}_${keyDefinition.stateDefinition.defaultStorageLocation}_${userId}`;
get<T>(userId: UserId, userKeyDefinition: UserKeyDefinition<T>): SingleUserState<T> {
this.mock.get(userId, userKeyDefinition);
const cacheKey = `${userKeyDefinition.fullName}_${userKeyDefinition.stateDefinition.defaultStorageLocation}_${userId}`;
let result = this.states.get(cacheKey);
if (result == null) {
let fake: FakeSingleUserState<T>;
// Look for established mock
if (this.establishedMocks.has(keyDefinition.key)) {
fake = this.establishedMocks.get(keyDefinition.key) as FakeSingleUserState<T>;
if (this.establishedMocks.has(userKeyDefinition.key)) {
fake = this.establishedMocks.get(userKeyDefinition.key) as FakeSingleUserState<T>;
} else {
fake = new FakeSingleUserState<T>(userId);
}
fake.keyDefinition = keyDefinition;
fake.keyDefinition = userKeyDefinition;
result = fake;
this.states.set(cacheKey, result);
}
return result as SingleUserState<T>;
}
getFake<T>(
userId: UserId,
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
): FakeSingleUserState<T> {
return this.get(userId, keyDefinition) as FakeSingleUserState<T>;
getFake<T>(userId: UserId, userKeyDefinition: UserKeyDefinition<T>): FakeSingleUserState<T> {
return this.get(userId, userKeyDefinition) as FakeSingleUserState<T>;
}
mockFor<T>(userId: UserId, keyDefinitionKey: string, initialValue?: T): FakeSingleUserState<T> {
@ -122,28 +111,25 @@ export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
this.activeUserId$ = accountService.activeAccountSubject.asObservable().pipe(map((a) => a?.id));
}
get<T>(keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>): ActiveUserState<T> {
if (keyDefinition instanceof KeyDefinition) {
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
}
const cacheKey = `${keyDefinition.fullName}_${keyDefinition.stateDefinition.defaultStorageLocation}`;
get<T>(userKeyDefinition: UserKeyDefinition<T>): ActiveUserState<T> {
const cacheKey = `${userKeyDefinition.fullName}_${userKeyDefinition.stateDefinition.defaultStorageLocation}`;
let result = this.states.get(cacheKey);
if (result == null) {
// Look for established mock
if (this.establishedMocks.has(keyDefinition.key)) {
result = this.establishedMocks.get(keyDefinition.key);
if (this.establishedMocks.has(userKeyDefinition.key)) {
result = this.establishedMocks.get(userKeyDefinition.key);
} else {
result = new FakeActiveUserState<T>(this.accountService);
}
result.keyDefinition = keyDefinition;
result.keyDefinition = userKeyDefinition;
this.states.set(cacheKey, result);
}
return result as ActiveUserState<T>;
}
getFake<T>(keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>): FakeActiveUserState<T> {
return this.get(keyDefinition) as FakeActiveUserState<T>;
getFake<T>(userKeyDefinition: UserKeyDefinition<T>): FakeActiveUserState<T> {
return this.get(userKeyDefinition) as FakeActiveUserState<T>;
}
mockFor<T>(keyDefinitionKey: string, initialValue?: T): FakeActiveUserState<T> {
@ -159,70 +145,56 @@ export class FakeActiveUserStateProvider implements ActiveUserStateProvider {
export class FakeStateProvider implements StateProvider {
mock = mock<StateProvider>();
getUserState$<T>(
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
userId?: UserId,
): Observable<T> {
if (isUserKeyDefinition(keyDefinition)) {
this.mock.getUserState$(keyDefinition, userId);
} else {
this.mock.getUserState$(keyDefinition, userId);
}
getUserState$<T>(userKeyDefinition: UserKeyDefinition<T>, userId?: UserId): Observable<T> {
this.mock.getUserState$(userKeyDefinition, userId);
if (userId) {
return this.getUser<T>(userId, keyDefinition).state$;
return this.getUser<T>(userId, userKeyDefinition).state$;
}
return this.getActive(keyDefinition).state$;
return this.getActive(userKeyDefinition).state$;
}
getUserStateOrDefault$<T>(
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
userKeyDefinition: UserKeyDefinition<T>,
config: { userId: UserId | undefined; defaultValue?: T },
): Observable<T> {
const { userId, defaultValue = null } = config;
if (isUserKeyDefinition(keyDefinition)) {
this.mock.getUserStateOrDefault$(keyDefinition, config);
} else {
this.mock.getUserStateOrDefault$(keyDefinition, config);
}
this.mock.getUserStateOrDefault$(userKeyDefinition, config);
if (userId) {
return this.getUser<T>(userId, keyDefinition).state$;
return this.getUser<T>(userId, userKeyDefinition).state$;
}
return this.activeUserId$.pipe(
take(1),
switchMap((userId) =>
userId != null ? this.getUser(userId, keyDefinition).state$ : of(defaultValue),
userId != null ? this.getUser(userId, userKeyDefinition).state$ : of(defaultValue),
),
);
}
async setUserState<T>(
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
userKeyDefinition: UserKeyDefinition<T>,
value: T,
userId?: UserId,
): Promise<[UserId, T]> {
await this.mock.setUserState(keyDefinition, value, userId);
await this.mock.setUserState(userKeyDefinition, value, userId);
if (userId) {
return [userId, await this.getUser(userId, keyDefinition).update(() => value)];
return [userId, await this.getUser(userId, userKeyDefinition).update(() => value)];
} else {
return await this.getActive(keyDefinition).update(() => value);
return await this.getActive(userKeyDefinition).update(() => value);
}
}
getActive<T>(keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>): ActiveUserState<T> {
return this.activeUser.get(keyDefinition);
getActive<T>(userKeyDefinition: UserKeyDefinition<T>): ActiveUserState<T> {
return this.activeUser.get(userKeyDefinition);
}
getGlobal<T>(keyDefinition: KeyDefinition<T>): GlobalState<T> {
return this.global.get(keyDefinition);
}
getUser<T>(
userId: UserId,
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
): SingleUserState<T> {
return this.singleUser.get(userId, keyDefinition);
getUser<T>(userId: UserId, userKeyDefinition: UserKeyDefinition<T>): SingleUserState<T> {
return this.singleUser.get(userId, userKeyDefinition);
}
getDerived<TFrom, TTo, TDeps extends DerivedStateDependencies>(

View File

@ -2,8 +2,7 @@ import { Observable, distinctUntilChanged, map } from "rxjs";
import { AccountService } from "../../../auth/abstractions/account.service";
import { UserId } from "../../../types/guid";
import { KeyDefinition } from "../key-definition";
import { UserKeyDefinition, isUserKeyDefinition } from "../user-key-definition";
import { UserKeyDefinition } from "../user-key-definition";
import { ActiveUserState } from "../user-state";
import { ActiveUserStateProvider, SingleUserStateProvider } from "../user-state.provider";
@ -23,11 +22,7 @@ export class DefaultActiveUserStateProvider implements ActiveUserStateProvider {
);
}
get<T>(keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>): ActiveUserState<T> {
if (!isUserKeyDefinition(keyDefinition)) {
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
}
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.

View File

@ -1,8 +1,7 @@
import { UserId } from "../../../types/guid";
import { StorageServiceProvider } from "../../services/storage-service.provider";
import { KeyDefinition } from "../key-definition";
import { StateEventRegistrarService } from "../state-event-registrar.service";
import { UserKeyDefinition, isUserKeyDefinition } from "../user-key-definition";
import { UserKeyDefinition } from "../user-key-definition";
import { SingleUserState } from "../user-state";
import { SingleUserStateProvider } from "../user-state.provider";
@ -16,13 +15,7 @@ export class DefaultSingleUserStateProvider implements SingleUserStateProvider {
private readonly stateEventRegistrarService: StateEventRegistrarService,
) {}
get<T>(
userId: UserId,
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
): SingleUserState<T> {
if (!isUserKeyDefinition(keyDefinition)) {
keyDefinition = UserKeyDefinition.fromBaseKeyDefinition(keyDefinition);
}
get<T>(userId: UserId, keyDefinition: UserKeyDefinition<T>): SingleUserState<T> {
const [location, storageService] = this.storageServiceProvider.get(
keyDefinition.stateDefinition.defaultStorageLocation,
keyDefinition.stateDefinition.storageLocationOverrides,

View File

@ -17,6 +17,7 @@ import { UserId } from "../../../types/guid";
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";
@ -52,12 +53,12 @@ describe("DefaultStateProvider", () => {
describe.each([
[
"getUserState$",
(keyDefinition: KeyDefinition<string>, userId?: UserId) =>
(keyDefinition: UserKeyDefinition<string>, userId?: UserId) =>
sut.getUserState$(keyDefinition, userId),
],
[
"getUserStateOrDefault$",
(keyDefinition: KeyDefinition<string>, userId?: UserId) =>
(keyDefinition: UserKeyDefinition<string>, userId?: UserId) =>
sut.getUserStateOrDefault$(keyDefinition, { userId: userId }),
],
])(
@ -65,7 +66,7 @@ describe("DefaultStateProvider", () => {
(
_testName: string,
methodUnderTest: (
keyDefinition: KeyDefinition<string>,
keyDefinition: UserKeyDefinition<string>,
userId?: UserId,
) => Observable<string>,
) => {
@ -75,9 +76,14 @@ describe("DefaultStateProvider", () => {
name: "name",
status: AuthenticationStatus.LoggedOut,
};
const keyDefinition = new KeyDefinition<string>(new StateDefinition("test", "disk"), "test", {
deserializer: (s) => s,
});
const keyDefinition = new UserKeyDefinition<string>(
new StateDefinition("test", "disk"),
"test",
{
deserializer: (s) => s,
clearOn: [],
},
);
it("should follow the specified user if userId is provided", async () => {
const state = singleUserStateProvider.getFake(userId, keyDefinition);
@ -125,9 +131,14 @@ describe("DefaultStateProvider", () => {
name: "name",
status: AuthenticationStatus.LoggedOut,
};
const keyDefinition = new KeyDefinition<string>(new StateDefinition("test", "disk"), "test", {
deserializer: (s) => s,
});
const keyDefinition = new UserKeyDefinition<string>(
new StateDefinition("test", "disk"),
"test",
{
deserializer: (s) => s,
clearOn: [],
},
);
it("should not emit any values until a truthy user id is supplied", async () => {
accountService.activeAccountSubject.next(null);
@ -149,9 +160,14 @@ describe("DefaultStateProvider", () => {
});
describe("getUserStateOrDefault$", () => {
const keyDefinition = new KeyDefinition<string>(new StateDefinition("test", "disk"), "test", {
deserializer: (s) => s,
});
const keyDefinition = new UserKeyDefinition<string>(
new StateDefinition("test", "disk"),
"test",
{
deserializer: (s) => s,
clearOn: [],
},
);
it("should emit default value if no userId supplied and first active user id emission in falsy", async () => {
accountService.activeAccountSubject.next(null);
@ -168,9 +184,14 @@ describe("DefaultStateProvider", () => {
});
describe("setUserState", () => {
const keyDefinition = new KeyDefinition<string>(new StateDefinition("test", "disk"), "test", {
deserializer: (s) => s,
});
const keyDefinition = new UserKeyDefinition<string>(
new StateDefinition("test", "disk"),
"test",
{
deserializer: (s) => s,
clearOn: [],
},
);
it("should set the state for the active user if no userId is provided", async () => {
const value = "value";
@ -202,8 +223,9 @@ describe("DefaultStateProvider", () => {
});
it("should bind the activeUserStateProvider", () => {
const keyDefinition = new KeyDefinition(new StateDefinition("test", "disk"), "test", {
const keyDefinition = new UserKeyDefinition(new StateDefinition("test", "disk"), "test", {
deserializer: () => null,
clearOn: [],
});
const existing = activeUserStateProvider.get(keyDefinition);
const actual = sut.getActive(keyDefinition);
@ -212,8 +234,9 @@ describe("DefaultStateProvider", () => {
it("should bind the singleUserStateProvider", () => {
const userId = "user" as UserId;
const keyDefinition = new KeyDefinition(new StateDefinition("test", "disk"), "test", {
const keyDefinition = new UserKeyDefinition(new StateDefinition("test", "disk"), "test", {
deserializer: () => null,
clearOn: [],
});
const existing = singleUserStateProvider.get(userId, keyDefinition);
const actual = sut.getUser(userId, keyDefinition);

View File

@ -6,7 +6,6 @@ import { DeriveDefinition } from "../derive-definition";
import { DerivedState } from "../derived-state";
import { DerivedStateProvider } from "../derived-state.provider";
import { GlobalStateProvider } from "../global-state.provider";
import { KeyDefinition } from "../key-definition";
import { StateProvider } from "../state.provider";
import { UserKeyDefinition } from "../user-key-definition";
import { ActiveUserStateProvider, SingleUserStateProvider } from "../user-state.provider";
@ -22,47 +21,44 @@ export class DefaultStateProvider implements StateProvider {
this.activeUserId$ = this.activeUserStateProvider.activeUserId$;
}
getUserState$<T>(
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
userId?: UserId,
): Observable<T> {
getUserState$<T>(userKeyDefinition: UserKeyDefinition<T>, userId?: UserId): Observable<T> {
if (userId) {
return this.getUser<T>(userId, keyDefinition).state$;
return this.getUser<T>(userId, userKeyDefinition).state$;
} else {
return this.activeUserId$.pipe(
filter((userId) => userId != null), // Filter out null-ish user ids since we can't get state for a null user id
take(1),
switchMap((userId) => this.getUser<T>(userId, keyDefinition).state$),
switchMap((userId) => this.getUser<T>(userId, userKeyDefinition).state$),
);
}
}
getUserStateOrDefault$<T>(
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
userKeyDefinition: UserKeyDefinition<T>,
config: { userId: UserId | undefined; defaultValue?: T },
): Observable<T> {
const { userId, defaultValue = null } = config;
if (userId) {
return this.getUser<T>(userId, keyDefinition).state$;
return this.getUser<T>(userId, userKeyDefinition).state$;
} else {
return this.activeUserId$.pipe(
take(1),
switchMap((userId) =>
userId != null ? this.getUser<T>(userId, keyDefinition).state$ : of(defaultValue),
userId != null ? this.getUser<T>(userId, userKeyDefinition).state$ : of(defaultValue),
),
);
}
}
async setUserState<T>(
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
userKeyDefinition: UserKeyDefinition<T>,
value: T,
userId?: UserId,
): Promise<[UserId, T]> {
if (userId) {
return [userId, await this.getUser<T>(userId, keyDefinition).update(() => value)];
return [userId, await this.getUser<T>(userId, userKeyDefinition).update(() => value)];
} else {
return await this.getActive<T>(keyDefinition).update(() => value);
return await this.getActive<T>(userKeyDefinition).update(() => value);
}
}

View File

@ -7,6 +7,7 @@ import { StorageServiceProvider } from "../../services/storage-service.provider"
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";
@ -41,94 +42,132 @@ describe("Specific State Providers", () => {
const fakeDiskStateDefinition = new StateDefinition("fake", "disk");
const fakeAlternateDiskStateDefinition = new StateDefinition("fakeAlternate", "disk");
const fakeMemoryStateDefinition = new StateDefinition("fake", "memory");
const fakeDiskKeyDefinition = new KeyDefinition<boolean>(fakeDiskStateDefinition, "fake", {
deserializer: (b) => b,
});
const fakeAlternateKeyDefinition = new KeyDefinition<boolean>(
fakeAlternateDiskStateDefinition,
"fake",
{
const makeKeyDefinition = (stateDefinition: StateDefinition, key: string) =>
new KeyDefinition<boolean>(stateDefinition, key, {
deserializer: (b) => b,
},
);
const fakeMemoryKeyDefinition = new KeyDefinition<boolean>(fakeMemoryStateDefinition, "fake", {
deserializer: (b) => b,
});
const fakeDiskKeyDefinitionAlternate = new KeyDefinition<boolean>(
fakeDiskStateDefinition,
"fakeAlternate",
{
});
const makeUserKeyDefinition = (stateDefinition: StateDefinition, key: string) =>
new UserKeyDefinition<boolean>(stateDefinition, key, {
deserializer: (b) => b,
clearOn: [],
});
const keyDefinitions = {
disk: {
keyDefinition: makeKeyDefinition(fakeDiskStateDefinition, "fake"),
userKeyDefinition: makeUserKeyDefinition(fakeDiskStateDefinition, "fake"),
altKeyDefinition: makeKeyDefinition(fakeDiskStateDefinition, "fakeAlternate"),
altUserKeyDefinition: makeUserKeyDefinition(fakeDiskStateDefinition, "fakeAlternate"),
},
);
memory: {
keyDefinition: makeKeyDefinition(fakeMemoryStateDefinition, "fake"),
userKeyDefinition: makeUserKeyDefinition(fakeMemoryStateDefinition, "fake"),
},
alternateDisk: {
keyDefinition: makeKeyDefinition(fakeAlternateDiskStateDefinition, "fake"),
userKeyDefinition: makeUserKeyDefinition(fakeAlternateDiskStateDefinition, "fake"),
},
};
const globalAndSingle = [
{
getMethod: (keyDefinition: KeyDefinition<boolean>) => globalSut.get(keyDefinition),
expectedInstance: DefaultGlobalState,
},
{
// Use a static user id so that it has the same signature as the rest and then write special tests
// handling differing user id
getMethod: (keyDefinition: KeyDefinition<boolean>) => singleSut.get(fakeUser1, keyDefinition),
expectedInstance: DefaultSingleUserState,
},
];
describe("active provider", () => {
it("returns a DefaultActiveUserState", () => {
const state = activeSut.get(keyDefinitions.disk.userKeyDefinition);
describe.each([
{
getMethod: (keyDefinition: KeyDefinition<boolean>) => activeSut.get(keyDefinition),
expectedInstance: DefaultActiveUserState,
},
...globalAndSingle,
])("common behavior %s", ({ getMethod, expectedInstance }) => {
it("returns expected instance", () => {
const state = getMethod(fakeDiskKeyDefinition);
expect(state).toBeTruthy();
expect(state).toBeInstanceOf(expectedInstance);
expect(state).toBeInstanceOf(DefaultActiveUserState);
});
it("returns different instances when the storage location differs", () => {
const stateDisk = getMethod(fakeDiskKeyDefinition);
const stateMemory = getMethod(fakeMemoryKeyDefinition);
const stateDisk = activeSut.get(keyDefinitions.disk.userKeyDefinition);
const stateMemory = activeSut.get(keyDefinitions.memory.userKeyDefinition);
expect(stateDisk).not.toStrictEqual(stateMemory);
});
it("returns different instances when the state name differs", () => {
const state = getMethod(fakeDiskKeyDefinition);
const stateAlt = getMethod(fakeAlternateKeyDefinition);
const state = activeSut.get(keyDefinitions.disk.userKeyDefinition);
const stateAlt = activeSut.get(keyDefinitions.alternateDisk.userKeyDefinition);
expect(state).not.toStrictEqual(stateAlt);
});
it("returns different instances when the key differs", () => {
const state = getMethod(fakeDiskKeyDefinition);
const stateAlt = getMethod(fakeDiskKeyDefinitionAlternate);
const state = activeSut.get(keyDefinitions.disk.userKeyDefinition);
const stateAlt = activeSut.get(keyDefinitions.disk.altUserKeyDefinition);
expect(state).not.toStrictEqual(stateAlt);
});
});
describe.each(globalAndSingle)("Global And Single Behavior", ({ getMethod }) => {
it("returns cached instance on repeated request", () => {
const stateFirst = getMethod(fakeDiskKeyDefinition);
const stateCached = getMethod(fakeDiskKeyDefinition);
expect(stateFirst).toStrictEqual(stateCached);
});
});
describe("single provider", () => {
it("returns a DefaultSingleUserState", () => {
const state = singleSut.get(fakeUser1, keyDefinitions.disk.userKeyDefinition);
expect(state).toBeInstanceOf(DefaultSingleUserState);
});
it("returns different instances when the storage location differs", () => {
const stateDisk = singleSut.get(fakeUser1, keyDefinitions.disk.userKeyDefinition);
const stateMemory = singleSut.get(fakeUser1, keyDefinitions.memory.userKeyDefinition);
expect(stateDisk).not.toStrictEqual(stateMemory);
});
it("returns different instances when the state name differs", () => {
const state = singleSut.get(fakeUser1, keyDefinitions.disk.userKeyDefinition);
const stateAlt = singleSut.get(fakeUser1, keyDefinitions.alternateDisk.userKeyDefinition);
expect(state).not.toStrictEqual(stateAlt);
});
it("returns different instances when the key differs", () => {
const state = singleSut.get(fakeUser1, keyDefinitions.disk.userKeyDefinition);
const stateAlt = singleSut.get(fakeUser1, keyDefinitions.disk.altUserKeyDefinition);
expect(state).not.toStrictEqual(stateAlt);
});
describe("DefaultSingleUserStateProvider only behavior", () => {
const fakeUser2 = "00000000-0000-1000-a000-000000000002" as UserId;
it("returns different instances when the user id differs", () => {
const user1State = singleSut.get(fakeUser1, fakeDiskKeyDefinition);
const user2State = singleSut.get(fakeUser2, fakeDiskKeyDefinition);
const user1State = singleSut.get(fakeUser1, keyDefinitions.disk.userKeyDefinition);
const user2State = singleSut.get(fakeUser2, keyDefinitions.disk.userKeyDefinition);
expect(user1State).not.toStrictEqual(user2State);
});
it("returns an instance with the userId property corresponding to the user id passed in", () => {
const userState = singleSut.get(fakeUser1, fakeDiskKeyDefinition);
const userState = singleSut.get(fakeUser1, keyDefinitions.disk.userKeyDefinition);
expect(userState.userId).toBe(fakeUser1);
});
it("returns cached instance on repeated request", () => {
const stateFirst = singleSut.get(fakeUser1, keyDefinitions.disk.userKeyDefinition);
const stateCached = singleSut.get(fakeUser1, keyDefinitions.disk.userKeyDefinition);
expect(stateFirst).toStrictEqual(stateCached);
});
});
describe("global provider", () => {
it("returns a DefaultGlobalState", () => {
const state = globalSut.get(keyDefinitions.disk.keyDefinition);
expect(state).toBeInstanceOf(DefaultGlobalState);
});
it("returns different instances when the storage location differs", () => {
const stateDisk = globalSut.get(keyDefinitions.disk.keyDefinition);
const stateMemory = globalSut.get(keyDefinitions.memory.keyDefinition);
expect(stateDisk).not.toStrictEqual(stateMemory);
});
it("returns different instances when the state name differs", () => {
const state = globalSut.get(keyDefinitions.disk.keyDefinition);
const stateAlt = globalSut.get(keyDefinitions.alternateDisk.keyDefinition);
expect(state).not.toStrictEqual(stateAlt);
});
it("returns different instances when the key differs", () => {
const state = globalSut.get(keyDefinitions.disk.keyDefinition);
const stateAlt = globalSut.get(keyDefinitions.disk.altKeyDefinition);
expect(state).not.toStrictEqual(stateAlt);
});
it("returns cached instance on repeated request", () => {
const stateFirst = globalSut.get(keyDefinitions.disk.keyDefinition);
const stateCached = globalSut.get(keyDefinitions.disk.keyDefinition);
expect(stateFirst).toStrictEqual(stateCached);
});
});
});

View File

@ -21,22 +21,6 @@ export abstract class StateProvider {
/** @see{@link ActiveUserStateProvider.activeUserId$} */
abstract activeUserId$: Observable<UserId | undefined>;
/**
* Gets a state observable for a given key and userId.
*
* @remarks If userId is falsy the observable returned will attempt to point to the currently active user _and not update if the active user changes_.
* This is different to how `getActive` works and more similar to `getUser` for whatever user happens to be active at the time of the call.
* If no user happens to be active at the time this method is called with a falsy userId then this observable will not emit a value until
* a user becomes active. If you are not confident a user is active at the time this method is called, you may want to pipe a call to `timeout`
* or instead call {@link getUserStateOrDefault$} and supply a value you would rather have given in the case of no passed in userId and no active user.
*
* @note consider converting your {@link KeyDefinition} to a {@link UserKeyDefinition} for additional features.
*
* @param keyDefinition - The key definition for the state you want to get.
* @param userId - The userId for which you want the state for. If not provided, the state for the currently active user will be returned.
*/
abstract getUserState$<T>(keyDefinition: KeyDefinition<T>, userId?: UserId): Observable<T>;
/**
* Gets a state observable for a given key and userId.
*
@ -51,23 +35,6 @@ export abstract class StateProvider {
*/
abstract getUserState$<T>(keyDefinition: UserKeyDefinition<T>, userId?: UserId): Observable<T>;
/**
* Gets a state observable for a given key and userId
*
* @remarks If userId is falsy the observable return will first attempt to point to the currently active user but will not follow subsequent active user changes,
* if there is no immediately available active user, then it will fallback to returning a default value in an observable that immediately completes.
*
* @note consider converting your {@link KeyDefinition} to a {@link UserKeyDefinition} for additional features.
*
* @param keyDefinition - The key definition for the state you want to get.
* @param config.userId - The userId for which you want the state for. If not provided, the state for the currently active user will be returned.
* @param config.defaultValue - The default value that should be wrapped in an observable if no active user is immediately available and no truthy userId is passed in.
*/
abstract getUserStateOrDefault$<T>(
keyDefinition: KeyDefinition<T>,
config: { userId: UserId | undefined; defaultValue?: T },
): Observable<T>;
/**
* Gets a state observable for a given key and userId
*
@ -97,56 +64,11 @@ export abstract class StateProvider {
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>,
value: T,
userId?: UserId,
): Promise<[UserId, T]>;
abstract setUserState<T>(
keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>,
value: T,
userId?: UserId,
): Promise<[UserId, T]>;
/** @see{@link ActiveUserStateProvider.get} */
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>;
abstract getActive<T>(userKeyDefinition: UserKeyDefinition<T>): ActiveUserState<T>;
/** @see{@link SingleUserStateProvider.get} */
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>;
abstract getUser<T>(userId: UserId, userKeyDefinition: UserKeyDefinition<T>): SingleUserState<T>;
/** @see{@link GlobalStateProvider.get} */
abstract getGlobal<T>(keyDefinition: KeyDefinition<T>): GlobalState<T>;

View File

@ -3,7 +3,7 @@ import { StorageKey } from "../../types/state";
import { Utils } from "../misc/utils";
import { array, record } from "./deserialization-helpers";
import { KeyDefinition, KeyDefinitionOptions } from "./key-definition";
import { KeyDefinitionOptions } from "./key-definition";
import { StateDefinition } from "./state-definition";
export type ClearEvent = "lock" | "logout";
@ -14,15 +14,6 @@ export type UserKeyDefinitionOptions<T> = KeyDefinitionOptions<T> & {
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;
/**
@ -63,20 +54,6 @@ export class UserKeyDefinition<T> {
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

View File

@ -2,22 +2,11 @@ import { Observable } from "rxjs";
import { UserId } from "../../types/guid";
import { KeyDefinition } from "./key-definition";
import { UserKeyDefinition } from "./user-key-definition";
import { ActiveUserState, SingleUserState } from "./user-state";
/** A provider for getting an implementation of state scoped to a given key and userId */
export abstract class SingleUserStateProvider {
/**
* 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 keyDefinition - The {@link KeyDefinition} for which you want the user state for.
*/
abstract get<T>(userId: UserId, keyDefinition: KeyDefinition<T>): SingleUserState<T>;
/**
* Gets a {@link SingleUserState} scoped to the given {@link UserKeyDefinition} and {@link UserId}
*
@ -25,11 +14,6 @@ export abstract class SingleUserStateProvider {
* @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
@ -48,16 +32,4 @@ export abstract class ActiveUserStateProvider {
* @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.
*/
abstract get<T>(keyDefinition: KeyDefinition<T>): ActiveUserState<T>;
abstract get<T>(keyDefinition: KeyDefinition<T> | UserKeyDefinition<T>): ActiveUserState<T>;
}