Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Propagate Hookified options into Toggle constructor

Upgrading to hookified v2 changes unhandled emit behavior, but Toggle still does not accept or forward Hookified options (ToggleOptions lacks HookifiedOptions and the constructor uses super()), so new Toggle({ throwOnEmptyListeners: false }) has no effect. In error paths like Toggle.get() (which emits error before returning defaultValue), callers now get thrown exceptions unless they add listeners, which breaks the documented migration behavior for standalone Toggle instances.

Useful? React with 👍 / 👎.

"pino": "^10.3.1"
}
}
9 changes: 7 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 1 addition & 19 deletions src/base-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ export interface HttpResponse<T = any> {
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.",
Expand All @@ -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,
});
Expand All @@ -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 {
Expand Down
9 changes: 0 additions & 9 deletions src/hyphen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ToggleOptions, "publicApiKey">;
/**
* Options for the NetInfo service.
* Excludes apiKey and throwErrors from NetInfoOptions.
* @see NetInfoOptions
* @default {NetInfo}
*/
netInfo?: Omit<NetInfoOptions, "apiKey">;
/**
* Options for the Link service.
* Excludes apiKey and throwErrors from LinkOptions.
* @see LinkOptions
* @default {Link}
*/
Expand Down
14 changes: 3 additions & 11 deletions test/base-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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");
});
Expand Down
3 changes: 3 additions & 0 deletions test/hyphen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
Expand All @@ -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");
Expand Down
9 changes: 5 additions & 4 deletions test/net-info.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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;
Expand All @@ -65,7 +64,7 @@ describe("NetInfo", () => {
let didThrow = false;
try {
new NetInfo({
throwErrors: true,
throwOnEmitError: true,
apiKey: "public_api_key",
});
} catch (error) {
Expand All @@ -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) {
Expand All @@ -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)
Expand Down Expand Up @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions test/toggle-evals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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");
Expand All @@ -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");
Expand Down Expand Up @@ -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");
Expand All @@ -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");
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand All @@ -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");
Expand Down
1 change: 1 addition & 0 deletions test/toggle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading