mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-07-27 15:30:01 +02:00
perf: Make NIP-17 inbox sync on-demand for optimal login performance
This commit eliminates automatic inbox initialization on login, preventing
unwanted network requests and heavy I/O operations. Inbox sync now only
activates when users explicitly enable it.
## Problem
When logging in, the app would automatically:
- Initialize gift wrap service immediately
- Auto-enable inbox sync without user consent
- Load encrypted content from Dexie
- Wait up to 1 second for cache readiness
- Fetch inbox relay lists from network
- Subscribe to gift wrap events
- Open persistent relay connections
This caused:
- App hangs during login (network/IO blocking)
- Unwanted network activity before user opts in
- Poor performance on initial load
- Unnecessary resource consumption when DMs not needed
## Solution
### 1. On-Demand Initialization (useAccountSync.ts)
**Before**: Auto-init and auto-enable on every login
**After**: Watch settings$ and only init when user enables
```typescript
// Only initialize when user explicitly enables inbox sync
const settingsSub = giftWrapService.settings$.subscribe((settings) => {
if (settings.enabled && giftWrapService.userPubkey !== pubkey) {
giftWrapService.init(pubkey, signer);
}
});
```
### 2. Early Exit for Disabled State (gift-wrap.ts)
**Before**: Always loaded cache and relays, then checked enabled flag
**After**: Check enabled FIRST, exit early if disabled
```typescript
async init(pubkey: string, signer: ISigner | null) {
// Set basic properties
this.userPubkey = pubkey;
this.signer = signer;
// Early exit if disabled (prevents expensive operations)
if (!this.settings$.value.enabled) {
return;
}
// Only do expensive operations when enabled
await getStoredEncryptedContentIds();
await this.waitForCacheReady();
this.loadInboxRelays();
// ...
}
```
### 3. Updated Documentation
- Clarified on-demand initialization flow
- Updated lifecycle documentation with performance notes
- Changed "auto-enable" section to "on-demand" section
## Performance Impact
**Login Performance**:
- ✅ No automatic Dexie reads
- ✅ No cache waiting (up to 1s saved)
- ✅ No network requests for inbox relays
- ✅ No relay subscriptions until needed
- ✅ Instant login when DMs not needed
**User Control**:
- Users explicitly opt-in via InboxViewer toggle
- Clear UI feedback about enabling inbox sync
- No surprise network activity
**When Enabled**:
- Full functionality identical to before
- All optimizations from previous commits preserved
## Testing
- ✅ All 864 tests pass
- ✅ Build succeeds with no errors
- ✅ Verified on-demand initialization flow
- ✅ Confirmed no auto-init on login
## Files Changed
- `src/hooks/useAccountSync.ts` - Watch settings, init only when enabled
- `src/services/gift-wrap.ts` - Early exit if disabled, expose userPubkey
- `src/components/InboxViewer.tsx` - Updated comments
- `docs/gift-wrap-architecture.md` - Updated flow and lifecycle docs
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -124,14 +124,15 @@ Chat Component
|
||||
|
||||
### Receiving Messages (Inbox Flow)
|
||||
|
||||
1. **Account Login** → `useAccountSync` calls `giftWrapService.init(pubkey, signer)`
|
||||
2. **Fetch Inbox Relays** → Load kind 10050 from user's outbox relays
|
||||
3. **Subscribe to Gift Wraps** → Open subscription to inbox relays for `kind 1059` with `#p` = user pubkey
|
||||
4. **Gift Wrap Arrival** → EventStore receives event → GiftWrapService detects new gift wrap
|
||||
5. **Decrypt** (if auto-decrypt enabled) → Call `unlockGiftWrap(event, signer)`
|
||||
6. **Extract Rumor** → Get kind 14 DM from gift wrap inner content
|
||||
7. **Group into Conversations** → Compute conversation ID from participants → Update `conversations$` observable
|
||||
8. **UI Update** → InboxViewer/ChatViewer re-renders with new messages
|
||||
1. **User Enables Inbox Sync** → User toggles "Enable Inbox Sync" in InboxViewer settings
|
||||
2. **Service Initialization** → `useAccountSync` detects enabled setting and calls `giftWrapService.init(pubkey, signer)`
|
||||
3. **Fetch Inbox Relays** → Load kind 10050 from user's outbox relays
|
||||
4. **Subscribe to Gift Wraps** → Open subscription to inbox relays for `kind 1059` with `#p` = user pubkey
|
||||
5. **Gift Wrap Arrival** → EventStore receives event → GiftWrapService detects new gift wrap
|
||||
6. **Decrypt** (if auto-decrypt enabled) → Call `unlockGiftWrap(event, signer)`
|
||||
7. **Extract Rumor** → Get kind 14 DM from gift wrap inner content
|
||||
8. **Group into Conversations** → Compute conversation ID from participants → Update `conversations$` observable
|
||||
9. **UI Update** → InboxViewer/ChatViewer re-renders with new messages
|
||||
|
||||
### Sending Messages (Outbox Flow)
|
||||
|
||||
@@ -161,18 +162,19 @@ Chat Component
|
||||
|
||||
### Lifecycle
|
||||
|
||||
**Init** (on account login):
|
||||
**Init** (when user enables inbox sync):
|
||||
```typescript
|
||||
giftWrapService.init(pubkey, signer)
|
||||
1. Load persisted encrypted content IDs from Dexie
|
||||
2. Wait for cache readiness (prevents race condition)
|
||||
3. Subscribe to user's kind 10050 (inbox relays)
|
||||
4. Load stored gift wraps from Dexie into EventStore
|
||||
5. Subscribe to EventStore timeline for real-time updates
|
||||
6. Open persistent relay subscription for new gift wraps
|
||||
1. Check if enabled (early return if disabled for performance)
|
||||
2. Load persisted encrypted content IDs from Dexie
|
||||
3. Wait for cache readiness (prevents race condition)
|
||||
4. Subscribe to user's kind 10050 (inbox relays)
|
||||
5. Load stored gift wraps from Dexie into EventStore
|
||||
6. Subscribe to EventStore timeline for real-time updates
|
||||
7. Open persistent relay subscription for new gift wraps
|
||||
```
|
||||
|
||||
**Cleanup** (on account logout):
|
||||
**Cleanup** (on account logout or disable):
|
||||
```typescript
|
||||
giftWrapService.cleanup()
|
||||
1. Unsubscribe from all observables
|
||||
@@ -180,6 +182,8 @@ giftWrapService.cleanup()
|
||||
3. Clear in-memory state
|
||||
```
|
||||
|
||||
**Performance Note**: Init is only called when user explicitly enables inbox sync via InboxViewer toggle. This prevents automatic network requests and heavy I/O operations on login.
|
||||
|
||||
## Cache Strategy
|
||||
|
||||
### Encrypted Content Persistence
|
||||
@@ -231,13 +235,16 @@ giftWrapService.cleanup()
|
||||
- Clear UI feedback about why sending is blocked
|
||||
- Relay lists can be fetched in background without blocking UI
|
||||
|
||||
### Auto-Enable Inbox Sync
|
||||
### On-Demand Inbox Sync
|
||||
|
||||
**Default**: Inbox sync is **auto-enabled** on first login to ensure users receive DMs.
|
||||
**Default**: Inbox sync is **disabled** on login for optimal performance.
|
||||
|
||||
**Rationale**: Better UX to auto-enable with opt-out than require manual setup.
|
||||
**Rationale**: Prevents automatic network requests and heavy I/O operations on login. Users must explicitly enable inbox sync to receive DMs.
|
||||
|
||||
**User Control**: Settings UI allows disabling inbox sync and auto-decrypt.
|
||||
**User Control**:
|
||||
- Enable/disable inbox sync via toggle in InboxViewer
|
||||
- Configure auto-decrypt behavior in settings
|
||||
- Service initializes only when explicitly enabled
|
||||
|
||||
### Relay List Privacy
|
||||
|
||||
|
||||
@@ -51,8 +51,9 @@ function InboxViewer() {
|
||||
|
||||
const [isDecryptingAll, setIsDecryptingAll] = useState(false);
|
||||
|
||||
// Note: Gift wrap service is now initialized globally in useAccountSync
|
||||
// This ensures DM subscriptions are active even when inbox viewer isn't open
|
||||
// Note: Gift wrap service initializes ON-DEMAND when user enables inbox sync
|
||||
// This prevents automatic network requests and heavy I/O on login
|
||||
// Toggle "Enable Inbox Sync" below to start receiving DMs
|
||||
|
||||
// Update signer when it changes (in case user switches signers)
|
||||
useEffect(() => {
|
||||
|
||||
@@ -127,12 +127,10 @@ export function useAccountSync() {
|
||||
};
|
||||
}, [activeAccount?.pubkey, eventStore, setActiveAccountBlossomServers]);
|
||||
|
||||
// Initialize gift wrap service for NIP-17 DM subscriptions when account changes
|
||||
// Initialize gift wrap service ONLY when user enables inbox sync
|
||||
// This ensures no automatic network requests or heavy operations on login
|
||||
useEffect(() => {
|
||||
if (!activeAccount?.pubkey) {
|
||||
console.log(
|
||||
"[useAccountSync] No active account, cleaning up gift wrap service",
|
||||
);
|
||||
giftWrapService.cleanup();
|
||||
return;
|
||||
}
|
||||
@@ -140,28 +138,22 @@ export function useAccountSync() {
|
||||
const pubkey = activeAccount.pubkey;
|
||||
const signer = activeAccount.signer ?? null;
|
||||
|
||||
console.log(
|
||||
`[useAccountSync] Initializing gift wrap service for user ${pubkey.slice(0, 8)}`,
|
||||
);
|
||||
|
||||
// Initialize the service (loads inbox relays, sets up subscriptions)
|
||||
giftWrapService.init(pubkey, signer);
|
||||
|
||||
// Auto-enable inbox sync if not already set
|
||||
// This ensures users receive DMs without manually enabling inbox
|
||||
const currentSettings = giftWrapService.settings$.value;
|
||||
if (!currentSettings.enabled) {
|
||||
console.log(
|
||||
"[useAccountSync] Auto-enabling inbox sync for NIP-17 DM subscriptions",
|
||||
);
|
||||
giftWrapService.updateSettings({ enabled: true });
|
||||
}
|
||||
// Watch settings changes and init when enabled
|
||||
const settingsSub = giftWrapService.settings$.subscribe((settings) => {
|
||||
if (settings.enabled) {
|
||||
// Only init if not already initialized for this account
|
||||
if (giftWrapService.userPubkey !== pubkey) {
|
||||
console.log(
|
||||
`[useAccountSync] Initializing gift wrap service for user ${pubkey.slice(0, 8)}`,
|
||||
);
|
||||
giftWrapService.init(pubkey, signer);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup on account change or logout
|
||||
return () => {
|
||||
console.log(
|
||||
`[useAccountSync] Cleaning up gift wrap service for user ${pubkey.slice(0, 8)}`,
|
||||
);
|
||||
settingsSub.unsubscribe();
|
||||
giftWrapService.cleanup();
|
||||
};
|
||||
}, [activeAccount?.pubkey, activeAccount?.signer]);
|
||||
|
||||
@@ -81,8 +81,8 @@ function saveSettings(settings: InboxSettings) {
|
||||
}
|
||||
|
||||
class GiftWrapService {
|
||||
/** Current user's pubkey */
|
||||
private userPubkey: string | null = null;
|
||||
/** Current user pubkey (null if not initialized) */
|
||||
userPubkey: string | null = null;
|
||||
/** Current signer for decryption */
|
||||
private signer: ISigner | null = null;
|
||||
|
||||
@@ -192,6 +192,15 @@ class GiftWrapService {
|
||||
this.decryptStates$.next(new Map());
|
||||
this.pendingCount$.next(0);
|
||||
|
||||
// Only perform expensive operations if inbox sync is enabled
|
||||
// This prevents automatic network requests and heavy I/O on login
|
||||
if (!this.settings$.value.enabled) {
|
||||
dmDebug("GiftWrap", "Inbox sync disabled, skipping initialization");
|
||||
return;
|
||||
}
|
||||
|
||||
dmInfo("GiftWrap", `Initializing inbox sync for ${pubkey.slice(0, 8)}`);
|
||||
|
||||
// Load persisted encrypted content IDs to know which gift wraps are already decrypted
|
||||
this.persistedIds = await getStoredEncryptedContentIds();
|
||||
|
||||
@@ -225,11 +234,9 @@ class GiftWrapService {
|
||||
});
|
||||
this.subscriptions.push(updateSub);
|
||||
|
||||
// If enabled, load stored gift wraps and start syncing
|
||||
if (this.settings$.value.enabled) {
|
||||
await this.loadStoredGiftWraps();
|
||||
this.startSync();
|
||||
}
|
||||
// Load stored gift wraps and start syncing
|
||||
await this.loadStoredGiftWraps();
|
||||
this.startSync();
|
||||
}
|
||||
|
||||
/** Load stored gift wraps from Dexie into EventStore */
|
||||
|
||||
Reference in New Issue
Block a user