@@ -20,6 +20,9 @@ import { ZipCompressionMethod, getConnectedPort } from './common.js';
2020 * @property {Uint8Array } fileData The bytes of the file.
2121 */
2222
23+ /** The number of milliseconds to periodically send any pending files to the Worker. */
24+ const FLUSH_TIMER_MS = 50 ;
25+
2326/**
2427 * Data elements are packed into bytes in order of increasing bit number within the byte,
2528 * i.e., starting with the least-significant bit of the byte.
@@ -46,15 +49,28 @@ export const CompressStatus = {
4649} ;
4750
4851// TODO: Extend EventTarget and introduce subscribe methods (onProgress, onInsert, onFinish, etc).
49- // TODO: I think appendFiles() is still a good idea so that all files do not have to live in memory
50- // at once, but the API is wonky here... re-think it. Maybe something more like a builder?
5152
5253/**
5354 * A thing that zips files.
5455 * NOTE: THIS IS A WORK-IN-PROGRESS! THE API IS NOT FROZEN! USE AT YOUR OWN RISK!
5556 * TODO(2.0): Add semantic onXXX methods for an event-driven API.
5657 */
5758export class Zipper {
59+ /**
60+ * @type {Uint8Array }
61+ * @private
62+ */
63+ byteArray = new Uint8Array ( 0 ) ;
64+
65+ /**
66+ * The overall state of the Zipper.
67+ * @type {CompressStatus }
68+ * @private
69+ */
70+ compressStatus_ = CompressStatus . NOT_STARTED ;
71+ // Naming of this property preserved for compatibility with 1.2.4-.
72+ get compressState ( ) { return this . compressStatus_ ; }
73+
5874 /**
5975 * The client-side port that sends messages to, and receives messages from the
6076 * decompressor implementation.
@@ -70,6 +86,28 @@ export class Zipper {
7086 */
7187 disconnectFn_ ;
7288
89+ /**
90+ * A timer that periodically flushes pending files to the Worker. Set upon start() and stopped
91+ * upon the last file being compressed by the Worker.
92+ * @type {Number }
93+ * @private
94+ */
95+ flushTimer_ = 0 ;
96+
97+ /**
98+ * Whether the last files have been added by the client.
99+ * @type {boolean }
100+ * @private
101+ */
102+ lastFilesReceived_ = false ;
103+
104+ /**
105+ * The pending files to be sent to the Worker.
106+ * @type {FileInfo[] }
107+ * @private
108+ */
109+ pendingFilesToSend_ = [ ] ;
110+
73111 /**
74112 * @param {CompressorOptions } options
75113 */
@@ -93,46 +131,49 @@ export class Zipper {
93131 throw `CompressionStream with deflate-raw not supported by JS runtime: ${ err } ` ;
94132 }
95133 }
96-
97- /**
98- * @type {CompressStatus }
99- * @private
100- */
101- this . compressState = CompressStatus . NOT_STARTED ;
102-
103- /**
104- * @type {Uint8Array }
105- * @private
106- */
107- this . byteArray = new Uint8Array ( 0 ) ;
108134 }
109135
110136 /**
111137 * Must only be called on a Zipper that has been started. See start().
112138 * @param {FileInfo[] } files
113139 * @param {boolean } isLastFile
114140 */
115- appendFiles ( files , isLastFile ) {
116- if ( ! this . port_ ) {
117- throw `Port not initialized. Did you forget to call start() ?` ;
118- }
119- if ( ! [ CompressStatus . READY , CompressStatus . WORKING ] . includes ( this . compressState ) ) {
120- throw `Zipper not in the right state: ${ this . compressState } ` ;
141+ appendFiles ( files , isLastFile = false ) {
142+ if ( this . compressStatus_ === CompressStatus . NOT_STARTED ) {
143+ throw `appendFiles() called, but Zipper not started.` ;
121144 }
145+ if ( this . lastFilesReceived_ ) throw `appendFiles() called, but last file already received.` ;
122146
123- this . port_ . postMessage ( { files, isLastFile } ) ;
147+ this . lastFilesReceived_ = isLastFile ;
148+ this . pendingFilesToSend_ . push ( ...files ) ;
124149 }
125150
126151 /**
127- * Send in a set of files to be compressed. Set isLastFile to true if no more files are to added
128- * at some future state . The Promise will not resolve until isLastFile is set to true either in
129- * this method or in appendFiles().
152+ * Send in a set of files to be compressed. Set isLastFile to true if no more files are to be
153+ * added in the future . The return Promise will not resolve until isLastFile is set to true either
154+ * in this method or in an appendFiles() call .
130155 * @param {FileInfo[] } files
131156 * @param {boolean } isLastFile
132- * @returns {Promise<Uint8Array> } A Promise that will contain the entire zipped archive as an array
133- * of bytes.
157+ * @returns {Promise<Uint8Array> } A Promise that will resolve once the final file has been sent.
158+ * The Promise resolves to an array of bytes of the entire zipped archive .
134159 */
135- async start ( files , isLastFile ) {
160+ async start ( files = [ ] , isLastFile = false ) {
161+ if ( this . compressStatus_ !== CompressStatus . NOT_STARTED ) {
162+ throw `start() called, but Zipper already started.` ;
163+ }
164+
165+ // We optimize for the case where isLastFile=true in a start() call by posting to the Worker
166+ // immediately upon async resolving below. Otherwise, we push these files into the pending set
167+ // and rely on the flush timer to send them into the Worker.
168+ if ( ! isLastFile ) {
169+ this . pendingFilesToSend_ . push ( ...files ) ;
170+ this . flushTimer_ = setInterval ( ( ) => this . flushAnyPendingFiles_ ( ) , FLUSH_TIMER_MS ) ;
171+ }
172+ this . compressStatus_ = CompressStatus . READY ;
173+ this . lastFilesReceived_ = isLastFile ;
174+
175+ // After this point, the function goes async, so appendFiles() may run before anything else in
176+ // this function.
136177 const impl = await getConnectedPort ( './zip.js' ) ;
137178 this . port_ = impl . hostPort ;
138179 this . disconnectFn_ = impl . disconnectFn ;
@@ -148,26 +189,36 @@ export class Zipper {
148189 console . log ( evt . data ) ;
149190 } else {
150191 switch ( evt . data . type ) {
192+ // Message sent back upon the first message the Worker receives, which may or may not
193+ // have sent any files for compression, e.g. start([]).
151194 case 'start' :
152- this . compressState = CompressStatus . WORKING ;
195+ this . compressStatus_ = CompressStatus . WORKING ;
153196 break ;
197+ // Message sent back when the last file has been compressed by the Worker.
154198 case 'finish' :
155- this . compressState = CompressStatus . COMPLETE ;
199+ if ( this . flushTimer_ ) {
200+ clearInterval ( this . flushTimer_ ) ;
201+ this . flushTimer_ = 0 ;
202+ }
203+ this . compressStatus_ = CompressStatus . COMPLETE ;
156204 this . port_ . close ( ) ;
157205 this . disconnectFn_ ( ) ;
158206 this . port_ = null ;
159207 this . disconnectFn_ = null ;
160208 resolve ( this . byteArray ) ;
161209 break ;
210+ // Message sent back when the Worker has written some bytes to the zip file.
162211 case 'compress' :
163212 this . addBytes_ ( evt . data . bytes ) ;
164213 break ;
165214 }
166215 }
167216 } ;
168217
169- this . compressState = CompressStatus . READY ;
170- this . port_ . postMessage ( { files, isLastFile, compressionMethod : this . zipCompressionMethod } ) ;
218+ // See note above about optimizing for the start(files, true) case.
219+ if ( isLastFile ) {
220+ this . port_ . postMessage ( { files, isLastFile, compressionMethod : this . zipCompressionMethod } ) ;
221+ }
171222 } ) ;
172223 }
173224
@@ -182,4 +233,27 @@ export class Zipper {
182233 this . byteArray . set ( oldArray ) ;
183234 this . byteArray . set ( newBytes , oldArray . byteLength ) ;
184235 }
236+
237+ /**
238+ * Called internally by the async machinery to send any pending files to the Worker. This method
239+ * sends at most one message to the Worker.
240+ * @private
241+ */
242+ flushAnyPendingFiles_ ( ) {
243+ if ( this . compressStatus_ === CompressStatus . NOT_STARTED ) {
244+ throw `flushAppendFiles_() called but Zipper not started.` ;
245+ }
246+ // If the port is not initialized or we have no pending files, just return immediately and
247+ // try again on the next flush.
248+ if ( ! this . port_ || this . pendingFilesToSend_ . length === 0 ) return ;
249+
250+ // Send all files to the worker. If we have received the last file, then let the Worker know.
251+ this . port_ . postMessage ( {
252+ files : this . pendingFilesToSend_ ,
253+ isLastFile : this . lastFilesReceived_ ,
254+ compressionMethod : this . zipCompressionMethod ,
255+ } ) ;
256+ // Release the memory from the browser's main thread.
257+ this . pendingFilesToSend_ = [ ] ;
258+ }
185259}
0 commit comments