@eyepop.ai/react-native-eyepop 1.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +33 -0
  3. package/lib/commonjs/index.js +52 -0
  4. package/lib/commonjs/index.js.map +1 -0
  5. package/lib/commonjs/package.json +1 -0
  6. package/lib/commonjs/rn_http_client.js +113 -0
  7. package/lib/commonjs/rn_http_client.js.map +1 -0
  8. package/lib/commonjs/rn_http_header_parser.js +135 -0
  9. package/lib/commonjs/rn_http_header_parser.js.map +1 -0
  10. package/lib/commonjs/rn_http_response_parser.js +199 -0
  11. package/lib/commonjs/rn_http_response_parser.js.map +1 -0
  12. package/lib/commonjs/rn_local_file.js +38 -0
  13. package/lib/commonjs/rn_local_file.js.map +1 -0
  14. package/lib/commonjs/rn_streaming_fetch.js +111 -0
  15. package/lib/commonjs/rn_streaming_fetch.js.map +1 -0
  16. package/lib/module/index.js +48 -0
  17. package/lib/module/index.js.map +1 -0
  18. package/lib/module/package.json +1 -0
  19. package/lib/module/rn_http_client.js +109 -0
  20. package/lib/module/rn_http_client.js.map +1 -0
  21. package/lib/module/rn_http_header_parser.js +131 -0
  22. package/lib/module/rn_http_header_parser.js.map +1 -0
  23. package/lib/module/rn_http_response_parser.js +194 -0
  24. package/lib/module/rn_http_response_parser.js.map +1 -0
  25. package/lib/module/rn_local_file.js +34 -0
  26. package/lib/module/rn_local_file.js.map +1 -0
  27. package/lib/module/rn_streaming_fetch.js +107 -0
  28. package/lib/module/rn_streaming_fetch.js.map +1 -0
  29. package/lib/typescript/commonjs/package.json +1 -0
  30. package/lib/typescript/commonjs/src/index.d.ts +8 -0
  31. package/lib/typescript/commonjs/src/index.d.ts.map +1 -0
  32. package/lib/typescript/commonjs/src/rn_http_client.d.ts +19 -0
  33. package/lib/typescript/commonjs/src/rn_http_client.d.ts.map +1 -0
  34. package/lib/typescript/commonjs/src/rn_http_header_parser.d.ts +2 -0
  35. package/lib/typescript/commonjs/src/rn_http_header_parser.d.ts.map +1 -0
  36. package/lib/typescript/commonjs/src/rn_http_response_parser.d.ts +21 -0
  37. package/lib/typescript/commonjs/src/rn_http_response_parser.d.ts.map +1 -0
  38. package/lib/typescript/commonjs/src/rn_local_file.d.ts +5 -0
  39. package/lib/typescript/commonjs/src/rn_local_file.d.ts.map +1 -0
  40. package/lib/typescript/commonjs/src/rn_streaming_fetch.d.ts +7 -0
  41. package/lib/typescript/commonjs/src/rn_streaming_fetch.d.ts.map +1 -0
  42. package/lib/typescript/module/package.json +1 -0
  43. package/lib/typescript/module/src/index.d.ts +8 -0
  44. package/lib/typescript/module/src/index.d.ts.map +1 -0
  45. package/lib/typescript/module/src/rn_http_client.d.ts +19 -0
  46. package/lib/typescript/module/src/rn_http_client.d.ts.map +1 -0
  47. package/lib/typescript/module/src/rn_http_header_parser.d.ts +2 -0
  48. package/lib/typescript/module/src/rn_http_header_parser.d.ts.map +1 -0
  49. package/lib/typescript/module/src/rn_http_response_parser.d.ts +21 -0
  50. package/lib/typescript/module/src/rn_http_response_parser.d.ts.map +1 -0
  51. package/lib/typescript/module/src/rn_local_file.d.ts +5 -0
  52. package/lib/typescript/module/src/rn_local_file.d.ts.map +1 -0
  53. package/lib/typescript/module/src/rn_streaming_fetch.d.ts +7 -0
  54. package/lib/typescript/module/src/rn_streaming_fetch.d.ts.map +1 -0
  55. package/package.json +202 -0
  56. package/src/index.ts +48 -0
  57. package/src/rn_http_client.ts +134 -0
  58. package/src/rn_http_header_parser.ts +138 -0
  59. package/src/rn_http_response_parser.ts +248 -0
  60. package/src/rn_local_file.ts +43 -0
  61. package/src/rn_streaming_fetch.ts +131 -0
@@ -0,0 +1,134 @@
1
+ import 'react-native-polyfill-globals/auto';
2
+ import { Logger } from 'pino';
3
+ import { reactNativeStreamingFetch } from './rn_streaming_fetch';
4
+ import { HttpClient } from '@eyepop.ai/eyepop';
5
+ import TcpSockets from 'react-native-tcp-socket';
6
+ import { ReadableStream } from 'web-streams-polyfill/ponyfill/es6';
7
+
8
+ /** HTTP client that will work around the RN fetch implementation and truly stream request and response bodies.
9
+ *
10
+ * While '{ textStreaming: true }' helps somewhat for the response body, it does not truly stream the response
11
+ * body, instead it keeps a large buffer and does not stream bytes to the consumer as they come in.
12
+ * Additionally, it does not support streaming request bodies which forces the consumer to load large files
13
+ * into memory w/o any possibility to avoid OOMs.
14
+ *
15
+ * This implementation implements selected HTTP interactions by using TCP sockets directly. The implementation
16
+ * is minimal and far from complete, e.g. no redirect support. For that reason, EyePop wil prefer to use the
17
+ * built-in fetch() except for cases when (a) the request requires a ReadableStream request body, or
18
+ * (b) the response is supposed to truly stream as indicated by the proprietary init parameter
19
+ * '{ eyepop: { responseStreaming: true} }'.
20
+ *
21
+ * */
22
+ export async function createHttpClientRN(logger: Logger): Promise<HttpClient> {
23
+ class ReactNativeHttpClient {
24
+ private readonly logger: Logger;
25
+
26
+ constructor(logger_: Logger) {
27
+ this.logger = logger_;
28
+ }
29
+
30
+ public async fetch(
31
+ input: RequestInfo | URL,
32
+ init?: RequestInit
33
+ ): Promise<Response> {
34
+ init ||= {};
35
+ let streamBody: ReadableStream<Uint8Array> | undefined;
36
+ if (input instanceof Request && input.body instanceof ReadableStream) {
37
+ // Request body streaming not supported by RN fetch
38
+ streamBody = input.body;
39
+ } else if (init?.body instanceof ReadableStream) {
40
+ streamBody = init.body;
41
+ // @ts-ignore
42
+ } else if (init?.eyepop?.responseStreaming) {
43
+ const requestBody = input instanceof Request ? input.body : init?.body;
44
+ if (requestBody !== undefined) {
45
+ // The limited HTTP post requires a ReadableStream request body
46
+ if (typeof requestBody === 'string') {
47
+ streamBody = new ReadableStream({
48
+ start: (controller) => {
49
+ controller.enqueue(Buffer.from(requestBody as string, 'utf8'));
50
+ controller.close();
51
+ },
52
+ });
53
+ } else if (requestBody instanceof Uint8Array) {
54
+ streamBody = new ReadableStream({
55
+ start: (controller) => {
56
+ controller.enqueue(requestBody as Uint8Array);
57
+ controller.close();
58
+ },
59
+ });
60
+ } else {
61
+ this.logger.warn(
62
+ `EyePop responseStreaming requires a request body of type ReadableStream | string | Uint8Array but is ${typeof requestBody}`
63
+ );
64
+ }
65
+ }
66
+ }
67
+ if (streamBody) {
68
+ let url: URL;
69
+ let method: string;
70
+ let headers: Headers;
71
+ if (input instanceof Request) {
72
+ url = new URL(input.url);
73
+ method = input.method;
74
+ headers = input.headers;
75
+ } else {
76
+ if (input instanceof URL) {
77
+ url = input;
78
+ } else {
79
+ url = new URL(input);
80
+ }
81
+ method = init?.method || 'POST';
82
+ headers = new Headers(init?.headers);
83
+ }
84
+ let contentLength;
85
+ if (headers.has('content-length')) {
86
+ contentLength = parseInt(headers.get('content-length') || '', 10);
87
+ } else {
88
+ headers.append('Transfer-Encoding', 'chunked');
89
+ }
90
+ this.logger.debug(
91
+ `reactNativeStreamingFetch() BEFORE ${method} ${url}`
92
+ );
93
+ const response = await reactNativeStreamingFetch(
94
+ url,
95
+ method,
96
+ headers,
97
+ streamBody,
98
+ contentLength,
99
+ TcpSockets,
100
+ this.logger
101
+ );
102
+ this.logger.debug(`AFTER ${method} ${url} -> ${response.status}`);
103
+ return response;
104
+ }
105
+ // @ts-ignore
106
+ init.reactNative = { textStreaming: true };
107
+ const logLine = logLineFromRequest(input, init);
108
+ this.logger.debug(`ReactNative fetch() BEFORE ${logLine}`);
109
+ const response = await fetch(input, init);
110
+ this.logger.debug(`AFTER ${logLine} -> ${response.status}`);
111
+ return response;
112
+ }
113
+
114
+ public async close(): Promise<void> {}
115
+
116
+ public isFullDuplex() {
117
+ return true;
118
+ }
119
+ }
120
+
121
+ return new ReactNativeHttpClient(logger);
122
+ }
123
+
124
+ function logLineFromRequest(
125
+ input: RequestInfo | URL,
126
+ init?: RequestInit
127
+ ): string {
128
+ const url = input instanceof Request ? input.url : input;
129
+ const method =
130
+ input instanceof Request
131
+ ? input.method
132
+ : init?.method || (init?.body !== undefined ? 'POST' : 'GET');
133
+ return `${method} ${url}`;
134
+ }
@@ -0,0 +1,138 @@
1
+ // RFC-2068 Start-Line definitions:
2
+ // Request-Line: Method SP Request-URI SP HTTP-Version CRLF
3
+ // Status-Line: HTTP-Version SP Status-Code SP Reason-Phrase CRLF
4
+ var startLine = /^[A-Z_]+(\/\d\.\d)? /;
5
+ var requestLine = /^([A-Z_]+) (.+) [A-Z]+\/(\d)\.(\d)$/;
6
+ var statusLine = /^[A-Z]+\/(\d)\.(\d) (\d{3}) (.*)$/;
7
+
8
+ export function parseHttpHeader(lines: string[], onlyHeaders: boolean) {
9
+ let line = lines[0] || '';
10
+ let match;
11
+
12
+ if (onlyHeaders && startLine.test(line)) {
13
+ return parseHeaders(lines);
14
+ } else if ((match = line.match(requestLine)) !== null) {
15
+ return {
16
+ method: match[1],
17
+ url: match[2],
18
+ version: {
19
+ major: parseInt(match[3] || '', 10),
20
+ minor: parseInt(match[4] || '', 10),
21
+ },
22
+ headers: parseHeaders(lines),
23
+ };
24
+ } else if ((match = line.match(statusLine)) !== null) {
25
+ return {
26
+ version: {
27
+ major: parseInt(match[1] || '', 10),
28
+ minor: parseInt(match[2] || '', 10),
29
+ },
30
+ statusCode: parseInt(match[3] || '', 10),
31
+ statusMessage: match[4],
32
+ headers: parseHeaders(lines),
33
+ };
34
+ } else {
35
+ return parseHeaders(lines);
36
+ }
37
+ }
38
+
39
+ function parseHeaders(lines: string[]) {
40
+ var headers = {};
41
+ var i = 0;
42
+ var line = lines[i] || '';
43
+ var index, name, value;
44
+
45
+ if (startLine.test(line)) {
46
+ line = line[++i] || '';
47
+ }
48
+
49
+ while (line) {
50
+ // subsequent lines in multi-line headers start with whitespace
51
+ if (line[0] === ' ' || line[0] === '\t') {
52
+ value += ' ' + line.trim();
53
+ i++;
54
+ if (i < lines.length) {
55
+ line = lines[i] || '';
56
+ } else {
57
+ line = '';
58
+ }
59
+ continue;
60
+ }
61
+
62
+ if (name && value) {
63
+ addHeaderLine(name, value, headers);
64
+ }
65
+
66
+ index = line.indexOf(':');
67
+ name = line.substr(0, index);
68
+ value = line.substr(index + 1).trim();
69
+ i++;
70
+ if (i < lines.length) {
71
+ line = lines[i] || '';
72
+ } else {
73
+ line = '';
74
+ }
75
+ }
76
+
77
+ if (name && value) {
78
+ addHeaderLine(name, value, headers);
79
+ }
80
+
81
+ return headers;
82
+ }
83
+
84
+ // The following function is lifted from:
85
+ // https://github.com/nodejs/node/blob/f1294f5bfd7f02bce8029818be9c92de59749137/lib/_http_incoming.js#L116-L170
86
+ //
87
+ // Add the given (field, value) pair to the message
88
+ //
89
+ // Per RFC2616, section 4.2 it is acceptable to join multiple instances of the
90
+ // same header with a ', ' if the header in question supports specification of
91
+ // multiple values this way. If not, we declare the first instance the winner
92
+ // and drop the second. Extended header fields (those beginning with 'x-') are
93
+ // always joined.
94
+ function addHeaderLine(field: string, value: string, dest: any) {
95
+ field = field.toLowerCase();
96
+ switch (field) {
97
+ // Array headers:
98
+ case 'set-cookie':
99
+ if (dest[field] !== undefined) {
100
+ dest[field].push(value);
101
+ } else {
102
+ dest[field] = [value];
103
+ }
104
+ break;
105
+
106
+ // list is taken from:
107
+ // https://mxr.mozilla.org/mozilla/source/netwerk/protocol/http/src/nsHttpHeaderArray.cpp
108
+ case 'content-type':
109
+ case 'content-length':
110
+ case 'user-agent':
111
+ case 'referer':
112
+ case 'host':
113
+ case 'authorization':
114
+ case 'proxy-authorization':
115
+ case 'if-modified-since':
116
+ case 'if-unmodified-since':
117
+ case 'from':
118
+ case 'location':
119
+ case 'max-forwards':
120
+ case 'retry-after':
121
+ case 'etag':
122
+ case 'last-modified':
123
+ case 'server':
124
+ case 'age':
125
+ case 'expires':
126
+ // drop duplicates
127
+ if (dest[field] === undefined) dest[field] = value;
128
+ break;
129
+
130
+ default:
131
+ // make comma-separated list
132
+ if (typeof dest[field] === 'string') {
133
+ dest[field] += ', ' + value;
134
+ } else {
135
+ dest[field] = value;
136
+ }
137
+ }
138
+ }
@@ -0,0 +1,248 @@
1
+ import 'react-native-polyfill-globals/auto';
2
+
3
+ import { EyepopLineDecoder } from '@eyepop.ai/eyepop';
4
+ import { parseHttpHeader } from './rn_http_header_parser';
5
+ import { Logger } from 'pino';
6
+ import {
7
+ ReadableStream,
8
+ ReadableStreamDefaultController,
9
+ } from 'web-streams-polyfill/ponyfill/es6';
10
+
11
+ enum ResponseState {
12
+ RECEIVING_HEADER,
13
+ RECEIVING_BODY,
14
+ RECEIVING_BODY_CHUNKED,
15
+ RECEIVING_COMPLETED,
16
+ }
17
+
18
+ export class ResponseParser {
19
+ private error: any;
20
+ private readonly logger: Logger;
21
+ private readonly resolve: (value: Response | PromiseLike<Response>) => void;
22
+ private readonly reject: (reason?: any) => void;
23
+ private responseChunkRemainder = 0;
24
+ private responseReadBuffer: Buffer | undefined;
25
+ private responseState: ResponseState;
26
+
27
+ private pullController: any;
28
+ private pullResolve: (() => void) | undefined;
29
+ private pullReject: ((reason?: any) => void) | undefined;
30
+
31
+ constructor(
32
+ logger: Logger,
33
+ resolve: (value: Response | PromiseLike<Response>) => void,
34
+ reject: (reason?: any) => void
35
+ ) {
36
+ this.logger = logger;
37
+ this.resolve = resolve;
38
+ this.reject = reject;
39
+ this.error = null;
40
+ this.responseReadBuffer = undefined;
41
+ this.responseState = ResponseState.RECEIVING_HEADER;
42
+ this.pullController = undefined;
43
+ this.pullResolve = undefined;
44
+ this.pullReject = undefined;
45
+ }
46
+
47
+ public processResponseReadBuffer(data: Buffer): boolean {
48
+ if (this.error) {
49
+ this.logger.info(
50
+ `RN http streamed response, received ${data.length} bytes in error mode`
51
+ );
52
+ return false;
53
+ }
54
+ if (this.responseReadBuffer) {
55
+ this.responseReadBuffer = Buffer.concat([this.responseReadBuffer, data]);
56
+ } else {
57
+ this.responseReadBuffer = Buffer.from(data.subarray());
58
+ }
59
+
60
+ if (this.responseState === ResponseState.RECEIVING_COMPLETED) {
61
+ if (this.pullController) {
62
+ this.pullController.close();
63
+ if (this.pullResolve) {
64
+ this.pullResolve();
65
+ }
66
+ }
67
+ return false;
68
+ } else if (this.responseState === ResponseState.RECEIVING_HEADER) {
69
+ const headerEnd = this.responseReadBuffer.indexOf('\r\n\r\n');
70
+ if (headerEnd >= 0) {
71
+ let headerChunk = this.responseReadBuffer.subarray(0, headerEnd + 4);
72
+ this.responseReadBuffer = Buffer.from(
73
+ this.responseReadBuffer.subarray(headerEnd + 4)
74
+ );
75
+ const maxHeaderLines = 128;
76
+ let responseHeaderLines: string[] = [];
77
+ const lineDecoder = new EyepopLineDecoder();
78
+ for (const line of lineDecoder.decode(headerChunk)) {
79
+ if (line.length > 0) {
80
+ responseHeaderLines.push(line);
81
+ if (responseHeaderLines.length > maxHeaderLines) {
82
+ this.error = `Response header exceeds ${maxHeaderLines} lines`;
83
+ this.reject(this.error);
84
+ return false;
85
+ }
86
+ }
87
+ }
88
+ let responseHeader: any = parseHttpHeader(responseHeaderLines, false);
89
+ if (responseHeader.statusCode === undefined) {
90
+ this.logger.warn(
91
+ `Could not find status header in HTTP response ${JSON.stringify(responseHeader)}`
92
+ );
93
+ this.reject('missing status header in HTTP response');
94
+ return false;
95
+ }
96
+
97
+ const headers = new Headers(responseHeader.headers);
98
+ if (headers.has('transfer-encoding')) {
99
+ if (headers.get('transfer-encoding')?.toLowerCase() === 'chunked') {
100
+ this.responseState = ResponseState.RECEIVING_BODY_CHUNKED;
101
+ this.responseChunkRemainder = 0;
102
+ } else {
103
+ this.error = `Response with unsupported transfer-encoding '${headers.get('transfer-encoding')}'`;
104
+ this.reject(this.error);
105
+ return false;
106
+ }
107
+ } else if (headers.has('content-length')) {
108
+ this.responseState = ResponseState.RECEIVING_BODY;
109
+ this.responseChunkRemainder = parseInt(
110
+ headers.get('content-length') || '-1',
111
+ 10
112
+ );
113
+ } else {
114
+ this.responseState = ResponseState.RECEIVING_BODY_CHUNKED;
115
+ this.responseChunkRemainder = 0;
116
+ }
117
+ const responseBody = new ReadableStream<Uint8Array>({
118
+ pull: (controller) => {
119
+ if (
120
+ this.error ||
121
+ this.responseState === ResponseState.RECEIVING_COMPLETED
122
+ ) {
123
+ // done with the whole request
124
+ return;
125
+ }
126
+ if (this.pull(controller)) {
127
+ // one chunk enqueued, fulfilled the pull() protocol
128
+ return;
129
+ }
130
+ // nothing to enqueue yet, need to defer top a promise
131
+ return new Promise((resolve, reject) => {
132
+ if (this.pullController) {
133
+ if (this.pullReject) {
134
+ this.pullReject('pull, we got a pending controller already');
135
+ }
136
+ }
137
+ this.pullController = controller;
138
+ this.pullResolve = resolve;
139
+ this.pullReject = reject;
140
+ });
141
+ },
142
+ cancel: (reason) => {
143
+ if (!this.error) {
144
+ this.error = `cancelled: ${reason}`;
145
+ this.reject(this.error);
146
+ }
147
+ },
148
+ });
149
+ this.resolve(
150
+ // @ts-ignore
151
+ new Response(responseBody, {
152
+ status: responseHeader.statusCode,
153
+ statusText: responseHeader.statusText,
154
+ headers: headers,
155
+ })
156
+ );
157
+ }
158
+ } else if (this.pullController) {
159
+ if (this.pull(this.pullController)) {
160
+ this.pullController = undefined;
161
+ if (this.pullResolve) {
162
+ this.pullResolve();
163
+ }
164
+ this.pullResolve = undefined;
165
+ this.pullReject = undefined;
166
+ }
167
+ }
168
+ return true;
169
+ }
170
+
171
+ private pull(
172
+ controller: ReadableStreamDefaultController<Uint8Array>
173
+ ): boolean {
174
+ if (this.responseState === ResponseState.RECEIVING_COMPLETED) {
175
+ controller.close();
176
+ return false;
177
+ }
178
+ if (this.responseReadBuffer === undefined) {
179
+ return false;
180
+ }
181
+ if (this.responseState === ResponseState.RECEIVING_BODY) {
182
+ controller.enqueue(this.responseReadBuffer);
183
+ this.responseChunkRemainder -= this.responseReadBuffer.length;
184
+ this.responseReadBuffer = undefined;
185
+ if (this.responseChunkRemainder <= 0) {
186
+ this.responseState = ResponseState.RECEIVING_COMPLETED;
187
+ controller.close();
188
+ }
189
+ return true;
190
+ } else if (this.responseState === ResponseState.RECEIVING_BODY_CHUNKED) {
191
+ while (this.responseReadBuffer.length > 1) {
192
+ if (this.responseChunkRemainder === 0) {
193
+ // need to find the new chunk length
194
+ let chunkHeaderEnd = 0;
195
+ while (this.responseReadBuffer.length >= 2 && chunkHeaderEnd === 0) {
196
+ chunkHeaderEnd = this.responseReadBuffer.indexOf('\r\n');
197
+ if (chunkHeaderEnd === 0) {
198
+ // skip closing \r\n line of a previous chunk
199
+ this.responseReadBuffer = Buffer.from(
200
+ this.responseReadBuffer.subarray(2)
201
+ );
202
+ }
203
+ }
204
+ if (chunkHeaderEnd > 0) {
205
+ this.responseChunkRemainder = parseInt(
206
+ Buffer.from(this.responseReadBuffer, 0, chunkHeaderEnd).toString(
207
+ 'utf8'
208
+ ),
209
+ 16
210
+ );
211
+ this.responseReadBuffer = Buffer.from(
212
+ this.responseReadBuffer.subarray(chunkHeaderEnd + 2)
213
+ );
214
+ if (this.responseChunkRemainder <= 0) {
215
+ this.responseState = ResponseState.RECEIVING_COMPLETED;
216
+ controller.close();
217
+ return true;
218
+ }
219
+ } else if (this.responseReadBuffer.length > 64) {
220
+ this.error =
221
+ 'Cannot find chunked encoding header within 64 bytes, given up';
222
+ controller.error(this.error);
223
+ }
224
+ }
225
+ if (
226
+ this.responseChunkRemainder > 0 &&
227
+ this.responseReadBuffer.length > 0
228
+ ) {
229
+ const chunk = Buffer.from(
230
+ this.responseReadBuffer.subarray(0, this.responseChunkRemainder)
231
+ );
232
+ this.responseReadBuffer = Buffer.from(
233
+ this.responseReadBuffer.subarray(this.responseChunkRemainder)
234
+ );
235
+ controller.enqueue(chunk);
236
+ this.responseChunkRemainder -= chunk.length;
237
+ return true;
238
+ } else {
239
+ break;
240
+ }
241
+ }
242
+ } else {
243
+ this.error = `Received more data in wrong state ${this.responseState}`;
244
+ controller.error(this.error);
245
+ }
246
+ return false;
247
+ }
248
+ }
@@ -0,0 +1,43 @@
1
+ import 'react-native-polyfill-globals/auto';
2
+ import { PathSource, StreamSource } from '@eyepop.ai/eyepop';
3
+ import { Logger } from 'pino';
4
+ import { ReadableStream } from 'web-streams-polyfill/ponyfill/es6';
5
+
6
+ import { FileSystem } from 'react-native-file-access';
7
+
8
+ export async function resolvePathRN(
9
+ source: PathSource,
10
+ _: Logger
11
+ ): Promise<StreamSource> {
12
+ const bufferSize = 1024 * 1024;
13
+ let pos = 0;
14
+ const size = (await FileSystem.stat(source.path)).size;
15
+ let eof = false;
16
+ const stream = new ReadableStream({
17
+ async pull(controller: any) {
18
+ if (eof) {
19
+ return;
20
+ }
21
+ const chunkSize = Math.min(bufferSize, size - pos);
22
+ const chunkAsB64 = await FileSystem.readFileChunk(
23
+ source.path,
24
+ pos,
25
+ chunkSize,
26
+ 'base64'
27
+ );
28
+ if (chunkAsB64.length > 0) {
29
+ const chunk = Buffer.from(chunkAsB64, 'base64').subarray(0, chunkSize);
30
+ controller.enqueue(chunk);
31
+ pos += chunk.length;
32
+ } else {
33
+ eof = true;
34
+ controller.close();
35
+ }
36
+ },
37
+ });
38
+ return {
39
+ stream: stream,
40
+ size: size,
41
+ mimeType: source.mimeType || 'image/*',
42
+ };
43
+ }
@@ -0,0 +1,131 @@
1
+ import 'react-native-polyfill-globals/auto';
2
+
3
+ import { Logger } from 'pino';
4
+ import { ResponseParser } from './rn_http_response_parser';
5
+ import { ReadableStream } from 'web-streams-polyfill/ponyfill/es6';
6
+
7
+ export async function reactNativeStreamingFetch(
8
+ url: URL,
9
+ method: string,
10
+ headers: Headers,
11
+ body: ReadableStream<Uint8Array> | undefined,
12
+ contentLength: number | undefined,
13
+ sockets: any,
14
+ logger: Logger
15
+ ): Promise<Response> {
16
+ const isHttps = url.protocol === 'https:';
17
+ const port = url.port ? parseInt(url.port, 10) : isHttps ? 443 : 80;
18
+ return new Promise((resolve, reject) => {
19
+ const connectOptions = {
20
+ port: port,
21
+ host: url.hostname,
22
+ reuseAddress: true,
23
+ };
24
+ const clientSocket = new sockets.Socket();
25
+ const client = isHttps ? new sockets.TLSSocket(clientSocket) : clientSocket;
26
+
27
+ let error: any;
28
+ const responseParser = new ResponseParser(logger, resolve, (reason) => {
29
+ error = reason;
30
+ reject(error);
31
+ client.destroy();
32
+ });
33
+
34
+ client.on('data', function (data: Buffer) {
35
+ responseParser.processResponseReadBuffer(data);
36
+ });
37
+
38
+ client.on('error', function (err: any) {
39
+ logger.info(`error: ${err}`);
40
+ if (!error) {
41
+ error = err;
42
+ reject(error);
43
+ }
44
+ client.destroy();
45
+ });
46
+
47
+ client.on('close', function () {
48
+ if (!error) {
49
+ error = 'socket closed';
50
+ reject(error);
51
+ }
52
+ });
53
+
54
+ client.connect(connectOptions, async () => {
55
+ const headerLines = [
56
+ `${method} ${url.pathname}${url.search} HTTP/1.1\r\n`,
57
+ `Host: ${url.host}\r\n`,
58
+ ];
59
+ headers.forEach((value: string, key: string) => {
60
+ headerLines.push(`${key}: ${value}\r\n`);
61
+ });
62
+ headerLines.push('\r\n');
63
+
64
+ const textEncoder = new TextEncoder();
65
+ const headerChunk = headerLines.join('');
66
+ const bodyReader = body ? body.getReader() : undefined;
67
+
68
+ let headerEnqueued = false;
69
+ let eof = false;
70
+ const readableHeaderChunkedBody = new ReadableStream<Uint8Array>({
71
+ async pull(controller: any) {
72
+ if (!headerEnqueued) {
73
+ controller.enqueue(headerChunk);
74
+ headerEnqueued = true;
75
+ return;
76
+ }
77
+ if (eof) {
78
+ controller.close();
79
+ return;
80
+ }
81
+ if (!bodyReader) {
82
+ controller.close();
83
+ return;
84
+ }
85
+ const chunk = await bodyReader.read();
86
+ if (chunk.value) {
87
+ let outputChunk;
88
+ if (contentLength) {
89
+ outputChunk = chunk.value;
90
+ } else {
91
+ outputChunk = Buffer.concat([
92
+ textEncoder.encode(`${chunk.value.length.toString(16)}\r\n`),
93
+ chunk.value,
94
+ textEncoder.encode(`\r\n`),
95
+ ]);
96
+ }
97
+ controller.enqueue(outputChunk);
98
+ } else if (chunk.done) {
99
+ if (!contentLength) {
100
+ controller.enqueue(Buffer.from(`0\r\n\r\n`, 'utf8'));
101
+ }
102
+ eof = true;
103
+ }
104
+ },
105
+ });
106
+
107
+ const readableHeaderChunkedBodyReader =
108
+ readableHeaderChunkedBody.getReader();
109
+ const pipeChunk = async () => {
110
+ while (true) {
111
+ try {
112
+ const chunk = await readableHeaderChunkedBodyReader.read();
113
+ if (chunk.value) {
114
+ if (!client.write(chunk.value)) {
115
+ // interrupt this loop and wait for "drain" event
116
+ break;
117
+ }
118
+ } else if (chunk.done) {
119
+ break;
120
+ }
121
+ } catch (e) {
122
+ logger.error(e);
123
+ break;
124
+ }
125
+ }
126
+ };
127
+ client.on('drain', pipeChunk);
128
+ await pipeChunk();
129
+ });
130
+ });
131
+ }