refactor(sync): extract shared utilities into sync/util.lua
Problem: sync epilogue code (`s:save()`, `_recompute_counts()`, `buffer.render()`) and `fmt_counts` were duplicated across `gcal.lua` and `gtasks.lua`. The concurrency guard lived in `oauth.lua`, coupling non-OAuth backends to the OAuth module. Solution: create `sync/util.lua` with `async`, `system`, `with_guard`, `finish`, and `fmt_counts`. Delegate from `oauth.lua` and replace duplicated code in both backends. Add per-backend `auth()` and `auth_complete()` methods to `gcal.lua` and `gtasks.lua`.
This commit is contained in:
parent
ff197c1f7c
commit
27190fd26c
3 changed files with 166 additions and 61 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
local config = require('pending.config')
|
local config = require('pending.config')
|
||||||
local log = require('pending.log')
|
local log = require('pending.log')
|
||||||
local oauth = require('pending.sync.oauth')
|
local oauth = require('pending.sync.oauth')
|
||||||
|
local util = require('pending.sync.util')
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
|
@ -154,21 +155,6 @@ local function unlink_remote(task, extra, now_ts)
|
||||||
task.modified = now_ts
|
task.modified = now_ts
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param parts {[1]: integer, [2]: string}[]
|
|
||||||
---@return string
|
|
||||||
local function fmt_counts(parts)
|
|
||||||
local items = {}
|
|
||||||
for _, p in ipairs(parts) do
|
|
||||||
if p[1] > 0 then
|
|
||||||
table.insert(items, p[1] .. ' ' .. p[2])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if #items == 0 then
|
|
||||||
return 'nothing to do'
|
|
||||||
end
|
|
||||||
return table.concat(items, ' | ')
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.push()
|
function M.push()
|
||||||
oauth.with_token(oauth.google_client, 'gcal', function(access_token)
|
oauth.with_token(oauth.google_client, 'gcal', function(access_token)
|
||||||
local calendars, cal_err = get_all_calendars(access_token)
|
local calendars, cal_err = get_all_calendars(access_token)
|
||||||
|
|
@ -246,13 +232,8 @@ function M.push()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
s:save()
|
util.finish(s)
|
||||||
require('pending')._recompute_counts()
|
log.info('gcal push: ' .. util.fmt_counts({
|
||||||
local buffer = require('pending.buffer')
|
|
||||||
if buffer.bufnr() and vim.api.nvim_buf_is_valid(buffer.bufnr()) then
|
|
||||||
buffer.render(buffer.bufnr())
|
|
||||||
end
|
|
||||||
log.info('gcal push: ' .. fmt_counts({
|
|
||||||
{ created, 'added' },
|
{ created, 'added' },
|
||||||
{ updated, 'updated' },
|
{ updated, 'updated' },
|
||||||
{ deleted, 'removed' },
|
{ deleted, 'removed' },
|
||||||
|
|
@ -261,6 +242,32 @@ function M.push()
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param args? string
|
||||||
|
---@return nil
|
||||||
|
function M.auth(args)
|
||||||
|
if args == 'clear' then
|
||||||
|
oauth.google_client:clear_tokens()
|
||||||
|
log.info('gcal: OAuth tokens cleared — run :Pending auth gcal to re-authenticate.')
|
||||||
|
elseif args == 'reset' then
|
||||||
|
oauth.google_client:_wipe()
|
||||||
|
log.info(
|
||||||
|
'gcal: OAuth tokens and credentials cleared — run :Pending auth gcal to set up from scratch.'
|
||||||
|
)
|
||||||
|
else
|
||||||
|
local creds = oauth.google_client:resolve_credentials()
|
||||||
|
if creds.client_id == oauth.BUNDLED_CLIENT_ID then
|
||||||
|
oauth.google_client:setup()
|
||||||
|
else
|
||||||
|
oauth.google_client:auth()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return string[]
|
||||||
|
function M.auth_complete()
|
||||||
|
return { 'clear', 'reset' }
|
||||||
|
end
|
||||||
|
|
||||||
---@return nil
|
---@return nil
|
||||||
function M.health()
|
function M.health()
|
||||||
oauth.health(M.name)
|
oauth.health(M.name)
|
||||||
|
|
@ -268,7 +275,7 @@ function M.health()
|
||||||
if tokens and tokens.refresh_token then
|
if tokens and tokens.refresh_token then
|
||||||
vim.health.ok('gcal tokens found')
|
vim.health.ok('gcal tokens found')
|
||||||
else
|
else
|
||||||
vim.health.info('no gcal tokens — run :Pending auth')
|
vim.health.info('no gcal tokens — run :Pending auth gcal')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
local config = require('pending.config')
|
local config = require('pending.config')
|
||||||
local log = require('pending.log')
|
local log = require('pending.log')
|
||||||
local oauth = require('pending.sync.oauth')
|
local oauth = require('pending.sync.oauth')
|
||||||
|
local util = require('pending.sync.util')
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
|
@ -195,21 +196,6 @@ local function unlink_remote(task, now_ts)
|
||||||
task.modified = now_ts
|
task.modified = now_ts
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param parts {[1]: integer, [2]: string}[]
|
|
||||||
---@return string
|
|
||||||
local function fmt_counts(parts)
|
|
||||||
local items = {}
|
|
||||||
for _, p in ipairs(parts) do
|
|
||||||
if p[1] > 0 then
|
|
||||||
table.insert(items, p[1] .. ' ' .. p[2])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if #items == 0 then
|
|
||||||
return 'nothing to do'
|
|
||||||
end
|
|
||||||
return table.concat(items, ' | ')
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param task pending.Task
|
---@param task pending.Task
|
||||||
---@return table
|
---@return table
|
||||||
local function task_to_gtask(task)
|
local function task_to_gtask(task)
|
||||||
|
|
@ -447,13 +433,8 @@ function M.push()
|
||||||
local by_gtasks_id = build_id_index(s)
|
local by_gtasks_id = build_id_index(s)
|
||||||
local created, updated, deleted, failed =
|
local created, updated, deleted, failed =
|
||||||
push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
|
push_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
|
||||||
s:save()
|
util.finish(s)
|
||||||
require('pending')._recompute_counts()
|
log.info('gtasks push: ' .. util.fmt_counts({
|
||||||
local buffer = require('pending.buffer')
|
|
||||||
if buffer.bufnr() and vim.api.nvim_buf_is_valid(buffer.bufnr()) then
|
|
||||||
buffer.render(buffer.bufnr())
|
|
||||||
end
|
|
||||||
log.info('gtasks push: ' .. fmt_counts({
|
|
||||||
{ created, 'added' },
|
{ created, 'added' },
|
||||||
{ updated, 'updated' },
|
{ updated, 'updated' },
|
||||||
{ deleted, 'deleted' },
|
{ deleted, 'deleted' },
|
||||||
|
|
@ -474,13 +455,8 @@ function M.pull()
|
||||||
local created, updated, failed, seen_remote_ids, fetched_list_ids =
|
local created, updated, failed, seen_remote_ids, fetched_list_ids =
|
||||||
pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
|
pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
|
||||||
local unlinked = detect_remote_deletions(s, seen_remote_ids, fetched_list_ids, now_ts)
|
local unlinked = detect_remote_deletions(s, seen_remote_ids, fetched_list_ids, now_ts)
|
||||||
s:save()
|
util.finish(s)
|
||||||
require('pending')._recompute_counts()
|
log.info('gtasks pull: ' .. util.fmt_counts({
|
||||||
local buffer = require('pending.buffer')
|
|
||||||
if buffer.bufnr() and vim.api.nvim_buf_is_valid(buffer.bufnr()) then
|
|
||||||
buffer.render(buffer.bufnr())
|
|
||||||
end
|
|
||||||
log.info('gtasks pull: ' .. fmt_counts({
|
|
||||||
{ created, 'added' },
|
{ created, 'added' },
|
||||||
{ updated, 'updated' },
|
{ updated, 'updated' },
|
||||||
{ unlinked, 'unlinked' },
|
{ unlinked, 'unlinked' },
|
||||||
|
|
@ -503,18 +479,13 @@ function M.sync()
|
||||||
local pulled_create, pulled_update, pulled_failed, seen_remote_ids, fetched_list_ids =
|
local pulled_create, pulled_update, pulled_failed, seen_remote_ids, fetched_list_ids =
|
||||||
pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
|
pull_pass(access_token, tasklists, s, now_ts, by_gtasks_id)
|
||||||
local unlinked = detect_remote_deletions(s, seen_remote_ids, fetched_list_ids, now_ts)
|
local unlinked = detect_remote_deletions(s, seen_remote_ids, fetched_list_ids, now_ts)
|
||||||
s:save()
|
util.finish(s)
|
||||||
require('pending')._recompute_counts()
|
log.info('gtasks sync — push: ' .. util.fmt_counts({
|
||||||
local buffer = require('pending.buffer')
|
|
||||||
if buffer.bufnr() and vim.api.nvim_buf_is_valid(buffer.bufnr()) then
|
|
||||||
buffer.render(buffer.bufnr())
|
|
||||||
end
|
|
||||||
log.info('gtasks sync — push: ' .. fmt_counts({
|
|
||||||
{ pushed_create, 'added' },
|
{ pushed_create, 'added' },
|
||||||
{ pushed_update, 'updated' },
|
{ pushed_update, 'updated' },
|
||||||
{ pushed_delete, 'deleted' },
|
{ pushed_delete, 'deleted' },
|
||||||
{ pushed_failed, 'failed' },
|
{ pushed_failed, 'failed' },
|
||||||
}) .. ' pull: ' .. fmt_counts({
|
}) .. ' pull: ' .. util.fmt_counts({
|
||||||
{ pulled_create, 'added' },
|
{ pulled_create, 'added' },
|
||||||
{ pulled_update, 'updated' },
|
{ pulled_update, 'updated' },
|
||||||
{ unlinked, 'unlinked' },
|
{ unlinked, 'unlinked' },
|
||||||
|
|
@ -533,6 +504,32 @@ M._push_pass = push_pass
|
||||||
M._pull_pass = pull_pass
|
M._pull_pass = pull_pass
|
||||||
M._detect_remote_deletions = detect_remote_deletions
|
M._detect_remote_deletions = detect_remote_deletions
|
||||||
|
|
||||||
|
---@param args? string
|
||||||
|
---@return nil
|
||||||
|
function M.auth(args)
|
||||||
|
if args == 'clear' then
|
||||||
|
oauth.google_client:clear_tokens()
|
||||||
|
log.info('gtasks: OAuth tokens cleared — run :Pending auth gtasks to re-authenticate.')
|
||||||
|
elseif args == 'reset' then
|
||||||
|
oauth.google_client:_wipe()
|
||||||
|
log.info(
|
||||||
|
'gtasks: OAuth tokens and credentials cleared — run :Pending auth gtasks to set up from scratch.'
|
||||||
|
)
|
||||||
|
else
|
||||||
|
local creds = oauth.google_client:resolve_credentials()
|
||||||
|
if creds.client_id == oauth.BUNDLED_CLIENT_ID then
|
||||||
|
oauth.google_client:setup()
|
||||||
|
else
|
||||||
|
oauth.google_client:auth()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return string[]
|
||||||
|
function M.auth_complete()
|
||||||
|
return { 'clear', 'reset' }
|
||||||
|
end
|
||||||
|
|
||||||
---@return nil
|
---@return nil
|
||||||
function M.health()
|
function M.health()
|
||||||
oauth.health(M.name)
|
oauth.health(M.name)
|
||||||
|
|
@ -540,7 +537,7 @@ function M.health()
|
||||||
if tokens and tokens.refresh_token then
|
if tokens and tokens.refresh_token then
|
||||||
vim.health.ok('gtasks tokens found')
|
vim.health.ok('gtasks tokens found')
|
||||||
else
|
else
|
||||||
vim.health.info('no gtasks tokens — run :Pending auth')
|
vim.health.info('no gtasks tokens — run :Pending auth gtasks')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
101
spec/sync_util_spec.lua
Normal file
101
spec/sync_util_spec.lua
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
require('spec.helpers')
|
||||||
|
|
||||||
|
local config = require('pending.config')
|
||||||
|
local util = require('pending.sync.util')
|
||||||
|
|
||||||
|
describe('sync util', function()
|
||||||
|
before_each(function()
|
||||||
|
config.reset()
|
||||||
|
end)
|
||||||
|
|
||||||
|
after_each(function()
|
||||||
|
config.reset()
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('fmt_counts', function()
|
||||||
|
it('returns nothing to do for empty counts', function()
|
||||||
|
assert.equals('nothing to do', util.fmt_counts({}))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('returns nothing to do when all zero', function()
|
||||||
|
assert.equals('nothing to do', util.fmt_counts({ { 0, 'added' }, { 0, 'failed' } }))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('formats single non-zero count', function()
|
||||||
|
assert.equals('3 added', util.fmt_counts({ { 3, 'added' }, { 0, 'failed' } }))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('joins multiple non-zero counts with pipe', function()
|
||||||
|
local result = util.fmt_counts({ { 2, 'added' }, { 1, 'updated' }, { 0, 'failed' } })
|
||||||
|
assert.equals('2 added | 1 updated', result)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('with_guard', function()
|
||||||
|
it('prevents concurrent calls', function()
|
||||||
|
local inner_called = false
|
||||||
|
local blocked = false
|
||||||
|
|
||||||
|
local msgs = {}
|
||||||
|
local orig = vim.notify
|
||||||
|
vim.notify = function(m, level)
|
||||||
|
if level == vim.log.levels.WARN then
|
||||||
|
table.insert(msgs, m)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
util.with_guard('test', function()
|
||||||
|
inner_called = true
|
||||||
|
util.with_guard('test2', function()
|
||||||
|
blocked = true
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
vim.notify = orig
|
||||||
|
assert.is_true(inner_called)
|
||||||
|
assert.is_false(blocked)
|
||||||
|
assert.equals(1, #msgs)
|
||||||
|
assert.truthy(msgs[1]:find('Sync already in progress'))
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('clears guard after error', function()
|
||||||
|
pcall(util.with_guard, 'err-test', function()
|
||||||
|
error('boom')
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert.is_false(util.sync_in_flight())
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('clears guard after success', function()
|
||||||
|
util.with_guard('ok-test', function() end)
|
||||||
|
assert.is_false(util.sync_in_flight())
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
|
||||||
|
describe('finish', function()
|
||||||
|
it('calls save and recompute', function()
|
||||||
|
local helpers = require('spec.helpers')
|
||||||
|
local store_mod = require('pending.store')
|
||||||
|
local tmpdir = helpers.tmpdir()
|
||||||
|
vim.g.pending = { data_path = tmpdir .. '/tasks.json' }
|
||||||
|
config.reset()
|
||||||
|
package.loaded['pending'] = nil
|
||||||
|
local pending = require('pending')
|
||||||
|
|
||||||
|
local s = store_mod.new(tmpdir .. '/tasks.json')
|
||||||
|
s:load()
|
||||||
|
s:add({ description = 'Test', status = 'pending', category = 'Work', priority = 0 })
|
||||||
|
|
||||||
|
util.finish(s)
|
||||||
|
|
||||||
|
local reloaded = store_mod.new(tmpdir .. '/tasks.json')
|
||||||
|
reloaded:load()
|
||||||
|
assert.equals(1, #reloaded:tasks())
|
||||||
|
|
||||||
|
vim.fn.delete(tmpdir, 'rf')
|
||||||
|
vim.g.pending = nil
|
||||||
|
config.reset()
|
||||||
|
package.loaded['pending'] = nil
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue