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: [] };
  },
);