UNPKG

accounts

Version:

Tempo Accounts SDK

173 lines 6.44 kB
import { createStore, del, get, set } from 'idb-keyval'; import { Json } from 'ox'; /** Creates a storage adapter from a custom implementation, optionally scoping all keys under a prefix. */ export function from(storage, options = {}) { const key = options.key ?? 'tempo'; const prefix = `${key}.`; return { getItem: (name) => storage.getItem(`${prefix}${name}`), setItem: (name, value) => storage.setItem(`${prefix}${name}`, value), removeItem: (name) => storage.removeItem(`${prefix}${name}`), }; } /** * Combines multiple storage adapters into one. Reads return the first * non-null result; writes propagate to all storages (failures are isolated * via `Promise.allSettled`). */ export function combine(...storages) { return { async getItem(name) { const results = await Promise.allSettled(storages.map((x) => x.getItem(name))); const result = results.find((x) => x.status === 'fulfilled' && x.value != null); if (result?.status !== 'fulfilled') return null; return result.value; }, async removeItem(name) { await Promise.allSettled(storages.map((x) => x.removeItem(name))); }, async setItem(name, value) { await Promise.allSettled(storages.map((x) => x.setItem(name, value))); }, }; } /** Creates a `document.cookie`-backed storage adapter. Uses `SameSite=None; Secure` with a 1-year expiry. Deep objects are flattened into individual cookies to stay within the 4KB-per-cookie browser limit. */ export function cookie(options = {}) { function getRaw(name) { return document.cookie.split('; ').find((x) => x.startsWith(`${name}=`)); } function setRaw(name, value) { document.cookie = `${name}=${value};path=/;samesite=None;secure;max-age=31536000`; } function removeRaw(name) { document.cookie = `${name}=;max-age=-1;path=/`; } function flatten(prefix, value, result = []) { if (Array.isArray(value)) { for (let i = 0; i < value.length; i++) flatten(`${prefix}[${i}]`, value[i], result); // Store length so we know how many indices to reconstruct. result.push([`${prefix}.__length`, Json.stringify(value.length)]); } else if (value !== null && typeof value === 'object') { for (const [k, v] of Object.entries(value)) flatten(`${prefix}.${k}`, v, result); } else result.push([prefix, Json.stringify(value)]); return result; } function unflatten(prefix) { // Check for a direct (leaf) cookie first. const direct = getRaw(prefix); if (direct) { try { return Json.parse(direct.substring(prefix.length + 1)); } catch { return null; } } // Check if this is an array (has a __length cookie). const lengthCookie = getRaw(`${prefix}.__length`); if (lengthCookie) { const length = Json.parse(lengthCookie.substring(`${prefix}.__length`.length + 1)); const result = []; for (let i = 0; i < length; i++) result.push(unflatten(`${prefix}[${i}]`)); return result; } // Collect all sub-keys (object children use `.`, array children use `[`). const dotPrefix = `${prefix}.`; const bracketPrefix = `${prefix}[`; const children = document.cookie .split('; ') .filter((x) => x.startsWith(dotPrefix) || x.startsWith(bracketPrefix)); if (children.length === 0) return null; const result = {}; for (const entry of children) { const key = entry.substring(dotPrefix.length, entry.indexOf('=')); const segment = key.split(/[.[]/)[0]; if (segment === '__length') continue; if (!(segment in result)) result[segment] = unflatten(`${dotPrefix}${segment}`); } return result; } return from({ getItem(name) { return unflatten(name); }, setItem(name, value) { // Remove existing keys before writing. this.removeItem(name); for (const [k, v] of flatten(name, value)) setRaw(k, v); }, removeItem(name) { removeRaw(name); for (const entry of document.cookie.split('; ')) if (entry.startsWith(`${name}.`) || entry.startsWith(`${name}[`)) removeRaw(entry.substring(0, entry.indexOf('='))); }, }, options); } /** Creates an IndexedDB-backed storage adapter. Stores raw values (no JSON serialization). */ export function idb(options = {}) { const store = typeof indexedDB !== 'undefined' ? createStore('tempo', 'store') : undefined; return from({ async getItem(name) { const value = await get(name, store); if (value === null) return null; return value; }, async setItem(name, value) { await set(name, value, store); }, async removeItem(name) { await del(name, store); }, }, options); } /** Creates a `localStorage`-backed storage adapter. */ export function localStorage(options = {}) { return from({ getItem(name) { const value = globalThis.localStorage.getItem(name); if (value === null) return null; try { return Json.parse(value); } catch { return null; } }, setItem(name, value) { globalThis.localStorage.setItem(name, Json.stringify(value)); }, removeItem(name) { globalThis.localStorage.removeItem(name); }, }, options); } /** Creates an in-memory storage adapter. Useful for SSR and tests. */ export function memory(options = {}) { const store = new Map(); return from({ getItem(name) { return store.get(name) ?? null; }, setItem(name, value) { store.set(name, value); }, removeItem(name) { store.delete(name); }, }, options); } //# sourceMappingURL=Storage.js.map