diff --git a/packages/client/src/pizzax.ts b/packages/client/src/pizzax.ts index 0611599a2..1ebff07fa 100644 --- a/packages/client/src/pizzax.ts +++ b/packages/client/src/pizzax.ts @@ -1,6 +1,7 @@ import { onUnmounted, Ref, ref, watch } from 'vue'; import { $i } from './account'; import { api } from './os'; +import { get, set } from './scripts/idb-proxy'; 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; +const connection = $i ? stream.useChannel('main') : null; + export class Storage { + public readonly ready: PromiseLike; + public readonly key: string; - public readonly keyForLocalStorage: string; + public readonly deviceStateKeyName: string; + public readonly deviceAccountStateKeyName: string; + public readonly registryCacheKeyName: string; public readonly def: T; // TODO: これが実装されたらreadonlyにしたい: https://github.com/microsoft/TypeScript/issues/37487 - public readonly state: { [K in keyof T]: T[K]['default'] }; - public readonly reactiveState: { [K in keyof T]: Ref }; + public readonly state = {} as State; + public readonly reactiveState = {} as ReactiveState; - private connection = stream.useChannel('main'); + private nextIdbJob: Promise = Promise.resolve(); + private addIdbSetJob(job: () => Promise) { + const promise = this.nextIdbJob.then(job, e => { + console.error('Pizzax failed to save data to idb!', e); + return job(); + }); + this.nextIdbJob = promise; + return promise; + } constructor(key: string, def: T) { this.key = key; - this.keyForLocalStorage = 'pizzax::' + key; + this.deviceStateKeyName = `pizzax::${key}`; + this.deviceAccountStateKeyName = $i ? `pizzax::${key}::${$i.id}` : ''; + this.registryCacheKeyName = $i ? `pizzax::${key}::cache::${$i.id}` : ''; this.def = def; - // 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) || '{}') : {}; - - const state = {}; - const reactiveState = {}; - for (const [k, v] of Object.entries(def)) { - if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) { - state[k] = deviceState[k]; - } else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) { - state[k] = registryCache[k]; - } else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) { - state[k] = deviceAccountState[k]; - } else { - state[k] = v.default; - if (_DEV_) console.log('Use default value', k, v.default); - } - } - for (const [k, v] of Object.entries(state)) { - reactiveState[k] = ref(v); - } - this.state = state as any; - this.reactiveState = reactiveState as any; - - if ($i) { - // なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう) - 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イベントを監視して更新 - this.connection.on('registryUpdated', ({ scope, key, value }: { scope: string[], key: keyof T, value: T[typeof key]['default'] }) => { - if (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)); - } - }); - } + this.ready = this.init(); } - public set(key: K, value: T[K]['default']): void { + private init(): Promise { + return new Promise(async (resolve, reject) => { + 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]][]) { + if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) { + this.state[k] = deviceState[k]; + } else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) { + this.state[k] = registryCache[k]; + } else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) { + this.state[k] = deviceAccountState[k]; + } else { + this.state[k] = v.default; + if (_DEV_) console.log('Use default value', k, v.default); + } + } + for (const [k, v] of Object.entries(this.state) as [keyof T, T[keyof T]][]) { + this.reactiveState[k] = ref(v); + } + + if ($i) { + // なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう) + setTimeout(() => { + api('i/registry/get-all', { scope: ['client', this.key] }) + .then(kvs => { + const cache = {}; + for (const [k, v] of Object.entries(this.def)) { + if (v.where === 'account') { + if (Object.prototype.hasOwnProperty.call(kvs, k)) { + this.state[k as keyof T] = kvs[k]; + this.reactiveState[k as keyof T].value = kvs[k as string]; + cache[k] = kvs[k]; + } else { + this.state[k as keyof T] = v.default; + this.reactiveState[k].value = v.default; + } + } + } + + return set(this.registryCacheKeyName, cache); + }) + .then(() => resolve()); + }, 1); + // streamingのuser storage updateイベントを監視して更新 + connection?.on('registryUpdated', async ({ scope, key, value }: { scope: string[], key: keyof T, value: T[typeof key]['default'] }) => { + if (scope[1] !== this.key || this.state[key] === value) return; + + this.state[key] = value; + this.reactiveState[key].value = value; + + const cache = await get(this.registryCacheKeyName); + if (cache[key] !== value) { + cache[key] = value; + await set(this.registryCacheKeyName, cache); + } + }); + } + }); + } + + public set(key: K, value: T[K]['default']): Promise { if (_DEV_) console.log('set', key, value); 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; + return this.addIdbSetJob(async () => { + switch (this.def[key].where) { + case 'device': { + const deviceState = await get(this.deviceStateKeyName) || {}; + deviceState[key] = value; + await set(this.deviceStateKeyName, deviceState); + break; + } + case 'deviceAccount': { + if ($i == null) break; + const deviceAccountState = await get(this.deviceAccountStateKeyName) || {}; + deviceAccountState[key] = value; + await set(this.deviceAccountStateKeyName, deviceAccountState); + break; + } + case 'account': { + if ($i == null) break; + const cache = await get(this.registryCacheKeyName) || {}; + cache[key] = value; + await set(this.registryCacheKeyName, cache); + await api('i/registry/set', { + scope: ['client', this.key], + key: key, + value: value + }); + 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; - } - 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 {