support switching cache relay without reload

This commit is contained in:
hzrd149 2025-01-15 12:27:38 -06:00
parent a957ddb5d2
commit ea15fa1dd1
39 changed files with 321 additions and 433 deletions

View File

@ -1,194 +0,0 @@
import { nanoid } from "nanoid";
import { Filter, NostrEvent } from "nostr-tools";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import { IConnectionPool } from "applesauce-net/connection";
import { isFilterEqual } from "applesauce-core/helpers";
import ControlledObservable from "./controlled-observable";
import PersistentSubscription from "./persistent-subscription";
import Dataflow01 from "../components/icons/dataflow-01";
import processManager from "../services/process-manager";
import { localRelay } from "../services/local-relay";
import { eventStore } from "../services/event-store";
import Process from "./process";
/** @deprecated use MultiSubscription from applesauce-net */
export default class MultiSubscription {
static OPEN = "open";
static CLOSED = "closed";
id: string;
pool: IConnectionPool;
name: string;
process: Process;
filters: Filter[] = [];
relays = new Set<AbstractRelay>();
subscriptions = new Map<AbstractRelay, PersistentSubscription>();
useCache = true;
cacheSubscription: PersistentSubscription | null = null;
onCacheEvent = new ControlledObservable<NostrEvent>();
state = MultiSubscription.CLOSED;
onEvent = new ControlledObservable<NostrEvent>();
seenEvents = new Set<string>();
constructor(pool: IConnectionPool, name: string) {
this.id = nanoid(8);
this.pool = pool;
this.name = name;
this.process = new Process("MultiSubscription", this);
this.process.name = this.name;
this.process.icon = Dataflow01;
processManager.registerProcess(this.process);
}
private handleEvent(event: NostrEvent, fromCache = false) {
if (this.seenEvents.has(event.id)) return;
if (fromCache) this.onCacheEvent.next(event);
else this.onEvent.next(event);
this.seenEvents.add(event.id);
}
setFilters(filters: Filter[]) {
if (isFilterEqual(this.filters, filters)) return;
this.filters = filters;
this.updateSubscriptions();
}
setRelays(relays: Iterable<string | URL | AbstractRelay>) {
const newRelays = Array.from(relays).map((relay) => {
if (typeof relay === "string" || relay instanceof URL) return this.pool.getConnection(relay);
return relay;
});
// remove relays
for (const relay of this.relays) {
if (!newRelays.includes(relay)) {
this.relays.delete(relay);
const sub = this.subscriptions.get(relay);
if (sub) {
sub.destroy();
this.subscriptions.delete(relay);
}
}
}
// add relays
for (const relay of newRelays) {
this.relays.add(relay);
}
this.updateSubscriptions();
}
private updateSubscriptions() {
// close all subscriptions if not open
if (this.state !== MultiSubscription.OPEN) {
for (const [relay, subscription] of this.subscriptions) subscription.close();
this.cacheSubscription?.close();
return;
}
// else open and update subscriptions
for (const relay of this.relays) {
let subscription = this.subscriptions.get(relay);
if (!subscription || !isFilterEqual(subscription.filters, this.filters) || subscription.closed) {
if (!subscription) {
subscription = new PersistentSubscription(relay, {
onevent: (event) => this.handleEvent(eventStore.add(event, relay.url)),
});
this.process.addChild(subscription.process);
this.subscriptions.set(relay, subscription);
}
if (subscription) {
subscription.filters = this.filters;
subscription.update().catch((err) => {
// eat error
});
}
}
}
if (this.useCache) {
// create cache sub if it does not exist
if (!this.cacheSubscription && localRelay) {
this.cacheSubscription = new PersistentSubscription(localRelay as AbstractRelay, {
onevent: (event) => this.handleEvent(eventStore.add(event, localRelay!.url), true),
});
this.process.addChild(this.cacheSubscription.process);
}
// update cache sub filters if they changed
if (
this.cacheSubscription &&
(!isFilterEqual(this.cacheSubscription.filters, this.filters) || this.cacheSubscription.closed)
) {
this.cacheSubscription.filters = this.filters;
this.cacheSubscription.update().catch((err) => {
// eat error
});
}
} else if (this.cacheSubscription?.closed === false) {
this.cacheSubscription.close();
}
}
publish(event: NostrEvent) {
return Promise.allSettled(
Array.from(this.relays).map(async (relay) => {
if (!relay.connected) await relay.connect();
return await relay.publish(event);
}),
);
}
open() {
if (this.state === MultiSubscription.OPEN) return this;
this.state = MultiSubscription.OPEN;
this.updateSubscriptions();
this.process.active = true;
return this;
}
waitForAllConnection(): Promise<void> {
return Promise.allSettled(
Array.from(this.relays)
.filter((r) => !r.connected)
.map((r) => r.connect()),
).then((v) => void 0);
}
close() {
if (this.state !== MultiSubscription.OPEN) return this;
// forget all seen events
this.forgetEvents();
// unsubscribe from relay messages
this.state = MultiSubscription.CLOSED;
this.process.active = false;
// close all
this.updateSubscriptions();
return this;
}
forgetEvents() {
// forget all seen events
this.seenEvents.clear();
}
destroy() {
for (const [relay, sub] of this.subscriptions) {
sub.destroy();
}
this.process.remove();
processManager.unregisterProcess(this.process);
}
}

View File

@ -1,8 +1,9 @@
import { NostrConnectConnectionMethods } from "applesauce-signer";
import { Filter, NostrEvent } from "nostr-tools";
import relayPoolService from "../services/relay-pool";
import { MultiSubscription } from "applesauce-net/subscription";
import relayPoolService from "../services/relay-pool";
export function createNostrConnectConnection(): NostrConnectConnectionMethods {
const sub = new MultiSubscription(relayPoolService);

View File

@ -10,7 +10,6 @@ import { getPubkeysMentionedInContent } from "../helpers/nostr/post";
import { TORRENT_COMMENT_KIND } from "../helpers/nostr/torrents";
import { getPubkeysFromList } from "../helpers/nostr/lists";
import { eventStore, queryStore } from "../services/event-store";
import RelaySet from "./relay-set";
export const NotificationTypeSymbol = Symbol("notificationType");

View File

@ -9,9 +9,7 @@ import { shareLatestValue } from "applesauce-core/observable";
import { MultiSubscription } from "applesauce-net/subscription";
import { logger } from "../helpers/debug";
import { isReplaceable } from "../helpers/nostr/event";
import { mergeFilter } from "../helpers/nostr/filter";
import { localRelay } from "../services/local-relay";
import SuperMap from "./super-map";
import ChunkedRequest from "./chunked-request";
import relayPoolService from "../services/relay-pool";
@ -19,6 +17,7 @@ import Process from "./process";
import AlignHorizontalCentre02 from "../components/icons/align-horizontal-centre-02";
import processManager from "../services/process-manager";
import { eventStore, queryStore } from "../services/event-store";
import { getCacheRelay } from "../services/cache-relay";
const BLOCK_SIZE = 100;
@ -85,9 +84,6 @@ export default class TimelineLoader {
private handleEvent(event: NostrEvent, fromCache = false) {
event = eventStore.add(event);
// publish to local relay
if (!fromCache && this.useCache && localRelay && !this.seenInCache.has(event.id)) localRelay.publish(event);
if (fromCache) this.seenInCache.add(event.id);
}
private handleChunkFinished() {
@ -137,8 +133,9 @@ export default class TimelineLoader {
// recreate cache chunk loader
if (this.cacheLoader) this.disconnectFromChunkLoader(this.cacheLoader);
if (localRelay && this.useCache) {
this.cacheLoader = new ChunkedRequest(localRelay, this.filters, this.log.extend("cache-relay"));
const cacheRelay = getCacheRelay();
if (cacheRelay && this.useCache) {
this.cacheLoader = new ChunkedRequest(cacheRelay, this.filters, this.log.extend("cache-relay"));
this.connectToChunkLoader(this.cacheLoader);
}

View File

@ -0,0 +1,6 @@
import { useObservable } from "applesauce-react/hooks";
import { cacheRelay$ } from "../services/cache-relay";
export default function useCacheRelay() {
return useObservable(cacheRelay$);
}

View File

@ -11,7 +11,7 @@ import { DraftNostrEvent } from "../../types/nostr-event";
import clientRelaysService from "../../services/client-relays";
import RelaySet from "../../classes/relay-set";
import { getAllRelayHints } from "../../helpers/nostr/event";
import { localRelay } from "../../services/local-relay";
import { getCacheRelay } from "../../services/cache-relay";
import deleteEventService from "../../services/delete-events";
import { eventStore } from "../../services/event-store";
import useCurrentAccount from "../../hooks/use-current-account";
@ -139,7 +139,8 @@ export default function PublishProvider({ children }: PropsWithChildren) {
setLog((arr) => arr.concat(entry));
// send it to the local relay
if (localRelay) localRelay.publish(signed);
const cacheRelay = getCacheRelay();
if (cacheRelay) cacheRelay.publish(signed);
// pass it to other services
eventStore.add(signed);

View File

@ -1,7 +1,8 @@
import dayjs from "dayjs";
import { BehaviorSubject, pairwise } from "rxjs";
import { CacheRelay, openDB } from "nostr-idb";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import { fakeVerifyEvent, isFromCache } from "applesauce-core/helpers";
import dayjs from "dayjs";
import { logger } from "../helpers/debug";
import { safeRelayUrl } from "../helpers/relay";
@ -10,16 +11,6 @@ import MemoryRelay from "../classes/memory-relay";
import localSettings from "./local-settings";
import { eventStore } from "./event-store";
// save the local relay from query params to localStorage
const params = new URLSearchParams(location.search);
const paramRelay = params.get("localRelay");
// save the cache relay to localStorage
if (paramRelay) {
localStorage.setItem("localRelay", paramRelay);
params.delete("localRelay");
if (params.size === 0) location.search = params.toString();
}
export const NOSTR_RELAY_TRAY_URL = "ws://localhost:4869/";
export async function checkNostrRelayTray() {
return new Promise((res) => {
@ -37,28 +28,27 @@ export async function checkNostrRelayTray() {
});
}
const log = logger.extend(`LocalRelay`);
const log = logger.extend(`cache-relay`);
export const localDatabase = await openDB();
// Setup relay
function createInternalRelay() {
return new CacheRelay(localDatabase, { maxEvents: localSettings.idbMaxEvents.value });
}
async function createRelay() {
const localRelayURL = localStorage.getItem("localRelay");
if (localRelayURL) {
if (localRelayURL === ":none:") {
return null;
}
if (localRelayURL === ":memory:") {
// create a cache relay instance
async function createRelay(url: string) {
if (url) {
if (url === ":none:") return null;
if (url === ":memory:") {
return new MemoryRelay();
} else if (localRelayURL === "nostr-idb://wasm-worker" && WasmRelay.SUPPORTED) {
} else if (url === "nostr-idb://wasm-worker" && WasmRelay.SUPPORTED) {
return new WasmRelay();
} else if (localRelayURL.startsWith("nostr-idb://")) {
} else if (url.startsWith("nostr-idb://")) {
return createInternalRelay();
} else if (safeRelayUrl(localRelayURL)) {
return new AbstractRelay(safeRelayUrl(localRelayURL)!, { verifyEvent: fakeVerifyEvent });
} else if (safeRelayUrl(url)) {
return new AbstractRelay(safeRelayUrl(url)!, { verifyEvent: fakeVerifyEvent });
}
} else if (window.CACHE_RELAY_ENABLED) {
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
@ -66,16 +56,18 @@ async function createRelay() {
verifyEvent: fakeVerifyEvent,
});
}
return createInternalRelay();
}
async function connectRelay() {
const relay = await createRelay();
// create and connect to the cache relay
async function connectRelay(url: string) {
const relay = await createRelay(url);
if (!relay) return relay;
try {
await relay.connect();
log("Connected");
log(`Connected to ${relay.url}`);
if (relay instanceof AbstractRelay) {
// set the base timeout to 2 second
@ -89,34 +81,67 @@ async function connectRelay() {
}
}
export const localRelay = await connectRelay();
export const cacheRelay$ = new BehaviorSubject<
AbstractRelay | CacheRelay | WasmRelay | AbstractRelay | MemoryRelay | null
>(null);
// keep the relay connection alive
setInterval(() => {
if (localRelay && !localRelay.connected) localRelay.connect().then(() => log("Reconnected"));
}, 1000 * 5);
// create a new cache relay instance when the url changes
localSettings.cacheRelayURL.subscribe(async (url) => {
if (cacheRelay$.value && cacheRelay$.value.url === url) return;
const relay = await connectRelay(url);
cacheRelay$.next(relay);
});
// keep the relay connected
cacheRelay$.subscribe((relay) => {
if (!relay) return;
const i = setInterval(() => {
if (!relay.connected) relay.connect();
}, 1000);
return () => clearInterval(i);
});
// disconnect from old cache relays
cacheRelay$.pipe(pairwise()).subscribe(([prev, current]) => {
if (prev) {
log(`Disconnecting from ${prev.url}`);
prev.close();
}
});
/** set the cache relay URL and waits for it to connect */
export async function setCacheRelayURL(url: string) {
return new Promise<void>((res) => {
const sub = cacheRelay$.subscribe(() => {
res();
sub.unsubscribe();
});
localSettings.cacheRelayURL.next(url);
});
}
export function getCacheRelay() {
return cacheRelay$.value;
}
// every minute, prune the database
setInterval(() => {
if (localRelay instanceof WasmRelay) {
const relay = getCacheRelay();
if (relay instanceof WasmRelay) {
const days = localSettings.wasmPersistForDays.value;
if (days) {
log(`Removing all events older than ${days} days in WASM relay`);
localRelay.worker?.delete(["REQ", "prune", { until: dayjs().subtract(days, "days").unix() }]);
relay.worker?.delete(["REQ", "prune", { until: dayjs().subtract(days, "days").unix() }]);
}
}
}, 60_000);
// watch for new events and send them to the cache relay
if (localRelay) {
eventStore.database.inserted.subscribe((event) => {
if (!isFromCache(event)) localRelay.publish(event);
});
}
if (import.meta.env.DEV) {
//@ts-expect-error debug
window.localDatabase = localDatabase;
//@ts-expect-error debug
window.localRelay = localRelay;
}
eventStore.database.inserted.subscribe((event) => {
const relay = getCacheRelay();
if (relay && !isFromCache(event)) relay.publish(event);
});

View File

@ -10,9 +10,9 @@ import { logger } from "../helpers/debug";
import { eventStore } from "./event-store";
import relayPoolService from "./relay-pool";
import PersistentSubscription from "../classes/persistent-subscription";
import { localRelay } from "./local-relay";
import { isFromCache, markFromCache } from "applesauce-core/helpers";
import { markFromCache } from "applesauce-core/helpers";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import { cacheRelay$, getCacheRelay } from "./cache-relay";
export type RequestOptions = {
/** Always request the event from the relays */
@ -110,10 +110,12 @@ class ChannelMetadataService {
log = logger.extend("ChannelMetadata");
constructor() {
if (localRelay) {
const loader = this.loaders.get(localRelay as AbstractRelay);
cacheRelay$.subscribe((cacheRelay) => {
if (!cacheRelay) return;
const loader = this.loaders.get(cacheRelay as AbstractRelay);
loader.isCache = true;
}
});
}
handleEvent(event: NostrEvent) {
@ -121,8 +123,6 @@ class ChannelMetadataService {
const channelId = getChannelPointer(event)?.id;
if (!channelId) return;
if (!isFromCache(event)) localRelay?.publish(event);
}
private requestChannelMetadataFromRelays(relays: Iterable<string>, channelId: string) {
@ -137,8 +137,9 @@ class ChannelMetadataService {
requestMetadata(relays: Iterable<string>, channelId: string, opts: RequestOptions = {}) {
const loaded = this.loaded.get(channelId);
if (!loaded && localRelay) {
this.loaders.get(localRelay as AbstractRelay).requestMetadata(channelId);
const cacheRelay = getCacheRelay();
if (!loaded && cacheRelay) {
this.loaders.get(cacheRelay as AbstractRelay).requestMetadata(channelId);
}
if (opts?.alwaysRequest || (!loaded && opts.ignoreCache)) {

View File

@ -3,7 +3,7 @@ import { clearDB, deleteDB as nostrIDBDelete } from "nostr-idb";
import { SchemaV1, SchemaV10, SchemaV2, SchemaV3, SchemaV4, SchemaV5, SchemaV6, SchemaV7, SchemaV9 } from "./schema";
import { logger } from "../../helpers/debug";
import { localDatabase } from "../local-relay";
import { localDatabase } from "../cache-relay";
const log = logger.extend("Database");

View File

@ -1,7 +1,6 @@
import accountService from "./account";
import channelMetadataService from "./channel-metadata";
import { eventStore, queryStore } from "./event-store";
import { localRelay } from "./local-relay";
import localSettings from "./local-settings";
import readStatusService from "./read-status";
import relayInfoService from "./relay-info";
@ -29,12 +28,6 @@ const noStrudel = {
/** Signing queue */
signingService,
/**
* Cache relay interface
* @type MemoryRelay|WasmRelay|CacheRelay|Relay|undefined
*/
cacheRelay: localRelay,
// other internal services
replaceableEventLoader,
userSearchDirectory,

View File

@ -10,9 +10,9 @@ import SuperMap from "../classes/super-map";
import BatchIdentifierLoader from "../classes/batch-identifier-loader";
import BookOpen01 from "../components/icons/book-open-01";
import processManager from "./process-manager";
import { localRelay } from "./local-relay";
import relayPoolService from "./relay-pool";
import { eventStore } from "./event-store";
import { getCacheRelay } from "./cache-relay";
class DictionaryService {
log = logger.extend("DictionaryService");
@ -68,8 +68,9 @@ class DictionaryService {
const subject = this.topics.get(topic);
if (subject.value && !alwaysRequest) return subject;
if (localRelay) {
this.loaders.get(localRelay as AbstractRelay).requestEvents(topic);
const cacheRelay = getCacheRelay();
if (cacheRelay) {
this.loaders.get(cacheRelay as AbstractRelay).requestEvents(topic);
}
const relays = relayPoolService.getRelays(urls);
@ -83,9 +84,10 @@ class DictionaryService {
handleEvent(event: NostrEvent) {
event = this.store.add(event);
const cacheRelay = getCacheRelay();
// pretend it came from the local relay
// TODO: remove this once DictionaryService uses subscriptions from event store
if (localRelay) this.loaders.get(localRelay as AbstractRelay).handleEvent(event);
if (cacheRelay) this.loaders.get(cacheRelay as AbstractRelay).handleEvent(event);
}
}

View File

@ -3,7 +3,7 @@ import stringify from "json-stringify-deterministic";
import { BehaviorSubject } from "rxjs";
import SuperMap from "../classes/super-map";
import { localRelay } from "./local-relay";
import { getCacheRelay } from "./cache-relay";
class EventCountService {
subjects = new SuperMap<string, BehaviorSubject<number | undefined>>(
@ -20,9 +20,11 @@ class EventCountService {
if (sub.value === undefined || alwaysRequest) {
// try to get a count from the local relay
localRelay?.count(Array.isArray(filter) ? filter : [filter], {}).then((count) => {
if (Number.isFinite(count)) sub.next(count);
});
getCacheRelay()
?.count(Array.isArray(filter) ? filter : [filter], {})
.then((count) => {
if (Number.isFinite(count)) sub.next(count);
});
}
return sub;

View File

@ -3,13 +3,13 @@ import _throttle from "lodash.throttle";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import SuperMap from "../classes/super-map";
import { localRelay } from "./local-relay";
import relayPoolService from "./relay-pool";
import Process from "../classes/process";
import { LightningIcon } from "../components/icons";
import processManager from "./process-manager";
import BatchRelationLoader from "../classes/batch-relation-loader";
import { logger } from "../helpers/debug";
import { getCacheRelay } from "./cache-relay";
class EventReactionsService {
log = logger.extend("EventReactionsService");
@ -33,8 +33,9 @@ class EventReactionsService {
requestReactions(uid: string, urls: Iterable<string | URL | AbstractRelay>, alwaysRequest = false) {
if (this.loaded.get(uid) && !alwaysRequest) return;
if (localRelay) {
this.loaders.get(localRelay as AbstractRelay).requestEvents(uid);
const cacheRelay = getCacheRelay();
if (cacheRelay) {
this.loaders.get(cacheRelay as AbstractRelay).requestEvents(uid);
}
const relays = relayPoolService.getRelays(urls);

View File

@ -3,13 +3,13 @@ import _throttle from "lodash.throttle";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import SuperMap from "../classes/super-map";
import { localRelay } from "./local-relay";
import relayPoolService from "./relay-pool";
import Process from "../classes/process";
import { LightningIcon } from "../components/icons";
import processManager from "./process-manager";
import BatchRelationLoader from "../classes/batch-relation-loader";
import { logger } from "../helpers/debug";
import { getCacheRelay } from "./cache-relay";
class EventZapsService {
log = logger.extend("EventZapsService");
@ -33,8 +33,9 @@ class EventZapsService {
requestZaps(uid: string, urls: Iterable<string | URL | AbstractRelay>, alwaysRequest = true) {
if (this.loaded.get(uid) && !alwaysRequest) return;
if (localRelay) {
this.loaders.get(localRelay as AbstractRelay).requestEvents(uid);
const cacheRelay = getCacheRelay();
if (cacheRelay) {
this.loaders.get(cacheRelay as AbstractRelay).requestEvents(uid);
}
const relays = relayPoolService.getRelays(urls);

View File

@ -63,6 +63,9 @@ const deviceId = new LocalStorageEntry("device-id", nanoid());
const ntfyTopic = new LocalStorageEntry("ntfy-topic", nanoid());
const ntfyServer = new LocalStorageEntry("ntfy-server", "https://ntfy.sh");
// cache relay
const cacheRelayURL = new LocalStorageEntry("cache-relay-url", "");
// bakery
const bakeryURL = new LocalStorageEntry<string>("bakery-url", "");
@ -84,6 +87,7 @@ const localSettings = {
ntfyTopic,
ntfyServer,
bakeryURL,
cacheRelayURL,
};
if (import.meta.env.DEV) {

View File

@ -1,6 +1,4 @@
import { AbstractRelay } from "nostr-tools/abstract-relay";
import RelayPool from "../classes/relay-pool";
import { localRelay } from "./local-relay";
import { offlineMode } from "./offline-mode";
const relayPoolService = new RelayPool();
@ -19,12 +17,6 @@ offlineMode.subscribe((offline) => {
}
});
// add local relay
if (localRelay instanceof AbstractRelay) {
relayPoolService.relays.set(localRelay.url, localRelay);
localRelay.onnotice = (notice) => relayPoolService.handleRelayNotice(localRelay as AbstractRelay, notice);
}
if (import.meta.env.DEV) {
// @ts-expect-error debug
window.relayPoolService = relayPoolService;

View File

@ -1,15 +1,15 @@
import _throttle from "lodash.throttle";
import { Filter } from "nostr-tools";
import { BehaviorSubject } from "rxjs";
import { BehaviorSubject, mergeMap } from "rxjs";
import SuperMap from "../classes/super-map";
import { NostrEvent } from "../types/nostr-event";
import relayInfoService from "./relay-info";
import { localRelay } from "./local-relay";
import { MONITOR_STATS_KIND, SELF_REPORTED_KIND, getRelayURL } from "../helpers/nostr/relay-stats";
import relayPoolService from "./relay-pool";
import { alwaysVerify } from "./verify-event";
import { eventStore } from "./event-store";
import { getCacheRelay } from "./cache-relay";
const MONITOR_PUBKEY = "151c17c9d234320cf0f189af7b761f63419fd6c38c6041587a008b7682e4640f";
const MONITOR_RELAY = "wss://relay.nostr.watch";
@ -24,12 +24,12 @@ class RelayStatsService {
constructor() {
// load all stats from cache and subscribe to future ones
localRelay?.subscribe([{ kinds: [SELF_REPORTED_KIND, MONITOR_STATS_KIND] }], {
onevent: (e) => this.handleEvent(e, false),
getCacheRelay()?.subscribe([{ kinds: [SELF_REPORTED_KIND, MONITOR_STATS_KIND] }], {
onevent: (e) => this.handleEvent(e),
});
}
handleEvent(event: NostrEvent, cache = true) {
handleEvent(event: NostrEvent) {
if (!alwaysVerify(event)) return;
// ignore all events before NIP-66 start date
@ -42,15 +42,9 @@ class RelayStatsService {
const sub = this.monitorStats.get(relay);
if (event.kind === SELF_REPORTED_KIND) {
if (!sub.value || event.created_at > sub.value.created_at) {
sub.next(event);
if (cache && localRelay) localRelay.publish(event);
}
if (!sub.value || event.created_at > sub.value.created_at) sub.next(event);
} else if (event.kind === MONITOR_STATS_KIND) {
if (!sub.value || event.created_at > sub.value.created_at) {
sub.next(event);
if (cache && localRelay) localRelay.publish(event);
}
if (!sub.value || event.created_at > sub.value.created_at) sub.next(event);
}
}

View File

@ -1,13 +1,12 @@
import { Filter, NostrEvent } from "nostr-tools";
import { ReplaceableLoader } from "applesauce-loaders/loaders";
import { localRelay } from "./local-relay";
import { truncateId } from "../helpers/string";
import { eventStore } from "./event-store";
import rxNostr from "./rx-nostr";
import { Observable } from "rxjs";
import { COMMON_CONTACT_RELAYS } from "../const";
import { isFromCache } from "applesauce-core/helpers";
import { getCacheRelay } from "./cache-relay";
export type RequestOptions = {
/** Always request the event from the relays */
@ -23,9 +22,10 @@ export function getHumanReadableCoordinate(kind: number, pubkey: string, d?: str
// load events from cache relay
export function cacheRequest(filters: Filter[]) {
return new Observable<NostrEvent>((observer) => {
if (!localRelay) return observer.complete();
const relay = getCacheRelay();
if (!relay) return observer.complete();
const sub = localRelay.subscribe(filters, {
const sub = relay.subscribe(filters, {
onevent: (event) => observer.next(event),
oneose: () => {
sub.close();

View File

@ -1,9 +1,6 @@
import { kinds } from "nostr-tools";
import _throttle from "lodash.throttle";
import { combineLatest, distinct, filter } from "rxjs";
import { AbstractRelay } from "nostr-tools/abstract-relay";
import { USER_BLOSSOM_SERVER_LIST_KIND } from "blossom-client-sdk";
import { isFromCache } from "applesauce-core/helpers";
import { COMMON_CONTACT_RELAYS } from "../const";
import { logger } from "../helpers/debug";
@ -15,7 +12,6 @@ import { eventStore, queryStore } from "./event-store";
import { Account } from "../classes/accounts/account";
import { MultiSubscription } from "applesauce-net/subscription";
import relayPoolService from "./relay-pool";
import { localRelay } from "./local-relay";
import { APP_SETTING_IDENTIFIER, APP_SETTINGS_KIND } from "../helpers/app-settings";
const log = logger.extend("UserEventSync");
@ -49,18 +45,11 @@ function downloadEvents(account: Account) {
if (mailboxes?.outboxes && mailboxes.outboxes.length > 0) {
log(`Loading delete events`);
const sub = new MultiSubscription(relayPoolService);
sub.setRelays(
localRelay
? [...mailboxes.outboxes.map((r) => relayPoolService.requestRelay(r)), localRelay as AbstractRelay]
: mailboxes.outboxes,
);
sub.setRelays(mailboxes.outboxes);
sub.setFilters([{ kinds: [kinds.EventDeletion], authors: [account.pubkey] }]);
sub.open();
sub.onEvent.subscribe((e) => {
eventStore.add(e);
if (!isFromCache(e) && localRelay) localRelay.publish(e);
});
sub.onEvent.subscribe((e) => eventStore.add(e));
cleanup.push(() => sub.close());
}

View File

@ -8,7 +8,6 @@ import WebRtcRelayClient from "../classes/webrtc/webrtc-relay-client";
import WebRtcRelayServer from "../classes/webrtc/webrtc-relay-server";
import NostrWebRTCPeer from "../classes/webrtc/nostr-webrtc-peer";
import verifyEventMethod from "./verify-event";
import { localRelay } from "./local-relay";
import localSettings from "./local-settings";
import { DEFAULT_ICE_SERVERS } from "../const";
@ -130,7 +129,7 @@ const signer = new SimpleSigner(localSettings.webRtcLocalIdentity.value);
const broker = new NostrWebRtcBroker(signer, new SimplePool(), ["wss://nos.lol", "wss://nostrue.com"]);
broker.iceServers = DEFAULT_ICE_SERVERS;
const webRtcRelaysService = new WebRtcRelaysService(broker, localRelay as AbstractRelay | null);
const webRtcRelaysService = new WebRtcRelaysService(broker, null);
webRtcRelaysService.start();

View File

@ -1,15 +1,23 @@
import { useState } from "react";
import { useAsync } from "react-use";
import { Button, Card, CardBody, CardHeader, Heading, Link, Text } from "@chakra-ui/react";
import { NOSTR_RELAY_TRAY_URL, checkNostrRelayTray, localRelay } from "../../../../services/local-relay";
import { NOSTR_RELAY_TRAY_URL, checkNostrRelayTray, setCacheRelayURL } from "../../../../services/cache-relay";
import useCacheRelay from "../../../../hooks/use-cache-relay";
export default function CitrineRelayCard() {
const { value: available, loading: checking } = useAsync(checkNostrRelayTray);
const enabled = localRelay?.url.startsWith(NOSTR_RELAY_TRAY_URL);
const enable = () => {
localStorage.setItem("localRelay", NOSTR_RELAY_TRAY_URL);
location.reload();
const cacheRelay = useCacheRelay();
const enabled = cacheRelay?.url.startsWith(NOSTR_RELAY_TRAY_URL);
const [enabling, setEnabling] = useState(false);
const enable = async () => {
try {
setEnabling(true);
await setCacheRelayURL(NOSTR_RELAY_TRAY_URL);
} catch (error) {}
setEnabling(false);
};
return (
@ -20,7 +28,14 @@ export default function CitrineRelayCard() {
GitHub
</Link>
{available ? (
<Button size="sm" colorScheme="primary" ml="auto" isLoading={checking} onClick={enable} isDisabled={enabled}>
<Button
size="sm"
colorScheme="primary"
ml="auto"
isLoading={checking || enabling}
onClick={enable}
isDisabled={enabled}
>
{enabled ? "Enabled" : "Enable"}
</Button>
) : (

View File

@ -17,11 +17,13 @@ export default function EnableWithDelete({
enable,
enabled,
wipe,
isLoading,
...props
}: Omit<ButtonGroupProps, "children"> & {
enable: () => void;
enabled: boolean;
wipe: () => Promise<void>;
isLoading?: boolean;
}) {
const [deleting, setDeleting] = useState(false);
const wipeDatabase = useCallback(async () => {
@ -39,7 +41,7 @@ export default function EnableWithDelete({
{enabled ? "Enabled" : "Enable"}
</Button>
<Menu>
<MenuButton as={IconButton} icon={<ChevronDownIcon />} aria-label="More options" />
<MenuButton as={IconButton} icon={<ChevronDownIcon />} aria-label="More options" isLoading={isLoading} />
<MenuList>
<MenuItem icon={<Trash01 />} color="red.500" onClick={wipeDatabase}>
Clear Database

View File

@ -1,12 +1,13 @@
import { Button, Card, CardBody, CardHeader, Heading, Text } from "@chakra-ui/react";
import { localRelay } from "../../../../services/local-relay";
import useCacheRelay from "../../../../hooks/use-cache-relay";
import localSettings from "../../../../services/local-settings";
export default function HostedRelayCard() {
const enabled = localRelay?.url.includes(location.host + "/local-relay");
const cacheRelay = useCacheRelay();
const enabled = cacheRelay?.url.includes(location.host + "/local-relay");
const enable = () => {
localStorage.removeItem("localRelay");
location.reload();
localSettings.cacheRelayURL.clear();
};
return (

View File

@ -1,15 +1,23 @@
import { useState } from "react";
import { Button, Card, CardBody, CardFooter, CardHeader, Heading, Text } from "@chakra-ui/react";
import { CacheRelay, clearDB } from "nostr-idb";
import { Link as RouterLink } from "react-router-dom";
import { localDatabase, localRelay } from "../../../../services/local-relay";
import { localDatabase, setCacheRelayURL } from "../../../../services/cache-relay";
import EnableWithDelete from "../components/enable-with-delete";
import useCacheRelay from "../../../../hooks/use-cache-relay";
export default function InternalRelayCard() {
const enabled = localRelay instanceof CacheRelay;
const enable = () => {
localStorage.setItem("localRelay", "nostr-idb://internal");
location.reload();
const cacheRelay = useCacheRelay();
const enabled = cacheRelay instanceof CacheRelay;
const [enabling, setEnabling] = useState(false);
const enable = async () => {
try {
setEnabling(true);
await setCacheRelayURL("nostr-idb://internal");
} catch (error) {}
setEnabling(false);
};
const wipe = async () => {
@ -20,7 +28,7 @@ export default function InternalRelayCard() {
<Card borderColor={enabled ? "primary.500" : undefined} variant="outline">
<CardHeader p="4" display="flex" gap="2" alignItems="center">
<Heading size="md">Browser Cache</Heading>
<EnableWithDelete size="sm" ml="auto" enable={enable} enabled={enabled} wipe={wipe} />
<EnableWithDelete size="sm" ml="auto" enable={enable} enabled={enabled} wipe={wipe} isLoading={enabling} />
</CardHeader>
<CardBody p="4" pt="0">
<Text mb="2">Use the browsers built-in database to cache events.</Text>

View File

@ -1,21 +1,29 @@
import { useState } from "react";
import { Button, Card, CardBody, CardFooter, CardHeader, Heading, Text } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { localRelay } from "../../../../services/local-relay";
import MemoryRelay from "../../../../classes/memory-relay";
import useCacheRelay from "../../../../hooks/use-cache-relay";
import { setCacheRelayURL } from "../../../../services/cache-relay";
export default function MemoryRelayCard() {
const enabled = localRelay instanceof MemoryRelay;
const enable = () => {
localStorage.setItem("localRelay", ":memory:");
location.reload();
const cacheRelay = useCacheRelay();
const enabled = cacheRelay instanceof MemoryRelay;
const [enabling, setEnabling] = useState(false);
const enable = async () => {
try {
setEnabling(true);
await setCacheRelayURL(":memory:");
} catch (error) {}
setEnabling(false);
};
return (
<Card borderColor={enabled ? "primary.500" : undefined} variant="outline">
<CardHeader p="4" display="flex" gap="2" alignItems="center">
<Heading size="md">In-memory Cache</Heading>
<Button size="sm" colorScheme="primary" ml="auto" onClick={enable} isDisabled={enabled}>
<Button size="sm" colorScheme="primary" ml="auto" onClick={enable} isDisabled={enabled} isLoading={enabling}>
{enabled ? "Enabled" : "Enable"}
</Button>
</CardHeader>

View File

@ -1,18 +1,27 @@
import { Button, Card, CardBody, CardHeader, Heading, Text } from "@chakra-ui/react";
import { localRelay } from "../../../../services/local-relay";
import useCacheRelay from "../../../../hooks/use-cache-relay";
import localSettings from "../../../../services/local-settings";
import { useState } from "react";
import { setCacheRelayURL } from "../../../../services/cache-relay";
export default function NoRelayCard() {
const enabled = localRelay === null;
const enable = () => {
localStorage.setItem("localRelay", ":none:");
location.reload();
const cacheRelay = useCacheRelay();
const enabled = cacheRelay === null;
const [enabling, setEnabling] = useState(false);
const enable = async () => {
try {
setEnabling(true);
await setCacheRelayURL(":none:");
} catch (error) {}
setEnabling(false);
};
return (
<Card borderColor={enabled ? "primary.500" : undefined} variant="outline">
<CardHeader p="4" display="flex" gap="2" alignItems="center">
<Heading size="md">No Cache</Heading>
<Button size="sm" colorScheme="primary" ml="auto" onClick={enable} isDisabled={enabled}>
<Button size="sm" colorScheme="primary" ml="auto" onClick={enable} isDisabled={enabled} isLoading={enabling}>
{enabled ? "Enabled" : "Enable"}
</Button>
</CardHeader>

View File

@ -1,15 +1,23 @@
import { useAsync } from "react-use";
import { Button, Card, CardBody, CardHeader, Heading, Link, Text } from "@chakra-ui/react";
import { NOSTR_RELAY_TRAY_URL, checkNostrRelayTray, localRelay } from "../../../../services/local-relay";
import { NOSTR_RELAY_TRAY_URL, checkNostrRelayTray, setCacheRelayURL } from "../../../../services/cache-relay";
import useCacheRelay from "../../../../hooks/use-cache-relay";
import { useState } from "react";
export default function NostrRelayTrayCard() {
const cacheRelay = useCacheRelay();
const { value: available, loading: checking } = useAsync(checkNostrRelayTray);
const enabled = localRelay?.url.startsWith(NOSTR_RELAY_TRAY_URL);
const enable = () => {
localStorage.setItem("localRelay", NOSTR_RELAY_TRAY_URL);
location.reload();
const enabled = cacheRelay?.url.startsWith(NOSTR_RELAY_TRAY_URL);
const [enabling, setEnabling] = useState(false);
const enable = async () => {
try {
setEnabling(true);
await setCacheRelayURL(NOSTR_RELAY_TRAY_URL);
} catch (error) {}
setEnabling(false);
};
return (
@ -20,7 +28,14 @@ export default function NostrRelayTrayCard() {
GitHub
</Link>
{available || enabled ? (
<Button size="sm" colorScheme="primary" ml="auto" isLoading={checking} onClick={enable} isDisabled={enabled}>
<Button
size="sm"
colorScheme="primary"
ml="auto"
isLoading={checking || enabling}
onClick={enable}
isDisabled={enabled}
>
{enabled ? "Enabled" : "Enable"}
</Button>
) : (

View File

@ -1,20 +1,27 @@
import { useState } from "react";
import { Button, Card, CardBody, CardFooter, CardHeader, Heading, Link, Text } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { localRelay } from "../../../../services/local-relay";
import WasmRelay from "../../../../services/wasm-relay";
import EnableWithDelete from "./enable-with-delete";
import useCacheRelay from "../../../../hooks/use-cache-relay";
import { setCacheRelayURL } from "../../../../services/cache-relay";
export default function WasmRelayCard() {
const enabled = localRelay instanceof WasmRelay;
const enable = () => {
localStorage.setItem("localRelay", "nostr-idb://wasm-worker");
location.reload();
const cacheRelay = useCacheRelay();
const enabled = cacheRelay instanceof WasmRelay;
const [enabling, setEnabling] = useState(false);
const enable = async () => {
try {
setEnabling(true);
await setCacheRelayURL("nostr-idb://wasm-worker");
} catch (error) {}
setEnabling(false);
};
const wipe = async () => {
if (localRelay instanceof WasmRelay) {
await localRelay.wipe();
if (cacheRelay instanceof WasmRelay) {
await cacheRelay.wipe();
} else {
// import and delete database
console.log("Importing worker to wipe database");
@ -27,7 +34,7 @@ export default function WasmRelayCard() {
<Card borderColor={enabled ? "primary.500" : undefined} variant="outline">
<CardHeader p="4" display="flex" gap="2" alignItems="center">
<Heading size="md">Internal SQLite Cache</Heading>
<EnableWithDelete size="sm" ml="auto" enable={enable} enabled={enabled} wipe={wipe} />
<EnableWithDelete size="sm" ml="auto" enable={enable} enabled={enabled} wipe={wipe} isLoading={enabling} />
</CardHeader>
<CardBody p="4" pt="0">
<Text mb="2">

View File

@ -1,19 +1,20 @@
import { lazy } from "react";
import { Flex, Heading, Link, Text } from "@chakra-ui/react";
import { Link, Text } from "@chakra-ui/react";
import { CacheRelay } from "nostr-idb";
import { Link as RouterLink } from "react-router-dom";
import BackButton from "../../../../components/router/back-button";
import { localRelay } from "../../../../services/local-relay";
import WasmRelay from "../../../../services/wasm-relay";
import MemoryRelay from "../../../../classes/memory-relay";
import SimpleView from "../../../../components/layout/presets/simple-view";
import useCacheRelay from "../../../../hooks/use-cache-relay";
const MemoryDatabasePage = lazy(() => import("./memory"));
const WasmDatabasePage = lazy(() => import("./wasm"));
const InternalDatabasePage = lazy(() => import("./internal"));
export default function DatabaseView() {
const cacheRelay = useCacheRelay();
let content = (
<Text>
noStrudel does not have access to the selected cache relays database{" "}
@ -23,9 +24,9 @@ export default function DatabaseView() {
</Text>
);
if (localRelay instanceof WasmRelay) content = <WasmDatabasePage />;
else if (localRelay instanceof CacheRelay) content = <InternalDatabasePage />;
else if (localRelay instanceof MemoryRelay) content = <MemoryDatabasePage />;
if (cacheRelay instanceof WasmRelay) content = <WasmDatabasePage />;
else if (cacheRelay instanceof CacheRelay) content = <InternalDatabasePage />;
else if (cacheRelay instanceof MemoryRelay) content = <MemoryDatabasePage />;
return <SimpleView title="Event Cache">{content}</SimpleView>;
}

View File

@ -20,7 +20,7 @@ import { useAsync } from "react-use";
import { NostrEvent } from "nostr-tools";
import { useObservable } from "applesauce-react/hooks";
import { localDatabase } from "../../../../services/local-relay";
import { localDatabase } from "../../../../services/cache-relay";
import EventKindsPieChart from "../../../../components/charts/event-kinds-pie-chart";
import EventKindsTable from "../../../../components/charts/event-kinds-table";
import ImportEventsButton from "./components/import-events-button";

View File

@ -1,8 +1,8 @@
import { useEffect, useMemo } from "react";
import { ButtonGroup, Card, Flex, Heading, Text } from "@chakra-ui/react";
import { useObservable } from "applesauce-react/hooks";
import { NostrEvent } from "nostr-tools";
import { localRelay } from "../../../../services/local-relay";
import EventKindsPieChart from "../../../../components/charts/event-kinds-pie-chart";
import EventKindsTable from "../../../../components/charts/event-kinds-table";
import ImportEventsButton from "./components/import-events-button";
@ -10,37 +10,39 @@ import ExportEventsButton from "./components/export-events-button";
import MemoryRelay from "../../../../classes/memory-relay";
import { getSortedKinds } from "../../../../helpers/nostr/event";
import useForceUpdate from "../../../../hooks/use-force-update";
async function importEvents(events: NostrEvent[]) {
for (const event of events) {
localRelay?.publish(event);
}
}
async function exportEvents() {
if (localRelay instanceof MemoryRelay) {
return Array.from(localRelay.store.database.iterateTime(0, Infinity));
}
return [];
}
import { cacheRelay$ } from "../../../../services/cache-relay";
export default function MemoryDatabasePage() {
const cacheRelay = useObservable(cacheRelay$);
const update = useForceUpdate();
useEffect(() => {
if (localRelay instanceof MemoryRelay) {
const sub = localRelay.store.database.inserted.subscribe(update);
if (cacheRelay instanceof MemoryRelay) {
const sub = cacheRelay.store.database.inserted.subscribe(update);
return () => sub.unsubscribe();
}
}, []);
const importEvents = async (events: NostrEvent[]) => {
for (const event of events) {
cacheRelay?.publish(event);
}
};
const exportEvents = async () => {
if (cacheRelay instanceof MemoryRelay) {
return Array.from(cacheRelay.store.database.iterateTime(0, Infinity));
}
return [];
};
const count = useMemo(() => {
if (localRelay instanceof MemoryRelay) return localRelay.store.database.events.size;
if (cacheRelay instanceof MemoryRelay) return cacheRelay.store.database.events.size;
return 0;
}, [update]);
const kinds = useMemo(() => {
if (localRelay instanceof MemoryRelay) {
return getSortedKinds(Array.from(localRelay.store.database.iterateTime(0, Infinity)));
if (cacheRelay instanceof MemoryRelay) {
return getSortedKinds(Array.from(cacheRelay.store.database.iterateTime(0, Infinity)));
}
return {};
}, [update]);

View File

@ -17,7 +17,7 @@ import {
import { NostrEvent } from "nostr-tools";
import { useObservable } from "applesauce-react/hooks";
import { localRelay } from "../../../../services/local-relay";
import { cacheRelay$ } from "../../../../services/cache-relay";
import WasmRelay from "../../../../services/wasm-relay";
import EventKindsPieChart from "../../../../components/charts/event-kinds-pie-chart";
import EventKindsTable from "../../../../components/charts/event-kinds-table";
@ -26,9 +26,9 @@ import ExportEventsButton from "./components/export-events-button";
import localSettings from "../../../../services/local-settings";
export default function WasmDatabasePage() {
const relay = localRelay;
if (!(relay instanceof WasmRelay)) return null;
const worker = relay.worker;
const cacheRelay = useObservable(cacheRelay$);
if (!(cacheRelay instanceof WasmRelay)) return null;
const worker = cacheRelay.worker;
if (!worker) return null;
const [summary, setSummary] = useState<Record<string, number>>();
@ -82,13 +82,13 @@ export default function WasmDatabasePage() {
const deleteDatabase = useCallback(async () => {
try {
setDeleting(true);
if (localRelay instanceof WasmRelay) {
await localRelay.wipe();
if (cacheRelay instanceof WasmRelay) {
await cacheRelay.wipe();
location.reload();
}
} catch (error) {}
setDeleting(false);
}, []);
}, [cacheRelay]);
useEffect(() => {
refresh();

View File

@ -1,7 +1,5 @@
import { Box, Button, Divider, Flex, Heading, Text, useDisclosure } from "@chakra-ui/react";
import BackButton from "../../../components/router/back-button";
import { localRelay } from "../../../services/local-relay";
import { ChevronDownIcon, ChevronUpIcon } from "../../../components/icons";
import WasmRelay from "../../../services/wasm-relay";
import WasmRelayCard from "./components/wasm-relay-card";
@ -12,9 +10,11 @@ import HostedRelayCard from "./components/hosted-relay-card";
import MemoryRelayCard from "./components/memory-relay-card";
import NoRelayCard from "./components/no-relay-card";
import SimpleView from "../../../components/layout/presets/simple-view";
import useCacheRelay from "../../../hooks/use-cache-relay";
export default function CacheRelayView() {
const showAdvanced = useDisclosure({ defaultIsOpen: localRelay?.url === ":none:" || localRelay?.url === ":memory:" });
const cacheRelay = useCacheRelay();
const showAdvanced = useDisclosure({ defaultIsOpen: cacheRelay?.url === ":none:" || cacheRelay?.url === ":memory:" });
return (
<SimpleView title="Cache Relay">

View File

@ -7,11 +7,12 @@ import UserName from "../../../../components/user/user-name";
import WebRtcRelayClient from "../../../../classes/webrtc/webrtc-relay-client";
import WebRtcRelayServer from "../../../../classes/webrtc/webrtc-relay-server";
import NostrWebRTCPeer from "../../../../classes/webrtc/nostr-webrtc-peer";
import { localRelay } from "../../../../services/local-relay";
import { cacheRelay$ } from "../../../../services/cache-relay";
import useCurrentAccount from "../../../../hooks/use-current-account";
import useUserContactList from "../../../../hooks/use-user-contact-list";
import { getPubkeysFromList } from "../../../../helpers/nostr/lists";
import useForceUpdate from "../../../../hooks/use-force-update";
import { useObservable } from "applesauce-react/hooks";
export default function Connection({
call,
@ -24,6 +25,7 @@ export default function Connection({
client: WebRtcRelayClient;
server: WebRtcRelayServer;
}) {
const cacheRelay = useObservable(cacheRelay$);
const update = useForceUpdate();
useInterval(update, 1000);
// const toggleRead = () => {
@ -35,10 +37,10 @@ export default function Connection({
const [sending, setSending] = useState(false);
const sendEvents = async () => {
if (!account?.pubkey || !localRelay) return;
if (!account?.pubkey || !cacheRelay) return;
setSending(true);
const sub = localRelay.subscribe([{ authors: [account.pubkey] }], {
const sub = cacheRelay.subscribe([{ authors: [account.pubkey] }], {
onevent: (event) => {
client.publish(event);
update();
@ -52,12 +54,12 @@ export default function Connection({
const [requesting, setRequesting] = useState(false);
const requestEvents = async () => {
if (!contacts || !localRelay) return;
if (!contacts || !cacheRelay) return;
setRequesting(true);
const sub = client.subscribe([{ authors: getPubkeysFromList(contacts).map((p) => p.pubkey) }], {
onevent: (event) => {
if (localRelay) localRelay.publish(event);
if (cacheRelay) cacheRelay.publish(event);
update();
},
oneose: () => {

View File

@ -4,22 +4,25 @@ import { AbstractRelay } from "nostr-tools/abstract-relay";
import useSearchRelays from "../../../hooks/use-search-relays";
import { useRelayInfo } from "../../../hooks/use-relay-info";
import { localRelay } from "../../../services/local-relay";
import WasmRelay from "../../../services/wasm-relay";
import relayPoolService from "../../../services/relay-pool";
import useCacheRelay from "../../../hooks/use-cache-relay";
export function useSearchRelay(relay?: string) {
const cacheRelay = useCacheRelay();
if (!relay) return undefined;
if (relay === "local") return localRelay as AbstractRelay;
if (relay === "local") return cacheRelay as AbstractRelay;
else return relayPoolService.requestRelay(relay);
}
const SearchRelayPicker = forwardRef<any, Omit<SelectProps, "children">>(({ value, onChange, ...props }) => {
const searchRelays = useSearchRelays();
const { info: localRelayInfo } = useRelayInfo(localRelay instanceof AbstractRelay ? localRelay : undefined, true);
const cacheRelay = useCacheRelay();
const { info: cacheRelayInfo } = useRelayInfo(cacheRelay instanceof AbstractRelay ? cacheRelay : undefined, true);
const localSearchSupported =
localRelay instanceof WasmRelay ||
(localRelay instanceof AbstractRelay && !!localRelayInfo?.supported_nips?.includes(50));
cacheRelay instanceof WasmRelay ||
(cacheRelay instanceof AbstractRelay && !!cacheRelayInfo?.supported_nips?.includes(50));
return (
<Select w="auto" value={value} onChange={onChange} {...props}>

View File

@ -14,17 +14,18 @@ import QRCodeScannerButton from "../../components/qr-code/qr-code-scanner-button
import SearchResults from "./components/search-results";
import useSearchRelays from "../../hooks/use-search-relays";
import { useRelayInfo } from "../../hooks/use-relay-info";
import { localRelay } from "../../services/local-relay";
import WasmRelay from "../../services/wasm-relay";
import relayPoolService from "../../services/relay-pool";
import useCacheRelay from "../../hooks/use-cache-relay";
export function SearchPage() {
const cacheRelay = useCacheRelay();
const navigate = useNavigate();
const searchRelays = useSearchRelays();
const { info: localRelayInfo } = useRelayInfo(localRelay instanceof AbstractRelay ? localRelay : undefined, true);
const { info: cacheRelayInfo } = useRelayInfo(cacheRelay instanceof AbstractRelay ? cacheRelay : undefined, true);
const localSearchSupported =
localRelay instanceof WasmRelay ||
(localRelay instanceof AbstractRelay && !!localRelayInfo?.supported_nips?.includes(50));
cacheRelay instanceof WasmRelay ||
(cacheRelay instanceof AbstractRelay && !!cacheRelayInfo?.supported_nips?.includes(50));
const autoFocusSearch = useBreakpointValue({ base: false, lg: true });
@ -33,21 +34,21 @@ export function SearchPage() {
const relayURL = params.get("relay");
const searchRelay = useMemo(() => {
if (relayURL === "local") return localRelay;
if (relayURL === "local") return cacheRelay;
else if (relayURL) return relayPoolService.requestRelay(relayURL);
else if (localSearchSupported) return localRelay;
else if (localSearchSupported) return cacheRelay;
else return relayPoolService.requestRelay(searchRelays[0]);
}, [relayURL, localSearchSupported, localRelay, searchRelays[0]]);
}, [relayURL, localSearchSupported, cacheRelay, searchRelays[0]]);
const { register, handleSubmit, setValue } = useForm({
defaultValues: { query: searchQuery, relay: searchRelay === localRelay ? "local" : searchRelay?.url },
defaultValues: { query: searchQuery, relay: searchRelay === cacheRelay ? "local" : searchRelay?.url },
mode: "all",
});
// reset the relay when the search relay changes
useEffect(
() => setValue("relay", searchRelay === localRelay ? "local" : searchRelay?.url),
[searchRelay, localRelay],
() => setValue("relay", searchRelay === cacheRelay ? "local" : searchRelay?.url),
[searchRelay, cacheRelay],
);
const handleSearchText = (text: string) => {

View File

@ -3,9 +3,6 @@ import {
Badge,
Box,
Flex,
FormControl,
FormHelperText,
FormLabel,
Link,
LinkBox,
Select,
@ -30,7 +27,6 @@ import { combineLatest, map } from "rxjs";
import relayPoolService from "../../../services/relay-pool";
import { RelayFavicon } from "../../../components/relay-favicon";
import HoverLinkOverlay from "../../../components/hover-link-overlay";
import { localRelay } from "../../../services/local-relay";
import { IconRelayAuthButton, useRelayAuthMethod } from "../../../components/relays/relay-auth-button";
import RelayConnectSwitch from "../../../components/relays/relay-connect-switch";
import useRouteSearchValue from "../../../hooks/use-route-search-value";
@ -40,6 +36,7 @@ import Timestamp from "../../../components/timestamp";
import localSettings from "../../../services/local-settings";
import useForceUpdate from "../../../hooks/use-force-update";
import DefaultAuthModeSelect from "../../../components/settings/default-auth-mode-select";
import useCacheRelay from "../../../hooks/use-cache-relay";
function RelayCard({ relay }: { relay: AbstractRelay }) {
return (
@ -105,11 +102,12 @@ export default function TaskManagerRelays() {
const update = useForceUpdate();
useInterval(update, 2000);
const cacheRelay = useCacheRelay();
const { value: tab, setValue: setTab } = useRouteSearchValue("tab", TABS[0]);
const tabIndex = TABS.indexOf(tab);
const relays = Array.from(relayPoolService.relays.values())
.filter((r) => r !== localRelay)
.filter((r) => r !== cacheRelay)
.sort((a, b) => +b.connected - +a.connected || a.url.localeCompare(b.url));
const observable = useMemo(
@ -137,7 +135,7 @@ export default function TaskManagerRelays() {
<TabPanels>
<TabPanel p="0">
<SimpleGrid spacing="2" columns={{ base: 1, md: 2 }} p="2">
{localRelay instanceof AbstractRelay && <RelayCard relay={localRelay} />}
{cacheRelay instanceof AbstractRelay && <RelayCard relay={cacheRelay} />}
{relays.map((relay) => (
<RelayCard key={relay.url} relay={relay} />
))}

View File

@ -25,7 +25,6 @@ import { useLocation, useSearchParams } from "react-router-dom";
import VerticalPageLayout from "../../../components/vertical-page-layout";
import BackButton from "../../../components/router/back-button";
import { localRelay } from "../../../services/local-relay";
import Play from "../../../components/icons/play";
import ClockRewind from "../../../components/icons/clock-rewind";
import HistoryDrawer from "./history-drawer";
@ -39,6 +38,7 @@ import { validateRelayURL } from "../../../helpers/relay";
import FilterEditor from "./filter-editor";
import { safeJson } from "../../../helpers/parse";
import relayPoolService from "../../../services/relay-pool";
import useCacheRelay from "../../../hooks/use-cache-relay";
const EventTimeline = memo(({ events }: { events: NostrEvent[] }) => {
return (
@ -51,6 +51,7 @@ const EventTimeline = memo(({ events }: { events: NostrEvent[] }) => {
});
export default function EventConsoleView() {
const cacheRelay = useCacheRelay();
const [params, setParams] = useSearchParams();
const location = useLocation();
const historyDrawer = useDisclosure();
@ -90,8 +91,8 @@ export default function EventConsoleView() {
if (sub) sub.close();
if (!localRelay) throw new Error("Local relay disabled");
let r = localRelay!;
if (!cacheRelay) throw new Error("Local relay disabled");
let r = cacheRelay!;
if (queryRelay.isOpen) {
const url = validateRelayURL(relayURL);
if (!relay || relay.url !== url.toString()) {
@ -128,7 +129,7 @@ export default function EventConsoleView() {
if (e instanceof Error) setError(e.message);
}
setLoading(false);
}, [queryRelay.isOpen, query, relayURL, relay, sub]);
}, [queryRelay.isOpen, query, relayURL, relay, sub, cacheRelay]);
const submitRef = useRef(loadEvents);
submitRef.current = loadEvents;

View File

@ -11,12 +11,14 @@ import useRouteSearchValue from "../../hooks/use-route-search-value";
import { subscribeMany } from "../../helpers/relay";
import { DEFAULT_SEARCH_RELAYS, WIKI_RELAYS } from "../../const";
import { WIKI_PAGE_KIND } from "../../helpers/nostr/wiki";
import { localRelay } from "../../services/local-relay";
import { cacheRelay$ } from "../../services/cache-relay";
import WikiPageResult from "./components/wiki-page-result";
import dictionaryService from "../../services/dictionary";
import { useWebOfTrust } from "../../providers/global/web-of-trust-provider";
import { useObservable } from "applesauce-react/hooks";
export default function WikiSearchView() {
const cacheRelay = useObservable(cacheRelay$);
const webOfTrust = useWebOfTrust();
const { value: query, setValue: setQuery } = useRouteSearchValue("q");
if (!query) return <Navigate to="/wiki" />;
@ -46,13 +48,13 @@ export default function WikiSearchView() {
oneose: () => remoteSearchSub.close(),
});
if (localRelay) {
const localSearchSub: Subscription = localRelay.subscribe([filter], {
if (cacheRelay) {
const localSearchSub: Subscription = cacheRelay.subscribe([filter], {
onevent: handleEvent,
oneose: () => localSearchSub.close(),
});
}
}, [query, setResults]);
}, [query, setResults, cacheRelay]);
const sorted = webOfTrust ? webOfTrust.sortByDistanceAndConnections(results, (p) => p.pubkey) : results;