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
|
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
}
}
|