mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-17 02:47:18 +02:00
refactor: remove wrapper classes, use replaceableEventCache directly
Major cleanup of caching layer - remove abstraction wrappers and use the generic cache directly throughout the codebase. Changes: - Add convenience methods to ReplaceableEventCache: - getOutboxRelays() / getOutboxRelaysSync() for kind:10002 - getInboxRelays() for kind:10002 - getBlossomServers() / getBlossomServersSync() for kind:10063 - normalizeRelays() helper - Remove wrapper classes: - Delete src/services/relay-list-cache.ts (148 lines removed) - Delete src/services/blossom-server-cache.ts (86 lines removed) - Update all imports and usages: - Services: loaders.ts, relay-selection.ts, hub.ts - Actions: delete-event.ts, publish-spell.ts - Components: ProfileViewer, ShareSpellbookDialog, ZapstoreApp renderers - Tests: relay-selection.test.ts, loaders.test.ts, publish-spell.test.ts Benefits: - Simpler architecture - one cache instead of three - Less code duplication (234 lines removed) - Single source of truth for all replaceable events - Easier to maintain and extend Deprecated interfaces (CachedRelayList, CachedBlossomServerList) kept for backward compatibility with older database versions.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import accountManager from "@/services/accounts";
|
||||
import pool from "@/services/relay-pool";
|
||||
import { EventFactory } from "applesauce-core/event-factory";
|
||||
import { relayListCache } from "@/services/relay-list-cache";
|
||||
import replaceableEventCache from "@/services/replaceable-event-cache";
|
||||
import { AGGREGATOR_RELAYS } from "@/services/loaders";
|
||||
import { mergeRelaySets } from "applesauce-core/helpers";
|
||||
import { grimoireStateAtom } from "@/core/state";
|
||||
@@ -31,7 +31,7 @@ export class DeleteEventAction {
|
||||
|
||||
// Get write relays from cache and state
|
||||
const authorWriteRelays =
|
||||
(await relayListCache.getOutboxRelays(account.pubkey)) || [];
|
||||
(await replaceableEventCache.getOutboxRelays(account.pubkey)) || [];
|
||||
|
||||
const store = getDefaultStore();
|
||||
const state = store.get(grimoireStateAtom);
|
||||
|
||||
@@ -25,8 +25,8 @@ vi.mock("@/services/spell-storage", () => ({
|
||||
markSpellPublished: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/services/relay-list-cache", () => ({
|
||||
relayListCache: {
|
||||
vi.mock("@/services/replaceable-event-cache", () => ({
|
||||
replaceableEventCache: {
|
||||
getOutboxRelays: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -5,7 +5,7 @@ import { encodeSpell } from "@/lib/spell-conversion";
|
||||
import { markSpellPublished } from "@/services/spell-storage";
|
||||
import { EventFactory } from "applesauce-core/event-factory";
|
||||
import { SpellEvent } from "@/types/spell";
|
||||
import { relayListCache } from "@/services/relay-list-cache";
|
||||
import replaceableEventCache from "@/services/replaceable-event-cache";
|
||||
import { AGGREGATOR_RELAYS } from "@/services/loaders";
|
||||
import { mergeRelaySets } from "applesauce-core/helpers";
|
||||
import eventStore from "@/services/event-store";
|
||||
@@ -57,7 +57,7 @@ export class PublishSpellAction {
|
||||
|
||||
if (!relays || relays.length === 0) {
|
||||
const authorWriteRelays =
|
||||
(await relayListCache.getOutboxRelays(account.pubkey)) || [];
|
||||
(await replaceableEventCache.getOutboxRelays(account.pubkey)) || [];
|
||||
|
||||
relays = mergeRelaySets(
|
||||
event.tags.find((t) => t[0] === "relays")?.slice(1) || [],
|
||||
|
||||
@@ -27,12 +27,12 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
||||
import { useRelayState } from "@/hooks/useRelayState";
|
||||
import { getConnectionIcon, getAuthIcon } from "@/lib/relay-status-utils";
|
||||
import { addressLoader } from "@/services/loaders";
|
||||
import { relayListCache } from "@/services/relay-list-cache";
|
||||
import replaceableEventCache from "@/services/replaceable-event-cache";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { Subscription } from "rxjs";
|
||||
import { useGrimoire } from "@/core/state";
|
||||
import { USER_SERVER_LIST_KIND, getServersFromEvent } from "@/services/blossom";
|
||||
import blossomServerCache from "@/services/blossom-server-cache";
|
||||
// blossomServerCache is now part of replaceableEventCache
|
||||
|
||||
export interface ProfileViewerProps {
|
||||
pubkey: string;
|
||||
@@ -59,15 +59,15 @@ export function ProfileViewer({ pubkey }: ProfileViewerProps) {
|
||||
let subscription: Subscription | null = null;
|
||||
if (!resolvedPubkey) return;
|
||||
|
||||
// Check if we have a valid cached relay list
|
||||
relayListCache.has(resolvedPubkey).then(async (hasCached) => {
|
||||
// Check if we have a valid cached relay list (kind:10002)
|
||||
replaceableEventCache.has(resolvedPubkey, 10002).then(async (hasCached) => {
|
||||
if (hasCached) {
|
||||
console.debug(
|
||||
`[ProfileViewer] Using cached relay list for ${resolvedPubkey.slice(0, 8)}`,
|
||||
);
|
||||
|
||||
// Load cached event into EventStore so UI can display it
|
||||
const cached = await relayListCache.get(resolvedPubkey);
|
||||
const cached = await replaceableEventCache.get(resolvedPubkey, 10002);
|
||||
if (cached?.event) {
|
||||
eventStore.add(cached.event);
|
||||
console.debug(
|
||||
@@ -135,7 +135,7 @@ export function ProfileViewer({ pubkey }: ProfileViewerProps) {
|
||||
}
|
||||
|
||||
// First, check cache for instant display
|
||||
blossomServerCache.getServers(resolvedPubkey).then((cachedServers) => {
|
||||
replaceableEventCache.getBlossomServers(resolvedPubkey).then((cachedServers) => {
|
||||
if (cachedServers && cachedServers.length > 0) {
|
||||
setBlossomServers(cachedServers);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { toast } from "sonner";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
import type { ParsedSpellbook } from "@/types/spell";
|
||||
import { relayListCache } from "@/services/relay-list-cache";
|
||||
import replaceableEventCache from "@/services/replaceable-event-cache";
|
||||
|
||||
interface ShareSpellbookDialogProps {
|
||||
open: boolean;
|
||||
@@ -43,7 +43,9 @@ export function ShareSpellbookDialog({
|
||||
let relays = event.tags.filter((t) => t[0] === "r").map((t) => t[1]);
|
||||
|
||||
if (relays.length === 0) {
|
||||
const authorRelays = await relayListCache.getOutboxRelays(event.pubkey);
|
||||
const authorRelays = await replaceableEventCache.getOutboxRelays(
|
||||
event.pubkey,
|
||||
);
|
||||
if (authorRelays) {
|
||||
relays = authorRelays;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
FileDown,
|
||||
} from "lucide-react";
|
||||
import { getSeenRelays } from "applesauce-core/helpers/relays";
|
||||
import { relayListCache } from "@/services/relay-list-cache";
|
||||
import replaceableEventCache from "@/services/replaceable-event-cache";
|
||||
import { AGGREGATOR_RELAYS } from "@/services/loaders";
|
||||
import { useLiveTimeline } from "@/hooks/useLiveTimeline";
|
||||
|
||||
@@ -184,7 +184,9 @@ export function ZapstoreAppDetailRenderer({
|
||||
}
|
||||
|
||||
// Add publisher's outbox relays
|
||||
const outboxRelays = relayListCache.getOutboxRelaysSync(event.pubkey);
|
||||
const outboxRelays = replaceableEventCache.getOutboxRelaysSync(
|
||||
event.pubkey,
|
||||
);
|
||||
if (outboxRelays) {
|
||||
for (const relay of outboxRelays.slice(0, 3)) {
|
||||
relaySet.add(relay);
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useMemo } from "react";
|
||||
import { useGrimoire } from "@/core/state";
|
||||
import { FileDown } from "lucide-react";
|
||||
import { getSeenRelays } from "applesauce-core/helpers/relays";
|
||||
import { relayListCache } from "@/services/relay-list-cache";
|
||||
import replaceableEventCache from "@/services/replaceable-event-cache";
|
||||
import { AGGREGATOR_RELAYS } from "@/services/loaders";
|
||||
import { useLiveTimeline } from "@/hooks/useLiveTimeline";
|
||||
|
||||
@@ -47,7 +47,9 @@ export function ZapstoreAppRenderer({ event }: BaseEventProps) {
|
||||
}
|
||||
|
||||
// Add publisher's outbox relays
|
||||
const outboxRelays = relayListCache.getOutboxRelaysSync(event.pubkey);
|
||||
const outboxRelays = replaceableEventCache.getOutboxRelaysSync(
|
||||
event.pubkey,
|
||||
);
|
||||
if (outboxRelays) {
|
||||
for (const relay of outboxRelays.slice(0, 3)) {
|
||||
relaySet.add(relay);
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
/**
|
||||
* Blossom Server Cache Service
|
||||
*
|
||||
* Wrapper around generic ReplaceableEventCache for BUD-03 blossom server lists (kind:10063).
|
||||
* Provides convenient helpers for accessing blossom servers.
|
||||
*
|
||||
* Now uses the generic cache for storage - parsing happens on-demand.
|
||||
*/
|
||||
|
||||
import { getServersFromEvent } from "./blossom";
|
||||
import replaceableEventCache from "./replaceable-event-cache";
|
||||
import type { IEventStore } from "applesauce-core/event-store";
|
||||
|
||||
const BLOSSOM_SERVER_LIST_KIND = 10063;
|
||||
|
||||
class BlossomServerCache {
|
||||
/**
|
||||
* Subscribe to EventStore to auto-cache kind:10063 events
|
||||
* @deprecated - Now handled by generic ReplaceableEventCache
|
||||
* Kept for backward compatibility with existing code
|
||||
*/
|
||||
subscribeToEventStore(_eventStore: IEventStore): void {
|
||||
console.warn(
|
||||
"[BlossomServerCache] subscribeToEventStore is deprecated - kind:10063 is now auto-cached by ReplaceableEventCache",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from EventStore
|
||||
* @deprecated - Now handled by generic ReplaceableEventCache
|
||||
* Kept for backward compatibility with existing code
|
||||
*/
|
||||
unsubscribe(): void {
|
||||
console.warn(
|
||||
"[BlossomServerCache] unsubscribe is deprecated - managed by ReplaceableEventCache",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blossom servers from memory cache only (synchronous, fast)
|
||||
* Used for real-time operations where async Dexie lookup would be too slow
|
||||
* Returns null if not in memory cache
|
||||
*/
|
||||
getServersSync(pubkey: string): string[] | null {
|
||||
const event = replaceableEventCache.getSync(
|
||||
pubkey,
|
||||
BLOSSOM_SERVER_LIST_KIND,
|
||||
);
|
||||
if (!event) return null;
|
||||
|
||||
// Parse on-demand
|
||||
return getServersFromEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blossom servers for a pubkey from cache
|
||||
*/
|
||||
async getServers(pubkey: string): Promise<string[] | null> {
|
||||
const event = await replaceableEventCache.getEvent(
|
||||
pubkey,
|
||||
BLOSSOM_SERVER_LIST_KIND,
|
||||
);
|
||||
if (!event) return null;
|
||||
|
||||
// Parse on-demand
|
||||
return getServersFromEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have a valid cache entry for a pubkey
|
||||
*/
|
||||
async has(pubkey: string): Promise<boolean> {
|
||||
return replaceableEventCache.has(pubkey, BLOSSOM_SERVER_LIST_KIND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate (delete) cache entry for a pubkey
|
||||
*/
|
||||
async invalidate(pubkey: string): Promise<void> {
|
||||
return replaceableEventCache.invalidate(pubkey, BLOSSOM_SERVER_LIST_KIND);
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const blossomServerCache = new BlossomServerCache();
|
||||
export default blossomServerCache;
|
||||
@@ -2,7 +2,7 @@ import { ActionRunner } from "applesauce-actions";
|
||||
import eventStore from "./event-store";
|
||||
import { EventFactory } from "applesauce-core/event-factory";
|
||||
import pool from "./relay-pool";
|
||||
import { relayListCache } from "./relay-list-cache";
|
||||
import replaceableEventCache from "./replaceable-event-cache";
|
||||
import { getSeenRelays } from "applesauce-core/helpers/relays";
|
||||
import type { NostrEvent } from "nostr-tools/core";
|
||||
import accountManager from "./accounts";
|
||||
@@ -15,7 +15,7 @@ import accountManager from "./accounts";
|
||||
*/
|
||||
export async function publishEvent(event: NostrEvent): Promise<void> {
|
||||
// Try to get author's outbox relays from EventStore (kind 10002)
|
||||
let relays = await relayListCache.getOutboxRelays(event.pubkey);
|
||||
let relays = await replaceableEventCache.getOutboxRelays(event.pubkey);
|
||||
|
||||
// Fallback to relays from the event itself (where it was seen)
|
||||
if (!relays || relays.length === 0) {
|
||||
|
||||
@@ -16,7 +16,7 @@ vi.mock("./event-store", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./relay-list-cache", () => ({
|
||||
relayListCache: {
|
||||
replaceableEventCache: {
|
||||
getOutboxRelaysSync: vi.fn(),
|
||||
},
|
||||
}));
|
||||
@@ -38,7 +38,7 @@ vi.mock("applesauce-loaders/loaders", () => ({
|
||||
}));
|
||||
|
||||
import eventStore from "./event-store";
|
||||
import { relayListCache } from "./relay-list-cache";
|
||||
import replaceableEventCache from "./replaceable-event-cache";
|
||||
|
||||
// Test helpers
|
||||
function createMockEvent(overrides: Partial<NostrEvent> = {}): NostrEvent {
|
||||
@@ -108,7 +108,7 @@ describe("eventLoader", () => {
|
||||
|
||||
describe("backward compatibility with string authorHint", () => {
|
||||
it("should accept string pubkey as context", () => {
|
||||
vi.mocked(relayListCache.getOutboxRelaysSync).mockReturnValue([
|
||||
vi.mocked(replaceableEventCache.getOutboxRelaysSync).mockReturnValue([
|
||||
"wss://author-relay.com/",
|
||||
]);
|
||||
|
||||
@@ -123,7 +123,7 @@ describe("eventLoader", () => {
|
||||
});
|
||||
|
||||
it("should use cached relays when authorHint provided", () => {
|
||||
vi.mocked(relayListCache.getOutboxRelaysSync).mockReturnValue([
|
||||
vi.mocked(replaceableEventCache.getOutboxRelaysSync).mockReturnValue([
|
||||
"wss://cached1.com/",
|
||||
"wss://cached2.com/",
|
||||
"wss://cached3.com/",
|
||||
@@ -195,7 +195,7 @@ describe("eventLoader", () => {
|
||||
});
|
||||
|
||||
it("should extract author hint from p tags", () => {
|
||||
vi.mocked(relayListCache.getOutboxRelaysSync).mockReturnValue([
|
||||
vi.mocked(replaceableEventCache.getOutboxRelaysSync).mockReturnValue([
|
||||
"wss://author-outbox.com/",
|
||||
]);
|
||||
|
||||
@@ -214,7 +214,7 @@ describe("eventLoader", () => {
|
||||
});
|
||||
|
||||
it("should combine all relay sources", () => {
|
||||
vi.mocked(relayListCache.getOutboxRelaysSync).mockReturnValue([
|
||||
vi.mocked(replaceableEventCache.getOutboxRelaysSync).mockReturnValue([
|
||||
"wss://cached.com/",
|
||||
]);
|
||||
|
||||
@@ -272,7 +272,7 @@ describe("eventLoader", () => {
|
||||
});
|
||||
|
||||
it("should prioritize seen relays over cached relays", () => {
|
||||
vi.mocked(relayListCache.getOutboxRelaysSync).mockReturnValue([
|
||||
vi.mocked(replaceableEventCache.getOutboxRelaysSync).mockReturnValue([
|
||||
"wss://cached.com/",
|
||||
]);
|
||||
|
||||
@@ -292,7 +292,7 @@ describe("eventLoader", () => {
|
||||
|
||||
describe("deduplication", () => {
|
||||
it("should deduplicate same relay from different sources", () => {
|
||||
vi.mocked(relayListCache.getOutboxRelaysSync).mockReturnValue([
|
||||
vi.mocked(replaceableEventCache.getOutboxRelaysSync).mockReturnValue([
|
||||
"wss://duplicate.com/",
|
||||
]);
|
||||
|
||||
@@ -364,7 +364,7 @@ describe("eventLoader", () => {
|
||||
it("should use existing event author when event is in store", () => {
|
||||
const existingEvent = createMockEvent({ pubkey: "existing-author" });
|
||||
vi.mocked(eventStore.getEvent).mockReturnValue(existingEvent);
|
||||
vi.mocked(relayListCache.getOutboxRelaysSync).mockReturnValue([
|
||||
vi.mocked(replaceableEventCache.getOutboxRelaysSync).mockReturnValue([
|
||||
"wss://existing-author-relay.com/",
|
||||
]);
|
||||
|
||||
@@ -381,7 +381,7 @@ describe("eventLoader", () => {
|
||||
|
||||
it("should fall back to aggregators when no other relays available", () => {
|
||||
vi.mocked(eventStore.getEvent).mockReturnValue(undefined);
|
||||
vi.mocked(relayListCache.getOutboxRelaysSync).mockReturnValue([]);
|
||||
vi.mocked(replaceableEventCache.getOutboxRelaysSync).mockReturnValue([]);
|
||||
|
||||
const event = createMockEvent({ tags: [] });
|
||||
|
||||
@@ -397,7 +397,7 @@ describe("eventLoader", () => {
|
||||
});
|
||||
|
||||
it("should limit cached relays to 3", () => {
|
||||
vi.mocked(relayListCache.getOutboxRelaysSync).mockReturnValue([
|
||||
vi.mocked(replaceableEventCache.getOutboxRelaysSync).mockReturnValue([
|
||||
"wss://cached1.com/",
|
||||
"wss://cached2.com/",
|
||||
"wss://cached3.com/",
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getEventPointerFromETag } from "applesauce-core/helpers/pointers";
|
||||
import { getTagValue } from "applesauce-core/helpers/event";
|
||||
import pool from "./relay-pool";
|
||||
import eventStore from "./event-store";
|
||||
import { relayListCache } from "./relay-list-cache";
|
||||
import replaceableEventCache from "./replaceable-event-cache";
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
|
||||
/**
|
||||
@@ -117,12 +117,13 @@ export function eventLoader(
|
||||
const existingEvent = eventStore.getEvent(pointer.id);
|
||||
if (existingEvent) {
|
||||
cachedOutboxRelays =
|
||||
relayListCache.getOutboxRelaysSync(existingEvent.pubkey) || [];
|
||||
replaceableEventCache.getOutboxRelaysSync(existingEvent.pubkey) || [];
|
||||
}
|
||||
|
||||
// If not in store but we have author hint (from reply "p" tag)
|
||||
if (cachedOutboxRelays.length === 0 && authorHint) {
|
||||
cachedOutboxRelays = relayListCache.getOutboxRelaysSync(authorHint) || [];
|
||||
cachedOutboxRelays =
|
||||
replaceableEventCache.getOutboxRelaysSync(authorHint) || [];
|
||||
}
|
||||
|
||||
// Limit cached relays to top 3 to avoid too many connections
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
/**
|
||||
* Relay List Cache Service
|
||||
*
|
||||
* Wrapper around generic ReplaceableEventCache for NIP-65 relay lists (kind:10002).
|
||||
* Provides convenient helpers for accessing inbox/outbox relays.
|
||||
*
|
||||
* Now uses the generic cache for storage - parsing happens on-demand using applesauce helpers.
|
||||
*/
|
||||
|
||||
import { getInboxes, getOutboxes } from "applesauce-core/helpers";
|
||||
import { normalizeRelayURL } from "@/lib/relay-url";
|
||||
import replaceableEventCache from "./replaceable-event-cache";
|
||||
import type { IEventStore } from "applesauce-core/event-store";
|
||||
|
||||
const RELAY_LIST_KIND = 10002;
|
||||
|
||||
class RelayListCache {
|
||||
/**
|
||||
* Subscribe to EventStore to auto-cache kind:10002 events
|
||||
* @deprecated - Now handled by generic ReplaceableEventCache
|
||||
* Kept for backward compatibility with existing code
|
||||
*/
|
||||
subscribeToEventStore(_eventStore: IEventStore): void {
|
||||
console.warn(
|
||||
"[RelayListCache] subscribeToEventStore is deprecated - kind:10002 is now auto-cached by ReplaceableEventCache",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from EventStore
|
||||
* @deprecated - Now handled by generic ReplaceableEventCache
|
||||
* Kept for backward compatibility with existing code
|
||||
*/
|
||||
unsubscribe(): void {
|
||||
console.warn(
|
||||
"[RelayListCache] unsubscribe is deprecated - managed by ReplaceableEventCache",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get outbox relays from memory cache only (synchronous, fast)
|
||||
* Used for real-time operations where async Dexie lookup would be too slow
|
||||
* Returns null if not in memory cache
|
||||
*/
|
||||
getOutboxRelaysSync(pubkey: string): string[] | null {
|
||||
const event = replaceableEventCache.getSync(pubkey, RELAY_LIST_KIND);
|
||||
if (!event) return null;
|
||||
|
||||
// Parse and normalize on-demand (applesauce caches this)
|
||||
const writeRelays = getOutboxes(event);
|
||||
return this.normalizeRelays(writeRelays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get outbox (write) relays for a pubkey from cache
|
||||
*/
|
||||
async getOutboxRelays(pubkey: string): Promise<string[] | null> {
|
||||
const event = await replaceableEventCache.getEvent(pubkey, RELAY_LIST_KIND);
|
||||
if (!event) return null;
|
||||
|
||||
// Parse and normalize on-demand (applesauce caches this)
|
||||
const writeRelays = getOutboxes(event);
|
||||
return this.normalizeRelays(writeRelays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inbox (read) relays for a pubkey from cache
|
||||
*/
|
||||
async getInboxRelays(pubkey: string): Promise<string[] | null> {
|
||||
const event = await replaceableEventCache.getEvent(pubkey, RELAY_LIST_KIND);
|
||||
if (!event) return null;
|
||||
|
||||
// Parse and normalize on-demand (applesauce caches this)
|
||||
const readRelays = getInboxes(event);
|
||||
return this.normalizeRelays(readRelays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize relay URLs and filter invalid ones
|
||||
*/
|
||||
private normalizeRelays(relays: string[]): string[] {
|
||||
return relays
|
||||
.map((url) => {
|
||||
try {
|
||||
return normalizeRelayURL(url);
|
||||
} catch {
|
||||
console.warn(`[RelayListCache] Invalid relay URL: ${url}`);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((url): url is string => url !== null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have a valid cache entry for a pubkey
|
||||
*/
|
||||
async has(pubkey: string): Promise<boolean> {
|
||||
return replaceableEventCache.has(pubkey, RELAY_LIST_KIND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate (delete) cache entry for a pubkey
|
||||
*/
|
||||
async invalidate(pubkey: string): Promise<void> {
|
||||
return replaceableEventCache.invalidate(pubkey, RELAY_LIST_KIND);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached relay list entry for a pubkey
|
||||
* Returns the full cached entry with event and parsed data
|
||||
*/
|
||||
async get(
|
||||
pubkey: string,
|
||||
): Promise<{ event: any; read: string[]; write: string[] } | null> {
|
||||
const event = await replaceableEventCache.getEvent(pubkey, RELAY_LIST_KIND);
|
||||
if (!event) return null;
|
||||
|
||||
const read = this.normalizeRelays(getInboxes(event));
|
||||
const write = this.normalizeRelays(getOutboxes(event));
|
||||
|
||||
return { event, read, write };
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached relay lists (for testing)
|
||||
*/
|
||||
async clear(): Promise<void> {
|
||||
return replaceableEventCache.clearKind(RELAY_LIST_KIND);
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
export const relayListCache = new RelayListCache();
|
||||
export default relayListCache;
|
||||
@@ -7,7 +7,7 @@ import { selectRelaysForFilter } from "./relay-selection";
|
||||
import { EventStore } from "applesauce-core";
|
||||
import type { NostrEvent } from "nostr-tools";
|
||||
import { finalizeEvent, generateSecretKey, getPublicKey } from "nostr-tools";
|
||||
import relayListCache from "./relay-list-cache";
|
||||
import replaceableEventCache from "./replaceable-event-cache";
|
||||
|
||||
// Helper to create valid test events
|
||||
function createRelayListEvent(
|
||||
@@ -40,7 +40,7 @@ describe("selectRelaysForFilter", () => {
|
||||
beforeEach(async () => {
|
||||
eventStore = new EventStore();
|
||||
// Clear the relay list cache to ensure test isolation
|
||||
await relayListCache.clear();
|
||||
await replaceableEventCache.clearKind(10002);
|
||||
});
|
||||
|
||||
describe("fallback behavior", () => {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { selectOptimalRelays } from "applesauce-core/helpers";
|
||||
import { addressLoader, AGGREGATOR_RELAYS } from "./loaders";
|
||||
import { normalizeRelayURL } from "@/lib/relay-url";
|
||||
import liveness from "./relay-liveness";
|
||||
import relayListCache from "./relay-list-cache";
|
||||
import replaceableEventCache from "./replaceable-event-cache";
|
||||
import type {
|
||||
RelaySelectionResult,
|
||||
RelaySelectionReasoning,
|
||||
@@ -92,7 +92,7 @@ async function getOutboxRelaysForPubkey(
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
// Check cache first
|
||||
const cachedRelays = await relayListCache.getOutboxRelays(pubkey);
|
||||
const cachedRelays = await replaceableEventCache.getOutboxRelays(pubkey);
|
||||
if (cachedRelays) {
|
||||
console.debug(
|
||||
`[RelaySelection] Using cached outbox relays for ${pubkey.slice(0, 8)} (${cachedRelays.length} relays)`,
|
||||
@@ -189,7 +189,7 @@ async function getInboxRelaysForPubkey(
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
// Check cache first
|
||||
const cachedRelays = await relayListCache.getInboxRelays(pubkey);
|
||||
const cachedRelays = await replaceableEventCache.getInboxRelays(pubkey);
|
||||
if (cachedRelays) {
|
||||
console.debug(
|
||||
`[RelaySelection] Using cached inbox relays for ${pubkey.slice(0, 8)} (${cachedRelays.length} relays)`,
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
import { getTagValue } from "applesauce-core/helpers/event";
|
||||
import { getInboxes, getOutboxes } from "applesauce-core/helpers";
|
||||
import { normalizeRelayURL } from "@/lib/relay-url";
|
||||
import { getServersFromEvent } from "./blossom";
|
||||
import db, { CachedReplaceableEvent } from "./db";
|
||||
import type { IEventStore } from "applesauce-core/event-store";
|
||||
import type { Subscription } from "rxjs";
|
||||
@@ -517,6 +520,77 @@ class ReplaceableEventCache {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Convenience Helpers for Common Operations =====
|
||||
|
||||
/**
|
||||
* Get outbox (write) relays for a pubkey from kind:10002 (NIP-65)
|
||||
*/
|
||||
async getOutboxRelays(pubkey: string): Promise<string[] | null> {
|
||||
const event = await this.getEvent(pubkey, 10002);
|
||||
if (!event) return null;
|
||||
|
||||
const relays = getOutboxes(event);
|
||||
return this.normalizeRelays(relays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get outbox relays from memory cache only (synchronous, fast)
|
||||
*/
|
||||
getOutboxRelaysSync(pubkey: string): string[] | null {
|
||||
const event = this.getSync(pubkey, 10002);
|
||||
if (!event) return null;
|
||||
|
||||
const relays = getOutboxes(event);
|
||||
return this.normalizeRelays(relays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inbox (read) relays for a pubkey from kind:10002 (NIP-65)
|
||||
*/
|
||||
async getInboxRelays(pubkey: string): Promise<string[] | null> {
|
||||
const event = await this.getEvent(pubkey, 10002);
|
||||
if (!event) return null;
|
||||
|
||||
const relays = getInboxes(event);
|
||||
return this.normalizeRelays(relays);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blossom servers for a pubkey from kind:10063 (BUD-03)
|
||||
*/
|
||||
async getBlossomServers(pubkey: string): Promise<string[] | null> {
|
||||
const event = await this.getEvent(pubkey, 10063);
|
||||
if (!event) return null;
|
||||
|
||||
return getServersFromEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blossom servers from memory cache only (synchronous, fast)
|
||||
*/
|
||||
getBlossomServersSync(pubkey: string): string[] | null {
|
||||
const event = this.getSync(pubkey, 10063);
|
||||
if (!event) return null;
|
||||
|
||||
return getServersFromEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize relay URLs and filter invalid ones
|
||||
*/
|
||||
private normalizeRelays(relays: string[]): string[] {
|
||||
return relays
|
||||
.map((url) => {
|
||||
try {
|
||||
return normalizeRelayURL(url);
|
||||
} catch {
|
||||
console.warn(`[ReplaceableEventCache] Invalid relay URL: ${url}`);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((url): url is string => url !== null);
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
|
||||
Reference in New Issue
Block a user