npm package: @oncely/client
Description: Client-side idempotency helpers for browser and frontend applications
Author: stacks0x
Latest version: 1.0.0

# @oncely/client

Client-side utilities for idempotency key generation and response handling.

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

## Installation

```bash
npm install @oncely/client
```

## Quick Start

```typescript
import { generateKey } from '@oncely/client';

const response = await fetch('/api/orders', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Idempotency-Key': generateKey(),
  },
  body: JSON.stringify(order),
});
```

## Key Generation

```typescript
import { generateKey, generatePrefixedKey, createKeyGenerator } from '@oncely/client';

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

// Prefixed UUID
const key = generatePrefixedKey('ord');
// => 'ord_550e8400-e29b-41d4-a716-446655440000'

// Deterministic key from components
const key = generateKey('user', userId, 'create-order');
// => 'f8e9b4a2' (consistent hash for same inputs)

// Key generator namespace
const key = createKeyGenerator();
key(); // UUID
key('a', 'b', 'c'); // Deterministic hash
key.prefixed('txn'); // Prefixed UUID
```

## Key Store

Track pending requests to prevent duplicate submissions:

```typescript
import { createStore } from '@oncely/client';

const store = createStore({
  prefix: 'oncely:',
  ttl: '5m',
  storage: localStorage,
});

// Mark request as pending
store.pending('key-123');

// Check if request is pending
if (store.isPending('key-123')) {
  throw new Error('Request already in progress');
}

// Mark as completed
store.complete('key-123');

// Clear a specific key
store.clear('key-123');

// Clear all oncely keys
store.clearAll();
```

## Response Helpers

```typescript
import { isReplay, isConflict, isMismatch, getRetryAfter } from '@oncely/client';

const response = await fetch('/api/orders', { ... });

// Check if response was served from cache
if (isReplay(response)) {
  console.log('Cached response');
}

// Handle 409 Conflict
if (isConflict(response)) {
  const seconds = getRetryAfter(response);
  await delay(seconds * 1000);
  // Retry with same key...
}

// Handle 422 Mismatch
if (isMismatch(response)) {
  // Key was reused with different body - generate new key
}
```

## Usage with Fetch Libraries

### ky

```typescript
import ky from 'ky';
import { generateKey, IDEMPOTENCY_KEY_HEADER } from '@oncely/client';

const api = ky.extend({
  hooks: {
    beforeRequest: [
      (request) => {
        if (['POST', 'PUT', 'PATCH'].includes(request.method)) {
          request.headers.set(IDEMPOTENCY_KEY_HEADER, generateKey());
        }
      },
    ],
  },
});
```

### axios

```typescript
import axios from 'axios';
import { generateKey, IDEMPOTENCY_KEY_HEADER } from '@oncely/client';

const api = axios.create();

api.interceptors.request.use((config) => {
  if (['post', 'put', 'patch'].includes(config.method ?? '')) {
    config.headers[IDEMPOTENCY_KEY_HEADER] = generateKey();
  }
  return config;
});
```

## Constants

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

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

## License

MIT