-
-
Notifications
You must be signed in to change notification settings - Fork 661
Expand file tree
/
Copy pathbackground.mjs
More file actions
136 lines (127 loc) · 4.89 KB
/
Copy pathbackground.mjs
File metadata and controls
136 lines (127 loc) · 4.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import {
classifyServiceRequest,
classifyStyleRequest,
createPageScope,
createTabTaskQueue,
} from "./service-scanner.mjs";
const MAX_REQUESTS_PER_TAB = 100;
const enqueue = createTabTaskQueue();
const scope = createPageScope();
/** Style documents seen per tab, keyed by the origin that served them. */
const stylesByTab = new Map();
/**
* The candidate most recently written for each tab. Panning a slippy map turns
* every tile into the same candidate, so without this each one would queue a
* `storage.session` read just to discover there is nothing to write.
*/
const lastWritten = new Map();
function candidateKey(service) {
return [service.url, service.layer ?? "", service.styleUrl ?? ""].join("\u0000");
}
function forgetTab(tabId) {
stylesByTab.delete(tabId);
lastWritten.delete(tabId);
}
function rememberStyle(tabId, style) {
let origins = stylesByTab.get(tabId);
if (!origins) {
origins = new Map();
stylesByTab.set(tabId, origins);
}
origins.set(style.origin, style.url);
}
function runForTab(tabId, task) {
void enqueue(tabId, task).catch((error) => {
// A failed write leaves the stored list unknown, so drop the shortcut and
// let the next matching request try again.
lastWritten.delete(tabId);
console.warn("GeoLibre could not update detected services.", error);
});
}
/**
* Fill in the style of vector tilesets already stored for this tab. Tiles and
* their style are separate requests and either can finish first, so a tileset
* recorded before its style arrived would otherwise stay unusable: without the
* style's source layers, Add Data cannot resolve the layer.
*/
async function applyStyleToStored(tabId, style) {
const key = `services:${tabId}`;
const stored = await chrome.storage.session.get(key);
const existing = Array.isArray(stored[key]) ? stored[key] : [];
let changed = false;
const next = existing.map((entry) => {
if (entry.format !== "Vector tiles" || entry.styleUrl) return entry;
if (new URL(entry.url).origin !== style.origin) return entry;
changed = true;
return { ...entry, styleUrl: style.url };
});
if (!changed) return;
lastWritten.set(tabId, candidateKey(next[0]));
await chrome.storage.session.set({ [key]: next });
}
// The page boundary is drawn when a navigation starts, not when it finishes,
// so the incoming page's own early requests are not retired along with the
// outgoing page's.
chrome.webRequest.onBeforeRequest.addListener(
({ tabId, type }) => {
if (tabId >= 0 && type === "main_frame") scope.beginPage(tabId);
},
{ urls: ["http://*/*", "https://*/*"] },
);
chrome.webRequest.onCompleted.addListener(
({ tabId, url, type, documentId }) => {
if (tabId < 0) return;
if (type === "main_frame") {
scope.startPage(tabId);
forgetTab(tabId);
runForTab(tabId, () => chrome.storage.session.remove(`services:${tabId}`));
}
if (!scope.accepts(tabId, documentId)) return;
const style = classifyStyleRequest(url);
if (style) {
rememberStyle(tabId, style);
const generation = scope.generation(tabId);
runForTab(tabId, async () => {
if (scope.generation(tabId) !== generation) return;
await applyStyleToStored(tabId, style);
});
}
const service = classifyServiceRequest(url);
if (!service) return;
// A vector tileset is only addable with the source layers its style names.
if (service.format === "Vector tiles") {
service.styleUrl = stylesByTab.get(tabId)?.get(new URL(service.url).origin) ?? null;
}
// Identical to the entry already at the head of the list: nothing to write.
const key = candidateKey(service);
if (lastWritten.get(tabId) === key) return;
lastWritten.set(tabId, key);
const generation = scope.generation(tabId);
runForTab(tabId, async () => {
// The tab may have navigated while this write waited its turn.
if (scope.generation(tabId) !== generation) {
lastWritten.delete(tabId);
return;
}
const storageKey = `services:${tabId}`;
const stored = await chrome.storage.session.get(storageKey);
const existing = Array.isArray(stored[storageKey]) ? stored[storageKey] : [];
// One service can serve several layers, so an entry is a duplicate only
// when it repeats the layer too — and a repeat that has since picked up a
// style still replaces the entry that lacked one.
const same = (entry) =>
Boolean(entry) && entry.url === service.url && (entry.layer ?? null) === service.layer;
const next = [service, ...existing.filter((entry) => !same(entry))].slice(
0,
MAX_REQUESTS_PER_TAB,
);
await chrome.storage.session.set({ [storageKey]: next });
});
},
{ urls: ["http://*/*", "https://*/*"] },
);
chrome.tabs.onRemoved.addListener((tabId) => {
scope.forget(tabId);
forgetTab(tabId);
runForTab(tabId, () => chrome.storage.session.remove(`services:${tabId}`));
});