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
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.26.8"
".": "0.26.9"
}
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## 0.26.9 (2023-09-25)

Full Changelog: [v0.26.8...v0.26.9](https://github.com/lithic-com/lithic-node/compare/v0.26.8...v0.26.9)

### Features

* **client:** handle retry-after with a date ([#213](https://github.com/lithic-com/lithic-node/issues/213)) ([53eb832](https://github.com/lithic-com/lithic-node/commit/53eb832e403bcd6ccf1820f66ecd47b44b8aad3f))
* **package:** export a root error type ([#212](https://github.com/lithic-com/lithic-node/issues/212)) ([78f89c1](https://github.com/lithic-com/lithic-node/commit/78f89c1b8ff5bf7e521c178da7af8abb2b466963))


### Documentation

* **api.md:** add shared models ([#211](https://github.com/lithic-com/lithic-node/issues/211)) ([bd02f27](https://github.com/lithic-com/lithic-node/commit/bd02f27a3126ffa6ccaee90b71c7c0a5b3301af5))
* **README:** fix variable names in some examples ([#209](https://github.com/lithic-com/lithic-node/issues/209)) ([4b28d0d](https://github.com/lithic-com/lithic-node/commit/4b28d0dcac8ab512eaff022608da00bdb74459d3))

## 0.26.8 (2023-09-20)

Full Changelog: [v0.26.7...v0.26.8](https://github.com/lithic-com/lithic-node/compare/v0.26.7...v0.26.8)
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,9 @@ const response = await lithic.cards.create({ type: 'SINGLE_USE' }).asResponse();
console.log(response.headers.get('X-My-Header'));
console.log(response.statusText); // access the underlying Response object

const { data: cards, response: raw } = await lithic.cards.create({ type: 'SINGLE_USE' }).withResponse();
const { data: card, response: raw } = await lithic.cards.create({ type: 'SINGLE_USE' }).withResponse();
console.log(raw.headers.get('X-My-Header'));
console.log(cards.token);
console.log(card.token);
```

## Configuring an HTTP(S) Agent (e.g., for proxies)
Expand Down
8 changes: 8 additions & 0 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ Methods:

- <code title="get /status">client.<a href="./src/index.ts">apiStatus</a>() -> APIStatus</code>

# Shared

Types:

- <code><a href="./src/resources/shared.ts">Address</a></code>
- <code><a href="./src/resources/shared.ts">Carrier</a></code>
- <code><a href="./src/resources/shared.ts">ShippingAddress</a></code>

# Accounts

Types:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lithic",
"version": "0.26.8",
"version": "0.26.9",
"description": "Client library for the Lithic API",
"author": "Lithic <sdk-feedback@lithic.com>",
"types": "dist/index.d.ts",
Expand Down
67 changes: 39 additions & 28 deletions src/core.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { VERSION } from './version';
import { APIError, APIConnectionError, APIConnectionTimeoutError, APIUserAbortError } from './error';
import {
LithicError,
APIError,
APIConnectionError,
APIConnectionTimeoutError,
APIUserAbortError,
} from './error';
import {
kind as shimsKind,
type Readable,
Expand Down Expand Up @@ -433,7 +439,7 @@ export abstract class APIClient {
if (value === null) {
return `${encodeURIComponent(key)}=`;
}
throw new Error(
throw new LithicError(
`Cannot stringify type ${typeof value}; Expected string, number, boolean, or null. If you need to pass nested query parameters, you can manually encode them, e.g. { query: { 'foo[key1]': value1, 'foo[key2]': value2 } }, and please open a GitHub issue requesting better support for your use case.`,
);
})
Expand Down Expand Up @@ -496,32 +502,37 @@ export abstract class APIClient {
retriesRemaining -= 1;

// About the Retry-After header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
//
// TODO: we may want to handle the case where the header is using the http-date syntax: "Retry-After: <http-date>".
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After#syntax for details.
const retryAfter = parseInt(responseHeaders?.['retry-after'] || '');
let timeoutMillis: number | undefined;
const retryAfterHeader = responseHeaders?.['retry-after'];
if (retryAfterHeader) {
const timeoutSeconds = parseInt(retryAfterHeader);
if (!Number.isNaN(timeoutSeconds)) {
timeoutMillis = timeoutSeconds * 1000;
} else {
timeoutMillis = Date.parse(retryAfterHeader) - Date.now();
}
}

const maxRetries = options.maxRetries ?? this.maxRetries;
const timeout = this.calculateRetryTimeoutSeconds(retriesRemaining, retryAfter, maxRetries) * 1000;
await sleep(timeout);
// If the API asks us to wait a certain amount of time (and it's a reasonable amount),
// just do what it says, but otherwise calculate a default
if (
!timeoutMillis ||
!Number.isInteger(timeoutMillis) ||
timeoutMillis <= 0 ||
timeoutMillis > 60 * 1000
) {
const maxRetries = options.maxRetries ?? this.maxRetries;
timeoutMillis = this.calculateDefaultRetryTimeoutMillis(retriesRemaining, maxRetries);
}
await sleep(timeoutMillis);

return this.makeRequest(options, retriesRemaining);
}

private calculateRetryTimeoutSeconds(
retriesRemaining: number,
retryAfter: number,
maxRetries: number,
): number {
private calculateDefaultRetryTimeoutMillis(retriesRemaining: number, maxRetries: number): number {
const initialRetryDelay = 0.5;
const maxRetryDelay = 2;

// If the API asks us to wait a certain amount of time (and it's a reasonable amount),
// just do what it says.
if (Number.isInteger(retryAfter) && retryAfter <= 60) {
return retryAfter;
}

const numRetries = maxRetries - retriesRemaining;

// Apply exponential backoff, but not more than the max.
Expand All @@ -530,7 +541,7 @@ export abstract class APIClient {
// Apply some jitter, plus-or-minus half a second.
const jitter = Math.random() - 0.5;

return sleepSeconds + jitter;
return (sleepSeconds + jitter) * 1000;
}

private getUserAgent(): string {
Expand Down Expand Up @@ -592,7 +603,7 @@ export abstract class AbstractPage<Item> implements AsyncIterable<Item> {
async getNextPage(): Promise<this> {
const nextInfo = this.nextPageInfo();
if (!nextInfo) {
throw new Error(
throw new LithicError(
'No next page expected; please check `.hasNextPage()` before calling `.getNextPage()`.',
);
}
Expand Down Expand Up @@ -918,10 +929,10 @@ export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve

const validatePositiveInteger = (name: string, n: unknown): number => {
if (typeof n !== 'number' || !Number.isInteger(n)) {
throw new Error(`${name} must be an integer`);
throw new LithicError(`${name} must be an integer`);
}
if (n < 0) {
throw new Error(`${name} must be a positive integer`);
throw new LithicError(`${name} must be a positive integer`);
}
return n;
};
Expand All @@ -932,7 +943,7 @@ export const castToError = (err: any): Error => {
};

export const ensurePresent = <T>(value: T | null | undefined): T => {
if (value == null) throw new Error(`Expected a value to be given but received ${value} instead.`);
if (value == null) throw new LithicError(`Expected a value to be given but received ${value} instead.`);
return value;
};

Expand All @@ -955,14 +966,14 @@ export const coerceInteger = (value: unknown): number => {
if (typeof value === 'number') return Math.round(value);
if (typeof value === 'string') return parseInt(value, 10);

throw new Error(`Could not coerce ${value} (type: ${typeof value}) into a number`);
throw new LithicError(`Could not coerce ${value} (type: ${typeof value}) into a number`);
};

export const coerceFloat = (value: unknown): number => {
if (typeof value === 'number') return value;
if (typeof value === 'string') return parseFloat(value);

throw new Error(`Could not coerce ${value} (type: ${typeof value}) into a number`);
throw new LithicError(`Could not coerce ${value} (type: ${typeof value}) into a number`);
};

export const coerceBoolean = (value: unknown): boolean => {
Expand Down Expand Up @@ -1066,5 +1077,5 @@ export const toBase64 = (str: string | null | undefined): string => {
return btoa(str);
}

throw new Error('Cannot generate b64 string; Expected `Buffer` or `btoa` to be defined');
throw new LithicError('Cannot generate b64 string; Expected `Buffer` or `btoa` to be defined');
};
4 changes: 3 additions & 1 deletion src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import { castToError, Headers } from './core';

export class APIError extends Error {
export class LithicError extends Error {}

export class APIError extends LithicError {
readonly status: number | undefined;
readonly headers: Headers | undefined;
readonly error: Object | undefined;
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export class Lithic extends Core.APIClient {
...opts
}: ClientOptions = {}) {
if (apiKey === undefined) {
throw new Error(
throw new Errors.LithicError(
"The LITHIC_API_KEY environment variable is missing or empty; either provide it, or instantiate the Lithic client with an apiKey option, like new Lithic({ apiKey: 'my apiKey' }).",
);
}
Expand Down Expand Up @@ -187,6 +187,7 @@ export class Lithic extends Core.APIClient {

static Lithic = this;

static LithicError = Errors.LithicError;
static APIError = Errors.APIError;
static APIConnectionError = Errors.APIConnectionError;
static APIConnectionTimeoutError = Errors.APIConnectionTimeoutError;
Expand All @@ -202,6 +203,7 @@ export class Lithic extends Core.APIClient {
}

export const {
LithicError,
APIError,
APIConnectionError,
APIConnectionTimeoutError,
Expand Down
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const VERSION = '0.26.8'; // x-release-please-version
export const VERSION = '0.26.9'; // x-release-please-version