Skip to content

Commit ba915fe

Browse files
author
Sebastian Benjamin
committed
Stream JSON results to client
1 parent 2fa67c8 commit ba915fe

File tree

4 files changed

+101
-49
lines changed

4 files changed

+101
-49
lines changed

jbrowse/src/client/JBrowse/VariantSearch/components/VariantTableWidget.tsx

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ const VariantTableWidget = observer(props => {
6868

6969
return obj
7070
}))
71-
setTotalHits(data.totalHits)
7271
setDataLoaded(true)
7372
}
7473

@@ -96,12 +95,35 @@ const VariantTableWidget = observer(props => {
9695
currentUrl.searchParams.set("sortDirection", sort.toString());
9796

9897
if (pushToHistory) {
99-
window.history.pushState(null, "", currentUrl.toString());
98+
window.history.pushState(null, "", currentUrl.toString());
10099
}
101100

102101
setFilters(passedFilters);
103-
setDataLoaded(false)
104-
fetchLuceneQuery(passedFilters, sessionId, trackGUID, page, pageSize, field, sort, (json)=>{handleSearch(json)}, (error) => {setDataLoaded(true); setError(error)});
102+
setDataLoaded(false);
103+
setFeatures([]);
104+
105+
fetchLuceneQuery(
106+
passedFilters,
107+
sessionId,
108+
trackGUID,
109+
page,
110+
pageSize,
111+
field,
112+
sort,
113+
(row) => {
114+
setFeatures(prev => {
115+
row.id = prev.length;
116+
row.trackId = trackId;
117+
return [...prev, row];
118+
});
119+
},
120+
() => setDataLoaded(true),
121+
(error) => {
122+
console.error("Stream error:", error);
123+
setError(error);
124+
setDataLoaded(true);
125+
}
126+
);
105127
}
106128

107129
const handleExport = () => {
@@ -274,7 +296,6 @@ const VariantTableWidget = observer(props => {
274296

275297
const [filterModalOpen, setFilterModalOpen] = useState(false);
276298
const [filters, setFilters] = useState([]);
277-
const [totalHits, setTotalHits] = useState(0);
278299
const [fieldTypeInfo, setFieldTypeInfo] = useState<FieldModel[]>([]);
279300
const [allowedGroupNames, setAllowedGroupNames] = useState<string[]>([]);
280301
const [promotedFilters, setPromotedFilters] = useState<Map<string, Filter[]>>(null);

jbrowse/src/client/JBrowse/utils.ts

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -328,23 +328,24 @@ export function serializeLocationToEncodedSearchString(contig, start, end) {
328328
return createEncodedFilterString(filters)
329329
}
330330

331-
export async function fetchLuceneQuery(filters, sessionId, trackGUID, offset, pageSize, sortField, sortReverseString, successCallback, failureCallback) {
331+
export async function fetchLuceneQuery(filters, sessionId, trackGUID, offset, pageSize, sortField, sortReverseString,
332+
handleRow, handleComplete, handleError) {
332333
if (!offset) {
333334
offset = 0
334335
}
335336

336337
if (!sessionId) {
337-
failureCallback("There was an error: " + "Lucene query: no session ID")
338+
handleError("There was an error: " + "Lucene query: no session ID")
338339
return
339340
}
340341

341342
if (!trackGUID) {
342-
failureCallback("There was an error: " + "Lucene query: no track ID")
343+
handleError("There was an error: " + "Lucene query: no track ID")
343344
return
344345
}
345346

346347
if (!filters) {
347-
failureCallback("There was an error: " + "Lucene query: no filters")
348+
handleError("There was an error: " + "Lucene query: no filters")
348349
return
349350
}
350351

@@ -358,27 +359,59 @@ export async function fetchLuceneQuery(filters, sessionId, trackGUID, offset, pa
358359
sortReverse = false
359360
}
360361

361-
return Ajax.request({
362-
url: ActionURL.buildURL('jbrowse', 'luceneQuery.api'),
363-
method: 'GET',
364-
success: async function(res){
365-
let jsonRes = JSON.parse(res.response);
366-
successCallback(jsonRes)
367-
},
368-
failure: function(res) {
369-
console.error("There was an error: " + res.status + "\n Status Body: " + res.responseText + "\n Session ID:" + sessionId)
370-
failureCallback("There was an error: status " + res.status)
371-
},
372-
params: {
373-
"searchString": encoded,
374-
"sessionId": sessionId,
375-
"trackId": trackGUID,
376-
"offset": offset,
377-
"pageSize": pageSize,
378-
"sortField": sortField ?? "genomicPosition",
379-
"sortReverse": sortReverse
380-
},
362+
const params = new URLSearchParams({
363+
searchString: encoded,
364+
sessionId,
365+
trackId: trackGUID,
366+
offset: offset,
367+
pageSize: pageSize,
368+
sortField: sortField ?? "genomicPosition",
369+
sortReverse: sortReverse,
381370
});
371+
372+
try {
373+
const response = await fetch(`/jbrowse/luceneQuery.api?${params.toString()}`);
374+
if (!response.ok || !response.body) {
375+
throw new Error(`HTTP error ${response.status}`);
376+
}
377+
378+
const reader = response.body.getReader();
379+
const decoder = new TextDecoder("utf-8");
380+
let buffer = '';
381+
382+
while (true) {
383+
const { done, value } = await reader.read();
384+
if (done) break;
385+
386+
buffer += decoder.decode(value, { stream: true });
387+
388+
let boundary;
389+
while ((boundary = buffer.indexOf('\n')) >= 0) {
390+
const line = buffer.slice(0, boundary).trim();
391+
buffer = buffer.slice(boundary + 1);
392+
if (line) {
393+
try {
394+
const parsed = JSON.parse(line);
395+
handleRow(parsed);
396+
} catch (err) {
397+
console.error('Failed to parse line:', line, err);
398+
}
399+
}
400+
}
401+
}
402+
403+
if (buffer.trim()) {
404+
try {
405+
handleRow(JSON.parse(buffer));
406+
} catch (err) {
407+
console.error('Final line parse error:', buffer, err);
408+
}
409+
}
410+
411+
handleComplete();
412+
} catch (error) {
413+
handleError(error.toString());
414+
}
382415
}
383416

384417
export class FieldModel {

jbrowse/src/org/labkey/jbrowse/JBrowseController.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -962,14 +962,18 @@ public ApiResponse execute(LuceneQueryForm form, BindException errors)
962962

963963
try
964964
{
965-
return new ApiSimpleResponse(searcher.doSearchJSON(
965+
HttpServletResponse response = getViewContext().getResponse();
966+
response.setContentType("application/x-ndjson");
967+
searcher.doSearchJSON(
966968
getUser(),
967969
PageFlowUtil.decode(form.getSearchString()),
968970
form.getPageSize(),
969971
form.getOffset(),
970972
form.getSortField(),
971-
form.getSortReverse()
972-
));
973+
form.getSortReverse(),
974+
response
975+
);
976+
return null;
973977
}
974978
catch (Exception e)
975979
{

jbrowse/src/org/labkey/jbrowse/JBrowseLuceneSearch.java

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.labkey.jbrowse;
22

33
import jakarta.servlet.http.HttpServletResponse;
4+
import org.apache.catalina.connector.Response;
45
import org.apache.commons.lang3.StringUtils;
56
import org.apache.logging.log4j.Logger;
67
import org.apache.lucene.analysis.Analyzer;
@@ -202,9 +203,9 @@ public String extractFieldName(String queryString)
202203
return parts.length > 0 ? parts[0].trim() : null;
203204
}
204205

205-
public JSONObject doSearchJSON(User u, String searchString, final int pageSize, final int offset, String sortField, boolean sortReverse) throws IOException, ParseException {
206+
public void doSearchJSON(User u, String searchString, final int pageSize, final int offset, String sortField, boolean sortReverse, HttpServletResponse response) throws IOException, ParseException {
206207
SearchConfig searchConfig = createSearchConfig(u, searchString, pageSize, offset, sortField, sortReverse);
207-
return paginateJSON(searchConfig);
208+
paginateJSON(searchConfig, response);
208209
}
209210

210211
public void doSearchCSV(User u, String searchString, String sortField, boolean sortReverse, HttpServletResponse response) throws IOException, ParseException {
@@ -330,32 +331,26 @@ else if (numericQueryParserFields.containsKey(fieldName))
330331
return new SearchConfig(cacheEntry, query, pageSize, offset, sort, fieldsList);
331332
}
332333

333-
private JSONObject paginateJSON(SearchConfig c) throws IOException, ParseException {
334+
private void paginateJSON(SearchConfig c, HttpServletResponse response) throws IOException, ParseException {
334335
IndexSearcher searcher = c.cacheEntry.indexSearcher;
335336
TopDocs topDocs;
337+
PrintWriter writer = response.getWriter();
336338

337339
if (c.offset == 0) {
338340
topDocs = searcher.search(c.query, c.pageSize, c.sort);
339341
} else {
340342
TopFieldDocs prev = searcher.search(c.query, c.pageSize * c.offset, c.sort);
341-
long totalHits = prev.totalHits.value;
342343
ScoreDoc[] prevHits = prev.scoreDocs;
343344

344345
if (prevHits.length < c.pageSize * c.offset)
345346
{
346-
JSONObject results = new JSONObject();
347-
results.put("data", Collections.emptyList());
348-
results.put("totalHits", totalHits);
349-
return results;
347+
return;
350348
}
351349

352350
ScoreDoc lastDoc = prevHits[c.pageSize * c.offset - 1];
353351
topDocs = searcher.searchAfter(lastDoc, c.query, c.pageSize, c.sort);
354352
}
355353

356-
JSONObject results = new JSONObject();
357-
List<JSONObject> data = new ArrayList<>(topDocs.scoreDocs.length);
358-
359354
for (ScoreDoc sd : topDocs.scoreDocs)
360355
{
361356
Document doc = searcher.storedFields().document(sd.doc);
@@ -366,12 +361,10 @@ private JSONObject paginateJSON(SearchConfig c) throws IOException, ParseExcepti
366361
String[] vals = doc.getValues(name);
367362
elem.put(name, vals.length > 1 ? Arrays.asList(vals) : vals[0]);
368363
}
369-
data.add(elem);
370-
}
371364

372-
results.put("data", data);
373-
results.put("totalHits", topDocs.totalHits.value);
374-
return results;
365+
writer.println(elem);
366+
writer.flush();
367+
}
375368
}
376369

377370
private void exportCSV(SearchConfig c, HttpServletResponse response) throws IOException
@@ -648,8 +641,9 @@ public void cacheDefaultQuery()
648641
{
649642
try
650643
{
644+
HttpServletResponse response = new Response();
651645
JBrowseLuceneSearch.clearCache(_jsonFile.getObjectId());
652-
doSearchJSON(_user, ALL_DOCS, 100, 0, GENOMIC_POSITION, false);
646+
doSearchJSON(_user, ALL_DOCS, 100, 0, GENOMIC_POSITION, false, response);
653647
}
654648
catch (ParseException | IOException e)
655649
{

0 commit comments

Comments
 (0)