A lightweight, smart JavaScript storage library that makes state persistence as easy as modifying a plain object.
Tired of writing this? 🤮
const data = JSON.parse(localStorage.getItem("settings"));
data.count += 1;
localStorage.setItem("settings", JSON.stringify(data));But what if, WHAT IF, you can just do this↓
settings.count += 1;- 🪄 Deep Reactive Proxy: Modify any nested property, and it saves automatically.
- 🏗️ Innovative Flat Storage: Breaks down nested JSON objects into flat Key-Value pairs. No need to serialize the entire object just to update a deep property!
- ⚡ Smart Debouncing: Automatically merges frequent writes (like array operations) for extreme performance.
- 🔒 Type Safety: Blocks un-storable values (like
undefinedorfunction) to keep your storage safe. - 🌐 Framework Agnostic: Works in any vanilla JS or framework environment.
/**
* @file test.ts
*/
import { WebStorageItemStorage, FlatWebStorage } from "./storage.js";
console.info("========================================");
console.info("WebStorageItemStorage (Whole JSON)");
console.info("========================================");
//Best for small data that needs to be read/written all at once.
const settings = new WebStorageItemStorage("settings", localStorage).data;
settings.count = settings.count ? settings.count + 1 : 1; //See? Auto-persists!
console.log("Old count:", settings.count);
console.info("\n========================================");
console.info("FlatWebStorage (Innovative Flat KV)");
console.info("========================================");
const flatLocalStorage = new FlatWebStorage({ namespace: "test", instance: localStorage });
await flatLocalStorage.init();
//Pre-load everything under the root, not necessary for synchronous storage but simulates typical async usage and demonstrates the load API.
await flatLocalStorage.load("");
//To load a single key: await flatLocalStorage.load("count");
console.log("Everything in flat storage(old): ", JSON.stringify(flatLocalStorage.data));
//Deep Reactive Modify (Auto-persists only the leaf)
flatLocalStorage.data.count = flatLocalStorage.data.count ? flatLocalStorage.data.count + 1 : 1;
console.log("Flat count:", flatLocalStorage.data.count);
//Smart Array Debouncing (Multiple mutations = 1 write)
flatLocalStorage.data.arrTest ??= [];
flatLocalStorage.data.arrTest.push("I am a, no, an array!!!~");
flatLocalStorage.data.arrTest[1] = `Time: ${Date.now()}`;
console.log("Everything in flat storage: ", JSON.stringify(flatLocalStorage.data));
console.info("\n========================================");
console.info("Advanced: Sync Read & Template Get");
console.info("========================================");
//Template String Get (Async API contract, works seamlessly, lighter then loading the whole object first, strongly recommended for asynchronous contexts)
console.info("\n[1] Testing template string get...");
console.log("Count via template string get:", await flatLocalStorage.get`count`);
//Synchronous Read (Lazy load via Proxy)
console.info("\n[2] Testing synchronous read...");
//Simulate cache miss to demonstrate synchronous lazy loading
flatLocalStorage.cache.clear();
flatLocalStorage.arrayDebouncers.clear();
console.info("(Internal cache cleared for testing)");
const syncLoadResult = flatLocalStorage.load("count");
console.log("Is load synchronous (not a Promise)?", !(syncLoadResult instanceof Promise));
console.log("Value from sync load:", syncLoadResult);
//Automatically triggers sync load if possiible
flatLocalStorage.cache.clear();
flatLocalStorage.arrayDebouncers.clear();
console.info("\nAccessing data.count directly after cache clear...");
console.log("Synchronously read count from proxy:", flatLocalStorage.data.count);
//Sync Read for Arrays
flatLocalStorage.cache.clear();
flatLocalStorage.arrayDebouncers.clear();
console.info("\nAccessing data.arrTest directly after cache clear...");
const syncReadArr = flatLocalStorage.data.arrTest;
console.log("Synchronously read arrTest from proxy:", JSON.stringify(syncReadArr));How does the magic work?
- Deep Proxy: We intercept all
get,set, anddeleteoperations on the object, tracking the exact path (e.g.,["user", "profile", "name"]). - Schema-Driven Flat Structure: In
FlatJSONStorage, we maintain a schema to flatten nested JSON objects in the storage layer. When you modifydata.a.b.c, only thea.b.ckey is updated in the adapter. Say goodbye to the performance nightmare of saving the whole object!
This project is under active development, but the current version is stable and usable.
- Whole JSON Storage (
WebStorageItemStorage) - localStorage / sessionStorage adaptation
- Flat Storage Engine (
FlatJSONStorage/FlatWebStorage) - Smart debouncing for array operations
- Schema-based deep property traversal and loading
- Synchronous read flat storage
- Docs
This is the initial roadmap. See Github issues for more incoming.
(XML storage was planned but dropped. We are focusing on making JSON storage perfect!)
Issues, PRs, and suggestions are super welcome! Let's make state persistence elegant, together!
/**
* @author 145a
* @license AGPL-3.0
*/
export type DeepProxyHandler = {
has?: (target: Object, path: readonly string[]) => boolean;
get?: (target: Object, path: readonly string[], receiver: Object) => any;
set?: (target: Object, path: readonly string[], value: any, receiver: Object | undefined) => boolean;
deleteProperty?: (target: Object, path: readonly string[]) => boolean;
ownKeys?: (target: Object, path: readonly string[]) => string[];
getOwnPropertyDescriptor?: (target: Object, path: readonly string[], prop: string | symbol) => PropertyDescriptor | undefined;
};
/**
* @param {object} target
* @param {DeepProxyHandler} handler
* @param {readonly string[]} [currentPath=[]]
* @returns {*}
*/
declare function createDeepProxy(target: object, handler: DeepProxyHandler, currentPath?: readonly string[]): any;
declare class StorageInterface {
scheduledUpdate: boolean | undefined;
/**
* @param {*} observed
* @returns {*}
*/
static getRaw(observed: any): any;
constructor();
/** @returns {Promise<void>|void} */
init(): Promise<void> | void;
isReady: boolean;
assertReady(): void;
/** @type {any} */
_data: any;
get data(): any;
/** @type {number|undefined} */
updateTimerID: number | undefined;
update(): Promise<void>;
}
declare class DebounceStorage extends StorageInterface {
updator: (value: any) => Promise<void> | void;
updateDelayMs: number;
/**
* @param {Exclude<any, undefined>} initialValue
* @param {(value: any)=>Promise<void>|void} updator
* @param {number} updateDelayMs
* @param {boolean} structuredCloneExempt Use raw initialValue as cache. DO NOT MODIFY THE OBJECT EVER IF YOU ENABLE THIS.
*/
constructor(initialValue: Exclude<any, undefined>, updator: (value: any) => Promise<void> | void, updateDelayMs?: number, structuredCloneExempt?: boolean);
/** @returns {Promise<void>|void} */
init(): Promise<void> | void;
/** @protected */
protected _cache: {};
get cache(): {};
scheduledUpdate: boolean;
abort(): void;
update(): Promise<void>;
requestUpdate(): void;
}
declare class JSONDebounceStorage extends DebounceStorage {
/**
* @param {object} initialValue
* @param {(value: Object)=>Promise<void>|void} updator
* @param {{updateDelayMs?: number, structuredCloneExempt?: boolean, onSet?: (value: Object, path: readonly string[])=>void}} options
*/
constructor(initialValue: object, updator: (value: Object) => Promise<void> | void, { updateDelayMs, structuredCloneExempt, onSet }?: {
updateDelayMs?: number;
structuredCloneExempt?: boolean;
onSet?: (value: Object, path: readonly string[]) => void;
});
/** @type {ReturnType<typeof createDeepProxy>} */
_data: ReturnType<typeof createDeepProxy>;
}
export type StorageUpdater = (name: string, data: Object) => void;
/** @deprecated */
declare class JSONStorageAdaptor {
initialValueGetter: (name: string) => Promise<Object> | Object;
updater: StorageUpdater;
/**
* @typedef {(name: string, data: Object)=>void} StorageUpdater
* @param {(name: string)=>Promise<Object>|Object} initialValueGetter
* @param {StorageUpdater} updater
*/
constructor(initialValueGetter: (name: string) => Promise<Object> | Object, updater: StorageUpdater);
}
export type FlatStorageAdapter = {
get: (key: string) => Promise<any> | any;
set: (key: string, value: any) => Promise<void> | void;
delete: (key: string) => Promise<void> | void;
};
export type FlatSchemaValueType = "0" | "{}" | "[]";
declare class FlatJSONStorage extends StorageInterface {
/** @type {FlatStorageAdapter} */
adapter: FlatStorageAdapter;
/** @type {{ [k: string]: any }} */
schema: {
[k: string]: any;
};
/** @type {Map<string, any>} */
cache: Map<string, any>;
/** @type {Map<string, JSONDebounceStorage>} */
arrayDebouncers: Map<string, JSONDebounceStorage>;
/**
* @type {DeepProxyHandler & { set: NonNullable<DeepProxyHandler["set"]>}}
* @readonly
*/
_handler: DeepProxyHandler & {
set: NonNullable<DeepProxyHandler["set"]>;
};
schemaStorage: JSONDebounceStorage | undefined;
/**
* @param {FlatStorageAdapter} adapter
* @param {object} [options]
* @param {string} [options.namespace]
*/
constructor(adapter: FlatStorageAdapter, options?: {
namespace?: string;
});
/** @override */
init(): Promise<void>;
/**
* @param {readonly string[]} path
*/
_clearCache(path: readonly string[]): Promise<void>;
/**
* @param {string} [key=""]
* @returns {string[]}
*/
getSubKeys(key?: string): string[];
/**
* @param {readonly string[]} path
*/
_deleteSchemaNode(path: readonly string[]): void;
/** @param {readonly string[]} path */
_getSchemaNode(path: readonly string[]): {
[k: string]: any;
} | undefined;
/** @deprecated */
getSchema(): {
[k: string]: any;
};
/**
* @param {string} key
* @param {any[]} [initialArr]
* @returns {JSONDebounceStorage}
*/
_getArrayDebouncer(key: string, initialArr?: any[]): JSONDebounceStorage;
/**
* @param {string} key
*/
_abortArrayDebouncer(key: string): void;
/**
* @param {string} [key=""]
*/
load(key?: string): any;
/**
* @param {readonly string[]} strings
* @param {readonly any[]} keys
*/
get(strings: readonly string[], ...keys: readonly any[]): Promise<any>;
/**
* @param {string} key
*/
delete(key?: string): Promise<void>;
}
declare class WebStorageItemStorage extends JSONDebounceStorage {
/**
* @param {string} itemName
* @param {Storage} instance
* @param {number=} updateDelayMs
*/
constructor(itemName: string, instance: Storage, updateDelayMs?: number | undefined);
}
declare class FlatWebStorage extends FlatJSONStorage {
/**
* @param {object} options
* @param {string} [options.namespace]
* @param {Storage} options.instance
*/
constructor(options: {
namespace?: string;
instance: Storage;
});
}
/** @deprecated */
declare class StorageHelper {
updateDelayMs: number;
constructor(updateDelayMs?: number);
/**
* @deprecated
* @param {string} name
* @param {JSONStorageAdaptor} adaptor
* @returns {Promise<any>}
*/
getStorage(name: string, adaptor: JSONStorageAdaptor): Promise<any>;
/** @deprecated */
static ADAPTORS: {
LOCAL_STORAGE: JSONStorageAdaptor;
};
}
export {
/** @deprecated */ JSONStorageAdaptor,
/** @deprecated */ JSONStorageAdaptor as StorageAdaptor, WebStorageItemStorage, StorageHelper, StorageInterface, FlatJSONStorage, FlatWebStorage };