Skip to content

Commit d28acb0

Browse files
committed
atomic removals
1 parent 6d74d35 commit d28acb0

File tree

5 files changed

+111
-139
lines changed

5 files changed

+111
-139
lines changed

special-pages/pages/new-tab/app/activity/ActivityProvider.js

Lines changed: 36 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import { BatchedActivityService } from './batched-activity.service.js';
1414
* @typedef {import('../../types/new-tab.js').ActivityConfig} ActivityConfig
1515
* @typedef {import('../../types/new-tab').TrackingStatus} TrackingStatus
1616
* @typedef {import('../../types/new-tab').HistoryEntry} HistoryEntry
17-
* @typedef {import('../service.hooks.js').State<ActivityData, ActivityConfig>} State
17+
* @typedef {import('../../types/new-tab').DomainActivity} DomainActivity
18+
* @typedef {import('../service.hooks.js').State<import("./batched-activity.service.js").Incoming, ActivityConfig>} State
1819
* @typedef {import('../service.hooks.js').Events<ActivityData, ActivityConfig>} Events
1920
*/
2021

@@ -87,22 +88,35 @@ export function ActivityProvider(props) {
8788
* @property {Record<string, HistoryEntry[]>} history
8889
* @property {Record<string, TrackingStatus>} trackingStatus
8990
* @property {Record<string, boolean>} favorites
91+
* @property {string[]} urls
92+
*/
93+
94+
/**
95+
* @typedef {{ activity: DomainActivity[], urls: string[] }} Incoming
9096
*/
9197

9298
/**
9399
* @param {NormalizedActivity} prev
94-
* @param {ActivityData} data
100+
* @param {Incoming} incoming
95101
* @return {NormalizedActivity}
96102
*/
97-
function normalizeItems(prev, data) {
103+
function normalizeItems(prev, incoming) {
98104
/** @type {NormalizedActivity} */
99105
const output = {
100106
favorites: {},
101107
items: {},
102108
history: {},
103109
trackingStatus: {},
110+
urls: [],
104111
};
105-
for (const item of data.activity) {
112+
113+
if (shallowDiffers(prev.urls, incoming.urls)) {
114+
output.urls = [...incoming.urls];
115+
} else {
116+
output.urls = prev.urls;
117+
}
118+
119+
for (const item of incoming.activity) {
106120
const id = item.url;
107121

108122
output.favorites[id] = item.favorite;
@@ -150,15 +164,6 @@ function normalizeKeys(prev, data) {
150164
return next;
151165
}
152166

153-
/**
154-
* @param {string[]} prev
155-
* @param {import('../../types/new-tab.js').UrlInfo} data
156-
* @return {string[]}
157-
*/
158-
function normalizeUrls(prev, data) {
159-
return shallowDiffers(prev, data.urls) ? [...data.urls] : prev;
160-
}
161-
162167
/**
163168
* Check if two objects have a different shape
164169
* @param {object} a
@@ -183,7 +188,6 @@ export function SignalStateProvider({ children }) {
183188
const service = /** @type {BatchedActivityService} */ (useContext(ActivityServiceContext));
184189
if (state.status !== 'ready') throw new Error('must have ready status here');
185190
if (!service) throw new Error('must have service here');
186-
if (!service.urlService.data) throw new Error('must have initialised the url service by this point');
187191

188192
/**
189193
* @param {MouseEvent} event
@@ -226,31 +230,26 @@ export function SignalStateProvider({ children }) {
226230
}
227231
} else if (toggle) {
228232
if (state.config?.expansion === 'collapsed') {
229-
const next = urls.value.slice(0, Math.min(service.INITIAL, urls.value.length));
233+
const next = activity.value.urls.slice(0, Math.min(service.INITIAL, activity.value.urls.length));
230234
setVisibleRange(next);
231235
}
232236
}
233237
}
234238

235239
const didClick = useCallback(didClick_, [service, state.config.expansion]);
240+
const firstUrls = state.data.activity.map((x) => x.url);
241+
const keys = useSignal(normalizeKeys([], firstUrls));
236242

237-
const keys = useSignal(
238-
normalizeKeys(
239-
[],
240-
state.data.activity.map((x) => x.url),
241-
),
242-
);
243-
244-
const urls = useSignal(normalizeUrls([], service.urlService.data));
245243
const activity = useSignal(
246244
normalizeItems(
247245
{
248246
items: {},
249247
history: {},
250248
trackingStatus: {},
251249
favorites: {},
250+
urls: [],
252251
},
253-
state.data,
252+
{ activity: state.data.activity, urls: state.data.urls },
254253
),
255254
);
256255

@@ -273,7 +272,7 @@ export function SignalStateProvider({ children }) {
273272
if (!batched) return;
274273
const visibleLength = keys.value.length;
275274
const end = visibleLength + service.CHUNK_SIZE;
276-
const nextVisibleRange = urls.value.slice(0, end);
275+
const nextVisibleRange = activity.value.urls.slice(0, end);
277276
setVisibleRange(nextVisibleRange);
278277
fillHoles();
279278
}
@@ -283,27 +282,17 @@ export function SignalStateProvider({ children }) {
283282
const src = /** @type {BatchedActivityService} */ (service);
284283
const unsub = src.onData((evt) => {
285284
batch(() => {
286-
activity.value = normalizeItems(activity.value, evt.data);
287-
if (!batched) {
288-
urls.value = normalizeUrls(urls.value, { urls: evt.data.activity.map((x) => x.url), totalTrackersBlocked: 0 });
289-
setVisibleRange(urls.value);
290-
}
285+
activity.value = normalizeItems(activity.value, { activity: evt.data.activity, urls: evt.data.urls });
286+
const visible = keys.value;
287+
const all = activity.value.urls;
288+
const nextVisibleRange = all.slice(0, Math.max(service.INITIAL, Math.max(service.INITIAL, visible.length)));
289+
setVisibleRange(nextVisibleRange);
290+
fillHoles();
291291
});
292292
});
293-
const unsubPatch = src.onUrlData((evt) => {
294-
if (!batched) return;
295-
urls.value = normalizeUrls(urls.value, evt.data);
296-
const visible = keys.value;
297-
const all = urls.value;
298-
const nextVisibleRange = all.slice(0, Math.max(service.INITIAL, Math.max(service.INITIAL, visible.length)));
299-
console.log({ nextVisibleRange });
300-
setVisibleRange(nextVisibleRange);
301-
fillHoles();
302-
});
303293

304294
return () => {
305295
unsub();
306-
unsubPatch();
307296
};
308297
});
309298

@@ -317,7 +306,12 @@ export function SignalStateProvider({ children }) {
317306
useEffect(() => {
318307
const handler = () => {
319308
if (document.visibilityState === 'visible') {
320-
service.triggerDataFetch();
309+
if (batched) {
310+
const visible = keys.value;
311+
service.triggerDataFetch(visible);
312+
} else {
313+
service.triggerDataFetch();
314+
}
321315
}
322316
};
323317

special-pages/pages/new-tab/app/activity/batched-activity.service.js

Lines changed: 49 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
*/
99
import { Service } from '../service.js';
1010

11+
/**
12+
* @typedef {{ activity: DomainActivity[], urls: string[] }} Incoming
13+
*/
14+
1115
export class BatchedActivityService {
1216
INITIAL = 5;
1317
CHUNK_SIZE = 10;
@@ -21,39 +25,49 @@ export class BatchedActivityService {
2125
this.ntp = ntp;
2226
this.batched = batched;
2327

24-
/** @type {Service<import('../../types/new-tab.js').UrlInfo>} */
25-
this.urlService = new Service({
26-
initial: () => {
27-
if (this.batched) {
28-
return this.ntp.messaging.request('activity_getUrls');
29-
} else {
30-
/** @type {UrlInfo} */
31-
const next = {
32-
urls: [],
33-
totalTrackersBlocked: 0,
34-
};
35-
return Promise.resolve(next);
36-
}
37-
},
38-
subscribe: (cb) => ntp.messaging.subscribe('activity_onDataPatch', cb),
39-
});
40-
41-
/** @type {Service<ActivityData>} */
28+
/** @type {Service<Incoming>} */
4229
this.dataService = new Service({
43-
initial: (params) => {
30+
initial: async (params) => {
4431
if (this.batched) {
45-
return this.ntp.messaging.request('activity_getDataForUrls', { urls: params.urls });
32+
if (params && Array.isArray(params.urls) && this.dataService.data?.urls) {
33+
const data = await this.ntp.messaging.request('activity_getDataForUrls', {
34+
urls: params.urls,
35+
});
36+
return { activity: data.activity, urls: this.dataService.data.urls };
37+
} else {
38+
const urlsResponse = await this.ntp.messaging.request('activity_getUrls');
39+
const data = await this.ntp.messaging.request('activity_getDataForUrls', {
40+
urls: urlsResponse.urls.slice(0, this.INITIAL),
41+
});
42+
return { activity: data.activity, urls: urlsResponse.urls };
43+
}
4644
} else {
47-
return this.ntp.messaging.request('activity_getData');
45+
const data = await this.ntp.messaging.request('activity_getData');
46+
return { activity: data.activity, urls: data.activity.map((x) => x.url) };
4847
}
4948
},
50-
subscribe: (cb) => ntp.messaging.subscribe('activity_onDataUpdate', cb),
49+
subscribe: (cb) => {
50+
const sub1 = ntp.messaging.subscribe('activity_onDataUpdate', (params) => {
51+
cb({ activity: params.activity, urls: params.activity.map((x) => x.url) });
52+
});
53+
const sub2 = ntp.messaging.subscribe('activity_onDataPatch', (params) => {
54+
if ('patch' in params && params.patch !== null) {
55+
cb({ activity: [/** @type {DomainActivity} */ (params.patch)], urls: params.urls });
56+
} else {
57+
cb({ activity: [], urls: params.urls });
58+
}
59+
});
60+
return () => {
61+
sub1();
62+
sub2();
63+
};
64+
},
5165
}).withUpdater((old, next, source) => {
5266
if (source === 'manual') {
5367
return next;
5468
}
5569
if (this.batched) {
56-
return { activity: old.activity.concat(next.activity) };
70+
return { activity: old.activity.concat(next.activity), urls: next.urls };
5771
}
5872
return next;
5973
});
@@ -70,7 +84,6 @@ export class BatchedActivityService {
7084
this.burnUnsub = this.ntp.messaging.subscribe('activity_onBurnComplete', () => {
7185
this.burns?.dispatchEvent(new CustomEvent('activity_onBurnComplete'));
7286
});
73-
this.patchesSub = this.onPatchData();
7487
}
7588

7689
name() {
@@ -82,19 +95,10 @@ export class BatchedActivityService {
8295
* @internal
8396
*/
8497
async getInitial() {
85-
if (this.batched) {
86-
const configPromise = this.configService.fetchInitial();
87-
const urlsPromise = this.urlService.fetchInitial();
88-
const [config, urlData] = await Promise.all([configPromise, urlsPromise]);
89-
const data = await this.dataService.fetchInitial({ urls: urlData.urls.slice(0, this.INITIAL) });
90-
return { config, data };
91-
} else {
92-
const configPromise = this.configService.fetchInitial();
93-
const dataPromise = this.dataService.fetchInitial();
94-
const urlInfoPromise = this.urlService.fetchInitial();
95-
const [config, data] = await Promise.all([configPromise, dataPromise, urlInfoPromise]);
96-
return { config, data };
97-
}
98+
const configPromise = this.configService.fetchInitial();
99+
const dataPromise = this.dataService.fetchInitial();
100+
const [config, data] = await Promise.all([configPromise, dataPromise]);
101+
return { config, data };
98102
}
99103

100104
/**
@@ -104,43 +108,20 @@ export class BatchedActivityService {
104108
this.configService.destroy();
105109
this.dataService.destroy();
106110
this.burnUnsub();
107-
this.patchesSub();
108111
this.burns = null;
109112
}
110113

111114
/**
112115
* @param {string[]} urls
113116
*/
114117
next(urls) {
115-
if (!this.urlService.data) throw new Error('unreachable');
116118
if (urls.length === 0) return;
117119
this.isFetchingNext = true;
118120
this.dataService.triggerFetch({ urls });
119121
}
120122

121123
/**
122-
* @param {(evt: {data: UrlInfo & PatchData, source: InvocationSource}) => void} cb
123-
* @internal
124-
*/
125-
onUrlData(cb) {
126-
return this.urlService.onData((params) => {
127-
if ('patch' in params.data && params.data.patch !== null) return console.log('ignoring patch');
128-
cb(params);
129-
});
130-
}
131-
132-
/**
133-
* @internal
134-
*/
135-
onPatchData() {
136-
return this.urlService.onData((params) => {
137-
if (!('patch' in params.data && params.data.patch !== null)) return console.log('ignoring none-patch');
138-
this.dataService.publish({ activity: [/** @type {DomainActivity} */ (params.data.patch)] });
139-
});
140-
}
141-
142-
/**
143-
* @param {(evt: {data: ActivityData, source: InvocationSource}) => void} cb
124+
* @param {(evt: {data: Incoming, source: InvocationSource}) => void} cb
144125
* @internal
145126
*/
146127
onData(cb) {
@@ -150,9 +131,12 @@ export class BatchedActivityService {
150131
});
151132
}
152133

153-
triggerDataFetch() {
154-
if (this.batched) {
155-
this.urlService.triggerFetch();
134+
/**
135+
* @param {string[]} [urls] - optional subset to refresh
136+
*/
137+
triggerDataFetch(urls) {
138+
if (urls) {
139+
this.dataService.triggerFetch({ urls });
156140
} else {
157141
this.dataService.triggerFetch();
158142
}
@@ -220,19 +204,13 @@ export class BatchedActivityService {
220204
* @param {string} url
221205
*/
222206
remove(url) {
223-
this.urlService.update((old) => {
224-
const next = old.urls.filter((x) => x !== url);
225-
return {
226-
...old,
227-
urls: next,
228-
};
229-
});
230207
this.dataService.update((old) => {
231208
return {
232209
...old,
233210
activity: old.activity.filter((item) => {
234211
return item.url !== url;
235212
}),
213+
urls: old.urls.filter((x) => x !== url),
236214
};
237215
});
238216
this.ntp.messaging.notify('activity_removeItem', { url });

0 commit comments

Comments
 (0)