mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-29 08:51:53 +00:00
188 lines
6.0 KiB
JavaScript
188 lines
6.0 KiB
JavaScript
import { SimplePool } from 'nostr-tools/pool';
|
|
import {
|
|
getQueryTimeoutMs,
|
|
getPublishTimeoutMs,
|
|
getMinSuccessfulRelayPublishes,
|
|
getRelayFallbacks,
|
|
} from './torch-config.mjs';
|
|
import { KIND_APP_DATA } from './constants.mjs';
|
|
|
|
function nowUnix() {
|
|
return Math.floor(Date.now() / 1000);
|
|
}
|
|
|
|
export function parseLockEvent(event) {
|
|
const dTag = event.tags.find((t) => t[0] === 'd')?.[1] ?? '';
|
|
const expTag = event.tags.find((t) => t[0] === 'expiration')?.[1];
|
|
const expiresAt = expTag ? parseInt(expTag, 10) : null;
|
|
|
|
let content = {};
|
|
try {
|
|
const parsed = JSON.parse(event.content);
|
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
content = parsed;
|
|
}
|
|
} catch {
|
|
// Ignore malformed JSON content
|
|
}
|
|
|
|
return {
|
|
eventId: event.id,
|
|
pubkey: event.pubkey,
|
|
createdAt: event.created_at,
|
|
createdAtIso: new Date(event.created_at * 1000).toISOString(),
|
|
expiresAt,
|
|
expiresAtIso: expiresAt ? new Date(expiresAt * 1000).toISOString() : null,
|
|
dTag,
|
|
agent: content.agent ?? null,
|
|
cadence: content.cadence ?? null,
|
|
status: content.status ?? null,
|
|
date: content.date ?? null,
|
|
platform: content.platform ?? null,
|
|
};
|
|
}
|
|
|
|
function filterActiveLocks(locks) {
|
|
const now = nowUnix();
|
|
return locks.filter((lock) => !lock.expiresAt || lock.expiresAt > now);
|
|
}
|
|
|
|
function withTimeout(promise, timeoutMs, timeoutMessage) {
|
|
let timeoutHandle;
|
|
const timeoutPromise = new Promise((_, reject) => {
|
|
timeoutHandle = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
|
|
});
|
|
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
});
|
|
}
|
|
|
|
function relayListLabel(relays) {
|
|
return relays.join(', ');
|
|
}
|
|
|
|
export async function queryLocks(relays, cadence, dateStr, namespace, deps = {}) {
|
|
const {
|
|
poolFactory = () => new SimplePool(),
|
|
getQueryTimeoutMsFn = getQueryTimeoutMs,
|
|
getRelayFallbacksFn = getRelayFallbacks,
|
|
errorLogger = console.error,
|
|
} = deps;
|
|
|
|
const pool = poolFactory();
|
|
const tagFilter = `${namespace}-lock-${cadence}-${dateStr}`;
|
|
const queryTimeoutMs = getQueryTimeoutMsFn();
|
|
const fallbackRelays = getRelayFallbacksFn();
|
|
|
|
const runQuery = async (relaySet, phase) => {
|
|
try {
|
|
const events = await withTimeout(
|
|
pool.querySync(relaySet, {
|
|
kinds: [KIND_APP_DATA],
|
|
'#t': [tagFilter],
|
|
}),
|
|
queryTimeoutMs,
|
|
`[${phase}] Relay query timed out after ${queryTimeoutMs}ms (relays: ${relayListLabel(relaySet)})`,
|
|
);
|
|
return filterActiveLocks(events.map(parseLockEvent));
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
throw new Error(
|
|
`[${phase}] Relay query failed (timeout=${queryTimeoutMs}ms, relays=${relayListLabel(relaySet)}): ${message}`,
|
|
{ cause: err },
|
|
);
|
|
}
|
|
};
|
|
|
|
try {
|
|
try {
|
|
return await runQuery(relays, 'query:primary');
|
|
} catch (primaryErr) {
|
|
if (!fallbackRelays.length) {
|
|
throw primaryErr;
|
|
}
|
|
errorLogger(`WARN: ${primaryErr.message}`);
|
|
errorLogger(`WARN: retrying query with fallback relays (${relayListLabel(fallbackRelays)})`);
|
|
return await runQuery(fallbackRelays, 'query:fallback');
|
|
}
|
|
} finally {
|
|
const allRelays = [...new Set([...relays, ...fallbackRelays])];
|
|
pool.close(allRelays);
|
|
}
|
|
}
|
|
|
|
async function publishToRelays(pool, relays, event, publishTimeoutMs, phase) {
|
|
const publishPromises = pool.publish(relays, event);
|
|
const settled = await Promise.allSettled(
|
|
publishPromises.map((publishPromise, index) => withTimeout(
|
|
publishPromise,
|
|
publishTimeoutMs,
|
|
`[${phase}] Publish timed out after ${publishTimeoutMs}ms (relay=${relays[index]})`,
|
|
)),
|
|
);
|
|
|
|
return settled.map((result, index) => {
|
|
if (result.status === 'fulfilled') {
|
|
return { relay: relays[index], success: true, phase };
|
|
}
|
|
const reason = result.reason;
|
|
const message = reason instanceof Error ? reason.message : String(reason ?? 'unknown');
|
|
return { relay: relays[index], success: false, phase, message };
|
|
});
|
|
}
|
|
|
|
export async function publishLock(relays, event, deps = {}) {
|
|
const {
|
|
poolFactory = () => new SimplePool(),
|
|
getPublishTimeoutMsFn = getPublishTimeoutMs,
|
|
getMinSuccessfulRelayPublishesFn = getMinSuccessfulRelayPublishes,
|
|
getRelayFallbacksFn = getRelayFallbacks,
|
|
} = deps;
|
|
|
|
const pool = poolFactory();
|
|
const publishTimeoutMs = getPublishTimeoutMsFn();
|
|
const minSuccesses = getMinSuccessfulRelayPublishesFn();
|
|
const fallbackRelays = getRelayFallbacksFn().filter((relay) => !relays.includes(relay));
|
|
|
|
const attempted = new Set();
|
|
const publishResults = [];
|
|
|
|
const attemptPhase = async (phaseRelays, phaseName) => {
|
|
if (!phaseRelays.length) return;
|
|
for (const relay of phaseRelays) {
|
|
attempted.add(relay);
|
|
}
|
|
const phaseResults = await publishToRelays(pool, phaseRelays, event, publishTimeoutMs, phaseName);
|
|
publishResults.push(...phaseResults);
|
|
};
|
|
|
|
try {
|
|
await attemptPhase(relays, 'publish:primary');
|
|
|
|
let successCount = publishResults.filter((result) => result.success).length;
|
|
if (successCount < minSuccesses && fallbackRelays.length > 0) {
|
|
await attemptPhase(fallbackRelays, 'publish:fallback');
|
|
successCount = publishResults.filter((result) => result.success).length;
|
|
}
|
|
|
|
if (successCount < minSuccesses) {
|
|
const failureLines = publishResults
|
|
.filter((result) => !result.success)
|
|
.map((result) => `${result.relay} (${result.phase}): ${result.message}`);
|
|
|
|
throw new Error(
|
|
`Failed relay publish quorum in publish phase: ${successCount}/${attempted.size} successful ` +
|
|
`(required=${minSuccesses}, timeout=${publishTimeoutMs}ms)\n ${failureLines.join('\n ')}`,
|
|
);
|
|
}
|
|
|
|
console.error(
|
|
` Published to ${successCount}/${attempted.size} relays ` +
|
|
`(required=${minSuccesses}, timeout=${publishTimeoutMs}ms)`,
|
|
);
|
|
return event;
|
|
} finally {
|
|
pool.close([...attempted]);
|
|
}
|
|
}
|