@findlane/browser 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +3 -1
- package/dist/react/cache.d.ts +2 -0
- package/dist/react/cache.js +43 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -20,7 +20,9 @@ export class SearchBrowserClient {
|
|
|
20
20
|
async post(operation, request, signal) {
|
|
21
21
|
const response = await this.fetcher(`${this.url}/${operation}`, {
|
|
22
22
|
method: "POST",
|
|
23
|
-
|
|
23
|
+
// JSON as text/plain: browsers send it without asking the API first (a CORS preflight), which
|
|
24
|
+
// saves a round trip.
|
|
25
|
+
headers: { "content-type": "text/plain" },
|
|
24
26
|
body: JSON.stringify(request),
|
|
25
27
|
signal,
|
|
26
28
|
});
|
package/dist/react/cache.d.ts
CHANGED
|
@@ -4,4 +4,6 @@ import type { SearchBrowserClient, SearchRequest, SearchResponse } from "../inde
|
|
|
4
4
|
export declare function searchKey(request: SearchRequest): string;
|
|
5
5
|
/** A cached response, if there's a recent one. */
|
|
6
6
|
export declare function peek<T>(client: SearchBrowserClient, request: SearchRequest): SearchResponse<T> | null;
|
|
7
|
+
/** The search's response: from memory, from the same search in flight, or a new request. A caller's
|
|
8
|
+
* signal stops its own wait; the request stops once every caller waiting for it has. */
|
|
7
9
|
export declare function cachedSearch<T>(client: SearchBrowserClient, request: SearchRequest, signal?: AbortSignal): Promise<SearchResponse<T>>;
|
package/dist/react/cache.js
CHANGED
|
@@ -2,6 +2,7 @@ import { canonical } from "./state.js";
|
|
|
2
2
|
const size = 50;
|
|
3
3
|
const maxAge = 5 * 60_000;
|
|
4
4
|
const caches = new WeakMap();
|
|
5
|
+
const flights = new WeakMap();
|
|
5
6
|
/** Requests the API answers the same way share a key, as at the edge (workers/api/src/edge.ts):
|
|
6
7
|
* the query's first eight lowercase words, defaults left out, and list order ignored. */
|
|
7
8
|
export function searchKey(request) {
|
|
@@ -24,19 +25,56 @@ export function peek(client, request) {
|
|
|
24
25
|
const entry = caches.get(client)?.get(searchKey(request));
|
|
25
26
|
return entry && Date.now() - entry.at < maxAge ? entry.response : null;
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
/** The search's response: from memory, from the same search in flight, or a new request. A caller's
|
|
29
|
+
* signal stops its own wait; the request stops once every caller waiting for it has. */
|
|
30
|
+
export function cachedSearch(client, request, signal) {
|
|
28
31
|
const found = peek(client, request);
|
|
29
32
|
if (found)
|
|
30
|
-
return found;
|
|
31
|
-
|
|
33
|
+
return Promise.resolve(found);
|
|
34
|
+
if (signal?.aborted)
|
|
35
|
+
return Promise.reject(signal.reason);
|
|
36
|
+
const key = searchKey(request);
|
|
37
|
+
let inFlight = flights.get(client);
|
|
38
|
+
if (!inFlight)
|
|
39
|
+
flights.set(client, (inFlight = new Map()));
|
|
40
|
+
let flight = inFlight.get(key);
|
|
41
|
+
if (!flight) {
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const started = {
|
|
44
|
+
controller,
|
|
45
|
+
waiting: 0,
|
|
46
|
+
response: client.search(request, { signal: controller.signal }).then((response) => {
|
|
47
|
+
remember(client, key, response);
|
|
48
|
+
return response;
|
|
49
|
+
}).finally(() => { if (inFlight.get(key) === started)
|
|
50
|
+
inFlight.delete(key); }),
|
|
51
|
+
};
|
|
52
|
+
inFlight.set(key, (flight = started));
|
|
53
|
+
}
|
|
54
|
+
const shared = flight;
|
|
55
|
+
shared.waiting++;
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const leave = () => {
|
|
58
|
+
reject(signal.reason);
|
|
59
|
+
if (--shared.waiting > 0)
|
|
60
|
+
return;
|
|
61
|
+
// A later caller starts a new request rather than joining a stopped one.
|
|
62
|
+
if (inFlight.get(key) === shared)
|
|
63
|
+
inFlight.delete(key);
|
|
64
|
+
shared.controller.abort();
|
|
65
|
+
};
|
|
66
|
+
signal?.addEventListener("abort", leave, { once: true });
|
|
67
|
+
shared.response.then((response) => resolve(response), reject)
|
|
68
|
+
.finally(() => signal?.removeEventListener("abort", leave));
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function remember(client, key, response) {
|
|
32
72
|
let cache = caches.get(client);
|
|
33
73
|
if (!cache)
|
|
34
74
|
caches.set(client, (cache = new Map()));
|
|
35
|
-
const key = searchKey(request);
|
|
36
75
|
cache.delete(key);
|
|
37
76
|
cache.set(key, { at: Date.now(), response });
|
|
38
77
|
// A Map keeps insertion order, so the first key is the oldest.
|
|
39
78
|
if (cache.size > size)
|
|
40
79
|
cache.delete(cache.keys().next().value);
|
|
41
|
-
return response;
|
|
42
80
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@findlane/browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Findlane browser SDK: fast, typo-tolerant public search from the browser with an index's public ID, plus React hooks and components.",
|
|
5
5
|
"keywords": ["findlane", "search", "full-text search", "typo tolerance", "instant search", "ecommerce search", "browser", "react", "autocomplete"],
|
|
6
6
|
"license": "MIT",
|