From 05b5cf053205d2f0e0ab4ee6faff1dbb43cbe518 Mon Sep 17 00:00:00 2001 From: Tsotne Nazarashvili Date: Thu, 16 Nov 2023 00:43:47 +0400 Subject: [PATCH 1/2] Add web storage driver for localStorage and sessionStorage --- docs/content/6.drivers/web-storage.md | 26 ++++++++++ src/drivers/web-storage.ts | 75 +++++++++++++++++++++++++++ src/index.ts | 7 ++- test/drivers/web-storage.test.ts | 71 +++++++++++++++++++++++++ 4 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 docs/content/6.drivers/web-storage.md create mode 100644 src/drivers/web-storage.ts create mode 100644 test/drivers/web-storage.test.ts diff --git a/docs/content/6.drivers/web-storage.md b/docs/content/6.drivers/web-storage.md new file mode 100644 index 00000000..43cb8f1b --- /dev/null +++ b/docs/content/6.drivers/web-storage.md @@ -0,0 +1,26 @@ +--- +navigation.title: Web Storage +--- + +# Web Storage + +Store data in [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) or [sessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage). + +```js +import { createStorage } from "unstorage"; +import webStorageDriver from "unstorage/drivers/web-storage"; + +const localStorage = createStorage({ + driver: webStorageDriver({ base: "app:", storageArea: window.localStorage }), +}); + +const sessionStorage = createStorage({ + driver: webStorageDriver({ base: "app:", storageArea: window.sessionStorage }), +}); +``` + +**Options:** + +- `base`: Add `${base}:` to all keys to avoid collision +- `storageArea`: Provide [Storage](https://developer.mozilla.org/en-US/docs/Web/API/Storage) object (**required**) +- `window`: Optionally provide `window` object diff --git a/src/drivers/web-storage.ts b/src/drivers/web-storage.ts new file mode 100644 index 00000000..8318603f --- /dev/null +++ b/src/drivers/web-storage.ts @@ -0,0 +1,75 @@ +import { createRequiredError, defineDriver } from "./utils"; + +export interface WebStorageOptions { + base?: string; + window?: typeof window; + storageArea?: Storage; +} + +const DRIVER_NAME = "web-storage"; + +export default defineDriver((opts: WebStorageOptions = {}) => { + if (!opts.window) { + opts.window = typeof window !== "undefined" ? window : undefined; + } + + if (!opts.storageArea) { + throw createRequiredError(DRIVER_NAME, "storageArea"); + } + + const r = (key: string) => (opts.base ? opts.base + ":" : "") + key; + + let _storageListener: undefined | ((ev: StorageEvent) => void); + const _unwatch = () => { + if (_storageListener) { + opts.window?.removeEventListener("storage", _storageListener); + } + _storageListener = undefined; + }; + + return { + name: DRIVER_NAME, + options: opts, + hasItem(key) { + return Object.prototype.hasOwnProperty.call(opts.storageArea!, r(key)); + }, + getItem(key) { + return opts.storageArea!.getItem(r(key)); + }, + setItem(key, value) { + return opts.storageArea!.setItem(r(key), value); + }, + removeItem(key) { + return opts.storageArea!.removeItem(r(key)); + }, + getKeys() { + return Object.keys(opts.storageArea!); + }, + clear() { + if (!opts.base) { + opts.storageArea!.clear(); + } else { + for (const key of Object.keys(opts.storageArea!)) { + opts.storageArea?.removeItem(key); + } + } + if (opts.window && _storageListener) { + opts.window.removeEventListener("storage", _storageListener); + } + }, + watch(callback) { + if (!opts.window) { + return _unwatch; + } + _storageListener = (ev: StorageEvent) => { + if (ev.storageArea !== opts.storageArea) return; + + if (ev.key) { + callback(ev.newValue ? "update" : "remove", ev.key); + } + }; + opts.window.addEventListener("storage", _storageListener); + return _unwatch; + }, + }; +}); diff --git a/src/index.ts b/src/index.ts index 26f4875a..84428358 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,7 +17,6 @@ export const builtinDrivers = { github: "unstorage/drivers/github", http: "unstorage/drivers/http", indexedb: "unstorage/drivers/indexedb", - localStorage: "unstorage/drivers/localstorage", lruCache: "unstorage/drivers/lru-cache", memory: "unstorage/drivers/memory", mongodb: "unstorage/drivers/mongodb", @@ -25,9 +24,13 @@ export const builtinDrivers = { overlay: "unstorage/drivers/overlay", planetscale: "unstorage/drivers/planetscale", redis: "unstorage/drivers/redis", - sessionStorage: "unstorage/drivers/session-storage", vercelKV: "unstorage/drivers/vercel-kv", + webStorage: "unstorage/drivers/web-storage", + /** @deprecated */ + sessionStorage: "unstorage/drivers/session-storage", + /** @deprecated */ + localStorage: "unstorage/drivers/localstorage", /** @deprecated */ "cloudflare-kv-binding": "unstorage/drivers/cloudflare-kv-binding", /** @deprecated */ diff --git a/test/drivers/web-storage.test.ts b/test/drivers/web-storage.test.ts new file mode 100644 index 00000000..b3342c7e --- /dev/null +++ b/test/drivers/web-storage.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect, vi } from "vitest"; +import driver from "../../src/drivers/web-storage"; +import { testDriver } from "./utils"; +import { JSDOM } from "jsdom"; + +describe("drivers: web-storage", () => { + const jsdom = new JSDOM("", { + url: "http://localhost", + }); + jsdom.virtualConsole.sendTo(console); + + testDriver({ + driver: driver({ window: jsdom.window as unknown as typeof window, storageArea: jsdom.window.localStorage}), + additionalTests: (ctx) => { + it("check localstorage", () => { + expect(jsdom.window.localStorage.getItem("s1:a")).toBe("test_data"); + }); + it("watch localstorage", async () => { + const watcher = vi.fn(); + await ctx.storage.watch(watcher); + + // Emulate + // jsdom.window.localStorage.setItem('s1:random_file', 'random') + const ev = jsdom.window.document.createEvent("CustomEvent"); + ev.initEvent("storage", true); + // @ts-ignore + ev.key = "s1:random_file"; + // @ts-ignore + ev.newValue = "random"; + // @ts-ignore + ev.storageArea = jsdom.window.localStorage; + jsdom.window.dispatchEvent(ev); + + expect(watcher).toHaveBeenCalledWith("update", "s1:random_file"); + }); + it("unwatch localstorage", async () => { + const watcher = vi.fn(); + const unwatch = await ctx.storage.watch(watcher); + + // Emulate + // jsdom.window.localStorage.setItem('s1:random_file', 'random') + const ev = jsdom.window.document.createEvent("CustomEvent"); + ev.initEvent("storage", true); + // @ts-ignore + ev.key = "s1:random_file"; + // @ts-ignore + ev.newValue = "random"; + // @ts-ignore + ev.storageArea = jsdom.window.localStorage; + + const ev2 = jsdom.window.document.createEvent("CustomEvent"); + ev2.initEvent("storage", true); + // @ts-ignore + ev2.key = "s1:random_file2"; + // @ts-ignore + ev2.newValue = "random"; + // @ts-ignore + ev.storageArea = jsdom.window.localStorage; + + jsdom.window.dispatchEvent(ev); + + await unwatch(); + + jsdom.window.dispatchEvent(ev2); + + expect(watcher).toHaveBeenCalledWith("update", "s1:random_file"); + expect(watcher).toHaveBeenCalledTimes(1); + }); + }, + }); +}); From 21e83ffd129feca1b9eacfe100022c6761f734dc Mon Sep 17 00:00:00 2001 From: Tsotne Nazarashvili Date: Thu, 16 Nov 2023 00:44:47 +0400 Subject: [PATCH 2/2] Refactor web storage driver to use shared code --- src/drivers/localstorage.ts | 59 ++++---------------------------- src/drivers/session-storage.ts | 62 ++++------------------------------ 2 files changed, 13 insertions(+), 108 deletions(-) diff --git a/src/drivers/localstorage.ts b/src/drivers/localstorage.ts index 38683b6d..7ef94495 100644 --- a/src/drivers/localstorage.ts +++ b/src/drivers/localstorage.ts @@ -1,4 +1,5 @@ import { createRequiredError, defineDriver } from "./utils"; +import webStorage from "./web-storage"; export interface LocalStorageOptions { base?: string; @@ -19,57 +20,9 @@ export default defineDriver((opts: LocalStorageOptions = {}) => { throw createRequiredError(DRIVER_NAME, "localStorage"); } - const r = (key: string) => (opts.base ? opts.base + ":" : "") + key; - - let _storageListener: undefined | ((ev: StorageEvent) => void); - const _unwatch = () => { - if (_storageListener) { - opts.window?.removeEventListener("storage", _storageListener); - } - _storageListener = undefined; - }; - - return { - name: DRIVER_NAME, - options: opts, - hasItem(key) { - return Object.prototype.hasOwnProperty.call(opts.localStorage!, r(key)); - }, - getItem(key) { - return opts.localStorage!.getItem(r(key)); - }, - setItem(key, value) { - return opts.localStorage!.setItem(r(key), value); - }, - removeItem(key) { - return opts.localStorage!.removeItem(r(key)); - }, - getKeys() { - return Object.keys(opts.localStorage!); - }, - clear() { - if (!opts.base) { - opts.localStorage!.clear(); - } else { - for (const key of Object.keys(opts.localStorage!)) { - opts.localStorage?.removeItem(key); - } - } - if (opts.window && _storageListener) { - opts.window.removeEventListener("storage", _storageListener); - } - }, - watch(callback) { - if (!opts.window) { - return _unwatch; - } - _storageListener = (ev: StorageEvent) => { - if (ev.key) { - callback(ev.newValue ? "update" : "remove", ev.key); - } - }; - opts.window.addEventListener("storage", _storageListener); - return _unwatch; - }, - }; + return webStorage({ + base: opts.base, + window: opts.window, + storageArea: opts.localStorage, + }) }); diff --git a/src/drivers/session-storage.ts b/src/drivers/session-storage.ts index 897167ff..ebb15f84 100644 --- a/src/drivers/session-storage.ts +++ b/src/drivers/session-storage.ts @@ -1,4 +1,5 @@ -import { createError, createRequiredError, defineDriver } from "./utils"; +import { createRequiredError, defineDriver } from "./utils"; +import webStorage from "./web-storage"; export interface SessionStorageOptions { base?: string; @@ -19,58 +20,9 @@ export default defineDriver((opts: SessionStorageOptions = {}) => { throw createRequiredError(DRIVER_NAME, "sessionStorage"); } - const r = (key: string) => (opts.base ? opts.base + ":" : "") + key; - - let _storageListener: undefined | ((ev: StorageEvent) => void); - const _unwatch = () => { - if (_storageListener) { - opts.window!.removeEventListener("storage", _storageListener); - } - _storageListener = undefined; - }; - - return { - name: DRIVER_NAME, - options: opts, - hasItem(key) { - return Object.prototype.hasOwnProperty.call(opts.sessionStorage, r(key)); - }, - getItem(key) { - return opts.sessionStorage!.getItem(r(key)); - }, - setItem(key, value) { - return opts.sessionStorage!.setItem(r(key), value); - }, - removeItem(key) { - return opts.sessionStorage!.removeItem(r(key)); - }, - getKeys() { - return Object.keys(opts.sessionStorage!); - }, - clear() { - if (!opts.base) { - opts.sessionStorage!.clear(); - } else { - for (const key of Object.keys(opts.sessionStorage!)) { - opts.sessionStorage?.removeItem(key); - } - } - if (opts.window && _storageListener) { - opts.window.removeEventListener("storage", _storageListener); - } - }, - watch(callback) { - if (!opts.window) { - return _unwatch; - } - _storageListener = ({ key, newValue }: StorageEvent) => { - if (key) { - callback(newValue ? "update" : "remove", key); - } - }; - opts.window!.addEventListener("storage", _storageListener); - - return _unwatch; - }, - }; + return webStorage({ + base: opts.base, + window: opts.window, + storageArea: opts.sessionStorage, + }) });