ableton-js icon indicating copy to clipboard operation
ableton-js copied to clipboard

session_record listener timeout

Open eliaskg opened this issue 11 months ago • 4 comments

When I have the following script running:

ableton.on("connect", function(e) {
  console.log("Connect", e);
  
  ableton.song.addListener("session_record", function(s) {
    session_record_did_change(s);
  })
})

Sometimes the script crashes after changing the project or re-opening Ableton:

/node_modules/ableton-js/index.js:328
                rej(new TimeoutError(`The command ${cls}.${command.name}(${arg}) timed out after ${timeout} ms.`, payload));
                    ^

TimeoutError: The command song.add_listener({"prop":"session_record","eventId":"7f287dc5-85fb-4ddb-8afc-7bb6dc8383e7"}) timed out after 2000 ms.
    at Timeout._onTimeout (node_modules/ableton-js/index.js:328:21)
    at listOnTimeout (node:internal/timers:573:17)
    at process.processTimers (node:internal/timers:514:7) {
  payload: {
    uuid: 'f012d8f5-f1f3-4462-a23b-45caa7065786',
    ns: 'song',
    nsid: undefined,
    name: 'add_listener',
    args: {
      prop: 'session_record',
      nsid: undefined,
      eventId: '7f287dc5-85fb-4ddb-8afc-7bb6dc8383e7'
    }
  }
}

It seems to be very random and is hard to re-produce.

Is there anything I do wrong on my side?

eliaskg avatar Dec 19 '24 12:12 eliaskg

Hey @eliaskg,

This happens sometimes, unfortunately. I haven't quite been able to figure out what's causing this, but if you'd like to make extra sure that the connection is stable, you could use something like this:

/**
 * Calls the callback function after the timeout.
 *
 * If another function callback is still running,
 * waits until it's finished before calling it again.
 */
function debouncePromise(func: () => Promise<unknown>, trailing = true) {
  let currentFunction: Promise<unknown> | null = null;
  let callCount = 0;

  return async function (this: any) {
    const context = this;
    callCount++;

    if (currentFunction) {
      return currentFunction;
    }

    const later = async function () {
      if (callCount > 0) {
        callCount = 0;
        currentFunction = func.apply(context, []);
        await currentFunction.catch(() => {});
        currentFunction = null;
        if (trailing) {
          later();
        }
      }
    };

    await later();
  };
}

ableton.on(
  "connect",
  debouncePromise(async () => {
    console.log("Got a connect event from Live, waiting for project file to finish loading...");

    // Try to send a ping to Ableton to check if it's available
    for (let i = 0; ableton.isConnected(); i++) {
      try {
        console.log("Checking if Live is actually reachable...", { try: i });

        for (let i = 0; i < 5; i++) {
          console.log("Pinging Live...", { try: i });
          await ableton.internal.get("ping");
        }

        log.info("Ableton is connected");
        break;
      } catch (e) {
        console.warn("Couldn't reach Live", { error: String(e) });
        await new Promise(res => setTimeout(res, 500));
      }
    }

	// Your actual setup code goes here
  }),
);

It's a bit complex, but I hope this helps!

leolabs avatar Dec 27 '24 21:12 leolabs

Thank you @leolabs, I will give this a try!

eliaskg avatar Jan 02 '25 12:01 eliaskg

Tried out the code you provided. Worked great for a couple of days. Today when I opened Ableton while my script was running it crashed again with the following error output:

Got a connect event from Live, waiting for project file to finish loading... Checking if Live is actually reachable... { try: 0 } Pinging Live... { try: 0 } Couldn't reach Live { error: 'Error: The command internal.get_prop({"prop":"ping"}) timed out after 2000 ms.' } Disconnect heartbeat /Users/elias/Documents/Code/Scripts/ableton-js/node_modules/ableton-js/index.js:328 rej(new TimeoutError(The command ${cls}.${command.name}(${arg}) timed out after ${timeout} ms., payload)); ^

TimeoutError: The command song.add_listener({"prop":"session_record","eventId":"5b706c82-e659-4dce-83f9-e6e9cee1de3a"}) timed out after 2000 ms. at Timeout._onTimeout (/Users/elias/Documents/Code/Scripts/ableton-js/node_modules/ableton-js/index.js:328:21) at listOnTimeout (node:internal/timers:573:17) at process.processTimers (node:internal/timers:514:7) { payload: { uuid: '9757e123-534c-4f6b-8e1e-63e0e212e414', ns: 'song', nsid: undefined, name: 'add_listener', args: { prop: 'session_record', nsid: undefined, eventId: '5b706c82-e659-4dce-83f9-e6e9cee1de3a' } } }

Seems to happen super randomly, sometimes when opening Ableton, sometimes after opening a different project.

eliaskg avatar Jan 30 '25 11:01 eliaskg

Hmm, that's strange! I wonder if this happens because Live disconnects while checking for a connection. The following might be able to fix this:

/**
 * Calls the callback function after the timeout.
 *
 * If another function callback is still running,
 * waits until it's finished before calling it again.
 */
function debouncePromise(func: () => Promise<unknown>, trailing = true) {
  let currentFunction: Promise<unknown> | null = null;
  let callCount = 0;

  return async function (this: any) {
    const context = this;
    callCount++;

    if (currentFunction) {
      return currentFunction;
    }

    const later = async function () {
      if (callCount > 0) {
        callCount = 0;
        currentFunction = func.apply(context, []);
        await currentFunction.catch(() => {});
        currentFunction = null;
        if (trailing) {
          later();
        }
      }
    };

    await later();
  };
}

ableton.on(
  "connect",
  debouncePromise(async () => {
    console.log("Got a connect event from Live, waiting for project file to finish loading...");

    // Try to send a ping to Ableton to check if it's available
    for (let i = 0; ableton.isConnected(); i++) {
      try {
        console.log("Checking if Live is actually reachable...", { try: i });

        for (let i = 0; i < 5; i++) {
          console.log("Pinging Live...", { try: i });
          await ableton.internal.get("ping");
        }

        log.info("Ableton is connected");
        break;
      } catch (e) {
        console.warn("Couldn't reach Live", { error: String(e) });
        await new Promise(res => setTimeout(res, 500));
      }
    }

    // If Ableton Live has disconnected during connection check, don't do anything
    if (!ableton.isConnected()) {
      return;
    }
 
	// Your actual setup code goes here
  }),
);

Let me know if that works for you!

leolabs avatar Feb 25 '25 14:02 leolabs