Initial commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
*.md
|
||||
node_modules
|
||||
dist
|
||||
public
|
||||
build
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"printWidth": 150,
|
||||
"tabWidth": 2,
|
||||
"endOfLine": "lf",
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"bracketSpacing": true,
|
||||
"semi": true,
|
||||
"bracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"arrowParens": "always",
|
||||
"importOrderCaseSensitive": false
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
# [←](../../../docs/README.md) @crux-kit/local-storage
|
||||
|
||||
Сервіс для роботи з `localStorage`: збереження, читання, видалення даних із серіалізацією/десеріалізацією та підтримкою TTL і namespace-префіксів.
|
||||
|
||||
## Як використовувати
|
||||
|
||||
### Ініціалізація
|
||||
|
||||
```ts
|
||||
import { LocalStorageService } from '@crux-kit/local-storage';
|
||||
|
||||
export const storage = new LocalStorageService({
|
||||
prefix: 'app',
|
||||
logger: console,
|
||||
});
|
||||
```
|
||||
|
||||
- `prefix` — необов'язковий. Усі ключі зберігаються як `prefix:key`, що дозволяє уникнути конфліктів між різними модулями.
|
||||
- `logger` — необов'язковий. Передається `console` або будь-який об'єкт із методами `error` / `log`.
|
||||
|
||||
### Запис і читання
|
||||
|
||||
```ts
|
||||
storage.set('user', { id: 1, name: 'Randy Marsh' });
|
||||
|
||||
const user = storage.get<{ id: number; name: string }>('user');
|
||||
```
|
||||
|
||||
Дані автоматично серіалізуються в JSON при записі та десеріалізуються при читанні.
|
||||
|
||||
### TTL (час життя)
|
||||
|
||||
```ts
|
||||
// значення стане недоступним через 30 хвилин
|
||||
storage.set('session', data, { ttlMs: 30 * 60 * 1000 });
|
||||
|
||||
// get поверне undefined і автоматично видалить ключ після закінчення TTL
|
||||
const session = storage.get('session');
|
||||
```
|
||||
|
||||
### getOrSet
|
||||
|
||||
Повертає збережене значення, або записує та повертає `fallback`, якщо ключ відсутній.
|
||||
|
||||
```ts
|
||||
const lang = storage.getOrSet('lang', 'en');
|
||||
|
||||
// також підтримує TTL
|
||||
const theme = storage.getOrSet('theme', 'dark', { ttlMs: 60 * 60 * 1000 });
|
||||
```
|
||||
|
||||
### Перевірка існування ключа
|
||||
|
||||
```ts
|
||||
storage.has('token'); // true | false
|
||||
```
|
||||
|
||||
### Видалення
|
||||
|
||||
```ts
|
||||
storage.remove('token');
|
||||
```
|
||||
|
||||
### Всі значення як об'єкт
|
||||
|
||||
Повертає всі збережені записи поточного namespace у вигляді одного об'єкта. Ключі з вичерпаним TTL автоматично виключаються.
|
||||
|
||||
```ts
|
||||
const lss = new LocalStorageService({ prefix: 'settings' });
|
||||
|
||||
lss.set('view', 'grid');
|
||||
lss.set('sortBy', 'name');
|
||||
lss.set('showHidden', true);
|
||||
|
||||
type Settings = {
|
||||
view: string;
|
||||
sortBy: string;
|
||||
showHidden: boolean;
|
||||
};
|
||||
|
||||
lss.getAll<Settings>();
|
||||
// → Partial<Settings> — { view: 'grid', sortBy: 'name', showHidden: true }
|
||||
```
|
||||
|
||||
### Список ключів
|
||||
|
||||
Повертає всі ключі. Якщо заданий `prefix` — повертає лише ключі поточного namespace без префікса.
|
||||
|
||||
```ts
|
||||
storage.keys(); // ['user', 'session', 'lang']
|
||||
```
|
||||
|
||||
### Очищення
|
||||
|
||||
```ts
|
||||
storage.clear();
|
||||
```
|
||||
|
||||
Якщо заданий `prefix` — видаляє лише ключі поточного namespace. Без `prefix` — очищає весь `localStorage`.
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@crux-kit/local-storage",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"types": "src/index.ts",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './main';
|
||||
@@ -0,0 +1,125 @@
|
||||
interface StorageEnvelope<T> {
|
||||
value: T;
|
||||
expiresAt?: number;
|
||||
}
|
||||
|
||||
interface LocalStorageServiceConfig {
|
||||
prefix?: string;
|
||||
logger?: Console;
|
||||
}
|
||||
|
||||
interface SetOptions {
|
||||
ttlMs?: number;
|
||||
}
|
||||
|
||||
export class LocalStorageService {
|
||||
private _prefix?: string;
|
||||
private _logger?: Console;
|
||||
|
||||
constructor(config?: LocalStorageServiceConfig) {
|
||||
this._prefix = config?.prefix;
|
||||
this._logger = config?.logger;
|
||||
}
|
||||
|
||||
private _buildKey(key: string): string {
|
||||
return this._prefix ? `${this._prefix}:${key}` : key;
|
||||
}
|
||||
|
||||
private _stripPrefix(rawKey: string): string {
|
||||
return this._prefix ? rawKey.slice(this._prefix.length + 1) : rawKey;
|
||||
}
|
||||
|
||||
public get<T>(key: string): T | undefined {
|
||||
try {
|
||||
const raw = localStorage.getItem(this._buildKey(key));
|
||||
|
||||
if (raw == null) return undefined;
|
||||
|
||||
const envelope = JSON.parse(raw) as StorageEnvelope<T>;
|
||||
|
||||
if (envelope.expiresAt != null && Date.now() > envelope.expiresAt) {
|
||||
this.remove(key);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return envelope.value;
|
||||
} catch (error) {
|
||||
this._logger?.error(`[LocalStorageService] get "${key}":`, error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public set<T>(key: string, value: T, options?: SetOptions): void {
|
||||
try {
|
||||
const envelope: StorageEnvelope<T> = {
|
||||
value,
|
||||
expiresAt: options?.ttlMs != null ? Date.now() + options.ttlMs : undefined,
|
||||
};
|
||||
|
||||
localStorage.setItem(this._buildKey(key), JSON.stringify(envelope));
|
||||
} catch (error) {
|
||||
this._logger?.error(`[LocalStorageService] set "${key}":`, error);
|
||||
}
|
||||
}
|
||||
|
||||
public getOrSet<T>(key: string, fallback: T, options?: SetOptions): T {
|
||||
const existing = this.get<T>(key);
|
||||
|
||||
if (existing != null) return existing;
|
||||
|
||||
this.set(key, fallback, options);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
public has(key: string): boolean {
|
||||
return localStorage.getItem(this._buildKey(key)) != null;
|
||||
}
|
||||
|
||||
public remove(key: string): void {
|
||||
try {
|
||||
localStorage.removeItem(this._buildKey(key));
|
||||
} catch (error) {
|
||||
this._logger?.error(`[LocalStorageService] remove "${key}":`, error);
|
||||
}
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
try {
|
||||
if (this._prefix) {
|
||||
this.keys().forEach((key) => this.remove(key));
|
||||
} else {
|
||||
localStorage.clear();
|
||||
}
|
||||
} catch (error) {
|
||||
this._logger?.error('[LocalStorageService] clear:', error);
|
||||
}
|
||||
}
|
||||
|
||||
public getAll<T extends object>(): Partial<T> {
|
||||
return this.keys().reduce<T>((acc, key) => {
|
||||
const value = this.get(key);
|
||||
|
||||
if (value != null) {
|
||||
acc[key as keyof T] = value as T[keyof T];
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {} as T);
|
||||
}
|
||||
|
||||
public keys(): string[] {
|
||||
const result: string[] = [];
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const rawKey = localStorage.key(i);
|
||||
|
||||
if (rawKey == null) continue;
|
||||
|
||||
if (this._prefix && !rawKey.startsWith(`${this._prefix}:`)) continue;
|
||||
|
||||
result.push(this._stripPrefix(rawKey));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2023", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user