Support for node status list and duration

Follow Pull request proposal - temporary node status :

I stumbled upon this interesting topic by chance.

I would have a suggestion with the following format:

type NodeStatusFill = "red" | "green" | "yellow" | "blue" | "grey";
type NodeStatusShape = "ring" | "dot";

interface NodeStatus {
  fill?: NodeStatusFill;
  shape?: NodeStatusShape;
  text?: string;
  duration?: number;
}

// Node object
status(status: string | NodeStatus | NodeStatus[]): void;

If the parameter is an object, the end of duration clears the status. And if it's an array, the end of duration displays the next status. When the last status is reached, if it contains a duration, it clears the status.

Note: in the array, objects must have a duration (except the last element of the array) otherwise the status is ignored.

1 Like

That thread generated a lot of good ideas but ultimately the details were not pinned down.

Sticking points:

  • Should these temporary statuses trigger the status node? Nick had a strong opinion of yes (no exceptions)
  • Should the status revert to previous previous_state after duration expires?
    • IMO that is an excellent opportunity to simplify showing inflight status without having to worry about maintaining or recomputing the original state and adding it to the end of an array. For example the node can be "connected" but you want to temporarily show a message of "Insert Done" or "Next event in 10 mins" or "..."
    • e.g.:
    • animated-status
    • if it does revert, do we send the "connected" status again? if yes, then there should be a means of letting the user know this is a repeat (a duplicate flag or something to let the user know it hasnt JUST happened now)
      • reason: a flow might be monitoring the status and performing setup logic on the status becoming "connected". If status was resent everytime without a means to understand if it is a duplicate, the user will have to resort to setting context values to "remember".

Regarding your suggestion about an array: IMO, it does not easily solve the original need to be able to provide temporary status info having to either maintain a copy of the current state or re-compute the current state and add it to the end of the array.


Inner thoughts

I have deliberately hidden this as I am not confident I have covered all bases and there is implementation detail too - so not ideal for spit balling! I was very close to deleting it all because it will probably muddy waters. However, in the end, I decided to just leave it here, hidden, in case it spurs thoughts or provides an angle not considered, or ends up forming part of the final solution.

Feel free to ignore.


if I were asked to implement something today, and was given free reign, this is what comes to my mind...

  • As today, any call to status with the well established value of string | NodeStatus would be considered sticky and should emit the status :high_voltage:
  • If the status includes a duration property, it should update the status text and emit the new status⚔After the duration expires, it should revert back to the sticky state and also emit the reverted status :high_voltage:.
  • If the status includes a duration and noEmit: true prop, do NOT emit the status :prohibited:. After the duration has expired, it should revert back to the sticky status and NOT (re)emit the sticky status :prohibited:
order of events emits
node.status({ text: "online" }) :high_voltage:
node.status({ text: "processing", duration: 3000 }) :high_voltage:
(core) node.statusRevert() :high_voltage:(with duplicate flag or similar)
node.status({ text: "processing", duration: 3000, noEmit: true }) :prohibited:
(core) node.statusRevert() :prohibited:

Any temporary status send during a temporary status should replace the current temporary status and follow the above rules.

Benefits.

  1. Clear and (i think) simple rules
  2. Node developer can send a status, at any time, without ever having to keep track of existing sticky status via some global current_state object or have to recompute the current state and send our another status containing the current_state as we do today
  3. Can provide temporary statuses without emitting and therefore not affecting users init code (e.g. the noEmit flag would prevent duplicate "online" type status re-emitting)
  4. Can provide temporary statuses with emitting (so that user can consume the temporary status) however they can still ignore the reverted emit by inspecting the duplicate flag

Parts I dont like

  • the necessary evil magic flags required to permit the user to interpret the status messages and thus avoid re-running initialisation logic triggered by a specific state
    • One way of potentially solving this without flags in the status messages: the status node could have an option of "fixed only" (default / as is today), "in-flight only", "all". This might simplify matters and remove the need for any flags since the user would be able to ignore temporary statuses. Likewise, if they do only want to watch for inflight events, they could set it to "in-flight only"
  • Everything I have write in this hidden section
  • The fact I am actually gonna post this noise :face_with_peeking_eye:

ok, I am talking myself out of this altogether - there are lots of considerations need to be made :yikes:

I’m not in favor of using node.statusRevert() because it can make code maintenance more difficult. When reading the code, you’re forced to mentally simulate its execution just to understand the status changes, since the final state isn't explicitly written out. This adds unnecessary cognitive load. It's much clearer when the node's status is explicitly defined at each step, allowing readers to understand the current state without having to debug or trace the flow of execution.

For the same reason, I prefer that the status is simply cleared after a certain duration, and Im not in favor of the status change based on the duration. If the node developer wants to display a different status, they should explicitly call the status method with the new state. This approach keeps the status flow transparent and easier to follow.

I understand your take but disagree. The status reversion (in theory) simplifies DX by allowing the author to fire-and-forget inflight temporary status without having to programmatically maintain actual prior state or recompute it and add it to the end of an array.

Consider a plugin or a bit of code that does not have access to the internals of your code (and therefore has no idea what current state is) it would not be able to do the status reversion. Ok, that is a somewhat contrived example (it was off the top of my head) but I think some kind of auto revert is better because it removes the need to maintain or recompute the original state.

That said, an alternative might be to have something like node.getState() so that the suggested array method could be used as follows: node.status([{text: 'my temporary inflight status', duration 3000}, node.getState()]) // revert to existing state

Or, a callback in the status object that forces the developer to provide the fallback e.g.

const inflightTempStatus = {
  "my temp status",
  duration: 3000,
  onComplete: () => {
    node.status(getCurrentStatus())    
  }
}
node.status(inflightTempStatus)

The original thread was created to simplify transient messages without affecting the "status-quo" (hence the "temporary" bit). What you are saying here is kinda already possible if you think about it. e.g:

node.status("temp message")
await sleep(3000)
node.status("")

Because I haven't taken it into account - by default the behavior should be clear but we can add a retain or restore property that will restore the initial status. e.g. retain in one element or restore in the last element. (first status is the initial status)

Again, that does not consider my (somewhat contrived) situation:

And as I said to Allan:

Lastly, we are now getting into the weeds again without fully clarifying things like should temporary status emit etc.

Okay, I see what you mean, I'll think about it - I have to finish wiring an electrical panel :sweat_smile:

Thinking about it, it's better to use a default duration if not set (for elements before the last)

I'll apply a bit of Socratic strategy here and let you answer this question for yourself as you compare the two examples. Try reasoning through it on your own.

Could you tell us which code is less cognitive challenging to determine the current status of a node in a given line?

node.status({ fill: "blue", shape: "dot", text: "Waiting for input..." });

node.on("input", async (msg) => {
    node.status({ fill: "yellow", shape: "dot", text: "Validating..." });

    try {
        const isValid = await validate(msg.payload);
        if (!isValid) {
            node.status({ fill: "red", shape: "ring", text: "Invalid input" });
            setTimeout(() => {
                node.statusRevert(); // ← What will it revert to here?
            }, 3000);
            return;
        }

        node.status({ fill: "green", shape: "dot", text: "Processing..." });

        const result = await process(msg.payload);
        node.send({ payload: result });

        node.statusRevert(); // ← What will the status revert to now?
    } catch (err) {
        node.status({ fill: "red", shape: "ring", text: "Error occurred" });
        setTimeout(() => {
            node.statusRevert(); // ← Multiple places reverting... revert to what?
        }, 5000);
    }
});

This example creates several cognitive challenges:

  • You need to trace back what the original status was.
  • The meaning of each statusRevert() depends on where and when it’s called. It is necessary to ACCUMULATE context in your memory about the previous lines of code to be able to determine the status the node will be reverted to.
  • The mental model of status flow becomes increasingly hard to maintain as the code grows.
const DEFAULT_STATUS = { fill: "blue", shape: "dot", text: "Waiting for input..." };
node.status(DEFAULT_STATUS);

node.on("input", async (msg) => {
    node.status({ fill: "yellow", shape: "dot", text: "Validating..." });

    try {
        const isValid = await validate(msg.payload);
        if (!isValid) {
            node.status({ fill: "red", shape: "ring", text: "Invalid input" });
            setTimeout(() => {
                node.status(DEFAULT_STATUS); // Explicit and clear
            }, 3000);
            return;
        }

        node.status({ fill: "green", shape: "dot", text: "Processing..." });

        const result = await process(msg.payload);
        node.send({ payload: result });

        node.status(DEFAULT_STATUS); // No ambiguity
    } catch (err) {
        node.status({ fill: "red", shape: "ring", text: "Error occurred" });
        setTimeout(() => {
            node.status(DEFAULT_STATUS); // You always know what it becomes
        }, 5000);
    }
});

Benefits:

  • No guesswork — the final status is always visible in the code.
  • Easier to debug and maintain.
  • Safe with concurrent inputs.
  • Clear separation of concerns: status is treated as intentional state, not a side-effect to be "undone."

Please, if you have counter arguments, quote the argument as well. You quoted only the statement of my previous comment. This makes it harder for people to interpret and review the discussion.

Each status that doesn't contain either retain or duration is retained as the status to restore, as well as retain=true.

When duration is set, the default behavior is clear, and if restore is set, the behavior is now to restore the initial status. It may be a bit tedious, but it's the only way I see to avoid breaking the existing status. Like Allan, I don't really like the statusRevert method.

I don't know if clear as default behavior is the best solution because it depends on the usage. e.g. mqtt node will prefer to restore the initial status while function node will clear the current status.

1 Like

Not sure I've completely understood this thread so if this is not relevant, please ignore.

My thought - and hopefully one that maintains the best possible backwards compatibility - would be to add a new status type. That way, the original status would continue to act just as it does now. And so would the status node.

Then you could have a completely separate "live status". You could extend the status node to be triggered on these or ignore them. So avoiding all of the issues regarding rolling back to a previous state - you wouldn't need to. But, a flow using a status node could easily be extended to deal with temporary status changes if you wanted to. But you wouldn't be forced to.

For custom nodes, the issuing of a "live status" would overwrite any existing actual status and, on timeout, would simply revert. There would not be any need to re-issue the old status (now current again) because the status node understands they are different and your flow handles it, not the node.

As I say, this feels that it should be 100% backwards compatible with minimal changes needed to core. It also keeps the 2 concepts somewhat separated which, in my weird brain at least, seems easier to comprehend.

So the workflow would be:

  1. Trigger a standard, static status if you want - using the current functions.
  2. On receipt of a suitable event or msg, trigger a "live status" using a new RED function to be created in core. That fn will have access to the current non-live status automatically via reference to the node data, see the next para for why that is important. When the live status times out, all it has to do is restore the visual to the current non-live status (if set), nothing else.

If your flow updates the standard status while the live status is active, then the current status function would still be triggered and any connected status node would also be triggered, however, the display would not update (that might need to be an option). But because the new live status function "knows" the current non-live status setting, as the timeout expires, it will automatically set the display to the correct status.

Not sure if any of that makes sense, it has been a very long week for me and my brain is a little burned out.

Backward compatibility will be respected.

The request concerns:

  • duration for the status
  • the ability to provide a list of statuses to display with a duration for each
  • restore a previous status

If a status has no duration and/or has retain=true, it will be saved. When a status has restore=true (and therefore a duration), at the end of this duration, the status will be replaced by the saved one. If there is no restore, the status will be clear. As I mentioned just above, perhaps the most useful behavior would be to reverse it: by default, restore the saved status, and otherwise, if clear=true, clear the status.

↑ quoted code will be addressed below in "Clarification on the assumptions"

↑ I will address these at the end

Clarification on the assumptions

Your example code wrongly assumed some things from my hidden proposal (in which I stated something along the lines of not being fully fleshed out )

But just in case this ever comes to a conclusion I should clarify I suppose. The idea of the status reversion is that it is simple to use and works the same for everyone. It is not an implementation detail but rather a feature implemented by the core. In its implementation (as stated) it would take care of maintaining the sticky state (even if that is updated while a temporary status is in-flight - i.e. there is no contention)

Your code example of my hidden proposal (updated)

This is an update to the code you assumed would look like - its been fleshed out a little more to represent a rather basic node with a config that updates connection status

configNode.on('connect', () => {
    node.status({ fill: "green", shape: "dot", text: "connected" })
})
configNode.on('disconnect', () => {
    node.status({ fill: "gray", shape: "dot", text: "disconnected" })
})
configNode.on('error', (err) => {
    node.status({ fill: "red", shape: "ring", text: "error: " + err.message })
})

/**
 * Helper function for clean code purposes only.
 * Calls node.status with proposed duration & noEmit flags which instruct core to not emit
 * the status change and to revert to the last known status after the specified duration.
 */
const tempStatus = (text, shape="ring", fill="yellow", duration=3000, emit=false) => node.status({ fill, shape, text, timeout: duration, emit })

node.on("input", async (msg) => {
    if (!configNode.isConnected) {
        return
    }
    tempStatus("Processing input...") // yellow ring, 3s, by default

    try {
        const isValid = await validate(msg.payload)
        if (!isValid) {
            throw new Error("Invalid input")
        }
        const result = await process(msg.payload)
        tempStatus("Processing successful", "dot", "green") // green dot, 3s, noEmit
        node.send({ payload: result })
    } catch (err) {
        node.error("Processing failed", msg) // catchable error (msg.error will contain the details)
        tempStatus("Processing failed", "dot", "red", 5000) // show error for 5 seconds
    }
})
Benefits
  • Better DX
  • Not an implementation pattern that is open to interpretation
  • Not litter with timers or global status memories
  • Flags on the status object provide clear intent

Note there are no calls to this statusRevert it would be handled by the core code (as implied in the (core) text in the table)


I have a few critiques of this but I will stick with 2.

  1. There is nothing in your code example that requires any changes to core as of today. What you are proposing is an pattern that I, you or anyone could (and probably does) do this today - but that was not the requirement of the original OP.
  2. Because it is an pattern - it is open to interpretation and all developers follow these to the letter and never make mistakes /s

Taking the approach you demonstrated above, to achieve a temporary status (like the original OP intended) for the scenario "simple node with config" I demonstrated above, your pattern of reverting the status to its last known sticky state is a lot trickier...

let STICKY_STATUS = null; // global status variable will contain "something"
let timer1
let timer2
configNode.on('connect', () => {
    STICKY_STATUS = { fill: "green", shape: "dot", text: "connected" };
    node.status(STICKY_STATUS);
})
configNode.on('disconnect', () => {
    STICKY_STATUS = { fill: "gray", shape: "dot", text: "disconnected" };
    node.status(STICKY_STATUS);
})
configNode.on('error', (err) => {
    STICKY_STATUS = { fill: "red", shape: "ring", text: "error: " + err.message };
    node.status(STICKY_STATUS);
})

node.on("input", async (msg) => {
    node.status({ fill: "yellow", shape: "dot", text: "Validating..." });

    try {
        const isValid = await validate(msg.payload);
        if (!isValid) {
            node.status({ fill: "red", shape: "ring", text: "Invalid input" });
            timer1 = setTimeout(() => {
                node.status(STICKY_STATUS); // change status back to the last known state from THIS code's POV
            }, 3000);
            return;
        }

        node.status({ fill: "green", shape: "dot", text: "Processing..." });

        const result = await process(msg.payload);
        node.send({ payload: result });

        node.status(STICKY_STATUS); // change status back to the last known state from THIS code's POV
    } catch (err) {
        node.status({ fill: "red", shape: "ring", text: "Error occurred" });
        clearTimeout(timer1); // clear previous timer if it exists
        timer2 = setTimeout(() => {
            node.status(STICKY_STATUS); // change status back to the last known state from THIS code's POV
        }, 5000);
    }
});

node.on("close", () => {
    // prevent any pending status changes when the node is closed
    clearTimeout(timer1);
    clearTimeout(timer2);
});

Now lets compare the two approaches:

  1. First Approach:
    • Lines of code: 28
    • The flags are explicit - meaning it is clear these are temporary status changes.
    • Inline timers: None (the core handles reverting status automatically).
  2. Second Approach:
    • Lines of code: 42
    • Implementation relies on a global variable to maintain the last known state.
      • requires each developer to implement this pattern in their code.
      • requires each developer to remember to revert the status back to the last known state.
    • The status is reverted to the last known state after a delay
      • this may or may not be desired (core has no say here, will blindly revert to stored status unless extra logic is implemented)
      • requires additional logic to handle timer cancellations for successive inputs (concurrency protection) and for clean up to avoid stray timers (node close event)
    • Inline timers: required to revert the status after a delay

Also, I have not commented on how a status change coming from another place (like a hook in your node or an event from a config - IDK - I am just adding flavour at this point to try and iluminate why core implementation is preferable). With a core implementation, the core would take care or status reversion, timer cancelations, clean up etc.

So to address the benefits you state:

Agreed. But as state above, down to each developer and their implementation.

Disagree. setTimeout is akin to "hit and hope". There are so many edge cases jumping of the screen especially related to fast successive messages and stale state.

Disagree. See above point

Disagree. It is not a side effect if it is by design.


And lastly to address the stated "cognitive challenges:"

No, you dont. This is an assumption. My intent (like the original OP) was it would be a core feature.

Again, an assumption. The core implementation would (should) take care of this for users of the API in the same consistent manor for all callees.




Allan, I hope you can agree, responding in this manner, when assumptions easily made, is exhausting! I am more than happy to discuss implementation details when a clear proposal is laid down.

In future, unless responses are first clarified (to avoid any assumptions) by way of first asking for clarification, I will not be replying to large bodies of text like this again.

Sorry. I do love your enthusiasm and detail and all, but this is exhausting mate.


Final disclaimer - even though I have gone through this and (in my head) I feel there is a solution to achieve the original OP, I am not 100% confident I have covered all edge cases nor have I considered every node~contrib~pligin interaction. And in the end, this post and my hidden proposal that spurned this, could just end up being utter nonsense and noise (because I missed a critical detail or Nick says no for some valid reason) - this is why we should be pinning down the proposal not the implementation details.

I'm uncertain about the usefulness of the restore and retain feature values. I’m not comfortable with the idea of the core automatically changing the status without an explicit, imperative call, to something different than clearing it. This implicit behavior makes the code harder to reason about — a developer would have to scroll back to where the status was originally set in order to understand why it was retained or restored to a particular value.

That said, I do agree that the initial implementation of the duration feature should simply clear the status after a specified number of milliseconds. If node.status() is called again before the duration expires, the previous timer should be cancelled, and a new one should start only if the new call also includes a duration. This ensures that status updates remain predictable, avoid overlap or race conditions, and are easier to debug and reason about.

I'm sorry, but I think you did not understand the main point of the argument. The second example demonstrates that I don't need to read previous lines of code to determine the new status. While in the first example, which used node.revertStatus(), I must read previous lines to determine which status it will be reverted to. And because I need to accumulate information in my brain about a previous context, it can become hard to debug status changes. It is not "assumption", it is logical reasoning. When a piece of code is more cognitive challenging to interpret, its maintenance could become harder for people who did not participated in the development.

If we don't find an agreement, it will be ignored.

But I must admit that in my nodes I need to store the status to restore it later, so Steve's request corresponds to a use I have.

retain is optional; it allows a temporary status to be the new saved status.

1 Like

But I do. Let me paraphrase you to show that do.
You are saying that by being explicit on the setting of status that at "read time" (as in human reading the code) it will be easier for them to understand when and why the status changes.
Thus the benefit in debugging.

However, you are clearly missing my points about 1) simplified DX, 2) actually achieving the feature request and 3) doing it in a controlled way in the core so that is is not left open to dev implementation and all the edge cases like race conditions, dangling timers firing after node close, all the mistakes EVERYONE makes are avoided.

The fact there is some "magic" goes on in the core to achieve the OP in my rough outline is besides the point. Even if it makes the "read time" dev have to head off to RTFM to understand "why", the benefits outweigh simply the negatives IMO. Lets face it, there is magic everywhere and when a codebase gets so large that your brain cannot parse it in one screenful, you end up implementing the same pattern over and over and mistakes (and deviations) occur.

↑ Exactly this ↑

This kind of to & fro is what left the original post to rot.

For 3, I understand the concern of not trusting on node authors to properly destroy/reset timers. When developers forget about doing it, several issues can happen, such as memory leaks, race conditions, resource waste or undesired side effects. However I believe this type of issue should be solved differently, like using no-unused-vars with eslint/biome. If EVERYONE is making this issue we need to address the root cause and not place it under the carpet and pretend it doesnt exist. [My analogy was dumb so i removed it]

I think 2 is a more of a solution to solve what you said in 3 than actually a solution that provides a better DX. node.revertStatus() will lead to more cogntive challenging code, like I explained above. If it is more challenging to interpret I don't agree it improves DX. Maybe it facilitates the life of who is writting the code at the moment he/she is writting it because this person will have the context in his brain, but it makes it harder for people who are going to maintain it later.

[removed this third paragraph because I realized it was unecessary]

Must admit my current 2 cents worth is to agree with @totallytinformation way back above. The "temporary status" to me feels like maybe it ought to be a completely different call that just so happens to overlay the screen space of the status, and always reverts once timed out. By having a separate call you know that it is always going to be temporary, and it can explicitly NOT trigger the status node, and does not start overloading the existing status api with loads of possible flags, timeouts, etc.

I do also agree somewhat but must admit to carrying baggage from the original op (i.e. all status changes should trigger status node - so worked that into the proposal)

If we massage the language and instead call it a "temporary status overlay" and its purely for visual purposes, then that should satisfy the OP.

In a way, it is not too far off my gimmicky gif:


tho, to throw a virtual spanner in the works, the devil in me is hearing the future cries of "how can I this read this temporary status value"

Re spanner. Yes as I said in the original thread, how long is temporary ? If it’s too short I may miss it. If I’m on another tab and miss it does it matter ? If a tree falls and no- one is around, does it make a sound ? Etc… My take is that these are purely informational in the moment, so shouldn’t affect (real) status.