Sign in
Sign up

    Live Loading

    Global Posts

    View on GitHub
XState fetcher

@shimehituzi - 1w

machine.ts

1import { assign, fromPromise, setup } from 'xstate'
2import createClient from 'openapi-fetch'
3import type { paths } from './schema.d.ts'
4import type { ErrorResponseJSON, RequestBodyJSON, SuccessResponseJSON } from 'openapi-typescript-helpers'
5
6const client = createClient<paths>({ baseUrl: 'http://localhost:8000' })
7type RequestBody = RequestBodyJSON<paths['/dotimages']['post']>
8type ResponseBody =
9 | SuccessResponseJSON<paths['/dotimages']['post']>
10 | ErrorResponseJSON<paths['/dotimages']['post']>
11
12const createEmptyGrid = (size: number) =>
13 Array.from({ length: size }, () => Array.from({ length: size }, () => '#ffffff'))
14
15const machine = setup({
16 types: {
17 context: {} as {
18 title: RequestBody['title'],
19 tileSize: RequestBody['tileSize'],
20 pixels: RequestBody['pixels']
21 selectedColor: string
22
23 data?: ResponseBody
24 error?: unknown
25 },
26 events: {} as
27 | { type: 'updateTitle'; value: string }
28 | { type: 'updateTileSize'; value: RequestBody['tileSize'] }
29 | { type: 'updateSelectedColor'; value: string }
30 | { type: 'clickCell'; row: number; col: number }
31 | { type: 'save' }
32 | { type: 'cancel' }
33 | { type: 'retry' },
34 },
35 actors: {
36 postImage: fromPromise<ResponseBody, RequestBody>(async ({ input }) => {
37 const { data, error, response } = await client.POST('/dotimages', {
38 body: {
39 title: input.title,
40 tileSize: input.tileSize,
41 pixels: input.pixels,
42 },
43 })
44 if (response.ok && data !== undefined) {
45 return data
46 } else {
47 return { error: error?.error || 'Unexpected Error' }
48 }
49 }),
50 },
51}).createMachine({
52 id: 'dotImageApp',
53 initial: 'idle',
54 context: {
55 title: '',
56 tileSize: 8,
57 selectedColor: 'rgba(0,0,0,1)',
58 pixels: createEmptyGrid(8),
59 data: undefined,
60 error: undefined,
61 },
62 states: {
63 idle: {
64 on: {
65 updateTitle: {
66 actions: assign({
67 title: ({event}) => event.value,
68 }),
69 },
70 updateTileSize: {
71 actions: assign({
72 tileSize: ({event}) => event.value,
73 pixels: ({event}) => createEmptyGrid(event.value),
74 }),
75 },
76 updateSelectedColor: {
77 actions: assign({
78 selectedColor: ({event}) => event.value,
79 }),
80 },
81 clickCell: {
82 actions: assign({
83 pixels: ({context, event}) => {
84 const newGrid = context.pixels.map((row) => [...row])
85 newGrid[event.row][event.col] = context.selectedColor
86 return newGrid
87 },
88 }),
89 },
90 save: {
91 guard: ({context}) => context.title.trim().length > 0,
92 target: 'saving',
93 },
94 },
95 },
96 saving: {
97 invoke: {
98 src: 'postImage',
99 input: ({ context }) => ({
100 ...context,
101 title: context.title,
102 tileSize: context.tileSize,
103 pixels: context.pixels,
104 }),
105 onDone: {
106 target: 'success',
107 actions: assign({
108 data: ({event}) => event.output,
109 }),
110 },
111 onError: {
112 target: 'failure',
113 actions: assign({
114 error: ({event}) => event.error,
115 }),
116 },
117 },
118 on: {
119 cancel: { target: 'idle' },
120 },
121 },
122 success: {
123 after: {
124 2000: { target: 'idle' },
125 },
126 on: {
127 retry: { target: 'saving' },
128 },
129 },
130 failure: {
131 after: {
132 2000: { target: 'idle' },
133 },
134 on: {
135 retry: { target: 'saving' },
136 },
137 },
138 },
139})
Comment