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

@nbottarini/observable

v1.0.0-rc.4

Published

Tiny Observable pattern implementation for creating observable properties

Readme

npm License: MIT CI Status

Observable-Js

Tiny observable pattern primitives for TypeScript / JavaScript. No dependencies, no DOM, no React — pure patterns you can use in any layer of your app and test in isolation.

The library exposes three primitives:

  • Observable<T> — events as properties.
  • ObservableValue<T> — values that change and notify their observers.
  • ObservableResource<T> — values produced by an asynchronous source, with a load/ready/error lifecycle.

Installation

Npm:

$ npm install --save @nbottarini/observable

Yarn:

$ yarn add @nbottarini/observable

Observables (events)

observable<T>() returns an EmittableObservable<T> that the owner uses to fire events. Consumers should only see it as Observable<T>, which doesn't include notify — that way only the owner can fire the event.

import { observable, Observable } from '@nbottarini/observable'

class Button {
    private readonly _clicked = observable<ClickEvent>()
    public readonly clicked: Observable<ClickEvent> = this._clicked

    private onClick(e: ClickEvent) {
        this._clicked.notify(e)
    }
}

const button = new Button()
button.clicked.subscribe(this, (e) => { /* handle */ })

Filter and map

Observable<T> exposes chainable filter and map:

const big = source.filter(v => v > 10)
const labels = source.map(v => `value=${v}`)
const labelsBig = source.filter(v => v > 10).map(v => `big:${v}`)

Merge

import { merge } from '@nbottarini/observable'

const buttonClicked = observable<ClickEvent>()
const textChanged = observable<TextChangedEvent>()
const allEvents = merge(buttonClicked, textChanged)

allEvents.subscribe(this, (event) => { /* fired by either source */ })

Observable values

A reactive value with a current state and notifications on change.

import { observableValue } from '@nbottarini/observable'

const name$ = observableValue('John')
name$.value // 'John'

name$.subscribe(this, (newName) => { /* called whenever value changes */ })
name$.value = 'new name' // notifies subscribers

observableValue<T>(initial) returns a MutableObservableValue<T>. Expose it as ObservableValue<T> to make it read-only from the outside.

Computed values

import { observableComputed, observableValue } from '@nbottarini/observable'

const a$ = observableValue(1)
const b$ = observableValue(2)
const sum$ = observableComputed((a, b) => a + b, a$, b$)

sum$.value // 3
sum$.subscribe(this, (s) => { /* notified when a$ or b$ changes */ })
a$.value = 5 // sum$ recomputes and notifies

Map

const value$ = observableValue(1)
const tenfold$ = value$.map(v => v * 10)

Observable resources

An asynchronous value with a lifecycle. Useful for anything that requires a fetch step before being available — remote data, IndexedDB, heavy local computation, anything async.

Each piece of state is exposed twice: a $-suffixed reactive view (ObservableValue) for subscribing or chaining, and a plain synchronous getter for reading the current value. They are always in sync — resource.data === resource.data$.value.

import { observableResource } from '@nbottarini/observable'

const profile = observableResource(async () => {
    const res = await fetch('/api/me')
    return res.json()
})

await profile.whenReady()      // triggers the fetch on first call

// Sync reads
profile.data                   // current value
profile.status                 // 'uninitialized' | 'loading' | 'ready' | 'error'
profile.error                  // last fetch error, if any
profile.isRefreshing           // true while a background refresh is in flight

// Reactive views (subscribe / chain)
profile.data$.subscribe(this, (value) => { /* react to data changes */ })
profile.status$.subscribe(this, (status) => { /* react to status changes */ })

await profile.refresh()        // forces a new fetch
profile.reset()                // back to uninitialized, drops cached value

Stale-while-revalidate

Once data has been loaded, subsequent refreshes follow the stale-while-revalidate pattern: data keeps the previous value, status stays ready, and isRefreshing flips to true while the new fetch is in flight. The UI keeps showing the cached value instead of flickering, and can render a small refresh indicator off isRefreshing.

If a background refresh fails, error is populated but status remains ready and the cached data stays available.

// First load: status goes uninitialized → loading → ready
await profile.whenReady()

// Refresh: status stays ready; isRefreshing flips true → false
profile.isRefreshing$.subscribe(this, (refreshing) => {
    showSpinner(refreshing)  // small indicator, data stays visible
})
await profile.refresh()

Options

observableResource(fetchFn, {
    ttlSecs: 60,   // staleness threshold (default 60)
    eager: true,   // fetch on construction; default is lazy
})

Deriving resources

Use .map() to derive a new ObservableResource from an existing one. The derived resource keeps the source's status, error, isRefreshing, whenReady() and refresh(), and exposes a transformed view of data. reset() is a no-op on a derived resource — reset the source explicitly.

const profile = observableResource(async () => fetchProfile())
const userName = profile.map(p => p?.name)        // ObservableResource<string>
const isAdult = profile.map(p => p ? p.age >= 18 : undefined)

await profile.whenReady()
userName.data  // 'Jorge'

Composing data with observable values

If you only need a derived value (without the resource lifecycle), use data$.map() to get a plain ObservableValue:

const isLoading$ = profile.status$.map(s => s === 'loading')
const userName$ = profile.data$.map(p => p?.name ?? '')