Skip to content

Commit afa02de

Browse files
committed
query stuff
1 parent 17cc84c commit afa02de

File tree

2 files changed

+103
-43
lines changed

2 files changed

+103
-43
lines changed

special-pages/pages/history/app/global-state/HistoryServiceProvider.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ export function useRangeChange(service) {
135135
const dispatch = useHistoryServiceDispatch();
136136
useSignalEffect(() => {
137137
function handler(/** @type {CustomEvent<{start: number, end: number}>} */ event) {
138-
if (!service.query) throw new Error('unreachable');
138+
if (!service.data) throw new Error('unreachable');
139139
const { end } = event.detail;
140140
dispatch({ kind: 'request-more', end });
141141
}

special-pages/pages/history/app/history.service.js

Lines changed: 102 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,30 @@ import { HistoryRangeService } from './history.range.service.js';
1414

1515
export class HistoryService {
1616
static CHUNK_SIZE = 150;
17+
static QUERY_EVENT = 'query';
18+
static QUERY_MORE_EVENT = 'query-more';
19+
/**
20+
* @return {QueryData}
21+
*/
22+
static defaultData() {
23+
return {
24+
lastQueryParams: null,
25+
info: {
26+
query: { term: '' },
27+
finished: true,
28+
},
29+
results: [],
30+
};
31+
}
32+
33+
/**
34+
* @type {QueryData}
35+
*/
36+
data = HistoryService.defaultData();
1737

1838
internal = new EventTarget();
1939
dataReadinessSignal = new EventTarget();
2040

21-
/**
22-
* @type {QueryData|null}
23-
*/
24-
query = null;
2541
/** @type {HistoryQuery|null} */
2642
ongoing = null;
2743
index = 0;
@@ -32,56 +48,100 @@ export class HistoryService {
3248
constructor(history) {
3349
this.history = history;
3450
this.range = new HistoryRangeService(this.history);
35-
this.internal.addEventListener('query', (/** @type {CustomEvent<HistoryQuery>} */ evt) => {
51+
52+
/**
53+
* Conduct a query
54+
*/
55+
this.internal.addEventListener(HistoryService.QUERY_EVENT, (/** @type {CustomEvent<HistoryQuery>} */ evt) => {
3656
const { detail } = evt;
37-
const index = this.index + 1;
3857
this.index++;
58+
59+
// store a local index, we can check it when the promise resolves
60+
const index = this.index;
61+
62+
// reject duplicates
3963
if (eq(detail, this.ongoing)) return console.log('ignoring duplicate query');
40-
this.queryFetcher(detail).then((next) => {
41-
if (this.index !== index) return console.log('ignoring stale call...');
42-
const old = this.query;
43-
if (old === null) throw new Error('unreachable - this.query must always be there?');
44-
let publish;
45-
/**
46-
* concatenate results if this was a 'fetch more' request
47-
*/
48-
if (eq(old.info.query, next.info.query) && next.lastQueryParams?.offset > 0) {
49-
const results = old.results.concat(next.results);
50-
publish = { info: next.info, results, lastQueryParams: next.lastQueryParams };
51-
} else {
52-
publish = next;
53-
}
54-
this.accept(publish);
55-
});
64+
this.ongoing = JSON.parse(JSON.stringify(detail));
65+
66+
this.queryFetcher(detail)
67+
.then((next) => {
68+
const old = this.data;
69+
if (old === null) throw new Error('unreachable - typescript this.query must always be there?');
70+
71+
/**
72+
* First, reject overlapping promises
73+
*/
74+
const resolvedPromiseIsStale = this.index !== index;
75+
if (resolvedPromiseIsStale) return console.log('❌ rejected stale result');
76+
77+
/**
78+
* concatenate results if this was a 'fetch more' request, or overwrite
79+
*/
80+
let valueToPublish;
81+
if (eq(old.info.query, next.info.query) && next.lastQueryParams?.offset > 0) {
82+
const results = old.results.concat(next.results);
83+
valueToPublish = { info: next.info, results, lastQueryParams: next.lastQueryParams };
84+
} else {
85+
valueToPublish = next;
86+
}
87+
88+
this.accept(valueToPublish);
89+
})
90+
.catch((e) => {
91+
console.error(e, detail);
92+
});
5693
});
57-
this.internal.addEventListener('query-more', (/** @type {CustomEvent<{end: number}>} */ evt) => {
94+
95+
/**
96+
* Allow consumers to request 'more' - we'll ignore when the list is 'finished',
97+
* but otherwise will just increment the offset by the current length.
98+
*/
99+
this.internal.addEventListener(HistoryService.QUERY_MORE_EVENT, (/** @type {CustomEvent<{end: number}>} */ evt) => {
58100
// console.log('🦻 [query-more]', evt.detail, this.query?.info);
59-
if (!this.query) return;
60-
if (this.query.info.finished) return;
101+
if (!this.data) return;
102+
/**
103+
* 'end' is the index of the last seen element. We use that + the result set & OVERSCAN_AMOUNT
104+
* whether to decide to fetch more data.
105+
*
106+
* Example:
107+
* - if !finished (meaning the backend has more data)
108+
* - and 'end' was 146 (meaning the 146th element was scrolled into view)
109+
* - and memory.length was 150 (meaning we've got 150 items in memory)
110+
* - and OVERSCAN_AMOUNT = 5
111+
* - that means we WOULD fetch more, because memory.length - end = 4, which is less than OVERSCAN_AMOUNT
112+
* - but if 'end' was 140, we would NOT fetch. because memory.length - end = 10 which is not less than OVERSCAN_AMOUNT
113+
*
114+
*/
115+
if (this.data.info.finished) return;
61116
const { end } = evt.detail;
62-
const memory = this.query.results;
117+
118+
const memory = this.data.results;
63119
if (memory.length - end < OVERSCAN_AMOUNT) {
64-
const lastquery = this.query.info.query;
120+
const lastquery = this.data.info.query;
65121
/** @type {HistoryQuery} */
66122
const query = {
67123
query: lastquery,
68124
limit: HistoryService.CHUNK_SIZE,
69-
offset: this.query.results.length,
125+
offset: this.data.results.length,
70126
};
71-
this.internal.dispatchEvent(new CustomEvent('query', { detail: query }));
127+
this.internal.dispatchEvent(new CustomEvent(HistoryService.QUERY_EVENT, { detail: query }));
72128
}
73129
});
74130
}
75131

76132
/**
77-
* @param {QueryData} d
133+
* To 'accept' data is to store a local reference to it and treat it as 'latest'
134+
* We also want to broadcast the fact that new data can be read.
135+
* @param {QueryData} data
78136
*/
79-
accept(d) {
80-
this.query = d;
137+
accept(data) {
138+
this.data = data;
139+
this.ongoing = null;
81140
this.dataReadinessSignal.dispatchEvent(new Event('data'));
82141
}
83142

84143
/**
144+
* The single place for the query to be made
85145
* @param {HistoryQuery} query
86146
*/
87147
queryFetcher(query) {
@@ -105,15 +165,16 @@ export class HistoryService {
105165
}
106166

107167
/**
168+
* Allow consumers to be notified when data has changed
108169
* @param {(data: QueryData) => void} cb
109170
*/
110171
onResults(cb) {
111172
const controller = new AbortController();
112173
this.dataReadinessSignal.addEventListener(
113174
'data',
114175
() => {
115-
if (this.query === null) throw new Error('unreachable');
116-
cb(this.query);
176+
if (this.data === null) throw new Error('unreachable');
177+
cb(this.data);
117178
},
118179
{ signal: controller.signal },
119180
);
@@ -131,15 +192,14 @@ export class HistoryService {
131192
* @param {HistoryQuery} query
132193
*/
133194
trigger(query) {
134-
this.internal.dispatchEvent(new CustomEvent('query', { detail: query }));
195+
this.internal.dispatchEvent(new CustomEvent(HistoryService.QUERY_EVENT, { detail: query }));
135196
}
136197

137198
/**
138-
* @param {number} end
199+
* @param {number} end - the index of the last seen element
139200
*/
140201
requestMore(end) {
141-
if (!this.dataReadinessSignal || !this.query) return console.warn('unreachable?');
142-
this.internal.dispatchEvent(new CustomEvent('query-more', { detail: { end } }));
202+
this.internal.dispatchEvent(new CustomEvent(HistoryService.QUERY_MORE_EVENT, { detail: { end } }));
143203
}
144204

145205
/**
@@ -166,7 +226,7 @@ export class HistoryService {
166226
return { kind: 'none' };
167227
}
168228
if (response.action === 'domain-search' && ids.length === 1 && indexes.length === 1) {
169-
const target = this.query?.results[indexes[0]];
229+
const target = this.data?.results[indexes[0]];
170230
if (target?.domain) {
171231
return { kind: 'domain-search', value: target.domain };
172232
} else {
@@ -196,7 +256,7 @@ export class HistoryService {
196256
_collectIds(indexes) {
197257
const ids = [];
198258
for (let i = 0; i < indexes.length; i++) {
199-
const current = this.query?.results[indexes[i]];
259+
const current = this.data?.results[indexes[i]];
200260
if (!current) throw new Error('unreachable');
201261
ids.push(current.id);
202262
}
@@ -207,8 +267,8 @@ export class HistoryService {
207267
* @param {(d: QueryData) => QueryData} updater
208268
*/
209269
update(updater) {
210-
if (this.query === null) throw new Error('unreachable');
211-
this.accept(updater(this.query));
270+
if (this.data === null) throw new Error('unreachable');
271+
this.accept(updater(this.data));
212272
}
213273

214274
/**

0 commit comments

Comments
 (0)