import { BehaviorSubject, Observable, defer, firstValueFrom, map, share, switchMap, tap, } from "rxjs"; import { Jsonify } from "type-fest"; import { AccountService } from "../../auth/abstractions/account.service"; import { ActiveUserStateProvider } from "../abstractions/active-user-state.provider"; import { EncryptService } from "../abstractions/encrypt.service"; import { AbstractMemoryStorageService, AbstractStorageService, } from "../abstractions/storage.service"; import { ActiveUserState } from "../interfaces/active-user-state"; import { userKeyBuilder } from "../misc/key-builders"; import { UserKey } from "../models/domain/symmetric-crypto-key"; import { KeyDefinition } from "../types/key-definition"; import { StorageLocation } from "./default-global-state.provider"; class ConverterContext { constructor(readonly activeUserKey: UserKey, readonly encryptService: EncryptService) {} } class DerivedStateDefinition { constructor(readonly converter: (data: TFrom, context: ConverterContext) => Promise) {} } export class DerivedActiveUserState { state$: Observable; // TODO: Probably needs to take state service /** * */ constructor( private derivedStateDefinition: DerivedStateDefinition, private encryptService: EncryptService, private activeUserState: ActiveUserState ) { this.state$ = activeUserState.state$.pipe( switchMap(async (from) => { // TODO: How do I get the key? const convertedData = await derivedStateDefinition.converter( from, new ConverterContext(null, encryptService) ); return convertedData; }) ); } async getFromState(): Promise { const encryptedFromState = await this.activeUserState.getFromState(); const context = new ConverterContext(null, this.encryptService); const decryptedData = await this.derivedStateDefinition.converter(encryptedFromState, context); return decryptedData; } } class DefaultActiveUserState implements ActiveUserState { private seededInitial = false; private formattedKey$: Observable; private chosenStorageLocation: AbstractStorageService; protected stateSubject: BehaviorSubject = new BehaviorSubject(null); private stateSubject$ = this.stateSubject.asObservable(); state$: Observable; constructor( private keyDefinition: KeyDefinition, private accountService: AccountService, private encryptService: EncryptService, private memoryStorageService: AbstractMemoryStorageService, private secureStorageService: AbstractStorageService, private diskStorageService: AbstractStorageService ) { this.chosenStorageLocation = this.chooseStorage( this.keyDefinition.stateDefinition.storageLocation ); // startWith? this.formattedKey$ = this.accountService.activeAccount$.pipe( tap((user) => console.log("user", user)), // Temp map((account) => account != null && account.id != null ? userKeyBuilder(account.id, this.keyDefinition) : null ) ); const activeAccountData$ = this.formattedKey$.pipe( switchMap(async (key) => { console.log("user emitted: ", key); // temp if (key == null) { return null; } const jsonData = await this.chosenStorageLocation.get>(key); const data = keyDefinition.serializer(jsonData); return data; }), tap((data) => { this.seededInitial = true; this.stateSubject.next(data); }), // Share the execution share() ); // Whomever subscribes to this data, should be notified of updated data // if someone calls my update() method, or the active user changes. this.state$ = defer(() => { const subscription = activeAccountData$.subscribe(); return this.stateSubject$.pipe( tap({ complete: () => subscription.unsubscribe(), }) ); }); } async update(configureState: (state: T) => void): Promise { const key = await this.createKey(); if (key == null) { throw new Error("Attempting to active user state, when no user is active."); } const currentState = this.seededInitial ? this.stateSubject.getValue() : await this.seedInitial(key); configureState(currentState); await this.chosenStorageLocation.save(await this.createKey(), currentState); this.stateSubject.next(currentState); } async getFromState(): Promise { const activeUser = await firstValueFrom(this.accountService.activeAccount$); if (activeUser == null || activeUser.id == null) { throw new Error("You cannot get data from state while there is no active user."); } const key = userKeyBuilder(activeUser.id, this.keyDefinition); const data = (await this.chosenStorageLocation.get(key)) as Jsonify; return this.keyDefinition.serializer(data); } createDerived( derivedStateDefinition: DerivedStateDefinition ): DerivedActiveUserState { return new DerivedActiveUserState(derivedStateDefinition, this.encryptService, this); } private async createKey(): Promise { const formattedKey = await firstValueFrom(this.formattedKey$); if (formattedKey == null) { throw new Error("Cannot create a key while there is no active user."); } return formattedKey; } private async seedInitial(key: string): Promise { const data = await this.chosenStorageLocation.get>(key); this.seededInitial = true; return this.keyDefinition.serializer(data); } private chooseStorage(storageLocation: StorageLocation): AbstractStorageService { switch (storageLocation) { case "disk": return this.diskStorageService; case "secure": return this.secureStorageService; case "memory": return this.memoryStorageService; } } } export class DefaultActiveUserStateProvider implements ActiveUserStateProvider { private userStateCache: Record> = {}; constructor( private accountService: AccountService, // Inject the lightest weight service that provides accountUserId$ private encryptService: EncryptService, private memoryStorage: AbstractMemoryStorageService, private diskStorage: AbstractStorageService, private secureStorage: AbstractStorageService ) {} create(keyDefinition: KeyDefinition): DefaultActiveUserState { const locationDomainKey = `${keyDefinition.stateDefinition.storageLocation}_${keyDefinition.stateDefinition.name}_${keyDefinition.key}`; const existingActiveUserState = this.userStateCache[locationDomainKey]; if (existingActiveUserState != null) { // I have to cast out of the unknown generic but this should be safe if rules // around domain token are made return existingActiveUserState as DefaultActiveUserState; } const newActiveUserState = new DefaultActiveUserState( keyDefinition, this.accountService, this.encryptService, this.memoryStorage, this.secureStorage, this.diskStorage ); this.userStateCache[locationDomainKey] = newActiveUserState; return newActiveUserState; } }