Skip to content

Commit

Permalink
refactor: migrate node status machine to custom FSM
Browse files Browse the repository at this point in the history
  • Loading branch information
AlCalzone committed Dec 4, 2024
1 parent 5626aea commit 243957c
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 180 deletions.
11 changes: 6 additions & 5 deletions packages/core/src/fsm/FSM.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface StateMachineState {
}

export interface StateMachineInput {
input: number | string;
value: number | string;
}

export type StateMachineTransitionMap<
Expand All @@ -24,10 +24,11 @@ export type StateMachineTransitionMap<
state: State,
) => (input: Input) => StateMachineTransition<State, Effect> | undefined;

export type StateMachineTransitions<T extends StateMachine<any, any, any>> =
T extends StateMachine<infer S, infer I, infer E>
? StateMachineTransitionMap<S, I, E>
: never;
export type InferStateMachineTransitions<
T extends StateMachine<any, any, any>,
> = T extends StateMachine<infer S, infer I, infer E>
? StateMachineTransitionMap<S, I, E>
: never;

export class StateMachine<
State extends StateMachineState,
Expand Down
9 changes: 3 additions & 6 deletions packages/zwave-js/src/lib/node/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,9 +313,6 @@ export class ZWaveNode extends ZWaveNodeMixins implements QuerySecurityClasses {
* Cleans up all resources used by this node
*/
public destroy(): void {
// Stop all state machines
this.statusMachine.stop();

// Remove all timeouts
for (
const timeout of [
Expand Down Expand Up @@ -989,7 +986,7 @@ export class ZWaveNode extends ZWaveNodeMixins implements QuerySecurityClasses {

// Restart all state machines
this.restartReadyMachine();
this.statusMachine.restart();
this.restartStatusMachine();

// Remove queued polls that would interfere with the interview
this.cancelAllScheduledPolls();
Expand Down Expand Up @@ -1114,7 +1111,7 @@ export class ZWaveNode extends ZWaveNodeMixins implements QuerySecurityClasses {
this.cachedDeviceConfigHash = await this._deviceConfig?.getHash();

this.setInterviewStage(InterviewStage.Complete);
this.updateReadyMachine({ input: "INTERVIEW_DONE" });
this.updateReadyMachine({ value: "INTERVIEW_DONE" });

// Tell listeners that the interview is completed
// The driver will then send this node to sleep
Expand Down Expand Up @@ -4923,7 +4920,7 @@ protocol version: ${this.protocolVersion}`;

// Mark already-interviewed nodes as potentially ready
if (this.interviewStage === InterviewStage.Complete) {
this.updateReadyMachine({ input: "RESTART_FROM_CACHE" });
this.updateReadyMachine({ value: "RESTART_FROM_CACHE" });
}
}

Expand Down
10 changes: 5 additions & 5 deletions packages/zwave-js/src/lib/node/NodeReadyMachine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,25 @@ test(`The node should start in the notReady state`, (t) => {
test("when the driver is restarted from cache, the node should be ready as soon as it is known NOT to be dead", (t) => {
const machine = createNodeReadyMachine();

machine.transition(machine.next({ input: "RESTART_FROM_CACHE" })?.newState);
machine.transition(machine.next({ value: "RESTART_FROM_CACHE" })?.newState);
t.expect(machine.state.value).toBe("readyIfNotDead");

machine.transition(machine.next({ input: "NOT_DEAD" })?.newState);
machine.transition(machine.next({ value: "NOT_DEAD" })?.newState);
t.expect(machine.state.value).toBe("ready");
});

test("when the driver is restarted from cache and the node is known to be not dead, it should be ready immediately", (t) => {
const machine = createNodeReadyMachine();

machine.transition(machine.next({ input: "NOT_DEAD" })?.newState);
machine.transition(machine.next({ input: "RESTART_FROM_CACHE" })?.newState);
machine.transition(machine.next({ value: "NOT_DEAD" })?.newState);
machine.transition(machine.next({ value: "RESTART_FROM_CACHE" })?.newState);

t.expect(machine.state.value).toBe("ready");
});

test("when the interview is done, the node should be marked as ready", (t) => {
const machine = createNodeReadyMachine();

machine.transition(machine.next({ input: "INTERVIEW_DONE" })?.newState);
machine.transition(machine.next({ value: "INTERVIEW_DONE" })?.newState);
t.expect(machine.state.value).toBe("ready");
});
13 changes: 5 additions & 8 deletions packages/zwave-js/src/lib/node/NodeReadyMachine.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
type InferStateMachineTransitions,
StateMachine,
type StateMachineTransition,
type StateMachineTransitions,
} from "@zwave-js/core";

export type NodeReadyState = {
Expand All @@ -15,7 +15,7 @@ export type NodeReadyState = {
};

export type NodeReadyMachineInput = {
input: "NOT_DEAD" | "MAYBE_DEAD" | "RESTART_FROM_CACHE" | "INTERVIEW_DONE";
value: "NOT_DEAD" | "MAYBE_DEAD" | "RESTART_FROM_CACHE" | "INTERVIEW_DONE";
};

export type NodeReadyMachine = StateMachine<
Expand All @@ -35,11 +35,11 @@ export function createNodeReadyMachine(): NodeReadyMachine {

const READY: NodeReadyState = { value: "ready", done: true };

const transitions: StateMachineTransitions<NodeReadyMachine> =
const transitions: InferStateMachineTransitions<NodeReadyMachine> =
(state) => (input) => {
switch (state.value) {
case "notReady": {
switch (input.input) {
switch (input.value) {
case "NOT_DEAD":
return to({ ...state, maybeDead: false });
case "MAYBE_DEAD":
Expand All @@ -56,10 +56,7 @@ export function createNodeReadyMachine(): NodeReadyMachine {
break;
}
case "readyIfNotDead": {
switch (input.input) {
case "NOT_DEAD":
return to(READY);
}
if (input.value === "NOT_DEAD") return to(READY);
break;
}
}
Expand Down
63 changes: 20 additions & 43 deletions packages/zwave-js/src/lib/node/NodeStatusMachine.test.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,29 @@
import { type Interpreter, interpret } from "xstate";
// import { SimulatedClock } from "xstate/lib/SimulatedClock";
import { type TaskContext, type TestContext, test } from "vitest";
import { test } from "vitest";
import {
type NodeStatusEvent,
type NodeStatusMachine,
type NodeStatusStateSchema,
type NodeStatusMachineInput,
type NodeStatusState,
createNodeStatusMachine,
} from "./NodeStatusMachine.js";

const testNodeNonSleeping = { canSleep: false } as any;
const testNodeSleeping = { canSleep: true } as any;

function startMachine(
t: TaskContext & TestContext,
machine: NodeStatusMachine,
): Interpreter<any, NodeStatusStateSchema, NodeStatusEvent, any> {
const service = interpret(machine).start();
t.onTestFinished(() => {
service.stop();
});
return service;
}

test(`The node should start in the unknown state if it maybe cannot sleep`, (t) => {
const testMachine = createNodeStatusMachine({
canSleep: false,
} as any);
const machine = createNodeStatusMachine(testNodeNonSleeping);

const service = startMachine(t, testMachine);
t.expect(service.getSnapshot().value).toBe("unknown");
t.expect(machine.state.value).toBe("unknown");
});

test(`The node should start in the unknown state if it can definitely sleep`, (t) => {
const testMachine = createNodeStatusMachine({
canSleep: true,
} as any);
const machine = createNodeStatusMachine(testNodeSleeping);

const service = startMachine(t, testMachine);
t.expect(service.getSnapshot().value).toBe("unknown");
t.expect(machine.state.value).toBe("unknown");
});

const transitions: {
start: keyof NodeStatusStateSchema["states"];
event: NodeStatusEvent["type"];
target: keyof NodeStatusStateSchema["states"];
start: NodeStatusState["value"];
event: NodeStatusMachineInput["value"];
target: NodeStatusState["value"];
canSleep?: boolean;
}[] = [
{
Expand Down Expand Up @@ -181,27 +161,24 @@ for (const testCase of transitions) {
? testNodeSleeping
: testNodeNonSleeping;

const testMachine = createNodeStatusMachine(testNode);
testMachine.initial = testCase.start;
const machine = createNodeStatusMachine(testNode);
machine["_state"] = { value: testCase.start };

const service = startMachine(t, testMachine);
service.send(testCase.event);
t.expect(service.getSnapshot().value).toBe(testCase.target);
machine.transition(machine.next({ value: testCase.event })?.newState);
t.expect(machine.state.value).toBe(testCase.target);
});
}

test("A transition from unknown to awake should not happen if the node cannot sleep", (t) => {
const testMachine = createNodeStatusMachine(testNodeNonSleeping);
const machine = createNodeStatusMachine(testNodeNonSleeping);

const service = startMachine(t, testMachine);
service.send("AWAKE");
t.expect(service.getSnapshot().value).toBe("unknown");
machine.transition(machine.next({ value: "AWAKE" })?.newState);
t.expect(machine.state.value).toBe("unknown");
});

test("A transition from unknown to asleep should not happen if the node cannot sleep", (t) => {
const testMachine = createNodeStatusMachine(testNodeNonSleeping);
const machine = createNodeStatusMachine(testNodeNonSleeping);

const service = startMachine(t, testMachine);
service.send("ASLEEP");
t.expect(service.getSnapshot().value).toBe("unknown");
machine.transition(machine.next({ value: "ASLEEP" })?.newState);
t.expect(machine.state.value).toBe("unknown");
});
Loading

0 comments on commit 243957c

Please sign in to comment.