ts / dist / cjs / http.js
1,295 words
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.uploadBytes = exports.pollUntil = exports.paginateOffset = exports.paginate = exports.requestUrl = exports.del = exports.patch = exports.put = exports.post = exports.get = exports.request = exports.decodeResponse = exports.buildRequest = exports.queryString = exports.encode = void 0;
const errors_js_1 = require("./errors.js");
/** Percent-encode one URL component, using %20 (never +) for spaces. */
const encode = (value) => encodeURIComponent(value).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
exports.encode = encode;
/** Build a query string with its leading `?`, or '' when nothing is set. */
const queryString = (query) => {
    if (!query)
        return '';
    const pairs = Object.entries(query)
        .filter(([, value]) => value !== undefined && value !== null)
        .map(([key, value]) => `${(0, exports.encode)(key)}=${(0, exports.encode)(String(value))}`);
    return pairs.length === 0 ? '' : `?${pairs.join('&')}`;
};
exports.queryString = queryString;
const fillPath = (template, params) => template.replace(/\{([^}]+)\}/g, (_, name) => {
    const value = params?.[name];
    if (value === undefined || value === null) {
        throw (0, errors_js_1.configError)(`Missing path parameter {${name}} for ${template}`, { template, params });
    }
    return (0, exports.encode)(String(value));
});
const baseHeaders = (token, jsonBody) => ({
    Accept: 'application/json',
    ...(token?.trim() ? { Authorization: `Bearer ${token}` } : {}),
    ...(jsonBody ? { 'Content-Type': 'application/json' } : {}),
});
/** Build the request for an API call without sending it. */
const buildRequest = (client, method, path, options = {}) => {
    const hasBody = options.body !== undefined;
    return {
        method: method.toUpperCase(),
        url: `${client.apiUrl}${fillPath(path, options.params)}${(0, exports.queryString)(options.query)}`,
        headers: { ...baseHeaders(client.apiKey, hasBody), ...client.headers },
        ...(hasBody ? { body: JSON.stringify(options.body) } : {}),
    };
};
exports.buildRequest = buildRequest;
const envelope = (parsed) => typeof parsed === 'object' && parsed !== null ? parsed.error : undefined;
/** The ird API envelope is {error:{code,message}}; the node-hosted mailbox uses a bare {error:"code"}. */
const errorMessage = (parsed, text) => {
    const error = envelope(parsed);
    if (typeof error === 'string')
        return error;
    return error?.message ?? text;
};
const errorCode = (parsed) => {
    const error = envelope(parsed);
    if (typeof error === 'string')
        return error;
    return error?.code ?? null;
};
/**
 * Turn a raw status + body into the parsed JSON value, throwing an api error
 * on non-2xx statuses and a transport error on unparseable success bodies.
 * An empty 2xx body decodes to `{}`.
 */
const decodeResponse = (status, body) => {
    const ok = status >= 200 && status <= 299;
    const text = body.trim();
    if (text === '') {
        if (ok)
            return {};
        throw (0, errors_js_1.apiError)('Empty error response', null, status, null);
    }
    let parsed;
    let parsable = true;
    try {
        parsed = JSON.parse(text);
    }
    catch (e) {
        if (ok)
            throw (0, errors_js_1.transportError)('Failed to parse JSON response', text, e);
        parsable = false;
    }
    if (!ok) {
        const response = parsable && typeof parsed === 'object' ? parsed : null;
        throw (0, errors_js_1.apiError)(errorMessage(response, text), errorCode(response), status, response);
    }
    return parsed;
};
exports.decodeResponse = decodeResponse;
const describe = (e) => (e instanceof Error ? e.message : String(e));
const isStream = (body) => typeof ReadableStream !== 'undefined' && body instanceof ReadableStream;
const send = async (fetchFn, req, timeoutMs) => {
    const init = {
        method: req.method,
        headers: req.headers,
        ...(req.body === undefined ? {} : { body: req.body }),
        ...(timeoutMs ? { signal: AbortSignal.timeout(timeoutMs) } : {}),
        ...(isStream(req.body) ? { duplex: 'half' } : {}),
    };
    let response;
    let text;
    try {
        response = await fetchFn(req.url, init);
        text = await response.text();
    }
    catch (e) {
        throw (0, errors_js_1.transportError)(`Request failed: ${describe(e)}`, { url: req.url }, e);
    }
    return (0, exports.decodeResponse)(response.status, text);
};
/**
 * Execute an API request. METHOD and PATH are spec keys, so the options and
 * the result are typed from the OpenAPI document:
 *
 *   await request(client, 'get', '/domains/check', { query: { domain: 'x.ro' } })
 *
 * Throws an api error on non-2xx responses and a transport error on
 * connection/parse failures.
 */
const request = (client, method, path, options) => send(client.fetch, (0, exports.buildRequest)(client, method, path, options), client.timeoutMs);
exports.request = request;
const get = (client, path, options) => (0, exports.request)(client, 'get', path, options);
exports.get = get;
const post = (client, path, options) => (0, exports.request)(client, 'post', path, options);
exports.post = post;
const put = (client, path, options) => (0, exports.request)(client, 'put', path, options);
exports.put = put;
const patch = (client, path, options) => (0, exports.request)(client, 'patch', path, options);
exports.patch = patch;
const del = (client, path, options) => (0, exports.request)(client, 'delete', path, options);
exports.del = del;
/**
 * Execute a request against an absolute URL rather than the API base — for
 * the node-hosted endpoints the API hands out (an IPFS upload grant, a p2p
 * app's mailbox). Throws like `request`.
 */
const requestUrl = (method, url, options = {}) => send(options.fetch ?? globalThis.fetch, {
    method,
    url: `${url}${(0, exports.queryString)(options.query)}`,
    headers: { ...baseHeaders(options.token, false), ...options.headers },
    ...(options.body === undefined ? {} : { body: options.body }),
}, options.timeoutMs);
exports.requestUrl = requestUrl;
/**
 * Follow cursor pagination for a list endpoint, collecting every item under
 * KEY (e.g. 'registrations'). `cursor` is advanced from each response's
 * `next_cursor` until it is absent.
 */
const paginate = async (client, path, key, options = {}) => {
    const items = [];
    let cursor;
    do {
        const query = { ...options.query, ...(cursor ? { cursor } : {}) };
        const page = (await (0, exports.get)(client, path, { params: options.params, query }));
        items.push(...(page[key] ?? []));
        cursor = page.next_cursor ?? undefined;
    } while (cursor);
    return items;
};
exports.paginate = paginate;
/**
 * Follow offset pagination for a list endpoint, collecting every item under
 * KEY (e.g. 'pins'). The page `limit` defaults to 100; `offset` advances by
 * the rows each page actually returns and paging stops on the first short
 * or empty page. The response's `total` is deliberately ignored — it can
 * move while the pages are walked.
 */
const paginateOffset = async (client, path, key, options = {}) => {
    const base = (options.query ?? {});
    const limit = typeof base.limit === 'number' ? base.limit : 100;
    const items = [];
    let offset = typeof base.offset === 'number' ? base.offset : 0;
    for (;;) {
        const query = { ...base, limit, offset };
        const page = (await (0, exports.get)(client, path, { params: options.params, query }));
        const rows = page[key] ?? [];
        items.push(...rows);
        if (rows.length < limit)
            return items;
        offset += rows.length;
    }
};
exports.paginateOffset = paginateOffset;
const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/**
 * Poll STEP until DONE holds for its result, sleeping between attempts with
 * exponential backoff (the interval doubles up to `maxIntervalMs`). Returns
 * the first result that satisfies DONE; throws a timeout error (carrying the
 * last result) when the deadline passes first.
 */
const pollUntil = async (step, done, options = {}) => {
    const { intervalMs = 1000, maxIntervalMs = 8000, timeoutMs = 120000, sleep = defaultSleep } = options;
    const deadline = Date.now() + timeoutMs;
    let interval = intervalMs;
    for (;;) {
        const result = await step();
        if (done(result))
            return result;
        if (Date.now() >= deadline)
            throw (0, errors_js_1.timeoutError)(options.message ?? 'Timed out polling', result);
        await sleep(interval);
        interval = Math.min(maxIntervalMs, interval * 2);
    }
};
exports.pollUntil = pollUntil;
const isUploadSource = (source) => source instanceof Uint8Array ||
    source instanceof ArrayBuffer ||
    (typeof Blob !== 'undefined' && source instanceof Blob) ||
    (typeof ReadableStream !== 'undefined' && source instanceof ReadableStream);
/**
 * POST raw bytes to an IPFS upload-grant URL (from `ipfs.startUpload`). The
 * token is single-use and may be null once consumed. `format: 'car'`
 * appends ?format=car for a client-packed CAR archive.
 */
const uploadBytes = (url, token, source, options = {}) => {
    if (!isUploadSource(source)) {
        throw (0, errors_js_1.transportError)('Unsupported upload source; expected bytes, an ArrayBuffer, a Blob, or a ReadableStream', {
            source: typeof source,
        });
    }
    return (0, exports.requestUrl)('POST', url, {
        token,
        query: options.format === 'car' ? { format: 'car' } : undefined,
        headers: { 'Content-Type': 'application/octet-stream' },
        body: source,
        fetch: options.fetch,
        timeoutMs: options.timeoutMs,
    });
};
exports.uploadBytes = uploadBytes;

static mirror of HEAD · about · clone: git clone https://ird.cod-sursa.ro/ts.git