> ## Documentation Index
> Fetch the complete documentation index at: https://docs.markpdf.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Streaming and asynchronous jobs

> Consume streaming and jobs 202 with MarkpdfService.

# Streaming and asynchronous jobs

## Streaming with `convertStream`

```ts theme={null}
import { Component, inject, signal } from "@angular/core";
import { MarkpdfService } from "@markpdf/angular";

@Component({
  selector: "app-stream-viewer",
  standalone: true,
  template: `<pre>{{ text() }}</pre>`,
})
export class StreamViewerComponent {
  private markpdf = inject(MarkpdfService);
  text = signal("");

  loadFromUrl(url: string) {
    this.text.set("");
    this.markpdf.convertStream({ url }).subscribe({
      next: (chunk) => this.text.update((t) => t + chunk),
      error: (err) => console.error(err),
      complete: () => console.log("stream terminado"),
    });
  }
}
```

`convertStream` returns a `Observable<string>` that emits a value for each fragment — combine it with RxJS's `scan` if you prefer to accumulate text reactively instead of `signal.update`:

```ts theme={null}
import { scan } from "rxjs/operators";

markdown$ = this.markpdf
  .convertStream({ url })
  .pipe(scan((acc, chunk) => acc + chunk, ""));
```

```markup theme={null}
<pre>{{ markdown$ | async }}</pre>
```

<Note>
  `convertStream` requires your proxy backend to forward the stream without buffering (see [Framework Guide](/docs/sdks/angular/framework-guide)). If your backend accumulates the entire response before forwarding it, you will lose the latency benefit of streaming even if the `Observable` is still working (you will receive everything in a single `next`).
</Note>

## Jobs due to saturation (202)

When all backends of API are busy, the request is queued and `202` responds with a `job_id`. See [`GET /jobs/{id}`](/docs/api/jobs). How you handle it depends on whether your proxy backend does the polling for you or delegates it to the frontend.

### Poll delegado al backend (recomendado)

If your proxy backend uses SDK of Node.js/Python/etc. with `autoPoll: true` (the default in those SDKs), Angular never sees the `202` — the HTTP request to your backend simply takes longer and `convertFile`/`convertFromUrl` resolve to the final result.

### Manual poll from Angular

If your backend forwards the `202` as is (for example, to not block a long HTTP connection), handle the poll in the component:

```ts theme={null}
import { catchError, delay, expand, takeWhile } from "rxjs/operators";
import { of, throwError } from "rxjs";

pollJob(jobId: string) {
  return this.markpdf.getJob(jobId).pipe(
    expand((status) =>
      status.status === "queued" || status.status === "processing"
        ? this.markpdf.getJob(jobId).pipe(delay(5000))
        : of(status).pipe(takeWhile(() => false, true))
    ),
    takeWhile((status) => status.status !== "completed" && status.status !== "failed", true)
  );
}
```

```ts theme={null}
this.markpdf.convertFile(file).subscribe({
  next: (result) => {
    if (typeof result === "object" && "jobId" in result) {
      this.pollJob(result.jobId).subscribe((status) => {
        if (status.status === "completed") this.markdown.set(status.body as string);
        if (status.status === "failed") console.error(status.error);
      });
    } else {
      this.markdown.set(result as string);
    }
  },
});
```

<Tip>
  Under normal load conditions you won't see `202` — it only happens when all backends are saturated. If your backend uses any of the other official SDKs with auto-poll (the default behavior), you don't need to implement manual polling in Angular at all.
</Tip>

<Warning>
  Results for completed jobs expire in approximately 1 hour. If you save a `jobId` and `getJob` returns `404`, resend the original conversion.
</Warning>
