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

@event-kit/core

v0.1.0

Published

Isomorphic, zero-dependency event engine providing structured types, i18n formatting, deduplication, and client sanitization.

Readme

@event-kit/core

Isomorphic, zero-dependency eventging primitives, structured types, and a lightweight eventger engine.

Designed for high-throughput Node.js, browser, edge runtime, and serverless environments. @event-kit/core provides strongly-typed eventging contracts with native i18n support, runtime deduplication, and audience-based filtering, with zero runtime dependencies.


Features

  • Isomorphic & zero-dependency: runs identically in Node.js, browsers, Bun, Deno, and edge environments.
  • Strongly typed contracts: structured interfaces (event, eventClient, SerializedError) and string-union constants (eventType, eventErrorType, eventAudience).
  • Rust serde parity: constants and properties use lowerCamelCase, matching serde(rename_all = "camelCase") mapping.
  • Context-aware eventger: child scopes, level filtering, time-window deduplication, and pluggable transports.
  • Environment-aware console output: minLevel alone decides whether a event is emitted; the default transport separately uses a resolved isDev flag to decide how much detail to print for events that pass, keeping raw payloads out of production consoles by default without silently overriding your level configuration. Bundler-specific dev flags (e.g. Vite/SvelteKit's import.meta.env.DEV) can be passed in via new eventger({ isDev }).
  • Built-in utilities: structured error serialization with sensitive-key redaction (including Axios-style .response errors), deduplication, audience sanitization/filtering, multi-event aggregation, and type guards — every utility is a standalone function, independently importable from @event-kit/core/utils.

Installation

npm install @event-kit/core
# or
pnpm add @event-kit/core
# or
yarn add @event-kit/core
# or
bun add @event-kit/core

Quick start

1. eventger

import { eventger, eventType, eventErrorType } from "@event-kit/core";

const eventger = new eventger({
  namespace: "api-gateway",
  minLevel: eventType.debug,
  dedupeWindowMs: 1000 // suppress an identical event re-emitted within 1s
});

eventger.info({
  code: "USER_AUTH_SUCCESS",
  message: "User usr_1024 eventged in from 192.168.1.1"
});

// Scoped child eventger — namespaces are joined with ":"
const autheventger = eventger.child({ group: "oauth-flow" });

autheventger.warn({
  code: "ERR_RATE_LIMIT",
  message: "IP exceeded maximum auth attempts",
  errorType: eventErrorType.rateLimit,
  status: 429
});

Or use the createeventger factory if you'd rather not use new:

import { createeventger } from "@event-kit/core";
const eventger = createeventger({ namespace: "api-gateway" });

Each level method (trace, debug, info, event, warn, error, fatal) returns the normalized event object it emitted, or null when the entry was filtered out by minLevel or suppressed as a duplicate.

Note: templating/interpolation ({key}-style placeholders) is intentionally out of scope for this package — build the final message string yourself, or reach for a dedicated templating package.

2. Custom transports

By default events are written to the console. minLevel is the only thing that decides whether a event is emitted — the default console transport never re-filters by severity. It does use isDev for one thing: outside production, it hides event.details (arbitrary, unredacted, caller-supplied context) from the console, since — unlike event.error, which passes through serializeError's redaction — nothing in this package sanitizes details. event.error is always shown for error/fatal events, since that's the point of eventging an error. Provide transports to ship events anywhere else (a file, an HTTP sink, an observability platform) — transport errors are caught internally so one broken transport can't take down the others or the calling app. An empty transports: [] is treated the same as omitting it (falls back to the console transport) — pass a no-op transport ([() => {}]) if you want to silence output entirely.

import { eventger, type eventTransportHandler } from "@event-kit/core";

const shipToDatadog: eventTransportHandler = (event) => {
  fetch("https://http-intake.events.datadoghq.com/v1/input", {
    method: "POST",
    body: JSON.stringify(event)
  });
};

const eventger = new eventger({ transports: [shipToDatadog] });

isDev (whether event.details gets shown) defaults to this package's own isDev() utility, which only checks process.env.NODE_ENV. That's the standard convention in Node and in bundlers that replace it (webpack, esbuild, Next.js), but Vite and SvelteKit expose their dev flag as import.meta.env.DEV insteadisDev()'s process check runs before any bundler-side text replacement could apply, so it can't see that flag on its own. Pass it in explicitly:

import { eventger, isDev } from "@event-kit/core";

// In a Vite/SvelteKit app:
const eventger = new eventger({ isDev: import.meta.env.DEV });

// Or, to keep a graceful fallback for environments without `import.meta.env`
// (e.g. this same code also running under plain Node/Jest):
const eventger = new eventger({ isDev: isDev(import.meta.env.DEV) });

isDev is resolved once per eventger instance at construction time (not re-checked on every event), and child() inherits the parent's resolved value unless a child explicitly overrides it.

3. Sanitizing events for public APIs (toClientevent)

Strip internal stack traces, system namespaces, and non-client-visible fields before returning event payloads to frontend clients:

import { toClientevent, type event } from "@event-kit/core";

const internalevent: event = {
  code: "ERR_DB_TIMEOUT",
  message: "Database query timed out for query ID q_88",
  namespaces: "internal-db-cluster",
  stack: "Error: Query failed at Pool.query...",
  details: { internalHost: "10.0.0.5" }
};

const publicevent = toClientevent(internalevent);
// { code: "ERR_DB_TIMEOUT", message: "..." }

4. Serializing caught errors (serializeError)

Handles plain Errors, primitive throws, and structured HTTP-client errors (e.g. Axios) that carry a .response. Sensitive-looking keys (password, token, authorization, cookie, ...) are redacted wherever this function touches external data — the error's own properties, response.headers, and response.data — as a best-effort safety net against accidentally eventging credentials. Redaction recurses into nested objects/arrays (default 3 levels deep, with circular-reference protection), so a nested response.data.user.password is caught too, not just a top-level field:

import { serializeError } from "@event-kit/core";

try {
  await axios.get("/users/1");
} catch (err) {
  const serialized = serializeError(err);
  // { name: "AxiosError", message: "...", code: "ERR_BAD_REQUEST",
  //   response: { status: 404, statusText: "Not Found",
  //     data: { user: { password: "[REDACTED]" } },
  //     headers: { authorization: "[REDACTED]", "content-type": "application/json" } } }
}

5. Redacting sensitive keys directly (redactObject / redactValue)

The same redaction serializeError uses internally is available standalone, for scrubbing any plain object (e.g. request headers) or arbitrary value (object, array, or primitive) before you hand it to a transport or a details payload:

import { redactObject, redactValue } from "@event-kit/core";

redactObject({ userId: "u1", password: "hunter2" });
// { userId: "u1", password: "[REDACTED]" }

redactObject({ user: { profile: { password: "hunter2" } } });
// { user: { profile: { password: "[REDACTED]" } } } — recurses by default

// redactValue also handles arrays and primitives, for when you don't know the shape ahead of time
redactValue([{ token: "a" }, { token: "b" }]);
// [{ token: "[REDACTED]" }, { token: "[REDACTED]" }]

6. Building event objects without a eventger instance (createevent)

The same normalization eventger uses internally — building a complete event from a level and payload, serializing payload.error — is exported standalone so you can compose it into your own pipeline:

import { createevent, eventType } from "@event-kit/core";

try {
  await db.query(sql);
} catch (error) {
  const event = createevent(eventType.error, { code: "ERR_DB_QUERY", error });
  // event.error -> SerializedError, event.stack derived automatically
}

7. Aggregating multiple events (toevent)

Collapse a batch of related events (e.g. several field-validation failures) into a single, localizable, displayable event:

import { toevent } from "@event-kit/core";

const summary = toevent([
  { code: "ERR_EMAIL_INVALID", type: "warn", errorType: "validation", message: "Invalid email" },
  { code: "ERR_AGE_INVALID", type: "error", errorType: "validation", message: "Invalid age" }
]);
// summary.type === "error", summary.code === "error.validation.error"

summary.audience is only set when you pass config.audiences (to the first requested audience); otherwise it's left undefined, same as any other event with no explicit audience — it does not silently default to eventAudience.admin.

8. Audience-based filtering (filterByAudience)

import { filterByAudience, eventAudience } from "@event-kit/core";

const clientVisibleevents = filterByAudience(allevents, eventAudience.client);

9. Checking for errors in a batch (hasError)

import { hasError, eventType } from "@event-kit/core";

if (hasError(aggregatedevents)) {
  // at least one event is `error` or `fatal` (the ERROR_TYPES default)
}

hasError(aggregatedevents, [eventType.warn, eventType.error, eventType.fatal]); // custom threshold

Architecture

No event class

event is a plain data shape (see Type contracts), not a class. A class would only add value here if event needed private state, inheritance, or behavior tightly bound to a single instance — it doesn't. events are plain objects created, passed around, serialized, and compared by value across process/network boundaries (they need to survive a JSON.stringify round-trip), which is exactly what plain objects are for. createevent() is the functional equivalent of a constructor: it takes a level and a payload and returns a fully normalized event, with no hidden state and no prototype.

eventger is a class, and that's the right call here

Unlike event, the eventger engine genuinely owns per-instance state: its resolved config, its transports, and a one-slot dedupe cache that has to persist across calls. That's real encapsulated, mutable state tied to one instance's lifetime — exactly what a class is for, and child() needs prototype-free, straightforward instantiation (new eventger({...})) to produce scoped instances cleanly. createeventger() is provided alongside it for callers who'd rather not use new.

Every method on eventger is deliberately thin — dispatch() reads like a short pipeline of calls into ./utils (meetsMinLevel, createevent, isDuplicate, mergeNamespace, isDev) rather than reimplementing that eventic inline. The class owns state and orchestration; the actual eventic lives in standalone, independently testable functions that other packages can also import without touching eventger at all.


Utilities reference

All utilities live under src/utils/ (one file per concern) and are re-exported from both @event-kit/core and @event-kit/core/utils.

| Function | Description | | --- | --- | | createevent(type, payload) | Builds a normalized event, serializing payload.error when present. | | serializeError(error) | Converts an unknown thrown value into a SerializedError, including Axios-style .response errors. Never throws. | | isDuplicate(eventA, eventB, options?) | Structural equivalence check between two events. | | toClientevent(event) | Strips server-only fields for public API responses. | | filterByAudience(events, targetAudience) | Filters events by visibility scope. | | toevent(events, config?) | Reduces multiple events into one representative event. | | pickeventType(types) | Picks the most severe eventType from a list. | | pickErrorType(errorTypes) | Picks the most specific eventErrorType from a list. | | summarizeMessages(messages, type?) | Joins multiple messages into one summary string. | | hasError(events, types?) | Checks whether any event's type is in types (defaults to ERROR_TYPES: fatal, error). | | meetsMinLevel(type, minLevel?) | Checks whether a severity level meets a minimum threshold. | | mergeNamespace(parent?, child?) | Joins a parent/child namespace pair the same way eventger.child does. | | isDev(fallback?) | Detects whether the current environment is non-production, via process.env.NODE_ENV (works in Node and in any bundler that statically replaces it — webpack, esbuild, Vite, Next.js, ...). | | redactObject(obj, depth?) | Recursively redacts sensitive-looking keys (password, token, ...) on a plain object, up to depth levels (default 3), with circular-reference protection. | | redactValue(value, depth?) | Same as redactObject, but for a value of unknown shape — object, array, or primitive. | | isSensitiveKey(key) | Checks whether a key name looks like it holds a secret. | | iseventType, iseventErrorType, iseventAudience | Runtime type guards for the string-union constants. |


Type contracts

All interfaces and type aliases live in src/types/types.d.ts.

export interface event extends eventClient, eventErrorPayload, eventContext {
  details?: Record<string, unknown>;
}

export interface eventClient extends eventLocalization {
  type?: eventType;
  status?: number;
  events?: event[];
}

export interface SerializedError {
  name: string;
  message: string;
  stack?: string;
  code?: string | number;
  response?: SerializedErrorResponse;
  raw?: unknown;
  [key: string]: unknown;
}

export interface eventgerOptions {
  namespace?: string;
  group?: string;
  defaultAudience?: eventAudience;
  minLevel?: eventType;
  dedupeWindowMs?: number;
  transports?: eventTransportHandler[];
  isDev?: boolean;
}

eventType, eventErrorType, and eventAudience are as const string-union objects (not TypeScript enums), so they compile away to plain string literals with no runtime enum object overhead beyond the small lookup table itself, and compare correctly across module/bundle boundaries.


License

MIT © livacom.com