experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(sync): Phase 1 - separate sync pull from user operations

- Add mergeServerItem() to datastore.ts that preserves server metadata
- Update sync.ts to use direct DB operations instead of addItem/updateItem
- Server _sync metadata now preserved during sync pull without device metadata
- Removes dependency on syncSource parameter for metadata guard
- Keeps syncSource column for backward compatibility (Phase 3 removal)

This implements the mobile pattern where sync pull is architecturally
separate from user-facing save/update commands, preventing accidental
overwrite of server device metadata (_sync.createdBy, _sync.modifiedBy).

+98 -25
+79
backend/electron/datastore.ts
··· 2466 2466 } 2467 2467 2468 2468 /** 2469 + * Merge a server item into local database during sync pull. 2470 + * 2471 + * This function is used ONLY for sync operations and bypasses addItem/updateItem 2472 + * to preserve server metadata (including _sync.createdBy, _sync.modifiedBy) without 2473 + * adding local device metadata. 2474 + * 2475 + * @param serverItem - Item from server with all metadata 2476 + * @returns Object with localId and action taken ('inserted' | 'updated' | 'skipped') 2477 + */ 2478 + export function mergeServerItem(serverItem: { 2479 + id: string; 2480 + type: string; 2481 + content?: string; 2482 + metadata?: Record<string, unknown>; 2483 + createdAt: number; 2484 + updatedAt: number; 2485 + deletedAt?: number; 2486 + }): { localId: string; action: 'inserted' | 'updated' | 'skipped' } { 2487 + const db = getDb(); 2488 + 2489 + // Find local item by syncId 2490 + const localItem = db.prepare( 2491 + 'SELECT * FROM items WHERE syncId = ?' 2492 + ).get(serverItem.id) as Item | undefined; 2493 + 2494 + const serverUpdatedAt = serverItem.updatedAt; 2495 + const timestamp = Date.now(); 2496 + const metadataJson = serverItem.metadata ? JSON.stringify(serverItem.metadata) : '{}'; 2497 + 2498 + // Item doesn't exist locally - insert it 2499 + if (!localItem) { 2500 + const localId = generateId('item'); 2501 + 2502 + db.prepare(` 2503 + INSERT INTO items ( 2504 + id, type, content, metadata, syncId, syncSource, syncedAt, 2505 + createdAt, updatedAt, deletedAt, starred, archived 2506 + ) 2507 + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, 0) 2508 + `).run( 2509 + localId, 2510 + serverItem.type, 2511 + serverItem.content || null, 2512 + metadataJson, 2513 + serverItem.id, 2514 + 'server', 2515 + timestamp, 2516 + serverItem.createdAt, 2517 + serverUpdatedAt, 2518 + serverItem.deletedAt || 0 2519 + ); 2520 + 2521 + return { localId, action: 'inserted' }; 2522 + } 2523 + 2524 + // Item exists - check if server is newer 2525 + if (serverUpdatedAt > localItem.updatedAt) { 2526 + // Server is newer - update local 2527 + db.prepare(` 2528 + UPDATE items 2529 + SET content = ?, metadata = ?, updatedAt = ?, syncedAt = ?, deletedAt = ? 2530 + WHERE id = ? 2531 + `).run( 2532 + serverItem.content || null, 2533 + metadataJson, 2534 + serverUpdatedAt, 2535 + timestamp, 2536 + serverItem.deletedAt || 0, 2537 + localItem.id 2538 + ); 2539 + 2540 + return { localId: localItem.id, action: 'updated' }; 2541 + } 2542 + 2543 + // Local is same or newer - skip 2544 + return { localId: localItem.id, action: 'skipped' }; 2545 + } 2546 + 2547 + /** 2469 2548 * Query items with optional filters 2470 2549 */ 2471 2550 export function queryItems(filter: ItemFilter = {}): Item[] {
+19 -25
backend/electron/sync.ts
··· 23 23 queryItems, 24 24 addItem, 25 25 updateItem, 26 + mergeServerItem, 26 27 getItemTags, 27 28 getOrCreateTag, 28 29 tagItem, ··· 386 387 // Item doesn't exist locally - insert it 387 388 DEBUG && console.log(`[sync] Inserting new item from server: ${serverItem.id}`); 388 389 389 - const { id: localId } = addItem(serverItem.type as ItemType, { 390 - content: serverItem.content || undefined, 391 - metadata: serverItem.metadata ? JSON.stringify(serverItem.metadata) : undefined, 392 - syncId: serverItem.id, 393 - syncSource: 'server', 390 + const { localId } = mergeServerItem({ 391 + id: serverItem.id, 392 + type: serverItem.type, 393 + content: serverItem.content, 394 + metadata: serverItem.metadata, 395 + createdAt: serverItem.createdAt, 396 + updatedAt: serverUpdatedAt, 397 + deletedAt: 0, 394 398 }); 395 - 396 - // Update timestamps to match server, and set syncedAt to track when we synced this item 397 - const now = Date.now(); 398 - db.prepare(` 399 - UPDATE items SET createdAt = ?, updatedAt = ?, syncedAt = ? WHERE id = ? 400 - `).run(serverItem.createdAt, serverUpdatedAt, now, localId); 401 399 402 400 // Add tags 403 401 syncTagsToItem(localId, serverItem.tags); ··· 410 408 if (serverUpdatedAt > localItem.updatedAt) { 411 409 // Server is newer — undelete locally and update content 412 410 DEBUG && console.log(`[sync] Undeleting local item from server: ${serverItem.id}`); 413 - updateItem(localItem.id, { 414 - content: serverItem.content || undefined, 415 - metadata: serverItem.metadata ? JSON.stringify(serverItem.metadata) : undefined, 416 - }); 417 411 const now = Date.now(); 412 + const metadataJson = serverItem.metadata ? JSON.stringify(serverItem.metadata) : '{}'; 418 413 db.prepare(` 419 - UPDATE items SET deletedAt = 0, updatedAt = ?, syncedAt = ? WHERE id = ? 420 - `).run(serverUpdatedAt, now, localItem.id); 414 + UPDATE items 415 + SET content = ?, metadata = ?, deletedAt = 0, updatedAt = ?, syncedAt = ? 416 + WHERE id = ? 417 + `).run(serverItem.content || null, metadataJson, serverUpdatedAt, now, localItem.id); 421 418 syncTagsToItem(localItem.id, serverItem.tags); 422 419 return 'pulled'; 423 420 } ··· 431 428 // Server is newer - update local 432 429 DEBUG && console.log(`[sync] Updating local item from server: ${serverItem.id}`); 433 430 434 - updateItem(localItem.id, { 435 - content: serverItem.content || undefined, 436 - metadata: serverItem.metadata ? JSON.stringify(serverItem.metadata) : undefined, 437 - }); 438 - 439 - // Update timestamps to match server, and set syncedAt to track when we synced this item 440 431 const now = Date.now(); 432 + const metadataJson = serverItem.metadata ? JSON.stringify(serverItem.metadata) : '{}'; 441 433 db.prepare(` 442 - UPDATE items SET updatedAt = ?, syncedAt = ? WHERE id = ? 443 - `).run(serverUpdatedAt, now, localItem.id); 434 + UPDATE items 435 + SET content = ?, metadata = ?, updatedAt = ?, syncedAt = ? 436 + WHERE id = ? 437 + `).run(serverItem.content || null, metadataJson, serverUpdatedAt, now, localItem.id); 444 438 445 439 // Update tags 446 440 syncTagsToItem(localItem.id, serverItem.tags);