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:
parent
9f10569e9c
commit
96d4312b82
@ -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>(
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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>;
|
||||
|
@ -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
|
||||
|
@ -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>;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user