From 31cc5a97a31d69f015f6ecfd089fb3147c1db7dd Mon Sep 17 00:00:00 2001 From: monster Date: Fri, 9 Jun 2023 21:34:40 +0800 Subject: [PATCH] feat: add ISettingService and IndexedDBSettingService --- .gitignore | 3 +- package.json | 7 +-- src/core/storage.spec.ts | 126 ++++++++++++++++++++++++++++++++++++++ src/core/storage.ts | 127 +++++++++++++++++++++++++++++++++++++++ src/vite-env.d.ts | 2 + 5 files changed, 260 insertions(+), 5 deletions(-) create mode 100644 src/core/storage.spec.ts create mode 100644 src/core/storage.ts diff --git a/.gitignore b/.gitignore index cd20b0f..652939d 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ dist-ssr *.sw? -yarn.lock \ No newline at end of file +yarn.lock +doc.md \ No newline at end of file diff --git a/package.json b/package.json index 7c12adb..82afeeb 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,14 @@ "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" }, - "dependencies": { + "devDependencies": { + "fake-indexeddb": "^4.0.1", "js-base64": "^3.7.5", "nanoid": "^4.0.2", "react": "^18.2.0", "react-dom": "^18.2.0", "vitest": "^0.32.0", - "yjs": "^13.6.2" - }, - "devDependencies": { + "yjs": "^13.6.2", "@types/react": "^18.0.37", "@types/react-dom": "^18.0.11", "@typescript-eslint/eslint-plugin": "^5.59.0", diff --git a/src/core/storage.spec.ts b/src/core/storage.spec.ts new file mode 100644 index 0000000..1bc5958 --- /dev/null +++ b/src/core/storage.spec.ts @@ -0,0 +1,126 @@ +import { indexedDB } from 'fake-indexeddb'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { + ISettingService, + IndexedDBSettingService, + SettingsValue, +} from './storage'; + +class FakeIndexedDBSettingService extends IndexedDBSettingService { + protected getIndexedDB(): IDBFactory { + return indexedDB; + } +} + +function createSettingService(): ISettingService { + // 返回一个新的 IndexedDBSettingService 实例 + return new FakeIndexedDBSettingService(); +} + +describe('ISettingService', () => { + let service: ISettingService; + + beforeEach(() => { + // 在每个测试用例之前创建一个新的 ISettingService 实例 + service = createSettingService(); + }); + + afterEach(async () => { + // 在每个测试用例之后清除所有设置 + await service.clearAll(); + }); + + describe('get', () => { + it('should return default value when key does not exist', async () => { + const defaultValue = 'default value'; + const value = await service.get('non-existent key', defaultValue); + expect(value).toEqual(defaultValue); + }); + + it('should return stored value when key exists', async () => { + const key = 'stored key'; + const value: SettingsValue = true; + await service.set(key, value); + const retrievedValue = await service.get(key, false); + expect(retrievedValue).toEqual(value); + }); + + it('should return boolean value when requested', async () => { + const key = 'boolean key'; + const value: SettingsValue = true; + await service.set(key, value); + const retrievedValue = await service.get(key, false); + expect(retrievedValue).toEqual(value); + }); + + it('should return string value when requested', async () => { + const key = 'string key'; + const value: SettingsValue = 'my string value'; + await service.set(key, value); + const retrievedValue = await service.get(key, ''); + expect(retrievedValue).toEqual(value); + }); + }); + + describe('set', () => { + it('should store boolean value', async () => { + const key = 'boolean key'; + const value: SettingsValue = true; + await service.set(key, value); + const retrievedValue = await service.get(key, false); + expect(retrievedValue).toEqual(value); + }); + + it('should store string value', async () => { + const key = 'string key'; + const value: SettingsValue = 'my string value'; + await service.set(key, value); + const retrievedValue = await service.get(key, ''); + expect(retrievedValue).toEqual(value); + }); + }); + + describe('readConfig', () => { + it('should return default values when no settings are stored', async () => { + const defaultValue = { + booleanKey: false, + stringKey: 'default string value', + }; + const config = await service.readConfig(defaultValue); + expect(config).to.deep.equal(defaultValue); + }); + + it('should return stored values when settings are stored', async () => { + const storedValue = { + booleanKey: true, + stringKey: 'stored string value', + }; + await service.set('booleanKey', storedValue.booleanKey); + await service.set('stringKey', storedValue.stringKey); + + const defaultValue = { + booleanKey: false, + stringKey: 'default string value', + }; + const config = await service.readConfig(defaultValue); + expect(config).to.deep.equal(storedValue); + }); + + it('should return default values for missing keys', async () => { + const storedValue = { + booleanKey: true, + }; + await service.set('booleanKey', storedValue.booleanKey); + + const defaultValue = { + booleanKey: false, + stringKey: 'default string value', + }; + const config = await service.readConfig(defaultValue); + expect(config).to.deep.equal({ + ...storedValue, + stringKey: defaultValue.stringKey, + }); + }); + }); +}); diff --git a/src/core/storage.ts b/src/core/storage.ts new file mode 100644 index 0000000..a9fd9d0 --- /dev/null +++ b/src/core/storage.ts @@ -0,0 +1,127 @@ +export type SettingsValue = string | boolean; + +export enum StorageKeys { + 'hamsterbaseURL' = 'hamsterbaseURL', + 'hamsterUsername' = 'hamsterUsername', + 'hamsterPassword' = 'hamsterPassword', +} + +export interface ISettingService { + get(key: string, defaultValue: V): Promise; + + set(key: string, value: SettingsValue): Promise; + + readConfig>( + defaultValue: T + ): Promise; + + clearAll(): Promise; +} +export interface ConfigDatabase { + settings: Record; +} + +export class IndexedDBSettingService implements ISettingService { + private readonly dbName: string = 'my-settings-db'; + private readonly dbVersion: number = 1; + private readonly storeName: string = 'settings'; + + private async openDB(): Promise { + return new Promise((resolve, reject) => { + const request = this.getIndexedDB().open(this.dbName, this.dbVersion); + request.onerror = (event) => { + console.error('Error opening database', event); + reject(event); + }; + + request.onsuccess = () => { + resolve(request.result); + }; + + request.onupgradeneeded = () => { + const db = request.result; + if (!db.objectStoreNames.contains(this.storeName)) { + db.createObjectStore(this.storeName); + } + }; + }); + } + + public async get( + key: string, + defaultValue: V + ): Promise { + const db = await this.openDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction(this.storeName, 'readonly'); + const objectStore = transaction.objectStore(this.storeName); + + const request = objectStore.get(key); + + request.onerror = (event) => { + console.error(`Error getting value for key "${key}"`, event); + reject(event); + }; + + request.onsuccess = () => { + const value = + request.result !== undefined ? request.result : defaultValue; + resolve(value); + }; + }); + } + + public async set(key: string, value: SettingsValue): Promise { + const db = await this.openDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction(this.storeName, 'readwrite'); + const objectStore = transaction.objectStore(this.storeName); + + const request = objectStore.put(value, key); + + request.onerror = (event) => { + console.error(`Error setting value for key "${key}"`, event); + reject(event); + }; + request.onsuccess = () => { + resolve(); + }; + }); + } + + public async readConfig>( + defaultValue: T + ): Promise { + const result = await Promise.all( + Object.keys(defaultValue).map(async (key: string) => { + return { + [key]: await this.get(key, defaultValue[key]), + }; + }) + ); + return Object.assign({}, ...result); + } + + public async clearAll(): Promise { + const db = await this.openDB(); + return new Promise((resolve, reject) => { + const transaction = db.transaction(this.storeName, 'readwrite'); + const objectStore = transaction.objectStore(this.storeName); + + const request = objectStore.clear(); + + request.onerror = () => { + console.error('Error clearing settings', event); + reject(event); + }; + + request.onsuccess = () => { + resolve(); + }; + }); + } + + protected getIndexedDB() { + return indexedDB; + } +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 11f02fe..8c73603 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,3 @@ /// + +declare module 'fake-indexeddb';