Skip to content

Commit 1294006

Browse files
committed
Improve best-effort bulk flushing
1 parent bc384c5 commit 1294006

4 files changed

Lines changed: 74 additions & 3 deletions

File tree

packages/browser-sdk/src/client.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,8 +328,14 @@ export type InitOptions = ReflagDeprecatedContext & {
328328
maxSize?: number;
329329

330330
/**
331-
* No retries are performed; failed batches are dropped.
331+
* Deprecated: retries are no longer performed for bulk delivery.
332332
*/
333+
retryBaseDelayMs?: number;
334+
335+
/**
336+
* Deprecated: retries are no longer performed for bulk delivery.
337+
*/
338+
retryMaxDelayMs?: number;
333339
};
334340
};
335341

@@ -423,6 +429,7 @@ export class ReflagClient {
423429
private autoFeedbackInit: Promise<void> | undefined;
424430
private readonly flagsClient: FlagsClient;
425431
private readonly bulkQueue: BulkQueue | undefined;
432+
private readonly handleBeforeUnload?: () => void;
426433

427434
public readonly logger: Logger;
428435

@@ -480,6 +487,14 @@ export class ReflagClient {
480487
},
481488
);
482489
}
490+
if (this.bulkQueue && !IS_SERVER) {
491+
this.handleBeforeUnload = () => {
492+
void this.bulkQueue?.flush();
493+
};
494+
window.addEventListener("beforeunload", this.handleBeforeUnload, {
495+
capture: true,
496+
});
497+
}
483498

484499
const bulkQueue = this.bulkQueue;
485500

@@ -590,6 +605,12 @@ export class ReflagClient {
590605
*
591606
**/
592607
async stop() {
608+
if (this.handleBeforeUnload && !IS_SERVER) {
609+
window.removeEventListener("beforeunload", this.handleBeforeUnload, {
610+
capture: true,
611+
});
612+
}
613+
593614
if (this.bulkQueue) {
594615
await this.bulkQueue.flush();
595616
let remaining = await this.bulkQueue.size();

packages/browser-sdk/src/httpClient.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
import { createAbortController } from "./utils/abortController";
22
import { API_BASE_URL, SDK_VERSION, SDK_VERSION_HEADER_NAME } from "./config";
33

4+
const KEEPALIVE_MAX_BODY_BYTES = 60 * 1024;
5+
46
export interface HttpClientOptions {
57
baseUrl?: string;
68
sdkVersion?: string;
79
credentials?: RequestCredentials;
810
}
911

12+
function getBodyByteLength(value: string) {
13+
return new TextEncoder().encode(value).length;
14+
}
15+
1016
export class HttpClient {
1117
private readonly baseUrl: string;
1218
private readonly sdkVersion: string;
@@ -85,16 +91,21 @@ export class HttpClient {
8591
body: any;
8692
keepalive?: boolean;
8793
}): ReturnType<typeof fetch> {
94+
const serializedBody = JSON.stringify(body);
95+
const shouldUseKeepalive =
96+
keepalive &&
97+
getBodyByteLength(serializedBody) <= KEEPALIVE_MAX_BODY_BYTES;
98+
8899
return fetch(this.getUrl(path), {
89100
...this.fetchOptions,
90101
method: "POST",
91-
keepalive,
102+
keepalive: shouldUseKeepalive,
92103
headers: {
93104
"Content-Type": "application/json",
94105
[SDK_VERSION_HEADER_NAME]: this.sdkVersion,
95106
Authorization: `Bearer ${this.publishableKey}`,
96107
},
97-
body: JSON.stringify(body),
108+
body: serializedBody,
98109
});
99110
}
100111
}

packages/browser-sdk/test/client.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ describe("ReflagClient", () => {
4646
await vi.waitFor(() =>
4747
expect(httpClientPost).toHaveBeenCalledWith({
4848
path: "/bulk",
49+
keepalive: true,
4950
body: [
5051
{
5152
type: "user",
@@ -73,6 +74,7 @@ describe("ReflagClient", () => {
7374
await vi.waitFor(() =>
7475
expect(httpClientPost).toHaveBeenCalledWith({
7576
path: "/bulk",
77+
keepalive: true,
7678
body: [
7779
{
7880
type: "company",
@@ -197,6 +199,20 @@ describe("ReflagClient", () => {
197199
});
198200

199201
describe("stop", () => {
202+
it("flushes the bulk queue on beforeunload and removes the listener on stop", async () => {
203+
const bulkQueue = client["bulkQueue"];
204+
expect(bulkQueue).toBeDefined();
205+
206+
const flushSpy = vi.spyOn(bulkQueue!, "flush").mockResolvedValue();
207+
window.dispatchEvent(new Event("beforeunload"));
208+
expect(flushSpy).toHaveBeenCalledTimes(1);
209+
210+
await client.stop();
211+
212+
window.dispatchEvent(new Event("beforeunload"));
213+
expect(flushSpy).toHaveBeenCalledTimes(2);
214+
});
215+
200216
it("throws if queued bulk events remain after final flush attempt", async () => {
201217
const bulkQueue = client["bulkQueue"];
202218
expect(bulkQueue).toBeDefined();

packages/browser-sdk/test/httpClient.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,29 @@ describe("sets `credentials`", () => {
6464
);
6565
});
6666

67+
test("uses keepalive for small request bodies when requested", async () => {
68+
const client = new HttpClient("publishableKey");
69+
70+
await client.post({ path: "/test", body: { ok: true }, keepalive: true });
71+
72+
expect(global.fetch).toHaveBeenCalledWith(
73+
expect.any(URL),
74+
expect.objectContaining({ keepalive: true }),
75+
);
76+
});
77+
78+
test("does not use keepalive for large request bodies", async () => {
79+
const client = new HttpClient("publishableKey");
80+
const largeBody = { payload: "x".repeat(70 * 1024) };
81+
82+
await client.post({ path: "/test", body: largeBody, keepalive: true });
83+
84+
expect(global.fetch).toHaveBeenCalledWith(
85+
expect.any(URL),
86+
expect.objectContaining({ keepalive: false }),
87+
);
88+
});
89+
6790
test("does not require a writable `URL.search` property", async () => {
6891
const OriginalURL = global.URL;
6992

0 commit comments

Comments
 (0)