UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

263 lines (262 loc) 8.07 kB
import { StorageKeyRequiredError, NoStorageAvailableError, NoStorageEventApiError, InvalidStorageDataFormatError, InvalidStorageObjectFormatError, SerializationError } from "./errors.js"; function validateJsonSerializable(value, operation) { try { JSON.stringify(value); } catch (error) { throw new SerializationError( operation, error instanceof Error ? error.message : String(error) ); } } function generateUuid() { return crypto.randomUUID(); } function localStorageCollectionOptions(config) { if (!config.storageKey) { throw new StorageKeyRequiredError(); } const storage = config.storage || (typeof window !== `undefined` ? window.localStorage : null); if (!storage) { throw new NoStorageAvailableError(); } const storageEventApi = config.storageEventApi || (typeof window !== `undefined` ? window : null); if (!storageEventApi) { throw new NoStorageEventApiError(); } const lastKnownData = /* @__PURE__ */ new Map(); const sync = createLocalStorageSync( config.storageKey, storage, storageEventApi, config.getKey, lastKnownData ); const triggerLocalSync = () => { if (sync.manualTrigger) { sync.manualTrigger(); } }; const saveToStorage = (dataMap) => { try { const objectData = {}; dataMap.forEach((storedItem, key) => { objectData[String(key)] = storedItem; }); const serialized = JSON.stringify(objectData); storage.setItem(config.storageKey, serialized); } catch (error) { console.error( `[LocalStorageCollection] Error saving data to storage key "${config.storageKey}":`, error ); throw error; } }; const clearStorage = () => { storage.removeItem(config.storageKey); }; const getStorageSize = () => { const data = storage.getItem(config.storageKey); return data ? new Blob([data]).size : 0; }; const wrappedOnInsert = async (params) => { params.transaction.mutations.forEach((mutation) => { validateJsonSerializable(mutation.modified, `insert`); }); let handlerResult = {}; if (config.onInsert) { handlerResult = await config.onInsert(params) ?? {}; } const currentData = loadFromStorage( config.storageKey, storage ); params.transaction.mutations.forEach((mutation) => { const key = config.getKey(mutation.modified); const storedItem = { versionKey: generateUuid(), data: mutation.modified }; currentData.set(key, storedItem); }); saveToStorage(currentData); triggerLocalSync(); return handlerResult; }; const wrappedOnUpdate = async (params) => { params.transaction.mutations.forEach((mutation) => { validateJsonSerializable(mutation.modified, `update`); }); let handlerResult = {}; if (config.onUpdate) { handlerResult = await config.onUpdate(params) ?? {}; } const currentData = loadFromStorage( config.storageKey, storage ); params.transaction.mutations.forEach((mutation) => { const key = config.getKey(mutation.modified); const storedItem = { versionKey: generateUuid(), data: mutation.modified }; currentData.set(key, storedItem); }); saveToStorage(currentData); triggerLocalSync(); return handlerResult; }; const wrappedOnDelete = async (params) => { let handlerResult = {}; if (config.onDelete) { handlerResult = await config.onDelete(params) ?? {}; } const currentData = loadFromStorage( config.storageKey, storage ); params.transaction.mutations.forEach((mutation) => { const key = config.getKey(mutation.original); currentData.delete(key); }); saveToStorage(currentData); triggerLocalSync(); return handlerResult; }; const { storageKey: _storageKey, storage: _storage, storageEventApi: _storageEventApi, onInsert: _onInsert, onUpdate: _onUpdate, onDelete: _onDelete, id, ...restConfig } = config; const collectionId = id ?? `local-collection:${config.storageKey}`; return { ...restConfig, id: collectionId, sync, onInsert: wrappedOnInsert, onUpdate: wrappedOnUpdate, onDelete: wrappedOnDelete, utils: { clearStorage, getStorageSize } }; } function loadFromStorage(storageKey, storage) { try { const rawData = storage.getItem(storageKey); if (!rawData) { return /* @__PURE__ */ new Map(); } const parsed = JSON.parse(rawData); const dataMap = /* @__PURE__ */ new Map(); if (typeof parsed === `object` && parsed !== null && !Array.isArray(parsed)) { Object.entries(parsed).forEach(([key, value]) => { if (value && typeof value === `object` && `versionKey` in value && `data` in value) { const storedItem = value; dataMap.set(key, storedItem); } else { throw new InvalidStorageDataFormatError(storageKey, key); } }); } else { throw new InvalidStorageObjectFormatError(storageKey); } return dataMap; } catch (error) { console.warn( `[LocalStorageCollection] Error loading data from storage key "${storageKey}":`, error ); return /* @__PURE__ */ new Map(); } } function createLocalStorageSync(storageKey, storage, storageEventApi, _getKey, lastKnownData) { let syncParams = null; const findChanges = (oldData, newData) => { const changes = []; oldData.forEach((oldStoredItem, key) => { const newStoredItem = newData.get(key); if (!newStoredItem) { changes.push({ type: `delete`, key, value: oldStoredItem.data }); } else if (oldStoredItem.versionKey !== newStoredItem.versionKey) { changes.push({ type: `update`, key, value: newStoredItem.data }); } }); newData.forEach((newStoredItem, key) => { if (!oldData.has(key)) { changes.push({ type: `insert`, key, value: newStoredItem.data }); } }); return changes; }; const processStorageChanges = () => { if (!syncParams) return; const { begin, write, commit } = syncParams; const newData = loadFromStorage(storageKey, storage); const changes = findChanges(lastKnownData, newData); if (changes.length > 0) { begin(); changes.forEach(({ type, value }) => { if (value) { validateJsonSerializable(value, type); write({ type, value }); } }); commit(); lastKnownData.clear(); newData.forEach((storedItem, key) => { lastKnownData.set(key, storedItem); }); } }; const syncConfig = { sync: (params) => { const { begin, write, commit, markReady } = params; syncParams = params; const initialData = loadFromStorage(storageKey, storage); if (initialData.size > 0) { begin(); initialData.forEach((storedItem) => { validateJsonSerializable(storedItem.data, `load`); write({ type: `insert`, value: storedItem.data }); }); commit(); } lastKnownData.clear(); initialData.forEach((storedItem, key) => { lastKnownData.set(key, storedItem); }); markReady(); const handleStorageEvent = (event) => { if (event.key !== storageKey || event.storageArea !== storage) { return; } processStorageChanges(); }; storageEventApi.addEventListener(`storage`, handleStorageEvent); }, /** * Get sync metadata - returns storage key information * @returns Object containing storage key and storage type metadata */ getSyncMetadata: () => ({ storageKey, storageType: storage === (typeof window !== `undefined` ? window.localStorage : null) ? `localStorage` : `custom` }), // Manual trigger function for local updates manualTrigger: processStorageChanges }; return syncConfig; } export { localStorageCollectionOptions }; //# sourceMappingURL=local-storage.js.map