type Getter = (query: string) => Promise; type Filter = (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 { private shortKey: string | undefined; private shortKeyCache: T[] = []; constructor( private getter: Getter, private filter: Filter, private prefetchThrethold: number = 1 ) {} async get(query: string): Promise { 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; } }