Skip to content

Commit

Permalink
Rewrite code into workers
Browse files Browse the repository at this point in the history
This commit addresses the timeout issue. The current API is synchronous : if
JSZip takes too much time to finish its task, the page crashes
(it freezes during the task anyway). This commit does a the following :

- rewrite the code into workers which can be asynchronous
- add the needed public methods
- add nodejs stream support
- break the compatibility with existing code

Workers
-------

A worker is like a nodejs stream but with some differences. On the good side :

- it works on IE 6-9 without any issue / polyfill
- it weights less than the full dependencies bundled with browserify
- it forwards errors (no need to declare an error handler EVERYWHERE)

On the bad side :
To get sync AND async methods on the public API without duplicating a lot of
code, this class has `isSync` attribute and some if/then to choose between
doing stuff now, or using an async callback. It is dangerously close to
releasing Zalgo (see http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony for more).

A chunk is an object with 2 attributes : `meta` and `data`. The former is an
object containing anything (`percent` for example), see each worker for more
details. The latter is the real data (String, Uint8Array, etc).

Public API
----------

Each method generating data (generate, asText, etc) gain a stream sibling :
generateStream, asTextStream, etc.
This will need a solid discussion because I'm not really satified with this.

Nodejs stream support
---------------------

With this commit, `file(name, data)` accepts a nodejs stream as data. It also
adds a `asNodejsStream()` on the StreamHelper.

Breaking changes
----------------

The undocumented JSZip.compressions object changed : the object now returns
workers to do the job, the previous methods are not used anymore.

Not broken yet, but the the `checkCRC32` (when loading a zip file, it
synchronously check the crc32 of every files) will need to be replaced.
  • Loading branch information
dduponchel committed Jan 8, 2015
1 parent d8ab178 commit 0ceb14c
Show file tree
Hide file tree
Showing 55 changed files with 2,490 additions and 774 deletions.
1 change: 1 addition & 0 deletions .jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"undef": true,
"strict": true,
"sub": true,
"es3": true,

"globals": {
"TextEncoder": false,
Expand Down
9 changes: 9 additions & 0 deletions documentation/_layouts/default.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,20 @@ <h4>JSZip developers :</h4>
<li><a href="{{site.baseurl}}/documentation/api_jszip/filter.html">JSZip#filter(predicate)</a></li>
<li><a href="{{site.baseurl}}/documentation/api_jszip/remove.html">JSZip#remove(name)</a></li>
<li><a href="{{site.baseurl}}/documentation/api_jszip/generate.html">JSZip#generate(options)</a></li>
<li><a href="{{site.baseurl}}/documentation/api_jszip/generate_stream.html">JSZip#generateStream(options)</a></li>
<li><a href="{{site.baseurl}}/documentation/api_jszip/load.html">JSZip#load(data [, options])</a></li>
<li><a href="{{site.baseurl}}/documentation/api_jszip/support.html">JSZip.support</a></li>
</ul>
</li>
<li><a href="{{site.baseurl}}/documentation/api_zipobject.html">ZipObject</a></li>
<li><a href="{{site.baseurl}}/documentation/api_streamhelper.html">StreamHelper</a>
<ul>
<li><a href="{{site.baseurl}}/documentation/api_streamhelper/on.html">StreamHelper#on(event, callback)</a></li>
<li><a href="{{site.baseurl}}/documentation/api_streamhelper/accumulate.html">StreamHelper#accumulate(callback [,updateCallback])</a></li>
<li><a href="{{site.baseurl}}/documentation/api_streamhelper/resume.html">StreamHelper#resume()</a></li>
<li><a href="{{site.baseurl}}/documentation/api_streamhelper/pause.html">StreamHelper#pause()</a></li>
</ul>
</li>
</ul>
{% when "example" %}
<h4>How to ...</h4>
Expand Down
10 changes: 10 additions & 0 deletions documentation/api_jszip/generate.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ options.base64 | boolean | false | **deprecated**, use `type` instead. If
options.compression | string | `STORE` (no compression) | the default file compression method to use. Available methods are `STORE` and `DEFLATE`. You can also provide your own compression method.
options.type | string | `base64` | The type of zip to return, see below for the other types.
options.comment | string | | The comment to use for the zip file.
options.streamFiles | boolean | false | Stream the files and create file descriptors, see below.

Possible values for `type` :

Expand All @@ -33,6 +34,15 @@ encoding of this field and JSZip will use UTF-8. With non ASCII characters you
might get encoding issues if the file archiver doesn't use UTF-8 to decode the
comment.

Note for the `streamFiles` option : in a zip file, the size and the crc32 of
the content are placed before the actual content : to write it we must process
the whole file. When this option is `false` (the default) the processed file is
held in memory. It takes more memory but generates a zip file which should be
read by every program.
When this options is `true`, we stream the file and use data descriptors at the
end of the entry. This option uses less memory but some program might not
support data descriptors (and won't accept the generated zip file).

If not set, JSZip will use the field `comment` on its `options`.

__Returns__ : The generated zip file.
Expand Down
38 changes: 38 additions & 0 deletions documentation/api_jszip/generate_stream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
title: "generateStream(options)"
layout: default
section: api
---

__Description__ : Generates the complete zip file asynchronously.

__Arguments__

name | type | default | description
--------------------|----------|---------|------------
options | object | | the options to generate the zip file, see [the options of `generate()`]({{site.baseurl}}/documentation/api_jszip/generate.html)

__Metadata__ : this stream generates the following metadata :

name | type | description
------------|--------|------------
currentFile | string | the name of the file currently added to the zip file, `null` if none
percent | number | the percent of completion (a double between 0 and 100)

__Returns__ : a [StreamHelper]({{site.baseurl}}/documentation/api_streamhelper.html).

__Throws__ : Nothing.

__Example__

```js
zip.generateStream({type:"blob"}).accumulate(function callback(err, content) {
if (err) {
// handle error
}
// see FileSaver.js
saveAs(content, "hello.zip");
}, function updateCallback(metadata) {
// print progression with metadata.percent and metadata.currentFile
});
```
15 changes: 15 additions & 0 deletions documentation/api_streamhelper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
title: "StreamHelper API"
layout: default
section: api
---

A `StreamHelper` can be viewed as a pausable stream with some helper methods.
It is not a full featured stream like in nodejs (and can't directly used as one)
but the exposed methods should be enough to write the glue code with other async
libraries : `on('data', function)`, `on('end', function)` and `on('error', function)`.

It starts paused, be sure to `resume()` it when ready.

If you are looking for an asynchronous helper without writing glue code, take a
look at `accumulate(function)`.
38 changes: 38 additions & 0 deletions documentation/api_streamhelper/accumulate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
title: "accumulate(callback [,updateCallback])"
layout: default
section: api
---

__Description__ : Read the whole stream and call a callback with the complete content.

__Arguments__

name | type | description
----------------|----------|------------
callback | function | the function called once when the final content is ready.
updateCallback | function | the function called every time the stream updates. This function is optional.


The callback function takes 2 parameters :
- the error if any
- the complete content

The update callback function takes 1 parameter : the metadata (see the [`on` method]({{site.baseurl}}/documentation/api_streamhelper/on.html)).

__Returns__ : Nothing.

__Throws__ : Nothing.

__Example__

```js
zip
.generateStream({type:"uint8array"})
.accumulate(function callback(err, data) {
// err contains the error if something went wrong, null otherwise.
// data contains here the complete zip file as a uint8array (the type asked in generateStream)
}, function updateCallback(metadata) {
// metadata contains for example currentFile and percent, see the generateStream doc.
});
```
48 changes: 48 additions & 0 deletions documentation/api_streamhelper/on.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
title: "on(event, callback)"
layout: default
section: api
---

__Description__ : Register a listener on an event.

__Arguments__

name | type | description
----------|----------|------------
event | string | the name of the event. Only 3 events are supported : `data`, `end` and `error`.
callback | function | the function called when the event occurs. See below for the arguments.


A `data` callback takes 2 parameters :
- the current chunk of data (in a format specified by the method which
generated this StreamHelper)
- the metadata (see each method to know what's inside)

A `end` callback does not take any parameter.

A `error` callback takes an `Error` as parameter.

The callbacks are executed in with the current `StreamHelper` as `this`.

__Returns__ : The current StreamHelper object, for chaining.

__Throws__ : An exception if the event is unkown.

__Example__

```js
zip
.generateStream({type:"uint8array"})
.on('data', function (data, metadata) {
// data is a Uint8Array because that's the type asked in generateStream
// metadata contains for example currentFile and percent, see the generateStream doc.
})
.on('error', function (e) {
// e is the error
})
.on('end', function () {
// no parameter
})
.resume();
```
29 changes: 29 additions & 0 deletions documentation/api_streamhelper/pause.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
title: "pause()"
layout: default
section: api
---

__Description__ : Pause the stream if the stream is running. Once paused, the
stream stops sending `data` events.

__Arguments__ : None.

__Returns__ : The current StreamHelper object, for chaining.

__Throws__ : Nothing.

__Example__

```js
zip
.generateStream({type:"uint8array"})
.on('data', function(chunk) {

// if we push the chunk to an other service which is overloaded, we can
// pause the stream as backpressure.
this.pause();

}).resume(); // start the stream the first time
```

23 changes: 23 additions & 0 deletions documentation/api_streamhelper/resume.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
title: "resume()"
layout: default
section: api
---

__Description__ : Resume the stream if the stream is paused. Once resumed, the
stream starts sending `data` events again.

__Arguments__ : None.

__Returns__ : The current StreamHelper object, for chaining.

__Throws__ : Nothing.

__Example__

```js
zip
.generateStream({type:"uint8array"})
.on('data', function() {...})
.resume();
```
27 changes: 27 additions & 0 deletions documentation/api_zipobject.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,34 @@ attribute name | type | description
method | return type | description
------------------|---------------|-------------
`asText()` | string | the content as an unicode string.
`asTextStream()` | [StreamHelper]({{site.baseurl}}/documentation/api_streamhelper.html) | the content as an unicode string, asynchronous version.
`asBinary()` | string | the content as binary string.
`asBinaryStream()` | [StreamHelper]({{site.baseurl}}/documentation/api_streamhelper.html) | the content as binary string, asynchronous version.
`asArrayBuffer()` | ArrayBuffer | need a [compatible browser]({{site.baseurl}}/documentation/api_jszip/support.html).
`asArrayBufferStream()` | [StreamHelper]({{site.baseurl}}/documentation/api_streamhelper.html) | asynchronous version, need a [compatible browser]({{site.baseurl}}/documentation/api_jszip/support.html).
`asUint8Array()` | Uint8Array | need a [compatible browser]({{site.baseurl}}/documentation/api_jszip/support.html).
`asUint8ArrayStream()` | [StreamHelper]({{site.baseurl}}/documentation/api_streamhelper.html) | asynchronous version, need a [compatible browser]({{site.baseurl}}/documentation/api_jszip/support.html).
`asNodeBuffer()` | nodejs Buffer | need [nodejs]({{site.baseurl}}/documentation/api_jszip/support.html).
`asNodeBufferStream()` | [StreamHelper]({{site.baseurl}}/documentation/api_streamhelper.html) | asynchronous version, need [nodejs]({{site.baseurl}}/documentation/api_jszip/support.html).

__Metadata__ : the `as*Stream()` methods generate the following metadata :

name | type | description
------------|--------|------------
percent | number | the percent of completion (a double between 0 and 100)

__Example__

```js
var txt = zip.file("my_text.txt").asText();

zip.file("my_text.txt").asTextStream().accumulate(function callback(err, content) {
if (err) {
// handle error
}
// the content var contains the text
}, function updateCallback(metadata) {
// print progression with metadata.percent
});
```

25 changes: 25 additions & 0 deletions documentation/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,27 @@ You can access the file content with `.file(name)` and
[its getters]({{site.baseurl}}/documentation/api_zipobject.html) :

```js
/*
* sync version
*/
zip.file("hello.txt").asText(); // "Hello World\n"

if (JSZip.support.uint8array) {
zip.file("hello.txt").asUint8Array(); // Uint8Array { 0=72, 1=101, 2=108, more...}
}

/*
* async version
*/
zip.file("hello.txt").asTextStream().accumulate(function (err, data) {
// data is "Hello World\n"
});

if (JSZip.support.uint8array) {
zip.file("hello.txt").asUint8ArrayStream().accumulate(function (err, data) {
// data is Uint8Array { 0=72, 1=101, 2=108, more...}
});
}
```

You can also remove files or folders with `.remove(name)` :
Expand Down Expand Up @@ -98,6 +114,15 @@ if (JSZip.support.uint8array) {
}
```

This method is synchronous and will freeze the browser until completion. To
avoid that, you can/should use the async version :

```js
zip.generateStream({type : "uint8array"}).accumulate(function (err, data) {
// data contains the zip file
});
```

### Read a zip file

With `.load(data)` you can load a zip file. Check
Expand Down
20 changes: 13 additions & 7 deletions documentation/examples/download-zip-file.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ <h2>The data URL</h2>
var blobLink = document.getElementById('blob');
if (JSZip.support.blob) {
function downloadWithBlob() {
try {
var blob = zip.generate({type:"blob"});
// see FileSaver.js
zip.generateStream({type:"blob"}).accumulate(function (err, blob) {
if (err) {
blobLink.innerHTML += " " + e;
return;
}
saveAs(blob, "hello.zip");
} catch(e) {
blobLink.innerHTML += " " + e;
}
});
return false;
}
bindEvent(blobLink, 'click', downloadWithBlob);
Expand All @@ -50,7 +50,13 @@ <h2>The data URL</h2>

// data URI
function downloadWithDataURI() {
window.location = "data:application/zip;base64," + zip.generate({type:"base64"});
zip.generateStream({type:"base64"}).accumulate(function (err, base64) {
if (err) {
// shouldn't happen with a base64...
return;
}
window.location = "data:application/zip;base64," + zip.generate({type:"base64"});
});
}
var dataUriLink = document.getElementById('data_uri');
bindEvent(dataUriLink, 'click', downloadWithDataURI);
Expand Down
6 changes: 5 additions & 1 deletion documentation/examples/downloader.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,12 @@ <h3>Please select your files</h3>
<button type="submit" class="btn btn-primary">pack them !</button>
</form>

<p class="hide" id="result"></p>
<div class="progress hide" id="progress_bar">
<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%;">
</div>
</div>

<p class="hide" id="result"></p>

</div>

Expand Down
Loading

0 comments on commit 0ceb14c

Please sign in to comment.