251 lines
6.5 KiB
JavaScript
251 lines
6.5 KiB
JavaScript
import Service, { service } from '@ember/service';
|
|
import { tracked } from '@glimmer/tracking';
|
|
import { EventStore } from 'applesauce-core/event-store';
|
|
import { ProfileModel } from 'applesauce-core/models/profile';
|
|
import { MailboxesModel } from 'applesauce-core/models/mailboxes';
|
|
import { npubEncode } from 'applesauce-core/helpers/pointers';
|
|
import { persistEventsToCache } from 'applesauce-core/helpers/event-cache';
|
|
import { NostrIDB, openDB } from 'nostr-idb';
|
|
import { normalizeRelayUrl } from '../utils/nostr';
|
|
|
|
const DIRECTORY_RELAYS = [
|
|
'wss://purplepag.es',
|
|
'wss://relay.damus.io',
|
|
'wss://nos.lol',
|
|
];
|
|
|
|
const DEFAULT_READ_RELAYS = ['wss://nostr.kosmos.org'];
|
|
const DEFAULT_WRITE_RELAYS = [];
|
|
|
|
export default class NostrDataService extends Service {
|
|
@service nostrRelay;
|
|
@service nostrAuth;
|
|
@service settings;
|
|
|
|
store = new EventStore();
|
|
|
|
@tracked profile = null;
|
|
@tracked mailboxes = null;
|
|
@tracked blossomServers = [];
|
|
|
|
_profileSub = null;
|
|
_mailboxesSub = null;
|
|
_blossomSub = null;
|
|
|
|
_requestSub = null;
|
|
_cachePromise = null;
|
|
|
|
constructor() {
|
|
super(...arguments);
|
|
|
|
// Initialize the IndexedDB cache
|
|
this._cachePromise = openDB('applesauce-events').then(async (db) => {
|
|
this.cache = new NostrIDB(db, {
|
|
cacheIndexes: 1000,
|
|
maxEvents: 10000,
|
|
});
|
|
|
|
await this.cache.start();
|
|
|
|
// Automatically persist new events to the cache
|
|
this._stopPersisting = persistEventsToCache(
|
|
this.store,
|
|
async (events) => {
|
|
// Only cache profiles, mailboxes, and blossom servers
|
|
const toCache = events.filter(
|
|
(e) => e.kind === 0 || e.kind === 10002 || e.kind === 10063
|
|
);
|
|
|
|
if (toCache.length > 0) {
|
|
await Promise.all(toCache.map((event) => this.cache.add(event)));
|
|
}
|
|
},
|
|
{
|
|
batchTime: 1000, // Batch writes every 1 second
|
|
maxBatchSize: 100,
|
|
}
|
|
);
|
|
});
|
|
|
|
// Feed events from the relay pool into the event store
|
|
this.nostrRelay.pool.relays$.subscribe(() => {
|
|
// Setup relay subscription tracking if needed, or we just rely on request()
|
|
// which returns an Observable<NostrEvent>
|
|
});
|
|
}
|
|
|
|
get defaultReadRelays() {
|
|
const mailboxes = (this.mailboxes?.inboxes || [])
|
|
.map(normalizeRelayUrl)
|
|
.filter(Boolean);
|
|
const defaults = DEFAULT_READ_RELAYS.map(normalizeRelayUrl).filter(Boolean);
|
|
return Array.from(new Set([...defaults, ...mailboxes]));
|
|
}
|
|
|
|
get defaultWriteRelays() {
|
|
const mailboxes = (this.mailboxes?.outboxes || [])
|
|
.map(normalizeRelayUrl)
|
|
.filter(Boolean);
|
|
const defaults =
|
|
DEFAULT_WRITE_RELAYS.map(normalizeRelayUrl).filter(Boolean);
|
|
return Array.from(new Set([...defaults, ...mailboxes]));
|
|
}
|
|
|
|
get activeReadRelays() {
|
|
if (this.settings.nostrReadRelays) {
|
|
return Array.from(
|
|
new Set(
|
|
this.settings.nostrReadRelays.map(normalizeRelayUrl).filter(Boolean)
|
|
)
|
|
);
|
|
}
|
|
return this.defaultReadRelays;
|
|
}
|
|
|
|
get activeWriteRelays() {
|
|
if (this.settings.nostrWriteRelays) {
|
|
return Array.from(
|
|
new Set(
|
|
this.settings.nostrWriteRelays.map(normalizeRelayUrl).filter(Boolean)
|
|
)
|
|
);
|
|
}
|
|
return this.defaultWriteRelays;
|
|
}
|
|
|
|
async loadProfile(pubkey) {
|
|
if (!pubkey) return;
|
|
|
|
// Reset state
|
|
this.profile = null;
|
|
this.mailboxes = null;
|
|
this.blossomServers = [];
|
|
|
|
this._cleanupSubscriptions();
|
|
|
|
// Setup models to track state reactively FIRST
|
|
// This way, if cached events populate the store, the UI updates instantly.
|
|
this._profileSub = this.store
|
|
.model(ProfileModel, pubkey)
|
|
.subscribe((profileContent) => {
|
|
this.profile = profileContent;
|
|
});
|
|
|
|
this._mailboxesSub = this.store
|
|
.model(MailboxesModel, pubkey)
|
|
.subscribe((mailboxesData) => {
|
|
this.mailboxes = mailboxesData;
|
|
});
|
|
|
|
this._blossomSub = this.store
|
|
.replaceable(10063, pubkey)
|
|
.subscribe((event) => {
|
|
if (event && event.tags) {
|
|
this.blossomServers = event.tags
|
|
.filter((t) => t[0] === 'server' && t[1])
|
|
.map((t) => t[1]);
|
|
} else {
|
|
this.blossomServers = [];
|
|
}
|
|
});
|
|
|
|
// 1. Await cache initialization and populate the EventStore with local data
|
|
try {
|
|
await this._cachePromise;
|
|
|
|
const cachedEvents = await this.cache.query([
|
|
{
|
|
authors: [pubkey],
|
|
kinds: [0, 10002, 10063],
|
|
},
|
|
]);
|
|
|
|
if (cachedEvents && cachedEvents.length > 0) {
|
|
for (const event of cachedEvents) {
|
|
this.store.add(event);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to read from local Nostr IDB cache', e);
|
|
}
|
|
|
|
// 2. Request new events from the network in the background and dump them into the store
|
|
const profileRelays = Array.from(
|
|
new Set([...DIRECTORY_RELAYS, ...this.activeWriteRelays])
|
|
);
|
|
this._requestSub = this.nostrRelay.pool
|
|
.request(profileRelays, [
|
|
{
|
|
authors: [pubkey],
|
|
kinds: [0, 10002, 10063],
|
|
},
|
|
])
|
|
.subscribe({
|
|
next: (event) => {
|
|
this.store.add(event);
|
|
},
|
|
error: (err) => {
|
|
console.error('Error fetching profile events:', err);
|
|
},
|
|
});
|
|
}
|
|
|
|
get userDisplayName() {
|
|
if (this.profile) {
|
|
if (this.profile.nip05) {
|
|
return this.profile.nip05;
|
|
}
|
|
if (this.profile.displayName || this.profile.display_name) {
|
|
return this.profile.displayName || this.profile.display_name;
|
|
}
|
|
if (this.profile.name) {
|
|
return this.profile.name;
|
|
}
|
|
}
|
|
|
|
// Fallback to npub
|
|
if (this.nostrAuth.pubkey) {
|
|
try {
|
|
const npub = npubEncode(this.nostrAuth.pubkey);
|
|
return `${npub.slice(0, 9)}...${npub.slice(-4)}`;
|
|
} catch {
|
|
return this.nostrAuth.pubkey;
|
|
}
|
|
}
|
|
|
|
return 'Not connected';
|
|
}
|
|
|
|
_cleanupSubscriptions() {
|
|
if (this._requestSub) {
|
|
this._requestSub.unsubscribe();
|
|
this._requestSub = null;
|
|
}
|
|
if (this._profileSub) {
|
|
this._profileSub.unsubscribe();
|
|
this._profileSub = null;
|
|
}
|
|
if (this._mailboxesSub) {
|
|
this._mailboxesSub.unsubscribe();
|
|
this._mailboxesSub = null;
|
|
}
|
|
if (this._blossomSub) {
|
|
this._blossomSub.unsubscribe();
|
|
this._blossomSub = null;
|
|
}
|
|
}
|
|
|
|
willDestroy() {
|
|
super.willDestroy(...arguments);
|
|
this._cleanupSubscriptions();
|
|
|
|
if (this._stopPersisting) {
|
|
this._stopPersisting();
|
|
}
|
|
|
|
if (this.cache) {
|
|
this.cache.stop();
|
|
}
|
|
}
|
|
}
|