UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

340 lines (339 loc) 10.3 kB
import { StorageKeyRequiredError, SerializationError, InvalidStorageDataFormatError, InvalidStorageObjectFormatError } from "./errors.js"; function validateJsonSerializable(parser, value, operation) { try { parser.stringify(value); } catch (error) { throw new SerializationError( operation, error instanceof Error ? error.message : String(error) ); } } function generateUuid() { return crypto.randomUUID(); } function encodeStorageKey(key) { if (typeof key === `number`) { return `n:${key}`; } return `s:${key}`; } function decodeStorageKey(encodedKey) { if (encodedKey.startsWith(`n:`)) { return Number(encodedKey.slice(2)); } if (encodedKey.startsWith(`s:`)) { return encodedKey.slice(2); } return encodedKey; } function createInMemoryStorage() { const storage = /* @__PURE__ */ new Map(); return { getItem(key) { return storage.get(key) ?? null; }, setItem(key, value) { storage.set(key, value); }, removeItem(key) { storage.delete(key); } }; } function createNoOpStorageEventApi() { return { addEventListener: () => { }, removeEventListener: () => { } }; } function localStorageCollectionOptions(config) { if (!config.storageKey) { throw new StorageKeyRequiredError(); } const storage = config.storage || (typeof window !== `undefined` ? window.localStorage : null) || createInMemoryStorage(); const storageEventApi = config.storageEventApi || (typeof window !== `undefined` ? window : null) || createNoOpStorageEventApi(); const parser = config.parser || JSON; const lastKnownData = /* @__PURE__ */ new Map(); const sync = createLocalStorageSync( config.storageKey, storage, storageEventApi, parser, config.getKey, lastKnownData ); const saveToStorage = (dataMap) => { try { const objectData = {}; dataMap.forEach((storedItem, key) => { objectData[encodeStorageKey(key)] = storedItem; }); const serialized = parser.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(parser, mutation.modified, `insert`); }); let handlerResult = {}; if (config.onInsert) { handlerResult = await config.onInsert(params) ?? {}; } params.transaction.mutations.forEach((mutation) => { const storedItem = { versionKey: generateUuid(), data: mutation.modified }; lastKnownData.set(mutation.key, storedItem); }); saveToStorage(lastKnownData); sync.confirmOperationsSync(params.transaction.mutations); return handlerResult; }; const wrappedOnUpdate = async (params) => { params.transaction.mutations.forEach((mutation) => { validateJsonSerializable(parser, mutation.modified, `update`); }); let handlerResult = {}; if (config.onUpdate) { handlerResult = await config.onUpdate(params) ?? {}; } params.transaction.mutations.forEach((mutation) => { const storedItem = { versionKey: generateUuid(), data: mutation.modified }; lastKnownData.set(mutation.key, storedItem); }); saveToStorage(lastKnownData); sync.confirmOperationsSync(params.transaction.mutations); return handlerResult; }; const wrappedOnDelete = async (params) => { let handlerResult = {}; if (config.onDelete) { handlerResult = await config.onDelete(params) ?? {}; } params.transaction.mutations.forEach((mutation) => { lastKnownData.delete(mutation.key); }); saveToStorage(lastKnownData); sync.confirmOperationsSync(params.transaction.mutations); 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}`; const acceptMutations = (transaction) => { const collectionMutations = transaction.mutations.filter((m) => { if (sync.collection && m.collection === sync.collection) { return true; } return m.collection.id === collectionId; }); if (collectionMutations.length === 0) { return; } for (const mutation of collectionMutations) { switch (mutation.type) { case `insert`: case `update`: validateJsonSerializable(parser, mutation.modified, mutation.type); break; case `delete`: validateJsonSerializable(parser, mutation.original, mutation.type); break; } } for (const mutation of collectionMutations) { switch (mutation.type) { case `insert`: case `update`: { const storedItem = { versionKey: generateUuid(), data: mutation.modified }; lastKnownData.set(mutation.key, storedItem); break; } case `delete`: { lastKnownData.delete(mutation.key); break; } } } saveToStorage(lastKnownData); sync.confirmOperationsSync(collectionMutations); }; return { ...restConfig, id: collectionId, sync, onInsert: wrappedOnInsert, onUpdate: wrappedOnUpdate, onDelete: wrappedOnDelete, utils: { clearStorage, getStorageSize, acceptMutations } }; } function loadFromStorage(storageKey, storage, parser) { try { const rawData = storage.getItem(storageKey); if (!rawData) { return /* @__PURE__ */ new Map(); } const parsed = parser.parse(rawData); const dataMap = /* @__PURE__ */ new Map(); if (typeof parsed === `object` && parsed !== null && !Array.isArray(parsed)) { Object.entries(parsed).forEach(([encodedKey, value]) => { if (value && typeof value === `object` && `versionKey` in value && `data` in value) { const storedItem = value; const decodedKey = decodeStorageKey(encodedKey); dataMap.set(decodedKey, storedItem); } else { throw new InvalidStorageDataFormatError(storageKey, encodedKey); } }); } 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, parser, _getKey, lastKnownData) { let syncParams = null; let collection = 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, parser); const changes = findChanges(lastKnownData, newData); if (changes.length > 0) { begin(); changes.forEach(({ type, value }) => { if (value) { validateJsonSerializable(parser, 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; collection = params.collection; const initialData = loadFromStorage(storageKey, storage, parser); if (initialData.size > 0) { begin(); initialData.forEach((storedItem) => { validateJsonSerializable(parser, 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, // Collection instance reference collection }; const confirmOperationsSync = (mutations) => { if (!syncParams) { return; } const { begin, write, commit } = syncParams; begin(); mutations.forEach((mutation) => { write({ type: mutation.type, value: mutation.type === `delete` ? mutation.original : mutation.modified }); }); commit(); }; return { ...syncConfig, confirmOperationsSync }; } export { localStorageCollectionOptions }; //# sourceMappingURL=local-storage.js.map