All files / src/core/strategies LocalStorageStrategy.ts

99.17% Statements 120/121
92.68% Branches 38/41
100% Functions 20/20
99.17% Lines 120/121

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 1661x       1x 174x 174x 174x   174x 174x   174x 174x   174x 174x   174x   22046x   22046x 22046x 4392x 4392x 22046x 2282x 2282x 2282x 2282x 2282x 2282x 22046x 15372x 15372x 15372x 15372x 22046x   22046x 22046x   174x 200x 200x   174x 261x 2453x 2453x 2419x 2419x 2453x 261x   174x 174x   174x 1242x 1242x 174x 174x     174x 178x 178x   174x 86x 86x   174x 174x 174x 174x 174x   174x 134x 134x   174x     256x 256x   174x 32x 32x   174x                       66x 66x   174x 4x 4x   174x   34x 34x   174x 91x 91x   174x 127x   105x 127x 79x 79x 79x 79x 79x 79x 127x 26x 26x 26x 26x 26x 26x 26x 105x 127x   174x 194x 194x   174x 15x   8x 8x 8x 8x 8x 8x 8x 8x 15x   174x 174x 174x 174x  
import type { DataModel, StorageBase, ValueType } from "../types";
import { StorageEngine } from "../types";
 
/** @ignore */
export class LocalStorageStrategy implements StorageBase {
	private prefixKey: string;
	private memoryCache: Map<string, DataModel<ValueType>> = new Map();
	private channel: BroadcastChannel;
 
	constructor(prefixKey = "HybridWebCache") {
		this.prefixKey = `${prefixKey.trim()}::`;
 
		this.channel = new BroadcastChannel(`${this.prefixKey}`);
		this.channel.onmessage = this.handleSyncEvent.bind(this);
 
		this.loadMemoryCache(); // Load existing data into memory cache on initialization
	}
 
	private handleSyncEvent(event: MessageEvent): void {
		// Handle sync events for multi-instance communication
		const action = event.data?.action || "";
 
		switch (action) {
			case "clear":
				this.memoryCache.clear();
				break;
			case "unset": {
				const { key } = event.data;
				if (key) {
					this.memoryCache.delete(key);
				}
				break;
			}
			case "sync": {
				const { key, value } = event.data;
				this.memoryCache.set(key, value);
				break;
			}
			default:
				break;
		}
	}
 
	private formattedKey(key: string): string {
		return `${this.prefixKey}${key}`;
	}
 
	private _forEachStorage(callback: (originalKey: string, value: string | null) => void): void {
		for (let i = 0; i < localStorage.length; i++) {
			const key = localStorage.key(i);
			if (key?.startsWith(this.prefixKey)) {
				callback(key.replace(this.prefixKey, ""), localStorage.getItem(key));
			}
		}
	}
 
	private loadMemoryCache(): void {
		this.memoryCache.clear(); // Clear existing cache before loading
 
		this._forEachStorage((key, value) => {
			const data: DataModel<ValueType> = JSON.parse(value ?? "{}");
			this.memoryCache.set(key, data);
		});
	}
 
	/** @internal */
	async init(): Promise<void> {
		return Promise.resolve();
	}
 
	set<T extends ValueType>(key: string, data: DataModel<T>): Promise<void> {
		return Promise.resolve(this.setSync(key, data));
	}
 
	setSync<T extends ValueType>(key: string, data: DataModel<T>): void {
		localStorage.setItem(this.formattedKey(key), JSON.stringify(data));
		this.memoryCache.set(key, data);
		this.channel.postMessage({ action: "sync", key, value: data });
	}
 
	get<T extends ValueType>(key: string): Promise<DataModel<T> | undefined> {
		return Promise.resolve(this.getSync(key));
	}
 
	getSync<T extends ValueType>(key: string): DataModel<T> | undefined {
		// const item = localStorage.getItem(this.formattedKey(key));
		// return item ? JSON.parse(item) : undefined;
		return this.memoryCache.get(key) as DataModel<T>;
	}
 
	getAll<T extends ValueType>(): Promise<Map<string, DataModel<T>> | null> {
		return Promise.resolve(this.getAllSync<T>());
	}
 
	getAllSync<T extends ValueType>(): Map<string, DataModel<T>> | null {
		// const data = new Map();
 
		// for (let i = 0; i < localStorage.length; i++) {
		// 	const key = localStorage.key(i);
		// 	if (key?.startsWith(this.prefixKey)) {
		// 		const item = localStorage.getItem(key);
		// 		data.set(key.replace(this.prefixKey, ""), item ? JSON.parse(item) : item);
		// 	}
		// }
 
		// return data.size > 0 ? data : null;
		return this.memoryCache.size > 0 ? (this.memoryCache as Map<string, DataModel<T>>) : null;
	}
 
	has(key: string): Promise<boolean> {
		return Promise.resolve(this.hasSync(key));
	}
 
	hasSync(key: string): boolean {
		// return !!localStorage.getItem(this.formattedKey(key));
		return this.memoryCache.has(key);
	}
 
	unset(key?: string): Promise<boolean> {
		return Promise.resolve(this.unsetSync(key));
	}
 
	unsetSync(key?: string): boolean {
		if (this.memoryCache.size === 0) return false;
 
		let result = false;
		if (!key) {
			const keysToRemove: string[] = [];
			this._forEachStorage((originalKey, _value) => keysToRemove.push(originalKey));
			keysToRemove.forEach((k) => localStorage.removeItem(k));
			this.memoryCache.clear();
			this.channel.postMessage({ action: "clear", key: undefined, value: undefined });
			result = true;
		} else {
			if (this.hasSync(key)) {
				const fKey = this.formattedKey(key);
				localStorage.removeItem(fKey);
				result = this.memoryCache.delete(key);
				this.channel.postMessage({ action: "unset", key, value: undefined });
			}
		}
		return result;
	}
 
	get length(): number {
		return this.memoryCache.size;
	}
 
	get bytes(): number {
		if (this.memoryCache.size === 0) return 0;
 
		let totalBytes = 0;
		this._forEachStorage((key, value) => {
			totalBytes += new TextEncoder().encode(key).length;
			if (value) {
				totalBytes += new TextEncoder().encode(value).length;
			}
		});
		return totalBytes;
	}
 
	get type(): StorageEngine {
		return StorageEngine.LocalStorage;
	}
}