pub const REQUEST_JS: &str = "// The outbound request a `request` declaration is \u{2014} issue #19.\n//\n// Its own module, and not part of `rpc.js`, because the two are different\n// promises. `rpc.js` talks to an endpoint this compiler emitted, on this\n// origin, whose body it wrote and can therefore read as its own; this file\n// talks to a host the *program* named, whose answer nobody vouches for. A\n// program that declares no `request` must not ship the one `fetch` in the\n// runtime that can name a host, and keeping them in separate files is what\n// makes that a fact about the bytes rather than about a code path (\u{a7}16.3.1).\n//\n// Four properties are this file\'s alone to keep, and each is the reason a\n// clause of the language\'s design was not needed:\n//\n// 1. **The method is `GET` and there is no body.** A `Remote of T` is a\n// read. A request that changed something on a third party would be a\n// command, and commands are handler statements with an outcome cell,\n// not signal initialisers.\n// 2. **The headers are `HEADERS` below and nothing else.** No program\n// value reaches one, which is why the language has no header clause\n// for a credential to be written into.\n// 3. **The query string is built here, with `encodeURIComponent`.** A\n// value cannot leave its own parameter, so it cannot add a parameter,\n// change the path, or reach the host.\n// 4. **A `Failed` message is composed from this file\'s own control flow.**\n// `rpc.js` reads `body.error` out of a failed response, which is right\n// for a body it wrote and wrong for one it did not: a third party\n// would otherwise choose text a program renders.\n\nimport { signal, effect } from \'./signal.js\';\n\nconst LOADING = { tag: \'Loading\', fields: [] };\n\nfunction ready(value) {\n return { tag: \'Ready\', fields: [value] };\n}\n\n/**\n * The three codes, the same closed set `rpc.js` uses and `zdc-types`\'s\n * `FailureCode` pins. Every one of them is decided by *this file\'s*\n * control flow, so none is a channel the answering host can write into:\n * `Unreachable` means no response object came back, `Timeout` means the\n * deadline below fired, and `Rejected` means a response came back and its\n * status line said no.\n */\nconst CODES = Object.freeze({\n UNREACHABLE: \'Unreachable\',\n TIMEOUT: \'Timeout\',\n REJECTED: \'Rejected\',\n});\n\n/**\n * Every header the request carries, frozen.\n *\n * `accept` and nothing else. There is deliberately no way to add one: an\n * `Authorization` header is the shortest path from a credential to a third\n * party, and the compiler\'s answer to that route is that the route does\n * not exist. A cross-origin request with a header outside CORS\'s\n * safelisted set also needs a preflight the other host has to answer, so a\n * header clause would mostly be a way to fail.\n */\nconst HEADERS = Object.freeze({ accept: \'text/plain, application/json\' });\n\n/** How long a request may take before the runtime stops waiting. */\nconst DEADLINE_MS = 30000;\n\n/**\n * The URL a request is sent to: the destination, then the parameters.\n *\n * `encodeURIComponent` on both halves of every pair, so a value is a\n * value. Without it `with q is \"a&admin=1\"` would be two parameters, and\n * a value holding `#` would truncate the query \u{2014} neither is a leak on its\n * own, and both are the shape of one.\n *\n * The destination is **not** encoded and must not be: it arrived as a\n * literal that `zdc_hir::destination` already parsed into a scheme, a host\n * and a path, and encoding it would turn its slashes into `%2F`.\n */\nexport function requestUrl(destination, pairs) {\n if (pairs.length === 0) return destination;\n const query = pairs\n .map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`)\n .join(\'&\');\n return `${destination}?${query}`;\n}\n\n/**\n * A failure this file classified. Nothing else constructs one, and the\n * code travels on the object rather than in the message, so nothing\n * downstream recovers it by parsing text.\n */\nclass RequestFailure extends Error {\n constructor(code, message) {\n super(message);\n this.name = \'RequestFailure\';\n this.zdCode = code;\n }\n}\n\nfunction codeOf(error) {\n if (error instanceof RequestFailure) return error.zdCode;\n const name = error && error.name;\n if (name === \'AbortError\' || name === \'TimeoutError\') return CODES.TIMEOUT;\n return CODES.UNREACHABLE;\n}\n\n/**\n * The `Failed` payload.\n *\n * **The message is this file\'s sentence, not the host\'s.** A\n * `RequestFailure` carries text composed below from the destination and\n * the status line; anything else \u{2014} a transport a host page replaced, a\n * `TypeError` from the platform \u{2014} is reported as its `name` and no more.\n * `String(error.message)` is what `rpc.js` writes, and it is what would\n * let an answering host put its own prose on the page.\n */\nfunction failed(destination, error) {\n const message =\n error instanceof RequestFailure\n ? error.message\n : `${destination} could not be reached (${(error && error.name) || \'error\'})`;\n return {\n tag: \'Failed\',\n fields: [{ message, code: { tag: codeOf(error), fields: [] } }],\n };\n}\n\n/** A cancellable deadline, or a no-op where the platform has no timers. */\nfunction startDeadline() {\n const has = typeof AbortController === \'function\' && typeof setTimeout === \'function\';\n if (!has) return { signal: undefined, cancel: () => {}, expired: () => false };\n const controller = new AbortController();\n let fired = false;\n const timer = setTimeout(() => {\n fired = true;\n controller.abort();\n }, DEADLINE_MS);\n return {\n signal: controller.signal,\n cancel: () => clearTimeout(timer),\n // This file\'s own boolean, so nothing on the wire chooses the code.\n expired: () => fired,\n };\n}\n\nlet transport = defaultTransport;\n\n/** Replace the transport. Used by tests, which answer without a network. */\nexport function setRequestTransport(next) {\n transport = next || defaultTransport;\n}\n\nasync function defaultTransport(url) {\n const deadline = startDeadline();\n let response;\n try {\n response = await fetch(url, {\n // Written out rather than defaulted, so a reader of this file can\n // see there is no method and no body to be changed.\n method: \'GET\',\n headers: HEADERS,\n body: undefined,\n // `omit`, so a browser sends neither cookies nor HTTP credentials to\n // the host the program named. A same-origin destination gets the\n // same treatment: an endpoint is `rpc.js`\'s business, and this file\n // has no reason to carry anybody\'s session.\n credentials: \'omit\',\n signal: deadline.signal,\n });\n } catch (error) {\n throw deadline.expired()\n ? new RequestFailure(CODES.TIMEOUT, `${url} did not answer within ${DEADLINE_MS}ms`)\n : new RequestFailure(CODES.UNREACHABLE, `${url} could not be reached`);\n } finally {\n deadline.cancel();\n }\n if (!response.ok) {\n // The status line, and not the body. A number the host chose out of a\n // set the protocol fixed is a far smaller channel than prose it wrote.\n throw new RequestFailure(CODES.REJECTED, `${url} answered with ${response.status}`);\n }\n // `text()`, never `json()`: the language says a request gives `Text`, so\n // there is nothing to decode and no decoder to disagree with a host.\n return response.text();\n}\n\n/**\n * A `request` declaration, as the getter of `Remote of Text` it is.\n *\n * `pairs` is `[[name, getter], \u{2026}]` in source order. The getters are read\n * **inside** the effect, which is what makes the request re-run when \u{2014} and\n * only when \u{2014} one of its arguments changes.\n *\n * Generation-guarded for the reason `remoteCell` is: typing into a bound\n * signal starts a request per keystroke, and the first answer must not\n * overwrite the last.\n */\nexport function request(destination, pairs) {\n const [read, write] = signal(LOADING);\n let generation = 0;\n\n effect(() => {\n const resolved = pairs.map(([name, get]) => [name, String(get())]);\n const url = requestUrl(destination, resolved);\n const mine = ++generation;\n write(LOADING);\n Promise.resolve()\n .then(() => transport(url))\n .then(\n (text) => {\n if (mine === generation) write(ready(text));\n },\n (error) => {\n if (mine === generation) write(failed(destination, error));\n },\n );\n });\n\n return read;\n}\n";Expand description
The outbound request a request declaration is (#19).
Its own module for the reason the modules above are theirs, and with
more riding on it: a program that declares no
request must not ship the one fetch in the runtime that can name a
host it was not given. It imports signal.js and nothing else.