@tanstack/db
Version:
A reactive client store for building super fast apps on sync
263 lines (262 loc) • 8.08 kB
JavaScript
;
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const errors = require("./errors.cjs");
function validateJsonSerializable(value, operation) {
try {
JSON.stringify(value);
} catch (error) {
throw new errors.SerializationError(
operation,
error instanceof Error ? error.message : String(error)
);
}
}
function generateUuid() {
return crypto.randomUUID();
}
function localStorageCollectionOptions(config) {
if (!config.storageKey) {
throw new errors.StorageKeyRequiredError();
}
const storage = config.storage || (typeof window !== `undefined` ? window.localStorage : null);
if (!storage) {
throw new errors.NoStorageAvailableError();
}
const storageEventApi = config.storageEventApi || (typeof window !== `undefined` ? window : null);
if (!storageEventApi) {
throw new errors.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 errors.InvalidStorageDataFormatError(storageKey, key);
}
});
} else {
throw new errors.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;
}
exports.localStorageCollectionOptions = localStorageCollectionOptions;
//# sourceMappingURL=local-storage.cjs.map