OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web / spike / worklet-storage-probe.mjs

130 lines · javascript ref:
// Spike 2 (partial, org #36) — can rocksdb-native open a real Corestore in a worklet-shaped thread?
//
// ⚠ HOW TO RUN. This needs `bare-worker`, which favela-web does NOT depend on — it is a probe, not a
// feature, and adding a dep to ship a probe would put it in every published tree. Run it from a tree
// that already has both a worker stack and corestore, e.g.:
//
//     cp spike/worklet-storage-*.mjs /tmp/p/ && cd /tmp/p
//     ln -s ../../hyper-fleeky/pear-git/node_modules node_modules
//     ~/.local/share/store/apps/pgh/runtime/bare worklet-storage-probe.mjs
//
// ⛔ VERSION TRAP, and it cost real time. `bare-worker@4.4.0` + `bare-channel@5.2.4` (npm latest as of
// 2026-08-07) FATALS on both bare 1.26.0 and 1.28.7, before any user code runs:
//     Fatal error in v8::SharedArrayBuffer::New
//     Cannot construct SharedArrayBuffer with BackingStore of ArrayBuffer
// `bare-worker@4.1.6` + `bare-channel@5.2.2` (what pear-git ships) works. Downgrading those two in a
// fresh tree then pulled a `bare-module` prebuild that failed ABI against the runtime
// (`undefined symbol: js_get_function_id`) — so use a WHOLE tree that already works rather than
// resolving a new one. Recorded because "the worker stack fatals" reads like a worklet finding and is
// not one.
//
// Results 2026-08-07, on pear-git's tree (corestore 7.9.2, rocksdb-native 3.13.0, hypercore-storage
// 2.7.1 — OLDER than the fleet's current 3.17.2 / 3.2.0; the threading property is not version-specific
// but re-running on current versions is a cheap follow-up): 8/8.
//
// ⛔ WHAT THIS DOES AND DOES NOT PROVE. bare-kit's worklet is a Bare thread inside the app process with
// an IPC bridge; `bare-worker` is a Bare thread with the same IPC primitive. So this genuinely tests the
// THREADING half — which is the part most likely to break rocksdb, since rocksdb runs its own background
// threads and holds a process-wide exclusive lock per store.
//
// It does NOT test: the android-arm64 prebuild under Bionic, JNI, bare-kit's own worklet lifecycle, or
// Android's file-system semantics. Those need a device or an emulator (this host has no JDK, no Android
// SDK, and no /dev/kvm). celltron does NOT close the gap either — it runs the linux-arm64 GLIBC build
// under proot, never the android Bionic prebuild.
//
// Four questions, in the order they would bite a phone:
//   1. does a Corestore open at all off the main thread?
//   2. can the main thread hold its OWN store at the same time? (the real app shape: app + worklet)
//   3. does the data survive the thread exiting?
//   4. is the RocksDB lock RELEASED when the worklet dies — i.e. can it be reopened?
// (4) is the one that decides whether a phone survives Doze: spec §5 says the node will be killed and
// restarted constantly, and a lock that outlives the worklet makes every restart fail.

const Worker = require('bare-worker')
const Corestore = require('corestore')
const Hyperbee = require('hyperbee')
const fs = require('bare-fs')
const os = require('bare-os')
const path = require('bare-path')

const N = 2000
const root = path.join(os.tmpdir(), 'spike2-' + Date.now())
const workerDir = path.join(root, 'worklet-store')
const mainDir = path.join(root, 'main-store')
fs.mkdirSync(workerDir, { recursive: true })
fs.mkdirSync(mainDir, { recursive: true })

const results = []
const ok = (label, pass, detail = '') => { results.push([pass, label, detail]); console.log(`  ${pass ? 'ok  ' : 'FAIL'} - ${label}${detail ? '  ' + detail : ''}`) }

async function main () {
  console.log('spike 2 — Corestore/rocksdb in a worklet-shaped thread\n')
  console.log('  scratch:', root, '\n')

  // --- 2. the MAIN thread holds its own store, concurrently ---
  // The real app shape is not "a worklet alone": it is a host process that may also touch storage. Two
  // rocksdb instances in one address space, on different threads, is the thing to prove.
  const mainStore = new Corestore(mainDir)
  await mainStore.ready()
  const mainBee = new Hyperbee(mainStore.get({ name: 'main' }), { keyEncoding: 'utf-8', valueEncoding: 'json' })
  await mainBee.ready()
  await mainBee.put('hello', { from: 'main-thread' })
  ok('main thread opened its OWN Corestore', true)

  // --- 1. the worklet-shaped thread opens one too ---
  const steps = []
  let workerKey = null
  const worker = new Worker(require.resolve('./worklet-storage-worker.mjs'), { workerData: { dir: workerDir, n: N } })

  const done = new Promise((resolve, reject) => {
    worker.on('message', (m) => {
      steps.push(m)
      if (m.step === 'ERROR') reject(new Error(m.error))
      if (m.step === 'closed') { workerKey = m.key; resolve() }
    })
    worker.on('exit', (code) => { if (!workerKey) reject(new Error('worker exited (' + code + ') before closing its store')) })
  })

  await done
  const by = (s) => steps.find((x) => x.step === s)
  ok('worklet-thread opened a Corestore', !!by('corestore-ready'), by('corestore-ready') ? by('corestore-ready').ms + 'ms' : '')
  ok('worklet-thread opened a Hyperbee', !!by('hyperbee-ready'))
  ok(`worklet-thread wrote ${N} records`, by('wrote') && by('wrote').n === N, by('wrote') ? by('wrote').ms + 'ms' : '')
  ok('range scan read them all back', by('read') && by('read').seen === N && !by('read').firstBad,
    by('read') ? `seen=${by('read').seen}` + (by('read').firstBad ? ` BAD@${by('read').firstBad}` : '') : '')

  // Main-thread store still usable while the worklet was hammering its own.
  const still = await mainBee.get('hello')
  ok('main-thread store still readable throughout', !!(still && still.value.from === 'main-thread'))

  await worker.terminate()

  // --- 3 + 4. data survived, and the lock was released ---
  // Reopening from the main thread after the worklet is gone is exactly the Doze-restart case.
  let reopened = null
  let lockErr = null
  try {
    const s2 = new Corestore(workerDir)
    await s2.ready()
    const b2 = new Hyperbee(s2.get({ name: 'probe' }), { keyEncoding: 'utf-8', valueEncoding: 'json' })
    await b2.ready()
    let count = 0
    for await (const _ of b2.createReadStream()) count++
    reopened = count
    await s2.close()
  } catch (e) { lockErr = (e && e.message) || String(e) }

  ok('store REOPENS after the worklet exits (the Doze-restart case)', lockErr === null, lockErr || '')
  ok(`all ${N} records survived the thread`, reopened === N, `found ${reopened}`)

  await mainStore.close()

  const passed = results.filter(([p]) => p).length
  console.log(`\n${passed}/${results.length} checks passed`)
  console.log('\nNOT covered here: android-arm64 under Bionic, JNI, bare-kit worklet lifecycle, Android FS semantics.')
  try { fs.rmSync(root, { recursive: true, force: true }) } catch {}
  if (passed !== results.length) Bare.exitCode = 1
}

main().catch((e) => { console.error('\nSPIKE FAILED:', (e && e.stack) || e); Bare.exitCode = 1 })