import { mock } from "jest-mock-extended"; import { Observable, map, of, switchMap, take } from "rxjs"; import { GlobalState, GlobalStateProvider, KeyDefinition, ActiveUserState, SingleUserState, SingleUserStateProvider, StateProvider, ActiveUserStateProvider, DerivedState, DeriveDefinition, DerivedStateProvider, UserKeyDefinition, } from "../src/platform/state"; import { UserId } from "../src/types/guid"; import { DerivedStateDependencies } from "../src/types/state"; import { FakeAccountService } from "./fake-account-service"; import { FakeActiveUserState, FakeDerivedState, FakeGlobalState, FakeSingleUserState, } from "./fake-state"; export class FakeGlobalStateProvider implements GlobalStateProvider { mock = mock(); establishedMocks: Map> = new Map(); states: Map> = new Map(); get(keyDefinition: KeyDefinition): GlobalState { this.mock.get(keyDefinition); const cacheKey = this.cacheKey(keyDefinition); let result = this.states.get(cacheKey); if (result == null) { let fake: FakeGlobalState; // Look for established mock if (this.establishedMocks.has(keyDefinition.key)) { fake = this.establishedMocks.get(keyDefinition.key) as FakeGlobalState; } else { fake = new FakeGlobalState(); } fake.keyDefinition = keyDefinition; result = fake; this.states.set(cacheKey, result); result = new FakeGlobalState(); this.states.set(cacheKey, result); } return result as GlobalState; } private cacheKey(keyDefinition: KeyDefinition) { return `${keyDefinition.fullName}_${keyDefinition.stateDefinition.defaultStorageLocation}`; } getFake(keyDefinition: KeyDefinition): FakeGlobalState { return this.get(keyDefinition) as FakeGlobalState; } mockFor(keyDefinition: KeyDefinition, initialValue?: T): FakeGlobalState { const cacheKey = this.cacheKey(keyDefinition); if (!this.states.has(cacheKey)) { this.states.set(cacheKey, new FakeGlobalState(initialValue)); } return this.states.get(cacheKey) as FakeGlobalState; } } export class FakeSingleUserStateProvider implements SingleUserStateProvider { mock = mock(); states: Map> = new Map(); constructor( readonly updateSyncCallback?: ( key: UserKeyDefinition, userId: UserId, newValue: unknown, ) => Promise, ) {} get(userId: UserId, userKeyDefinition: UserKeyDefinition): SingleUserState { this.mock.get(userId, userKeyDefinition); const cacheKey = this.cacheKey(userId, userKeyDefinition); let result = this.states.get(cacheKey); if (result == null) { result = this.buildFakeState(userId, userKeyDefinition); this.states.set(cacheKey, result); } return result as SingleUserState; } getFake( userId: UserId, userKeyDefinition: UserKeyDefinition, { allowInit }: { allowInit: boolean } = { allowInit: true }, ): FakeSingleUserState { if (!allowInit && this.states.get(this.cacheKey(userId, userKeyDefinition)) == null) { return null; } return this.get(userId, userKeyDefinition) as FakeSingleUserState; } mockFor( userId: UserId, userKeyDefinition: UserKeyDefinition, initialValue?: T, ): FakeSingleUserState { const cacheKey = this.cacheKey(userId, userKeyDefinition); if (!this.states.has(cacheKey)) { this.states.set(cacheKey, this.buildFakeState(userId, userKeyDefinition, initialValue)); } return this.states.get(cacheKey) as FakeSingleUserState; } private buildFakeState( userId: UserId, userKeyDefinition: UserKeyDefinition, initialValue?: T, ) { const state = new FakeSingleUserState(userId, initialValue, async (...args) => { await this.updateSyncCallback?.(userKeyDefinition, ...args); }); state.keyDefinition = userKeyDefinition; return state; } private cacheKey(userId: UserId, userKeyDefinition: UserKeyDefinition) { return `${userKeyDefinitionCacheKey(userKeyDefinition)}_${userId}`; } } export class FakeActiveUserStateProvider implements ActiveUserStateProvider { activeUserId$: Observable; states: Map> = new Map(); constructor( public accountService: FakeAccountService, readonly updateSyncCallback?: ( key: UserKeyDefinition, userId: UserId, newValue: unknown, ) => Promise, ) { this.activeUserId$ = accountService.activeAccountSubject.asObservable().pipe(map((a) => a?.id)); } get(userKeyDefinition: UserKeyDefinition): ActiveUserState { const cacheKey = userKeyDefinitionCacheKey(userKeyDefinition); let result = this.states.get(cacheKey); if (result == null) { result = this.buildFakeState(userKeyDefinition); this.states.set(cacheKey, result); } return result as ActiveUserState; } getFake( userKeyDefinition: UserKeyDefinition, { allowInit }: { allowInit: boolean } = { allowInit: true }, ): FakeActiveUserState { if (!allowInit && this.states.get(userKeyDefinitionCacheKey(userKeyDefinition)) == null) { return null; } return this.get(userKeyDefinition) as FakeActiveUserState; } mockFor(userKeyDefinition: UserKeyDefinition, initialValue?: T): FakeActiveUserState { const cacheKey = userKeyDefinitionCacheKey(userKeyDefinition); if (!this.states.has(cacheKey)) { this.states.set(cacheKey, this.buildFakeState(userKeyDefinition, initialValue)); } return this.states.get(cacheKey) as FakeActiveUserState; } private buildFakeState(userKeyDefinition: UserKeyDefinition, initialValue?: T) { const state = new FakeActiveUserState(this.accountService, initialValue, async (...args) => { await this.updateSyncCallback?.(userKeyDefinition, ...args); }); state.keyDefinition = userKeyDefinition; return state; } } function userKeyDefinitionCacheKey(userKeyDefinition: UserKeyDefinition) { return `${userKeyDefinition.fullName}_${userKeyDefinition.stateDefinition.defaultStorageLocation}`; } export class FakeStateProvider implements StateProvider { mock = mock(); getUserState$(userKeyDefinition: UserKeyDefinition, userId?: UserId): Observable { this.mock.getUserState$(userKeyDefinition, userId); if (userId) { return this.getUser(userId, userKeyDefinition).state$; } return this.getActive(userKeyDefinition).state$; } getUserStateOrDefault$( userKeyDefinition: UserKeyDefinition, config: { userId: UserId | undefined; defaultValue?: T }, ): Observable { const { userId, defaultValue = null } = config; this.mock.getUserStateOrDefault$(userKeyDefinition, config); if (userId) { return this.getUser(userId, userKeyDefinition).state$; } return this.activeUserId$.pipe( take(1), switchMap((userId) => userId != null ? this.getUser(userId, userKeyDefinition).state$ : of(defaultValue), ), ); } async setUserState( userKeyDefinition: UserKeyDefinition, value: T, userId?: UserId, ): Promise<[UserId, T]> { await this.mock.setUserState(userKeyDefinition, value, userId); if (userId) { return [userId, await this.getUser(userId, userKeyDefinition).update(() => value)]; } else { return await this.getActive(userKeyDefinition).update(() => value); } } getActive(userKeyDefinition: UserKeyDefinition): ActiveUserState { return this.activeUser.get(userKeyDefinition); } getGlobal(keyDefinition: KeyDefinition): GlobalState { return this.global.get(keyDefinition); } getUser(userId: UserId, userKeyDefinition: UserKeyDefinition): SingleUserState { return this.singleUser.get(userId, userKeyDefinition); } getDerived( parentState$: Observable, deriveDefinition: DeriveDefinition, dependencies: TDeps, ): DerivedState { return this.derived.get(parentState$, deriveDefinition, dependencies); } constructor(public accountService: FakeAccountService) {} private distributeSingleUserUpdate( key: UserKeyDefinition, userId: UserId, newState: unknown, ) { if (this.activeUser.accountService.activeUserId === userId) { const state = this.activeUser.getFake(key, { allowInit: false }); state?.nextState(newState, { syncValue: false }); } } private distributeActiveUserUpdate( key: UserKeyDefinition, userId: UserId, newState: unknown, ) { this.singleUser .getFake(userId, key, { allowInit: false }) ?.nextState(newState, { syncValue: false }); } global: FakeGlobalStateProvider = new FakeGlobalStateProvider(); singleUser: FakeSingleUserStateProvider = new FakeSingleUserStateProvider( this.distributeSingleUserUpdate.bind(this), ); activeUser: FakeActiveUserStateProvider = new FakeActiveUserStateProvider( this.accountService, this.distributeActiveUserUpdate.bind(this), ); derived: FakeDerivedStateProvider = new FakeDerivedStateProvider(); activeUserId$: Observable = this.activeUser.activeUserId$; } export class FakeDerivedStateProvider implements DerivedStateProvider { states: Map> = new Map(); get( parentState$: Observable, deriveDefinition: DeriveDefinition, dependencies: TDeps, ): DerivedState { let result = this.states.get(deriveDefinition.buildCacheKey()) as DerivedState; if (result == null) { result = new FakeDerivedState(parentState$, deriveDefinition, dependencies); this.states.set(deriveDefinition.buildCacheKey(), result); } return result; } }