
Security News
PolinRider: North Korea-Linked Supply Chain Campaign Expands Across Open Source Ecosystems
PolinRider expands across npm, Packagist, Go modules, and Chrome extensions, using hidden loaders to target developer environments.
@sebspark/emulator
Advanced tools
@sebspark/emulatorHelper for building emulators or test fakes.
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()
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())
}
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.
.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',
}))
.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' })
})
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.
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' })
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)
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(...)
.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' }
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
FAQs
Helper for building emulators or test fakes.
The npm package @sebspark/emulator receives a total of 79 weekly downloads. As such, @sebspark/emulator popularity was classified as not popular.
We found that @sebspark/emulator demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
PolinRider expands across npm, Packagist, Go modules, and Chrome extensions, using hidden loaders to target developer environments.

Security News
Open source attacks are accelerating as AI coding agents pull in dependencies faster, with less human review.

Research
/Security News
Malicious Chrome and Firefox extensions posed as free VPNs while stealing clipboard data through later extension updates.