@lighthouse-web3/sdk
Version:
NPM package and CLI tool to interact with lighthouse protocol
246 lines (245 loc) • 9.74 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.fetchWithDirectStream = exports.fetchWithTimeout = exports.checkDuplicateFileNames = exports.addressValidator = exports.isPrivateKey = exports.isCID = void 0;
const ethers_1 = require("ethers");
const isCID = (cid) => {
return /^(Qm[1-9A-HJ-NP-Za-km-z]{44}|b[A-Za-z2-7]{58}|B[A-Z2-7]{58}|z[1-9A-HJ-NP-Za-km-z]{48}|F[0-9A-F]{50})*$/.test(cid);
};
exports.isCID = isCID;
const isPrivateKey = (key) => {
return /^([0-9a-f]{64})$/i.test(key);
};
exports.isPrivateKey = isPrivateKey;
const addressValidator = (value) => {
if (ethers_1.ethers.isAddress(value?.toLowerCase())) {
return value.toLowerCase();
}
else if (/^[A-HJ-NP-Za-km-z1-9]*$/.test(value) && value.length == 44) {
return value;
}
return '';
};
exports.addressValidator = addressValidator;
function checkDuplicateFileNames(files) {
const fileNames = new Set();
for (let i = 0; i < files.length; i++) {
const fileName = files[i].name;
if (fileNames.has(fileName)) {
throw new Error(`Duplicate file name found: ${fileName}`);
}
fileNames.add(fileName);
}
}
exports.checkDuplicateFileNames = checkDuplicateFileNames;
async function fetchWithTimeout(endpointURL, options) {
const { timeout = 8000, onProgress, ...rest } = options;
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
try {
if (onProgress && rest.body instanceof FormData) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(rest.method || 'GET', endpointURL);
if (rest.headers) {
if (rest.headers instanceof Headers) {
rest.headers.forEach((value, key) => {
xhr.setRequestHeader(key, value);
});
}
else if (typeof rest.headers === 'object') {
for (const [key, value] of Object.entries(rest.headers)) {
xhr.setRequestHeader(key, value);
}
}
}
xhr.timeout = timeout;
xhr.onload = () => {
const headers = new Headers();
xhr
.getAllResponseHeaders()
.trim()
.split(/[\r\n]+/)
.forEach((line) => {
const parts = line.split(': ');
const header = parts.shift();
const value = parts.join(': ');
if (header)
headers.set(header, value);
});
resolve(new Response(xhr.response, {
status: xhr.status,
statusText: xhr.statusText,
headers: headers,
}));
};
xhr.onerror = () => reject(new Error('Network error'));
xhr.ontimeout = () => reject(new Error('Request timed out'));
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
onProgress(event.loaded / event.total);
}
};
// Handle different body types
if (rest.body instanceof FormData) {
xhr.send(rest.body);
}
else if (rest.body instanceof ReadableStream) {
// Convert ReadableStream to Blob and then send
new Response(rest.body).blob().then((blob) => xhr.send(blob));
}
else if (typeof rest.body === 'string' ||
rest.body instanceof Blob ||
rest.body instanceof ArrayBuffer) {
xhr.send(rest.body);
}
else if (rest.body == null) {
xhr.send();
}
else {
reject(new Error('Unsupported body type'));
}
});
}
else {
const response = await fetch(endpointURL, {
...rest,
signal: controller.signal,
});
clearTimeout(id);
return response;
}
}
catch (error) {
clearTimeout(id);
throw error;
}
}
exports.fetchWithTimeout = fetchWithTimeout;
async function fetchWithDirectStream(endpointURL, options, streamData) {
const { method = 'POST', headers = {}, timeout = 7200000, onProgress, } = options;
const http = eval(`require`)('http');
const https = eval(`require`)('https');
const url = eval(`require`)('url');
const parsedUrl = url.parse(endpointURL);
const isHttps = parsedUrl.protocol === 'https:';
const client = isHttps ? https : http;
return new Promise((resolve, reject) => {
const requestOptions = {
hostname: parsedUrl.hostname,
port: parsedUrl.port || (isHttps ? 443 : 80),
path: parsedUrl.path,
method,
headers: {
...headers,
'Content-Type': `multipart/form-data; boundary=${streamData.boundary}`,
},
};
const req = client.request(requestOptions, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
try {
const responseData = JSON.parse(data);
resolve({ data: responseData });
}
catch (error) {
reject(new Error('Invalid JSON response'));
}
}
else {
reject(new Error(`Request failed with status code ${res.statusCode}`));
}
});
});
req.on('error', (error) => {
reject(new Error(error.message));
});
// Handle timeout
const timeoutId = setTimeout(() => {
req.destroy();
reject(new Error('Request timed out'));
}, timeout);
req.on('close', () => {
clearTimeout(timeoutId);
});
// Track total bytes for progress calculation
let totalBytesUploaded = 0;
let totalBytesToUpload = 0;
// Calculate total size for progress tracking
if (onProgress) {
for (const file of streamData.files) {
totalBytesToUpload += file.size;
}
}
// Stream files sequentially with backpressure handling and proper part delimiters
const writeAsync = (data) => {
return new Promise((resolve) => {
const canWrite = req.write(data);
if (canWrite) {
resolve();
}
else {
req.once('drain', () => resolve());
}
});
};
const pumpStream = (stream) => {
return new Promise((resolve, rejectPump) => {
const onData = (chunk) => {
// Update progress if callback is provided
if (onProgress && totalBytesToUpload > 0) {
totalBytesUploaded += chunk.length;
const progress = Math.min((totalBytesUploaded / totalBytesToUpload) * 100, 100);
onProgress({ progress });
}
const canWrite = req.write(chunk);
if (!canWrite) {
stream.pause();
req.once('drain', () => stream.resume());
}
};
const onEnd = () => {
cleanup();
resolve();
};
const onError = (err) => {
cleanup();
rejectPump(new Error(`File stream error: ${err?.message || err}`));
};
const cleanup = () => {
stream.off('data', onData);
stream.off('end', onEnd);
stream.off('error', onError);
};
stream.on('data', onData);
stream.on('end', onEnd);
stream.on('error', onError);
});
};
(async () => {
try {
for (let idx = 0; idx < streamData.files.length; idx++) {
const file = streamData.files[idx];
const headersPart = `--${streamData.boundary}\r\n` +
`Content-Disposition: form-data; name="file"; filename="${file.filename}"\r\n` +
`Content-Type: application/octet-stream\r\n\r\n`;
await writeAsync(headersPart);
await pumpStream(file.stream);
await writeAsync(`\r\n`);
}
await writeAsync(`--${streamData.boundary}--\r\n`);
req.end();
}
catch (err) {
if (req && !req.destroyed) {
req.destroy();
}
reject(new Error(err?.message || String(err)));
}
})();
});
}
exports.fetchWithDirectStream = fetchWithDirectStream;