# @vercel/kv2

A type-safe key-value store backed by Vercel Blob with edge caching and copy-on-write branch isolation.

## Installation

```bash
npm install @vercel/kv2
# or
pnpm add @vercel/kv2
```

## Features

- **Edge caching** - Vercel Runtime Cache for low-latency reads
- **Copy-on-write** - Preview branches inherit from production, writes stay local
- **Typed stores** - Type-safe sub-stores with automatic key prefixing
- **Concurrent iteration** - `entries()` and `getMany()` with bounded concurrency
- **Pagination** - Cursor-based pagination for HTTP APIs
- **Schema & Tree** - Hierarchical data with batched tree loading
- **Streaming** - Large values streamed without buffering

## Quick Start

```typescript
import { createKV } from "@vercel/kv2";

// Creates KV with automatic upstream fallback to production/main
const kv = createKV({ prefix: "myapp/" });

interface User {
  name: string;
  email: string;
}

// Type-safe sub-store
const users = kv.getStore<User>("users/");

await users.set("alice", { name: "Alice", email: "alice@example.com" });

const result = await users.get("alice");
if (result.exists) {
  console.log((await result.value).name); // "Alice"
}
```

## Iterating Entries

Fetch multiple values concurrently:

```typescript
// Iterate with concurrent fetching (default: 20 concurrent)
for await (const [key, entry] of users.entries()) {
  console.log(key, await entry.value);
}

// With prefix filter
for await (const [key, entry] of users.entries("active/")) {
  console.log(key, entry.metadata);
}

// Fetch specific keys concurrently
const results = await users.getMany(["alice", "bob", "charlie"]);
for (const [key, entry] of results) {
  console.log(key, await entry.value);
}
```

## Pagination

For HTTP APIs, use cursor-based pagination:

```typescript
// GET /api/users?cursor=xxx
export async function GET(req: Request) {
  const url = new URL(req.url);
  const cursor = url.searchParams.get("cursor") ?? undefined;

  // Get a page of keys with cursor
  const { keys, cursor: nextCursor } = await users.keys().page(20, cursor);

  // Fetch values for those keys
  const entries = await users.getMany(keys);

  return Response.json({
    users: keys.map((k) => entries.get(k)?.value),
    nextCursor,
  });
}
```

Both `keys()` and `entries()` support pagination:

```typescript
// Paginate keys
const { keys, cursor } = await kv.keys("users/").page(10, cursor);

// Paginate entries (fetches values concurrently)
const { entries, cursor } = await kv.entries("users/").page(10, cursor);
for (const [key, entry] of entries) {
  console.log(key, await entry.value);
}
```

## Typed Stores

Create nested, type-safe stores:

```typescript
interface Post {
  title: string;
  content: string;
}

const posts = kv.getStore<Post>("posts/");

await posts.set("hello-world", { title: "Hello", content: "World" });

// Keys are relative to the store
for await (const key of posts.keys()) {
  console.log(key); // "hello-world" (not "posts/hello-world")
}

// Nested stores accumulate prefixes
const drafts = posts.getStore<Post>("drafts/");
await drafts.set("my-draft", { title: "Draft", content: "..." });
// Actual key: "posts/drafts/my-draft"
```

## Copy-on-Write Branches

Preview deployments automatically inherit from production:

```typescript
// On preview branch "feature-x":
const kv = createKV({ prefix: "app/" });

// Read falls back to production/main if not found locally
const user = await kv.get("users/alice"); // Found in production

// Write only affects this branch
await kv.set("users/alice", { name: "Alice Updated" });

// Delete creates tombstone (won't fall back to production)
await kv.delete("users/bob");
```

Configure upstream explicitly:

```typescript
const kv = createKV({
  prefix: "app/",
  upstream: { env: "production", branch: "main" }, // default
  // upstream: null, // disable fallback
});
```

## Optimistic Locking

Safely update values with conflict detection using versions (etags):

```typescript
const entry = await users.get("alice");
if (entry.exists) {
  const user = await entry.value;
  user.name = "Alice Updated";

  // Update using the version from when we read the entry
  // Throws KVVersionConflictError if another process modified it
  await entry.update(user);
}
```

Manual version control with `expectedVersion`:

```typescript
const { version } = await kv.set("counter", { count: 0 }, metadata);

// Later, conditionally update
await kv.set("counter", { count: 1 }, metadata, { expectedVersion: version });
```

Create-only (fails if key exists):

```typescript
await kv.set("user/new-id", userData, metadata, { override: false });
```

Retry pattern for concurrent updates:

```typescript
import { KVVersionConflictError } from "@vercel/kv2";

async function incrementCounter(key: string): Promise<number> {
  for (let attempt = 0; attempt < 3; attempt++) {
    const entry = await kv.get<{ count: number }>(key);
    if (!entry.exists) throw new Error("Key not found");

    const current = await entry.value;
    try {
      await entry.update({ count: current.count + 1 });
      return current.count + 1;
    } catch (error) {
      if (error instanceof KVVersionConflictError) continue;
      throw error;
    }
  }
  throw new Error("Max retries exceeded");
}
```

## With Metadata

Track metadata per entry:

```typescript
interface Metadata {
  updatedAt: number;
  version: number;
}

const kv = createKV<Metadata>({ prefix: "app/" });
const users = kv.getStore<User>("users/");

// Metadata required on set
await users.set("alice", { name: "Alice" }, { updatedAt: Date.now(), version: 1 });

const result = await users.get("alice");
if (result.exists) {
  console.log(result.metadata.version); // 1
}
```

## Schema and Tree

For hierarchical data, define a schema and fetch trees with batched concurrent loading:

```typescript
import { defineSchema, createSchemaKV, createKV } from "@vercel/kv2";

interface Board { name: string }
interface Column { name: string; order: number }
interface Task { title: string; done: boolean }

const schema = defineSchema("kanban/", {
  boards: {
    pattern: "*",
    value: {} as Board,
    children: {
      columns: {
        pattern: "columns/*",
        value: {} as Column,
        children: {
          tasks: { pattern: "tasks/*", value: {} as Task }
        }
      }
    }
  }
});

const kv = createKV({ prefix: "kanban/" });
const kanban = createSchemaKV(schema, kv);

// Type-safe key builders
kanban.key.boards("board-1");                      // "board-1"
kanban.key.columns("board-1", "col-1");            // "board-1/columns/col-1"
kanban.key.tasks("board-1", "col-1", "task-1");    // "board-1/columns/col-1/tasks/task-1"

// Fetch entire tree with batched concurrent loading
const board = await kanban.tree("boards", "board-1");
console.log(board.value.name);

// Lazy async iteration over children
for await (const column of board.columns) {
  console.log(column.value.name);
  for await (const task of column.tasks) {
    console.log(task.value.title);
  }
}
```

The `tree()` method makes a single `keys()` call to discover all descendants, then uses `getMany()` to fetch values with bounded concurrency.

## API

### `createKV<M>(options)`

Creates a KV store with automatic environment detection and upstream fallback.

| Option | Default | Description |
|--------|---------|-------------|
| `prefix` | `""` | Key prefix (must end with `/`) |
| `upstream` | `{ env: "production", branch: "main" }` | Fallback config or `null` |
| `env` | `VERCEL_ENV` | Environment name |
| `branch` | `VERCEL_GIT_COMMIT_REF` | Branch name |

### `KVLike<M>` Interface

All stores implement this interface:

```typescript
interface KVLike<M> {
  get<V>(key: string): Promise<KVGetResult<V, M>>;
  set<V>(key: string, value: V, metadata?: M, options?: SetOptions): Promise<KVSetResult>;
  delete(key: string): Promise<void>;
  keys(prefix?: string): KeysIterable;
  entries<V>(prefix?: string): EntriesIterable<V, M>;
  getMany<V>(keys: string[], concurrency?: number): Promise<Map<string, KVEntry<V, M>>>;
}

interface SetOptions {
  expectedVersion?: string;  // Only succeed if current version matches
  override?: boolean;        // Allow overwriting (default: true)
}

interface KVSetResult {
  version: string;  // etag of the written blob
}

interface KeysIterable extends AsyncIterable<string> {
  page(limit: number, cursor?: string): Promise<{ keys: string[]; cursor?: string }>;
}

interface EntriesIterable<V, M> extends AsyncIterable<[string, KVEntry<V, M>]> {
  page(limit: number, cursor?: string): Promise<{ entries: [string, KVEntry<V, M>][]; cursor?: string }>;
}
```

### Get Result

```typescript
const result = await store.get("key");
if (result.exists) {
  result.metadata;      // M - immediate
  result.version;       // string - etag for optimistic locking
  await result.value;   // V - lazy
  await result.stream;  // ReadableStream<Uint8Array>

  // Conditionally update using captured version
  await result.update(newValue);  // throws KVVersionConflictError on conflict
}
```

## Environment Variables

```
BLOB_READ_WRITE_TOKEN=vercel_blob_...
```

## Testing

```bash
pnpm test              # Unit tests (fake blob store)
pnpm test:integration  # Integration tests (real Vercel Blob)
```
