kubo-rpc-client
Version:
A client library for the Kubo RPC API
211 lines • 7.84 kB
JavaScript
/* eslint-disable no-undef */
import { logger } from '@libp2p/logger';
import { anySignal } from 'any-signal';
import browserReableStreamToIt from 'browser-readablestream-to-it';
import { URL, URLSearchParams } from 'iso-url';
import all from 'it-all';
import mergeOpts from 'merge-options';
import { isBrowser, isWebWorker } from 'wherearewe';
import { TimeoutError, HTTPError } from './http/error.js';
import { fetch, Request, Headers } from './http/fetch.js';
const merge = mergeOpts.bind({ ignoreUndefined: true });
const log = logger('kubo-rpc-client:fetch');
const defaults = {
throwHttpErrors: true,
credentials: 'same-origin'
};
export class HTTP {
static HTTPError = HTTPError;
static TimeoutError = TimeoutError;
static post = async (resource, options) => new HTTP(options).post(resource, options);
static get = async (resource, options) => new HTTP(options).get(resource, options);
static put = async (resource, options) => new HTTP(options).put(resource, options);
static delete = async (resource, options) => new HTTP(options).delete(resource, options);
static options = async (resource, options) => new HTTP(options).options(resource, options);
opts;
constructor(options = {}) {
this.opts = merge({}, defaults, options);
}
/**
* Fetch
*/
async fetch(resource, options = {}) {
const opts = merge({}, this.opts, options);
const headers = new Headers(opts.headers);
// validate resource type
if (typeof resource !== 'string' && !(resource instanceof URL || resource instanceof Request)) {
throw new TypeError('`resource` must be a string, URL, or Request');
}
// eslint-disable-next-line @typescript-eslint/no-base-to-string
const url = new URL(resource.toString(), opts.base);
const { searchParams, transformSearchParams, json } = opts;
if (searchParams != null) {
if (typeof transformSearchParams === 'function') {
url.search = transformSearchParams(new URLSearchParams(opts.searchParams));
}
else {
url.search = new URLSearchParams(opts.searchParams).toString();
}
}
if (json != null) {
opts.body = JSON.stringify(opts.json);
headers.set('content-type', 'application/json');
}
const signals = [opts.signal];
if (opts.timeout != null && isNaN(opts.timeout) && opts.timeout > 0) {
signals.push(AbortSignal.timeout(opts.timeout));
}
const signal = anySignal(signals);
try {
if (globalThis.ReadableStream != null && opts.body instanceof globalThis.ReadableStream && (isBrowser || isWebWorker)) {
// https://bugzilla.mozilla.org/show_bug.cgi?id=1387483
opts.body = new Blob(await all(browserReableStreamToIt(opts.body)));
}
log.trace('outgoing headers', opts.headers);
log.trace('%s %s', opts.method, url);
// @ts-expect-error extra properties are added later
const response = await fetch(url.toString(), {
...opts,
signal: opts.signal,
timeout: undefined,
headers,
// https://fetch.spec.whatwg.org/#dom-requestinit-duplex
// https://github.com/whatwg/fetch/issues/1254
duplex: 'half'
});
log('%s %s %d', opts.method, url, response.status);
log.trace('incoming headers', response.headers);
if (!response.ok && opts.throwHttpErrors === true) {
if (opts.handleError != null) {
await opts.handleError(response);
}
throw new HTTPError(response);
}
response.iterator = async function* () {
yield* fromStream(response.body);
};
response.ndjson = async function* () {
for await (const chunk of ndjson(response.iterator())) {
if (options.transform != null) {
yield options.transform(chunk);
}
else {
yield chunk;
}
}
};
return response;
}
finally {
signal.clear();
}
}
async post(resource, options = {}) {
return this.fetch(resource, { ...options, method: 'POST' });
}
async get(resource, options = {}) {
return this.fetch(resource, { ...options, method: 'GET' });
}
async put(resource, options = {}) {
return this.fetch(resource, { ...options, method: 'PUT' });
}
async delete(resource, options = {}) {
return this.fetch(resource, { ...options, method: 'DELETE' });
}
async options(resource, options = {}) {
return this.fetch(resource, { ...options, method: 'OPTIONS' });
}
}
/**
* Parses NDJSON chunks from an iterator
*/
const ndjson = async function* (source) {
const decoder = new TextDecoder();
let buf = '';
for await (const chunk of source) {
buf += decoder.decode(chunk, { stream: true });
const lines = buf.split(/\r?\n/);
for (let i = 0; i < lines.length - 1; i++) {
const l = lines[i].trim();
if (l.length > 0) {
yield JSON.parse(l);
}
}
buf = lines[lines.length - 1];
}
buf += decoder.decode();
buf = buf.trim();
if (buf.length !== 0) {
yield JSON.parse(buf);
}
};
/**
* Stream to AsyncIterable
*
* @template TChunk
* @param {ReadableStream<TChunk> | NodeReadableStream | null} source
* @returns {AsyncIterable<TChunk>}
*/
const fromStream = (source) => {
if (isAsyncIterable(source)) {
return source;
}
// Workaround for https://github.com/node-fetch/node-fetch/issues/766
if (isNodeReadableStream(source)) {
const iter = source[Symbol.asyncIterator]();
return {
[Symbol.asyncIterator]() {
return {
next: iter.next.bind(iter),
return(value) {
source.destroy();
if (typeof iter.return === 'function') {
return iter.return();
}
return Promise.resolve({ done: true, value });
}
};
}
};
}
if (isWebReadableStream(source)) {
const reader = source.getReader();
return (async function* () {
try {
while (true) {
// Read from the stream
const { done, value } = await reader.read();
// Exit if we're done
if (done)
return;
// Else yield the chunk
if (value != null) {
yield value;
}
}
}
finally {
reader.releaseLock();
}
})();
}
throw new TypeError('Body can\'t be converted to AsyncIterable');
};
/**
* Check if it's an AsyncIterable
*/
const isAsyncIterable = (value) => {
return value !== null && typeof value[Symbol.asyncIterator] === 'function';
};
/**
* Check for web readable stream
*/
const isWebReadableStream = (value) => {
return value != null && typeof value.getReader === 'function';
};
/**
* Check for node readable stream
*/
const isNodeReadableStream = (value) => Object.prototype.hasOwnProperty.call(value, 'readable') &&
Object.prototype.hasOwnProperty.call(value, 'writable');
//# sourceMappingURL=http.js.map