Compare commits
No commits in common. "a23276f0b8b70e5111d61b29ffaf4345dd968797" and "1822004f3dcffc919d1721eafa1c8095afb14d7c" have entirely different histories.
a23276f0b8
...
1822004f3d
|
|
@ -166,47 +166,9 @@ function createContextPoller({
|
||||||
return refresh({ force: true });
|
return refresh({ force: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function adopt(snapshot) {
|
|
||||||
if (
|
|
||||||
stopped || activeRequest || retainedSnapshot || !snapshot ||
|
|
||||||
typeof snapshot !== 'object' ||
|
|
||||||
!Object.prototype.hasOwnProperty.call(snapshot, 'context')
|
|
||||||
) return false;
|
|
||||||
retainedSnapshot = { ...snapshot };
|
|
||||||
revisions = { ...(snapshot.revisions || {}) };
|
|
||||||
failureStreak = 0;
|
|
||||||
lastSuccessAt = Date.now();
|
|
||||||
nextDelayMs = snapshotDelay(snapshot);
|
|
||||||
const changedSections = ['context', 'events', 'notifications'].filter(
|
|
||||||
section => Object.prototype.hasOwnProperty.call(snapshot, section)
|
|
||||||
);
|
|
||||||
onSnapshot(retainedSnapshot, changedSections);
|
|
||||||
schedule(nextDelayMs);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function adoptPending(snapshotPromise) {
|
|
||||||
if (!snapshotPromise || typeof snapshotPromise.then !== 'function') return false;
|
|
||||||
let deadline = null;
|
|
||||||
const timeout = new Promise(resolve => {
|
|
||||||
deadline = setDeadlineTimer(() => resolve(null), timeoutMs);
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
const snapshot = await Promise.race([
|
|
||||||
Promise.resolve(snapshotPromise).catch(() => null),
|
|
||||||
timeout,
|
|
||||||
]);
|
|
||||||
return adopt(snapshot);
|
|
||||||
} finally {
|
|
||||||
clearDeadlineTimer(deadline);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
start: refresh,
|
start: refresh,
|
||||||
refresh,
|
refresh,
|
||||||
adopt,
|
|
||||||
adoptPending,
|
|
||||||
setVisible,
|
setVisible,
|
||||||
getState() {
|
getState() {
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -8441,12 +8441,9 @@
|
||||||
if (!deviceSetup) await (await ensureDeviceSetup()).open(event);
|
if (!deviceSetup) await (await ensureDeviceSetup()).open(event);
|
||||||
});
|
});
|
||||||
pushControllerReady.then(ensureDeviceSetup).catch(console.warn);
|
pushControllerReady.then(ensureDeviceSetup).catch(console.warn);
|
||||||
let adoptedProgressiveSnapshot = contextPoller.adopt(progressiveWorkHandoff?.liveSnapshot);
|
await load();
|
||||||
if (!adoptedProgressiveSnapshot && progressiveWorkHandoff?.liveSnapshotPromise) {
|
|
||||||
adoptedProgressiveSnapshot = await contextPoller.adoptPending(progressiveWorkHandoff.liveSnapshotPromise);
|
|
||||||
}
|
|
||||||
if (!adoptedProgressiveSnapshot) await load();
|
|
||||||
await appShortcut.run();
|
await appShortcut.run();
|
||||||
|
contextPoller.start();
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
contextPoller.setVisible(!document.hidden);
|
contextPoller.setVisible(!document.hidden);
|
||||||
if (!document.hidden) deviceSetup?.render();
|
if (!document.hidden) deviceSetup?.render();
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ function createProgressiveMyWork({ document, fetchSnapshot }) {
|
||||||
let active = 'all';
|
let active = 'all';
|
||||||
let stopped = false;
|
let stopped = false;
|
||||||
let selectedByUser = false;
|
let selectedByUser = false;
|
||||||
let liveSnapshot = null;
|
|
||||||
let liveSnapshotPromise = null;
|
|
||||||
const deferredQueues = {
|
const deferredQueues = {
|
||||||
today:'Today', agenda:'Agenda', later:'Later', draft:'Drafts',
|
today:'Today', agenda:'Agenda', later:'Later', draft:'Drafts',
|
||||||
};
|
};
|
||||||
|
|
@ -61,31 +59,13 @@ function createProgressiveMyWork({ document, fetchSnapshot }) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handoff() {
|
handoff() {
|
||||||
const state = { selectedFilter:selectedByUser ? active : null };
|
return { selectedFilter:selectedByUser ? active : null };
|
||||||
if (liveSnapshot) {
|
|
||||||
state.liveSnapshot = liveSnapshot;
|
|
||||||
liveSnapshot = null;
|
|
||||||
liveSnapshotPromise = null;
|
|
||||||
} else if (liveSnapshotPromise) {
|
|
||||||
state.liveSnapshotPromise = liveSnapshotPromise;
|
|
||||||
liveSnapshotPromise = null;
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
},
|
},
|
||||||
async start() {
|
async start() {
|
||||||
if (status) status.textContent = 'Loading assigned work…';
|
if (status) status.textContent = 'Loading assigned work…';
|
||||||
const request = Promise.resolve().then(() => fetchSnapshot());
|
|
||||||
liveSnapshotPromise = request.then(snapshot => (
|
|
||||||
snapshot && typeof snapshot === 'object' &&
|
|
||||||
Object.prototype.hasOwnProperty.call(snapshot, 'context') ? snapshot : null
|
|
||||||
), () => null);
|
|
||||||
try {
|
try {
|
||||||
const snapshot = await request;
|
const snapshot = await fetchSnapshot();
|
||||||
if (stopped) return false;
|
if (stopped) return false;
|
||||||
const transferable = snapshot && typeof snapshot === 'object' &&
|
|
||||||
Object.prototype.hasOwnProperty.call(snapshot, 'context');
|
|
||||||
if (transferable) liveSnapshot = snapshot;
|
|
||||||
else liveSnapshotPromise = null;
|
|
||||||
const context = snapshot?.context || snapshot || {};
|
const context = snapshot?.context || snapshot || {};
|
||||||
items = buildMyWork({ ...context, notifications:snapshot?.notifications || context.notifications || [] });
|
items = buildMyWork({ ...context, notifications:snapshot?.notifications || context.notifications || [] });
|
||||||
updateCounts();
|
updateCounts();
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,6 @@ def test_release_artifact_bootstraps_mobile_home_and_returns_from_insights(
|
||||||
page.goto(origin + "/", wait_until="networkidle")
|
page.goto(origin + "/", wait_until="networkidle")
|
||||||
page.locator('input[name="device_label"]').fill("Home bootstrap release phone")
|
page.locator('input[name="device_label"]').fill("Home bootstrap release phone")
|
||||||
page.locator('input[name="access_token"]').fill(ACCESS_TOKEN)
|
page.locator('input[name="access_token"]').fill(ACCESS_TOKEN)
|
||||||
live_requests.clear()
|
|
||||||
page.locator("#submit-sign-in").click()
|
page.locator("#submit-sign-in").click()
|
||||||
page.wait_for_url(origin + "/", wait_until="networkidle")
|
page.wait_for_url(origin + "/", wait_until="networkidle")
|
||||||
assert launch_transfer_events.index("workspace-requested") < (
|
assert launch_transfer_events.index("workspace-requested") < (
|
||||||
|
|
@ -194,8 +193,6 @@ def test_release_artifact_bootstraps_mobile_home_and_returns_from_insights(
|
||||||
), launch_transfer_events
|
), launch_transfer_events
|
||||||
|
|
||||||
expect(page.locator("#my-work-status")).to_contain_text("2")
|
expect(page.locator("#my-work-status")).to_contain_text("2")
|
||||||
revisionless_live_requests = [url for url in live_requests if "?" not in url]
|
|
||||||
assert len(revisionless_live_requests) == 1, live_requests
|
|
||||||
initial_live_requests = len(live_requests)
|
initial_live_requests = len(live_requests)
|
||||||
page.evaluate(
|
page.evaluate(
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -117,91 +117,6 @@ const poller = createContextPoller({{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_context_poller_adopts_progressive_snapshot_before_revision_conditional_refresh():
|
|
||||||
script = f"""
|
|
||||||
const createContextPoller = require({json.dumps(str(POLLER))});
|
|
||||||
const requested = [];
|
|
||||||
const rendered = [];
|
|
||||||
const timers = [];
|
|
||||||
const seed = {{
|
|
||||||
context:{{user:{{login:'timmy'}}}}, events:[{{id:7}}], notifications:[],
|
|
||||||
revisions:{{context:'0123456789abcdef.1',events:'fedcba9876543210.2',notifications:'0011223344556677.3'}},
|
|
||||||
freshness:{{fresh_for_seconds:8,sections:{{
|
|
||||||
context:{{degraded:false,age_seconds:2}},events:{{degraded:false,age_seconds:5}},notifications:{{degraded:false,age_seconds:1}},
|
|
||||||
}}}},
|
|
||||||
}};
|
|
||||||
const poller = createContextPoller({{
|
|
||||||
fetchContext: revisions => {{
|
|
||||||
requested.push({{...revisions}});
|
|
||||||
return Promise.resolve({{revisions:{{...revisions}},freshness:seed.freshness}});
|
|
||||||
}},
|
|
||||||
onSnapshot: (snapshot, changed) => rendered.push({{login:snapshot.context.user.login,changed}}),
|
|
||||||
onError: error => {{ throw error; }},
|
|
||||||
setTimer: (callback, delay) => {{ const timer={{callback,delay}}; timers.push(timer); return timer; }},
|
|
||||||
clearTimer: () => {{}},
|
|
||||||
intervalMs:8000,
|
|
||||||
}});
|
|
||||||
(async()=>{{
|
|
||||||
const adopted=poller.adopt(seed);
|
|
||||||
const before={{adopted,requests:requested.length,rendered:[...rendered],delay:timers[0].delay}};
|
|
||||||
timers[0].callback();
|
|
||||||
await new Promise(resolve=>setImmediate(resolve));
|
|
||||||
process.stdout.write(JSON.stringify({{before,requested}}));
|
|
||||||
}})();
|
|
||||||
"""
|
|
||||||
|
|
||||||
assert run_node(script) == {
|
|
||||||
"before": {
|
|
||||||
"adopted": True,
|
|
||||||
"requests": 0,
|
|
||||||
"rendered": [{
|
|
||||||
"login": "timmy",
|
|
||||||
"changed": ["context", "events", "notifications"],
|
|
||||||
}],
|
|
||||||
"delay": 3000,
|
|
||||||
},
|
|
||||||
"requested": [{
|
|
||||||
"context": "0123456789abcdef.1",
|
|
||||||
"events": "fedcba9876543210.2",
|
|
||||||
"notifications": "0011223344556677.3",
|
|
||||||
}],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_context_poller_adopts_inflight_progressive_snapshot_without_fetching():
|
|
||||||
script = f"""
|
|
||||||
const createContextPoller = require({json.dumps(str(POLLER))});
|
|
||||||
let calls=0; const rendered=[]; const timers=[];
|
|
||||||
const snapshot={{context:{{user:{{login:'timmy'}}}},events:[],notifications:[]}};
|
|
||||||
const poller=createContextPoller({{
|
|
||||||
fetchContext:()=>{{calls+=1;return Promise.resolve(snapshot);}},
|
|
||||||
onSnapshot:value=>rendered.push(value.context.user.login),
|
|
||||||
onError:error=>{{throw error;}},
|
|
||||||
setTimer:(callback,delay)=>{{const timer={{callback,delay}};timers.push(timer);return timer;}},
|
|
||||||
clearTimer:()=>{{}}, setDeadlineTimer:()=>1, clearDeadlineTimer:()=>{{}}, intervalMs:8000,
|
|
||||||
}});
|
|
||||||
(async()=>{{
|
|
||||||
const adopted=await poller.adoptPending(Promise.resolve(snapshot));
|
|
||||||
process.stdout.write(JSON.stringify({{adopted,calls,rendered,delay:timers[0].delay}}));
|
|
||||||
}})();
|
|
||||||
"""
|
|
||||||
|
|
||||||
assert run_node(script) == {
|
|
||||||
"adopted": True,
|
|
||||||
"calls": 0,
|
|
||||||
"rendered": ["timmy"],
|
|
||||||
"delay": 8000,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def test_dashboard_adopts_progressive_snapshot_or_falls_back_to_immediate_load():
|
|
||||||
source = DASHBOARD.read_text()
|
|
||||||
|
|
||||||
assert "let adoptedProgressiveSnapshot = contextPoller.adopt(progressiveWorkHandoff?.liveSnapshot);" in source
|
|
||||||
assert "contextPoller.adoptPending(progressiveWorkHandoff.liveSnapshotPromise)" in source
|
|
||||||
assert "if (!adoptedProgressiveSnapshot) await load();" in source
|
|
||||||
|
|
||||||
|
|
||||||
def test_context_poller_waits_for_server_cooldown_when_every_section_is_degraded():
|
def test_context_poller_waits_for_server_cooldown_when_every_section_is_degraded():
|
||||||
script = f"""
|
script = f"""
|
||||||
const createContextPoller = require({json.dumps(str(POLLER))});
|
const createContextPoller = require({json.dumps(str(POLLER))});
|
||||||
|
|
|
||||||
|
|
@ -96,13 +96,7 @@ async def test_mobile_launch_progressively_discloses_secondary_controls():
|
||||||
assert "BASE + 'static/mobile-app-shortcuts.js'" in service_worker
|
assert "BASE + 'static/mobile-app-shortcuts.js'" in service_worker
|
||||||
bootstrap = (Path(__file__).resolve().parents[1] / "frontend" / "dashboard.js").read_text()
|
bootstrap = (Path(__file__).resolve().parents[1] / "frontend" / "dashboard.js").read_text()
|
||||||
assert "mobileAppShortcuts.createController" in bootstrap
|
assert "mobileAppShortcuts.createController" in bootstrap
|
||||||
adopted = bootstrap.index(
|
assert bootstrap.index("await load();\n await appShortcut.run();") < bootstrap.index("contextPoller.start();")
|
||||||
"let adoptedProgressiveSnapshot = contextPoller.adopt(progressiveWorkHandoff?.liveSnapshot);"
|
|
||||||
)
|
|
||||||
fallback = bootstrap.index("if (!adoptedProgressiveSnapshot) await load();")
|
|
||||||
shortcuts = bootstrap.index("await appShortcut.run();")
|
|
||||||
assert adopted < fallback < shortcuts
|
|
||||||
assert "contextPoller.start();" not in bootstrap
|
|
||||||
assert "mobileLaunch.chooseFilter" in html
|
assert "mobileLaunch.chooseFilter" in html
|
||||||
assert "agenda: counts.agenda" in html
|
assert "agenda: counts.agenda" in html
|
||||||
assert "mobileLaunch.createDisclosure" in html
|
assert "mobileLaunch.createDisclosure" in html
|
||||||
|
|
|
||||||
|
|
@ -134,89 +134,3 @@ const flow=context.module.exports({{document,fetchSnapshot:async()=>({{user:{{lo
|
||||||
dashboard = DASHBOARD.read_text()
|
dashboard = DASHBOARD.read_text()
|
||||||
assert "const progressiveWorkHandoff = window.stackchainProgressiveMyWork?.handoff?.();" in dashboard
|
assert "const progressiveWorkHandoff = window.stackchainProgressiveMyWork?.handoff?.();" in dashboard
|
||||||
assert "progressiveWorkHandoff?.selectedFilter" in dashboard
|
assert "progressiveWorkHandoff?.selectedFilter" in dashboard
|
||||||
|
|
||||||
|
|
||||||
def test_progressive_live_snapshot_is_handed_to_the_workspace_exactly_once():
|
|
||||||
harness = f"""
|
|
||||||
const fs=require('fs'); const vm=require('vm');
|
|
||||||
const document={{
|
|
||||||
querySelector:selector=>selector==='#my-work-list'?{{innerHTML:''}}:selector==='#my-work-status'?{{textContent:''}}:null,
|
|
||||||
querySelectorAll:()=>[],
|
|
||||||
}};
|
|
||||||
const context={{module:{{exports:{{}}}},exports:{{}},console,URL,document}}; vm.createContext(context);
|
|
||||||
vm.runInContext(fs.readFileSync({json.dumps(str(MY_WORK))},'utf8'),context);
|
|
||||||
context.buildMyWork=context.module.exports; context.module={{exports:{{}}}};
|
|
||||||
vm.runInContext(fs.readFileSync({json.dumps(str(MODULE))},'utf8'),context);
|
|
||||||
const snapshot={{
|
|
||||||
context:{{user:{{login:'timmy'}},issues:[],pull_requests:[]}},
|
|
||||||
events:[{{id:7}}], notifications:[],
|
|
||||||
revisions:{{context:'0123456789abcdef.1',events:'fedcba9876543210.2',notifications:'0011223344556677.3'}},
|
|
||||||
freshness:{{fresh_for_seconds:8,sections:{{
|
|
||||||
context:{{degraded:false,age_seconds:2}},events:{{degraded:false,age_seconds:2}},notifications:{{degraded:false,age_seconds:2}},
|
|
||||||
}}}},
|
|
||||||
}};
|
|
||||||
const flow=context.module.exports({{document,fetchSnapshot:async()=>snapshot}});
|
|
||||||
(async()=>{{
|
|
||||||
await flow.start();
|
|
||||||
const first=flow.handoff(); const second=flow.handoff();
|
|
||||||
console.log(JSON.stringify({{
|
|
||||||
firstSnapshot:first.liveSnapshot,
|
|
||||||
secondHasSnapshot:Object.prototype.hasOwnProperty.call(second,'liveSnapshot'),
|
|
||||||
}}));
|
|
||||||
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
|
||||||
"""
|
|
||||||
result = subprocess.run(["node", "-e", harness], check=True, capture_output=True, text=True)
|
|
||||||
state = json.loads(result.stdout)
|
|
||||||
|
|
||||||
assert state == {"firstSnapshot": {
|
|
||||||
"context": {"user": {"login": "timmy"}, "issues": [], "pull_requests": []},
|
|
||||||
"events": [{"id": 7}],
|
|
||||||
"notifications": [],
|
|
||||||
"revisions": {
|
|
||||||
"context": "0123456789abcdef.1",
|
|
||||||
"events": "fedcba9876543210.2",
|
|
||||||
"notifications": "0011223344556677.3",
|
|
||||||
},
|
|
||||||
"freshness": {
|
|
||||||
"fresh_for_seconds": 8,
|
|
||||||
"sections": {
|
|
||||||
"context": {"degraded": False, "age_seconds": 2},
|
|
||||||
"events": {"degraded": False, "age_seconds": 2},
|
|
||||||
"notifications": {"degraded": False, "age_seconds": 2},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, "secondHasSnapshot": False}
|
|
||||||
|
|
||||||
|
|
||||||
def test_progressive_inflight_snapshot_survives_workspace_handoff_and_stop():
|
|
||||||
harness = f"""
|
|
||||||
const fs=require('fs'); const vm=require('vm');
|
|
||||||
const document={{
|
|
||||||
querySelector:selector=>selector==='#my-work-list'?{{innerHTML:''}}:selector==='#my-work-status'?{{textContent:''}}:null,
|
|
||||||
querySelectorAll:()=>[],
|
|
||||||
}};
|
|
||||||
const context={{module:{{exports:{{}}}},exports:{{}},console,URL,document}}; vm.createContext(context);
|
|
||||||
vm.runInContext(fs.readFileSync({json.dumps(str(MY_WORK))},'utf8'),context);
|
|
||||||
context.buildMyWork=context.module.exports; context.module={{exports:{{}}}};
|
|
||||||
vm.runInContext(fs.readFileSync({json.dumps(str(MODULE))},'utf8'),context);
|
|
||||||
let resolveSnapshot;
|
|
||||||
const snapshot={{context:{{user:{{login:'timmy'}},issues:[],pull_requests:[]}},events:[],notifications:[]}};
|
|
||||||
const flow=context.module.exports({{document,fetchSnapshot:()=>new Promise(resolve=>{{resolveSnapshot=resolve;}})}});
|
|
||||||
(async()=>{{
|
|
||||||
const started=flow.start();
|
|
||||||
await Promise.resolve();
|
|
||||||
const handoff=flow.handoff();
|
|
||||||
flow.stop();
|
|
||||||
resolveSnapshot(snapshot);
|
|
||||||
const transferred=await handoff.liveSnapshotPromise;
|
|
||||||
await started;
|
|
||||||
console.log(JSON.stringify({{transferred}}));
|
|
||||||
}})().catch(error=>{{console.error(error);process.exit(1);}});
|
|
||||||
"""
|
|
||||||
result = subprocess.run(["node", "-e", harness], check=True, capture_output=True, text=True)
|
|
||||||
|
|
||||||
assert json.loads(result.stdout) == {"transferred": {
|
|
||||||
"context": {"user": {"login": "timmy"}, "issues": [], "pull_requests": []},
|
|
||||||
"events": [],
|
|
||||||
"notifications": [],
|
|
||||||
}}
|
|
||||||
Loading…
Reference in New Issue
Block a user