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'
79import { 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) {
175173export 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) {
233232export 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) {
255260export 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) {
284293export 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) {
390405export 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