feat: converge Today sync across active devices (#361)
This commit is contained in:
1 parent
7ec3980713
commit
3c743a2b71
9 files changed
+217
-11
No files matched your search
@@ -137,4 +137,4 @@ def test_markdown_work_bodies_are_mobile_safe_block_containers():
|
||||
assert ".markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere;" in css
|
||||
assert ".markdown-content pre { max-width:100%; overflow-x:auto;" in css
|
||||
assert ".markdown-content a { min-height:44px;" in css
|
||||
assert "stackchain-dashboard-shell-v46" in worker
|
||||
assert "stackchain-dashboard-shell-v47" in worker
|
||||
@@ -35,4 +35,4 @@ def test_offline_shell_contains_every_local_dashboard_runtime_asset():
|
||||
shell_assets = set(re.findall(r"BASE \+ '([^']+)'", worker.split("async function sessionCsrf", 1)[0]))
|
||||
|
||||
assert local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
|
||||
assert "stackchain-dashboard-shell-v46" in worker
|
||||
assert "stackchain-dashboard-shell-v47" in worker
|
||||
@@ -108,23 +108,30 @@ async function dispatchNotificationClick(route) {{
|
||||
def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v46" in source
|
||||
assert "stackchain-dashboard-shell-v47" in source
|
||||
assert "BASE + 'static/dashboard.css'" in source
|
||||
assert "BASE + 'static/dashboard.js'" in source
|
||||
assert "BASE + 'static/install-app.js'" in source
|
||||
|
||||
|
||||
def test_today_convergence_ships_in_a_new_shell_cache():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v47" in source
|
||||
assert "BASE + 'static/today-sync.js'" in source
|
||||
|
||||
|
||||
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v46" in source
|
||||
assert "stackchain-dashboard-shell-v47" in source
|
||||
assert "BASE + 'static/mobile-search-viewport.js'" in source
|
||||
|
||||
|
||||
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
|
||||
source = WORKER.read_text()
|
||||
|
||||
assert "stackchain-dashboard-shell-v46" in source
|
||||
assert "stackchain-dashboard-shell-v47" in source
|
||||
assert "BASE + 'static/update-ownership.js'" in source
|
||||
|
||||
|
||||
|
||||
@@ -171,3 +171,139 @@ sync.enqueue('add', '6');
|
||||
"ids": ["1", "2", "3", "4", "5"],
|
||||
"status": "full",
|
||||
}
|
||||
|
||||
|
||||
def test_tabs_ignore_an_older_response_after_a_newer_revision_is_broadcast():
|
||||
script = f"""
|
||||
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
|
||||
const values = new Map();
|
||||
const storage = {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}};
|
||||
const listeners = [];
|
||||
const createChannel = () => ({{
|
||||
addEventListener: (_name, listener) => listeners.push(listener),
|
||||
postMessage: data => listeners.forEach(listener => listener({{data}})),
|
||||
}});
|
||||
let resolveOld;
|
||||
let resolveNew;
|
||||
const oldHistory = [];
|
||||
const newHistory = [];
|
||||
const oldTab = createTodaySync({{
|
||||
storage, getLogin:()=> 'timmy', createChannel,
|
||||
fetchJson:()=>new Promise(resolve=>{{resolveOld=resolve}}),
|
||||
onRemoteIds:ids=>oldHistory.push(ids), onStatus:()=>{{}},
|
||||
}});
|
||||
const newTab = createTodaySync({{
|
||||
storage, getLogin:()=> 'timmy', createChannel,
|
||||
fetchJson:()=>new Promise(resolve=>{{resolveNew=resolve}}),
|
||||
onRemoteIds:ids=>newHistory.push(ids), onStatus:()=>{{}},
|
||||
}});
|
||||
(async()=>{{
|
||||
const oldFlush = oldTab.flush();
|
||||
const newFlush = newTab.flush();
|
||||
await Promise.resolve();
|
||||
resolveNew({{revision:2,ids:['new']}});
|
||||
await newFlush;
|
||||
resolveOld({{revision:1,ids:['old']}});
|
||||
await oldFlush;
|
||||
process.stdout.write(JSON.stringify({{oldHistory,newHistory}}));
|
||||
}})();
|
||||
"""
|
||||
result = json.loads(
|
||||
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
|
||||
)
|
||||
|
||||
assert result["oldHistory"][-1] == ["new"]
|
||||
assert result["newHistory"][-1] == ["new"]
|
||||
|
||||
|
||||
def test_returning_online_replays_pending_operations():
|
||||
script = f"""
|
||||
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
|
||||
const values = new Map();
|
||||
const handlers = {{}};
|
||||
const requests = [];
|
||||
let revision = 1;
|
||||
const sync = createTodaySync({{
|
||||
storage: {{getItem:k=>values.get(k)||null,setItem:(k,v)=>values.set(k,v),removeItem:k=>values.delete(k)}},
|
||||
getLogin:()=> 'timmy', createOperationId:()=> 'offline-add',
|
||||
fetchJson:async (_url, options={{}})=>{{
|
||||
requests.push(options.method || 'GET');
|
||||
if (options.method) return {{revision:++revision,ids:['issue:r:1:']}};
|
||||
return {{revision,ids:[]}};
|
||||
}},
|
||||
onRemoteIds:()=>{{}}, onStatus:()=>{{}},
|
||||
}});
|
||||
sync.enqueue('add', 'issue:r:1:');
|
||||
sync.startLifecycle({{
|
||||
window: {{addEventListener:(name, handler)=>{{handlers[name]=handler}}}},
|
||||
document: {{addEventListener:()=>{{}}, hidden:false}},
|
||||
}});
|
||||
(async()=>{{
|
||||
await handlers.online();
|
||||
process.stdout.write(JSON.stringify({{requests,pending:sync.pending()}}));
|
||||
}})();
|
||||
"""
|
||||
result = json.loads(
|
||||
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
|
||||
)
|
||||
|
||||
assert result == {"requests": ["GET", "PATCH"], "pending": []}
|
||||
|
||||
|
||||
def test_foregrounding_a_stale_tab_refreshes_the_saved_plan():
|
||||
script = f"""
|
||||
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
|
||||
const handlers = {{}};
|
||||
const adopted = [];
|
||||
const documentObject = {{
|
||||
hidden:true,
|
||||
addEventListener:(name, handler)=>{{handlers[name]=handler}},
|
||||
}};
|
||||
const sync = createTodaySync({{
|
||||
storage: {{getItem:()=>null,setItem:()=>{{}},removeItem:()=>{{}}}},
|
||||
getLogin:()=> 'timmy',
|
||||
fetchJson:async ()=>({{revision:7,ids:['issue:r:7:']}}),
|
||||
onRemoteIds:ids=>adopted.push(ids), onStatus:()=>{{}},
|
||||
}});
|
||||
sync.startLifecycle({{
|
||||
window: {{addEventListener:()=>{{}}}}, document:documentObject,
|
||||
}});
|
||||
(async()=>{{
|
||||
await handlers.visibilitychange();
|
||||
documentObject.hidden = false;
|
||||
await handlers.visibilitychange();
|
||||
process.stdout.write(JSON.stringify(adopted));
|
||||
}})();
|
||||
"""
|
||||
result = json.loads(
|
||||
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
|
||||
)
|
||||
|
||||
assert result == [["issue:r:7:"]]
|
||||
|
||||
|
||||
def test_old_account_broadcasts_are_ignored_after_identity_changes():
|
||||
script = f"""
|
||||
const createTodaySync = require({json.dumps(str(TODAY_SYNC))});
|
||||
let login = 'timmy';
|
||||
let listener;
|
||||
const adopted = [];
|
||||
const sync = createTodaySync({{
|
||||
storage: {{getItem:()=>null,setItem:()=>{{}},removeItem:()=>{{}}}},
|
||||
getLogin:()=>login,
|
||||
createChannel:()=>({{addEventListener:(_name, handler)=>{{listener=handler}},postMessage:()=>{{}},close:()=>{{}}}}),
|
||||
fetchJson:async ()=>({{revision:1,ids:['timmy-plan']}}),
|
||||
onRemoteIds:ids=>adopted.push(ids), onStatus:()=>{{}},
|
||||
}});
|
||||
(async()=>{{
|
||||
await sync.flush();
|
||||
login = 'alexander';
|
||||
listener({{data:{{revision:2,ids:['timmy-secret']}}}});
|
||||
process.stdout.write(JSON.stringify(adopted));
|
||||
}})();
|
||||
"""
|
||||
result = json.loads(
|
||||
subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True).stdout
|
||||
)
|
||||
|
||||
assert result == [["timmy-plan"]]
|
||||
@@ -173,6 +173,13 @@ async def test_dashboard_runs_the_curated_today_queue_as_a_mobile_work_flow():
|
||||
assert "Another device filled Today · showing its saved plan." in html
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_dashboard_activates_today_lifecycle_convergence():
|
||||
html = await dashboard()
|
||||
|
||||
assert "todaySync.startLifecycle({ window, document });" in html
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_retained_authenticated_context_keeps_local_planning_separate_from_fresh_delivery():
|
||||
html = await dashboard()
|
||||
|
||||
Reference in new issue
Block a user