OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet commits / a31acbb

Fix pear-runtime OTA for large binary apps (8 patches)
pear-runtime was designed for small JS apps (~1MB). For godot-keet (71MB Godot binary + node_modules), every update re-downloaded everything. Eight CI patches to pear-runtime/index.js fix this: 1. Lazy-load bare-sidecar (prevents staging traversal failure) 2. Don't delete staging on launch (interrupted mirrors can resume) 3. Move this.applied after fsx.swap (retry works on failure) 4. Fixed staging path (no per-version dirs, enables cross-version diffs) 5. Don't delete staging after swap (old files serve as diff base) 6. Numeric version comparison (prevents downgrades during replication race) 7. Update this.version after mirror (prevents double-fire) 8. Validate staging completeness (belt-and-suspenders for race conditions) Result: OTA updates download only changed files (~5 files in seconds) instead of the full 71MB+ app over P2P. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
a31acbb5fff9a989902b58680655dd660bc63700
by plankatron <fleeky@gmail.com> · 2026-02-22T03:08:57.000Z

Changed files (2) +50 -15

.pear-git/ci.yaml +49 -14
……@@ -25,22 +25,57 @@ steps:
2525 # Must overwrite keet-cli's version since rsync already copied it.
2626 run: rm -rf node_modules/keet-daemon && cp -r /storage/projects/code/hypercore-kb/hyper-fleeky/packages/keet-daemon node_modules/keet-daemon
2727 - name: patch-pear-runtime
28 # Three patches to pear-runtime:
29 # 1. bare-sidecar has native prebuilds that break pear stage. Lazy-load it since we never call run().
30 # 2. applyUpdate() sets this.applied=true BEFORE fsx.swap — if swap fails (Hyperdrive not fully
31 # replicated), the retry on next 'updated' event is a no-op. Move flag after successful swap.
32 # 3. Version comparison: use numeric > instead of !== to prevent replication race.
33 # On first launch, pear-runtime replicates the Hyperdrive from scratch. As entries arrive,
34 # 'append' fires at intermediate lengths. checkout(oldLength).get('/package.json') returns
35 # an OLD version, triggering an update with incomplete by-arch data. The swap then destroys
36 # the install dir. Fix: only update when remote version is strictly NEWER (commit count).
28 # Four patches to pear-runtime for large binary apps (71MB Godot binary):
29 # 1. Lazy-load bare-sidecar — native prebuilds break pear stage module traversal.
30 # 2. Don't delete staging on launch — enables incremental mirrors. Without this,
31 # _open() runs rm -rf on pear-runtime/next/ every launch, forcing a full 71MB
32 # re-mirror even when only index.js changed. With staging preserved, interrupted
33 # mirrors resume and repeated _update() calls during replication reuse existing files.
34 # 3. Move this.applied AFTER fsx.swap — if swap fails, retry on next 'updated' event
35 # actually retries instead of being a no-op.
36 # 4. Validate staging completeness before signaling update ready — prevents replication
37 # race where checkout(intermediateLength) returns an old deploy state's package.json
38 # triggering a mirror of incomplete by-arch data. Replaces the old version comparison
39 # hack with a proper file-presence check.
3740 run: |
38 sed -i "s/const Sidecar = require('bare-sidecar')/let Sidecar = null/" node_modules/pear-runtime/index.js
39 sed -i "/return new Sidecar/i\\ if (!Sidecar) Sidecar = require('bare-sidecar')" node_modules/pear-runtime/index.js
41 RT=node_modules/pear-runtime/index.js
42
43 # 1. Lazy-load bare-sidecar (prevents staging traversal failure)
44 sed -i "s/const Sidecar = require('bare-sidecar')/let Sidecar = null/" $RT
45 sed -i "/return new Sidecar/i\\ if (!Sidecar) Sidecar = require('bare-sidecar')" $RT
4046 rm -rf node_modules/bare-sidecar node_modules/pear-runtime/node_modules/bare-sidecar
41 sed -i '/if (!this.updated || this.applied || !this.bundled) return/{n;/this.applied = true/d}' node_modules/pear-runtime/index.js
42 sed -i '/await fsx.swap/a\ this.applied = true' node_modules/pear-runtime/index.js
43 sed -i 's/JSON.parse(manifest).version === this.version/(() => { const rv = JSON.parse(manifest).version; const rn = parseInt((rv || "0").split(".")[1] || "0"); const ln = parseInt((String(this.version) || "0").split(".")[1] || "0"); return rn <= ln; })()/' node_modules/pear-runtime/index.js
47
48 # 2. Don't delete staging on launch — enables incremental mirrors
49 # Lines 53-56: await fs.promises.rm(path.join(this.dir, 'pear-runtime/next'), {...})
50 sed -i '/await.*rm.*pear-runtime\/next/,+3d' $RT
51
52 # 3. Move this.applied AFTER fsx.swap (not before — prevents retry no-op on failure)
53 sed -i '/if (!this.updated || this.applied || !this.bundled) return/{n;/this.applied = true/d}' $RT
54 sed -i '/await fsx.swap/a\ this.applied = true' $RT
55
56 # 4. Fixed staging path — remove per-version id so mirror-drive diffs against
57 # previous version's files instead of always starting fresh. Each new Hypercore
58 # length created a new empty staging dir, forcing full 71MB re-download.
59 sed -i "s|path.join(this.dir, 'pear-runtime/next', id)|path.join(this.dir, 'pear-runtime/next')|" $RT
60
61 # 5. Don't delete staging after swap — fsx.swap() exchanges dirs, so after swap
62 # staging has the OLD app files. Next mirror diffs against those = incremental.
63 # Costs 71MB disk but saves minutes of P2P bandwidth per update.
64 sed -i '/fs.promises.rm(this.next/d' $RT
65
66 # 6. Numeric version comparison — prevent downgrades during replication race.
67 # On fresh corestore, append fires at intermediate lengths. checkout(oldLength)
68 # returns an old deploy's package.json with a LOWER version. Skip unless newer.
69 sed -i 's/JSON.parse(manifest).version === this.version/(() => { const rn = parseInt((JSON.parse(manifest).version || "0").split(".")[1] || "0"); const ln = parseInt((String(this.version) || "0").split(".")[1] || "0"); return rn <= ln })()/' $RT
70
71 # 7. Update this.version after successful mirror — prevents double-fire.
72 # Without this, _update() sees version mismatch again (this.version is from
73 # startup, manifest has new version) and mirrors a second time redundantly.
74 sed -i '/this.next = next/a\ this.version = JSON.parse(manifest).version' $RT
75
76 # 8. Validate staging completeness before signaling update ready
77 # Belt-and-suspenders: if staging doesn't have index.js + package.json, skip.
78 sed -i '/this.updated = true/i\ const _sa = path.join(next, "by-arch", host, "app", this.name); try { fs.statSync(path.join(_sa, "index.js")); fs.statSync(path.join(_sa, "package.json")) } catch { return }' $RT
4479 - name: verify-deps
4580 # Prove all critical imports resolve before staging
4681 run: |
index.js +1 -1
……@@ -248,7 +248,7 @@ async function main () {
248248 appVersion = fs.readFileSync(path.join(getAppDir(), 'version.txt'), 'utf-8').trim()
249249 } catch {}
250250
251 console.log(`[launcher] godot-keet ${appVersion} (by-arch OTA)`)
251 console.log(`[launcher] godot-keet ${appVersion} (incremental OTA v6)`)
252252 console.log(`[launcher] Platform: ${platformDir}`)
253253
254254 // Start daemon in-process (or detect existing external daemon)