Effect CommunityEC
Effect Community7mo ago
2 replies
gambaru

Handling state transitions in a state machine with asynchronous operations can indeed be challeng...

I have a state machine described as a discriminated union of pure data classes. The transition functions are embedded on the data it can transition from.

export type UploadState =
  | UploadUploading
  | UploadCompleted
  | UploadError
  | UploadCancelled

export class UploadUploading extends Data.TaggedClass('UploadUploading')<
  UploadStateContext & {
    onComplete(file: UploadFile): UploadCompleted
    onProgress(progress: Option.Option<number>): UploadUploading
  }
> {}


the state of the uploading file is stored in zustand for components to react to. To invoke the onComplete or onProgress I have an async promise in the zustand store with a callback for progress

const progressCallback = (progress: Option.Option<number>) => {
  store.setState(
    create((draft) => {
      const upload = draft.uploadsByFileId[id]
      invariant(isUploading(upload))
      draft.uploadsByFileId[id] = upload.onProgress(progress)
    })
  )
}
const response = yield* api.uploadFile(localFile, progressCallback)


This is where the problem is, if I want to do any transition on the machine from outside (like when I need to do an async request), I have to check that the state is in the right type that allows that transition. Here I'm doing it with invariant to narrow the machine state type, because I know it should not be possible and anything else I tried was kind of ugly and overcomplicated.

If I just chained the transitions, TS would be able to infer that I'm already in the right state

const machineResult = uploadingState.onProgress(0.2).onProgress(0.8).onComplete(serverFile)


But the onProgress and onComplete need to be invoked asynchronously. Of course I could go for the reducer event-driven pattern but I don't like that the events are essentially just ignored if the machine is not in an allowed state to transition from for that event. I'm not sure if fixing this requires dependent types. Maybe it can be done with some sort of monadic type?
Was this page helpful?