UNPKG

@orpc/standard-server-fetch

Version:

<div align="center"> <image align="center" src="https://orpc.dev/logo.webp" width=280 alt="oRPC logo" /> </div>

301 lines (294 loc) • 9.74 kB
import { AsyncIteratorClass, startSpan, runInSpanContext, AbortError, parseEmptyableJSON, isTypescriptObject, setSpanError, stringifyJSON, runWithSpan, isAsyncIteratorObject, once } from '@orpc/shared'; import { EventDecoderStream, withEventMeta, ErrorEvent, encodeEventMessage, getEventMeta, getFilenameFromContentDisposition, generateContentDisposition } from '@orpc/standard-server'; function toEventIterator(stream, options = {}) { const eventStream = stream?.pipeThrough(new TextDecoderStream()).pipeThrough(new EventDecoderStream()); const reader = eventStream?.getReader(); let span; let isCancelled = false; return new AsyncIteratorClass(async () => { span ??= startSpan("consume_event_iterator_stream"); try { while (true) { if (reader === void 0) { return { done: true, value: void 0 }; } const { done, value } = await runInSpanContext(span, () => reader.read()); if (done) { if (isCancelled) { throw new AbortError("Stream was cancelled"); } return { done: true, value: void 0 }; } switch (value.event) { case "message": { let message = parseEmptyableJSON(value.data); if (isTypescriptObject(message)) { message = withEventMeta(message, value); } span?.addEvent("message"); return { done: false, value: message }; } case "error": { let error = new ErrorEvent({ data: parseEmptyableJSON(value.data) }); error = withEventMeta(error, value); span?.addEvent("error"); throw error; } case "done": { let done2 = parseEmptyableJSON(value.data); if (isTypescriptObject(done2)) { done2 = withEventMeta(done2, value); } span?.addEvent("done"); return { done: true, value: done2 }; } default: { span?.addEvent("maybe_keepalive"); } } } } catch (e) { if (!(e instanceof ErrorEvent)) { setSpanError(span, e, options); } throw e; } }, async (reason) => { try { if (reason !== "next") { isCancelled = true; span?.addEvent("cancelled"); } await runInSpanContext(span, () => reader?.cancel()); } catch (e) { setSpanError(span, e, options); throw e; } finally { span?.end(); } }); } function toEventStream(iterator, options = {}) { const keepAliveEnabled = options.eventIteratorKeepAliveEnabled ?? true; const keepAliveInterval = options.eventIteratorKeepAliveInterval ?? 5e3; const keepAliveComment = options.eventIteratorKeepAliveComment ?? ""; const initialCommentEnabled = options.eventIteratorInitialCommentEnabled ?? true; const initialComment = options.eventIteratorInitialComment ?? ""; let cancelled = false; let timeout; let span; const stream = new ReadableStream({ start(controller) { span = startSpan("stream_event_iterator"); if (initialCommentEnabled) { controller.enqueue(encodeEventMessage({ comments: [initialComment] })); } }, async pull(controller) { try { if (keepAliveEnabled) { timeout = setInterval(() => { controller.enqueue(encodeEventMessage({ comments: [keepAliveComment] })); span?.addEvent("keepalive"); }, keepAliveInterval); } const value = await runInSpanContext(span, () => iterator.next()); clearInterval(timeout); if (cancelled) { return; } const meta = getEventMeta(value.value); if (!value.done || value.value !== void 0 || meta !== void 0) { const event = value.done ? "done" : "message"; controller.enqueue(encodeEventMessage({ ...meta, event, data: stringifyJSON(value.value) })); span?.addEvent(event); } if (value.done) { controller.close(); span?.end(); } } catch (err) { clearInterval(timeout); if (cancelled) { return; } if (err instanceof ErrorEvent) { controller.enqueue(encodeEventMessage({ ...getEventMeta(err), event: "error", data: stringifyJSON(err.data) })); span?.addEvent("error"); controller.close(); } else { setSpanError(span, err); controller.error(err); } span?.end(); } }, async cancel() { try { cancelled = true; clearInterval(timeout); span?.addEvent("cancelled"); await runInSpanContext(span, () => iterator.return?.()); } catch (e) { setSpanError(span, e); throw e; } finally { span?.end(); } } }).pipeThrough(new TextEncoderStream()); return stream; } function toStandardBody(re, options = {}) { return runWithSpan( { name: "parse_standard_body", signal: options.signal }, async () => { const contentDisposition = re.headers.get("content-disposition"); if (typeof contentDisposition === "string") { const fileName = getFilenameFromContentDisposition(contentDisposition) ?? "blob"; const blob2 = await re.blob(); return new File([blob2], fileName, { type: blob2.type }); } const contentType = re.headers.get("content-type"); if (!contentType || contentType.startsWith("application/json")) { const text = await re.text(); return parseEmptyableJSON(text); } if (contentType.startsWith("multipart/form-data")) { return await re.formData(); } if (contentType.startsWith("application/x-www-form-urlencoded")) { const text = await re.text(); return new URLSearchParams(text); } if (contentType.startsWith("text/event-stream")) { return toEventIterator(re.body, options); } if (contentType.startsWith("text/plain")) { return await re.text(); } const blob = await re.blob(); return new File([blob], "blob", { type: blob.type }); } ); } function toFetchBody(body, headers, options = {}) { if (body instanceof ReadableStream) { return body; } const currentContentDisposition = headers.get("content-disposition"); headers.delete("content-type"); headers.delete("content-disposition"); if (body === void 0) { return void 0; } if (body instanceof Blob) { headers.set("content-type", body.type); headers.set("content-length", body.size.toString()); headers.set( "content-disposition", currentContentDisposition ?? generateContentDisposition(body instanceof File ? body.name : "blob") ); return body; } if (body instanceof FormData) { return body; } if (body instanceof URLSearchParams) { return body; } if (isAsyncIteratorObject(body)) { headers.set("content-type", "text/event-stream"); return toEventStream(body, options); } headers.set("content-type", "application/json"); return stringifyJSON(body); } function toStandardHeaders(headers, standardHeaders = {}) { headers.forEach((value, key) => { if (Array.isArray(standardHeaders[key])) { standardHeaders[key].push(value); } else if (standardHeaders[key] !== void 0) { standardHeaders[key] = [standardHeaders[key], value]; } else { standardHeaders[key] = value; } }); return standardHeaders; } function toFetchHeaders(headers, fetchHeaders = new Headers()) { for (const [key, value] of Object.entries(headers)) { if (Array.isArray(value)) { for (const v of value) { fetchHeaders.append(key, v); } } else if (value !== void 0) { fetchHeaders.append(key, value); } } return fetchHeaders; } function toStandardLazyRequest(request) { return { url: new URL(request.url), signal: request.signal, method: request.method, body: once(() => toStandardBody(request, { signal: request.signal })), get headers() { const headers = toStandardHeaders(request.headers); Object.defineProperty(this, "headers", { value: headers, writable: true }); return headers; }, set headers(value) { Object.defineProperty(this, "headers", { value, writable: true }); } }; } function toFetchRequest(request, options = {}) { const headers = toFetchHeaders(request.headers); const body = toFetchBody(request.body, headers, options); return new Request(request.url, { signal: request.signal, method: request.method, headers, body }); } function toFetchResponse(response, options = {}) { const headers = toFetchHeaders(response.headers); const body = toFetchBody(response.body, headers, options); return new Response(body, { headers, status: response.status }); } function toStandardLazyResponse(response, options = {}) { return { body: once(() => toStandardBody(response, options)), status: response.status, get headers() { const headers = toStandardHeaders(response.headers); Object.defineProperty(this, "headers", { value: headers, writable: true }); return headers; }, set headers(value) { Object.defineProperty(this, "headers", { value, writable: true }); } }; } export { toEventIterator, toEventStream, toFetchBody, toFetchHeaders, toFetchRequest, toFetchResponse, toStandardBody, toStandardHeaders, toStandardLazyRequest, toStandardLazyResponse };