feat: make mobile queue priority self-healing (Closes #1466)
This commit is contained in:
1 parent
c5b801460d
commit
1e8d27c18f
4 files changed
+168
-31
No files matched your search
@@ -149,42 +149,109 @@ const fetchJson = async (url, init={{}}) => {{
|
||||
]
|
||||
|
||||
|
||||
def test_mobile_queue_priority_serializes_rapid_edits_and_publishes_latest_order():
|
||||
def test_mobile_queue_priority_coalesces_rapid_edits_and_publishes_latest_order():
|
||||
script = f"""
|
||||
const createPriority = require({json.dumps(str(QUEUE_PRIORITY))});
|
||||
const values = new Map(); const pending=[]; const payloads=[];
|
||||
const fetchJson = async (_url, init) => new Promise(resolve => {{
|
||||
const payload=JSON.parse(init.body); payloads.push(payload); pending.push(() => resolve({{revision:payload.revision+1,order:payload.order}}));
|
||||
}});
|
||||
const values = new Map(); const payloads=[]; const timers=[];
|
||||
const setTimer = (callback, delay) => {{ const timer={{callback,delay,cancelled:false}}; timers.push(timer); return timer; }};
|
||||
const clearTimer = timer => {{ if (timer) timer.cancelled=true; }};
|
||||
const fetchJson = async (_url, init) => {{
|
||||
const payload=JSON.parse(init.body); payloads.push(payload);
|
||||
return {{revision:payload.revision+1,order:payload.order}};
|
||||
}};
|
||||
(async () => {{
|
||||
let priority;
|
||||
priority=createPriority({{
|
||||
const priority=createPriority({{
|
||||
storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}},
|
||||
getLogin:()=>'alice', fetchJson, onChange:()=>{{ void priority.sync(); }},
|
||||
getLogin:()=>'alice', fetchJson, setTimeout:setTimer, clearTimeout:clearTimer, debounceMs:150,
|
||||
}});
|
||||
priority.move('following', -1);
|
||||
priority.move('following', -1);
|
||||
for (let index=0; index<4; index += 1) priority.move('following', -1);
|
||||
const before=payloads.length;
|
||||
const active=timers.filter(timer=>!timer.cancelled);
|
||||
active[0].callback();
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
const firstPending=pending.length;
|
||||
pending.shift()();
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
const secondPending=pending.length;
|
||||
pending.shift()();
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
process.stdout.write(JSON.stringify({{firstPending,secondPending,payloads,state:priority.state()}}));
|
||||
process.stdout.write(JSON.stringify({{before,active:active.map(timer=>timer.delay),payloads,state:priority.state()}}));
|
||||
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||
"""
|
||||
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
payload = json.loads(result.stdout)
|
||||
assert payload["firstPending"] == 1
|
||||
assert payload["secondPending"] == 1
|
||||
assert len(payload["payloads"]) == 2
|
||||
assert payload["before"] == 0
|
||||
assert payload["active"] == [150]
|
||||
assert len(payload["payloads"]) == 1
|
||||
assert payload["payloads"][0]["revision"] == 0
|
||||
assert payload["payloads"][1]["revision"] == 1
|
||||
assert payload["payloads"][1]["order"] == payload["state"]["order"]
|
||||
assert payload["state"]["revision"] == 2
|
||||
assert payload["payloads"][0]["order"] == payload["state"]["order"]
|
||||
assert payload["state"]["revision"] == 1
|
||||
assert payload["state"]["status"] == "ready"
|
||||
|
||||
|
||||
def test_mobile_queue_priority_retries_transient_failure_without_another_edit():
|
||||
script = f"""
|
||||
const createPriority = require({json.dumps(str(QUEUE_PRIORITY))});
|
||||
const values = new Map(); const timers=[]; let calls=0;
|
||||
const setTimer = (callback, delay) => {{ const timer={{callback,delay,cancelled:false}}; timers.push(timer); return timer; }};
|
||||
const clearTimer = timer => {{ if (timer) timer.cancelled=true; }};
|
||||
const fetchJson = async (_url, init) => {{
|
||||
calls += 1;
|
||||
if (calls === 1) {{ const error=new Error('temporary'); error.status=503; throw error; }}
|
||||
const payload=JSON.parse(init.body);
|
||||
return {{revision:payload.revision+1,order:payload.order}};
|
||||
}};
|
||||
(async () => {{
|
||||
const priority=createPriority({{
|
||||
storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}},
|
||||
getLogin:()=>'alice', fetchJson, setTimeout:setTimer, clearTimeout:clearTimer,
|
||||
debounceMs:150, retryBaseMs:1000, retryMaxMs:30000,
|
||||
}});
|
||||
priority.move('following', -1);
|
||||
timers.find(timer=>!timer.cancelled).callback();
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
const afterFailure=priority.state();
|
||||
const retry=timers.filter(timer=>!timer.cancelled).at(-1);
|
||||
retry.callback();
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
process.stdout.write(JSON.stringify({{calls,afterFailure,retryDelay:retry.delay,settled:priority.state()}}));
|
||||
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||
"""
|
||||
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
payload = json.loads(result.stdout)
|
||||
assert payload["afterFailure"]["pending"] is True
|
||||
assert payload["afterFailure"]["status"] == "pending"
|
||||
assert payload["retryDelay"] == 1000
|
||||
assert payload["calls"] == 2
|
||||
assert payload["settled"]["pending"] is False
|
||||
assert payload["settled"]["status"] == "ready"
|
||||
|
||||
|
||||
def test_mobile_queue_priority_refreshes_clean_device_when_foregrounded():
|
||||
script = f"""
|
||||
const createPriority = require({json.dumps(str(QUEUE_PRIORITY))});
|
||||
const values = new Map(); const windowListeners={{}}; const documentListeners={{}};
|
||||
const remoteOrder=['attention','following','today','update','agenda','authored','filed','later','draft'];
|
||||
let calls=0;
|
||||
const documentRef={{hidden:false,addEventListener:(name, callback)=>documentListeners[name]=callback}};
|
||||
const windowRef={{addEventListener:(name, callback)=>windowListeners[name]=callback}};
|
||||
const priority=createPriority({{
|
||||
storage:{{getItem:key=>values.get(key)||null,setItem:(key,value)=>values.set(key,value),removeItem:key=>values.delete(key)}},
|
||||
getLogin:()=>'alice', fetchJson:async () => {{ calls += 1; return {{revision:4,order:remoteOrder.slice()}}; }},
|
||||
}});
|
||||
(async () => {{
|
||||
priority.startLifecycle({{window:windowRef,document:documentRef}});
|
||||
documentListeners.visibilitychange();
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
process.stdout.write(JSON.stringify({{calls,state:priority.state(),listeners:[...Object.keys(windowListeners),...Object.keys(documentListeners)]}}));
|
||||
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
||||
"""
|
||||
result = subprocess.run(["node", "-e", script], capture_output=True, text=True)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
payload = json.loads(result.stdout)
|
||||
assert set(payload["listeners"]) == {"online", "visibilitychange"}
|
||||
assert payload["calls"] == 1
|
||||
assert payload["state"]["revision"] == 4
|
||||
assert payload["state"]["order"][1] == "following"
|
||||
assert payload["state"]["status"] == "ready"
|
||||
|
||||
|
||||
@@ -535,7 +602,7 @@ async def test_mobile_queue_priority_wires_cross_device_hydration_and_explicit_c
|
||||
assert 'id="use-remote-mobile-queue-priority"' in html
|
||||
assert "fetchJson: fetchReviewJson" in html
|
||||
assert "void mobileQueuePriority.load();" in html
|
||||
assert "void mobileQueuePriority.sync();" in html
|
||||
assert "mobileQueuePriority.startLifecycle({window, document});" in html
|
||||
assert "keepLocalButton: qs('#keep-local-mobile-queue-priority')" in html
|
||||
assert "useRemoteButton: qs('#use-remote-mobile-queue-priority')" in html
|
||||
assert "error.payload = payload;" in html
|
||||
|
||||
Reference in new issue
Block a user