| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | |
| 8 | |
| 9 | |
| 10 |
|
| 11 |
|
| 12 | const TIME_LIMIT_MS = 1;
|
| 13 | const getNow = () => Date.now();
|
| 14 | const yieldToMain = () => new Promise((resolve) => setTimeout(resolve, 0));
|
| 15 | async function flatten(input) {
|
| 16 | const { indices } = this;
|
| 17 | const existing = indices.get(input);
|
| 18 | if (existing) return [existing];
|
| 19 | if (input === void 0) return -7;
|
| 20 | if (input === null) return -5;
|
| 21 | if (Number.isNaN(input)) return -2;
|
| 22 | if (input === Number.POSITIVE_INFINITY) return -6;
|
| 23 | if (input === Number.NEGATIVE_INFINITY) return -3;
|
| 24 | if (input === 0 && 1 / input < 0) return -4;
|
| 25 | const index = this.index++;
|
| 26 | indices.set(input, index);
|
| 27 | const stack = [[input, index]];
|
| 28 | await stringify.call(this, stack);
|
| 29 | return index;
|
| 30 | }
|
| 31 | async function stringify(stack) {
|
| 32 | const { deferred, indices, plugins, postPlugins } = this;
|
| 33 | const str = this.stringified;
|
| 34 | let lastYieldTime = getNow();
|
| 35 | const flattenValue = (value) => {
|
| 36 | const existing = indices.get(value);
|
| 37 | if (existing) return [existing];
|
| 38 | if (value === void 0) return -7;
|
| 39 | if (value === null) return -5;
|
| 40 | if (Number.isNaN(value)) return -2;
|
| 41 | if (value === Number.POSITIVE_INFINITY) return -6;
|
| 42 | if (value === Number.NEGATIVE_INFINITY) return -3;
|
| 43 | if (value === 0 && 1 / value < 0) return -4;
|
| 44 | const index = this.index++;
|
| 45 | indices.set(value, index);
|
| 46 | stack.push([value, index]);
|
| 47 | return index;
|
| 48 | };
|
| 49 | let i = 0;
|
| 50 | while (stack.length > 0) {
|
| 51 | const now = getNow();
|
| 52 | if (++i % 6e3 === 0 && now - lastYieldTime >= TIME_LIMIT_MS) {
|
| 53 | await yieldToMain();
|
| 54 | lastYieldTime = getNow();
|
| 55 | }
|
| 56 | const [input, index] = stack.pop();
|
| 57 | const partsForObj = (obj) => Object.keys(obj).map((k) => `"_${flattenValue(k)}":${flattenValue(obj[k])}`).join(",");
|
| 58 | let error = null;
|
| 59 | switch (typeof input) {
|
| 60 | case "boolean":
|
| 61 | case "number":
|
| 62 | case "string":
|
| 63 | str[index] = JSON.stringify(input);
|
| 64 | break;
|
| 65 | case "bigint":
|
| 66 | str[index] = `["B","${input}"]`;
|
| 67 | break;
|
| 68 | case "symbol": {
|
| 69 | const keyFor = Symbol.keyFor(input);
|
| 70 | if (!keyFor) error = new Error("Cannot encode symbol unless created with Symbol.for()");
|
| 71 | else str[index] = `["Y",${JSON.stringify(keyFor)}]`;
|
| 72 | break;
|
| 73 | }
|
| 74 | case "object": {
|
| 75 | if (!input) {
|
| 76 | str[index] = `-5`;
|
| 77 | break;
|
| 78 | }
|
| 79 | const isArray = Array.isArray(input);
|
| 80 | let pluginHandled = false;
|
| 81 | if (!isArray && plugins) for (const plugin of plugins) {
|
| 82 | const pluginResult = plugin(input);
|
| 83 | if (Array.isArray(pluginResult)) {
|
| 84 | pluginHandled = true;
|
| 85 | const [pluginIdentifier, ...rest] = pluginResult;
|
| 86 | str[index] = `[${JSON.stringify(pluginIdentifier)}`;
|
| 87 | if (rest.length > 0) str[index] += `,${rest.map((v) => flattenValue(v)).join(",")}`;
|
| 88 | str[index] += "]";
|
| 89 | break;
|
| 90 | }
|
| 91 | }
|
| 92 | if (!pluginHandled) {
|
| 93 | let result = isArray ? "[" : "{";
|
| 94 | if (isArray) {
|
| 95 | for (let i = 0; i < input.length; i++) result += (i ? "," : "") + (i in input ? flattenValue(input[i]) : -1);
|
| 96 | str[index] = `${result}]`;
|
| 97 | } else if (input instanceof Date) {
|
| 98 | const dateTime = input.getTime();
|
| 99 | str[index] = `["D",${Number.isNaN(dateTime) ? JSON.stringify("invalid") : dateTime}]`;
|
| 100 | } else if (input instanceof URL) str[index] = `["U",${JSON.stringify(input.href)}]`;
|
| 101 | else if (input instanceof RegExp) str[index] = `["R",${JSON.stringify(input.source)},${JSON.stringify(input.flags)}]`;
|
| 102 | else if (input instanceof Set) if (input.size > 0) str[index] = `["S",${[...input].map((val) => flattenValue(val)).join(",")}]`;
|
| 103 | else str[index] = `["S"]`;
|
| 104 | else if (input instanceof Map) if (input.size > 0) str[index] = `["M",${[...input].flatMap(([k, v]) => [flattenValue(k), flattenValue(v)]).join(",")}]`;
|
| 105 | else str[index] = `["M"]`;
|
| 106 | else if (input instanceof Promise) {
|
| 107 | str[index] = `["P",${index}]`;
|
| 108 | deferred[index] = input;
|
| 109 | } else if (input instanceof Error) {
|
| 110 | str[index] = `["E",${JSON.stringify(input.message)}`;
|
| 111 | if (input.name !== "Error") str[index] += `,${JSON.stringify(input.name)}`;
|
| 112 | str[index] += "]";
|
| 113 | } else if (Object.getPrototypeOf(input) === null) str[index] = `["N",{${partsForObj(input)}}]`;
|
| 114 | else if (isPlainObject(input)) str[index] = `{${partsForObj(input)}}`;
|
| 115 | else error = new Error("Cannot encode object with prototype");
|
| 116 | }
|
| 117 | break;
|
| 118 | }
|
| 119 | default: {
|
| 120 | const isArray = Array.isArray(input);
|
| 121 | let pluginHandled = false;
|
| 122 | if (!isArray && plugins) for (const plugin of plugins) {
|
| 123 | const pluginResult = plugin(input);
|
| 124 | if (Array.isArray(pluginResult)) {
|
| 125 | pluginHandled = true;
|
| 126 | const [pluginIdentifier, ...rest] = pluginResult;
|
| 127 | str[index] = `[${JSON.stringify(pluginIdentifier)}`;
|
| 128 | if (rest.length > 0) str[index] += `,${rest.map((v) => flattenValue(v)).join(",")}`;
|
| 129 | str[index] += "]";
|
| 130 | break;
|
| 131 | }
|
| 132 | }
|
| 133 | if (!pluginHandled) error = new Error("Cannot encode function or unexpected type");
|
| 134 | }
|
| 135 | }
|
| 136 | if (error) {
|
| 137 | let pluginHandled = false;
|
| 138 | if (postPlugins) for (const plugin of postPlugins) {
|
| 139 | const pluginResult = plugin(input);
|
| 140 | if (Array.isArray(pluginResult)) {
|
| 141 | pluginHandled = true;
|
| 142 | const [pluginIdentifier, ...rest] = pluginResult;
|
| 143 | str[index] = `[${JSON.stringify(pluginIdentifier)}`;
|
| 144 | if (rest.length > 0) str[index] += `,${rest.map((v) => flattenValue(v)).join(",")}`;
|
| 145 | str[index] += "]";
|
| 146 | break;
|
| 147 | }
|
| 148 | }
|
| 149 | if (!pluginHandled) throw error;
|
| 150 | }
|
| 151 | }
|
| 152 | }
|
| 153 | const objectProtoNames = Object.getOwnPropertyNames(Object.prototype).sort().join("\0");
|
| 154 | function isPlainObject(thing) {
|
| 155 | const proto = Object.getPrototypeOf(thing);
|
| 156 | return proto === Object.prototype || proto === null || Object.getOwnPropertyNames(proto).sort().join("\0") === objectProtoNames;
|
| 157 | }
|
| 158 |
|
| 159 | export { flatten };
|