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

@sebspark/emulator

v0.1.0

Published

Helper for building emulators or test fakes.

Readme

@sebspark/emulator

Helper for building emulators or test fakes.

Overview

This package provides a generic, type-safe emulator engine. The idea is that you wrap it in a concrete emulator that adapts a real transport (HTTP, Pub/Sub, gRPC, etc.) to the emulator's simple request/response model. Tests then configure the emulator to respond in specific ways, without needing a real backend.

Real transport (Pub/Sub message, HTTP request, …)
        │
        ▼
  Your emulator adapter        ← decodes, calls emulator.handle(...)
        │
        ▼
   createEmulator()            ← dispatches to registered responders
        │
        ▼
  Your test                    ← registers responders with .reply() / .callback()

Building an emulator

Define a MethodMap that describes every operation your external system exposes, then wire up the transport to call emulator.handle(...).

import { createEmulator, disposable, type Disposable } from '@sebspark/emulator'

// 1. Declare every method with its request and response types
type PaymentMethodMap = {
  authorise: {
    args: { amount: number; currency: string }
    resp: { authCode: string; status: 'approved' | 'declined' }
  }
  refund: {
    args: { authCode: string; amount: number }
    resp: { success: boolean }
  }
}

// 2. Expose a typed emulator handle
export type PaymentEmulator = Disposable<
  ReturnType<typeof createEmulator<PaymentMethodMap>>
>

// 3. Wire up the transport
export const startPaymentEmulator = (server: HttpServer): PaymentEmulator => {
  const emulator = createEmulator<PaymentMethodMap>()

  server.on('POST /authorise', async (req, res) => {
    await emulator.handle('authorise', req.body, async (response) => {
      res.json(response)
    })
  })

  server.on('POST /refund', async (req, res) => {
    await emulator.handle('refund', req.body, async (response) => {
      res.json(response)
    })
  })

  return disposable(emulator, () => server.close())
}

Using the emulator in tests

The intended test pattern is setup → execute → assert, keeping each step explicit and local to the test. Register exactly one responder, trigger exactly one call, check the result:

it('returns an auth code on approval', async () => {
  // Setup
  payments.authorise().reply({ authCode: 'ABC123', status: 'approved' })

  // Execute
  const result = await client.authorise({ amount: 100, currency: 'SEK' })

  // Assert
  expect(result.authCode).toBe('ABC123')
})

The responder is consumed after the call, so a missing setup will throw immediately rather than silently reusing state from another test.

Single response — .reply()

Register a static response or a function. The responder is consumed after one use.

// Static response
payments.authorise().reply({ authCode: 'ABC123', status: 'approved' })

// Computed from the request
payments.authorise().reply((args) => ({
  authCode: `CODE-${args.amount}`,
  status: args.amount > 0 ? 'approved' : 'declined',
}))

Streaming responses — .callback()

Use .callback() when a single trigger produces multiple responses (e.g. order status updates).

payments.authorise().callback((args, cb) => {
  cb({ authCode: 'PENDING', status: 'approved' })
  cb({ authCode: 'SETTLED', status: 'approved' })
})

Lifetime control

In most tests the one-shot default is exactly what you want. Lifetime modifiers are intended for more complex scenarios such as integration-style tests or helpers that need to serve many calls. Prefer explicit per-test setup over persistent responders wherever possible.

By default, a responder is consumed after one use. Control this with:

| Method | Behaviour | |---|---| | .reply(...) / .callback(...) | One-time (default) | | .once().reply(...) | One-time (explicit) | | .twice().reply(...) | Two uses | | .thrice().reply(...) | Three uses | | .times(n).reply(...) | n uses | | .persist().reply(...) | Unlimited uses |

// Approve the first two, then always decline
payments.authorise().persist().reply({ authCode: '', status: 'declined' })
payments.authorise().twice().reply({ authCode: 'ABC', status: 'approved' })

Responders are matched in LIFO order — the most recently registered matching responder wins. This makes it easy to stack overrides.

Filters

Pass a filter function to restrict which requests a responder handles:

payments
  .authorise((args) => args.currency === 'SEK')
  .reply({ authCode: 'SEK-OK', status: 'approved' })

payments
  .authorise()
  .reply({ authCode: 'OTHER', status: 'declined' })

Stacking overrides

The most common pattern is a persistent default with one-time overrides layered on top. Because responders resolve in LIFO order, the override is consumed first, then every subsequent request falls through to the default:

// Always decline...
payments.authorise().persist().reply({ authCode: '', status: 'declined' })

// ...except the very next call, which is approved
payments.authorise().reply({ authCode: 'ABC123', status: 'approved' })

// First call  → approved (override consumed)
// Second call → declined (fallback)
// Third call  → declined (fallback)

Unhandled requests

If a request arrives with no matching responder registered, the emulator throws. This is intentional — it surfaces missing setup immediately rather than returning a silent default:

// No responder registered
await payments.authorise(...)
// throws: No responder found for .authorise(...)

Direct invocation — .execute()

Each registration returns an .execute() helper for triggering the responder directly in a test without going through the transport:

const { execute } = payments
  .authorise()
  .reply({ authCode: 'TEST', status: 'approved' })

const result = await execute({ amount: 100, currency: 'SEK' })
// result → { authCode: 'TEST', status: 'approved' }

Cleanup

disposable() adds .dispose() and the Symbol.dispose / Symbol.asyncDispose symbols for using / await using (Node 20+).

// Explicit
await payments.dispose()

// Or with the `using` keyword (TypeScript 5.2+, Node 20+)
await using payments = startPaymentEmulator(server)
// automatically disposed when the block exits