Skip to content

Commit

Permalink
perf(LiteStorage): save 操作时若无数据变更则忽略写文件操作
Browse files Browse the repository at this point in the history
  • Loading branch information
renxia committed Jan 3, 2025
1 parent 383fdc8 commit 0c89727
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 46 deletions.
20 changes: 11 additions & 9 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
// @ts-check

/** @type {import('ts-jest').InitialOptionsTsJest } */
/** @type {import('ts-jest').JestConfigWithTsJest} **/
// eslint-disable-next-line no-undef
module.exports = {
preset: "ts-jest",
preset: 'ts-jest',
resolver: 'ts-jest-resolver',
testEnvironment: 'node',
moduleFileExtensions: [
"ts",
"tsx",
"js"
],
moduleFileExtensions: ['ts', 'tsx', 'js'],
moduleNameMapper: {
'^src/(.*).js$': '$1.ts',
},
transform: {
'^.+.tsx?$': ['ts-jest', { isolatedModules: true }],
},
// transform: {
// '^.+\\.(t|j)sx?$': [
// '@swc/jest',
Expand Down
13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@
"@types/eslint": "^9.6.1",
"@types/jest": "^29.5.14",
"@types/micromatch": "^4.0.9",
"@types/node": "^22.10.2",
"@typescript-eslint/eslint-plugin": "^8.18.1",
"@typescript-eslint/parser": "^8.18.1",
"@types/node": "^22.10.4",
"@typescript-eslint/eslint-plugin": "^8.19.0",
"@typescript-eslint/parser": "^8.19.0",
"compressing": "^1.10.1",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jest": "^28.9.0",
"eslint-plugin-jest": "^28.10.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-unicorn": "^56.0.1",
"husky": "^9.1.7",
Expand All @@ -87,10 +87,11 @@
"prettier": "^3.4.2",
"standard-version": "^9.5.0",
"ts-jest": "^29.2.5",
"ts-jest-resolver": "^2.0.1",
"ts-node": "^10.9.2",
"typedoc": "^0.27.5",
"typedoc": "^0.27.6",
"typescript": "^5.7.2",
"typescript-eslint": "^8.18.1",
"typescript-eslint": "^8.19.0",
"windows-process-tree": "^0.4.0"
},
"peerDependencies": {
Expand Down
55 changes: 55 additions & 0 deletions src/node/LiteStorage.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { existsSync, unlinkSync } from 'node:fs';
import { resolve } from 'node:path';
import { LiteStorage } from './LiteStorage';
import { AnyObject } from '../types';

describe('node/LiteStorage', () => {
// eslint-disable-next-line no-console
// console.log = jest.fn();
console.error = jest.fn();

const filepath = resolve('./cache/test.json');

it('init', async () => {
if (existsSync(filepath)) unlinkSync(filepath);

const stor = LiteStorage.getInstance<AnyObject>({ filepath });
await stor.ready();

expect(stor.config.filepath).toBe(filepath);
expect(existsSync(filepath)).toBe(false);

await stor.set({ a: 1, b: 2 });
expect(stor.getItem('a')).toBe(1);
// 写了数据才创建文件
expect(existsSync(filepath)).toBe(true);

await stor.set({ a: 2, c: 3 }, 'cover');
expect(stor.length).toBe(2);
expect(stor.getItem('b')).toBeUndefined();

await stor.save({ a: 3, b: 10 });
expect(stor.getItem('a')).toBe(3);
// 默认 merge 模式合并数据
expect(stor.length).toBe(3);

const d = stor.get();
expect(d).toEqual({ a: 3, b: 10, c: 3 });

await stor.removeItem('a');
expect(stor.getItem('a')).toBeUndefined();

await stor.setItem('a', 1);
expect(stor.getItem('a')).toBe(1);

await stor.del('b');
expect(stor.getItem('b')).toBeUndefined();

await stor.clear();
expect(stor.getItem('a')).toBeUndefined();
expect(stor.length).toBe(0);

await stor.clear(true);
expect(existsSync(filepath)).toBe(false);
});
});
91 changes: 61 additions & 30 deletions src/node/LiteStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export interface LSOptions<T extends object = Record<string, unknown>> {
uuid?: string;
/** 默认初始值 */
initial?: T;
/** 是否仅考虑单进程模式读写(内存)。默认为 false,每次保持前都会重载数据 */
singleMode?: boolean;
}

/**
Expand Down Expand Up @@ -53,6 +55,7 @@ export class LiteStorage<T extends object = Record<string, unknown>> {
private barrier = new Barrier();
private isToml = false;
private isJson5 = false;
private isChanged = false;

// @ts-ignore
private cache: LSCache<T>;
Expand All @@ -64,6 +67,7 @@ export class LiteStorage<T extends object = Record<string, unknown>> {
uuid: 'defaults',
filepath: 'ls.json',
initial: {},
singleMode: false,
...options,
};

Expand Down Expand Up @@ -95,25 +99,47 @@ export class LiteStorage<T extends object = Record<string, unknown>> {
return this;
}
/** 主动保存 */
public async save(value?: T, mode: 'merge' | 'cover' = 'merge') {
public async save(value?: T, mode: 'merge' | 'cover' = 'merge', reload = true) {
if (value) return this.set(value, mode);
await this.reload();
return this.toCache();
}
private async toCache() {
const cacheDir = dirname(this.cachePath);
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
if (!this.isChanged) return this;

let content = '';
if (this.isToml) {
const TOML = await import('@iarna/toml');
content = TOML.stringify(this.cache as never);
} else {
content = safeStringify(this.cache, 4, this.isJson5);
await this.barrier.wait();
this.barrier = new Barrier();
try {
if (reload && !this.options.singleMode) await this.reload();
await this.toCache();
} catch (e) {
console.log(e);
}
fs.writeFileSync(this.cachePath, content, 'utf8');

this.barrier.open();
return this;
}
private cacheTimer: NodeJS.Timeout | undefined;
private async toCache(): Promise<this> {
const tc = async () => {
const cacheDir = dirname(this.cachePath);
if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });

let content = '';
if (this.isToml) {
const TOML = await import('@iarna/toml');
content = TOML.stringify(this.cache as never);
} else {
content = safeStringify(this.cache, 4, this.isJson5);
}
fs.writeFileSync(this.cachePath, content, 'utf8');
this.isChanged = false;
return this;
};

if (!this.options.singleMode) return tc();

return new Promise(rs => {
if (this.cacheTimer) clearTimeout(this.cacheTimer);
this.cacheTimer = setTimeout(async () => tc().then(rs), 100);
});
}
/** 从文件中重载数据至内存。在多进程、多线程模式下,需读取最新数据时,可手动调用 */
public async reload() {
if (fs.existsSync(this.cachePath)) {
Expand All @@ -134,16 +160,18 @@ export class LiteStorage<T extends object = Record<string, unknown>> {
return this;
}
/** 以 options.uuid 为 key 设置数据 */
public set(value: T, mode: 'merge' | 'cover' = 'merge') {
public async set(value: T, mode: 'merge' | 'cover' = 'merge') {
const uuid = this.options.uuid;

if (value != null && uuid) {
if (!this.options.singleMode) await this.reload();
if (!this.cache.data[uuid]) this.cache.data[uuid] = {} as T;
if (value !== this.cache.data[uuid]) {
if (mode === 'merge') assign(this.cache.data[uuid], value);
else this.cache.data[uuid] = value;
}
this.save();
this.isChanged = true;
await this.toCache();
} else {
console.warn('[LiteStorage][set]error', uuid, value);
}
Expand All @@ -161,18 +189,12 @@ export class LiteStorage<T extends object = Record<string, unknown>> {
public getAll() {
return this.cache;
}
/** 移除一项数据 */
public async del(key: keyof T) {
const info = this.cache.data[this.options.uuid];
if (key in info) {
await this.reload();
delete info[key];
return this.toCache();
}
return this;
/** 移除一项数据。同 removeItem 方法 */
public del(key: keyof T) {
return this.removeItem(key);
}
/** 设置并保存一个数据项。提示:setItem、removeItem 都会触发文件读写,应避免密集高频调用 */
public setItem<K extends keyof T>(key: K, value: Partial<T[K]>, mode: 'merge' | 'cover' = 'merge') {
public setItem<K extends keyof T>(key: K, value: T[K] extends object ? Partial<T[K]> : T[K], mode: 'merge' | 'cover' = 'cover') {
const data = this.get(true);
if (mode === 'cover') data[key] = value as T[K];
else assign(data, { [key]: value });
Expand All @@ -184,9 +206,18 @@ export class LiteStorage<T extends object = Record<string, unknown>> {
if (value && !Array.isArray(value) && typeof value === 'object') return assign({}, value) as T[K];
return value;
}
/** 移除一项数据。同 del 方法 */
public removeItem(key: keyof T) {
return this.del(key);
/** 移除一项数据 */
public async removeItem(key: keyof T) {
const info = this.cache.data[this.options.uuid];
if (key in info) {
if (!this.options.singleMode) await this.reload();

if (key in info) {
delete info[key];
return this.toCache();
}
}
return this;
}
/**
* 清理缓存
Expand All @@ -199,7 +230,7 @@ export class LiteStorage<T extends object = Record<string, unknown>> {
} else {
const uuid = this.options.uuid;
if (this.cache.data[uuid]) {
await this.reload();
if (!this.options.singleMode) await this.reload();
delete this.cache.data[uuid];
return this.toCache();
}
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.eslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "build/module",
"target": "esnext"
"target": "esnext",
},
"include": ["**/*.ts"]
}

0 comments on commit 0c89727

Please sign in to comment.