extract sync and cache from styleMan

This commit is contained in:
tophf 2022-01-23 14:07:55 +03:00
parent e54178a43c
commit c95f74e089
3 changed files with 137 additions and 150 deletions

View File

@ -8,7 +8,7 @@
/* exported /* exported
addAPI addAPI
bgReady bgReady
compareRevision createCache
*/ */
const bgReady = {}; const bgReady = {};
@ -26,6 +26,63 @@ function addAPI(methods) {
} }
} }
function compareRevision(rev1, rev2) { /** Creates a FIFO limit-size map. */
return rev1 - rev2; function createCache({size = 1000, onDeleted} = {}) {
const map = new Map();
const buffer = Array(size);
let index = 0;
let lastIndex = 0;
return {
get(id) {
const item = map.get(id);
return item && item.data;
},
set(id, data) {
if (map.size === size) {
// full
map.delete(buffer[lastIndex].id);
if (onDeleted) {
onDeleted(buffer[lastIndex].id, buffer[lastIndex].data);
}
lastIndex = (lastIndex + 1) % size;
}
const item = {id, data, index};
map.set(id, item);
buffer[index] = item;
index = (index + 1) % size;
},
delete(id) {
const item = map.get(id);
if (!item) {
return false;
}
map.delete(item.id);
const lastItem = buffer[lastIndex];
lastItem.index = item.index;
buffer[item.index] = lastItem;
lastIndex = (lastIndex + 1) % size;
if (onDeleted) {
onDeleted(item.id, item.data);
}
return true;
},
clear() {
map.clear();
index = lastIndex = 0;
},
has: id => map.has(id),
*entries() {
for (const [id, item] of map) {
yield [id, item.data];
}
},
*values() {
for (const item of map.values()) {
yield item.data;
}
},
get size() {
return map.size;
},
};
} }

View File

@ -1,6 +1,6 @@
/* global API msg */// msg.js /* global API msg */// msg.js
/* global CHROME URLS isEmptyObj stringAsRegExp tryRegExp tryURL */// toolbox.js /* global CHROME URLS isEmptyObj stringAsRegExp tryRegExp tryURL */// toolbox.js
/* global bgReady compareRevision */// common.js /* global bgReady createCache */// common.js
/* global calcStyleDigest styleCodeEmpty styleSectionGlobal */// sections-util.js /* global calcStyleDigest styleCodeEmpty styleSectionGlobal */// sections-util.js
/* global db */ /* global db */
/* global prefs */ /* global prefs */
@ -18,8 +18,15 @@ The live preview feature relies on `runtime.connect` and `port.onDisconnect`
to cleanup the temporary code. See livePreview in /edit. to cleanup the temporary code. See livePreview in /edit.
*/ */
const styleUtil = {};
const styleMan = (() => { const styleMan = (() => {
Object.assign(styleUtil, {
id2style,
handleSave,
});
//#region Declarations //#region Declarations
/** @typedef {{ /** @typedef {{
@ -29,7 +36,6 @@ const styleMan = (() => {
}} StyleMapData */ }} StyleMapData */
/** @type {Map<number,StyleMapData>} */ /** @type {Map<number,StyleMapData>} */
const dataMap = new Map(); const dataMap = new Map();
const uuidIndex = new Map();
/** @typedef {Object<styleId,{id: number, code: string[]}>} StyleSectionsToApply */ /** @typedef {Object<styleId,{id: number, code: string[]}>} StyleSectionsToApply */
/** @type {Map<string,{maybeMatch: Set<styleId>, sections: StyleSectionsToApply}>} */ /** @type {Map<string,{maybeMatch: Set<styleId>, sections: StyleSectionsToApply}>} */
const cachedStyleForUrl = createCache({ const cachedStyleForUrl = createCache({
@ -77,16 +83,17 @@ const styleMan = (() => {
} }
}); });
prefs.subscribe(['injectionOrder'], (key, value) => { // TODO: will fix in subsequent commit
order = {}; // prefs.subscribe(['injectionOrder'], (key, value) => {
value.forEach((uid, i) => { // order = {};
const id = uuidIndex.get(uid); // value.forEach((uid, i) => {
if (id) { // const id = uuidIndex.get(uid);
order[id] = i; // if (id) {
} // order[id] = i;
}); // }
msg.broadcast({method: 'styleSort', order}); // });
}); // msg.broadcast({method: 'styleSort', order});
// });
//#endregion //#endregion
//#region Exports //#region Exports
@ -107,7 +114,6 @@ const styleMan = (() => {
if (cache) delete cache.sections[id]; if (cache) delete cache.sections[id];
} }
dataMap.delete(id); dataMap.delete(id);
uuidIndex.delete(style._id);
if (style._usw && style._usw.token) { if (style._usw && style._usw.token) {
// Must be called after the style is deleted from dataMap // Must be called after the style is deleted from dataMap
API.usw.revoke(id); API.usw.revoke(id);
@ -120,17 +126,6 @@ const styleMan = (() => {
return id; return id;
}, },
/** @returns {Promise<number>} style id */
async deleteByUUID(_id, rev) {
if (ready.then) await ready;
const id = uuidIndex.get(_id);
const oldDoc = id && id2style(id);
if (oldDoc && compareRevision(oldDoc._rev, rev) <= 0) {
// FIXME: does it make sense to set reason to 'sync' in deleteByUUID?
return styleMan.delete(id, 'sync');
}
},
/** @returns {Promise<StyleObj>} */ /** @returns {Promise<StyleObj>} */
async editSave(style) { async editSave(style) {
if (ready.then) await ready; if (ready.then) await ready;
@ -157,12 +152,6 @@ const styleMan = (() => {
return Array.from(dataMap.values(), data2style); return Array.from(dataMap.values(), data2style);
}, },
/** @returns {Promise<StyleObj>} */
async getByUUID(uuid) {
if (ready.then) await ready;
return id2style(uuidIndex.get(uuid));
},
/** @returns {Promise<StyleSectionsToApply>} */ /** @returns {Promise<StyleSectionsToApply>} */
async getSectionsByUrl(url, id, isInitialApply) { async getSectionsByUrl(url, id, isInitialApply) {
if (ready.then) await ready; if (ready.then) await ready;
@ -263,10 +252,9 @@ const styleMan = (() => {
} }
} }
const events = await db.exec('putMany', items); const events = await db.exec('putMany', items);
return Promise.all(items.map((item, i) => { return Promise.all(items.map((item, i) =>
afterSave(item, events[i]); handleSave(item, {reason: 'import'}, events[i])
return handleSave(item, {reason: 'import'}); ));
}));
}, },
/** @returns {Promise<StyleObj>} */ /** @returns {Promise<StyleObj>} */
@ -279,31 +267,6 @@ const styleMan = (() => {
return saveStyle(style, {reason}); return saveStyle(style, {reason});
}, },
/** @returns {Promise<?StyleObj>} */
async putByUUID(doc) {
if (ready.then) await ready;
const id = uuidIndex.get(doc._id);
if (id) {
doc.id = id;
} else {
delete doc.id;
}
const oldDoc = id && id2style(id);
let diff = -1;
if (oldDoc) {
diff = compareRevision(oldDoc._rev, doc._rev);
if (diff > 0) {
API.sync.put(oldDoc._id, oldDoc._rev);
return;
}
}
if (diff < 0) {
doc.id = await db.exec('put', doc);
uuidIndex.set(doc._id, doc.id);
return handleSave(doc, {reason: 'sync'});
}
},
save: saveStyle, save: saveStyle,
/** @returns {Promise<number>} style id */ /** @returns {Promise<number>} style id */
@ -472,29 +435,24 @@ const styleMan = (() => {
fixKnownProblems(style); fixKnownProblems(style);
} }
function afterSave(style, newId) {
if (style.id == null) {
style.id = newId;
}
uuidIndex.set(style._id, style.id);
API.sync.put(style._id, style._rev);
}
async function saveStyle(style, handlingOptions) { async function saveStyle(style, handlingOptions) {
beforeSave(style); beforeSave(style);
const newId = await db.exec('put', style); const newId = await db.exec('put', style);
afterSave(style, newId); return handleSave(style, handlingOptions, newId);
return handleSave(style, handlingOptions);
} }
function handleSave(style, {reason, broadcast = true}) { function handleSave(style, {reason, broadcast = true}, id = style.id) {
const data = id2data(style.id); if (style.id == null) style.id = id;
const data = id2data(id);
const method = data ? 'styleUpdated' : 'styleAdded'; const method = data ? 'styleUpdated' : 'styleAdded';
if (!data) { if (!data) {
storeInMap(style); storeInMap(style);
} else { } else {
data.style = style; data.style = style;
} }
if (reason !== 'sync') {
API.sync.putStyle(style);
}
if (broadcast) broadcastStyleUpdated(style, reason, method); if (broadcast) broadcastStyleUpdated(style, reason, method);
return style; return style;
} }
@ -524,10 +482,7 @@ const styleMan = (() => {
if (updated.length) { if (updated.length) {
await db.exec('putMany', updated); await db.exec('putMany', updated);
} }
for (const style of styles) { styles.forEach(storeInMap);
storeInMap(style);
uuidIndex.set(style._id, style.id);
}
ready = true; ready = true;
bgReady._resolveStyles(); bgReady._resolveStyles();
} }
@ -732,64 +687,3 @@ const styleMan = (() => {
//#endregion //#endregion
})(); })();
/** Creates a FIFO limit-size map. */
function createCache({size = 1000, onDeleted} = {}) {
const map = new Map();
const buffer = Array(size);
let index = 0;
let lastIndex = 0;
return {
get(id) {
const item = map.get(id);
return item && item.data;
},
set(id, data) {
if (map.size === size) {
// full
map.delete(buffer[lastIndex].id);
if (onDeleted) {
onDeleted(buffer[lastIndex].id, buffer[lastIndex].data);
}
lastIndex = (lastIndex + 1) % size;
}
const item = {id, data, index};
map.set(id, item);
buffer[index] = item;
index = (index + 1) % size;
},
delete(id) {
const item = map.get(id);
if (!item) {
return false;
}
map.delete(item.id);
const lastItem = buffer[lastIndex];
lastItem.index = item.index;
buffer[item.index] = lastItem;
lastIndex = (lastIndex + 1) % size;
if (onDeleted) {
onDeleted(item.id, item.data);
}
return true;
},
clear() {
map.clear();
index = lastIndex = 0;
},
has: id => map.has(id),
*entries() {
for (const [id, item] of map) {
yield [id, item.data];
}
},
*values() {
for (const item of map.values()) {
yield item.data;
}
},
get size() {
return map.size;
},
};
}

View File

@ -1,8 +1,9 @@
/* global API msg */// msg.js /* global API msg */// msg.js
/* global chromeLocal chromeSync */// storage-util.js /* global chromeLocal chromeSync */// storage-util.js
/* global compareRevision */// common.js /* global db */
/* global iconMan */ /* global iconMan */
/* global prefs */ /* global prefs */
/* global styleUtil */
/* global tokenMan */ /* global tokenMan */
'use strict'; 'use strict';
@ -28,11 +29,17 @@ const syncMan = (() => {
errorMessage: null, errorMessage: null,
login: false, login: false,
}; };
const uuidIndex = new Map();
const uuid2style = uuid => styleUtil.id2style(uuidIndex.get(uuid));
const compareRevision = (rev1, rev2) => rev1 - rev2;
let lastError = null; let lastError = null;
let ctrl; let ctrl;
let currentDrive; let currentDrive;
/** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */ /** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
let ready = prefs.ready.then(() => { let ready = prefs.ready.then(async () => {
for (const {id, _id} of await API.styles.getAll()) {
uuidIndex.set(_id, id);
}
ready = true; ready = true;
prefs.subscribe('sync.enabled', prefs.subscribe('sync.enabled',
(_, val) => val === 'none' (_, val) => val === 'none'
@ -52,11 +59,12 @@ const syncMan = (() => {
return { return {
async delete(...args) { async delete(_id, rev) {
if (ready.then) await ready; if (ready.then) await ready;
if (!currentDrive) return; if (!currentDrive) return;
schedule(); schedule();
return ctrl.delete(...args); uuidIndex.delete(_id);
return ctrl.delete(_id, rev);
}, },
/** @returns {Promise<SyncManager.Status>} */ /** @returns {Promise<SyncManager.Status>} */
@ -86,6 +94,11 @@ const syncMan = (() => {
return ctrl.put(...args); return ctrl.put(...args);
}, },
putStyle({id, _id, _rev}) {
uuidIndex.set(_id, id);
return syncMan.put(_id, _rev);
},
async setDriveOptions(driveName, options) { async setDriveOptions(driveName, options) {
const key = `secure/sync/driveOptions/${driveName}`; const key = `secure/sync/driveOptions/${driveName}`;
await chromeSync.setValue(key, options); await chromeSync.setValue(key, options);
@ -178,14 +191,37 @@ const syncMan = (() => {
async function initController() { async function initController() {
await require(['/vendor/db-to-cloud/db-to-cloud.min']); /* global dbToCloud */ await require(['/vendor/db-to-cloud/db-to-cloud.min']); /* global dbToCloud */
ctrl = dbToCloud.dbToCloud({ ctrl = dbToCloud.dbToCloud({
onGet(id) { onGet: uuid2style,
return API.styles.getByUUID(id); async onPut(doc) {
const id = uuidIndex.get(doc._id);
const oldDoc = uuid2style(doc._id);
if (id) {
doc.id = id;
} else {
delete doc.id;
}
let diff = -1;
if (oldDoc) {
diff = compareRevision(oldDoc._rev, doc._rev);
if (diff > 0) {
syncMan.put(oldDoc);
return;
}
}
if (diff < 0) {
doc.id = await db.exec('put', doc);
uuidIndex.set(doc._id, doc.id);
return styleUtil.handleSave(doc, {reason: 'sync'});
}
}, },
onPut(doc) { onDelete(_id, rev) {
return API.styles.putByUUID(doc); const id = uuidIndex.get(_id);
}, const oldDoc = uuid2style(_id);
onDelete(id, rev) { if (oldDoc && compareRevision(oldDoc._rev, rev) <= 0) {
return API.styles.deleteByUUID(id, rev); // FIXME: does it make sense to set reason to 'sync' in deleteByUUID?
uuidIndex.delete(id);
return API.styles.delete(id, 'sync');
}
}, },
async onFirstSync() { async onFirstSync() {
for (const i of await API.styles.getAll()) { for (const i of await API.styles.getAll()) {