npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@umpire/async

v1.0.0-rc.1

Published

Async-aware field-availability engine — superset of @umpire/core with async rules and validators

Readme

@umpire/async

Async-aware field-availability engine — a superset of @umpire/core where predicates and validators can return Promises. Use this package when any of your availability rules need to reach outside the current call stack: a remote config check, a debounced uniqueness query, or a permission lookup that requires a round trip.

If all your predicates are synchronous, stay on @umpire/core — it's lighter and its check() returns a value, not a Promise.

Docs · Quick Start

Install

npm install @umpire/async

@umpire/core is a peer dependency and is included automatically when you install @umpire/async through npm or yarn.

Quick example

A signup form where username availability is verified against an API before the submit button becomes enabled:

import { umpire, enabledWhen, requires, fairWhen } from '@umpire/async'

const ump = umpire({
  fields: {
    email: { required: true },
    password: { required: true },
    username: { required: true },
    referralCode: {},
  },
  rules: [
    // async predicate — checks the server for username availability
    enabledWhen(
      'username',
      async (values, conditions) => {
        if (!values.email) return false
        const taken = await checkUsernameTaken(values.username)
        return !taken
      },
      { reason: 'username is already taken' },
    ),

    // sync rule from @umpire/async — mixed freely with async rules
    requires('referralCode', (values) =>
      values.email?.endsWith('@invite.example.com'),
    ),
  ],
})

const availability = await ump.check(values)
// availability.username.enabled — false if username is taken
// availability.referralCode.enabled — true only for invited email domains

Sync rules from @umpire/core can also be passed into rules — they are wrapped transparently.

When to use this

Reach for @umpire/async when:

  • A predicate needs to call an API, query a database, or perform any I/O
  • A validator must resolve asynchronously (e.g. a Zod schema with .safeParseAsync())
  • You want built-in cancellation — starting a new check() auto-cancels the previous one

All evaluation methods (check, play, scorecard, challenge) return Promises. That overhead is intentional: it lets async and sync rules compose uniformly. If that tradeoff isn't worth it for your case, @umpire/core evaluates synchronously and has a smaller footprint.

@umpire/async is not a store adapter, a React hook, or a state manager. It evaluates what you pass it. For reactive bindings use @umpire/react, @umpire/signals, or one of the store adapters.

API

umpire(config)

umpire({
  fields: FInput,
  rules: AnyRule[],
  validators?: AnyValidationMap,
  onAbort?: (reason?: unknown) => void,
}): Umpire
  • fields — same shape as @umpire/core. Each key is a field name; the value is a FieldDef (required, default, isEmpty).
  • rules — array of async rules from @umpire/async builders, sync rules from @umpire/core, or both. Mixed freely.
  • validators — optional per-field validators. Accepts sync validators (functions, safeParse, test, named checks) and async ones (AsyncValidationFunction, AsyncSafeParseValidator).
  • onAbort — optional hook called whenever a check() is cancelled, either by auto-cancel or an external AbortSignal. The abort reason is passed as the first argument. If this function throws, the error is swallowed — it will not cause an unhandled rejection.

Returns an Umpire instance.

Rule builders

All builders return AsyncRule and accept both sync and async predicates.

enabledWhen(field, predicate, options?)

Makes field enabled only when predicate returns (or resolves to) true. When it returns false, the field is disabled and reason is attached.

type predicate = (
  values: FieldValues,
  conditions: C,
) => boolean | Promise<boolean>
enabledWhen(
  'teamSize',
  async (_values, conditions) => {
    const plan = await fetchPlan(conditions.accountId)
    return plan.allowsTeams
  },
  { reason: 'upgrade to a team plan to set team size' },
)

fairWhen(field, predicate, options?)

Marks the current value of field as foul when predicate returns false. Only evaluated when the field is satisfied — an empty field is always fair. The predicate receives the current value, the full values map, and conditions.

type predicate = (
  value: NonNullable<V>,
  values: FieldValues,
  conditions: C,
) => boolean | Promise<boolean>
fairWhen(
  'email',
  async (email) => {
    const domain = email.split('@')[1]
    return checkDomainValid(domain)
  },
  { reason: 'email domain is not reachable' },
)

requires(field, ...deps, options?)

Makes field enabled only when all of its dependencies are satisfied and available. Dependencies can be field names (checked for satisfaction + availability) or predicates (evaluated directly).

requires('billingAddress', 'plan', (values) => values.plan !== 'free')

Multiple dependencies are ANDed. requires controls enabled, not required — to block a submit on a missing conditional field, set required: true in the field def.

disables(source, targets[], options?)

Disables every field in targets when source is satisfied. source can be a field name or an async predicate.

disables('useSso', ['password', 'confirmPassword'], {
  reason: 'managed by SSO provider',
})

oneOf(groupName, branches, options?)

Mutually exclusive field groups. Exactly one branch can be active at a time; fields in inactive branches are disabled.

The activeBranch option pins a branch by name, or you can provide a function (sync or async) that resolves the active branch name.

oneOf(
  'authMethod',
  {
    password: ['password', 'confirmPassword'],
    sso: ['ssoProvider', 'ssoToken'],
  },
  {
    activeBranch: async (values) => {
      const config = await fetchOrgConfig()
      return config.ssoEnabled ? 'sso' : 'password'
    },
  },
)

anyOf(...rules)

OR combinator. The field is enabled (or fair) if any of the wrapped rules passes. Rules run in parallel via Promise.all. All wrapped rules must target the same fields and must be the same constraint type (all availability rules, or all fairness rules).

anyOf(
  enabledWhen('discount', (values) => values.plan === 'annual'),
  enabledWhen('discount', check('referralCode', isValidCode)),
)

eitherOf(groupName, branches)

OR across named branches, where each branch is itself a set of rules ANDed together. A field passes if any branch's rules all pass. Rules within each branch run in parallel.

eitherOf('accessPath', {
  directInvite: [enabledWhen('dashboard', (v) => Boolean(v.inviteToken))],
  verifiedEmail: [
    enabledWhen('dashboard', (v) => Boolean(v.email)),
    enabledWhen('dashboard', (v) => v.emailVerified === true),
  ],
})

check(field, validator)

Builds a predicate that passes when the field's current value satisfies validator. Returns a predicate function for use inside requires, enabledWhen, etc. The validator can be async.

requires(
  'confirmPassword',
  'password',
  check('password', async (pw) => {
    return meetsStrengthPolicy(pw)
  }),
)

createRules<F, C>()

Returns all builders narrowed to your field and condition types. Zero runtime overhead — purely a type-level convenience that avoids repeated type annotations.

const { enabledWhen, requires, fairWhen } = createRules<
  typeof fields,
  AppConditions
>()

defineRule(config)

Low-level escape hatch for custom async evaluation. Prefer the built-in builders. Use this only when you need to plug custom logic directly into Umpire's evaluation pipeline.

defineRule({
  type: 'myCustomRule',
  targets: ['field'],
  sources: ['otherField'],
  evaluate: async (values, conditions, prev, fields, availability, signal) => {
    signal.throwIfAborted()
    const passed = await myCheck(values)
    return new Map([
      ['field', { enabled: passed, reason: passed ? null : 'blocked' }],
    ])
  },
})

Options shared by rule builders

All rule builders accept an optional trailing options object:

type RuleOptions = {
  reason?: string | ((values, conditions) => string | Promise<string>)
  trace?: RuleTraceAttachment | RuleTraceAttachment[]
}

reason can be a static string or a function — sync or async — that produces the reason string at evaluation time.

Async validators

Pass validators in the validators config to attach validation results to the availability map. Validators only run on enabled, satisfied fields.

Accepted shapes:

// Async function — return boolean or { valid, error? }
type AsyncValidationFunction<T> = (
  value: NonNullable<T>,
) => ValidationOutcome | Promise<ValidationOutcome>

// Object with safeParseAsync (e.g. Zod v4 with async refinements)
type AsyncSafeParseValidator<T> = {
  safeParseAsync(value: NonNullable<T>): Promise<{ success: boolean }>
}

All sync validator shapes accepted by @umpire/core also work: plain functions, objects with safeParse, objects with test, and named checks.

To override the error message from any validator, wrap it:

validators: {
  username: {
    validator: myAsyncUsernameValidator,
    error: 'username is not available',
  },
}

After check() resolves, enabled and satisfied fields with a validator will include valid: boolean and, when invalid, error?: string on their availability entry.

Umpire instance methods

check(values, conditions?, prev?, signal?): Promise<AvailabilityMap>

Evaluates availability for all fields. Returns a map from field name to { enabled, satisfied, fair, required, reason?, reasons?, valid?, error? }.

Accepts partial values — omitted fields are treated as unsatisfied. Pass prev when your rules inspect the previous snapshot (e.g. oneOf transition logic).

Starting a new check() automatically cancels the previous in-flight check. The cancelled check's Promise rejects with an AbortError. Pass an AbortSignal as the fourth argument to cancel externally.

play(before, after, signal?): Promise<Foul[]>

Compares two snapshots and returns suggested resets for fields that became disabled or foul and still hold stale values. Each Foul entry has { field, reason, suggestedValue }.

const fouls = await ump.play(
  { values: prevValues, conditions },
  { values: nextValues, conditions },
)

scorecard(snapshot, options?): Promise<ScorecardResult>

Debugging surface. Returns a full picture of every field including transition analysis, rule traces, and graph edges. Not intended as an app-state input — use check() for that.

Pass options.signal to cancel externally. Pass options.before to include transition analysis. Pass options.includeChallenge: true to attach per-field rule traces.

challenge(field, values, conditions?, prev?): Promise<ChallengeTrace>

Explains exactly which rules affected a single field and why. Safe to call with partial values. Does not support external cancellation — it uses an internal non-cancellable signal.

const trace = await ump.challenge('username', values, conditions)
// trace.directReasons — per-rule breakdown with passed/failed and reason

init(overrides?): FieldValues

Returns a values object populated from field defaults. Overrides replace specific fields. Synchronous.

graph(): UmpireGraph

Returns the dependency graph as { nodes, edges }. Synchronous. Returns a defensive copy — mutating it does not affect evaluation.

rules(): AsyncRuleEntry[]

Returns rule metadata including index, id, and inspection data. Synchronous.

Cancellation

@umpire/async has first-class cancellation at every layer.

Auto-cancel. Starting a new check() cancels any in-flight check on the same Umpire instance. The cancelled Promise rejects with an AbortError. This keeps your UI consistent when values change faster than rules evaluate — only the latest check matters.

const first = ump.check(staleValues) // cancelled automatically
const result = await ump.check(freshValues) // this one wins
await first.catch(() => {}) // suppress the AbortError from the first check

External signal. Pass an AbortSignal to cancel from outside — useful for route navigation, component unmount, or request deduplication.

const controller = new AbortController()

// cancel on unmount, navigation, etc.
onDestroy(() => controller.abort())

const availability = await ump.check(
  values,
  conditions,
  undefined,
  controller.signal,
)

play() and scorecard() also support external signals. challenge() does not.

onAbort hook. The onAbort option fires whenever a check is cancelled, whether by auto-cancel or an external signal. Use it to clear loading state or update UI.

const ump = umpire({
  fields: { ... },
  rules: [ ... ],
  onAbort: (reason) => {
    setLoading(false)
  },
})

If onAbort throws, the error is swallowed — it will not produce an unhandled rejection.

Docs