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

@bsky/jetstream

v1.0.0

Published

Client library for Jetstream (atproto event streaming)

Readme

@bsky/jetstream

Client library for Jetstream, a friendly way to consume data published to AT Protocol in realtime.

Quick start

Install the Jetstream client:

npm install @bsky/jetstream @atproto/lex

@atproto/lex is a peer dependency providing the lexicon type system that events are typed with.

This package speaks Jetstream's v2 wire and supports all three of its consumption modes: live() for realtime events, snapshot() for the sealed archive of past events, and replay() for the archive followed by a seamless handoff to realtime. Consuming live events looks like:

import { Jetstream, isCreate } from '@bsky/jetstream'

const js = new Jetstream('https://jetstream.us-east.bsky.network')

for await (const evt of js.live({ collections: ['app.bsky.feed.post'] })) {
  if (isCreate(evt)) {
    console.log(evt.commit.collection, evt.commit.record)
  }
}

collections constrains commit events only — identity, account, and sync events flow regardless — so a commits-only stream also needs kinds: ['commit']. kinds accepts commit, identity, account, and sync, and omitting it means all kinds. Conversely, a kinds list without commit leaves collections with nothing to constrain: that pair is rejected rather than silently ignoring either side.

Using a lexicon as your collection filter will validate and type the events for you:

import { Jetstream, isCreate } from '@bsky/jetstream'
import { app } from '@bsky/sdk/lexicons'

const js = new Jetstream('https://jetstream.us-east.bsky.network')

for await (const evt of js.live({ collections: [app.bsky.feed.post] })) {
  if (isCreate(evt)) {
    console.log(evt.commit.collection, evt.commit.record.text)
  }
}

isCreate(), isUpdate(), isDelete(), and isPut() (create-or-update) narrow an event to a commit with that operation. Pass a lexicon — or a bare NSID — as a second argument to also narrow to its collection: isCreate(evt, app.bsky.feed.post). The record narrows exactly as app.bsky.feed.post.$isTypeOf() would narrow it, so a record the event already types precisely keeps that type. Like $isTypeOf(), these helpers narrow types without validating; when shape guarantees matter, use a validating collection filter as above.

Typed records are converted lex data — Cid, Uint8Array, and BlobRef values rather than wire JSON's {$link} / {$bytes} shapes. Pass live({ raw: true }) when you want the wire-faithful record instead (its $link/$bytes markers intact).

A validating collection filter (a lexicon, or { collection, validateRecord: true }, the default) routes a schema-invalid record away from delivery. Passing validateRecord: false only drops the schema check — a record that isn't a $type'd map still fails conversion and is skipped, reported via onError (or, for LexIndexer, onValidationError) rather than delivered.

For indexing workloads, LexIndexer dispatches schema-validated records to per-collection handlers with bounded concurrency and per-record ordering, and JetstreamRunner drives it with durable cursor tracking. The runner asks the server only for the kinds a LexIndexer has registered handlers for — register .identity()/.account()/.sync() (or a collection via .commit()) to receive that kind at all:

import { Jetstream, LexIndexer, MemoryCursorStore } from '@bsky/jetstream'
import { app } from '@bsky/sdk/lexicons'

const js = new Jetstream('https://jetstream.us-east.bsky.network')

const indexer = new LexIndexer()
  .commit(app.bsky.feed.like, {
    put: async (e) => {
      console.log('index', e.uri, e.record)
    },
    del: async (e) => {
      console.log('remove', e.uri)
    },
  })
  .identity(async (e) => {
    console.log('identity change', e.did, e.handle)
  })

// Implement CursorStore to persist the resume cursor durably.
const cursor = new MemoryCursorStore()

await js.runner(indexer).live({ cursor })

Cursors

live()'s cursor is a v2 seq. Values >= 1e15 are read by the server as unix-microsecond timestamps instead. When the server clamps a stale timestamp it sends an OutdatedCursor advisory, reported through onInfo without ending the stream — an advisory is dropped silently if no onInfo is registered.

Cursors are not portable between versions: a v1 cursor is a time_us value and a v2 cursor is a seq. Never replay a stored cursor against the other version.

Legacy v1 instances

The public jetstream*.bsky.network hosts speak the legacy v1 format. Use JetstreamV1, which offers live() only — v1 does not support historical network replay, has no kinds filter, and no runner/indexer path.

import { JetstreamV1 } from '@bsky/jetstream'

const js = new JetstreamV1('https://jetstream1.us-east.bsky.network')
for await (const evt of js.live({ collections: ['app.bsky.feed.post'] })) {
  console.log(evt.seq, evt.kind)
}

Connection behavior

This package runs on Node.js and in the browser.

A live stream stays up on its own: it reconnects automatically — including when the server closes cleanly or goes silent — and resumes from its cursor, so no events are lost or duplicated across reconnects. It ends only when you break out of the loop, abort the signal, or a genuinely fatal error occurs (which rejects the loop).

To observe or tune connection behavior, configure a transport with websocketTransport() and pass it via the liveTransport option:

import { Jetstream, websocketTransport } from '@bsky/jetstream'

const js = new Jetstream('https://jetstream.us-east.bsky.network')
for await (const ev of js.live({
  collections: ['app.bsky.feed.post'],
  liveTransport: websocketTransport({
    onReconnect: (err, { attempt }) => console.warn('reconnecting', attempt),
  }),
})) {
  // ...
}

Commonly useful options: onReconnect to observe connection trouble (retries are otherwise silent), shouldReconnect to change when the stream gives up, and idleTimeoutMs to tune dead-connection detection (default 60s; false disables). websocketTransport() inherits the rest of its options from @atproto/ws-client.