Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add onDownloadProgress option #34

Merged
merged 43 commits into from
May 10, 2019
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
dc8990d
Streams
szmarczak Feb 22, 2019
b0f698c
onProgress & stream
szmarczak Feb 22, 2019
0e6fe67
Update browser.js
szmarczak Feb 22, 2019
4ad995f
Update browser.js
szmarczak Feb 22, 2019
73c82cc
Update index.js
szmarczak Feb 22, 2019
e6340e6
Tests
szmarczak Feb 22, 2019
2094a20
Update index.js
szmarczak Apr 9, 2019
2b025da
Update readme.md
szmarczak Apr 9, 2019
b70363e
Update readme.md
szmarczak Apr 9, 2019
469ebce
Update index.js
szmarczak Apr 9, 2019
ed371d6
Update browser.js
szmarczak Apr 9, 2019
7a0008d
Update browser.js
szmarczak Apr 9, 2019
14bc3e8
Update index.js
szmarczak Apr 9, 2019
69d36af
Fix cov
szmarczak Apr 9, 2019
eed391d
Mention `ky-universal` in the readme
sindresorhus Feb 22, 2019
9cd0f5b
Fix note about non-2xx status codes in readme (#104)
callumlocke Feb 26, 2019
4811366
Remove slash in prefixUrl example (#108)
tusbar Mar 11, 2019
6e4c645
Clarify usage of `.json()` (#111)
mesqueeb Mar 25, 2019
643b364
Fix Error types (#113)
stramel Apr 3, 2019
e91bb49
Fix TypeScript types to allow hooks to return a Promise (#123)
etienne-dldc Apr 9, 2019
037e3e6
Fix problem with debugging timed-out requests (#122)
arty-name Apr 9, 2019
64581b4
Improve the TypeScript definition
sindresorhus Apr 9, 2019
b48de7d
0.9.1
sindresorhus Apr 9, 2019
0f7e8a6
Don't export the `Ky` TypeScript namespace
sindresorhus Apr 9, 2019
2bae27f
Fix regression for environments without `AbortController` (#125)
gmaclennan Apr 19, 2019
7af3243
Set accept header for ky shortcut methods (#118)
selrond Apr 19, 2019
b686e3d
Make it possible to install Ky in Node.js 8
sindresorhus Apr 19, 2019
ee6fa03
0.10.0
sindresorhus Apr 19, 2019
6829d06
Fix Travis
sindresorhus Apr 19, 2019
accf022
Fixes
szmarczak Apr 21, 2019
824a76e
Merge branch 'master' into stream
szmarczak Apr 22, 2019
c5fcecc
Update index.js
szmarczak Apr 22, 2019
c1d40cc
Workaround for undefined
szmarczak Apr 22, 2019
fc36dcf
Update browser.js
szmarczak Apr 22, 2019
16ab480
tests
szmarczak Apr 22, 2019
0f3f37e
Merge branch 'master' into stream
szmarczak Apr 23, 2019
48d7443
Update index.d.ts
sindresorhus Apr 24, 2019
acfdc38
Merge branch 'master' into stream
szmarczak May 10, 2019
8923275
final touches
szmarczak May 10, 2019
48060e6
The final final (?) touches
szmarczak May 10, 2019
910e3cf
Update index.d.ts
sindresorhus May 10, 2019
c156a06
Update readme.md
sindresorhus May 10, 2019
badf3a1
Update index.d.ts
sindresorhus May 10, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ export interface Options extends RequestInit {
*/
timeout?: number;

/**
Download progress event handler.
The function takes `percent`, `transferredBytes`, `totalBytes` and `chunk` arguments.
If it's not possible to retrieve the body size, total will be `0`.
*/
onDownloadProgress?: (percent: number, transferredBytes: number, totalBytes: number, chunk?: Uint8Array) => void;

/**
Hooks allow modifications during the request lifecycle. Hook functions may be async and are run serially.
*/
Expand Down
55 changes: 54 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ const getGlobal = property => {
const document = getGlobal('document');
const Headers = getGlobal('Headers');
const Response = getGlobal('Response');
const ReadableStream = getGlobal('ReadableStream');
const fetch = getGlobal('fetch');
const AbortController = getGlobal('AbortController');

const isObject = value => value !== null && typeof value === 'object';
const supportsAbortController = typeof getGlobal('AbortController') === 'function';
const supportsAbortController = typeof AbortController === 'function';
const supportsStreams = typeof ReadableStream === 'function';

const deepMerge = (...sources) => {
let returnValue = {};
Expand Down Expand Up @@ -222,6 +224,20 @@ class Ky {
throw new HTTPError(response);
}

// If `onDownloadProgress` is passed, use stream API internally
szmarczak marked this conversation as resolved.
Show resolved Hide resolved
/* istanbul ignore next */
if (this._options.onDownloadProgress) {
if (typeof this._options.onDownloadProgress !== 'function') {
throw new TypeError('The `onDownloadProgress` option must be a function');
}

if (!supportsStreams) {
throw new Error('Streams are not supported in your environment. `ReadableStream` is missing.');
}

return this._stream(response.clone(), this._options.onDownloadProgress);
}

return response;
};

Expand Down Expand Up @@ -295,6 +311,43 @@ class Ky {

return timeout(fetch(this._input, this._options), this._timeout, this.abortController);
}

/* istanbul ignore next */
_stream(response, onDownloadProgress) {
const totalBytes = Number(response.headers.get('content-length')) || 0;
let transferredBytes = 0;

return new Response(
new ReadableStream({
szmarczak marked this conversation as resolved.
Show resolved Hide resolved
start(controller) {
const reader = response.body.getReader();

if (onDownloadProgress) {
onDownloadProgress(0, 0, totalBytes);
}

async function read() {
const {done, value} = await reader.read();
if (done) {
controller.close();
return;
}

if (onDownloadProgress) {
transferredBytes += value.byteLength;
const percent = totalBytes === 0 ? 0 : transferredBytes / totalBytes;
onDownloadProgress(percent, transferredBytes, totalBytes, value);
}

controller.enqueue(value);
read();
}

read();
}
})
);
}
}

const createInstance = (defaults = {}) => {
Expand Down
10 changes: 10 additions & 0 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,13 @@ interface Result {
value: number;
}
expectType<Promise<Result>>(ky(url).json<Result>());

// `onDownloadProgress` option
ky(url, {
onDownloadProgress: (percent, transferredBytes, totalBytes, chunk) => {
expectType<number>(percent);
expectType<number>(transferredBytes);
expectType<number>(totalBytes);
expectType<Uint8Array | undefined>(chunk);
}
});
18 changes: 18 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,24 @@ Default: `10000`

Timeout in milliseconds for getting a response.

##### onDownloadProgress

Type: `Function`

Download progress event handler. The function takes `percent`, `transferredBytes`, `totalBytes` and `chunk` arguments. If it's not possible to retrieve the body size, total will be `0`.


```js
szmarczak marked this conversation as resolved.
Show resolved Hide resolved
await ky('https://example.com', {
onProgress: (percent, transferred, total, chunk) => {
// Example output:
// `0% - 0 of 1271 bytes`
// `100% - 1271 of 1271 bytes`
console.log(`${percent * 100}% - ${transferred} of ${total} bytes`);
}
})
```

##### hooks

Type: `Object<string, Function[]>`<br>
Expand Down
64 changes: 64 additions & 0 deletions test/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,67 @@ test('aborting a request', withPage, async (t, page) => {

await server.close();
});

test('onDownloadProgress works', withPage, async (t, page) => {
const server = await createTestServer();

server.get('/', (request, response) => {
response.writeHead(200, {
'content-length': 4
});

response.write('me');
setTimeout(() => {
response.end('ow');
}, 1000);
});

await page.goto(server.url);
await page.addScriptTag({path: './umd.js'});

const result = await page.evaluate(async url => {
// `new TextDecoder('utf-8').decode` hangs up?
const decodeUTF8 = array => String.fromCharCode.apply(null, array);
szmarczak marked this conversation as resolved.
Show resolved Hide resolved

const data = [];
window.ky = window.ky.default;

const text = await window.ky(url, {
onDownloadProgress: (percent, transferredBytes, totalBytes, chunk) => {
const stringifiedChunk = chunk instanceof Uint8Array ? decodeUTF8(chunk) : String(chunk);
szmarczak marked this conversation as resolved.
Show resolved Hide resolved
data.push([percent, transferredBytes, totalBytes, stringifiedChunk]);
}
}).text();

return {data, text};
}, server.url);

t.deepEqual(result.data, [
[0, 0, 4, 'undefined'],
[0.5, 2, 4, 'me'],
[1, 4, 4, 'ow']
]);
t.is(result.text, 'meow');

await server.close();
});

test('throws if onDownloadProgress is not a function', withPage, async (t, page) => {
const server = await createTestServer();

server.get('/', (request, response) => {
response.end();
});

await page.goto(server.url);
await page.addScriptTag({path: './umd.js'});

const error = await page.evaluate(url => {
window.ky = window.ky.default;
const request = window.ky(url, {onDownloadProgress: 1}).text();
return request.catch(error => error.toString());
}, server.url);
t.is(error, 'TypeError: The `onDownloadProgress` option must be a function');

await server.close();
});