feat: lazy-load full lineage segments
This commit is contained in:
1 parent
15d620392f
commit
f1b2a21bd4
2 files changed
+211
-3
No files matched your search
+79
-3
@@ -1272,6 +1272,9 @@ let _sessionActionAnchor = null;
|
||||
let _sessionActionSessionId = null;
|
||||
const _expandedChildSessionKeys = new Set();
|
||||
const _expandedLineageKeys = new Set();
|
||||
const _lineageReportCache = new Map();
|
||||
const _lineageReportInflight = new Map();
|
||||
let _lineageReportCacheGeneration = 0;
|
||||
let _sessionVisibleSidebarIds = [];
|
||||
const SESSION_VIRTUAL_ROW_HEIGHT = 52;
|
||||
const SESSION_VIRTUAL_BUFFER_ROWS = 12;
|
||||
@@ -1758,6 +1761,7 @@ async function renderSessionList(){
|
||||
// without a second round-trip. Stashed on the module for renderSessionListFromCache.
|
||||
_otherProfileCount = sessData.other_profile_count || 0;
|
||||
_allSessions = _mergeOptimisticFirstTurnSessions(sessData.sessions||[]);
|
||||
_clearLineageReportCache();
|
||||
_allProjects = projData.projects||[];
|
||||
// Capture server clock for clock-skew compensation (issue #1144).
|
||||
// server_time is epoch seconds from the server's time.time().
|
||||
@@ -2095,6 +2099,73 @@ function _sessionSegmentCount(s){
|
||||
return count>1?count:0;
|
||||
}
|
||||
|
||||
function _clearLineageReportCache(){
|
||||
_lineageReportCache.clear();
|
||||
_lineageReportInflight.clear();
|
||||
_lineageReportCacheGeneration++;
|
||||
}
|
||||
|
||||
function _lineageReportCacheKey(s,lineageKey){
|
||||
return lineageKey||_sidebarLineageKeyForRow(s)||null;
|
||||
}
|
||||
|
||||
function _lineageLocalSegmentCount(s){
|
||||
if(!s) return 0;
|
||||
if(Array.isArray(s._lineage_segments)) return s._lineage_segments.length;
|
||||
return s.session_id?1:0;
|
||||
}
|
||||
|
||||
function _lineageReportNeedsFetch(s,lineageKey,segmentCount){
|
||||
const key=_lineageReportCacheKey(s,lineageKey);
|
||||
if(!s||!s.session_id||!key) return false;
|
||||
if(_lineageReportCache.has(key)||_lineageReportInflight.has(key)) return false;
|
||||
return Number(segmentCount||0)>_lineageLocalSegmentCount(s);
|
||||
}
|
||||
|
||||
function _lineageSegmentsForRender(s,lineageKey){
|
||||
const segments=[];
|
||||
const seen=new Set();
|
||||
const currentSid=s&&s.session_id;
|
||||
const addSegment=(seg)=>{
|
||||
if(!seg||!seg.session_id||seg.session_id===currentSid||seen.has(seg.session_id)) return;
|
||||
if(seg.role==='child_session') return;
|
||||
seen.add(seg.session_id);
|
||||
segments.push({...seg});
|
||||
};
|
||||
for(const seg of (Array.isArray(s&&s._lineage_segments)?s._lineage_segments:[])) addSegment(seg);
|
||||
const cached=_lineageReportCache.get(_lineageReportCacheKey(s,lineageKey));
|
||||
if(cached&&Array.isArray(cached.segments)){
|
||||
for(const seg of cached.segments) addSegment(seg);
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
function _fetchLineageReportForRow(s,lineageKey){
|
||||
const key=_lineageReportCacheKey(s,lineageKey);
|
||||
if(!s||!s.session_id||!key) return Promise.resolve(null);
|
||||
if(_lineageReportCache.has(key)) return Promise.resolve(_lineageReportCache.get(key));
|
||||
if(_lineageReportInflight.has(key)) return _lineageReportInflight.get(key);
|
||||
const generation=_lineageReportCacheGeneration;
|
||||
let request;
|
||||
request=api('/api/session/lineage/report?session_id='+encodeURIComponent(s.session_id))
|
||||
.then(report=>{
|
||||
if(generation===_lineageReportCacheGeneration){
|
||||
_lineageReportCache.set(key,(report&&report.found!==false)?report:{error:true});
|
||||
}
|
||||
return report;
|
||||
})
|
||||
.catch(err=>{
|
||||
console.warn('lineage report',err);
|
||||
if(generation===_lineageReportCacheGeneration) _lineageReportCache.set(key,{error:true});
|
||||
return null;
|
||||
})
|
||||
.finally(()=>{
|
||||
if(_lineageReportInflight.get(key)===request) _lineageReportInflight.delete(key);
|
||||
});
|
||||
_lineageReportInflight.set(key,request);
|
||||
return request;
|
||||
}
|
||||
|
||||
function _sidebarLineageKeyForRow(s){
|
||||
if(!s) return null;
|
||||
return s._lineage_key||s._lineage_root_id||s.lineage_root_id||s.parent_session_id||s.session_id||null;
|
||||
@@ -2703,8 +2774,10 @@ function renderSessionListFromCache(){
|
||||
}
|
||||
const lineageKey=_sidebarLineageKeyForRow(s);
|
||||
const segmentCount=_sessionSegmentCount(s);
|
||||
const lineageSegments=Array.isArray(s._lineage_segments)?s._lineage_segments.filter(seg=>seg&&seg.session_id&&seg.session_id!==s.session_id):[];
|
||||
const canExpandLineageSegments=Boolean(lineageKey&&segmentCount>1&&lineageSegments.length>0);
|
||||
const lineageSegments=_lineageSegmentsForRender(s,lineageKey);
|
||||
const needsLineageReport=_lineageReportNeedsFetch(s,lineageKey,segmentCount);
|
||||
const lineageReportKey=_lineageReportCacheKey(s,lineageKey);
|
||||
const canExpandLineageSegments=Boolean(lineageKey&&segmentCount>1&&(lineageSegments.length>0||needsLineageReport||_lineageReportInflight.has(lineageReportKey)));
|
||||
const lineageSegmentsExpanded=canExpandLineageSegments&&_expandedLineageKeys.has(lineageKey);
|
||||
if(segmentCount>0){
|
||||
const segmentCountEl=document.createElement('span');
|
||||
@@ -2721,7 +2794,10 @@ function renderSessionListFromCache(){
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if(_expandedLineageKeys.has(lineageKey)) _expandedLineageKeys.delete(lineageKey);
|
||||
else _expandedLineageKeys.add(lineageKey);
|
||||
else {
|
||||
_expandedLineageKeys.add(lineageKey);
|
||||
if(needsLineageReport) _fetchLineageReportForRow(s,lineageKey).then(()=>renderSessionListFromCache());
|
||||
}
|
||||
renderSessionListFromCache();
|
||||
};
|
||||
segmentCountEl.onclick=toggleLineageSegments;
|
||||
|
||||
@@ -338,11 +338,16 @@ def test_lineage_segment_expansion_static_contract():
|
||||
js = SESSIONS_JS_PATH.read_text(encoding="utf-8")
|
||||
css = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8")
|
||||
assert "const _expandedLineageKeys = new Set();" in js
|
||||
assert "const _lineageReportCache = new Map();" in js
|
||||
assert "const _lineageReportInflight = new Map();" in js
|
||||
assert "session-lineage-count,.session-lineage-segments,.session-lineage-segment" in js
|
||||
assert "segmentCountEl.setAttribute('aria-expanded'" in js
|
||||
assert "_expandedLineageKeys.has(lineageKey)" in js
|
||||
assert "_expandedLineageKeys.add(lineageKey)" in js
|
||||
assert "_expandedLineageKeys.delete(lineageKey)" in js
|
||||
assert "_fetchLineageReportForRow(s,lineageKey).then" in js
|
||||
assert "'/api/session/lineage/report?session_id='" in js
|
||||
assert "encodeURIComponent(s.session_id)" in js
|
||||
assert "className='session-lineage-segments'" in js
|
||||
assert "className='session-lineage-segment'" in js
|
||||
assert "const segTitle=seg.title||t('session_lineage_segment_untitled');" in js
|
||||
@@ -354,6 +359,133 @@ def test_lineage_segment_expansion_static_contract():
|
||||
assert ".session-lineage-segment{" in css
|
||||
|
||||
|
||||
def test_lineage_report_fetch_is_needed_only_when_backend_count_exceeds_materialized_segments():
|
||||
js = SESSIONS_JS_PATH.read_text(encoding="utf-8")
|
||||
source = f"""
|
||||
const src = {js!r};
|
||||
function extractFunc(name) {{
|
||||
const re = new RegExp('function\\\\s+' + name + '\\\\s*\\\\(');
|
||||
const start = src.search(re);
|
||||
if (start < 0) throw new Error(name + ' not found');
|
||||
let i = src.indexOf('{{', start);
|
||||
let depth = 1; i++;
|
||||
while (depth > 0 && i < src.length) {{
|
||||
if (src[i] === '{{') depth++;
|
||||
else if (src[i] === '}}') depth--;
|
||||
i++;
|
||||
}}
|
||||
return src.slice(start, i);
|
||||
}}
|
||||
const _lineageReportCache = new Map();
|
||||
const _lineageReportInflight = new Map();
|
||||
eval(extractFunc('_lineageReportCacheKey'));
|
||||
eval(extractFunc('_lineageLocalSegmentCount'));
|
||||
eval(extractFunc('_lineageReportNeedsFetch'));
|
||||
const backendOnly = {{session_id:'tip', _lineage_key:'root', _compression_segment_count:25}};
|
||||
const localFull = {{
|
||||
session_id:'tip',
|
||||
_lineage_key:'root',
|
||||
_compression_segment_count:2,
|
||||
_lineage_segments:[{{session_id:'tip'}}, {{session_id:'root'}}],
|
||||
}};
|
||||
const before = _lineageReportNeedsFetch(backendOnly, 'root', 25);
|
||||
_lineageReportCache.set('root', {{segments:[{{session_id:'tip'}}, {{session_id:'root'}}]}});
|
||||
const afterCache = _lineageReportNeedsFetch(backendOnly, 'root', 25);
|
||||
const fullLocal = _lineageReportNeedsFetch(localFull, 'root', 2);
|
||||
console.log(JSON.stringify({{before, afterCache, fullLocal}}));
|
||||
"""
|
||||
assert json.loads(_run_node(source)) == {"before": True, "afterCache": False, "fullLocal": False}
|
||||
|
||||
|
||||
def test_cached_lineage_report_segments_merge_with_materialized_segments_without_duplicates_or_children():
|
||||
js = SESSIONS_JS_PATH.read_text(encoding="utf-8")
|
||||
source = f"""
|
||||
const src = {js!r};
|
||||
function extractFunc(name) {{
|
||||
const re = new RegExp('function\\\\s+' + name + '\\\\s*\\\\(');
|
||||
const start = src.search(re);
|
||||
if (start < 0) throw new Error(name + ' not found');
|
||||
let i = src.indexOf('{{', start);
|
||||
let depth = 1; i++;
|
||||
while (depth > 0 && i < src.length) {{
|
||||
if (src[i] === '{{') depth++;
|
||||
else if (src[i] === '}}') depth--;
|
||||
i++;
|
||||
}}
|
||||
return src.slice(start, i);
|
||||
}}
|
||||
const _lineageReportCache = new Map();
|
||||
eval(extractFunc('_lineageReportCacheKey'));
|
||||
eval(extractFunc('_lineageSegmentsForRender'));
|
||||
_lineageReportCache.set('root', {{
|
||||
segments:[
|
||||
{{session_id:'tip', title:'Tip', role:'tip', started_at:30}},
|
||||
{{session_id:'root', title:'Root', role:'hidden_segment', started_at:20}},
|
||||
{{session_id:'older', title:'Older', role:'hidden_segment', started_at:10}},
|
||||
{{session_id:'child', title:'Child', role:'child_session', started_at:40}},
|
||||
],
|
||||
children:[{{session_id:'child', title:'Child', role:'child_session'}}],
|
||||
}});
|
||||
const row = {{
|
||||
session_id:'tip',
|
||||
_lineage_key:'root',
|
||||
_lineage_segments:[{{session_id:'tip', title:'Tip'}}, {{session_id:'root', title:'Root'}}],
|
||||
}};
|
||||
const segments = _lineageSegmentsForRender(row, 'root').map(seg => seg.session_id);
|
||||
console.log(JSON.stringify(segments));
|
||||
"""
|
||||
assert json.loads(_run_node(source)) == ["root", "older"]
|
||||
|
||||
|
||||
def test_lineage_report_fetch_uses_endpoint_once_and_caches_result():
|
||||
js = SESSIONS_JS_PATH.read_text(encoding="utf-8")
|
||||
source = f"""
|
||||
const src = {js!r};
|
||||
function extractFunc(name) {{
|
||||
const re = new RegExp('function\\\\s+' + name + '\\\\s*\\\\(');
|
||||
const start = src.search(re);
|
||||
if (start < 0) throw new Error(name + ' not found');
|
||||
let i = src.indexOf('{{', start);
|
||||
let depth = 1; i++;
|
||||
while (depth > 0 && i < src.length) {{
|
||||
if (src[i] === '{{') depth++;
|
||||
else if (src[i] === '}}') depth--;
|
||||
i++;
|
||||
}}
|
||||
return src.slice(start, i);
|
||||
}}
|
||||
const _lineageReportCache = new Map();
|
||||
const _lineageReportInflight = new Map();
|
||||
let _lineageReportCacheGeneration = 0;
|
||||
const calls = [];
|
||||
function api(path) {{
|
||||
calls.push(path);
|
||||
return Promise.resolve({{found:true, segments:[{{session_id:'tip'}}, {{session_id:'root'}}]}});
|
||||
}}
|
||||
eval(extractFunc('_lineageReportCacheKey'));
|
||||
eval(extractFunc('_fetchLineageReportForRow'));
|
||||
(async()=>{{
|
||||
const row = {{session_id:'tip', _lineage_key:'root'}};
|
||||
const [first, second] = await Promise.all([
|
||||
_fetchLineageReportForRow(row, 'root'),
|
||||
_fetchLineageReportForRow(row, 'root'),
|
||||
]);
|
||||
await _fetchLineageReportForRow(row, 'root');
|
||||
console.log(JSON.stringify({{
|
||||
calls,
|
||||
cached:_lineageReportCache.has('root'),
|
||||
same:first===second,
|
||||
}}));
|
||||
}})().catch(err=>{{console.error(err); process.exit(1);}});
|
||||
"""
|
||||
result = json.loads(_run_node(source))
|
||||
assert result == {
|
||||
"calls": ["/api/session/lineage/report?session_id=tip"],
|
||||
"cached": True,
|
||||
"same": True,
|
||||
}
|
||||
|
||||
|
||||
def test_active_hidden_lineage_segment_auto_expands_parent():
|
||||
js = SESSIONS_JS_PATH.read_text(encoding="utf-8")
|
||||
source = f"""
|
||||
|
||||
Reference in new issue
Block a user