mirror of
https://github.com/bitwarden/browser
synced 2026-02-24 00:23:17 +00:00
* Use typescript-strict-plugin to iteratively turn on strict * Add strict testing to pipeline Can be executed locally through either `npm run test:types` for full type checking including spec files, or `npx tsc-strict` for only tsconfig.json included files. * turn on strict for scripts directory * Use plugin for all tsconfigs in monorepo vscode is capable of executing tsc with plugins, but uses the most relevant tsconfig to do so. If the plugin is not a part of that config, it is skipped and developers get no feedback of strict compile time issues. These updates remedy that at the cost of slightly more complex removal of the plugin when the time comes. * remove plugin from configs that extend one that already has it * Update workspace settings to honor strict plugin * Apply strict-plugin to native message test runner * Update vscode workspace to use root tsc version * `./node_modules/.bin/update-strict-comments` 🤖 This is a one-time operation. All future files should adhere to strict type checking. * Add fixme to `ts-strict-ignore` comments * `update-strict-comments` 🤖 repeated for new merge files
132 lines
4.8 KiB
TypeScript
132 lines
4.8 KiB
TypeScript
// FIXME: Update this file to be type safe and remove this and next line
|
|
// @ts-strict-ignore
|
|
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);
|
|
}
|
|
}
|