Concurrency
@praxisjs/concurrent — async concurrency control decorators @Task, @Queue, and @Pool with reactive loading, error, and pending state.
Concurrency
Async concurrency control from @praxisjs/concurrent. Decorate an async method, declare a companion field, and get reactive .loading(), .error(), and .pending() state automatically — scoped per component instance.
npm install @praxisjs/concurrentpnpm add @praxisjs/concurrentyarn add @praxisjs/concurrentbun add @praxisjs/concurrentThe pattern
Every decorator follows the same structure: the first argument is the async method name, and the decorated field becomes the callable task handle with reactive state attached.
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 the method
// taskLoadUser.loading() → reactive boolean
// taskLoadUser.error() → reactive Error | null
// taskLoadUser.lastResult() → reactive last return value
}AbortSignal support (opt-in)
Name the first parameter of your async method signal to receive an AbortSignal. The decorator detects the parameter name and injects the signal automatically — callers never pass it.
async loadUser(signal: AbortSignal, id: number) {
// Pass to fetch, setTimeout wrappers, or any Web API that accepts a signal
this.user = await fetch(`/api/users/${id}`, { signal }).then(r => r.json())
}Methods without a signal parameter work exactly as before. The feature is additive and fully backward-compatible.
When a signal is present:
@Task— each new invocation aborts the previous signal@Queue—clear()also aborts the currently running item's signal@Pool—cancelAll()aborts all active signals
AbortError thrown by a cancelled operation is never stored in .error().
@Task(methodName) — concurrent execution
Runs calls concurrently. If multiple calls are in-flight, only the last one updates state — earlier results are discarded. With signal, earlier requests are also actively aborted.
@Component()
class UserProfile extends StatefulComponent {
@State() user: User | null = null
async loadUser(signal: AbortSignal, id: number) {
this.user = await fetch(`/api/users/${id}`, { signal }).then(r => r.json())
}
@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 user 1</button>
<button onClick={() => this.taskLoadUser.cancelAll()}>Cancel</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. With signal, calling clear() also aborts the currently running call.
import { Queue, QueueOf } from '@praxisjs/concurrent'
@Component()
class DocumentEditor extends StatefulComponent {
async saveDocument(signal: AbortSignal, data: DocumentData) {
await fetch('/api/save', { method: 'POST', body: JSON.stringify(data), signal })
}
@Queue('saveDocument')
taskSaveDocument!: QueueOf<DocumentEditor, 'saveDocument'>
render() {
return (
<div>
<button onClick={() => this.taskSaveDocument(data)}>Save</button>
{() => this.taskSaveDocument.pending() > 0 && (
<p>{() => this.taskSaveDocument.pending()} save(s) queued</p>
)}
</div>
)
}
}Reactive state: .loading(), .error(), .pending(), .clear()
Clearing the queue
.clear() cancels all queued (not-yet-started) calls and, when signal is declared, aborts the running call too. Each cancelled call's promise rejects with QueueClearedError:
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 it
throw e
}
}@Pool(methodName, concurrency?) — limited parallelism
Limits how many calls run simultaneously. Excess calls are queued automatically and run as slots free up. concurrency defaults to 1.
import { Pool, PoolOf } from '@praxisjs/concurrent'
@Component()
class FileUploader extends StatefulComponent {
async uploadFile(signal: AbortSignal, file: File) {
await fetch('/upload', { method: 'POST', body: file, signal })
}
@Pool('uploadFile', 3) // max 3 uploads at once
taskUploadFile!: PoolOf<FileUploader, 'uploadFile'>
onMount() {
files.forEach(f => this.taskUploadFile(f))
}
render() {
return (
<p>
Uploading: {() => this.taskUploadFile.active()} /
Queued: {() => this.taskUploadFile.pending()}
<button onClick={() => this.taskUploadFile.cancelAll()}>Cancel all</button>
</p>
)
}
}Reactive state: .loading(), .error(), .active(), .pending(), .cancelAll()
.cancelAll() aborts all active signals (when signal is declared) and resolves all pending calls as undefined.
Reactive state reference
| Property | @Task | @Queue | @Pool | Description |
|---|---|---|---|---|
.loading() | ✓ | ✓ | ✓ | True while any call is in-flight |
.error() | ✓ | ✓ | ✓ | Last error, or null (AbortError excluded) |
.lastResult() | ✓ | Return value of last successful call | ||
.pending() | ✓ | ✓ | Calls waiting in the queue | |
.active() | ✓ | Calls currently running | ||
.cancelAll() | ✓ | ✓ | Abort in-flight / pending operations | |
.clear() | ✓ | Cancel all queued calls with QueueClearedError |
Type helpers
Use these generics to type the companion field with full intellisense:
| Helper | Usage |
|---|---|
TaskOf<Class, 'method'> | Type for a @Task companion field |
QueueOf<Class, 'method'> | Type for a @Queue companion field |
PoolOf<Class, 'method'> | Type for a @Pool companion field |
When the method declares signal: AbortSignal as its first parameter, the type helper automatically strips it from the public callable signature — callers only see the remaining arguments.
State Machines
@praxisjs/fsm — finite state machines via @StateMachine and @Transition decorators, with typed states/events, reactive state signal, guards, per-transition actions, and per-state lifecycle hooks.
Extending PraxisJS
Build custom composables and decorators using the same primitives that power all built-in PraxisJS APIs.