pub const WIRE_JS: &str = "// The wire format: how a ZD value survives the trip to the store and back.\n//\n// # The bug this file exists to fix\n//\n// `JSON.stringify` cannot represent a JavaScript `Map`. It does not throw,\n// it does not warn, it returns `{}`:\n//\n// JSON.stringify(new Map([[\'ada\', 1]])) // \u{2192} \"{}\"\n//\n// A `Map` is what \u{a7}5.4\'s `Map of K to V` compiles to \u{2014} an object would\n// coerce every key to a string, which is exactly why it is a `Map` \u{2014} so\n// every `durable Map` wrote an empty object and read nothing back. It\n// failed silently, which for a persistence bug is the worst way to fail.\n//\n// # Why a marker and not an array\n//\n// `Map` and `record` cannot share an encoding: a record is a plain object\n// and a map has to be distinguishable from one, or `decode` cannot know\n// which to rebuild. So a map is tagged:\n//\n// new Map([[\'ada\', 1]]) \u{21c4} { \"$map\": [[\"ada\", 1]] }\n//\n// `$` cannot appear in a ZD identifier \u{2014} the lexer\'s rule is\n// `[\\p{XID_Start}_][\\p{XID_Continue}]*`, and `$` is in neither class \u{2014} so\n// no record field can ever be named `$map` and the marker is unambiguous\n// by construction rather than by convention.\n//\n// # What the four shapes look like on the wire\n//\n// Whole, Decimal number Text string\n// Truth boolean empty [] or {\"$map\":[]}\n// List of T array record object, fields by name\n// Map of K to V {\"$map\":[[k,v]]} choice {tag, fields}\n//\n// A choice is `{ tag, fields }` (see `variant` in `dom.js`) and a record is\n// a plain object, so both ride as ordinary JSON objects and recurse.\n//\n// # One definition, three users\n//\n// The browser encodes a request body, the adapter decodes it and encodes\n// what it stores, and the live-sync stream carries the encoded form\n// straight through. Three places, one file \u{2014} a second copy of these rules\n// anywhere is how the two halves come to disagree about what `{}` means.\n//\n// # Why `encode` consults `toJSON`, and why it is not a second marker\n//\n// `encode` runs *before* `JSON.stringify` and hands it a value that has\n// already been walked. That is what makes the `$map` marker possible, and\n// it also means `JSON.stringify` never sees the original object, so every\n// `toJSON` in the program was silently defeated, and any type that grew one\n// later would have been defeated the same way.\n//\n// It cost this once already. `append` compiles to a chain of links rather\n// than to an array, because appending has to be O(1) or a builder is\n// quadratic, and the class carries a `toJSON` that flattens the chain for\n// exactly this trip. `encode` walked past it: a link is not a `Map` and\n// `Array.isArray` is false for one, so it fell through to the record branch\n// and a durable `[1]` was stored as `{\"base\":[],\"item\":1,\"flat\":null}`\n// (#204).\n//\n// The narrow fix would have been a third branch that recognises the link\n// class. It was rejected: the mistake is not that this file does not know\n// about `append`, it is that walking structurally overrides what a value\n// says about its own JSON form, and a third branch leaves that true for the\n// fourth type. So `toJSON` is consulted generally and first, which is the\n// rule `JSON.stringify` itself follows, and a type that has an opinion\n// about its JSON form now gets it honoured at both layers instead of one.\n//\n// This does not weaken the `$map` marker\'s argument. A `Map` has no\n// `toJSON`, which is the whole reason this file exists, so nothing about\n// how a map rides has changed.\n\n/** A ZD value as JSON-representable data. */\nexport function encode(value) {\n if (value !== null && typeof value === \'object\' && typeof value.toJSON === \'function\') {\n const declared = value.toJSON();\n // A `toJSON` that hands back its own receiver has declared nothing, and\n // recursing on it would not terminate. Walking it structurally is what\n // this function did for every value before, so that is what it falls\n // back to rather than throwing on a value it can still encode.\n if (declared !== value) return encode(declared);\n }\n if (value instanceof Map) {\n const entries = [];\n // `forEach` rather than `for\u{2026}of` \u{2014} see the engine note in `signal.js`.\n value.forEach((item, key) => entries.push([encode(key), encode(item)]));\n return { $map: entries };\n }\n if (Array.isArray(value)) {\n return value.map(encode);\n }\n if (value !== null && typeof value === \'object\') {\n const out = {};\n for (const field of Object.keys(value)) {\n Object.defineProperty(out, field, {\n value: encode(value[field]),\n enumerable: true,\n configurable: true,\n writable: true,\n });\n }\n return out;\n }\n // `undefined` is not JSON. A durable key that holds nothing reads back as\n // absent, and the endpoint applies the declared `starting` value \u{2014} so\n // sending `null` here is what makes those two agree.\n return value === undefined ? null : value;\n}\n\n/** The inverse. */\nexport function decode(value) {\n if (Array.isArray(value)) {\n return value.map(decode);\n }\n if (value !== null && typeof value === \'object\') {\n if (Object.prototype.hasOwnProperty.call(value, \'$map\')) {\n // Strict, because `decode` does not only run on data this runtime\n // encoded. `rpc.js` decodes whatever an endpoint answers with and\n // `store.js` decodes whatever a live-sync frame carries, and neither\n // is under the program\'s control. The marker\'s unambiguity is an\n // argument about ZD *identifiers*, and it says nothing about a\n // payload that is merely shaped like one.\n //\n // Every one of these used to be a silent conversion: a non-array\n // `$map` became an empty map, sibling fields vanished, and a\n // malformed pair was skipped. Silent is the one thing a persistence\n // format must not be \u{2014} that is the whole reason this file exists.\n const keys = Object.keys(value);\n if (keys.length !== 1) {\n throw new Error(\n `A map on the wire carries only \"$map\"; this one also carried ${JSON.stringify(\n keys.filter((key) => key !== \'$map\')\n )}.`\n );\n }\n const entries = value.$map;\n if (!Array.isArray(entries)) {\n throw new Error(\'A map on the wire is an array of [key, value] pairs.\');\n }\n const rebuilt = new Map();\n for (const entry of entries) {\n if (!Array.isArray(entry) || entry.length !== 2) {\n throw new Error(\'A map entry on the wire is a [key, value] pair.\');\n }\n rebuilt.set(decode(entry[0]), decode(entry[1]));\n }\n return rebuilt;\n }\n const out = {};\n for (const field of Object.keys(value)) {\n // Assignment to `__proto__` invokes Object.prototype\'s legacy\n // setter instead of creating the record field. ZD identifiers may\n // legally spell `__proto__`, so define every field as an own data\n // property and keep the decoded record\'s prototype unchanged.\n Object.defineProperty(out, field, {\n value: decode(value[field]),\n enumerable: true,\n configurable: true,\n writable: true,\n });\n }\n return out;\n }\n return value;\n}\n\n// $dev\n/**\n * Assert `encode` left nothing `JSON.stringify` writes as `{}`.\n *\n * **This checks #204\'s family rather than its instance.** The bug at the\n * top of this file was a `Map` reaching `JSON.stringify`, which does not\n * throw and does not warn: it returns `{}`, so a `durable Map` wrote an\n * empty object and read nothing back. `encode` fixes that for `Map`, and\n * since the `toJSON` change for a type that declares its own JSON form.\n * What neither fixes is the *next* type with the same property, and no\n * static pass anywhere would see it \u{2014} the value is a JavaScript object\n * either way.\n *\n * So the invariant is checked instead of the case: after `encode`, every\n * object left is an array or a plain object and every leaf is a JSON\n * scalar. A `Map`, a `Set`, a `Date` or any class instance that does not\n * declare a `toJSON` fails here, naming the path to itself, instead of\n * silently becoming `{}` in somebody\'s store.\n *\n * Development only. A release build runs `JSON.stringify` against the same\n * encoded value with no check in front of it, which is what it has always\n * done.\n *\n * The walk is a worklist rather than recursion, and that is not a style\n * choice: `encode` is already recursive, and a second recursion over the\n * same value doubles the stack a nested value needs. `wire_fuzz.rs`\n * generates values deep enough that it does not fit, so an assertion\n * written the obvious way would fail on values the format carries.\n */\nexport function assertEncoded(root, path) {\n const pending = [[root, path === \'\' ? \'the value\' : path]];\n while (pending.length > 0) {\n const [value, at] = pending.pop();\n if (value === null) continue;\n const type = typeof value;\n if (type === \'boolean\' || type === \'string\' || type === \'number\') continue;\n // `NaN` and the infinities are deliberately *not* refused here. They\n // are numbers JSON writes as `null`, so they do not survive the trip \u{2014}\n // but that is the format\'s own recorded behaviour (#144), asserted by\n // `wire_fuzz.rs`, and an assertion that refused them would be this file\n // changing the format rather than checking it.\n if (type !== \'object\') {\n throw new Error(`${at} is a ${type}, which JSON cannot represent.`);\n }\n if (Array.isArray(value)) {\n for (let i = 0; i < value.length; i += 1) pending.push([value[i], `${at}[${i}]`]);\n continue;\n }\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== Object.prototype && prototype !== null) {\n const name = (value.constructor && value.constructor.name) || \'class instance\';\n throw new Error(\n `${at} is a ${name} after encoding, and JSON.stringify writes that ` +\n `as {} without saying so. Give it a toJSON, or encode it in encode().`\n );\n }\n for (const field of Object.keys(value)) pending.push([value[field], `${at}.${field}`]);\n }\n}\n// $end\n\n/** A ZD value as the JSON text that crosses the wire. */\nexport function stringify(value) {\n const encoded = encode(value);\n // $dev\n assertEncoded(encoded, \'\');\n // $end\n const text = JSON.stringify(encoded);\n return text === undefined ? \'null\' : text;\n}\n\n/** JSON text back into a ZD value. */\nexport function parse(text) {\n return decode(JSON.parse(text));\n}\n";Expand description
The wire format: how a ZD value survives JSON.
Its own module because three separate things encode and decode with it — the browser, the platform adapter, and the live-sync stream — and a second copy of the rules is how they come to disagree.