Skip to content

Commit e6d13d9

Browse files
committed
Commonize on port-connected behavior for compressors and decompressors (part of the refactor for issue #44)
1 parent 48766d0 commit e6d13d9

File tree

7 files changed

+114
-89
lines changed

7 files changed

+114
-89
lines changed

archive/common.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* common.js
3+
*
4+
* Provides common functionality for compressing and decompressing.
5+
*
6+
* Licensed under the MIT License
7+
*
8+
* Copyright(c) 2023 Google Inc.
9+
*/
10+
11+
// Requires the following JavaScript features: MessageChannel, MessagePort, and dynamic imports.
12+
13+
/**
14+
* Connects a host to a compress/decompress implementation via MessagePorts. The implementation must
15+
* have an exported connect() function that accepts a MessagePort. If the runtime support Workers
16+
* (e.g. web browsers, deno), imports the implementation inside a Web Worker. Otherwise, it
17+
* dynamically imports the implementation inside the current JS context (node, bun).
18+
* @param {string} implFilename The compressor/decompressor implementation filename relative to this
19+
* path (e.g. './unzip.js').
20+
* @returns {Promise<MessagePort>} The Promise resolves to the MessagePort connected to the
21+
* implementation that the host should use.
22+
*/
23+
export async function getConnectedPort(implFilename) {
24+
const messageChannel = new MessageChannel();
25+
const hostPort = messageChannel.port1;
26+
const implPort = messageChannel.port2;
27+
28+
if (typeof Worker === 'undefined') {
29+
const implModule = await import(`${implFilename}`);
30+
await implModule.connect(implPort);
31+
return hostPort;
32+
}
33+
34+
return new Promise((resolve, reject) => {
35+
const workerScriptPath = new URL(`./webworker-wrapper.js`, import.meta.url).href;
36+
const worker = new Worker(workerScriptPath, { type: 'module' });
37+
worker.postMessage({ implSrc: implFilename }, [implPort]);
38+
resolve(hostPort);
39+
});
40+
}

archive/compress.js

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
* Copyright(c) 2023 Google Inc.
99
*/
1010

11+
import { getConnectedPort } from './common.js';
12+
1113
// NOTE: THIS IS A VERY HACKY WORK-IN-PROGRESS! THE API IS NOT FROZEN! USE AT YOUR OWN RISK!
1214

1315
/**
@@ -59,28 +61,6 @@ export const CompressStatus = {
5961
ERROR: 'error',
6062
};
6163

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-
8464
/**
8565
* A thing that zips files.
8666
* NOTE: THIS IS A VERY HACKY WORK-IN-PROGRESS! THE API IS NOT FROZEN! USE AT YOUR OWN RISK!
@@ -144,9 +124,7 @@ export class Zipper {
144124
* of bytes.
145125
*/
146126
async start(files, isLastFile) {
147-
const messageChannel = new MessageChannel();
148-
this.port_ = messageChannel.port1;
149-
await connectPortFn('./zip.js', messageChannel.port2);
127+
this.port_ = await getConnectedPort('./zip.js');
150128
return new Promise((resolve, reject) => {
151129
this.port_.onerror = (evt) => {
152130
console.log('Impl error: message = ' + evt.message);

archive/decompress.js

Lines changed: 29 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import { UnarchiveAppendEvent, UnarchiveErrorEvent, UnarchiveEvent, UnarchiveEventType,
1212
UnarchiveExtractEvent, UnarchiveFinishEvent, UnarchiveInfoEvent,
1313
UnarchiveProgressEvent, UnarchiveStartEvent } from './events.js';
14+
import { getConnectedPort } from './common.js';
1415
import { findMimeType } from '../file/sniffer.js';
1516

1617
// Exported as a convenience (and also because this module used to contain these).
@@ -43,29 +44,6 @@ export {
4344
* @property {boolean=} debug Set to true for verbose unarchiver logging.
4445
*/
4546

46-
/**
47-
* Connects the MessagePort to the unarchiver implementation (e.g. unzip.js). If Workers exist
48-
* (e.g. web browsers or deno), imports the implementation inside a Web Worker. Otherwise, it
49-
* dynamically imports the implementation inside the current JS context.
50-
* The MessagePort is used for communication between host and implementation.
51-
* @param {string} implFilename The decompressor implementation filename relative to this path
52-
* (e.g. './unzip.js').
53-
* @param {MessagePort} implPort The MessagePort to connect to the decompressor implementation.
54-
* @returns {Promise<void>} The Promise resolves once the ports are connected.
55-
*/
56-
const connectPortFn = async (implFilename, implPort) => {
57-
if (typeof Worker === 'undefined') {
58-
return import(`${implFilename}`).then(implModule => implModule.connect(implPort));
59-
}
60-
61-
return new Promise((resolve, reject) => {
62-
const workerScriptPath = new URL(`./webworker-wrapper.js`, import.meta.url).href;
63-
const worker = new Worker(workerScriptPath, { type: 'module' });
64-
worker.postMessage({ implSrc: implFilename }, [implPort]);
65-
resolve();
66-
});
67-
};
68-
6947
/**
7048
* Base class for all Unarchivers.
7149
*/
@@ -82,13 +60,11 @@ export class Unarchiver extends EventTarget {
8260
* @param {ArrayBuffer} arrayBuffer The Array Buffer. Note that this ArrayBuffer must not be
8361
* referenced once it is sent to the Unarchiver, since it is marked as Transferable and sent
8462
* to the decompress implementation.
85-
* @param {Function(string, MessagePort):Promise<*>} connectPortFn A function that takes a path
86-
* to a JS decompression implementation (unzip.js) and connects it to a MessagePort.
8763
* @param {UnarchiverOptions|string} options An optional object of options, or a string
8864
* representing where the BitJS files are located. The string version of this argument is
8965
* deprecated.
9066
*/
91-
constructor(arrayBuffer, connectPortFn, options = {}) {
67+
constructor(arrayBuffer, options = {}) {
9268
super();
9369

9470
if (typeof options === 'string') {
@@ -104,13 +80,6 @@ export class Unarchiver extends EventTarget {
10480
*/
10581
this.ab = arrayBuffer;
10682

107-
/**
108-
* A factory method that connects a port to the decompress implementation.
109-
* @type {Function(MessagePort): Promise<*>}
110-
* @private
111-
*/
112-
this.connectPortFn_ = connectPortFn;
113-
11483
/**
11584
* @orivate
11685
* @type {boolean}
@@ -170,6 +139,7 @@ export class Unarchiver extends EventTarget {
170139
* Receive an event and pass it to the listener functions.
171140
*
172141
* @param {Object} obj
142+
* @returns {boolean} Returns true if the decompression is finished.
173143
* @private
174144
*/
175145
handlePortEvent_(obj) {
@@ -179,31 +149,36 @@ export class Unarchiver extends EventTarget {
179149
this.dispatchEvent(evt);
180150
if (evt.type == UnarchiveEventType.FINISH) {
181151
this.stop();
152+
return true;
182153
}
183154
} else {
184155
console.log(`Unknown object received from port: ${obj}`);
185156
}
157+
return false;
186158
}
187159

188160
/**
189161
* Starts the unarchive by connecting the ports and sending the first ArrayBuffer.
162+
* @returns {Promise<void>} A Promise that resolves when the decompression is complete. While the
163+
* decompression is proceeding, you can send more bytes of the archive to the decompressor
164+
* using the update() method.
190165
*/
191-
start() {
192-
const me = this;
193-
const messageChannel = new MessageChannel();
194-
this.port_ = messageChannel.port1;
195-
this.connectPortFn_(this.getScriptFileName(), messageChannel.port2).then(() => {
196-
this.port_.onerror = function (e) {
197-
console.log('Impl error: message = ' + e.message);
198-
throw e;
166+
async start() {
167+
this.port_ = await getConnectedPort(this.getScriptFileName());
168+
return new Promise((resolve, reject) => {
169+
this.port_.onerror = (evt) => {
170+
console.log('Impl error: message = ' + evt.message);
171+
reject(evt);
199172
};
200173

201-
this.port_.onmessage = function (e) {
202-
if (typeof e.data == 'string') {
203-
// Just log any strings the port pumps our way.
204-
console.log(e.data);
174+
this.port_.onmessage = (evt) => {
175+
if (typeof evt.data == 'string') {
176+
// Just log any strings the implementation pumps our way.
177+
console.log(evt.data);
205178
} else {
206-
me.handlePortEvent_(e.data);
179+
if (this.handlePortEvent_(evt.data)) {
180+
resolve();
181+
}
207182
}
208183
};
209184

@@ -212,10 +187,11 @@ export class Unarchiver extends EventTarget {
212187
file: ab,
213188
logToConsole: this.debugMode_,
214189
}, [ab]);
215-
this.ab = null;
190+
this.ab = null;
216191
});
217192
}
218193

194+
// TODO(bitjs): Test whether ArrayBuffers must be transferred...
219195
/**
220196
* Adds more bytes to the unarchiver.
221197
* @param {ArrayBuffer} ab The ArrayBuffer with more bytes in it. If opt_transferable is
@@ -258,7 +234,7 @@ export class Unzipper extends Unarchiver {
258234
* @param {UnarchiverOptions} options
259235
*/
260236
constructor(ab, options = {}) {
261-
super(ab, connectPortFn, options);
237+
super(ab, options);
262238
}
263239

264240
getMIMEType() { return 'application/zip'; }
@@ -271,7 +247,7 @@ export class Unrarrer extends Unarchiver {
271247
* @param {UnarchiverOptions} options
272248
*/
273249
constructor(ab, options = {}) {
274-
super(ab, connectPortFn, options);
250+
super(ab, options);
275251
}
276252

277253
getMIMEType() { return 'application/x-rar-compressed'; }
@@ -284,7 +260,7 @@ export class Untarrer extends Unarchiver {
284260
* @param {UnarchiverOptions} options
285261
*/
286262
constructor(ab, options = {}) {
287-
super(ab, connectPortFn, options);
263+
super(ab, options);
288264
}
289265

290266
getMIMEType() { return 'application/x-tar'; }
@@ -311,11 +287,11 @@ export function getUnarchiver(ab, options = {}) {
311287
const mimeType = findMimeType(ab);
312288

313289
if (mimeType === 'application/x-rar-compressed') { // Rar!
314-
unarchiver = new Unrarrer(ab, connectPortFn, options);
290+
unarchiver = new Unrarrer(ab, options);
315291
} else if (mimeType === 'application/zip') { // PK (Zip)
316-
unarchiver = new Unzipper(ab, connectPortFn, options);
292+
unarchiver = new Unzipper(ab, options);
317293
} else { // Try with tar
318-
unarchiver = new Untarrer(ab, connectPortFn, options);
294+
unarchiver = new Untarrer(ab, options);
319295
}
320296
return unarchiver;
321297
}

types/archive/common.d.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* common.js
3+
*
4+
* Provides common functionality for compressing and decompressing.
5+
*
6+
* Licensed under the MIT License
7+
*
8+
* Copyright(c) 2023 Google Inc.
9+
*/
10+
/**
11+
* Connects a host to a compress/decompress implementation via MessagePorts. The implementation must
12+
* have an exported connect() function that accepts a MessagePort. If the runtime support Workers
13+
* (e.g. web browsers, deno), imports the implementation inside a Web Worker. Otherwise, it
14+
* dynamically imports the implementation inside the current JS context (node, bun).
15+
* @param {string} implFilename The compressor/decompressor implementation filename relative to this
16+
* path (e.g. './unzip.js').
17+
* @returns {Promise<MessagePort>} The Promise resolves to the MessagePort connected to the
18+
* implementation that the host should use.
19+
*/
20+
export function getConnectedPort(implFilename: string): Promise<MessagePort>;
21+
//# sourceMappingURL=common.d.ts.map

types/archive/common.d.ts.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

types/archive/decompress.d.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@
1010
* @returns {Unarchiver}
1111
*/
1212
export function getUnarchiver(ab: ArrayBuffer, options?: UnarchiverOptions | string): Unarchiver;
13+
/**
14+
* All extracted files returned by an Unarchiver will implement
15+
* the following interface:
16+
*/
17+
/**
18+
* @typedef UnarchivedFile
19+
* @property {string} filename
20+
* @property {Uint8Array} fileData
21+
*/
22+
/**
23+
* @typedef UnarchiverOptions
24+
* @property {boolean=} debug Set to true for verbose unarchiver logging.
25+
*/
1326
/**
1427
* Base class for all Unarchivers.
1528
*/
@@ -18,13 +31,11 @@ export class Unarchiver extends EventTarget {
1831
* @param {ArrayBuffer} arrayBuffer The Array Buffer. Note that this ArrayBuffer must not be
1932
* referenced once it is sent to the Unarchiver, since it is marked as Transferable and sent
2033
* to the decompress implementation.
21-
* @param {Function(string, MessagePort):Promise<*>} connectPortFn A function that takes a path
22-
* to a JS decompression implementation (unzip.js) and connects it to a MessagePort.
2334
* @param {UnarchiverOptions|string} options An optional object of options, or a string
2435
* representing where the BitJS files are located. The string version of this argument is
2536
* deprecated.
2637
*/
27-
constructor(arrayBuffer: ArrayBuffer, connectPortFn: any, options?: UnarchiverOptions | string);
38+
constructor(arrayBuffer: ArrayBuffer, options?: UnarchiverOptions | string);
2839
/**
2940
* The client-side port that sends messages to, and receives messages from the
3041
* decompressor implementation.
@@ -38,12 +49,6 @@ export class Unarchiver extends EventTarget {
3849
* @protected
3950
*/
4051
protected ab: ArrayBuffer;
41-
/**
42-
* A factory method that connects a port to the decompress implementation.
43-
* @type {Function(MessagePort): Promise<*>}
44-
* @private
45-
*/
46-
private connectPortFn_;
4752
/**
4853
* @orivate
4954
* @type {boolean}
@@ -72,13 +77,17 @@ export class Unarchiver extends EventTarget {
7277
* Receive an event and pass it to the listener functions.
7378
*
7479
* @param {Object} obj
80+
* @returns {boolean} Returns true if the decompression is finished.
7581
* @private
7682
*/
7783
private handlePortEvent_;
7884
/**
7985
* Starts the unarchive by connecting the ports and sending the first ArrayBuffer.
86+
* @returns {Promise<void>} A Promise that resolves when the decompression is complete. While the
87+
* decompression is proceeding, you can send more bytes of the archive to the decompressor
88+
* using the update() method.
8089
*/
81-
start(): void;
90+
start(): Promise<void>;
8291
/**
8392
* Adds more bytes to the unarchiver.
8493
* @param {ArrayBuffer} ab The ArrayBuffer with more bytes in it. If opt_transferable is

types/archive/decompress.d.ts.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)