Added opt-in canonical observer polling lifecycle

This commit is contained in:
2026-10-20 20:11:57 -04:00
parent 8cfcf6c025
commit 7025c37390
8 changed files with 200 additions and 15 deletions
+21 -4
View File
@@ -20,7 +20,8 @@ import { marketCommandV3Schema } from '../../shared/market-v3.ts';
import { custodyCommandSchema } from '../../shared/custody.ts';
import { CustodyMarket, CustodyError, custodyMode } from '../market/custody.ts';
import { openCustodyStorage } from '../market/custody-storage.ts';
import type { PostgresCustody } from '../market/postgres-custody.ts';
import { PostgresCustody } from '../market/postgres-custody.ts';
import { CanonicalObserver } from '../market/canonical-observer.ts';
import { validateMarketPolicy } from '../market/policy.ts';
import { IS_MAINNET, PROFILE } from '../../shared/network.ts';
import { assertLaunchAllowed, launchManifest } from '../launch.ts';
@@ -32,6 +33,14 @@ assertLaunchAllowed();
const paymentConfig = marketConfig(); // Fail before opening node resources if live payments are requested.
await validateMarketPolicy(paymentConfig); // Reject invalid recipient checksum/network before opening node resources.
const custodyConfig = custodyMode(); // Explicit opt-in sandbox only. Unknown/live modes fail before opening resources.
const canonicalObserverMode = process.env.WIKIDECK_CUSTODY_CANONICAL_REHEARSAL;
if (canonicalObserverMode !== undefined && canonicalObserverMode !== '0' && canonicalObserverMode !== '1') {
throw new Error('WIKIDECK_CUSTODY_CANONICAL_REHEARSAL must be exactly 0 or 1');
}
if (canonicalObserverMode === '1' && (process.env.NODE_ENV === 'production' || custodyConfig !== 'sandbox' ||
process.env.WIKIDECK_CUSTODY_STORAGE !== 'postgres' || process.env.WIKIDECK_PG_DISPOSABLE_TEST !== '1')) {
throw new Error('Canonical observer rehearsal requires sandbox custody, PostgreSQL, and disposable test isolation');
}
const limits = resourceConfig();
const proxyTrust=trustedProxies();
const opsPort=operationsPort();
@@ -57,6 +66,7 @@ let market: Market;
try { market = await Market.open(dataDir, ledger, paymentConfig); }
catch(e) {peers.close();ledger.close();engine.close();await lock.close();await unlink(lockPath);throw e;}
let custody: CustodyMarket | PostgresCustody | undefined;
let custodyObserver: CanonicalObserver | undefined;
try {
if (custodyConfig === 'sandbox') custody = await openCustodyStorage(dataDir, { ...paymentConfig,
card: (hash, owner, sequence) => {
@@ -65,7 +75,12 @@ try {
ledger.pendingTransactions().some(tx => transactionCardIds(tx).includes(hash))) return undefined;
return { title: nft.block.article.title };
} });
} catch(e) {market.close();peers.close();ledger.close();engine.close();await lock.close();await unlink(lockPath);throw e;}
if (canonicalObserverMode === '1') {
if (!(custody instanceof PostgresCustody)) throw new Error('Canonical observer rehearsal requires PostgreSQL custody');
custodyObserver = new CanonicalObserver(custody, ledger);
if ((await custodyObserver.start()).state !== 'running') throw new Error('Canonical observer rehearsal failed to start; inspect custody diagnostics');
}
} catch(e) {await custodyObserver?.close();await custody?.close();market.close();peers.close();ledger.close();engine.close();await lock.close();await unlink(lockPath);throw e;}
const app = express();
app.disable('x-powered-by');
if(proxyTrust.length)app.set('trust proxy',proxyTrust);
@@ -226,6 +241,7 @@ const operations=startOperations(opsPort,()=>({network:NETWORK,paymentMode:'mock
ledger:ledger.diagnostics(),native:engine.diagnostics(),
peers:{configured:peers.status().length,connected:peers.status().filter(p=>p.connected).length},
rpc:{api:apiBudget.status(),p2p:peerBudget.status()},market:market.diagnostics(),
canonicalObserver:custodyObserver?.status(),
custody: custody && 'diagnostics' in custody ? custody.diagnostics() : { backend: custody ? 'sqlite-sandbox' : 'off' }}),resolve(dataDir,PROFILE));
server.maxConnections=256;
server.headersTimeout=10000;
@@ -238,7 +254,8 @@ marketTimer.unref();
let stopping=false;
for(const signal of ['SIGTERM','SIGINT']as const)process.once(signal,()=>{
if(stopping)return;stopping=true;peers.close();operations.close();clearInterval(marketTimer);
const observerDrain=custodyObserver?.close()??Promise.resolve();
const timeout=setTimeout(()=>process.exit(1),25000);timeout.unref();
server.close(async()=>{await custody?.close();market.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});clearTimeout(timeout);process.exit(0);});
server.close(async()=>{await observerDrain;await custody?.close();market.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});clearTimeout(timeout);process.exit(0);});
});
server.on('error',async()=>{clearInterval(marketTimer);operations.close();peers.close();await custody?.close();market.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});process.exit(1);});
server.on('error',async()=>{clearInterval(marketTimer);operations.close();peers.close();await custodyObserver?.close();await custody?.close();market.close();ledger.close();engine.close();await lock.close();await unlink(lockPath).catch(()=>{});process.exit(1);});