mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-28 12:45:45 +01:00
ActiveUserState Update should return the userId of the impacted user. (#7869)
This allows us to ensure that linked updates all go to the same user without risking active account changes in the middle of an operation.
This commit is contained in:
parent
78730ff18a
commit
4c051f8d7f
@ -147,11 +147,15 @@ export class FakeStateProvider implements StateProvider {
|
|||||||
return this.getActive<T>(keyDefinition).state$;
|
return this.getActive<T>(keyDefinition).state$;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setUserState<T>(keyDefinition: KeyDefinition<T>, value: T, userId?: UserId): Promise<void> {
|
async setUserState<T>(
|
||||||
|
keyDefinition: KeyDefinition<T>,
|
||||||
|
value: T,
|
||||||
|
userId?: UserId,
|
||||||
|
): Promise<[UserId, T]> {
|
||||||
if (userId) {
|
if (userId) {
|
||||||
await this.getUser(userId, keyDefinition).update(() => value);
|
return [userId, await this.getUser(userId, keyDefinition).update(() => value)];
|
||||||
} else {
|
} else {
|
||||||
await this.getActive(keyDefinition).update(() => value);
|
return await this.getActive(keyDefinition).update(() => value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,7 +170,7 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
async update<TCombine>(
|
async update<TCombine>(
|
||||||
configureState: (state: T, dependency: TCombine) => T,
|
configureState: (state: T, dependency: TCombine) => T,
|
||||||
options?: StateUpdateOptions<T, TCombine>,
|
options?: StateUpdateOptions<T, TCombine>,
|
||||||
): Promise<T> {
|
): Promise<[UserId, T]> {
|
||||||
options = populateOptionsWithDefault(options);
|
options = populateOptionsWithDefault(options);
|
||||||
const current = await firstValueFrom(this.state$.pipe(timeout(options.msTimeout)));
|
const current = await firstValueFrom(this.state$.pipe(timeout(options.msTimeout)));
|
||||||
const combinedDependencies =
|
const combinedDependencies =
|
||||||
@ -178,12 +178,12 @@ export class FakeActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
? await firstValueFrom(options.combineLatestWith.pipe(timeout(options.msTimeout)))
|
? await firstValueFrom(options.combineLatestWith.pipe(timeout(options.msTimeout)))
|
||||||
: null;
|
: null;
|
||||||
if (!options.shouldUpdate(current, combinedDependencies)) {
|
if (!options.shouldUpdate(current, combinedDependencies)) {
|
||||||
return current;
|
return [this.userId, current];
|
||||||
}
|
}
|
||||||
const newState = configureState(current, combinedDependencies);
|
const newState = configureState(current, combinedDependencies);
|
||||||
this.stateSubject.next([this.userId, newState]);
|
this.stateSubject.next([this.userId, newState]);
|
||||||
this.nextMock([this.userId, newState]);
|
this.nextMock([this.userId, newState]);
|
||||||
return newState;
|
return [this.userId, newState];
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMock = this.update as jest.MockedFunction<typeof this.update>;
|
updateMock = this.update as jest.MockedFunction<typeof this.update>;
|
||||||
|
@ -266,12 +266,13 @@ describe("DefaultActiveUserState", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should save on update", async () => {
|
it("should save on update", async () => {
|
||||||
const result = await userState.update((state, dependencies) => {
|
const [setUserId, result] = await userState.update((state, dependencies) => {
|
||||||
return newData;
|
return newData;
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(diskStorageService.mock.save).toHaveBeenCalledTimes(1);
|
expect(diskStorageService.mock.save).toHaveBeenCalledTimes(1);
|
||||||
expect(result).toEqual(newData);
|
expect(result).toEqual(newData);
|
||||||
|
expect(setUserId).toEqual("00000000-0000-1000-a000-000000000001");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should emit once per update", async () => {
|
it("should emit once per update", async () => {
|
||||||
@ -316,7 +317,7 @@ describe("DefaultActiveUserState", () => {
|
|||||||
const emissions = trackEmissions(userState.state$);
|
const emissions = trackEmissions(userState.state$);
|
||||||
await awaitAsync(); // Need to await for the initial value to be emitted
|
await awaitAsync(); // Need to await for the initial value to be emitted
|
||||||
|
|
||||||
const result = await userState.update(
|
const [userIdResult, result] = await userState.update(
|
||||||
(state, dependencies) => {
|
(state, dependencies) => {
|
||||||
return newData;
|
return newData;
|
||||||
},
|
},
|
||||||
@ -328,6 +329,7 @@ describe("DefaultActiveUserState", () => {
|
|||||||
await awaitAsync();
|
await awaitAsync();
|
||||||
|
|
||||||
expect(diskStorageService.mock.save).not.toHaveBeenCalled();
|
expect(diskStorageService.mock.save).not.toHaveBeenCalled();
|
||||||
|
expect(userIdResult).toEqual("00000000-0000-1000-a000-000000000001");
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
expect(emissions).toEqual([null]);
|
expect(emissions).toEqual([null]);
|
||||||
});
|
});
|
||||||
@ -422,12 +424,13 @@ describe("DefaultActiveUserState", () => {
|
|||||||
await originalSave(key, obj);
|
await originalSave(key, obj);
|
||||||
});
|
});
|
||||||
|
|
||||||
const val = await userState.update(() => {
|
const [userIdResult, val] = await userState.update(() => {
|
||||||
return newData;
|
return newData;
|
||||||
});
|
});
|
||||||
|
|
||||||
await awaitAsync(10);
|
await awaitAsync(10);
|
||||||
|
|
||||||
|
expect(userIdResult).toEqual(userId);
|
||||||
expect(val).toEqual(newData);
|
expect(val).toEqual(newData);
|
||||||
expect(emissions).toEqual([initialData, newData]);
|
expect(emissions).toEqual([initialData, newData]);
|
||||||
expect(emissions2).toEqual([initialData, newData]);
|
expect(emissions2).toEqual([initialData, newData]);
|
||||||
@ -447,7 +450,7 @@ describe("DefaultActiveUserState", () => {
|
|||||||
expect(emissions).toEqual([initialData]);
|
expect(emissions).toEqual([initialData]);
|
||||||
|
|
||||||
let emissions2: TestState[];
|
let emissions2: TestState[];
|
||||||
const val = await userState.update(
|
const [userIdResult, val] = await userState.update(
|
||||||
(state) => {
|
(state) => {
|
||||||
return newData;
|
return newData;
|
||||||
},
|
},
|
||||||
@ -461,6 +464,7 @@ describe("DefaultActiveUserState", () => {
|
|||||||
|
|
||||||
await awaitAsync();
|
await awaitAsync();
|
||||||
|
|
||||||
|
expect(userIdResult).toEqual(userId);
|
||||||
expect(val).toEqual(initialData);
|
expect(val).toEqual(initialData);
|
||||||
expect(emissions).toEqual([initialData]);
|
expect(emissions).toEqual([initialData]);
|
||||||
|
|
||||||
@ -497,10 +501,11 @@ describe("DefaultActiveUserState", () => {
|
|||||||
|
|
||||||
test("updates with FAKE_DEFAULT initial value should resolve correctly", async () => {
|
test("updates with FAKE_DEFAULT initial value should resolve correctly", async () => {
|
||||||
expect(diskStorageService["updatesSubject"]["observers"]).toHaveLength(0);
|
expect(diskStorageService["updatesSubject"]["observers"]).toHaveLength(0);
|
||||||
const val = await userState.update((state) => {
|
const [userIdResult, val] = await userState.update((state) => {
|
||||||
return newData;
|
return newData;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(userIdResult).toEqual(userId);
|
||||||
expect(val).toEqual(newData);
|
expect(val).toEqual(newData);
|
||||||
const call = diskStorageService.mock.save.mock.calls[0];
|
const call = diskStorageService.mock.save.mock.calls[0];
|
||||||
expect(call[0]).toEqual(`user_${userId}_fake_fake`);
|
expect(call[0]).toEqual(`user_${userId}_fake_fake`);
|
||||||
|
@ -31,7 +31,7 @@ const FAKE = Symbol("fake");
|
|||||||
|
|
||||||
export class DefaultActiveUserState<T> implements ActiveUserState<T> {
|
export class DefaultActiveUserState<T> implements ActiveUserState<T> {
|
||||||
[activeMarker]: true;
|
[activeMarker]: true;
|
||||||
private updatePromise: Promise<T> | null = null;
|
private updatePromise: Promise<[UserId, T]> | null = null;
|
||||||
|
|
||||||
private activeUserId$: Observable<UserId | null>;
|
private activeUserId$: Observable<UserId | null>;
|
||||||
|
|
||||||
@ -120,15 +120,15 @@ export class DefaultActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
async update<TCombine>(
|
async update<TCombine>(
|
||||||
configureState: (state: T, dependency: TCombine) => T,
|
configureState: (state: T, dependency: TCombine) => T,
|
||||||
options: StateUpdateOptions<T, TCombine> = {},
|
options: StateUpdateOptions<T, TCombine> = {},
|
||||||
): Promise<T> {
|
): Promise<[UserId, T]> {
|
||||||
options = populateOptionsWithDefault(options);
|
options = populateOptionsWithDefault(options);
|
||||||
try {
|
try {
|
||||||
if (this.updatePromise != null) {
|
if (this.updatePromise != null) {
|
||||||
await this.updatePromise;
|
await this.updatePromise;
|
||||||
}
|
}
|
||||||
this.updatePromise = this.internalUpdate(configureState, options);
|
this.updatePromise = this.internalUpdate(configureState, options);
|
||||||
const newState = await this.updatePromise;
|
const [userId, newState] = await this.updatePromise;
|
||||||
return newState;
|
return [userId, newState];
|
||||||
} finally {
|
} finally {
|
||||||
this.updatePromise = null;
|
this.updatePromise = null;
|
||||||
}
|
}
|
||||||
@ -137,20 +137,20 @@ export class DefaultActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
private async internalUpdate<TCombine>(
|
private async internalUpdate<TCombine>(
|
||||||
configureState: (state: T, dependency: TCombine) => T,
|
configureState: (state: T, dependency: TCombine) => T,
|
||||||
options: StateUpdateOptions<T, TCombine>,
|
options: StateUpdateOptions<T, TCombine>,
|
||||||
) {
|
): Promise<[UserId, T]> {
|
||||||
const [key, currentState] = await this.getStateForUpdate();
|
const [userId, key, currentState] = await this.getStateForUpdate();
|
||||||
const combinedDependencies =
|
const combinedDependencies =
|
||||||
options.combineLatestWith != null
|
options.combineLatestWith != null
|
||||||
? await firstValueFrom(options.combineLatestWith.pipe(timeout(options.msTimeout)))
|
? await firstValueFrom(options.combineLatestWith.pipe(timeout(options.msTimeout)))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!options.shouldUpdate(currentState, combinedDependencies)) {
|
if (!options.shouldUpdate(currentState, combinedDependencies)) {
|
||||||
return currentState;
|
return [userId, currentState];
|
||||||
}
|
}
|
||||||
|
|
||||||
const newState = configureState(currentState, combinedDependencies);
|
const newState = configureState(currentState, combinedDependencies);
|
||||||
await this.saveToStorage(key, newState);
|
await this.saveToStorage(key, newState);
|
||||||
return newState;
|
return [userId, newState];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** For use in update methods, does not wait for update to complete before yielding state.
|
/** For use in update methods, does not wait for update to complete before yielding state.
|
||||||
@ -170,6 +170,7 @@ export class DefaultActiveUserState<T> implements ActiveUserState<T> {
|
|||||||
}
|
}
|
||||||
const fullKey = userKeyBuilder(userId, this.keyDefinition);
|
const fullKey = userKeyBuilder(userId, this.keyDefinition);
|
||||||
return [
|
return [
|
||||||
|
userId,
|
||||||
fullKey,
|
fullKey,
|
||||||
await getStoredValue(fullKey, this.chosenStorageLocation, this.keyDefinition.deserializer),
|
await getStoredValue(fullKey, this.chosenStorageLocation, this.keyDefinition.deserializer),
|
||||||
] as const;
|
] as const;
|
||||||
|
@ -32,11 +32,15 @@ export class DefaultStateProvider implements StateProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setUserState<T>(keyDefinition: KeyDefinition<T>, value: T, userId?: UserId): Promise<void> {
|
async setUserState<T>(
|
||||||
|
keyDefinition: KeyDefinition<T>,
|
||||||
|
value: T,
|
||||||
|
userId?: UserId,
|
||||||
|
): Promise<[UserId, T]> {
|
||||||
if (userId) {
|
if (userId) {
|
||||||
await this.getUser<T>(userId, keyDefinition).update(() => value);
|
return [userId, await this.getUser<T>(userId, keyDefinition).update(() => value)];
|
||||||
} else {
|
} else {
|
||||||
await this.getActive<T>(keyDefinition).update(() => value);
|
return await this.getActive<T>(keyDefinition).update(() => value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,7 +36,11 @@ export abstract class StateProvider {
|
|||||||
* @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>(keyDefinition: KeyDefinition<T>, value: T, userId?: UserId) => Promise<void>;
|
setUserState: <T>(
|
||||||
|
keyDefinition: KeyDefinition<T>,
|
||||||
|
value: T,
|
||||||
|
userId?: UserId,
|
||||||
|
) => Promise<[UserId, T]>;
|
||||||
/** @see{@link ActiveUserStateProvider.get} */
|
/** @see{@link ActiveUserStateProvider.get} */
|
||||||
getActive: <T>(keyDefinition: KeyDefinition<T>) => ActiveUserState<T>;
|
getActive: <T>(keyDefinition: KeyDefinition<T>) => ActiveUserState<T>;
|
||||||
/** @see{@link SingleUserStateProvider.get} */
|
/** @see{@link SingleUserStateProvider.get} */
|
||||||
|
@ -19,6 +19,28 @@ export interface UserState<T> {
|
|||||||
* Emits a stream of data alongside the user id the data corresponds to.
|
* Emits a stream of data alongside the user id the data corresponds to.
|
||||||
*/
|
*/
|
||||||
readonly combinedState$: Observable<CombinedState<T>>;
|
readonly combinedState$: Observable<CombinedState<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const activeMarker: unique symbol = Symbol("active");
|
||||||
|
export interface ActiveUserState<T> extends UserState<T> {
|
||||||
|
readonly [activeMarker]: true;
|
||||||
|
/**
|
||||||
|
* Updates backing stores for the active user.
|
||||||
|
* @param configureState function that takes the current state and returns the new state
|
||||||
|
* @param options Defaults to @see {module:state-update-options#DEFAULT_OPTIONS}
|
||||||
|
* @param options.shouldUpdate A callback for determining if you want to update state. Defaults to () => true
|
||||||
|
* @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null
|
||||||
|
* @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set.
|
||||||
|
|
||||||
|
* @returns The new state
|
||||||
|
*/
|
||||||
|
readonly update: <TCombine>(
|
||||||
|
configureState: (state: T, dependencies: TCombine) => T,
|
||||||
|
options?: StateUpdateOptions<T, TCombine>,
|
||||||
|
) => Promise<[UserId, T]>;
|
||||||
|
}
|
||||||
|
export interface SingleUserState<T> extends UserState<T> {
|
||||||
|
readonly userId: UserId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates backing stores for the active user.
|
* Updates backing stores for the active user.
|
||||||
@ -35,11 +57,3 @@ export interface UserState<T> {
|
|||||||
options?: StateUpdateOptions<T, TCombine>,
|
options?: StateUpdateOptions<T, TCombine>,
|
||||||
) => Promise<T>;
|
) => Promise<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const activeMarker: unique symbol = Symbol("active");
|
|
||||||
export interface ActiveUserState<T> extends UserState<T> {
|
|
||||||
readonly [activeMarker]: true;
|
|
||||||
}
|
|
||||||
export interface SingleUserState<T> extends UserState<T> {
|
|
||||||
readonly userId: UserId;
|
|
||||||
}
|
|
||||||
|
Loading…
Reference in New Issue
Block a user