Skip to content

Concurrency

Async concurrency decorators from @praxisjs/concurrent. Decorate async methods to get reactive loading, error, and result state — automatically scoped per instance.

sh
npm install @praxisjs/concurrent
sh
pnpm add @praxisjs/concurrent
sh
yarn add @praxisjs/concurrent
sh
bun add @praxisjs/concurrent

Each decorator goes on a separate field. The first argument is always the method name, followed by any options. Type the field with the matching TaskOf / QueueOf / PoolOf helper for full intellisense.


Pattern

tsx
import { Task, TaskOf } from '@praxisjs/concurrent'

@Component()
class UserProfile extends StatefulComponent {
  @State() user: User | null = null

  async loadUser(id: number) {
    this.user = await api.getUser(id)
  }

  @Task('loadUser')
  taskLoadUser!: TaskOf<UserProfile, 'loadUser'>
  // taskLoadUser(1)           — call it
  // taskLoadUser.loading()    — reactive boolean
  // taskLoadUser.error()      — reactive Error | null
  // taskLoadUser.lastResult() — reactive last return value
}

@Task(methodName)

Runs calls concurrently. Each call races — if a new call starts before the previous finishes, only the last one updates state.

tsx
import { Task, TaskOf } from '@praxisjs/concurrent'

@Component()
class UserProfile extends StatefulComponent {
  @State() user: User | null = null

  async loadUser(id: number) {
    this.user = await api.getUser(id)
  }

  @Task('loadUser')
  taskLoadUser!: TaskOf<UserProfile, 'loadUser'>

  render() {
    return (
      <div>
        {() => this.taskLoadUser.loading() && <Spinner />}
        {() => this.taskLoadUser.error() && <p>Error: {this.taskLoadUser.error()!.message}</p>}
        {() => this.user && <UserCard user={this.user} />}
        <button onClick={() => this.taskLoadUser(1)}>Load</button>
      </div>
    )
  }
}

Reactive state: .loading(), .error(), .lastResult(), .cancelAll()


@Queue(methodName)

Serial execution — calls run one at a time. If a call arrives while one is running, it waits its turn.

tsx
import { Queue, QueueOf } from '@praxisjs/concurrent'

@Component()
class DocumentEditor extends StatefulComponent {
  async saveDocument(data: DocumentData) {
    await api.save(data)
  }

  @Queue('saveDocument')
  taskSaveDocument!: QueueOf<DocumentEditor, 'saveDocument'>

  render() {
    return (
      <div>
        <button onClick={() => this.taskSaveDocument(data)}>Save</button>
        {() => this.taskSaveDocument.pending() > 0 && (
          <p>{() => this.taskSaveDocument.pending()} saves queued</p>
        )}
      </div>
    )
  }
}

Reactive state: .loading(), .error(), .pending(), .clear()

Clearing the queue

.clear() cancels all queued (not-yet-started) calls. Each cancelled call's promise rejects with a QueueClearedError.

tsx
import { QueueClearedError } from '@praxisjs/concurrent'

onUnmount() {
  this.taskSaveDocument.clear()
}

async saveAll() {
  try {
    await this.taskSaveDocument(data)
  } catch (e) {
    if (e instanceof QueueClearedError) return  // expected — ignore
    throw e
  }
}

@Pool(methodName, concurrency?)

Limits how many calls run simultaneously. Excess calls are queued automatically. concurrency defaults to 1.

tsx
import { Pool, PoolOf } from '@praxisjs/concurrent'

@Component()
class FileUploader extends StatefulComponent {
  async uploadFile(file: File) {
    await api.upload(file)
  }

  @Pool('uploadFile', 3)
  taskUploadFile!: PoolOf<FileUploader, 'uploadFile'>

  onMount() {
    files.forEach(f => this.taskUploadFile(f))
  }

  render() {
    return (
      <p>
        Uploading: {() => this.taskUploadFile.active()} /
        Queued: {() => this.taskUploadFile.pending()}
      </p>
    )
  }
}

Reactive state: .loading(), .error(), .active(), .pending()


Reactive state reference

Property@Task@Queue@PoolDescription
.loading()True while any call is in-flight
.error()Last error, or null
.lastResult()Return value of last successful call
.pending()Calls waiting in the queue
.active()Calls currently running
.clear()Cancels all queued calls with QueueClearedError
.cancelAll()Discards in-flight result (does not abort the promise)

Type helpers

HelperUsage
TaskOf<Class, 'method'>Type for a @Task field
QueueOf<Class, 'method'>Type for a @Queue field
PoolOf<Class, 'method'>Type for a @Pool field

Released under the MIT License.