import { type Client, type RequestResult, createClient, createConfig } from '../openapi-ts/client/index.js'; // @transform-path ../../../package.json import pkg from '../../package.json' with { type: 'json' }; import { HttpError } from '../errors/HttpError.js'; import { ApiError } from '../errors/ApiError.js'; import * as sdk from '../openapi-ts/sdk.gen.js'; import { CallResult, SuccessCallResult, ResponseTypes, KnownResponseTypes, TypedMeasurementRequest, TypedMeasurementResponse, TypedMeasurementResponses, FinishedMeasurementResponse, NoResponseTypes, Awaitable, } from '../types.js'; import { CreateMeasurementResponses, CreateMeasurementErrors, GetLimitsResponses, GetMeasurementErrors, ListProbesResponses, MeasurementResponse, MeasurementStatus, MeasurementType, type ClientOptions, } from '../openapi-ts/index.js'; import { wait } from '../utils.js'; export type GlobalpingOptions = { auth?: string; userAgent?: string; throwApiErrors?: ThrowOnKnownErrors; timeout?: number; }; export type AwaitMeasurementOptions = { signal?: AbortSignal; }; export class Globalping { private readonly auth: string | undefined; private readonly client: Client; private readonly userAgent: string; private readonly throwApiErrors: ThrowApiErrors; private readonly timeout: number; constructor ({ auth, userAgent, throwApiErrors, timeout }: GlobalpingOptions = {}) { this.userAgent = userAgent ?? `globalping-typescript/${pkg.version} (https://github.com/jsdelivr/globalping-typescript)`; this.throwApiErrors = throwApiErrors ?? false as ThrowApiErrors; this.timeout = timeout ?? 30000; this.auth = auth; this.client = createClient(createConfig({ baseUrl: 'https://api.globalping.io', fetch: (...args: Parameters) => { const request = new Request(...args); const headers = new Headers(request.headers); headers.set('User-Agent', this.userAgent); if (this.auth) { headers.set('Authorization', `Bearer ${this.auth}`); } return fetch(new Request(request, { headers, signal: AbortSignal.any([ request.signal, AbortSignal.timeout(this.timeout) ]), })); }, })); } /** * @see {@link sdk.createMeasurement} for the API docs */ createMeasurement (measurement: TypedMeasurementRequest) { return this.transformResult(sdk.createMeasurement({ body: measurement, client: this.client, })); } /** * @see {@link sdk.getMeasurement} for the API docs */ async getMeasurement (id: string) { const result = await this.transformResult, GetMeasurementErrors>(sdk.getMeasurement({ path: { id }, client: this.client, })); if (!result.ok) { return result; } return result; } /** * @see {@link sdk.getMeasurement} for the API docs */ async awaitMeasurement (id: string, { signal }: AwaitMeasurementOptions = {}) { const getMeasurement = (eTag?: string | null) => { return sdk.getMeasurement({ path: { id }, headers: eTag ? { 'If-None-Match': eTag, } : {}, ...signal ? { signal } : {}, client: this.client, }); }; const start = Date.now(); let internalResult = Globalping.requireRequestCompleted(await getMeasurement()); const maxTime = internalResult.data?.timeout === undefined ? 35000 : (internalResult.data.timeout + 10) * 1000; while (internalResult.data && internalResult.data.status === MeasurementStatus.IN_PROGRESS) { if (Date.now() - start > maxTime) { throw new Error(`Timed out waiting for measurement ${id} to finish.`); } await wait(500, signal); const newInternalResult = Globalping.requireRequestCompleted(await getMeasurement(internalResult.response.headers.get('ETag'))); if (newInternalResult.response.status !== 304) { internalResult = newInternalResult; } } const result = await this.transformResult, GetMeasurementErrors>(internalResult); if (!result.ok) { return result; } const data = result.data; Globalping.assertMeasurementFinished(data); return { ...result, data }; } /** * @see {@link sdk.listProbes} for the API docs */ async listProbes () { return this.transformResult(sdk.listProbes({ client: this.client, })); } /** * @see {@link sdk.getLimits} for the API docs */ async getLimits () { return this.transformResult(sdk.getLimits({ client: this.client, })); } private static requireRequestCompleted (result: T): T & { request: Request; response: Response } { if (!result.request || !result.response) { if ('error' in result) { throw result.error; } throw new Error('Request failed before receiving a response.'); } return result as T & { request: Request; response: Response }; } private async transformResult (requestResult: Awaitable>>) { const internalResult = Globalping.requireRequestCompleted(await requestResult); const { data, error, request, response } = internalResult; if (error != null) { if (response.ok) { if (error instanceof Error) { throw error; } const responseError = new Error('Failed to process response.'); Object.defineProperty(responseError, 'cause', { value: error }); throw responseError; } if (typeof error !== 'object' || !('error' in error)) { throw new HttpError(request, response); } if (this.throwApiErrors) { throw new ApiError(request, response, error); } return { ok: false, data: error, request, response } as ThrowApiErrors extends true ? never : CallResult; } if (data == null) { throw new Error('Unexpected undefined data'); } return { ok: true, data, request, response } as SuccessCallResult; } static assertHttpStatus (status: S, result: CallResult): asserts result is CallResult { if (!Globalping.isHttpStatus(status, result)) { throw new Error(`Expected HTTP status ${status}, got ${result.response.status}`); } } static assertMeasurementFinished (measurement: MeasurementResponse): asserts measurement is FinishedMeasurementResponse { if (!Globalping.isMeasurementFinished(measurement)) { throw new Error(`Expected measurement status ${MeasurementStatus.FINISHED}, got ${measurement.status}`); } } static assertMeasurementType (type: T, measurement: MeasurementResponse): asserts measurement is TypedMeasurementResponse { if (!Globalping.isMeasurementType(type, measurement)) { throw new Error(`Expected measurement type ${type}, got ${measurement.type}`); } } static isHttpStatus (status: S, result: CallResult): result is CallResult { return result.response.status === status; } static isMeasurementFinished (measurement: MeasurementResponse): measurement is FinishedMeasurementResponse { return measurement.status === MeasurementStatus.FINISHED; } static isMeasurementType (type: T, measurement: MeasurementResponse): measurement is TypedMeasurementResponse { return measurement.type === type; } } export default Globalping;