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

svas

v3.1.0

Published

Svelte stores for asynchronous data with fetching, caching, revalidation, and persistence.

Readme

Svelte Async Stores

Svelte stores for asynchronous data with fetching, caching, revalidation, and persistence.


Everything below this line is written by AI.

  • Maybe<T> convention (null | T | Error) for all async states
  • Built-in revalidation with stale-while-revalidate
  • Optional localStorage / sessionStorage persistence
  • Composable utilities for guards, awaits, and combinators

TL;DR

npm i svas
<script lang="ts">
  import { value, ok } from 'svas'

  const user = value<User>({
    get: () => fetch('/api/me').then((r) => r.json()),
    persist: 'user'
  })
</script>

{#if $user instanceof Error}
  <p>Failed to load</p>
{:else if $user === null}
  <p>Loading…</p>
{:else}
  <p>Hello, {$user.name}</p>
{/if}

Conventions

All async stores expose their state as Maybe<T, E> = null | T | E where E extends Error:

  • null — no data yet (loading, cleared, or not fetched)
  • T — resolved value
  • E — fetch error

Stores share a common lifecycle:

  • Fetch is triggered lazily on first subscribe
  • Revalidation happens automatically after revalidate ms (default 300_000)
  • stale: true keeps the previous value visible while revalidating
  • bind: Readable<unknown | null> nullifies the store when the bound store becomes null (useful for tying data to a session/user)
  • persist: string mirrors the store into localStorage under the given key

Stores

value<T>(options)

A single asynchronous value.

import { value } from 'svas'

const profile = value<Profile>({
  get: () => api.getProfile(),
  revalidate: 60_000,
  persist: 'profile',
  session: false,
  default: null,
  bind: session
})

Interface:

class Value<T> implements Writable<T | null> {
  subscribe(run): Unsubscriber   // triggers sync on first subscribe
  set(value: T | null): void
  update(updater): void
  extract(): T | null            // synchronous read
  sync(): void                   // revalidate if stale
}

Options:

  • get?: () => Promise<T | Error> — fetcher
  • revalidate?: number — ms before re-fetching (default 300_000)
  • persist?: string — storage key
  • session?: boolean — use sessionStorage instead of localStorage
  • default?: T — initial value when no persisted data exists
  • bind?: Readable<unknown | null> — reset to default when bound store is null

values<T>(options)

A keyed cache of asynchronous values. Each key has its own Readable<Maybe<T>> lifecycle.

import { values } from 'svas'

const products = values<Product>({
  get: (id) => api.getProduct(id),
  revalidate: 60_000,
  permanent: false,
  stale: true,
  persist: 'products',
  bind: session
})

const product = products.get('42')

Interface:

class Values<T, E extends Error = Error> {
  get(key, options?: { fetch?: boolean }): Readable<Maybe<T, E>>
  set(key, value: T | E, options?: { stash?: boolean }): Readable<T | E>
  reset(key): Maybe<T | E>      // restore stashed persistent value
  extract(key): T | null        // synchronous read, ignores errors
  delete(key): void
  clear(): void
}

Options:

  • get?: (key: string) => Promise<T | E> — per-key fetcher
  • revalidate?: number — ms before re-fetching (default 300_000)
  • stale?: boolean — keep value while revalidating (default false)
  • persist?: string — storage key for the whole map
  • permanent?: boolean — don't revalidate persisted entries on startup; combine with revalidate: Infinity to disable revalidation entirely
  • bind?: Readable<unknown | null> — clear the cache when bound store is null

The stash flag marks a value as transient: the previously persisted value is remembered and can be restored with reset(key). Useful for optimistic updates.

collection<T>(options)

A list of identifiable items, optionally backed by a values store so individual items can be observed independently.

Items must extend Identifiable:

interface Identifiable {
  id: string
}
import { collection, values } from 'svas'

const items = values<Todo>({ persist: 'todos' })

const todos = collection<Todo>({
  get: () => api.listTodos(),
  values: items,
  revalidate: 60_000,
  stale: true,
  persist: 'todos:list',
  bind: session
})

Interface:

class Collection<T extends Identifiable, E extends Error = Error>
  implements Readable<Maybe<T[], E>>
{
  subscribe(run): Unsubscriber
  add(item: T): void
  set(item: T, options?: { add?: boolean }): void
  update(id, updater: (item) => T | void, options?): void
  delete(id): void
  get(id, options?): Readable<Maybe<T, E>>   // requires `values`
  extract(id): T | null                       // requires `values`
  replace(items: T[]): void
  sync(): this
  fetch(): Promise<this>
}

Options:

  • get?: () => Promise<T[] | E> — list fetcher
  • revalidate?: number — ms before re-fetching (default 300_000)
  • stale?: boolean — keep list while revalidating (default false)
  • values?: Values<T, E> — side store for per-item subscriptions
  • persist?: string — storage key for the list
  • bind?: Readable<unknown | null> — clear on bound store nullification

When a values store is provided, mutations to the collection are mirrored into it, so components subscribed via get(id) update without re-fetching.

reflection<T>(options)

A copy of a collection a server streams, kept in IndexedDB and kept current. It reads the collection once, and from then on what changed in it, from the token the last read ended with. Queries answer from the copy, through indexes declared up front.

import { reflection, expired, ok } from 'svas'

const pots = reflection<Pot>({
  name: 'pots',
  stream: async (token) => {
    const response = await api.stream('/pots/stream/', token)

    // the server no longer continues from this token: the copy is read again from the start
    if (response.status === 410) return expired

    return parts(response) // the application's: the parts of the multipart body, as they arrive
  },
  get: (id) => api.getPot(id),
  indexes: { type: 'type', due: ['type', 'due'] },
  bind: session
})

const green = pots.query('due', { gte: ['green', 0], lte: ['green', Infinity] })
const pot = pots.get(id)

const created = await api.createPot(input)
if (ok(created)) await pots.apply(created)

events.on('pots.changed', () => pots.sync())

Interface:

class Reflection<T extends Comparable> {
  query(index, criteria?, options?: { order?: 'asc' | 'desc', limit?: number }): Readable<Maybe<T[]>>
  get(id): Readable<Maybe<T>>
  apply(entry: T): Promise<void>
  sync(): Promise<Error | null>
  empty(): Promise<void>
}

Options:

  • name: string — the IndexedDB database, svas:<name>
  • stream: (token?: string) => Promise<AsyncIterable<StreamPart<T>> | typeof expired> — reads the collection from a token, or from the start without one. It yields { entry }, { removed } and, last, { token }, and answers expired where the server no longer continues from the token
  • get?: (id) => Promise<Maybe<T>> — an entry the copy does not hold
  • indexes?: Record<string, string | string[]> — by name: a property, or a list of them
  • bind?: Readable<unknown | null> — deletes the copy when the bound store is null

The copy is read on the first subscription, and on sync() — call it when something says the collection changed. A query answers null until the copy holds the whole collection, and from the copy at once on every later start. criteria is a key of the index, or bounds of one: { gt, gte, lt, lte }; id is always an index.

apply(entry) takes the state a write answered without reading it: kept where its VERSION is higher than the copy's, taken out where it is DELETED.

empty() says the collection is empty, as it is for an account just made — call it on registration, before anything subscribes: queries answer [], and nothing is read until sync() or the next start. Once a read has started, it does nothing.

A read is kept whole or not at all: one that ends without a token was cut, and the next sync reads it again. A token the server no longer continues from drops the copy, which is read again. One tab reads at a time, and the others take what it read.

Utilities

ok(value)

Type guard narrowing a Maybe<T> to T:

import { ok } from 'svas'

if (ok($user)) {
  $user.name // typed as T, excludes null | Error
}

ensure(store)

Synchronously reads a store and throws if the value is null or an Error. Use in code paths where the value is known to be resolved.

import { ensure } from 'svas'

const user = ensure(userStore)

having(store)

Returns a promise that resolves with the first non-null, non-error value of the store.

import { having } from 'svas'

const user = await having(userStore) // T

awaited(store)

Returns a promise that resolves with the first non-null value of the store, including errors.

import { awaited } from 'svas'

const result = await awaited(userStore) // T | Error

once(store, condition)

Returns a promise that resolves with the first value satisfying condition. Building block for having and awaited.

import { once } from 'svas'

const ready = await once(status, (s) => s === 'ready')

combined(...stores)

Combines multiple Maybe stores into one. Resolves to the tuple of values when all are non-null, to the first encountered Error, or to null while any is still loading.

import { combined } from 'svas'

const both = combined(user, settings)

// $both: [User, Settings] | null | Error

sync(store, item, options?)

Merges a versioned item into a Collection or Value based on VERSION, and optionally deletes when DELETED is set. Useful for applying server events.

interface Comparable {
  id: string
  VERSION: number
  DELETED?: number | null
}
import { sync } from 'svas'

sync(todos, incoming)                    // insert/update if newer
sync(todos, incoming, { delete: false }) // ignore tombstones

Async

Svelte component for rendering a Maybe store with waiting, error, and awaited snippets.

While the store has no value, Async renders waiting, and nothing where it is not given: what loading looks like is the app's to declare. An error it renders with error, or a default message with a reload button — nothing, with silent.

<script lang="ts">
  import { Async } from 'svas'
</script>

<Async store={user}>
  {#snippet waiting()}<Spinner />{/snippet}
  {#snippet error(e)}<ErrorView {e} />{/snippet}
  {#snippet awaited(value)}<Profile {value} />{/snippet}
</Async>

Types

import type { Maybe } from 'svas'

type Maybe<T, E extends Error = Error> = null | T | E