diff --git a/docs/content/6.drivers/session-storage.md b/docs/content/6.drivers/session-storage.md new file mode 100644 index 00000000..789345dc --- /dev/null +++ b/docs/content/6.drivers/session-storage.md @@ -0,0 +1,22 @@ +--- +navigation.title: Session Storage +--- + +# Session Storage + +Store data in [sessionStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage). + +```js +import { createStorage } from "unstorage"; +import sessionStorageDriver from "unstorage/drivers/session-storage"; + +const storage = createStorage({ + driver: sessionStorageDriver({ base: "app:" }), +}); +``` + +**Options:** + +- `base`: Add `${base}:` to all keys to avoid collision +- `sessionStorage`: Optionally provide `sessionStorage` object +- `window`: Optionally provide `window` object diff --git a/src/drivers/session-storage.ts b/src/drivers/session-storage.ts new file mode 100644 index 00000000..7fb6745e --- /dev/null +++ b/src/drivers/session-storage.ts @@ -0,0 +1,71 @@ +import { defineDriver } from "./utils"; + +export interface SessionStorageOptions { + base?: string; + window?: typeof window; + sessionStorage?: typeof window.sessionStorage; +} + +export default defineDriver((opts: SessionStorageOptions = {}) => { + if (!opts.window) { + opts.window = typeof window !== "undefined" ? window : undefined; + } + if (!opts.sessionStorage) { + opts.sessionStorage = opts.window?.sessionStorage; + } + if (!opts.sessionStorage) { + throw new Error("sessionStorage not available"); + } + + const r = (key: string) => (opts.base ? opts.base + ":" : "") + key; + + let _storageListener: (ev: StorageEvent) => void; + + return { + name: "session-storage", + 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; + } + _storageListener = ({ key, newValue }: StorageEvent) => { + if (key) { + callback(newValue ? "update" : "remove", key); + } + }; + opts.window.addEventListener("storage", _storageListener); + + return () => { + opts.window.removeEventListener("storage", _storageListener); + _storageListener = undefined; + }; + }, + }; +}); diff --git a/src/index.ts b/src/index.ts index eb5be639..dde6255a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ export const builtinDrivers = { planetscale: "unstorage/drivers/planetscale", redis: "unstorage/drivers/redis", azureKeyVault: "unstorage/drivers/azure-key-vault", + sessionStorage: "unstorage/drivers/session-storage", }; export type BuiltinDriverName = keyof typeof builtinDrivers; diff --git a/test/drivers/session-storage.test.ts b/test/drivers/session-storage.test.ts new file mode 100644 index 00000000..f416176b --- /dev/null +++ b/test/drivers/session-storage.test.ts @@ -0,0 +1,64 @@ +import { JSDOM } from "jsdom"; +import { describe, expect, it, vi } from "vitest"; +import driver from "../../src/drivers/session-storage"; +import { testDriver } from "./utils"; + +describe("drivers: session-storage", () => { + const jsdom = new JSDOM("", { + url: "http://localhost", + }); + jsdom.virtualConsole.sendTo(console); + + testDriver({ + driver: driver({ window: jsdom.window as unknown as typeof window }), + additionalTests: (ctx) => { + it("check session storage", () => { + expect(jsdom.window.sessionStorage.getItem("s1:a")).toBe("test_data"); + }); + it("watch session storage", async () => { + const watcher = vi.fn(); + await ctx.storage.watch(watcher); + + // Emulate + // jsdom.window.sessionStorage.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"; + jsdom.window.dispatchEvent(ev); + + expect(watcher).toHaveBeenCalledWith("update", "s1:random_file"); + }); + it("unwatch session storage", async () => { + const watcher = vi.fn(); + const unwatch = await ctx.storage.watch(watcher); + + // Emulate + // jsdom.window.sessionStorage.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"; + const ev2 = jsdom.window.document.createEvent("CustomEvent"); + ev2.initEvent("storage", true); + // @ts-ignore + ev2.key = "s1:random_file2"; + // @ts-ignore + ev2.newValue = "random"; + + jsdom.window.dispatchEvent(ev); + + await unwatch(); + + jsdom.window.dispatchEvent(ev2); + + expect(watcher).toHaveBeenCalledWith("update", "s1:random_file"); + expect(watcher).toHaveBeenCalledTimes(1); + }); + }, + }); +});