Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EARLY DRAFT] Create an xstate v5 gpt #4917

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions gpt/funky-count.benchmark.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
I would like to build a counting state machine that allows me to
increment and decrement the count with special behaviors at
specific thresholds.

When the count reaches three, the machine enforces a 400 ms wait before
allowing further increments; if the user attempts to increment during
this wait, the machine transitions to inverted count mode, incrementing when
decrement event is received and vice versa.

In reverse state, the user can hold the increment button for 2-seconds to reset
the behavior to normal

celebrates milestones when the count reaches 7

tests for all functionality
184 changes: 184 additions & 0 deletions gpt/issues.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { assign, createActor, setup, SimulatedClock } from 'xstate';

function celebrate(_: any) {
console.log('Celebration! Count reached 7!');
}

interface CountingContext {
count: number;
}

const initialContext: CountingContext = {
count: 0
};

type CountingEvents =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'holdIncrement' };

const countingMachine = setup({
types: {
context: {} as CountingContext,
events: {} as CountingEvents
},
actions: {
increment: assign({
count: ({ context }) => context.count + 1
}),
decrement: assign({
count: ({ context }) => context.count - 1
}),
invertIncrement: assign({
count: ({ context }) => context.count - 1
}),
invertDecrement: assign({
count: ({ context }) => context.count + 1
}),
celebrate
},
guards: {
reachedThreshold: ({ context }) => context.count === 3,
reachedMilestone: ({ context }) => context.count === 7
},
delays: {
waitDelay: 400
}
}).createMachine({
context: initialContext,
initial: 'normal',
states: {
normal: {
on: {
increment: [
{
target: 'delayed',
guard: 'reachedThreshold'
},
{
actions: 'increment',
guard: 'reachedMilestone',
internal: false,
after: {
type: 'celebrate'
}
},
{
actions: 'increment'
}
],
decrement: {
actions: 'decrement'
}
}
},
delayed: {
after: {
waitDelay: 'normal'
},
on: {
increment: {
target: 'inverted'
}
}
},
inverted: {
on: {
increment: {
actions: 'invertIncrement'
},
decrement: {
actions: 'invertDecrement'
},
holdIncrement: {
target: 'normal',
internal: false,
delay: 2000
}
}
}
}
});

// Type tests to ensure type satisfaction
import {
type ActorRefFrom,
type SnapshotFrom,
type EventFromLogic
} from 'xstate';

// Strongly-typed actor reference
type CountingActorRef = ActorRefFrom<typeof countingMachine>;
// @ts-expect-error
const invalidActorRef: CountingActorRef = { invalid: true }; // Should produce a type error
const validActorRef: CountingActorRef = createActor(countingMachine); // Should be valid

// Strongly-typed snapshot
type CountingSnapshot = SnapshotFrom<typeof countingMachine>;
// @ts-expect-error
const invalidSnapshot: CountingSnapshot = { invalid: true }; // Should produce a type error
const validSnapshot: CountingSnapshot = validActorRef.getSnapshot(); // Should be valid

// Union of all event types
type CountingEvent = EventFromLogic<typeof countingMachine>;
// @ts-expect-error
const invalidEvent: CountingEvent = { invalid: true }; // Should produce a type error
const validEvent: CountingEvent = { type: 'increment' }; // Should be valid

describe('countingMachine', () => {
it('should increment normally', () => {
const actor = createActor(countingMachine).start();
actor.send({ type: 'increment' });
expect(actor.getSnapshot().context.count).toEqual(1);
});

it('should transition to "delayed" state when count reaches 3', () => {
const actor = createActor(countingMachine).start();
actor.send({ type: 'increment' });
actor.send({ type: 'increment' });
actor.send({ type: 'increment' });
expect(actor.getSnapshot().matches('delayed')).toBeTruthy();
});

it('should transition to "inverted" state if incremented during delay', () => {
const clock = new SimulatedClock();
const actor = createActor(countingMachine, { clock }).start();
actor.send({ type: 'increment' });
actor.send({ type: 'increment' });
actor.send({ type: 'increment' });
actor.send({ type: 'increment' });
expect(actor.getSnapshot().matches('inverted')).toBeTruthy();
});

it('should increment when decrement event is received in inverted state', () => {
const actor = createActor(countingMachine).start();
actor.send({ type: 'increment' });
actor.send({ type: 'increment' });
actor.send({ type: 'increment' });
actor.send({ type: 'increment' });
actor.send({ type: 'decrement' });
expect(actor.getSnapshot().context.count).toEqual(3);
});

it('should transition back to normal state when holdIncrement is held for 2 seconds', () => {
const clock = new SimulatedClock();
const actor = createActor(countingMachine, { clock }).start();
actor.send({ type: 'increment' });
actor.send({ type: 'increment' });
actor.send({ type: 'increment' });
actor.send({ type: 'increment' });
actor.send({ type: 'holdIncrement' });
clock.increment(2000);
expect(actor.getSnapshot().matches('normal')).toBeTruthy();
});

it('should celebrate when count reaches 7', () => {
const actor = createActor(countingMachine).start();
const celebrateSpy = jest.spyOn(console, 'log').mockImplementation();
for (let i = 0; i < 7; i++) {
actor.send({ type: 'increment' });
}
expect(celebrateSpy).toHaveBeenCalledWith('Celebration! Count reached 7!');
celebrateSpy.mockRestore();
});
});
Loading
Loading