Platform Acceleration Phase 1: - @bytelyst/sync package: Offline-first sync engine with conflict resolution - Storage adapters: LocalStorage, InMemory, MMKV - Deduplication, retry with backoff, auto-flush on reconnect - 12 comprehensive tests - @bytelyst/dashboard-components package: Shared React components - ErrorPage, NotFoundPage, LoadingSpinner, LoadingSkeleton, EmptyState, PageHeader - Theme-aware with CSS custom properties A/B Testing Framework (Complete): - Admin UI at /ops/ab-testing with experiments list, variant performance, AI suggestions - Sidebar navigation with Beaker icon - 40 tests passing in ab-testing module All 909 platform-service tests pass.
128 lines
4.1 KiB
TypeScript
128 lines
4.1 KiB
TypeScript
/**
|
|
* Storage Adapters
|
|
*
|
|
* @module @bytelyst/sync/storage
|
|
*/
|
|
|
|
import type { StorageAdapter } from './types.js';
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// LocalStorage Adapter (Web)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
export class LocalStorageAdapter implements StorageAdapter {
|
|
private prefix: string;
|
|
|
|
constructor(prefix = 'bytelyst:sync:') {
|
|
this.prefix = prefix;
|
|
}
|
|
|
|
getItem<T>(key: string): T | null {
|
|
if (typeof localStorage === 'undefined') return null;
|
|
const value = localStorage.getItem(this.prefix + key);
|
|
if (!value) return null;
|
|
try {
|
|
return JSON.parse(value) as T;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
setItem<T>(key: string, value: T): void {
|
|
if (typeof localStorage === 'undefined') return;
|
|
localStorage.setItem(this.prefix + key, JSON.stringify(value));
|
|
}
|
|
|
|
removeItem(key: string): void {
|
|
if (typeof localStorage === 'undefined') return;
|
|
localStorage.removeItem(this.prefix + key);
|
|
}
|
|
|
|
keys(): string[] {
|
|
if (typeof localStorage === 'undefined') return [];
|
|
const keys: string[] = [];
|
|
for (let i = 0; i < localStorage.length; i++) {
|
|
const key = localStorage.key(i);
|
|
if (key && key.startsWith(this.prefix)) {
|
|
keys.push(key.slice(this.prefix.length));
|
|
}
|
|
}
|
|
return keys;
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// In-Memory Adapter (Testing / SSR)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
export class InMemoryAdapter implements StorageAdapter {
|
|
private store = new Map<string, unknown>();
|
|
|
|
getItem<T>(key: string): T | null {
|
|
const value = this.store.get(key);
|
|
return value !== undefined ? (value as T) : null;
|
|
}
|
|
|
|
setItem<T>(key: string, value: T): void {
|
|
this.store.set(key, value);
|
|
}
|
|
|
|
removeItem(key: string): void {
|
|
this.store.delete(key);
|
|
}
|
|
|
|
keys(): string[] {
|
|
return Array.from(this.store.keys());
|
|
}
|
|
|
|
clear(): void {
|
|
this.store.clear();
|
|
}
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
// MMKV Adapter Interface (React Native)
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
export interface MMKVInstance {
|
|
getString(key: string): string | undefined;
|
|
set(key: string, value: string): void;
|
|
delete(key: string): void;
|
|
getAllKeys(): string[];
|
|
}
|
|
|
|
export class MMKVAdapter implements StorageAdapter {
|
|
private mmkv: MMKVInstance;
|
|
private prefix: string;
|
|
|
|
constructor(mmkv: MMKVInstance, prefix = 'sync:') {
|
|
this.mmkv = mmkv;
|
|
this.prefix = prefix;
|
|
}
|
|
|
|
getItem<T>(key: string): T | null {
|
|
const value = this.mmkv.getString(this.prefix + key);
|
|
if (!value) return null;
|
|
try {
|
|
return JSON.parse(value) as T;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
setItem<T>(key: string, value: T): void {
|
|
this.mmkv.set(this.prefix + key, JSON.stringify(value));
|
|
}
|
|
|
|
removeItem(key: string): void {
|
|
this.mmkv.delete(this.prefix + key);
|
|
}
|
|
|
|
keys(): string[] {
|
|
return this.mmkv
|
|
.getAllKeys()
|
|
.filter(k => k.startsWith(this.prefix))
|
|
.map(k => k.slice(this.prefix.length));
|
|
}
|
|
}
|