wip
This commit is contained in:
		
							parent
							
								
									32eb7503b7
								
							
						
					
					
						commit
						7b7368b815
					
				|  | @ -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<string, { | ||||
|  | @ -8,119 +9,144 @@ type StateDef = Record<string, { | |||
| 	default: any; | ||||
| }>; | ||||
| 
 | ||||
| type State<T extends StateDef> = { [K in keyof T]: T[K]['default']; }; | ||||
| type ReactiveState<T extends StateDef> = { [K in keyof T]: Ref<T[K]['default']>; }; | ||||
| 
 | ||||
| type ArrayElement<A> = A extends readonly (infer T)[] ? T : never; | ||||
| 
 | ||||
| const connection = $i ? stream.useChannel('main') : null; | ||||
| 
 | ||||
| export class Storage<T extends StateDef> { | ||||
| 	public readonly ready: PromiseLike<void>; | ||||
| 
 | ||||
| 	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<T[K]['default']> }; | ||||
| 	public readonly state = {} as State<T>; | ||||
| 	public readonly reactiveState = {} as ReactiveState<T>; | ||||
| 
 | ||||
| 	private connection = stream.useChannel('main'); | ||||
| 	private nextIdbJob: Promise<any> = Promise.resolve(); | ||||
| 	private addIdbSetJob<T>(job: () => Promise<T>) { | ||||
| 		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<K extends keyof T>(key: K, value: T[K]['default']): void { | ||||
| 	private init(): Promise<void> { | ||||
| 		return new Promise(async (resolve, reject) => { | ||||
| 			const deviceState: State<T> = 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<K extends keyof T>(key: K, value: T[K]['default']): Promise<void> { | ||||
| 		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<K extends keyof T>(key: K, value: ArrayElement<T[K]['default']>): void { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue