scrapbox-link-database
from /programming-notes/scrapbox-link-database
dependencies
idb
scrapbox-api-helper
date-fns.min.js
code:script.js
import { openDB } from '../idb/with-async-ittr.js';
import {
fetchPages,
fetchLinks,
getProjectUpdated
} from '../scrapbox-api-helper/scrapboxAPI.js';
import {sub, getUnixTime} from '../date-fns.min.js/script.js';
export class CacheStorage {
constructor(expired = 3600) {
this._expired = expired;
// databaseを初期化する
this._initialized = (async () => {
this._db = await openDB(CacheStorage.name, CacheStorage.version, {
upgrade(db) {
// Object Storeをすべて消す
...db.objectStoreNames.forEach(storeName =>
db.deleteObjectStore(storeName)
);
const linkStore = db.createObjectStore(CacheStorage.linkStoreName, {
keyPath: 'project',
});
linkStore.createIndex('fetched', 'fetched');
const iconStore = db.createObjectStore(CacheStorage.iconStoreName, {
keyPath: 'project',
});
iconStore.createIndex('fetched', 'fetched');
},
});
})();
}
static name = 'UserScript';
static version = 4;
static linkStoreName = 'cache-links';
static iconStoreName = 'cache-icons';
async get(projects, {icon = false, reload = false} = {}) {
// 一旦更新処理をしてから取得する
await this.update(projects, {icon, reload});
// databaseから取ってくる
const result = [];
await this._transaction(
store => Promise.all(
projects.map(async project => {
const {titles, pages} = await store.get(project);
result.push(icon ? {project, titles} : {project, pages});
})
),
{icon, mode: 'readonly'},
);
return result;
}
async clear(projects, {icon = false} = {}) {
await this.waitForInitialization();
await this._transaction(
store => Promise.all(projects.map(project => tx.store.delete(project))),
{icon, mode: 'readwrite'}
);
}
// databaseの構築が終わるまで待機する
async waitForInitialization() {
await this._initialized;
}
async update(projects, {icon = false, reload = false} = {}) {
await this.waitForInitialization();
const now = new Date();
// 更新を取得するproject listを作る
// projectsに含まれるもののみ対象とする
const projectUpdateds = [];
await this._transaction(async store => {
// cacheのないprojectを先に入れておく
const availableProjects = await store.getAllKeys();
projectUpdateds.push(...projects.flatMap(project => {
return availableProjects.includes(project) ? [] : {project, prevFetched: 0};
}));
const index = store.index('fetched');
const iterator = reload ?
index.iterate() :
// fetched + this._expired < 現在時刻であるもののみ更新対象とする
index.iterate(
IDBKeyRange.upperBound(sub(now, {seconds: this._expired}), true)
);
for await (const cursor of iterator) {
const updateData = cursor.value;
if (!projects.includes(updateData.project)) continue;
projectUpdateds.push({
project: updateData.project,
prevFetched: getUnixTime(updateData.fetched), // 秒単位のtimestampに変換しておく
});
}
//await tx.done;
}, {icon, mode: 'readonly'});
// 先にfetchedを更新しておく
await this._transaction(async store => {
await Promise.all(projectUpdateds.flatMap(async ({project, prevFetched}) => {
if (prevFetched === 0) return [];
const {fetched, ...rest} = await store.get(project);
return await store.put({fetched: now, ...rest});
}));
}, {icon, mode: 'readwrite'});
// 更新する必要のあるproject listを作る
const targetProjects = (await Promise.all(
projectUpdateds.map(async ({project, prevFetched}) => {
const fetched = await getProjectUpdated(project);
return fetched > prevFetched ? project : [];
})
)).flat();
// networkからdataを取得する
const data = await Promise.all(targetProjects.map(async project => icon ?
{project, titles: await fetchAllIcons(project)} :
{project, pages: await fetchAllLinks(project)}
));
// transactionを開いてdataを格納する
await this._transaction(async store => {
const keys = await store.getAllKeys();
await Promise.all(
data.map(({project, titles, pages}) =>
keys.includes(project) ?
store.put(icon ? {project, titles, fetched: now} : {project, pages, fetched: now}) :
store.add(icon ? {project, titles, fetched: now} : {project, pages, fetched: now})
)
)
}, {icon, mode: 'readwrite'});
// 更新したproject listを返す
return data.map(({project}) => project);
}
async _transaction(callback, {icon, mode}) {
const storeName = icon ? CacheStorage.iconStoreName : CacheStorage.linkStoreName;
const tx = this._db.transaction(storeName, mode);
const store = tx.objectStore(storeName);
await callback(store);
await tx.done;
}
}
async function fetchAllIcons(project) {
const pages = await fetchPages({project});
return pages.flatMap(({title, image}) => image ? title : []);
}
async function fetchAllLinks(project) {
const pages = await fetchLinks({project});
return pages.map(({title, links}) => {return {title, links};});
}
#2021-04-08 17:39:56