@findlane/server 0.11.0 → 0.12.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/README.md +2 -1
- package/dist/index.cjs +23 -10
- package/dist/index.d.cts +19 -3
- package/dist/index.d.ts +19 -3
- package/dist/index.js +23 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,7 +57,8 @@ const results = await products.search({ q }, { visitor: { ip: req.ip, userAgent:
|
|
|
57
57
|
| `getSettings()`, `replaceSettings(settings)`, `waitForSettings()` | Read and change searchable fields, facets, filters, sorts, custom ranking, synonyms, languages, typo tolerance, and text normalization. |
|
|
58
58
|
| `getIndexingStatus()`, `waitUntilIndexed(revision)` | Writes return an accepted revision and are searchable when accepted, so `waitUntilIndexed` returns at once. |
|
|
59
59
|
| `getPublicSearchAccess()`, `setPublicSearchPolicy(policy)`, `usePublicIndexDefaults()` | Restrict what browser search may read and query. |
|
|
60
|
-
| `events.send(events)`, `events.importOrders(orders)`, `events.cancelOrder(orderID)` | Orders and other events for the index's Events page and Bought together; the key needs the `events` or `write` scope. |
|
|
60
|
+
| `events.send(events, { visitorId, visitId }?)`, `events.importOrders(orders)`, `events.cancelOrder(orderID)` | Orders and other events for the index's Events page and Bought together; the key needs the `events` or `write` scope. Your own anonymous visitor IDs link a shopper's events, when the index takes them. |
|
|
61
|
+
| `events.forget(ids)` | Deletes the events sent with these visitor or visit IDs, within a week. |
|
|
61
62
|
| `events.proxy(request)`, `events.relay(body, visitor)` | A route on your own site for the browser SDK's events (its `eventsUrl`), which ad blockers let through. See [Events](https://findlane.dev/docs/events#through-your-own-domain). |
|
|
62
63
|
|
|
63
64
|
### Settings
|
package/dist/index.cjs
CHANGED
|
@@ -149,7 +149,7 @@ class SearchServerClient {
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
};
|
|
152
|
-
const postEvents = async (
|
|
152
|
+
const postEvents = async (payload, signal) => {
|
|
153
153
|
for (let attempt = 0;; attempt++) {
|
|
154
154
|
signal?.throwIfAborted();
|
|
155
155
|
let wait = 250 * 2 ** attempt;
|
|
@@ -158,11 +158,11 @@ class SearchServerClient {
|
|
|
158
158
|
method: "POST",
|
|
159
159
|
signal,
|
|
160
160
|
headers: { authorization: `Bearer ${this.key}`, "content-type": "application/json" },
|
|
161
|
-
body: JSON.stringify(
|
|
161
|
+
body: JSON.stringify(payload)
|
|
162
162
|
});
|
|
163
163
|
const data = await response.json().catch(() => ({}));
|
|
164
164
|
if (response.ok)
|
|
165
|
-
return
|
|
165
|
+
return data;
|
|
166
166
|
const error = new FindlaneError(response.status, data.error ?? `HTTP ${response.status}`);
|
|
167
167
|
if (attempt >= 5 || ![408, 429, 502, 503, 504].includes(response.status))
|
|
168
168
|
throw error;
|
|
@@ -194,16 +194,23 @@ class SearchServerClient {
|
|
|
194
194
|
const retryAfter = response.headers.get("retry-after");
|
|
195
195
|
return jsonResponse(await response.text(), response.status, retryAfter ? { "retry-after": retryAfter } : {});
|
|
196
196
|
};
|
|
197
|
-
const sendEvents = async (events,
|
|
197
|
+
const sendEvents = async (events, options = {}) => {
|
|
198
|
+
const { signal, visitorId, visitId } = options;
|
|
198
199
|
const result = { accepted: 0, rejected: [] };
|
|
199
200
|
let batch = [];
|
|
200
201
|
let sent = 0;
|
|
201
202
|
const flush = async () => {
|
|
202
203
|
if (!batch.length)
|
|
203
204
|
return;
|
|
204
|
-
const answer = await postEvents(
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
const answer = await postEvents({
|
|
206
|
+
...visitorId === undefined ? {} : { visitorId },
|
|
207
|
+
...visitId === undefined ? {} : { visitId },
|
|
208
|
+
events: batch
|
|
209
|
+
}, signal);
|
|
210
|
+
result.accepted += answer.accepted ?? 0;
|
|
211
|
+
result.rejected.push(...(answer.rejected ?? []).map((item) => ({ ...item, index: item.index + sent })));
|
|
212
|
+
if (answer.visitorIds === "off")
|
|
213
|
+
result.visitorIds = "off";
|
|
207
214
|
sent += batch.length;
|
|
208
215
|
batch = [];
|
|
209
216
|
};
|
|
@@ -240,7 +247,7 @@ class SearchServerClient {
|
|
|
240
247
|
return request("/recommendations", "POST", { objectID, ...options }, signal, visitorHeaders(visitor));
|
|
241
248
|
},
|
|
242
249
|
events: {
|
|
243
|
-
send: (events,
|
|
250
|
+
send: (events, options) => sendEvents(events, options === undefined ? {} : ("aborted" in options) ? { signal: options } : options),
|
|
244
251
|
importOrders: (orders, signal) => sendEvents(async function* () {
|
|
245
252
|
for await (const { timestamp, ...order } of orders)
|
|
246
253
|
yield {
|
|
@@ -248,8 +255,14 @@ class SearchServerClient {
|
|
|
248
255
|
...order,
|
|
249
256
|
...timestamp === undefined ? {} : { timestamp: typeof timestamp === "number" ? timestamp : new Date(timestamp).getTime() }
|
|
250
257
|
};
|
|
251
|
-
}(), signal),
|
|
252
|
-
cancelOrder: (orderID, signal) => sendEvents([{ type: "cancel", orderID }], signal),
|
|
258
|
+
}(), { signal }),
|
|
259
|
+
cancelOrder: (orderID, signal) => sendEvents([{ type: "cancel", orderID }], { signal }),
|
|
260
|
+
forget: async (ids, signal) => {
|
|
261
|
+
let forgotten = 0;
|
|
262
|
+
for (let start = 0;start < ids.length; start += 1000)
|
|
263
|
+
forgotten += (await postEvents({ forget: ids.slice(start, start + 1000) }, signal)).forgotten ?? 0;
|
|
264
|
+
return { forgotten };
|
|
265
|
+
},
|
|
253
266
|
proxy: async (request2, visitor = {}) => {
|
|
254
267
|
if (request2.method !== "POST")
|
|
255
268
|
return jsonResponse(JSON.stringify({ error: "Only POST is allowed" }), 405, { allow: "POST" });
|
package/dist/index.d.cts
CHANGED
|
@@ -314,13 +314,23 @@ export type Order = {
|
|
|
314
314
|
currency?: string;
|
|
315
315
|
timestamp?: number | Date | string;
|
|
316
316
|
};
|
|
317
|
-
/** How many events were accepted, and which were refused and why; `index` counts from the first event
|
|
317
|
+
/** How many events were accepted, and which were refused and why; `index` counts from the first event
|
|
318
|
+
* sent. `visitorIds: "off"` when IDs were sent and dropped: the index doesn't take them. */
|
|
318
319
|
export type EventsResult = {
|
|
319
320
|
accepted: number;
|
|
320
321
|
rejected: {
|
|
321
322
|
index: number;
|
|
322
323
|
reason: string;
|
|
323
324
|
}[];
|
|
325
|
+
visitorIds?: "off";
|
|
326
|
+
};
|
|
327
|
+
/** Options for `events.send`. `visitorId` and `visitId` are your own anonymous IDs for the shopper and
|
|
328
|
+
* the visit, for all the events sent: 16 to 64 letters, digits, - or _, never an email address, a
|
|
329
|
+
* phone number or a customer number. They're kept only when the index takes visitor IDs. */
|
|
330
|
+
export type EventOptions = {
|
|
331
|
+
signal?: AbortSignal;
|
|
332
|
+
visitorId?: string;
|
|
333
|
+
visitId?: string;
|
|
324
334
|
};
|
|
325
335
|
export declare class FindlaneError extends Error {
|
|
326
336
|
status: number;
|
|
@@ -357,13 +367,19 @@ export declare class SearchServerClient {
|
|
|
357
367
|
/** Events for the index's Events page and Bought together; the API key needs the `events` or `write`
|
|
358
368
|
* scope. Orders count once each, however often they're sent. */
|
|
359
369
|
events: {
|
|
360
|
-
/** Sends events, 200 a request. One bad event doesn't refuse the rest: they're in `rejected`.
|
|
361
|
-
|
|
370
|
+
/** Sends events, 200 a request. One bad event doesn't refuse the rest: they're in `rejected`. Pass
|
|
371
|
+
* `{ visitorId, visitId }` to link a shopper's events (see EventOptions). */
|
|
372
|
+
send: (events: ServerEvent[], options?: AbortSignal | EventOptions) => Promise<EventsResult>;
|
|
362
373
|
/** Sends past orders, up to 400 days old, from a list or an async iterable (such as a database
|
|
363
374
|
* cursor), 200 a request. */
|
|
364
375
|
importOrders: (orders: Iterable<Order> | AsyncIterable<Order>, signal?: AbortSignal) => Promise<EventsResult>;
|
|
365
376
|
/** Takes a cancelled or refunded order out of Bought together. */
|
|
366
377
|
cancelOrder: (orderID: string, signal?: AbortSignal) => Promise<EventsResult>;
|
|
378
|
+
/** Deletes the events sent with these visitor or visit IDs, for shoppers who asked: Findlane does it
|
|
379
|
+
* within a week. 1,000 IDs a request. */
|
|
380
|
+
forget: (ids: string[], signal?: AbortSignal) => Promise<{
|
|
381
|
+
forgotten: number;
|
|
382
|
+
}>;
|
|
367
383
|
/** A route on your own domain for the browser SDK's events (its `eventsUrl` option): ad blockers
|
|
368
384
|
* block beacons to other sites, not to the page's own. Give it the browser's POST request (Next.js
|
|
369
385
|
* route handlers, Workers, Hono, Bun, Deno); it finds the visitor's address in the headers
|
package/dist/index.d.ts
CHANGED
|
@@ -314,13 +314,23 @@ export type Order = {
|
|
|
314
314
|
currency?: string;
|
|
315
315
|
timestamp?: number | Date | string;
|
|
316
316
|
};
|
|
317
|
-
/** How many events were accepted, and which were refused and why; `index` counts from the first event
|
|
317
|
+
/** How many events were accepted, and which were refused and why; `index` counts from the first event
|
|
318
|
+
* sent. `visitorIds: "off"` when IDs were sent and dropped: the index doesn't take them. */
|
|
318
319
|
export type EventsResult = {
|
|
319
320
|
accepted: number;
|
|
320
321
|
rejected: {
|
|
321
322
|
index: number;
|
|
322
323
|
reason: string;
|
|
323
324
|
}[];
|
|
325
|
+
visitorIds?: "off";
|
|
326
|
+
};
|
|
327
|
+
/** Options for `events.send`. `visitorId` and `visitId` are your own anonymous IDs for the shopper and
|
|
328
|
+
* the visit, for all the events sent: 16 to 64 letters, digits, - or _, never an email address, a
|
|
329
|
+
* phone number or a customer number. They're kept only when the index takes visitor IDs. */
|
|
330
|
+
export type EventOptions = {
|
|
331
|
+
signal?: AbortSignal;
|
|
332
|
+
visitorId?: string;
|
|
333
|
+
visitId?: string;
|
|
324
334
|
};
|
|
325
335
|
export declare class FindlaneError extends Error {
|
|
326
336
|
status: number;
|
|
@@ -357,13 +367,19 @@ export declare class SearchServerClient {
|
|
|
357
367
|
/** Events for the index's Events page and Bought together; the API key needs the `events` or `write`
|
|
358
368
|
* scope. Orders count once each, however often they're sent. */
|
|
359
369
|
events: {
|
|
360
|
-
/** Sends events, 200 a request. One bad event doesn't refuse the rest: they're in `rejected`.
|
|
361
|
-
|
|
370
|
+
/** Sends events, 200 a request. One bad event doesn't refuse the rest: they're in `rejected`. Pass
|
|
371
|
+
* `{ visitorId, visitId }` to link a shopper's events (see EventOptions). */
|
|
372
|
+
send: (events: ServerEvent[], options?: AbortSignal | EventOptions) => Promise<EventsResult>;
|
|
362
373
|
/** Sends past orders, up to 400 days old, from a list or an async iterable (such as a database
|
|
363
374
|
* cursor), 200 a request. */
|
|
364
375
|
importOrders: (orders: Iterable<Order> | AsyncIterable<Order>, signal?: AbortSignal) => Promise<EventsResult>;
|
|
365
376
|
/** Takes a cancelled or refunded order out of Bought together. */
|
|
366
377
|
cancelOrder: (orderID: string, signal?: AbortSignal) => Promise<EventsResult>;
|
|
378
|
+
/** Deletes the events sent with these visitor or visit IDs, for shoppers who asked: Findlane does it
|
|
379
|
+
* within a week. 1,000 IDs a request. */
|
|
380
|
+
forget: (ids: string[], signal?: AbortSignal) => Promise<{
|
|
381
|
+
forgotten: number;
|
|
382
|
+
}>;
|
|
367
383
|
/** A route on your own domain for the browser SDK's events (its `eventsUrl` option): ad blockers
|
|
368
384
|
* block beacons to other sites, not to the page's own. Give it the browser's POST request (Next.js
|
|
369
385
|
* route handlers, Workers, Hono, Bun, Deno); it finds the visitor's address in the headers
|
package/dist/index.js
CHANGED
|
@@ -103,7 +103,7 @@ class SearchServerClient {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
};
|
|
106
|
-
const postEvents = async (
|
|
106
|
+
const postEvents = async (payload, signal) => {
|
|
107
107
|
for (let attempt = 0;; attempt++) {
|
|
108
108
|
signal?.throwIfAborted();
|
|
109
109
|
let wait = 250 * 2 ** attempt;
|
|
@@ -112,11 +112,11 @@ class SearchServerClient {
|
|
|
112
112
|
method: "POST",
|
|
113
113
|
signal,
|
|
114
114
|
headers: { authorization: `Bearer ${this.key}`, "content-type": "application/json" },
|
|
115
|
-
body: JSON.stringify(
|
|
115
|
+
body: JSON.stringify(payload)
|
|
116
116
|
});
|
|
117
117
|
const data = await response.json().catch(() => ({}));
|
|
118
118
|
if (response.ok)
|
|
119
|
-
return
|
|
119
|
+
return data;
|
|
120
120
|
const error = new FindlaneError(response.status, data.error ?? `HTTP ${response.status}`);
|
|
121
121
|
if (attempt >= 5 || ![408, 429, 502, 503, 504].includes(response.status))
|
|
122
122
|
throw error;
|
|
@@ -148,16 +148,23 @@ class SearchServerClient {
|
|
|
148
148
|
const retryAfter = response.headers.get("retry-after");
|
|
149
149
|
return jsonResponse(await response.text(), response.status, retryAfter ? { "retry-after": retryAfter } : {});
|
|
150
150
|
};
|
|
151
|
-
const sendEvents = async (events,
|
|
151
|
+
const sendEvents = async (events, options = {}) => {
|
|
152
|
+
const { signal, visitorId, visitId } = options;
|
|
152
153
|
const result = { accepted: 0, rejected: [] };
|
|
153
154
|
let batch = [];
|
|
154
155
|
let sent = 0;
|
|
155
156
|
const flush = async () => {
|
|
156
157
|
if (!batch.length)
|
|
157
158
|
return;
|
|
158
|
-
const answer = await postEvents(
|
|
159
|
-
|
|
160
|
-
|
|
159
|
+
const answer = await postEvents({
|
|
160
|
+
...visitorId === undefined ? {} : { visitorId },
|
|
161
|
+
...visitId === undefined ? {} : { visitId },
|
|
162
|
+
events: batch
|
|
163
|
+
}, signal);
|
|
164
|
+
result.accepted += answer.accepted ?? 0;
|
|
165
|
+
result.rejected.push(...(answer.rejected ?? []).map((item) => ({ ...item, index: item.index + sent })));
|
|
166
|
+
if (answer.visitorIds === "off")
|
|
167
|
+
result.visitorIds = "off";
|
|
161
168
|
sent += batch.length;
|
|
162
169
|
batch = [];
|
|
163
170
|
};
|
|
@@ -194,7 +201,7 @@ class SearchServerClient {
|
|
|
194
201
|
return request("/recommendations", "POST", { objectID, ...options }, signal, visitorHeaders(visitor));
|
|
195
202
|
},
|
|
196
203
|
events: {
|
|
197
|
-
send: (events,
|
|
204
|
+
send: (events, options) => sendEvents(events, options === undefined ? {} : ("aborted" in options) ? { signal: options } : options),
|
|
198
205
|
importOrders: (orders, signal) => sendEvents(async function* () {
|
|
199
206
|
for await (const { timestamp, ...order } of orders)
|
|
200
207
|
yield {
|
|
@@ -202,8 +209,14 @@ class SearchServerClient {
|
|
|
202
209
|
...order,
|
|
203
210
|
...timestamp === undefined ? {} : { timestamp: typeof timestamp === "number" ? timestamp : new Date(timestamp).getTime() }
|
|
204
211
|
};
|
|
205
|
-
}(), signal),
|
|
206
|
-
cancelOrder: (orderID, signal) => sendEvents([{ type: "cancel", orderID }], signal),
|
|
212
|
+
}(), { signal }),
|
|
213
|
+
cancelOrder: (orderID, signal) => sendEvents([{ type: "cancel", orderID }], { signal }),
|
|
214
|
+
forget: async (ids, signal) => {
|
|
215
|
+
let forgotten = 0;
|
|
216
|
+
for (let start = 0;start < ids.length; start += 1000)
|
|
217
|
+
forgotten += (await postEvents({ forget: ids.slice(start, start + 1000) }, signal)).forgotten ?? 0;
|
|
218
|
+
return { forgotten };
|
|
219
|
+
},
|
|
207
220
|
proxy: async (request2, visitor = {}) => {
|
|
208
221
|
if (request2.method !== "POST")
|
|
209
222
|
return jsonResponse(JSON.stringify({ error: "Only POST is allowed" }), 405, { allow: "POST" });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@findlane/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Findlane server SDK: upload catalogs, manage index settings, and search with a secret workspace API key.",
|
|
5
5
|
"keywords": ["findlane", "search", "full-text search", "typo tolerance", "ecommerce search", "search api", "sdk"],
|
|
6
6
|
"license": "MIT",
|