Skip to content

Commit 1a728ed

Browse files
committed
refactor(persist): preserve cache update timing on write failures
Split the orchestrator API into read_*/refresh_cache_* variants so the cache is only mutated when callers explicitly want to reflect on-disk state (restore, list_sessions) or after a write succeeds. Save, delete, and rename now read without touching the cache, matching the original service.lua semantics where a failed write must not clobber the cached view.
1 parent a916d3a commit 1a728ed

3 files changed

Lines changed: 144 additions & 15 deletions

File tree

lua/peekstack/persist/orchestrator.lua

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,6 @@ function M.ensure_data(data)
1919
return migrate.ensure(data)
2020
end
2121

22-
---Migrate read data and refresh the in-memory cache.
23-
---@param read_data PeekstackStoreData
24-
---@return PeekstackStoreData
25-
function M.update_cache(read_data)
26-
return cache.update(M.ensure_data(read_data))
27-
end
28-
2922
---Check if persistence is enabled, optionally notifying when disabled.
3023
---@param silent? boolean
3124
---@return boolean
@@ -39,20 +32,42 @@ function M.ensure_enabled(silent)
3932
return true
4033
end
4134

42-
---Asynchronously read store data, refresh cache, and pass migrated data to `on_done`.
35+
---Asynchronously read store data and pass migrated data to `on_done`.
36+
---Does NOT touch the cache; callers that want to refresh it should use
37+
---`refresh_cache_async` instead. This keeps save/delete/rename flows from
38+
---updating the cache before a successful write.
4339
---@param on_done fun(data: PeekstackStoreData)
4440
function M.read_async(on_done)
4541
store.read(SCOPE, {
4642
on_done = function(read_data)
47-
on_done(M.update_cache(read_data))
43+
on_done(M.ensure_data(read_data))
4844
end,
4945
})
5046
end
5147

52-
---Synchronously read store data and refresh cache.
48+
---Synchronously read and migrate store data without touching the cache.
5349
---@return PeekstackStoreData
5450
function M.read_sync()
55-
return M.update_cache(store.read_sync(SCOPE))
51+
return M.ensure_data(store.read_sync(SCOPE))
52+
end
53+
54+
---Asynchronously read store data and refresh the cache from disk.
55+
---Used by read-only flows (restore, list_sessions) that should reflect the
56+
---latest persisted state in memory.
57+
---@param on_done fun(data: PeekstackStoreData)
58+
function M.refresh_cache_async(on_done)
59+
M.read_async(function(data)
60+
cache.update(data)
61+
on_done(data)
62+
end)
63+
end
64+
65+
---Synchronously read store data and refresh the cache from disk.
66+
---@return PeekstackStoreData
67+
function M.refresh_cache_sync()
68+
local data = M.read_sync()
69+
cache.update(data)
70+
return data
5671
end
5772

5873
---Asynchronously write data; on success refresh cache before calling `on_done`.

lua/peekstack/persist/service.lua

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ function M.save_current(name, opts)
5050
local items = sessions.collect_items(opts and opts.root_winid or nil)
5151

5252
if sync then
53-
local data = sessions.upsert(orchestrator.ensure_data(orchestrator.read_sync()), resolved_name, items)
53+
local data = sessions.upsert(orchestrator.read_sync(), resolved_name, items)
5454
local success = orchestrator.write_sync(data)
5555
notify_save_result(success, resolved_name, items, silent)
5656
finish(success)
@@ -84,7 +84,7 @@ function M.restore(name, opts)
8484
end
8585

8686
local resolved_name = sessions.resolve_name(name)
87-
orchestrator.read_async(function(data)
87+
orchestrator.refresh_cache_async(function(data)
8888
local session = data.sessions[resolved_name]
8989

9090
if not session or not session.items or #session.items == 0 then
@@ -158,11 +158,11 @@ function M.list_sessions(opts)
158158
end
159159

160160
if on_done then
161-
orchestrator.read_async(function(data)
161+
orchestrator.refresh_cache_async(function(data)
162162
on_done(data.sessions or {})
163163
end)
164164
elseif not orchestrator.cache_loaded() then
165-
orchestrator.read_sync()
165+
orchestrator.refresh_cache_sync()
166166
end
167167

168168
return orchestrator.cache_sessions()

tests/persist_sessions_spec.lua

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,4 +899,118 @@ describe("peekstack.persist.sessions", function()
899899

900900
stack_view.toggle()
901901
end)
902+
903+
it("should not update cache when sync save write fails", function()
904+
push_popup("write_fail_sync", { title = "Sync fail" })
905+
906+
local original_write_sync = store.write_sync
907+
local ok, err = pcall(function()
908+
store.write_sync = function(_scope, _data)
909+
return false
910+
end
911+
912+
local saved = nil
913+
persist.save_current("write_fail_sync_session", {
914+
silent = true,
915+
sync = true,
916+
on_done = function(success)
917+
saved = success
918+
end,
919+
})
920+
921+
assert.is_false(saved)
922+
local sessions = persist.list_sessions({ silent = true })
923+
assert.is_nil(sessions["write_fail_sync_session"])
924+
end)
925+
926+
store.write_sync = original_write_sync
927+
928+
if not ok then
929+
error(err)
930+
end
931+
end)
932+
933+
it("should not update cache when async save write fails", function()
934+
push_popup("write_fail_async", { title = "Async fail" })
935+
936+
local original_write = store.write
937+
local ok, err = pcall(function()
938+
store.write = function(_scope, _data, opts)
939+
if opts and opts.on_done then
940+
vim.schedule(function()
941+
opts.on_done(false)
942+
end)
943+
end
944+
end
945+
946+
local saved = nil
947+
persist.save_current("write_fail_async_session", {
948+
silent = true,
949+
on_done = function(success)
950+
saved = success
951+
end,
952+
})
953+
954+
local waited = vim.wait(wait_timeout_ms, function()
955+
return saved ~= nil
956+
end, wait_interval_ms)
957+
assert.is_true(waited, "Timed out waiting for save callback")
958+
assert.is_false(saved)
959+
960+
local listed = nil
961+
persist.list_sessions({
962+
silent = true,
963+
on_done = function(sessions)
964+
listed = sessions
965+
end,
966+
})
967+
local list_waited = vim.wait(wait_timeout_ms, function()
968+
return listed ~= nil
969+
end, wait_interval_ms)
970+
assert.is_true(list_waited, "Timed out waiting for list callback")
971+
assert.is_nil(listed["write_fail_async_session"])
972+
end)
973+
974+
store.write = original_write
975+
976+
if not ok then
977+
error(err)
978+
end
979+
end)
980+
981+
it("should not update cache when delete write fails", function()
982+
push_popup("delete_write_fail", { title = "To delete" })
983+
persist.save_current("delete_write_fail_session", { silent = true, sync = true })
984+
wait_for_session("delete_write_fail_session", true)
985+
986+
local original_write = store.write
987+
local write_done = false
988+
local ok, err = pcall(function()
989+
store.write = function(_scope, _data, opts)
990+
if opts and opts.on_done then
991+
vim.schedule(function()
992+
opts.on_done(false)
993+
write_done = true
994+
end)
995+
end
996+
end
997+
998+
persist.delete_session("delete_write_fail_session")
999+
1000+
local waited = vim.wait(wait_timeout_ms, function()
1001+
return write_done
1002+
end, wait_interval_ms)
1003+
assert.is_true(waited, "Timed out waiting for delete write attempt")
1004+
1005+
-- Cache should still hold the session because the write failed.
1006+
local cached = persist.list_sessions({ silent = true })
1007+
assert.is_not_nil(cached["delete_write_fail_session"])
1008+
end)
1009+
1010+
store.write = original_write
1011+
1012+
if not ok then
1013+
error(err)
1014+
end
1015+
end)
9021016
end)

0 commit comments

Comments
 (0)