Skip to content

Commit 48766d0

Browse files
committed
For issue #44, make the Zipper use MessageChannel and not have a hard dependency on Worker
1 parent eeb228a commit 48766d0

File tree

5 files changed

+112
-77
lines changed

5 files changed

+112
-77
lines changed

archive/compress.js

Lines changed: 44 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
// NOTE: THIS IS A VERY HACKY WORK-IN-PROGRESS! THE API IS NOT FROZEN! USE AT YOUR OWN RISK!
1212

1313
/**
14-
* @typedef FileInfo An object that is sent to the worker to represent a file to zip.
14+
* @typedef FileInfo An object that is sent to the implementation to represent a file to zip.
1515
* @property {string} fileName The name of the file. TODO: Includes the path?
1616
* @property {number} lastModTime The number of ms since the Unix epoch (1970-01-01 at midnight).
1717
* @property {ArrayBuffer} fileData The bytes of the file.
@@ -42,7 +42,6 @@ export const ZipCompressionMethod = {
4242

4343
/**
4444
* @typedef CompressorOptions
45-
* @property {string} pathToBitJS A string indicating where the BitJS files are located.
4645
* @property {ZipCompressionMethod} zipCompressionMethod
4746
* @property {DeflateCompressionMethod=} deflateCompressionMethod Only present if
4847
* zipCompressionMethod is set to DEFLATE.
@@ -60,36 +59,52 @@ export const CompressStatus = {
6059
ERROR: 'error',
6160
};
6261

62+
/**
63+
* Connects the MessagePort to the compressor implementation (e.g. zip.js). If Workers exist
64+
* (e.g. web browsers or deno), imports the implementation inside a Web Worker. Otherwise, it
65+
* dynamically imports the implementation inside the current JS context.
66+
* The MessagePort is used for communication between host and implementation.
67+
* @param {string} implFilename The compressor implementation filename relative to this path
68+
* (e.g. './zip.js').
69+
* @param {MessagePort} implPort The MessagePort to connect to the compressor implementation.
70+
* @returns {Promise<void>} The Promise resolves once the ports are connected.
71+
*/
72+
const connectPortFn = async (implFilename, implPort) => {
73+
if (typeof Worker === 'undefined') {
74+
return import(`${implFilename}`).then(implModule => implModule.connect(implPort));
75+
}
76+
return new Promise((resolve, reject) => {
77+
const workerScriptPath = new URL(`./webworker-wrapper.js`, import.meta.url).href;
78+
const worker = new Worker(workerScriptPath, { type: 'module' });
79+
worker.postMessage({ implSrc: implFilename }, [ implPort ]);
80+
resolve();
81+
});
82+
};
83+
6384
/**
6485
* A thing that zips files.
6586
* NOTE: THIS IS A VERY HACKY WORK-IN-PROGRESS! THE API IS NOT FROZEN! USE AT YOUR OWN RISK!
6687
* TODO: Make a streaming / event-driven API.
6788
*/
6889
export class Zipper {
90+
/**
91+
* The client-side port that sends messages to, and receives messages from the
92+
* decompressor implementation.
93+
* @type {MessagePort}
94+
* @private
95+
*/
96+
port_;
97+
6998
/**
7099
* @param {CompressorOptions} options
71100
*/
72101
constructor(options) {
73-
/**
74-
* The path to the BitJS files.
75-
* @type {string}
76-
* @private
77-
*/
78-
this.pathToBitJS = options.pathToBitJS || '/';
79-
80102
/**
81103
* @type {ZipCompressionMethod}
82104
* @private
83105
*/
84106
this.zipCompressionMethod = options.zipCompressionMethod || ZipCompressionMethod.STORE;
85107

86-
/**
87-
* Private web worker initialized during start().
88-
* @type {Worker}
89-
* @private
90-
*/
91-
this.worker_ = null;
92-
93108
/**
94109
* @type {CompressStatus}
95110
* @private
@@ -109,14 +124,14 @@ export class Zipper {
109124
* @param {boolean} isLastFile
110125
*/
111126
appendFiles(files, isLastFile) {
112-
if (!this.worker_) {
113-
throw `Worker not initialized. Did you forget to call start() ?`;
127+
if (!this.port_) {
128+
throw `Port not initialized. Did you forget to call start() ?`;
114129
}
115130
if (![CompressStatus.READY, CompressStatus.WORKING].includes(this.compressState)) {
116131
throw `Zipper not in the right state: ${this.compressState}`;
117132
}
118133

119-
this.worker_.postMessage({ files, isLastFile });
134+
this.port_.postMessage({ files, isLastFile });
120135
}
121136

122137
/**
@@ -128,18 +143,19 @@ export class Zipper {
128143
* @returns {Promise<Uint8Array>} A Promise that will contain the entire zipped archive as an array
129144
* of bytes.
130145
*/
131-
start(files, isLastFile) {
146+
async start(files, isLastFile) {
147+
const messageChannel = new MessageChannel();
148+
this.port_ = messageChannel.port1;
149+
await connectPortFn('./zip.js', messageChannel.port2);
132150
return new Promise((resolve, reject) => {
133-
// TODO: Only use Worker if it exists (like decompress).
134-
// TODO: Remove need for pathToBitJS (like decompress).
135-
this.worker_ = new Worker(this.pathToBitJS + `archive/zip.js`);
136-
this.worker_.onerror = (evt) => {
137-
console.log('Worker error: message = ' + evt.message);
138-
throw evt.message;
151+
this.port_.onerror = (evt) => {
152+
console.log('Impl error: message = ' + evt.message);
153+
reject(evt.message);
139154
};
140-
this.worker_.onmessage = (evt) => {
155+
156+
this.port_.onmessage = (evt) => {
141157
if (typeof evt.data == 'string') {
142-
// Just log any strings the worker pumps our way.
158+
// Just log any strings the implementation pumps our way.
143159
console.log(evt.data);
144160
} else {
145161
switch (evt.data.type) {

archive/decompress.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import { UnarchiveAppendEvent, UnarchiveErrorEvent, UnarchiveEvent, UnarchiveEve
1313
UnarchiveProgressEvent, UnarchiveStartEvent } from './events.js';
1414
import { findMimeType } from '../file/sniffer.js';
1515

16+
// Exported as a convenience (and also because this module used to contain these).
17+
// TODO(bitjs): Remove this export in a future release?
1618
export {
1719
UnarchiveAppendEvent,
1820
UnarchiveErrorEvent,
@@ -57,7 +59,7 @@ const connectPortFn = async (implFilename, implPort) => {
5759
}
5860

5961
return new Promise((resolve, reject) => {
60-
const workerScriptPath = new URL(`./unarchiver-webworker.js`, import.meta.url).href;
62+
const workerScriptPath = new URL(`./webworker-wrapper.js`, import.meta.url).href;
6163
const worker = new Worker(workerScriptPath, { type: 'module' });
6264
worker.postMessage({ implSrc: implFilename }, [implPort]);
6365
resolve();

archive/unarchiver-webworker.js

Lines changed: 0 additions & 22 deletions
This file was deleted.

archive/webworker-wrapper.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* webworker-wrapper.js
3+
*
4+
* Licensed under the MIT License
5+
*
6+
* Copyright(c) 2023 Google Inc.
7+
*/
8+
9+
/**
10+
* A WebWorker wrapper for a decompress/compress implementation. Upon creation and being sent its
11+
* first message, it dynamically imports the decompressor / compressor implementation and connects
12+
* the message port. All other communication takes place over the MessageChannel.
13+
*/
14+
15+
/** @type {MessagePort} */
16+
let implPort;
17+
18+
onmessage = async (evt) => {
19+
const module = await import(evt.data.implSrc);
20+
module.connect(evt.ports[0]);
21+
};

archive/zip.js

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,37 +11,42 @@
1111
* DEFLATE format: http://tools.ietf.org/html/rfc1951
1212
*/
1313

14-
// This file expects to be invoked as a Worker (see onmessage below).
1514
import { ByteBuffer } from '../io/bytebuffer.js';
1615

16+
/** @type {MessagePort} */
17+
let hostPort;
18+
1719
/**
18-
* The client sends messages to this Worker containing files to archive in order. The client
19-
* indicates to the Worker when the last file has been sent to be compressed.
20+
* The client sends a set of CompressFilesMessage to the MessagePort containing files to archive in
21+
* order. The client sets isLastFile to true to indicate to the implementation when the last file
22+
* has been sent to be compressed.
2023
*
21-
* The Worker emits an event to indicate compression has started: { type: 'start' }
22-
* As the files compress, bytes are sent back in order: { type: 'compress', bytes: Uint8Array }
23-
* After the last file compresses, the Worker indicates finish by: { type 'finish' }
24+
* The impl posts an event to the port indicating compression has started: { type: 'start' }.
25+
* As each file compresses, bytes are sent back in order: { type: 'compress', bytes: Uint8Array }.
26+
* After the last file compresses, we indicate finish by: { type 'finish' }
2427
*
25-
* Clients should append the bytes to a single buffer in the order they were received.
28+
* The client should append the bytes to a single buffer in the order they were received.
2629
*/
2730

31+
// TODO(bitjs): De-dupe this typedef and the one in compress.js.
2832
/**
29-
* @typedef FileInfo An object that is sent to this worker by the client to represent a file.
33+
* @typedef FileInfo An object that is sent by the client to represent a file.
3034
* @property {string} fileName The name of this file. TODO: Includes the path?
3135
* @property {number} lastModTime The number of ms since the Unix epoch (1970-01-01 at midnight).
3236
* @property {Uint8Array} fileData The raw bytes of the file.
3337
*/
3438

35-
// TODO: Support DEFLATE.
36-
// TODO: Support options that can let client choose levels of compression/performance.
37-
39+
// TODO(bitjs): Figure out where this typedef should live.
3840
/**
39-
* Ideally these constants should be defined in a common isomorphic ES module. Unfortunately, the
40-
* state of JavaScript is such that modules cannot be shared easily across browsers, worker threads,
41-
* NodeJS environments, etc yet. Thus, these constants, as well as logic that should be extracted to
42-
* common modules and shared with unzip.js are not yet easily possible.
41+
* @typedef CompressFilesMessage A message the client sends to the implementation.
42+
* @property {FileInfo[]} files A set of files to add to the zip file.
43+
* @property {boolean} isLastFile Indicates this is the last set of files to add to the zip file.
4344
*/
4445

46+
// TODO: Support DEFLATE.
47+
// TODO: Support options that can let client choose levels of compression/performance.
48+
49+
// TODO(bitjs): These constants should be defined in a common isomorphic ES module.
4550
const zLocalFileHeaderSignature = 0x04034b50;
4651
const zCentralFileHeaderSignature = 0x02014b50;
4752
const zEndOfCentralDirSignature = 0x06054b50;
@@ -231,39 +236,52 @@ function writeCentralFileDirectory() {
231236
}
232237

233238
/**
234-
* @param {{data: {isLastFile?: boolean, files: FileInfo[]}}} evt The event for the Worker
235-
* to process. It is an error to send any more events to the Worker if a previous event had
236-
* isLastFile is set to true.
239+
* @param {{data: CompressFilesMessage}} evt The event for the implementation to process. It is an
240+
* error to send any more events after a previous event had isLastFile is set to true.
237241
*/
238-
onmessage = function(evt) {
242+
const onmessage = function(evt) {
239243
if (state === CompressorState.FINISHED) {
240-
throw `The zip worker was sent a message after last file received.`;
244+
throw `The zip implementation was sent a message after last file received.`;
241245
}
242246

243247
if (state === CompressorState.NOT_STARTED) {
244-
postMessage({ type: 'start' });
248+
hostPort.postMessage({ type: 'start' });
245249
}
246250

247251
state = CompressorState.COMPRESSING;
248252

249-
/** @type {FileInfo[]} */
250-
const filesToCompress = evt.data.files;
253+
const msg = evt.data;
254+
const filesToCompress = msg.files;
251255
while (filesToCompress.length > 0) {
252256
const fileInfo = filesToCompress.shift();
253257
const fileBuffer = zipOneFile(fileInfo);
254258
filesCompressed.push(fileInfo);
255259
numBytesWritten += fileBuffer.data.byteLength;
256-
this.postMessage({ type: 'compress', bytes: fileBuffer.data }, [ fileBuffer.data.buffer ]);
260+
hostPort.postMessage({ type: 'compress', bytes: fileBuffer.data }, [ fileBuffer.data.buffer ]);
257261
}
258262

259263
if (evt.data.isLastFile) {
260264
const centralBuffer = writeCentralFileDirectory();
261265
numBytesWritten += centralBuffer.data.byteLength;
262-
this.postMessage({ type: 'compress', bytes: centralBuffer.data }, [ centralBuffer.data.buffer ]);
266+
hostPort.postMessage({ type: 'compress', bytes: centralBuffer.data },
267+
[ centralBuffer.data.buffer ]);
263268

264269
state = CompressorState.FINISHED;
265-
this.postMessage({ type: 'finish' });
270+
hostPort.postMessage({ type: 'finish' });
266271
} else {
267272
state = CompressorState.WAITING;
268273
}
269274
};
275+
276+
277+
/**
278+
* Connect the host to the zip implementation with the given MessagePort.
279+
* @param {MessagePort} port
280+
*/
281+
export function connect(port) {
282+
if (hostPort) {
283+
throw `hostPort already connected`;
284+
}
285+
hostPort = port;
286+
port.onmessage = onmessage;
287+
}

0 commit comments

Comments
 (0)