experiments in a post-browser web
10
fork

Configure Feed

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

refactor(sync): remove syncSource from unified sync module and update docs

Replace syncSource-based push filtering with syncedAt timestamp checks
across the unified sync module. Push filter now uses syncedAt === 0
(never synced) instead of syncSource === ''. Also fixes pre-existing
bugs: _mergeServerItem now converts server ISO timestamps via
fromISOString(), and test data uses correct camelCase field names.

Updated all related documentation to reflect syncSource removal status.

+83 -117
+1 -1
DEVELOPMENT.md
··· 687 687 | `tabs.js` | `peek_tabs_enabled` | `from:tab` | Add-and-skip: open tabs imported, listens for `onUpdated` | 688 688 | `history.js` | `peek_history_enabled` | `from:history` | **Update-on-revisit**: existing items get fresh visit metadata via `updateItem()` | 689 689 690 - **Cross-source tagging**: When a URL already exists in Peek (from any source), each sync module still adds its own source tag to the existing item. Stats count by tag, not `syncSource`. This means an item imported via bookmarks and later visited in history will have both `from:bookmark` and `from:history` tags. 690 + **Cross-source tagging**: When a URL already exists in Peek (from any source), each sync module still adds its own source tag to the existing item. Stats count by tag (e.g. `from:bookmark`, `from:history`). This means an item imported via bookmarks and later visited in history will have both `from:bookmark` and `from:history` tags. 691 691 692 692 **Diagnostics**: Stats are fetched from the background script via message passing (`get-bookmark-stats`, `get-tab-stats`, `get-history-stats`), not queried directly from the options page, due to Firefox IndexedDB isolation between extension contexts. 693 693
-1
backend/server/test.js
··· 617 617 const columnNames = tableInfo.map((col) => col.name); 618 618 619 619 assert.ok(columnNames.includes("syncId"), "should have syncId column"); 620 - assert.ok(columnNames.includes("syncSource"), "should have syncSource column"); 621 620 assert.ok(columnNames.includes("syncedAt"), "should have syncedAt column"); 622 621 }); 623 622
+2 -1
backend/tauri-mobile/src-tauri/tauri.conf.json
··· 5 5 "identifier": "com.dietrich.peek-mobile", 6 6 "build": { 7 7 "beforeBuildCommand": "npm run build", 8 - "frontendDist": "../dist" 8 + "frontendDist": "../dist", 9 + "devUrl": "http://192.168.50.143:51684" 9 10 }, 10 11 "app": { 11 12 "windows": [
-1
docs/datastore.md
··· 47 47 | mimeType | TEXT | MIME type (e.g., `text/html`) | 48 48 | metadata | TEXT | JSON for flexible extra data | 49 49 | syncId | TEXT | Server-assigned ID for sync | 50 - | syncSource | TEXT | Origin of sync (`server`, `history`, etc.) | 51 50 | syncedAt | INTEGER | Last sync timestamp (ms) | 52 51 | createdAt | INTEGER | Creation timestamp (ms) | 53 52 | updatedAt | INTEGER | Last update timestamp (ms) |
+8 -5
docs/sync-source-refactor.md
··· 90 90 91 91 ## Changes 92 92 93 - ### 1. Remove syncSource from sync algorithm 93 + ### 1. Remove syncSource from sync algorithm ✅ DONE (desktop, server, unified sync module) 94 94 95 95 **Push query** — replace syncSource filter with timestamp-based: 96 96 ··· 122 122 123 123 **On server change** — reset `syncedAt = 0` to force full re-sync. No need to touch syncSource. 124 124 125 - **Files to update:** 126 - - `backend/electron/sync.ts` — lines 393, 515-525, 619-620, 667-668, 773-775 127 - - `sync/sync.js` — lines 102-107, 162-166, 277-282, 399-400 128 - - `backend/tauri/src-tauri/src/sync.rs` — lines 521-522, 634-665, 799-800, 894 125 + **Files updated:** 126 + - ✅ `backend/electron/sync.ts` — syncSource removed from push filter, post-push update, server-change reset 127 + - ✅ `sync/sync.js` — syncSource replaced with syncedAt-based filtering, fromISOString() added for server timestamp conversion 128 + - ✅ `sync/adapters/better-sqlite3.js` — syncSource removed from schema, migration, INSERT 129 + - ✅ `sync/data.js` — syncSource removed from addItem and saveItem 130 + - ⬜ `backend/tauri/src-tauri/src/sync.rs` — still has sync_source (Tauri desktop) 131 + - ⬜ `backend/tauri-mobile/src-tauri/src/lib.rs` — still has sync_source (iOS mobile) 129 132 130 133 ### 2. Stop setting syncSource on item creation 131 134
+5 -5
docs/sync.md
··· 42 42 43 43 ### Push (Client → Server) 44 44 45 - 1. Query items where `syncSource = ''` OR `updatedAt > lastSyncTime` 46 - - Includes soft-deleted items (those with `deleted_at` set) so tombstones propagate 45 + 1. Query items where `syncedAt = 0` (never synced) OR `updatedAt > syncedAt` (modified since last sync) 46 + - Includes soft-deleted items (those with `deletedAt` set) so tombstones propagate 47 47 2. For each item: `POST /items` with type, content, tags 48 - - If item has `deleted_at`: include it in the push payload so the server records the tombstone 49 - 3. On success: update local `syncId` and `syncSource` 48 + - If item has `deletedAt`: include it in the push payload so the server records the tombstone 49 + 3. On success: update local `syncId` and `syncedAt` 50 50 51 51 ### Conflict Resolution 52 52 ··· 149 149 150 150 ## Server-Change Detection 151 151 152 - When a user changes sync servers (or configures sync for the first time after having synced previously), per-item sync markers (`syncSource`, `syncedAt`, `syncId`) from the old server would prevent items from being pushed to the new server. Both desktop and mobile detect this: 152 + When a user changes sync servers (or configures sync for the first time after having synced previously), per-item sync markers (`syncedAt`, `syncId`) from the old server would prevent items from being pushed to the new server. Both desktop and mobile detect this: 153 153 154 154 1. After every pull or full sync, the current server URL and profile ID are saved to a `settings` table (`lastSyncServerUrl`, `lastSyncProfileId`). 155 155 2. Before `syncAll()`, the stored values are compared to the current config.
+12 -18
notes/plan-syncsource-migration-test.md
··· 8 8 9 9 | Platform | syncSource Status | Details | 10 10 |----------|------------------|---------| 11 - | Desktop Electron (`backend/electron/`) | Removed from schema + code | No `syncSource` in generated schema or sync code | 12 - | Server (`backend/server/`) | Removed from schema + code | No `syncSource` in db.js or index.js | 13 - | Unified sync module (`sync/sync.js`) | **STILL USES IT** | Push filter, post-push update, pending count all reference `syncSource` | 11 + | Desktop Electron (`backend/electron/`) | **Removed** | No `syncSource` in generated schema or sync code | 12 + | Server (`backend/server/`) | **Removed** | No `syncSource` in db.js or index.js | 13 + | Unified sync module (`sync/sync.js`) | **Removed** | Push filter uses `syncedAt === 0`, no syncSource references | 14 14 | iOS mobile (`backend/tauri-mobile/`) | Still has `sync_source` | Actively used for push filtering, merge, post-push | 15 15 | Tauri Desktop (`backend/tauri/`) | Still has `sync_source` | Item struct, column access, sync operations | 16 16 | Browser Extension (`backend/extension/`) | Clean | No references | 17 17 18 18 ## Issues Found During Investigation 19 19 20 - ### Critical: `sync/sync.js` not refactored 21 - 22 - The unified sync module still actively uses `syncSource`: 23 - - `sync.js:102,107` — push filter: `i.syncSource === ''` 24 - - `sync.js:165` — sets `syncSource: 'server'` after push 25 - - `sync.js:217` — pending count: `i.syncSource === ''` 26 - - `sync.js:279` — server change reset: `syncSource: ''` 27 - - `better-sqlite3.js:34` — adapter schema includes `syncSource TEXT DEFAULT ''` 20 + ### ~~Critical: `sync/sync.js` not refactored~~ (DONE) 28 21 29 - If this module is used against a DB without `syncSource`, the `i.syncSource === ''` filter evaluates `undefined === ''` → `false`, causing all items to be excluded from push. 22 + The unified sync module has been refactored: 23 + - Push filter now uses `syncedAt === 0` instead of `syncSource === ''` 24 + - Post-push update no longer sets `syncSource` 25 + - Server change reset no longer resets `syncSource` 26 + - `better-sqlite3.js` schema no longer includes `syncSource` column 27 + - `_mergeServerItem` now uses `fromISOString()` for server timestamp conversion (bugfix) 30 28 31 - ### Stale test assertion 29 + ### ~~Stale test assertion~~ (FIXED) 32 30 33 - `backend/server/test.js:620` asserts `syncSource` column exists: 34 - ```js 35 - assert.ok(columnNames.includes("syncSource")) 36 - ``` 37 - This will fail with the current schema. 31 + `backend/server/test.js:620` — `syncSource` assertion removed. Legacy migration test schemas preserved for backward compatibility testing. 38 32 39 33 ### Behavioral change: extension-origin items 40 34
+5 -6
notes/research-url-history-unification.md
··· 24 24 - **Use case**: Tracking all navigation activity across any source 25 25 26 26 **Items (Curated) - `backend/electron/datastore.ts`:** 27 - - **items table**: Stores user-curated items (type: url|text|tagset|image, content, metadata, syncId, syncSource, starred, archived, visitCount, lastVisitAt) 27 + - **items table**: Stores user-curated items (type: url|text|tagset|image, content, metadata, syncId, starred, archived, visitCount, lastVisitAt) 28 28 - **item_tags table**: M2M junction for tagging items 29 29 - **Use case**: Saved URLs, notes, groups, images with annotations 30 - - **Key difference**: Explicit user creation, sync-aware (syncId, syncSource, syncedAt) 30 + - **Key difference**: Explicit user creation, sync-aware (syncId, syncedAt) 31 31 32 32 **Browser Extension History Import:** 33 33 - **history.js**: One-way import of browser history as URL items (tags with `from:history`) ··· 45 45 | **Visit tracking** | Explicit visits table | visitCount + lastVisitAt only | 46 46 | **Timestamps** | createdAt/updatedAt | createdAt/updatedAt/syncedAt/deletedAt | 47 47 | **Deletion** | Hard delete | Soft delete (deletedAt) | 48 - | **Source tracking** | Source field in visits | syncSource in items | 48 + | **Source tracking** | Source field in visits | Tags (`from:*`) + metadata._sync | 49 49 50 50 --- 51 51 ··· 103 103 1. **Base item** with type, content, metadata 104 104 2. **Annotation layer** (tags, starred, archived, groups) 105 105 3. **Visit tracking** (detailed history with frecency) 106 - 4. **Sync support** (syncId, syncSource, deleted records) 106 + 4. **Sync support** (syncId, syncedAt, deleted records) 107 107 5. **Addressability** (peek:// URLs for all items) 108 108 109 109 ### Proposed Schema ··· 122 122 123 123 -- Sync fields 124 124 syncId TEXT DEFAULT '', 125 - syncSource TEXT DEFAULT '', 126 125 syncedAt INTEGER DEFAULT 0, 127 126 128 127 -- Audit ··· 217 216 api.datastore.addItem(type, content, options) 218 217 // type: 'url' | 'text' | 'tagset' | 'image' 219 218 // content: string (URL for type:url, text for type:text, etc) 220 - // options: { title, metadata, syncSource, syncId } 219 + // options: { title, metadata, syncId } 221 220 222 221 api.datastore.recordVisit(itemId, options) 223 222 // Replaces trackNavigation()
+2 -4
notes/sync-architecture-review.md
··· 42 42 id TEXT PRIMARY KEY, 43 43 type TEXT NOT NULL CHECK(type IN ('url', 'text', 'tagset', 'image')), 44 44 syncId TEXT DEFAULT '', 45 - syncSource TEXT DEFAULT '', 46 45 syncedAt INTEGER DEFAULT 0, 47 46 createdAt INTEGER NOT NULL, 48 47 updatedAt INTEGER NOT NULL, ··· 83 82 84 83 **Migrations Implemented:** 85 84 1. `migrateTinyBaseData()` - Legacy format conversion 86 - 2. `migrateSyncColumns()` - Add syncId, syncSource, syncedAt 85 + 2. `migrateSyncColumns()` - Add syncId, syncedAt 87 86 3. `migrateItemTypes()` - 'note' → 'url'/'text' conversion 88 87 4. `migrateItemVisitColumns()` - Add visitCount, lastVisitAt 89 88 5. `migrateAddressesToItems()` - Convert tagged addresses to items ··· 207 206 type: { type: text, not_null: true, check: "type IN ('url', 'text', 'tagset', 'image')" } 208 207 content: { type: text } 209 208 syncId: { type: text, default: '' } 210 - syncSource: { type: text, default: '' } 211 209 syncedAt: { type: integer, default: 0 } 212 210 createdAt: { type: integer, not_null: true } 213 211 updatedAt: { type: integer, not_null: true } ··· 283 281 // Tauri validation (add to both desktop and mobile) 284 282 fn validate_schema(conn: &Connection) -> Result<(), String> { 285 283 let required = vec![ 286 - ("items", vec!["id", "type", "syncId", "syncSource", "syncedAt", "createdAt", "updatedAt", "deletedAt"]), 284 + ("items", vec!["id", "type", "syncId", "syncedAt", "createdAt", "updatedAt", "deletedAt"]), 287 285 ("tags", vec!["id", "name", "frequency", "lastUsed", "frecencyScore", "createdAt", "updatedAt"]), 288 286 ("item_tags", vec!["itemId", "tagId", "createdAt"]), 289 287 ];
+4 -4
notes/sync-architecture.md
··· 45 45 1. Fetch items: `GET /items` (full) or `GET /items/since/:timestamp` (incremental) 46 46 2. For each server item: 47 47 - Find local item by `syncId` matching server `id` 48 - - If not found: insert with `syncId=server.id`, `syncSource='server'` 48 + - If not found: insert with `syncId=server.id` 49 49 - If found and server is newer (`updated_at > local.updatedAt`): update local 50 50 - If found and local is newer: skip (will be pushed later) 51 51 52 52 ### Push (Desktop → Server) 53 53 54 - 1. Query items where `syncSource = ''` (never synced) OR `updatedAt > lastSyncTime` 54 + 1. Query items where `syncedAt = 0` (never synced) OR `updatedAt > syncedAt` (modified since last sync) 55 55 2. For each item: 56 56 - `POST /items` with type, content, tags, metadata 57 - - On success: update local `syncId` and `syncSource='server'` 57 + - On success: update local `syncId` and `syncedAt` 58 58 59 59 ### Conflict Resolution 60 60 ··· 132 132 mimeType TEXT DEFAULT '', 133 133 metadata TEXT DEFAULT '{}', 134 134 syncId TEXT DEFAULT '', -- Server item ID 135 - syncSource TEXT DEFAULT '', -- 'server' if synced 135 + syncedAt INTEGER DEFAULT 0, -- Last sync timestamp (ms) 136 136 createdAt INTEGER NOT NULL, 137 137 updatedAt INTEGER NOT NULL, 138 138 deletedAt INTEGER DEFAULT 0,
+2 -2
notes/sync-edge-cases.md
··· 37 37 ```javascript 38 38 items = db.prepare(` 39 39 SELECT * FROM items 40 - WHERE deletedAt = 0 AND (syncSource = '' OR updatedAt > ?) 41 - `).all(lastSyncTime); 40 + WHERE deletedAt = 0 AND (syncedAt = 0 OR updatedAt > syncedAt) 41 + `).all(); 42 42 ``` 43 43 44 44 **Server db.js - getItemsSince (line 717)**:
+1 -1
schema/README.md
··· 79 79 ### items 80 80 Unified content storage for URLs, text notes, tagsets, and images. 81 81 82 - **Sync columns**: id, type, content, mimeType, metadata, syncId, syncSource, syncedAt, createdAt, updatedAt, deletedAt, starred, archived 82 + **Sync columns**: id, type, content, mimeType, metadata, syncId, syncedAt, createdAt, updatedAt, deletedAt, starred, archived 83 83 84 84 **Local-only columns**: visitCount, lastVisitAt, frecencyScore, title, domain, favicon 85 85
+1 -1
scripts/e2e-version-test.sh
··· 389 389 else 390 390 log_fail "$PHASE" "$LABEL: got $ITEM_COUNT items, expected $EXPECTED_COUNT" 391 391 log "$PHASE" " Items in DB:" 392 - sqlite3 "$DB_PATH" "SELECT id, type, content, syncSource FROM items WHERE deletedAt = 0;" | while IFS= read -r row; do 392 + sqlite3 "$DB_PATH" "SELECT id, type, content, syncedAt FROM items WHERE deletedAt = 0;" | while IFS= read -r row; do 393 393 log "$PHASE" " $row" 394 394 done 395 395 fi
+2 -6
sync/adapters/better-sqlite3.js
··· 31 31 content TEXT, 32 32 metadata TEXT, 33 33 syncId TEXT DEFAULT '', 34 - syncSource TEXT DEFAULT '', 35 34 syncedAt INTEGER DEFAULT 0, 36 35 createdAt INTEGER NOT NULL, 37 36 updatedAt INTEGER NOT NULL, ··· 90 89 if (!itemColNames.has('syncId')) { 91 90 db.exec("ALTER TABLE items ADD COLUMN syncId TEXT DEFAULT ''"); 92 91 } 93 - if (!itemColNames.has('syncSource')) { 94 - db.exec("ALTER TABLE items ADD COLUMN syncSource TEXT DEFAULT ''"); 95 - } 96 92 if (!itemColNames.has('syncedAt')) { 97 93 db.exec('ALTER TABLE items ADD COLUMN syncedAt INTEGER DEFAULT 0'); 98 94 } ··· 103 99 getItem: db.prepare('SELECT * FROM items WHERE id = ? AND deletedAt = 0'), 104 100 getItemIncludeDeleted: db.prepare('SELECT * FROM items WHERE id = ?'), 105 101 insertItem: db.prepare(` 106 - INSERT INTO items (id, type, content, metadata, syncId, syncSource, syncedAt, createdAt, updatedAt, deletedAt) 107 - VALUES (@id, @type, @content, @metadata, @syncId, @syncSource, @syncedAt, @createdAt, @updatedAt, @deletedAt) 102 + INSERT INTO items (id, type, content, metadata, syncId, syncedAt, createdAt, updatedAt, deletedAt) 103 + VALUES (@id, @type, @content, @metadata, @syncId, @syncedAt, @createdAt, @updatedAt, @deletedAt) 108 104 `), 109 105 deleteItemSoft: db.prepare('UPDATE items SET deletedAt = @deletedAt, updatedAt = @updatedAt WHERE id = @id AND deletedAt = 0'), 110 106 hardDeleteItem: db.prepare('DELETE FROM items WHERE id = ?'),
-1
sync/adapters/interface.js
··· 11 11 * @property {string|null} content 12 12 * @property {string|null} metadata - JSON string 13 13 * @property {string} syncId 14 - * @property {string} syncSource 15 14 * @property {number} syncedAt - Unix ms 16 15 * @property {number} createdAt - Unix ms 17 16 * @property {number} updatedAt - Unix ms
-3
sync/data.js
··· 38 38 * @param {string|null} [options.content] 39 39 * @param {string|null} [options.metadata] - JSON string 40 40 * @param {string} [options.syncId] 41 - * @param {string} [options.syncSource] 42 41 * @param {number} [options.createdAt] - Override creation timestamp (for imports) 43 42 * @returns {Promise<{id: string}>} 44 43 */ ··· 61 60 content: options.content ?? null, 62 61 metadata, 63 62 syncId: options.syncId || '', 64 - syncSource: options.syncSource || '', 65 63 syncedAt: 0, 66 64 createdAt, 67 65 updatedAt: now, ··· 250 248 content: content ?? null, 251 249 metadata: metadataStr, 252 250 syncId: syncId || '', 253 - syncSource: '', 254 251 syncedAt: 0, 255 252 createdAt: timestamp, 256 253 updatedAt: timestamp,
+6 -9
sync/sync.js
··· 99 99 // Incremental: never synced OR locally modified after their last sync 100 100 itemsToPush = allItems.filter( 101 101 i => 102 - i.syncSource === '' || 102 + i.syncedAt === 0 || 103 103 (i.syncedAt > 0 && i.updatedAt > i.syncedAt) 104 104 ); 105 105 } else { 106 106 // Full: all items that haven't been synced 107 - itemsToPush = allItems.filter(i => i.syncSource === ''); 107 + itemsToPush = allItems.filter(i => i.syncedAt === 0); 108 108 } 109 109 110 110 // Also include deleted items that need tombstone push ··· 162 162 // Update local item with sync info 163 163 await this.data.adapter.updateItem(item.id, { 164 164 syncId: response.id, 165 - syncSource: 'server', 166 165 syncedAt: Date.now(), 167 166 }); 168 167 ··· 214 213 const allItems = await this.data.queryItems({ includeDeleted: false }); 215 214 const pendingCount = allItems.filter( 216 215 i => 217 - i.syncSource === '' || 216 + i.syncedAt === 0 || 218 217 (i.syncedAt > 0 && i.updatedAt > i.syncedAt) 219 218 ).length; 220 219 ··· 276 275 const allItems = await this.data.queryItems({ includeDeleted: false }); 277 276 for (const item of allItems) { 278 277 await this.data.adapter.updateItem(item.id, { 279 - syncSource: '', 280 278 syncedAt: 0, 281 279 syncId: '', 282 280 }); ··· 359 357 * @returns {Promise<'pulled'|'conflict'|'skipped'>} 360 358 */ 361 359 async _mergeServerItem(serverItem) { 362 - const serverUpdatedAt = serverItem.updatedAt; 363 - const serverDeletedAt = serverItem.deletedAt || 0; 360 + const serverUpdatedAt = fromISOString(serverItem.updatedAt); 361 + const serverDeletedAt = fromISOString(serverItem.deletedAt) || 0; 364 362 365 363 // Find local item by syncId 366 364 const localItem = await this.data.adapter.findItemBySyncId(serverItem.id); ··· 396 394 ? JSON.stringify(serverItem.metadata) 397 395 : null, 398 396 syncId: serverItem.id, 399 - syncSource: 'server', 400 397 }); 401 398 402 399 // Overwrite timestamps to match server 403 400 await this.data.adapter.updateItem(localId, { 404 - createdAt: serverItem.createdAt, 401 + createdAt: fromISOString(serverItem.createdAt), 405 402 updatedAt: serverUpdatedAt, 406 403 syncedAt: Date.now(), 407 404 });
+32 -48
sync/test.js
··· 235 235 const { id } = await data.addItem('url', { 236 236 content: 'https://example.com', 237 237 syncId: 'server-123', 238 - syncSource: 'server', 239 - }); 238 + }); 240 239 const item = await data.getItem(id); 241 240 assert.strictEqual(item.syncId, 'server-123'); 242 - assert.strictEqual(item.syncSource, 'server'); 243 241 }); 244 242 }); 245 243 ··· 662 660 assert.strictEqual(items.length, 1); 663 661 assert.strictEqual(items[0].content, 'https://from-server.com'); 664 662 assert.strictEqual(items[0].syncId, 'server-1'); 665 - assert.strictEqual(items[0].syncSource, 'server'); 666 663 667 664 const tags = await data.getItemTags(items[0].id); 668 665 assert.strictEqual(tags.length, 1); ··· 691 688 content: 'https://old.com', 692 689 metadata: null, 693 690 syncId: 'server-1', 694 - syncSource: 'server', 695 - syncedAt: 1000, 691 + syncedAt: 1000, 696 692 createdAt: 1000, 697 693 updatedAt: 2000, 698 694 deletedAt: 0, ··· 727 723 content: 'https://local-new.com', 728 724 metadata: null, 729 725 syncId: 'server-1', 730 - syncSource: 'server', 731 - syncedAt: 500, 726 + syncedAt: 500, 732 727 createdAt: 500, 733 728 updatedAt: Date.now() + 5000, // much newer 734 729 deletedAt: 0, ··· 820 815 821 816 // Item should now have sync info 822 817 const items = await data.queryItems(); 823 - assert.strictEqual(items[0].syncSource, 'server'); 824 818 assert.ok(items[0].syncedAt > 0); 825 819 }); 826 820 ··· 835 829 content: 'https://server.com', 836 830 metadata: null, 837 831 syncId: 'server-id', 838 - syncSource: 'server', 839 - syncedAt: Date.now(), 832 + syncedAt: Date.now(), 840 833 createdAt: 1000, 841 834 updatedAt: 1000, 842 835 deletedAt: 0, ··· 951 944 metadata: null, 952 945 createdAt: new Date(1000).toISOString(), 953 946 updatedAt: new Date(Date.now() + 10000).toISOString(), 954 - deleted_at: Date.now() + 5000, 947 + deletedAt: Date.now() + 5000, 955 948 }, 956 949 ]; 957 950 const { adapter, data, sync } = createSyncTestEngine(serverItems); ··· 964 957 content: 'https://deleted-on-server.com', 965 958 metadata: null, 966 959 syncId: 'server-del-1', 967 - syncSource: 'server', 968 - syncedAt: 1000, 960 + syncedAt: 1000, 969 961 createdAt: 1000, 970 962 updatedAt: 2000, 971 963 deletedAt: 0, ··· 995 987 metadata: null, 996 988 createdAt: new Date(1000).toISOString(), 997 989 updatedAt: new Date(2000).toISOString(), 998 - deleted_at: 3000, 990 + deletedAt: 3000, 999 991 }, 1000 992 ]; 1001 993 const { adapter, data, sync } = createSyncTestEngine(serverItems); ··· 1031 1023 content: 'https://undeleted.com', 1032 1024 metadata: null, 1033 1025 syncId: 'server-undelete-1', 1034 - syncSource: 'server', 1035 - syncedAt: 1000, 1026 + syncedAt: 1000, 1036 1027 createdAt: 1000, 1037 1028 updatedAt: 2000, 1038 1029 deletedAt: 3000, ··· 1070 1061 content: 'https://conflict.com', 1071 1062 metadata: null, 1072 1063 syncId: 'server-conflict-1', 1073 - syncSource: 'server', 1074 - syncedAt: 500, 1064 + syncedAt: 500, 1075 1065 createdAt: 500, 1076 1066 updatedAt: Date.now() + 5000, // much newer 1077 1067 deletedAt: Date.now() + 5000, ··· 1142 1132 content: 'https://was-deleted.com', 1143 1133 metadata: null, 1144 1134 syncId: 'server-id-123', 1145 - syncSource: 'server', 1146 - syncedAt: 1000, 1135 + syncedAt: 1000, 1147 1136 createdAt: 1000, 1148 1137 updatedAt: 2000, 1149 1138 deletedAt: 2000, ··· 1152 1141 const result = await sync.pushToServer(); 1153 1142 assert.strictEqual(result.pushed, 1); 1154 1143 assert.strictEqual(pushedBodies.length, 1); 1155 - assert.ok(pushedBodies[0].deleted_at > 0, 'should include deleted_at in push body'); 1144 + assert.ok(pushedBodies[0].deletedAt > 0, 'should include deletedAt in push body'); 1156 1145 }); 1157 1146 1158 1147 it('should not push deleted items without syncId (never synced)', async () => { ··· 1166 1155 content: 'https://never-synced.com', 1167 1156 metadata: null, 1168 1157 syncId: '', 1169 - syncSource: '', 1170 - syncedAt: 0, 1158 + syncedAt: 0, 1171 1159 createdAt: 1000, 1172 1160 updatedAt: 2000, 1173 1161 deletedAt: 2000, ··· 1191 1179 content: 'https://pending-delete.com', 1192 1180 metadata: null, 1193 1181 syncId: 'server-xyz', 1194 - syncSource: 'server', 1195 - syncedAt: 1000, 1182 + syncedAt: 1000, 1196 1183 createdAt: 1000, 1197 1184 updatedAt: 2000, 1198 1185 deletedAt: 2000, ··· 1221 1208 content: 'https://synced.com', 1222 1209 metadata: null, 1223 1210 syncId: 'remote-id', 1224 - syncSource: 'server', 1225 - syncedAt: 1000, 1211 + syncedAt: 1000, 1226 1212 createdAt: 1000, 1227 1213 updatedAt: 1000, 1228 1214 deletedAt: 0, ··· 1234 1220 1235 1221 // Item sync markers should be cleared 1236 1222 const item = await data.getItem('synced-item'); 1237 - assert.strictEqual(item.syncSource, ''); 1238 1223 assert.strictEqual(item.syncedAt, 0); 1239 1224 assert.strictEqual(item.syncId, ''); 1240 1225 }); ··· 1254 1239 const { adapter, data, sync } = createSyncTestEngine(); 1255 1240 await adapter.open(); 1256 1241 1257 - // No stored server config, but items exist with syncSource='server' 1242 + // No stored server config, but items exist from prior sync 1258 1243 await adapter.insertItem({ 1259 1244 id: 'orphan', 1260 1245 type: 'url', 1261 1246 content: 'https://orphan.com', 1262 1247 metadata: null, 1263 1248 syncId: 'old-server-id', 1264 - syncSource: 'server', 1265 - syncedAt: 1000, 1249 + syncedAt: 1000, 1266 1250 createdAt: 1000, 1267 1251 updatedAt: 1000, 1268 1252 deletedAt: 0, ··· 1274 1258 assert.strictEqual(reset, false); 1275 1259 1276 1260 const item = await data.getItem('orphan'); 1277 - assert.strictEqual(item.syncSource, 'server'); 1261 + assert.ok(item.syncedAt > 0); 1278 1262 }); 1279 1263 }); 1280 1264 ··· 1287 1271 1288 1272 await adapter.insertItem({ 1289 1273 id: 'test', type: 'url', content: 'https://test.com', 1290 - metadata: null, syncId: '', syncSource: '', syncedAt: 0, 1274 + metadata: null, syncId: '', syncedAt: 0, 1291 1275 createdAt: 1000, updatedAt: 1000, deletedAt: 0, 1292 1276 }); 1293 1277 ··· 1323 1307 1324 1308 await adapter.insertItem({ 1325 1309 id: 'local-id', type: 'url', content: 'https://test.com', 1326 - metadata: null, syncId: 'remote-id', syncSource: 'server', syncedAt: 1000, 1310 + metadata: null, syncId: 'remote-id', syncedAt: 1000, 1327 1311 createdAt: 1000, updatedAt: 1000, deletedAt: 0, 1328 1312 }); 1329 1313 ··· 1344 1328 1345 1329 await adapter.insertItem({ 1346 1330 id: 'del', type: 'url', content: 'https://deleted.com', 1347 - metadata: null, syncId: 'del-sync', syncSource: '', syncedAt: 0, 1331 + metadata: null, syncId: 'del-sync', syncedAt: 0, 1348 1332 createdAt: 1000, updatedAt: 1000, deletedAt: 2000, 1349 1333 }); 1350 1334 ··· 1437 1421 await adapter.open(); 1438 1422 const item = { 1439 1423 id: 'test-1', type: 'url', content: 'https://example.com', 1440 - metadata: null, syncId: '', syncSource: '', syncedAt: 0, 1424 + metadata: null, syncId: '', syncedAt: 0, 1441 1425 createdAt: 1000, updatedAt: 1000, deletedAt: 0, 1442 1426 }; 1443 1427 await adapter.insertItem(item); ··· 1450 1434 await adapter.open(); 1451 1435 await adapter.insertItem({ 1452 1436 id: 'del-1', type: 'url', content: 'https://deleted.com', 1453 - metadata: null, syncId: '', syncSource: '', syncedAt: 0, 1437 + metadata: null, syncId: '', syncedAt: 0, 1454 1438 createdAt: 1000, updatedAt: 1000, deletedAt: 2000, 1455 1439 }); 1456 1440 const item = await adapter.getItem('del-1'); ··· 1461 1445 await adapter.open(); 1462 1446 await adapter.insertItem({ 1463 1447 id: 'upd-1', type: 'text', content: 'original', 1464 - metadata: null, syncId: '', syncSource: '', syncedAt: 0, 1448 + metadata: null, syncId: '', syncedAt: 0, 1465 1449 createdAt: 1000, updatedAt: 1000, deletedAt: 0, 1466 1450 }); 1467 1451 await adapter.updateItem('upd-1', { content: 'updated', updatedAt: 2000 }); ··· 1474 1458 await adapter.open(); 1475 1459 await adapter.insertItem({ 1476 1460 id: 'sd-1', type: 'url', content: 'https://test.com', 1477 - metadata: null, syncId: '', syncSource: '', syncedAt: 0, 1461 + metadata: null, syncId: '', syncedAt: 0, 1478 1462 createdAt: 1000, updatedAt: 1000, deletedAt: 0, 1479 1463 }); 1480 1464 await adapter.deleteItem('sd-1'); ··· 1485 1469 await adapter.open(); 1486 1470 await adapter.insertItem({ 1487 1471 id: 'hd-1', type: 'url', content: 'https://test.com', 1488 - metadata: null, syncId: '', syncSource: '', syncedAt: 0, 1472 + metadata: null, syncId: '', syncedAt: 0, 1489 1473 createdAt: 1000, updatedAt: 1000, deletedAt: 0, 1490 1474 }); 1491 1475 await adapter.insertTag({ ··· 1522 1506 await adapter.open(); 1523 1507 await adapter.insertItem({ 1524 1508 id: 'it-1', type: 'url', content: 'https://test.com', 1525 - metadata: null, syncId: '', syncSource: '', syncedAt: 0, 1509 + metadata: null, syncId: '', syncedAt: 0, 1526 1510 createdAt: 1000, updatedAt: 1000, deletedAt: 0, 1527 1511 }); 1528 1512 await adapter.insertTag({ ··· 1549 1533 await adapter.open(); 1550 1534 await adapter.insertItem({ 1551 1535 id: 'ct-1', type: 'url', content: 'https://test.com', 1552 - metadata: null, syncId: '', syncSource: '', syncedAt: 0, 1536 + metadata: null, syncId: '', syncedAt: 0, 1553 1537 createdAt: 1000, updatedAt: 1000, deletedAt: 0, 1554 1538 }); 1555 1539 await adapter.insertTag({ ··· 1580 1564 await adapter.open(); 1581 1565 await adapter.insertItem({ 1582 1566 id: 'local-1', type: 'url', content: 'https://test.com', 1583 - metadata: null, syncId: 'remote-1', syncSource: 'server', syncedAt: 1000, 1567 + metadata: null, syncId: 'remote-1', syncedAt: 1000, 1584 1568 createdAt: 1000, updatedAt: 1000, deletedAt: 0, 1585 1569 }); 1586 1570 ··· 1603 1587 await adapter.open(); 1604 1588 await adapter.insertItem({ 1605 1589 id: 'del-sync', type: 'url', content: 'https://deleted.com', 1606 - metadata: null, syncId: 'del-remote', syncSource: '', syncedAt: 0, 1590 + metadata: null, syncId: 'del-remote', syncedAt: 0, 1607 1591 createdAt: 1000, updatedAt: 1000, deletedAt: 2000, 1608 1592 }); 1609 1593 const bySyncId = await adapter.findItemBySyncId('del-remote'); ··· 1620 1604 await adapter.open(); 1621 1605 await adapter.insertItem({ 1622 1606 id: 'f-1', type: 'url', content: 'https://a.com', 1623 - metadata: null, syncId: '', syncSource: '', syncedAt: 0, 1607 + metadata: null, syncId: '', syncedAt: 0, 1624 1608 createdAt: 1000, updatedAt: 1000, deletedAt: 0, 1625 1609 }); 1626 1610 await adapter.insertItem({ 1627 1611 id: 'f-2', type: 'text', content: 'note', 1628 - metadata: null, syncId: '', syncSource: '', syncedAt: 0, 1612 + metadata: null, syncId: '', syncedAt: 0, 1629 1613 createdAt: 2000, updatedAt: 2000, deletedAt: 0, 1630 1614 }); 1631 1615 await adapter.insertItem({ 1632 1616 id: 'f-3', type: 'url', content: 'https://b.com', 1633 - metadata: null, syncId: '', syncSource: '', syncedAt: 0, 1617 + metadata: null, syncId: '', syncedAt: 0, 1634 1618 createdAt: 3000, updatedAt: 3000, deletedAt: 0, 1635 1619 }); 1636 1620