UNPKG

@stainless-code/persist

Version:

Hydration-aware persistence for any reactive store — zero-dep persistSource core; codecs, backends, cross-tab transport, source + framework hydration adapters ship as opt-in recipes

68 lines (67 loc) • 2.54 kB
//#region src/adapters/backends/compressed.ts /** * Native `CompressionStream`/`DecompressionStream` compression over a * string-wire `StateStorage`. Formats: `gzip` (default), `deflate`, * `deflate-raw`; output is base64 so it stays string-wire. * * A backend **wrapper**, not a sync `StorageCodec`, because the stream APIs * are async. Compose: `createStorage(() => createCompressedStorage(backend), codec)`. * Returns `undefined` when the stream APIs are unavailable. Stacks with * `createEncryptedStorage` (compress-then-encrypt is the standard order). * * @example * ```ts * import { createStorage } from "@stainless-code/persist"; * import { createCompressedStorage } from "@stainless-code/persist/backends/compressed"; * import { serovalCodec } from "@stainless-code/persist/codecs/seroval"; * * const storage = createStorage<Prefs>( * () => createCompressedStorage(() => localStorage)!, * serovalCodec(), * ); * ``` */ function createCompressedStorage(getStorage, options) { if (typeof CompressionStream === "undefined" || typeof DecompressionStream === "undefined") return; let backend; try { backend = getStorage(); } catch { return; } if (typeof backend.getItem !== "function" || typeof backend.setItem !== "function" || typeof backend.removeItem !== "function") return; const format = options?.format ?? "gzip"; return { getItem: async (name) => { const raw = await backend.getItem(name); if (raw == null) return null; return decompress(raw, format); }, setItem: async (name, value) => { const compressed = await compress(value, format); await backend.setItem(name, compressed); }, removeItem: (name) => backend.removeItem(name) }; } async function compress(plaintext, format) { const stream = new Blob([new TextEncoder().encode(plaintext)]).stream().pipeThrough(new CompressionStream(format)); return toBase64(new Uint8Array(await new Response(stream).arrayBuffer())); } async function decompress(payload, format) { const bytes = fromBase64(payload); const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream(format)); const buf = await new Response(stream).arrayBuffer(); return new TextDecoder().decode(buf); } function toBase64(bytes) { return btoa(Array.from(bytes, (b) => String.fromCharCode(b)).join("")); } function fromBase64(s) { const bin = atob(s); const bytes = new Uint8Array(new ArrayBuffer(bin.length)); for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i); return bytes; } //#endregion export { createCompressedStorage };