Browse Source

bug-fix settings

update version
imwald
Silberengel 4 months ago
parent
commit
bd9d5fb247
  1. 2
      package.json
  2. 251
      src/components/CacheRelaysSetting/index.tsx
  3. 4
      src/components/MailboxSetting/NewMailboxRelayInput.tsx
  4. 31
      src/components/MailboxSetting/SaveButton.tsx
  5. 6
      src/components/MailboxSetting/index.tsx
  6. 27
      src/providers/NostrProvider/index.tsx
  7. 4
      src/services/client.service.ts
  8. 72
      src/services/indexed-db.service.ts

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "jumble-imwald", "name": "jumble-imwald",
"version": "11.1", "version": "11.2",
"description": "A user-friendly Nostr client focused on relay feed browsing and relay discovery, forked from Jumble", "description": "A user-friendly Nostr client focused on relay feed browsing and relay discovery, forked from Jumble",
"private": true, "private": true,
"type": "module", "type": "module",

251
src/components/CacheRelaysSetting/index.tsx

@ -2,7 +2,7 @@ import { Button } from '@/components/ui/button'
import { normalizeUrl, isLocalNetworkUrl } from '@/lib/url' import { normalizeUrl, isLocalNetworkUrl } from '@/lib/url'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { TMailboxRelay, TMailboxRelayScope } from '@/types' import { TMailboxRelay, TMailboxRelayScope } from '@/types'
import { useEffect, useState, useMemo, useRef } from 'react' import { useEffect, useState, useMemo, useRef, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
DndContext, DndContext,
@ -28,15 +28,17 @@ import DiscoveredRelays from '../MailboxSetting/DiscoveredRelays'
import { createCacheRelaysDraftEvent } from '@/lib/draft-event' import { createCacheRelaysDraftEvent } from '@/lib/draft-event'
import { getRelayListFromEvent } from '@/lib/event-metadata' import { getRelayListFromEvent } from '@/lib/event-metadata'
import { showPublishingFeedback, showSimplePublishSuccess, showPublishingError } from '@/lib/publishing-feedback' import { showPublishingFeedback, showSimplePublishSuccess, showPublishingError } from '@/lib/publishing-feedback'
import { CloudUpload, Loader, Trash2, RefreshCw, Database, WrapText, Search, X } from 'lucide-react' import { CloudUpload, Loader, Trash2, RefreshCw, Database, WrapText, Search, X, TriangleAlert } from 'lucide-react'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import indexedDb from '@/services/indexed-db.service' import indexedDb from '@/services/indexed-db.service'
import postEditorCache from '@/services/post-editor-cache.service' import postEditorCache from '@/services/post-editor-cache.service'
import { StorageKey } from '@/constants' import { StorageKey } from '@/constants'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription } from '@/components/ui/drawer' import { Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription } from '@/components/ui/drawer'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { toast } from 'sonner' import { toast } from 'sonner'
import { Event } from 'nostr-tools'
export default function CacheRelaysSetting() { export default function CacheRelaysSetting() {
const { t } = useTranslation() const { t } = useTranslation()
@ -374,7 +376,20 @@ export default function CacheRelaysSetting() {
setSearchQuery('') setSearchQuery('')
// Update cache info // Update cache info
loadCacheInfo() loadCacheInfo()
toast.success(t('Cleaned up {{deleted}} duplicate entries, kept {{kept}}', { deleted: result.deleted, kept: result.kept })) // Reload items to get accurate count after cleanup
const itemsAfterCleanup = await indexedDb.getStoreItems(selectedStore)
const actualCount = itemsAfterCleanup.length
// Show message with actual count
if (actualCount !== result.kept) {
toast.success(t('Cleaned up {{deleted}} duplicate entries, kept {{kept}} (total items after cleanup: {{total}})', {
deleted: result.deleted,
kept: result.kept,
total: actualCount
}))
} else {
toast.success(t('Cleaned up {{deleted}} duplicate entries, kept {{kept}}', { deleted: result.deleted, kept: result.kept }))
}
} catch (error) { } catch (error) {
console.error('Failed to cleanup duplicates:', error) console.error('Failed to cleanup duplicates:', error)
if (error instanceof Error && error.message === 'Not a replaceable event store') { if (error instanceof Error && error.message === 'Not a replaceable event store') {
@ -387,6 +402,52 @@ export default function CacheRelaysSetting() {
} }
} }
// Check if an event is invalid
const isInvalidEvent = useCallback((item: { key: string; value: any; addedAt: number }): boolean => {
if (!item || !item.value) return true
const event = item.value as Event
// Check for required Nostr event fields
if (!event.pubkey || !event.kind || typeof event.created_at !== 'number') {
return true
}
// Check for tags array (required for Nostr events)
if (!event.tags || !Array.isArray(event.tags)) {
return true
}
// Check for id and sig (these should be present in valid events)
if (!event.id || !event.sig) {
return true
}
return false
}, [])
// Get explanation for why an event is invalid
const getInvalidEventExplanation = useCallback((item: { key: string; value: any; addedAt: number }): string => {
if (!item || !item.value) {
return t('Event has no value data')
}
const event = item.value as Event
const missing: string[] = []
if (!event.pubkey) missing.push(t('pubkey'))
if (!event.kind) missing.push(t('kind'))
if (typeof event.created_at !== 'number') missing.push(t('created_at'))
if (!event.tags || !Array.isArray(event.tags)) missing.push(t('tags'))
if (!event.id) missing.push(t('id'))
if (!event.sig) missing.push(t('sig'))
if (missing.length > 0) {
return t('Event is missing required fields: {{fields}}', { fields: missing.join(', ') })
}
return t('Event appears to be invalid or corrupted')
}, [t])
const save = async () => { const save = async () => {
if (!pubkey) return if (!pubkey) return
@ -477,10 +538,10 @@ export default function CacheRelaysSetting() {
<div className="text-xs text-muted-foreground space-y-1"> <div className="text-xs text-muted-foreground space-y-1">
<div>{t('Clear cached data stored in your browser, including IndexedDB events, localStorage settings, and service worker caches.')}</div> <div>{t('Clear cached data stored in your browser, including IndexedDB events, localStorage settings, and service worker caches.')}</div>
</div> </div>
<div className="flex flex-row gap-2"> <div className="flex flex-col sm:flex-row gap-2">
<Button <Button
variant="outline" variant="outline"
className="flex-1" className="flex-1 w-full sm:w-auto"
onClick={handleClearCache} onClick={handleClearCache}
> >
<Trash2 className="h-4 w-4 mr-2" /> <Trash2 className="h-4 w-4 mr-2" />
@ -488,7 +549,7 @@ export default function CacheRelaysSetting() {
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
className="flex-1" className="flex-1 w-full sm:w-auto"
onClick={handleRefreshCache} onClick={handleRefreshCache}
> >
<RefreshCw className="h-4 w-4 mr-2" /> <RefreshCw className="h-4 w-4 mr-2" />
@ -496,7 +557,7 @@ export default function CacheRelaysSetting() {
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
className="flex-1" className="flex-1 w-full sm:w-auto"
onClick={handleBrowseCache} onClick={handleBrowseCache}
> >
<Database className="h-4 w-4 mr-2" /> <Database className="h-4 w-4 mr-2" />
@ -581,35 +642,114 @@ export default function CacheRelaysSetting() {
<div className="flex items-center justify-center py-8"> <div className="flex items-center justify-center py-8">
<Loader className="animate-spin h-6 w-6" /> <Loader className="animate-spin h-6 w-6" />
</div> </div>
) : storeItems.length === 0 ? (
<div className="text-sm text-muted-foreground">{t('No items in this store.')}</div>
) : ( ) : (
<div className="space-y-2"> <>
<div className="text-xs text-muted-foreground mb-2"> <div className="relative py-1">
{storeItems.length} {t('items')} <Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
type="text"
placeholder={t('Search items...')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-8"
/>
</div> </div>
{storeItems.map((item, index) => { {storeItems.length === 0 ? (
const nestedCount = (item as any).nestedCount <div className="text-sm text-muted-foreground">{t('No items in this store.')}</div>
return ( ) : (
<div key={item.key || index} className="border rounded-lg p-3 break-words"> <div className="space-y-2">
<div className="font-semibold text-xs mb-2 break-all"> <div className="flex items-center justify-between mb-2">
{item.key} <div className="text-xs text-muted-foreground">
{typeof nestedCount === 'number' && nestedCount > 0 && ( {filteredStoreItems.length} {t('of')} {storeItems.length} {t('items')}
<span className="ml-2 text-muted-foreground"> {searchQuery.trim() && ` ${t('matching')} "${searchQuery}"`}
({nestedCount} {t('nested events')})
</span>
)}
</div> </div>
<div className="text-xs text-muted-foreground mb-2"> <div className="flex gap-2">
{t('Added at')}: {new Date(item.addedAt).toLocaleString()} <Button
variant="outline"
size="sm"
onClick={handleCleanupDuplicates}
className="h-7 text-xs"
>
<RefreshCw className="h-3 w-3 mr-1" />
{t('Cleanup Duplicates')}
</Button>
<Button
variant="destructive"
size="sm"
onClick={handleDeleteAllItems}
className="h-7 text-xs"
>
<Trash2 className="h-3 w-3 mr-1" />
{t('Delete All')}
</Button>
</div> </div>
<pre className={`text-xs bg-muted p-2 rounded overflow-auto max-h-96 select-text ${wordWrapEnabled ? 'overflow-x-hidden whitespace-pre-wrap break-words' : 'overflow-x-auto whitespace-pre'}`}>
{JSON.stringify(item.value, null, 2)}
</pre>
</div> </div>
) {filteredStoreItems.length === 0 ? (
})} <div className="text-sm text-muted-foreground">{t('No items match your search.')}</div>
</div> ) : (
filteredStoreItems.map((item, index) => {
const nestedCount = (item as any).nestedCount
const invalid = isInvalidEvent(item)
const invalidExplanation = invalid ? getInvalidEventExplanation(item) : ''
return (
<div key={item.key || index} className="border rounded-lg p-3 break-words relative">
<div className="absolute top-2 right-2 flex items-center gap-1">
{invalid && (
<HoverCard>
<HoverCardTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 text-amber-600 dark:text-amber-500 hover:text-amber-700 dark:hover:text-amber-400"
title={invalidExplanation}
>
<TriangleAlert className="h-3 w-3" />
</Button>
</HoverCardTrigger>
<HoverCardContent className="w-80">
<div className="space-y-2">
<div className="font-semibold text-sm flex items-center gap-2">
<TriangleAlert className="h-4 w-4 text-amber-600 dark:text-amber-500" />
{t('Invalid Event')}
</div>
<div className="text-sm text-muted-foreground">
{invalidExplanation}
</div>
</div>
</HoverCardContent>
</HoverCard>
)}
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteItem(item.key)}
className="h-6 w-6 p-0"
title={t('Delete item')}
>
<X className="h-3 w-3" />
</Button>
</div>
<div className={`font-semibold text-xs mb-2 break-all ${invalid ? 'pr-16' : 'pr-8'}`}>
{item.key}
{typeof nestedCount === 'number' && nestedCount > 0 && (
<span className="ml-2 text-muted-foreground">
({nestedCount} {t('nested events')})
</span>
)}
</div>
<div className="text-xs text-muted-foreground mb-2">
{t('Added at')}: {new Date(item.addedAt).toLocaleString()}
</div>
<pre className={`text-xs bg-muted p-2 rounded overflow-auto max-h-96 select-text ${wordWrapEnabled ? 'overflow-x-hidden whitespace-pre-wrap break-words' : 'overflow-x-auto whitespace-pre'}`}>
{JSON.stringify(item.value, null, 2)}
</pre>
</div>
)
})
)}
</div>
)}
</>
) )
)} )}
</div> </div>
@ -729,18 +869,47 @@ export default function CacheRelaysSetting() {
) : ( ) : (
filteredStoreItems.map((item, index) => { filteredStoreItems.map((item, index) => {
const nestedCount = (item as any).nestedCount const nestedCount = (item as any).nestedCount
const invalid = isInvalidEvent(item)
const invalidExplanation = invalid ? getInvalidEventExplanation(item) : ''
return ( return (
<div key={item.key || index} className="border rounded-lg p-3 break-words relative"> <div key={item.key || index} className="border rounded-lg p-3 break-words relative">
<Button <div className="absolute top-2 right-2 flex items-center gap-1">
variant="ghost" {invalid && (
size="sm" <HoverCard>
onClick={() => handleDeleteItem(item.key)} <HoverCardTrigger asChild>
className="absolute top-2 right-2 h-6 w-6 p-0" <Button
title={t('Delete item')} variant="ghost"
> size="sm"
<X className="h-3 w-3" /> className="h-6 w-6 p-0 text-amber-600 dark:text-amber-500 hover:text-amber-700 dark:hover:text-amber-400"
</Button> title={invalidExplanation}
<div className="font-semibold text-xs mb-2 break-all pr-8"> >
<TriangleAlert className="h-3 w-3" />
</Button>
</HoverCardTrigger>
<HoverCardContent className="w-80">
<div className="space-y-2">
<div className="font-semibold text-sm flex items-center gap-2">
<TriangleAlert className="h-4 w-4 text-amber-600 dark:text-amber-500" />
{t('Invalid Event')}
</div>
<div className="text-sm text-muted-foreground">
{invalidExplanation}
</div>
</div>
</HoverCardContent>
</HoverCard>
)}
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteItem(item.key)}
className="h-6 w-6 p-0"
title={t('Delete item')}
>
<X className="h-3 w-3" />
</Button>
</div>
<div className={`font-semibold text-xs mb-2 break-all ${invalid ? 'pr-16' : 'pr-8'}`}>
{item.key} {item.key}
{typeof nestedCount === 'number' && nestedCount > 0 && ( {typeof nestedCount === 'number' && nestedCount > 0 && (
<span className="ml-2 text-muted-foreground"> <span className="ml-2 text-muted-foreground">

4
src/components/MailboxSetting/NewMailboxRelayInput.tsx

@ -35,7 +35,7 @@ export default function NewMailboxRelayInput({
return ( return (
<div> <div>
<div className="flex gap-4"> <div className="flex flex-col sm:flex-row gap-4">
<Input <Input
className={newRelayUrlError ? 'border-destructive' : ''} className={newRelayUrlError ? 'border-destructive' : ''}
placeholder={t('Add a new relay')} placeholder={t('Add a new relay')}
@ -44,7 +44,7 @@ export default function NewMailboxRelayInput({
onChange={handleRelayUrlInputChange} onChange={handleRelayUrlInputChange}
onBlur={save} onBlur={save}
/> />
<Button onClick={save}>{t('Add')}</Button> <Button className="w-full sm:w-auto" onClick={save}>{t('Add')}</Button>
</div> </div>
{newRelayUrlError && <div className="text-destructive text-xs mt-1">{newRelayUrlError}</div>} {newRelayUrlError && <div className="text-destructive text-xs mt-1">{newRelayUrlError}</div>}
</div> </div>

31
src/components/MailboxSetting/SaveButton.tsx

@ -1,6 +1,6 @@
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { createRelayListDraftEvent } from '@/lib/draft-event' import { createRelayListDraftEvent } from '@/lib/draft-event'
import { showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishing-feedback' import { showPublishingFeedback, showSimplePublishSuccess, showPublishingError } from '@/lib/publishing-feedback'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { TMailboxRelay } from '@/types' import { TMailboxRelay } from '@/types'
import { CloudUpload, Loader } from 'lucide-react' import { CloudUpload, Loader } from 'lucide-react'
@ -27,16 +27,20 @@ export default function SaveButton({
try { try {
const event = createRelayListDraftEvent(mailboxRelays) const event = createRelayListDraftEvent(mailboxRelays)
const result = await publish(event) const result = await publish(event)
// Read relayStatuses immediately before it might be deleted
const relayStatuses = (result as any).relayStatuses
await updateRelayListEvent(result) await updateRelayListEvent(result)
setHasChange(false) setHasChange(false)
// Show publishing feedback // Show publishing feedback
if ((result as any).relayStatuses) { if (relayStatuses && relayStatuses.length > 0) {
showPublishingFeedback({ showPublishingFeedback({
success: true, success: true,
relayStatuses: (result as any).relayStatuses, relayStatuses: relayStatuses,
successCount: (result as any).relayStatuses.filter((s: any) => s.success).length, successCount: relayStatuses.filter((s: any) => s.success).length,
totalCount: (result as any).relayStatuses.length totalCount: relayStatuses.length
}, { }, {
message: t('Mailbox relays saved'), message: t('Mailbox relays saved'),
duration: 6000 duration: 6000
@ -44,6 +48,23 @@ export default function SaveButton({
} else { } else {
showSimplePublishSuccess(t('Mailbox relays saved')) showSimplePublishSuccess(t('Mailbox relays saved'))
} }
} catch (error) {
console.error('Failed to save relay list:', error)
// Show error feedback with relay statuses if available
if (error instanceof Error && (error as any).relayStatuses) {
const errorRelayStatuses = (error as any).relayStatuses
showPublishingFeedback({
success: false,
relayStatuses: errorRelayStatuses,
successCount: errorRelayStatuses.filter((s: any) => s.success).length,
totalCount: errorRelayStatuses.length
}, {
message: error.message || t('Failed to save relay list'),
duration: 6000
})
} else {
showPublishingError(error instanceof Error ? error : new Error(t('Failed to save relay list')))
}
} finally { } finally {
setPushing(false) setPushing(false)
} }

6
src/components/MailboxSetting/index.tsx

@ -1,5 +1,5 @@
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl, isLocalNetworkUrl } from '@/lib/url'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { TMailboxRelay, TMailboxRelayScope } from '@/types' import { TMailboxRelay, TMailboxRelayScope } from '@/types'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
@ -67,7 +67,9 @@ export default function MailboxSetting() {
useEffect(() => { useEffect(() => {
if (!relayList) return if (!relayList) return
setRelays(relayList.originalRelays) // Filter out cache relays (local network URLs) - they belong in kind 10432, not kind 10002
const mailboxRelays = relayList.originalRelays.filter(relay => !isLocalNetworkUrl(relay.url))
setRelays(mailboxRelays)
}, [relayList]) }, [relayList])
if (!pubkey) { if (!pubkey) {

27
src/providers/NostrProvider/index.tsx

@ -848,13 +848,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
// Attach relayStatuses only temporarily for UI feedback, then remove it // Attach relayStatuses only temporarily for UI feedback, then remove it
// This prevents it from being included in the event when serialized // This prevents it from being included in the event when serialized
// Use a longer delay to ensure UI components can read it before deletion
if (relayStatuses) { if (relayStatuses) {
(event as any).relayStatuses = relayStatuses (event as any).relayStatuses = relayStatuses
// Remove it immediately after return so it's not persisted // Remove it after a delay to allow UI components to read it
// The components that need it will read it synchronously // Components should read it immediately after publish() returns
setTimeout(() => { setTimeout(() => {
delete (event as any).relayStatuses delete (event as any).relayStatuses
}, 0) }, 100)
} }
return event return event
@ -946,17 +947,27 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const updateRelayListEvent = async (relayListEvent: Event) => { const updateRelayListEvent = async (relayListEvent: Event) => {
await indexedDb.putReplaceableEvent(relayListEvent) await indexedDb.putReplaceableEvent(relayListEvent)
// Clear the relay list cache to force a fresh fetch
if (account?.pubkey) {
client.clearRelayListCache(account.pubkey)
}
// Fetch updated relay list (which merges both 10002 and 10432) // Fetch updated relay list (which merges both 10002 and 10432)
const mergedRelayList = await client.fetchRelayList(account?.pubkey || '') const mergedRelayList = await client.fetchRelayList(account?.pubkey || '')
setRelayList(mergedRelayList) setRelayList(mergedRelayList)
} }
const updateCacheRelayListEvent = async (cacheRelayListEvent: Event) => { const updateCacheRelayListEvent = async (cacheRelayListEvent: Event) => {
const newCacheRelayList = await indexedDb.putReplaceableEvent(cacheRelayListEvent) await indexedDb.putReplaceableEvent(cacheRelayListEvent)
setCacheRelayListEvent(newCacheRelayList) // Clear the relay list cache to ensure fresh fetches use the updated event
// Fetch updated relay list (which merges both 10002 and 10432) if (account?.pubkey) {
const mergedRelayList = await client.fetchRelayList(account?.pubkey || '') client.clearRelayListCache(account.pubkey)
setRelayList(mergedRelayList) }
// Set local state immediately with the event we just saved
// This will trigger the component's useEffect to update the UI immediately
setCacheRelayListEvent(cacheRelayListEvent)
// Don't update relayList here - it's a computed merge of kind 10002 + 10432
// The merged list will be computed on-the-fly when needed via fetchRelayList()
// This ensures kind 10002 and 10432 remain separate and are only merged when publishing/using
} }
const updateProfileEvent = async (profileEvent: Event) => { const updateProfileEvent = async (profileEvent: Event) => {

4
src/services/client.service.ts

@ -1285,6 +1285,10 @@ class ClientService extends EventTarget {
return relayEvent ?? null return relayEvent ?? null
} }
clearRelayListCache(pubkey: string) {
this.relayListRequestCache.delete(pubkey)
}
async fetchRelayList(pubkey: string): Promise<TRelayList> { async fetchRelayList(pubkey: string): Promise<TRelayList> {
// Deduplicate concurrent requests for the same pubkey's relay list // Deduplicate concurrent requests for the same pubkey's relay list
const existingRequest = this.relayListRequestCache.get(pubkey) const existingRequest = this.relayListRequestCache.get(pubkey)

72
src/services/indexed-db.service.ts

@ -965,35 +965,59 @@ class IndexedDbService {
const allItems = await this.getStoreItems(storeName) const allItems = await this.getStoreItems(storeName)
const eventMap = new Map<string, { key: string; event: Event; addedAt: number }>() const eventMap = new Map<string, { key: string; event: Event; addedAt: number }>()
const keysToDelete: string[] = [] const keysToDelete: string[] = []
let invalidItemsCount = 0
for (const item of allItems) { for (const item of allItems) {
if (!item || !item.value) continue if (!item || !item.value) {
invalidItemsCount++
continue
}
const replaceableKey = this.getReplaceableEventKeyFromEvent(item.value) // Skip if event doesn't have required fields
const existing = eventMap.get(replaceableKey) if (!item.value.pubkey || !item.value.kind || !item.value.created_at) {
invalidItemsCount++
continue
}
if (!existing || try {
item.value.created_at > existing.event.created_at || const replaceableKey = this.getReplaceableEventKeyFromEvent(item.value)
(item.value.created_at === existing.event.created_at && const existing = eventMap.get(replaceableKey)
item.addedAt > existing.addedAt)) {
// This event is newer, mark the old one for deletion if it exists if (!existing ||
if (existing) { item.value.created_at > existing.event.created_at ||
keysToDelete.push(existing.key) (item.value.created_at === existing.event.created_at &&
item.addedAt > existing.addedAt)) {
// This event is newer, mark the old one for deletion if it exists
if (existing) {
keysToDelete.push(existing.key)
}
eventMap.set(replaceableKey, {
key: item.key,
event: item.value,
addedAt: item.addedAt
})
} else {
// This event is older or same, mark it for deletion
keysToDelete.push(item.key)
} }
eventMap.set(replaceableKey, { } catch (error) {
key: item.key, // If we can't generate a replaceable key, skip this item
event: item.value, console.warn('Failed to get replaceable key for item:', item.key, error)
addedAt: item.addedAt invalidItemsCount++
}) continue
} else {
// This event is older or same, mark it for deletion
keysToDelete.push(item.key)
} }
} }
// Second pass: delete duplicates // Second pass: delete duplicates
const totalProcessed = eventMap.size + keysToDelete.length
const actualKept = eventMap.size
if (keysToDelete.length === 0) { if (keysToDelete.length === 0) {
return Promise.resolve({ deleted: 0, kept: eventMap.size }) // No duplicates found, but verify counts match
if (totalProcessed + invalidItemsCount !== allItems.length) {
console.warn(`Count mismatch: total items=${allItems.length}, processed=${totalProcessed}, invalid=${invalidItemsCount}`)
}
return Promise.resolve({ deleted: 0, kept: actualKept })
} }
return new Promise((resolve) => { return new Promise((resolve) => {
@ -1010,14 +1034,20 @@ class IndexedDbService {
completedCount++ completedCount++
if (completedCount === keysToDelete.length) { if (completedCount === keysToDelete.length) {
transaction.commit() transaction.commit()
resolve({ deleted: deletedCount, kept: eventMap.size }) const actualKept = eventMap.size
const totalProcessed = actualKept + deletedCount
if (totalProcessed + invalidItemsCount !== allItems.length) {
console.warn(`Count mismatch after deletion: total items=${allItems.length}, kept=${actualKept}, deleted=${deletedCount}, invalid=${invalidItemsCount}`)
}
resolve({ deleted: deletedCount, kept: actualKept })
} }
} }
deleteRequest.onerror = () => { deleteRequest.onerror = () => {
completedCount++ completedCount++
if (completedCount === keysToDelete.length) { if (completedCount === keysToDelete.length) {
transaction.commit() transaction.commit()
resolve({ deleted: deletedCount, kept: eventMap.size }) const actualKept = eventMap.size
resolve({ deleted: deletedCount, kept: actualKept })
} }
} }
}) })

Loading…
Cancel
Save