From e6d13d9404a1a47a9841bfb517fb42406791a64f Mon Sep 17 00:00:00 2001 From: Jeff Schiller Date: Wed, 13 Dec 2023 14:25:17 -0800 Subject: [PATCH] Commonize on port-connected behavior for compressors and decompressors (part of the refactor for issue #44) --- archive/common.js | 40 +++++++++++++++ archive/compress.js | 28 ++--------- archive/decompress.js | 82 +++++++++++-------------------- types/archive/common.d.ts | 21 ++++++++ types/archive/common.d.ts.map | 1 + types/archive/decompress.d.ts | 29 +++++++---- types/archive/decompress.d.ts.map | 2 +- 7 files changed, 114 insertions(+), 89 deletions(-) create mode 100644 archive/common.js create mode 100644 types/archive/common.d.ts create mode 100644 types/archive/common.d.ts.map diff --git a/archive/common.js b/archive/common.js new file mode 100644 index 0000000..4635c66 --- /dev/null +++ b/archive/common.js @@ -0,0 +1,40 @@ +/** + * common.js + * + * Provides common functionality for compressing and decompressing. + * + * Licensed under the MIT License + * + * Copyright(c) 2023 Google Inc. + */ + +// Requires the following JavaScript features: MessageChannel, MessagePort, and dynamic imports. + +/** + * Connects a host to a compress/decompress implementation via MessagePorts. The implementation must + * have an exported connect() function that accepts a MessagePort. If the runtime support Workers + * (e.g. web browsers, deno), imports the implementation inside a Web Worker. Otherwise, it + * dynamically imports the implementation inside the current JS context (node, bun). + * @param {string} implFilename The compressor/decompressor implementation filename relative to this + * path (e.g. './unzip.js'). + * @returns {Promise} The Promise resolves to the MessagePort connected to the + * implementation that the host should use. + */ +export async function getConnectedPort(implFilename) { + const messageChannel = new MessageChannel(); + const hostPort = messageChannel.port1; + const implPort = messageChannel.port2; + + if (typeof Worker === 'undefined') { + const implModule = await import(`${implFilename}`); + await implModule.connect(implPort); + return hostPort; + } + + return new Promise((resolve, reject) => { + const workerScriptPath = new URL(`./webworker-wrapper.js`, import.meta.url).href; + const worker = new Worker(workerScriptPath, { type: 'module' }); + worker.postMessage({ implSrc: implFilename }, [implPort]); + resolve(hostPort); + }); +} diff --git a/archive/compress.js b/archive/compress.js index b0610ab..0dc16ee 100644 --- a/archive/compress.js +++ b/archive/compress.js @@ -8,6 +8,8 @@ * Copyright(c) 2023 Google Inc. */ +import { getConnectedPort } from './common.js'; + // NOTE: THIS IS A VERY HACKY WORK-IN-PROGRESS! THE API IS NOT FROZEN! USE AT YOUR OWN RISK! /** @@ -59,28 +61,6 @@ export const CompressStatus = { ERROR: 'error', }; -/** - * Connects the MessagePort to the compressor implementation (e.g. zip.js). If Workers exist - * (e.g. web browsers or deno), imports the implementation inside a Web Worker. Otherwise, it - * dynamically imports the implementation inside the current JS context. - * The MessagePort is used for communication between host and implementation. - * @param {string} implFilename The compressor implementation filename relative to this path - * (e.g. './zip.js'). - * @param {MessagePort} implPort The MessagePort to connect to the compressor implementation. - * @returns {Promise} The Promise resolves once the ports are connected. - */ -const connectPortFn = async (implFilename, implPort) => { - if (typeof Worker === 'undefined') { - return import(`${implFilename}`).then(implModule => implModule.connect(implPort)); - } - return new Promise((resolve, reject) => { - const workerScriptPath = new URL(`./webworker-wrapper.js`, import.meta.url).href; - const worker = new Worker(workerScriptPath, { type: 'module' }); - worker.postMessage({ implSrc: implFilename }, [ implPort ]); - resolve(); - }); -}; - /** * A thing that zips files. * 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 { * of bytes. */ async start(files, isLastFile) { - const messageChannel = new MessageChannel(); - this.port_ = messageChannel.port1; - await connectPortFn('./zip.js', messageChannel.port2); + this.port_ = await getConnectedPort('./zip.js'); return new Promise((resolve, reject) => { this.port_.onerror = (evt) => { console.log('Impl error: message = ' + evt.message); diff --git a/archive/decompress.js b/archive/decompress.js index 3c78a30..4bd0579 100644 --- a/archive/decompress.js +++ b/archive/decompress.js @@ -11,6 +11,7 @@ import { UnarchiveAppendEvent, UnarchiveErrorEvent, UnarchiveEvent, UnarchiveEventType, UnarchiveExtractEvent, UnarchiveFinishEvent, UnarchiveInfoEvent, UnarchiveProgressEvent, UnarchiveStartEvent } from './events.js'; +import { getConnectedPort } from './common.js'; import { findMimeType } from '../file/sniffer.js'; // Exported as a convenience (and also because this module used to contain these). @@ -43,29 +44,6 @@ export { * @property {boolean=} debug Set to true for verbose unarchiver logging. */ -/** - * Connects the MessagePort to the unarchiver implementation (e.g. unzip.js). If Workers exist - * (e.g. web browsers or deno), imports the implementation inside a Web Worker. Otherwise, it - * dynamically imports the implementation inside the current JS context. - * The MessagePort is used for communication between host and implementation. - * @param {string} implFilename The decompressor implementation filename relative to this path - * (e.g. './unzip.js'). - * @param {MessagePort} implPort The MessagePort to connect to the decompressor implementation. - * @returns {Promise} The Promise resolves once the ports are connected. - */ -const connectPortFn = async (implFilename, implPort) => { - if (typeof Worker === 'undefined') { - return import(`${implFilename}`).then(implModule => implModule.connect(implPort)); - } - - return new Promise((resolve, reject) => { - const workerScriptPath = new URL(`./webworker-wrapper.js`, import.meta.url).href; - const worker = new Worker(workerScriptPath, { type: 'module' }); - worker.postMessage({ implSrc: implFilename }, [implPort]); - resolve(); - }); -}; - /** * Base class for all Unarchivers. */ @@ -82,13 +60,11 @@ export class Unarchiver extends EventTarget { * @param {ArrayBuffer} arrayBuffer The Array Buffer. Note that this ArrayBuffer must not be * referenced once it is sent to the Unarchiver, since it is marked as Transferable and sent * to the decompress implementation. - * @param {Function(string, MessagePort):Promise<*>} connectPortFn A function that takes a path - * to a JS decompression implementation (unzip.js) and connects it to a MessagePort. * @param {UnarchiverOptions|string} options An optional object of options, or a string * representing where the BitJS files are located. The string version of this argument is * deprecated. */ - constructor(arrayBuffer, connectPortFn, options = {}) { + constructor(arrayBuffer, options = {}) { super(); if (typeof options === 'string') { @@ -104,13 +80,6 @@ export class Unarchiver extends EventTarget { */ this.ab = arrayBuffer; - /** - * A factory method that connects a port to the decompress implementation. - * @type {Function(MessagePort): Promise<*>} - * @private - */ - this.connectPortFn_ = connectPortFn; - /** * @orivate * @type {boolean} @@ -170,6 +139,7 @@ export class Unarchiver extends EventTarget { * Receive an event and pass it to the listener functions. * * @param {Object} obj + * @returns {boolean} Returns true if the decompression is finished. * @private */ handlePortEvent_(obj) { @@ -179,31 +149,36 @@ export class Unarchiver extends EventTarget { this.dispatchEvent(evt); if (evt.type == UnarchiveEventType.FINISH) { this.stop(); + return true; } } else { console.log(`Unknown object received from port: ${obj}`); } + return false; } /** * Starts the unarchive by connecting the ports and sending the first ArrayBuffer. + * @returns {Promise} A Promise that resolves when the decompression is complete. While the + * decompression is proceeding, you can send more bytes of the archive to the decompressor + * using the update() method. */ - start() { - const me = this; - const messageChannel = new MessageChannel(); - this.port_ = messageChannel.port1; - this.connectPortFn_(this.getScriptFileName(), messageChannel.port2).then(() => { - this.port_.onerror = function (e) { - console.log('Impl error: message = ' + e.message); - throw e; + async start() { + this.port_ = await getConnectedPort(this.getScriptFileName()); + return new Promise((resolve, reject) => { + this.port_.onerror = (evt) => { + console.log('Impl error: message = ' + evt.message); + reject(evt); }; - this.port_.onmessage = function (e) { - if (typeof e.data == 'string') { - // Just log any strings the port pumps our way. - console.log(e.data); + this.port_.onmessage = (evt) => { + if (typeof evt.data == 'string') { + // Just log any strings the implementation pumps our way. + console.log(evt.data); } else { - me.handlePortEvent_(e.data); + if (this.handlePortEvent_(evt.data)) { + resolve(); + } } }; @@ -212,10 +187,11 @@ export class Unarchiver extends EventTarget { file: ab, logToConsole: this.debugMode_, }, [ab]); - this.ab = null; + this.ab = null; }); } + // TODO(bitjs): Test whether ArrayBuffers must be transferred... /** * Adds more bytes to the unarchiver. * @param {ArrayBuffer} ab The ArrayBuffer with more bytes in it. If opt_transferable is @@ -258,7 +234,7 @@ export class Unzipper extends Unarchiver { * @param {UnarchiverOptions} options */ constructor(ab, options = {}) { - super(ab, connectPortFn, options); + super(ab, options); } getMIMEType() { return 'application/zip'; } @@ -271,7 +247,7 @@ export class Unrarrer extends Unarchiver { * @param {UnarchiverOptions} options */ constructor(ab, options = {}) { - super(ab, connectPortFn, options); + super(ab, options); } getMIMEType() { return 'application/x-rar-compressed'; } @@ -284,7 +260,7 @@ export class Untarrer extends Unarchiver { * @param {UnarchiverOptions} options */ constructor(ab, options = {}) { - super(ab, connectPortFn, options); + super(ab, options); } getMIMEType() { return 'application/x-tar'; } @@ -311,11 +287,11 @@ export function getUnarchiver(ab, options = {}) { const mimeType = findMimeType(ab); if (mimeType === 'application/x-rar-compressed') { // Rar! - unarchiver = new Unrarrer(ab, connectPortFn, options); + unarchiver = new Unrarrer(ab, options); } else if (mimeType === 'application/zip') { // PK (Zip) - unarchiver = new Unzipper(ab, connectPortFn, options); + unarchiver = new Unzipper(ab, options); } else { // Try with tar - unarchiver = new Untarrer(ab, connectPortFn, options); + unarchiver = new Untarrer(ab, options); } return unarchiver; } diff --git a/types/archive/common.d.ts b/types/archive/common.d.ts new file mode 100644 index 0000000..3193116 --- /dev/null +++ b/types/archive/common.d.ts @@ -0,0 +1,21 @@ +/** + * common.js + * + * Provides common functionality for compressing and decompressing. + * + * Licensed under the MIT License + * + * Copyright(c) 2023 Google Inc. + */ +/** + * Connects a host to a compress/decompress implementation via MessagePorts. The implementation must + * have an exported connect() function that accepts a MessagePort. If the runtime support Workers + * (e.g. web browsers, deno), imports the implementation inside a Web Worker. Otherwise, it + * dynamically imports the implementation inside the current JS context (node, bun). + * @param {string} implFilename The compressor/decompressor implementation filename relative to this + * path (e.g. './unzip.js'). + * @returns {Promise} The Promise resolves to the MessagePort connected to the + * implementation that the host should use. + */ +export function getConnectedPort(implFilename: string): Promise; +//# sourceMappingURL=common.d.ts.map \ No newline at end of file diff --git a/types/archive/common.d.ts.map b/types/archive/common.d.ts.map new file mode 100644 index 0000000..2f911ff --- /dev/null +++ b/types/archive/common.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../archive/common.js"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH;;;;;;;;;GASG;AACH,+CALW,MAAM,GAEJ,QAAQ,WAAW,CAAC,CAoBhC"} \ No newline at end of file diff --git a/types/archive/decompress.d.ts b/types/archive/decompress.d.ts index 4e52c82..1d3c8f6 100644 --- a/types/archive/decompress.d.ts +++ b/types/archive/decompress.d.ts @@ -10,6 +10,19 @@ * @returns {Unarchiver} */ export function getUnarchiver(ab: ArrayBuffer, options?: UnarchiverOptions | string): Unarchiver; +/** + * All extracted files returned by an Unarchiver will implement + * the following interface: + */ +/** + * @typedef UnarchivedFile + * @property {string} filename + * @property {Uint8Array} fileData + */ +/** + * @typedef UnarchiverOptions + * @property {boolean=} debug Set to true for verbose unarchiver logging. + */ /** * Base class for all Unarchivers. */ @@ -18,13 +31,11 @@ export class Unarchiver extends EventTarget { * @param {ArrayBuffer} arrayBuffer The Array Buffer. Note that this ArrayBuffer must not be * referenced once it is sent to the Unarchiver, since it is marked as Transferable and sent * to the decompress implementation. - * @param {Function(string, MessagePort):Promise<*>} connectPortFn A function that takes a path - * to a JS decompression implementation (unzip.js) and connects it to a MessagePort. * @param {UnarchiverOptions|string} options An optional object of options, or a string * representing where the BitJS files are located. The string version of this argument is * deprecated. */ - constructor(arrayBuffer: ArrayBuffer, connectPortFn: any, options?: UnarchiverOptions | string); + constructor(arrayBuffer: ArrayBuffer, options?: UnarchiverOptions | string); /** * The client-side port that sends messages to, and receives messages from the * decompressor implementation. @@ -38,12 +49,6 @@ export class Unarchiver extends EventTarget { * @protected */ protected ab: ArrayBuffer; - /** - * A factory method that connects a port to the decompress implementation. - * @type {Function(MessagePort): Promise<*>} - * @private - */ - private connectPortFn_; /** * @orivate * @type {boolean} @@ -72,13 +77,17 @@ export class Unarchiver extends EventTarget { * Receive an event and pass it to the listener functions. * * @param {Object} obj + * @returns {boolean} Returns true if the decompression is finished. * @private */ private handlePortEvent_; /** * Starts the unarchive by connecting the ports and sending the first ArrayBuffer. + * @returns {Promise} A Promise that resolves when the decompression is complete. While the + * decompression is proceeding, you can send more bytes of the archive to the decompressor + * using the update() method. */ - start(): void; + start(): Promise; /** * Adds more bytes to the unarchiver. * @param {ArrayBuffer} ab The ArrayBuffer with more bytes in it. If opt_transferable is diff --git a/types/archive/decompress.d.ts.map b/types/archive/decompress.d.ts.map index 839fbf0..12eaec2 100644 --- a/types/archive/decompress.d.ts.map +++ b/types/archive/decompress.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"decompress.d.ts","sourceRoot":"","sources":["../../archive/decompress.js"],"names":[],"mappings":"AAmSA;;;;;;;;;;GAUG;AACH,kCARW,WAAW,YAGX,iBAAiB,GAAC,MAAM,GAGtB,UAAU,CAkBtB;AA5PD;;GAEG;AACH;IASE;;;;;;;;;OASG;IACH,yBATW,WAAW,gCAKX,iBAAiB,GAAC,MAAM,EAgClC;IA9CD;;;;;OAKG;IACH,cAAM;IAqBJ;;;;OAIG;IACH,cAHU,WAAW,CAGA;IAErB;;;;OAIG;IACH,uBAAmC;IAEnC;;;OAGG;IACH,YAFU,OAAO,CAEkB;IAGrC;;;;OAIG;IACH,yBAHa,MAAM,CAKlB;IAED;;;;OAIG;IACH,+BAHa,MAAM,CAKlB;IAED;;;;;OAKG;IACH,8BAsBC;IAED;;;;;OAKG;IACH,yBAWC;IAED;;OAEG;IACH,cA0BC;IAED;;;;;;;;OAQG;IACH,WAPW,WAAW,qBAGX,OAAO,oBAgBjB;IAED;;OAEG;IACH,aAKC;CACF;AAID;IACE;;;OAGG;IACH,gBAHW,WAAW,YACX,iBAAiB,EAI3B;CAIF;AAED;IACE;;;OAGG;IACH,gBAHW,WAAW,YACX,iBAAiB,EAI3B;CAIF;AAED;IACE;;;OAGG;IACH,gBAHW,WAAW,YACX,iBAAiB,EAI3B;CAIF;;cA/Pa,MAAM;cACN,UAAU;;;;;;YAKV,OAAO"} \ No newline at end of file +{"version":3,"file":"decompress.d.ts","sourceRoot":"","sources":["../../archive/decompress.js"],"names":[],"mappings":"AA6QA;;;;;;;;;;GAUG;AACH,kCARW,WAAW,YAGX,iBAAiB,GAAC,MAAM,GAGtB,UAAU,CAkBtB;AA1QD;;;GAGG;AAEH;;;;GAIG;AAEH;;;GAGG;AAEH;;GAEG;AACH;IASE;;;;;;;OAOG;IACH,yBAPW,WAAW,YAGX,iBAAiB,GAAC,MAAM,EAyBlC;IArCD;;;;;OAKG;IACH,cAAM;IAmBJ;;;;OAIG;IACH,cAHU,WAAW,CAGA;IAErB;;;OAGG;IACH,YAFU,OAAO,CAEkB;IAGrC;;;;OAIG;IACH,yBAHa,MAAM,CAKlB;IAED;;;;OAIG;IACH,+BAHa,MAAM,CAKlB;IAED;;;;;OAKG;IACH,8BAsBC;IAED;;;;;;OAMG;IACH,yBAaC;IAED;;;;;OAKG;IACH,SAJa,QAAQ,IAAI,CAAC,CA8BzB;IAGD;;;;;;;;OAQG;IACH,WAPW,WAAW,qBAGX,OAAO,oBAgBjB;IAED;;OAEG;IACH,aAKC;CACF;AAID;IACE;;;OAGG;IACH,gBAHW,WAAW,YACX,iBAAiB,EAI3B;CAIF;AAED;IACE;;;OAGG;IACH,gBAHW,WAAW,YACX,iBAAiB,EAI3B;CAIF;AAED;IACE;;;OAGG;IACH,gBAHW,WAAW,YACX,iBAAiB,EAI3B;CAIF;;cAtOa,MAAM;cACN,UAAU;;;;;;YAKV,OAAO"} \ No newline at end of file