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

@arcraz/common

v1.6.0

Published

Common utilities for the arcraz ecosystem

Readme

@arcraz/common

A TypeScript utilities library for the arcraz ecosystem, providing shared functionality across microservices.

Complete overhaul with factory pattern, Zod validation, and ESM-only build.

Requirements

  • Node.js 24+ (ESM-only)
  • Yarn package manager

Installation

yarn add @arcraz/common

Features

  • Factory Pattern - No singletons, full control over instances
  • Zod Config Validation - Type-safe configuration with runtime validation
  • Subpath Exports - Tree-shaking support for smaller bundles
  • Namespace Isolation - Redis keys and RabbitMQ queues are automatically namespaced by NODE_ENV
  • Native Drivers - Direct pg Pool and amqplib (no wrappers)
  • Error Hierarchy - Structured AppError base class with HTTP subclasses and type guard
  • Redis Rate Limiter - Fixed-window rate limiting with atomic Redis operations
  • Enhanced Repository - Soft-delete, sort-validated pagination, and findById out of the box

Quick Start

Configuration

import { loadEnv, getEnv, requireEnv } from '@arcraz/common/config';
import { DatabaseConfigSchema, RedisConfigSchema } from '@arcraz/common/config';

// Load .env file
loadEnv();

// Get environment variables
const nodeEnv = getEnv('NODE_ENV', 'development');
const dbHost = requireEnv('DATABASE_HOST'); // Throws if not set

// Validate configuration with Zod
const dbConfig = DatabaseConfigSchema.parse({
  name: 'main',
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT || '5432'),
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
});

Database (PostgreSQL)

import { createDatabasePool } from '@arcraz/common/database';
import { BaseRepository, PagedResponseHelper, ConverterHelper } from '@arcraz/common/database';

// Create a connection pool
const pool = createDatabasePool({
  name: 'main',
  host: 'localhost',
  port: 5432,
  database: 'myapp',
  user: 'api',
  password: 'secret',
});

// Direct query
const result = await pool.query('SELECT * FROM users WHERE id = $1', [userId]);

// Using BaseRepository
class UserRepository extends BaseRepository {
  async getById(id: string) {
    return this.queryOne<User>(
      'SELECT * FROM users WHERE id = $1',
      [id]
    );
  }

  async getPaged(page: number, pageSize: number) {
    const pager = new PagedResponseHelper(page, pageSize);
    const results = await this.query<User>(
      `SELECT *, COUNT(*) OVER() as full_count
       FROM users
       LIMIT $1 OFFSET $2`,
      [pager.limit, pager.offset]
    );
    return pager.result(results);
  }
}

const userRepo = new UserRepository(pool);
const user = await userRepo.getById('123');

// Cleanup
await pool.end();

Enhanced Repository

import { createDatabasePool, EnhancedRepository } from '@arcraz/common/database';
import type { EnhancedRepositoryConfig } from '@arcraz/common/database';

interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
}

class UserRepository extends EnhancedRepository<User> {
  constructor(db: DatabasePool, readOnlyDb?: DatabasePool) {
    super(db, {
      table: 'users',
      primaryKey: 'id',
      softDelete: true,
      sort: {
        allowedColumns: ['name', 'email', 'created_at'],
        defaultColumn: 'created_at',
        defaultDirection: 'DESC',
      },
    }, readOnlyDb);
  }
}

const pool = createDatabasePool({ /* ... */ });
const userRepo = new UserRepository(pool);

// Find by primary key (respects soft-delete)
const user = await userRepo.findById(123);

// Paginated + sorted (sort column validated against allowlist)
const page = await userRepo.findAll({
  page: 1,
  pageSize: 20,
  sortBy: 'name',
  sortDir: 'ASC',
});
// { pageNumber: 1, pageCount: 5, totalCount: 100, results: [...] }

// Soft-delete (sets is_active = false)
await userRepo.deleteById(123);

// Restore soft-deleted row
await userRepo.restoreById(123);

// Invalid sort columns throw (SQL injection prevention)
await userRepo.findAll({ sortBy: 'DROP TABLE users;--' }); // throws Error

Redis

import { createRedisClient, getNamespacedKey } from '@arcraz/common/redis';

// Create client
const redis = createRedisClient({
  host: 'localhost',
  port: 6379,
});

// Keys are automatically namespaced by NODE_ENV
// With NODE_ENV=production: "{production}:user:123"
const key = getNamespacedKey('user:123');

await redis.set(key, JSON.stringify({ name: 'John' }), 'EX', 3600);
const data = await redis.get(key);

// Cleanup
await redis.quit();

Redis Rate Limiter

import { createRedisClient, createRateLimiter } from '@arcraz/common/redis';

const redis = createRedisClient({ host: 'localhost' });

// Create a rate limiter (fixed-window algorithm)
const apiLimiter = createRateLimiter(redis, {
  prefix: 'api',
  maxRequests: 100,
  windowSizeSeconds: 60,
});

// Check rate limit for a user/IP
const result = await apiLimiter.checkLimit('user-123');
// {
//   allowed: true,
//   current: 1,
//   limit: 100,
//   remaining: 99,
//   resetAtMs: 1735689660000
// }

if (!result.allowed) {
  throw new RateLimitError('Too many requests');
}

// Different limiters for different endpoints
const loginLimiter = createRateLimiter(redis, {
  prefix: 'login',
  maxRequests: 5,
  windowSizeSeconds: 300, // 5 minutes
});

// Reset a specific identifier's limit
await apiLimiter.resetLimit('user-123');

RabbitMQ

import {
  createRabbitMQConnection,
  publish,
  consume,
  getNamespacedQueue
} from '@arcraz/common/rabbitmq';

// Create connection with reconnection options
const conn = await createRabbitMQConnection({
  host: 'localhost',
  port: 5672,
  username: 'guest',
  password: 'guest',
  appName: 'my-service',

  // Reconnection settings (all optional, shown with defaults)
  reconnectDelayMs: 5000,          // Base delay between attempts
  reconnectMaxDelayMs: 60000,      // Max delay cap (1 minute)
  reconnectBackoffMultiplier: 2,   // Exponential backoff multiplier
  maxReconnectAttempts: 10,        // Max attempts (0 = infinite)
});

// Queues are namespaced by NODE_ENV
// With NODE_ENV=production: "production-my-queue"
const queueName = getNamespacedQueue('my-queue');

await conn.channel.assertQueue(queueName, { durable: true });

// Publish messages
await publish(conn, 'my-queue', { event: 'user.created', data: { id: '123' } });

// Consume messages
await consume(conn, 'my-queue', async (msg, content) => {
  console.log('Received:', content);
});

// Register onReconnect callback to re-setup after connection recovery
conn.onReconnect(async () => {
  await conn.channel.assertQueue(queueName, { durable: true });
  await consume(conn, 'my-queue', handler);
  console.log('Consumer re-registered after reconnect');
});

// Cleanup
await conn.close();

Reconnection Behavior

When the broker connection drops, createRabbitMQConnection automatically reconnects with exponential backoff and jitter:

| Attempt | Delay (base=5s, multiplier=2) | |---------|-------------------------------| | 1 | ~5s + jitter | | 2 | ~10s + jitter | | 3 | ~20s + jitter | | 4 | ~40s + jitter | | 5+ | ~60s + jitter (capped) |

  • Jitter (0–1s random) prevents thundering herd when multiple services reconnect simultaneously
  • maxReconnectAttempts: 0 disables the attempt limit for infinite retries
  • onReconnect(callback) runs after each successful reconnection — use this to re-register consumers and re-assert queues, since the old channel is replaced on reconnect

Caching

import { createRedisClient } from '@arcraz/common/redis';
import { ApiCache, DatabaseCache } from '@arcraz/common/caches';

const redis = createRedisClient({ host: 'localhost' });

// API Cache - for caching external API responses
const apiCache = new ApiCache(redis, 'weather', async (city: string) => {
  const response = await fetch(`https://api.weather.com/${city}`);
  return response.json();
}, 3600); // 1 hour TTL

const weather = await apiCache.getCached('new-york');

// Database Cache - for caching query results
const dbCache = new DatabaseCache(redis, 'users', async (id: string) => {
  return userRepo.getById(id);
}, 300); // 5 minute TTL

const user = await dbCache.getCached('123');
await dbCache.deleteCached('123'); // Invalidate on update

Logging

import { createLogger } from '@arcraz/common/logging';

const logger = createLogger({
  level: 'info',
  format: 'json', // 'json' | 'logfmt' | 'pretty'
  service: 'my-service',
});

logger.info('Server started', { port: 3000 });
logger.error('Failed to connect', { error: err.message });

Security (Express Middleware)

import { createHelmetConfig, createCorsConfig } from '@arcraz/common/security';
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';

const app = express();

// Secure headers
app.use(helmet(createHelmetConfig()));

// CORS
app.use(cors(createCorsConfig({
  origins: ['https://myapp.com'],
  credentials: true,
})));

Errors

import {
  AppError,
  BadRequestError,
  NotFoundError,
  ValidationError,
  InternalError,
  isAppError
} from '@arcraz/common/errors';

// Throw typed HTTP errors with default status codes and messages
throw new NotFoundError('User not found');
throw new BadRequestError('Invalid email format');

// ValidationError supports field-level details
throw new ValidationError('Validation failed', {
  email: 'must be a valid email',
  password: 'must be at least 8 characters',
});

// InternalError defaults to isOperational=false (unexpected errors)
throw new InternalError('Database connection lost');

// Type guard for error handling middleware
app.use((err, req, res, next) => {
  if (isAppError(err)) {
    res.status(err.statusCode).json({
      code: err.code,
      message: err.message,
      details: err.details,
    });
  } else {
    res.status(500).json({ code: 'INTERNAL_ERROR', message: 'Something went wrong' });
  }
});

Available error classes:

| Class | Status | Code | isOperational | |-------|--------|------|---------------| | BadRequestError | 400 | BAD_REQUEST | true | | UnauthorizedError | 401 | UNAUTHORIZED | true | | ForbiddenError | 403 | FORBIDDEN | true | | NotFoundError | 404 | NOT_FOUND | true | | ConflictError | 409 | CONFLICT | true | | ValidationError | 422 | VALIDATION_ERROR | true | | RateLimitError | 429 | RATE_LIMIT_EXCEEDED | true | | InternalError | 500 | INTERNAL_ERROR | false |

Helpers

import {
  dataObscurer,
  removeSensitiveValues,
  redactSensitiveValues,
  convertToEnum,
  isValidEnumValue
} from '@arcraz/common/helpers';

// Mask sensitive data
const masked = dataObscurer('4111111111111111', { showLeft: 4, showRight: 4 });
// "4111********1111"

// Remove sensitive fields
const clean = removeSensitiveValues(
  { name: 'John', password: 'secret', ssn: '123-45-6789' },
  ['password', 'ssn']
);
// { name: 'John' }

// Redact instead of remove
const redacted = redactSensitiveValues(
  { name: 'John', password: 'secret' },
  ['password']
);
// { name: 'John', password: '[REDACTED]' }

// Enum conversion
enum Status { Active = 'ACTIVE', Inactive = 'INACTIVE' }
const status = convertToEnum('ACTIVE', Status); // Status.Active
const isValid = isValidEnumValue('ACTIVE', Status); // true

Socket.IO

Single-Namespace Server

import { createSocketIOServer } from '@arcraz/common/socketio';

const io = await createSocketIOServer({
  httpServer,
  cors: { origin: 'https://myapp.com', credentials: true },
  authVerify: async (auth) => {
    const user = await verifyToken(auth.token as string);
    return user; // attached to socket.data.user
  },
});

io.namespace.on('connection', (socket) => {
  io.trackConnection(socket.data.user.id, socket.id);
  socket.on('disconnect', () => io.untrackConnection(socket.id));
});

// Emit helpers
io.emitToUser('user-123', 'notification', { text: 'Hello' });
io.emitToRoom('lobby', 'message', { from: 'system', text: 'Welcome' });
io.broadcast('announcement', { text: 'Server restarting' });

Multi-Namespace Hub (Typed Events)

import { createSocketIOHub } from '@arcraz/common/socketio';
import type { EventMap } from '@arcraz/common/socketio';

// Define typed events per namespace
type ChatClientEvents = { sendMessage: (text: string) => void };
type ChatServerEvents = { newMessage: (msg: { from: string; text: string }) => void };

type NotifServerEvents = { alert: (data: { level: string; message: string }) => void };

const hub = await createSocketIOHub({
  httpServer,
  cors: { origin: 'https://myapp.com', credentials: true },
});

// Each namespace gets independent auth, events, tracking, and room management
const chat = hub.addNamespace<ChatClientEvents, ChatServerEvents, EventMap, User>({
  path: '/chat',
  authVerify: async (auth) => verifyToken(auth.token as string),
  eventHandlers: {
    sendMessage: (socket, text) => {
      chat.emitToRoom('general', 'newMessage', { from: socket.data.user.name, text });
    },
  },
  onConnection: (socket, roomManager) => {
    chat.trackConnection(socket.data.user.id, socket.id);
    roomManager.joinRoom(socket.id, 'channel', 'general');
  },
  onDisconnect: (socket) => {
    chat.untrackConnection(socket.id);
  },
});

const notifications = hub.addNamespace<EventMap, NotifServerEvents>({
  path: '/notifications',
});

// Typed emit — TypeScript enforces correct event names and argument types
chat.broadcast('newMessage', { from: 'system', text: 'Welcome!' });
notifications.emitToRoom('admins', 'alert', { level: 'warn', message: 'CPU high' });

// Room manager: structured naming
chat.roomManager.buildRoomName('workspace', 'w1', 'channel', 'general');
// -> 'workspace:w1:channel:general'

Types

import { Result, ok, err, isOk, isErr, unwrap, unwrapOr } from '@arcraz/common/types';

// Result type for error handling without exceptions
function divide(a: number, b: number): Result<number, string> {
  if (b === 0) return err('Division by zero');
  return ok(a / b);
}

const result = divide(10, 2);

if (isOk(result)) {
  console.log('Result:', unwrap(result)); // 5
}

// Or with default value
const value = unwrapOr(divide(10, 0), 0); // 0

Subpath Exports

Import only what you need for optimal tree-shaking:

import { ... } from '@arcraz/common';           // Everything
import { ... } from '@arcraz/common/config';    // Config & Zod schemas
import { ... } from '@arcraz/common/database';  // PostgreSQL, BaseRepository, EnhancedRepository
import { ... } from '@arcraz/common/redis';     // Redis client, operations, rate limiter
import { ... } from '@arcraz/common/rabbitmq';  // RabbitMQ
import { ... } from '@arcraz/common/caches';    // Caching utilities
import { ... } from '@arcraz/common/logging';   // Logger
import { ... } from '@arcraz/common/security';  // Helmet & CORS configs
import { ... } from '@arcraz/common/errors';    // AppError hierarchy & isAppError guard
import { ... } from '@arcraz/common/helpers';   // Utility functions
import { ... } from '@arcraz/common/types';     // TypeScript types & enums
import { ... } from '@arcraz/common/constants'; // Default values
import { ... } from '@arcraz/common/socketio';  // Socket.IO server, hub, room manager

Namespace Isolation

Redis and RabbitMQ resources are automatically namespaced by NODE_ENV to prevent collisions in shared environments:

| Service | Pattern | Example (NODE_ENV=staging) | |-----------|--------------------------|--------------------------------| | Redis | {NODE_ENV}:key | {staging}:user:123 | | RabbitMQ | NODE_ENV-queue | staging-notifications |

// Redis - curly braces ensure cluster slot compatibility
getNamespacedKey('user:123');           // "{staging}:user:123"
stripNamespace('{staging}:user:123');   // "user:123"
extractNamespace('{staging}:user:123'); // "staging"

// RabbitMQ
getNamespacedQueue('notifications');           // "staging-notifications"
getNamespacedExchange('events');               // "staging-events"
stripQueueNamespace('staging-notifications');  // "notifications"

Development

Scripts

yarn install    # Install dependencies
yarn lint       # Run ESLint
yarn test       # Run Vitest tests
yarn build      # Build to dist/

Testing

Tests use Vitest and are located in __tests__/:

yarn test              # Run all tests
yarn test:watch        # Watch mode
yarn test:coverage     # With coverage report

License

Apache-2.0 - See LICENSE for details.