From b23d54d74f4c0ad2d2ee7046ea455c24a99e2d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Tue, 21 Apr 2026 14:58:05 +0400 Subject: [PATCH] Cache user profile/settings events in IndexedDB --- app/services/nostr-data.js | 97 +++++++++++++++++++++++++++++++------- package.json | 1 + pnpm-lock.yaml | 18 +++++++ 3 files changed, 98 insertions(+), 18 deletions(-) diff --git a/app/services/nostr-data.js b/app/services/nostr-data.js index c737046..455af6e 100644 --- a/app/services/nostr-data.js +++ b/app/services/nostr-data.js @@ -4,6 +4,8 @@ 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'; const BOOTSTRAP_RELAYS = [ 'wss://purplepag.es', @@ -26,10 +28,40 @@ export default class NostrDataService extends Service { _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() @@ -63,24 +95,8 @@ export default class NostrDataService extends Service { const relayList = Array.from(relays); - // Request events and dump them into the store - this._requestSub = this.nostrRelay.pool - .request(relayList, [ - { - authors: [pubkey], - kinds: [0, 10002, 10063], - }, - ]) - .subscribe({ - next: (event) => { - this.store.add(event); - }, - error: (err) => { - console.error('Error fetching profile events:', err); - }, - }); - - // Setup models to track state reactively + // 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) => { @@ -104,6 +120,43 @@ export default class NostrDataService extends Service { 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 + this._requestSub = this.nostrRelay.pool + .request(relayList, [ + { + authors: [pubkey], + kinds: [0, 10002, 10063], + }, + ]) + .subscribe({ + next: (event) => { + this.store.add(event); + }, + error: (err) => { + console.error('Error fetching profile events:', err); + }, + }); } get userDisplayName() { @@ -154,5 +207,13 @@ export default class NostrDataService extends Service { willDestroy() { super.willDestroy(...arguments); this._cleanupSubscriptions(); + + if (this._stopPersisting) { + this._stopPersisting(); + } + + if (this.cache) { + this.cache.stop(); + } } } diff --git a/package.json b/package.json index aae5195..52fd0fb 100644 --- a/package.json +++ b/package.json @@ -111,6 +111,7 @@ "blurhash": "^2.0.5", "ember-concurrency": "^5.2.0", "ember-lifeline": "^7.0.0", + "nostr-idb": "^5.0.0", "oauth2-pkce": "^2.1.3", "qrcode": "^1.5.4", "rxjs": "^7.8.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad0a0df..b6993c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ importers: ember-lifeline: specifier: ^7.0.0 version: 7.0.0(@ember/test-helpers@5.4.1(@babel/core@7.28.6)) + nostr-idb: + specifier: ^5.0.0 + version: 5.0.0 oauth2-pkce: specifier: ^2.1.3 version: 2.1.3 @@ -3709,6 +3712,9 @@ packages: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} + idb@8.0.3: + resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -4405,6 +4411,9 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + nostr-idb@5.0.0: + resolution: {integrity: sha512-w5y4AnHefZIwCCL11NryfM2xp3U0Ka4qVNQEYAjnQbPwyoV+bZTdwuPXHCdRDWvhOFP2bZr1WBegcsAmkBjrxQ==} + nostr-signer-capacitor-plugin@0.0.5: resolution: {integrity: sha512-/EvqWz71HZ5cWmzvfXWTm48AWZtbeZDbOg3vLwXyXPjnIp1DR7Wurww/Mo41ORNu1DNPlqH20l7kIXKO6vR5og==} peerDependencies: @@ -10286,6 +10295,8 @@ snapshots: dependencies: safer-buffer: 2.1.2 + idb@8.0.3: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -11021,6 +11032,13 @@ snapshots: normalize-path@3.0.0: {} + nostr-idb@5.0.0: + dependencies: + debug: 4.4.3 + idb: 8.0.3 + transitivePeerDependencies: + - supports-color + nostr-signer-capacitor-plugin@0.0.5(@capacitor/core@7.6.2): dependencies: '@capacitor/core': 7.6.2