@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'56const 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']>1112const createEmptyGrid = (size: number) =>13 Array.from({ length: size }, () => Array.from({ length: size }, () => '#ffffff'))1415const machine = setup({16 types: {17 context: {} as {18 title: RequestBody['title'],19 tileSize: RequestBody['tileSize'],20 pixels: RequestBody['pixels']21 selectedColor: string2223 data?: ResponseBody24 error?: unknown25 },26 events: {} as27 | { 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 data46 } 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.selectedColor86 return newGrid87 },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})