Spaces:
Building
Building
File size: 5,376 Bytes
f152ae2 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
import { name } from "../../package.json";
import { addLogEntry } from "./logEntries";
import { getSearchTokenHash } from "./searchTokenHash";
export type SearchResults = {
textResults: [title: string, snippet: string, url: string][];
imageResults: [
title: string,
url: string,
thumbnailUrl: string,
sourceUrl: string,
][];
};
/**
* Creates a cached version of a search function using IndexedDB for storage.
*
* @param fn - The original search function to be cached.
* @returns A new function that wraps the original, adding caching functionality.
*
* This function implements a caching mechanism for search results using IndexedDB.
* It stores search results with a 15-minute time-to-live (TTL) to improve performance
* for repeated searches. The cache is automatically cleaned of expired entries.
*
* The returned function behaves as follows:
* 1. Checks IndexedDB for a cached result matching the query.
* 2. If a valid (non-expired) cached result exists, it is returned immediately.
* 3. Otherwise, the original search function is called, and its result is both
* returned and stored in the cache for future use.
*
* If IndexedDB is not available, the function falls back to using the original
* search function without caching.
*/
function cacheSearchWithIndexedDB(
fn: (query: string, limit?: number) => Promise<SearchResults>,
): (query: string, limit?: number) => Promise<SearchResults> {
const storeName = "searches";
const timeToLive = 15 * 60 * 1000;
async function openDB(): Promise<IDBDatabase> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(name, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const db = request.result;
cleanExpiredCache(db);
resolve(db);
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
db.createObjectStore(storeName);
};
});
}
async function cleanExpiredCache(db: IDBDatabase): Promise<void> {
const transaction = db.transaction(storeName, "readwrite");
const store = transaction.objectStore(storeName);
const currentTime = Date.now();
return new Promise((resolve) => {
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest).result;
if (cursor) {
if (currentTime - cursor.value.timestamp >= timeToLive) {
cursor.delete();
}
cursor.continue();
} else {
resolve();
}
};
});
}
/**
* Generates a hash for a given query string.
*
* This function implements a simple hash algorithm:
* 1. It iterates through each character in the query string.
* 2. For each character, it updates the hash value using bitwise operations.
* 3. The final hash is converted to a 32-bit integer.
* 4. The result is returned as a base-36 string representation.
*
* @param query - The input string to be hashed.
* @returns A string representation of the hash in base-36.
*/
function hashQuery(query: string): string {
return query
.split("")
.reduce((acc, char) => ((acc << 5) - acc + char.charCodeAt(0)) | 0, 0)
.toString(36);
}
const dbPromise = openDB();
return async (query: string, limit?: number): Promise<SearchResults> => {
addLogEntry("Starting new search");
if (!indexedDB) return fn(query, limit);
const db = await dbPromise;
const transaction = db.transaction(storeName, "readwrite");
const store = transaction.objectStore(storeName);
const key = hashQuery(query);
const cachedResult = await new Promise<
| {
results: SearchResults;
timestamp: number;
}
| undefined
>((resolve) => {
const request = store.get(key);
request.onerror = () => resolve(undefined);
request.onsuccess = () => resolve(request.result);
});
if (cachedResult && Date.now() - cachedResult.timestamp < timeToLive) {
addLogEntry(
`Search cache hit, returning cached results containing ${cachedResult.results.textResults.length} texts and ${cachedResult.results.imageResults.length} images`,
);
return cachedResult.results;
}
addLogEntry("Search cache miss, fetching new results");
const results = await fn(query, limit);
const writeTransaction = db.transaction(storeName, "readwrite");
const writeStore = writeTransaction.objectStore(storeName);
writeStore.put({ results, timestamp: Date.now() }, key);
addLogEntry(
`Search completed with ${results.textResults.length} text results and ${results.imageResults.length} image results`,
);
return results;
};
}
export const search = cacheSearchWithIndexedDB(
async (query: string, limit?: number): Promise<SearchResults> => {
const searchUrl = new URL("/search", self.location.origin);
searchUrl.searchParams.set("q", query);
searchUrl.searchParams.set("token", await getSearchTokenHash());
if (limit && limit > 0) {
searchUrl.searchParams.set("limit", limit.toString());
}
const response = await fetch(searchUrl.toString());
return response.ok
? response.json()
: { textResults: [], imageResults: [] };
},
);
|