Skip to content

Commit 591d6e4

Browse files
committed
Make Storage gracefully fallback to storage.local whenever storage.sync is unavaiable.
1 parent ac2eb2b commit 591d6e4

2 files changed

Lines changed: 101 additions & 18 deletions

File tree

common/Storage.js

Lines changed: 95 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,35 +21,109 @@
2121
"use strict";
2222
var Storage = (() => {
2323

24-
let chunksKey = k => `${k}/CHUNKS`;
24+
const chunksKey = k => `${k}/CHUNKS`;
25+
26+
let lazyInitSync = async () => {
27+
lazyInitSync = null;
28+
29+
const SYNC_KEYS = "__ALL_SYNC_KEYS__";
30+
let allSyncData;
31+
try {
32+
allSyncData = await browser.storage.sync.get();
33+
} catch (e) {
34+
// sync storage is disabled, bail out
35+
const syncKeys = (await browser.storage.local.get(SYNC_KEYS))[SYNC_KEYS];
36+
if (syncKeys) {
37+
const fallbackKeys = await getLocalFallback();
38+
await setLocalFallback(new Set([...fallbackKeys].concat(syncKeys)))
39+
}
40+
return;
41+
}
42+
43+
const chunkedRx = /^([^/]+)\/(\d+|CHUNKS)$/;
44+
let keys = Object.keys(allSyncData);
45+
46+
// sanitize / repair chunked keys
47+
const safeKeys = [];
48+
const repaired = {};
49+
50+
for (let k of keys) {
51+
if (!k.endsWith("/0")) continue;
52+
const [keyName] = k.split("/");
53+
if (allSyncData[keyName] !== "[CHUNKED]") {
54+
// not flagged as chunked, bail out and doome the remnants
55+
continue;
56+
}
57+
const ccKey = chunksKey(keyName);
58+
let count = parseInt(allSyncData[ccKey]);
59+
let contiguousKeys = [];
60+
const keyPrefix = keyName.concat('/');
61+
for (let j = 1;; j++) {
62+
contiguousKeys.push(k);
63+
if (j >= count) break;
64+
k = keyPrefix.concat(j);
65+
if (!keys.includes(k)) break;
66+
}
67+
let actualCount = contiguousKeys.length;
68+
if (count === actualCount) continue;
69+
// try to repair
70+
repaired[ccKey] = actualCount;
71+
safeKeys.push(ccKey, ...contiguousKeys);
72+
}
73+
74+
const doomedKeys = keys.filter(k => chunkedRx.test(k) && !safeKeys.includes(k));
75+
if (doomedKeys.length) {
76+
await browser.storage.sync.remove(doomedKeys);
77+
}
78+
if (Object.keys(repaired).length) {
79+
await browser.storage.sync.set(repaired);
80+
}
81+
82+
{
83+
// backup sync data on local to survive on the fly sync disablement
84+
const localKeys = Object.keys(await browser.storage.local.get());
85+
const syncKeys = keys.filter(k => !chunkedRx.test(k));
86+
const backupKeys = syncKeys.filter(k => !localKeys.includes(k));
87+
const backupData = await Storage.get("sync", backupKeys);
88+
backupData[SYNC_KEYS] = syncKeys;
89+
await browser.storage.local.set(backupData);
90+
}
91+
92+
93+
};
2594

2695
async function safeOp(op, type, keys) {
2796
let sync = type === "sync";
28-
2997
try {
3098
if (sync) {
99+
100+
if (lazyInitSync) await lazyInitSync();
101+
31102
let remove = op === "remove";
32103
if (remove || op === "get") {
33104
keys = [].concat(keys); // don't touch the passed argument
105+
if (remove) {
106+
// remove local backup
107+
await browser.storage.local.remove(keys);
108+
}
34109
let mergeResults = {};
35110
let localFallback = await getLocalFallback();
36111
if (localFallback.size) {
37112
let localKeys = keys.filter(k => localFallback.has(k));
38113
if (localKeys.length) {
39114
if (remove) {
40-
await browser.storage.local.remove(localKeys);
41115
for (let k of localKeys) {
42116
localFallback.delete(k);
43117
}
44118
await setLocalFallback(localFallback);
45119
} else {
46120
mergeResults = await browser.storage.local.get(localKeys);
121+
keys = keys.filter(k => !localFallback.has(k));
47122
}
48-
keys = keys.filter(k => !localFallback.has(k));
49123
}
50124
}
51125

52-
if (keys.length) { // we may not have non-fallback keys anymore
126+
if (keys.length) { // we may not have non-fallback keys anymore (for get)
53127
let chunkCounts = Object.entries(await browser.storage.sync.get(
54128
keys.map(chunksKey)))
55129
.map(([k, count]) => [k.split("/")[0], count]);
@@ -60,9 +134,10 @@ var Storage = (() => {
60134
while (count-- > 0) chunkedKeys.push(`${k}/${count}`);
61135
}
62136
if (remove) {
63-
let doomedKeys = keys
137+
const doomedKeys = keys
64138
.concat(chunkCounts.map(([k, count]) => chunksKey(k)))
65139
.concat(chunkedKeys);
140+
// remove all the keys included chunked, if any, from sync storage
66141
return await browser.storage.sync.remove(doomedKeys);
67142
} else {
68143
let chunks = await browser.storage.sync.get(chunkedKeys);
@@ -86,6 +161,9 @@ var Storage = (() => {
86161
Object.assign(mergeResults, await browser.storage.sync[op](keys))
87162
: mergeResults;
88163
} else if (op === "set") {
164+
// create/update local backup
165+
await browser.storage.local.set(keys);
166+
89167
keys = Object.assign({}, keys); // don't touch the passed argument
90168
const MAX_ITEM_SIZE = 4096;
91169
// Firefox Sync's max object BYTEs size is 16384, Chrome's 8192.
@@ -134,17 +212,17 @@ var Storage = (() => {
134212
}
135213
return ret;
136214
} catch (e) {
137-
error(e, "%s.%s(%o)", type, op, keys);
138215
if (sync) {
139-
debug("Sync disabled? Falling back to local storage (%s %o)", op, keys);
140-
let localFallback = await getLocalFallback();
141-
let failedKeys = Array.isArray(keys) ? keys
216+
debug("Sync disabled? Falling back to local storage", op, keys, e);
217+
const localFallback = await getLocalFallback();
218+
const failedKeys = Array.isArray(keys) ? keys
142219
: typeof keys === "string" ? [keys] : Object.keys(keys);
143220
for (let k of failedKeys) {
144221
localFallback.add(k);
145222
}
146223
await setLocalFallback(localFallback);
147224
} else {
225+
error(e, "%s.%s(%o)", type, op, keys);
148226
throw e;
149227
}
150228
}
@@ -161,6 +239,12 @@ var Storage = (() => {
161239
return new Set(Array.isArray(keys) ? keys : []);
162240
}
163241

242+
async function isChunked(key) {
243+
let ccKey = chunksKey(key);
244+
let data = await browser.storage.sync.get([key, ccKey]);
245+
return data[key] === "[CHUNKED]" && parseInt(data[ccKey]);
246+
}
247+
164248
return {
165249
async get(type, keys) {
166250
return await safeOp("get", type, keys);
@@ -178,10 +262,6 @@ var Storage = (() => {
178262
return (await getLocalFallback()).has(key);
179263
},
180264

181-
async isChunked(key) {
182-
let ccKey = chunksKey(key);
183-
let data = await browser.storage.sync.get([key, ccKey]);
184-
return data[key] === "[CHUNKED]" && parseInt(data[ccKey]);
185-
}
265+
isChunked,
186266
};
187267
})()

test/Storage_test.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,14 @@
8585
async () => eq("bigObject", "tiny", "prop"),
8686
async () => {
8787
await Storage.remove("sync", keys);
88-
let myItems = await Storage.get("sync", keys);
89-
return Object.keys(myItems).length === 0;
88+
await Storage.remove("local", keys);
89+
const myKeys = Object.keys(await Storage.get("sync", keys)).concat(Object.keys(await Storage.get("local", keys)));
90+
return myKeys.length === 0;
9091
},
9192
]) {
92-
await Test.run(t);
93+
if (!await Test.run(t)) {
94+
error("Storage test failed.\nSync: %o.\nLocal: %o.", await browser.storage.sync.get(), await browser.storage.local.get());
95+
}
9396
}
9497
Test.report();
9598
})();

0 commit comments

Comments
 (0)