aboutsummaryrefslogtreecommitdiff
path: root/src/background/completion/impl/PrefetchAndCache.ts
blob: d2889b04278f62d5d2b08ac01f704bcc27245ca4 (plain) (blame)
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
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;
  }
}