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