import type { Static } from '@sinclair/typebox' import type { ApplogForInsert, ApplogValue, EntityID, } from '@wovin/core/applog' import type { Thread } from '@wovin/core/thread' import type { TypeMapKeys } from './utils-typemap' import { dateNowIso, ensureTsPvAndFinalizeApplog, EntityID_LENGTH, getHashID, } from '@wovin/core/applog' import { action } from '@wovin/core/mobx' import { Logger } from 'besonders-logger' import { useCurrentThread } from '../../ui/reactive' import { addAgentToApplog, insertApplogs } from '../ApplogDB' import { Entity } from './TypeMap' const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line unused-imports/no-unused-vars type Expand = { [K in keyof T]: T[K] } interface ValueBuilder, K extends string> { value: (value: V) => ObjectBuilder<{ [Key in keyof (T & { [k in K]: V })]: (T & { [k in K]: V })[Key] }> } /* A note about TypeMap and the relation to our CUB builder pattern This Class is generic, but for our auto mapped VMs we only pass VMstatic to ObjectBuilder as the Generic type This means that the built objects can be checked against the property maps within TypeMap // TODO check during update and build and warn/error appropriately */ /* inspired by: https://medium.hexlabs.io/the-builder-pattern-with-typescript-using-advanced-types-e05a03ffc36e */ export class ObjectBuilder< TARGET extends {} = {}, CURRENT extends Partial = {}, > { constructor( private jsonObject: CURRENT, public en: EntityID = null, private typeTB: TypeMapKeys = null, private atPrefix = typeTB?.toLowerCase(), ) {} static create( init: Partial> = {}, en?: EntityID, typeTB?: TypeMapKeys, ): ObjectBuilder { // en = en ?? getHashID({ ...init, ts: dateNowIso() }, L) // - we should do that as late as possible (when building) return new ObjectBuilder(init, en, typeTB) } update(updateObj: Partial>) { // TODO: typebox check partial? Object.assign(this.jsonObject, updateObj) return this as ObjectBuilder // return new ObjectBuilder({ ...this.jsonObject, ...updateObj }, this.en, this.typeTB) } build(thread?: Thread): ApplogForInsert[] { // ds = ds ?? useCurrentDS() ?? getApplogDB() // ds = ds.filters.includes('lastWriteWins') ? ds : lastWriteWins(ds) this.ensureEn() const en = this.en // TODO: typebox check const applogs = [] const atPrefix = this.atPrefix for (const [atName, vl] of Object.entries(this.jsonObject as Record)) { const at = atPrefix && !Object.hasOwn(Entity.properties, atName) // HACK: proper attr mapping via VM ? `${atPrefix}/${atName}` : atName let appLogToAdd = addAgentToApplog({ en, at, vl }) // ? pv if (thread) appLogToAdd = ensureTsPvAndFinalizeApplog(appLogToAdd, thread) // if (!ds.hasApplogWithDiffTs(appLogToAdd)) { applogs.push(appLogToAdd) // ? i think this should only be added if the most recent applog lastWriteWins is not identical // - I'd like to avoid the builder needing to know about data // } } return applogs } commit = action(function commitBuilder(this: ObjectBuilder, thread = useCurrentThread()) { // ? or 'save' const applogs = this.build(thread) DEBUG('commiting', this, { applogs }) return { en: this.en, logs: insertApplogs(thread, applogs), } }) ensureEn() { if (!this.en) this.en = getHashID({ ...this.jsonObject, ts: dateNowIso() }, EntityID_LENGTH) // ? id generation //TODO add agent (=> lamport clock)? return this // for chaining } /* actually prefer to exclude these, as update can cover it just fine // add vs set - add feels more buildy set more familiar set(key: K, value: TARGET[K]) ///* : ObjectBuilder<{ [Key in keyof (CURRENT & { [k in K]: TARGET[K] })]: (CURRENT & { [k in K]: TARGET[K] })[Key] }> { const nextPart = { [key]: value } as { [k in K]: TARGET[K] }; return new ObjectBuilder({ ...this.jsonObject, ...nextPart }, this.en); } key(key: K) //* : ValueBuilder { return { value: (value: TARGET[K]) => this.set(key, value) }; } */ }