import { from, Observable, Observer, Subject, Subscription } from 'rxjs' import { map } from 'rxjs/operators' import { Args, internalToOriginalEvent, RemeshCommand, RemeshCommandAction, RemeshCommandContext, RemeshCommandOutput, RemeshDomain, RemeshDomainAction, RemeshDomainContext, RemeshDomainDefinition, RemeshDomainPreloadCommandContext, RemeshDomainPreloadOptions, RemeshEffect, RemeshEffectContext, RemeshAction, RemeshEvent, RemeshEventAction, RemeshEventContext, RemeshEventOptions, RemeshExtern, RemeshExternImpl, RemeshInjectedContext, RemeshQuery, RemeshQueryAction, RemeshQueryContext, RemeshState, RemeshStateItem, RemeshSubscribeOnlyEvent, Serializable, toValidRemeshDomainDefinition, VerifiedRemeshDomainDefinition, RemeshDomainPreloadQueryContext, getCommandTaskSet, DefaultStateValue, } from './remesh' import { createInspectorManager, InspectorType } from './inspector' export type PreloadedState = Record export type RemeshStore = ReturnType let uid = 0 export type RemeshStateStorage = { id: number type: 'RemeshStateStorage' State: RemeshState stateItem: RemeshStateItem currentState: DefaultStateValue | T downstreamSet: Set> } export type RemeshQueryStorage, U> = { id: number type: 'RemeshQueryStorage' Query: RemeshQuery arg: T[0] key: string currentValue: U upstreamSet: Set | RemeshStateStorage> downstreamSet: Set> subject: Subject innerRefCount: number outerRefCount: number status: 'default' | 'wip' | 'updated' wipUpstreamSet: Set | RemeshStateStorage> } export type RemeshEventStorage = { id: number type: 'RemeshEventStorage' Event: RemeshEvent subject: Subject observable: Observable innerRefCount: number outerRefCount: number } export type RemeshDomainStorage> = { id: number type: 'RemeshDomainStorage' Domain: RemeshDomain key: string arg: U[0] domain: VerifiedRemeshDomainDefinition domainContext: RemeshDomainContext domainAction: RemeshDomainAction upstreamSet: Set> downstreamSet: Set> preloadOptionsList: RemeshDomainPreloadOptions[] preloadedPromise?: Promise preloadedState: PreloadedState effectList: RemeshEffect[] effectMap: Map stateMap: Map, RemeshStateStorage> queryMap: Map> eventMap: Map, RemeshEventStorage> refCount: number ignited: boolean hasBeenPreloaded: boolean isDiscarding: boolean } export type RemeshExternStorage = { id: number type: 'RemeshExternStorage' Extern: RemeshExtern currentValue: T } export type RemeshStoreInspector = typeof RemeshStore export type RemeshStoreOptions = { name?: string externs?: RemeshExternImpl[] inspectors?: (RemeshStoreInspector | false | undefined | null)[] preloadedState?: PreloadedState } export const RemeshStore = (options?: RemeshStoreOptions) => { const config = { ...options, } const inspectorManager = createInspectorManager(config) const pendingEmitSet = new Set | RemeshEventAction>() /** * Leaf means that the query storage has no downstream query storages */ const pendingLeafSet = new Set>() const pendingClearSet = new Set>() const domainStorageMap = new Map>() const externStorageWeakMap = new WeakMap, RemeshExternStorage>() const getExternValue = (Extern: RemeshExtern): T => { for (const externImpl of config.externs ?? []) { if (externImpl.Extern === Extern) { return externImpl.value } } return Extern.default } const getExternStorage = (Extern: RemeshExtern): RemeshExternStorage => { const externStorage = externStorageWeakMap.get(Extern) if (externStorage) { return externStorage } const currentValue = getExternValue(Extern) const currentExternStorage: RemeshExternStorage = { id: uid++, type: 'RemeshExternStorage', Extern, currentValue, } externStorageWeakMap.set(Extern, currentExternStorage) return currentExternStorage } const getExternCurrentValue = (Extern: RemeshExtern): T => { return getExternStorage(Extern).currentValue } const storageKeyWeakMap = new WeakMap | RemeshQueryAction, string>() const getQueryStorageKey = , U>(queryAction: RemeshQueryAction): string => { const key = storageKeyWeakMap.get(queryAction) if (key) { return key } const queryName = queryAction.Query.queryName const argString = JSON.stringify(queryAction.arg) ?? '' const keyString = `Query/${queryAction.Query.queryId}/${queryName}:${argString}` storageKeyWeakMap.set(queryAction, keyString) return keyString } const getDomainStorageKey = >( domainAction: RemeshDomainAction, ): string => { const key = storageKeyWeakMap.get(domainAction) if (key) { return key } const domainName = domainAction.Domain.domainName const argString = JSON.stringify(domainAction.arg) ?? '' const keyString = `Domain/${domainAction.Domain.domainId}/${domainName}:${argString}` storageKeyWeakMap.set(domainAction, keyString) return keyString } const getStorageKey = , U>( input: RemeshQueryAction | RemeshDomainAction, ): string => { if (input.type === 'RemeshQueryAction') { return getQueryStorageKey(input) } return getDomainStorageKey(input) } const stateStorageWeakMap = new WeakMap, RemeshStateStorage>() const getStateValue = (State: RemeshState) => { if (typeof State.default === 'function') { return (State.default as () => T)() } return State.default } const createStateStorage = (stateItem: RemeshStateItem): RemeshStateStorage => { const domainStorage = getDomainStorage(stateItem.State.owner) const currentState = getStateValue(stateItem.State) const stateStorage: RemeshStateStorage = { id: uid++, type: 'RemeshStateStorage', State: stateItem.State, stateItem, currentState, downstreamSet: new Set(), } domainStorage.stateMap.set(stateItem, stateStorage) stateStorageWeakMap.set(stateItem, stateStorage) inspectorManager.inspectStateStorage(InspectorType.StateCreated, stateStorage) return stateStorage } const restoreStateStorage = (stateStorage: RemeshStateStorage) => { const domainStorage = getDomainStorage(stateStorage.State.owner) if (domainStorage.stateMap.has(stateStorage.stateItem)) { return } stateStorage.currentState = getStateValue(stateStorage.State) domainStorage.stateMap.set(stateStorage.stateItem, stateStorage) inspectorManager.inspectStateStorage(InspectorType.StateReused, stateStorage) } const getStateStorage = (stateItem: RemeshStateItem): RemeshStateStorage => { const domainStorage = getDomainStorage(stateItem.State.owner) const stateStorage = domainStorage.stateMap.get(stateItem) if (stateStorage) { return stateStorage as RemeshStateStorage } const cachedStorage = stateStorageWeakMap.get(stateItem) if (cachedStorage) { restoreStateStorage(cachedStorage) return cachedStorage } return createStateStorage(stateItem) } const eventStorageWeakMap = new WeakMap, RemeshEventStorage>() const createEventStorage = (Event: RemeshEvent): RemeshEventStorage => { const domainStorage = getDomainStorage(Event.owner) const eventContext: RemeshEventContext = { get: remeshInjectedContext.get, } const subject = new Subject() const observableMapToImplIfNeeded = subject.pipe( map((arg) => { if (Event.impl) { return Event.impl(eventContext, arg) } return arg as unknown as U }), ) const observable = new Observable((observer) => { const subscription = observableMapToImplIfNeeded.subscribe(observer) return () => { subscription.unsubscribe() clearEventStorageIfNeeded(eventStorage) } }) const cachedStorage = eventStorageWeakMap.get(Event) const eventStorage = Object.assign(cachedStorage ?? {}, { id: uid++, type: 'RemeshEventStorage', Event, subject, observable, innerRefCount: 0, outerRefCount: 0, } as RemeshEventStorage) domainStorage.eventMap.set(Event, eventStorage) eventStorageWeakMap.set(Event, eventStorage) return eventStorage } const getEventStorage = (Event: RemeshEvent): RemeshEventStorage => { const domainStorage = getDomainStorage(Event.owner) const eventStorage = domainStorage.eventMap.get(Event) ?? createEventStorage(Event) return eventStorage as RemeshEventStorage } const queryStorageWeakMap = new WeakMap, RemeshQueryStorage>() const createQueryStorage = , U>( queryAction: RemeshQueryAction, ): RemeshQueryStorage => { const domainStorage = getDomainStorage(queryAction.Query.owner) const key = getQueryStorageKey(queryAction) const subject = new Subject() const upstreamSet: RemeshQueryStorage['upstreamSet'] = new Set() const currentQueryStorage = { id: uid++, type: 'RemeshQueryStorage', Query: queryAction.Query, arg: queryAction.arg, key, upstreamSet, downstreamSet: new Set(), subject, innerRefCount: 0, outerRefCount: 0, status: 'default', wipUpstreamSet: new Set(), } as RemeshQueryStorage const { Query } = queryAction const queryContext: RemeshQueryContext = { get: (input: RemeshStateItem | RemeshQueryAction) => { if (currentQueryStorage.upstreamSet !== upstreamSet) { return remeshInjectedContext.get(input) } if (input.type === 'RemeshStateItem') { const upstreamStateStorage = getStateStorage(input) currentQueryStorage.upstreamSet.add(upstreamStateStorage) upstreamStateStorage.downstreamSet.add(currentQueryStorage) return remeshInjectedContext.get(input) } if (input.type === 'RemeshQueryAction') { const upstreamQueryStorage = getQueryStorage(input) currentQueryStorage.upstreamSet.add(upstreamQueryStorage) upstreamQueryStorage.downstreamSet.add(currentQueryStorage) return remeshInjectedContext.get(input) } return remeshInjectedContext.get(input) }, } const currentValue = Query.impl(queryContext, queryAction.arg) currentQueryStorage.currentValue = currentValue domainStorage.queryMap.set(key, currentQueryStorage) queryStorageWeakMap.set(queryAction, currentQueryStorage) inspectorManager.inspectQueryStorage(InspectorType.QueryCreated, currentQueryStorage) return currentQueryStorage } const restoreQueryStorage = , U>(queryStorage: RemeshQueryStorage) => { const domainStorage = getDomainStorage(queryStorage.Query.owner) if (domainStorage.queryMap.has(queryStorage.key)) { return } const subject = new Subject() queryStorage.status = 'default' queryStorage.subject = subject domainStorage.queryMap.set(queryStorage.key, queryStorage) for (const upstream of queryStorage.upstreamSet) { upstream.downstreamSet.add(queryStorage) if (upstream.type === 'RemeshQueryStorage') { restoreQueryStorage(upstream) } else if (upstream.type === 'RemeshStateStorage') { restoreStateStorage(upstream) } else { throw new Error(`Unknown upstream: ${upstream}`) } } updateQueryStorage(queryStorage) inspectorManager.inspectQueryStorage(InspectorType.QueryReused, queryStorage) } const getQueryStorage = , U>( queryAction: RemeshQueryAction, ): RemeshQueryStorage => { const domainStorage = getDomainStorage(queryAction.Query.owner) const key = getQueryStorageKey(queryAction) const queryStorage = domainStorage.queryMap.get(key) if (queryStorage) { return queryStorage } const cachedStorage = queryStorageWeakMap.get(queryAction) if (cachedStorage) { restoreQueryStorage(cachedStorage) return cachedStorage } return createQueryStorage(queryAction) } const domainStorageWeakMap = new WeakMap, RemeshDomainStorage>() const createDomainStorage = >( domainAction: RemeshDomainAction, ): RemeshDomainStorage => { const key = getDomainStorageKey(domainAction) const upstreamSet: RemeshDomainStorage['upstreamSet'] = new Set() const domainContext: RemeshDomainContext = { state: (options) => { const State = RemeshState(options) State.owner = domainAction return State }, query: (options) => { const Query = RemeshQuery(options) Query.owner = domainAction return Query }, event: (options: Omit, 'impl'> | RemeshEventOptions) => { const Event = RemeshEvent(options) Event.owner = domainAction return Event as RemeshEvent }, command: (options) => { const Command = RemeshCommand(options) Command.owner = domainAction return Command }, effect: (effect) => { if (!currentDomainStorage.ignited) { currentDomainStorage.effectList.push(effect) } }, preload: (preloadOptions) => { if (!currentDomainStorage.ignited) { currentDomainStorage.preloadOptionsList.push(preloadOptions) } }, getDomain: (upstreamDomainAction) => { const upstreamDomainStorage = getDomainStorage(upstreamDomainAction) upstreamSet.add(upstreamDomainStorage) upstreamDomainStorage.downstreamSet.add(currentDomainStorage) if (currentDomainStorage.ignited) { igniteDomain(upstreamDomainAction) } return upstreamDomainStorage.domain }, forgetDomain: (upstreamDomainAction) => { const upstreamDomainStorage = getDomainStorage(upstreamDomainAction) upstreamSet.delete(upstreamDomainStorage) upstreamDomainStorage.downstreamSet.delete(currentDomainStorage) clearDomainStorageIfNeeded(upstreamDomainStorage) }, getExtern: (Extern) => { return getExternCurrentValue(Extern) }, } const currentDomainStorage: RemeshDomainStorage = { id: uid++, type: 'RemeshDomainStorage', Domain: domainAction.Domain, get domain() { return domain }, arg: domainAction.arg, domainContext, domainAction, key, upstreamSet, downstreamSet: new Set(), effectList: [], stateMap: new Map(), queryMap: new Map(), eventMap: new Map(), effectMap: new Map(), preloadOptionsList: [], preloadedState: {}, refCount: 0, ignited: false, hasBeenPreloaded: false, isDiscarding: false, } const domain = toValidRemeshDomainDefinition(domainAction.Domain.impl(domainContext, domainAction.arg)) domainStorageMap.set(key, currentDomainStorage) domainStorageWeakMap.set(domainAction, currentDomainStorage) inspectorManager.inspectDomainStorage(InspectorType.DomainCreated, currentDomainStorage) injectPreloadState(currentDomainStorage) return currentDomainStorage } const injectPreloadState = >( domainStorage: RemeshDomainStorage, ) => { if (!options?.preloadedState) { return } const preloadCommandContext = { get: remeshInjectedContext.get, } for (const preloadOptions of domainStorage.preloadOptionsList) { if (Object.prototype.hasOwnProperty.call(options.preloadedState, preloadOptions.key)) { const data = options.preloadedState[preloadOptions.key] handleCommandOutput(preloadOptions.command(preloadCommandContext, data)) } } } const getDomainStorage = >( domainAction: RemeshDomainAction, ): RemeshDomainStorage => { const key = getDomainStorageKey(domainAction) const domainStorage = domainStorageMap.get(key) if (domainStorage) { domainStorageWeakMap.set(domainAction, domainStorage) return domainStorage } const cachedDomainStorage = domainStorageWeakMap.get(domainAction) if (cachedDomainStorage) { cachedDomainStorage.ignited = false cachedDomainStorage.isDiscarding = false domainStorageMap.set(cachedDomainStorage.key, cachedDomainStorage) for (const upstreamDomainStorage of cachedDomainStorage.upstreamSet) { upstreamDomainStorage.downstreamSet.add(cachedDomainStorage) } inspectorManager.inspectDomainStorage(InspectorType.DomainReused, cachedDomainStorage) return cachedDomainStorage } return createDomainStorage(domainAction) } const clearQueryStorage = , U>(queryStorage: RemeshQueryStorage) => { const domainStorage = getDomainStorage(queryStorage.Query.owner) if (!domainStorage.queryMap.has(queryStorage.key)) { return } domainStorage.queryMap.delete(queryStorage.key) queryStorage.subject.complete() inspectorManager.inspectQueryStorage(InspectorType.QueryDiscarded, queryStorage) for (const upstreamStorage of queryStorage.upstreamSet) { if (!upstreamStorage.downstreamSet.has(queryStorage)) { continue } upstreamStorage.downstreamSet.delete(queryStorage) if (upstreamStorage.type === 'RemeshQueryStorage') { clearQueryStorageIfNeeded(upstreamStorage) } } clearDomainStorageIfNeeded(domainStorage) } const shouldClearQueryStorage = , U>( queryStorage: RemeshQueryStorage, ): boolean => { const domainStorage = getDomainStorage(queryStorage.Query.owner) if (domainStorage.refCount > 0) { if (queryStorage.innerRefCount > 0 || queryStorage.outerRefCount > 0) { return false } } else { if (queryStorage.outerRefCount > 0) { return false } } if (queryStorage.downstreamSet.size !== 0) { return false } return true } const clearQueryStorageIfNeeded = , U>(queryStorage: RemeshQueryStorage) => { if (shouldClearQueryStorage(queryStorage)) { clearQueryStorage(queryStorage) } } const clearStateStorage = (stateStorage: RemeshStateStorage) => { const domainStorage = getDomainStorage(stateStorage.State.owner) if (!domainStorage.stateMap.has(stateStorage.stateItem)) { return } domainStorage.stateMap.delete(stateStorage.stateItem) stateStorage.downstreamSet.clear() inspectorManager.inspectStateStorage(InspectorType.StateDiscarded, stateStorage) } const clearEventStorage = (eventStorage: RemeshEventStorage) => { const domainStorage = getDomainStorage(eventStorage.Event.owner) eventStorage.subject.complete() domainStorage.eventMap.delete(eventStorage.Event) clearDomainStorageIfNeeded(domainStorage) } const shouldClearEventStorage = (eventStorage: RemeshEventStorage): boolean => { const domainStorage = getDomainStorage(eventStorage.Event.owner) if (domainStorage.refCount > 0) { if (eventStorage.innerRefCount > 0 || eventStorage.outerRefCount > 0) { return false } } else { if (eventStorage.outerRefCount > 0) { return false } } return true } const clearEventStorageIfNeeded = (eventStorage: RemeshEventStorage) => { if (shouldClearEventStorage(eventStorage)) { clearEventStorage(eventStorage) } } const clearDomainStorage = >( domainStorage: RemeshDomainStorage, ) => { if (domainStorage.isDiscarding) { return } domainStorage.isDiscarding = true domainStorage.ignited = false for (const subscription of domainStorage.effectMap.values()) { subscription.unsubscribe() } for (const eventStorage of domainStorage.eventMap.values()) { clearEventStorage(eventStorage) } for (const queryStorage of domainStorage.queryMap.values()) { clearQueryStorage(queryStorage) } for (const stateStorage of domainStorage.stateMap.values()) { clearStateStorage(stateStorage) } domainStorage.downstreamSet.clear() domainStorage.stateMap.clear() domainStorage.queryMap.clear() domainStorage.eventMap.clear() domainStorage.effectMap.clear() domainStorageMap.delete(domainStorage.key) inspectorManager.inspectDomainStorage(InspectorType.DomainDiscarded, domainStorage) for (const upstreamDomainStorage of domainStorage.upstreamSet) { if (!upstreamDomainStorage.downstreamSet.has(domainStorage)) { continue } upstreamDomainStorage.downstreamSet.delete(domainStorage) clearDomainStorageIfNeeded(upstreamDomainStorage) } } const shouldClearDomainStorage = >( domainStorage: RemeshDomainStorage, ): boolean => { if (domainStorage.refCount > 0) { return false } if (domainStorage.isDiscarding) { return false } if (domainStorage.downstreamSet.size !== 0) { return false } /** * we only check the refCount of queryStorage and eventStorage * when their refCount is 0, it means there is no consumers outside of the domain * so the domain resources can be cleared */ for (const queryStorage of domainStorage.queryMap.values()) { if (queryStorage.outerRefCount > 0) { return false } } for (const eventStorage of domainStorage.eventMap.values()) { if (eventStorage.outerRefCount > 0) { return false } } return true } const clearDomainStorageIfNeeded = >( domainStorage: RemeshDomainStorage, ) => { if (shouldClearDomainStorage(domainStorage)) { clearDomainStorage(domainStorage) } } const getCurrentState = (stateItem: RemeshStateItem): T => { const stateStorage = getStateStorage(stateItem) if (stateStorage.currentState === DefaultStateValue) { throw new Error(`Unexpected reading domain.state before initializing: ${stateStorage.State.stateName}`) } return stateStorage.currentState } const getCurrentQueryValue = , U>(queryAction: RemeshQueryAction): U => { const queryStorage = getQueryStorage(queryAction) updateWipQueryStorage(queryStorage) const currentValue = queryStorage.currentValue return currentValue } const remeshInjectedContext: RemeshInjectedContext = { get: (input: RemeshStateItem | RemeshQueryAction) => { if (input.type === 'RemeshStateItem') { return getCurrentState(input) } if (input.type === 'RemeshQueryAction') { return getCurrentQueryValue(input) } throw new Error(`Unexpected input in ctx.get(..): ${input}`) }, fromEvent: (Event) => { const eventStorage = Event.type === 'RemeshSubscribeOnlyEvent' ? getEventStorage(internalToOriginalEvent(Event)) : getEventStorage(Event) const observable: Observable = new Observable((subscriber) => { const subscription = eventStorage.observable.subscribe(subscriber) eventStorage.innerRefCount += 1 return () => { eventStorage.innerRefCount -= 1 subscription.unsubscribe() clearEventStorageIfNeeded(eventStorage) } }) return observable }, fromQuery: (queryAction) => { const queryStorage = getQueryStorage(queryAction) const observable: Observable = new Observable((subscriber) => { const subscription = queryStorage.subject.subscribe(subscriber) queryStorage.innerRefCount += 1 return () => { queryStorage.innerRefCount -= 1 subscription.unsubscribe() clearQueryStorageIfNeeded(queryStorage) } }) return observable }, } const updateWipQueryStorage = , U>(queryStorage: RemeshQueryStorage) => { if (queryStorage.status !== 'wip') { return } if (queryStorage.wipUpstreamSet.size !== 0) { let shouldUpdate = false for (const upstream of queryStorage.wipUpstreamSet) { if (upstream.type === 'RemeshStateStorage') { shouldUpdate = true } else if (upstream.type === 'RemeshQueryStorage') { if (upstream.status === 'wip') { updateWipQueryStorage(upstream) } if (upstream.status === 'updated') { shouldUpdate = true } } else { throw new Error(`Unexpected upstream: ${upstream}`) } } queryStorage.wipUpstreamSet.clear() if (!shouldUpdate) { queryStorage.status = 'default' return } } const isUpdated = updateQueryStorage(queryStorage) if (isUpdated) { queryStorage.status = 'updated' } else { queryStorage.status = 'default' } } const updateQueryStorage = , U>(queryStorage: RemeshQueryStorage) => { const { Query } = queryStorage for (const upstream of queryStorage.upstreamSet) { if (!upstream.downstreamSet.has(queryStorage)) { continue } upstream.downstreamSet.delete(queryStorage) if (upstream.downstreamSet.size === 0) { if (upstream.type !== 'RemeshStateStorage') { pendingClearSet.add(upstream) } } } const upstreamSet: RemeshQueryStorage['upstreamSet'] = new Set() queryStorage.upstreamSet = upstreamSet const queryContext: RemeshQueryContext = { get: (input: RemeshStateItem | RemeshQueryAction) => { if (queryStorage.upstreamSet !== upstreamSet) { return remeshInjectedContext.get(input) } if (input.type === 'RemeshStateItem') { const upstreamStateStorage = getStateStorage(input) queryStorage.upstreamSet.add(upstreamStateStorage) upstreamStateStorage.downstreamSet.add(queryStorage) return remeshInjectedContext.get(input) } if (input.type === 'RemeshQueryAction') { const upstreamQueryStorage = getQueryStorage(input) queryStorage.upstreamSet.add(upstreamQueryStorage) upstreamQueryStorage.downstreamSet.add(queryStorage) return remeshInjectedContext.get(input) } return remeshInjectedContext.get(input) }, } let newValue: U const currentValue = queryStorage.currentValue if (!Query.onError) { newValue = Query.impl(queryContext, queryStorage.arg) } else { try { newValue = Query.impl(queryContext, queryStorage.arg) } catch (e) { const error = e instanceof Error ? e : new Error(`${e}`) newValue = Query.onError(error, currentValue) } } const isEqual = Query.compare(currentValue, newValue) if (isEqual) { return false } queryStorage.currentValue = newValue pendingEmitSet.add(queryStorage) inspectorManager.inspectQueryStorage(InspectorType.QueryUpdated, queryStorage) const changedTaskSet = getCommandTaskSet(Query, 'changed') if (changedTaskSet) { const data = { current: newValue, previous: currentValue, } for (const task of changedTaskSet) { handleCommandOutput(task(commandContext, data)) } } return true } const clearPendingStorageSetIfNeeded = () => { if (pendingClearSet.size === 0) { return } const storageList = [...pendingClearSet] pendingClearSet.clear() for (const storage of storageList) { clearQueryStorageIfNeeded(storage) } clearPendingStorageSetIfNeeded() } const clearPendingEmitSetIfNeeded = () => { if (pendingEmitSet.size === 0) { return } const list = [...pendingEmitSet] pendingEmitSet.clear() for (const item of list) { if (!pendingEmitSet.has(item)) { if (item.type === 'RemeshQueryStorage') { item.subject.next(item.currentValue) } else if (item.type === 'RemeshEventAction') { emitEvent(item) } } } /** * recursively consuming dynamic set until it become empty. */ clearPendingEmitSetIfNeeded() } const mark = , U>(queryStorage: RemeshQueryStorage) => { queryStorage.status = 'wip' if (queryStorage.downstreamSet.size > 0) { for (const downstream of queryStorage.downstreamSet) { if (!downstream.wipUpstreamSet.has(queryStorage)) { downstream.wipUpstreamSet.add(queryStorage) mark(downstream) } } } else { pendingLeafSet.add(queryStorage) } } const clearPendingLeafSetIfNeeded = () => { if (pendingLeafSet.size === 0) { return } const queryStorageList = [...pendingLeafSet] pendingLeafSet.clear() for (const queryStorage of queryStorageList) { updateWipQueryStorage(queryStorage) } /** * recursively consuming dynamic set until it become empty. */ clearPendingLeafSetIfNeeded() } const commit = () => { clearPendingLeafSetIfNeeded() clearPendingStorageSetIfNeeded() clearPendingEmitSetIfNeeded() } const updateStateItem = (stateItem: RemeshStateItem, newState: T) => { const stateStorage = getStateStorage(stateItem) if (stateStorage.currentState !== DefaultStateValue) { const isEqual = stateItem.State.compare(stateStorage.currentState, newState) if (isEqual) { return } } stateStorage.currentState = newState inspectorManager.inspectStateStorage(InspectorType.StateUpdated, stateStorage) for (const downstream of stateStorage.downstreamSet) { downstream.wipUpstreamSet.add(stateStorage) mark(downstream) } } const emitEvent = (eventAction: RemeshEventAction) => { const { Event, arg } = eventAction const eventStorage = getEventStorage(Event) inspectorManager.inspectEventEmitted(InspectorType.EventEmitted, eventAction) eventStorage.subject.next(arg) } const commandContext: RemeshCommandContext = { get: remeshInjectedContext.get, } const handleCommandAction = (commandAction: RemeshCommandAction) => { inspectorManager.inspectCommandReceived(InspectorType.CommandReceived, commandAction) const { Command, arg } = commandAction const fn = Command.impl const beforeTaskSet = getCommandTaskSet(Command, 'before') if (beforeTaskSet) { for (const task of beforeTaskSet) { handleCommandOutput(task(commandContext, arg)) } } handleCommandOutput(fn(commandContext, arg)) const afterTaskSet = getCommandTaskSet(Command, 'after') if (afterTaskSet) { for (const task of afterTaskSet) { handleCommandOutput(task(commandContext, arg)) } } } const handleCommandOutput = (output: RemeshCommandOutput) => { if (!output) { return } if (Array.isArray(output)) { for (const item of output) { handleCommandOutput(item) } } else if (output.type === 'RemeshCommandAction') { handleCommandAction(output) } else if (output.type === 'RemeshStateItemUpdatePayload') { updateStateItem(output.stateItem, output.value) } else if (output.type === 'RemeshEventAction') { handleRemeshEventAction(output) } } const handleRemeshEventAction = (eventAction: RemeshEventAction) => { const Event = eventAction.Event const emittedTaskSet = getCommandTaskSet(Event, 'emitted') pendingEmitSet.add(eventAction) if (emittedTaskSet) { for (const task of emittedTaskSet) { handleCommandOutput(task(commandContext, eventAction.arg)) } } } const subscribeDomain = >( domainAction: RemeshDomainAction, ) => { const domainStorage = getDomainStorage(domainAction) const subscription = new Subscription(() => { domainStorage.refCount -= 1 if (domainStorage.refCount === 0) { clearDomainStorageIfNeeded(domainStorage) } }) domainStorage.refCount += 1 return subscription } const subscribeQuery = , U>( queryAction: RemeshQueryAction, subscriber: ((data: U) => unknown) | Partial>, ): Subscription => { const queryStorage = getQueryStorage(queryAction) const observable: Observable = new Observable((subscriber) => { const subscription = queryStorage.subject.subscribe(subscriber) queryStorage.outerRefCount += 1 return () => { queryStorage.outerRefCount -= 1 subscription.unsubscribe() clearQueryStorageIfNeeded(queryStorage) } }) if (typeof subscriber === 'function') { return observable.subscribe(subscriber) } return observable.subscribe(subscriber) } const subscribeEvent = ( Event: RemeshEvent | RemeshSubscribeOnlyEvent, subscriber: (event: U) => unknown, ): Subscription => { const eventStorage = Event.type === 'RemeshSubscribeOnlyEvent' ? getEventStorage(internalToOriginalEvent(Event)) : getEventStorage(Event) const observable = new Observable((subscriber) => { const subscription = eventStorage.observable.subscribe(subscriber) eventStorage.outerRefCount += 1 return () => { eventStorage.outerRefCount -= 1 subscription.unsubscribe() clearEventStorageIfNeeded(eventStorage) } }) return observable.subscribe(subscriber) } const getDomain = >( domainAction: RemeshDomainAction, ) => { const domainStorage = getDomainStorage(domainAction) return domainStorage.domain } const effectContext: RemeshEffectContext = { get: remeshInjectedContext.get, fromEvent: remeshInjectedContext.fromEvent, fromQuery: remeshInjectedContext.fromQuery, } const handleEffectOutput = (output: RemeshAction) => { handleCommandOutput(output) commit() } const igniteDomain = >( domainAction: RemeshDomainAction, ) => { const domainStorage = getDomainStorage(domainAction) if (domainStorage.ignited) { return } domainStorage.ignited = true for (const upstreamDomainStorage of domainStorage.upstreamSet) { igniteDomain(upstreamDomainStorage.domainAction) } for (const effect of domainStorage.effectList) { igniteDomainEffect(domainStorage, effect) } } const igniteDomainEffect = >( domainStorage: RemeshDomainStorage, effect: RemeshEffect, ) => { const effectResult = effect.impl(effectContext) if (effectResult) { const subscription = from(effectResult).subscribe(handleEffectOutput) domainStorage.effectMap.set(effect, subscription) } } const discardDomain = >( domainAction: RemeshDomainAction, ) => { const domainStorage = getDomainStorage(domainAction) clearDomainStorage(domainStorage) } const discard = () => { inspectorManager.destroyInspectors() for (const domainStorage of domainStorageMap.values()) { clearDomainStorage(domainStorage) } domainStorageMap.clear() pendingEmitSet.clear() } const preload = >( domainAction: RemeshDomainAction, ) => { const domainStorage = getDomainStorage(domainAction) if (domainStorage.ignited) { throw new Error(`Domain ${domainAction.Domain.domainName} was ignited before preloading`) } if (domainStorage.preloadedPromise) { return domainStorage.preloadedPromise } const preloadedPromise = preloadDomain(domainAction) domainStorage.preloadedPromise = preloadedPromise return preloadedPromise } const domainPreloadCommandContext: RemeshDomainPreloadCommandContext = { get: remeshInjectedContext.get, } const domainPreloadQueryContext: RemeshDomainPreloadQueryContext = { get: remeshInjectedContext.get, } const preloadDomain = async >( domainAction: RemeshDomainAction, ) => { const domainStorage = getDomainStorage(domainAction) if (domainStorage.hasBeenPreloaded) { return } domainStorage.hasBeenPreloaded = true await Promise.all( Array.from(domainStorage.upstreamSet).map((upstreamDomainStorage) => { return preload(upstreamDomainStorage.domainAction) }), ) await Promise.all( domainStorage.preloadOptionsList.map(async (preloadOptions) => { const data = await preloadOptions.query(domainPreloadQueryContext) if (Object.prototype.hasOwnProperty.call(domainStorage.preloadedState, preloadOptions.key)) { throw new Error(`Duplicate key ${preloadOptions.key}`) } domainStorage.preloadedState[preloadOptions.key] = data const commandOutput = preloadOptions.command(domainPreloadCommandContext, data) handleCommandOutput(commandOutput) }), ) } const getPreloadedState = () => { const preloadedState = {} as PreloadedState for (const domainStorage of domainStorageMap.values()) { Object.assign(preloadedState, domainStorage.preloadedState) } return preloadedState } const getDomainPreloadedState = >( domainAction: RemeshDomainAction, ): PreloadedState => { const domainStorage = getDomainStorage(domainAction) return domainStorage.preloadedState } const send = (output: RemeshAction) => { handleCommandOutput(output) commit() } return { name: config.name, getDomain, igniteDomain, discardDomain, query: getCurrentQueryValue, send, discard, preload, getPreloadedState, getDomainPreloadedState, subscribeDomain, subscribeQuery, subscribeEvent, getKey: getStorageKey, } }