UNPKG

kitcn

Version:

kitcn - React Query integration and CLI tools for Convex

194 lines (193 loc) 5.93 kB
//#region src/crpc/transformer.ts const isPlainObject = (value) => { if (!value || typeof value !== "object" || Array.isArray(value)) return false; const prototype = Object.getPrototypeOf(value); return prototype === Object.prototype || prototype === null; }; const CODEC_MARKER_KEY = "__crpc"; const CODEC_MARKER_VALUE = 1; const CODEC_TAG_KEY = "t"; const CODEC_VALUE_KEY = "v"; const hasOnlyCodecPayloadKeys = (value) => { let keyCount = 0; for (const key in value) { if (!Object.hasOwn(value, key)) continue; keyCount += 1; if (key !== CODEC_MARKER_KEY && key !== CODEC_TAG_KEY && key !== CODEC_VALUE_KEY) return false; if (keyCount > 3) return false; } return keyCount === 3; }; /** * Date wire tag (Convex-style reserved key). */ const DATE_CODEC_TAG = "$date"; /** * Built-in Date codec. */ const dateWireCodec = { tag: DATE_CODEC_TAG, isType: (value) => value instanceof Date, encode: (value) => value.getTime(), decode: (value) => { if (typeof value !== "number") return value; return new Date(value); } }; /** * Build a recursive tagged transformer from codecs. */ const createTaggedTransformer = (codecs) => { const codecByTag = /* @__PURE__ */ new Map(); for (const codec of codecs) { if (!codec.tag.startsWith("$")) throw new Error(`Invalid wire codec tag '${codec.tag}'. Tags must start with '$'.`); if (codecByTag.has(codec.tag)) throw new Error(`Duplicate wire codec tag '${codec.tag}'.`); codecByTag.set(codec.tag, codec); } const serialize = (value) => { for (const codec of codecs) if (codec.isType(value)) return { [CODEC_MARKER_KEY]: CODEC_MARKER_VALUE, [CODEC_TAG_KEY]: codec.tag, [CODEC_VALUE_KEY]: serialize(codec.encode(value)) }; if (Array.isArray(value)) { let result; for (let index = 0; index < value.length; index += 1) { const item = value[index]; const serialized = serialize(item); if (serialized !== item) { if (!result) result = value.slice(); result[index] = serialized; } } return result ?? value; } if (isPlainObject(value)) { let result; for (const key in value) { if (!Object.hasOwn(value, key)) continue; const nested = value[key]; const serialized = serialize(nested); if (serialized !== nested) { if (!result) result = { ...value }; result[key] = serialized; } } return result ?? value; } return value; }; const deserialize = (value) => { if (Array.isArray(value)) { let result; for (let index = 0; index < value.length; index += 1) { const item = value[index]; const deserialized = deserialize(item); if (deserialized !== item) { if (!result) result = value.slice(); result[index] = deserialized; } } return result ?? value; } if (isPlainObject(value)) { const marker = value[CODEC_MARKER_KEY]; const tag = value[CODEC_TAG_KEY]; if (marker === CODEC_MARKER_VALUE && typeof tag === "string" && CODEC_VALUE_KEY in value && hasOnlyCodecPayloadKeys(value)) { const codec = codecByTag.get(tag); if (codec) return codec.decode(deserialize(value[CODEC_VALUE_KEY])); } let result; for (const key in value) { if (!Object.hasOwn(value, key)) continue; const nested = value[key]; const deserialized = deserialize(nested); if (deserialized !== nested) { if (!result) result = { ...value }; result[key] = deserialized; } } return result ?? value; } return value; }; return { serialize, deserialize }; }; /** * Default cRPC transformer (Date-enabled). */ const defaultCRPCTransformer = createTaggedTransformer([dateWireCodec]); const DEFAULT_COMBINED_TRANSFORMER = { input: defaultCRPCTransformer, output: defaultCRPCTransformer }; const IDENTITY_TRANSFORMER = { input: { serialize: (value) => value, deserialize: (value) => value }, output: { serialize: (value) => value, deserialize: (value) => value } }; /** * Normalize transformer config to split input/output shape. */ const normalizeCustomTransformer = (transformer) => { if (!transformer) return; if ("input" in transformer && "output" in transformer) return transformer; return { input: transformer, output: transformer }; }; /** * Compose user transformer with default Date transformer. * * Date transformer is always active: * - serialize: user -> default(Date) * - deserialize: default(Date) -> user */ const composeWithDefault = (transformer) => { if (!transformer) return defaultCRPCTransformer; return { serialize: (value) => defaultCRPCTransformer.serialize(transformer.serialize(value)), deserialize: (value) => transformer.deserialize(defaultCRPCTransformer.deserialize(value)) }; }; const transformerCache = /* @__PURE__ */ new WeakMap(); /** * Normalize transformer config to split input/output shape. * User transformers are additive and always composed with default Date handling. */ const getTransformer = (transformer) => { if (!transformer) return DEFAULT_COMBINED_TRANSFORMER; const cacheKey = transformer; const cached = transformerCache.get(cacheKey); if (cached) return cached; const custom = normalizeCustomTransformer(transformer); const resolved = { input: composeWithDefault(custom?.input), output: composeWithDefault(custom?.output) }; transformerCache.set(cacheKey, resolved); return resolved; }; /** * Encode request payloads (input direction). */ const encodeWire = (value, transformer) => getTransformer(transformer).input.serialize(value); /** * Decode response payloads (output direction). */ const decodeWire = (value, transformer) => getTransformer(transformer).output.deserialize(value); /** * Exposed identity transformer for advanced composition. */ const identityTransformer = IDENTITY_TRANSFORMER; //#endregion export { defaultCRPCTransformer as a, identityTransformer as c, decodeWire as i, createTaggedTransformer as n, encodeWire as o, dateWireCodec as r, getTransformer as s, DATE_CODEC_TAG as t };