import { entries, get, has, keys, remove, values } from "mobx" import { modelAction } from "../action/modelAction" import type { AnyModel } from "../model/BaseModel" import { Model } from "../model/Model" import { modelIdKey } from "../model/metadata" import { model } from "../modelShared/modelDecorator" import { idProp } from "../modelShared/prop" import { typesRecord } from "../types/objectBased/typesRecord" import { tProp } from "../types/tProp" import { typesUnchecked } from "../types/utility/typesUnchecked" import { namespace } from "../utils" import { setIfDifferent } from "../utils/setIfDifferent" const objectMapBase = Model({ [modelIdKey]: idProp, items: tProp(typesRecord(typesUnchecked()), () => ({})), // will be properly checked by types.objectMap(subType) }) as abstract new ( data: any ) => AnyModel & { readonly $: { items: Record } readonly items: Record } /** * A map that is backed by an object-like map. * Use `objectMap` to create it. */ @model(`${namespace}/ObjectMap`) // biome-ignore lint/suspicious/noUnsafeDeclarationMerging: model base defines these properties at runtime. export class ObjectMap extends objectMapBase implements Map { @modelAction clear(): void { const items = this.items const keys = Object.keys(items) const len = keys.length for (let i = 0; i < len; i++) { const k = keys[i] remove(items, k) } } @modelAction delete(key: string): boolean { const hasKey = this.has(key) if (hasKey) { remove(this.items, key) return true } else { return false } } forEach(callbackfn: (value: V, key: string, map: Map) => void, thisArg?: any): void { // we cannot use the map implementation since we need to pass this as map const items = this.items const keys = Object.keys(items) const len = keys.length for (let i = 0; i < len; i++) { const k = keys[i] callbackfn.call(thisArg, items[k], k, this) } } get(key: string): V | undefined { return get(this.items, key) } getOrInsert(key: string, defaultValue: V): V { if (this.has(key)) { return this.get(key) as V } this.set(key, defaultValue) return defaultValue } getOrInsertComputed(key: string, callback: (key: string) => V): V { if (this.has(key)) { return this.get(key) as V } const value = callback(key) this.set(key, value) return value } has(key: string): boolean { return has(this.items, key) } @modelAction set(key: string, value: V): this { setIfDifferent(this.items, key, value) return this } get size(): number { return keys(this.items).length } *keys(): ReturnType["keys"]> { for (const key of keys(this.items)) { yield key as string } } *values(): ReturnType["values"]> { for (const value of values(this.items)) { yield value } } *entries(): ReturnType["entries"]> { for (const entry of entries(this.items)) { yield entry } } [Symbol.iterator](): ReturnType[typeof Symbol.iterator]> { return this.entries() } readonly [Symbol.toStringTag] = "ObjectMap" } export interface ObjectMap { readonly $: { items: Record } readonly items: Record } /** * Creates a new ObjectMap model instance. * * @template V Value type. * @param [entries] Optional initial values. */ export function objectMap(entries?: ReadonlyArray | null): ObjectMap { const initialObj: Record = {} if (entries) { const len = entries.length for (let i = 0; i < len; i++) { const entry = entries[i] initialObj[entry[0]] = entry[1] } } return new ObjectMap({ items: initialObj }) }