diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts index b8d5e66bf6..7b03823407 100644 --- a/packages/frontend/src/preferences.ts +++ b/packages/frontend/src/preferences.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { BroadcastChannel } from 'broadcast-channel'; import type { StorageProvider } from '@/preferences/manager.js'; import { cloudBackup } from '@/preferences/utility.js'; import { miLocalStorage } from '@/local-storage.js'; @@ -12,6 +13,7 @@ import { $i } from '@/i.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { TAB_ID } from '@/tab-id.js'; +// クラウド同期用グループ名 const syncGroup = 'default'; const io: StorageProvider = { @@ -26,7 +28,6 @@ const io: StorageProvider = { save: (ctx) => { miLocalStorage.setItem('preferences', JSON.stringify(ctx.profile)); - miLocalStorage.setItem('latestPreferencesUpdate', `${TAB_ID}/${Date.now()}`); }, cloudGet: async (ctx) => { @@ -99,33 +100,47 @@ const io: StorageProvider = { export const prefer = new PreferencesManager(io, $i); -let latestSyncedAt = Date.now(); +//#region タブ間同期 +let latestPreferencesUpdate: { + tabId: string; + timestamp: number; +} | null = null; -function syncBetweenTabs() { - const latest = miLocalStorage.getItem('latestPreferencesUpdate'); - if (latest == null) return; +const preferencesChannel = new BroadcastChannel<{ + type: 'preferencesUpdate'; + tabId: string; + timestamp: number; +}>('preferences'); - const latestTab = latest.split('/')[0]; - const latestAt = parseInt(latest.split('/')[1]); - - if (latestTab === TAB_ID) return; - if (latestAt <= latestSyncedAt) return; - - prefer.reloadProfile(); - - latestSyncedAt = Date.now(); - - if (_DEV_) console.log('prefer:synced'); -} - -window.setInterval(syncBetweenTabs, 5000); - -window.document.addEventListener('visibilitychange', () => { - if (window.document.visibilityState === 'visible') { - syncBetweenTabs(); - } +prefer.on('committed', () => { + latestPreferencesUpdate = { + tabId: TAB_ID, + timestamp: Date.now(), + }; + preferencesChannel.postMessage({ + type: 'preferencesUpdate', + tabId: TAB_ID, + timestamp: latestPreferencesUpdate.timestamp, + }); }); +preferencesChannel.addEventListener('message', (msg) => { + if (msg.type === 'preferencesUpdate') { + if (msg.tabId === TAB_ID) return; + if (latestPreferencesUpdate != null) { + if (msg.timestamp <= latestPreferencesUpdate.timestamp) return; + } + prefer.reloadProfile(); + if (_DEV_) console.log('prefer:received update from other tab'); + latestPreferencesUpdate = { + tabId: msg.tabId, + timestamp: msg.timestamp, + }; + } +}); +//#endregion + +//#region 定期クラウドバックアップ let latestBackupAt = 0; window.setInterval(() => { @@ -138,6 +153,7 @@ window.setInterval(() => { latestBackupAt = Date.now(); }); }, 1000 * 60 * 3); +//#endregion if (_DEV_) { (window as any).prefer = prefer; diff --git a/packages/frontend/src/preferences/manager.ts b/packages/frontend/src/preferences/manager.ts index b6d3d55a5f..5949ee71eb 100644 --- a/packages/frontend/src/preferences/manager.ts +++ b/packages/frontend/src/preferences/manager.ts @@ -4,6 +4,7 @@ */ import { computed, onUnmounted, ref, watch } from 'vue'; +import { EventEmitter } from 'eventemitter3'; import { host, version } from '@@/js/config.js'; import { PREF_DEF } from './def.js'; import type { Ref, WritableComputedRef } from 'vue'; @@ -100,6 +101,14 @@ type PreferencesDefinitionRecord export type PreferencesDefinition = Record>; +type PreferencesManagerEvents = { + 'committed': (ctx: { + key: K; + value: ValueOf; + oldValue: ValueOf; + }) => void; +}; + export function definePreferences>(x: { [K in keyof T]: PreferencesDefinitionRecord }): { @@ -180,7 +189,7 @@ function normalizePreferences(preferences: PossiblyNonNormalizedPreferencesProfi // TODO: PreferencesManagerForGuest のような非ログイン専用のクラスを分離すればthis.currentAccountのnullチェックやaccountがnullであるスコープのレコード挿入などが不要になり綺麗になるかもしれない // と思ったけど操作アカウントが存在しない場合も考慮する現在の設計の方が汎用的かつ堅牢かもしれない // NOTE: accountDependentな設定は初期状態であってもアカウントごとのスコープでレコードを作成しておかないと、サーバー同期する際に正しく動作しなくなる -export class PreferencesManager { +export class PreferencesManager extends EventEmitter { private io: StorageProvider; private currentAccount: { id: string } | null; public profile: PreferencesProfile; @@ -201,6 +210,8 @@ export class PreferencesManager { }; constructor(io: StorageProvider, currentAccount: { id: string } | null) { + super(); + this.io = io; this.currentAccount = currentAccount; @@ -246,6 +257,12 @@ export class PreferencesManager { this.rewriteRawState(key, v); + this.emit('committed', { + key, + value: v, + oldValue: this.s[key], + }); + const record = this.getMatchedRecordOf(key); if (parseScope(record[0]).account == null && isAccountDependentKey(key) && currentAccount != null) { diff --git a/packages/frontend/src/tab-id.ts b/packages/frontend/src/tab-id.ts index 6525763582..db8a5b147f 100644 --- a/packages/frontend/src/tab-id.ts +++ b/packages/frontend/src/tab-id.ts @@ -5,7 +5,5 @@ import { genId } from '@/utility/id.js'; -// HMR有効時にバグか知らんけど複数回実行されるのでその対策 -export const TAB_ID = window.sessionStorage.getItem('TAB_ID') ?? genId(); -window.sessionStorage.setItem('TAB_ID', TAB_ID); +export const TAB_ID = genId(); if (_DEV_) console.log('TAB_ID', TAB_ID);