# ⚡️ Limitem JavaScript SDK

<p align="center">
  <img src="https://img.shields.io/npm/v/limitem?color=crimson&label=%F0%9F%93%85%20npm" alt="npm version" />
  <img src="https://img.shields.io/bundlephobia/minzip/limitem?color=blueviolet&label=%F0%9F%92%BE%20gzip+min" alt="bundle size" />
  <img src="https://img.shields.io/npm/l/limitem?color=brightgreen&label=%F0%9F%93%9C%20license" alt="license" />
</p>

A ✨ **tiny** client for the [Limitem](https://limitem.com) rate limiting API. 

---

## 📚 Table of Contents

1. 🚀 [Install](#-install)
2. 🏃‍♂️ [Quick Start](#-quick-start)
3. 🔍 [API Reference](#-api-reference)
4. 🍳 [Usage Recipes](#-usage-recipes)
5. 🛠️ [Error Handling](#️-error-handling)
6. 🧾 [License](#-license)

---

## 🚀 Install

```bash
npm install limitem       # npm
pnpm add limitem          # pnpm
yarn add limitem          # yarn
bun i limitem             # bun
```

> 💡 **Tip:** The package is ESM only. If you are on an older Node.js version (<14), consider upgrading or using `import()`.

---

## 🏃‍♂️ Quick Start

```js
import { limit } from "limitem";

// 1. Create a limiter instance
const ratelimit = new limit.em({
  token: process.env.LIMITEM_TOKEN,   // or pass it directly
  namespace: "my-app",               // required
  limit: 100,                         // requests
  duration: "1h",                    // 1 hour window (or number in seconds)
  // Everything below is optional ↓
  product: "specific-api",           // group limits per product
  algorithm: "sliding-window"        // or "fixed-window", "token-bucket", "leaky-bucket"
});

// 2. Guard your request ✋
const result = await ratelimit.limit("user:123");

if (result.success) {
  console.log("🎉 Request allowed – remaining:", result.remaining);
} else {
  console.log("⛔️ Rate limit exceeded – retry at", new Date(result.resetTime));
}
```

---

## 🔍 API Reference

### `new limit.em(options)`

| Option      | Type               | Required | Description                                                          |
| ----------- | ------------------ | -------- | -------------------------------------------------------------------- |
| `token`     | `string`           | ➖       | Limitem API key. Falls back to `LIMITEM_TOKEN` / `LIMITEM_KEY`.      |
| `namespace` | `string`           | ✅       | Application namespace used to bucket your limits.                    |
| `limit`     | `number`           | ✅       | Max requests allowed in one window.                                  |
| `duration`  | `string \| number` | ✅       | Window size (`"30s"`, `"5m"`, `"1h"` or seconds).                   |
| `product`   | `string`           | ➖       | Fine-grained sub-category (e.g. `"email"`).                           |
| `algorithm` | `string`           | ➖       | One of `"sliding-window"` *(default)*, `"fixed-window"`, `"token-bucket"`, `"leaky-bucket"`. |
| `timeout`   | `number`           | ➖       | Abort Limitem requests after _(ms)_. Defaults to `800`.              |

### `ratelimit.limit(identifier)`

Consume **one** unit from the quota linked to `identifier` (e.g. *user-id*, *IP-address*).

Returns a promise that resolves to:

```ts
{
  success: boolean;      // ✅/⛔️ request allowed?
  app: string;           // namespace
  product?: string;      // optional product bucket
  limit: number;         // max allowed per window
  remaining: number;     // how many calls still left
  resetTime: number;     // unix ms when window resets
  count: number;         // current consumption this window
  algorithm: string;     // algorithm in effect
  failover?: true;       // present when Limitem unreachable (fail-open)
}
```

> 🏗 **Fail-open design:** If Limitem is unreachable or times out, the SDK returns `success: true` to keep your app 🟢 online. `failover: true` tells you this happened.

---

## 🍳 Usage Recipes

### Different Algorithms 🤹‍♂️

```js
import { limit } from "limitem";

const sliding = new limit.em({ token: t, namespace: "app", limit: 100, duration: "1h" });
const fixed   = new limit.em({ token: t, namespace: "app", limit: 100, duration: "1h", algorithm: "fixed-window" });
const bucket  = new limit.em({ token: t, namespace: "app", limit: 100, duration: "1h", algorithm: "token-bucket" });
const leaky   = new limit.em({ token: t, namespace: "app", limit: 100, duration: "1h", algorithm: "leaky-bucket" });

await sliding.limit("user:1");
await fixed.limit("user:2");
```

### Product-level Limits 🛍

```js
const sms   = new limit.em({ token: t, namespace: "my-app", product: "sms", limit: 50,  duration: "1h" });
const email = new limit.em({ token: t, namespace: "my-app", product: "email", limit: 200, duration: "1h" });

await sms.limit("user:123");   // sms quota
await email.limit("user:123"); // email quota
```

### Multiple Users 👥

```js
const limiter = new limit.em({ token: t, namespace: "my-app", limit: 100, duration: "1h" });

await limiter.limit("user:123");
await limiter.limit("user:456");
await limiter.limit("ip:192.168.0.1");
```

---

## 🛠️ Error Handling

The SDK throws **typed errors** for:

- `401` Invalid API key 🔑
- `400` Bad request (malformed payload) 📄
- `500` Server error 💥
- Network issues 🌐
- Invalid `duration` format ⏱️

```js
try {
  await limiter.limit("user:123");
} catch (err) {
  if (err.message.includes("401")) console.error("❌ Invalid API key");
  else if (err.message.includes("Invalid duration")) console.error("🕰 Bad duration format");
  else console.error("⚠️ Other error:", err.message);
}
```

---

## 🧾 License

Released under the **MIT** license. Made with ❤️ by [Contiguity](https://contiguity.com)