@firecms/core
Version:
Awesome Firebase/Firestore-based headless open-source CMS
205 lines (177 loc) • 6.7 kB
text/typescript
import { EntityReference, GeoPoint, Vector } from "../types";
// Define a unique prefix for entity keys in localStorage to avoid key collisions
const LOCAL_STORAGE_PREFIX = "entity_cache::";
// In-memory cache to store entities for quick access
const entityCache: Map<string, object> = new Map();
// Check `localStorage` availability once during initialization
const isLocalStorageAvailable = typeof localStorage !== "undefined";
// Define custom replacer for JSON.stringify
function customReplacer(key: string): any {
// @ts-ignore
const value = this[key];
// Handle Date objects
// @ts-ignore
if (value instanceof Date) {
return { __type: "Date", value: value.toISOString() };
}
// Handle EntityReference
// @ts-ignore
if (value instanceof EntityReference) {
return { __type: "EntityReference", id: value.id, path: value.path };
}
// Handle GeoPoint
// @ts-ignore
if (value instanceof GeoPoint) {
return { __type: "GeoPoint", latitude: value.latitude, longitude: value.longitude };
}
// Handle Vector
// @ts-ignore
if (value instanceof Vector) {
return { __type: "Vector", value: value.value };
}
return value;
}
// Define custom reviver for JSON.parse
function customReviver(key: string, value: any): any {
if (value && typeof value === "object" && "__type" in value) {
switch (value.__type) {
case "Date":
return new Date(value.value);
case "EntityReference":
return new EntityReference(value.id, value.path);
case "GeoPoint":
return new GeoPoint(value.latitude, value.longitude);
case "Vector":
return new Vector(value.value);
default:
return value;
}
}
return value;
}
// Initialize the in-memory cache by loading entities from `localStorage`
if (isLocalStorageAvailable) {
try {
// Iterate over all keys in localStorage to find those with the specified prefix
for (let i = 0; i < localStorage.length; i++) {
const fullKey = localStorage.key(i);
if (fullKey && fullKey.startsWith(LOCAL_STORAGE_PREFIX)) {
const path = fullKey.substring(LOCAL_STORAGE_PREFIX.length);
const entityString = localStorage.getItem(fullKey);
if (entityString) {
try {
const entity: object = JSON.parse(entityString, customReviver);
entityCache.set(path, entity);
} catch (parseError) {
console.error(
`Failed to parse entity for path "${path}" from localStorage:`,
parseError
);
}
}
}
}
} catch (error) {
console.error("Error accessing localStorage during initialization:", error);
}
}
/**
* Saves data to the in-memory cache and persists it individually in `localStorage`.
* @param path - The unique path/key for the data.
* @param data - The data to cache and persist.
*/
export function saveEntityToCache(path: string, data: object): void {
// Update the in-memory cache
entityCache.set(path, data);
// Persist the data individually in localStorage
if (isLocalStorageAvailable) {
try {
const key = LOCAL_STORAGE_PREFIX + path;
const entityString = JSON.stringify(data, customReplacer);
localStorage.setItem(key, entityString);
} catch (error) {
console.error(
`Failed to save entity for path "${path}" to localStorage:`,
error
);
}
}
}
/**
* Retrieves an entity from the in-memory cache or `localStorage`.
* If the entity is not in the cache but exists in `localStorage`, it loads it into the cache.
* @param path - The unique path/key for the entity.
* @returns The cached entity or `undefined` if not found.
*/
export function getEntityFromCache(path: string): object | undefined {
// Attempt to retrieve the entity from the in-memory cache
if (entityCache.has(path)) {
return entityCache.get(path);
}
// If not in the cache, attempt to load it from localStorage
if (isLocalStorageAvailable) {
try {
const key = LOCAL_STORAGE_PREFIX + path;
const entityString = localStorage.getItem(key);
if (entityString) {
const entity: object = JSON.parse(entityString, customReviver);
entityCache.set(path, entity); // Update the cache
return entity;
}
} catch (error) {
console.error(
`Failed to load entity for path "${path}" from localStorage:`,
error
);
}
}
// Entity not found
return undefined;
}
export function hasEntityInCache(path: string): boolean {
return entityCache.has(path);
}
/**
* Removes an entity from both the in-memory cache and `localStorage`.
* @param path - The unique path/key for the entity to remove.
*/
export function removeEntityFromCache(path: string): void {
console.debug("Removing entity from cache", path);
// Remove from the in-memory cache
entityCache.delete(path);
// Remove from localStorage
if (isLocalStorageAvailable) {
try {
const key = LOCAL_STORAGE_PREFIX + path;
localStorage.removeItem(key);
} catch (error) {
console.error(
`Failed to remove entity for path "${path}" from localStorage:`,
error
);
}
}
}
/**
* Clears the entire in-memory cache and removes all related entities from `localStorage`.
*/
export function clearEntityCache(): void {
// Clear the in-memory cache
entityCache.clear();
// Remove all entities with the specified prefix from localStorage
if (isLocalStorageAvailable) {
try {
const keysToRemove: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const fullKey = localStorage.key(i);
if (fullKey && fullKey.startsWith(LOCAL_STORAGE_PREFIX)) {
keysToRemove.push(fullKey);
}
}
// Remove the keys after collecting them to avoid issues while iterating
keysToRemove.forEach((key) => localStorage.removeItem(key));
} catch (error) {
console.error("Failed to clear entity cache from localStorage:", error);
}
}
}