1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 07:43:35 +00:00

Rework derived state (#7290)

* Remove derived state from state classes

* Create provider for derived state

Derived state is automatically stored to memory storage, but can be derived from any observable.

* Fixup state provider method definitions

* Test `DefaultDerivedState`

* remove implementation notes

* Write docs for derived state

* fixup derived state provider types

* Implement buffered delayUntil operator

* Move state types to a common module

* Move mock ports to centra location

* Alias DerivedStateDependency type

* Add dependencies to browser

* Prefer internal rxjs operators for ref counting

* WIP

* Ensure complete on subjects

* Foreground/background messaging for browser

Defers work for browser to the background

* Test foreground port behaviors

* Inject foreground and background derived state services

* remove unnecessary class field

* Adhere to required options

* Add dderived state to CLI

* Prefer type definition in type parameters to options

* Prefer instance method

* Implements factory methods for common uses

* Remove nothing test

* Remove share subject reference

Share manages connector subjects internally and will reuse them until
refcount is 0 and the cleanup time has passed. Saving our own reference
just risks memory leaks without real testability benefits.

* Fix interaction state
This commit is contained in:
Matt Gibson
2024-01-04 14:47:49 -05:00
committed by GitHub
parent 8e46ef1ae5
commit 06affa9654
33 changed files with 1182 additions and 79 deletions

View File

@@ -0,0 +1,131 @@
import { Jsonify } from "type-fest";
import { DerivedStateDependencies, ShapeToInstances, StorageKey } from "../../types/state";
import { KeyDefinition } from "./key-definition";
import { StateDefinition } from "./state-definition";
declare const depShapeMarker: unique symbol;
/**
* A set of options for customizing the behavior of a {@link DeriveDefinition}
*/
type DeriveDefinitionOptions<TFrom, TTo, TDeps extends DerivedStateDependencies = never> = {
/**
* A function to use to convert values from TFrom to TTo. This is called on each emit of the parent state observable
* and the resulting value will be emitted from the derived state observable.
*
* @param from Populated with the latest emission from the parent state observable.
* @param deps Populated with the dependencies passed into the constructor of the derived state.
* These are constant for the lifetime of the derived state.
* @returns The derived state value or a Promise that resolves to the derived state value.
*/
derive: (from: TFrom, deps: ShapeToInstances<TDeps>) => TTo | Promise<TTo>;
/**
* A function to use to safely convert your type from json to your expected type.
*
* **Important:** Your data may be serialized/deserialized at any time and this
* callback needs to be able to faithfully re-initialize from the JSON object representation of your type.
*
* @param jsonValue The JSON object representation of your state.
* @returns The fully typed version of your state.
*/
deserializer: (serialized: Jsonify<TTo>) => TTo;
/**
* An object defining the dependencies of the derive function. The keys of the object are the names of the dependencies
* and the values are the types of the dependencies.
*
* for example:
* ```
* {
* myService: MyService,
* myOtherService: MyOtherService,
* }
* ```
*/
[depShapeMarker]?: TDeps;
/**
* The number of milliseconds to wait before cleaning up the state after the last subscriber has unsubscribed.
* Defaults to 1000ms.
*/
cleanupDelayMs?: number;
};
/**
* DeriveDefinitions describe state derived from another observable, the value type of which is given by `TFrom`.
*
* The StateDefinition is used to describe the domain of the state, and the DeriveDefinition
* sub-divides that domain into specific keys. These keys are used to cache data in memory and enables derived state to
* be calculated once regardless of multiple execution contexts.
*/
export class DeriveDefinition<TFrom, TTo, TDeps extends DerivedStateDependencies> {
/**
* Creates a new instance of a DeriveDefinition. Derived state is always stored in memory, so the storage location
* defined in @link{StateDefinition} is ignored.
*
* @param stateDefinition The state definition for which this key belongs to.
* @param uniqueDerivationName The name of the key, this should be unique per domain.
* @param options A set of options to customize the behavior of {@link DeriveDefinition}.
* @param options.derive A function to use to convert values from TFrom to TTo. This is called on each emit of the parent state observable
* and the resulting value will be emitted from the derived state observable.
* @param options.cleanupDelayMs The number of milliseconds to wait before cleaning up the state after the last subscriber has unsubscribed.
* Defaults to 1000ms.
* @param options.dependencyShape An object defining the dependencies of the derive function. The keys of the object are the names of the dependencies
* and the values are the types of the dependencies.
* for example:
* ```
* {
* myService: MyService,
* myOtherService: MyOtherService,
* }
* ```
*
* @param options.deserializer A function to use to safely convert your type from json to your expected type.
* Your data may be serialized/deserialized at any time and this needs callback needs to be able to faithfully re-initialize
* from the JSON object representation of your type.
*/
constructor(
readonly stateDefinition: StateDefinition,
readonly uniqueDerivationName: string,
readonly options: DeriveDefinitionOptions<TFrom, TTo, TDeps>,
) {}
/**
* Factory that produces a {@link DeriveDefinition} from a {@link KeyDefinition} and a set of options. The returned
* definition will have the same key as the given key definition, but will not collide with it in storage, even if
* they both reside in memory.
* @param keyDefinition
* @param options
* @returns
*/
static from<TFrom, TTo, TDeps extends DerivedStateDependencies = never>(
keyDefinition: KeyDefinition<TFrom>,
options: DeriveDefinitionOptions<TFrom, TTo, TDeps>,
) {
return new DeriveDefinition(keyDefinition.stateDefinition, keyDefinition.key, options);
}
get derive() {
return this.options.derive;
}
deserialize(serialized: Jsonify<TTo>): TTo {
return this.options.deserializer(serialized);
}
get cleanupDelayMs() {
return this.options.cleanupDelayMs < 0 ? 0 : this.options.cleanupDelayMs ?? 1000;
}
buildCacheKey(): string {
return `derived_${this.stateDefinition.name}_${this.uniqueDerivationName}`;
}
/**
* Creates a {@link StorageKey} that points to the data for the given derived definition.
* @returns A key that is ready to be used in a storage service to get data.
*/
get storageKey(): StorageKey {
return `derived_${this.stateDefinition.name}_${this.uniqueDerivationName}` as StorageKey;
}
}

View File

@@ -0,0 +1,25 @@
import { Observable } from "rxjs";
import { ShapeToInstances, DerivedStateDependencies } from "../../types/state";
import { DeriveDefinition } from "./derive-definition";
import { DerivedState } from "./derived-state";
/**
* State derived from an observable and a derive function
*/
export abstract class DerivedStateProvider {
/**
* Creates a derived state observable from a parent state observable, a deriveDefinition, and the dependencies
* required by the deriveDefinition
* @param parentState$ The parent state observable
* @param deriveDefinition The deriveDefinition that defines conversion from the parent state to the derived state as
* well as some memory persistent information.
* @param dependencies The dependencies of the derive function
*/
get: <TFrom, TTo, TDeps extends DerivedStateDependencies>(
parentState$: Observable<TFrom>,
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
dependencies: ShapeToInstances<TDeps>,
) => DerivedState<TTo>;
}

View File

@@ -0,0 +1,23 @@
import { Observable } from "rxjs";
export type StateConverter<TFrom extends Array<unknown>, TTo> = (...args: TFrom) => TTo;
/**
* State derived from an observable and a converter function
*
* Derived state is cached and persisted to memory for sychronization across execution contexts.
* For clients with multiple execution contexts, the derived state will be executed only once in the background process.
*/
export interface DerivedState<T> {
/**
* The derived state observable
*/
state$: Observable<T>;
/**
* Forces the derived state to a given value.
*
* Useful for setting an in-memory value as a side effect of some event, such as emptying state as a result of a lock.
* @param value The value to force the derived state to
*/
forceValue(value: T): Promise<T>;
}

View File

@@ -1,5 +0,0 @@
import { Observable } from "rxjs";
export interface DerivedUserState<T> {
state$: Observable<T>;
}

View File

@@ -18,12 +18,10 @@ import {
AbstractStorageService,
ObservableStorageService,
} from "../../abstractions/storage.service";
import { DerivedUserState } from "../derived-user-state";
import { KeyDefinition, userKeyBuilder } from "../key-definition";
import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options";
import { Converter, ActiveUserState, activeMarker } from "../user-state";
import { ActiveUserState, activeMarker } from "../user-state";
import { DefaultDerivedUserState } from "./default-derived-state";
import { getStoredValue } from "./util";
const FAKE_DEFAULT = Symbol("fakeDefault");
@@ -91,10 +89,6 @@ export class DefaultActiveUserState<T> implements ActiveUserState<T> {
return await getStoredValue(key, this.chosenStorageLocation, this.keyDefinition.deserializer);
}
createDerived<TTo>(converter: Converter<T, TTo>): DerivedUserState<TTo> {
return new DefaultDerivedUserState<T, TTo>(converter, this.encryptService, this);
}
private async internalUpdate<TCombine>(
configureState: (state: T, dependency: TCombine) => T,
options: StateUpdateOptions<T, TCombine>,

View File

@@ -0,0 +1,49 @@
import { Observable } from "rxjs";
import { DerivedStateDependencies, ShapeToInstances } from "../../../types/state";
import {
AbstractStorageService,
ObservableStorageService,
} from "../../abstractions/storage.service";
import { DeriveDefinition } from "../derive-definition";
import { DerivedState } from "../derived-state";
import { DerivedStateProvider } from "../derived-state.provider";
import { DefaultDerivedState } from "./default-derived-state";
export class DefaultDerivedStateProvider implements DerivedStateProvider {
private cache: Record<string, DerivedState<unknown>> = {};
constructor(protected memoryStorage: AbstractStorageService & ObservableStorageService) {}
get<TFrom, TTo, TDeps extends DerivedStateDependencies>(
parentState$: Observable<TFrom>,
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
dependencies: ShapeToInstances<TDeps>,
): DerivedState<TTo> {
const cacheKey = deriveDefinition.buildCacheKey();
const existingDerivedState = this.cache[cacheKey];
if (existingDerivedState != null) {
// I have to cast out of the unknown generic but this should be safe if rules
// around domain token are made
return existingDerivedState as DefaultDerivedState<TFrom, TTo, TDeps>;
}
const newDerivedState = this.buildDerivedState(parentState$, deriveDefinition, dependencies);
this.cache[cacheKey] = newDerivedState;
return newDerivedState;
}
protected buildDerivedState<TFrom, TTo, TDeps extends DerivedStateDependencies>(
parentState$: Observable<TFrom>,
deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
dependencies: ShapeToInstances<TDeps>,
): DerivedState<TTo> {
return new DefaultDerivedState<TFrom, TTo, TDeps>(
parentState$,
deriveDefinition,
this.memoryStorage,
dependencies,
);
}
}

View File

@@ -0,0 +1,222 @@
/**
* need to update test environment so trackEmissions works appropriately
* @jest-environment ../shared/test.environment.ts
*/
import { Subject, firstValueFrom } from "rxjs";
import { awaitAsync, trackEmissions } from "../../../../spec";
import { FakeStorageService } from "../../../../spec/fake-storage.service";
import { DeriveDefinition } from "../derive-definition";
import { StateDefinition } from "../state-definition";
import { DefaultDerivedState } from "./default-derived-state";
let callCount = 0;
const cleanupDelayMs = 10;
const stateDefinition = new StateDefinition("test", "memory");
const deriveDefinition = new DeriveDefinition<string, Date, { date: typeof Date }>(
stateDefinition,
"test",
{
derive: (dateString: string) => {
callCount++;
return new Date(dateString);
},
deserializer: (dateString: string) => new Date(dateString),
cleanupDelayMs,
},
);
describe("DefaultDerivedState", () => {
let parentState$: Subject<string>;
let memoryStorage: FakeStorageService;
let sut: DefaultDerivedState<string, Date, { date: typeof Date }>;
const deps = {
date: new Date(),
};
beforeEach(() => {
callCount = 0;
parentState$ = new Subject();
memoryStorage = new FakeStorageService();
sut = new DefaultDerivedState(parentState$, deriveDefinition, memoryStorage, deps);
});
afterEach(() => {
parentState$.complete();
jest.resetAllMocks();
});
it("should derive the state", async () => {
const dateString = "2020-01-01";
const emissions = trackEmissions(sut.state$);
parentState$.next(dateString);
await awaitAsync();
expect(emissions).toEqual([new Date(dateString)]);
});
it("should derive the state once", async () => {
const dateString = "2020-01-01";
trackEmissions(sut.state$);
parentState$.next(dateString);
expect(callCount).toBe(1);
});
it("should store the derived state in memory", async () => {
const dateString = "2020-01-01";
trackEmissions(sut.state$);
parentState$.next(dateString);
await awaitAsync();
expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual(
new Date(dateString),
);
const calls = memoryStorage.mock.save.mock.calls;
expect(calls.length).toBe(1);
expect(calls[0][0]).toBe(deriveDefinition.buildCacheKey());
expect(calls[0][1]).toEqual(new Date(dateString));
});
describe("forceValue", () => {
const initialParentValue = "2020-01-01";
const forced = new Date("2020-02-02");
let emissions: Date[];
describe("without observers", () => {
beforeEach(async () => {
parentState$.next(initialParentValue);
await awaitAsync();
});
it("should store the forced value", async () => {
await sut.forceValue(forced);
expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual(forced);
});
});
describe("with observers", () => {
beforeEach(async () => {
emissions = trackEmissions(sut.state$);
parentState$.next(initialParentValue);
await awaitAsync();
});
it("should store the forced value", async () => {
await sut.forceValue(forced);
expect(memoryStorage.internalStore[deriveDefinition.buildCacheKey()]).toEqual(forced);
});
it("should force the value", async () => {
await sut.forceValue(forced);
expect(emissions).toEqual([new Date(initialParentValue), forced]);
});
it("should only force the value once", async () => {
await sut.forceValue(forced);
parentState$.next(initialParentValue);
await awaitAsync();
expect(emissions).toEqual([
new Date(initialParentValue),
forced,
new Date(initialParentValue),
]);
});
});
});
describe("cleanup", () => {
const newDate = "2020-02-02";
it("should cleanup after last subscriber", async () => {
const subscription = sut.state$.subscribe();
await awaitAsync();
subscription.unsubscribe();
// Wait for cleanup
await awaitAsync(cleanupDelayMs * 2);
expect(parentState$.observed).toBe(false);
});
it("should not cleanup if there are still subscribers", async () => {
const subscription1 = sut.state$.subscribe();
const sub2Emissions: Date[] = [];
const subscription2 = sut.state$.subscribe((v) => sub2Emissions.push(v));
await awaitAsync();
subscription1.unsubscribe();
// Wait for cleanup
await awaitAsync(cleanupDelayMs * 2);
// Still be listening to parent updates
parentState$.next(newDate);
await awaitAsync();
expect(sub2Emissions).toEqual([new Date(newDate)]);
subscription2.unsubscribe();
// Wait for cleanup
await awaitAsync(cleanupDelayMs * 2);
expect(parentState$.observed).toBe(false);
});
it("can re-initialize after cleanup", async () => {
const subscription = sut.state$.subscribe();
await awaitAsync();
subscription.unsubscribe();
// Wait for cleanup
await awaitAsync(cleanupDelayMs * 2);
const emissions = trackEmissions(sut.state$);
await awaitAsync();
parentState$.next(newDate);
await awaitAsync();
expect(emissions).toEqual([new Date(newDate)]);
});
it("should not cleanup if a subscriber joins during the cleanup delay", async () => {
const subscription = sut.state$.subscribe();
await awaitAsync();
await parentState$.next(newDate);
await awaitAsync();
subscription.unsubscribe();
// Do not wait long enough for cleanup
await awaitAsync(cleanupDelayMs / 2);
expect(parentState$.observed).toBe(true); // still listening to parent
const emissions = trackEmissions(sut.state$);
expect(emissions).toEqual([new Date(newDate)]); // we didn't lose our buffered value
});
it("state$ observables are durable to cleanup", async () => {
const observable = sut.state$;
let subscription = observable.subscribe();
await parentState$.next(newDate);
await awaitAsync();
subscription.unsubscribe();
// Wait for cleanup
await awaitAsync(cleanupDelayMs * 2);
subscription = observable.subscribe();
await parentState$.next(newDate);
await awaitAsync();
expect(await firstValueFrom(observable)).toEqual(new Date(newDate));
});
});
});

View File

@@ -1,23 +1,57 @@
import { Observable, switchMap } from "rxjs";
import { Observable, ReplaySubject, Subject, concatMap, merge, share, timer } from "rxjs";
import { EncryptService } from "../../abstractions/encrypt.service";
import { DerivedUserState } from "../derived-user-state";
import { Converter, DeriveContext, UserState } from "../user-state";
import { ShapeToInstances, DerivedStateDependencies } from "../../../types/state";
import {
AbstractStorageService,
ObservableStorageService,
} from "../../abstractions/storage.service";
import { DeriveDefinition } from "../derive-definition";
import { DerivedState } from "../derived-state";
/**
* Default derived state
*/
export class DefaultDerivedState<TFrom, TTo, TDeps extends DerivedStateDependencies>
implements DerivedState<TTo>
{
private readonly storageKey: string;
private forcedValueSubject = new Subject<TTo>();
export class DefaultDerivedUserState<TFrom, TTo> implements DerivedUserState<TTo> {
state$: Observable<TTo>;
constructor(
private converter: Converter<TFrom, TTo>,
private encryptService: EncryptService,
private userState: UserState<TFrom>,
private parentState$: Observable<TFrom>,
protected deriveDefinition: DeriveDefinition<TFrom, TTo, TDeps>,
private memoryStorage: AbstractStorageService & ObservableStorageService,
private dependencies: ShapeToInstances<TDeps>,
) {
this.state$ = userState.state$.pipe(
switchMap(async (from) => {
// TODO: How do I get the key?
const convertedData = await this.converter(from, new DeriveContext(null, encryptService));
return convertedData;
this.storageKey = deriveDefinition.storageKey;
const derivedState$ = this.parentState$.pipe(
concatMap(async (state) => {
let derivedStateOrPromise = this.deriveDefinition.derive(state, this.dependencies);
if (derivedStateOrPromise instanceof Promise) {
derivedStateOrPromise = await derivedStateOrPromise;
}
const derivedState = derivedStateOrPromise;
await this.memoryStorage.save(this.storageKey, derivedState);
return derivedState;
}),
);
this.state$ = merge(this.forcedValueSubject, derivedState$).pipe(
share({
connector: () => {
return new ReplaySubject<TTo>(1);
},
resetOnRefCountZero: () => timer(this.deriveDefinition.cleanupDelayMs),
}),
);
}
async forceValue(value: TTo) {
await this.memoryStorage.save(this.storageKey, value);
this.forcedValueSubject.next(value);
return value;
}
}

View File

@@ -14,12 +14,10 @@ import {
AbstractStorageService,
ObservableStorageService,
} from "../../abstractions/storage.service";
import { DerivedUserState } from "../derived-user-state";
import { KeyDefinition, userKeyBuilder } from "../key-definition";
import { StateUpdateOptions, populateOptionsWithDefault } from "../state-update-options";
import { Converter, SingleUserState } from "../user-state";
import { SingleUserState } from "../user-state";
import { DefaultDerivedUserState } from "./default-derived-state";
import { getStoredValue } from "./util";
const FAKE_DEFAULT = Symbol("fakeDefault");
@@ -68,10 +66,6 @@ export class DefaultSingleUserState<T> implements SingleUserState<T> {
}
}
createDerived<TTo>(converter: Converter<T, TTo>): DerivedUserState<TTo> {
return new DefaultDerivedUserState<T, TTo>(converter, this.encryptService, this);
}
private async internalUpdate<TCombine>(
configureState: (state: T, dependency: TCombine) => T,
options: StateUpdateOptions<T, TCombine>,

View File

@@ -1,9 +1,13 @@
import { of } from "rxjs";
import {
FakeActiveUserStateProvider,
FakeDerivedStateProvider,
FakeGlobalStateProvider,
FakeSingleUserStateProvider,
} from "../../../../spec/fake-state-provider";
import { UserId } from "../../../types/guid";
import { DeriveDefinition } from "../derive-definition";
import { KeyDefinition } from "../key-definition";
import { StateDefinition } from "../state-definition";
@@ -14,15 +18,18 @@ describe("DefaultStateProvider", () => {
let activeUserStateProvider: FakeActiveUserStateProvider;
let singleUserStateProvider: FakeSingleUserStateProvider;
let globalStateProvider: FakeGlobalStateProvider;
let derivedStateProvider: FakeDerivedStateProvider;
beforeEach(() => {
activeUserStateProvider = new FakeActiveUserStateProvider();
singleUserStateProvider = new FakeSingleUserStateProvider();
globalStateProvider = new FakeGlobalStateProvider();
derivedStateProvider = new FakeDerivedStateProvider();
sut = new DefaultStateProvider(
activeUserStateProvider,
singleUserStateProvider,
globalStateProvider,
derivedStateProvider,
);
});
@@ -53,4 +60,15 @@ describe("DefaultStateProvider", () => {
const actual = sut.getGlobal(keyDefinition);
expect(actual).toBe(existing);
});
it("should bind the derivedStateProvider", () => {
const derivedDefinition = new DeriveDefinition(new StateDefinition("test", "disk"), "test", {
derive: () => null,
deserializer: () => null,
});
const parentState$ = of(null);
const existing = derivedStateProvider.get(parentState$, derivedDefinition, {});
const actual = sut.getDerived(parentState$, derivedDefinition, {});
expect(actual).toBe(existing);
});
});

View File

@@ -1,3 +1,9 @@
import { Observable } from "rxjs";
import { ShapeToInstances, DerivedStateDependencies } from "../../../types/state";
import { DeriveDefinition } from "../derive-definition";
import { DerivedState } from "../derived-state";
import { DerivedStateProvider } from "../derived-state.provider";
import { GlobalStateProvider } from "../global-state.provider";
import { StateProvider } from "../state.provider";
import { ActiveUserStateProvider, SingleUserStateProvider } from "../user-state.provider";
@@ -7,6 +13,7 @@ export class DefaultStateProvider implements StateProvider {
private readonly activeUserStateProvider: ActiveUserStateProvider,
private readonly singleUserStateProvider: SingleUserStateProvider,
private readonly globalStateProvider: GlobalStateProvider,
private readonly derivedStateProvider: DerivedStateProvider,
) {}
getActive: InstanceType<typeof ActiveUserStateProvider>["get"] =
@@ -16,4 +23,9 @@ export class DefaultStateProvider implements StateProvider {
getGlobal: InstanceType<typeof GlobalStateProvider>["get"] = this.globalStateProvider.get.bind(
this.globalStateProvider,
);
getDerived: <TFrom, TTo, TDeps extends DerivedStateDependencies>(
parentState$: Observable<TFrom>,
deriveDefinition: DeriveDefinition<unknown, TTo, TDeps>,
dependencies: ShapeToInstances<TDeps>,
) => DerivedState<TTo> = this.derivedStateProvider.get.bind(this.derivedStateProvider);
}

View File

@@ -1,4 +1,6 @@
export { DerivedUserState } from "./derived-user-state";
export { DeriveDefinition } from "./derive-definition";
export { DerivedStateProvider } from "./derived-state.provider";
export { DerivedState } from "./derived-state";
export { GlobalState } from "./global-state";
export { StateProvider } from "./state.provider";
export { GlobalStateProvider } from "./global-state.provider";

View File

@@ -1,6 +1,7 @@
import { Jsonify, Opaque } from "type-fest";
import { Jsonify } from "type-fest";
import { UserId } from "../../types/guid";
import { StorageKey } from "../../types/state";
import { Utils } from "../misc/utils";
import { StateDefinition } from "./state-definition";
@@ -159,8 +160,6 @@ export class KeyDefinition<T> {
}
}
export type StorageKey = Opaque<string, "StorageKey">;
/**
* Creates a {@link StorageKey} that points to the data at the given key definition for the specified user.
* @param userId The userId of the user you want the key to be for.

View File

@@ -1,5 +1,10 @@
import { UserId } from "../../types/guid";
import { Observable } from "rxjs";
import { UserId } from "../../types/guid";
import { ShapeToInstances, DerivedStateDependencies } from "../../types/state";
import { DeriveDefinition } from "./derive-definition";
import { DerivedState } from "./derived-state";
import { GlobalState } from "./global-state";
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- used in docs
import { GlobalStateProvider } from "./global-state.provider";
@@ -18,4 +23,9 @@ export abstract class StateProvider {
getUser: <T>(userId: UserId, keyDefinition: KeyDefinition<T>) => SingleUserState<T>;
/** @see{@link GlobalStateProvider.get} */
getGlobal: <T>(keyDefinition: KeyDefinition<T>) => GlobalState<T>;
getDerived: <TFrom, TTo, TDeps extends DerivedStateDependencies>(
parentState$: Observable<TFrom>,
deriveDefinition: DeriveDefinition<unknown, TTo, TDeps>,
dependencies: ShapeToInstances<TDeps>,
) => DerivedState<TTo>;
}

View File

@@ -1,22 +1,9 @@
import { Observable } from "rxjs";
import { UserId } from "../../types/guid";
import { EncryptService } from "../abstractions/encrypt.service";
import { UserKey } from "../models/domain/symmetric-crypto-key";
import { StateUpdateOptions } from "./state-update-options";
import { DerivedUserState } from ".";
export class DeriveContext {
constructor(
readonly activeUserKey: UserKey,
readonly encryptService: EncryptService,
) {}
}
export type Converter<TFrom, TTo> = (data: TFrom, context: DeriveContext) => Promise<TTo>;
/**
* A helper object for interacting with state that is scoped to a specific user.
*/
@@ -37,13 +24,6 @@ export interface UserState<T> {
configureState: (state: T, dependencies: TCombine) => T,
options?: StateUpdateOptions<T, TCombine>,
) => Promise<T>;
/**
* Creates a derives state from the current state. Derived states are always tied to the active user.
* @param converter
* @returns
*/
createDerived: <TTo>(converter: Converter<T, TTo>) => DerivedUserState<TTo>;
}
export const activeMarker: unique symbol = Symbol("active");