Decorators
Stack decorators on action class methods to add cross-cutting behavior.
import {
OnError,
OnSuccess,
Debounce,
Throttle,
Authorized,
Retry,
Queue,
Log,
Validate,
} from 'comwit'
Built-in decorators
| Decorator | Purpose |
|---|---|
@OnError(fn) | Error handler. Receives the error. Re-throw to propagate. |
@OnSuccess(fn) | Runs after successful completion. |
@Debounce(ms) | Debounces the method call. |
@Throttle(ms) | Throttles the method call. |
@Authorized({ when, onDeny }) | Auth guard. when: () => boolean | Promise<boolean> |
@Retry({ times, delay? }) | Retries on error up to times. Optional delay between retries. |
@Queue() | Queues concurrent calls, executing one at a time. |
@Log(level?) | Logs method execution with timing. Levels: 'info' | 'debug' | 'warn'. |
@Validate(validators) | Validates arguments before execution. Throws ValidationError on failure. |
Example
import { action, OnError, OnSuccess, Debounce } from 'comwit'
export const postActions = action<Pick<PostActions, 'create' | 'search'>, AppContext>(
({ state, context }) => {
class Actions {
private model = state(post)
@OnSuccess(() => context.router.push('/posts'))
@OnError((e) => toast.error(e instanceof Error ? e.message : 'Failed'))
async create(title: string) {
const created = await api.post.create({ title })
this.model.posts.data.push(created)
}
@Debounce(300)
async search(keyword: string) {
await this.model.posts.query(keyword)
}
}
return new Actions()
}
)
Custom Decorators with intercept()
Build reusable decorators with full lifecycle control.
import { intercept } from 'comwit'
intercept() supports two modes:
- Immediate mode — hooks applied at decoration time. Use when you don't need runtime context.
- Lazy mode — factory receives
ActionContextwithstateandcontext, resolved when the action binds to a provider.
Immediate mode
const MyLog = intercept({
onBefore: (...args) => console.log('called with', args),
onSuccess: (result) => console.log('returned', result),
onError: (err) => console.error('failed', err),
})
Available hooks: onBefore, onSuccess, onError, onSettled, intercept.
Lazy mode
Use when the decorator needs access to model state or provider context.
const LoginRequired = intercept<AppContext>(({ state, context }) => {
const u = state(user)
return {
intercept: (execute, args) => {
if (!u.me) {
context.router.push('/login')
return
}
return execute(...args)
},
}
})
Use it like any other decorator:
@LoginRequired
@OnSuccess(() => context.router.push('/posts'))
async create(title: string) {
const created = await api.post.create({ title })
this.model.posts.data.push(created)
}
Decorators with arguments
Create parameterized decorators by wrapping intercept() in a function:
function MinLength(field: string, min: number) {
return intercept({
intercept: (execute, args) => {
if (args[0]?.length < min) throw new Error(`${field} must be at least ${min} chars`)
return execute(...args)
},
})
}
@Validate
Validates arguments before execution. Validators map keys to argument positions.
import { Validate, ValidationError } from 'comwit'
class TodoActions {
@Validate({
title: (v) => (typeof v === 'string' && v.length > 0) || 'Title required',
priority: (v) => [1, 2, 3].includes(v) || 'Priority must be 1-3',
})
async createTodo(title: string, priority: number) {
// ...
}
}
Each validator returns true for pass, or a string error message. On failure, throws ValidationError with an .errors record containing all failed validations.