Kind 17375 is a replaceable event (NIP-60, d-tag=''), so publishing a new one automatically supersedes the old one on relays. The republish now only: 1. ensureDirectNutzapP2pk() - generate privkey if missing 2. Build NIP-60 tag-array payload 3. NIP-44 encrypt + sign + publish single 17375 event That's 1 signing round-trip instead of 100+. Reduced timeout from 120s back to 30s. Bumped WORKER_REVISION to nip60-lightweight-republish-1.
1556 lines
52 KiB
JavaScript
1556 lines
52 KiB
JavaScript
/**
|
|
* NDK Initialization Module
|
|
* Shared by all pages for authentication and worker connection
|
|
*/
|
|
|
|
import {
|
|
configureMuteList,
|
|
loadMuteList as loadMuteListCore,
|
|
isEventMuted,
|
|
addMute as addMuteCore,
|
|
removeMute as removeMuteCore,
|
|
getMuteList,
|
|
getMuteCount,
|
|
onMuteListChanged,
|
|
isMuteListLoaded
|
|
} from './mute-list.mjs';
|
|
|
|
let VERSION = "0.0.1";
|
|
let versionData = null;
|
|
let versionLoaded = false;
|
|
|
|
// Load version asynchronously
|
|
const versionPromise = (async () => {
|
|
try {
|
|
const versionResponse = await fetch('./js/version.json');
|
|
versionData = await versionResponse.json();
|
|
VERSION = versionData.VERSION;
|
|
versionLoaded = true;
|
|
console.log(`[init-ndk] Loaded version: ${VERSION}`);
|
|
} catch (error) {
|
|
console.warn('[init-ndk] Could not load version.json, using default:', error);
|
|
versionLoaded = true;
|
|
}
|
|
})();
|
|
|
|
let ndkWorker = null;
|
|
let requestCounter = 0;
|
|
let pendingRequests = new Map();
|
|
const ANON_WORKER_PUBKEY = '0'.repeat(64);
|
|
let muteListBootstrapStarted = false;
|
|
let muteListBootstrapPromise = null;
|
|
|
|
function normalizeHexPubkey(value) {
|
|
const normalized = String(value || '').trim().toLowerCase();
|
|
return /^[a-f0-9]{64}$/.test(normalized) ? normalized : '';
|
|
}
|
|
|
|
async function getPublicKeyWithTimeout(timeoutMs = 7000) {
|
|
if (!window.nostr || typeof window.nostr.getPublicKey !== 'function') {
|
|
throw new Error('window.nostr.getPublicKey unavailable');
|
|
}
|
|
|
|
return await Promise.race([
|
|
window.nostr.getPublicKey(),
|
|
new Promise((_, reject) => {
|
|
setTimeout(() => reject(new Error(`getPublicKey timeout after ${timeoutMs}ms`)), timeoutMs);
|
|
})
|
|
]);
|
|
}
|
|
|
|
async function getExistingPubkeyNoPrompt() {
|
|
if (!window.nostr || typeof window.nostr.getPublicKey !== 'function') return '';
|
|
try {
|
|
const pubkey = await getPublicKeyWithTimeout(2500);
|
|
return normalizeHexPubkey(pubkey);
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function buildMuteListEncryptor() {
|
|
return async (recipientPubkey, plaintext) => {
|
|
if (window?.nostr?.nip44?.encrypt) {
|
|
return await window.nostr.nip44.encrypt(recipientPubkey, plaintext);
|
|
}
|
|
if (window?.nostr?.nip04?.encrypt) {
|
|
return await window.nostr.nip04.encrypt(recipientPubkey, plaintext);
|
|
}
|
|
throw new Error('No encryption support available in signer');
|
|
};
|
|
}
|
|
|
|
function buildMuteListDecryptor() {
|
|
return async (senderPubkey, ciphertext) => {
|
|
const content = String(ciphertext || '');
|
|
if (content.includes('?iv=')) {
|
|
if (!window?.nostr?.nip04?.decrypt) throw new Error('NIP-04 decrypt unavailable');
|
|
return await window.nostr.nip04.decrypt(senderPubkey, content);
|
|
}
|
|
if (window?.nostr?.nip44?.decrypt) {
|
|
return await window.nostr.nip44.decrypt(senderPubkey, content);
|
|
}
|
|
if (window?.nostr?.nip04?.decrypt) {
|
|
return await window.nostr.nip04.decrypt(senderPubkey, content);
|
|
}
|
|
throw new Error('No decryption support available in signer');
|
|
};
|
|
}
|
|
|
|
async function syncWorkerMuteList() {
|
|
try {
|
|
if (!ndkWorker) return false;
|
|
await sendWorkerRequest('syncMuteList', {}, 10000, 'syncMuteList timeout');
|
|
return true;
|
|
} catch (error) {
|
|
console.warn('[init-ndk] Worker mute-list sync failed:', error?.message || error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function bootstrapMuteList({ forceReload = false } = {}) {
|
|
configureMuteList({
|
|
ndkFetchEvents,
|
|
queryCache,
|
|
publishEvent,
|
|
getPubkey,
|
|
nip44Encrypt: buildMuteListEncryptor(),
|
|
nip44Decrypt: buildMuteListDecryptor()
|
|
});
|
|
|
|
if (forceReload) {
|
|
muteListBootstrapStarted = false;
|
|
muteListBootstrapPromise = null;
|
|
}
|
|
|
|
if (muteListBootstrapStarted && muteListBootstrapPromise) {
|
|
return muteListBootstrapPromise;
|
|
}
|
|
|
|
muteListBootstrapStarted = true;
|
|
muteListBootstrapPromise = (async () => {
|
|
try {
|
|
await loadMuteListCore();
|
|
await syncWorkerMuteList();
|
|
return true;
|
|
} catch (error) {
|
|
console.warn('[init-ndk] Mute-list bootstrap failed:', error?.message || error);
|
|
return false;
|
|
}
|
|
})();
|
|
|
|
return muteListBootstrapPromise;
|
|
}
|
|
|
|
/**
|
|
* Initialize NDK for any page - handles authentication automatically
|
|
* This function can be called from any page (index.html, profile.html, etc.)
|
|
*
|
|
* Options:
|
|
* - allowUnauthenticated: initialize worker without forcing login modal
|
|
* - publicPubkey: optional fallback pubkey for unauthenticated worker init
|
|
*/
|
|
export async function initNDKPage(options = {}) {
|
|
await versionPromise;
|
|
console.log(`[init-ndk v${VERSION}] Initializing NDK page...`);
|
|
|
|
const {
|
|
allowUnauthenticated = false,
|
|
publicPubkey = ''
|
|
} = options || {};
|
|
|
|
let authenticated = false;
|
|
let initPubkey = '';
|
|
|
|
if (allowUnauthenticated) {
|
|
initPubkey = await getExistingPubkeyNoPrompt();
|
|
authenticated = Boolean(initPubkey);
|
|
|
|
if (!initPubkey) {
|
|
initPubkey = normalizeHexPubkey(publicPubkey) || ANON_WORKER_PUBKEY;
|
|
console.log('[init-ndk] Optional auth mode: continuing without login');
|
|
}
|
|
} else {
|
|
authenticated = await ensureAuthenticated();
|
|
if (!authenticated) {
|
|
console.log('[init-ndk] Not authenticated - launching nostr-login-lite');
|
|
throw new Error("Authentication required");
|
|
}
|
|
initPubkey = await getPublicKeyWithTimeout(10000);
|
|
authenticated = true;
|
|
}
|
|
|
|
console.log('[init-ndk] Initializing NDK worker with pubkey context');
|
|
|
|
// All pages in www/ directory, so worker path is always the same.
|
|
// Include explicit worker revision token so updated SharedWorker code is guaranteed to restart.
|
|
const WORKER_REVISION = 'nip60-lightweight-republish-1';
|
|
const workerPath = `./ndk-worker.js?v=${encodeURIComponent(VERSION)}&wr=${encodeURIComponent(WORKER_REVISION)}`;
|
|
|
|
// Initialize NDK SharedWorker (shared across all tabs/pages)
|
|
// Don't use type: 'module' - the worker uses importScripts()
|
|
ndkWorker = new SharedWorker(workerPath);
|
|
|
|
// Set up message handlers BEFORE sending init message to ensure we don't miss the ready response
|
|
ndkWorker.port.onmessage = handleWorkerMessage;
|
|
// Explicitly start the SharedWorker port to ensure message delivery across browsers.
|
|
ndkWorker.port.start();
|
|
|
|
// Send pubkey context to worker
|
|
const pubkey = initPubkey;
|
|
|
|
// Wait for the worker to signal it's ready before returning
|
|
const initPromise = new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => {
|
|
cleanup();
|
|
console.error('[init-ndk] Worker initialization timeout - did not receive ready signal');
|
|
reject(new Error('Worker initialization timeout'));
|
|
}, 30000); // 30 second timeout
|
|
|
|
const cleanup = () => {
|
|
clearTimeout(timeout);
|
|
window.removeEventListener('ndkReady', readyHandler);
|
|
window.removeEventListener('ndkInitError', errorHandler);
|
|
};
|
|
|
|
// Listen for the ndkReady event dispatched by handleWorkerMessage
|
|
const readyHandler = () => {
|
|
cleanup();
|
|
console.log('[init-ndk] Worker signaled ready, initialization complete');
|
|
tryRegisterActiveSigningPort();
|
|
resolve();
|
|
};
|
|
|
|
const errorHandler = (evt) => {
|
|
cleanup();
|
|
const message = evt?.detail?.message || 'Worker initialization failed';
|
|
console.error('[init-ndk] Worker reported initialization error:', message);
|
|
reject(new Error(message));
|
|
};
|
|
|
|
window.addEventListener('ndkReady', readyHandler);
|
|
window.addEventListener('ndkInitError', errorHandler);
|
|
});
|
|
|
|
ndkWorker.port.postMessage({
|
|
type: 'init',
|
|
pubkey: pubkey
|
|
});
|
|
|
|
console.log('[init-ndk] Waiting for worker ready signal...');
|
|
await initPromise;
|
|
|
|
if (authenticated) {
|
|
await bootstrapMuteList();
|
|
}
|
|
|
|
return { authenticated, worker: ndkWorker };
|
|
}
|
|
|
|
/**
|
|
* Ensure user is authenticated - works from any page
|
|
* If not authenticated, launches nostr-login-lite modal
|
|
*/
|
|
async function ensureAuthenticated() {
|
|
// Check if window.nostr exists (from nostr-login-lite or extension)
|
|
if (window.nostr) {
|
|
try {
|
|
await getPublicKeyWithTimeout(4000);
|
|
console.log('[init-ndk] Already authenticated');
|
|
return true;
|
|
} catch (error) {
|
|
console.log('[init-ndk] window.nostr exists but not authenticated/ready:', error.message);
|
|
}
|
|
}
|
|
|
|
// Initialize and launch nostr-login-lite
|
|
if (window.NOSTR_LOGIN_LITE) {
|
|
console.log('[init-ndk] Initializing nostr-login-lite...');
|
|
|
|
// Initialize if not already done
|
|
if (!window.NOSTR_LOGIN_LITE._initialized) {
|
|
await window.NOSTR_LOGIN_LITE.init({
|
|
methods: {
|
|
extension: true,
|
|
local: true,
|
|
nip46: true,
|
|
seedphrase: true,
|
|
nsigner: true
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check if auth was restored during init
|
|
if (window.nostr) {
|
|
try {
|
|
const pubkey = await getPublicKeyWithTimeout(4000);
|
|
console.log('[init-ndk] Auth already restored, pubkey:', pubkey);
|
|
return true;
|
|
} catch (error) {
|
|
console.log('[init-ndk] Auth restored but not ready, will launch modal:', error.message);
|
|
}
|
|
}
|
|
|
|
console.log('[init-ndk] Launching nostr-login-lite...');
|
|
await window.NOSTR_LOGIN_LITE.launch();
|
|
|
|
// Wait for authentication
|
|
return new Promise((resolve) => {
|
|
let settled = false;
|
|
const cleanup = () => {
|
|
window.removeEventListener('nlAuth', authHandler);
|
|
window.removeEventListener('nlAuthRestored', authHandler);
|
|
window.removeEventListener('nlMethodSelected', authHandler);
|
|
if (authTimeout) clearTimeout(authTimeout);
|
|
};
|
|
|
|
const finish = (result) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
cleanup();
|
|
resolve(result);
|
|
};
|
|
|
|
const authHandler = async (event) => {
|
|
console.log('[init-ndk] Received auth event:', event.type);
|
|
|
|
// Give window.nostr a moment to be ready
|
|
await new Promise(r => setTimeout(r, 100));
|
|
|
|
if (window.nostr) {
|
|
try {
|
|
const pubkey = await getPublicKeyWithTimeout(4000);
|
|
console.log('[init-ndk] Authentication successful, pubkey:', pubkey);
|
|
tryRegisterActiveSigningPort();
|
|
finish(true);
|
|
} catch (error) {
|
|
console.log('[init-ndk] window.nostr not ready yet:', error.message);
|
|
// Not ready yet, wait for next event
|
|
}
|
|
}
|
|
};
|
|
|
|
const authTimeout = setTimeout(() => {
|
|
console.error('[init-ndk] Authentication wait timeout');
|
|
finish(false);
|
|
}, 30000);
|
|
|
|
window.addEventListener('nlAuth', authHandler);
|
|
window.addEventListener('nlAuthRestored', authHandler);
|
|
window.addEventListener('nlMethodSelected', authHandler);
|
|
window.addEventListener('nlAuthStateChanged', authHandler);
|
|
});
|
|
}
|
|
|
|
console.error('[init-ndk] nostr-login-lite not available');
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Handle messages from NDK worker
|
|
*/
|
|
function handleWorkerMessage(event) {
|
|
const message = event.data;
|
|
// console.log('[init-ndk] Received from worker:', message.type);
|
|
|
|
// Handle different message types
|
|
if (message.type === 'ready') {
|
|
console.log('[init-ndk] Worker ready');
|
|
window.dispatchEvent(new CustomEvent('ndkReady'));
|
|
} else if (message.type === 'error') {
|
|
const detail = message?.data || {};
|
|
console.error('[init-ndk] Worker error:', detail.message || detail);
|
|
window.dispatchEvent(new CustomEvent('ndkInitError', { detail }));
|
|
} else if (message.type === 'event') {
|
|
// Broadcast event to page
|
|
window.dispatchEvent(new CustomEvent('ndkEvent', { detail: message.data }));
|
|
} else if (message.type === 'eose') {
|
|
// EOSE message has subId directly on message, not in data
|
|
window.dispatchEvent(new CustomEvent('ndkEose', { detail: { subId: message.subId } }));
|
|
} else if (message.type === 'profile') {
|
|
window.dispatchEvent(new CustomEvent('ndkProfile', { detail: message.data }));
|
|
} else if (message.type === 'relays') {
|
|
window.dispatchEvent(new CustomEvent('ndkRelays', { detail: message.data }));
|
|
} else if (message.type === 'muteListUpdated') {
|
|
window.dispatchEvent(new CustomEvent('ndkMuteListUpdated', { detail: message.data || {} }));
|
|
} else if (message.type === 'startupStatus') {
|
|
window.dispatchEvent(new CustomEvent('ndkStartupStatus', { detail: message.data || {} }));
|
|
} else if (message.type === 'relayEvent') {
|
|
// Broadcast relay event for debugging
|
|
window.dispatchEvent(new CustomEvent('ndkRelayEvent', {
|
|
detail: {
|
|
relayUrl: message.relayUrl,
|
|
event: message.event,
|
|
side: message.side,
|
|
data: message.data,
|
|
timestamp: message.timestamp
|
|
}
|
|
}));
|
|
} else if (message.type === 'relayActivity') {
|
|
// Broadcast relay activity to page
|
|
window.dispatchEvent(new CustomEvent('ndkRelayActivity', {
|
|
detail: {
|
|
relayUrl: message.relayUrl,
|
|
activity: message.activity,
|
|
stats: message.stats
|
|
}
|
|
}));
|
|
} else if (message.type === 'signRequest') {
|
|
// Handle signing request from worker
|
|
handleSignRequest(message.requestId, message.method, message.params);
|
|
} else if (message.type === 'fetchEventsResult') {
|
|
// Handle fetchEvents response for single relay
|
|
const pending = pendingRequests.get(message.requestId);
|
|
if (pending) {
|
|
pendingRequests.delete(message.requestId);
|
|
if (message.error) {
|
|
console.log('[init-ndk] Resolving with error:', message.error);
|
|
pending.reject(new Error(message.error));
|
|
} else {
|
|
pending.resolve(message.events);
|
|
}
|
|
}
|
|
// No warning for missing pending — relay responses arriving after timeout are expected
|
|
} else if (message.type === 'queryCacheResult') {
|
|
// Handle cache query response
|
|
const pending = pendingRequests.get(message.requestId);
|
|
if (pending) {
|
|
pendingRequests.delete(message.requestId);
|
|
if (message.error) {
|
|
pending.resolve([]); // Resolve with empty array on error
|
|
} else {
|
|
pending.resolve(message.events || []);
|
|
}
|
|
}
|
|
} else if (message.type === 'ndkFetchEventsResult') {
|
|
// Handle ndkFetchEvents response
|
|
const pending = pendingRequests.get(message.requestId);
|
|
if (pending) {
|
|
pendingRequests.delete(message.requestId);
|
|
if (message.error) {
|
|
pending.reject(new Error(message.error));
|
|
} else {
|
|
pending.resolve(message.events || []);
|
|
}
|
|
}
|
|
} else if (message.type === 'fetchCachedProfileResult') {
|
|
// Handle cached profile response
|
|
const pending = pendingRequests.get(message.requestId);
|
|
if (pending) {
|
|
pendingRequests.delete(message.requestId);
|
|
pending.resolve(message.profile); // Resolve with profile (could be null)
|
|
}
|
|
} else if (message.type === 'userSettingsResult') {
|
|
const pending = pendingRequests.get(message.requestId);
|
|
if (pending) {
|
|
pendingRequests.delete(message.requestId);
|
|
if (message.error) {
|
|
pending.reject(new Error(message.error));
|
|
} else {
|
|
pending.resolve(message.settings || {});
|
|
}
|
|
}
|
|
} else if (message.type === 'userSettingsUpdated') {
|
|
window.dispatchEvent(new CustomEvent('ndkUserSettings', { detail: message.data || {} }));
|
|
} else if (message.type === 'walletStatusChanged') {
|
|
window.dispatchEvent(new CustomEvent('ndkWalletStatus', { detail: message.data || {} }));
|
|
} else if (message.type === 'walletBalanceUpdated') {
|
|
window.dispatchEvent(new CustomEvent('ndkWalletBalance', { detail: message.data || {} }));
|
|
} else if (message.type === 'walletDepositConfirmed') {
|
|
window.dispatchEvent(new CustomEvent('ndkWalletDepositConfirmed', { detail: message.data || {} }));
|
|
} else if (message.type === 'walletSendRedeemed') {
|
|
console.log('[init-ndk] walletSendRedeemed received from worker', message.data || {});
|
|
window.dispatchEvent(new CustomEvent('ndkWalletSendRedeemed', { detail: message.data || {} }));
|
|
} else if (message.type === 'walletTransactionReceived') {
|
|
window.dispatchEvent(new CustomEvent('ndkWalletTransaction', { detail: message.data || {} }));
|
|
} else if (message.type === 'response') {
|
|
// Handle request/response pattern
|
|
const pending = pendingRequests.get(message.requestId);
|
|
if (pending) {
|
|
pendingRequests.delete(message.requestId);
|
|
if (message.error) {
|
|
pending.reject(new Error(message.error));
|
|
} else {
|
|
pending.resolve(message.data);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function isExpectedSignerCapabilityError(method, error) {
|
|
const msg = String(error?.message || error || '').toLowerCase();
|
|
if (!msg) return false;
|
|
if (method === 'nip44Decrypt' && msg.includes('nip-44 decryption not supported')) return true;
|
|
if (method === 'nip44Encrypt' && msg.includes('nip-44 encryption not supported')) return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Handle sign request from worker
|
|
*/
|
|
function tryRegisterActiveSigningPort() {
|
|
if (!ndkWorker?.port) return;
|
|
try {
|
|
const auth = window?.nostr?.authState || window?.NOSTR_LOGIN_LITE?.getAuthState?.() || null;
|
|
if (auth?.method === 'nsigner' && auth?.signer?.driver) {
|
|
ndkWorker.port.postMessage({ type: 'setActiveSigningPort' });
|
|
}
|
|
} catch (_) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
async function handleSignRequest(requestId, method, params) {
|
|
try {
|
|
let result;
|
|
|
|
if (method === 'getPubkey') {
|
|
result = await window.nostr.getPublicKey();
|
|
} else if (method === 'signEvent') {
|
|
result = await window.nostr.signEvent(params.event);
|
|
} else if (method === 'encrypt') {
|
|
if (window.nostr.nip04 && window.nostr.nip04.encrypt) {
|
|
result = { ciphertext: await window.nostr.nip04.encrypt(params.recipientPubkey, params.plaintext) };
|
|
} else {
|
|
throw new Error('NIP-04 encryption not supported');
|
|
}
|
|
} else if (method === 'decrypt') {
|
|
if (window.nostr.nip04 && window.nostr.nip04.decrypt) {
|
|
result = { plaintext: await window.nostr.nip04.decrypt(params.senderPubkey, params.ciphertext) };
|
|
} else {
|
|
throw new Error('NIP-04 decryption not supported');
|
|
}
|
|
} else if (method === 'nip44Encrypt') {
|
|
if (window.nostr?.nip44?.encrypt) {
|
|
result = { ciphertext: await window.nostr.nip44.encrypt(params.recipientPubkey, params.plaintext) };
|
|
} else {
|
|
throw new Error('NIP-44 encryption not supported');
|
|
}
|
|
} else if (method === 'nip44Decrypt') {
|
|
if (window.nostr?.nip44?.decrypt) {
|
|
result = { plaintext: await window.nostr.nip44.decrypt(params.senderPubkey, params.ciphertext) };
|
|
} else {
|
|
throw new Error('NIP-44 decryption not supported');
|
|
}
|
|
} else {
|
|
throw new Error(`Unknown sign method: ${method}`);
|
|
}
|
|
|
|
// Send response back to worker
|
|
ndkWorker.port.postMessage({
|
|
type: 'signResponse',
|
|
requestId: requestId,
|
|
result: result
|
|
});
|
|
|
|
// If this tab successfully signs using hardware signer, pin worker routing to this tab.
|
|
tryRegisterActiveSigningPort();
|
|
|
|
} catch (error) {
|
|
if (isExpectedSignerCapabilityError(method, error)) {
|
|
console.warn(`[init-ndk] Sign request skipped (${method}):`, error?.message || error);
|
|
} else {
|
|
console.error('[init-ndk] Sign request failed:', error);
|
|
}
|
|
ndkWorker.port.postMessage({
|
|
type: 'signResponse',
|
|
requestId: requestId,
|
|
error: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to create subscriptions
|
|
*/
|
|
export function subscribe(filters, opts = {}) {
|
|
if (!ndkWorker) {
|
|
throw new Error('NDK worker not initialized. Call initNDKPage() first.');
|
|
}
|
|
|
|
const subId = `sub_${Date.now()}_${++requestCounter}`;
|
|
|
|
ndkWorker.port.postMessage({
|
|
type: 'subscribe',
|
|
subId: subId,
|
|
filters: filters,
|
|
opts: opts
|
|
});
|
|
|
|
console.log('[init-ndk] Created subscription:', subId, filters);
|
|
|
|
// Return subscription handle
|
|
return {
|
|
subId: subId,
|
|
close: () => {
|
|
ndkWorker.port.postMessage({
|
|
type: 'unsubscribe',
|
|
subId: subId
|
|
});
|
|
console.log('[init-ndk] Closed subscription:', subId);
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Helper to publish events
|
|
*/
|
|
export async function publishEvent(event) {
|
|
if (!ndkWorker) {
|
|
throw new Error('NDK worker not initialized. Call initNDKPage() first.');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const requestId = `pub_${Date.now()}_${++requestCounter}`;
|
|
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
|
|
// Set timeout
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('Publish timeout'));
|
|
}
|
|
}, 30000);
|
|
|
|
ndkWorker.port.postMessage({
|
|
type: 'publish',
|
|
requestId: requestId,
|
|
event: event
|
|
});
|
|
|
|
console.log('[init-ndk] Publishing event:', event.kind);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Helper to publish pre-signed raw events without worker-side signing.
|
|
*/
|
|
export async function publishRawEvent(event) {
|
|
if (!ndkWorker) {
|
|
throw new Error('NDK worker not initialized. Call initNDKPage() first.');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const requestId = `pub_raw_${Date.now()}_${++requestCounter}`;
|
|
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
|
|
// Set timeout
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('Publish raw timeout'));
|
|
}
|
|
}, 30000);
|
|
|
|
ndkWorker.port.postMessage({
|
|
type: 'publishRaw',
|
|
requestId: requestId,
|
|
event: event
|
|
});
|
|
|
|
console.log('[init-ndk] Publishing raw event:', event.kind);
|
|
});
|
|
}
|
|
|
|
export async function publishRawEventToRelay(event, relayUrl) {
|
|
if (!ndkWorker) {
|
|
throw new Error('NDK worker not initialized. Call initNDKPage() first.');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const requestId = `pub_raw_relay_${Date.now()}_${++requestCounter}`;
|
|
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('Publish raw to relay timeout'));
|
|
}
|
|
}, 30000);
|
|
|
|
ndkWorker.port.postMessage({
|
|
type: 'publishRawToRelay',
|
|
requestId,
|
|
event,
|
|
relayUrl
|
|
});
|
|
|
|
console.log('[init-ndk] Publishing raw event to relay:', relayUrl, 'kind:', event?.kind);
|
|
});
|
|
}
|
|
|
|
export function sendNip17Message(recipientPubkey, content) {
|
|
return sendWorkerRequest(
|
|
'sendNip17Message',
|
|
{
|
|
recipientPubkey: String(recipientPubkey || '').trim(),
|
|
content: String(content || '')
|
|
},
|
|
30000,
|
|
'sendNip17Message timeout'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Fetch events from a single relay
|
|
* Returns events from that specific relay
|
|
* @param {Object} filters - Nostr filters
|
|
* @param {string} relayUrl - The relay URL to fetch from
|
|
*/
|
|
export async function fetchEvents(filters, relayUrl) {
|
|
if (!ndkWorker) {
|
|
throw new Error('NDK worker not initialized. Call initNDKPage() first.');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
requestCounter++;
|
|
const requestId = `fetch_${Date.now()}_${requestCounter}`;
|
|
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
|
|
// Set timeout
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('Fetch events timeout'));
|
|
}
|
|
}, 10000);
|
|
|
|
ndkWorker.port.postMessage({
|
|
type: 'fetchEvents',
|
|
requestId: requestId,
|
|
filters: filters,
|
|
relayUrl: relayUrl
|
|
});
|
|
|
|
// console.log('[init-ndk] Fetching events from', relayUrl, ':', filters);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetch events using NDK's internal routing and caching
|
|
* Returns a flat, deduplicated array of raw events
|
|
* @param {Object} filters - Nostr filters
|
|
* @returns {Promise<Array>} - Flat array of events
|
|
*/
|
|
export async function ndkFetchEvents(filters) {
|
|
if (!ndkWorker) {
|
|
throw new Error('NDK worker not initialized. Call initNDKPage() first.');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
requestCounter++;
|
|
const requestId = `ndkFetch_${Date.now()}_${requestCounter}`;
|
|
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
|
|
// Set timeout
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('ndkFetchEvents timeout'));
|
|
}
|
|
}, 15000);
|
|
|
|
ndkWorker.port.postMessage({
|
|
type: 'ndkFetchEvents',
|
|
requestId: requestId,
|
|
filters: filters
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetch events from all relays in parallel
|
|
* Returns events grouped by relay URL
|
|
* @param {Object} filters - Nostr filters
|
|
* @param {Object} opts - Options
|
|
* @param {Function} opts.onProgress - Callback for incremental progress (relayUrl, events, completed, total)
|
|
* @returns {Object} eventsByRelay - Object mapping relay URLs to arrays of events
|
|
*/
|
|
export async function fetchEventsFromAllRelays(filters, opts = {}) {
|
|
if (!ndkWorker) {
|
|
throw new Error('NDK worker not initialized. Call initNDKPage() first.');
|
|
}
|
|
|
|
// For backward compatibility (especially event.html), we still return an object mapping relay URL to events.
|
|
// However, we now use NDK's internal routing to get the events.
|
|
// Note: NDK fetchEvents deduplicates across relays, so we can't perfectly attribute events to specific relays
|
|
// in the same way the old raw WebSocket fetch did. We'll put all events under a 'combined' key or similar
|
|
// if we can't get per-relay attribution from NDK easily.
|
|
|
|
try {
|
|
const events = await ndkFetchEvents(filters);
|
|
|
|
// Return in the format expected by existing callers: { "ndk": [events] }
|
|
// Most callers do Object.values(result).flat() so this works perfectly.
|
|
return { "ndk": events || [] };
|
|
} catch (error) {
|
|
console.warn('[init-ndk] fetchEventsFromAllRelays (via NDK) failed:', error);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Query the local cache for events without going to relays
|
|
* Returns cached events immediately from Dexie/IndexedDB
|
|
* @param {Object} filters - Nostr filters (kinds, authors, etc.)
|
|
* @returns {Array} Array of cached events
|
|
*/
|
|
export async function queryCache(filters) {
|
|
if (!ndkWorker) {
|
|
throw new Error('NDK worker not initialized. Call initNDKPage() first.');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
requestCounter++;
|
|
const requestId = `cache_${Date.now()}_${requestCounter}`;
|
|
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
|
|
// Set timeout - cache queries should be fast
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('Cache query timeout'));
|
|
}
|
|
}, 5000);
|
|
|
|
ndkWorker.port.postMessage({
|
|
type: 'queryCache',
|
|
requestId: requestId,
|
|
filters: filters
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Purge cached playlist events for one playlist identifier from worker cache/IndexedDB.
|
|
* This only affects local cache storage, not relays.
|
|
*/
|
|
export function purgePlaylistCache({
|
|
playlistIdentifier,
|
|
ownerPubkey,
|
|
playlistKind,
|
|
eventIds = []
|
|
} = {}) {
|
|
const payload = {
|
|
playlistIdentifier: String(playlistIdentifier || '').trim(),
|
|
ownerPubkey: String(ownerPubkey || '').trim(),
|
|
playlistKind: Number(playlistKind || 0),
|
|
eventIds: Array.isArray(eventIds)
|
|
? eventIds.map((id) => String(id || '').trim()).filter(Boolean)
|
|
: []
|
|
};
|
|
|
|
console.log('[init-ndk] purgePlaylistCache request', payload);
|
|
|
|
return sendWorkerRequest(
|
|
'purgePlaylistCache',
|
|
payload,
|
|
15000,
|
|
'purgePlaylistCache timeout'
|
|
).then((result) => {
|
|
console.log('[init-ndk] purgePlaylistCache response', result);
|
|
return result;
|
|
}).catch((error) => {
|
|
console.error('[init-ndk] purgePlaylistCache failed', error);
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Helper to get current user's pubkey
|
|
*/
|
|
export async function getPubkey() {
|
|
if (!window.nostr) {
|
|
throw new Error('Not authenticated');
|
|
}
|
|
return await window.nostr.getPublicKey();
|
|
}
|
|
|
|
/**
|
|
* Helper to get relay data from worker
|
|
*/
|
|
export async function getRelayData() {
|
|
if (!ndkWorker) {
|
|
throw new Error('NDK worker not initialized. Call initNDKPage() first.');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const requestId = `relay_${Date.now()}_${++requestCounter}`;
|
|
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
|
|
// Set timeout
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
// Resolve with empty array instead of rejecting to prevent UI errors
|
|
console.warn('[init-ndk] Get relay data timeout, returning empty array');
|
|
resolve([]);
|
|
}
|
|
}, 5000);
|
|
|
|
ndkWorker.port.postMessage({
|
|
type: 'getRelayData',
|
|
requestId: requestId
|
|
});
|
|
|
|
// console.log('[init-ndk] Requesting relay data');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Helper to get relay activity statistics from worker
|
|
* @returns {Promise<Object>} Object mapping relay URLs to their activity stats
|
|
*/
|
|
export async function getRelayStats() {
|
|
if (!ndkWorker) {
|
|
throw new Error('NDK worker not initialized. Call initNDKPage() first.');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const requestId = `stats_${Date.now()}_${++requestCounter}`;
|
|
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
|
|
// Set timeout
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
// Resolve with empty object instead of rejecting to prevent UI errors
|
|
console.warn('[init-ndk] Get relay stats timeout, returning empty object');
|
|
resolve({});
|
|
}
|
|
}, 5000);
|
|
|
|
ndkWorker.port.postMessage({
|
|
type: 'getRelayStats',
|
|
requestId: requestId
|
|
});
|
|
|
|
// console.log('[init-ndk] Requesting relay stats');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetch a profile from the local cache (instant, no relay fetch)
|
|
* Returns cached profile data immediately from in-memory/Dexie cache
|
|
* @param {string} pubkey - The user's pubkey
|
|
* @returns {Promise<Object|null>} Profile object or null if not in cache
|
|
*/
|
|
export async function fetchCachedProfile(pubkey) {
|
|
if (!ndkWorker) {
|
|
throw new Error('NDK worker not initialized. Call initNDKPage() first.');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
requestCounter++;
|
|
const requestId = `profile_${Date.now()}_${requestCounter}`;
|
|
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
|
|
// Set timeout - cache lookups should be fast
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
resolve(null); // Resolve with null on timeout, don't reject
|
|
}
|
|
}, 1000); // 1 second timeout for cache lookup
|
|
|
|
ndkWorker.port.postMessage({
|
|
type: 'fetchCachedProfile',
|
|
requestId: requestId,
|
|
pubkey: pubkey
|
|
});
|
|
|
|
// console.log('[init-ndk] Requesting cached profile for:', pubkey.substring(0, 8) + '…');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Store a fetched profile in the worker's Dexie cache for future instant lookups.
|
|
* Fire-and-forget — no response needed.
|
|
* @param {string} pubkey - The user's pubkey
|
|
* @param {Object} profile - The profile object { name, display_name, picture, ... }
|
|
*/
|
|
|
|
|
|
/**
|
|
* Inject a login button (same footprint as avatar) into top-right header area.
|
|
* Clicking launches nostr-login-lite.
|
|
*/
|
|
export async function injectHeaderLoginButton() {
|
|
const headerRight = document.getElementById('divHeaderFlexRight');
|
|
if (!headerRight) {
|
|
console.warn('[init-ndk] injectHeaderLoginButton: #divHeaderFlexRight not found');
|
|
return;
|
|
}
|
|
|
|
if (headerRight.querySelector('.divHeaderAvatarLink') || headerRight.querySelector('.divHeaderLoginBtn')) {
|
|
return;
|
|
}
|
|
|
|
const loginButton = document.createElement('button');
|
|
loginButton.type = 'button';
|
|
loginButton.className = 'divHeaderLoginBtn';
|
|
loginButton.title = 'Login';
|
|
loginButton.setAttribute('aria-label', 'Login with Nostr');
|
|
loginButton.innerHTML = `<svg viewBox="0 0 10 10" class="svgHeaderButtons"><circle stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke="black" stroke-opacity="1" fill="black" fill-opacity="0" r="4" cx="5" cy="10" /><circle stroke-width="2" stroke-linecap="round" stroke-linejoin="round" stroke="black" stroke-opacity="1" fill="black" fill-opacity="0" r="1.4142135623730951" cx="5" cy="3" /></svg>`;
|
|
|
|
loginButton.addEventListener('click', async () => {
|
|
try {
|
|
if (!window.NOSTR_LOGIN_LITE) {
|
|
console.warn('[init-ndk] injectHeaderLoginButton: NOSTR_LOGIN_LITE unavailable');
|
|
return;
|
|
}
|
|
|
|
if (!window.NOSTR_LOGIN_LITE._initialized) {
|
|
await window.NOSTR_LOGIN_LITE.init({
|
|
methods: {
|
|
extension: true,
|
|
local: true,
|
|
nip46: true,
|
|
seedphrase: true,
|
|
nsigner: true
|
|
}
|
|
});
|
|
}
|
|
|
|
await window.NOSTR_LOGIN_LITE.launch();
|
|
} catch (error) {
|
|
console.warn('[init-ndk] injectHeaderLoginButton: failed to launch modal', error);
|
|
}
|
|
});
|
|
|
|
headerRight.appendChild(loginButton);
|
|
}
|
|
|
|
/**
|
|
* Inject signed-in user's avatar into the top-right header area.
|
|
* @param {string} pubkey - Current authenticated user's pubkey
|
|
*/
|
|
export async function injectHeaderAvatar(pubkey) {
|
|
if (!pubkey) {
|
|
console.warn('[init-ndk] injectHeaderAvatar: missing pubkey');
|
|
return;
|
|
}
|
|
|
|
const headerRight = document.getElementById('divHeaderFlexRight');
|
|
if (!headerRight) {
|
|
console.warn('[init-ndk] injectHeaderAvatar: #divHeaderFlexRight not found');
|
|
return;
|
|
}
|
|
|
|
// Avoid duplicate injection
|
|
if (headerRight.querySelector('.divHeaderAvatarLink')) {
|
|
console.log('[init-ndk] injectHeaderAvatar: avatar already injected');
|
|
return;
|
|
}
|
|
|
|
const loginButton = headerRight.querySelector('.divHeaderLoginBtn');
|
|
if (loginButton) {
|
|
loginButton.remove();
|
|
}
|
|
|
|
const shortPubkey = `${pubkey.slice(0, 8)}…`;
|
|
console.log('[init-ndk] injectHeaderAvatar: requesting cached profile for', shortPubkey);
|
|
|
|
const avatarLink = document.createElement('a');
|
|
avatarLink.className = 'divHeaderAvatarLink';
|
|
avatarLink.href = './profile.html';
|
|
avatarLink.title = 'Profile';
|
|
avatarLink.setAttribute('aria-label', 'Open profile');
|
|
|
|
const avatar = document.createElement('img');
|
|
avatar.className = 'divHeaderAvatar';
|
|
avatar.alt = '';
|
|
avatar.loading = 'lazy';
|
|
avatar.decoding = 'async';
|
|
avatar.referrerPolicy = 'no-referrer';
|
|
|
|
const applyPicture = (nextPicture, source = 'unknown') => {
|
|
const resolvedPicture = String(nextPicture || '').trim();
|
|
if (!resolvedPicture) return false;
|
|
|
|
avatar.classList.remove('is-placeholder');
|
|
|
|
avatar.addEventListener('load', () => {
|
|
console.log(`[init-ndk] injectHeaderAvatar: image loaded (${source})`);
|
|
}, { once: true });
|
|
|
|
avatar.addEventListener('error', () => {
|
|
console.warn(`[init-ndk] injectHeaderAvatar: image failed (${source}), using placeholder border`);
|
|
avatar.removeAttribute('src');
|
|
avatar.classList.add('is-placeholder');
|
|
}, { once: true });
|
|
|
|
avatar.src = resolvedPicture;
|
|
return true;
|
|
};
|
|
|
|
let profile = null;
|
|
try {
|
|
profile = await fetchCachedProfile(pubkey);
|
|
console.log('[init-ndk] injectHeaderAvatar: cache response', profile ? 'HIT' : 'MISS');
|
|
} catch (error) {
|
|
console.warn('[init-ndk] injectHeaderAvatar: cache lookup failed', error);
|
|
profile = null;
|
|
}
|
|
|
|
const picture = String(profile?.picture || '').trim();
|
|
console.log('[init-ndk] injectHeaderAvatar: picture present =', Boolean(picture));
|
|
|
|
if (!applyPicture(picture, 'cache')) {
|
|
avatar.classList.add('is-placeholder');
|
|
console.log('[init-ndk] injectHeaderAvatar: no cached picture, rendering placeholder border');
|
|
|
|
let profileListenerTimeout = null;
|
|
const onProfile = (event) => {
|
|
const livePicture = String(event?.detail?.picture || '').trim();
|
|
if (!livePicture) {
|
|
console.log('[init-ndk] injectHeaderAvatar: ndkProfile arrived without picture');
|
|
return;
|
|
}
|
|
|
|
console.log('[init-ndk] injectHeaderAvatar: ndkProfile picture received, updating avatar');
|
|
applyPicture(livePicture, 'ndkProfile');
|
|
window.removeEventListener('ndkProfile', onProfile);
|
|
if (profileListenerTimeout) clearTimeout(profileListenerTimeout);
|
|
};
|
|
|
|
window.addEventListener('ndkProfile', onProfile);
|
|
profileListenerTimeout = setTimeout(() => {
|
|
window.removeEventListener('ndkProfile', onProfile);
|
|
console.log('[init-ndk] injectHeaderAvatar: ndkProfile listener expired after 30s');
|
|
}, 30000);
|
|
}
|
|
|
|
avatarLink.appendChild(avatar);
|
|
|
|
// In row-reverse, appending makes this the visual top-right item.
|
|
headerRight.appendChild(avatarLink);
|
|
console.log('[init-ndk] injectHeaderAvatar: avatar element appended');
|
|
}
|
|
|
|
export function storeProfile(pubkey, profile) {
|
|
if (!ndkWorker || !pubkey || !profile) return;
|
|
ndkWorker.port.postMessage({
|
|
type: 'storeProfile',
|
|
pubkey,
|
|
profile
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get all cached profiles from the worker's Dexie cache for pre-warming.
|
|
* @returns {Promise<Array>} Array of profile records from the profiles table
|
|
*/
|
|
export function getAllCachedProfiles() {
|
|
return new Promise((resolve) => {
|
|
if (!ndkWorker) {
|
|
resolve([]);
|
|
return;
|
|
}
|
|
const requestId = `getAllProfiles_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
const handler = (e) => {
|
|
if (e.data.type === 'getAllCachedProfilesResult' && e.data.requestId === requestId) {
|
|
ndkWorker.port.removeEventListener('message', handler);
|
|
resolve(e.data.profiles || []);
|
|
}
|
|
};
|
|
|
|
ndkWorker.port.addEventListener('message', handler);
|
|
ndkWorker.port.postMessage({ type: 'getAllCachedProfiles', requestId });
|
|
|
|
// Timeout after 2 seconds
|
|
setTimeout(() => {
|
|
ndkWorker.port.removeEventListener('message', handler);
|
|
resolve([]);
|
|
}, 2000);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get merged app-wide user settings from worker (cache + relay hydrated).
|
|
* @returns {Promise<Object>}
|
|
*/
|
|
export function getUserSettings() {
|
|
if (!ndkWorker) {
|
|
throw new Error('NDK worker not initialized. Call initNDKPage() first.');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const requestId = `userSettings_get_${Date.now()}_${++requestCounter}`;
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('Get user settings timeout'));
|
|
}
|
|
}, 7000);
|
|
|
|
ndkWorker.port.postMessage({
|
|
type: 'getUserSettings',
|
|
requestId
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Patch app-wide user settings with a partial object.
|
|
* @param {Object} patch
|
|
* @param {Object} options
|
|
* @returns {Promise<Object>}
|
|
*/
|
|
export function patchUserSettings(patch, options = {}) {
|
|
if (!ndkWorker) {
|
|
throw new Error('NDK worker not initialized. Call initNDKPage() first.');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const requestId = `userSettings_patch_${Date.now()}_${++requestCounter}`;
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('Patch user settings timeout'));
|
|
}
|
|
}, 7000);
|
|
|
|
ndkWorker.port.postMessage({
|
|
type: 'patchUserSettings',
|
|
requestId,
|
|
patch,
|
|
options
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Subscribe to worker-broadcast user settings updates.
|
|
* @param {(settings: Object) => void} callback
|
|
* @returns {() => void} unsubscribe
|
|
*/
|
|
export function onUserSettings(callback) {
|
|
const handler = (e) => callback(e.detail || {});
|
|
window.addEventListener('ndkUserSettings', handler);
|
|
return () => window.removeEventListener('ndkUserSettings', handler);
|
|
}
|
|
|
|
/**
|
|
* Replace app-wide user settings document.
|
|
* @param {Object} settings
|
|
* @param {Object} options
|
|
* @returns {Promise<Object>}
|
|
*/
|
|
export function setUserSettings(settings, options = {}) {
|
|
if (!ndkWorker) {
|
|
throw new Error('NDK worker not initialized. Call initNDKPage() first.');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const requestId = `userSettings_set_${Date.now()}_${++requestCounter}`;
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('Set user settings timeout'));
|
|
}
|
|
}, 7000);
|
|
|
|
ndkWorker.port.postMessage({
|
|
type: 'setUserSettings',
|
|
requestId,
|
|
settings,
|
|
options
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Reset app-wide user settings document to defaults.
|
|
* @param {Object} options
|
|
* @returns {Promise<Object>}
|
|
*/
|
|
export function resetUserSettings(options = {}) {
|
|
if (!ndkWorker) {
|
|
throw new Error('NDK worker not initialized. Call initNDKPage() first.');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const requestId = `userSettings_reset_${Date.now()}_${++requestCounter}`;
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('Reset user settings timeout'));
|
|
}
|
|
}, 7000);
|
|
|
|
ndkWorker.port.postMessage({
|
|
type: 'resetUserSettings',
|
|
requestId,
|
|
options
|
|
});
|
|
});
|
|
}
|
|
|
|
function sendWorkerRequest(type, payload = {}, timeoutMs = 15000, timeoutMessage = `${type} timeout`) {
|
|
if (!ndkWorker) {
|
|
throw new Error('NDK worker not initialized. Call initNDKPage() first.');
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const requestId = `${type}_${Date.now()}_${++requestCounter}`;
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error(timeoutMessage));
|
|
}
|
|
}, timeoutMs);
|
|
|
|
ndkWorker.port.postMessage({
|
|
type,
|
|
requestId,
|
|
...payload
|
|
});
|
|
});
|
|
}
|
|
|
|
export function encryptContent(recipientPubkey, plaintext, scheme = 'nip44') {
|
|
return sendWorkerRequest(
|
|
'encryptContent',
|
|
{ recipientPubkey, plaintext: String(plaintext || ''), scheme },
|
|
15000,
|
|
'encryptContent timeout'
|
|
);
|
|
}
|
|
|
|
export function decryptContent(senderPubkey, ciphertext, scheme = 'auto') {
|
|
return sendWorkerRequest(
|
|
'decryptContent',
|
|
{ senderPubkey, ciphertext: String(ciphertext || ''), scheme },
|
|
15000,
|
|
'decryptContent timeout'
|
|
);
|
|
}
|
|
|
|
export function walletInit() {
|
|
return sendWorkerRequest('walletInit', {}, 12000, 'walletInit timeout');
|
|
}
|
|
|
|
export function walletStart(pubkey = null) {
|
|
return sendWorkerRequest('walletStart', { pubkey }, 30000, 'walletStart timeout');
|
|
}
|
|
|
|
export function walletCreate(mints, relays = []) {
|
|
return sendWorkerRequest('walletCreate', { mints, relays }, 45000, 'walletCreate timeout');
|
|
}
|
|
|
|
export function walletHasWallet() {
|
|
return sendWorkerRequest('walletHasWallet', {}, 8000, 'walletHasWallet timeout');
|
|
}
|
|
|
|
export function walletGetBalance() {
|
|
return sendWorkerRequest('walletGetBalance', {}, 12000, 'walletGetBalance timeout');
|
|
}
|
|
|
|
export function walletGetMints() {
|
|
return sendWorkerRequest('walletGetMints', {}, 12000, 'walletGetMints timeout');
|
|
}
|
|
|
|
export function walletReceiveToken(token, description = 'Receive token') {
|
|
return sendWorkerRequest('walletReceiveToken', { token, description }, 30000, 'walletReceiveToken timeout');
|
|
}
|
|
|
|
export function walletSendToken(amount, memo = 'Cashu send', mint = null) {
|
|
return sendWorkerRequest('walletSendToken', { amount, memo, mint }, 30000, 'walletSendToken timeout');
|
|
}
|
|
|
|
export function walletSendNutzap({
|
|
amount,
|
|
memo = '',
|
|
targetPubkey,
|
|
eventId = null,
|
|
mint = null,
|
|
recipientMints = [],
|
|
recipientP2pk = null,
|
|
recipientRelays = []
|
|
} = {}) {
|
|
return sendWorkerRequest(
|
|
'walletSendNutzap',
|
|
{ amount, memo, targetPubkey, eventId, mint, recipientMints, recipientP2pk, recipientRelays },
|
|
45000,
|
|
'walletSendNutzap timeout'
|
|
);
|
|
}
|
|
|
|
export function walletCreateDeposit(amount, mint) {
|
|
return sendWorkerRequest('walletCreateDeposit', { amount, mint }, 30000, 'walletCreateDeposit timeout');
|
|
}
|
|
|
|
export function walletPayInvoice(invoice, mint = null) {
|
|
return sendWorkerRequest('walletPayInvoice', { invoice, mint }, 90000, 'walletPayInvoice timeout');
|
|
}
|
|
|
|
export function walletCheckProofs() {
|
|
return sendWorkerRequest('walletCheckProofs', {}, 45000, 'walletCheckProofs timeout');
|
|
}
|
|
|
|
export function walletAddMint(mintUrl) {
|
|
return sendWorkerRequest('walletAddMint', { mintUrl }, 20000, 'walletAddMint timeout');
|
|
}
|
|
|
|
export function walletRemoveMint(mintUrl) {
|
|
return sendWorkerRequest('walletRemoveMint', { mintUrl }, 20000, 'walletRemoveMint timeout');
|
|
}
|
|
|
|
export function walletGetTransactions() {
|
|
return sendWorkerRequest('walletGetTransactions', {}, 20000, 'walletGetTransactions timeout');
|
|
}
|
|
|
|
export function walletShutdown() {
|
|
return sendWorkerRequest('walletShutdown', {}, 10000, 'walletShutdown timeout');
|
|
}
|
|
|
|
export function walletRepublish() {
|
|
return sendWorkerRequest('walletRepublish', {}, 30000, 'walletRepublish timeout');
|
|
}
|
|
|
|
export function walletPublishMintList(relays = [], receiveMints = []) {
|
|
return sendWorkerRequest('walletPublishMintList', { relays, receiveMints }, 25000, 'walletPublishMintList timeout');
|
|
}
|
|
|
|
export function walletFetchMintList(targetPubkey) {
|
|
return sendWorkerRequest('walletFetchMintList', { targetPubkey }, 12000, 'walletFetchMintList timeout');
|
|
}
|
|
|
|
export function onWalletStatus(callback) {
|
|
const handler = (e) => callback(e.detail || {});
|
|
window.addEventListener('ndkWalletStatus', handler);
|
|
return () => window.removeEventListener('ndkWalletStatus', handler);
|
|
}
|
|
|
|
export function onWalletBalance(callback) {
|
|
const handler = (e) => callback(e.detail || {});
|
|
window.addEventListener('ndkWalletBalance', handler);
|
|
return () => window.removeEventListener('ndkWalletBalance', handler);
|
|
}
|
|
|
|
export function onWalletDepositConfirmed(callback) {
|
|
const handler = (e) => callback(e.detail || {});
|
|
window.addEventListener('ndkWalletDepositConfirmed', handler);
|
|
return () => window.removeEventListener('ndkWalletDepositConfirmed', handler);
|
|
}
|
|
|
|
export function onWalletTransaction(callback) {
|
|
const handler = (e) => callback(e.detail || {});
|
|
window.addEventListener('ndkWalletTransaction', handler);
|
|
return () => window.removeEventListener('ndkWalletTransaction', handler);
|
|
}
|
|
|
|
export function onWalletSendRedeemed(callback) {
|
|
const handler = (e) => callback(e.detail || {});
|
|
window.addEventListener('ndkWalletSendRedeemed', handler);
|
|
return () => window.removeEventListener('ndkWalletSendRedeemed', handler);
|
|
}
|
|
|
|
/**
|
|
* Helper to reconnect a specific relay
|
|
*/
|
|
export function reconnectRelay(relayUrl) {
|
|
if (!ndkWorker) {
|
|
throw new Error('NDK worker not initialized. Call initNDKPage() first.');
|
|
}
|
|
|
|
ndkWorker.port.postMessage({
|
|
type: 'reconnectRelay',
|
|
relayUrl: relayUrl
|
|
});
|
|
|
|
console.log('[init-ndk] Requesting reconnect for relay:', relayUrl);
|
|
}
|
|
|
|
/**
|
|
* Helper to disconnect worker
|
|
*/
|
|
export function disconnect() {
|
|
if (ndkWorker) {
|
|
ndkWorker.port.postMessage({ type: 'disconnect' });
|
|
ndkWorker.port.close();
|
|
ndkWorker = null;
|
|
console.log('[init-ndk] Disconnected from worker');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get worker instance (for advanced usage)
|
|
*/
|
|
export function getWorker() {
|
|
return ndkWorker;
|
|
}
|
|
|
|
/**
|
|
* Get version information
|
|
* Waits for version to be loaded if not already loaded
|
|
* @returns {Promise<Object>} Version data including VERSION, VERSION_NUMBER, and BUILD_DATE
|
|
*/
|
|
export async function getVersion() {
|
|
if (!versionLoaded) {
|
|
await versionPromise;
|
|
}
|
|
return versionData || { VERSION: VERSION, VERSION_NUMBER: VERSION, BUILD_DATE: null };
|
|
}
|
|
|
|
/**
|
|
* Update version display element if it exists
|
|
* Call this after page initialization to update the version display
|
|
*/
|
|
export async function updateVersionDisplay() {
|
|
if (!versionLoaded) {
|
|
await versionPromise;
|
|
}
|
|
const versionDisplay = document.getElementById('versionDisplay');
|
|
if (versionDisplay && VERSION) {
|
|
versionDisplay.textContent = VERSION;
|
|
console.log('[init-ndk] Updated version display to:', VERSION);
|
|
}
|
|
}
|
|
|
|
export async function ensureMuteListLoaded(options = {}) {
|
|
return await bootstrapMuteList(options || {});
|
|
}
|
|
|
|
export async function addMute(type, value, isPrivate = false) {
|
|
await addMuteCore(type, value, isPrivate);
|
|
await syncWorkerMuteList();
|
|
}
|
|
|
|
export async function removeMute(type, value) {
|
|
await removeMuteCore(type, value);
|
|
await syncWorkerMuteList();
|
|
}
|
|
|
|
export {
|
|
isEventMuted,
|
|
getMuteList,
|
|
getMuteCount,
|
|
onMuteListChanged,
|
|
isMuteListLoaded,
|
|
syncWorkerMuteList
|
|
};
|
|
|
|
console.log(`[init-ndk v${VERSION}] Module loaded`);
|