UNPKG

expo

Version:
147 lines (124 loc) 4.65 kB
import { ExpoFetchModule } from './ExpoFetchModule'; import type { NativeResponse } from './NativeRequest'; const ConcreteNativeResponse = ExpoFetchModule.NativeResponse as typeof NativeResponse; export type AbortSubscriptionCleanupFunction = () => void; // FormData from react-native is not compatible with the web standard. // We need to extend it with the react-native FormData. type RNFormData = Awaited<ReturnType<globalThis.Response['formData']>>; type UniversalFormData = globalThis.FormData & RNFormData; /** * A response implementation for the `fetch.Response` API. */ export class FetchResponse extends ConcreteNativeResponse implements Response { private streamingState: 'none' | 'started' | 'completed' = 'none'; private bodyStream: ReadableStream<Uint8Array> | null = null; constructor(private readonly abortCleanupFunction: AbortSubscriptionCleanupFunction) { super(); this.addListener('readyForJSFinalization', this.finalize); } get body(): ReadableStream<Uint8Array> | null { if (this.bodyStream == null) { const response = this; // This flag prevents enqueuing data after the stream is closed or canceled. // Because it might be too late for the multithreaded native code to stop enqueuing data, // we cannot simply rely on the native code to stop sending `didReceiveResponseData`. let isControllerClosed = false; this.bodyStream = new ReadableStream({ start(controller) { if (response.streamingState === 'completed') { return; } response.addListener('didReceiveResponseData', (data: Uint8Array) => { if (!isControllerClosed) { controller.enqueue(data); } }); response.addListener('didComplete', () => { controller.close(); isControllerClosed = true; }); response.addListener('didFailWithError', (error: string) => { controller.error(new Error(error)); isControllerClosed = true; }); }, async pull(controller) { if (response.streamingState === 'none') { const completedData = await response.startStreaming(); if (completedData != null) { if (!isControllerClosed) { controller.enqueue(completedData); controller.close(); isControllerClosed = true; } response.streamingState = 'completed'; } else { response.streamingState = 'started'; } } else if (response.streamingState === 'completed') { controller.close(); isControllerClosed = true; } }, cancel(reason) { response.cancelStreaming(String(reason)); isControllerClosed = true; }, }); } return this.bodyStream; } get headers(): Headers { return new Headers(this._rawHeaders); } get ok(): boolean { return this.status >= 200 && this.status < 300; } public readonly type = 'default'; /** * This method is not currently supported by react-native's Blob constructor. */ async blob(): Promise<Blob> { const buffer = await this.arrayBuffer(); return new Blob([buffer]); } async formData(): Promise<UniversalFormData> { // Reference implementation: // https://chromium.googlesource.com/chromium/src/+/ed9f0b5933cf5ffb413be1ca844de5be140514bf/third_party/blink/renderer/core/fetch/body.cc#120 const text = await this.text(); const searchParams = new URLSearchParams(text); const formData = new FormData() as UniversalFormData; searchParams.forEach((value, key) => { formData.append(key, value); }); return formData; } async json(): Promise<any> { const text = await this.text(); return JSON.parse(text); } async bytes(): Promise<Uint8Array> { return new Uint8Array(await this.arrayBuffer()); } toString(): string { return `FetchResponse: { status: ${this.status}, statusText: ${this.statusText}, url: ${this.url} }`; } toJSON(): object { return { status: this.status, statusText: this.statusText, redirected: this.redirected, url: this.url, }; } clone(): Response { throw new Error('Not implemented'); } private finalize = (): void => { this.removeListener('readyForJSFinalization', this.finalize); this.abortCleanupFunction(); this.removeAllListeners('didReceiveResponseData'); this.removeAllListeners('didComplete'); this.removeAllListeners('didFailWithError'); }; }