diff --git a/README.md b/README.md index 61c5f29..f45efba 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ The Hyphen Node.js SDK is a JavaScript library that allows developers to easily - [Creating a QR Code from a Short Code](#creating-a-qr-code-from-a-short-code) - [Get QR Codes for a Short Code](#get-qr-codes-for-a-short-code) - [Deleting a QR Code](#deleting-a-qr-code) +- [Migrating to v3](#migrating-to-v3) - [Contributing](#contributing) - [Testing Your Changes](#testing-your-changes) - [License and Copyright](#license-and-copyright) @@ -958,6 +959,54 @@ const response = await link.deleteQrCode(code, qr); console.log('Delete QR Code Response:', response); ``` +# Migrating to v3 + +v3 upgrades the underlying event system to [hookified v2](https://hookified.org). There are two breaking changes: + +## `throwOnEmptyListeners` now defaults to `true` + +In v2, emitting events (`error`) without any registered listeners was silently ignored. In v3, this will throw an error by default. If your code emits events, you must register listeners: + +```typescript +const hyphen = new Hyphen({ apiKey: 'your-key' }); + +// Register listeners before triggering operations that emit events +hyphen.on('error', (message) => { + console.error('Error:', message); +}); +``` + +If you want to restore the previous behavior where unhandled events are silently ignored, pass `throwOnEmptyListeners: false` in the options: + +```typescript +const hyphen = new Hyphen({ + apiKey: 'your-key', + throwOnEmptyListeners: false, +}); + +const toggle = new Toggle({ + publicApiKey: 'public_your-key', + throwOnEmptyListeners: false, +}); +``` + +## `throwErrors` option removed + +The `throwErrors` option has been removed from `BaseServiceOptions`, `HyphenOptions`, and all service classes. Error throwing is now handled natively by hookified v2. + +To get the previous `throwErrors: true` behavior, use `throwOnEmitError: true` instead: + +```typescript +// v2 (old) +const netInfo = new NetInfo({ throwErrors: true }); + +// v3 (new) +const netInfo = new NetInfo({ throwOnEmitError: true }); +``` + +The `throwErrors` getter/setter on service instances has also been removed as you can use `.throwOnEmitError` getter/setter now. + + # Contributing We welcome contributions to the Hyphen Node.js SDK! If you have an idea for a new feature, bug fix, or improvement, please follow these steps: diff --git a/package.json b/package.json index 357e7d7..16cc7e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hyphen/sdk", - "version": "2.2.1", + "version": "3.0.0", "description": "Hyphen SDK for Node.js", "type": "module", "main": "dist/index.cjs", @@ -51,7 +51,7 @@ "@cacheable/net": "^2.0.6", "@faker-js/faker": "^10.3.0", "cacheable": "^2.3.3", - "hookified": "^1.15.1", + "hookified": "^2.0.1", "pino": "^10.3.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b101f2..2924643 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^2.3.3 version: 2.3.3 hookified: - specifier: ^1.15.1 - version: 1.15.1 + specifier: ^2.0.1 + version: 2.0.1 pino: specifier: ^10.3.1 version: 10.3.1 @@ -1201,6 +1201,9 @@ packages: hookified@1.15.1: resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} + hookified@2.0.1: + resolution: {integrity: sha512-rCuuGZ7qae0+C0ySqe8nRj8JJ5/6zym5YBs19iga9ju+ubLELkgpvnfi4A6NP8Aow7mCxfCqyAAbkj9QqyEXRg==} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -2792,6 +2795,8 @@ snapshots: hookified@1.15.1: {} + hookified@2.0.1: {} + hosted-git-info@2.8.9: {} hosted-git-info@4.1.0: diff --git a/src/base-service.ts b/src/base-service.ts index 00a3c4b..861c102 100644 --- a/src/base-service.ts +++ b/src/base-service.ts @@ -14,9 +14,7 @@ export interface HttpResponse { request?: any; } -export type BaseServiceOptions = { - throwErrors?: boolean; -} & HookifiedOptions; +export type BaseServiceOptions = HookifiedOptions; export enum ErrorMessages { API_KEY_REQUIRED = "API key is required. Please provide it via options or set the HYPHEN_API_KEY environment variable.", @@ -26,14 +24,10 @@ export enum ErrorMessages { export class BaseService extends Hookified { private _log: pino.Logger = pino(); private _cache = new Cacheable(); - private _throwErrors = false; private _net: CacheableNet; constructor(options?: BaseServiceOptions) { super(options); - if (options && options.throwErrors !== undefined) { - this._throwErrors = options.throwErrors; - } this._net = new CacheableNet({ cache: this._cache, }); @@ -56,21 +50,9 @@ export class BaseService extends Hookified { this._net.cache = value; } - public get throwErrors(): boolean { - return this._throwErrors; - } - - public set throwErrors(value: boolean) { - this._throwErrors = value; - } - public error(message: string, ...args: any[]): void { this._log.error(message, ...args); - this.emit("error", message, ...args); - if (this.throwErrors) { - throw new Error(message); - } } public warn(message: string, ...args: any[]): void { diff --git a/src/hyphen.ts b/src/hyphen.ts index 44c8450..e517d70 100644 --- a/src/hyphen.ts +++ b/src/hyphen.ts @@ -14,29 +14,20 @@ export type HyphenOptions = { * This is used for authenticated endpoints that require an API key. */ apiKey?: string; - /** - * Whether to throw errors or not. - * If set to true, errors will be thrown instead of logged. - * @default false - */ - throwErrors?: boolean; /** * Options for the Toggle service. - * Excludes publicApiKey and throwErrors from ToggleOptions. * @see ToggleOptions * @default {Toggle} */ toggle?: Omit; /** * Options for the NetInfo service. - * Excludes apiKey and throwErrors from NetInfoOptions. * @see NetInfoOptions * @default {NetInfo} */ netInfo?: Omit; /** * Options for the Link service. - * Excludes apiKey and throwErrors from LinkOptions. * @see LinkOptions * @default {Link} */ diff --git a/test/base-service.test.ts b/test/base-service.test.ts index 141ab65..75f30bb 100644 --- a/test/base-service.test.ts +++ b/test/base-service.test.ts @@ -17,7 +17,6 @@ describe("BaseService", () => { const service = new BaseService(); expect(service.log).toBeDefined(); expect(service.cache).toBeDefined(); - expect(service.throwErrors).toBe(false); }); test("should allow setting and getting log", () => { @@ -34,19 +33,12 @@ describe("BaseService", () => { expect(service.cache).toBe(cache); }); - test("should allow setting and getting throwErrors", () => { - const service = new BaseService({ throwErrors: true }); - service.throwErrors = true; - expect(service.throwErrors).toBe(true); - }); - test("should log error and emit error event", () => { - const service = new BaseService({ throwErrors: true }); + const service = new BaseService(); + service.on("error", () => {}); const errorSpy = vi.spyOn(service.log, "error"); const emitSpy = vi.spyOn(service, "emit"); - expect(() => { - service.error("Test error"); - }).toThrow("Test error"); + service.error("Test error"); expect(errorSpy).toHaveBeenCalledWith("Test error"); expect(emitSpy).toHaveBeenCalledWith("error", "Test error"); }); diff --git a/test/hyphen.test.ts b/test/hyphen.test.ts index 484e2e9..e4a2cdb 100644 --- a/test/hyphen.test.ts +++ b/test/hyphen.test.ts @@ -58,6 +58,7 @@ describe("Hyphen", () => { describe("Hyphen Emitters", () => { test("should emit link error events", () => { const hyphen = new Hyphen(); + hyphen.on("error", () => {}); const errorSpy = vi.spyOn(hyphen.link, "emit"); hyphen.link.error("Test error"); expect(errorSpy).toHaveBeenCalledWith("error", "Test error"); @@ -79,6 +80,7 @@ describe("Hyphen Emitters", () => { test("should emit netInfo error events", () => { const hyphen = new Hyphen(); + hyphen.on("error", () => {}); const errorSpy = vi.spyOn(hyphen, "emit"); hyphen.netInfo.error("Test error"); expect(errorSpy).toHaveBeenCalledWith("error", "Test error"); @@ -100,6 +102,7 @@ describe("Hyphen Emitters", () => { test("should emit toggle error events", () => { const hyphen = new Hyphen(); + hyphen.on("error", () => {}); const errorSpy = vi.spyOn(hyphen, "emit"); hyphen.toggle.emit("error", "Test error"); expect(errorSpy).toHaveBeenCalledWith("error", "Test error"); diff --git a/test/net-info.test.ts b/test/net-info.test.ts index 5c74d40..24a1854 100644 --- a/test/net-info.test.ts +++ b/test/net-info.test.ts @@ -21,7 +21,6 @@ describe("NetInfo", () => { const netInfo = new NetInfo(); expect(netInfo.log).toBeDefined(); expect(netInfo.cache).toBeDefined(); - expect(netInfo.throwErrors).toBe(false); }); test("should allow setting and getting apiKey", () => { @@ -44,7 +43,7 @@ describe("NetInfo", () => { delete process.env.HYPHEN_API_KEY; let didThrow = false; try { - new NetInfo({ throwErrors: true }); + new NetInfo({ throwOnEmitError: true }); } catch (error) { expect(error).toEqual(new Error(ErrorMessages.API_KEY_REQUIRED)); didThrow = true; @@ -65,7 +64,7 @@ describe("NetInfo", () => { let didThrow = false; try { new NetInfo({ - throwErrors: true, + throwOnEmitError: true, apiKey: "public_api_key", }); } catch (error) { @@ -89,7 +88,7 @@ describe("NetInfo", () => { delete process.env.HYPHEN_API_KEY; let didThrow = false; try { - const netInfo = new NetInfo({ throwErrors: true }); + const netInfo = new NetInfo({ throwOnEmitError: true }); netInfo.apiKey = undefined; // Explicitly set apiKey to undefined await netInfo.getIpInfo("1.1.1.1"); } catch (error) { @@ -109,6 +108,7 @@ describe("NetInfo", () => { async () => { // API key should be set in the environment variable HYPHEN_API_KEY const netInfo = new NetInfo(); + netInfo.on("error", () => {}); const invalidIpAddress = invalidIpAddresses[ Math.floor(Math.random() * invalidIpAddresses.length) @@ -150,6 +150,7 @@ describe("NetInfo", () => { test("should handle empty IP array gracefully", async () => { const netInfo = new NetInfo(); + netInfo.on("error", () => {}); const ipInfos = await netInfo.getIpInfos([]); expect(ipInfos).toBeDefined(); expect(ipInfos.length).toBe(0); diff --git a/test/toggle-evals.test.ts b/test/toggle-evals.test.ts index 325b0c7..0ae8c44 100644 --- a/test/toggle-evals.test.ts +++ b/test/toggle-evals.test.ts @@ -142,6 +142,7 @@ describe("Toggle Evaluations", () => { applicationId: "test-app", defaultContext: { targetingKey: "test" }, }); + toggle.on("error", () => {}); const result = await toggle.get("test-toggle", "default-value"); expect(result).toBe("default-value"); @@ -156,6 +157,7 @@ describe("Toggle Evaluations", () => { publicApiKey: "public_test-key", applicationId: "test-app", }); + toggle.on("error", () => {}); const result = await toggle.get("test-toggle", "fallback"); expect(result).toBe("fallback"); @@ -171,6 +173,7 @@ describe("Toggle Evaluations", () => { applicationId: "", defaultContext: { targetingKey: "test" }, }); + toggle.on("error", () => {}); const result = await toggle.get("test-toggle", "default"); expect(result).toBe("default"); @@ -199,6 +202,7 @@ describe("Toggle Evaluations", () => { defaultContext: { targetingKey: "test" }, horizonUrls: ["https://invalid-domain.test"], }); + toggle.on("error", () => {}); const result = await toggle.get("test-toggle", "fallback-value"); expect(result).toBe("fallback-value"); @@ -210,6 +214,7 @@ describe("Toggle Evaluations", () => { applicationId: hyphenApplicationId, defaultContext: { targetingKey: "test" }, }); + toggle.on("error", () => {}); const result = await toggle.get("non-existent-toggle", "not-found"); expect(result).toBe("not-found"); @@ -242,6 +247,7 @@ describe("Toggle Evaluations", () => { applicationId: undefined, // This will cause validation to fail defaultContext: { targetingKey: "test" }, }); + toggle.on("error", () => {}); const result = await toggle.get("test-toggle", "validation-failed"); expect(result).toBe("validation-failed"); @@ -270,6 +276,7 @@ describe("Toggle Evaluations", () => { applicationId: "", // This will cause validation to fail defaultContext: { targetingKey: "test" }, }); + toggle.on("error", () => {}); const result = await toggle.get("test-toggle", "validation-failed"); expect(result).toBe("validation-failed"); @@ -281,6 +288,7 @@ describe("Toggle Evaluations", () => { // No applicationId provided, will be undefined -> "" defaultContext: { targetingKey: "test" }, }); + toggle.on("error", () => {}); const result = await toggle.get("test-toggle", "validation-failed"); expect(result).toBe("validation-failed"); diff --git a/test/toggle.test.ts b/test/toggle.test.ts index 13f7f1c..7b091f4 100644 --- a/test/toggle.test.ts +++ b/test/toggle.test.ts @@ -657,6 +657,7 @@ describe("Hyphen sdk", () => { const toggle = new Toggle({ horizonUrls: ["https://api.test.com"], }); + toggle.on("error", () => {}); const result = await toggle.get("feature-flag", false); expect(result).toBe(false); // Returns defaultValue when error occurs