diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 21d9eeef..88d86832 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -687,7 +687,7 @@ Three sync modules import browser data as Peek URL items. All are one-way (brows | `tabs.js` | `peek_tabs_enabled` | `from:tab` | Add-and-skip: open tabs imported, listens for `onUpdated` | | `history.js` | `peek_history_enabled` | `from:history` | **Update-on-revisit**: existing items get fresh visit metadata via `updateItem()` | -**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. +**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. **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. diff --git a/backend/server/test.js b/backend/server/test.js index c89a4c76..3e2b60fa 100644 --- a/backend/server/test.js +++ b/backend/server/test.js @@ -617,7 +617,6 @@ describe("Database Tests", () => { const columnNames = tableInfo.map((col) => col.name); assert.ok(columnNames.includes("syncId"), "should have syncId column"); - assert.ok(columnNames.includes("syncSource"), "should have syncSource column"); assert.ok(columnNames.includes("syncedAt"), "should have syncedAt column"); }); diff --git a/backend/tauri-mobile/src-tauri/tauri.conf.json b/backend/tauri-mobile/src-tauri/tauri.conf.json index 42809f9b..09edb0b3 100644 --- a/backend/tauri-mobile/src-tauri/tauri.conf.json +++ b/backend/tauri-mobile/src-tauri/tauri.conf.json @@ -5,7 +5,8 @@ "identifier": "com.dietrich.peek-mobile", "build": { "beforeBuildCommand": "npm run build", - "frontendDist": "../dist" + "frontendDist": "../dist", + "devUrl": "http://192.168.50.143:51684" }, "app": { "windows": [ diff --git a/docs/datastore.md b/docs/datastore.md index d25e218e..b3a70b47 100644 --- a/docs/datastore.md +++ b/docs/datastore.md @@ -47,7 +47,6 @@ Stores all user content types: URLs, text notes, tagsets, images. | mimeType | TEXT | MIME type (e.g., `text/html`) | | metadata | TEXT | JSON for flexible extra data | | syncId | TEXT | Server-assigned ID for sync | -| syncSource | TEXT | Origin of sync (`server`, `history`, etc.) | | syncedAt | INTEGER | Last sync timestamp (ms) | | createdAt | INTEGER | Creation timestamp (ms) | | updatedAt | INTEGER | Last update timestamp (ms) | diff --git a/docs/sync-source-refactor.md b/docs/sync-source-refactor.md index deef1e50..94153cc9 100644 --- a/docs/sync-source-refactor.md +++ b/docs/sync-source-refactor.md @@ -90,7 +90,7 @@ Already stored redundantly — **removing syncSource loses nothing**: ## Changes -### 1. Remove syncSource from sync algorithm +### 1. Remove syncSource from sync algorithm ✅ DONE (desktop, server, unified sync module) **Push query** — replace syncSource filter with timestamp-based: @@ -122,10 +122,13 @@ UPDATE items SET syncId = ?, syncedAt = ? WHERE id = ? **On server change** — reset `syncedAt = 0` to force full re-sync. No need to touch syncSource. -**Files to update:** -- `backend/electron/sync.ts` — lines 393, 515-525, 619-620, 667-668, 773-775 -- `sync/sync.js` — lines 102-107, 162-166, 277-282, 399-400 -- `backend/tauri/src-tauri/src/sync.rs` — lines 521-522, 634-665, 799-800, 894 +**Files updated:** +- ✅ `backend/electron/sync.ts` — syncSource removed from push filter, post-push update, server-change reset +- ✅ `sync/sync.js` — syncSource replaced with syncedAt-based filtering, fromISOString() added for server timestamp conversion +- ✅ `sync/adapters/better-sqlite3.js` — syncSource removed from schema, migration, INSERT +- ✅ `sync/data.js` — syncSource removed from addItem and saveItem +- ⬜ `backend/tauri/src-tauri/src/sync.rs` — still has sync_source (Tauri desktop) +- ⬜ `backend/tauri-mobile/src-tauri/src/lib.rs` — still has sync_source (iOS mobile) ### 2. Stop setting syncSource on item creation diff --git a/docs/sync.md b/docs/sync.md index 2a0728d3..5aa9b7e7 100644 --- a/docs/sync.md +++ b/docs/sync.md @@ -42,11 +42,11 @@ All platforms use the same unified types: ### Push (Client → Server) -1. Query items where `syncSource = ''` OR `updatedAt > lastSyncTime` - - Includes soft-deleted items (those with `deleted_at` set) so tombstones propagate +1. Query items where `syncedAt = 0` (never synced) OR `updatedAt > syncedAt` (modified since last sync) + - Includes soft-deleted items (those with `deletedAt` set) so tombstones propagate 2. For each item: `POST /items` with type, content, tags - - If item has `deleted_at`: include it in the push payload so the server records the tombstone -3. On success: update local `syncId` and `syncSource` + - If item has `deletedAt`: include it in the push payload so the server records the tombstone +3. On success: update local `syncId` and `syncedAt` ### Conflict Resolution @@ -149,7 +149,7 @@ Deletions propagate across platforms via **tombstones** (soft-deletes with a `de ## Server-Change Detection -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: +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: 1. After every pull or full sync, the current server URL and profile ID are saved to a `settings` table (`lastSyncServerUrl`, `lastSyncProfileId`). 2. Before `syncAll()`, the stored values are compared to the current config. diff --git a/notes/plan-syncsource-migration-test.md b/notes/plan-syncsource-migration-test.md index 3f2c9fae..b523aff4 100644 --- a/notes/plan-syncsource-migration-test.md +++ b/notes/plan-syncsource-migration-test.md @@ -8,33 +8,27 @@ The `syncSource` column was removed from desktop and server schemas as part of t | Platform | syncSource Status | Details | |----------|------------------|---------| -| Desktop Electron (`backend/electron/`) | Removed from schema + code | No `syncSource` in generated schema or sync code | -| Server (`backend/server/`) | Removed from schema + code | No `syncSource` in db.js or index.js | -| Unified sync module (`sync/sync.js`) | **STILL USES IT** | Push filter, post-push update, pending count all reference `syncSource` | +| Desktop Electron (`backend/electron/`) | **Removed** | No `syncSource` in generated schema or sync code | +| Server (`backend/server/`) | **Removed** | No `syncSource` in db.js or index.js | +| Unified sync module (`sync/sync.js`) | **Removed** | Push filter uses `syncedAt === 0`, no syncSource references | | iOS mobile (`backend/tauri-mobile/`) | Still has `sync_source` | Actively used for push filtering, merge, post-push | | Tauri Desktop (`backend/tauri/`) | Still has `sync_source` | Item struct, column access, sync operations | | Browser Extension (`backend/extension/`) | Clean | No references | ## Issues Found During Investigation -### Critical: `sync/sync.js` not refactored +### ~~Critical: `sync/sync.js` not refactored~~ (DONE) -The unified sync module still actively uses `syncSource`: -- `sync.js:102,107` — push filter: `i.syncSource === ''` -- `sync.js:165` — sets `syncSource: 'server'` after push -- `sync.js:217` — pending count: `i.syncSource === ''` -- `sync.js:279` — server change reset: `syncSource: ''` -- `better-sqlite3.js:34` — adapter schema includes `syncSource TEXT DEFAULT ''` +The unified sync module has been refactored: +- Push filter now uses `syncedAt === 0` instead of `syncSource === ''` +- Post-push update no longer sets `syncSource` +- Server change reset no longer resets `syncSource` +- `better-sqlite3.js` schema no longer includes `syncSource` column +- `_mergeServerItem` now uses `fromISOString()` for server timestamp conversion (bugfix) -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. +### ~~Stale test assertion~~ (FIXED) -### Stale test assertion - -`backend/server/test.js:620` asserts `syncSource` column exists: -```js -assert.ok(columnNames.includes("syncSource")) -``` -This will fail with the current schema. +`backend/server/test.js:620` — `syncSource` assertion removed. Legacy migration test schemas preserved for backward compatibility testing. ### Behavioral change: extension-origin items diff --git a/notes/research-url-history-unification.md b/notes/research-url-history-unification.md index 4d6bd93a..7e28a6e9 100644 --- a/notes/research-url-history-unification.md +++ b/notes/research-url-history-unification.md @@ -24,10 +24,10 @@ The desktop app has **two parallel data models** that need unification: - **Use case**: Tracking all navigation activity across any source **Items (Curated) - `backend/electron/datastore.ts`:** -- **items table**: Stores user-curated items (type: url|text|tagset|image, content, metadata, syncId, syncSource, starred, archived, visitCount, lastVisitAt) +- **items table**: Stores user-curated items (type: url|text|tagset|image, content, metadata, syncId, starred, archived, visitCount, lastVisitAt) - **item_tags table**: M2M junction for tagging items - **Use case**: Saved URLs, notes, groups, images with annotations -- **Key difference**: Explicit user creation, sync-aware (syncId, syncSource, syncedAt) +- **Key difference**: Explicit user creation, sync-aware (syncId, syncedAt) **Browser Extension History Import:** - **history.js**: One-way import of browser history as URL items (tags with `from:history`) @@ -45,7 +45,7 @@ The desktop app has **two parallel data models** that need unification: | **Visit tracking** | Explicit visits table | visitCount + lastVisitAt only | | **Timestamps** | createdAt/updatedAt | createdAt/updatedAt/syncedAt/deletedAt | | **Deletion** | Hard delete | Soft delete (deletedAt) | -| **Source tracking** | Source field in visits | syncSource in items | +| **Source tracking** | Source field in visits | Tags (`from:*`) + metadata._sync | --- @@ -103,7 +103,7 @@ All content (URLs, notes, groups, images) and their visit history unified under 1. **Base item** with type, content, metadata 2. **Annotation layer** (tags, starred, archived, groups) 3. **Visit tracking** (detailed history with frecency) -4. **Sync support** (syncId, syncSource, deleted records) +4. **Sync support** (syncId, syncedAt, deleted records) 5. **Addressability** (peek:// URLs for all items) ### Proposed Schema @@ -122,7 +122,6 @@ CREATE TABLE items ( -- Sync fields syncId TEXT DEFAULT '', - syncSource TEXT DEFAULT '', syncedAt INTEGER DEFAULT 0, -- Audit @@ -217,7 +216,7 @@ CREATE TABLE item_group_members ( api.datastore.addItem(type, content, options) // type: 'url' | 'text' | 'tagset' | 'image' // content: string (URL for type:url, text for type:text, etc) - // options: { title, metadata, syncSource, syncId } + // options: { title, metadata, syncId } api.datastore.recordVisit(itemId, options) // Replaces trackNavigation() diff --git a/notes/sync-architecture-review.md b/notes/sync-architecture-review.md index 39a575a5..69e54800 100644 --- a/notes/sync-architecture-review.md +++ b/notes/sync-architecture-review.md @@ -42,7 +42,6 @@ CREATE TABLE items ( id TEXT PRIMARY KEY, type TEXT NOT NULL CHECK(type IN ('url', 'text', 'tagset', 'image')), syncId TEXT DEFAULT '', - syncSource TEXT DEFAULT '', syncedAt INTEGER DEFAULT 0, createdAt INTEGER NOT NULL, updatedAt INTEGER NOT NULL, @@ -83,7 +82,7 @@ CREATE TABLE tags ( **Migrations Implemented:** 1. `migrateTinyBaseData()` - Legacy format conversion -2. `migrateSyncColumns()` - Add syncId, syncSource, syncedAt +2. `migrateSyncColumns()` - Add syncId, syncedAt 3. `migrateItemTypes()` - 'note' → 'url'/'text' conversion 4. `migrateItemVisitColumns()` - Add visitCount, lastVisitAt 5. `migrateAddressesToItems()` - Convert tagged addresses to items @@ -207,7 +206,6 @@ tables: type: { type: text, not_null: true, check: "type IN ('url', 'text', 'tagset', 'image')" } content: { type: text } syncId: { type: text, default: '' } - syncSource: { type: text, default: '' } syncedAt: { type: integer, default: 0 } createdAt: { type: integer, not_null: true } updatedAt: { type: integer, not_null: true } @@ -283,7 +281,7 @@ fn migrate_tag_ids(conn: &Connection) -> Result<()> { // Tauri validation (add to both desktop and mobile) fn validate_schema(conn: &Connection) -> Result<(), String> { let required = vec![ - ("items", vec!["id", "type", "syncId", "syncSource", "syncedAt", "createdAt", "updatedAt", "deletedAt"]), + ("items", vec!["id", "type", "syncId", "syncedAt", "createdAt", "updatedAt", "deletedAt"]), ("tags", vec!["id", "name", "frequency", "lastUsed", "frecencyScore", "createdAt", "updatedAt"]), ("item_tags", vec!["itemId", "tagId", "createdAt"]), ]; diff --git a/notes/sync-architecture.md b/notes/sync-architecture.md index 7120c5d1..d2ce5b63 100644 --- a/notes/sync-architecture.md +++ b/notes/sync-architecture.md @@ -45,16 +45,16 @@ The sync module handles conversion transparently. 1. Fetch items: `GET /items` (full) or `GET /items/since/:timestamp` (incremental) 2. For each server item: - Find local item by `syncId` matching server `id` - - If not found: insert with `syncId=server.id`, `syncSource='server'` + - If not found: insert with `syncId=server.id` - If found and server is newer (`updated_at > local.updatedAt`): update local - If found and local is newer: skip (will be pushed later) ### Push (Desktop → Server) -1. Query items where `syncSource = ''` (never synced) OR `updatedAt > lastSyncTime` +1. Query items where `syncedAt = 0` (never synced) OR `updatedAt > syncedAt` (modified since last sync) 2. For each item: - `POST /items` with type, content, tags, metadata - - On success: update local `syncId` and `syncSource='server'` + - On success: update local `syncId` and `syncedAt` ### Conflict Resolution @@ -132,7 +132,7 @@ CREATE TABLE items ( mimeType TEXT DEFAULT '', metadata TEXT DEFAULT '{}', syncId TEXT DEFAULT '', -- Server item ID - syncSource TEXT DEFAULT '', -- 'server' if synced + syncedAt INTEGER DEFAULT 0, -- Last sync timestamp (ms) createdAt INTEGER NOT NULL, updatedAt INTEGER NOT NULL, deletedAt INTEGER DEFAULT 0, diff --git a/notes/sync-edge-cases.md b/notes/sync-edge-cases.md index 5803fbc0..71420ac5 100644 --- a/notes/sync-edge-cases.md +++ b/notes/sync-edge-cases.md @@ -37,8 +37,8 @@ const localItem = db.prepare( ```javascript items = db.prepare(` SELECT * FROM items - WHERE deletedAt = 0 AND (syncSource = '' OR updatedAt > ?) -`).all(lastSyncTime); + WHERE deletedAt = 0 AND (syncedAt = 0 OR updatedAt > syncedAt) +`).all(); ``` **Server db.js - getItemsSince (line 717)**: diff --git a/schema/README.md b/schema/README.md index 4fa9a8a4..c30b6a5c 100644 --- a/schema/README.md +++ b/schema/README.md @@ -79,7 +79,7 @@ The schema defines three core sync tables: ### items Unified content storage for URLs, text notes, tagsets, and images. -**Sync columns**: id, type, content, mimeType, metadata, syncId, syncSource, syncedAt, createdAt, updatedAt, deletedAt, starred, archived +**Sync columns**: id, type, content, mimeType, metadata, syncId, syncedAt, createdAt, updatedAt, deletedAt, starred, archived **Local-only columns**: visitCount, lastVisitAt, frecencyScore, title, domain, favicon diff --git a/scripts/e2e-version-test.sh b/scripts/e2e-version-test.sh index 486d539e..0f6001c5 100755 --- a/scripts/e2e-version-test.sh +++ b/scripts/e2e-version-test.sh @@ -389,7 +389,7 @@ run_desktop_sync() { else log_fail "$PHASE" "$LABEL: got $ITEM_COUNT items, expected $EXPECTED_COUNT" log "$PHASE" " Items in DB:" - sqlite3 "$DB_PATH" "SELECT id, type, content, syncSource FROM items WHERE deletedAt = 0;" | while IFS= read -r row; do + sqlite3 "$DB_PATH" "SELECT id, type, content, syncedAt FROM items WHERE deletedAt = 0;" | while IFS= read -r row; do log "$PHASE" " $row" done fi diff --git a/sync/adapters/better-sqlite3.js b/sync/adapters/better-sqlite3.js index 955de106..2356506c 100644 --- a/sync/adapters/better-sqlite3.js +++ b/sync/adapters/better-sqlite3.js @@ -31,7 +31,6 @@ export function createBetterSqliteAdapter(db) { content TEXT, metadata TEXT, syncId TEXT DEFAULT '', - syncSource TEXT DEFAULT '', syncedAt INTEGER DEFAULT 0, createdAt INTEGER NOT NULL, updatedAt INTEGER NOT NULL, @@ -90,9 +89,6 @@ export function createBetterSqliteAdapter(db) { if (!itemColNames.has('syncId')) { db.exec("ALTER TABLE items ADD COLUMN syncId TEXT DEFAULT ''"); } - if (!itemColNames.has('syncSource')) { - db.exec("ALTER TABLE items ADD COLUMN syncSource TEXT DEFAULT ''"); - } if (!itemColNames.has('syncedAt')) { db.exec('ALTER TABLE items ADD COLUMN syncedAt INTEGER DEFAULT 0'); } @@ -103,8 +99,8 @@ export function createBetterSqliteAdapter(db) { getItem: db.prepare('SELECT * FROM items WHERE id = ? AND deletedAt = 0'), getItemIncludeDeleted: db.prepare('SELECT * FROM items WHERE id = ?'), insertItem: db.prepare(` - INSERT INTO items (id, type, content, metadata, syncId, syncSource, syncedAt, createdAt, updatedAt, deletedAt) - VALUES (@id, @type, @content, @metadata, @syncId, @syncSource, @syncedAt, @createdAt, @updatedAt, @deletedAt) + INSERT INTO items (id, type, content, metadata, syncId, syncedAt, createdAt, updatedAt, deletedAt) + VALUES (@id, @type, @content, @metadata, @syncId, @syncedAt, @createdAt, @updatedAt, @deletedAt) `), deleteItemSoft: db.prepare('UPDATE items SET deletedAt = @deletedAt, updatedAt = @updatedAt WHERE id = @id AND deletedAt = 0'), hardDeleteItem: db.prepare('DELETE FROM items WHERE id = ?'), diff --git a/sync/adapters/interface.js b/sync/adapters/interface.js index 9ea7ecb4..2ad3ba60 100644 --- a/sync/adapters/interface.js +++ b/sync/adapters/interface.js @@ -11,7 +11,6 @@ * @property {string|null} content * @property {string|null} metadata - JSON string * @property {string} syncId - * @property {string} syncSource * @property {number} syncedAt - Unix ms * @property {number} createdAt - Unix ms * @property {number} updatedAt - Unix ms diff --git a/sync/data.js b/sync/data.js index 98251028..ab2ab575 100644 --- a/sync/data.js +++ b/sync/data.js @@ -38,7 +38,6 @@ export class DataEngine { * @param {string|null} [options.content] * @param {string|null} [options.metadata] - JSON string * @param {string} [options.syncId] - * @param {string} [options.syncSource] * @param {number} [options.createdAt] - Override creation timestamp (for imports) * @returns {Promise<{id: string}>} */ @@ -61,7 +60,6 @@ export class DataEngine { content: options.content ?? null, metadata, syncId: options.syncId || '', - syncSource: options.syncSource || '', syncedAt: 0, createdAt, updatedAt: now, @@ -250,7 +248,6 @@ export class DataEngine { content: content ?? null, metadata: metadataStr, syncId: syncId || '', - syncSource: '', syncedAt: 0, createdAt: timestamp, updatedAt: timestamp, diff --git a/sync/sync.js b/sync/sync.js index 2708e86e..889c8631 100644 --- a/sync/sync.js +++ b/sync/sync.js @@ -99,12 +99,12 @@ export class SyncEngine { // Incremental: never synced OR locally modified after their last sync itemsToPush = allItems.filter( i => - i.syncSource === '' || + i.syncedAt === 0 || (i.syncedAt > 0 && i.updatedAt > i.syncedAt) ); } else { // Full: all items that haven't been synced - itemsToPush = allItems.filter(i => i.syncSource === ''); + itemsToPush = allItems.filter(i => i.syncedAt === 0); } // Also include deleted items that need tombstone push @@ -162,7 +162,6 @@ export class SyncEngine { // Update local item with sync info await this.data.adapter.updateItem(item.id, { syncId: response.id, - syncSource: 'server', syncedAt: Date.now(), }); @@ -214,7 +213,7 @@ export class SyncEngine { const allItems = await this.data.queryItems({ includeDeleted: false }); const pendingCount = allItems.filter( i => - i.syncSource === '' || + i.syncedAt === 0 || (i.syncedAt > 0 && i.updatedAt > i.syncedAt) ).length; @@ -276,7 +275,6 @@ export class SyncEngine { const allItems = await this.data.queryItems({ includeDeleted: false }); for (const item of allItems) { await this.data.adapter.updateItem(item.id, { - syncSource: '', syncedAt: 0, syncId: '', }); @@ -359,8 +357,8 @@ export class SyncEngine { * @returns {Promise<'pulled'|'conflict'|'skipped'>} */ async _mergeServerItem(serverItem) { - const serverUpdatedAt = serverItem.updatedAt; - const serverDeletedAt = serverItem.deletedAt || 0; + const serverUpdatedAt = fromISOString(serverItem.updatedAt); + const serverDeletedAt = fromISOString(serverItem.deletedAt) || 0; // Find local item by syncId const localItem = await this.data.adapter.findItemBySyncId(serverItem.id); @@ -396,12 +394,11 @@ export class SyncEngine { ? JSON.stringify(serverItem.metadata) : null, syncId: serverItem.id, - syncSource: 'server', }); // Overwrite timestamps to match server await this.data.adapter.updateItem(localId, { - createdAt: serverItem.createdAt, + createdAt: fromISOString(serverItem.createdAt), updatedAt: serverUpdatedAt, syncedAt: Date.now(), }); diff --git a/sync/test.js b/sync/test.js index 8cdb616d..5f78a1fa 100644 --- a/sync/test.js +++ b/sync/test.js @@ -235,11 +235,9 @@ describe('DataEngine: Items', () => { const { id } = await data.addItem('url', { content: 'https://example.com', syncId: 'server-123', - syncSource: 'server', - }); + }); const item = await data.getItem(id); assert.strictEqual(item.syncId, 'server-123'); - assert.strictEqual(item.syncSource, 'server'); }); }); @@ -662,7 +660,6 @@ describe('SyncEngine: Pull', () => { assert.strictEqual(items.length, 1); assert.strictEqual(items[0].content, 'https://from-server.com'); assert.strictEqual(items[0].syncId, 'server-1'); - assert.strictEqual(items[0].syncSource, 'server'); const tags = await data.getItemTags(items[0].id); assert.strictEqual(tags.length, 1); @@ -691,8 +688,7 @@ describe('SyncEngine: Pull', () => { content: 'https://old.com', metadata: null, syncId: 'server-1', - syncSource: 'server', - syncedAt: 1000, + syncedAt: 1000, createdAt: 1000, updatedAt: 2000, deletedAt: 0, @@ -727,8 +723,7 @@ describe('SyncEngine: Pull', () => { content: 'https://local-new.com', metadata: null, syncId: 'server-1', - syncSource: 'server', - syncedAt: 500, + syncedAt: 500, createdAt: 500, updatedAt: Date.now() + 5000, // much newer deletedAt: 0, @@ -820,7 +815,6 @@ describe('SyncEngine: Push', () => { // Item should now have sync info const items = await data.queryItems(); - assert.strictEqual(items[0].syncSource, 'server'); assert.ok(items[0].syncedAt > 0); }); @@ -835,8 +829,7 @@ describe('SyncEngine: Push', () => { content: 'https://server.com', metadata: null, syncId: 'server-id', - syncSource: 'server', - syncedAt: Date.now(), + syncedAt: Date.now(), createdAt: 1000, updatedAt: 1000, deletedAt: 0, @@ -951,7 +944,7 @@ describe('SyncEngine: Delete Propagation', () => { metadata: null, createdAt: new Date(1000).toISOString(), updatedAt: new Date(Date.now() + 10000).toISOString(), - deleted_at: Date.now() + 5000, + deletedAt: Date.now() + 5000, }, ]; const { adapter, data, sync } = createSyncTestEngine(serverItems); @@ -964,8 +957,7 @@ describe('SyncEngine: Delete Propagation', () => { content: 'https://deleted-on-server.com', metadata: null, syncId: 'server-del-1', - syncSource: 'server', - syncedAt: 1000, + syncedAt: 1000, createdAt: 1000, updatedAt: 2000, deletedAt: 0, @@ -995,7 +987,7 @@ describe('SyncEngine: Delete Propagation', () => { metadata: null, createdAt: new Date(1000).toISOString(), updatedAt: new Date(2000).toISOString(), - deleted_at: 3000, + deletedAt: 3000, }, ]; const { adapter, data, sync } = createSyncTestEngine(serverItems); @@ -1031,8 +1023,7 @@ describe('SyncEngine: Delete Propagation', () => { content: 'https://undeleted.com', metadata: null, syncId: 'server-undelete-1', - syncSource: 'server', - syncedAt: 1000, + syncedAt: 1000, createdAt: 1000, updatedAt: 2000, deletedAt: 3000, @@ -1070,8 +1061,7 @@ describe('SyncEngine: Delete Propagation', () => { content: 'https://conflict.com', metadata: null, syncId: 'server-conflict-1', - syncSource: 'server', - syncedAt: 500, + syncedAt: 500, createdAt: 500, updatedAt: Date.now() + 5000, // much newer deletedAt: Date.now() + 5000, @@ -1142,8 +1132,7 @@ describe('SyncEngine: Delete Propagation', () => { content: 'https://was-deleted.com', metadata: null, syncId: 'server-id-123', - syncSource: 'server', - syncedAt: 1000, + syncedAt: 1000, createdAt: 1000, updatedAt: 2000, deletedAt: 2000, @@ -1152,7 +1141,7 @@ describe('SyncEngine: Delete Propagation', () => { const result = await sync.pushToServer(); assert.strictEqual(result.pushed, 1); assert.strictEqual(pushedBodies.length, 1); - assert.ok(pushedBodies[0].deleted_at > 0, 'should include deleted_at in push body'); + assert.ok(pushedBodies[0].deletedAt > 0, 'should include deletedAt in push body'); }); it('should not push deleted items without syncId (never synced)', async () => { @@ -1166,8 +1155,7 @@ describe('SyncEngine: Delete Propagation', () => { content: 'https://never-synced.com', metadata: null, syncId: '', - syncSource: '', - syncedAt: 0, + syncedAt: 0, createdAt: 1000, updatedAt: 2000, deletedAt: 2000, @@ -1191,8 +1179,7 @@ describe('SyncEngine: Delete Propagation', () => { content: 'https://pending-delete.com', metadata: null, syncId: 'server-xyz', - syncSource: 'server', - syncedAt: 1000, + syncedAt: 1000, createdAt: 1000, updatedAt: 2000, deletedAt: 2000, @@ -1221,8 +1208,7 @@ describe('SyncEngine: Server Change Detection', () => { content: 'https://synced.com', metadata: null, syncId: 'remote-id', - syncSource: 'server', - syncedAt: 1000, + syncedAt: 1000, createdAt: 1000, updatedAt: 1000, deletedAt: 0, @@ -1234,7 +1220,6 @@ describe('SyncEngine: Server Change Detection', () => { // Item sync markers should be cleared const item = await data.getItem('synced-item'); - assert.strictEqual(item.syncSource, ''); assert.strictEqual(item.syncedAt, 0); assert.strictEqual(item.syncId, ''); }); @@ -1254,15 +1239,14 @@ describe('SyncEngine: Server Change Detection', () => { const { adapter, data, sync } = createSyncTestEngine(); await adapter.open(); - // No stored server config, but items exist with syncSource='server' + // No stored server config, but items exist from prior sync await adapter.insertItem({ id: 'orphan', type: 'url', content: 'https://orphan.com', metadata: null, syncId: 'old-server-id', - syncSource: 'server', - syncedAt: 1000, + syncedAt: 1000, createdAt: 1000, updatedAt: 1000, deletedAt: 0, @@ -1274,7 +1258,7 @@ describe('SyncEngine: Server Change Detection', () => { assert.strictEqual(reset, false); const item = await data.getItem('orphan'); - assert.strictEqual(item.syncSource, 'server'); + assert.ok(item.syncedAt > 0); }); }); @@ -1287,7 +1271,7 @@ describe('Memory Adapter', () => { await adapter.insertItem({ id: 'test', type: 'url', content: 'https://test.com', - metadata: null, syncId: '', syncSource: '', syncedAt: 0, + metadata: null, syncId: '', syncedAt: 0, createdAt: 1000, updatedAt: 1000, deletedAt: 0, }); @@ -1323,7 +1307,7 @@ describe('Memory Adapter', () => { await adapter.insertItem({ id: 'local-id', type: 'url', content: 'https://test.com', - metadata: null, syncId: 'remote-id', syncSource: 'server', syncedAt: 1000, + metadata: null, syncId: 'remote-id', syncedAt: 1000, createdAt: 1000, updatedAt: 1000, deletedAt: 0, }); @@ -1344,7 +1328,7 @@ describe('Memory Adapter', () => { await adapter.insertItem({ id: 'del', type: 'url', content: 'https://deleted.com', - metadata: null, syncId: 'del-sync', syncSource: '', syncedAt: 0, + metadata: null, syncId: 'del-sync', syncedAt: 0, createdAt: 1000, updatedAt: 1000, deletedAt: 2000, }); @@ -1437,7 +1421,7 @@ if (betterSqliteWorks) { await adapter.open(); const item = { id: 'test-1', type: 'url', content: 'https://example.com', - metadata: null, syncId: '', syncSource: '', syncedAt: 0, + metadata: null, syncId: '', syncedAt: 0, createdAt: 1000, updatedAt: 1000, deletedAt: 0, }; await adapter.insertItem(item); @@ -1450,7 +1434,7 @@ if (betterSqliteWorks) { await adapter.open(); await adapter.insertItem({ id: 'del-1', type: 'url', content: 'https://deleted.com', - metadata: null, syncId: '', syncSource: '', syncedAt: 0, + metadata: null, syncId: '', syncedAt: 0, createdAt: 1000, updatedAt: 1000, deletedAt: 2000, }); const item = await adapter.getItem('del-1'); @@ -1461,7 +1445,7 @@ if (betterSqliteWorks) { await adapter.open(); await adapter.insertItem({ id: 'upd-1', type: 'text', content: 'original', - metadata: null, syncId: '', syncSource: '', syncedAt: 0, + metadata: null, syncId: '', syncedAt: 0, createdAt: 1000, updatedAt: 1000, deletedAt: 0, }); await adapter.updateItem('upd-1', { content: 'updated', updatedAt: 2000 }); @@ -1474,7 +1458,7 @@ if (betterSqliteWorks) { await adapter.open(); await adapter.insertItem({ id: 'sd-1', type: 'url', content: 'https://test.com', - metadata: null, syncId: '', syncSource: '', syncedAt: 0, + metadata: null, syncId: '', syncedAt: 0, createdAt: 1000, updatedAt: 1000, deletedAt: 0, }); await adapter.deleteItem('sd-1'); @@ -1485,7 +1469,7 @@ if (betterSqliteWorks) { await adapter.open(); await adapter.insertItem({ id: 'hd-1', type: 'url', content: 'https://test.com', - metadata: null, syncId: '', syncSource: '', syncedAt: 0, + metadata: null, syncId: '', syncedAt: 0, createdAt: 1000, updatedAt: 1000, deletedAt: 0, }); await adapter.insertTag({ @@ -1522,7 +1506,7 @@ if (betterSqliteWorks) { await adapter.open(); await adapter.insertItem({ id: 'it-1', type: 'url', content: 'https://test.com', - metadata: null, syncId: '', syncSource: '', syncedAt: 0, + metadata: null, syncId: '', syncedAt: 0, createdAt: 1000, updatedAt: 1000, deletedAt: 0, }); await adapter.insertTag({ @@ -1549,7 +1533,7 @@ if (betterSqliteWorks) { await adapter.open(); await adapter.insertItem({ id: 'ct-1', type: 'url', content: 'https://test.com', - metadata: null, syncId: '', syncSource: '', syncedAt: 0, + metadata: null, syncId: '', syncedAt: 0, createdAt: 1000, updatedAt: 1000, deletedAt: 0, }); await adapter.insertTag({ @@ -1580,7 +1564,7 @@ if (betterSqliteWorks) { await adapter.open(); await adapter.insertItem({ id: 'local-1', type: 'url', content: 'https://test.com', - metadata: null, syncId: 'remote-1', syncSource: 'server', syncedAt: 1000, + metadata: null, syncId: 'remote-1', syncedAt: 1000, createdAt: 1000, updatedAt: 1000, deletedAt: 0, }); @@ -1603,7 +1587,7 @@ if (betterSqliteWorks) { await adapter.open(); await adapter.insertItem({ id: 'del-sync', type: 'url', content: 'https://deleted.com', - metadata: null, syncId: 'del-remote', syncSource: '', syncedAt: 0, + metadata: null, syncId: 'del-remote', syncedAt: 0, createdAt: 1000, updatedAt: 1000, deletedAt: 2000, }); const bySyncId = await adapter.findItemBySyncId('del-remote'); @@ -1620,17 +1604,17 @@ if (betterSqliteWorks) { await adapter.open(); await adapter.insertItem({ id: 'f-1', type: 'url', content: 'https://a.com', - metadata: null, syncId: '', syncSource: '', syncedAt: 0, + metadata: null, syncId: '', syncedAt: 0, createdAt: 1000, updatedAt: 1000, deletedAt: 0, }); await adapter.insertItem({ id: 'f-2', type: 'text', content: 'note', - metadata: null, syncId: '', syncSource: '', syncedAt: 0, + metadata: null, syncId: '', syncedAt: 0, createdAt: 2000, updatedAt: 2000, deletedAt: 0, }); await adapter.insertItem({ id: 'f-3', type: 'url', content: 'https://b.com', - metadata: null, syncId: '', syncSource: '', syncedAt: 0, + metadata: null, syncId: '', syncedAt: 0, createdAt: 3000, updatedAt: 3000, deletedAt: 0, });