/// /// /// import { createDebug } from "./debug.ts"; import { downloadLinks } from "./remote.ts"; import { fetchProjectStatus, ProjectStatus } from "./status.ts"; import { open, Source, SourceStatus, write } from "./db.ts"; import { emitChange } from "./subscribe.ts"; export { subscribe } from "./subscribe.ts"; export type { LinkEvent, Listener } from "./subscribe.ts"; export { setDebugMode } from "./debug.ts"; export type { Source }; export * from "./link.ts"; const logger = createDebug("scrapbox-storage:mod.ts"); /** 手動で更新を確認する。更新があればDBに反映する。 * * @param projects 更新を確認したい補完ソースのproject names * @param updateInterval 最後に更新を確認してからどのくらい経過したデータを更新すべきか (単位は秒) * @return 更新があったprojectのリンクデータ */ export const check = async ( projects: readonly string[], updateInterval: number, ): Promise => { const db = await open(); const projectsMaybeNeededUpgrade: ProjectStatus[] = []; const projectStatus: SourceStatus[] = []; try { // 更新する必要のあるデータを探し、更新中フラグを立てる { logger.debug("check updates of links..."); const tx = db.transaction("status", "readwrite"); await Promise.all(projects.map(async (project) => { const status = await tx.store.get(project); if (status?.isValid === false) return; const checked = status?.checked ?? 0; const now = new Date().getTime() / 1000; // 更新されたばかりのデータは飛ばす if (checked + updateInterval > now) return; // 更新中にタブが強制終了した可能性を考慮して、更新中フラグが経った時刻より10分経過していたらデータ更新対象に含める if (status?.updating && checked + 600 > now) return; const tempStatus: ProjectStatus = { project, id: status?.id, isValid: true, checked, updated: status?.updated ?? 0, updating: true, }; projectsMaybeNeededUpgrade.push(tempStatus); tx.store.put(tempStatus); })); await tx.done; logger.debug( `checked. ${projectsMaybeNeededUpgrade.length} projects maybe need upgrade.`, ); } // 更新するprojectsがなければ何もしない if (projectsMaybeNeededUpgrade.length === 0) return []; /** 更新されたprojects */ const updatedProjects: string[] = []; const result: Source[] = []; // 一つづつ更新する for await (const res of fetchProjectStatus(projectsMaybeNeededUpgrade)) { // project dataを取得できないときは、無効なprojectに分類しておく if (!res.ok) { projectStatus.push({ project: res.value.project, isValid: false }); switch (res.value.name) { case "NotFoundError": logger.warn(`"${res.value.project}" is not found.`); continue; case "NotMemberError": logger.warn(`You are not a member of "${res.value.project}".`); continue; case "NotLoggedInError": logger.warn( `You are not a member of "${res.value.project}" or You are not logged in yet.`, ); continue; } } // projectの最終更新日時から、updateの要不要を調べる if (res.value.updated < res.value.checked) { logger.debug(`no updates in "${res.value.name}"`); } else { const res2 = await downloadLinks(res.value.name); if (!res2.ok) { throw Error(`${res2.value.name} ${res2.value.message}`); } // リンクデータを更新する const data: Source = { project: res.value.name, links: res2.value, }; result.push(data); logger.time(`write data of "${res.value.name}"`); await write(data); updatedProjects.push(res.value.name); logger.timeEnd(`write data of "${res.value.name}"`); } projectStatus.push({ project: res.value.name, isValid: true, id: res.value.id, checked: new Date().getTime() / 1000, updated: res.value.updated, updating: false, }); } // 更新通知を出す if (updatedProjects.length > 0) emitChange(updatedProjects); return result; } finally { // エラーが起きた場合も含め、フラグをもとに戻しておく const tx = db.transaction("status", "readwrite"); const store = tx.store; await Promise.all( projectStatus.map((status) => store.put(status)), ); await tx.done; } }; /** リンクデータをDBから取得する。データの更新は行わない。 * * @param projects 取得したい補完ソースのproject nameのリスト * @return リンクデータのリスト projectsと同じ順番で並んでいる */ export const load = async ( projects: readonly string[], ): Promise => { const list: Source[] = []; const start = new Date(); { const tx = (await open()).transaction("links", "readonly"); await Promise.all(projects.map(async (project) => { const source = await tx.store.get(project); list.push(source ?? { project, links: [] }); })); await tx.done; } const ms = new Date().getTime() - start.getTime(); logger.debug(`Read links of ${projects.length} projects in ${ms}ms`); return list; };