Skip to content

Commit 97b0666

Browse files
authored
Merge pull request #16 from pheuberger/claude/fix-bookmark-undo-persistence-Y0fTB
Fix bookmark deletion sync conflicts with undo/redo operations
2 parents 08219ae + 143e081 commit 97b0666

1 file changed

Lines changed: 48 additions & 9 deletions

File tree

src/hooks/useNostrSync.js

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { useState, useEffect, useCallback, useRef } from 'react'
2222
import * as Y from 'yjs'
2323
import { NostrSyncService, CONNECTION_STATES } from '../services/nostr-sync'
2424
import { retrieveLEK } from '../services/key-storage'
25-
import { getYdocInstance } from './useYjs'
25+
import { getYdocInstance, getUndoManager } from './useYjs'
2626
import { getNostrDiagnostics } from '../services/nostr-diagnostics'
2727
import {
2828
SyncPerformanceManager,
@@ -405,18 +405,36 @@ export function useNostrSync(options = {}) {
405405
if (deletedBookmarkIds.has(bookmarkId)) {
406406
return
407407
}
408-
deletedBookmarkIds.add(bookmarkId)
409-
// Limit set size
410-
if (deletedBookmarkIds.size > 500) {
411-
const idsToRemove = [...deletedBookmarkIds].slice(0, 250)
412-
idsToRemove.forEach(id => deletedBookmarkIds.delete(id))
413-
}
414408

415409
// Defer to not block main thread
416410
setTimeout(() => {
417411
const ydoc = getYdocInstance()
418412
if (ydoc) {
419413
const bookmarksMap = ydoc.getMap('bookmarks')
414+
const existing = bookmarksMap.get(bookmarkId)
415+
416+
// Check if local bookmark exists and is newer than deletion event
417+
// This handles the case where user undid a deletion - the restored
418+
// bookmark will have a newer updatedAt than the deletion event
419+
if (existing) {
420+
const updatedAt = existing?.get ? existing.get('updatedAt') : existing?.updatedAt
421+
// Nostr events use seconds, our timestamps use milliseconds
422+
const deletionTime = event?.created_at ? event.created_at * 1000 : 0
423+
424+
if (updatedAt && updatedAt > deletionTime) {
425+
console.log('[useNostrSync] Skipping deletion - local bookmark is newer:', bookmarkId, { updatedAt, deletionTime })
426+
return
427+
}
428+
}
429+
430+
// Track this deletion
431+
deletedBookmarkIds.add(bookmarkId)
432+
// Limit set size
433+
if (deletedBookmarkIds.size > 500) {
434+
const idsToRemove = [...deletedBookmarkIds].slice(0, 250)
435+
idsToRemove.forEach(id => deletedBookmarkIds.delete(id))
436+
}
437+
420438
// Use transaction with 'nostr-sync' origin so observer knows not to re-publish
421439
ydoc.transact(() => {
422440
bookmarksMap.delete(bookmarkId)
@@ -457,23 +475,44 @@ export function useNostrSync(options = {}) {
457475
return
458476
}
459477

478+
// Check if this is an undo/redo operation
479+
const undoManager = getUndoManager()
480+
const isUndoRedo = ymapEvent.transaction.origin === undoManager
481+
460482
ymapEvent.changes.keys.forEach((change, key) => {
461483
if (nostrSyncService && nostrSyncService.isInitialized) {
462484
if (change.action === 'add' || change.action === 'update') {
463485
const bookmarkYMap = bookmarksMap.get(key)
464486
if (bookmarkYMap) {
465487
// Convert Y.Map to plain object for publishing
466-
const bookmarkData = bookmarkYMap.get ? {
488+
let bookmarkData = bookmarkYMap.get ? {
467489
url: bookmarkYMap.get('url'),
468490
title: bookmarkYMap.get('title'),
469491
description: bookmarkYMap.get('description') || '',
470492
tags: bookmarkYMap.get('tags')?.toArray() || [],
471493
readLater: bookmarkYMap.get('readLater') || false,
494+
inbox: bookmarkYMap.get('inbox') || false,
472495
favicon: bookmarkYMap.get('favicon') || null,
473496
preview: bookmarkYMap.get('preview') || null,
474497
createdAt: bookmarkYMap.get('createdAt'),
475498
updatedAt: bookmarkYMap.get('updatedAt'),
476-
} : bookmarkYMap // Already a plain object
499+
} : { ...bookmarkYMap } // Already a plain object, clone it
500+
501+
// If this is an undo restoration (bookmark was deleted and now restored),
502+
// bump updatedAt to ensure it's newer than any deletion events on relays.
503+
// This prevents the deletion from being replayed on refresh.
504+
if (isUndoRedo && change.action === 'add') {
505+
const newUpdatedAt = Date.now()
506+
console.log('[useNostrSync] Undo restoration detected, bumping updatedAt:', key, { old: bookmarkData.updatedAt, new: newUpdatedAt })
507+
bookmarkData = { ...bookmarkData, updatedAt: newUpdatedAt }
508+
509+
// Persist the updated timestamp to IndexedDB
510+
// Use 'nostr-sync' origin to avoid re-triggering this observer
511+
ydoc.transact(() => {
512+
bookmarksMap.set(key, bookmarkData)
513+
}, 'nostr-sync')
514+
}
515+
477516
// Queue for debounced publishing
478517
nostrSyncService.queueBookmarkUpdate(key, bookmarkData)
479518
}

0 commit comments

Comments
 (0)