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