From 16841f18e199c518eef5e5a170d8d9fcc49b6032 Mon Sep 17 00:00:00 2001 From: timmy Date: Thu, 20 Aug 2026 00:19:50 +0000 Subject: [PATCH] fix: drain first-task completion during Today refresh (Closes #1156) --- frontend/today-sync.js | 5 ++++- tests/test_today_sync.py | 47 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/frontend/today-sync.js b/frontend/today-sync.js index a6497de..33edd32 100644 --- a/frontend/today-sync.js +++ b/frontend/today-sync.js @@ -303,7 +303,10 @@ function createTodaySync({ storage, getLogin, fetchJson, onRemoteIds, onRemotePl let operations = pending(); let plan; let hadConflict = false; - if (!operations.length) plan = await fetchJson('api/v1/today'); + if (!operations.length) { + plan = await fetchJson('api/v1/today'); + operations = pending(); + } while (operations.length) { if (key() !== ownerKey) return false; const batch = operations.slice(0, 50); diff --git a/tests/test_today_sync.py b/tests/test_today_sync.py index 7826cf1..0fa94f0 100644 --- a/tests/test_today_sync.py +++ b/tests/test_today_sync.py @@ -84,6 +84,53 @@ sync.enqueue('add', 'issue:r:1:'); } +def test_activation_queued_during_an_empty_inflight_refresh_is_drained_before_it_settles(): + script = f""" +const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); +const values = new Map(); +const storage = {{get length(){{return values.size}},key:i=>[...values.keys()][i]||null, + getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}}; +let releaseRefresh; +const refreshStarted = new Promise(resolve => {{ globalThis.refreshStarted = resolve; }}); +const patches = []; +const statuses = []; +const sync = createTodaySync({{ + storage, getLogin: () => 'timmy', createOperationId: () => 'activation-1', + fetchJson: async (_url, options={{}}) => {{ + if (!options.method) {{ + globalThis.refreshStarted(); + await new Promise(resolve => {{ releaseRefresh = resolve; }}); + return {{revision:0, ids:[]}}; + }} + const operations = JSON.parse(options.body).operations; + patches.push(...operations); + return {{revision:1, ids:[], first_task_state:'complete', + accepted_operation_ids:operations.map(item => item.operation_id)}}; + }}, + onRemoteIds: () => {{}}, onStatus: state => statuses.push(state), +}}); +(async () => {{ + const refreshing = sync.flush(); + await refreshStarted; + sync.enqueueActivation('complete'); + const sharedFlush = sync.flush(); + releaseRefresh(); + await sharedFlush; + process.stdout.write(JSON.stringify({{ + sharesInflight: refreshing === sharedFlush, patches, pending:sync.pending(), statuses, + }})); +}})(); +""" + result = json.loads(subprocess.run( + ["node", "-e", script], check=True, capture_output=True, text=True + ).stdout) + + assert result["sharesInflight"] is True + assert [operation["activation_state"] for operation in result["patches"]] == ["complete"] + assert result["pending"] == [] + assert result["statuses"][-1] == "saved" + + def test_operations_queued_in_the_same_millisecond_preserve_enqueue_order(): script = f""" const createTodaySync = require({json.dumps(str(TODAY_SYNC))}); -- 2.43.0