Skip to content

Commit

Permalink
feat: add ISettingService and IndexedDBSettingService
Browse files Browse the repository at this point in the history
  • Loading branch information
monster committed Jun 9, 2023
1 parent 3a3e3a4 commit 31cc5a9
Show file tree
Hide file tree
Showing 5 changed files with 260 additions and 5 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Expand Up @@ -24,4 +24,5 @@ dist-ssr
*.sw?


yarn.lock
yarn.lock
doc.md
7 changes: 3 additions & 4 deletions package.json
Expand Up @@ -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",
Expand Down
126 changes: 126 additions & 0 deletions 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<boolean>(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<string>(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<boolean>(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<string>(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,
});
});
});
});
127 changes: 127 additions & 0 deletions 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<V extends SettingsValue>(key: string, defaultValue: V): Promise<V>;

set(key: string, value: SettingsValue): Promise<void>;

readConfig<T extends Record<string, SettingsValue>>(
defaultValue: T
): Promise<T>;

clearAll(): Promise<void>;
}
export interface ConfigDatabase {
settings: Record<string, SettingsValue>;
}

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<IDBDatabase> {
return new Promise<IDBDatabase>((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<V extends SettingsValue>(
key: string,
defaultValue: V
): Promise<V> {
const db = await this.openDB();
return new Promise<V>((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<void> {
const db = await this.openDB();
return new Promise<void>((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<T extends Record<string, SettingsValue>>(
defaultValue: T
): Promise<T> {
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<void> {
const db = await this.openDB();
return new Promise<void>((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;
}
}
2 changes: 2 additions & 0 deletions src/vite-env.d.ts
@@ -1 +1,3 @@
/// <reference types="vite/client" />

declare module 'fake-indexeddb';

0 comments on commit 31cc5a9

Please sign in to comment.