"DOM object constructor cannot be called as a function", specifically AudioWorklet

184 views
Skip to first unread message

Mark Hills

unread,
May 23, 2019, 6:17:28 PM5/23/19
to closure-comp...@googlegroups.com
The compiler makes the following transformation on "extends", which is not
valid for all objects:

class TestProcessor extends AudioWorkletProcessor {
constructor() {
super();
}

[...]

into:

function q(){
return AudioWorkletProcessor.call(this) || this
}

The error (Chrome browser):

Uncaught TypeError: Failed to construct 'AudioWorkletProcessor': Please
use the 'new' operator, this DOM object constructor cannot be called as
a function.

I am short of a workaround here, and this pattern is used extensively in
AudioWorklet.

* Whilst I am sometimes able write the source as a member variable instead
of inheritence, I couldn't do this for eg. AudioWorkletProcessor

* Is there some annotation I can use to prevent this transform on
TestProcessor

* I had to add some simple externs for these objects. Does the compiler
have some understanding of the various AudioWorklet classes that I am
missing?

Tested compiler v20190513 with no additional flags. Both -O SIMPLE or -O
ADVANCED exhibit the behaviour, and I cannot find any flags which affect
it.

Externs:

/**
* @constructor
*/
var AudioWorkletProcessor = function() {}
AudioWorkletProcessor.prototype.process = null;
function registerProcessor(a, b) {}

Thanks

--
Mark

Bradford Smith

unread,
May 23, 2019, 7:37:59 PM5/23/19
to Closure Compiler Discuss
I believe the fix for this problem is to have the ES6 Class -> ES5 class transpilation use Reflect.construct, depending on a polyfill if needed.

We (closure-compiler team) have generally tried to avoid using possibly-polyfilled methods when transpiling code, but this is a case where that no longer works.
We made some efforts to do fix this particular case in the past but ran into a blocker related to the way we do the polyfills.
This quarter I'm working  to clean up that blocker so the transpilation code can start using possibly-polyfilled stuff.

Sorry I don't have a specific ETA, though.

Bradford

Mark Hills

unread,
May 23, 2019, 8:34:28 PM5/23/19
to 'Bradford Smith' via Closure Compiler Discuss
On Thu, 23 May 2019, 'Bradford Smith' via Closure Compiler Discuss wrote:

> I believe the fix for this problem is to have the ES6 Class -> ES5 class
> transpilation use Reflect.construct, depending on a polyfill if needed.
[...]

Bradford, thanks for the prompt reply.

In search of a practical stop-gap solution, if the compiler doens't handle
this does this mean I have practical options:

* write the source code using Reflect.construct(), so the compiler doesn't
intervene

* use some sed etc. to attempt to attempt to transplant Reflect.contruct
in this specific case

I'd welcome any guidance on what the code below should translate to based
on Reflect.contruct, including the procss() function.

For compilation, if it helps: compatibility (which I assume you mean by
"polyfill") isn't an issue here. Older browsers don't have the
AudioWorklet functionality, so I have a completely different
implementation for them. Actually, I am switching between compiled and
un-compiled source during development.

class TestProcessor extends AudioWorkletProcessor {
constructor() {
super();
}

process(ins, outs, parameters) {
return true;
}
}

registerProcessor('test', TestProcessor);

--
Mark

Mark Hills

unread,
May 24, 2019, 12:32:59 PM5/24/19
to 'Bradford Smith' via Closure Compiler Discuss
On Thu, 23 May 2019, 'Bradford Smith' via Closure Compiler Discuss wrote:

> I believe the fix for this problem is to have the ES6 Class -> ES5 class
> transpilation use Reflect.construct, depending on a polyfill if needed.
[...]

I did some investigation today. I don't think the compiler would produce
usable code in the case of Audio Worklets (and potentially others),
because this would assume to control the calling code.

Because, registerProcessor() is given the class name, and takes
responsibity for constructing objects (with 'new', I suppose)

I tried many things but could not find a way to exploit
Reflect.construct() to operate in the context of 'this'; the usual
.call() is off-limits for the DOM object.

I'm not an expert so it would be a pleasure to be proved wrong here --
what should I use in the constructor, below?

It's interesting, as I have read today suggestions ES6 classes are a nicer
syntax but otherwise the same; this would mean they have a small
piece of additional capability.

I don't think I have an option but to bridge this with un-compiled code.

Here's my test case (no compliation required), many thanks.


<!DOCTYPE html>
<html>
<body>
<button onClick="go()">Start</button>

<script>
async function go() {
var ac = new AudioContext();

await ac.audioWorklet.addModule('./test-awp.js');
console.log('Loaded');

var worklet = new AudioWorkletNode(ac, 'reflect'); // or 'example'

worklet.port.onmessage = function(event) {
console.log(event);
}
}
</script>
</body>


/*
* An attempt to make this work using EC5 classes
*/

var ReflectProcessor = function() {
console.log("ReflectProcessor");

//
// How do we invoke the AudioWorkletProcessor constructor here?
//
// AudioWorkletProcessor.call(this); // not on a DOM object
//
// Reflect.construct(AudioWorkletProcessor); // returns object; no effect
//
// var x = new AudioWorkletProcessor();
// this.port = x.port; // can't assign
//
// var x = new AudioWorkletProcessor();
// Object.assign(this, x); // returns object; no effect
//

this.port.postMessage('Hello from ReflectProcessor');
}

ReflectProcessor.prototype = Object.create(AudioWorkletProcessor.prototype);

ReflectProcessor.prototype.process = function(ins, outs, parameters) {
return true;
}

registerProcessor('reflect', ReflectProcessor);


/*
* Suggested use of AudioWorkletProcessor, which works
*/

class ExampleProcessor extends AudioWorkletProcessor {
constructor() {
console.log("ExampleProcessor");
super();
this.port.postMessage('Hello from ExampleProcessor');
}

process(ins, outs, parameters) {
return true;
}
}

registerProcessor('example', ExampleProcessor);

--
Mark

Bradford Smith

unread,
May 24, 2019, 3:02:58 PM5/24/19
to Closure Compiler Discuss
In general trying to extend ES6 classes with ES5 syntax is just a problem.
There's an interesting thread on it here.

I think theoretically you should be able to do something like this.

function MyEs5Class() {
  const self = Reflect.construct(Es6Class, [], new.target);
  self.es5ClassProp1 = 'something'
}
Object.setPrototypeOf(MyEs5Class.prototype, Es6Class.prototype);

MyEs5Class.prototype.method1 = ...

However
* I'm not sure that works 100% even uncompiled
* closure-compiler's support for `new.target` is very limited and will possibly choke on this
* if this gets transpiled to ES5 output, I'm not confident it will work

The ES6 spec is careful to make sure you can extend ES5 classes with ES6, but it created a definite problem for doing things the other way around.

HTH,
Bradford

--

---
You received this message because you are subscribed to the Google Groups "Closure Compiler Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to closure-compiler-d...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/closure-compiler-discuss/1905241702280.19810%40stax.localdomain.
For more options, visit https://groups.google.com/d/optout.

Mark Hills

unread,
May 27, 2019, 9:45:18 AM5/27/19
to 'Bradford Smith' via Closure Compiler Discuss
On Fri, 24 May 2019, 'Bradford Smith' via Closure Compiler Discuss wrote:

> In general trying to extend ES6 classes with ES5 syntax is just a problem.
> There's an interesting thread on it here.
> https://esdiscuss.org/topic/extending-an-es6-class-using-es5-syntax

Thanks, this looks like a good reference, which is a more general version
of my question without the context of the compiler.

> I think theoretically you should be able to do something like this.
>
> function MyEs5Class() {
> const self = Reflect.construct(Es6Class, [], new.target);
> self.es5ClassProp1 = 'something'
> }
> Object.setPrototypeOf(MyEs5Class.prototype, Es6Class.prototype);
>
> MyEs5Class.prototype.method1 = ...
>
> However
> * I'm not sure that works 100% even uncompiled
> * closure-compiler's support for `new.target` is very limited and will
> possibly choke on this
> * if this gets transpiled to ES5 output, I'm not confident it will work

I didn't have these problems, but I did have the problem which I think is
the same as the original poster in the thread above.

In testing I can see that it still creates a new object, which is
different to 'this'.

I could easily be incorrect, as there's a lot in that thread I would need
more time to fully understand.

And so I think this leaves .process() being called on the original 'this'
object (not the 'self' one) -- though it's a bit tricky to tell as I don't
have the same level of reporting/debugging from the audio thread.

Here is a quote from that thread:

> You simply can’t get around the fact that calling an ES6 class
> constructor will create a new object instance, even if you already have
> one with the right prototype chain

and this would almost be by design, as the class constructor can only be
used in a specific way, for safety. That suggests to me it's logical that
there's no 'workaround' here; and that I must use an ES6 class. That would
be otherwise ok (there's no browser compatibilty issues for this audio
code); I'm left looking at some way to prevent the compiler intervening.

Thanks again

--
Mark
Reply all
Reply to author
Forward
0 new messages