🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@oncely/core

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install
Package was removed
Sorry, it seems this package was removed from the registry

@oncely/core

Idempotency engine for HTTP APIs - ensures operations execute exactly once

Source
npmnpm
Version
0.2.1
Version published
Weekly downloads
0
Maintainers
1
Weekly downloads
 
Created
Source

@oncely/core

Core idempotency library with memory storage adapter. Provides the lean kernel with full TypeScript support.

Installation

npm install @oncely/core

Quick Start

import { oncely, MemoryStorage } from '@oncely/core';

// Configure globally
oncely.configure({ ttl: '1h' });

// Create an instance
const idempotency = oncely.createInstance({
  storage: new MemoryStorage(),
});

// Run with idempotency
const result = await idempotency.run({
  key: 'unique-request-id',
  handler: async () => {
    return await createOrder({ amount: 100 });
  },
});

console.log(result.data); // { id: 'ord_123' }
console.log(result.cached); // false (first request)
console.log(result.createdAt); // 1704888000000

API Reference

oncely Namespace

Global namespace for configuration and instance creation.

// Configure global defaults
oncely.configure({
  storage: new MemoryStorage(),
  ttl: '24h',
  debug: false,
});

// Get current configuration
const config = oncely.getConfig();

// Reset configuration
oncely.resetConfig();

// Get default storage
const storage = oncely.getDefaultStorage();

// Create instance with merged config
const instance = oncely.createInstance({
  ttl: '1h', // Override global ttl
});

// Constants
oncely.HEADER; // 'Idempotency-Key'
oncely.HEADER_REPLAY; // 'Idempotency-Replay'

idempotency.run(options)

Execute a handler with idempotency protection.

interface RunOptions<T = any> {
  key: string; // Idempotency key (required)
  hash?: string; // Request hash for mismatch detection
  handler: () => Promise<T>; // Async function to execute
  ttl?: number | string; // Override global TTL
}

interface RunResult<T = any> {
  data: T; // Handler return value
  cached: boolean; // Was this a cache hit?
  status: 'created' | 'hit'; // Result status
  createdAt: number; // Timestamp of original request
}

const result = await idempotency.run({
  key: 'order-123',
  hash: hashObject(req.body),
  handler: async () => {
    const order = await createOrder(req.body);
    return order;
  },
  ttl: '2h',
});

Configuration

Global Configuration

oncely.configure({
  // Storage adapter (required)
  storage: new MemoryStorage(),

  // Time-to-live for cached responses
  // Accepts: milliseconds (number) or string ('1h', '30m', '5s')
  ttl: '24h',

  // Enable debug logging
  debug: false,

  // Callbacks
  onHit: (key, response) => {
    console.log(`Cache hit for key: ${key}`);
  },
  onMiss: (key) => {
    console.log(`Cache miss for key: ${key}`);
  },
  onConflict: (key, error) => {
    console.log(`Conflict for key: ${key}`);
  },
});

Per-Instance Configuration

const instance = oncely.createInstance({
  storage: redis(),
  ttl: '1h',
  onHit: (key) => analytics.track('cache_hit', { key }),
});

Error Handling

All errors are instances of IdempotencyError and include a statusCode.

import {
  IdempotencyError,
  MissingKeyError,
  ConflictError,
  MismatchError,
  StorageError,
} from '@oncely/core';

try {
  const result = await idempotency.run({ key, handler });
} catch (error) {
  if (error instanceof MissingKeyError) {
    // status: 400
    // Idempotency key is required but not provided
    return new Response('Idempotency-Key header required', { status: 400 });
  }

  if (error instanceof ConflictError) {
    // status: 409
    // Request with this key is already in progress
    return new Response('Request in progress', {
      status: 409,
      headers: { 'Retry-After': String(error.retryAfter ?? 30) },
    });
  }

  if (error instanceof MismatchError) {
    // status: 422
    // Same key used with different request body
    return new Response('Idempotency key mismatch', {
      status: 422,
      headers: {
        'Content-Type': 'application/problem+json',
      },
      body: JSON.stringify({
        type: 'https://oncely.dev/errors/mismatch',
        title: 'Idempotency Key Mismatch',
        detail: `Key was previously used with different request body`,
        existingHash: error.existingHash,
        providedHash: error.providedHash,
      }),
    });
  }

  if (error instanceof StorageError) {
    // status: 500
    // Storage adapter failed
    console.error('Storage error:', error);
    return new Response('Storage error', { status: 500 });
  }

  // Other errors
  throw error;
}

Utilities

Key Generation

import { generateKey, composeKey } from '@oncely/core';

// Generate UUID-based key
const key = generateKey();
// => '550e8400-e29b-41d4-a716-446655440000'

// Compose keys from parts
const key = composeKey('order', orderId, 'attempt', attemptNumber);
// => 'order:123:attempt:1'

Hashing

import { hashObject } from '@oncely/core';

// Hash request body for mismatch detection
const hash = hashObject(req.body);
// => 'f8e9b4a2c1d5e3f6...'

// Works with any serializable object
const hash = hashObject({
  amount: 100,
  currency: 'USD',
  items: [{ id: 1, qty: 2 }],
});

TTL Parsing

import { parseTtl } from '@oncely/core';

parseTtl(3600000); // 3600000 (milliseconds)
parseTtl('1h'); // 3600000
parseTtl('30m'); // 1800000
parseTtl('5s'); // 5000
parseTtl('1d'); // 86400000

// Invalid values throw Error
parseTtl('invalid'); // throws

Storage Adapters

Memory (Built-in, Default)

In-memory storage for development and testing.

import { oncely, MemoryStorage } from '@oncely/core';

// Implicit (default)
const idempotency = oncely.createInstance();

// Explicit
const storage = new MemoryStorage();
const idempotency = oncely.createInstance({ storage });

// Shared singleton
import { memory } from '@oncely/core';
const idempotency = oncely.createInstance({ storage: memory });

Pros:

  • Zero configuration
  • Fast (no I/O)
  • Easy to test

Cons:

  • Data lost on restart
  • Not shared between processes
  • Only suitable for single-instance apps

Redis (Production)

For production deployments with ioredis.

npm install @oncely/redis ioredis
import { redis } from '@oncely/redis';

// From environment variable (ONCELY_REDIS_URL)
const storage = redis();

// From explicit URL
const storage = redis('redis://localhost:6379');

// With options
const storage = redis('redis://localhost:6379', {
  keyPrefix: 'oncely:',
});

// From existing client
import Redis from 'ioredis';
const client = new Redis(process.env.REDIS_URL);
const storage = new RedisStorage(client, { keyPrefix: 'oncely:' });

See @oncely/redis for full documentation.

Upstash (Serverless)

For serverless and edge deployments.

npm install @oncely/upstash
import { upstash } from '@oncely/upstash';

// From environment variables
const storage = upstash();

// With explicit config
const storage = upstash({
  url: process.env.UPSTASH_REDIS_REST_URL,
  token: process.env.UPSTASH_REDIS_REST_TOKEN,
});

See @oncely/upstash for full documentation.

Custom Adapters

Implement the StorageAdapter interface for custom backends.

import type { StorageAdapter, AcquireResult, StoredResponse } from '@oncely/core';

export class DatabaseStorage implements StorageAdapter {
  async acquire(key: string, hash: string | null, ttl: number): Promise<AcquireResult> {
    // Acquire lock and check for existing response
    // Return { status: 'acquired', startedAt: Date.now() }
    // Or { status: 'conflict', startedAt, retryAfter }
    // Or { status: 'mismatch', existingHash, providedHash }
  }

  async save(key: string, response: StoredResponse): Promise<void> {
    // Save response and release lock
  }

  async release(key: string): Promise<void> {
    // Release lock without saving
  }

  async delete(key: string): Promise<void> {
    // Optional: delete stored response
  }

  async clear(): Promise<void> {
    // Optional: clear all stored responses
  }
}

// Usage
const storage = new DatabaseStorage();
const idempotency = oncely.createInstance({ storage });

Testing

The library includes testing utilities via @oncely/core/testing.

import { MockStorage, createTestInstance } from '@oncely/core/testing';

// Use MockStorage to track operations
const storage = new MockStorage();
const idempotency = createTestInstance({ storage });

// Run handler
await idempotency.run({
  key: 'test-key',
  handler: async () => ({ id: 1 }),
});

// Verify operations
const operations = storage.operations;
// [
//   { type: 'acquire', key: 'test-key', timestamp: ... },
//   { type: 'save', key: 'test-key', timestamp: ... },
// ]

const saveOps = storage.getOperationsForKey('test-key', 'save');
expect(saveOps).toHaveLength(1);

Callbacks

Hooks for monitoring and logging.

oncely.configure({
  onHit: (key, response) => {
    console.log(`Cache hit: ${key}`);
    analytics.track('idempotency_hit', { key });
  },

  onMiss: (key) => {
    console.log(`Cache miss: ${key}`);
    analytics.track('idempotency_miss', { key });
  },

  onConflict: (key, error) => {
    console.log(`Conflict: ${key}`);
    metrics.increment('idempotency_conflicts');
  },
});

Type Definitions

Full TypeScript support with strict typing.

interface Oncely {
  configure(options: OncelyConfig): void;
  getConfig(): OncelyConfig;
  resetConfig(): void;
  getDefaultStorage(): StorageAdapter;
  createInstance(options?: Partial<OncelyOptions>): OncelyInstance;
  HEADER: string;
  HEADER_REPLAY: string;
}

interface OncelyOptions {
  storage: StorageAdapter;
  ttl: number | string;
  debug: boolean;
  onHit?: (key: string, response: StoredResponse) => void;
  onMiss?: (key: string) => void;
  onConflict?: (key: string, error: ConflictError) => void;
}

interface OncelyInstance {
  run<T>(options: RunOptions<T>): Promise<RunResult<T>>;
}

Performance

  • Small bundle: ~11 KB (ESM)
  • No external dependencies
  • Tree-shakeable exports
  • Efficient hashing (SHA256)
  • Minimal memory overhead

FAQ

Q: Should I use memory storage in production?
A: Only for single-instance deployments. Use Redis or Upstash for distributed systems.

Q: How do I generate idempotency keys on the client?
A: See @oncely/client package.

Q: Can I customize error responses?
A: Yes, catch errors and format responses as needed. Framework packages (Express, Next.js) handle this automatically.

Q: How do I migrate from another idempotency library?
A: See migration guide in root README.

License

MIT © stacks0x

import { fromEnv } from 'oncely';

// Read REDIS_URL or UPSTASH_REDIS_REST_URL + TOKEN
const storage = fromEnv();

// With options
const storage = fromEnv({
  preferUpstash: true, // Prefer Upstash when both configured
  env: {
    redisUrl: 'MY_REDIS_URL', // Custom env var names
    upstashUrl: 'MY_UPSTASH_URL',
    upstashToken: 'MY_UPSTASH_TOKEN',
  },
});

redis(url, options?)

Quick helper to create Redis storage from a URL.

import { redis } from 'oncely';

const storage = redis('redis://localhost:6379');
const storage = redis(process.env.REDIS_URL!, { keyPrefix: 'myapp:' });

idempotency.run(options)

Run an operation with idempotency protection.

const result = await idempotency.run({
  key: 'unique-key', // Required: idempotency key
  hash: 'request-hash', // Optional: for mismatch detection
  handler: async () => {
    // Required: your operation
    return { id: 1 };
  },
});

// result.data     - your handler's return value
// result.cached   - boolean, was this a cache hit?
// result.status   - 'created' | 'hit'
// result.createdAt - when the response was created

Errors

All errors extend IdempotencyError and include a statusCode property.

ErrorStatusWhen
MissingKeyError400Key is undefined/null
ConflictError409Same key already in progress
MismatchError422Same key, different hash
StorageError500Storage adapter failed
import { IdempotencyError, ConflictError } from 'oncely';

try {
  await idempotency.run({ key, handler });
} catch (err) {
  if (err instanceof ConflictError) {
    // Return 409 with Retry-After header
    return new Response('Request in progress', {
      status: 409,
      headers: { 'Retry-After': String(err.retryAfter) },
    });
  }
}

Utilities

import { generateKey, composeKey, hashObject } from 'oncely';

// Generate a UUID-like key
const key = generateKey(); // "f47ac10b-58cc-4372-a567-0e02b2c3d479"

// Compose a deterministic key
const key = composeKey('user', userId, 'order', orderId); // "user:123:order:456"

// Hash an object for fingerprinting
const hash = hashObject({ amount: 100, currency: 'usd' });

Storage Adapters

Memory (Built-in, Default)

For development and testing. Used automatically when no storage is configured:

import { oncely, memory, MemoryStorage } from 'oncely';

// Zero-config — uses shared memory
const idempotency = oncely();

// Explicit memory storage
const idempotency = oncely({ storage: memory });

// Custom instance
const myStorage = new MemoryStorage();
const idempotency = oncely({ storage: myStorage });

Redis with ioredis

For production with self-hosted Redis or managed services:

npm install ioredis
import { oncely, redis, ioredis } from 'oncely';
import Redis from 'ioredis';

// Quick setup from URL
const idempotency = oncely({
  storage: redis('redis://localhost:6379'),
});

// With existing client
const client = new Redis(process.env.REDIS_URL);
const idempotency = oncely({
  storage: ioredis(client, { keyPrefix: 'idempotent:' }),
});

Upstash / Vercel KV

For serverless environments:

npm install @upstash/redis
import { oncely, upstash } from 'oncely';
import { Redis } from '@upstash/redis';

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

const idempotency = oncely({
  storage: upstash(redis, { keyPrefix: 'idempotent:' }),
});

Custom Adapters

Implement the StorageAdapter interface:

import type { StorageAdapter } from 'oncely';

const myAdapter: StorageAdapter = {
  async acquire(key, hash, ttl) {
    // Try to acquire lock, return { acquired: true/false, response?: cached }
  },
  async save(key, response) {
    // Save response, release lock
  },
  async release(key) {
    // Release lock without saving
  },
  async delete(key) {
    // Delete stored response (optional)
  },
  async clear() {
    // Clear all stored responses (optional)
  },
};

License

MIT

Keywords

idempotency

FAQs

Package last updated on 24 Jan 2026

Did you know?

Socket

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.

Install

Related posts