|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +/* |
| 4 | + * Simulation of AAPS V1 DataSyncSelectorV1.processChangedProfileStore() |
| 5 | + * to test whether changing `confirmLastProfileStore(now)` to |
| 6 | + * `confirmLastProfileStore(lastChangeAtSyncStart)` fixes the lost-edit race. |
| 7 | + * |
| 8 | + * Faithfully models (from DataSyncSelectorV1.kt:786-805): |
| 9 | + * 1. read lastChange = LocalProfileLastChange preference |
| 10 | + * 2. read lastSync = ProfileStoreLastSyncedId preference |
| 11 | + * 3. if lastChange == 0 -> return |
| 12 | + * 4. if lastChange > lastSync: |
| 13 | + * send profile, wait for ack (~60s) |
| 14 | + * on ack: confirmLastProfileStore(<TIMESTAMP>) |
| 15 | + * |
| 16 | + * The bug: <TIMESTAMP> is dateUtil.now() captured AFTER the ack returns. |
| 17 | + * The fix: <TIMESTAMP> should be the lastChange value captured BEFORE send. |
| 18 | + * |
| 19 | + * Race scenario: user edits at T1, sync starts and sends profile, while we |
| 20 | + * wait for the server ack the user edits AGAIN at T3, ack arrives at T4. |
| 21 | + * After buggy confirm, lastSync = T4 > T3, so the T3 edit is permanently |
| 22 | + * unsyncable until LocalProfileLastChange exceeds T4. |
| 23 | + */ |
| 24 | + |
| 25 | +function makeStrategy(name, confirmTimestampFn) { |
| 26 | + return { name, confirmTimestampFn }; |
| 27 | +} |
| 28 | + |
| 29 | +async function runScenario(strategy) { |
| 30 | + const prefs = { |
| 31 | + LocalProfileLastChange: 0, |
| 32 | + ProfileStoreLastSyncedId: 0, |
| 33 | + }; |
| 34 | + const sentProfiles = []; // each entry: {sentAt, lastChangeAtSend} |
| 35 | + |
| 36 | + // Simulate: pretend initial state — never synced |
| 37 | + function storeSettings(t) { prefs.LocalProfileLastChange = t; } |
| 38 | + function userEdit(t) { storeSettings(t); } |
| 39 | + |
| 40 | + // The sync loop, modeled as a single invocation taking a "now" generator. |
| 41 | + // Returns whether a profile was actually sent. |
| 42 | + async function processChangedProfileStore(nowAtStart, nowAtAck, concurrentEditAt) { |
| 43 | + if (prefs.LocalProfileLastChange === 0) return { sent: false }; |
| 44 | + const lastChange = prefs.LocalProfileLastChange; |
| 45 | + const lastSync = prefs.ProfileStoreLastSyncedId; |
| 46 | + if (!(lastChange > lastSync)) return { sent: false }; |
| 47 | + |
| 48 | + sentProfiles.push({ sentAt: nowAtStart, lastChangeAtSend: lastChange }); |
| 49 | + |
| 50 | + // ---- 60s wait for ack window: simulate concurrent user edit ---- |
| 51 | + if (concurrentEditAt != null) { |
| 52 | + userEdit(concurrentEditAt); |
| 53 | + } |
| 54 | + |
| 55 | + // ack arrived |
| 56 | + const confirmTimestamp = strategy.confirmTimestampFn({ |
| 57 | + lastChangeAtSend: lastChange, |
| 58 | + nowAtStart, |
| 59 | + nowAtAck, |
| 60 | + }); |
| 61 | + prefs.ProfileStoreLastSyncedId = confirmTimestamp; |
| 62 | + return { sent: true, confirmTimestamp }; |
| 63 | + } |
| 64 | + |
| 65 | + // ---- Scenario timeline (ms) ---- |
| 66 | + // T=1000 user edit #1 |
| 67 | + // T=1100 sync poll fires, sends profile |
| 68 | + // T=1500 user edit #2 (concurrent, while ack pending) |
| 69 | + // T=2000 ack returns |
| 70 | + // T=2100 next sync poll fires |
| 71 | + userEdit(1000); |
| 72 | + const r1 = await processChangedProfileStore(1100, 2000, /*concurrentEditAt=*/1500); |
| 73 | + const r2 = await processChangedProfileStore(2100, 2200, /*concurrentEditAt=*/null); |
| 74 | + |
| 75 | + return { strategy: strategy.name, prefs, sentProfiles, r1, r2 }; |
| 76 | +} |
| 77 | + |
| 78 | +(async function main() { |
| 79 | + const buggy = makeStrategy( |
| 80 | + 'BUGGY: confirmLastProfileStore(dateUtil.now())', |
| 81 | + ({ nowAtAck }) => nowAtAck |
| 82 | + ); |
| 83 | + const fixed = makeStrategy( |
| 84 | + 'FIXED: confirmLastProfileStore(lastChangeAtSend)', |
| 85 | + ({ lastChangeAtSend }) => lastChangeAtSend |
| 86 | + ); |
| 87 | + |
| 88 | + for (const strat of [buggy, fixed]) { |
| 89 | + const out = await runScenario(strat); |
| 90 | + console.log('=== ' + out.strategy + ' ==='); |
| 91 | + console.log(' sends:', out.sentProfiles); |
| 92 | + console.log(' final prefs:', out.prefs); |
| 93 | + const editsMade = [1000, 1500]; |
| 94 | + const editsSynced = out.sentProfiles.map(p => p.lastChangeAtSend); |
| 95 | + const lostEdits = editsMade.filter(e => !editsSynced.includes(e)); |
| 96 | + console.log(' edits made: ', editsMade); |
| 97 | + console.log(' edits sent: ', editsSynced); |
| 98 | + console.log(' LOST edits: ', lostEdits.length ? lostEdits : 'none'); |
| 99 | + console.log(); |
| 100 | + } |
| 101 | +})(); |
0 commit comments