npm package: @oncely/core
Description: Idempotency engine for HTTP APIs - ensures operations execute exactly once
Author: stacks0x
Latest version: 1.0.0

# @oncely/core

Core idempotency library with memory storage. The foundation for all oncely packages.

[![npm version](https://img.shields.io/npm/v/@oncely/core.svg)](https://www.npmjs.com/package/@oncely/core)

## Installation

```bash
npm install @oncely/core
```

## Quick Start

```typescript
import { MemoryStorage, hashObject, parseTtl } from '@oncely/core';

const storage = new MemoryStorage();

// Acquire a lock for a request
const result = await storage.acquire('request-key', hashObject(body), parseTtl('1h'));

if (result.status === 'acquired') {
  // Execute the operation
  const response = await processRequest(body);

  // Save the response
  await storage.save('request-key', {
    data: response,
    createdAt: Date.now(),
    hash: hashObject(body),
  });
}

if (result.status === 'hit') {
  // Return cached response
  return result.response.data;
}
```

## Storage Adapter

`MemoryStorage` is the default adapter, ideal for:

- Development and testing
- Single-instance deployments
- Prototyping

For production multi-instance deployments, use:

- [@oncely/redis](https://www.npmjs.com/package/@oncely/redis) - Standard Redis
- [@oncely/upstash](https://www.npmjs.com/package/@oncely/upstash) - Serverless Redis

### StorageAdapter Interface

```typescript
interface StorageAdapter {
  acquire(key: string, hash: string | null, ttl: number): Promise<AcquireResult>;
  save(key: string, response: StoredResponse): Promise<void>;
  release(key: string): Promise<void>;
  delete(key: string): Promise<void>;
  clear(): Promise<void>;
}

type AcquireResult =
  | { status: 'acquired' }
  | { status: 'hit'; response: StoredResponse }
  | { status: 'conflict'; startedAt: number }
  | { status: 'mismatch'; existingHash: string; providedHash: string };
```

## Utilities

### Hash Objects

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

const hash = hashObject({ amount: 100, currency: 'USD' });
// => 'a1b2c3d4...'
```

### Parse TTL

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

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

## Error Classes

```typescript
import {
  IdempotencyError,
  MissingKeyError, // 400 - Key required but missing
  ConflictError, // 409 - Request in progress
  MismatchError, // 422 - Key reused with different body
} from '@oncely/core';
```

All errors extend `IdempotencyError` and include:

- `statusCode` - HTTP status code
- `toProblemDetails()` - RFC 7807 Problem Details

## Constants

```typescript
import { IDEMPOTENCY_KEY_HEADER, IDEMPOTENCY_REPLAY_HEADER } from '@oncely/core';

IDEMPOTENCY_KEY_HEADER; // 'Idempotency-Key'
IDEMPOTENCY_REPLAY_HEADER; // 'Idempotency-Replay'
```

## Testing Utilities

```typescript
import { createTestStorage, MockStorage } from '@oncely/core/testing';

// Create a test storage with optional pre-seeded data
const storage = createTestStorage({
  'existing-key': { data: { id: 1 }, createdAt: Date.now(), hash: null },
});

// Or use MockStorage for fine-grained control
const mock = new MockStorage();
mock.simulateConflict('key');
mock.simulateError(new Error('Connection failed'));
```

## Related Packages

- [@oncely/express](https://www.npmjs.com/package/@oncely/express) - Express middleware
- [@oncely/next](https://www.npmjs.com/package/@oncely/next) - Next.js integration
- [@oncely/client](https://www.npmjs.com/package/@oncely/client) - Client utilities
- [@oncely/redis](https://www.npmjs.com/package/@oncely/redis) - Redis adapter
- [@oncely/upstash](https://www.npmjs.com/package/@oncely/upstash) - Upstash adapter

## License

MIT