import type { Static, TObject } from '@sinclair/typebox' import type { EntityID } from '@wovin/core/applog' import type { Thread } from '@wovin/core/thread' import type { ConstructorType } from '@wovin/core/types' import type { TypeMapKeys, TypeMapValueTB, VMap, VMstatic } from './utils-typemap' import { TypeCompiler } from '@sinclair/typebox/compiler' import { dateNowIso, EntityID_LENGTH, getHashID } from '@wovin/core/applog' import { lastWriteWins } from '@wovin/core/query' import { rollingFilter } from '@wovin/core/thread' import { Logger } from 'besonders-logger' import { useEntityAt, useRawThread, withDS } from '../../ui/reactive' import { insertApplogs } from '../ApplogDB' import { ObjectBuilder } from './builder' import { KnownAttrs, UniVMaps } from './utils-typemap' const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO, { prefix: '[VMfactory]' }) // eslint-disable-line unused-imports/no-unused-vars 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 getBaseVMtoExtend( VMname: string, VMtype: TBtype, skipGetters: string[] = [], ) { // const attrs = VMtype.properties const attrs = KnownAttrs[VMname] const lowerCaseAttrNamespace = VMname.toLowerCase() const getMappedVM = (en, thread) => { const thisVMap = getVMap(thread) if (!thisVMap) { throw ERROR('missing VMap for', { thisVMap, thread }) } return thisVMap.get(en) } const getVMap = (thread: Thread) => { const mapForThread = getInitializedMap(VMname, thread) const thisVMap = mapForThread.get(VMname) VERBOSE('get VMap', VMname, { thread, UniVMaps, thisVMap }) return thisVMap } VERBOSE('getBaseVMtoExtend', { VMname, VMtype, attrs }) const compiledChecker = TypeCompiler.Compile(VMtype) type VMtypeT = Static type VMtypeI = { [P in keyof VMtypeT]: string } class BaseVM { static get(this: T, en: EntityID, thread: Thread) /* : InstanceType */ { VERBOSE(`[getVM<${VMname}>]`, en, this, thread) const thisVMap = getVMap(thread) const existing = getMappedVM(en, thread) if (existing) { const dummyThis = new this(null, null, 'imsure') !!(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, 'imsure') 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 return withDS(lastWriteWins(this.thread, { tolerateAlreadyFiltered: true }), () => useEntityAt(this.en, `isDeleted`)[0]()) } /* // TODO consider generic isDeleted setter set isDeleted(deleted:boolean) { useEntityAt(this.en, `isDeleted`)[1](true) } */ constructor( public en, public thread: Thread, deprecationWarning: 'imsure', ) { if (deprecationWarning !== 'imsure') { const existing = getMappedVM(en, thread) if (existing) { throw ERROR('deprecated: use getVM instead of constructor') } } 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 } const [getter, setter] = withDS( lastWriteWins(this.thread, { tolerateAlreadyFiltered: true }), // HACK: lastWriteWins () => useEntityAt(this.en, `${lowerCaseAttrNamespace}/${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 // type ReturnT = BaseVM & VMtypeT // // type ReturnT = ReturnType & VMTypeT // return this as ReturnT } 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 ObjectBuilder.create(init, en, VMname) } /** * @param init * @returns ObjectBuilder for the given type */ buildUpdate(init: Partial = {}) { return ObjectBuilder.create(init, this.en, VMname) } check = compiledChecker // TODO try it out get typeErrors() { return [...this.check.Errors(this)] } } // type ConstructorType = new(...args: any[]) => T type ReturnT = ConstructorType & ConstructorType return BaseVM as unknown as ReturnT } export function getUseFx< entityVM extends ReturnType>>, 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) 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() { }