Skip to content

54145a/145Storage2

Repository files navigation

📦 145 Storage 2: My KV is a plain Object

npm versionlicenseGitHub stars

A lightweight, smart JavaScript storage library that makes state persistence as easy as modifying a plain object.

Why 145 Storage 2

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;

Core Features

  • 🪄 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 undefined or function) to keep your storage safe.
  • 🌐 Framework Agnostic: Works in any vanilla JS or framework environment.

🚀 Try it now

/**
 * @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));

🧠 Under the Hood: Deep Proxy & Flat Schema

How does the magic work?

  1. Deep Proxy: We intercept all get, set, and delete operations on the object, tracking the exact path (e.g., ["user", "profile", "name"]).
  2. Schema-Driven Flat Structure: In FlatJSONStorage, we maintain a schema to flatten nested JSON objects in the storage layer. When you modify data.a.b.c, only the a.b.c key is updated in the adapter. Say goodbye to the performance nightmare of saving the whole object!

🗺 Roadmap

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!)

🤝 Contributing

Issues, PRs, and suggestions are super welcome! Let's make state persistence elegant, together!

📚 Reference

/**
 * @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 };

About

A lightweight, intelligent storage library for JavaScript that makes state persistence as easy as modifying an object.

Topics

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Contributors