Skip to content

Commit f78d556

Browse files
authored
Merge pull request #15 from pheuberger/claude/fix-bookmark-undo-delete-TXvn9
Simplify bookmarks storage: use plain objects instead of nested Yjs types
2 parents 39c5383 + b73af47 commit f78d556

1 file changed

Lines changed: 152 additions & 81 deletions

File tree

src/services/bookmarks.js

Lines changed: 152 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
/**
22
* Bookmark Service
33
* CRUD operations for bookmarks using Yjs
4+
*
5+
* Bookmarks are stored as plain objects in a Y.Map for simplicity.
6+
* This gives us sync + undo/redo without the complexity of nested Yjs types.
47
*/
58

6-
import * as Y from 'yjs'
79
import { getYdocInstance, LOCAL_ORIGIN } from '../hooks/useYjs'
810

911
// Helper to get ydoc instance
@@ -139,24 +141,20 @@ export function createBookmark(bookmarkData) {
139141
const id = `bookmark:${generateId()}`
140142
const now = Date.now()
141143

142-
// Create Y.Array for tags and populate it
143-
const tagsArray = new Y.Array()
144-
tagsArray.insert(0, validated.tags)
145-
146-
// Create Y.Map for bookmark
147-
const bookmark = new Y.Map([
148-
['id', id],
149-
['url', validated.url],
150-
['title', validated.title],
151-
['description', validated.description],
152-
['tags', tagsArray],
153-
['readLater', validated.readLater],
154-
['inbox', validated.inbox],
155-
['favicon', validated.favicon],
156-
['preview', validated.preview],
157-
['createdAt', now],
158-
['updatedAt', now],
159-
])
144+
// Create plain object for bookmark
145+
const bookmark = {
146+
id,
147+
url: validated.url,
148+
title: validated.title,
149+
description: validated.description,
150+
tags: validated.tags,
151+
readLater: validated.readLater,
152+
inbox: validated.inbox,
153+
favicon: validated.favicon,
154+
preview: validated.preview,
155+
createdAt: now,
156+
updatedAt: now,
157+
}
160158

161159
// Add to bookmarks map (wrapped in transaction for undo support)
162160
const doc = getYdoc()
@@ -175,39 +173,40 @@ export function createBookmark(bookmarkData) {
175173
export function updateBookmark(id, updates) {
176174
const doc = getYdoc()
177175
const bookmarksMap = doc.getMap('bookmarks')
178-
const bookmark = bookmarksMap.get(id)
176+
const raw = bookmarksMap.get(id)
179177

180-
if (!bookmark) {
178+
if (!raw) {
181179
throw new Error(`Bookmark not found: ${id}`)
182180
}
183181

182+
// Convert to plain object (handles Y.Map migration)
183+
const existing = toPlainObject(id, raw)
184+
184185
// Merge with existing for validation
185-
const existing = bookmarkToObject(id, bookmark)
186186
const merged = { ...existing, ...updates }
187187
const validated = validateBookmark(merged)
188188

189-
// Wrap all updates in a single transaction for undo support
190-
doc.transact(() => {
191-
if (updates.title !== undefined) bookmark.set('title', validated.title)
192-
if (updates.url !== undefined) bookmark.set('url', validated.url)
193-
if (updates.description !== undefined) bookmark.set('description', validated.description)
194-
if (updates.readLater !== undefined) bookmark.set('readLater', validated.readLater)
195-
if (updates.inbox !== undefined) bookmark.set('inbox', validated.inbox)
196-
if (updates.favicon !== undefined) bookmark.set('favicon', validated.favicon)
197-
if (updates.preview !== undefined) bookmark.set('preview', validated.preview)
198-
199-
// Update tags (replace entire array)
200-
if (updates.tags !== undefined) {
201-
const tagsArray = bookmark.get('tags')
202-
tagsArray.delete(0, tagsArray.length) // Clear
203-
tagsArray.insert(0, validated.tags) // Insert new
204-
}
189+
// Create updated bookmark object
190+
const updated = {
191+
...existing,
192+
url: validated.url,
193+
title: validated.title,
194+
description: validated.description,
195+
tags: validated.tags,
196+
readLater: validated.readLater,
197+
inbox: validated.inbox,
198+
favicon: validated.favicon,
199+
preview: validated.preview,
200+
updatedAt: Date.now(),
201+
}
205202

206-
bookmark.set('updatedAt', Date.now())
203+
// Replace entire bookmark (wrapped in transaction for undo support)
204+
doc.transact(() => {
205+
bookmarksMap.set(id, updated)
207206
}, LOCAL_ORIGIN)
208207

209208
console.log('[Bookmarks] Updated:', id)
210-
return bookmarkToObject(id, bookmark)
209+
return bookmarkToObject(id, updated)
211210
}
212211

213212
/**
@@ -233,20 +232,26 @@ export function deleteBookmark(id) {
233232
export function toggleReadLater(id) {
234233
const doc = getYdoc()
235234
const bookmarksMap = doc.getMap('bookmarks')
236-
const bookmark = bookmarksMap.get(id)
235+
const raw = bookmarksMap.get(id)
237236

238-
if (!bookmark) {
237+
if (!raw) {
239238
throw new Error(`Bookmark not found: ${id}`)
240239
}
241240

242-
const current = bookmark.get('readLater')
241+
const bookmark = toPlainObject(id, raw)
242+
const newValue = !bookmark.readLater
243+
const updated = {
244+
...bookmark,
245+
readLater: newValue,
246+
updatedAt: Date.now(),
247+
}
248+
243249
doc.transact(() => {
244-
bookmark.set('readLater', !current)
245-
bookmark.set('updatedAt', Date.now())
250+
bookmarksMap.set(id, updated)
246251
}, LOCAL_ORIGIN)
247252

248-
console.log('[Bookmarks] Toggled read-later:', id, !current)
249-
return !current
253+
console.log('[Bookmarks] Toggled read-later:', id, newValue)
254+
return newValue
250255
}
251256

252257
/**
@@ -255,24 +260,28 @@ export function toggleReadLater(id) {
255260
export function addTag(id, tag) {
256261
const doc = getYdoc()
257262
const bookmarksMap = doc.getMap('bookmarks')
258-
const bookmark = bookmarksMap.get(id)
263+
const raw = bookmarksMap.get(id)
259264

260-
if (!bookmark) {
265+
if (!raw) {
261266
throw new Error(`Bookmark not found: ${id}`)
262267
}
263268

269+
const bookmark = toPlainObject(id, raw)
264270
const normalized = tag.toLowerCase().trim()
265271
if (!normalized) {
266272
throw new Error('Tag cannot be empty')
267273
}
268274

269-
const tags = bookmark.get('tags')
270-
const existingTags = tags.toArray()
275+
const tags = bookmark.tags || []
276+
if (!tags.includes(normalized)) {
277+
const updated = {
278+
...bookmark,
279+
tags: [...tags, normalized],
280+
updatedAt: Date.now(),
281+
}
271282

272-
if (!existingTags.includes(normalized)) {
273283
doc.transact(() => {
274-
tags.push([normalized])
275-
bookmark.set('updatedAt', Date.now())
284+
bookmarksMap.set(id, updated)
276285
}, LOCAL_ORIGIN)
277286
console.log('[Bookmarks] Added tag:', id, normalized)
278287
}
@@ -284,20 +293,26 @@ export function addTag(id, tag) {
284293
export function removeTag(id, tag) {
285294
const doc = getYdoc()
286295
const bookmarksMap = doc.getMap('bookmarks')
287-
const bookmark = bookmarksMap.get(id)
296+
const raw = bookmarksMap.get(id)
288297

289-
if (!bookmark) {
298+
if (!raw) {
290299
throw new Error(`Bookmark not found: ${id}`)
291300
}
292301

302+
const bookmark = toPlainObject(id, raw)
293303
const normalized = tag.toLowerCase().trim()
294-
const tags = bookmark.get('tags')
295-
const index = tags.toArray().indexOf(normalized)
304+
const tags = bookmark.tags || []
305+
const index = tags.indexOf(normalized)
296306

297307
if (index !== -1) {
308+
const updated = {
309+
...bookmark,
310+
tags: tags.filter(t => t !== normalized),
311+
updatedAt: Date.now(),
312+
}
313+
298314
doc.transact(() => {
299-
tags.delete(index, 1)
300-
bookmark.set('updatedAt', Date.now())
315+
bookmarksMap.set(id, updated)
301316
}, LOCAL_ORIGIN)
302317
console.log('[Bookmarks] Removed tag:', id, normalized)
303318
}
@@ -390,15 +405,21 @@ export function createInboxItem(url) {
390405
export function moveFromInbox(id) {
391406
const doc = getYdoc()
392407
const bookmarksMap = doc.getMap('bookmarks')
393-
const bookmark = bookmarksMap.get(id)
408+
const raw = bookmarksMap.get(id)
394409

395-
if (!bookmark) {
410+
if (!raw) {
396411
throw new Error(`Bookmark not found: ${id}`)
397412
}
398413

414+
const bookmark = toPlainObject(id, raw)
415+
const updated = {
416+
...bookmark,
417+
inbox: false,
418+
updatedAt: Date.now(),
419+
}
420+
399421
doc.transact(() => {
400-
bookmark.set('inbox', false)
401-
bookmark.set('updatedAt', Date.now())
422+
bookmarksMap.set(id, updated)
402423
}, LOCAL_ORIGIN)
403424

404425
console.log('[Bookmarks] Moved from inbox:', id)
@@ -411,11 +432,10 @@ export function getAllTags() {
411432
const bookmarksMap = getYdoc().getMap('bookmarks')
412433
const tagsSet = new Set()
413434

414-
for (const [_, bookmark] of bookmarksMap.entries()) {
415-
const tags = bookmark.get('tags')
416-
if (tags) {
417-
tags.toArray().forEach(tag => tagsSet.add(tag))
418-
}
435+
for (const [id, raw] of bookmarksMap.entries()) {
436+
const bookmark = toPlainObject(id, raw)
437+
const tags = bookmark.tags || []
438+
tags.forEach(tag => tagsSet.add(tag))
419439
}
420440

421441
return Array.from(tagsSet).sort()
@@ -444,20 +464,71 @@ function generateId() {
444464
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
445465
}
446466

447-
function bookmarkToObject(id, ymap) {
467+
/**
468+
* Normalize bookmark data to plain object (handles Y.Map migration)
469+
*/
470+
function toPlainObject(id, bookmark) {
471+
const isYMap = typeof bookmark.get === 'function'
472+
473+
if (isYMap) {
474+
const tags = bookmark.get('tags')
475+
return {
476+
id: bookmark.get('id') || id,
477+
url: bookmark.get('url'),
478+
title: bookmark.get('title'),
479+
description: bookmark.get('description') || '',
480+
tags: tags?.toArray?.() || tags || [],
481+
readLater: bookmark.get('readLater') || false,
482+
inbox: bookmark.get('inbox') || false,
483+
favicon: bookmark.get('favicon') || null,
484+
preview: bookmark.get('preview') || null,
485+
createdAt: bookmark.get('createdAt'),
486+
updatedAt: bookmark.get('updatedAt'),
487+
}
488+
}
489+
490+
return bookmark
491+
}
492+
493+
function bookmarkToObject(id, bookmark) {
494+
// Handle both old Y.Map format and new plain object format
495+
// This enables migration from the old nested Yjs types
496+
const isYMap = typeof bookmark.get === 'function'
497+
498+
if (isYMap) {
499+
// Old format: Y.Map with nested Y.Array for tags
500+
const tags = bookmark.get('tags')
501+
return {
502+
_id: id,
503+
id: bookmark.get('id') || id,
504+
type: 'bookmark',
505+
url: bookmark.get('url'),
506+
title: bookmark.get('title'),
507+
description: bookmark.get('description') || '',
508+
tags: tags?.toArray?.() || tags || [],
509+
readLater: bookmark.get('readLater') || false,
510+
inbox: bookmark.get('inbox') || false,
511+
favicon: bookmark.get('favicon') || null,
512+
preview: bookmark.get('preview') || null,
513+
createdAt: bookmark.get('createdAt'),
514+
updatedAt: bookmark.get('updatedAt'),
515+
}
516+
}
517+
518+
// New format: plain object
448519
return {
449520
_id: id,
450-
id: id,
521+
id: bookmark.id || id,
451522
type: 'bookmark',
452-
url: ymap.get('url'),
453-
title: ymap.get('title'),
454-
description: ymap.get('description') || '',
455-
tags: ymap.get('tags')?.toArray() || [],
456-
readLater: ymap.get('readLater') || false,
457-
inbox: ymap.get('inbox') || false,
458-
favicon: ymap.get('favicon') || null,
459-
preview: ymap.get('preview') || null,
460-
createdAt: ymap.get('createdAt'),
461-
updatedAt: ymap.get('updatedAt'),
523+
url: bookmark.url,
524+
title: bookmark.title,
525+
description: bookmark.description || '',
526+
tags: bookmark.tags || [],
527+
readLater: bookmark.readLater || false,
528+
inbox: bookmark.inbox || false,
529+
favicon: bookmark.favicon || null,
530+
preview: bookmark.preview || null,
531+
createdAt: bookmark.createdAt,
532+
updatedAt: bookmark.updatedAt,
462533
}
463534
}

0 commit comments

Comments
 (0)