// Copyright (c) 2026, The Robot Web Tools Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
'use strict';
const { TimeoutError } = require('./errors.js');
/**
* Wait for a single message on a topic.
*
* Creates a temporary subscription, waits for the first message to arrive,
* and returns it. The temporary subscription is always cleaned up, even on
* timeout or error. The node must be spinning before calling this function.
*
* This is the rclnodejs equivalent of rclpy's `wait_for_message`.
*
* @param {function|string|object} typeClass - The ROS message type class.
* @param {Node} node - The node to create the temporary subscription on.
* @param {string} topic - The topic name to listen on.
* @param {object} [options] - Options.
* @param {number} [options.timeout] - Timeout in milliseconds. If omitted, waits indefinitely.
* @param {object} [options.qos] - QoS profile for the subscription.
* @returns {Promise<object>} - Resolves with the received message.
* @throws {Error} If timeout expires before a message arrives.
*
* @example
* node.spin();
* const msg = await waitForMessage(
* 'std_msgs/msg/String',
* node,
* '/my_topic',
* { timeout: 5000 }
* );
* console.log('Received:', msg.data);
*/
function waitForMessage(typeClass, node, topic, options = {}) {
return new Promise((resolve, reject) => {
let subscription = null;
let timer = null;
let settled = false;
const cleanup = () => {
if (timer) {
clearTimeout(timer);
timer = null;
}
if (subscription) {
try {
node.destroySubscription(subscription);
} catch {
// Subscription may already be destroyed if node is shutting down
}
subscription = null;
}
};
const settle = (err, msg) => {
if (settled) return;
settled = true;
cleanup();
if (err) {
reject(err);
} else {
resolve(msg);
}
};
try {
const subOptions = {};
if (options.qos) {
subOptions.qos = options.qos;
}
subscription = node.createSubscription(
typeClass,
topic,
subOptions,
(msg) => {
settle(null, msg);
}
);
if (options.timeout != null && options.timeout >= 0) {
timer = setTimeout(() => {
settle(
new TimeoutError(
`waitForMessage timed out after ${options.timeout}ms on topic '${topic}'`,
{ entityType: 'topic', entityName: topic }
)
);
}, options.timeout);
}
} catch (err) {
cleanup();
reject(err);
}
});
}
module.exports = waitForMessage;