Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot read properties of null (reading 'id') #361

Closed
stephanedupont opened this issue May 30, 2023 · 8 comments
Closed

Cannot read properties of null (reading 'id') #361

stephanedupont opened this issue May 30, 2023 · 8 comments
Assignees
Labels
Status: info needed Info is needed before moving on

Comments

@stephanedupont
Copy link

Description
I'm using webmidi.js on a project that is deployed both as a web app and an Electron app. I'm using Sentry for crash reports, and I've got a lot of errors Cannot read properties of null (reading 'id') from webmidi.js

Environment:

  • Library version and flavour:
webmidi@^3.1.4:
  version "3.1.5"
  resolved "https://registry.yarnpkg.com/webmidi/-/webmidi-3.1.5.tgz#1dfea41fde25cb02d77cc98f0bac06047cd4baac"
  integrity sha512-l+keAdhhWbW4ygJPgThcidDEMRW3l39Ppz389hbcxijiWwyo1k62GgO0JGb5V7we59qYLTVCCjC9fwYWob1wXA==
  dependencies:
    djipevents "^2.0.7"
  optionalDependencies:
    jzz "^1.5.6"
  • Runtime: Chrome 112, Chrome 113, Edge 113, Electron 22.3.2, Electron 22.3.9 (and probably many more)
  • Language: TypeScript
  • Operating system: Windows

Details
Here's the stack trace from Sentry:
image

@djipco djipco self-assigned this May 30, 2023
@djipco
Copy link
Owner

djipco commented May 30, 2023

I am not too familiar with Sentry but it seems that MIDIInput objects are not available in this particular setup. In a "normal" environment, this problem does not occur.

Could you provide a simple code excerpt that generates the error and that I could run on my end?

@djipco djipco added the Status: info needed Info is needed before moving on label May 30, 2023
@stephanedupont
Copy link
Author

I cannot as I cannot reproduce it myself. I just have plenty of these errors as crash reports, but didn't find a way to reproduce them.

The full stack trace shown above shows that it's called in onInterfaceStateChange, so I cannot really wrap this around a try/catch or do anything about it. It seems that it's something internal to the library.

Here's my full (and very simple) use of webmidi if it can help:

import {WebMidi} from "webmidi";
import {CONFIG_LOG_D, CONFIG_LOG_V, CONFIG_LOG_W} from "../config/Config";
import {Log} from "./Log";
import {Alteration, NoteName} from "./MusicTheory/Note";
import {globalManager} from "./GlobalManager";
import {getSettingsStore} from "../stores/settings/SettingsStore";
import {App} from "../types/App";

const LOG_TAG = "MIDIHelper: ";
const MINIMUM_DELTA_BETWEEN_TWO_MIDI_MESSAGES = 30 * 1000000; // 30ms

export interface MIDIEventListener {
  onNoteOn?(note: NoteName, alteration: Alteration, octave: number, velocity: number, timestamp: number): void;
  onNoteOff?(note: NoteName, alteration: Alteration, octave: number, timestamp: number): void;
}

export class MIDIHelper {

  private static isEnabling: boolean = false;
  private static listener: MIDIEventListener | null = null;

  private static lastNoteOnMessageTimestamps: { [key: number]: number } = {};
  private static lastNoteOffMessageTimestamps: { [key: number]: number } = {};

  static init(): void {
    if (MIDIHelper.isEnabling || WebMidi.enabled) return;
    if (CONFIG_LOG_D) Log.D(LOG_TAG + "Enabling WebMidi");
    WebMidi.enable()
      .then(MIDIHelper.onEnabled)
      .catch(e => MIDIHelper.onEnableError(e));
  }

  static disable(): void {
    if (CONFIG_LOG_D) Log.D(LOG_TAG + "Disabling WebMidi");
    WebMidi.disable()
      .then(MIDIHelper.onDisabled)
      .catch(e => MIDIHelper.onDisableError(e));
  }

  static setListener(listener: MIDIEventListener | null): void {
    MIDIHelper.listener = listener;
  }

  static getListener(): MIDIEventListener | null {
    return MIDIHelper.listener;
  }

  private static onEnabled(): void {
    MIDIHelper.isEnabling = false;

    if (CONFIG_LOG_D) Log.D(
      LOG_TAG + "WebMidi enabled with success, starting to listening on available inputs...");

    WebMidi.inputs.forEach(input => {
      if (CONFIG_LOG_D) Log.D(LOG_TAG + `Starting to listen on input ${input.manufacturer} ${input.name}`);
      input.addListener("noteon", e => {
        MIDIHelper.onNoteOnMessageReceived(e.note.number, e.note.attack, Math.round(e.timestamp * 1000000));
      })
      input.addListener("noteoff", e => {
        MIDIHelper.onNoteOffMessageReceived(e.note.number, Math.round(e.timestamp * 1000000));
      })
    });
  }

  private static onEnableError(e: any) {
    MIDIHelper.isEnabling = false;

    if (CONFIG_LOG_W) Log.W(
      LOG_TAG + "Error enabling WebMidi", e);

    setTimeout(() => {
      globalManager.displaySnack("Web MIDI API doesn't seem to be supported on this browser.")
    }, 2000);
  }

  private static onDisabled(): void {
    if (CONFIG_LOG_D) Log.D(
      LOG_TAG + "WebMidi disabled with success.");
  }

  private static onDisableError(e: any) {
    // Beside logging the error, we don't do anything, that's not very important...
    if (CONFIG_LOG_W) Log.W(
      LOG_TAG + "Problem disabling WebMidi", e);
  }

  private static onNoteOnMessageReceived(pitch: number, velocity: number, timestamp: number): void {
    // Whatever, code here is not important and is not using webmidi
  }

  private static onNoteOffMessageReceived(pitch: number, timestamp: number): void {
    // Whatever, code here is not important and is not using webmidi
  }
}

@djipco
Copy link
Owner

djipco commented Jun 1, 2023

As you can surely understand, without a means to reproduce the problem, it becomes very hard for me to troubleshoot the problem.

If you run the small TypeScript example provided in this repo, do you get any errors?

@stephanedupont
Copy link
Author

If I understand correctly, when we call WebMidi.disable(), _destroyInputsAndOutputs() is called and makes an array or promises that will call input.destroy() and output.destroy() on all inputs and outputs, then set the this._inputs and this._outputs variable to empty arrays.

... but as they are promises, it won't be executed instantly, so the following can happen (and is probably what I see in my error reports):

Let's imagine we have several inputs (A,B,C,...)

  • Input A is destroyed
  • There are still inputs/outputs to destroy so it's possible that before all promises are executed, input A is changing state and _onInterfaceStateChange is called.
  • This could lead to the stack trace above, where destroy() has been called on Input A, and therefore, the _midiInput field of input A is null (has been set to null in destroy())

... so shouldn't the getInputById add a null check like this?

getInputById(id, options = {disconnected: false}) {

    if (this.validation) {
      if (!this.enabled) throw new Error("WebMidi is not enabled.");
      if (!id) return;
    }

    if (options.disconnected) {
      for (let i = 0; i < this._disconnectedInputs.length; i++) {
        if (this._disconnectedInputs[i]._midiInput && this._disconnectedInputs[i].id === id.toString()) return this._disconnectedInputs[i];
      }
    } else {
      for (let i = 0; i < this.inputs.length; i++) {
        if (this.inputs[i]._midiInput && this.inputs[i].id === id.toString()) return this.inputs[i];
      }
    }

  };

@stephanedupont
Copy link
Author

... or maybe something like this:

async disable() {
  this._isDisabling = true;
  ...
}

_onInterfaceStateChange(e) {
  if (this.isDisabling) return;
  ...
}

@djipco
Copy link
Owner

djipco commented Jun 3, 2023

Stéphane,

Thank you so much for this analysis. This is really helpful. I made a minor adjustment to the codebase that will probably fix the issue. The fix passes all unit tests but, before I release it, could you try it out in your context to see if it indeed fixes the problem?

You can just replace the /dist/cjs folder in your project with the one attached here: cjs.zip

@stephanedupont
Copy link
Author

Thank you so much (for this and for the really useful library!).

Unfortunately, as I cannot reproduce it directly, I can just wait to see if I have any new crash reports.

I would also have to deploy a new build of the app, so the changes won't necessarily be pushed before at least a few days, probably more a couple of weeks.

But I'm very confident that your fix should works, your code seem to be fixing this edge case. So don't wait on me :)

Again, thank you very much!

@djipco
Copy link
Owner

djipco commented Jun 4, 2023

Okay, no worries. I just published release v3.1.6 on NPM. Hopefully it will fix your issue. Feel free to chime in if it didn't. Thx.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Status: info needed Info is needed before moving on
Projects
None yet
Development

No branches or pull requests

2 participants