Repository navigation
Expand file tree
/
Copy pathstorage-queue.js
More file actions
123 lines (109 loc) · 4.26 KB
/
Copy pathstorage-queue.js
File metadata and controls
123 lines (109 loc) · 4.26 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
// Only the service worker owns this queue. Pages send typed commands through
// storage-client.js; a queue in a page is never a storage authority.
export const PRIVACY_MARKER = 'privacyMutationState';
let writeQueue = Promise.resolve();
let storageGeneration = 0;
let deletionActive = false;
let commitGeneration = null;
let initialization = null;
export function storageCall(area, method, value) {
return new Promise((resolve, reject) => {
const callback = result => {
const error = globalThis.chrome.runtime?.lastError;
if (error) reject(new Error(error.message));
else resolve(result || {});
};
if (method === 'clear') chrome.storage[area][method](callback);
else chrome.storage[area][method](value, callback);
});
}
export function assertStorageEpoch(epoch) {
if (!Number.isSafeInteger(epoch) || epoch !== storageGeneration || deletionActive) {
throw new Error('Data changed or deletion is pending. Please retry the action.');
}
}
export function assertStorageCommit() {
assertStorageEpoch(commitGeneration);
}
export function runStorageMutation(operation, epoch = storageGeneration) {
return enqueueStorageMutation(async () => {
assertStorageEpoch(epoch);
commitGeneration = epoch;
try {
const result = await operation();
assertStorageEpoch(epoch);
return result;
} finally {
commitGeneration = null;
}
});
}
export function initializeStorageAuthority(onFence = () => {}) {
if (initialization) return initialization;
const wasDeleting = deletionActive;
deletionActive = true; // No collection while the durable marker is unknown.
initialization = enqueueStorageMutation(async () => {
const data = await storageCall('local', 'get', [PRIVACY_MARKER]);
const marker = data[PRIVACY_MARKER];
if (marker !== undefined && (!Number.isSafeInteger(marker?.epoch) || marker.epoch < 0 || typeof marker.deleting !== 'boolean')) {
deletionActive = true;
throw new Error('Invalid privacy safety marker. Deletion retry is required.');
}
storageGeneration = Math.max(storageGeneration, marker?.epoch || 0);
if (wasDeleting || marker?.deleting === true) {
onFence();
await finishStorageDeletion();
} else {
if (!marker) {
await storageCall('local', 'set', { [PRIVACY_MARKER]: { epoch: storageGeneration, deleting: false } });
}
// Only confirmed durable initialization may open the mutation boundary.
deletionActive = false;
}
});
return initialization;
}
async function finishStorageDeletion() {
// Never clear the local area: removing the marker even briefly would let a
// restarted worker accept an old epoch. Enumerate all other keys instead.
const data = await storageCall('local', 'get', null);
const keys = Object.keys(data).filter(key => key !== PRIVACY_MARKER);
const results = await Promise.allSettled([
keys.length ? storageCall('local', 'remove', keys) : Promise.resolve(),
chrome.storage.session ? storageCall('session', 'clear') : Promise.resolve(),
]);
const failure = results.find(result => result.status === 'rejected');
if (failure) throw failure.reason;
await storageCall('local', 'set', { [PRIVACY_MARKER]: { epoch: storageGeneration, deleting: false } });
deletionActive = false;
}
export function deleteStorageData(onFence = () => {}) {
beginStorageDeletion();
onFence();
return enqueueStorageMutation(async () => {
// Re-read on retry, including initialization failures, so epochs never go back.
const data = await storageCall('local', 'get', [PRIVACY_MARKER]);
storageGeneration = Math.max(storageGeneration, (data[PRIVACY_MARKER]?.epoch || 0) + 1);
await storageCall('local', 'set', { [PRIVACY_MARKER]: { epoch: storageGeneration, deleting: true } });
await finishStorageDeletion();
});
}
export function enqueueStorageMutation(operation) {
const next = writeQueue.then(operation, operation);
writeQueue = next.catch(() => {});
return next;
}
export function getStorageGeneration() {
return storageGeneration;
}
export function beginStorageDeletion() {
storageGeneration += 1;
deletionActive = true;
return storageGeneration;
}
export function endStorageDeletion() {
deletionActive = false;
}
export function isStorageDeletionActive() {
return deletionActive;
}