Files
AFFiNE-Mirror/packages/frontend/apps/ios/src/fetch.ts
2024-11-05 11:16:00 +00:00

194 lines
6.2 KiB
TypeScript

/**
* this file is modified from part of https://github.com/ionic-team/capacitor/blob/74c3e9447e1e32e73f818d252eb12f453d849e8d/ios/Capacitor/Capacitor/assets/native-bridge.js#L466
*
* for support arraybuffer response type
*/
import { FetchProvider } from '@affine/core/modules/cloud/provider/fetch';
import { CapacitorHttp } from '@capacitor/core';
import type { Framework } from '@toeverything/infra';
const readFileAsBase64 = (file: File) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => {
const data = reader.result;
if (data === null) {
reject(new Error('Failed to read file'));
} else {
resolve(btoa(data as string));
}
};
reader.onerror = reject;
reader.readAsBinaryString(file);
});
const convertFormData = async (formData: FormData) => {
const newFormData = [];
// @ts-expect-error FormData.entries
for (const pair of formData.entries()) {
const [key, value] = pair;
if (value instanceof File) {
const base64File = await readFileAsBase64(value);
newFormData.push({
key,
value: base64File,
type: 'base64File',
contentType: value.type,
fileName: value.name,
});
} else {
newFormData.push({ key, value, type: 'string' });
}
}
return newFormData;
};
const convertBody = async (body: unknown, contentType: string) => {
if (body instanceof ReadableStream || body instanceof Uint8Array) {
let encodedData;
if (body instanceof ReadableStream) {
const reader = body.getReader();
const chunks = [];
// eslint-disable-next-line no-constant-condition
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const concatenated = new Uint8Array(
chunks.reduce((acc, chunk) => acc + chunk.length, 0)
);
let position = 0;
for (const chunk of chunks) {
concatenated.set(chunk, position);
position += chunk.length;
}
encodedData = concatenated;
} else {
encodedData = body;
}
let data = new TextDecoder().decode(encodedData);
let type;
if (contentType === 'application/json') {
try {
data = JSON.parse(data);
} catch {
// ignore
}
type = 'json';
} else if (contentType === 'multipart/form-data') {
type = 'formData';
} else if (
contentType === null || contentType === void 0
? void 0
: contentType.startsWith('image')
) {
type = 'image';
} else if (contentType === 'application/octet-stream') {
type = 'binary';
} else {
type = 'text';
}
return {
data,
type,
headers: { 'Content-Type': contentType || 'application/octet-stream' },
};
} else if (body instanceof URLSearchParams) {
return {
data: body.toString(),
type: 'text',
};
} else if (body instanceof FormData) {
const formData = await convertFormData(body);
return {
data: formData,
type: 'formData',
};
} else if (body instanceof File) {
const fileData = await readFileAsBase64(body);
return {
data: fileData,
type: 'file',
headers: { 'Content-Type': body.type },
};
}
return { data: body, type: 'json' };
};
function base64ToUint8Array(base64: string) {
const binaryString = atob(base64);
const binaryArray = binaryString.split('').map(function (char) {
return char.charCodeAt(0);
});
return new Uint8Array(binaryArray);
}
export function configureFetchProvider(framework: Framework) {
framework.override(FetchProvider, {
fetch: async (input, init) => {
const request = new Request(input, init);
const { method } = request;
const tag = `CapacitorHttp fetch ${Date.now()} ${input}`;
console.time(tag);
try {
const { body } = request;
// @ts-expect-error Headers.entries
const optionHeaders = Object.fromEntries(request.headers.entries());
const {
data: requestData,
type,
headers,
} = await convertBody(
(init === null || init === void 0 ? void 0 : init.body) ||
body ||
undefined,
optionHeaders['Content-Type'] || optionHeaders['content-type']
);
const accept = optionHeaders['Accept'] || optionHeaders['accept'];
const nativeResponse = await CapacitorHttp.request({
url: request.url,
method: method,
data: requestData,
dataType: type as any,
responseType:
accept === 'application/octet-stream' ? 'arraybuffer' : undefined,
headers: Object.assign(Object.assign({}, headers), optionHeaders),
});
const contentType =
nativeResponse.headers['Content-Type'] ||
nativeResponse.headers['content-type'];
let data =
accept === 'application/octet-stream'
? base64ToUint8Array(nativeResponse.data)
: contentType === null || contentType === void 0
? void 0
: contentType.startsWith('application/json')
? JSON.stringify(nativeResponse.data)
: contentType === 'application/octet-stream'
? base64ToUint8Array(nativeResponse.data)
: nativeResponse.data;
// use null data for 204 No Content HTTP response
if (nativeResponse.status === 204) {
data = null;
}
// intercept & parse response before returning
const response = new Response(new Blob([data], { type: contentType }), {
headers: nativeResponse.headers,
status: nativeResponse.status,
});
/*
* copy url to response, `cordova-plugin-ionic` uses this url from the response
* we need `Object.defineProperty` because url is an inherited getter on the Response
* see: https://stackoverflow.com/a/57382543
* */
Object.defineProperty(response, 'url', {
value: nativeResponse.url,
});
console.timeEnd(tag);
return response;
} catch (error) {
console.timeEnd(tag);
throw error;
}
},
});
}