import type { ModelPropTransform } from "../../modelShared/prop" import { isMap } from "../../utils" import { asMap } from "../../wrappers/asMap" import { typesArray } from "../arrayBased/typesArray" import { typesTuple } from "../arrayBased/typesTuple" import { typesRecord } from "../objectBased/typesRecord" import type { AnyType, ArrayType, RecordType, TypeToData } from "../schemas" import { TypeCheckerBaseType } from "../TypeChecker" import type { CodecFromEncoded, RuntimeAdapter } from "./typesCodecCore" import { createCodecType, identityRuntimeAdapter } from "./typesCodecCore" import { resolveCodecSupport } from "./typesCodecSupport" /** * Creates a Map proxy that adapts stored keys/values to runtime keys/values. * Shared by object-backed and array-backed map transforms. */ function makeMapProxy( storedMap: Map, keyAdapter: RuntimeAdapter, valueAdapter: RuntimeAdapter ): Map { return new Proxy(storedMap, { get(target, prop, receiver) { switch (prop) { case "get": return (key: TKeyRuntime) => { const storedKey = keyAdapter.toStored(key) if (!target.has(storedKey)) { return undefined } return valueAdapter.toRuntime( target.get(storedKey) as TValueStored, (newStoredValue) => { target.set(storedKey, newStoredValue) } ) } case "getOrInsert": return (key: TKeyRuntime, defaultValue: TValueRuntime) => { const storedKey = keyAdapter.toStored(key) if (target.has(storedKey)) { return valueAdapter.toRuntime( target.get(storedKey) as TValueStored, (newStoredValue) => { target.set(storedKey, newStoredValue) } ) } target.set(storedKey, valueAdapter.toStored(defaultValue)) return defaultValue } case "getOrInsertComputed": return (key: TKeyRuntime, callback: (key: TKeyRuntime) => TValueRuntime) => { const storedKey = keyAdapter.toStored(key) if (target.has(storedKey)) { return valueAdapter.toRuntime( target.get(storedKey) as TValueStored, (newStoredValue) => { target.set(storedKey, newStoredValue) } ) } const value = callback(key) target.set(storedKey, valueAdapter.toStored(value)) return value } case "set": return (key: TKeyRuntime, value: TValueRuntime) => { target.set(keyAdapter.toStored(key), valueAdapter.toStored(value)) return receiver } case "has": return (key: TKeyRuntime) => target.has(keyAdapter.toStored(key)) case "delete": return (key: TKeyRuntime) => target.delete(keyAdapter.toStored(key)) case "clear": return () => target.clear() case "size": return target.size case "forEach": return ( callback: ( value: TValueRuntime, key: TKeyRuntime, map: Map ) => void, thisArg?: unknown ) => { target.forEach((value, key) => { callback.call( thisArg, valueAdapter.toRuntime(value, (newStoredValue) => { target.set(key, newStoredValue) }), keyAdapter.toRuntime(key), receiver ) }) } case "entries": case Symbol.iterator: return function* () { for (const [key, value] of target.entries()) { yield [ keyAdapter.toRuntime(key), valueAdapter.toRuntime(value, (newStoredValue) => { target.set(key, newStoredValue) }), ] as const } } case "keys": return function* () { for (const key of target.keys()) { yield keyAdapter.toRuntime(key) } } case "values": return function* () { for (const [key, value] of target.entries()) { yield valueAdapter.toRuntime(value, (newStoredValue) => { target.set(key, newStoredValue) }) } } default: return Reflect.get(target, prop, receiver) } }, }) as unknown as Map } function makeObjectBackedMapTransform( valueAdapter: RuntimeAdapter ): ModelPropTransform, Map> { const storedByRuntime = new WeakMap, Record>() return { transform({ originalValue, cachedTransformedValue }) { if (cachedTransformedValue) { return cachedTransformedValue } const runtimeMap = makeMapProxy(asMap(originalValue), identityRuntimeAdapter, valueAdapter) storedByRuntime.set(runtimeMap, originalValue) return runtimeMap }, untransform({ transformedValue }) { const cachedStored = storedByRuntime.get(transformedValue) if (cachedStored) { return cachedStored } const result: Record = {} transformedValue.forEach((value, key) => { result[key] = valueAdapter.toStored(value) }) return result }, } } function makeArrayBackedMapTransform( keyAdapter: RuntimeAdapter, valueAdapter: RuntimeAdapter ): ModelPropTransform, Map> { const storedByRuntime = new WeakMap< Map, Array<[TKeyStored, TValueStored]> >() return { transform({ originalValue, cachedTransformedValue }) { if (cachedTransformedValue) { return cachedTransformedValue } const runtimeMap = makeMapProxy(asMap(originalValue), keyAdapter, valueAdapter) storedByRuntime.set(runtimeMap, originalValue) return runtimeMap }, untransform({ transformedValue }) { const cachedStored = storedByRuntime.get(transformedValue) if (cachedStored) { return cachedStored } const result: Array<[TKeyStored, TValueStored]> = [] transformedValue.forEach((value, key) => { result.push([keyAdapter.toStored(key), valueAdapter.toStored(value)]) }) return result }, } } export function typesMapFromObject( valueType: TValueType ): CodecFromEncoded, Map>> { const valueSupport = resolveCodecSupport(valueType) return createCodecType( { typeName: "mapFromObject", encodedType: typesRecord(valueSupport.storedType), is(value): value is Map> { if (!isMap(value)) { return false } for (const key of value.keys()) { if (typeof key !== "string") { return false } } return true }, ...makeObjectBackedMapTransform(valueSupport.adapter), }, TypeCheckerBaseType.Object ) } export function typesMapFromArray( keyType: TKeyType, valueType: TValueType ): CodecFromEncoded< ArrayType[]>, Map, TypeToData> > { const keySupport = resolveCodecSupport(keyType) const valueSupport = resolveCodecSupport(valueType) return createCodecType( { typeName: "mapFromArray", encodedType: typesArray(typesTuple(keySupport.storedType, valueSupport.storedType)), is(value): value is Map, TypeToData> { return isMap(value) }, ...makeArrayBackedMapTransform(keySupport.adapter, valueSupport.adapter), }, TypeCheckerBaseType.Object ) }