mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 17:23:37 +00:00
130 lines
4.7 KiB
TypeScript
130 lines
4.7 KiB
TypeScript
import { Observable, combineLatest, concatMap, filter, map, of, concat, merge } from "rxjs";
|
|
|
|
import {
|
|
StateProvider,
|
|
SingleUserState,
|
|
CombinedState,
|
|
StateUpdateOptions,
|
|
} from "../../platform/state";
|
|
|
|
import { BufferedKeyDefinition } from "./buffered-key-definition";
|
|
|
|
/** Stateful storage that overwrites one state with a buffered state.
|
|
* When a overwrite occurs, the input state is automatically deleted.
|
|
* @remarks The buffered state can only overwrite non-nullish values. If the
|
|
* buffer key contains `null` or `undefined`, it will do nothing.
|
|
*/
|
|
export class BufferedState<Input, Output, Dependency> implements SingleUserState<Output> {
|
|
/**
|
|
* Instantiate a buffered state
|
|
* @param provider constructs the buffer.
|
|
* @param key defines the buffer location.
|
|
* @param output updates when a overwrite occurs
|
|
* @param dependency$ provides data the buffer depends upon to evaluate and
|
|
* transform its data. If this is omitted, then `true` is injected as
|
|
* a dependency, which with a default output will trigger a overwrite immediately.
|
|
*
|
|
* @remarks `dependency$` enables overwrite control during dynamic circumstances,
|
|
* such as when a overwrite should occur only if a user key is available.
|
|
*/
|
|
constructor(
|
|
provider: StateProvider,
|
|
private key: BufferedKeyDefinition<Input, Output, Dependency>,
|
|
private output: SingleUserState<Output>,
|
|
dependency$: Observable<Dependency> = null,
|
|
) {
|
|
this.bufferedState = provider.getUser(output.userId, key.toKeyDefinition());
|
|
|
|
// overwrite the output value
|
|
const hasValue$ = concat(of(null), this.bufferedState.state$).pipe(
|
|
map((buffer) => (buffer ?? null) !== null),
|
|
);
|
|
const overwriteDependency$ = (dependency$ ?? of(true as unknown as Dependency)).pipe(
|
|
map((dependency) => [key.shouldOverwrite(dependency), dependency] as const),
|
|
);
|
|
const overwrite$ = combineLatest([hasValue$, overwriteDependency$]).pipe(
|
|
concatMap(async ([hasValue, [shouldOverwrite, dependency]]) => {
|
|
if (hasValue && shouldOverwrite) {
|
|
await this.overwriteOutput(dependency);
|
|
}
|
|
return [false, null] as const;
|
|
}),
|
|
);
|
|
|
|
// drive overwrites only when there's a subscription;
|
|
// the output state determines when emissions occur
|
|
const output$ = this.output.state$.pipe(map((output) => [true, output] as const));
|
|
this.state$ = merge(overwrite$, output$).pipe(
|
|
filter(([emit]) => emit),
|
|
map(([, output]) => output),
|
|
);
|
|
|
|
this.combinedState$ = this.state$.pipe(map((state) => [this.output.userId, state]));
|
|
|
|
this.bufferedState$ = this.bufferedState.state$;
|
|
}
|
|
|
|
private bufferedState: SingleUserState<Input>;
|
|
|
|
private async overwriteOutput(dependency: Dependency) {
|
|
// take the latest value from the buffer
|
|
let buffered: Input;
|
|
await this.bufferedState.update((state) => {
|
|
buffered = state ?? null;
|
|
return null;
|
|
});
|
|
|
|
// update the output state
|
|
const isValid = await this.key.isValid(buffered, dependency);
|
|
if (isValid) {
|
|
const output = await this.key.map(buffered, dependency);
|
|
await this.output.update(() => output);
|
|
}
|
|
}
|
|
|
|
/** {@link SingleUserState.userId} */
|
|
get userId() {
|
|
return this.output.userId;
|
|
}
|
|
|
|
/** Observes changes to the output state. This updates when the output
|
|
* state updates, when the buffer is moved to the output, and when `BufferedState.buffer`
|
|
* is invoked.
|
|
*/
|
|
readonly state$: Observable<Output>;
|
|
|
|
/** {@link SingleUserState.combinedState$} */
|
|
readonly combinedState$: Observable<CombinedState<Output>>;
|
|
|
|
/** Buffers a value state. The buffered state overwrites the output
|
|
* state when a subscription occurs.
|
|
* @param value the state to roll over. Setting this to `null` or `undefined`
|
|
* has no effect.
|
|
*/
|
|
async buffer(value: Input): Promise<void> {
|
|
const normalized = value ?? null;
|
|
if (normalized !== null) {
|
|
await this.bufferedState.update(() => normalized);
|
|
}
|
|
}
|
|
|
|
/** The data presently being buffered. This emits the pending value each time
|
|
* new buffer data is provided. It emits null when the buffer is empty.
|
|
*/
|
|
readonly bufferedState$: Observable<Input>;
|
|
|
|
/** Updates the output state.
|
|
* @param configureState a callback that returns an updated output
|
|
* state. The callback receives the state's present value as its
|
|
* first argument and the dependencies listed in `options.combinedLatestWith`
|
|
* as its second argument.
|
|
* @param options configures how the update is applied. See {@link StateUpdateOptions}.
|
|
*/
|
|
update<TCombine>(
|
|
configureState: (state: Output, dependencies: TCombine) => Output,
|
|
options: StateUpdateOptions<Output, TCombine> = null,
|
|
): Promise<Output> {
|
|
return this.output.update(configureState, options);
|
|
}
|
|
}
|