OverviewFilesCommitsIssuesDocsReleasesPeers

favela-surface-android / lib / host / loader-core.js

84 lines · javascript ref:
// The decision half of the worklet loader: what to preload, what to run, and in which order.
//
// Pure and dependency-free like choose-bundle.js, for the same reason — this is the code path that
// decides what EXECUTES on a device with no adb, so it has to be testable without one.
// lib/loader.cjs is the shim that supplies the real preloader and the real require.

const BUNDLE_FLAG = '--bundle='
const ADDONS_FLAG = '--addons='

/**
 * Read the loader's inputs out of argv.
 *
 * ⛔ BY TAG, NOT BY POSITION. bare-kit calls NativeBareKit.startFile(handle, filename, args) and whether
 * the native side prepends the filename to Bare.argv is settled in native code that is not visible from
 * JS. lib/node.js works around the same unknown by reading the LAST argv; tagging makes this immune from
 * both ends — and leaves storage in last place, so a bundle staged before the loader existed still finds
 * it unchanged. That backwards compatibility is what makes the loader safe to ship to a device that is
 * already holding a staged bundle.
 */
export function loaderArgs (argv) {
  const find = (flag) => {
    const hit = (argv || []).find((v) => typeof v === 'string' && v.startsWith(flag))
    return hit ? hit.slice(flag.length) : null
  }
  const bundle = find(BUNDLE_FLAG)
  // ⛔ NO DEFAULT. Guessing a path here would run something nobody chose, on the one code path that
  // decides what executes. A throw reaches the watchdog; a guess reaches the user.
  if (!bundle) throw new Error('loader: no ' + BUNDLE_FLAG + ' in argv')
  return { bundle, addons: find(ADDONS_FLAG) || null }
}

/**
 * Preload the staged addons, then run the bundle.
 *
 * ⛔ NOTHING IS CAUGHT. worklet.js writes pending.json before starting and clears it once the node
 * reports ready; a marker that survives a boot condemns that bundle for good. If a failure here were
 * swallowed and the APK's bundle run instead, the node WOULD reach ready, pending.json WOULD be cleared,
 * and the broken bundle would be chosen again on every boot for ever. Failing loudly is the thing that
 * makes the existing watchdog work — the fallback already exists, one layer up, and it only works if
 * this layer stays honest.
 */
export function runLoader ({ argv, preload, load, say = () => {} }) {
  const { bundle, addons } = loaderArgs(argv)
  const n = preload({ dir: addons, say })
  say('preloaded ' + n + ' staged addon(s); starting ' + bundle)
  return load(bundle)
}

// ⛔ THE PIECE THAT REMOVES THE SIDELOAD. A preloader that only lives in the APK means gaining this
// capability costs a new APK — which is the very cost the work exists to remove. It does not have to:
// `new Addon(fileURL)` dlopens an absolute path and needs no filesystem module, and a module inside a
// bundle already knows where its bundle is. Measured on a real packed bundle:
//
//   import.meta.url = file:///…/L-boot/probe.bundle/selfloc-probe.mjs
//
// So a STAGED bundle can locate the addons staged beside it with no help from the shell, which means
// the preloader ships as an ordinary JS release over the OTA that already works — to the APK that is
// already on the device.
//
// ⛔ `.bundle` MUST BE A WHOLE SEGMENT that ends the bundle file. Substring matching would take
// `/x/my.bundles/lib/a.js` or `/x/a.bundle.old/…` for a bundle and derive an addon directory that does
// not exist — a silent no-op, which is the worst kind of wrong here.
export function bundlePathFromUrl (metaUrl) {
  if (typeof metaUrl !== 'string' || metaUrl === '') return null
  const path = metaUrl.startsWith('file://') ? metaUrl.slice('file://'.length) : metaUrl
  const parts = path.split('/')
  for (let i = 0; i < parts.length - 1; i++) {
    if (parts[i].endsWith('.bundle')) return parts.slice(0, i + 1).join('/')
  }
  return null
}

/**
 * Where this module's own bundle keeps its staged addons, or null when not running from a bundle.
 *
 * ⛔ NAMED OFF THE BUNDLE FILE, not the tree hash, precisely so it is DERIVABLE. A name keyed on the
 * tree hash would have to be handed in by the shell, and a shell old enough to predate this cannot hand
 * in anything — which would put the capability back behind an APK release.
 */
export function addonsDirFor (metaUrl) {
  const bundle = bundlePathFromUrl(metaUrl)
  return bundle === null ? null : bundle + '.addons'
}