yume-chan/ya-webadb

ReferenceError when trying to create a connection to device

Closed this issue · 3 comments

Issue Checklist

  • I'm using the library programmatically
  • For Scrcpy related issues, I have searched in Genymobile/scrcpy repository.

Library version

0.0.23

Environment

Windows, Chrome 123, Angular 17

Device

Android

Describe the bug

I'm using
Angular 17.3.2
TypeScript 5.4.2
node 18.19.1
running on localhost:4200

after successfully getting a device object, the connect() call throws the error below

app.component.ts:40 ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
    at Object.start (wrap-readable.js:27:42)
    at new WrapReadableStream (wrap-readable.js:26:9)
    at DuplexStreamFactory.wrapReadable (duplex.js:29:16)
    at new AdbDaemonWebUsbConnection (device.js:111:33)
    at AdbDaemonWebUsbDevice.<anonymous> (device.js:245:16)
    at Generator.next (<anonymous>)
    at fulfilled (chunk-J4B6MK7R.js?v=2fd0d645:36:24)
    at _ZoneDelegate.invoke (zone.js:368:26)
    at Object.onInvoke (core.mjs:14882:33)
    at _ZoneDelegate.invoke (zone.js:367:52)

Steps to reproduce

  1. create a new Angular project
  2. connect and get an AdbDaemonWebUsbDevice object
  3. call connect()

This is caused by Angular compiler.


For this code:

class Base {
  // @ts-expect-error
  constructor(callback) {
    callback();
  }
}

class Derived extends Base {
  constructor() {
    super(async () => {
      await Promise.resolve();
      console.log(this);
    });
  }
}

new Derived();

In Derived's constructor, callback is invoked before super() returns, so it can't access this synchronously. await Promise.resolve() is used to run the following code in a microtask, after super() returns.

In any modern browser with native async function support, the above code will run fine.


However, because Angular uses Zone.js to track async contexts, it must down-level compile async functions to generators. For the code above, the result is:

var Base = class {
  // @ts-expect-error
  constructor(callback) {
    callback();
  }
};
var Derived = class extends Base {
  constructor() {
    super(() => __async(this, null, function* () {
      yield Promise.resolve();
      console.log(this);
    }));
  }
};

As this is an argument to __async, it's evaluated immediately. It's not what the original code does, and violates JavaScript specs.


The offending code is:

super(
{
start: async (controller) => {
// `start` is invoked before `ReadableStream`'s constructor finish,
// so using `this` synchronously causes
// "Must call super constructor in derived class before accessing 'this' or returning from derived constructor".
// Queue a microtask to avoid this.
await Promise.resolve();
this.readable = await getWrappedReadableStream(
wrapper,
controller,
);
this.#reader = this.readable.getReader();
},


Similar TypeScript down-level compilation bug:

Good news! Since Angular 18 it is possible to run Angular without zone.js.

I can confirm that this library and all functionality does work on Angular 18+ with the following provider enabled: provideExperimentalZonelessChangeDetection in the application config.

More info: https://angular.dev/guide/experimental/zoneless

It should be possible to rewrite the code to work around that down-level compilation bug:

class Base {
  // @ts-expect-error
  constructor(callback) {
    callback();
  }
}

class Derived extends Base {
  constructor() {
    super(() => Promise.resolve().then(async () => { console.log(this) }));
  }
}

new Derived();

TypeScript produces this (I didn't check Angular compiler):

"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
class Base {
    // @ts-expect-error
    constructor(callback) {
        callback();
    }
}
class Derived extends Base {
    constructor() {
        super(() => Promise.resolve().then(() => __awaiter(this, void 0, void 0, function* () { console.log(this); })));
    }
}
new Derived();

You can patch your node_modules if needed (there might be more places need to be patched). I don't want to add intentionally bad code for a compiler bug.