How to create async iterator over callback which calls back multiple times (or a stream) in TypeScript?

42 views Asked by At

How do I get this working in TypeScript? Here is the EventIterator library, which seems to make it possible to build a queue to handle the callbacks of the xhr progress event nicely.

import { EventIterator } from 'event-iterator'

export async function* requestAndWaitForWorkToComplete(request: Request) {
  for await (const data of callXhr(request)) {
    yield data

    switch (data.type) {
      case 'request-progress':
        console.log('progress', data.percentComplete)
        break
      // ...
    }
  }
  // TODO: do more stuff here.
}


export type RequestBody = FormData | object

export type Request = {
  path: string
  method: string
  body: RequestBody
}

export type RequestProgress = {
  type: 'request-progress'
  request: Request
  percentComplete: number
}

export type RequestComplete = {
  type: 'request-complete'
  request: Request
}

export type RequestFailure = {
  type: 'request-failure'
  request: Request
}

export type CallXhr = RequestProgress | RequestComplete | RequestFailure

export async function* callXhr(
  request: Request,
): AsyncIterator<CallXhr> {
  return new EventIterator<CallXhr>(({ push }) => {
    const xhr = new XMLHttpRequest()
    let body
    if (typeof request.body === 'string') {
      body = request.body
      xhr.setRequestHeader('content-type', 'application/json')
    } else if (request.body instanceof FormData) {
      body = request.body
      // headers['content-type'] = 'multipart/form-data'
    } else {
      body = JSON.stringify(body)
      xhr.setRequestHeader('content-type', 'application/json')
    }

    const isAsync = true

    xhr.open(request.method, request.path, isAsync)

    xhr.upload.addEventListener('progress', (e: ProgressEvent) => {
      const percentComplete = (e.loaded / e.total) * 100
      push({ type: 'request-progress', percentComplete, request })
    })

    xhr.addEventListener('load', function () {
      if (this.status == 200) {
        push({ type: 'request-complete', request })
      } else {
        push({ type: 'request-failure', request })
      }
    })

    xhr.send(body)
  })
}

Right at callXhr(request) I am getting this in TypeScript:

Type 'AsyncIterator<CallXhr, any, undefined>' must have a '[Symbol.asyncIterator]()' method that returns an async iterator.ts(2504)
(alias) callXhr(request: Request): AsyncIterator<CallXhr, any, undefined>
import callXhr

I don't know if I have typed the callXhr function properly, or really what else I need to add to get this to work. Without the type annotation on callXhr I get this on the data.type call in the switch statement:

Property 'type' does not exist on type 'never'.ts(2339)

I would have assumed the EventIterator, which is class EventIterator<T> extends AsyncIterator<T> would have the type figured out automatically.

How do I properly type this in TypeScript? I have this in my tsconfig.json, do I need to change anything so it works cross-browser?

{
  "compilerOptions": {
    "module": "ESNext",
    "target": "ES2020",
    "lib": ["es2020", "dom"],
    ...
  }
}

I tried changing "target": "esnext" but it didn't seem to have any effect.

Also, if there is a simpler way to handle the xhr progress callback without using the third-party library, feel free to show how to do that :). But not a requirement.

0

There are 0 answers