From 0acd01a3928462ad5b54642bacdd359091b0a975 Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Sat, 8 Feb 2025 20:29:36 +0000 Subject: [PATCH 01/10] feat(node-sdk): add configurable exit event flushing - Introduce `flushOnExit` option in batch buffer configuration - Add new `flusher.ts` module to handle graceful process exit and event flushing - Update client initialization to conditionally register exit flush handler - Enhance README with documentation about exit flushing behavior - Add comprehensive test coverage for exit flushing mechanism --- packages/node-sdk/README.md | 17 +- packages/node-sdk/package.json | 2 +- packages/node-sdk/src/client.ts | 12 ++ packages/node-sdk/src/config.ts | 1 + packages/node-sdk/src/flusher.ts | 64 ++++++++ packages/node-sdk/src/types.ts | 11 ++ packages/node-sdk/test/client.test.ts | 27 ++++ packages/node-sdk/test/flusher.test.ts | 212 +++++++++++++++++++++++++ 8 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 packages/node-sdk/src/flusher.ts create mode 100644 packages/node-sdk/test/flusher.test.ts diff --git a/packages/node-sdk/README.md b/packages/node-sdk/README.md index 63441612..57964a7c 100644 --- a/packages/node-sdk/README.md +++ b/packages/node-sdk/README.md @@ -315,7 +315,22 @@ the number of calls that are sent to Bucket's servers. During process shutdown, some messages could be waiting to be sent, and thus, would be discarded if the buffer is not flushed. -A naive example: +By default, the SDK automatically subscribes to process exit signals and attempts to flush +any pending events. This behavior is controlled by the `flushOnExit` option in the client configuration: + +```typescript +const client = new BucketClient({ + batchOptions: { + flushOnExit: false, // disable automatic flushing on exit + }, +}); +``` + +> [!NOTE] +> If you are creating multiple client instances in your application, it's recommended to disable `flushOnExit` +> to avoid potential conflicts during process shutdown. In such cases, you should implement your own flush handling. + +A naive example of manual flush handling: ```typescript process.on("SIGINT", () => { diff --git a/packages/node-sdk/package.json b/packages/node-sdk/package.json index 81766026..81bde285 100644 --- a/packages/node-sdk/package.json +++ b/packages/node-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@bucketco/node-sdk", - "version": "1.5.1", + "version": "1.5.2", "license": "MIT", "repository": { "type": "git", diff --git a/packages/node-sdk/src/client.ts b/packages/node-sdk/src/client.ts index ba55d857..dc033180 100644 --- a/packages/node-sdk/src/client.ts +++ b/packages/node-sdk/src/client.ts @@ -14,6 +14,7 @@ import { SDK_VERSION_HEADER_NAME, } from "./config"; import fetchClient from "./fetch-http-client"; +import { subscribe as triggerOnExit } from "./flusher"; import { newRateLimiter } from "./rate-limiter"; import type { EvaluatedFeaturesAPIResponse, @@ -102,6 +103,7 @@ export class BucketClient { offline: boolean; configFile?: string; }; + private _initialize = once(async () => { if (!this._config.offline) { await this.getFeaturesCache().refresh(); @@ -217,6 +219,10 @@ export class BucketClient { : () => config.featureOverrides, }; + if ((config.batchOptions?.flushOnExit ?? true) && !this._config.offline) { + triggerOnExit(this.flush); + } + if (!new URL(this._config.apiBaseUrl).pathname.endsWith("/")) { this._config.apiBaseUrl += "/"; } @@ -404,8 +410,14 @@ export class BucketClient { * @remarks * It is recommended to call this method when the application is shutting down to ensure all events are sent * before the process exits. + * + * This method is automatically called when the process exits if `batchOptions.flushOnExit` is `true` in the options (default). */ public async flush() { + if (this._config.offline) { + return; + } + await this._config.batchBuffer.flush(); } diff --git a/packages/node-sdk/src/config.ts b/packages/node-sdk/src/config.ts index 503629c2..c470300e 100644 --- a/packages/node-sdk/src/config.ts +++ b/packages/node-sdk/src/config.ts @@ -9,6 +9,7 @@ export const API_BASE_URL = "https://front.bucket.co"; export const SDK_VERSION_HEADER_NAME = "bucket-sdk-version"; export const SDK_VERSION = `node-sdk/${version}`; export const API_TIMEOUT_MS = 5000; +export const END_FLUSH_TIMEOUT_MS = 5000; export const BUCKET_LOG_PREFIX = "[Bucket]"; diff --git a/packages/node-sdk/src/flusher.ts b/packages/node-sdk/src/flusher.ts new file mode 100644 index 00000000..3840a2f6 --- /dev/null +++ b/packages/node-sdk/src/flusher.ts @@ -0,0 +1,64 @@ +import { constants } from "os"; + +import { END_FLUSH_TIMEOUT_MS } from "./config"; + +type Callback = () => Promise; + +const killSignals = ["SIGINT", "SIGTERM", "SIGHUP", "SIGBREAK"] as const; + +export function subscribe( + callback: Callback, + timeout: number = END_FLUSH_TIMEOUT_MS, +) { + let state: boolean | undefined; + + const wrappedCallback = async () => { + if (state !== undefined) { + return; + } + + state = false; + + try { + const result = await Promise.race([ + new Promise((resolve) => setTimeout(() => resolve(true), timeout)), + callback(), + ]); + + if (result === true) { + console.error( + "[Bucket SDK] Timeout while flushing events on process exit.", + ); + } + } catch (error) { + console.error( + "[Bucket SDK] An error occurred while flushing events on process exit.", + error, + ); + } + + state = true; + }; + + killSignals.forEach((signal) => { + const hasListeners = process.listenerCount(signal) > 0; + + if (hasListeners) { + process.prependListener(signal, wrappedCallback); + } else { + process.on(signal, async () => { + await wrappedCallback(); + process.exit(0x100 + constants.signals[signal]); + }); + } + }); + + process.on("beforeExit", wrappedCallback); + process.on("exit", () => { + if (!state) { + console.error( + "[Bucket SDK] Failed to finalize the flushing of events on process exit.", + ); + } + }); +} diff --git a/packages/node-sdk/src/types.ts b/packages/node-sdk/src/types.ts index 25b8b8b2..552f7f1b 100644 --- a/packages/node-sdk/src/types.ts +++ b/packages/node-sdk/src/types.ts @@ -288,13 +288,24 @@ export type BatchBufferOptions = { /** * The maximum size of the buffer before it is flushed. + * + * @defaultValue `100` **/ maxSize?: number; /** * The interval in milliseconds at which the buffer is flushed. + * + * @defaultValue `1000` **/ intervalMs?: number; + + /** + * Whether to flush the buffer on exit. + * + * @defaultValue `true` + */ + flushOnExit?: boolean; }; /** diff --git a/packages/node-sdk/test/client.test.ts b/packages/node-sdk/test/client.test.ts index ed6fac69..7b8d9085 100644 --- a/packages/node-sdk/test/client.test.ts +++ b/packages/node-sdk/test/client.test.ts @@ -23,6 +23,7 @@ import { SDK_VERSION_HEADER_NAME, } from "../src/config"; import fetchClient from "../src/fetch-http-client"; +import { subscribe as triggerOnExit } from "../src/flusher"; import { newRateLimiter } from "../src/rate-limiter"; import { ClientOptions, Context, FeaturesAPIResponse } from "../src/types"; @@ -45,6 +46,10 @@ vi.mock("../src/rate-limiter", async (importOriginal) => { }; }); +vi.mock("../src/flusher", () => ({ + subscribe: vi.fn(), +})); + const user = { id: "user123", age: 1, @@ -82,6 +87,7 @@ const validOptions: ClientOptions = { batchOptions: { maxSize: 99, intervalMs: 100, + flushOnExit: false, }, offline: false, }; @@ -300,6 +306,27 @@ describe("BucketClient", () => { ); }); + it("should not register an exit flush handler if `batchOptions.flushOnExit` is false", () => { + new BucketClient({ + ...validOptions, + batchOptions: { ...validOptions.batchOptions, flushOnExit: false }, + }); + + expect(triggerOnExit).not.toHaveBeenCalled(); + }); + + it.each([undefined, true])( + "should register an exit flush handler if `batchOptions.flushOnExit` is `%s`", + (flushOnExit) => { + new BucketClient({ + ...validOptions, + batchOptions: { ...validOptions.batchOptions, flushOnExit }, + }); + + expect(triggerOnExit).toHaveBeenCalledWith(expect.any(Function)); + }, + ); + it.each([ ["https://api.example.com", "https://api.example.com/bulk"], ["https://api.example.com/", "https://api.example.com/bulk"], diff --git a/packages/node-sdk/test/flusher.test.ts b/packages/node-sdk/test/flusher.test.ts new file mode 100644 index 00000000..d5b48ad4 --- /dev/null +++ b/packages/node-sdk/test/flusher.test.ts @@ -0,0 +1,212 @@ +import { constants } from "os"; +import { + afterEach, + beforeEach, + describe, + expect, + it, + MockInstance, + vi, +} from "vitest"; + +import { subscribe } from "../src/flusher"; + +describe("flusher", () => { + const mockExit = vi + .spyOn(process, "exit") + .mockImplementation((() => undefined) as any); + + const mockConsoleError = vi + .spyOn(console, "error") + .mockImplementation(() => undefined); + + const mockProcessOn = vi + .spyOn(process, "on") + .mockImplementation((_, __) => process); + + const mockProcessPrependListener = ( + vi.spyOn(process, "prependListener") as unknown as MockInstance< + [event: NodeJS.Signals, listener: NodeJS.SignalsListener], + NodeJS.Process + > + ).mockImplementation((_, __) => process); + + const mockListenerCount = vi + .spyOn(process, "listenerCount") + .mockReturnValue(0); + + function timedCallback(ms: number) { + return vi.fn().mockImplementation( + () => + new Promise((resolve) => { + setTimeout(resolve, ms); + }), + ); + } + + function getHandler(eventName: string, prepended = false) { + return prepended + ? mockProcessPrependListener.mock.calls.filter( + ([evt]) => evt === eventName, + )[0][1] + : mockProcessOn.mock.calls.filter(([evt]) => evt === eventName)[0][1]; + } + + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + vi.resetAllMocks(); + }); + + describe("signal handling", () => { + const signals = ["SIGINT", "SIGTERM", "SIGHUP", "SIGBREAK"] as const; + + describe.each(signals)("signal %s", (signal) => { + it("should handle signal with no existing listeners", async () => { + mockListenerCount.mockReturnValue(0); + const callback = vi.fn().mockResolvedValue(undefined); + + subscribe(callback); + expect(mockProcessOn).toHaveBeenCalledWith( + signal, + expect.any(Function), + ); + + getHandler(signal)(signal); + await vi.runAllTimersAsync(); + + expect(callback).toHaveBeenCalledTimes(1); + expect(mockExit).toHaveBeenCalledWith( + 0x100 + constants.signals[signal], + ); + }); + + it("should prepend handler when listeners exist", async () => { + mockListenerCount.mockReturnValue(1); + const callback = vi.fn().mockResolvedValue(undefined); + + subscribe(callback); + + expect(mockProcessPrependListener).toHaveBeenCalledWith( + signal, + expect.any(Function), + ); + + getHandler(signal, true)(signal); + + expect(callback).toHaveBeenCalledTimes(1); + expect(mockExit).not.toHaveBeenCalled(); + }); + }); + }); + + describe("beforeExit handling", () => { + it("should call callback on beforeExit", async () => { + const callback = vi.fn().mockResolvedValue(undefined); + + subscribe(callback); + + getHandler("beforeExit")(); + + expect(callback).toHaveBeenCalledTimes(1); + }); + + it("should not call callback multiple times", async () => { + const callback = vi.fn().mockResolvedValue(undefined); + + subscribe(callback); + + getHandler("beforeExit")(); + getHandler("beforeExit")(); + + expect(callback).toHaveBeenCalledTimes(1); + }); + }); + + describe("timeout handling", () => { + it("should handle timeout when callback takes too long", async () => { + subscribe(timedCallback(2000), 1000); + + getHandler("beforeExit")(); + + await vi.advanceTimersByTimeAsync(1000); + + expect(mockConsoleError).toHaveBeenCalledWith( + "[Bucket SDK] Timeout while flushing events on process exit.", + ); + }); + + it("should not timeout when callback completes in time", async () => { + subscribe(timedCallback(500), 1000); + + getHandler("beforeExit")(); + await vi.advanceTimersByTimeAsync(500); + + expect(mockConsoleError).not.toHaveBeenCalled(); + }); + }); + + describe("exit state handling", () => { + it("should log error if exit occurs before flushing starts", () => { + subscribe(timedCallback(0)); + + getHandler("exit")(); + + expect(mockConsoleError).toHaveBeenCalledWith( + "[Bucket SDK] Failed to finalize the flushing of events on process exit.", + ); + }); + + it("should log error if exit occurs before flushing completes", async () => { + subscribe(timedCallback(2000)); + getHandler("beforeExit")(); + + await vi.advanceTimersByTimeAsync(1000); + + getHandler("exit")(); + + expect(mockConsoleError).toHaveBeenCalledWith( + "[Bucket SDK] Failed to finalize the flushing of events on process exit.", + ); + }); + + it("should not log error if flushing completes before exit", async () => { + subscribe(timedCallback(500)); + + getHandler("beforeExit")(); + await vi.advanceTimersByTimeAsync(500); + + getHandler("exit")(); + + expect(mockConsoleError).not.toHaveBeenCalled(); + }); + + it("should handle callback errors gracefully", async () => { + subscribe(vi.fn().mockRejectedValue(new Error("Test error"))); + + getHandler("beforeExit")(); + await vi.runAllTimersAsync(); + + expect(mockConsoleError).toHaveBeenCalledWith( + "[Bucket SDK] An error occurred while flushing events on process exit.", + expect.any(Error), + ); + }); + }); + + it("should run the callback only once", async () => { + const callback = vi.fn().mockResolvedValue(undefined); + + subscribe(callback); + + getHandler("SIGINT")("SIGINT"); + getHandler("beforeExit")(); + + await vi.runAllTimersAsync(); + + expect(callback).toHaveBeenCalledTimes(1); + }); +}); From 5d994682b6d781d0deb51b93b9f680d53a25ab1d Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Sat, 8 Feb 2025 20:32:07 +0000 Subject: [PATCH 02/10] test(node-sdk): add offline mode tests for BucketClient - Add test to verify no exit flush handler when `offline` is true - Add test to ensure no data is flushed when in offline mode --- packages/node-sdk/test/client.test.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/node-sdk/test/client.test.ts b/packages/node-sdk/test/client.test.ts index 7b8d9085..6e9b8a78 100644 --- a/packages/node-sdk/test/client.test.ts +++ b/packages/node-sdk/test/client.test.ts @@ -315,6 +315,15 @@ describe("BucketClient", () => { expect(triggerOnExit).not.toHaveBeenCalled(); }); + it("should not register an exit flush handler if `offline` is true", () => { + new BucketClient({ + ...validOptions, + offline: true, + }); + + expect(triggerOnExit).not.toHaveBeenCalled(); + }); + it.each([undefined, true])( "should register an exit flush handler if `batchOptions.flushOnExit` is `%s`", (flushOnExit) => { @@ -928,6 +937,18 @@ describe("BucketClient", () => { ], ); }); + + it("should not flush all bulk data if `offline` is true", async () => { + const client = new BucketClient({ + ...validOptions, + offline: true, + }); + + await client.updateUser(user.id, { attributes: { age: 2 } }); + await client.flush(); + + expect(httpClient.post).not.toHaveBeenCalled(); + }); }); describe("getFeature", () => { From 1388dea61b34748c112c41a1599e48ee4fb52690 Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Sun, 9 Feb 2025 18:19:41 +0000 Subject: [PATCH 03/10] whoops: tried to over-optimize --- packages/node-sdk/src/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node-sdk/src/client.ts b/packages/node-sdk/src/client.ts index dc033180..9ae573c1 100644 --- a/packages/node-sdk/src/client.ts +++ b/packages/node-sdk/src/client.ts @@ -220,7 +220,7 @@ export class BucketClient { }; if ((config.batchOptions?.flushOnExit ?? true) && !this._config.offline) { - triggerOnExit(this.flush); + triggerOnExit(() => this.flush()); } if (!new URL(this._config.apiBaseUrl).pathname.endsWith("/")) { From 81ec58cfe2ac21bed44b11f71c3624696ee94ece Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Wed, 12 Feb 2025 21:27:22 +0700 Subject: [PATCH 04/10] feat(node-sdk): add promise timeout utility function Introduces `withTimeout()` and `TimeoutError` to provide a robust way of adding timeouts to promises. The new utility allows wrapping promises with a configurable timeout, rejecting with a custom error if the promise doesn't resolve within the specified time. --- packages/node-sdk/src/utils.ts | 41 ++++++++++++ packages/node-sdk/test/utils.test.ts | 95 +++++++++++++++++++++++++++- 2 files changed, 135 insertions(+), 1 deletion(-) diff --git a/packages/node-sdk/src/utils.ts b/packages/node-sdk/src/utils.ts index e2ec10c9..150714da 100644 --- a/packages/node-sdk/src/utils.ts +++ b/packages/node-sdk/src/utils.ts @@ -175,3 +175,44 @@ export function once ReturnType>( return returned; }; } + +export class TimeoutError extends Error { + constructor(timeoutMs: number) { + super(`Operation timed out after ${timeoutMs}ms`); + this.name = "TimeoutError"; + } +} + +/** + * Wraps a promise with a timeout. If the promise doesn't resolve within the specified + * timeout, it will reject with a timeout error. The original promise will still + * continue to execute but its result will be ignored. + * + * @param promise - The promise to wrap with a timeout + * @param timeoutMs - The timeout in milliseconds + * @returns A promise that resolves with the original promise result or rejects with a timeout error + * @throws {Error} If the timeout is reached before the promise resolves + **/ +export function withTimeout( + promise: Promise, + timeoutMs: number, +): Promise { + ok(timeoutMs > 0, "timeout must be a positive number"); + + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + reject(new TimeoutError(timeoutMs)); + }, timeoutMs); + + promise + .then((result) => { + resolve(result); + }) + .catch((error) => { + reject(error); + }) + .finally(() => { + clearTimeout(timeoutId); + }); + }); +} diff --git a/packages/node-sdk/test/utils.test.ts b/packages/node-sdk/test/utils.test.ts index 0ef0e5be..f2e98f80 100644 --- a/packages/node-sdk/test/utils.test.ts +++ b/packages/node-sdk/test/utils.test.ts @@ -1,5 +1,5 @@ import { createHash } from "crypto"; -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; import { decorateLogger, @@ -8,6 +8,8 @@ import { mergeSkipUndefined, ok, once, + withTimeout, + TimeoutError, } from "../src/utils"; describe("isObject", () => { @@ -205,3 +207,94 @@ describe("once()", () => { expect(fn).toHaveBeenCalledTimes(1); }); }); + +describe("withTimeout()", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("should resolve when promise completes before timeout", async () => { + const promise = Promise.resolve("success"); + const result = withTimeout(promise, 1000); + + await expect(result).resolves.toBe("success"); + }); + + it("should reject with TimeoutError when promise takes too long", async () => { + const slowPromise = new Promise((resolve) => { + setTimeout(() => resolve("too late"), 2000); + }); + + const result = withTimeout(slowPromise, 1000); + + vi.advanceTimersByTime(1000); + + await expect(result).rejects.toThrow("Operation timed out after 1000ms"); + await expect(result).rejects.toBeInstanceOf(TimeoutError); + }); + + it("should propagate original promise rejection", async () => { + const error = new Error("original error"); + const failedPromise = Promise.reject(error); + + const result = withTimeout(failedPromise, 1000); + + await expect(result).rejects.toBe(error); + }); + + it("should reject immediately for negative timeout", async () => { + const promise = Promise.resolve("success"); + + await expect(async () => { + await withTimeout(promise, -1); + }).rejects.toThrow("validation failed: timeout must be a positive number"); + }); + + it("should reject immediately for zero timeout", async () => { + const promise = Promise.resolve("success"); + + await expect(async () => { + await withTimeout(promise, 0); + }).rejects.toThrow("validation failed: timeout must be a positive number"); + }); + + it("should clean up timeout when promise resolves", async () => { + const clearTimeoutSpy = vi.spyOn(global, "clearTimeout"); + const promise = Promise.resolve("success"); + + await withTimeout(promise, 1000); + await vi.runAllTimersAsync(); + + expect(clearTimeoutSpy).toHaveBeenCalled(); + clearTimeoutSpy.mockRestore(); + }); + + it("should clean up timeout when promise rejects", async () => { + const clearTimeoutSpy = vi.spyOn(global, "clearTimeout"); + const promise = Promise.reject(new Error("fail")); + + await expect(withTimeout(promise, 1000)).rejects.toThrow("fail"); + + expect(clearTimeoutSpy).toHaveBeenCalled(); + clearTimeoutSpy.mockRestore(); + }); + + it("should not resolve after timeout occurs", async () => { + const slowPromise = new Promise((resolve) => { + setTimeout(() => resolve("too late"), 2000); + }); + + const result = withTimeout(slowPromise, 1000); + + vi.advanceTimersByTime(1000); // Trigger timeout + await expect(result).rejects.toThrow("Operation timed out after 1000ms"); + + vi.advanceTimersByTime(1000); // Complete the original promise + // The promise should still be rejected with the timeout error + await expect(result).rejects.toThrow("Operation timed out after 1000ms"); + }); +}); From 39da372aba8754900a95ecf67285657426deda00 Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Wed, 12 Feb 2025 21:31:30 +0700 Subject: [PATCH 05/10] refactor(node-sdk): simplify event flushing timeout handling Use the new `withTimeout()` utility to replace manual Promise.race() implementation in flusher, improving error handling and readability --- packages/node-sdk/src/flusher.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/node-sdk/src/flusher.ts b/packages/node-sdk/src/flusher.ts index 3840a2f6..895eb2a0 100644 --- a/packages/node-sdk/src/flusher.ts +++ b/packages/node-sdk/src/flusher.ts @@ -1,6 +1,7 @@ import { constants } from "os"; import { END_FLUSH_TIMEOUT_MS } from "./config"; +import { withTimeout, TimeoutError } from "./utils"; type Callback = () => Promise; @@ -20,21 +21,18 @@ export function subscribe( state = false; try { - const result = await Promise.race([ - new Promise((resolve) => setTimeout(() => resolve(true), timeout)), - callback(), - ]); - - if (result === true) { + await withTimeout(callback(), timeout); + } catch (error) { + if (error instanceof TimeoutError) { console.error( "[Bucket SDK] Timeout while flushing events on process exit.", ); + } else { + console.error( + "[Bucket SDK] An error occurred while flushing events on process exit.", + error, + ); } - } catch (error) { - console.error( - "[Bucket SDK] An error occurred while flushing events on process exit.", - error, - ); } state = true; From 1e11c48e863bb96cf1ec6d372754bfb4cd37e3ae Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Thu, 13 Feb 2025 12:21:03 +0700 Subject: [PATCH 06/10] refactor(node-sdk): sort imports in test and flusher files --- packages/node-sdk/src/flusher.ts | 2 +- packages/node-sdk/test/utils.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/node-sdk/src/flusher.ts b/packages/node-sdk/src/flusher.ts index 895eb2a0..684f1e99 100644 --- a/packages/node-sdk/src/flusher.ts +++ b/packages/node-sdk/src/flusher.ts @@ -1,7 +1,7 @@ import { constants } from "os"; import { END_FLUSH_TIMEOUT_MS } from "./config"; -import { withTimeout, TimeoutError } from "./utils"; +import { TimeoutError, withTimeout } from "./utils"; type Callback = () => Promise; diff --git a/packages/node-sdk/test/utils.test.ts b/packages/node-sdk/test/utils.test.ts index f2e98f80..43fc90ae 100644 --- a/packages/node-sdk/test/utils.test.ts +++ b/packages/node-sdk/test/utils.test.ts @@ -1,5 +1,5 @@ import { createHash } from "crypto"; -import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { decorateLogger, @@ -8,8 +8,8 @@ import { mergeSkipUndefined, ok, once, - withTimeout, TimeoutError, + withTimeout, } from "../src/utils"; describe("isObject", () => { From b1684eb3b9d58029b4a41556af8e0d25277492ad Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Thu, 13 Feb 2025 12:22:40 +0700 Subject: [PATCH 07/10] docs(node-sdk): remove manual flush handling example from README --- packages/node-sdk/README.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/node-sdk/README.md b/packages/node-sdk/README.md index 57964a7c..cff744dc 100644 --- a/packages/node-sdk/README.md +++ b/packages/node-sdk/README.md @@ -330,17 +330,6 @@ const client = new BucketClient({ > If you are creating multiple client instances in your application, it's recommended to disable `flushOnExit` > to avoid potential conflicts during process shutdown. In such cases, you should implement your own flush handling. -A naive example of manual flush handling: - -```typescript -process.on("SIGINT", () => { - console.log("Flushing batch buffer..."); - client.flush().then(() => { - process.exit(0); - }); -}); -``` - When you bind a client to a user/company, this data is matched against the targeting rules. To get accurate targeting, you must ensure that the user/company information provided is sufficient to match against the targeting rules you've From fc9d28c733701d151796b060472d724a4ba3197b Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Thu, 13 Feb 2025 12:38:34 +0700 Subject: [PATCH 08/10] chore(node-sdk): remove unused host configuration and add dotenv import - Remove manual host configuration in bucket example - Add dotenv import in serve example to load environment variables --- packages/node-sdk/example/bucket.ts | 5 ----- packages/node-sdk/example/serve.ts | 2 ++ 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/node-sdk/example/bucket.ts b/packages/node-sdk/example/bucket.ts index 6aebcfd8..37add14d 100644 --- a/packages/node-sdk/example/bucket.ts +++ b/packages/node-sdk/example/bucket.ts @@ -14,11 +14,6 @@ let featureOverrides = (context: Context): FeatureOverrides => { return { "delete-todos": true }; // feature keys checked at compile time }; -let host = undefined; -if (process.env.BUCKET_HOST) { - host = process.env.BUCKET_HOST; -} - // Create a new BucketClient instance with the secret key and default features // The default features will be used if the user does not have any features set // Create a bucketConfig.json file to configure the client or set environment variables diff --git a/packages/node-sdk/example/serve.ts b/packages/node-sdk/example/serve.ts index b4abd9d9..ada781f5 100644 --- a/packages/node-sdk/example/serve.ts +++ b/packages/node-sdk/example/serve.ts @@ -1,3 +1,5 @@ +import "dotenv/config"; + import bucket from "./bucket"; import app from "./app"; From ecea7a6624e65cb6693a69f84f2af842dedef2de Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Thu, 13 Feb 2025 12:40:54 +0700 Subject: [PATCH 09/10] fix(node-sdk): adjust process exit code for signal handling Update the exit code calculation in signal handling to use 0x80 instead of 0x100, ensuring consistent exit code behavior --- packages/node-sdk/src/flusher.ts | 2 +- packages/node-sdk/test/flusher.test.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/node-sdk/src/flusher.ts b/packages/node-sdk/src/flusher.ts index 684f1e99..47e4937d 100644 --- a/packages/node-sdk/src/flusher.ts +++ b/packages/node-sdk/src/flusher.ts @@ -46,7 +46,7 @@ export function subscribe( } else { process.on(signal, async () => { await wrappedCallback(); - process.exit(0x100 + constants.signals[signal]); + process.exit(0x80 + constants.signals[signal]); }); } }); diff --git a/packages/node-sdk/test/flusher.test.ts b/packages/node-sdk/test/flusher.test.ts index d5b48ad4..e6b09511 100644 --- a/packages/node-sdk/test/flusher.test.ts +++ b/packages/node-sdk/test/flusher.test.ts @@ -79,9 +79,7 @@ describe("flusher", () => { await vi.runAllTimersAsync(); expect(callback).toHaveBeenCalledTimes(1); - expect(mockExit).toHaveBeenCalledWith( - 0x100 + constants.signals[signal], - ); + expect(mockExit).toHaveBeenCalledWith(0x80 + constants.signals[signal]); }); it("should prepend handler when listeners exist", async () => { From 3050f4f4ab86a4924429e92583872718adcfb25f Mon Sep 17 00:00:00 2001 From: Alexandru Ciobanu Date: Thu, 13 Feb 2025 12:42:12 +0700 Subject: [PATCH 10/10] chore(node-sdk): bump package version to 1.5.3 --- packages/node-sdk/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node-sdk/package.json b/packages/node-sdk/package.json index 81bde285..3ba99cea 100644 --- a/packages/node-sdk/package.json +++ b/packages/node-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@bucketco/node-sdk", - "version": "1.5.2", + "version": "1.5.3", "license": "MIT", "repository": { "type": "git",