diff --git a/packages/client/package.json b/packages/client/package.json index a8ca3a2f0..8860662cd 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -27,7 +27,6 @@ "compare-versions": "5.0.1", "cropperjs": "2.0.0-beta", "date-fns": "2.29.3", - "deepcopy": "2.1.0", "escape-regexp": "0.0.1", "eventemitter3": "4.0.7", "idb-keyval": "6.2.0", diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts index 5af113606..f9e08d79b 100644 --- a/packages/client/src/init.ts +++ b/packages/client/src/init.ts @@ -38,7 +38,6 @@ import { reloadChannel } from '@/scripts/unison-reload'; import { reactionPicker } from '@/scripts/reaction-picker'; import { getUrlWithoutLoginId } from '@/scripts/login-id'; import { getAccountFromId } from '@/scripts/get-account-from-id'; -import { deckStore } from './ui/deck/deck-store'; (async () => { console.info(`Misskey v${version}`); @@ -74,8 +73,6 @@ import { deckStore } from './ui/deck/deck-store'; }); } - await defaultStore.ready; - // タッチデバイスでCSSの:hoverを機能させる document.addEventListener('touchend', () => {}, { passive: true }); @@ -191,8 +188,6 @@ import { deckStore } from './ui/deck/deck-store'; splash.remove(); }); - await deckStore.ready; - // https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 // なぜかinit.tsの内容が2回実行されることがあるため、mountするdivを1つに制限する const rootEl = (() => { diff --git a/packages/client/src/pizzax.ts b/packages/client/src/pizzax.ts index c98de098a..642e1f4f7 100644 --- a/packages/client/src/pizzax.ts +++ b/packages/client/src/pizzax.ts @@ -1,12 +1,8 @@ // PIZZAX --- A lightweight store import { onUnmounted, Ref, ref, watch } from 'vue'; -import { BroadcastChannel } from 'broadcast-channel'; -import deepcopy from 'deepcopy'; import { $i } from './account'; import { api } from './os'; -import { get, set } from './scripts/idb-proxy'; -import { defaultStore } from './store'; import { stream } from './stream'; type StateDef = Record; -type State = { [K in keyof T]: T[K]['default']; }; -type ReactiveState = { [K in keyof T]: Ref; }; - type ArrayElement = A extends readonly (infer T)[] ? T : never; -type PizzaxChannelMessage = { - where: 'device' | 'deviceAccount'; - key: keyof T; - value: T[keyof T]['default']; - userId?: string; -}; - const connection = $i && stream.useChannel('main'); export class Storage { - public readonly ready: Promise; - public readonly loaded: Promise; - public readonly key: string; - public readonly deviceStateKeyName: `pizzax::${this['key']}`; - public readonly deviceAccountStateKeyName: `pizzax::${this['key']}::${string}` | ''; - public readonly registryCacheKeyName: `pizzax::${this['key']}::cache::${string}` | ''; + public readonly keyForLocalStorage: string; public readonly def: T; // TODO: これが実装されたらreadonlyにしたい: https://github.com/microsoft/TypeScript/issues/37487 - public readonly state: State; - public readonly reactiveState: ReactiveState; - - private pizzaxChannel: BroadcastChannel>; - - // 簡易的にキューイングして占有ロックとする - private currentIdbJob: Promise = Promise.resolve(); - private addIdbSetJob(job: () => Promise) { - const promise = this.currentIdbJob.then(job, e => { - console.error('Pizzax failed to save data to idb!', e); - return job(); - }); - this.currentIdbJob = promise; - return promise; - } + public readonly state: { [K in keyof T]: T[K]['default'] }; + public readonly reactiveState: { [K in keyof T]: Ref }; constructor(key: string, def: T) { this.key = key; - this.deviceStateKeyName = `pizzax::${key}`; - this.deviceAccountStateKeyName = $i ? `pizzax::${key}::${$i.id}` : ''; - this.registryCacheKeyName = $i ? `pizzax::${key}::cache::${$i.id}` : ''; + this.keyForLocalStorage = 'pizzax::' + key; this.def = def; - this.pizzaxChannel = new BroadcastChannel(`pizzax::${key}`); + // TODO: indexedDBにする + const deviceState = JSON.parse(localStorage.getItem(this.keyForLocalStorage) || '{}'); + const deviceAccountState = $i ? JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::' + $i.id) || '{}') : {}; + const registryCache = $i ? JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}') : {}; - this.state = {} as State; - this.reactiveState = {} as ReactiveState; - - for (const [k, v] of Object.entries(def) as [keyof T, T[keyof T]['default']][]) { - this.state[k] = v.default; - this.reactiveState[k] = ref(v.default); - } - - this.ready = this.init(); - this.loaded = this.ready.then(() => this.load()); - } - - private async init(): Promise { - await this.migrate(); - - const deviceState: State = await get(this.deviceStateKeyName) || {}; - const deviceAccountState = $i ? await get(this.deviceAccountStateKeyName) || {} : {}; - const registryCache = $i ? await get(this.registryCacheKeyName) || {} : {}; - - for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) { + const state = {}; + const reactiveState = {}; + for (const [k, v] of Object.entries(def)) { if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) { - this.reactiveState[k].value = this.state[k] = deviceState[k]; + state[k] = deviceState[k]; } else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) { - this.reactiveState[k].value = this.state[k] = registryCache[k]; + state[k] = registryCache[k]; } else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) { - this.reactiveState[k].value = this.state[k] = deviceAccountState[k]; + state[k] = deviceAccountState[k]; } else { - this.reactiveState[k].value = this.state[k] = v.default; + state[k] = v.default; if (_DEV_) console.log('Use default value', k, v.default); } } - - this.pizzaxChannel.addEventListener('message', ({ where, key, value, userId }) => { - // アカウント変更すればunisonReloadが効くため、このreturnが発火することは - // まずないと思うけど一応弾いておく - if (where === 'deviceAccount' && !($i && userId !== $i.id)) return; - this.reactiveState[key].value = this.state[key] = value; - }); - - if ($i) { - // streamingのuser storage updateイベントを監視して更新 - connection?.on('registryUpdated', ({ scope, key, value }: { scope?: string[], key: keyof T, value: T[typeof key]['default'] }) => { - if (!scope || scope.length !== 2 || scope[0] !== 'client' || scope[1] !== this.key || this.state[key] === value) return; - - this.reactiveState[key].value = this.state[key] = value; + for (const [k, v] of Object.entries(state)) { + reactiveState[k] = ref(v); + } + this.state = state as any; + this.reactiveState = reactiveState as any; - this.addIdbSetJob(async () => { - const cache = await get(this.registryCacheKeyName); - if (cache[key] !== value) { - cache[key] = value; - await set(this.registryCacheKeyName, cache); + if ($i) { + // なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう) + window.setTimeout(() => { + api('i/registry/get-all', { scope: ['client', this.key] }).then(kvs => { + const cache = {}; + for (const [k, v] of Object.entries(def)) { + if (v.where === 'account') { + if (Object.prototype.hasOwnProperty.call(kvs, k)) { + state[k] = kvs[k]; + reactiveState[k].value = kvs[k]; + cache[k] = kvs[k]; + } else { + state[k] = v.default; + reactiveState[k].value = v.default; + } + } } + localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache)); }); + }, 1); + // streamingのuser storage updateイベントを監視して更新 + connection?.on('registryUpdated', ({ scope, key, value }: { scope: string[], key: keyof T, value: T[typeof key]['default'] }) => { + if (scope.length !== 2 || scope[0] !== 'client' || scope[1] !== this.key || this.state[key] === value) return; + + this.state[key] = value; + this.reactiveState[key].value = value; + + const cache = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}'); + if (cache[key] !== value) { + cache[key] = value; + localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache)); + } }); } } - private load(): Promise { - return new Promise((resolve, reject) => { - if ($i) { - // api関数と循環参照なので一応setTimeoutしておく - window.setTimeout(async () => { - await defaultStore.ready; + public set(key: K, value: T[K]['default']): void { + if (_DEV_) console.log('set', key, value); - api('i/registry/get-all', { scope: ['client', this.key] }) - .then(kvs => { - const cache: Partial = {}; - for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) { - if (v.where === 'account') { - if (Object.prototype.hasOwnProperty.call(kvs, k)) { - this.reactiveState[k].value = this.state[k] = (kvs as Partial)[k]; - cache[k] = (kvs as Partial)[k]; - } else { - this.reactiveState[k].value = this.state[k] = v.default; - } - } - } - - return set(this.registryCacheKeyName, cache); - }) - .then(() => resolve()); - }, 1); + this.state[key] = value; + this.reactiveState[key].value = value; + + switch (this.def[key].where) { + case 'device': { + const deviceState = JSON.parse(localStorage.getItem(this.keyForLocalStorage) || '{}'); + deviceState[key] = value; + localStorage.setItem(this.keyForLocalStorage, JSON.stringify(deviceState)); + break; } - - resolve(); - }); - } - - public set(key: K, value: T[K]['default']): Promise { - // IndexedDBやBroadcastChannelで扱うために単純なオブジェクトにする - // (JSON.parse(JSON.stringify(value))の代わり) - const rawValue = deepcopy(value); - - if (_DEV_) console.log('set', key, rawValue, value); - - this.reactiveState[key].value = this.state[key] = rawValue; - - return this.addIdbSetJob(async () => { - if (_DEV_) console.log(`set ${key} start`); - switch (this.def[key].where) { - case 'device': { - this.pizzaxChannel.postMessage({ - where: 'device', - key, - value: rawValue, - }); - const deviceState = await get(this.deviceStateKeyName) || {}; - deviceState[key] = rawValue; - await set(this.deviceStateKeyName, deviceState); - break; - } - case 'deviceAccount': { - if ($i == null) break; - this.pizzaxChannel.postMessage({ - where: 'deviceAccount', - key, - value: rawValue, - userId: $i.id, - }); - const deviceAccountState = await get(this.deviceAccountStateKeyName) || {}; - deviceAccountState[key] = rawValue; - await set(this.deviceAccountStateKeyName, deviceAccountState); - break; - } - case 'account': { - if ($i == null) break; - const cache = await get(this.registryCacheKeyName) || {}; - cache[key] = rawValue; - await set(this.registryCacheKeyName, cache); - await api('i/registry/set', { - scope: ['client', this.key], - key: key.toString(), - value: rawValue, - }); - break; - } + case 'deviceAccount': { + if ($i == null) break; + const deviceAccountState = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::' + $i.id) || '{}'); + deviceAccountState[key] = value; + localStorage.setItem(this.keyForLocalStorage + '::' + $i.id, JSON.stringify(deviceAccountState)); + break; } - if (_DEV_) console.log(`set ${key} complete`); - }); + case 'account': { + if ($i == null) break; + const cache = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}'); + cache[key] = value; + localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache)); + api('i/registry/set', { + scope: ['client', this.key], + key: key, + value: value, + }); + break; + } + } } public push(key: K, value: ArrayElement): void { @@ -213,7 +132,6 @@ export class Storage { public reset(key: keyof T) { this.set(key, this.def[key].default); - return this.def[key].default; } /** @@ -248,25 +166,4 @@ export class Storage { }, }; } - - // localStorage => indexedDBのマイグレーション - private async migrate() { - const deviceState = localStorage.getItem(this.deviceStateKeyName); - if (deviceState) { - await set(this.deviceStateKeyName, JSON.parse(deviceState)); - localStorage.removeItem(this.deviceStateKeyName); - } - - const deviceAccountState = $i && localStorage.getItem(this.deviceAccountStateKeyName); - if ($i && deviceAccountState) { - await set(this.deviceAccountStateKeyName, JSON.parse(deviceAccountState)); - localStorage.removeItem(this.deviceAccountStateKeyName); - } - - const registryCache = $i && localStorage.getItem(this.registryCacheKeyName); - if ($i && registryCache) { - await set(this.registryCacheKeyName, JSON.parse(registryCache)); - localStorage.removeItem(this.registryCacheKeyName); - } - } } diff --git a/packages/client/src/scripts/unison-reload.ts b/packages/client/src/scripts/unison-reload.ts index b5afe9b07..59af584c1 100644 --- a/packages/client/src/scripts/unison-reload.ts +++ b/packages/client/src/scripts/unison-reload.ts @@ -1,3 +1,4 @@ +// SafariがBroadcastChannel未実装なのでライブラリを使う import { BroadcastChannel } from 'broadcast-channel'; export const reloadChannel = new BroadcastChannel('reload'); diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index b83904b6a..3971214af 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -303,16 +303,6 @@ export class ColdDeviceStorage { } } - public static getAll(): Partial { - return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce((acc, key) => { - const value = localStorage.getItem(PREFIX + key); - if (value != null) { - acc[key] = JSON.parse(value); - } - return acc; - }, {} as any); - } - public static set(key: T, value: typeof ColdDeviceStorage.default[T]): void { // 呼び出し側のバグ等で undefined が来ることがある // undefined を文字列として localStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視 diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 2e968ca7f..ae86db54e 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -11,8 +11,7 @@ "sourceMap": false, "target": "es2017", "module": "esnext", - "moduleResolution": "node", - "allowSyntheticDefaultImports": true, + "moduleResolution": "node" "removeComments": false, "noLib": false, "strict": true, diff --git a/yarn.lock b/yarn.lock index 9ce17130f..8ac348436 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5067,7 +5067,6 @@ __metadata: cross-env: 7.0.3 cypress: 11.1.0 date-fns: 2.29.3 - deepcopy: 2.1.0 escape-regexp: 0.0.1 eslint: 8.28.0 eslint-plugin-import: 2.26.0 @@ -6067,15 +6066,6 @@ __metadata: languageName: node linkType: hard -"deepcopy@npm:2.1.0": - version: 2.1.0 - resolution: "deepcopy@npm:2.1.0" - dependencies: - type-detect: ^4.0.8 - checksum: 7890ccaa8ad672bdc33d02626d13666bfbb33a68f7c6e8921a348b962b77edc4daf7f1301a789ec74a97e7f0acd611099c5cdd1e8b32ce93287819722f57b92e - languageName: node - linkType: hard - "deepmerge@npm:^4.2.2": version: 4.2.2 resolution: "deepmerge@npm:4.2.2" @@ -16564,7 +16554,7 @@ __metadata: languageName: node linkType: hard -"type-detect@npm:4.0.8, type-detect@npm:^4.0.8": +"type-detect@npm:4.0.8": version: 4.0.8 resolution: "type-detect@npm:4.0.8" checksum: 62b5628bff67c0eb0b66afa371bd73e230399a8d2ad30d852716efcc4656a7516904570cd8631a49a3ce57c10225adf5d0cbdcb47f6b0255fe6557c453925a15