const enum Types { NULL = 'null', UNDEFINED = 'undefined', STRING = 'string', INTEGER = 'integer', FLOAT = 'float', BOOLEAN = 'boolean', EMPTY = 'empty', } const enum Tokens { 'TRUE' = -1, 'FALSE' = -2, 'NULL' = -3, 'EMPTY' = -4, 'UNDEFINED' = -5, } type Token = { type: Types; index: number; }; type NestedToken = ['@' | '$', ...(Token | NestedToken)[]]; /** * Compress any input object or array down to a minimal size reproduction in a string * Inspired by `jsonpack` */ export function compress(obj: Record | Record[]) { const strings = new Map(); const integers = new Map(); const floats = new Map(); const getAst = (part: unknown | unknown[]): Token | NestedToken => { if (part === null) { return { type: Types.NULL, index: Tokens.NULL, }; } if (part === undefined) { return { type: Types.UNDEFINED, index: Tokens.UNDEFINED, }; } if (Array.isArray(part)) { return ['@', ...part.map((subPart) => getAst(subPart))]; } if (part instanceof Date) { const value = encode(part.toJSON()); if (strings.has(value)) { return { type: Types.STRING, index: strings.get(value)!, }; } const index = strings.size; strings.set(value, index); return { type: Types.STRING, index, }; } if (typeof part === 'object') { return [ '$', ...Object.entries(part) .map(([key, value]) => [getAst(key), getAst(value)]) .flat(), ]; } if (part === '') { return { type: Types.EMPTY, index: Tokens.EMPTY, }; } if (typeof part === 'string') { const value = encode(part); if (strings.has(value)) { return { type: Types.STRING, index: strings.get(value)!, }; } const index = strings.size; strings.set(value, index); return { type: Types.STRING, index, }; } if (typeof part === 'number' && Number.isInteger(part)) { const value = to36(part); if (integers.has(value)) { return { type: Types.INTEGER, index: integers.get(value)!, }; } const index = integers.size; integers.set(value, index); return { type: Types.INTEGER, index, }; } if (typeof part === 'number') { if (floats.has(part)) { return { type: Types.FLOAT, index: floats.get(part)!, }; } const index = floats.size; floats.set(part, index); return { type: Types.FLOAT, index, }; } if (typeof part === 'boolean') { return { type: Types.BOOLEAN, index: part ? Tokens.TRUE : Tokens.FALSE, }; } throw new Error(`Unexpected argument of type ${typeof part}`); }; const ast = getAst(obj); const getCompressed = (part: Token | NestedToken) => { if (Array.isArray(part)) { let compressed: string = part.shift() as '@' | '$'; part.forEach((subPart) => (compressed += getCompressed(subPart as Token) + '|')); if (compressed.endsWith('|')) compressed = compressed.slice(0, -1); return compressed + ']'; } const { type, index } = part; switch (type) { case Types.STRING: return to36(index); case Types.INTEGER: return to36(strings.size + index); case Types.FLOAT: return to36(strings.size + integers.size + index); default: return index; } }; let compressed = mapToSortedArray(strings).join('|'); compressed += '^' + mapToSortedArray(integers).join('|'); compressed += '^' + mapToSortedArray(floats).join('|'); compressed += '^' + getCompressed(ast); return compressed; } export function decompress(input: string): unknown { const parts = input.split('^'); if (parts.length !== 4) throw new Error(`Invalid input string given`); const values: (string | number)[] = []; if (parts[0]) { values.push(...parts[0]!.split('|').map((part) => decode(part))); } if (parts[1]) { values.push(...parts[1]!.split('|').map((part) => to10(part))); } if (parts[2]) { values.push(...parts[2]!.split('|').map((part) => parseFloat(part))); } let num36Buffer = ''; const tokens: (string | number)[] = []; parts[3]!.split('').forEach((symbol) => { if (['|', '$', '@', ']'].includes(symbol)) { if (num36Buffer) { tokens.push(to10(num36Buffer)); num36Buffer = ''; } if (symbol !== '|') tokens.push(symbol); } else { num36Buffer += symbol; } }); let tokenIndex = 0; const getDecompressed = (): Record | Record[] => { const type = tokens[tokenIndex++]; if (type === '$') { const node: Record = {}; for (; tokenIndex < tokens.length; tokenIndex++) { const rawKey = tokens[tokenIndex]!; if (rawKey === ']') return node; const rawValue = tokens[++tokenIndex]!; const key = values[rawKey as number] as string; if (rawValue === '$' || rawValue === '@') { node[key] = getDecompressed(); } else { const value = values[rawValue as number] ?? getValueForToken(rawValue as Tokens); node[key] = value; } } } if (type === '@') { const node: any[] = []; for (; tokenIndex < tokens.length; tokenIndex++) { const rawValue = tokens[tokenIndex]!; if (rawValue === ']') return node; if (rawValue === '$' || rawValue === '@') { node.push(getDecompressed()); } else { const value = values[tokens[tokenIndex]! as number] ?? getValueForToken(tokens[tokenIndex] as Tokens); node.push(value); } } } throw new Error('Bad token: ' + type); }; return getDecompressed(); } export function mapToSortedArray(map: Map): (string | number)[] { const output: (string | number)[] = []; map.forEach((index, value) => { output[index] = value; }); return output; } export function encode(str: string) { return str.replace(/[+ |^%]/g, (a) => { switch (a) { case ' ': return '+'; case '+': return '%2B'; case '|': return '%7C'; case '^': return '%5E'; case '%': default: // The regex matches explicit, so this default shouldn't be hit return '%25'; } }); } export function decode(str: string) { return str.replace(/\+|%2B|%7C|%5E|%25/g, (a) => { switch (a) { case '%25': return '%'; case '%2B': return '+'; case '%7C': return '|'; case '%5E': return '^'; case '+': default: // The regex matches explicit, so this default shouldn't be hit return ' '; } }); } export function to36(num: number): string { return num.toString(36).toUpperCase(); } export function to10(str: string): number { return parseInt(str, 36); } export function getValueForToken(token: Tokens) { switch (token) { case Tokens.TRUE: return true; case Tokens.FALSE: return false; case Tokens.NULL: return null; case Tokens.EMPTY: return ''; case Tokens.UNDEFINED: return undefined; } }