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

@nostr-wot/data

v0.4.3

Published

Pure-function Nostr data layer: profiles, notes, threads, engagement, NIP-65 outbox. Optional SWR cache + React hooks.

Readme

@nostr-wot/data

Pure-function Nostr data layer. Profiles, notes, threads, follows, follower lists, engagement (reactions / reposts / zaps), NIP-65 outbox-model relay discovery, and a streaming subscription coalescer. Optional SWR cache + React hooks ship as separate entrypoints.

Three entrypoints

| Import path | What's in it | Depends on | |---|---|---| | @nostr-wot/data | Pure fetchers, parsers, outbox helper, getPool(), sharedCoalescer | nostr-tools (peer) | | @nostr-wot/data/cache | SWR cache wrapping the fetchers; observable primitive; localStorage persistence | + the above | | @nostr-wot/data/react | useProfile, useNote, useThread, <NostrDataProvider>, plus the shared session context (<NostrSessionProvider>, useSession, useKEKSigner, …) | + react (peer) |

Use only what you need.

Install

npm i @nostr-wot/data nostr-tools

Vanilla fetchers (no cache, no React)

import {
  fetchProfile,
  fetchNote,
  fetchNotesByAuthor,
  fetchThread,
  fetchFollows,
  fetchEngagement,
  fetchRelayList,
  relaysForAuthor,
  setDefaultRelays,
  setProfileAggregators,
} from "@nostr-wot/data";

setDefaultRelays(["wss://relay.damus.io", "wss://nos.lol"]);
setProfileAggregators(["wss://purplepag.es"]);

const profile = await fetchProfile("hex-pubkey");
// → { displayName, name, picture, banner, about, nip05, lud16, fetchedAt }

const note = await fetchNote("hex-event-id");
const notes = await fetchNotesByAuthor("hex-pubkey", { limit: 50 });
const replies = await fetchThread("hex-event-id");

const follows = await fetchFollows("hex-pubkey");
// → { event, pubkeys, fetchedAt }

const engagement = await fetchEngagement(["id1", "id2"]);
// → Map<id, { reactionCount, repostCount, zapTotalSats }>

// Outbox: union of defaults + the author's NIP-65 write relays
const relays = await relaysForAuthor("hex-pubkey");
const relayList = await fetchRelayList("hex-pubkey");
// → { read: string[], write: string[], event } | null

Streaming variants

streamProfile yields entry updates as relay events arrive (instead of resolving on EOSE).

import { streamProfile } from "@nostr-wot/data";

for await (const entry of streamProfile("hex-pubkey")) {
  // each yield is an updated ProfileEntry — useful for showing partial results
}

Subscription coalescer

sharedCoalescer merges concurrent reads from across your app — DM cache, profile cache, follower lists, etc. — into a single REQ per relay-set within a 50ms window. When the last consumer unsubscribes, the underlying subscription tears down.

import { sharedCoalescer } from "@nostr-wot/data";

// Live subscription
const stop = sharedCoalescer.enqueue({
  filters: [{ kinds: [1], authors: ["hex"], limit: 50 }],
  relays: ["wss://relay.damus.io"],
  onEvent: (e) => { /* ... */ },
  onEose: (relay) => { /* ... */ },
});

// One-shot
const events = await sharedCoalescer.querySync(
  [{ kinds: [10002], authors: ["hex"], limit: 1 }],
  { relays, timeoutMs: 5000 },
);

SWR cache layer

import {
  getProfile,
  getNote,
  getThread,
  getFollows,
  getRelayList,
  fetchEngagementBatch,
  configurePersistence,
  createKeyedObservable,
} from "@nostr-wot/data/cache";

configurePersistence({ namespace: "myapp", ttlMs: 24 * 3600_000 });

const profile = await getProfile("hex-pubkey");
// Cold-loads from localStorage if cached, refreshes in the background
// from relays as newer kind-0 events arrive.

The cache exposes its own primitive — createKeyedObservable<K, V>() — used internally by every cached fetcher and by @nostr-wot/dm/cache. Useful if you're building your own SWR-style stream:

const obs = createKeyedObservable<string, MyValue>();
obs.set("key", { ... });
obs.subscribe("key", (v) => { /* re-render */ });
const slot = obs.get("key");

React hooks

import {
  NostrDataProvider,
  useProfile,
  useNote,
  useThread,
  useEngagement,
  useFollows,
  useRelayList,
} from "@nostr-wot/data/react";

<NostrDataProvider
  relays={["wss://relay.damus.io", "wss://nos.lol"]}
  profileAggregators={["wss://purplepag.es"]}
  persistence={{ namespace: "myapp", ttlMs: 86400_000 }}
>
  <App />
</NostrDataProvider>;

function ProfileCard({ pubkey }: { pubkey: string }) {
  const profile = useProfile(pubkey); // SWR
  if (!profile) return <Skeleton />;
  return <h1>{profile.displayName ?? profile.name}</h1>;
}

<NostrDataProvider> configures defaults (relays, aggregators, persistence) on mount. If you're using @nostr-wot/wot or nostr-wot-sdk, prefer <NostrSdkProvider> instead — it wraps <NostrDataProvider> and adds opt-in WoT context.


Session context

@nostr-wot/data/react also hosts the shared session context — the single mount point for the active NostrSigner and the user's pubkey. It lives here (not in @nostr-wot/ui) so non-UI packages — DM hooks, blossom uploads, wallet/zap hooks — can read the signer from context without depending on the React UI package.

import {
  NostrSessionProvider,
  useSession,
  useSigner,
  usePubkey,
  useLogin,
  useLogout,
  useKEKSigner,
} from "@nostr-wot/data/react";

<NostrSessionProvider
  initialSigner={someSigner /* optional — e.g. constructed at boot */}
  onChange={({ signer, pubkey }) => /* mirror to your state */}
  onLogout={async () => { /* clear app caches */ }}
>
  <App />
</NostrSessionProvider>;

| Hook | Returns | |---|---| | useSession() | { signer, pubkey, isLoading, error, setSigner, logout } | | useSigner() | The active NostrSigner or null | | usePubkey() | The active hex pubkey or null | | useLogin() | (signer) => Promise<void> callback — set the active signer | | useLogout() | () => Promise<void> callback — drops the signer + runs onLogout | | useKEKSigner() | A narrow KEKSigner (NIP-44 encrypt + decrypt + pubkey) when the active signer supports NIP-44, else null |

useKEKSigner exists because some operations — DM cache-key derivation, wallet local-store encryption — need NIP-44 specifically and must skip signers that don't expose it (e.g. NIP-46 bunkers with restricted perms). The hook returns null instead of throwing, so consumers can branch on capability:

const kek = useKEKSigner();
if (!kek) return <p>Your signer doesn't support NIP-44 — encrypted-at-rest cache disabled.</p>;
const cacheKey = await getOrCreateCacheKey(kek.pubkey, kek);

@nostr-wot/ui mounts and consumes this same context for its login flows, and re-exports the hooks for convenience. If you're only using @nostr-wot/data (no UI), construct a signer yourself (from @nostr-wot/signers) and call useLogin()(signer) to attach it.


Outbox model (NIP-65)

Every fetcher routes through the outbox helper by default. When you ask for an author's content, the SDK resolves their kind-10002 (NIP-65) relay list and queries the union of their declared write relays + your defaults. This is the only reliable way to find content that isn't pinned to popular aggregators.

Override globally:

setDefaultRelays(["wss://relay.damus.io", "wss://nos.lol"]);
setProfileAggregators(["wss://purplepag.es"]);

Or per-call (most fetchers accept an optional relays override):

await fetchProfile(pubkey, ["wss://my-private-relay"]);

Pool sharing

@nostr-wot/data owns a single SimplePool instance, accessible via getPool(). If you have your own pool (e.g. for binary-frame coercion or NDK interop), wire it once at startup:

import { setPool } from "@nostr-wot/data";

const pool = new SimplePool({ websocketImplementation: MyWebSocket });
setPool(pool);

Other @nostr-wot/* packages reuse this same pool — DM subscriptions, blossom uploads, WoT lookups, all share connections.


License

MIT