enhance(frontend): preferenceのタブ間同期にBroadcast Channelを使用するように (#16819)
* enhance(frontend): preferenceのタブ間同期にBroadcast Channelを使用するように * fix * refactor: EventEmitterをextendする形に変更
This commit is contained in:
parent
25afb5d279
commit
2ee04860fb
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<Default, T = Default extends (...args: any) =>
|
|||
|
||||
export type PreferencesDefinition = Record<string, PreferencesDefinitionRecord<any>>;
|
||||
|
||||
type PreferencesManagerEvents = {
|
||||
'committed': <K extends keyof PREF>(ctx: {
|
||||
key: K;
|
||||
value: ValueOf<K>;
|
||||
oldValue: ValueOf<K>;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
export function definePreferences<T extends Record<string, unknown>>(x: {
|
||||
[K in keyof T]: PreferencesDefinitionRecord<T[K]>
|
||||
}): {
|
||||
|
|
@ -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<PreferencesManagerEvents> {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue