Skip to main content

STORE_JS

Constant STORE_JS 

Source
pub const STORE_JS: &str = "// Live sync: the browser half of `durable` placement.\n//\n// # The transport is a seam, and here is why\n//\n// Spec \u{a7}8.1 says holding a stream open is solved on every platform. It is\n// not. Checked against vendor documentation:\n//\n//   Cloudflare Workers   no documented duration limit; billed on CPU, so an\n//                        idle stream is nearly free. The best case.\n//   Lambda, streaming    900 s hard ceiling, and the full duration is billed\n//                        even after the client disconnects.\n//   Lambda, buffered     no streaming at all \u{2014} the response is delivered\n//                        only once complete.\n//   Lambda behind an ALB no streaming at all \u{2014} the ALB takes a JSON body,\n//                        rejects upgrades, and drops `Transfer-Encoding`.\n//   Vercel               300 s Hobby, 800 s Pro.\n//   Azure Functions      contested: 230 s total, or a 4-minute *idle*\n//                        timeout a heartbeat defeats. Unverified either way.\n//   Deno Deploy          no documented timeout, but an isolate can be\n//                        evicted mid-stream at any moment.\n//\n// Two of those shapes cannot hold a stream at all, so a runtime that hard-\n// coded `EventSource` would simply not work on them. `subscribe` therefore\n// takes a transport, and two are provided: `streamTransport` over\n// `EventSource`, and `pollTransport` over `fetch`. They speak the *same*\n// protocol \u{2014} a cursor goes up, ordered events come down \u{2014} so the difference\n// between Cloudflare (never disconnects) and Lambda behind an ALB (cannot\n// connect) is a transport choice, not a second code path.\n//\n// # Resume is load-bearing, not polish\n//\n// Because the ceiling is 900 s on Lambda, the stream *will* be cut, on a\n// timer, in normal operation. Every event carries a sequence number and\n// every reconnection sends the last one seen \u{2014} `Last-Event-ID` for the\n// stream transport, `?since=` for the poll transport. That is what makes a\n// bounded stream behave like an unbounded one.\n//\n// When the server cannot prove it has the whole tail a client missed, it\n// says `resync` instead of guessing. Continuing silently is the dropped\n// update \u{a7}8.1 forbids, and it is invisible in testing because it only\n// happens after a real disconnection.\n\nimport { remoteCell } from \'./rpc.js\';\nimport { decode as decodeValue } from \'./wire.js\';\n\n/** Where the live-sync endpoints live. One place, so the shape is one decision. */\nexport function liveUrl(keys, cursor) {\n  const query = new URLSearchParams();\n  query.set(\'keys\', keys.join(\',\'));\n  if (cursor !== null && cursor !== undefined) query.set(\'since\', String(cursor));\n  return `/_zd/live?${query.toString()}`;\n}\n\nexport function pollUrl(keys, cursor) {\n  const query = new URLSearchParams();\n  query.set(\'keys\', keys.join(\',\'));\n  if (cursor !== null && cursor !== undefined) query.set(\'since\', String(cursor));\n  return `/_zd/poll?${query.toString()}`;\n}\n\n// --- the cells ------------------------------------------------------------\n\n/// Every durable key this page reads, and how to update it.\nconst cells = new Map();\n\n/**\n * A `durable` signal read from the browser.\n *\n * The same `Remote of T` a `server` signal gives, plus a registration: when\n * a write to `key` is announced, the announcement carries the value, so\n * this cell updates without a round trip. That is \u{a7}17.2.5 fatal 4\'s\n * `LiveValue` edge, and it is the whole reason two windows move together\n * rather than one of them moving a second later.\n */\nexport function durable(name, key, inputs) {\n  const [read, apply, refetch] = remoteCell(name, inputs);\n  let existing = cells.get(key);\n  if (!existing) {\n    existing = [];\n    cells.set(key, existing);\n  }\n  existing.push({ apply, refetch });\n  return read;\n}\n\n/** Which keys have a cell. This is what a subscription asks for \u{2014} never a\n * prefix, because the stores this has to run on do not have prefix watch. */\nexport function watchedKeys() {\n  // `forEach` rather than `Array.from` \u{2014} see the engine note in `signal.js`.\n  const keys = [];\n  cells.forEach((_bound, key) => keys.push(key));\n  return keys.sort();\n}\n\n/** Apply one announced write. Exported so a transport test can drive it. */\nexport function applyUpdate(key, value) {\n  const bound = cells.get(key);\n  if (!bound) return false;\n  for (const cell of bound) cell.apply(value);\n  return true;\n}\n\n/**\n * Re-read every cell.\n *\n * The answer to `resync`: the server could not prove it had the whole tail,\n * so nothing about the current values can be trusted and the honest move is\n * to ask again.\n */\nexport function resyncAll() {\n  // `forEach` rather than `for\u{2026}of` \u{2014} see the engine note in `signal.js`.\n  cells.forEach((bound) => {\n    for (const cell of bound) cell.refetch();\n  });\n}\n\n// --- the protocol ---------------------------------------------------------\n\n/**\n * Route one decoded event.\n *\n * Returns the cursor to resume from. Pure apart from the cell writes, so\n * both transports share it and cannot drift into two dialects.\n */\nexport function receive(event, cursor) {\n  const seq = typeof event.seq === \'number\' ? event.seq : undefined;\n  // A sequence number that does not advance is an event this client has\n  // already seen. Resume is not exact: `Last-Event-ID` and `?since=` both\n  // ask for \"everything after N\", and a server that cannot seek precisely\n  // answers from a little earlier \u{2014} which is allowed, and is why the\n  // protocol carries the number at all. Applying such an event replays a\n  // value that has since been overwritten, so the page shows the older one\n  // until something writes again. It is invisible in testing because it can\n  // only happen after a real reconnection.\n  const seen = seq !== undefined && typeof cursor === \'number\' && seq <= cursor;\n\n  if (event.event === \'resync\') {\n    // Never skipped. `resync` is the server saying it cannot prove it has\n    // the tail this client missed, and re-reading is the answer whether or\n    // not the number moved.\n    resyncAll();\n    return seen ? cursor : (seq ?? cursor);\n  }\n  if (event.event === \'update\') {\n    if (seen) return cursor;\n    applyUpdate(event.key, event.value);\n    return seq ?? cursor;\n  }\n  // `ready` and anything a newer server invents: advance the cursor if it\n  // carried one, and change nothing. An unknown event must not be an error\n  // \u{2014} a browser holding a stale page open across a deploy would then break\n  // instead of simply learning nothing new.\n  return seen ? cursor : (seq ?? cursor);\n}\n\n// --- the two transports ---------------------------------------------------\n\n/**\n * Hold a stream open.\n *\n * `EventSource` reconnects on its own and replays `Last-Event-ID`, which is\n * exactly the resume protocol \u{2014} so on a platform that can hold a stream,\n * the 900-second cut is handled by the browser and costs one round trip.\n */\nexport function streamTransport(keys, cursor, onEvent) {\n  const source = new EventSource(liveUrl(keys, cursor));\n  const handle = (name) => (message) => {\n    onEvent(decodeFrame(name, message.data, message.lastEventId));\n  };\n  for (const name of [\'update\', \'resync\', \'ready\']) {\n    source.addEventListener(name, handle(name));\n  }\n  return () => source.close();\n}\n\n/**\n * Ask, repeatedly.\n *\n * The fallback for the two shapes that cannot stream at all \u{2014} Lambda in\n * buffered mode and Lambda behind an ALB \u{2014} and the cheaper choice anywhere\n * the full stream duration is billed whether or not anyone is listening.\n *\n * It is the same protocol with a zero-length stream: the cursor goes up in\n * the query string instead of a header, and the events come down in an\n * array instead of one at a time.\n */\nexport function pollTransport(keys, cursor, onEvent, options) {\n  const wait = (options && options.interval) || 1000;\n  // Resolved with `typeof` rather than named directly: a bare `fetch` in a\n  // runtime without one is a `ReferenceError` at subscription time, which\n  // would take down module evaluation \u{2014} and therefore the whole page \u{2014}\n  // over a feature the page can simply do without.\n  const fetchImpl =\n    (options && options.fetch) || (typeof fetch === \'function\' ? fetch : null);\n  if (!fetchImpl) {\n    // Neither a stream nor a request. The page still works; it just will\n    // not learn about another window\'s writes until something re-reads.\n    return () => {};\n  }\n  const sleep = (options && options.sleep) || ((ms) => new Promise((r) => setTimeout(r, ms)));\n  let live = true;\n  let at = cursor;\n\n  (async () => {\n    while (live) {\n      try {\n        const response = await fetchImpl(pollUrl(keys, at));\n        const events = await response.json();\n        for (const event of events) {\n          if (!live) return;\n          at = onEvent(event);\n        }\n      } catch (error) {\n        // A failed poll is not fatal: the next one carries the same cursor,\n        // so nothing is lost by one round trip going missing. Throwing here\n        // would end live sync for the life of the page over one dropped\n        // packet.\n      }\n      if (!live) return;\n      await sleep(wait);\n    }\n  })();\n\n  return () => {\n    live = false;\n  };\n}\n\n/** Whether this runtime can hold a stream. */\nexport function canStream() {\n  return typeof EventSource === \'function\';\n}\n\n/**\n * Start live sync.\n *\n * `transport` defaults to a stream where one is available and a poll where\n * it is not, which is the honest default: the capability differs by\n * platform and by deployment shape, and a page cannot know which it is\n * behind.\n */\nexport function subscribe(options) {\n  const settings = options || {};\n  const keys = settings.keys || watchedKeys();\n  if (keys.length === 0) return () => {};\n\n  let cursor = settings.since === undefined ? null : settings.since;\n  const transport = settings.transport || (canStream() ? streamTransport : pollTransport);\n  const onEvent = (event) => {\n    cursor = receive(event, cursor);\n    return cursor;\n  };\n  return transport(keys, cursor, onEvent, settings);\n}\n\n/**\n * One `event:`/`data:` pair, as an object both transports produce.\n *\n * Named `decodeFrame` rather than `decode` because `wire.js` exports a\n * `decode` too, and the two do different jobs at different layers: this\n * one turns an SSE frame into an event, that one turns JSON into a ZD\n * value. Two `decode`s one import apart is a name collision waiting for\n * whichever file gets flattened into the other\'s scope.\n */\nexport function decodeFrame(name, data, lastEventId) {\n  let payload = {};\n  try {\n    payload = JSON.parse(data);\n  } catch (error) {\n    payload = {};\n  }\n  const seq =\n    typeof payload.seq === \'number\'\n      ? payload.seq\n      : lastEventId === undefined || lastEventId === null || lastEventId === \'\'\n        ? undefined\n        : Number(lastEventId);\n  // Decoded here rather than at the cell, because this is the one place\n  // bytes become values \u{2014} and a `Map` pushed to a second window has to\n  // arrive as a `Map`, not as the `{\"$map\":[...]}` it travelled as.\n  //\n  // A frame this runtime cannot decode becomes a `resync` rather than an\n  // exception. The alternative is a throw out of an `EventSource` listener,\n  // which nothing catches; and the alternative to *that* is applying a\n  // value we could not read, which is the dropped update \u{a7}8.1 forbids.\n  // Asking again is the only answer that is neither.\n  let value = null;\n  try {\n    if (payload.value !== undefined) value = decodeValue(payload.value);\n  } catch (error) {\n    return { event: \'resync\', seq: Number.isFinite(seq) ? seq : undefined, key: undefined, value: null };\n  }\n  return {\n    event: name,\n    seq: Number.isFinite(seq) ? seq : undefined,\n    key: payload.key,\n    value,\n  };\n}\n";
Expand description

Live sync for durable placement, and the transport seam it needs.

Shipped only when the split found a durable key. It imports rpc.js, which a program with a crossing already has.