@findlane/server 0.10.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 +3 -0
- package/dist/index.cjs +68 -10
- package/dist/index.d.cts +35 -3
- package/dist/index.d.ts +35 -3
- package/dist/index.js +68 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,6 +57,9 @@ 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, { 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. |
|
|
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). |
|
|
60
63
|
|
|
61
64
|
### Settings
|
|
62
65
|
|
package/dist/index.cjs
CHANGED
|
@@ -69,6 +69,18 @@ class StagedReplacementError extends Error {
|
|
|
69
69
|
}
|
|
70
70
|
var eventsPerRequest = 200;
|
|
71
71
|
var requestOptions = (value) => value === undefined ? {} : ("aborted" in value) ? { signal: value } : value;
|
|
72
|
+
var relayedBytes = 256 * 1024;
|
|
73
|
+
function requestVisitor(request) {
|
|
74
|
+
const headers = request.headers;
|
|
75
|
+
const cf = request.cf;
|
|
76
|
+
return {
|
|
77
|
+
ip: headers.get("cf-connecting-ip") ?? headers.get("x-real-ip") ?? headers.get("x-forwarded-for")?.split(",")[0]?.trim(),
|
|
78
|
+
userAgent: headers.get("user-agent") ?? undefined,
|
|
79
|
+
origin: headers.get("origin") ?? new URL(request.url).origin,
|
|
80
|
+
country: (typeof cf?.country == "string" ? cf.country : null) ?? headers.get("cf-ipcountry") ?? headers.get("x-vercel-ip-country") ?? undefined
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
var jsonResponse = (body, status, headers = {}) => new Response(body, { status, headers: { "content-type": "application/json", "cache-control": "no-store", ...headers } });
|
|
72
84
|
var visitorHeaders = (visitor) => visitor?.ip ? {
|
|
73
85
|
"findlane-visitor-ip": visitor.ip.trim(),
|
|
74
86
|
...visitor.userAgent ? { "findlane-visitor-agent": visitor.userAgent.replace(/[^\x20-\x7e]/g, "").slice(0, 512) } : {}
|
|
@@ -137,7 +149,7 @@ class SearchServerClient {
|
|
|
137
149
|
}
|
|
138
150
|
}
|
|
139
151
|
};
|
|
140
|
-
const postEvents = async (
|
|
152
|
+
const postEvents = async (payload, signal) => {
|
|
141
153
|
for (let attempt = 0;; attempt++) {
|
|
142
154
|
signal?.throwIfAborted();
|
|
143
155
|
let wait = 250 * 2 ** attempt;
|
|
@@ -146,11 +158,11 @@ class SearchServerClient {
|
|
|
146
158
|
method: "POST",
|
|
147
159
|
signal,
|
|
148
160
|
headers: { authorization: `Bearer ${this.key}`, "content-type": "application/json" },
|
|
149
|
-
body: JSON.stringify(
|
|
161
|
+
body: JSON.stringify(payload)
|
|
150
162
|
});
|
|
151
163
|
const data = await response.json().catch(() => ({}));
|
|
152
164
|
if (response.ok)
|
|
153
|
-
return
|
|
165
|
+
return data;
|
|
154
166
|
const error = new FindlaneError(response.status, data.error ?? `HTTP ${response.status}`);
|
|
155
167
|
if (attempt >= 5 || ![408, 429, 502, 503, 504].includes(response.status))
|
|
156
168
|
throw error;
|
|
@@ -164,16 +176,41 @@ class SearchServerClient {
|
|
|
164
176
|
await pause(wait, signal);
|
|
165
177
|
}
|
|
166
178
|
};
|
|
167
|
-
const
|
|
179
|
+
const relayEvents = async (body, visitor, signal) => {
|
|
180
|
+
if (!visitor.ip?.trim())
|
|
181
|
+
throw new Error("Relaying events needs the visitor's IP address");
|
|
182
|
+
const response = await this.fetcher(this.baseUrl + path + "/events", {
|
|
183
|
+
method: "POST",
|
|
184
|
+
signal,
|
|
185
|
+
body,
|
|
186
|
+
headers: {
|
|
187
|
+
authorization: `Bearer ${this.key}`,
|
|
188
|
+
"content-type": "application/json",
|
|
189
|
+
...visitorHeaders(visitor),
|
|
190
|
+
...visitor.origin ? { "findlane-visitor-origin": visitor.origin } : {},
|
|
191
|
+
...visitor.country ? { "findlane-visitor-country": visitor.country } : {}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
const retryAfter = response.headers.get("retry-after");
|
|
195
|
+
return jsonResponse(await response.text(), response.status, retryAfter ? { "retry-after": retryAfter } : {});
|
|
196
|
+
};
|
|
197
|
+
const sendEvents = async (events, options = {}) => {
|
|
198
|
+
const { signal, visitorId, visitId } = options;
|
|
168
199
|
const result = { accepted: 0, rejected: [] };
|
|
169
200
|
let batch = [];
|
|
170
201
|
let sent = 0;
|
|
171
202
|
const flush = async () => {
|
|
172
203
|
if (!batch.length)
|
|
173
204
|
return;
|
|
174
|
-
const answer = await postEvents(
|
|
175
|
-
|
|
176
|
-
|
|
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";
|
|
177
214
|
sent += batch.length;
|
|
178
215
|
batch = [];
|
|
179
216
|
};
|
|
@@ -210,7 +247,7 @@ class SearchServerClient {
|
|
|
210
247
|
return request("/recommendations", "POST", { objectID, ...options }, signal, visitorHeaders(visitor));
|
|
211
248
|
},
|
|
212
249
|
events: {
|
|
213
|
-
send: (events,
|
|
250
|
+
send: (events, options) => sendEvents(events, options === undefined ? {} : ("aborted" in options) ? { signal: options } : options),
|
|
214
251
|
importOrders: (orders, signal) => sendEvents(async function* () {
|
|
215
252
|
for await (const { timestamp, ...order } of orders)
|
|
216
253
|
yield {
|
|
@@ -218,8 +255,29 @@ class SearchServerClient {
|
|
|
218
255
|
...order,
|
|
219
256
|
...timestamp === undefined ? {} : { timestamp: typeof timestamp === "number" ? timestamp : new Date(timestamp).getTime() }
|
|
220
257
|
};
|
|
221
|
-
}(), signal),
|
|
222
|
-
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
|
+
},
|
|
266
|
+
proxy: async (request2, visitor = {}) => {
|
|
267
|
+
if (request2.method !== "POST")
|
|
268
|
+
return jsonResponse(JSON.stringify({ error: "Only POST is allowed" }), 405, { allow: "POST" });
|
|
269
|
+
const tooLarge = () => jsonResponse(JSON.stringify({ error: "The request is too large" }), 413);
|
|
270
|
+
if (Number(request2.headers.get("content-length") ?? 0) > relayedBytes)
|
|
271
|
+
return tooLarge();
|
|
272
|
+
const body = await request2.text();
|
|
273
|
+
if (new TextEncoder().encode(body).length > relayedBytes)
|
|
274
|
+
return tooLarge();
|
|
275
|
+
const found = { ...requestVisitor(request2), ...visitor };
|
|
276
|
+
if (!found.ip)
|
|
277
|
+
throw new Error("No visitor IP address in the request's headers; pass it: events.proxy(request, { ip })");
|
|
278
|
+
return relayEvents(body, { ...found, ip: found.ip });
|
|
279
|
+
},
|
|
280
|
+
relay: (body, visitor, signal) => relayEvents(body, visitor, signal)
|
|
223
281
|
},
|
|
224
282
|
upsert: (records, signal) => request("/records", "POST", { records }, signal),
|
|
225
283
|
getRecord: (objectID, signal) => request(`/records/${encodeURIComponent(objectID)}`, "GET", undefined, signal),
|
package/dist/index.d.cts
CHANGED
|
@@ -263,6 +263,13 @@ export type RequestOptions = {
|
|
|
263
263
|
signal?: AbortSignal;
|
|
264
264
|
visitor?: Visitor;
|
|
265
265
|
};
|
|
266
|
+
/** A visitor whose events a server relays from its own domain (`events.relay`). */
|
|
267
|
+
export type RelayedVisitor = Visitor & {
|
|
268
|
+
/** The page's origin, such as `https://shop.example`: the site the Events page shows. */
|
|
269
|
+
origin?: string;
|
|
270
|
+
/** The visitor's country as two letters, such as `IS`, when the host knows it. */
|
|
271
|
+
country?: string;
|
|
272
|
+
};
|
|
266
273
|
/** A product in a cart or an order. */
|
|
267
274
|
export type EventItem = {
|
|
268
275
|
objectID: string;
|
|
@@ -307,13 +314,23 @@ export type Order = {
|
|
|
307
314
|
currency?: string;
|
|
308
315
|
timestamp?: number | Date | string;
|
|
309
316
|
};
|
|
310
|
-
/** 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. */
|
|
311
319
|
export type EventsResult = {
|
|
312
320
|
accepted: number;
|
|
313
321
|
rejected: {
|
|
314
322
|
index: number;
|
|
315
323
|
reason: string;
|
|
316
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;
|
|
317
334
|
};
|
|
318
335
|
export declare class FindlaneError extends Error {
|
|
319
336
|
status: number;
|
|
@@ -350,13 +367,28 @@ export declare class SearchServerClient {
|
|
|
350
367
|
/** Events for the index's Events page and Bought together; the API key needs the `events` or `write`
|
|
351
368
|
* scope. Orders count once each, however often they're sent. */
|
|
352
369
|
events: {
|
|
353
|
-
/** Sends events, 200 a request. One bad event doesn't refuse the rest: they're in `rejected`.
|
|
354
|
-
|
|
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>;
|
|
355
373
|
/** Sends past orders, up to 400 days old, from a list or an async iterable (such as a database
|
|
356
374
|
* cursor), 200 a request. */
|
|
357
375
|
importOrders: (orders: Iterable<Order> | AsyncIterable<Order>, signal?: AbortSignal) => Promise<EventsResult>;
|
|
358
376
|
/** Takes a cancelled or refunded order out of Bought together. */
|
|
359
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
|
+
}>;
|
|
383
|
+
/** A route on your own domain for the browser SDK's events (its `eventsUrl` option): ad blockers
|
|
384
|
+
* block beacons to other sites, not to the page's own. Give it the browser's POST request (Next.js
|
|
385
|
+
* route handlers, Workers, Hono, Bun, Deno); it finds the visitor's address in the headers
|
|
386
|
+
* Cloudflare, Vercel and most proxies set. Where a visitor reaches your server directly, they can
|
|
387
|
+
* set those too, so pass `{ ip }`. The API key needs the `events` or `write` scope. */
|
|
388
|
+
proxy: (request: Request, visitor?: Partial<RelayedVisitor>) => Promise<Response>;
|
|
389
|
+
/** `proxy` for servers without Fetch API requests, such as Express: the request's body as text,
|
|
390
|
+
* and its visitor. Answers with the API's response. */
|
|
391
|
+
relay: (body: string, visitor: RelayedVisitor, signal?: AbortSignal) => Promise<Response>;
|
|
360
392
|
};
|
|
361
393
|
upsert: (records: T[], signal?: AbortSignal) => Promise<WriteReceipt>;
|
|
362
394
|
getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<T>>;
|
package/dist/index.d.ts
CHANGED
|
@@ -263,6 +263,13 @@ export type RequestOptions = {
|
|
|
263
263
|
signal?: AbortSignal;
|
|
264
264
|
visitor?: Visitor;
|
|
265
265
|
};
|
|
266
|
+
/** A visitor whose events a server relays from its own domain (`events.relay`). */
|
|
267
|
+
export type RelayedVisitor = Visitor & {
|
|
268
|
+
/** The page's origin, such as `https://shop.example`: the site the Events page shows. */
|
|
269
|
+
origin?: string;
|
|
270
|
+
/** The visitor's country as two letters, such as `IS`, when the host knows it. */
|
|
271
|
+
country?: string;
|
|
272
|
+
};
|
|
266
273
|
/** A product in a cart or an order. */
|
|
267
274
|
export type EventItem = {
|
|
268
275
|
objectID: string;
|
|
@@ -307,13 +314,23 @@ export type Order = {
|
|
|
307
314
|
currency?: string;
|
|
308
315
|
timestamp?: number | Date | string;
|
|
309
316
|
};
|
|
310
|
-
/** 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. */
|
|
311
319
|
export type EventsResult = {
|
|
312
320
|
accepted: number;
|
|
313
321
|
rejected: {
|
|
314
322
|
index: number;
|
|
315
323
|
reason: string;
|
|
316
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;
|
|
317
334
|
};
|
|
318
335
|
export declare class FindlaneError extends Error {
|
|
319
336
|
status: number;
|
|
@@ -350,13 +367,28 @@ export declare class SearchServerClient {
|
|
|
350
367
|
/** Events for the index's Events page and Bought together; the API key needs the `events` or `write`
|
|
351
368
|
* scope. Orders count once each, however often they're sent. */
|
|
352
369
|
events: {
|
|
353
|
-
/** Sends events, 200 a request. One bad event doesn't refuse the rest: they're in `rejected`.
|
|
354
|
-
|
|
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>;
|
|
355
373
|
/** Sends past orders, up to 400 days old, from a list or an async iterable (such as a database
|
|
356
374
|
* cursor), 200 a request. */
|
|
357
375
|
importOrders: (orders: Iterable<Order> | AsyncIterable<Order>, signal?: AbortSignal) => Promise<EventsResult>;
|
|
358
376
|
/** Takes a cancelled or refunded order out of Bought together. */
|
|
359
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
|
+
}>;
|
|
383
|
+
/** A route on your own domain for the browser SDK's events (its `eventsUrl` option): ad blockers
|
|
384
|
+
* block beacons to other sites, not to the page's own. Give it the browser's POST request (Next.js
|
|
385
|
+
* route handlers, Workers, Hono, Bun, Deno); it finds the visitor's address in the headers
|
|
386
|
+
* Cloudflare, Vercel and most proxies set. Where a visitor reaches your server directly, they can
|
|
387
|
+
* set those too, so pass `{ ip }`. The API key needs the `events` or `write` scope. */
|
|
388
|
+
proxy: (request: Request, visitor?: Partial<RelayedVisitor>) => Promise<Response>;
|
|
389
|
+
/** `proxy` for servers without Fetch API requests, such as Express: the request's body as text,
|
|
390
|
+
* and its visitor. Answers with the API's response. */
|
|
391
|
+
relay: (body: string, visitor: RelayedVisitor, signal?: AbortSignal) => Promise<Response>;
|
|
360
392
|
};
|
|
361
393
|
upsert: (records: T[], signal?: AbortSignal) => Promise<WriteReceipt>;
|
|
362
394
|
getRecord: (objectID: string, signal?: AbortSignal) => Promise<RecordSnapshot<T>>;
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,18 @@ class StagedReplacementError extends Error {
|
|
|
23
23
|
}
|
|
24
24
|
var eventsPerRequest = 200;
|
|
25
25
|
var requestOptions = (value) => value === undefined ? {} : ("aborted" in value) ? { signal: value } : value;
|
|
26
|
+
var relayedBytes = 256 * 1024;
|
|
27
|
+
function requestVisitor(request) {
|
|
28
|
+
const headers = request.headers;
|
|
29
|
+
const cf = request.cf;
|
|
30
|
+
return {
|
|
31
|
+
ip: headers.get("cf-connecting-ip") ?? headers.get("x-real-ip") ?? headers.get("x-forwarded-for")?.split(",")[0]?.trim(),
|
|
32
|
+
userAgent: headers.get("user-agent") ?? undefined,
|
|
33
|
+
origin: headers.get("origin") ?? new URL(request.url).origin,
|
|
34
|
+
country: (typeof cf?.country == "string" ? cf.country : null) ?? headers.get("cf-ipcountry") ?? headers.get("x-vercel-ip-country") ?? undefined
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
var jsonResponse = (body, status, headers = {}) => new Response(body, { status, headers: { "content-type": "application/json", "cache-control": "no-store", ...headers } });
|
|
26
38
|
var visitorHeaders = (visitor) => visitor?.ip ? {
|
|
27
39
|
"findlane-visitor-ip": visitor.ip.trim(),
|
|
28
40
|
...visitor.userAgent ? { "findlane-visitor-agent": visitor.userAgent.replace(/[^\x20-\x7e]/g, "").slice(0, 512) } : {}
|
|
@@ -91,7 +103,7 @@ class SearchServerClient {
|
|
|
91
103
|
}
|
|
92
104
|
}
|
|
93
105
|
};
|
|
94
|
-
const postEvents = async (
|
|
106
|
+
const postEvents = async (payload, signal) => {
|
|
95
107
|
for (let attempt = 0;; attempt++) {
|
|
96
108
|
signal?.throwIfAborted();
|
|
97
109
|
let wait = 250 * 2 ** attempt;
|
|
@@ -100,11 +112,11 @@ class SearchServerClient {
|
|
|
100
112
|
method: "POST",
|
|
101
113
|
signal,
|
|
102
114
|
headers: { authorization: `Bearer ${this.key}`, "content-type": "application/json" },
|
|
103
|
-
body: JSON.stringify(
|
|
115
|
+
body: JSON.stringify(payload)
|
|
104
116
|
});
|
|
105
117
|
const data = await response.json().catch(() => ({}));
|
|
106
118
|
if (response.ok)
|
|
107
|
-
return
|
|
119
|
+
return data;
|
|
108
120
|
const error = new FindlaneError(response.status, data.error ?? `HTTP ${response.status}`);
|
|
109
121
|
if (attempt >= 5 || ![408, 429, 502, 503, 504].includes(response.status))
|
|
110
122
|
throw error;
|
|
@@ -118,16 +130,41 @@ class SearchServerClient {
|
|
|
118
130
|
await pause(wait, signal);
|
|
119
131
|
}
|
|
120
132
|
};
|
|
121
|
-
const
|
|
133
|
+
const relayEvents = async (body, visitor, signal) => {
|
|
134
|
+
if (!visitor.ip?.trim())
|
|
135
|
+
throw new Error("Relaying events needs the visitor's IP address");
|
|
136
|
+
const response = await this.fetcher(this.baseUrl + path + "/events", {
|
|
137
|
+
method: "POST",
|
|
138
|
+
signal,
|
|
139
|
+
body,
|
|
140
|
+
headers: {
|
|
141
|
+
authorization: `Bearer ${this.key}`,
|
|
142
|
+
"content-type": "application/json",
|
|
143
|
+
...visitorHeaders(visitor),
|
|
144
|
+
...visitor.origin ? { "findlane-visitor-origin": visitor.origin } : {},
|
|
145
|
+
...visitor.country ? { "findlane-visitor-country": visitor.country } : {}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
const retryAfter = response.headers.get("retry-after");
|
|
149
|
+
return jsonResponse(await response.text(), response.status, retryAfter ? { "retry-after": retryAfter } : {});
|
|
150
|
+
};
|
|
151
|
+
const sendEvents = async (events, options = {}) => {
|
|
152
|
+
const { signal, visitorId, visitId } = options;
|
|
122
153
|
const result = { accepted: 0, rejected: [] };
|
|
123
154
|
let batch = [];
|
|
124
155
|
let sent = 0;
|
|
125
156
|
const flush = async () => {
|
|
126
157
|
if (!batch.length)
|
|
127
158
|
return;
|
|
128
|
-
const answer = await postEvents(
|
|
129
|
-
|
|
130
|
-
|
|
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";
|
|
131
168
|
sent += batch.length;
|
|
132
169
|
batch = [];
|
|
133
170
|
};
|
|
@@ -164,7 +201,7 @@ class SearchServerClient {
|
|
|
164
201
|
return request("/recommendations", "POST", { objectID, ...options }, signal, visitorHeaders(visitor));
|
|
165
202
|
},
|
|
166
203
|
events: {
|
|
167
|
-
send: (events,
|
|
204
|
+
send: (events, options) => sendEvents(events, options === undefined ? {} : ("aborted" in options) ? { signal: options } : options),
|
|
168
205
|
importOrders: (orders, signal) => sendEvents(async function* () {
|
|
169
206
|
for await (const { timestamp, ...order } of orders)
|
|
170
207
|
yield {
|
|
@@ -172,8 +209,29 @@ class SearchServerClient {
|
|
|
172
209
|
...order,
|
|
173
210
|
...timestamp === undefined ? {} : { timestamp: typeof timestamp === "number" ? timestamp : new Date(timestamp).getTime() }
|
|
174
211
|
};
|
|
175
|
-
}(), signal),
|
|
176
|
-
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
|
+
},
|
|
220
|
+
proxy: async (request2, visitor = {}) => {
|
|
221
|
+
if (request2.method !== "POST")
|
|
222
|
+
return jsonResponse(JSON.stringify({ error: "Only POST is allowed" }), 405, { allow: "POST" });
|
|
223
|
+
const tooLarge = () => jsonResponse(JSON.stringify({ error: "The request is too large" }), 413);
|
|
224
|
+
if (Number(request2.headers.get("content-length") ?? 0) > relayedBytes)
|
|
225
|
+
return tooLarge();
|
|
226
|
+
const body = await request2.text();
|
|
227
|
+
if (new TextEncoder().encode(body).length > relayedBytes)
|
|
228
|
+
return tooLarge();
|
|
229
|
+
const found = { ...requestVisitor(request2), ...visitor };
|
|
230
|
+
if (!found.ip)
|
|
231
|
+
throw new Error("No visitor IP address in the request's headers; pass it: events.proxy(request, { ip })");
|
|
232
|
+
return relayEvents(body, { ...found, ip: found.ip });
|
|
233
|
+
},
|
|
234
|
+
relay: (body, visitor, signal) => relayEvents(body, visitor, signal)
|
|
177
235
|
},
|
|
178
236
|
upsert: (records, signal) => request("/records", "POST", { records }, signal),
|
|
179
237
|
getRecord: (objectID, signal) => request(`/records/${encodeURIComponent(objectID)}`, "GET", undefined, signal),
|
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",
|