WIP
This commit is contained in:
parent
24b1eea8a4
commit
0f148eac32
134
background/db.js
Normal file
134
background/db.js
Normal file
|
@ -0,0 +1,134 @@
|
|||
const db = (() => {
|
||||
let exec;
|
||||
const preparing = prepare();
|
||||
return {
|
||||
exec: (...args) =>
|
||||
preparing.then(() => exec(...args))
|
||||
};
|
||||
|
||||
function prepare() {
|
||||
// we use chrome.storage.local fallback if IndexedDB doesn't save data,
|
||||
// which, once detected on the first run, is remembered in chrome.storage.local
|
||||
// for reliablility and in localStorage for fast synchronous access
|
||||
// (FF may block localStorage depending on its privacy options)
|
||||
|
||||
// test localStorage
|
||||
const fallbackSet = localStorage.dbInChromeStorage;
|
||||
if (fallbackSet === 'true' || !tryCatch(() => indexedDB)) {
|
||||
useChromeStorage();
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (fallbackSet === 'false') {
|
||||
useIndexedDB();
|
||||
return Promise.resolve();
|
||||
}
|
||||
// test storage.local
|
||||
return chromeLocal.get('dbInChromeStorage')
|
||||
.then(data =>
|
||||
data && data.dbInChromeStorage && Promise.reject())
|
||||
.then(() =>
|
||||
tryCatch(dbExecIndexedDB, 'getAllKeys', IDBKeyRange.lowerBound(1), 1) ||
|
||||
Promise.reject())
|
||||
.then(({target}) => (
|
||||
(target.result || [])[0] ?
|
||||
Promise.reject('ok') :
|
||||
dbExecIndexedDB('put', {id: -1})))
|
||||
.then(() =>
|
||||
dbExecIndexedDB('get', -1))
|
||||
.then(({target}) => (
|
||||
(target.result || {}).id === -1 ?
|
||||
dbExecIndexedDB('delete', -1) :
|
||||
Promise.reject()))
|
||||
.then(() =>
|
||||
Promise.reject('ok'))
|
||||
.catch(result => {
|
||||
if (result === 'ok') {
|
||||
useIndexedDB();
|
||||
} else {
|
||||
useChromeStorage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function useChromeStorage() {
|
||||
exec = dbExecChromeStorage;
|
||||
chromeLocal.set({dbInChromeStorage: true}, ignoreChromeError);
|
||||
localStorage.dbInChromeStorage = 'true';
|
||||
}
|
||||
|
||||
function useIndexedDB() {
|
||||
exec = dbExecIndexedDB;
|
||||
chromeLocal.set({dbInChromeStorage: false}, ignoreChromeError);
|
||||
localStorage.dbInChromeStorage = 'false';
|
||||
}
|
||||
|
||||
function dbExecIndexedDB(method, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Object.assign(indexedDB.open('stylish', 2), {
|
||||
onsuccess(event) {
|
||||
const database = event.target.result;
|
||||
if (!method) {
|
||||
resolve(database);
|
||||
} else {
|
||||
const transaction = database.transaction(['styles'], 'readwrite');
|
||||
const store = transaction.objectStore('styles');
|
||||
Object.assign(store[method](...args), {
|
||||
onsuccess: event => resolve(event, store, transaction, database),
|
||||
onerror: reject,
|
||||
});
|
||||
}
|
||||
},
|
||||
onerror(event) {
|
||||
console.warn(event.target.error || event.target.errorCode);
|
||||
reject(event);
|
||||
},
|
||||
onupgradeneeded(event) {
|
||||
if (event.oldVersion === 0) {
|
||||
event.target.result.createObjectStore('styles', {
|
||||
keyPath: 'id',
|
||||
autoIncrement: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function dbExecChromeStorage(method, data) {
|
||||
const STYLE_KEY_PREFIX = 'style-';
|
||||
switch (method) {
|
||||
case 'get':
|
||||
return chromeLocal.getValue(STYLE_KEY_PREFIX + data)
|
||||
.then(result => ({target: {result}}));
|
||||
|
||||
case 'put':
|
||||
if (!data.id) {
|
||||
return getStyles().then(() => {
|
||||
data.id = 1;
|
||||
for (const style of cachedStyles.list) {
|
||||
data.id = Math.max(data.id, style.id + 1);
|
||||
}
|
||||
return dbExecChromeStorage('put', data);
|
||||
});
|
||||
}
|
||||
return chromeLocal.setValue(STYLE_KEY_PREFIX + data.id, data)
|
||||
.then(() => (chrome.runtime.lastError ? Promise.reject() : data.id));
|
||||
|
||||
case 'delete':
|
||||
return chromeLocal.remove(STYLE_KEY_PREFIX + data);
|
||||
|
||||
case 'getAll':
|
||||
return chromeLocal.get(null).then(storage => {
|
||||
const styles = [];
|
||||
for (const key in storage) {
|
||||
if (key.startsWith(STYLE_KEY_PREFIX) &&
|
||||
Number(key.substr(STYLE_KEY_PREFIX.length))) {
|
||||
styles.push(storage[key]);
|
||||
}
|
||||
}
|
||||
return {target: {result: styles}};
|
||||
});
|
||||
}
|
||||
return Promise.reject();
|
||||
}
|
||||
})();
|
|
@ -29,139 +29,6 @@ var cachedStyles = {
|
|||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var dbExec = dbExecIndexedDB;
|
||||
dbExec.initialized = false;
|
||||
|
||||
// we use chrome.storage.local fallback if IndexedDB doesn't save data,
|
||||
// which, once detected on the first run, is remembered in chrome.storage.local
|
||||
// for reliablility and in localStorage for fast synchronous access
|
||||
// (FF may block localStorage depending on its privacy options)
|
||||
do {
|
||||
const done = () => {
|
||||
cachedStyles.mutex.inProgress = false;
|
||||
getStyles().then(() => {
|
||||
dbExec.initialized = true;
|
||||
window.dispatchEvent(new Event('storageReady'));
|
||||
});
|
||||
};
|
||||
const fallback = () => {
|
||||
dbExec = dbExecChromeStorage;
|
||||
chromeLocal.set({dbInChromeStorage: true});
|
||||
localStorage.dbInChromeStorage = 'true';
|
||||
ignoreChromeError();
|
||||
done();
|
||||
};
|
||||
const fallbackSet = localStorage.dbInChromeStorage;
|
||||
if (fallbackSet === 'true' || !tryCatch(() => indexedDB)) {
|
||||
fallback();
|
||||
break;
|
||||
} else if (fallbackSet === 'false') {
|
||||
done();
|
||||
break;
|
||||
}
|
||||
chromeLocal.get('dbInChromeStorage')
|
||||
.then(data =>
|
||||
data && data.dbInChromeStorage && Promise.reject())
|
||||
.then(() =>
|
||||
tryCatch(dbExecIndexedDB, 'getAllKeys', IDBKeyRange.lowerBound(1), 1) ||
|
||||
Promise.reject())
|
||||
.then(({target}) => (
|
||||
(target.result || [])[0] ?
|
||||
Promise.reject('ok') :
|
||||
dbExecIndexedDB('put', {id: -1})))
|
||||
.then(() =>
|
||||
dbExecIndexedDB('get', -1))
|
||||
.then(({target}) => (
|
||||
(target.result || {}).id === -1 ?
|
||||
dbExecIndexedDB('delete', -1) :
|
||||
Promise.reject()))
|
||||
.then(() =>
|
||||
Promise.reject('ok'))
|
||||
.catch(result => {
|
||||
if (result === 'ok') {
|
||||
chromeLocal.set({dbInChromeStorage: false});
|
||||
localStorage.dbInChromeStorage = 'false';
|
||||
done();
|
||||
} else {
|
||||
fallback();
|
||||
}
|
||||
});
|
||||
} while (0);
|
||||
|
||||
|
||||
function dbExecIndexedDB(method, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Object.assign(indexedDB.open('stylish', 2), {
|
||||
onsuccess(event) {
|
||||
const database = event.target.result;
|
||||
if (!method) {
|
||||
resolve(database);
|
||||
} else {
|
||||
const transaction = database.transaction(['styles'], 'readwrite');
|
||||
const store = transaction.objectStore('styles');
|
||||
Object.assign(store[method](...args), {
|
||||
onsuccess: event => resolve(event, store, transaction, database),
|
||||
onerror: reject,
|
||||
});
|
||||
}
|
||||
},
|
||||
onerror(event) {
|
||||
console.warn(event.target.error || event.target.errorCode);
|
||||
reject(event);
|
||||
},
|
||||
onupgradeneeded(event) {
|
||||
if (event.oldVersion === 0) {
|
||||
event.target.result.createObjectStore('styles', {
|
||||
keyPath: 'id',
|
||||
autoIncrement: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function dbExecChromeStorage(method, data) {
|
||||
const STYLE_KEY_PREFIX = 'style-';
|
||||
switch (method) {
|
||||
case 'get':
|
||||
return chromeLocal.getValue(STYLE_KEY_PREFIX + data)
|
||||
.then(result => ({target: {result}}));
|
||||
|
||||
case 'put':
|
||||
if (!data.id) {
|
||||
return getStyles().then(() => {
|
||||
data.id = 1;
|
||||
for (const style of cachedStyles.list) {
|
||||
data.id = Math.max(data.id, style.id + 1);
|
||||
}
|
||||
return dbExecChromeStorage('put', data);
|
||||
});
|
||||
}
|
||||
return chromeLocal.setValue(STYLE_KEY_PREFIX + data.id, data)
|
||||
.then(() => (chrome.runtime.lastError ? Promise.reject() : data.id));
|
||||
|
||||
case 'delete':
|
||||
return chromeLocal.remove(STYLE_KEY_PREFIX + data);
|
||||
|
||||
case 'getAll':
|
||||
return chromeLocal.get(null).then(storage => {
|
||||
const styles = [];
|
||||
for (const key in storage) {
|
||||
if (key.startsWith(STYLE_KEY_PREFIX) &&
|
||||
Number(key.substr(STYLE_KEY_PREFIX.length))) {
|
||||
styles.push(storage[key]);
|
||||
}
|
||||
}
|
||||
return {target: {result: styles}};
|
||||
});
|
||||
}
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
|
||||
function getStyles(options) {
|
||||
if (cachedStyles.list) {
|
||||
return Promise.resolve(filterStyles(options));
|
||||
|
@ -172,24 +39,6 @@ function getStyles(options) {
|
|||
});
|
||||
}
|
||||
cachedStyles.mutex.inProgress = true;
|
||||
|
||||
return dbExec('getAll').then(event => {
|
||||
cachedStyles.list = event.target.result || [];
|
||||
cachedStyles.byId.clear();
|
||||
for (const style of cachedStyles.list) {
|
||||
cachedStyles.byId.set(style.id, style);
|
||||
if (!style.name) {
|
||||
style.name = 'ID: ' + style.id;
|
||||
}
|
||||
}
|
||||
|
||||
cachedStyles.mutex.inProgress = false;
|
||||
for (const {options, resolve} of cachedStyles.mutex.onDone) {
|
||||
resolve(filterStyles(options));
|
||||
}
|
||||
cachedStyles.mutex.onDone = [];
|
||||
return filterStyles(options);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -202,29 +51,10 @@ function filterStyles({
|
|||
omitCode,
|
||||
strictRegexp = true, // used by the popup to detect bad regexps
|
||||
} = {}) {
|
||||
if (id) id = Number(id);
|
||||
if (asHash) enabled = true;
|
||||
|
||||
if (
|
||||
enabled === null &&
|
||||
id === null &&
|
||||
matchUrl === null &&
|
||||
md5Url === null &&
|
||||
asHash !== true
|
||||
) {
|
||||
return cachedStyles.list;
|
||||
}
|
||||
|
||||
if (matchUrl && !URLS.supported(matchUrl)) {
|
||||
return asHash ? {length: 0} : [];
|
||||
}
|
||||
|
||||
const blankHash = asHash && {
|
||||
length: 0,
|
||||
disableAll: prefs.get('disableAll'),
|
||||
exposeIframes: prefs.get('exposeIframes'),
|
||||
};
|
||||
|
||||
// make sure to use the same order in updateFiltersCache()
|
||||
const cacheKey =
|
||||
enabled + '\t' +
|
||||
|
@ -287,14 +117,6 @@ function filterStylesInternal({
|
|||
}
|
||||
}
|
||||
|
||||
const styles = id === null
|
||||
? cachedStyles.list
|
||||
: [cachedStyles.byId.get(id)];
|
||||
if (!styles[0]) {
|
||||
// may happen when users [accidentally] reopen an old URL
|
||||
// of edit.html with a non-existent style id parameter
|
||||
return asHash ? blankHash : [];
|
||||
}
|
||||
const filtered = asHash ? {length: 0} : [];
|
||||
const needSections = asHash || matchUrl !== null;
|
||||
const matchUrlBase = matchUrl && matchUrl.includes('#') && matchUrl.split('#', 1)[0];
|
||||
|
@ -342,97 +164,6 @@ function filterStylesInternal({
|
|||
|
||||
|
||||
function saveStyle(style) {
|
||||
const id = Number(style.id) || null;
|
||||
const reason = style.reason;
|
||||
const notify = style.notify !== false;
|
||||
delete style.method;
|
||||
delete style.reason;
|
||||
delete style.notify;
|
||||
if (!style.name) {
|
||||
delete style.name;
|
||||
}
|
||||
let existed;
|
||||
let codeIsUpdated;
|
||||
return maybeCalcDigest()
|
||||
.then(maybeImportFix)
|
||||
.then(decide);
|
||||
|
||||
function maybeCalcDigest() {
|
||||
if (['install', 'update', 'update-digest'].includes(reason)) {
|
||||
return calcStyleDigest(style).then(digest => {
|
||||
style.originalDigest = digest;
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function maybeImportFix() {
|
||||
if (reason === 'import') {
|
||||
style.originalDigest = style.originalDigest || style.styleDigest; // TODO: remove in the future
|
||||
delete style.styleDigest; // TODO: remove in the future
|
||||
if (typeof style.originalDigest !== 'string' || style.originalDigest.length !== 40) {
|
||||
delete style.originalDigest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function decide() {
|
||||
if (id !== null) {
|
||||
// Update or create
|
||||
style.id = id;
|
||||
return dbExec('get', id).then((event, store) => {
|
||||
const oldStyle = event.target.result;
|
||||
existed = Boolean(oldStyle);
|
||||
if (reason === 'update-digest' && oldStyle.originalDigest === style.originalDigest) {
|
||||
return style;
|
||||
}
|
||||
codeIsUpdated = !existed
|
||||
|| 'sections' in style && !styleSectionsEqual(style, oldStyle)
|
||||
|| reason === 'exclusionsUpdated';
|
||||
style = Object.assign({installDate: Date.now()}, oldStyle, style);
|
||||
return write(style, store);
|
||||
});
|
||||
} else {
|
||||
// Create
|
||||
delete style.id;
|
||||
style = Object.assign({
|
||||
// Set optional things if they're undefined
|
||||
enabled: true,
|
||||
updateUrl: null,
|
||||
md5Url: null,
|
||||
url: null,
|
||||
originalMd5: null,
|
||||
installDate: Date.now(),
|
||||
exclusions: {}
|
||||
}, style);
|
||||
return write(style);
|
||||
}
|
||||
}
|
||||
|
||||
function write(style, store) {
|
||||
style.sections = normalizeStyleSections(style);
|
||||
if (store) {
|
||||
return new Promise(resolve => {
|
||||
store.put(style).onsuccess = event => resolve(done(event));
|
||||
});
|
||||
} else {
|
||||
return dbExec('put', style).then(done);
|
||||
}
|
||||
}
|
||||
|
||||
function done(event) {
|
||||
if (reason === 'update-digest') {
|
||||
return style;
|
||||
}
|
||||
style.id = style.id || event.target.result;
|
||||
invalidateCache(existed ? {updated: style} : {added: style});
|
||||
if (notify) {
|
||||
const method = reason === 'exclusionsUpdated' ? reason :
|
||||
existed ? 'styleUpdated' : 'styleAdded';
|
||||
notifyAllTabs({method, style, codeIsUpdated, reason});
|
||||
}
|
||||
return style;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
212
background/style-manager.js
Normal file
212
background/style-manager.js
Normal file
|
@ -0,0 +1,212 @@
|
|||
const styleManager = (() => {
|
||||
const preparing = prepare();
|
||||
const styles = new Map;
|
||||
const cachedStyleForUrl = createCache();
|
||||
const compiledRe = createCache();
|
||||
const compiledExclusion = createCache();
|
||||
const BAD_MATCHER = {test: () => false};
|
||||
|
||||
// FIXME: do we have to prepare `styles` map for all methods?
|
||||
return ensurePrepared({
|
||||
getSectionsForURL,
|
||||
installStyle,
|
||||
deleteStyle,
|
||||
setStyleExclusions,
|
||||
editSave
|
||||
// TODO: get all styles API?
|
||||
// TODO: get style by ID?
|
||||
});
|
||||
|
||||
function editSave() {}
|
||||
|
||||
function setStyleExclusions() {}
|
||||
|
||||
function ensurePrepared(methods) {
|
||||
for (const [name, fn] in Object.entries(methods)) {
|
||||
methods[name] = (...args) =>
|
||||
preparing.then(() => fn(...args));
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
|
||||
function deleteStyle(id) {
|
||||
return db.exec('delete', id)
|
||||
.then(() => {
|
||||
// FIXME: do we really need to clear the entire cache?
|
||||
cachedStyleForUrl.clear();
|
||||
notifyAllTabs({method: 'styleDeleted', id});
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
function installStyle(style) {
|
||||
return calcStyleDigest(style)
|
||||
.then(digest => {
|
||||
style.originalDigest = digest;
|
||||
return saveStyle(style);
|
||||
})
|
||||
.then(style => {
|
||||
// FIXME: do we really need to clear the entire cache?
|
||||
cachedStyleForUrl.clear();
|
||||
// FIXME: invalid signature
|
||||
notifyAllTabs();
|
||||
});
|
||||
}
|
||||
|
||||
function importStyle(style) {
|
||||
// FIXME: move this to importer
|
||||
// style.originalDigest = style.originalDigest || style.styleDigest; // TODO: remove in the future
|
||||
// delete style.styleDigest; // TODO: remove in the future
|
||||
// if (typeof style.originalDigest !== 'string' || style.originalDigest.length !== 40) {
|
||||
// delete style.originalDigest;
|
||||
// }
|
||||
}
|
||||
|
||||
function saveStyle(style) {
|
||||
return (style.id == null ? getNewStyle() : getOldStyle())
|
||||
.then(oldStyle => {
|
||||
// FIXME: update installDate?
|
||||
style = Object.assign(oldStyle, style);
|
||||
style.sections = normalizeStyleSections(style);
|
||||
return dbExec('put', style);
|
||||
})
|
||||
.then(event => {
|
||||
if (style.id == null) {
|
||||
style.id = event.target.result;
|
||||
}
|
||||
return style;
|
||||
});
|
||||
|
||||
function getOldStyle() {
|
||||
return db.exec('get', style.id)
|
||||
.then((event, store) => {
|
||||
if (!event.target.result) {
|
||||
throw new Error(`Unknown style id: ${style.id}`);
|
||||
}
|
||||
return event.target.result;
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME: don't overwrite style name when the name is empty
|
||||
|
||||
function getNewStyle() {
|
||||
return Promise.resolve({
|
||||
enabled: true,
|
||||
updateUrl: null,
|
||||
md5Url: null,
|
||||
url: null,
|
||||
originalMd5: null,
|
||||
installDate: Date.now()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getSectionsForURL(url) {
|
||||
// if (!URLS.supported(url) || prefs.get('disableAll')) {
|
||||
// return [];
|
||||
// }
|
||||
let result = cachedStyleForUrl.get(url);
|
||||
if (!result) {
|
||||
result = [];
|
||||
for (const style of styles) {
|
||||
if (!urlMatchStyle(url, style)) {
|
||||
continue;
|
||||
}
|
||||
const item = {
|
||||
id: style.id,
|
||||
code: ''
|
||||
};
|
||||
for (const section of style.sections) {
|
||||
if (urlMatchSection(url, section)) {
|
||||
item.code += section.code;
|
||||
}
|
||||
}
|
||||
if (item.code) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function prepare() {
|
||||
return db.exec('getAll').then(event => {
|
||||
const styleList = event.target.result || [];
|
||||
for (const style of styleList) {
|
||||
styles.set(style.id, style);
|
||||
if (!style.name) {
|
||||
style.name = 'ID: ' + style.id;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function urlMatchStyle(url, style) {
|
||||
if (style.exclusions && style.exclusions.some(e => compileExclusion(e).test(url)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function urlMatchSection(url, section) {
|
||||
// FIXME: match sub domains?
|
||||
if (section.domains && section.domains.includes(getDomain(url))) {
|
||||
return true;
|
||||
}
|
||||
if (section.urlPrefixes && section.urlPrefixes.some(p => url.startsWith(p))) {
|
||||
return true;
|
||||
}
|
||||
if (section.urls && section.urls.includes(getUrlNoHash(url))) {
|
||||
return true;
|
||||
}
|
||||
if (section.regexps && section.regexps.some(r => compileRe(r).test(url))) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function compileRe(text) {
|
||||
let re = compiledRe.get(text);
|
||||
if (!re) {
|
||||
// FIXME: it should be `$({text})$` but we don't use the standard for compatibility
|
||||
re = tryRegExp(`^${text}$`);
|
||||
if (!re) {
|
||||
re = BAD_MATCHER;
|
||||
}
|
||||
compiledRe.set(text, re);
|
||||
}
|
||||
return re;
|
||||
}
|
||||
|
||||
function compileExclusion(text) {
|
||||
let re = compiledExclusion.get(text);
|
||||
if (!re) {
|
||||
re = tryRegExp(buildGlob(text));
|
||||
if (!re) {
|
||||
re = BAD_MATCHER;
|
||||
}
|
||||
compiledExclusion.set(text, re);
|
||||
}
|
||||
return re;
|
||||
}
|
||||
|
||||
function buildGlob(text) {
|
||||
const prefix = text[0] === '^' ? '' : '\\b';
|
||||
const suffix = text[text.length - 1] === '$' ? '' : '\\b';
|
||||
return `${prefix}${escape(text)}${suffix}`;
|
||||
|
||||
function escape(text) {
|
||||
// FIXME: using .* everywhere is slow
|
||||
return text.replace(/[.*]/g, m => m === '.' ? '\\.' : '.*');
|
||||
}
|
||||
}
|
||||
|
||||
function getDomain(url) {
|
||||
// FIXME: use a naive regexp
|
||||
return url.match(/\w+:\/\//);
|
||||
}
|
||||
|
||||
function getUrlNoHash(url) {
|
||||
return url.split('#')[0];
|
||||
}
|
||||
})();
|
48
js/cache.js
Normal file
48
js/cache.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
function createCache(size = 1000) {
|
||||
const map = new Map;
|
||||
const buffer = Array(size);
|
||||
let index = 0;
|
||||
let lastIndex = 0;
|
||||
return {
|
||||
get,
|
||||
set,
|
||||
delete: delete_,
|
||||
clear,
|
||||
has: id => map.has(id),
|
||||
get size: () => map.size
|
||||
};
|
||||
|
||||
function get(id) {
|
||||
const item = map.get(id);
|
||||
return item && item.data;
|
||||
}
|
||||
|
||||
function set(id, data) {
|
||||
if (map.size === size) {
|
||||
// full
|
||||
map.delete(buffer[lastIndex].id);
|
||||
lastIndex = (lastIndex + 1) % size;
|
||||
}
|
||||
const item = {id, data, index};
|
||||
map.set(id, item);
|
||||
buffer[index] = item;
|
||||
index = (index + 1) % size;
|
||||
}
|
||||
|
||||
function delete_(id) {
|
||||
const item = map.get(id);
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
map.delete(item.id);
|
||||
const lastItem = buffer[lastIndex];
|
||||
lastItem.index = item.index;
|
||||
buffer[item.index] = lastItem;
|
||||
lastIndex = (lastIndex + 1) % size;
|
||||
}
|
||||
|
||||
function clear() {
|
||||
map.clear();
|
||||
index = lastIndex = 0;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user