Skip to main content

REQUEST_JS

Constant REQUEST_JS 

Source
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.