import type {} from '@sinclair/typebox' import type { EntityID } from '@wovin/core/applog' import type { Thread } from '@wovin/core/thread' import type { Accessor } from 'solid-js' import type { TypeMapKeys, VMap, VMstatic } from './utils-typemap' import { TypeCompiler } from '@sinclair/typebox/compiler' import { dateNowIso, EntityID_LENGTH, getHashID } from '@wovin/core/applog' import { isObservableArray } from '@wovin/core/mobx' import { lastWriteWins, observableArrayMap } from '@wovin/core/query' import { rollingFilter } from '@wovin/core/thread' import { Logger } from 'besonders-logger' import { createMemo } from 'solid-js' import { useEntityAt, useRawThread, useThreadFromContext, withDS } from '../../ui/reactive' import { insertApplogs } from '../ApplogDB' import { ObjectBuilder } from './builder' import { TypeMap } from './TypeMap' import { KnownAttrs, UniVMaps } from './utils-typemap' const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO, { prefix: '[MappedVM]' }) // eslint-disable-line unused-imports/no-unused-vars export interface BaseVM { new( en: EntityID, thread: Thread, ): any // get(this: T, en: EntityID, thread: Thread): InstanceType } export function getInitializedMap(VMname: VMnameT, thread: Thread) { let mapForThread = UniVMaps.get(thread) if (!mapForThread) { mapForThread = new Map() // empty map for the actual VMs DEBUG('created new map for thread', thread) UniVMaps.set(thread, mapForThread) } if (mapForThread.get(VMname)) { VERBOSE('using existing map', VMname, { UniVMaps, thread }) } else { mapForThread.set(VMname, new Map()) DEBUG('created new map for VM', VMname, { UniVMaps, thread }) } return mapForThread as VMap } export function getMappedVMtoExtend( VMname: whichVM, VMtype = TypeMap[VMname], skipGetters = [] as typeof KnownAttrs[whichVM], atPrefix: string = VMtype.wovinPrefix ?? VMname.toLowerCase(), ) { const attrs = KnownAttrs[VMname] // interface NamedMapVM { // // new(en: EntityID, thread: Thread, initial?: VMstatic): VMstatic // HACK ts nasty // // getVMap: (thread: Thread) => VMap // isDeleted: Accessor // deleteEntity: () => void // initialize: (instance: VMstatic, thread: Thread) => void // } const getVMap = (thread: Thread) => { const mapForThread = getInitializedMap(VMname, thread) const thisVMap = mapForThread.get(VMname) VERBOSE('get VMap', VMname, { thread, UniVMaps, thisVMap }) return thisVMap } const getMappedVM = (en, thread) => { const thisVMap = getVMap(thread) if (!thisVMap) { throw ERROR('missing VMap for', { thisVMap, thread }) } return thisVMap.get(en) } VERBOSE({ VMname, VMtype }) const compiledChecker = TypeCompiler.Compile(VMtype) type VMTypeT = VMstatic // const BuilderClass = class extends ObjectBuilder { } // doesn't really do anything useful const NamedMapVM = class { static DEFAULTS = {} as Partial static get(this: T, en: EntityID, thread: Thread) /* : InstanceType */ { VERBOSE(`[getVM<${VMname}>]`, en, this, thread) if (!en || typeof en !== 'string') throw ERROR(`[${VMname}VM.get] invalid en:`, en) const thisVMap = getVMap(thread) const existing = getMappedVM(en, thread) if (existing) { const dummyThis = new this(null, null) !!(typeof existing !== typeof dummyThis) && WARN('type mismatch in VMap', { existing, dummyThis }) // HACK: type "check" return existing as InstanceType // HACK: type assumption } const newVM = new this(en, thread) as InstanceType VERBOSE(`[getVM<${VMname}>] newVM:`, { newVM, hasInit: (newVM as any).initialize }) if ((newVM as any).initialize) { ;(newVM as any).initialize(newVM, thread) } thisVMap.set(en, newVM) return newVM } setDeleted(thread = this.thread) { insertApplogs(thread, [{ en: this.en, at: 'isDeleted', vl: true }]) } get isDeleted() { // HACK: lastWriteWins if (this.thread.filters.includes('withoutDeleted')) WARN(`BlockVM.isDeleted on a withoutDeleted thread`, this) return withDS(lastWriteWins(this.thread, { tolerateAlreadyFiltered: true }), () => useEntityAt(this.en, `isDeleted`)[0]()) } constructor( public en: EntityID, public thread: Thread, ) { if (this.en === null && this.thread === null) { VERBOSE('returning dummy instance for typechecking never to be initialized') return } DEBUG('initalizing', VMname, en, { skipGetters, thread, VMtype, selff: self, thiss: this, init: (this as any).initialize }) // add getters and setters for eachKnownAttr of the VMtype for (const eachAttr of attrs) { if (eachAttr === 'en' || skipGetters.includes(eachAttr)) { continue } if (typeof eachAttr !== 'string') throw ERROR(`VM attribute is not a string (add to skipGetters?):`, eachAttr) DEBUG(`[getVM<${VMname}>] attr`, eachAttr, this) const [getter, setter] = withDS( lastWriteWins(this.thread, { tolerateAlreadyFiltered: true }), // HACK: lastWriteWins () => useEntityAt(this.en, `${atPrefix}/${eachAttr}`, this.constructor.DEFAULTS[eachAttr.toString()]), ) Object.defineProperty(this, eachAttr.toString(), { get: getter, // () { return getter }, // warning this was not reactive when useEntityAt returned a memo set: setter, enumerable: true, configurable: true, }) // ? also add {at}PvVl and {at}PvCID ? VERBOSE('added getter and setter for', { eachAttr, instance: this }) } // // @ts-expect-error // return existing as VMstatic & ReturnType // return this as this & VMstatic // & ReturnType } get entityThread() { return rollingFilter(this.thread, { en: this.en }) } get description() { return `I am an instance of ${this.constructor.name} with en=${this.en}` } static buildNew(init: Partial = {}, en?: EntityID) { return new ObjectBuilder(init, en, VMname, atPrefix) } buildUpdate(init: Partial = {}) { return new ObjectBuilder(init, this.en, VMname, atPrefix) } get typed() { // generic - only needed within class methods, instances used by consumers are typed return this as unknown as ReturnType & VMTypeT } check = compiledChecker // TODO try it out } return NamedMapVM /* as unknown as NamedMapVM */ } export function getUseFx< entityVM extends ReturnType>, entityB extends typeof ObjectBuilder>, entityT extends TypeMapKeys = TypeMapKeys, >( VMname: entityT, VMclass: entityVM, Bclass: entityB, ) { return function use(prop: string | Partial>, ds?: Thread) { if (!prop) { return null // HACK: wrongly makes TS think the result is never empty } let en: string, initUp: Omit>, 'en'> if (typeof prop === 'string') { en = prop initUp = {} } else { ;({ en, ...initUp } = prop as Partial>) } if (!en) { const init = { ...initUp, ts: dateNowIso() } en = getHashID(init, EntityID_LENGTH) DEBUG('creating new', { en, init }) } ds = ds ?? useRawThread() VERBOSE('use', VMname, en, VMclass, Bclass) const instanceVM = VMclass.get(en, ds) // if new, this RelVM will have no data, but should react to relB.commit() const instanceB = Bclass.create(initUp, en, VMname) as InstanceType // the "restProps" will be already on the Builder const returnArray = [instanceVM, instanceB] as const VERBOSE('deprecating use with the array nonesense') return instanceVM as InstanceType } } // export function getObjectBuilderFor() { // } export function getVM(VMclass: VM, en: EntityID, thread: Thread = useThreadFromContext()) { // HACK: how to do VM extends ..? const instanceVM: InstanceType = VMclass.get(en, thread) return instanceVM } export function useVM(VMclass: VM, en: Accessor, thread: Thread = useThreadFromContext()) { // HACK: how to do VM extends ..? return createMemo(() => { if (!en()) return null const instanceVM: InstanceType = VMclass.get(en(), thread) return instanceVM }) } export function getVMs(VMclass: VM, ens: readonly EntityID[], thread: Thread = useThreadFromContext()) { const get = () => { return ens.map(en => getVM(VMclass, en, thread)) } if (isObservableArray(ens)) { return observableArrayMap(get) } return get() }