(
payload: DeviceSyncPayload,
options: { forceApply?: boolean } = {},
)
| 276 | // --------------------------------------------------------------------------- |
| 277 | |
| 278 | export async function applyChanges( |
| 279 | payload: DeviceSyncPayload, |
| 280 | options: { forceApply?: boolean } = {}, |
| 281 | ): Promise<{ applied: number; skipped: number }> { |
| 282 | return runSerializedDbTask(() => |
| 283 | withDatabaseLockRetry(async () => { |
| 284 | await ensureNoTransaction(); |
| 285 | const db = await getDB(); |
| 286 | if (shouldRunSyncCleanup()) { |
| 287 | await cleanupOrphanedSyncRows(db); |
| 288 | } |
| 289 | let applied = 0; |
| 290 | let skipped = 0; |
| 291 | |
| 292 | // Keep this transaction-free. On some adapters, explicit BEGIN/COMMIT can |
| 293 | // lose state across awaited calls and end with "cannot commit - no transaction is active". |
| 294 | for (const tableInfo of SYNC_TABLES) { |
| 295 | const tableName = tableInfo.name; |
| 296 | const tableData = payload.tables[tableName]; |
| 297 | if (!tableData) continue; |
| 298 | |
| 299 | const { pk, timestampCol } = tableInfo; |
| 300 | const exclude = tableInfo.excludeColumns ?? []; |
| 301 | console.log( |
| 302 | `[SimpleSync] Applying table ${tableName}: ${tableData.records.length} record(s), ${tableData.deletedIds.length} deletion(s)`, |
| 303 | ); |
| 304 | const allIdsToCheck = [ |
| 305 | ...tableData.records.map((record) => record[pk]).filter((value) => value !== undefined), |
| 306 | ...tableData.deletedIds, |
| 307 | ]; |
| 308 | const existingRecords = await loadExistingRecordStates( |
| 309 | db, |
| 310 | tableName, |
| 311 | pk, |
| 312 | timestampCol, |
| 313 | allIdsToCheck, |
| 314 | ); |
| 315 | const remoteRecordIds = new Set( |
| 316 | tableData.records |
| 317 | .map((record) => record[pk]) |
| 318 | .filter((value) => value !== undefined) |
| 319 | .map(String), |
| 320 | ); |
| 321 | let processedRecords = 0; |
| 322 | |
| 323 | for (const record of tableData.records) { |
| 324 | const pkValue = record[pk]; |
| 325 | const remoteTs = record[timestampCol] as number; |
| 326 | |
| 327 | const safeRecord = |
| 328 | exclude.length > 0 |
| 329 | ? Object.fromEntries(Object.entries(record).filter(([k]) => !exclude.includes(k))) |
| 330 | : record; |
| 331 | |
| 332 | const localState = existingRecords.get(String(pkValue)); |
| 333 | if (!options.forceApply && !shouldApplyRemoteRecord(record, timestampCol, localState)) { |
| 334 | skipped++; |
| 335 | } else { |
no test coverage detected