UNPKG

kubo-rpc-client

Version:
211 lines 7.84 kB
/* 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