Theseus: read BNS names from Ariadne's indexer; its own is the standby
Migration step 3 (DESIGN-bns-indexer-service.md). Ariadne's Thread now owns BNS indexing on the machine, so Theseus no longer runs a second electrum indexer beside it. bns-indexer.js keeps its process and its messages to main.js, but inside it is now an index host on the shared source chain: - Ariadne's indexer over its pipe, trusted only after ariadne-helper.exe has checked the server process on that connection (found through Ariadne's uninstall key), then pushes; - the local copies: Ariadne's files for both scopes, Theseus's own raw copy, the bundled one. The richest wins. - Theseus's own index copy, written from the pipe data. The shared core runs as Theseus's own indexer only while Ariadne is unhealthy. That means: no pipe 4 s after launch, a pipe that fails the check, a pipe that went silent, or an index not confirmed for 10 min while Ariadne is not paused. The own indexer warm-starts from Ariadne's snapshot, so there is no download and no cold sync. It hands back after 90 s of health, so a flapping service does not start and stop it. Economy is not a failure and never triggers a takeover. With no checkable Ariadne (portable, not installed, older than the pipe) the own indexer starts at once, as in 0.3.70. The one thing Theseus does on Ariadne's side is run the indexer's task at launch when "Launch at start" is off. main.js passes the shared module paths (packaged as .mjs, which is why the shared modules no longer import each other) and keeps the host's status. The Ariadne panel takes its state from the indexer task when one exists, and the sub-page says where Theseus's names come from.
This commit is contained in:
parent
0ba0e735ed
commit
7254ffe6f4
5 changed files with 360 additions and 283 deletions
|
|
@ -11,6 +11,8 @@ Already implemented:
|
||||||
modules, the indexer/resolver split, the installer. See "Implementation
|
modules, the indexer/resolver split, the installer. See "Implementation
|
||||||
notes" at the end for where the code deviates from this text, and what
|
notes" at the end for where the code deviates from this text, and what
|
||||||
step 3 (the Theseus client) gets from the pipe.
|
step 3 (the Theseus client) gets from the pipe.
|
||||||
|
- Migration step 3 (Theseus, not released yet): `bns-indexer.js` is now the
|
||||||
|
index host described under "Theseus" (see "Step 3 in Theseus" at the end).
|
||||||
|
|
||||||
## The model
|
## The model
|
||||||
|
|
||||||
|
|
@ -468,13 +470,43 @@ settings:
|
||||||
- **Settings:** edit `policy.json` directly (`mode`, `power`, `economyWhen`,
|
- **Settings:** edit `policy.json` directly (`mode`, `power`, `economyWhen`,
|
||||||
`startAtBoot`), and run the task where `ariadne-ctl` does (mode ->
|
`startAtBoot`), and run the task where `ariadne-ctl` does (mode ->
|
||||||
all-browsers, start-at-boot -> on). Task names:
|
all-browsers, start-at-boot -> on). Task names:
|
||||||
- All users: `BNS Indexer`, `BNS Resolver Daemon` (unchanged, so 0.3.70's
|
- All users: `Ariadne BNS Indexer`, `BNS Resolver Daemon` (unchanged, so 0.3.70's
|
||||||
panel still finds it);
|
panel still finds it);
|
||||||
- Just me: `BNS Indexer (<user>)`, `BNS Resolver (<user>)`.
|
- Just me: `Ariadne BNS Indexer (<user>)`, `Ariadne BNS Resolver (<user>)`.
|
||||||
- **Until step 3 ships:** Theseus 0.3.70's Ariadne panel only knows `BNS
|
- **Until step 3 ships:** Theseus 0.3.70's Ariadne panel only knows `BNS
|
||||||
Resolver Daemon`. In Theseus-only mode that task is Ready, not Running, so
|
Resolver Daemon`. In Theseus-only mode that task is Ready, not Running, so
|
||||||
the panel shows Ariadne as off.
|
the panel shows Ariadne as off.
|
||||||
|
|
||||||
|
### Step 3 in Theseus
|
||||||
|
- **Where it runs:** `bns-indexer.js` stays the utilityProcess with the same
|
||||||
|
messages to main.js, so main.js changed only in small ways: the shared
|
||||||
|
module paths, a `status` message, and the Ariadne panel. Inside, it runs
|
||||||
|
the source chain, and runs the shared core as Theseus's own indexer only
|
||||||
|
on standby.
|
||||||
|
- **Packaging:** the shared modules ship as `bns-index-core.mjs`,
|
||||||
|
`bns-source-chain.mjs` and `bns-pipe.mjs` in the resources, like
|
||||||
|
`resolver-web.mjs`. That is why they import nothing from each other.
|
||||||
|
- **Finding Ariadne:** `InstallLocation` from its uninstall key (HKLM, then
|
||||||
|
HKCU), which gives the helper and the expected server image. Its files
|
||||||
|
are read from both scopes' `index\` folders even without the helper. The
|
||||||
|
copy with the most evidence wins, compared against Theseus's own raw copy
|
||||||
|
and the bundled one.
|
||||||
|
- **Health:** checked every 10 s.
|
||||||
|
- Healthy means: the pipe is connected, and Ariadne is either paused
|
||||||
|
(Economy) or confirmed its index within 10 min.
|
||||||
|
- Otherwise, 4 s after launch Theseus starts its own indexer, warm from
|
||||||
|
the richest copy (normally Ariadne's file).
|
||||||
|
- It hands back after 90 s of health.
|
||||||
|
- **Own copies:**
|
||||||
|
- `bns-name-index.json` (index format, from the pipe);
|
||||||
|
- `bns-name-snapshot.json` / `.prev.json` (raw, only while the own
|
||||||
|
indexer runs).
|
||||||
|
- **What does not change:** the quick single-name lookup stays in main.js
|
||||||
|
(it goes through Tor and the session proxy); the source chain in the host
|
||||||
|
has its public sources switched off.
|
||||||
|
- **Not done:** controls in Settings for mode, Power and Launch at start.
|
||||||
|
The panel's on/off switch still acts on the resolver task, as before.
|
||||||
|
|
||||||
### Bundling Ariadne into the Theseus installer (designed, not wired)
|
### Bundling Ariadne into the Theseus installer (designed, not wired)
|
||||||
- **Payload:** the Ariadne setup exe, as an `extraResources` file of
|
- **Payload:** the Ariadne setup exe, as an `extraResources` file of
|
||||||
Theseus's NSIS build, so the two keep separate version lifecycles.
|
Theseus's NSIS build, so the two keep separate version lifecycles.
|
||||||
|
|
|
||||||
531
bns-indexer.js
531
bns-indexer.js
|
|
@ -1,31 +1,47 @@
|
||||||
// BNS name indexer — runs in its own process (Electron utilityProcess),
|
// BNS index host. Runs in its own process (Electron utilityProcess), started
|
||||||
// started by main.js, so the snapshot parsing, index builds and electrum
|
// by main.js, so snapshot parsing, index builds, the Ariadne pipe and any
|
||||||
// traffic never compete with the browser's main thread.
|
// electrum traffic never compete with the browser's main thread.
|
||||||
|
// Spec: DESIGN-bns-indexer-service.md ("Theseus").
|
||||||
//
|
//
|
||||||
// Boot is two-phase:
|
// Ariadne's Thread owns BNS indexing on a machine; Theseus reads from it
|
||||||
// 1. fast lane: load the local snapshot (user cache > bundled), build the
|
// through the shared source chain (Argus/src/lib/bns-source-chain.js):
|
||||||
// index from it and publish it. No network. This is what the first tab
|
// 1. Ariadne's indexer over its pipe, server-checked (ariadne-helper.exe):
|
||||||
// needs, and it is all that runs while the browser is still starting.
|
// a snapshot, then pushes
|
||||||
// 2. full work, once main says "go" (its first page has loaded): electrum
|
// 2. local copies: Ariadne's two files (All users / Just me), Theseus's own
|
||||||
// server discovery, the Sia snapshot refresh, and the 30 s delta poll.
|
// raw copy, the bundled snapshot (the one with the most evidence wins),
|
||||||
// A lookup that misses the snapshot before then still gets a live
|
// and Theseus's own index copy written from the pipe
|
||||||
// delta poll on demand — only that, nothing else.
|
// 3. (main.js) the quick single-name lookup on the gateway, provisional
|
||||||
|
// Theseus's own electrum indexer (the shared core, bns-index-core.js) is a
|
||||||
|
// standby, the last resort:
|
||||||
|
// - started when Ariadne is unhealthy: no pipe a few seconds after launch,
|
||||||
|
// a pipe that fails the server check, a pipe that went silent, or an
|
||||||
|
// index that is not being confirmed while Ariadne says it is not paused.
|
||||||
|
// Warm-started from the richest local copy (normally Ariadne's), so no
|
||||||
|
// download and no cold sync.
|
||||||
|
// - stopped again once Ariadne has been healthy for 90 s, so a flapping
|
||||||
|
// service cannot start and stop it over and over.
|
||||||
|
// Under Economy Ariadne is paused, not unhealthy: no takeover.
|
||||||
|
// With no Ariadne at all (portable, not installed, a pre-0.2 install) it is
|
||||||
|
// the 0.3.70 behaviour: the own indexer runs throughout.
|
||||||
//
|
//
|
||||||
// Protocol (process.parentPort messages):
|
// The only thing Theseus ever does to Ariadne: when "Launch at start" is off,
|
||||||
// main -> indexer { type: "init", resolverPath, userData, bundledSnapshot, torPort }
|
// it runs the indexer's task once at launch. It never writes Ariadne's files.
|
||||||
// { type: "go" } start phase 2
|
//
|
||||||
|
// Protocol (process.parentPort messages), unchanged for main.js:
|
||||||
|
// main -> host { type: "init", resolverPath, coreLib, chainLib, pipeLib,
|
||||||
|
// userData, bundledSnapshot, torPort }
|
||||||
|
// { type: "go" } network work may start
|
||||||
// { type: "tor", port } electrum via Tor (null = direct)
|
// { type: "tor", port } electrum via Tor (null = direct)
|
||||||
// { type: "stop-polling" }
|
// { type: "stop-polling" }
|
||||||
// { id, type: "poll" | "rebuild" | "ready" } replies { type: "reply", id, ok }
|
// { id, type: "poll" | "rebuild" | "ready" } replies { type: "reply", id, ok }
|
||||||
// indexer -> main { type: "index", builtAt, names: [[key, entry]...], network }
|
// host -> main { type: "index", builtAt, names: [[key, entry]...], network, source }
|
||||||
// { type: "fresh", builtAt } poll succeeded, nothing changed
|
// { type: "fresh", builtAt } confirmed, nothing changed
|
||||||
|
// { type: "status", ... } where the names come from (Settings)
|
||||||
// { type: "reply", id, ok, error? }
|
// { type: "reply", id, ok, error? }
|
||||||
//
|
|
||||||
// The index entries are the resolver's own (records, owner, txid, height, …)
|
|
||||||
// built from the raw beacon transactions — the trust model is unchanged.
|
|
||||||
"use strict";
|
"use strict";
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
const { execFile } = require("child_process");
|
||||||
const { pathToFileURL } = require("url");
|
const { pathToFileURL } = require("url");
|
||||||
const WebSocket = require("ws");
|
const WebSocket = require("ws");
|
||||||
|
|
||||||
|
|
@ -41,17 +57,16 @@ const log = (...a) => console.log("[bns]", ...a);
|
||||||
return close.apply(this, args);
|
return close.apply(this, args);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// A flaky network must not take the indexer down; main restarts it if it
|
|
||||||
// exits, but the index would go cold for a moment.
|
|
||||||
process.on("uncaughtException", (err) => { try { console.error("[bns] uncaught:", (err && err.stack) || err); } catch {} });
|
process.on("uncaughtException", (err) => { try { console.error("[bns] uncaught:", (err && err.stack) || err); } catch {} });
|
||||||
process.on("unhandledRejection", (err) => { try { console.error("[bns] unhandled rejection:", (err && err.message) || err); } catch {} });
|
process.on("unhandledRejection", (err) => { try { console.error("[bns] unhandled rejection:", (err && err.message) || err); } catch {} });
|
||||||
|
|
||||||
let cfg = null;
|
const LAUNCH_GRACE_MS = 4_000; // the pipe gets this long before the own indexer starts
|
||||||
let R = null;
|
const STALE_MS = 10 * 60_000; // unpaused Ariadne whose index is not confirmed this long: unhealthy
|
||||||
async function getResolver() {
|
const HANDBACK_MS = 90_000; // healthy this long before the own indexer is stopped
|
||||||
if (!R) R = await import(pathToFileURL(cfg.resolverPath).href);
|
const CHECK_EVERY_MS = 10_000;
|
||||||
return R;
|
|
||||||
}
|
let cfg = null, R = null, core = null, chainLib = null, pipeLib = null;
|
||||||
|
const importEsm = (p) => import(pathToFileURL(p).href);
|
||||||
|
|
||||||
// ---- Tor ------------------------------------------------------------------
|
// ---- Tor ------------------------------------------------------------------
|
||||||
let torAgent = null;
|
let torAgent = null;
|
||||||
|
|
@ -63,248 +78,232 @@ async function setTor(torPort) {
|
||||||
class TorWebSocket extends WebSocket { constructor(url, opts) { super(url, { agent: torAgent, ...opts }); } }
|
class TorWebSocket extends WebSocket { constructor(url, opts) { super(url, { agent: torAgent, ...opts }); } }
|
||||||
const currentWS = () => (torAgent ? TorWebSocket : WebSocket);
|
const currentWS = () => (torAgent ? TorWebSocket : WebSocket);
|
||||||
|
|
||||||
// ---- electrum server pool: hardcoded seed + on-chain discovery, persisted ----
|
// ---- Ariadne's Thread on this machine ---------------------------------------
|
||||||
// Bootstrap from the baked-in seed (with pinned IPs), then refresh from the
|
// Each scope it may be installed in: where its files are, its pipe, and how
|
||||||
// on-chain ELECTRUM_LIST_NAME record so the pool can be rotated without a new
|
// to check the pipe's server (Ariadne's helper; the server's image is
|
||||||
// build. The last discovered list is cached to disk and tried first next launch.
|
// Ariadne's own node.exe). The install folder comes from Ariadne's uninstall
|
||||||
let electrumPool = null;
|
// key. A pre-0.2 Ariadne has no helper and no pipe: only files (none either).
|
||||||
let lastElectrumRefresh = 0;
|
const ARIADNE_KEY = "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{7E7A5F1C-3B4E-4C8A-9E1D-ARIADNERSLVR}_is1";
|
||||||
// Per-network cache files. Chipnet keeps the historical names so existing
|
function execOut(file, args) {
|
||||||
// profiles are not invalidated; any other network gets its own files.
|
return new Promise((resolve) => execFile(file, args, { windowsHide: true, timeout: 10_000 }, (err, out) => resolve(err ? "" : String(out))));
|
||||||
|
}
|
||||||
|
async function installLocation(hive) {
|
||||||
|
const out = await execOut("reg.exe", ["query", `${hive}\\${ARIADNE_KEY}`, "/v", "InstallLocation", "/reg:64"]);
|
||||||
|
const m = /InstallLocation\s+REG_\w+\s+(.+)/.exec(out);
|
||||||
|
return m ? m[1].trim() : null;
|
||||||
|
}
|
||||||
|
async function findAriadne() {
|
||||||
|
// Tests point Theseus at a scratch Ariadne: {scope, pipe, stateDir, helper, imagePrefix}.
|
||||||
|
if (process.env.THESEUS_ARIADNE_TEST) {
|
||||||
|
try { return [JSON.parse(process.env.THESEUS_ARIADNE_TEST)]; } catch { return []; }
|
||||||
|
}
|
||||||
|
if (process.platform !== "win32") return [];
|
||||||
|
const scopes = [];
|
||||||
|
const programData = process.env.ProgramData || process.env.PROGRAMDATA || "C:\\ProgramData";
|
||||||
|
const localAppData = process.env.LOCALAPPDATA || "";
|
||||||
|
const machineLoc = await installLocation("HKLM");
|
||||||
|
scopes.push({
|
||||||
|
scope: "machine", pipe: "\\\\.\\pipe\\ariadne-bns", task: "Ariadne BNS Indexer",
|
||||||
|
stateDir: path.join(programData, "Ariadne"), loc: machineLoc,
|
||||||
|
});
|
||||||
|
const userLoc = await installLocation("HKCU");
|
||||||
|
if (userLoc || (localAppData && fs.existsSync(path.join(localAppData, "Ariadne", "index")))) {
|
||||||
|
const sid = (/"(S-1-[\d-]+)"/.exec(await execOut("whoami", ["/user", "/fo", "csv", "/nh"])) || [])[1];
|
||||||
|
scopes.push({
|
||||||
|
scope: "user", pipe: sid ? `\\\\.\\pipe\\ariadne-bns-${sid}` : null, task: `Ariadne BNS Indexer (${process.env.USERNAME || ""})`,
|
||||||
|
stateDir: path.join(localAppData, "Ariadne"), loc: userLoc,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const s of scopes) {
|
||||||
|
if (!s.loc) continue;
|
||||||
|
const exe = path.join(s.loc, "tools", "ariadne-helper.exe");
|
||||||
|
if (fs.existsSync(exe)) { s.helper = { exe }; s.imagePrefix = path.join(s.loc, "runtime") + path.sep; }
|
||||||
|
}
|
||||||
|
return scopes;
|
||||||
|
}
|
||||||
|
const ariadneCopies = (scopes) => scopes.flatMap((s) => [
|
||||||
|
path.join(s.stateDir, "index", "bns-name-snapshot.json"),
|
||||||
|
path.join(s.stateDir, "index", "bns-name-snapshot.prev.json"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// "Launch at start" off: the indexer runs on demand, i.e. now. The one thing
|
||||||
|
// Theseus starts on Ariadne's side; its task lets local users run it.
|
||||||
|
function runIndexerOnDemand(scopes) {
|
||||||
|
for (const s of scopes) {
|
||||||
|
if (!s.task || !s.loc) continue;
|
||||||
|
let pol = null;
|
||||||
|
try { pol = JSON.parse(fs.readFileSync(path.join(s.stateDir, "policy.json"), "utf8").replace(/^\uFEFF/, "")); } catch {}
|
||||||
|
if (pol && pol.startAtBoot === false) {
|
||||||
|
execFile("schtasks.exe", ["/Run", "/TN", s.task], { windowsHide: true }, (err) => {
|
||||||
|
log(err ? `Ariadne's indexer (${s.scope}) could not be started on demand` : `Ariadne's indexer (${s.scope}) started on demand (Launch at start is off)`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- state ------------------------------------------------------------------
|
||||||
let networkId = "chipnet";
|
let networkId = "chipnet";
|
||||||
const netSuffix = () => (networkId === "chipnet" ? "" : `.${networkId}`);
|
const netSuffix = () => (networkId === "chipnet" ? "" : `.${networkId}`);
|
||||||
const electrumFile = () => path.join(cfg.userData, `electrum-servers${netSuffix()}.json`);
|
let scopes = [];
|
||||||
const snapshotUserPath = () => path.join(cfg.userData, `bns-name-snapshot${netSuffix()}.json`);
|
let chain = null; // the source chain (Ariadne + local copies)
|
||||||
const serverKey = (s) => (typeof s === "string" ? s : s && s.url);
|
let own = null; // Theseus's own indexer, while it runs
|
||||||
function mergeServers(preferred, rest) {
|
let ownReason = null;
|
||||||
const seen = new Set(), out = [];
|
let goReceived = false;
|
||||||
for (const s of [...(preferred || []), ...(rest || [])]) {
|
let launchedAt = Date.now();
|
||||||
const k = serverKey(s);
|
let healthySince = 0;
|
||||||
if (k && !seen.has(k)) { seen.add(k); out.push(s); }
|
let lastPublished = null; // which index main has: "chain" | "own"
|
||||||
|
|
||||||
|
function active() { return own && own.index ? "own" : "chain"; }
|
||||||
|
function publish(reason) {
|
||||||
|
const which = active();
|
||||||
|
const idx = which === "own" ? own.index : chain && chain.index;
|
||||||
|
if (!idx || !idx.size) return;
|
||||||
|
lastPublished = which;
|
||||||
|
const builtAt = which === "own" ? own.builtAt : chain.builtAt;
|
||||||
|
port.postMessage({ type: "index", builtAt, names: [...idx], network: networkId, reason, source: which === "own" ? "theseus-indexer" : chain.source });
|
||||||
|
sendStatus();
|
||||||
}
|
}
|
||||||
return out;
|
function sendStatus() {
|
||||||
}
|
const cs = chain ? chain.status() : null;
|
||||||
async function initElectrumPool() {
|
port.postMessage({
|
||||||
const r = await getResolver();
|
type: "status",
|
||||||
const seed = r.ELECTRUM || r.CHIPNET_ELECTRUM;
|
source: active() === "own" ? "theseus-indexer" : cs && cs.source,
|
||||||
let saved = [];
|
ariadne: {
|
||||||
try { if (fs.existsSync(electrumFile())) saved = JSON.parse(fs.readFileSync(electrumFile(), "utf8")); } catch {}
|
installed: scopes.filter((s) => s.loc).map((s) => s.scope),
|
||||||
electrumPool = mergeServers(saved, seed); // discovered first, seed always kept
|
pipe: cs ? cs.pipe : null,
|
||||||
}
|
paused: cs ? cs.paused : false,
|
||||||
async function refreshElectrumPool() {
|
builtAt: cs ? cs.builtAt : 0,
|
||||||
try {
|
healthy: ariadneHealthy(),
|
||||||
const { fetchElectrumServers } = await getResolver();
|
},
|
||||||
const found = await fetchElectrumServers({ WebSocket: currentWS(), directIP: true, electrum: electrumPool });
|
ownIndexer: own ? { running: true, reason: ownReason, ...own.status() } : { running: false },
|
||||||
if (found && found.length) {
|
});
|
||||||
electrumPool = mergeServers(found, electrumPool);
|
|
||||||
try { fs.writeFileSync(electrumFile(), JSON.stringify(found, null, 2)); } catch {}
|
|
||||||
}
|
|
||||||
} catch { /* list unpublished or unreachable — keep the current pool */ }
|
|
||||||
}
|
|
||||||
function maybeRefreshElectrum() {
|
|
||||||
if (!started) return; // phase 2 only
|
|
||||||
if (Date.now() - lastElectrumRefresh < 30 * 60 * 1000) return;
|
|
||||||
lastElectrumRefresh = Date.now();
|
|
||||||
refreshElectrumPool();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- index state + publishing ---------------------------------------------
|
// ---- health -----------------------------------------------------------------
|
||||||
let index = null; // Map name -> entry
|
function ariadneHealthy() {
|
||||||
let currentSnapshotState = null; // raw { beacon, history, txs, … } behind `index`
|
if (!chain || !chain.pipeConnected) return false;
|
||||||
let lastSig = "";
|
// Economy: Ariadne pauses its network work and stops confirming. That is a
|
||||||
// Cheap fingerprint of the snapshot: an index rebuilt from the same history
|
// setting, not a failure; taking over would undo it.
|
||||||
// at the same heights is the same index, so main isn't sent a copy.
|
if (chain.paused) return true;
|
||||||
const sigOf = (snap) => `${snap.history.length}:${snap.history.reduce((a, h) => a + (Number(h.height) || 0), 0)}`;
|
return Date.now() - chain.builtAt < STALE_MS;
|
||||||
function publish(idx, snap, reason) {
|
}
|
||||||
index = idx;
|
function checkHealth() {
|
||||||
const sig = snap ? sigOf(snap) : `full:${Date.now()}`;
|
const now = Date.now();
|
||||||
const builtAt = reason === "snapshot" ? 0 : Date.now(); // a snapshot is a floor, not a ceiling
|
if (ariadneHealthy()) {
|
||||||
if (sig === lastSig) { port.postMessage({ type: "fresh", builtAt }); return; }
|
healthySince = healthySince || now;
|
||||||
lastSig = sig;
|
if (own && now - healthySince >= HANDBACK_MS) stopOwn();
|
||||||
port.postMessage({ type: "index", builtAt, names: [...idx], network: networkId, reason });
|
} else {
|
||||||
|
healthySince = 0;
|
||||||
|
if (!own && now - launchedAt >= LAUNCH_GRACE_MS) {
|
||||||
|
const cs = chain && chain.status();
|
||||||
|
const why = !scopes.some((s) => s.helper) ? "no Ariadne indexer installed"
|
||||||
|
: cs && cs.pipe && cs.pipe.error ? `Ariadne pipe: ${cs.pipe.error}`
|
||||||
|
: chain && chain.pipeConnected ? "Ariadne's index is not being confirmed"
|
||||||
|
: "Ariadne's indexer is not answering";
|
||||||
|
startOwn(why);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function readSnapshotFrom(p) {
|
// ---- Theseus's own indexer (standby) ------------------------------------------
|
||||||
try {
|
function startOwn(reason) {
|
||||||
if (!fs.existsSync(p)) return null;
|
if (own) return own;
|
||||||
const parsed = JSON.parse(fs.readFileSync(p, "utf8"));
|
ownReason = reason;
|
||||||
if (!parsed || !Array.isArray(parsed.history)) return null;
|
log(`own indexer starting: ${reason}`);
|
||||||
return parsed;
|
own = core.createIndexer({
|
||||||
} catch { return null; }
|
resolver: R,
|
||||||
|
WebSocket: currentWS(),
|
||||||
|
snapshotDir: cfg.userData,
|
||||||
|
snapshotBase: `bns-name-snapshot${netSuffix()}`,
|
||||||
|
// Ariadne's last snapshot first in line: no download, no cold sync.
|
||||||
|
warmPaths: [...ariadneCopies(scopes), cfg.bundledSnapshot],
|
||||||
|
electrumCacheFile: path.join(cfg.userData, `electrum-servers${netSuffix()}.json`),
|
||||||
|
log: (...a) => log(...a),
|
||||||
|
});
|
||||||
|
own.on("change", () => { if (own) publish("own"); });
|
||||||
|
own.on("fresh", ({ builtAt }) => { if (own && active() === "own") port.postMessage({ type: "fresh", builtAt }); });
|
||||||
|
own.warm();
|
||||||
|
if (own.index && lastPublished !== "own") publish("own-warm"); // its "change" normally did
|
||||||
|
if (goReceived) own.start();
|
||||||
|
sendStatus();
|
||||||
|
return own;
|
||||||
|
}
|
||||||
|
function stopOwn() {
|
||||||
|
if (!own) return;
|
||||||
|
log("Ariadne healthy again: own indexer stopped");
|
||||||
|
own.stop();
|
||||||
|
own.removeAllListeners();
|
||||||
|
own = null; ownReason = null;
|
||||||
|
publish("handback");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 1: the local snapshot, nothing else.
|
// ---- boot -------------------------------------------------------------------
|
||||||
async function warmFromSnapshot() {
|
async function init(m) {
|
||||||
const r = await getResolver();
|
cfg = m;
|
||||||
networkId = r.NETWORK?.id || "chipnet";
|
await setTor(m.torPort);
|
||||||
if (!r.buildIndexFromSnapshot) return false; // an older resolver-web.js
|
[R, core, chainLib, pipeLib] = await Promise.all([importEsm(m.resolverPath), importEsm(m.coreLib), importEsm(m.chainLib), importEsm(m.pipeLib)]);
|
||||||
const snap = readSnapshotFrom(snapshotUserPath()) || readSnapshotFrom(cfg.bundledSnapshot);
|
networkId = (R.NETWORK && R.NETWORK.id) || "chipnet";
|
||||||
if (!snap) return false;
|
scopes = await findAriadne();
|
||||||
try {
|
launchedAt = Date.now();
|
||||||
|
const checked = scopes.filter((s) => s.helper && s.pipe);
|
||||||
|
chain = chainLib.createSourceChain({
|
||||||
|
resolver: R,
|
||||||
|
// Machine scope first: if both are installed, readers use the machine one.
|
||||||
|
connectPipe: checked.length ? async () => {
|
||||||
|
let last = null;
|
||||||
|
for (const s of checked) {
|
||||||
|
try { return await pipeLib.connectIndexerPipe({ pipeName: s.pipe, helper: s.helper, expect: { scope: s.scope, imagePrefix: s.imagePrefix } }); }
|
||||||
|
catch (e) { last = e; }
|
||||||
|
}
|
||||||
|
throw last || new Error("no pipe");
|
||||||
|
} : null,
|
||||||
|
localPaths: [
|
||||||
|
...ariadneCopies(scopes),
|
||||||
|
path.join(cfg.userData, `bns-name-snapshot${netSuffix()}.json`),
|
||||||
|
path.join(cfg.userData, `bns-name-snapshot${netSuffix()}.prev.json`),
|
||||||
|
cfg.bundledSnapshot,
|
||||||
|
],
|
||||||
|
ownCopyPath: path.join(cfg.userData, `bns-name-index${netSuffix()}.json`),
|
||||||
|
// The public sources are not the chain's job here: the own indexer
|
||||||
|
// fetches the published snapshot when it runs, and main.js has the
|
||||||
|
// quick single-name lookup (through Tor or the session proxy).
|
||||||
|
publicSnapshotUrls: [],
|
||||||
|
publicIndexerUrl: null,
|
||||||
|
log: (...a) => log(...a),
|
||||||
|
});
|
||||||
|
chain.on("change", () => { if (active() === "chain") publish("chain"); });
|
||||||
|
chain.on("fresh", ({ builtAt }) => { if (active() === "chain") port.postMessage({ type: "fresh", builtAt }); });
|
||||||
|
chain.on("pipe", ({ connected, error }) => { log(connected ? "Ariadne's indexer connected" : `Ariadne's indexer unavailable (${error})`); checkHealth(); sendStatus(); });
|
||||||
|
chain.on("paused", (p) => { log(p ? "Ariadne is in Economy: sync paused" : "Ariadne resumed syncing"); sendStatus(); });
|
||||||
const t0 = Date.now();
|
const t0 = Date.now();
|
||||||
const idx = r.buildIndexFromSnapshot({ snapshot: snap });
|
chain.start(); // local copies now (synchronous), the pipe in the background
|
||||||
currentSnapshotState = snap;
|
if (chain.index.size) log(`local copy: ${chain.index.size} names from ${chain.source} in ${Date.now() - t0} ms`);
|
||||||
publish(idx, snap, "snapshot");
|
if (!lastPublished) publish("local"); // the chain's "change" has normally sent it already
|
||||||
log(`warm-started from snapshot: ${idx.size} names in ${Date.now() - t0} ms @ height=${snap.asOfHeight ?? "?"} root=${snap.root ?? "?"}`);
|
runIndexerOnDemand(scopes);
|
||||||
return true;
|
// Nothing to wait for (portable Theseus, no Ariadne, or one older than the
|
||||||
} catch (e) {
|
// pipe): the own indexer at once, as in 0.3.70.
|
||||||
log("snapshot warm-start failed:", e.message);
|
if (!checked.length) startOwn("no Ariadne indexer installed");
|
||||||
return false;
|
setTimeout(checkHealth, LAUNCH_GRACE_MS);
|
||||||
}
|
setInterval(checkHealth, CHECK_EVERY_MS);
|
||||||
}
|
}
|
||||||
|
let initP = null;
|
||||||
|
|
||||||
// ---- continuous background delta refresh ----------------------------------
|
// "ready": resolves once there is any index. The pipe gets its grace period;
|
||||||
// One electrum connection per poll: fetch the beacon's history (one call),
|
// then the own indexer (published snapshot, electrum as the last resort).
|
||||||
// fetch only the tx bodies not already held, rebuild locally, persist.
|
|
||||||
const POLL_INTERVAL_MS = 30_000;
|
|
||||||
// Neither the electrum connect nor its WebSocket handshake has a timeout of
|
|
||||||
// its own; a server that accepts TCP and then stalls would wedge the loop.
|
|
||||||
const POLL_DEADLINE_MS = 45_000;
|
|
||||||
let pollInFlight = null, pollTimer = null;
|
|
||||||
async function pollAndMerge() {
|
|
||||||
if (pollInFlight) return pollInFlight;
|
|
||||||
let conn = null, deadline;
|
|
||||||
const work = (async () => {
|
|
||||||
const r = await getResolver();
|
|
||||||
if (!r.connectElectrum || !r.BEACON_SCRIPTHASH || !r.buildIndexFromSnapshot) return rebuild();
|
|
||||||
if (!electrumPool) await initElectrumPool();
|
|
||||||
// Base state: memory > user cache > bundled > empty. The "empty" branch
|
|
||||||
// turns a first start without any snapshot into a full fetch.
|
|
||||||
const snap = currentSnapshotState
|
|
||||||
|| readSnapshotFrom(snapshotUserPath())
|
|
||||||
|| readSnapshotFrom(cfg.bundledSnapshot)
|
|
||||||
|| { beacon: r.BEACON_SCRIPTHASH, history: [], txs: {} };
|
|
||||||
const el = conn = await r.connectElectrum({ electrum: electrumPool, WebSocket: currentWS(), directIP: true });
|
|
||||||
try {
|
|
||||||
const freshHistory = await el.call("blockchain.scripthash.get_history", [r.BEACON_SCRIPTHASH]);
|
|
||||||
const known = new Set(snap.history.map((h) => h.tx_hash));
|
|
||||||
// Merge fresh into snapshot history (dedup by tx_hash, keep fresh height —
|
|
||||||
// an event that was mempool at snapshot time now has a real height).
|
|
||||||
const merged = new Map(snap.history.map((h) => [h.tx_hash, h]));
|
|
||||||
const txs = { ...(snap.txs || {}) };
|
|
||||||
let added = 0;
|
|
||||||
for (const h of freshHistory) {
|
|
||||||
if (!known.has(h.tx_hash)) {
|
|
||||||
try { txs[h.tx_hash] = await el.call("blockchain.transaction.get", [h.tx_hash, true]); added++; }
|
|
||||||
catch { /* unreadable — the reduction rules ignore missing txs */ }
|
|
||||||
}
|
|
||||||
merged.set(h.tx_hash, { tx_hash: h.tx_hash, height: h.height });
|
|
||||||
}
|
|
||||||
const history = [...merged.values()];
|
|
||||||
currentSnapshotState = { ...snap, beacon: r.BEACON_SCRIPTHASH, history, txs };
|
|
||||||
const idx = r.buildIndexFromSnapshot({ snapshot: currentSnapshotState });
|
|
||||||
publish(idx, currentSnapshotState, "poll");
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(path.dirname(snapshotUserPath()), { recursive: true });
|
|
||||||
fs.writeFileSync(snapshotUserPath(), JSON.stringify(currentSnapshotState));
|
|
||||||
} catch { /* readonly userdata / disk full — skip */ }
|
|
||||||
if (added > 0) log(`delta-refresh: +${added} new tx${added === 1 ? "" : "s"} (total ${history.length}, index has ${idx.size} names)`);
|
|
||||||
return true;
|
|
||||||
} finally { try { el.close(); } catch {} }
|
|
||||||
})().catch(() => false);
|
|
||||||
const timeout = new Promise((resolve) => {
|
|
||||||
deadline = setTimeout(() => { try { conn?.close(); } catch {} resolve(false); }, POLL_DEADLINE_MS);
|
|
||||||
});
|
|
||||||
const p = Promise.race([work, timeout]).finally(() => {
|
|
||||||
clearTimeout(deadline);
|
|
||||||
if (pollInFlight === p) pollInFlight = null;
|
|
||||||
maybeRefreshElectrum();
|
|
||||||
});
|
|
||||||
pollInFlight = p;
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Full walk over electrum — only for a resolver without the delta primitives,
|
|
||||||
// or an explicit rebuild request.
|
|
||||||
let rebuilding = null;
|
|
||||||
function rebuild() {
|
|
||||||
if (rebuilding) return rebuilding;
|
|
||||||
rebuilding = (async () => {
|
|
||||||
const r = await getResolver();
|
|
||||||
if (!electrumPool) await initElectrumPool();
|
|
||||||
const idx = await r.buildIndex({ WebSocket: currentWS(), directIP: true, electrum: electrumPool });
|
|
||||||
publish(idx, null, "rebuild");
|
|
||||||
return true;
|
|
||||||
})().catch(() => false).finally(() => { rebuilding = null; });
|
|
||||||
return rebuilding;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The published raw snapshot (public, refreshed every 10 min on the VPS):
|
|
||||||
// one download that can carry a long-closed browser across days of beacon
|
|
||||||
// events, so the electrum poll after it only fetches what is newer still.
|
|
||||||
// Merged in now — not saved for the next start — and only ever adds: the
|
|
||||||
// index is rebuilt here from the transactions, as with any other source.
|
|
||||||
async function refreshFromPublishedSnapshot() {
|
|
||||||
try {
|
|
||||||
const r = await getResolver();
|
|
||||||
// Two public copies (dl vhost, Sia via the gateway): first one that answers.
|
|
||||||
const urls = [r.NETWORK?.nameListUrl, r.NETWORK?.nameListMirrorUrl].filter(Boolean);
|
|
||||||
let pub = null;
|
|
||||||
for (const url of urls) {
|
|
||||||
try {
|
|
||||||
const res = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(15000) });
|
|
||||||
if (!res.ok) { log(`published snapshot ${new URL(url).host}: HTTP ${res.status}`); continue; }
|
|
||||||
pub = await res.json();
|
|
||||||
break;
|
|
||||||
} catch (e) { log(`published snapshot ${new URL(url).host}: ${e?.cause?.code || e?.message || e}`); }
|
|
||||||
}
|
|
||||||
if (!pub) return;
|
|
||||||
if (!pub || !Array.isArray(pub.history) || !pub.txs || (r.BEACON_SCRIPTHASH && pub.beacon !== r.BEACON_SCRIPTHASH)) return;
|
|
||||||
const base = currentSnapshotState || { beacon: r.BEACON_SCRIPTHASH, history: [], txs: {} };
|
|
||||||
const merged = new Map(base.history.map((h) => [h.tx_hash, h]));
|
|
||||||
const txs = { ...(base.txs || {}) };
|
|
||||||
let added = 0;
|
|
||||||
for (const h of pub.history) {
|
|
||||||
const mine = merged.get(h.tx_hash);
|
|
||||||
if (!pub.txs[h.tx_hash] && !txs[h.tx_hash]) continue; // no evidence, skip
|
|
||||||
if (!txs[h.tx_hash]) { txs[h.tx_hash] = pub.txs[h.tx_hash]; added++; }
|
|
||||||
// Keep a confirmed height over a mempool (0 / negative) one.
|
|
||||||
if (!mine || (!(Number(mine.height) > 0) && Number(h.height) > 0)) merged.set(h.tx_hash, { tx_hash: h.tx_hash, height: h.height });
|
|
||||||
}
|
|
||||||
const next = { ...base, beacon: base.beacon || pub.beacon, history: [...merged.values()], txs };
|
|
||||||
if (sigOf(next) === sigOf(base)) return;
|
|
||||||
currentSnapshotState = next;
|
|
||||||
publish(r.buildIndexFromSnapshot({ snapshot: next }), next, "published");
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(path.dirname(snapshotUserPath()), { recursive: true });
|
|
||||||
fs.writeFileSync(snapshotUserPath(), JSON.stringify(next));
|
|
||||||
} catch {}
|
|
||||||
log(`published snapshot merged: +${added} tx${added === 1 ? "" : "s"} (total ${next.history.length}, ${index.size} names)`);
|
|
||||||
} catch (e) { log("published snapshot unavailable:", e?.message || e); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- phases -----------------------------------------------------------------
|
|
||||||
let warmed = null; // promise: phase 1 finished (index may still be null)
|
|
||||||
let started = false; // phase 2 running
|
|
||||||
function startFullWork() {
|
|
||||||
if (started) return;
|
|
||||||
started = true;
|
|
||||||
(async () => {
|
|
||||||
await warmed;
|
|
||||||
if (!electrumPool) await initElectrumPool();
|
|
||||||
// Fastest catch-up first: one download of the published snapshot, then
|
|
||||||
// the electrum delta for whatever is newer than that.
|
|
||||||
await refreshFromPublishedSnapshot();
|
|
||||||
lastElectrumRefresh = Date.now();
|
|
||||||
refreshElectrumPool();
|
|
||||||
// No snapshot at all (first start of a build without one): the poll's
|
|
||||||
// "empty" base state does the full fetch.
|
|
||||||
pollAndMerge();
|
|
||||||
pollTimer = setInterval(() => pollAndMerge(), POLL_INTERVAL_MS);
|
|
||||||
})().catch((e) => log("start failed:", e?.message));
|
|
||||||
}
|
|
||||||
|
|
||||||
// "ready": resolves once there is any index — the local snapshot, or (none
|
|
||||||
// on disk) the published one, one download; the live poll only if that
|
|
||||||
// failed too, since a first electrum walk fetches every beacon transaction.
|
|
||||||
let firstFetch = null;
|
|
||||||
async function whenReady() {
|
async function whenReady() {
|
||||||
await warmed;
|
await initP;
|
||||||
if (index) return true;
|
if (active() === "chain" && chain.index.size) return true;
|
||||||
firstFetch = firstFetch || refreshFromPublishedSnapshot().finally(() => { firstFetch = null; });
|
if (own && own.index) return true;
|
||||||
await firstFetch;
|
const until = launchedAt + LAUNCH_GRACE_MS;
|
||||||
if (index) return true;
|
while (Date.now() < until && !chain.index.size) await new Promise((r) => setTimeout(r, 200));
|
||||||
return pollAndMerge();
|
if (chain.index.size && active() === "chain") return true;
|
||||||
|
return startOwn(own ? ownReason : "no index yet").ready();
|
||||||
|
}
|
||||||
|
async function poll() {
|
||||||
|
await initP;
|
||||||
|
if (own) return own.poll({ force: true });
|
||||||
|
if (chain.pipeConnected) return chain.requestPoll();
|
||||||
|
return false; // no live source yet: the health check starts the own indexer
|
||||||
}
|
}
|
||||||
|
|
||||||
port.on("message", async (e) => {
|
port.on("message", async (e) => {
|
||||||
|
|
@ -312,19 +311,15 @@ port.on("message", async (e) => {
|
||||||
const reply = (ok, error) => { if (m.id != null) port.postMessage({ type: "reply", id: m.id, ok: !!ok, error }); };
|
const reply = (ok, error) => { if (m.id != null) port.postMessage({ type: "reply", id: m.id, ok: !!ok, error }); };
|
||||||
try {
|
try {
|
||||||
switch (m.type) {
|
switch (m.type) {
|
||||||
case "init":
|
case "init": initP = init(m); await initP; break;
|
||||||
cfg = m;
|
case "go": goReceived = true; await initP; if (own) own.start(); break;
|
||||||
await setTor(m.torPort);
|
case "tor": await setTor(m.port); if (own) own.setWebSocket(currentWS()); break;
|
||||||
warmed = warmFromSnapshot();
|
case "stop-polling": if (own) own.stop(); if (chain) chain.stop(); break;
|
||||||
break;
|
|
||||||
case "go": startFullWork(); break;
|
|
||||||
case "tor": await setTor(m.port); break;
|
|
||||||
case "stop-polling": if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } break;
|
|
||||||
case "ready": reply(await whenReady()); break;
|
case "ready": reply(await whenReady()); break;
|
||||||
// An on-demand poll is allowed before phase 2: it is the "this tab's
|
// On-demand poll (a tab's name is not in the index): Ariadne's indexer
|
||||||
// name isn't in the snapshot" case, and costs one electrum round trip.
|
// when it is live, else the own one; allowed before "go".
|
||||||
case "poll": await warmed; reply(await pollAndMerge()); break;
|
case "poll": reply(await poll()); break;
|
||||||
case "rebuild": await warmed; reply(await rebuild()); break;
|
case "rebuild": await initP; reply(await startOwn(ownReason || "rebuild requested").rebuild()); break;
|
||||||
default: reply(false, "unknown request");
|
default: reply(false, "unknown request");
|
||||||
}
|
}
|
||||||
} catch (err) { reply(false, err?.message || String(err)); }
|
} catch (err) { reply(false, err?.message || String(err)); }
|
||||||
|
|
|
||||||
43
main.js
43
main.js
|
|
@ -1745,12 +1745,14 @@ async function fetchOpenSearch(href) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- BNS index: mirror of the indexer process --------------------------------
|
// ---- BNS index: mirror of the indexer process --------------------------------
|
||||||
// The index itself — snapshot warm-start, electrum delta poll, Sia snapshot
|
// The index itself runs in bns-indexer.js, a separate process (utilityProcess),
|
||||||
// refresh, electrum server discovery — runs in bns-indexer.js, a separate
|
// so none of it competes with the browser's main thread. Ariadne's Thread owns
|
||||||
// process (utilityProcess), so none of it competes with the browser's main
|
// BNS indexing on the machine: that process reads Ariadne's indexer over its
|
||||||
// thread. It boots in two phases: the local snapshot first (no network — what
|
// server-checked pipe, or Ariadne's local copies, and starts Theseus's own
|
||||||
// the first tab needs), then the network work once this side says "go", after
|
// electrum indexer only while Ariadne is unhealthy or absent
|
||||||
// the first page has loaded. This side keeps a read-only mirror of the name
|
// (DESIGN-bns-indexer-service.md). It boots in two phases: the local copies
|
||||||
|
// first (no network — what the first tab needs), then the network work once
|
||||||
|
// this side says "go", after the first page has loaded. This side keeps a read-only mirror of the name
|
||||||
// map for the lookups that must answer synchronously (knownUnregistered, the
|
// map for the lookups that must answer synchronously (knownUnregistered, the
|
||||||
// native-TLD set, error-page suggestions, Hermes reverse lookups).
|
// native-TLD set, error-page suggestions, Hermes reverse lookups).
|
||||||
|
|
||||||
|
|
@ -1762,6 +1764,12 @@ let sharedIndex = null; // Map name -> entry, mirrored from the indexer
|
||||||
let indexBuiltAt = 0; // when the indexer last confirmed it is current (0 = snapshot only)
|
let indexBuiltAt = 0; // when the indexer last confirmed it is current (0 = snapshot only)
|
||||||
let indexGen = 0; // bumps whenever the mirror's content changes
|
let indexGen = 0; // bumps whenever the mirror's content changes
|
||||||
const SNAPSHOT_BUNDLED = path.join(RES_DIR, "bns-name-snapshot.json");
|
const SNAPSHOT_BUNDLED = path.join(RES_DIR, "bns-name-snapshot.json");
|
||||||
|
// The shared index modules (also Ariadne's): .mjs in the packaged resources,
|
||||||
|
// like resolver-web; the engine copies in dev.
|
||||||
|
const bnsSharedLib = (name) => (app.isPackaged ? path.join(RES_DIR, `${name}.mjs`) : path.join(__dirname, "..", "Argus", "src", "lib", `${name}.js`));
|
||||||
|
// Where the names come from right now (Ariadne's pipe / its files / Theseus's
|
||||||
|
// own indexer), as the index process last reported it. Shown in Settings.
|
||||||
|
let bnsIndexStatus = null;
|
||||||
let indexer = null, indexerStarts = 0, indexerSeq = 0, indexerGo = false, indexerStopping = false;
|
let indexer = null, indexerStarts = 0, indexerSeq = 0, indexerGo = false, indexerStopping = false;
|
||||||
const indexerPending = new Map();
|
const indexerPending = new Map();
|
||||||
const indexWaiters = [];
|
const indexWaiters = [];
|
||||||
|
|
@ -1789,7 +1797,11 @@ function startIndexer() {
|
||||||
startIndexer();
|
startIndexer();
|
||||||
}, wait);
|
}, wait);
|
||||||
});
|
});
|
||||||
indexer.postMessage({ type: "init", resolverPath: RESOLVER, userData: app.getPath("userData"), bundledSnapshot: SNAPSHOT_BUNDLED, torPort: torState === "on" ? TOR_PORT : null });
|
indexer.postMessage({
|
||||||
|
type: "init", resolverPath: RESOLVER,
|
||||||
|
coreLib: bnsSharedLib("bns-index-core"), chainLib: bnsSharedLib("bns-source-chain"), pipeLib: bnsSharedLib("bns-pipe"),
|
||||||
|
userData: app.getPath("userData"), bundledSnapshot: SNAPSHOT_BUNDLED, torPort: torState === "on" ? TOR_PORT : null,
|
||||||
|
});
|
||||||
if (indexerGo) indexer.postMessage({ type: "go" });
|
if (indexerGo) indexer.postMessage({ type: "go" });
|
||||||
}
|
}
|
||||||
function onIndexerMessage(m) {
|
function onIndexerMessage(m) {
|
||||||
|
|
@ -1803,6 +1815,8 @@ function onIndexerMessage(m) {
|
||||||
verifyQuickAnswers();
|
verifyQuickAnswers();
|
||||||
} else if (m.type === "fresh") {
|
} else if (m.type === "fresh") {
|
||||||
indexBuiltAt = m.builtAt;
|
indexBuiltAt = m.builtAt;
|
||||||
|
} else if (m.type === "status") {
|
||||||
|
bnsIndexStatus = m;
|
||||||
} else if (m.type === "reply") {
|
} else if (m.type === "reply") {
|
||||||
const settle = indexerPending.get(m.id);
|
const settle = indexerPending.get(m.id);
|
||||||
if (settle) { indexerPending.delete(m.id); settle(!!m.ok); }
|
if (settle) { indexerPending.delete(m.id); settle(!!m.ok); }
|
||||||
|
|
@ -6220,7 +6234,7 @@ function ariadneQueryState() {
|
||||||
const shell = new Promise((resolve) => {
|
const shell = new Promise((resolve) => {
|
||||||
const ps = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command",
|
const ps = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command",
|
||||||
"$svc=New-Object -ComObject Schedule.Service;$svc.Connect();$f=$svc.GetFolder('\\');" +
|
"$svc=New-Object -ComObject Schedule.Service;$svc.Connect();$f=$svc.GetFolder('\\');" +
|
||||||
"foreach($n in 'BNS Resolver Daemon','BNS Sia Bridge'){try{$t=$f.GetTask($n);\"$n=$($t.State)\"}catch{\"$n=MISSING\"}};" +
|
"foreach($n in 'Ariadne BNS Indexer','BNS Resolver Daemon','BNS Sia Bridge'){try{$t=$f.GetTask($n);\"$n=$($t.State)\"}catch{\"$n=MISSING\"}};" +
|
||||||
ARIADNE_REG_LOOKUP +
|
ARIADNE_REG_LOOKUP +
|
||||||
"if ($r) { \"__VER__=$($r.DisplayVersion)\"; \"__UNINSTALL__=$($r.QuietUninstallString)\" }"
|
"if ($r) { \"__VER__=$($r.DisplayVersion)\"; \"__UNINSTALL__=$($r.QuietUninstallString)\" }"
|
||||||
], { windowsHide: true });
|
], { windowsHide: true });
|
||||||
|
|
@ -6232,7 +6246,11 @@ function ariadneQueryState() {
|
||||||
return Promise.all([shell, ariadneManifestFetch()]).then(([out, latest]) => {
|
return Promise.all([shell, ariadneManifestFetch()]).then(([out, latest]) => {
|
||||||
const lines = out.trim().split(/\r?\n/).filter(Boolean);
|
const lines = out.trim().split(/\r?\n/).filter(Boolean);
|
||||||
const map = Object.fromEntries(lines.map((l) => { const i = l.lastIndexOf("="); return [l.slice(0, i), l.slice(i + 1)]; }));
|
const map = Object.fromEntries(lines.map((l) => { const i = l.lastIndexOf("="); return [l.slice(0, i), l.slice(i + 1)]; }));
|
||||||
const primary = map["BNS Resolver Daemon"]; // TASK_STATE: 1 disabled, 2 queued, 3 ready, 4 running
|
// TASK_STATE: 1 disabled, 2 queued, 3 ready, 4 running. Since Ariadne
|
||||||
|
// 0.2 its indexer is the process that matters to Theseus; the resolver
|
||||||
|
// only runs in All-browsers mode.
|
||||||
|
const indexerTask = map["Ariadne BNS Indexer"];
|
||||||
|
const primary = indexerTask && indexerTask !== "MISSING" ? indexerTask : map["BNS Resolver Daemon"];
|
||||||
const installedVersion = map.__VER__ || null;
|
const installedVersion = map.__VER__ || null;
|
||||||
const quietUninstall = map.__UNINSTALL__ || null;
|
const quietUninstall = map.__UNINSTALL__ || null;
|
||||||
const latestVersion = latest ? latest.version : null;
|
const latestVersion = latest ? latest.version : null;
|
||||||
|
|
@ -6244,7 +6262,12 @@ function ariadneQueryState() {
|
||||||
// The "bundledVersion" field name is kept for renderer compatibility --
|
// The "bundledVersion" field name is kept for renderer compatibility --
|
||||||
// it now carries the latest version advertised by silentmode.st's
|
// it now carries the latest version advertised by silentmode.st's
|
||||||
// releases manifest, not a version physically bundled with Theseus.
|
// releases manifest, not a version physically bundled with Theseus.
|
||||||
return { state, installedVersion, bundledVersion: latestVersion, canUpdate, hasUninstaller: !!quietUninstall };
|
const resolverTask = map["BNS Resolver Daemon"];
|
||||||
|
return {
|
||||||
|
state, installedVersion, bundledVersion: latestVersion, canUpdate, hasUninstaller: !!quietUninstall,
|
||||||
|
resolverRunning: resolverTask === "4" || resolverTask === "Running",
|
||||||
|
index: bnsIndexStatus,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function cmpVersions(a, b) {
|
function cmpVersions(a, b) {
|
||||||
|
|
|
||||||
12
package.json
12
package.json
|
|
@ -120,6 +120,18 @@
|
||||||
"from": "../Argus/src/lib/password-vault.js",
|
"from": "../Argus/src/lib/password-vault.js",
|
||||||
"to": "password-vault.mjs"
|
"to": "password-vault.mjs"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"from": "../Argus/src/lib/bns-index-core.js",
|
||||||
|
"to": "bns-index-core.mjs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "../Argus/src/lib/bns-source-chain.js",
|
||||||
|
"to": "bns-source-chain.mjs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "../Argus/src/lib/bns-pipe.js",
|
||||||
|
"to": "bns-pipe.mjs"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"from": "lib/hermes.js",
|
"from": "lib/hermes.js",
|
||||||
"to": "lib/hermes.mjs"
|
"to": "lib/hermes.mjs"
|
||||||
|
|
|
||||||
|
|
@ -1733,6 +1733,21 @@
|
||||||
} else {
|
} else {
|
||||||
arStatSub.innerHTML = `<b>Not installed</b> on this machine.${bundledSuffix}`;
|
arStatSub.innerHTML = `<b>Not installed</b> on this machine.${bundledSuffix}`;
|
||||||
}
|
}
|
||||||
|
// Where Theseus's own BNS names come from right now: Ariadne's
|
||||||
|
// indexer (live), Ariadne's local copy, or Theseus's own indexer
|
||||||
|
// (the fallback while Ariadne's is not available).
|
||||||
|
const ix = r.index;
|
||||||
|
if (ix) {
|
||||||
|
const src = ix.source === "pipe" ? "Ariadne's indexer (live)"
|
||||||
|
: ix.source === "theseus-indexer" ? `Theseus's own indexer${ix.ownIndexer?.reason ? ` (${ix.ownIndexer.reason})` : ""}`
|
||||||
|
: ix.source === "own-copy" ? "Theseus's saved copy"
|
||||||
|
: String(ix.source || "").startsWith("local:") ? "a local snapshot copy" : "none yet";
|
||||||
|
const line = document.createElement("div");
|
||||||
|
line.className = "pmuted";
|
||||||
|
line.style.marginTop = "4px";
|
||||||
|
line.textContent = `Theseus reads names from ${src}${ix.ariadne?.paused ? " — Ariadne is in Economy, sync paused" : ""}.`;
|
||||||
|
arStatSub.appendChild(line);
|
||||||
|
}
|
||||||
// Toggle state on both views. When not installed the toggle is
|
// Toggle state on both views. When not installed the toggle is
|
||||||
// disabled — user has to hit Install first.
|
// disabled — user has to hit Install first.
|
||||||
if (!arBusy) {
|
if (!arBusy) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue