diff --git a/src/flatten.ts b/src/flatten.ts index ef6f82f..5112d12 100644 --- a/src/flatten.ts +++ b/src/flatten.ts @@ -42,132 +42,137 @@ function stringify(this: ThisEncode, input: unknown, index: number) { const { deferred, plugins } = this; const str = this.stringified; - const partsForObj = (obj: any) => - Object.keys(obj) - .map((k) => `"${flatten.call(this, k)}":${flatten.call(this, obj[k])}`) - .join(","); + const stack: [unknown, number][] = [[input, index]]; + while (stack.length > 0) { + const [input, index] = stack.pop()!; - switch (typeof input) { - case "boolean": - case "number": - case "string": - str[index] = JSON.stringify(input); - break; - case "bigint": - str[index] = `["${TYPE_BIGINT}","${input}"]`; - break; - case "symbol": { - const keyFor = Symbol.keyFor(input); - if (!keyFor) - throw new Error( - "Cannot encode symbol unless created with Symbol.for()" - ); - str[index] = `["${TYPE_SYMBOL}",${JSON.stringify(keyFor)}]`; - break; - } - case "object": { - if (!input) { - str[index] = `${NULL}`; + const partsForObj = (obj: any) => + Object.keys(obj) + .map((k) => `"${flatten.call(this, k)}":${flatten.call(this, obj[k])}`) + .join(","); + + switch (typeof input) { + case "boolean": + case "number": + case "string": + str[index] = JSON.stringify(input); + break; + case "bigint": + str[index] = `["${TYPE_BIGINT}","${input}"]`; + break; + case "symbol": { + const keyFor = Symbol.keyFor(input); + if (!keyFor) + throw new Error( + "Cannot encode symbol unless created with Symbol.for()" + ); + str[index] = `["${TYPE_SYMBOL}",${JSON.stringify(keyFor)}]`; break; } + case "object": { + if (!input) { + str[index] = `${NULL}`; + break; + } - const isArray = Array.isArray(input); - let pluginHandled = false; - if (!isArray && plugins) { - for (const plugin of plugins) { - const pluginResult = plugin(input); - if (Array.isArray(pluginResult)) { - pluginHandled = true; - const [pluginIdentifier, ...rest] = pluginResult; - str[index] = `[${JSON.stringify(pluginIdentifier)}`; - if (rest.length > 0) { - str[index] += `,${rest - .map((v) => flatten.call(this, v)) - .join(",")}`; + const isArray = Array.isArray(input); + let pluginHandled = false; + if (!isArray && plugins) { + for (const plugin of plugins) { + const pluginResult = plugin(input); + if (Array.isArray(pluginResult)) { + pluginHandled = true; + const [pluginIdentifier, ...rest] = pluginResult; + str[index] = `[${JSON.stringify(pluginIdentifier)}`; + if (rest.length > 0) { + str[index] += `,${rest + .map((v) => flatten.call(this, v)) + .join(",")}`; + } + str[index] += "]"; + break; } - str[index] += "]"; - break; } } - } - if (!pluginHandled) { - let result = isArray ? "[" : "{"; - if (isArray) { - for (let i = 0; i < input.length; i++) - result += - (i ? "," : "") + - (i in input ? flatten.call(this, input[i]) : HOLE); - str[index] = `${result}]`; - } else if (input instanceof Date) { - str[index] = `["${TYPE_DATE}",${input.getTime()}]`; - } else if (input instanceof URL) { - str[index] = `["${TYPE_URL}",${JSON.stringify(input.href)}]`; - } else if (input instanceof RegExp) { - str[index] = `["${TYPE_REGEXP}",${JSON.stringify( - input.source - )},${JSON.stringify(input.flags)}]`; - } else if (input instanceof Set) { - if (input.size > 0) { - str[index] = `["${TYPE_SET}",${[...input] - .map((val) => flatten.call(this, val)) - .join(",")}]`; - } else { - str[index] = `["${TYPE_SET}"]`; - } - } else if (input instanceof Map) { - if (input.size > 0) { - str[index] = `["${TYPE_MAP}",${[...input] - .flatMap(([k, v]) => [ - flatten.call(this, k), - flatten.call(this, v), - ]) - .join(",")}]`; + if (!pluginHandled) { + let result = isArray ? "[" : "{"; + if (isArray) { + for (let i = 0; i < input.length; i++) + result += + (i ? "," : "") + + (i in input ? flatten.call(this, input[i]) : HOLE); + str[index] = `${result}]`; + } else if (input instanceof Date) { + str[index] = `["${TYPE_DATE}",${input.getTime()}]`; + } else if (input instanceof URL) { + str[index] = `["${TYPE_URL}",${JSON.stringify(input.href)}]`; + } else if (input instanceof RegExp) { + str[index] = `["${TYPE_REGEXP}",${JSON.stringify( + input.source + )},${JSON.stringify(input.flags)}]`; + } else if (input instanceof Set) { + if (input.size > 0) { + str[index] = `["${TYPE_SET}",${[...input] + .map((val) => flatten.call(this, val)) + .join(",")}]`; + } else { + str[index] = `["${TYPE_SET}"]`; + } + } else if (input instanceof Map) { + if (input.size > 0) { + str[index] = `["${TYPE_MAP}",${[...input] + .flatMap(([k, v]) => [ + flatten.call(this, k), + flatten.call(this, v), + ]) + .join(",")}]`; + } else { + str[index] = `["${TYPE_MAP}"]`; + } + } else if (input instanceof Promise) { + str[index] = `["${TYPE_PROMISE}",${index}]`; + deferred[index] = input; + } else if (input instanceof Error) { + str[index] = `["${TYPE_ERROR}",${JSON.stringify(input.message)}`; + if (input.name !== "Error") { + str[index] += `,${JSON.stringify(input.name)}`; + } + str[index] += "]"; + } else if (Object.getPrototypeOf(input) === null) { + str[index] = `["${TYPE_NULL_OBJECT}",{${partsForObj(input)}}]`; + } else if (isPlainObject(input)) { + str[index] = `{${partsForObj(input)}}`; } else { - str[index] = `["${TYPE_MAP}"]`; + throw new Error("Cannot encode object with prototype"); } - } else if (input instanceof Promise) { - str[index] = `["${TYPE_PROMISE}",${index}]`; - deferred[index] = input; - } else if (input instanceof Error) { - str[index] = `["${TYPE_ERROR}",${JSON.stringify(input.message)}`; - if (input.name !== "Error") { - str[index] += `,${JSON.stringify(input.name)}`; - } - str[index] += "]"; - } else if (Object.getPrototypeOf(input) === null) { - str[index] = `["${TYPE_NULL_OBJECT}",{${partsForObj(input)}}]`; - } else if (isPlainObject(input)) { - str[index] = `{${partsForObj(input)}}`; - } else { - throw new Error("Cannot encode object with prototype"); } + break; } - break; - } - default: { - const isArray = Array.isArray(input); - let pluginHandled = false; - if (!isArray && plugins) { - for (const plugin of plugins) { - const pluginResult = plugin(input); - if (Array.isArray(pluginResult)) { - pluginHandled = true; - const [pluginIdentifier, ...rest] = pluginResult; - str[index] = `[${JSON.stringify(pluginIdentifier)}`; - if (rest.length > 0) { - str[index] += `,${rest - .map((v) => flatten.call(this, v)) - .join(",")}`; + default: { + const isArray = Array.isArray(input); + let pluginHandled = false; + if (!isArray && plugins) { + for (const plugin of plugins) { + const pluginResult = plugin(input); + if (Array.isArray(pluginResult)) { + pluginHandled = true; + const [pluginIdentifier, ...rest] = pluginResult; + str[index] = `[${JSON.stringify(pluginIdentifier)}`; + if (rest.length > 0) { + str[index] += `,${rest + .map((v) => flatten.call(this, v)) + .join(",")}`; + } + str[index] += "]"; + break; } - str[index] += "]"; - break; } } - } - if (!pluginHandled) { - throw new Error("Cannot encode function or unexpected type"); + if (!pluginHandled) { + throw new Error("Cannot encode function or unexpected type"); + } } } } diff --git a/src/unflatten.ts b/src/unflatten.ts index b7de0a0..b221e1a 100644 --- a/src/unflatten.ts +++ b/src/unflatten.ts @@ -45,112 +45,229 @@ export function unflatten(this: ThisDecode, parsed: unknown): unknown { function hydrate(this: ThisDecode, index: number): any { const { hydrated, values, deferred, plugins } = this; - switch (index) { - case UNDEFINED: - return; - case NULL: - return null; - case NAN: - return NaN; - case POSITIVE_INFINITY: - return Infinity; - case NEGATIVE_INFINITY: - return -Infinity; - case NEGATIVE_ZERO: - return -0; - } + let result: unknown; + const stack = [ + [ + index, + (v: unknown) => { + result = v; + }, + ] as const, + ]; - if (hydrated[index]) return hydrated[index]; - - const value = values[index]; - if (!value || typeof value !== "object") return (hydrated[index] = value); - - if (Array.isArray(value)) { - if (typeof value[0] === "string") { - const [type, b, c] = value; - switch (type) { - case TYPE_DATE: - return (hydrated[index] = new Date(b)); - case TYPE_URL: - return (hydrated[index] = new URL(b)); - case TYPE_BIGINT: - return (hydrated[index] = BigInt(b)); - case TYPE_REGEXP: - return (hydrated[index] = new RegExp(b, c)); - case TYPE_SYMBOL: - return (hydrated[index] = Symbol.for(b)); - case TYPE_SET: - const set = new Set(); - hydrated[index] = set; - for (let i = 1; i < value.length; i++) - set.add(hydrate.call(this, value[i])); - return set; - case TYPE_MAP: - const map = new Map(); - hydrated[index] = map; - for (let i = 1; i < value.length; i += 2) { - map.set( - hydrate.call(this, value[i]), - hydrate.call(this, value[i + 1]) - ); - } - return map; - case TYPE_NULL_OBJECT: - const obj = Object.create(null); - hydrated[index] = obj; - for (const key in b) - obj[hydrate.call(this, Number(key))] = hydrate.call(this, b[key]); - return obj; - case TYPE_PROMISE: - if (hydrated[b]) { - return (hydrated[index] = hydrated[b]); - } else { - const d = new Deferred(); - deferred[b] = d; - return (hydrated[index] = d.promise); - } - case TYPE_ERROR: - const [, message, errorType] = value; - let error = - errorType && globalObj && globalObj[errorType] - ? new globalObj[errorType](message) - : new Error(message); - hydrated[index] = error; - return error; - case TYPE_PREVIOUS_RESOLVED: - return hydrate.call(this, b); - default: - // Run plugins at the end so we have a chance to resolve primitives - // without running into a loop - if (Array.isArray(plugins)) { - const args = value.slice(1).map((i) => hydrate.call(this, i)); - for (const plugin of plugins) { - const result = plugin(value[0], ...args); - if (result) return (hydrated[index] = result.value); + let postRun: Array<() => void> = []; + + while (stack.length > 0) { + const [index, set] = stack.pop()!; + + switch (index) { + case UNDEFINED: + set(undefined); + continue; + case NULL: + set(null); + continue; + case NAN: + set(NaN); + continue; + case POSITIVE_INFINITY: + set(Infinity); + continue; + case NEGATIVE_INFINITY: + set(-Infinity); + continue; + case NEGATIVE_ZERO: + set(-0); + continue; + } + + if (hydrated[index]) { + set(hydrated[index]); + continue; + } + + const value = values[index]; + if (!value || typeof value !== "object") { + hydrated[index] = value; + set(value); + continue; + } + + if (Array.isArray(value)) { + if (typeof value[0] === "string") { + const [type, b, c] = value; + switch (type) { + case TYPE_DATE: + set((hydrated[index] = new Date(b))); + continue; + case TYPE_URL: + set((hydrated[index] = new URL(b))); + continue; + case TYPE_BIGINT: + set((hydrated[index] = BigInt(b))); + continue; + case TYPE_REGEXP: + set((hydrated[index] = new RegExp(b, c))); + continue; + case TYPE_SYMBOL: + set((hydrated[index] = Symbol.for(b))); + continue; + case TYPE_SET: + const newSet = new Set(); + hydrated[index] = newSet; + for (let i = 1; i < value.length; i++) + stack.push([ + value[i], + (v) => { + newSet.add(v); + }, + ]); + set(newSet); + continue; + case TYPE_MAP: + const map = new Map(); + hydrated[index] = map; + for (let i = 1; i < value.length; i += 2) { + const r: any[] = []; + stack.push([ + value[i + 1], + (v) => { + r[1] = v; + }, + ]); + stack.push([ + value[i], + (k) => { + r[0] = k; + }, + ]); + postRun.push(() => { + map.set(r[0], r[1]); + }); + } + set(map); + continue; + case TYPE_NULL_OBJECT: + const obj = Object.create(null); + hydrated[index] = obj; + for (const key in b) { + const r: any[] = []; + stack.push([ + b[key], + (v) => { + r[1] = v; + }, + ]); + stack.push([ + Number(key), + (k) => { + r[0] = k; + }, + ]); + postRun.push(() => { + obj[r[0]] = r[1]; + }); + } + set(obj); + continue; + case TYPE_PROMISE: + if (hydrated[b]) { + set((hydrated[index] = hydrated[b])); + } else { + const d = new Deferred(); + deferred[b] = d; + set((hydrated[index] = d.promise)); } + continue; + case TYPE_ERROR: + const [, message, errorType] = value; + let error = + errorType && globalObj && globalObj[errorType] + ? new globalObj[errorType](message) + : new Error(message); + hydrated[index] = error; + set(error); + continue; + case TYPE_PREVIOUS_RESOLVED: + set((hydrated[index] = hydrated[b])); + continue; + default: + // Run plugins at the end so we have a chance to resolve primitives + // without running into a loop + if (Array.isArray(plugins)) { + const r: unknown[] = []; + const vals = value.slice(1); + for (let i = 0; i < vals.length; i++) { + const v = vals[i]; + stack.push([ + v, + (v) => { + r[i] = v; + }, + ]); + } + postRun.push(() => { + for (const plugin of plugins) { + const result = plugin(value[0], ...r); + if (result) { + set((hydrated[index] = result.value)); + return; + } + } + throw new SyntaxError(); + }); + continue; + } + throw new SyntaxError(); + } + } else { + const array: unknown[] = []; + hydrated[index] = array; + + for (let i = 0; i < value.length; i++) { + const n = value[i]; + if (n !== HOLE) { + stack.push([ + n, + (v) => { + array[i] = v; + }, + ]); } - throw new SyntaxError(); + } + set(array); + continue; } } else { - const array: unknown[] = []; - hydrated[index] = array; + const object: Record = {}; + hydrated[index] = object; - for (let i = 0; i < value.length; i++) { - const n = value[i]; - if (n !== HOLE) array[i] = hydrate.call(this, n); + for (const key in value) { + const r: any[] = []; + stack.push([ + (value as Record)[key], + (v) => { + r[1] = v; + }, + ]); + stack.push([ + Number(key), + (k) => { + r[0] = k; + }, + ]); + postRun.push(() => { + object[r[0]] = r[1]; + }); } - return array; + set(object); + continue; } - } else { - const object: Record = {}; - hydrated[index] = object; - - for (const key in value) { - object[hydrate.call(this, Number(key))] = hydrate.call( - this, - (value as Record)[key] - ); - } - return object; } + + while (postRun.length > 0) { + postRun.pop()!(); + } + + return result; }