@findlane/browser 0.7.0 → 0.8.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.d.ts CHANGED
@@ -123,17 +123,57 @@ export declare class FindlaneError extends Error {
123
123
  status: number;
124
124
  constructor(status: number, message: string);
125
125
  }
126
+ /** A product in a cart or an order. */
127
+ export type EventItem = {
128
+ objectID: string;
129
+ quantity?: number;
130
+ price?: number;
131
+ };
132
+ /** Where a clicked product was: its place in the results (from 1), and the search's query or the
133
+ * recommendation list (and the product whose list it was). */
134
+ export type ClickDetails = {
135
+ position?: number;
136
+ query?: string;
137
+ list?: "goesWith" | "similar" | "together";
138
+ from?: string;
139
+ };
140
+ /** Events from the page, sent in the background: a batch a second, and what's left when the page is
141
+ * hidden or closed. They carry no cookie or ID. */
142
+ export type BrowserEvents = {
143
+ view(objectID: string): void;
144
+ click(objectID: string, details?: ClickDetails): void;
145
+ addToCart(items: EventItem[], options?: {
146
+ currency?: string;
147
+ }): void;
148
+ /** Counts for Bought together only when the index allows orders from browsers (usually a server
149
+ * sends them); always counts on the index's Events page. */
150
+ purchase(order: {
151
+ orderID: string;
152
+ items: EventItem[];
153
+ currency?: string;
154
+ }): void;
155
+ /** With `events: "afterConsent"`: true sends the events held so far and those after; false drops them. */
156
+ consent(granted: boolean): void;
157
+ /** Sends the events waiting now. */
158
+ flush(): void;
159
+ };
160
+ type Fetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
126
161
  /** The hosted Findlane API, used unless `baseUrl` says otherwise. */
127
162
  export declare const defaultBaseUrl = "https://api.findlane.dev";
128
163
  export type BrowserClientOptions = {
129
164
  /** The API to call; defaults to the hosted Findlane API. */
130
165
  baseUrl?: string;
131
166
  publicId: string;
132
- fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
167
+ fetch?: Fetch;
168
+ /** Whether `events` sends anything: true (the default), false, or "afterConsent" to hold events until
169
+ * `events.consent(true)`. */
170
+ events?: boolean | "afterConsent";
133
171
  };
134
172
  export declare class SearchBrowserClient {
135
173
  private readonly url;
136
174
  private readonly fetcher;
175
+ /** Views, clicks, add-to-carts and orders, for the index's Events page and Bought together. */
176
+ readonly events: BrowserEvents;
137
177
  constructor(options: BrowserClientOptions);
138
178
  private post;
139
179
  search<T = Record<string, unknown>>(request: SearchRequest, options?: {
@@ -151,3 +191,4 @@ export declare class SearchBrowserClient {
151
191
  signal?: AbortSignal;
152
192
  }): Promise<RecommendationsResponse<T>>;
153
193
  }
194
+ export {};
package/dist/index.js CHANGED
@@ -8,14 +8,60 @@ export class FindlaneError extends Error {
8
8
  }
9
9
  /** The hosted Findlane API, used unless `baseUrl` says otherwise. */
10
10
  export const defaultBaseUrl = "https://api.findlane.dev";
11
+ function eventSender(url, fetcher, mode) {
12
+ let queue = [];
13
+ let allowed = mode === "afterConsent" ? undefined : mode;
14
+ let timer;
15
+ let listening = false;
16
+ const flush = () => {
17
+ clearTimeout(timer);
18
+ timer = undefined;
19
+ while (allowed && queue.length) {
20
+ const body = JSON.stringify({ events: queue.splice(0, 100) });
21
+ // A beacon outlives the page; text/plain needs no CORS preflight.
22
+ if (fetcher || !(typeof navigator == "object" && navigator.sendBeacon?.(url, new Blob([body], { type: "text/plain" }))))
23
+ (fetcher ?? fetch)(url, { method: "POST", headers: { "content-type": "text/plain" }, body, keepalive: true })
24
+ .catch(() => undefined);
25
+ }
26
+ };
27
+ const add = (event) => {
28
+ if (allowed === false)
29
+ return;
30
+ if (queue.push(event) > 500)
31
+ queue.shift();
32
+ if (!listening && typeof document == "object") {
33
+ listening = true;
34
+ addEventListener("pagehide", flush);
35
+ document.addEventListener("visibilitychange", () => document.hidden && flush());
36
+ }
37
+ timer ??= setTimeout(flush, 1000);
38
+ };
39
+ return {
40
+ view: (objectID) => add({ type: "view", objectID }),
41
+ click: (objectID, details) => add({ type: "click", objectID, ...details }),
42
+ addToCart: (items, options) => add({ type: "addToCart", items, ...options }),
43
+ purchase: (order) => add({ type: "purchase", ...order }),
44
+ consent(granted) {
45
+ allowed = granted;
46
+ if (granted)
47
+ flush();
48
+ else
49
+ queue = [];
50
+ },
51
+ flush,
52
+ };
53
+ }
11
54
  export class SearchBrowserClient {
12
55
  url;
13
56
  fetcher;
57
+ /** Views, clicks, add-to-carts and orders, for the index's Events page and Bought together. */
58
+ events;
14
59
  constructor(options) {
15
60
  if (!/^pub_[a-f0-9]{32}$/.test(options.publicId))
16
61
  throw new Error("A valid public index ID (pub_…) is required");
17
62
  this.url = `${(options.baseUrl ?? defaultBaseUrl).replace(/\/$/, "")}/api/public/indexes/${options.publicId}`;
18
63
  this.fetcher = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
64
+ this.events = eventSender(`${this.url}/events`, options.fetch, options.events ?? true);
19
65
  }
20
66
  async post(operation, request, signal) {
21
67
  const response = await this.fetcher(`${this.url}/${operation}`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@findlane/browser",
3
- "version": "0.7.0",
3
+ "version": "0.8.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",