OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-sync commits / 1b69881

offline mode: `swarm: false` means NO NETWORK, not "make your own"
The constructor treated `opts.swarm` as truthy-or-absent: this._swarm = opts.swarm || null this._ownSwarm = !this._swarm and _open() then did `if (!this._swarm) this._swarm = new Hyperswarm(...)`. So there was no way to say "scan and serve locally, never touch the DHT" — passing null (or false, which || collapses to null) still produced a live DHT-connected swarm. pear-git-v3 hit exactly this: when its own swarm was disabled it passed `hyperswarmOpts: null`, `{...(null || {})}` became `{}`, and a real swarm was built anyway. Adds a THIRD state, distinct from "injected" and "omitted": swarm: <instance> -> use it (unchanged) swarm: null/undef -> create one (unchanged) swarm: false -> OFFLINE, none (new) Offline suppresses only announce/join/connect: no Hyperswarm constructed, no 'connection' handler, no swarm.join(), no swarm.flush(). Everything local is untouched — scan(), the repos map, info(), _repoPath and handleInfoRefs all behave identically, so HTTP git serving still works. _serveOne still derives and records the topic offline (callers read it and `serve` events drive downstream indexes) but leaves `discovery` null. The two unserve() paths and _close() are guarded accordingly — they previously called entry.discovery.destroy() unconditionally, which would have thrown on null. Extends the documented contract in pear-git-v3/spec/modules/hyper-sync.md ("swarm: optional: creates own if omitted") rather than contradicting it — `false` is not "omitted". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1b698814b55411f1d565928e805ce3dc91520e5a
by plankatron <fleeky@gmail.com> · 2026-07-31T16:44:23.000Z

Changed files (1) +31 -9

index.js +31 -9
……@@ -16,8 +16,21 @@ module.exports = class HyperSync extends ReadyResource {
1616 super();
1717
1818 this._reposDir = opts.repos || path.join(require('os').homedir(), 'git-repos');
19 this._swarm = opts.swarm || null;
20 this._ownSwarm = !this._swarm;
19
20 // OFFLINE mode: `swarm: false` means NO NETWORK — do not inject one, do
21 // not create one. Distinct from `swarm: null/undefined`, which still means
22 // "make your own". Without this distinction a caller had no way to say
23 // "scan and serve locally, but never touch the DHT": `_open()` below
24 // creates a Hyperswarm whenever none was injected, so passing null (as
25 // pear-git-v3 did when its own swarm was disabled) silently produced a
26 // live DHT-connected swarm that pulled from real peers.
27 //
28 // Offline keeps everything local intact — scan(), the repos map, info(),
29 // _repoPath, handleInfoRefs — so HTTP git serving still works. Only
30 // announce/join/connect is suppressed.
31 this._offline = opts.swarm === false;
32 this._swarm = this._offline ? null : (opts.swarm || null);
33 this._ownSwarm = !this._offline && !this._swarm;
2134 this._keypair = opts.keypair || null;
2235
2336 // Optional push authorization hook. git-upload-pack (read) is always
……@@ -46,12 +59,14 @@ module.exports = class HyperSync extends ReadyResource {
4659 this.repos = scanRepos(this._reposDir);
4760 this.emit('scan', this.repos);
4861
49 // Create swarm if not injected
50 if (!this._swarm) {
62 // Create swarm if not injected — unless explicitly offline.
63 if (!this._swarm && !this._offline) {
5164 this._swarm = new Hyperswarm({ keyPair: this._keypair });
5265 }
5366
54 this._swarm.on('connection', (conn, info) => this._onConnection(conn, info));
67 if (this._swarm) {
68 this._swarm.on('connection', (conn, info) => this._onConnection(conn, info));
69 }
5570 }
5671
5772 async _close () {
……@@ -138,7 +153,7 @@ module.exports = class HyperSync extends ReadyResource {
138153 }
139154
140155 // Single flush for all topics
141 await this._swarm.flush();
156 if (this._swarm) await this._swarm.flush();
142157 this._serving = true;
143158 this.emit('serving', this._topics.size);
144159 }
……@@ -150,17 +165,24 @@ module.exports = class HyperSync extends ReadyResource {
150165 if (!repo) throw new Error('Repo not found: ' + name);
151166
152167 const topic = deriveTopic(this._keypair.publicKey, name);
153 const discovery = this._swarm.join(topic, { server: true, client: false });
168
169 // Offline: derive + record the topic (callers read it, and `serve` events
170 // drive downstream indexes) but never ANNOUNCE it. No discovery handle.
171 const discovery = this._offline
172 ? null
173 : this._swarm.join(topic, { server: true, client: false });
154174
155175 this._topics.set(name, { topic, discovery, name });
156176 this.emit('serve', name, topic.toString('hex'));
157177 }
158178
159179 async unserve (name) {
180 // `discovery` is null in offline mode (nothing was announced, so there is
181 // nothing to unannounce) — guard rather than relying on a thrown TypeError.
160182 if (name) {
161183 const entry = this._topics.get(name);
162184 if (entry) {
163 await entry.discovery.destroy();
185 if (entry.discovery) await entry.discovery.destroy();
164186 this._topics.delete(name);
165187 }
166188 return;
……@@ -168,7 +190,7 @@ module.exports = class HyperSync extends ReadyResource {
168190
169191 // Unserve all
170192 for (const [n, entry] of this._topics) {
171 await entry.discovery.destroy();
193 if (entry.discovery) await entry.discovery.destroy();
172194 }
173195 this._topics.clear();
174196 this._serving = false;