diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index 6ae217a..39c673c 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -93,7 +93,7 @@ jobs: else npm publish fi env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.JUZI_ADMIN_SCOPE_NPM_TOKEN }} - name: Is Not A Publish Branch if: steps.check-branch.outputs.match != 'true' run: echo 'Not A Publish Branch' diff --git a/README.md b/README.md index e04a65c..d13a7ea 100644 --- a/README.md +++ b/README.md @@ -474,6 +474,16 @@ console.log(fileBox.remoteSize) 1. Serializable 1. Can be Transfered from server to server, server to browser. +## Environments + +Environment variables can be used to control some behavior. + +- `FILEBOX_HTTP_REQUEST_TIMEOUT` [default=10000] The timeout period for establishing a communication request with the server. For example, if the network is unavailable or the delay is too high, communication cannot be successfully established. +- `FILEBOX_HTTP_RESPONSE_TIMEOUT` [default=60000] Socket idle timeout when FileBox downloads data from URL. For example, when the network is temporarily interrupted, the request will be considered as failed after waiting for a specified time. +- ~~`FILEBOX_HTTP_TIMEOUT` [default=60000]~~ **Deprecated!** Please use `FILEBOX_HTTP_RESPONSE_TIMEOUT`. +- `NO_SLICE_DOWN` [default=false] Whether to turn off slice downloading. If the network is unstable, an error while downloading the file will cause the file download to fail. If it is not closed, the file will be divided into multiple fragments for downloading, and breakpoint re-downloading is supported. +- `HTTP_CHUNK_SIZE` [default=524288] When downloading a file using slicing, the number of bytes in each slice. + ## SCHEMAS ### Url @@ -515,6 +525,10 @@ console.log(fileBox.remoteSize) ## History +### main v1.7 (Feb 18, 2023) + +1. Environment variables `FILEBOX_HTTP_TIMEOUT` can be set by user. see [Environments](#Environments). ([#80](https://github.com/huan/file-box/issues/80), by @[binsee](https://github.com/binsee)) + ### main v1.5 (Jan 18, 2022) 1. `fileBox.md5` can be set by user. This filed is for the receiver of the filebox to check, and it is not computed from the file. diff --git a/package.json b/package.json index 2d81fd2..3677ea3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "file-box", - "version": "1.5.5", + "name": "@juzi/file-box", + "version": "1.7.8", "description": "Pack a File into Box for easy move/transfer between servers no matter of where it is.(local path, remote url, or cloud storage)", "type": "module", "exports": { @@ -22,7 +22,7 @@ "lint": "npm-run-all lint:es lint:ts", "lint:ts": "tsc --isolatedModules --noEmit", "test": "npm-run-all lint test:unit", - "test:unit": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" tap \"src/**/*.spec.ts\" \"tests/*.spec.ts\"", + "test:unit": "cross-env NODE_OPTIONS=\"--no-warnings --loader=ts-node/esm\" tap --timeout=90 \"src/**/*.spec.ts\" \"tests/*.spec.ts\"", "test:pack": "bash -x scripts/npm-pack-testing.sh", "lint:es": "eslint --ignore-pattern fixtures/ \"src/**/*.ts\" \"tests/**/*.ts\"" }, @@ -52,12 +52,16 @@ "@chatie/tsconfig": "^4.6.2", "@types/isomorphic-fetch": "0.0.35", "@types/mime": "^2.0.3", + "@types/node": "^18.18.7", "@types/qrcode": "^1.4.1", "@types/uuid": "^8.3.3", + "eslint-plugin-n": "^16.2.0", "gts": "^3.1.0", "pkg-jq": "^0.2.11", "read-pkg-up": "^8.0.0", - "reflect-metadata": "^0.1.13" + "https-proxy-agent": "^5.0.1", + "reflect-metadata": "^0.1.13", + "tap": "^16.3.9" }, "dependencies": { "brolog": "^1.14.2", diff --git a/src/config.ts b/src/config.ts index 7e129bc..37ff45b 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,2 +1,19 @@ /// export { VERSION } from './version.js' + +export const HTTP_REQUEST_TIMEOUT = Number(process.env['FILEBOX_HTTP_REQUEST_TIMEOUT']) + || 10 * 1000 + +export const HTTP_RESPONSE_TIMEOUT = Number(process.env['FILEBOX_HTTP_RESPONSE_TIMEOUT'] ?? process.env['FILEBOX_HTTP_TIMEOUT']) + || 60 * 1000 + +export const NO_SLICE_DOWN = process.env['FILEBOX_NO_SLICE_DOWN'] === 'true' + +export const HTTP_CHUNK_SIZE = Number(process.env['FILEBOX_HTTP_CHUNK_SIZE']) + || 1024 * 512 + +export const PROXY_TYPE = process.env['FILEBOX_PROXY_TYPE'] +export const PROXY_HOST = process.env['FILEBOX_PROXY_HOST'] || '' +export const PROXY_PORT = Number(process.env['FILEBOX_PROXY_PORT']) || 0 +export const PROXY_USERNAME = process.env['FILEBOX_PROXY_USERNAME'] || '' +export const PROXY_PASSWORD = process.env['FILEBOX_PROXY_PASSWORD'] || '' diff --git a/src/file-box.ts b/src/file-box.ts index 7d3dfbb..0a1932d 100644 --- a/src/file-box.ts +++ b/src/file-box.ts @@ -503,6 +503,7 @@ class FileBox implements Pipeable, FileBoxInterface { */ private readonly base64? : string private readonly remoteUrl? : string + public url? : string private readonly qrCode? : string private readonly uuid? : string @@ -921,7 +922,10 @@ class FileBox implements Pipeable, FileBoxInterface { await new Promise((resolve, reject) => { writeStream .once('close', resolve) - .once('error', reject) + .once('error', (error) => { + writeStream.close() + reject(error) + }) this.pipe(writeStream) }) @@ -1017,14 +1021,14 @@ class FileBox implements Pipeable, FileBoxInterface { pipe ( destination: T, ): T { - this.toStream().then(stream => { - stream.on('error', e => { - console.info('error:', e) - - destination.emit('error', e) + this.toStream() + .then(stream => { + stream.on('error', e => { + destination.emit('error', e) + }) + return stream.pipe(destination) }) - return stream.pipe(destination) - }).catch(e => destination.emit('error', e)) + .catch(e => destination.emit('error', e)) return destination } diff --git a/src/misc.spec.ts b/src/misc.spec.ts index ed04509..4563d1b 100755 --- a/src/misc.spec.ts +++ b/src/misc.spec.ts @@ -1,7 +1,7 @@ #!/usr/bin/env -S node --no-warnings --loader ts-node/esm // tslint:disable:no-shadowed-variable -import { test } from 'tstest' +import { test } from 'tstest' import { dataUrlToBase64, @@ -9,7 +9,7 @@ import { httpHeadHeader, httpStream, streamToBuffer, -} from './misc.js' +} from './misc.js' test('dataUrl to base64', async t => { const base64 = [ @@ -68,3 +68,13 @@ test('httpStream', async t => { const obj = JSON.parse(buffer.toString()) t.equal(obj.headers[MOL_KEY], MOL_VAL, 'should send the header right') }) + +test('httpStream in chunks', async (t) => { + const URL = 'https://media.w3.org/2010/05/sintel/trailer.mp4' + const res = await httpStream(URL) + let length = 0 + for await (const chunk of res) { + length += chunk.length + } + t.equal(length, 4372373, 'should get data in chunks right') +}) diff --git a/src/misc.ts b/src/misc.ts index 4157f45..da110a6 100644 --- a/src/misc.ts +++ b/src/misc.ts @@ -1,7 +1,39 @@ -import http from 'http' -import https from 'https' -import nodeUrl from 'url' -import type stream from 'stream' +import assert from 'assert' +import { randomUUID } from 'crypto' +import { once } from 'events' +import { createReadStream, createWriteStream } from 'fs' +import { rm } from 'fs/promises' +import http, { RequestOptions } from 'http' +import https from 'https' +import { HttpsProxyAgent } from 'https-proxy-agent' +import { tmpdir } from 'os' +import { join } from 'path' +import type { Readable } from 'stream' +import { URL } from 'url' + +import { + HTTP_CHUNK_SIZE, + HTTP_REQUEST_TIMEOUT, + HTTP_RESPONSE_TIMEOUT, + NO_SLICE_DOWN, + PROXY_HOST, + PROXY_PASSWORD, + PROXY_PORT, + PROXY_TYPE, + PROXY_USERNAME, +} from './config.js' + +const protocolMap: { + [key: string]: { agent: http.Agent; request: typeof http.request } +} = { + 'http:': { agent: http.globalAgent, request: http.request }, + 'https:': { agent: https.globalAgent, request: https.request }, +} + +function getProtocol (protocol: string) { + assert(protocolMap[protocol], new Error('unknown protocol: ' + protocol)) + return protocolMap[protocol]! +} export function dataUrlToBase64 (dataUrl: string): string { const dataList = dataUrl.split(',') @@ -15,7 +47,7 @@ export function dataUrlToBase64 (dataUrl: string): string { * @credit https://stackoverflow.com/a/43632171/1123955 */ export async function httpHeadHeader (url: string): Promise { - + const originUrl = url let REDIRECT_TTL = 7 while (true) { @@ -23,9 +55,15 @@ export async function httpHeadHeader (url: string): Promise${REDIRECT_TTL}) 302 redirection.`) } - const res = await _headHeader(url) + const res = await fetch(url, { + method: 'HEAD', + }) + res.destroy() if (!/^3/.test(String(res.statusCode))) { + if (originUrl !== url) { + res.headers.location = url + } return res.headers } @@ -37,36 +75,9 @@ export async function httpHeadHeader (url: string): Promise { - const parsedUrl = nodeUrl.parse(destUrl) - const options = { - ...parsedUrl, - method : 'HEAD', - // method : 'GET', - } - - let request: typeof http.request - - if (parsedUrl.protocol === 'https:') { - request = https.request - } else if (parsedUrl.protocol === 'http:') { - request = http.request - } else { - throw new Error('unknown protocol: ' + parsedUrl.protocol) - } - - return new Promise((resolve, reject) => { - request(options, resolve) - .on('error', reject) - .end() - }) - } } -export function httpHeaderToFileName ( - headers: http.IncomingHttpHeaders, -): null | string { +export function httpHeaderToFileName (headers: http.IncomingHttpHeaders): null | string { const contentDisposition = headers['content-disposition'] if (!contentDisposition) { @@ -83,60 +94,138 @@ export function httpHeaderToFileName ( return null } -export async function httpStream ( - url : string, - headers : http.OutgoingHttpHeaders = {}, -): Promise { - - /* eslint node/no-deprecated-api: off */ - // FIXME: - const parsedUrl = nodeUrl.parse(url) +export async function httpStream (url: string, headers: http.OutgoingHttpHeaders = {}): Promise { + const headHeaders = await httpHeadHeader(url) + if (headHeaders.location) { + url = headHeaders.location + const { protocol } = new URL(url) + getProtocol(protocol) + } - const protocol = parsedUrl.protocol + const options: http.RequestOptions = { + headers: { ...headers }, + method: 'GET', + } - let options: http.RequestOptions + const fileSize = Number(headHeaders['content-length']) - let get: typeof https.get + if (!NO_SLICE_DOWN && headHeaders['accept-ranges'] === 'bytes' && fileSize > HTTP_CHUNK_SIZE) { + return await downloadFileInChunks(url, options, fileSize, HTTP_CHUNK_SIZE) + } else { + return await fetch(url, options) + } +} - if (!protocol) { - throw new Error('protocol is empty') +async function fetch (url: string, options: http.RequestOptions): Promise { + const { protocol } = new URL(url) + const { request, agent } = getProtocol(protocol) + const opts: http.RequestOptions = { + agent, + ...options, } + setProxy(opts) + const req = request(url, opts).end() + req + .on('error', () => { + req.destroy() + }) + .setTimeout(HTTP_REQUEST_TIMEOUT, () => { + req.emit('error', new Error(`FileBox: Http request timeout (${HTTP_REQUEST_TIMEOUT})!`)) + }) + const responseEvents = await once(req, 'response') + const res = responseEvents[0] as http.IncomingMessage + res + .on('error', () => { + res.destroy() + }) + .setTimeout(HTTP_RESPONSE_TIMEOUT, () => { + res.emit('error', new Error(`FileBox: Http response timeout (${HTTP_RESPONSE_TIMEOUT})!`)) + }) + return res +} - if (protocol.match(/^https:/i)) { - get = https.get - options = parsedUrl - options.agent = https.globalAgent - } else if (protocol.match(/^http:/i)) { - get = http.get - options = parsedUrl - options.agent = http.globalAgent - } else { - throw new Error('protocol unknown: ' + protocol) +async function downloadFileInChunks ( + url: string, + options: http.RequestOptions, + fileSize: number, + chunkSize = HTTP_CHUNK_SIZE, +): Promise { + const tmpFile = join(tmpdir(), `filebox-${randomUUID()}`) + const writeStream = createWriteStream(tmpFile) + const allowStatusCode = [ 200, 206 ] + const requestBaseOptions: http.RequestOptions = { + headers: {}, + ...options, + } + let chunkSeq = 0 + let start = 0 + let end = 0 + let downSize = 0 + let retries = 3 + + while (downSize < fileSize) { + end = Math.min(start + chunkSize, fileSize - 1) + const range = `bytes=${start}-${end}` + const requestOptions = Object.assign({}, requestBaseOptions) + assert(requestOptions.headers, 'Errors that should not happen: Invalid headers') + requestOptions.headers['Range'] = range + + try { + const res = await fetch(url, requestOptions) + assert(allowStatusCode.includes(res.statusCode ?? 0), `Request failed with status code ${res.statusCode}`) + assert(Number(res.headers['content-length']) > 0, 'Server returned 0 bytes of data') + for await (const chunk of res) { + assert(Buffer.isBuffer(chunk)) + downSize += chunk.length + writeStream.write(chunk) + } + res.destroy() + } catch (error) { + const err = error as Error + if (--retries <= 0) { + void rm(tmpFile, { force: true }) + writeStream.close() + throw new Error(`Download file with chunk failed! ${err.message}`, { cause: err }) + } + } + chunkSeq++ + start = downSize } + writeStream.close() - options.headers = { - ...options.headers, - ...headers, + const readStream = createReadStream(tmpFile) + readStream + .once('end', () => readStream.close()) + .once('close', () => { + void rm(tmpFile, { force: true }) + }) + return readStream +} + +export async function streamToBuffer (stream: Readable): Promise { + const chunks: Buffer[] = [] + for await (const chunk of stream) { + chunks.push(chunk) } + return Buffer.concat(chunks) +} - const res = await new Promise((resolve, reject) => { - get(options, resolve) - .on('error', reject) - .end() - }) - return res +function getProxyUrl () { + const proxyType = PROXY_TYPE + const proxyHost = PROXY_HOST + const proxyPort = PROXY_PORT + const proxyUsername = PROXY_USERNAME + const proxyPassword = PROXY_PASSWORD + if (proxyType === 'http') { + return `http://${proxyUsername}:${proxyPassword}@${proxyHost}:${proxyPort}` + } + return '' } -export async function streamToBuffer ( - stream: stream.Readable, -): Promise { - return new Promise((resolve, reject) => { - const bufferList: Buffer[] = [] - stream.once('error', reject) - stream.once('end', () => { - const fullBuffer = Buffer.concat(bufferList) - resolve(fullBuffer) - }) - stream.on('data', buffer => bufferList.push(buffer)) - }) +function setProxy (options: RequestOptions): void { + const url = getProxyUrl() + if (url) { + const agent = new HttpsProxyAgent(url) + options.agent = agent + } } diff --git a/src/mod.ts b/src/mod.ts index 9d50c67..cd362e4 100644 --- a/src/mod.ts +++ b/src/mod.ts @@ -31,3 +31,5 @@ export { UniformResourceNameRegistry, VERSION, } + +export * from './misc.js' diff --git a/src/pure-functions/sized-chunk-transformer.ts b/src/pure-functions/sized-chunk-transformer.ts index 6463328..b221ded 100644 --- a/src/pure-functions/sized-chunk-transformer.ts +++ b/src/pure-functions/sized-chunk-transformer.ts @@ -18,7 +18,7 @@ function sizedChunkTransformer (chunkByte = DEFAULT_CHUNK_BYTE) { let buffer = Buffer.from([]) const transform: stream.TransformOptions['transform'] = function (chunk, _, done) { - buffer = Buffer.concat([buffer, chunk]) + buffer = Buffer.concat([ buffer, chunk ]) while (buffer.length >= chunkByte) { this.push(buffer.slice(0, chunkByte)) diff --git a/src/urn-registry/uniform-resource-name-registry.ts b/src/urn-registry/uniform-resource-name-registry.ts index 5e9af72..b436272 100644 --- a/src/urn-registry/uniform-resource-name-registry.ts +++ b/src/urn-registry/uniform-resource-name-registry.ts @@ -142,7 +142,7 @@ class UniformResourceNameRegistry { protected purgeExpiredUuid () { log.verbose('UniformResourceNameRegistry', 'purgeExpiredUuid()') - const expireTimeList = [...this.uuidExpiringTable.keys()] + const expireTimeList = [ ...this.uuidExpiringTable.keys() ] .sort((a, b) => Number(a) - Number(b)) for (const expireTime of expireTimeList) { @@ -291,7 +291,7 @@ class UniformResourceNameRegistry { */ destroy () { log.verbose('UniformResourceNameRegistry', 'destroy() %s UUIDs left', - [...this.uuidExpiringTable.values()].flat().length, + [ ...this.uuidExpiringTable.values() ].flat().length, ) if (this.purgerTimer) { diff --git a/src/version.ts b/src/version.ts index 2d26f07..c36f2c5 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,4 +1,3 @@ - /** * This file will be overwrite when we publish NPM module * by scripts/generate_version.ts diff --git a/tests/fixtures/smoke-testing.ts b/tests/fixtures/smoke-testing.ts index 54ffe41..ecb0afe 100644 --- a/tests/fixtures/smoke-testing.ts +++ b/tests/fixtures/smoke-testing.ts @@ -3,7 +3,7 @@ import assert from 'assert' import { FileBox, VERSION, -} from 'file-box' +} from '@juzi/file-box' async function main () { const box = FileBox.fromUrl('https://raw.githubusercontent.com/huan/file-box/main/docs/images/file-box-logo.jpg') diff --git a/tests/network-timeout.spec.ts b/tests/network-timeout.spec.ts new file mode 100755 index 0000000..e4db9c7 --- /dev/null +++ b/tests/network-timeout.spec.ts @@ -0,0 +1,145 @@ +#!/usr/bin/env -S node --no-warnings --loader ts-node/esm + +import { createServer } from 'http' +import type { AddressInfo } from 'net' +import { setTimeout } from 'timers/promises' +import { sinon, test } from 'tstest' + +import { HTTP_REQUEST_TIMEOUT, HTTP_RESPONSE_TIMEOUT } from '../src/config.js' +import { FileBox } from '../src/mod.js' + +test('slow network stall HTTP_TIMEOUT', async (t) => { + const sandbox = sinon.createSandbox() + sandbox.useFakeTimers({ + now: Date.now(), + shouldAdvanceTime: true, + shouldClearNativeTimers: true, + toFake: [ 'setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'nextTick' ], + }) + t.jobs = 3 + const port = Math.floor(Math.random() * (65535 - 49152 + 1)) + 49152 + const URL = { + NOT_TIMEOUT: '/not_timeout', + READY: '/ready', + TIMEOUT: '/timeout', + } + + /* eslint @typescript-eslint/no-misused-promises:off */ + const server = createServer(async (req, res) => { + res.write(Buffer.from('This is the first chunk of data.')) + + if (req.url === URL.NOT_TIMEOUT) { + await setTimeout(HTTP_REQUEST_TIMEOUT * 0.5) + res.write(Buffer.from('This is the second chunk of data.')) + } else if (req.url === URL.READY) { + await setTimeout(HTTP_REQUEST_TIMEOUT + 100) + } else if (req.url === URL.TIMEOUT) { + if (req.method === 'GET') { + await setTimeout(HTTP_RESPONSE_TIMEOUT + 100) + } + } + + // console.debug(`${new Date().toLocaleTimeString()} call res.end "${req.url}"`) + res.end(Buffer.from('All data end.')) + }) + + const host = await new Promise((resolve) => { + server.listen(port, '127.0.0.1', () => { + const addr = server.address() as AddressInfo + // console.debug(`Server is listening on port ${JSON.stringify(addr)}`) + resolve(`http://127.0.0.1:${addr.port}`) + }) + }) + + t.teardown(() => { + // console.debug('teardown') + server.close() + sandbox.restore() + }) + + /** eslint @typescript-eslint/no-floating-promises:off */ + t.test('should not timeout', async (t) => { + const url = `${host}${URL.NOT_TIMEOUT}` + const dataSpy = sandbox.spy() + const errorSpy = sandbox.spy() + + // console.debug(`${new Date().toLocaleTimeString()} Start request "${url}" ...`) + const start = Date.now() + const stream = await FileBox.fromUrl(url).toStream() + + stream.once('error', errorSpy).on('data', dataSpy) + + await sandbox.clock.tickAsync(1) + t.ok(dataSpy.calledOnce, `should get chunk 1 (${Date.now() - start} passed)`) + t.ok(errorSpy.notCalled, `should not get error (${Date.now() - start} passed)`) + + // FIXME: tickAsync does not work on socket timeout + await new Promise((resolve) => { + stream.once('error', resolve).on('close', resolve) + // resolve(setTimeout(HTTP_REQUEST_TIMEOUT)) + }) + await sandbox.clock.tickAsync(1) + // await sandbox.clock.tickAsync(HTTP_RESPONSE_TIMEOUT) + + t.comment('recv data count:', dataSpy.callCount) + t.comment('recv error count:', errorSpy.callCount) + t.ok(dataSpy.calledThrice, `should get chunk 3 after TIMEOUT ${HTTP_REQUEST_TIMEOUT} (${Date.now() - start} passed)`) + t.ok(errorSpy.notCalled, `should not get error after TIMEOUT ${HTTP_REQUEST_TIMEOUT} (${Date.now() - start} passed)`) + t.end() + }).catch(t.threw) + + /** eslint @typescript-eslint/no-floating-promises:off */ + t.test('should timeout', async (t) => { + const url = `${host}${URL.TIMEOUT}` + const dataSpy = sandbox.spy() + const errorSpy = sandbox.spy() + + // console.debug(`${new Date().toLocaleTimeString()} Start request "${url}" ...`) + const start = Date.now() + const stream = await FileBox.fromUrl(url).toStream() + + stream.once('error', errorSpy).once('data', dataSpy) + // .on('error', (e) => { + // console.error(`on error for req "${url}":`, e.stack) + // }) + // .on('data', (d: Buffer) => { + // console.error(`on data for req "${url}":`, d.toString()) + // }) + + await sandbox.clock.tickAsync(1) + + // t.comment('recv data count:', dataSpy.callCount) + // t.comment('recv error count:', errorSpy.callCount) + t.ok(dataSpy.calledOnce, `should get chunk 1 (${Date.now() - start} passed)`) + t.ok(errorSpy.notCalled, `should not get error (${Date.now() - start} passed)`) + + // FIXME: tickAsync does not work on socket timeout + await new Promise((resolve) => { + stream.once('error', resolve).on('close', resolve) + // resolve(setTimeout(HTTP_RESPONSE_TIMEOUT)) + }) + await sandbox.clock.tickAsync(1) + // await sandbox.clock.tickAsync(HTTP_RESPONSE_TIMEOUT) + + // t.comment('recv data count:', dataSpy.callCount) + // t.comment('recv error count:', errorSpy.callCount) + t.ok(errorSpy.calledOnce, `should get error after TIMEOUT ${HTTP_RESPONSE_TIMEOUT} (${Date.now() - start} passed)`) + t.end() + }).catch(t.threw) + + /** eslint @typescript-eslint/no-floating-promises:off */ + t.test('ready should timeout', async (t) => { + const url = `${host}${URL.READY}` + const errorSpy = sandbox.spy() + + // console.debug(`${new Date().toLocaleTimeString()} Start request "${url}" ...`) + const start = Date.now() + const fileBox = FileBox.fromUrl(url) + await fileBox.ready().catch(errorSpy) + + await sandbox.clock.tickAsync(1) + // t.comment('recv error count:', errorSpy.callCount) + t.ok(errorSpy.calledOnce, `should get error after TIMEOUT ${HTTP_REQUEST_TIMEOUT} (${Date.now() - start} passed)`) + t.end() + }).catch(t.threw) +}) diff --git a/tsconfig.json b/tsconfig.json index cd74e88..9e7f33d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,9 @@ "extends": "@chatie/tsconfig", "compilerOptions": { "outDir": "dist/esm", + "verbatimModuleSyntax": false, + // See: https://github.com/wechaty/wechaty/issues/2551 + "ignoreDeprecations": "5.0" }, "exclude": [ "node_modules/",