magicbell
Version:
MagicBell API wrapper
85 lines • 3.37 kB
JavaScript
import '../lib/signal-polyfill.js';
import ky from '@smeijer/ky';
import urlJoin from 'url-join';
import { tryParse } from '../lib/utils.js';
import { Cache } from './cache.js';
import { createError } from './error.js';
import { withRequestHeaders } from './headers.js';
import { withRequestLogging } from './log.js';
import { assertHasSomeOptions, assertHasValidOptions, mergeHooks } from './options.js';
import { withRequestTelemetry } from './telemetry.js';
export const DEFAULT_OPTIONS = {
host: 'https://api.magicbell.com',
timeout: 30_000,
maxRetries: 3,
maxRetryDelay: 60,
telemetry: true,
cacheTTL: 1000,
};
export class Client {
#options;
#cache;
constructor(options) {
assertHasValidOptions(options);
assertHasSomeOptions(options, ['token', 'apiKey']);
this.#options = { ...DEFAULT_OPTIONS, ...options };
this.#cache = new Cache({ ttl: this.#options.cacheTTL });
}
hasFlag(flag) {
return this.#options.features?.[flag] || false;
}
async request({ method, path, data, params, headers: reqHeaders }, options) {
const requestOptions = {
...this.#options,
...options,
headers: { ...this.#options.headers, ...reqHeaders },
};
// don't just use `new URL(path, host)` as that will strip the path from the host
const url = new URL(urlJoin(requestOptions.host, path));
for (const [key, value] of Object.entries(params || {}).sort()) {
const val = Array.isArray(value) ? [...value].sort().join(',') : value;
url.searchParams.append(key, val);
}
const hooks = mergeHooks(withRequestHeaders(requestOptions), withRequestTelemetry(requestOptions), withRequestLogging(), this.#options.hooks);
const requestKey = this.#cache.getRequestKey({ method, url, data, headers: reqHeaders });
// Check if there's already a pending request
let promise = this.#cache.get(requestKey);
if (promise) {
return promise;
}
promise = ky(url, {
method,
body: data && JSON.stringify(data),
retry: {
limit: requestOptions.maxRetries,
// all methods, as post get idempotency keys
methods: ['get', 'post', 'put', 'head', 'delete', 'options', 'trace'],
},
timeout: requestOptions.timeout,
hooks,
})
.then((response) => response
.text()
// handle cases where the response is empty
.then((text) => JSON.parse(text))
.catch(() => undefined))
.catch(async (error) => {
const body = tryParse(await error?.response?.text());
throw createError({
code: error.code,
name: error.name,
message: error.message,
type: error['type'],
status: error?.response?.status,
statusText: error?.response?.statusText,
responseBody: body,
...body?.errors?.[0],
stack: error.stack,
});
});
// Cache the promise so we can dedupe parallel requests
this.#cache.set(requestKey, promise);
return promise;
}
}
//# sourceMappingURL=client.js.map