@@ -4,10 +4,11 @@ import { useMessaging } from '../types.js';
4
4
import { ActivityService } from './activity.service.js' ;
5
5
import { reducer , useConfigSubscription , useInitialDataAndConfig } from '../service.hooks.js' ;
6
6
import { eventToTarget } from '../utils.js' ;
7
- import { usePlatformName } from '../settings.provider.js' ;
7
+ import { useBatchedActivityApi , usePlatformName } from '../settings.provider.js' ;
8
8
import { ACTION_ADD_FAVORITE , ACTION_BURN , ACTION_REMOVE , ACTION_REMOVE_FAVORITE } from './constants.js' ;
9
9
import { batch , signal , useSignal , useSignalEffect } from '@preact/signals' ;
10
10
import { DDG_DEFAULT_ICON_SIZE } from '../favorites/constants.js' ;
11
+ import { BatchedActivity } from './batched-activity.service.js' ;
11
12
12
13
/**
13
14
* @typedef {import('../../types/new-tab.js').ActivityData } ActivityData
@@ -54,9 +55,10 @@ export function ActivityProvider(props) {
54
55
55
56
const [ state , dispatch ] = useReducer ( reducer , initial ) ;
56
57
const platformName = usePlatformName ( ) ;
58
+ const batched = useBatchedActivityApi ( ) ;
57
59
58
60
// create an instance of `ActivityService` for the lifespan of this component.
59
- const service = useService ( ) ;
61
+ const service = useService ( batched ) ;
60
62
61
63
// get initial data
62
64
useInitialDataAndConfig ( { dispatch, service } ) ;
@@ -142,64 +144,81 @@ export function ActivityProvider(props) {
142
144
*/
143
145
function normalizeItems ( prev , data ) {
144
146
return {
145
- favorites : Object . fromEntries (
146
- data . activity . map ( ( x ) => {
147
- return [ x . url , x . favorite ] ;
148
- } ) ,
149
- ) ,
150
- items : Object . fromEntries (
151
- data . activity . map ( ( x ) => {
152
- /** @type {Item } */
153
- const next = {
154
- etldPlusOne : x . etldPlusOne ,
155
- title : x . title ,
156
- url : x . url ,
157
- faviconMax : x . favicon ?. maxAvailableSize ?? DDG_DEFAULT_ICON_SIZE ,
158
- favoriteSrc : x . favicon ?. src ,
159
- trackersFound : x . trackersFound ,
160
- } ;
161
- const differs = shallowDiffers ( next , prev . items [ x . url ] || { } ) ;
162
- return [ x . url , differs ? next : prev . items [ x . url ] || { } ] ;
163
- } ) ,
164
- ) ,
165
- history : Object . fromEntries (
166
- data . activity . map ( ( x ) => {
167
- const differs = shallowDiffers ( x . history , prev . history [ x . url ] || [ ] ) ;
168
- return [ x . url , differs ? [ ...x . history ] : prev . history [ x . url ] || [ ] ] ;
169
- } ) ,
170
- ) ,
171
- trackingStatus : Object . fromEntries (
172
- data . activity . map ( ( x ) => {
173
- const prevItem = prev . trackingStatus [ x . url ] || {
174
- totalCount : 0 ,
175
- trackerCompanies : [ ] ,
176
- } ;
177
- const differs = shallowDiffers ( x . trackingStatus . trackerCompanies , prevItem . trackerCompanies ) ;
178
- if ( prevItem . totalCount !== x . trackingStatus . totalCount || differs ) {
147
+ favorites : {
148
+ ...prev . favorites ,
149
+ ...Object . fromEntries (
150
+ data . activity . map ( ( x ) => {
151
+ return [ x . url , x . favorite ] ;
152
+ } ) ,
153
+ ) ,
154
+ } ,
155
+ items : {
156
+ ...prev . items ,
157
+ ...Object . fromEntries (
158
+ data . activity . map ( ( x ) => {
159
+ /** @type {Item } */
179
160
const next = {
180
- totalCount : x . trackingStatus . totalCount ,
181
- trackerCompanies : [ ...x . trackingStatus . trackerCompanies ] ,
161
+ etldPlusOne : x . etldPlusOne ,
162
+ title : x . title ,
163
+ url : x . url ,
164
+ faviconMax : x . favicon ?. maxAvailableSize ?? DDG_DEFAULT_ICON_SIZE ,
165
+ favoriteSrc : x . favicon ?. src ,
166
+ trackersFound : x . trackersFound ,
182
167
} ;
183
- return [ x . url , next ] ;
184
- }
185
- return [ x . url , prevItem ] ;
186
- } ) ,
187
- ) ,
168
+ const differs = shallowDiffers ( next , prev . items [ x . url ] || { } ) ;
169
+ return [ x . url , differs ? next : prev . items [ x . url ] || { } ] ;
170
+ } ) ,
171
+ ) ,
172
+ } ,
173
+ history : {
174
+ ...prev . history ,
175
+ ...Object . fromEntries (
176
+ data . activity . map ( ( x ) => {
177
+ const differs = shallowDiffers ( x . history , prev . history [ x . url ] || [ ] ) ;
178
+ return [ x . url , differs ? [ ...x . history ] : prev . history [ x . url ] || [ ] ] ;
179
+ } ) ,
180
+ ) ,
181
+ } ,
182
+ trackingStatus : {
183
+ ...prev . trackingStatus ,
184
+ ...Object . fromEntries (
185
+ data . activity . map ( ( x ) => {
186
+ const prevItem = prev . trackingStatus [ x . url ] || {
187
+ totalCount : 0 ,
188
+ trackerCompanies : [ ] ,
189
+ } ;
190
+ const differs = shallowDiffers ( x . trackingStatus . trackerCompanies , prevItem . trackerCompanies ) ;
191
+ if ( prevItem . totalCount !== x . trackingStatus . totalCount || differs ) {
192
+ const next = {
193
+ totalCount : x . trackingStatus . totalCount ,
194
+ trackerCompanies : [ ...x . trackingStatus . trackerCompanies ] ,
195
+ } ;
196
+ return [ x . url , next ] ;
197
+ }
198
+ return [ x . url , prevItem ] ;
199
+ } ) ,
200
+ ) ,
201
+ } ,
188
202
} ;
189
203
}
190
204
191
205
/**
192
- * @param {{ available: string[]; max: number } } prev
193
- * @param {ActivityData } data
194
- * @return {{ available: string[]; max: number } }
206
+ * @param {string[] } prev
207
+ * @param {string[] } data
208
+ * @return {string[] }
195
209
*/
196
210
function normalizeKeys ( prev , data ) {
197
- const keys = data . activity . map ( ( x ) => x . url ) ;
198
- const next = shallowDiffers ( prev , keys ) ? keys : prev . available ;
199
- return {
200
- available : next ,
201
- max : keys . length ,
202
- } ;
211
+ const next = shallowDiffers ( prev , data ) ? [ ...data ] : prev ;
212
+ return next ;
213
+ }
214
+
215
+ /**
216
+ * @param {string[] } prev
217
+ * @param {import('../../types/new-tab.js').UrlInfo } data
218
+ * @return {string[] }
219
+ */
220
+ function normalizeUrls ( prev , data ) {
221
+ return shallowDiffers ( prev , data . urls ) ? [ ...data . urls ] : prev ;
203
222
}
204
223
205
224
/**
@@ -216,16 +235,23 @@ export function shallowDiffers(a, b) {
216
235
217
236
export const SignalStateContext = createContext ( {
218
237
activity : signal ( /** @type {NormalizedActivity } */ ( { } ) ) ,
219
- keys : signal ( /** @type {{available: string[]; max: number} } */ ( { available : [ ] , max : 0 } ) ) ,
238
+ keys : signal ( /** @type {string[] } */ ( [ ] ) ) ,
220
239
} ) ;
221
240
222
241
export function SignalStateProvider ( { children } ) {
223
242
const { state } = useContext ( ActivityContext ) ;
224
- const service = useContext ( ActivityServiceContext ) ;
243
+ const service = /** @type { BatchedActivity } */ ( useContext ( ActivityServiceContext ) ) ;
225
244
if ( state . status !== 'ready' ) throw new Error ( 'must have ready status here' ) ;
226
245
if ( ! service ) throw new Error ( 'must have service here' ) ;
246
+ if ( ! service . urlService . data ) throw new Error ( 'must have initialised the url service by this point' ) ;
227
247
228
- const keys = useSignal ( normalizeKeys ( { available : [ ] , max : 0 } , state . data ) ) ;
248
+ const keys = useSignal (
249
+ normalizeKeys (
250
+ [ ] ,
251
+ state . data . activity . map ( ( x ) => x . url ) ,
252
+ ) ,
253
+ ) ;
254
+ const urls = useSignal ( normalizeUrls ( [ ] , service . urlService . data ) ) ;
229
255
const activity = useSignal (
230
256
normalizeItems (
231
257
{
@@ -240,23 +266,69 @@ export function SignalStateProvider({ children }) {
240
266
241
267
useSignalEffect ( ( ) => {
242
268
if ( ! service ) return console . warn ( 'could not access service' ) ;
243
- const unsub = service . onData ( ( evt ) => {
269
+ const src = /** @type {BatchedActivity } */ ( service ) ;
270
+ const unsub = src . onData ( ( evt ) => {
244
271
batch ( ( ) => {
245
- keys . value = normalizeKeys ( keys . value , evt . data ) ;
246
272
activity . value = normalizeItems ( activity . value , evt . data ) ;
273
+ // const data = Object.keys(activity.value.items);
247
274
} ) ;
248
275
} ) ;
249
- const handler = ( ) => {
250
- if ( document . visibilityState === 'visible' ) {
251
- console . log ( 'will fetch' ) ;
252
- service
253
- . triggerDataFetch ( )
254
- // eslint-disable-next-line promise/prefer-await-to-then
255
- . catch ( ( e ) => console . error ( 'trigger fetch errored' , e ) ) ;
276
+ const unsubPatch = src . onUrlData ( ( evt ) => {
277
+ if ( evt . data . patch ) {
278
+ activity . value = normalizeItems ( activity . value , { activity : [ evt . data . patch ] } ) ;
256
279
}
280
+ urls . value = normalizeUrls ( urls . value , evt . data ) ;
281
+ const visible = keys . value ;
282
+ const all = urls . value ;
283
+ const nextVisibleRange = all . slice ( 0 , visible . length ) ;
284
+ setVisibleRange ( nextVisibleRange ) ;
285
+ } ) ;
286
+
287
+ /**
288
+ * @param {string[] } nextVisibleRange
289
+ */
290
+ function setVisibleRange ( nextVisibleRange ) {
291
+ keys . value = normalizeKeys ( keys . value , nextVisibleRange ) ;
292
+ fillHoles ( ) ;
293
+ // todo, evict entries?
294
+ }
295
+
296
+ function fillHoles ( ) {
297
+ const visible = keys . value ;
298
+ const data = Object . keys ( activity . value . items ) ;
299
+ const missing = visible . filter ( ( x ) => ! data . includes ( x ) ) ;
300
+ src . next ( missing ) ;
301
+ }
302
+
303
+ function updateVisible ( ) {
304
+ const visibleLength = keys . value . length ;
305
+ const end = visibleLength + service . SIZE ;
306
+ const nextVisibleRange = urls . value . slice ( 0 , end ) ;
307
+ setVisibleRange ( nextVisibleRange ) ;
308
+ }
309
+
310
+ window . addEventListener ( 'activity.next' , updateVisible ) ;
311
+
312
+ return ( ) => {
313
+ unsub ( ) ;
314
+ unsubPatch ( ) ;
315
+ window . removeEventListener ( 'activity.next' , updateVisible ) ;
316
+ } ;
317
+ } ) ;
318
+
319
+ useEffect ( ( ) => {
320
+ const handler = ( ) => {
321
+ // todo: re-enable this
322
+ // if (document.visibilityState === 'visible') {
323
+ // // console.log('will fetch');
324
+ // src.triggerDataFetch()
325
+ // // eslint-disable-next-line promise/prefer-await-to-then
326
+ // .catch((e) => console.error('trigger fetch errored', e));
327
+ // }
257
328
} ;
258
329
259
- ( ( ) => {
330
+ // eslint-disable-next-line no-labels,no-unused-labels
331
+ $INTEGRATION: ( ( ) => {
260
332
// export the event in tests
261
333
if ( window . __playwright_01 ) {
262
334
/** @type {any } */ ( window ) . __trigger_document_visibilty__ = handler ;
@@ -265,26 +337,26 @@ export function SignalStateProvider({ children }) {
265
337
266
338
document . addEventListener ( 'visibilitychange' , handler ) ;
267
339
return ( ) => {
268
- unsub ( ) ;
269
340
document . removeEventListener ( 'visibilitychange' , handler ) ;
270
341
} ;
271
- } ) ;
342
+ } , [ ] ) ;
272
343
273
344
return < SignalStateContext . Provider value = { { activity, keys } } > { children } </ SignalStateContext . Provider > ;
274
345
}
275
346
276
347
/**
277
- * @return {import("preact").RefObject<ActivityService> }
348
+ * @param {boolean } useBatched
349
+ * @return {import("preact").RefObject<ActivityService|BatchedActivity> }
278
350
*/
279
- export function useService ( ) {
280
- const service = useRef ( /** @type {ActivityService|null } */ ( null ) ) ;
351
+ export function useService ( useBatched ) {
352
+ const service = useRef ( /** @type {ActivityService|BatchedActivity| null } */ ( null ) ) ;
281
353
const ntp = useMessaging ( ) ;
282
354
useEffect ( ( ) => {
283
- const stats = new ActivityService ( ntp ) ;
355
+ const stats = useBatched ? new BatchedActivity ( ntp ) : new ActivityService ( ntp ) ;
284
356
service . current = stats ;
285
357
return ( ) => {
286
358
stats . destroy ( ) ;
287
359
} ;
288
- } , [ ntp ] ) ;
360
+ } , [ ntp , useBatched ] ) ;
289
361
return service ;
290
362
}
0 commit comments