Merge pull request 'Bound Today readiness latency with demand-driven checks' (#442) from timmy/441-demand-driven-today-readiness into main
This commit is contained in:
commit
60bc7cd686
|
|
@ -1,6 +1,6 @@
|
||||||
const BASE = new URL('./', self.location.href).pathname;
|
const BASE = new URL('./', self.location.href).pathname;
|
||||||
importScripts(BASE + 'static/background-issue-sync.js');
|
importScripts(BASE + 'static/background-issue-sync.js');
|
||||||
const CACHE = 'stackchain-dashboard-shell-v73';
|
const CACHE = 'stackchain-dashboard-shell-v74';
|
||||||
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
const OFFLINE_LEASE_URL = new URL(BASE + '__offline-session-lease', self.location.origin).href;
|
||||||
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
|
||||||
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
|
||||||
|
|
|
||||||
|
|
@ -32,15 +32,19 @@
|
||||||
const candidates = preferredIndex >= 0
|
const candidates = preferredIndex >= 0
|
||||||
? ordered.slice(preferredIndex).concat(ordered.slice(0, preferredIndex))
|
? ordered.slice(preferredIndex).concat(ordered.slice(0, preferredIndex))
|
||||||
: [preferred].concat(ordered);
|
: [preferred].concat(ordered);
|
||||||
const states = [];
|
const targetState = { item:preferred, ...(await classify(preferred)) };
|
||||||
for (const item of candidates) states.push({ item, ...(await classify(item)) });
|
|
||||||
if (request !== generation) return 'superseded';
|
if (request !== generation) return 'superseded';
|
||||||
const targetState = states[0];
|
|
||||||
if (targetState.status === 'ready') {
|
if (targetState.status === 'ready') {
|
||||||
pending = null;
|
pending = null;
|
||||||
onOpen(action, preferred);
|
onOpen(action, preferred);
|
||||||
return 'opened';
|
return 'opened';
|
||||||
}
|
}
|
||||||
|
const fallbackStates = await Promise.all(candidates.slice(1).map(async item => ({
|
||||||
|
item,
|
||||||
|
...(await classify(item)),
|
||||||
|
})));
|
||||||
|
const states = [targetState, ...fallbackStates];
|
||||||
|
if (request !== generation) return 'superseded';
|
||||||
const nextReadyState = states.slice(1).find(state => state.status === 'ready');
|
const nextReadyState = states.slice(1).find(state => state.status === 'ready');
|
||||||
pending = {
|
pending = {
|
||||||
action,
|
action,
|
||||||
|
|
|
||||||
|
|
@ -347,5 +347,5 @@ async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
|
||||||
def test_later_sync_ships_atomically_in_the_offline_shell():
|
def test_later_sync_ships_atomically_in_the_offline_shell():
|
||||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v73" in source
|
assert "stackchain-dashboard-shell-v74" in source
|
||||||
assert "BASE + 'static/later-sync.js'" in source
|
assert "BASE + 'static/later-sync.js'" in source
|
||||||
|
|
|
||||||
|
|
@ -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 { 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 pre { max-width:100%; overflow-x:auto;" in css
|
||||||
assert ".markdown-content a { min-height:44px;" in css
|
assert ".markdown-content a { min-height:44px;" in css
|
||||||
assert "stackchain-dashboard-shell-v73" in worker
|
assert "stackchain-dashboard-shell-v74" 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]))
|
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 local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
|
||||||
assert "stackchain-dashboard-shell-v73" in worker
|
assert "stackchain-dashboard-shell-v74" in worker
|
||||||
|
|
|
||||||
|
|
@ -232,6 +232,6 @@ async def test_plan_today_wires_cancel_back_and_success_through_overlay_history(
|
||||||
def test_plan_today_controller_is_available_in_the_offline_shell():
|
def test_plan_today_controller_is_available_in_the_offline_shell():
|
||||||
source = SERVICE_WORKER.read_text()
|
source = SERVICE_WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v73" in source
|
assert "stackchain-dashboard-shell-v74" in source
|
||||||
assert "BASE + 'static/plan-today.js'" in source
|
assert "BASE + 'static/plan-today.js'" in source
|
||||||
assert "BASE + 'static/plan-today-preview.js'" in source
|
assert "BASE + 'static/plan-today-preview.js'" in source
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ async function dispatchNotificationClick(route) {{
|
||||||
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v73" in source
|
assert "stackchain-dashboard-shell-v74" in source
|
||||||
assert "BASE + 'static/my-work.js'" in source
|
assert "BASE + 'static/my-work.js'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
assert "BASE + 'static/dashboard.css'" in source
|
assert "BASE + 'static/dashboard.css'" in source
|
||||||
|
|
@ -131,7 +131,7 @@ def test_resumable_today_session_ships_in_a_new_offline_shell():
|
||||||
def test_offline_review_next_ships_today_completion_atomically():
|
def test_offline_review_next_ships_today_completion_atomically():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v73" in source
|
assert "stackchain-dashboard-shell-v74" in source
|
||||||
assert "BASE + 'static/today-completion.js'" in source
|
assert "BASE + 'static/today-completion.js'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
|
|
||||||
|
|
@ -139,7 +139,7 @@ def test_offline_review_next_ships_today_completion_atomically():
|
||||||
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v73" in source
|
assert "stackchain-dashboard-shell-v74" in source
|
||||||
assert "BASE + 'static/create-issue-sheet.js'" in source
|
assert "BASE + 'static/create-issue-sheet.js'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
|
|
||||||
|
|
@ -147,14 +147,14 @@ def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
|
||||||
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v73" in source
|
assert "stackchain-dashboard-shell-v74" in source
|
||||||
assert "BASE + 'static/later-picker.js'" in source
|
assert "BASE + 'static/later-picker.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_navigation_deadline_ships_in_a_new_shell_cache():
|
def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v73" in source
|
assert "stackchain-dashboard-shell-v74" in source
|
||||||
assert "BASE + 'static/dashboard.css'" in source
|
assert "BASE + 'static/dashboard.css'" in source
|
||||||
assert "BASE + 'static/dashboard.js'" in source
|
assert "BASE + 'static/dashboard.js'" in source
|
||||||
assert "BASE + 'static/install-app.js'" in source
|
assert "BASE + 'static/install-app.js'" in source
|
||||||
|
|
@ -163,21 +163,21 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
|
||||||
def test_today_convergence_ships_in_a_new_shell_cache():
|
def test_today_convergence_ships_in_a_new_shell_cache():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v73" in source
|
assert "stackchain-dashboard-shell-v74" in source
|
||||||
assert "BASE + 'static/today-sync.js'" in source
|
assert "BASE + 'static/today-sync.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
|
def test_mobile_search_viewport_ships_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v73" in source
|
assert "stackchain-dashboard-shell-v74" in source
|
||||||
assert "BASE + 'static/mobile-search-viewport.js'" in source
|
assert "BASE + 'static/mobile-search-viewport.js'" in source
|
||||||
|
|
||||||
|
|
||||||
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
|
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v73" in source
|
assert "stackchain-dashboard-shell-v74" in source
|
||||||
assert "BASE + 'static/update-ownership.js'" in source
|
assert "BASE + 'static/update-ownership.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -358,7 +358,7 @@ def test_one_session_bound_csrf_proof_is_reused_for_a_background_drain():
|
||||||
def test_queue_today_ships_atomically_in_a_new_offline_shell():
|
def test_queue_today_ships_atomically_in_a_new_offline_shell():
|
||||||
source = WORKER.read_text()
|
source = WORKER.read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v73" in source
|
assert "stackchain-dashboard-shell-v74" in source
|
||||||
assert "BASE + 'static/queue-today.js'" in source
|
assert "BASE + 'static/queue-today.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,23 +18,59 @@ def run_node(script):
|
||||||
).stdout
|
).stdout
|
||||||
|
|
||||||
|
|
||||||
def test_readiness_opens_ready_target_without_showing_a_gate():
|
def test_readiness_opens_ready_target_without_inspecting_the_rest_of_the_queue():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createTodayReadiness = require({json.dumps(str(READINESS))});
|
const createTodayReadiness = require({json.dumps(str(READINESS))});
|
||||||
const calls = [];
|
const calls = [];
|
||||||
|
const inspections = [];
|
||||||
const item = (number, kind='issue') => ({{kind, repository:'stackchain/dashboard', number, title:'Item ' + number}});
|
const item = (number, kind='issue') => ({{kind, repository:'stackchain/dashboard', number, title:'Item ' + number}});
|
||||||
const readiness = createTodayReadiness({{
|
const readiness = createTodayReadiness({{
|
||||||
inspect: async candidate => ({{available:true, dependencies:[]}}),
|
inspect: async candidate => {{ inspections.push(candidate.number); return {{available:true, dependencies:[]}}; }},
|
||||||
onOpen: (action, candidate) => calls.push(['open', action, candidate.number]),
|
onOpen: (action, candidate) => calls.push(['open', action, candidate.number]),
|
||||||
onGate: state => calls.push(['gate', state.status]),
|
onGate: state => calls.push(['gate', state.status]),
|
||||||
}});
|
}});
|
||||||
readiness.run('start', [item(1), item(2)]).then(result =>
|
readiness.run('start', [item(1), item(2), item(3)]).then(result =>
|
||||||
process.stdout.write(JSON.stringify({{result, calls}}))
|
process.stdout.write(JSON.stringify({{result, calls, inspections}}))
|
||||||
);
|
);
|
||||||
"""
|
"""
|
||||||
assert json.loads(run_node(script)) == {
|
assert json.loads(run_node(script)) == {
|
||||||
"result": "opened",
|
"result": "opened",
|
||||||
"calls": [["open", "start", 1]],
|
"calls": [["open", "start", 1]],
|
||||||
|
"inspections": [1],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_readiness_checks_fallbacks_concurrently_but_preserves_queue_order():
|
||||||
|
script = f"""
|
||||||
|
const createTodayReadiness = require({json.dumps(str(READINESS))});
|
||||||
|
const items = [1, 2, 3].map(number => ({{kind:'issue', repository:'r', number}}));
|
||||||
|
const starts = [];
|
||||||
|
const resolvers = {{}};
|
||||||
|
const readiness = createTodayReadiness({{
|
||||||
|
inspect: candidate => {{
|
||||||
|
starts.push(candidate.number);
|
||||||
|
if (candidate.number === 1) return Promise.resolve({{available:true, dependencies:[{{state:'open'}}]}});
|
||||||
|
return new Promise(resolve => {{ resolvers[candidate.number] = resolve; }});
|
||||||
|
}},
|
||||||
|
onOpen: () => undefined,
|
||||||
|
onGate: state => {{ globalThis.gated = state.nextReady.number; }},
|
||||||
|
}});
|
||||||
|
(async () => {{
|
||||||
|
const running = readiness.run('start', items);
|
||||||
|
await new Promise(resolve => setImmediate(resolve));
|
||||||
|
const startedTogether = starts.slice();
|
||||||
|
if (resolvers[3]) resolvers[3]({{available:true, dependencies:[]}});
|
||||||
|
resolvers[2]({{available:true, dependencies:[]}});
|
||||||
|
await new Promise(resolve => setImmediate(resolve));
|
||||||
|
if (!startedTogether.includes(3)) resolvers[3]({{available:true, dependencies:[]}});
|
||||||
|
const result = await running;
|
||||||
|
process.stdout.write(JSON.stringify({{result, startedTogether, gated:globalThis.gated}}));
|
||||||
|
}})();
|
||||||
|
"""
|
||||||
|
assert json.loads(run_node(script)) == {
|
||||||
|
"result": "gated",
|
||||||
|
"startedTogether": [1, 2, 3],
|
||||||
|
"gated": 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -135,7 +171,10 @@ async def test_dashboard_exposes_mobile_today_readiness_gate_and_runtime():
|
||||||
|
|
||||||
|
|
||||||
def test_readiness_runtime_is_available_in_offline_shell():
|
def test_readiness_runtime_is_available_in_offline_shell():
|
||||||
assert "BASE + 'static/today-readiness.js'" in SERVICE_WORKER.read_text()
|
service_worker = SERVICE_WORKER.read_text()
|
||||||
|
|
||||||
|
assert "const CACHE = 'stackchain-dashboard-shell-v74';" in service_worker
|
||||||
|
assert "BASE + 'static/today-readiness.js'" in service_worker
|
||||||
|
|
||||||
|
|
||||||
def test_mobile_readiness_gate_has_touch_safe_wrapping_actions():
|
def test_mobile_readiness_gate_has_touch_safe_wrapping_actions():
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ sync.enqueue('add', 'issue:r:1:');
|
||||||
def test_inflight_today_drain_ships_in_a_new_offline_shell():
|
def test_inflight_today_drain_ships_in_a_new_offline_shell():
|
||||||
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
|
||||||
|
|
||||||
assert "stackchain-dashboard-shell-v73" in source
|
assert "stackchain-dashboard-shell-v74" in source
|
||||||
assert "BASE + 'static/today-sync.js'" in source
|
assert "BASE + 'static/today-sync.js'" in source
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user