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

untiljs

v2.1.2

Published

Promised one-time watch for changes

Readme

untiljs

Promise-based one-time watch for changes - Framework Agnostic

NPM version Codacy Badge tree shaking typescript Test coverage npm download gzip License

Sonar

DocumentationChange Log

Read this in other languages: English | 简体中文

Features

  • Framework Agnostic - Works with Vue, React, Angular, Svelte, Node.js, and vanilla JavaScript
  • TypeScript Support - Full type definitions included
  • Multiple Input Types - Supports getter functions, Subscribable objects, RefLike objects, and plain values
  • Deep Comparison - Configurable depth for object/array comparison
  • Timeout Support - Built-in timeout handling with optional error throwing
  • Tree-shakeable - Only bundle what you use
  • Zero Dependencies - Only js-cool as a lightweight runtime dependency
  • Well Tested - 227+ test cases with comprehensive coverage

Experience Online

Installation

# use pnpm
$ pnpm install untiljs

# use npm
$ npm install untiljs --save

# use yarn
$ yarn add untiljs

Quick Start

import until from 'untiljs'

// Basic usage with getter function
let value = 1
setTimeout(() => {
  value = 2
}, 1000)

await until(() => value).toBe(2)
console.log('Value is now 2!')

API Reference

WatchSource

A WatchSource<T> can be one of:

| Type | Example | Description | | ------------------- | ---------------------- | ------------------------------------------------------ | | Getter Function | () => value | A function that returns the current value | | Subscribable | { value, subscribe } | An object with value property and subscribe method | | RefLike | { value } | An object with value property (e.g., Vue ref) | | Plain Value | 5, 'hello' | A static value |

Subscribable Interface

interface Subscribable<T> {
  readonly value: T
  subscribe(callback: (value: T) => void): () => void
}

Methods

| Method | Description | | ------------------------------ | --------------------------------------------- | | toBe(value, options?) | Wait until the source equals the given value | | toMatch(condition, options?) | Wait until the condition returns true | | toBeTruthy(options?) | Wait until the value is truthy | | toBeNull(options?) | Wait until the value is null | | toBeUndefined(options?) | Wait until the value is undefined | | toBeNaN(options?) | Wait until the value is NaN | | changed(options?) | Wait until the value changes | | changedTimes(n, options?) | Wait until the value changes n times | | toContains(value, options?) | Wait until array contains value (arrays only) | | not.* | Inverse of any method above |

Options

interface UntilToMatchOptions {
  /** Timeout in milliseconds (0 = never timeout) */
  timeout?: number

  /** Reject promise on timeout (default: false) */
  throwOnTimeout?: boolean

  /** Deep comparison depth (true = unlimited, number = specific depth) */
  deep?: boolean | number
}

Usage Examples

Basic Usage

import until from 'untiljs'

// Wait for value to equal something
let count = 0
setTimeout(() => {
  count = 5
}, 1000)
await until(() => count).toBe(5)

// Wait for custom condition
await until(() => count).toMatch(v => v > 3)

// Wait for truthy value
let data = null
setTimeout(() => {
  data = { name: 'John' }
}, 500)
await until(() => data).toBeTruthy()

// Wait for value to change
let value = 'initial'
setTimeout(() => {
  value = 'changed'
}, 1000)
await until(() => value).changed()

// Wait for multiple changes
await until(() => value).changedTimes(3)

Vue 3 Integration

Vue refs work directly because they are RefLike objects:

import { ref, computed } from 'vue'
import until from 'untiljs'

const count = ref(0)
const doubled = computed(() => count.value * 2)

// Method 1: Pass ref directly (recommended for Vue!)
async function waitForValue() {
  await until(count).toBe(5)
  console.log('Count reached 5!')
}

// Method 2: Use getter function
async function waitForValueGetter() {
  await until(() => count.value).toBe(5)
  console.log('Count reached 5!')
}

// Watch computed values
async function waitForDoubled() {
  await until(doubled).toBe(10)
  console.log('Doubled value reached 10!')
}

// Deep comparison for objects
const user = ref({ profile: { name: '' } })
setTimeout(() => {
  user.value = { profile: { name: 'John' } }
}, 1000)

await until(user).toMatch(v => v.profile.name === 'John', { deep: true })

React Integration

Important: Due to React's closure behavior, using until(() => stateValue) directly won't work properly. Use useRef or a custom hook instead.

❌ Wrong Way (Won't Work)

// This won't detect changes due to React's closure behavior
const [value, setValue] = useState(0)
await until(() => value).toBe(5) // ❌ Always sees old value

✅ Correct Way: useUntil Hook

import { useCallback, useRef, useState } from 'react'
import until from 'untiljs'

// Custom hook for untiljs
function useUntil<T>(initialValue: T) {
  const ref = useRef(initialValue)
  const [value, setValue] = useState(initialValue)

  const refLike = useRef({
    get value() {
      return ref.current
    },
    set value(newValue: T) {
      ref.current = newValue
      setValue(newValue)
    }
  })

  const setValueAndRef = useCallback((newValue: T) => {
    ref.current = newValue
    setValue(newValue)
  }, [])

  return {
    value,
    setValue: setValueAndRef,
    until: () => until(refLike.current)
  }
}

// Usage
function MyComponent() {
  const data = useUntil(0)

  const handleClick = async () => {
    data.setValue(0)
    setTimeout(() => data.setValue(5), 1000)
    await data.until().toBe(5) // ✅ Works correctly!
    console.log('Value is now 5!')
  }

  return <button onClick={handleClick}>Test</button>
}

✅ Alternative: Subscribable Store

function createSubscribable<T>(initialValue: T) {
  let value = initialValue
  const listeners = new Set<(value: T) => void>()

  return {
    get value() {
      return value
    },
    set value(newValue: T) {
      value = newValue
      listeners.forEach(l => l(value))
    },
    subscribe(callback: (value: T) => void) {
      listeners.add(callback)
      callback(value)
      return () => listeners.delete(callback)
    }
  }
}

// Usage with useEffect
const store = createSubscribable(0)
await until(store).toBe(5) // ✅ Most efficient!

✅ Built-in Solution: createStore

untiljs v2.1+ provides a built-in createStore function for React:

import { createStore } from 'untiljs'
import until from 'untiljs'

// Create store outside component or in useRef
const store = createStore(0)

function MyComponent() {
  const [value, setValue] = useState(store.value)

  useEffect(() => store.subscribe(setValue), [])

  const handleClick = async () => {
    store.value = 5
    await until(store).toBe(5) // ✅ Clean and efficient!
  }

  return <button onClick={handleClick}>Test</button>
}

Angular Integration

Angular 19+ signals work with getter functions:

import { Component, signal } from '@angular/core'
import until from 'untiljs'

@Component({
  selector: 'app-example',
  template: `
    <p>Count: {{ count() }}</p>
    <button (click)="waitForValue()">Wait for 5</button>
  `
})
export class ExampleComponent {
  count = signal(0)

  async waitForValue() {
    this.count.set(0)
    setTimeout(() => this.count.set(5), 1000)

    // Use getter function with signals
    await until(() => this.count()).toBe(5)
    console.log('Count reached 5!')
  }
}

Using createStore in Angular

import { Component } from '@angular/core'
import { createStore } from 'untiljs'
import until from 'untiljs'

@Component({
  selector: 'app-example',
  template: `<button (click)="test()">Test</button>`
})
export class ExampleComponent {
  private store = createStore(0)

  async test() {
    this.store.value = 0
    setTimeout(() => this.store.value = 5, 1000)

    // Store works directly with until
    await until(this.store).toBe(5)
  }
}

Svelte Integration

Svelte 5 runes work with getter functions:

<script>
  import until from 'untiljs'

  let count = $state(0)

  async function waitForValue() {
    count = 0
    setTimeout(() => count = 5, 1000)

    // Use getter function with $state
    await until(() => count).toBe(5)
    console.log('Count reached 5!')
  }
</script>

<p>Count: {count}</p>
<button onclick={waitForValue}>Wait for 5</button>

Using createStore in Svelte

<script>
  import { createStore } from 'untiljs'
  import until from 'untiljs'

  const store = createStore(0)
  let storeValue = $state(store.value)

  // Subscribe to changes
  $effect(() => {
    return store.subscribe(value => storeValue = value)
  })

  async function test() {
    store.value = 0
    setTimeout(() => store.value = 5, 1000)
    await until(store).toBe(5)
  }
</script>

Vue 2 Integration

Vue 2.7+ supports Composition API natively:

<script>
import { ref } from 'vue'
import until from 'untiljs'

export default {
  setup() {
    const count = ref(0)

    const waitForValue = async () => {
      count.value = 0
      setTimeout(() => count.value = 5, 1000)

      // Use getter function with ref
      await until(() => count.value).toBe(5)
      console.log('Count reached 5!')
    }

    return { count, waitForValue }
  }
}
</script>

<template>
  <p>Count: {{ count }}</p>
  <button @click="waitForValue">Wait for 5</button>
</template>

Note: Vue 2.6 and below require @vue/composition-api plugin. Import from @vue/composition-api instead of vue.

RxJS Integration

import { BehaviorSubject } from 'rxjs'
import until from 'untiljs'

// Convert BehaviorSubject to Subscribable
const subject = new BehaviorSubject(1)

const subscribable = {
  get value() {
    return subject.value
  },
  subscribe(callback: (value: number) => void) {
    const subscription = subject.subscribe(callback)
    return () => subscription.unsubscribe()
  }
}

await until(subscribable).toBe(2)

// Or use getter function
await until(() => subject.value).toBe(2)

Node.js Usage

import until from 'untiljs'
import { EventEmitter } from 'events'

// Wait for event-based state changes
const emitter = new EventEmitter()
let status = 'pending'

emitter.on('ready', () => {
  status = 'ready'
})

setTimeout(() => emitter.emit('ready'), 1000)
await until(() => status).toBe('ready')

// Wait for file changes (with fs.watch)
import fs from 'fs'
import { readFile } from 'fs/promises'

let fileContent = await readFile('./data.txt', 'utf-8')
const watcher = fs.watch('./data.txt', async () => {
  fileContent = await readFile('./data.txt', 'utf-8')
})

await until(() => fileContent).toMatch(content => content.includes('target'))
watcher.close()

Array Methods

import until from 'untiljs'

// Wait for array to contain value
let items = ['apple', 'banana']
setTimeout(() => {
  items.push('orange')
}, 500)

await until(() => items).toContains('orange')

// Arrays also support all value methods
let numbers = [1, 2, 3]
setTimeout(() => {
  numbers = [1, 2, 3, 4, 5]
}, 500)

await until(() => numbers).toBe([1, 2, 3, 4, 5], { deep: true })
await until(() => numbers).toMatch(arr => arr.length >= 5)

Timeout Handling

import until from 'untiljs'

// Timeout without throwOnTimeout - returns current value
let value = 0
const result = await until(() => value).toBe(5, { timeout: 1000 })
console.log(result) // 0 (current value after timeout)

// Timeout with throwOnTimeout - rejects promise
try {
  await until(() => value).toBe(5, { timeout: 1000, throwOnTimeout: true })
} catch (error) {
  console.error('Timeout!', error)
}

Not Modifier

import until from 'untiljs'

// Wait until value is NOT 5
let value = 5
setTimeout(() => {
  value = 10
}, 500)

await until(() => value).not.toBe(5)
console.log(value) // 10

// Wait until value is NOT null
let data = null
setTimeout(() => {
  data = 'loaded'
}, 500)

await until(() => data).not.toBeNull()
console.log(data) // 'loaded'

Deep Comparison

import until from 'untiljs'

// Compare nested objects
let config = { server: { port: 3000 } }
setTimeout(() => {
  config = { server: { port: 8080 } }
}, 500)

await until(() => config).toBe({ server: { port: 8080 } }, { deep: true })

// Limit comparison depth
await until(() => config).toBe(
  { server: { port: 8080 } },
  { deep: 2 } // Compare up to 2 levels deep
)

Migration Guide (v1.x → v2.x)

Breaking Changes

  1. No longer requires @vue/reactivity - The library is now framework-agnostic
  2. Vue refs work directly - Pass ref directly or use () => ref.value
  3. Removed flush option - This was Vue-specific and has no generic equivalent
  4. deep option now accepts boolean | number - More flexible depth control

Quick Migration

// v1.x
import { ref } from 'vue'
import until from 'untiljs'

const count = ref(0)
await until(count).toBe(5)

// v2.x - Same code still works!
import { ref } from 'vue'
import until from 'untiljs'

const count = ref(0)
await until(count).toBe(5) // Direct ref usage still supported
// OR
await until(() => count.value).toBe(5) // Getter function also works

Comparison Table

| v1.x | v2.x | | --------------------------- | ------------------------------------------------------------- | | until(ref) | until(ref) (still works!) or until(() => ref.value) | | until(ref).toBe(otherRef) | until(ref).toBe(otherRef.value) | | { flush: 'sync' } | (removed - uses requestAnimationFrame/setImmediate) | | { deep: true } | { deep: true } (unchanged) or { deep: 5 } for depth limit |

Using unpkg CDN

<script src="https://unpkg.com/untiljs@latest/dist/index.iife.min.js"></script>
<script>
  let value = 0
  setTimeout(() => {
    value = 5
  }, 1000)

  until(() => value)
    .toBe(5)
    .then(() => {
      console.log('Value is 5!')
    })
</script>

Browser Support

| Browser | Version | | ------- | ----------------- | | Chrome | Latest 2 versions | | Firefox | Latest 2 versions | | Safari | Latest 2 versions | | Edge | Latest 2 versions | | Node.js | >= 16 |

Support & Issues

Please open an issue here.

License

MIT