zwave-js
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
190 lines • 7.45 kB
JavaScript
import { MessagePriority, highResTimestamp, isZWaveError, } from "@zwave-js/core";
import { noop } from "@zwave-js/shared";
import { compareNumberOrString, } from "alcalzone-shared/comparable";
import { NodeStatus } from "../node/_Types.js";
/**
* Transactions are used to track and correlate messages with their responses.
*/
export class Transaction {
driver;
options;
constructor(driver, options) {
this.driver = driver;
this.options = options;
// Give the message generator a reference to this transaction
options.parts.parent = this;
// Initialize class fields
this.promise = options.promise;
this.message = options.message;
this.priority = options.priority;
this.parts = options.parts;
this.listener = options.listener;
// We need create the stack on a temporary object or the Error
// class will try to print the message
const tmp = { message: "" };
Error.captureStackTrace(tmp, Transaction);
this._stack = tmp.stack.replace(/^Error:?\s*\n/, "");
}
clone() {
const ret = new Transaction(this.driver, this.options);
for (const prop of [
"_stack",
"_progress",
"creationTimestamp",
"changeNodeStatusOnTimeout",
"pauseSendThread",
"priority",
"tag",
"requestWakeUpOnDemand",
]) {
ret[prop] = this[prop];
}
// The listener callback now lives on the clone
this.listener = undefined;
return ret;
}
/** Will be resolved/rejected by the Send Thread Machine when the entire transaction is handled */
promise;
/** The "primary" message this transaction contains, e.g. the un-encapsulated version of a SendData request */
message;
/** The message generator to create the actual messages for this transaction */
parts;
/** A callback which gets called with state updates of this transaction */
listener;
_progress;
setProgress(progress) {
// Ignore duplicate updates
if (this._progress?.state === progress.state)
return;
this._progress = progress;
this.listener?.({ ...progress });
}
/**
* Returns the current message of this transaction. This is either the currently active partial message
* or the primary message if the generator hasn't been started yet.
*/
getCurrentMessage() {
return this.parts.current ?? this.message;
}
/**
* Starts the transaction's message generator if it hasn't been started yet.
* Returns `true` when the generator was started, `false` if it was already started before.
*/
start() {
if (!this.parts.self) {
this.parts.start();
return true;
}
return false;
}
/**
* Resets this transaction's message generator
*/
reset() {
this.parts.reset();
}
async generateNextMessage(prevResult) {
if (!this.parts.self)
return;
// Get the next message from the generator
const { done, value } = await this.parts.self.next(prevResult);
if (!done)
return value;
}
/**
* Forcefully aborts the message generator by throwing the given result.
* Errors will be treated as a rejection of the transaction, everything else as success
*/
abort(result) {
if (this.parts.self) {
this.parts.self.throw(result).catch(noop);
}
else if (isZWaveError(result)) {
this.promise.reject(result);
}
else {
this.promise.resolve(result);
}
}
/** The priority of this transaction */
priority;
/** The timestamp at which the transaction was created */
creationTimestamp = highResTimestamp();
/** Whether the node status should be updated when this transaction times out */
changeNodeStatusOnTimeout = true;
/** Whether the send thread MUST be paused after this transaction was handled */
pauseSendThread = false;
/** If a Wake Up On Demand should be requested for the target node. */
requestWakeUpOnDemand = false;
/** Internal information used to identify or mark this transaction */
tag;
/** The stack trace where the transaction was created */
_stack;
get stack() {
return this._stack;
}
/** Compares two transactions in order to plan their transmission sequence */
compareTo(other) {
const compareWakeUpPriority = (_this, _other) => {
const thisNode = _this.message.tryGetNode(this.driver);
const otherNode = _other.message.tryGetNode(this.driver);
// We don't require existence of the node object
// If any transaction is not for a node, it targets the controller
// which is always awake
const thisIsAsleep = thisNode?.status === NodeStatus.Asleep;
const otherIsAsleep = otherNode?.status === NodeStatus.Asleep;
// If both nodes are asleep, the conventional order applies
// Asleep nodes always have the lowest priority
if (thisIsAsleep && !otherIsAsleep)
return 1;
if (otherIsAsleep && !thisIsAsleep)
return -1;
};
// delay messages for sleeping nodes
if (this.priority === MessagePriority.WakeUp) {
const result = compareWakeUpPriority(this, other);
if (result != undefined)
return result;
}
else if (other.priority === MessagePriority.WakeUp) {
const result = compareWakeUpPriority(other, this);
if (result != undefined)
return -result;
}
const compareNodeQueryPriority = (_this, _other) => {
const thisNode = _this.message.tryGetNode(this.driver);
const otherNode = _other.message.tryGetNode(this.driver);
if (thisNode && otherNode) {
// Both nodes exist
const thisListening = thisNode.isListening
|| thisNode.isFrequentListening;
const otherListening = otherNode.isListening
|| otherNode.isFrequentListening;
// prioritize (-1) the one node that is listening when the other is not
if (thisListening && !otherListening)
return -1;
if (!thisListening && otherListening)
return 1;
}
};
// delay NodeQuery messages for non-listening nodes
if (this.priority === MessagePriority.NodeQuery) {
const result = compareNodeQueryPriority(this, other);
if (result != undefined)
return result;
}
else if (other.priority === MessagePriority.NodeQuery) {
const result = compareNodeQueryPriority(other, this);
if (result != undefined)
return -result;
}
// by default, sort by priority
if (this.priority < other.priority)
return -1;
else if (this.priority > other.priority)
return 1;
// for equal priority, sort by the timestamp
return compareNumberOrString(other.creationTimestamp, this.creationTimestamp);
}
}
//# sourceMappingURL=Transaction.js.map