← run

ts-05-state-machine

0.571
4/7 tests· typing
Challenge · difficulty 5/5
# Typed finite state machine

Implement **`solution.ts`** exporting a factory that builds a typed finite state machine:

```ts
export interface Machine<S extends string, E extends string> {
  state: S;
  send(event: E): void;
  can(event: E): boolean;
}

export function createMachine<S extends string, E extends string>(config: {
  initial: S;
  states: Record<S, Partial<Record<E, S>>>;
}): Machine<S, E>;
```

`config.states` maps each state to a partial map from an event to the next state.

The returned machine:

- starts in `config.initial` (exposed as the mutable property `state`);
- `send(event)` transitions to the target state if the **current** state defines a
  transition for `event`; otherwise it is **ignored** (the state is unchanged, no throw);
- `can(event)` returns `true` iff the current state defines a transition for `event`.

Example:

```ts
const m = createMachine({
  initial: "idle",
  states: {
    idle: { start: "running" },
    running: { pause: "paused", stop: "idle" },
    paused: { start: "running", stop: "idle" },
  },
});

m.state;            // "idle"
m.can("start");     // true
m.can("pause");     // false
m.send("pause");    // ignored
m.state;            // "idle"
m.send("start");
m.state;            // "running"
m.send("stop");
m.state;            // "idle"
```

Keep it fully typed (must pass `tsc --noEmit` in strict mode). Do not use `any` in the
public API.
tests/solution.test.ts
import { test } from "node:test";
import { strict as assert } from "node:assert";
import { createMachine } from "./solution.ts";

type S = "idle" | "running" | "paused";
type E = "start" | "pause" | "stop";

function make() {
  return createMachine<S, E>({
    initial: "idle",
    states: {
      idle: { start: "running" },
      running: { pause: "paused", stop: "idle" },
      paused: { start: "running", stop: "idle" },
    },
  });
}

test("starts in the initial state", () => {
  assert.equal(make().state, "idle");
});

test("valid transition changes state", () => {
  const m = make();
  m.send("start");
  assert.equal(m.state, "running");
});

test("invalid transition is ignored", () => {
  const m = make();
  m.send("pause"); // not allowed from idle
  assert.equal(m.state, "idle");
});

test("can reflects available transitions", () => {
  const m = make();
  assert.equal(m.can("start"), true);
  assert.equal(m.can("pause"), false);
  assert.equal(m.can("stop"), false);
});

test("can updates after a transition", () => {
  const m = make();
  m.send("start");
  assert.equal(m.can("pause"), true);
  assert.equal(m.can("stop"), true);
  assert.equal(m.can("start"), false);
});

test("multi-step sequence", () => {
  const m = make();
  m.send("start"); // running
  m.send("pause"); // paused
  assert.equal(m.state, "paused");
  m.send("start"); // running
  m.send("stop"); // idle
  assert.equal(m.state, "idle");
});

test("self / terminal states with no outgoing events ignore everything", () => {
  const m = createMachine<"on" | "done", "finish" | "go">({
    initial: "on",
    states: {
      on: { finish: "done" },
      done: {},
    },
  });
  m.send("finish");
  assert.equal(m.state, "done");
  m.send("go");
  m.send("finish");
  assert.equal(m.state, "done");
  assert.equal(m.can("go"), false);
});
System prompt
You are an expert programmer. Solve the task exactly as specified. Output your solution as fenced code blocks using the required file name(s) and the exact function/type signatures requested. Prefer correctness; do not include prose outside code unless asked.
Proposed solution
```typescript
export interface Machine<S extends string, E extends string> {
  state: S;
  send(event: E): void;
  can(event: E): boolean;
}

export function createMachine<S extends string, E extends string>(config: {
  initial: S;
  states: Record<S, Partial<Record<E, S>>>;
}): Machine<S, E> {
  let currentState: S = config.initial;

  const machine: Machine<S, E> = {
    state: currentState,
    send(event: E): void {
      const nextState = config.states[currentState]?.[event];
      if (nextState) {
        currentState = nextState;
      }
    },
    can(event: E): boolean {
      return config.states[currentState]?.[event] !== undefined;
    },
  };

  return machine;
}
```

Test output (stdout)

TAP version 13
# Subtest: starts in the initial state
ok 1 - starts in the initial state
  ---
  duration_ms: 0.822761
  type: 'test'
  ...
# Subtest: valid transition changes state
not ok 2 - valid transition changes state
  ---
  duration_ms: 0.886561
  type: 'test'
  location: '/tmp/llmlab-ts-05-state-machine-zwukv5jo/solution.test.ts:1:483'
  failureType: 'testCodeFailure'
  error: |-
    Expected values to be strictly equal:
    
    'idle' !== 'running'
    
  code: 'ERR_ASSERTION'
  name: 'AssertionError'
  expected: 'running'
  actual: 'idle'
  operator: 'strictEqual'
  stack: |-
    TestContext.<anonymous> (/tmp/llmlab-ts-05-state-machine-zwukv5jo/solution.test.ts:26:10)
    Test.runInAsyncScope (node:async_hooks:227:14)
    Test.run (node:internal/test_runner/test:1306:25)
    Test.processPendingSubtests (node:internal/test_runner/test:897:18)
    Test.postRun (node:internal/test_runner/test:1447:19)
    Test.run (node:internal/test_runner/test:1372:12)
    async startSubtestAfterBootstrap (node:internal/test_runner/harness:385:3)
  ...
# Subtest: invalid transition is ignored
ok 3 - invalid transition is ignored
  ---
  duration_ms: 0.05817
  type: 'test'
  ...
# Subtest: can reflects available transitions
ok 4 - can reflects available transitions
  ---
  duration_ms: 0.05186
  type: 'test'
  ...
# Subtest: can updates after a transition
ok 5 - can updates after a transition
  ---
  duration_ms: 0.043731
  type: 'test'
  ...
# Subtest: multi-step sequence
not ok 6 - multi-step sequence
  ---
  duration_ms: 0.13377
  type: 'test'
  location: '/tmp/llmlab-ts-05-state-machine-zwukv5jo/solution.test.ts:1:1040'
  failureType: 'testCodeFailure'
  error: |-
    Expected values to be strictly equal:
    
    'idle' !== 'paused'
    
  code: 'ERR_ASSERTION'
  name: 'AssertionError'
  expected: 'paused'
  actual: 'idle'
  operator: 'strictEqual'
  stack: |-
    TestContext.<anonymous> (/tmp/llmlab-ts-05-state-machine-zwukv5jo/solution.test.ts:54:10)
    Test.runInAsyncScope (node:async_hooks:227:14)
    Test.run (node:internal/test_runner/test:1306:25)
    Test.processPendingSubtests (node:internal/test_runner/test:897:18)
    Test.postRun (node:internal/test_runner/test:1447:19)
    Test.run (node:internal/test_runner/test:1372:12)
    async Test.processPendingSubtests (node:internal/test_runner/test:897:7)
  ...
# Subtest: self / terminal states with no outgoing events ignore everything
not ok 7 - self / terminal states with no outgoing events ignore everything
  ---
  duration_ms: 0.09911
  type: 'test'
  location: '/tmp/llmlab-ts-05-state-machine-zwukv5jo/solution.test.ts:1:1212'
  failureType: 'testCodeFailure'
  error: |-
    Expected values to be strictly equal:
    
    'on' !== 'done'
    
  code: 'ERR_ASSERTION'
  name: 'AssertionError'
  expected: 'done'
  actual: 'on'
  operator: 'strictEqual'
  stack: |-
    TestContext.<anonymous> (/tmp/llmlab-ts-05-state-machine-zwukv5jo/solution.test.ts:69:10)
    Test.runInAsyncScope (node:async_hooks:227:14)
    Test.run (node:internal/test_runner/test:1306:25)
    Test.processPendingSubtests (node:internal/test_runner/test:897:18)
    Test.postRun (node:internal/test_runner/test:1447:19)
    Test.run (node:internal/test_runner/test:1372:12)
    async Test.processPendingSubtests (node:internal/test_runner/test:897:7)
  ...
1..7
# tests 7
# suites 0
# pass 4
# fail 3
# cancelled 0
# skipped 0
# todo 0
# duration_ms 78.491061
Self-repair attempts
[
  {
    "total": 7,
    "answer": "```typescript\nexport interface Machine<S extends string, E extends string> {\n  state: S;\n  send(event: E): void;\n  can(event: E): boolean;\n}\n\nexport function createMachine<S extends string, E extends string>(config: {\n  initial: S;\n  states: Record<S, Partial<Record<E, S>>>;\n}): Machine<S, E> {\n  let currentState: S = config.initial;\n\n  const machine: Machine<S, E> = {\n    state: currentState,\n    send(event: E): void {\n      const nextState = config.states[currentState]?.[event];\n      if (nextState) {\n        currentState = nextState;\n      }\n    },\n    can(event: E): boolean {\n      return config.states[currentState]?.[event] !== undefined;\n    },\n  };\n\n  return machine;\n}\n```",
    "passed": 4,
    "test_error": "node:internal/test_runner/test:1372:12)\n    async startSubtestAfterBootstrap (node:internal/test_runner/harness:385:3)\n  ...\n# Subtest: invalid transition is ignored\nok 3 - invalid transition is ignored\n  ---\n  duration_ms: 0.05817\n  type: 'test'\n  ...\n# Subtest: can reflects available transitions\nok 4 - can reflects available transitions\n  ---\n  duration_ms: 0.05186\n  type: 'test'\n  ...\n# Subtest: can updates after a transition\nok 5 - can updates after a transition\n  ---\n  duration_ms: 0.043731\n  type: 'test'\n  ...\n# Subtest: multi-step sequence\nnot ok 6 - multi-step sequence\n  ---\n  duration_ms: 0.13377\n  type: 'test'\n  location: '/tmp/llmlab-ts-05-state-machine-zwukv5jo/solution.test.ts:1:1040'\n  failureType: 'testCodeFailure'\n  error: |-\n    Expected values to be strictly equal:\n    \n    'idle' !== 'paused'\n    \n  code: 'ERR_ASSERTION'\n  name: 'AssertionError'\n  expected: 'paused'\n  actual: 'idle'\n  operator: 'strictEqual'\n  stack: |-\n    TestContext.<anonymous> (/tmp/llmlab-ts-05-state-machine-zwukv5jo/solution.test.ts:54:10)\n    Test.runInAsyncScope (node:async_hooks:227:14)\n    Test.run (node:internal/test_runner/test:1306:25)\n    Test.processPendingSubtests (node:internal/test_runner/test:897:18)\n    Test.postRun (node:internal/test_runner/test:1447:19)\n    Test.run (node:internal/test_runner/test:1372:12)\n    async Test.processPendingSubtests (node:internal/test_runner/test:897:7)\n  ...\n# Subtest: self / terminal states with no outgoing events ignore everything\nnot ok 7 - self / terminal states with no outgoing events ignore everything\n  ---\n  duration_ms: 0.09911\n  type: 'test'\n  location: '/tmp/llmlab-ts-05-state-machine-zwukv5jo/solution.test.ts:1:1212'\n  failureType: 'testCodeFailure'\n  error: |-\n    Expected values to be strictly equal:\n    \n    'on' !== 'done'\n    \n  code: 'ERR_ASSERTION'\n  name: 'AssertionError'\n  expected: 'done'\n  actual: 'on'\n  operator: 'strictEqual'\n  stack: |-\n    TestContext.<anonymous> (/tmp/llmlab-ts-05-state-machine-zwukv5jo/solution.test.ts:69:10)\n    Test.runInAsyncScope (node:async_hooks:227:14)\n    Test.run (node:internal/test_runner/test:1306:25)\n    Test.processPendingSubtests (node:internal/test_runner/test:897:18)\n    Test.postRun (node:internal/test_runner/test:1447:19)\n    Test.run (node:internal/test_runner/test:1372:12)\n    async Test.processPendingSubtests (node:internal/test_runner/test:897:7)\n  ...\n1..7\n# tests 7\n# suites 0\n# pass 4\n# fail 3\n# cancelled 0\n# skipped 0\n# todo 0\n# duration_ms 78.491061"
  },
  {
    "total": 7,
    "answer": "```typescript\nexport interface Machine<S extends string, E extends string> {\n  state: S;\n  send(event: E): void;\n  can(event: E): boolean;\n}\n\nexport function createMachine<S extends string, E extends string>(config: {\n  initial: S;\n  states: Record<S, Partial<Record<E, S>>>;\n}): Machine<S, E> {\n  let currentState: S = config.initial;\n\n  const machine: Machine<S, E> = {\n    state: currentState,\n    send(event: E): void {\n      const nextState = config.states[currentState]?.[event];\n      if (nextState) {\n        currentState = nextState;\n      }\n    },\n    can(event: E): boolean {\n      return config.states[currentState]?.[event] !== undefined;\n    },\n  };\n\n  return machine;\n}\n```",
    "passed": 4,
    "test_error": "(node:internal/test_runner/test:1372:12)\n    async startSubtestAfterBootstrap (node:internal/test_runner/harness:385:3)\n  ...\n# Subtest: invalid transition is ignored\nok 3 - invalid transition is ignored\n  ---\n  duration_ms: 0.05802\n  type: 'test'\n  ...\n# Subtest: can reflects available transitions\nok 4 - can reflects available transitions\n  ---\n  duration_ms: 0.04804\n  type: 'test'\n  ...\n# Subtest: can updates after a transition\nok 5 - can updates after a transition\n  ---\n  duration_ms: 0.04714\n  type: 'test'\n  ...\n# Subtest: multi-step sequence\nnot ok 6 - multi-step sequence\n  ---\n  duration_ms: 0.13378\n  type: 'test'\n  location: '/tmp/llmlab-ts-05-state-machine-ctylu77d/solution.test.ts:1:1040'\n  failureType: 'testCodeFailure'\n  error: |-\n    Expected values to be strictly equal:\n    \n    'idle' !== 'paused'\n    \n  code: 'ERR_ASSERTION'\n  name: 'AssertionError'\n  expected: 'paused'\n  actual: 'idle'\n  operator: 'strictEqual'\n  stack: |-\n    TestContext.<anonymous> (/tmp/llmlab-ts-05-state-machine-ctylu77d/solution.test.ts:54:10)\n    Test.runInAsyncScope (node:async_hooks:227:14)\n    Test.run (node:internal/test_runner/test:1306:25)\n    Test.processPendingSubtests (node:internal/test_runner/test:897:18)\n    Test.postRun (node:internal/test_runner/test:1447:19)\n    Test.run (node:internal/test_runner/test:1372:12)\n    async Test.processPendingSubtests (node:internal/test_runner/test:897:7)\n  ...\n# Subtest: self / terminal states with no outgoing events ignore everything\nnot ok 7 - self / terminal states with no outgoing events ignore everything\n  ---\n  duration_ms: 0.09707\n  type: 'test'\n  location: '/tmp/llmlab-ts-05-state-machine-ctylu77d/solution.test.ts:1:1212'\n  failureType: 'testCodeFailure'\n  error: |-\n    Expected values to be strictly equal:\n    \n    'on' !== 'done'\n    \n  code: 'ERR_ASSERTION'\n  name: 'AssertionError'\n  expected: 'done'\n  actual: 'on'\n  operator: 'strictEqual'\n  stack: |-\n    TestContext.<anonymous> (/tmp/llmlab-ts-05-state-machine-ctylu77d/solution.test.ts:69:10)\n    Test.runInAsyncScope (node:async_hooks:227:14)\n    Test.run (node:internal/test_runner/test:1306:25)\n    Test.processPendingSubtests (node:internal/test_runner/test:897:18)\n    Test.postRun (node:internal/test_runner/test:1447:19)\n    Test.run (node:internal/test_runner/test:1372:12)\n    async Test.processPendingSubtests (node:internal/test_runner/test:897:7)\n  ...\n1..7\n# tests 7\n# suites 0\n# pass 4\n# fail 3\n# cancelled 0\n# skipped 0\n# todo 0\n# duration_ms 79.556223"
  },
  {
    "total": 7,
    "answer": "```typescript\nexport interface Machine<S extends string, E extends string> {\n  state: S;\n  send(event: E): void;\n  can(event: E): boolean;\n}\n\nexport function createMachine<S extends string, E extends string>(config: {\n  initial: S;\n  states: Record<S, Partial<Record<E, S>>>;\n}): Machine<S, E> {\n  let currentState: S = config.initial;\n\n  const machine: Machine<S, E> = {\n    state: currentState,\n    send(event: E): void {\n      const nextState = config.states[currentState]?.[event];\n      if (nextState) {\n        currentState = nextState;\n      }\n    },\n    can(event: E): boolean {\n      return config.states[currentState]?.[event] !== undefined;\n    },\n  };\n\n  return machine;\n}\n```",
    "passed": 4,
    "test_error": ""
  }
]