diff options
| author | Adam Soltys <[email protected]> | 2026-10-16 12:57:19 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-16 12:57:19 -0700 |
| commit | a3d2dc2e05a4f7c5ab0d6e7689498da9ca63de7b (patch) | |
| tree | b1c9d687e1cf0cced71bb2460a69ccdcb512f0dc /names/server.js | |
| parent | 1dd70cc2c2b9b0a9f65a704ded9e322afabf736f (diff) | |
| download | coinosv3-server-a3d2dc2e05a4f7c5ab0d6e7689498da9ca63de7b.tar.gz coinosv3-server-a3d2dc2e05a4f7c5ab0d6e7689498da9ca63de7b.zip | |
names: keep state in SQLite
persist() rewrote the whole state file, close to 10 MB, on every settled
payment and every queue tick, so one crash during a write could take every
name with it. State now lives in SQLite (bun:sqlite, WAL,
synchronous=FULL): one row per name, offer, invoice and so on, with the
forwarder wallet's state kept as a single row. Each persist() is one
transaction that writes only the rows that changed, so a crash leaves the
previous state or the new one, never a torn file.
The in-memory state object and every persist() call stay as they were. On
the first start the JSON state file is imported, and a corrupt one stops
startup. The file is left in place, so a rollback to the JSON build still
finds its data. store.snapshot() makes a consistent copy while the service
runs, for backups.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'names/server.js')
| -rw-r--r-- | names/server.js | 33 |
1 files changed, 13 insertions, 20 deletions
diff --git a/names/server.js b/names/server.js index 9d94b51..c4ed650 100644 --- a/names/server.js +++ b/names/server.js @@ -13,10 +13,10 @@ // WHAT THIS SERVICE LEARNS: name ↔ nostr pubkey ↔ payment URI. All of it is // public by construction (DNS is public). It cannot spend anything. // -// Deliberately dumb: one JSON state file, no accounts, no email. +// Deliberately dumb: one SQLite file (store.js), no accounts, no email. -import { readFileSync, writeFileSync, renameSync, mkdirSync, openSync, fsyncSync, closeSync } from 'node:fs'; -import { dirname, join } from 'node:path'; +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; import { verifyEvent, finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure'; import * as nip44 from 'nostr-tools/nip44'; import { wrapManyEvents } from 'nostr-tools/nip17'; @@ -34,10 +34,12 @@ import { decodeNoffer } from '../coinosv3/src/noffer.js'; import { decodeBolt11 } from '../coinosv3/src/ark/lightning.js'; import { decodeOffer } from '../coinosv3/src/bolt12.js'; import { registrationIdentity } from './ownership.js'; +import { openStore } from './store.js'; const CFG = JSON.parse(readFileSync(process.env.NAMES_CONFIG || join(import.meta.dir, 'config.json'), 'utf8')); -// { port, domains: { '<domain>': { zoneId } }, domain (default), cfEmail, cfKey, stateFile, +// { port, domains: { '<domain>': { zoneId } }, domain (default), cfEmail, cfKey, +// dbFile (SQLite state), stateFile (the old JSON state, imported once), // ln: { kind:'socket', path } — the ASP's CLN, for per-name bolt12 offers, // forwarder: { mnemonic, ark, esplora, network } — a small float ark wallet // that forwards settled offer payments to the name's ark address } @@ -46,6 +48,7 @@ const DOMAIN = CFG.domain || 'coinos.io'; // the default domain for claims const PUBLIC_BASE = CFG.publicBase || 'https://names.coinos.io'; const DOMAINS = CFG.domains || { [DOMAIN]: { zoneId: CFG.zoneId } }; const STATE = CFG.stateFile || join(import.meta.dir, 'data', 'names.json'); +const DB = CFG.dbFile || STATE.replace(/\.json$/, '') + '.db'; const TTL = 300; // record TTL: BIP-353 wallets cache by this const log = (...a) => console.log(new Date().toISOString(), ...a); @@ -65,22 +68,12 @@ const NAME_RE = /^[a-z0-9][a-z0-9._-]{0,29}$/; const NEW_NAME_RE = /^[a-z0-9]{2,24}$/; const NEW_NAME_ERR = 'names can only have letters and numbers (2–24)'; -// A missing file is a fresh registrar; an unreadable one is not. Starting -// empty over a corrupt file would persist the empty state on top of it. -let state = (() => { - let raw; - try { raw = readFileSync(STATE, 'utf8'); } catch (e) { if (e.code === 'ENOENT') return { names: {} }; throw e; } - return JSON.parse(raw); -})(); -function persist() { - mkdirSync(dirname(STATE), { recursive: true }); - const tmp = STATE + '.tmp'; - writeFileSync(tmp, JSON.stringify(state)); - // flush before the rename, or a crash can leave a renamed-but-empty file - const fd = openSync(tmp, 'r'); fsyncSync(fd); closeSync(fd); - renameSync(tmp, STATE); - const dfd = openSync(dirname(STATE), 'r'); fsyncSync(dfd); closeSync(dfd); -} +// State lives in SQLite: each persist() is one transaction that rewrites only +// the records that changed. On the first start the old JSON state file is +// imported (a corrupt one stops startup rather than importing nothing). +const store = openStore(DB, { legacyJson: STATE }); +const state = store.state; +function persist() { store.persist(); } // --------------------------------------------------------------------------- // Cloudflare DNS |
