aboutsummaryrefslogtreecommitdiff
path: root/src/background/completion/impl/PrefetchAndCache.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/background/completion/impl/PrefetchAndCache.ts')
-rw-r--r--src/background/completion/impl/PrefetchAndCache.ts105
1 files changed, 105 insertions, 0 deletions
diff --git a/src/background/completion/impl/PrefetchAndCache.ts b/src/background/completion/impl/PrefetchAndCache.ts
new file mode 100644
index 0000000..3c074c2
--- /dev/null
+++ b/src/background/completion/impl/PrefetchAndCache.ts
@@ -0,0 +1,105 @@
+type Getter<T> = (query: string) => Promise<T[]>;
+type Filter<T> = (src: T[], query: string) => T[];
+
+const WHITESPACE = /\s/;
+
+// `shortKey` returns a shorten key to pre-fetch completions and store in the
+// cache. The shorten key is generated by the following rules:
+//
+// 1. If the query contains a space in the middle: i.e. the query consists of
+// multiple words, the method removes the last word from the query, and
+// returns joined remaining words with space.
+//
+// 2. If the query is a single word and it's an URL, the method returns a new
+// URL excluding search query with the upper path of the original URL.
+//
+// 3. If the query is a single word and it's not an URL, the method returns a
+// word with the half-length of the original query.
+//
+// Examples:
+//
+// shortKey("hello world good bye")
+// => "hello world good"
+//
+// shortKey("https://example.com/path/to/resource?q=hello")
+// => "https://example.com/path/to/"
+//
+// shortKey("the-query-with-super-long-word")
+// => "the-query-with-"
+//
+export const shortKey = (query: string): string => {
+ if (WHITESPACE.test(query)) {
+ return query.split(WHITESPACE).filter(word => word.length > 0).slice(0, -1).join(' ');
+ }
+ let url;
+ try {
+ url = new URL(query)
+ } catch (e) {
+ return query.slice(0, query.length / 2);
+ }
+
+ if (url.origin === query) {
+ // may be on typing or removing URLs such as "such as https://goog"
+ return query.slice(0, query.length / 2);
+ }
+ if (url.pathname.endsWith('/')) {
+ // remove parameters and move to upper path
+ return new URL('..', url).href;
+ }
+ // remove parameters
+ return new URL('.', url).href;
+};
+
+export default class PrefetchAndCache<T> {
+ private shortKey: string | undefined;
+
+ private shortKeyCache: T[] = [];
+
+ constructor(
+ private getter: Getter<T>,
+ private filter: Filter<T>,
+ private prefetchThrethold: number = 1,
+ ) {
+ }
+
+ async get(query: string): Promise<T[]> {
+ query = query.trim();
+ if (query.length < this.prefetchThrethold) {
+ this.shortKey = undefined;
+ return this.getter(query);
+ }
+
+ if (this.needToRefresh(query)) {
+ this.shortKey = shortKey(query);
+ this.shortKeyCache = await this.getter(this.shortKey);
+ }
+ return this.filter(this.shortKeyCache, query);
+ }
+
+ private needToRefresh(query: string): boolean {
+ if (!this.shortKey) {
+ // no cache
+ return true
+ }
+
+ if (query.length < this.shortKey.length) {
+ // query: "hello"
+ // cache: "hello_world"
+ return true;
+ }
+
+ if (!query.startsWith(this.shortKey)) {
+ // queyr: "hello_w"
+ // shorten: "hello_morning"
+ return true
+ }
+
+ if (query.slice(this.shortKey.length).includes(' ')) {
+ // queyr: "hello x"
+ // shorten: "hello"
+ return true;
+ }
+
+ return false
+ }
+}