kitcn
Version:
kitcn - React Query integration and CLI tools for Convex
194 lines (193 loc) • 5.93 kB
JavaScript
//#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 };