@vercel/kv2 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -0
- package/SKILL.md +65 -0
- package/dist/blob-format.d.ts +35 -0
- package/dist/blob-format.d.ts.map +1 -0
- package/dist/blob-format.js +91 -0
- package/dist/blob-format.js.map +1 -0
- package/dist/blob-store.d.ts +11 -0
- package/dist/blob-store.d.ts.map +1 -0
- package/dist/blob-store.js +32 -0
- package/dist/blob-store.js.map +1 -0
- package/dist/cache.d.ts +33 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +146 -0
- package/dist/cache.js.map +1 -0
- package/dist/cached-kv.d.ts +63 -0
- package/dist/cached-kv.d.ts.map +1 -0
- package/dist/cached-kv.js +891 -0
- package/dist/cached-kv.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +342 -0
- package/dist/cli.js.map +1 -0
- package/dist/create-kv.d.ts +86 -0
- package/dist/create-kv.d.ts.map +1 -0
- package/dist/create-kv.js +125 -0
- package/dist/create-kv.js.map +1 -0
- package/dist/disk-cache.d.ts.map +1 -0
- package/dist/disk-cache.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/indexed-kv.d.ts +44 -0
- package/dist/indexed-kv.d.ts.map +1 -0
- package/dist/indexed-kv.js +373 -0
- package/dist/indexed-kv.js.map +1 -0
- package/dist/manifest-log.d.ts +57 -0
- package/dist/manifest-log.d.ts.map +1 -0
- package/dist/manifest-log.js +128 -0
- package/dist/manifest-log.js.map +1 -0
- package/dist/memory-cache.d.ts +22 -0
- package/dist/memory-cache.d.ts.map +1 -0
- package/dist/memory-cache.js +90 -0
- package/dist/memory-cache.js.map +1 -0
- package/dist/proxy-cache.d.ts +40 -0
- package/dist/proxy-cache.d.ts.map +1 -0
- package/dist/proxy-cache.js +124 -0
- package/dist/proxy-cache.js.map +1 -0
- package/dist/readme.test.d.ts +9 -0
- package/dist/readme.test.d.ts.map +1 -0
- package/dist/readme.test.js +285 -0
- package/dist/readme.test.js.map +1 -0
- package/dist/schema/define-schema.d.ts +35 -0
- package/dist/schema/define-schema.d.ts.map +1 -0
- package/dist/schema/define-schema.js +70 -0
- package/dist/schema/define-schema.js.map +1 -0
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +5 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/key-builders.d.ts +40 -0
- package/dist/schema/key-builders.d.ts.map +1 -0
- package/dist/schema/key-builders.js +124 -0
- package/dist/schema/key-builders.js.map +1 -0
- package/dist/schema/schema-kv.d.ts +48 -0
- package/dist/schema/schema-kv.d.ts.map +1 -0
- package/dist/schema/schema-kv.js +96 -0
- package/dist/schema/schema-kv.js.map +1 -0
- package/dist/schema/tree.d.ts +14 -0
- package/dist/schema/tree.d.ts.map +1 -0
- package/dist/schema/tree.js +135 -0
- package/dist/schema/tree.js.map +1 -0
- package/dist/schema/types.d.ts +135 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +2 -0
- package/dist/schema/types.js.map +1 -0
- package/dist/testing/core-tests.d.ts +30 -0
- package/dist/testing/core-tests.d.ts.map +1 -0
- package/dist/testing/core-tests.js +383 -0
- package/dist/testing/core-tests.js.map +1 -0
- package/dist/testing/create-kv-test-setup.d.ts +21 -0
- package/dist/testing/create-kv-test-setup.d.ts.map +1 -0
- package/dist/testing/create-kv-test-setup.js +25 -0
- package/dist/testing/create-kv-test-setup.js.map +1 -0
- package/dist/testing/debug-manifest.d.ts +2 -0
- package/dist/testing/debug-manifest.d.ts.map +1 -0
- package/dist/testing/debug-manifest.js +14 -0
- package/dist/testing/debug-manifest.js.map +1 -0
- package/dist/testing/fake-blob-store.d.ts +23 -0
- package/dist/testing/fake-blob-store.d.ts.map +1 -0
- package/dist/testing/fake-blob-store.js +158 -0
- package/dist/testing/fake-blob-store.js.map +1 -0
- package/dist/testing/fake-cache.d.ts +54 -0
- package/dist/testing/fake-cache.d.ts.map +1 -0
- package/dist/testing/fake-cache.js +137 -0
- package/dist/testing/fake-cache.js.map +1 -0
- package/dist/testing/index.d.ts +34 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +101 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/manifest-test-setup.d.ts +22 -0
- package/dist/testing/manifest-test-setup.d.ts.map +1 -0
- package/dist/testing/manifest-test-setup.js +43 -0
- package/dist/testing/manifest-test-setup.js.map +1 -0
- package/dist/testing/perf-test.d.ts +13 -0
- package/dist/testing/perf-test.d.ts.map +1 -0
- package/dist/testing/perf-test.js +101 -0
- package/dist/testing/perf-test.js.map +1 -0
- package/dist/testing/run-tests.d.ts +2 -0
- package/dist/testing/run-tests.d.ts.map +1 -0
- package/dist/testing/run-tests.js +141 -0
- package/dist/testing/run-tests.js.map +1 -0
- package/dist/testing/setup.d.ts +2 -0
- package/dist/testing/setup.d.ts.map +1 -0
- package/dist/testing/setup.js +3 -0
- package/dist/testing/setup.js.map +1 -0
- package/dist/testing/test-index.d.ts +28 -0
- package/dist/testing/test-index.d.ts.map +1 -0
- package/dist/testing/test-index.js +35 -0
- package/dist/testing/test-index.js.map +1 -0
- package/dist/testing/test-setup.d.ts +32 -0
- package/dist/testing/test-setup.d.ts.map +1 -0
- package/dist/testing/test-setup.js +72 -0
- package/dist/testing/test-setup.js.map +1 -0
- package/dist/testing/upstream-kv-test-setup.d.ts +30 -0
- package/dist/testing/upstream-kv-test-setup.d.ts.map +1 -0
- package/dist/testing/upstream-kv-test-setup.js +66 -0
- package/dist/testing/upstream-kv-test-setup.js.map +1 -0
- package/dist/testing/vitest-compat.d.ts +92 -0
- package/dist/testing/vitest-compat.d.ts.map +1 -0
- package/dist/testing/vitest-compat.js +601 -0
- package/dist/testing/vitest-compat.js.map +1 -0
- package/dist/tracing.d.ts +71 -0
- package/dist/tracing.d.ts.map +1 -0
- package/dist/tracing.js +232 -0
- package/dist/tracing.js.map +1 -0
- package/dist/typed-kv.d.ts +120 -0
- package/dist/typed-kv.d.ts.map +1 -0
- package/dist/typed-kv.js +565 -0
- package/dist/typed-kv.js.map +1 -0
- package/dist/typed-upstream-kv.d.ts +17 -0
- package/dist/typed-upstream-kv.d.ts.map +1 -0
- package/dist/typed-upstream-kv.js +38 -0
- package/dist/typed-upstream-kv.js.map +1 -0
- package/dist/types.d.ts +199 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +23 -0
- package/dist/types.js.map +1 -0
- package/dist/upstream-kv.d.ts +84 -0
- package/dist/upstream-kv.d.ts.map +1 -0
- package/dist/upstream-kv.js +375 -0
- package/dist/upstream-kv.js.map +1 -0
- package/docs/api-reference.md +222 -0
- package/docs/caching.md +60 -0
- package/docs/cli.md +123 -0
- package/docs/copy-on-write-branches.md +98 -0
- package/docs/getting-started.md +61 -0
- package/docs/indexes.md +122 -0
- package/docs/iterating-and-pagination.md +93 -0
- package/docs/metadata.md +82 -0
- package/docs/optimistic-locking.md +72 -0
- package/docs/schema-and-trees.md +222 -0
- package/docs/streaming.md +61 -0
- package/docs/testing-and-tracing.md +141 -0
- package/docs/typed-stores.md +68 -0
- package/package.json +63 -0
package/docs/cli.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
[Home](../README.md) | [Previous: Testing and Tracing](testing-and-tracing.md) | [Next: API Reference](api-reference.md)
|
|
2
|
+
|
|
3
|
+
# CLI Explorer
|
|
4
|
+
|
|
5
|
+
An interactive command-line tool for exploring and manipulating KV stores. Read-only by default; pass `--allow-writes` to enable mutations.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
The package exposes a `kv2` binary. After installing:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx kv2 help
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or add it as a dev dependency and run via your package manager:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install -D @vercel/kv2
|
|
19
|
+
npx kv2 keys
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
kv2 [options] [command] [args...]
|
|
26
|
+
kv2 [options] # interactive REPL
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# One-shot commands
|
|
31
|
+
kv2 keys # List keys (first 100)
|
|
32
|
+
kv2 keys users/ # List keys under a prefix
|
|
33
|
+
kv2 --all keys # List all keys
|
|
34
|
+
kv2 --limit 500 keys # List first 500 keys
|
|
35
|
+
kv2 get users/alice # Print value as JSON to stdout
|
|
36
|
+
kv2 --verbose get users/alice # Also show version/metadata on stderr
|
|
37
|
+
kv2 --allow-writes set foo '{"a":1}' # Set a value
|
|
38
|
+
kv2 --allow-writes del foo # Delete a key
|
|
39
|
+
|
|
40
|
+
# Interactive REPL
|
|
41
|
+
kv2 # Launches kv2> prompt
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Commands
|
|
45
|
+
|
|
46
|
+
| Command | Description | Writes? |
|
|
47
|
+
|---------|-------------|---------|
|
|
48
|
+
| `keys [prefix]` | List keys, one per line | No |
|
|
49
|
+
| `get <key>` | Print JSON value to stdout | No |
|
|
50
|
+
| `set <key> <json> [metadata-json]` | Set a value | Yes |
|
|
51
|
+
| `del <key>` | Delete a key | Yes |
|
|
52
|
+
| `help` | Show usage | No |
|
|
53
|
+
|
|
54
|
+
Write commands (`set`, `del`) are rejected unless `--allow-writes` is passed.
|
|
55
|
+
|
|
56
|
+
## Options
|
|
57
|
+
|
|
58
|
+
| Option | Description | Default |
|
|
59
|
+
|--------|-------------|---------|
|
|
60
|
+
| `--prefix <prefix>` | Key prefix passed to `createKV()` | _(none)_ |
|
|
61
|
+
| `--env <env>` | Override `VERCEL_ENV` | `development` |
|
|
62
|
+
| `--branch <branch>` | Override `VERCEL_GIT_COMMIT_REF` | `local` |
|
|
63
|
+
| `--limit <n>` | Max keys to list | `100` |
|
|
64
|
+
| `--all` | List all keys (no limit) | _(disabled)_ |
|
|
65
|
+
| `--allow-writes` | Enable write operations | _(disabled)_ |
|
|
66
|
+
| `--verbose` | Show metadata and version on `get` | _(disabled)_ |
|
|
67
|
+
|
|
68
|
+
## Prefix matters
|
|
69
|
+
|
|
70
|
+
The blob path structure is `cached-kv/{env}/{branch}/{prefix}{key}.value`. If your app uses `createKV({ prefix: "myapp/" })`, the CLI must use `--prefix myapp/` to see those keys:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
kv2 --prefix myapp/ keys
|
|
74
|
+
kv2 --prefix myapp/ get users/alice
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## stdout / stderr contract
|
|
78
|
+
|
|
79
|
+
In one-shot mode, stdout contains only data (for piping) and stderr contains everything else (status, colors, errors).
|
|
80
|
+
|
|
81
|
+
| Command | stdout | stderr |
|
|
82
|
+
|---------|--------|--------|
|
|
83
|
+
| `keys` | one key per line | `N key(s)` count |
|
|
84
|
+
| `get` | JSON value (pretty-printed) | key, version, metadata (with `--verbose`) |
|
|
85
|
+
| `get` (not found) | _(nothing)_ | "Key not found: X" |
|
|
86
|
+
| `set` | _(nothing)_ | "Set X (version: ...)" |
|
|
87
|
+
| `del` | _(nothing)_ | "Deleted X" |
|
|
88
|
+
|
|
89
|
+
This enables piping:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
kv2 keys | wc -l # Count keys
|
|
93
|
+
kv2 get users/alice | jq .name # Extract field
|
|
94
|
+
kv2 keys | xargs -I{} kv2 get {} # Dump all values
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Interactive REPL
|
|
98
|
+
|
|
99
|
+
Running `kv2` without a command launches an interactive REPL. On startup it prints the current context:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
KV CLI Explorer
|
|
103
|
+
env: development
|
|
104
|
+
branch: local
|
|
105
|
+
prefix: (none)
|
|
106
|
+
writes: disabled
|
|
107
|
+
Type "help" for commands, Ctrl+C to exit.
|
|
108
|
+
|
|
109
|
+
kv2> keys
|
|
110
|
+
kv2> get users/alice
|
|
111
|
+
kv2> exit
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The same commands work in REPL mode. Type `exit`, `quit`, or press Ctrl+C to leave.
|
|
115
|
+
|
|
116
|
+
## Environment
|
|
117
|
+
|
|
118
|
+
The CLI automatically loads `.env.local` via dotenv (if installed). You can also export `BLOB_READ_WRITE_TOKEN` directly:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
export BLOB_READ_WRITE_TOKEN=vercel_blob_...
|
|
122
|
+
kv2 keys
|
|
123
|
+
```
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
[Home](../README.md) | [Previous: Streaming](streaming.md) | [Next: Testing and Tracing](testing-and-tracing.md)
|
|
2
|
+
|
|
3
|
+
# Copy-on-Write Branches
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
Most apps store data alongside code, but deployment pipelines treat them very differently. Code is branched, reviewed, and merged — you can test a feature branch in isolation without touching production. Data usually isn't: your preview deployment either points at production data (risky — a bug can corrupt it) or starts with an empty database (useless — you can't test real workflows).
|
|
8
|
+
|
|
9
|
+
## How Copy-on-Write Solves This
|
|
10
|
+
|
|
11
|
+
Copy-on-write gives every preview branch a virtual fork of production data — without copying anything. On a preview branch:
|
|
12
|
+
|
|
13
|
+
- **Reads** check the branch's local storage first, then transparently fall back to production
|
|
14
|
+
- **Writes** go only to local storage — production is never modified
|
|
15
|
+
- **Deletes** create a tombstone so the key stops falling back to production
|
|
16
|
+
|
|
17
|
+
The result: your preview deployment sees the full production dataset, but any changes it makes are isolated. When you merge, the branch's writes are discarded — the code change is what matters, not the test data.
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { createKV } from "@vercel/kv2";
|
|
21
|
+
|
|
22
|
+
// On preview branch "feature-x":
|
|
23
|
+
const kv = createKV({ prefix: "app/" });
|
|
24
|
+
|
|
25
|
+
// Read falls back to production/main if not found locally
|
|
26
|
+
const user = await kv.get("users/alice"); // Found in production
|
|
27
|
+
|
|
28
|
+
// Write only affects this branch
|
|
29
|
+
await kv.set("users/alice", { name: "Alice Updated" });
|
|
30
|
+
|
|
31
|
+
// Delete creates tombstone (won't fall back to production)
|
|
32
|
+
await kv.delete("users/bob");
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## When to Use This
|
|
36
|
+
|
|
37
|
+
Copy-on-write is most valuable when:
|
|
38
|
+
|
|
39
|
+
- **Preview deployments need realistic data** — test a new dashboard layout against actual content, not seed data
|
|
40
|
+
- **Multiple developers work in parallel** — each branch gets its own isolated overlay, no cross-contamination
|
|
41
|
+
- **You want safe data migrations** — test a schema change against production data in a preview branch before merging
|
|
42
|
+
- **Staging environments** — create a long-lived branch that mirrors production for QA
|
|
43
|
+
|
|
44
|
+
It's less useful when your data is purely ephemeral (e.g. session tokens) or when each environment genuinely needs its own independent dataset.
|
|
45
|
+
|
|
46
|
+
## Automatic Environment Detection
|
|
47
|
+
|
|
48
|
+
`createKV()` detects the environment from Vercel's built-in variables and sets up upstream automatically — no configuration needed for the common case:
|
|
49
|
+
|
|
50
|
+
| Environment | Behavior |
|
|
51
|
+
|-------------|----------|
|
|
52
|
+
| `production` + `main` | Direct KV2, no upstream (it's the root) |
|
|
53
|
+
| `preview` branch | UpstreamKV with fallback to same env + `main` |
|
|
54
|
+
| Same env and branch as upstream | Direct KV2, no upstream (avoid self-reference) |
|
|
55
|
+
|
|
56
|
+
## Configuring Upstream
|
|
57
|
+
|
|
58
|
+
### Custom Upstream
|
|
59
|
+
|
|
60
|
+
Point a branch at a specific upstream environment and branch:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
import { createKV } from "@vercel/kv2";
|
|
64
|
+
|
|
65
|
+
const kv = createKV({
|
|
66
|
+
prefix: "app/",
|
|
67
|
+
upstream: { env: "production", branch: "main" },
|
|
68
|
+
});
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Disabled Upstream
|
|
72
|
+
|
|
73
|
+
For fully isolated environments with no fallback:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { createKV } from "@vercel/kv2";
|
|
77
|
+
|
|
78
|
+
const kv = createKV({
|
|
79
|
+
prefix: "app/",
|
|
80
|
+
upstream: null, // no fallback — isolated environment
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## The Manifest
|
|
85
|
+
|
|
86
|
+
`UpstreamKV` uses a `ManifestLog` to track which keys have been written or deleted locally. On read, it checks the manifest to decide whether to read from local storage or fall back to upstream.
|
|
87
|
+
|
|
88
|
+
The manifest is stored in a separate blob namespace (`_manifest/{env}/{branch}/`) to avoid interfering with data key listings.
|
|
89
|
+
|
|
90
|
+
## Branch Name Encoding
|
|
91
|
+
|
|
92
|
+
Branch names are URL-encoded and lowercased to be filesystem-safe. Long branch names (> 64 chars) are truncated with a hash suffix to preserve uniqueness:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
feature/foo → feature%2Ffoo
|
|
96
|
+
feature-foo → feature-foo
|
|
97
|
+
very-long-name… → very-long-name…-<8-char-hash>
|
|
98
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[Home](../README.md) | [Next: Iterating and Pagination](iterating-and-pagination.md)
|
|
2
|
+
|
|
3
|
+
# Getting Started
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @vercel/kv2
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @vercel/kv2
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createKV } from "@vercel/kv2";
|
|
17
|
+
|
|
18
|
+
// Creates KV with automatic upstream fallback to production/main
|
|
19
|
+
const kv = createKV({ prefix: "myapp/" });
|
|
20
|
+
|
|
21
|
+
interface User {
|
|
22
|
+
name: string;
|
|
23
|
+
email: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Type-safe sub-store
|
|
27
|
+
const users = kv.getStore<User>("users/");
|
|
28
|
+
|
|
29
|
+
await users.set("alice", { name: "Alice", email: "alice@example.com" });
|
|
30
|
+
|
|
31
|
+
const result = await users.get("alice");
|
|
32
|
+
if (result.exists) {
|
|
33
|
+
console.log((await result.value).name); // "Alice"
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Environment Setup
|
|
38
|
+
|
|
39
|
+
`@vercel/kv2` requires a Vercel Blob token and uses environment variables for automatic branch detection:
|
|
40
|
+
|
|
41
|
+
| Variable | Required | Description |
|
|
42
|
+
|----------|----------|-------------|
|
|
43
|
+
| `BLOB_READ_WRITE_TOKEN` | Yes | Vercel Blob access token |
|
|
44
|
+
| `VERCEL_ENV` | Auto | Set by Vercel (`production`, `preview`, `development`) |
|
|
45
|
+
| `VERCEL_GIT_COMMIT_REF` | Auto | Current git branch, set by Vercel |
|
|
46
|
+
|
|
47
|
+
For local development, set the token in a `.env` file:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
BLOB_READ_WRITE_TOKEN=vercel_blob_...
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## How It Works
|
|
54
|
+
|
|
55
|
+
`@vercel/kv2` stores data as JSON blobs in Vercel Blob storage with an edge cache layer for low-latency reads:
|
|
56
|
+
|
|
57
|
+
1. **Writes** go directly to Vercel Blob, then invalidate the cache
|
|
58
|
+
2. **Reads** check the edge cache first, falling back to Blob storage on cache miss
|
|
59
|
+
3. **Preview branches** use copy-on-write: reads fall back to production data, writes stay isolated to the branch
|
|
60
|
+
|
|
61
|
+
This gives you strong consistency for writes with fast cached reads, while branch isolation enables safe preview deployments without affecting production data.
|
package/docs/indexes.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
[Home](../README.md) | [Previous: Schema and Trees](schema-and-trees.md) | [Next: Caching](caching.md)
|
|
2
|
+
|
|
3
|
+
# Indexes
|
|
4
|
+
|
|
5
|
+
Secondary indexes allow you to look up entries by attributes other than the primary key.
|
|
6
|
+
|
|
7
|
+
## Defining Indexes
|
|
8
|
+
|
|
9
|
+
Pass an `indexes` record to `getStore()`. Each index defines a `key` function that extracts the index key from the value:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
interface Doc {
|
|
13
|
+
slug: string;
|
|
14
|
+
status: string;
|
|
15
|
+
tags: string[];
|
|
16
|
+
title: string;
|
|
17
|
+
content: string;
|
|
18
|
+
authorId: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const docs = kv.getStore<Doc, undefined, "bySlug" | "byStatus">("docs/", {
|
|
22
|
+
bySlug: {
|
|
23
|
+
key: (doc) => doc.slug,
|
|
24
|
+
unique: true,
|
|
25
|
+
},
|
|
26
|
+
byStatus: {
|
|
27
|
+
key: (doc) => doc.status,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Unique Indexes
|
|
33
|
+
|
|
34
|
+
Set `unique: true` to enforce uniqueness. A `KVIndexConflictError` is thrown if a duplicate is detected:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { KVIndexConflictError } from "@vercel/kv2";
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await docs.set("doc-2", {
|
|
41
|
+
slug: "existing-slug", // already used by another doc
|
|
42
|
+
status: "draft",
|
|
43
|
+
tags: [],
|
|
44
|
+
title: "Duplicate",
|
|
45
|
+
content: "",
|
|
46
|
+
authorId: "author-1",
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (error instanceof KVIndexConflictError) {
|
|
50
|
+
console.log(error.indexName); // "bySlug"
|
|
51
|
+
console.log(error.indexKey); // "existing-slug"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Multi-Value Indexes
|
|
57
|
+
|
|
58
|
+
Return an array from the `key` function to index an entry under multiple keys:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
interface Doc {
|
|
62
|
+
slug: string;
|
|
63
|
+
status: string;
|
|
64
|
+
tags: string[];
|
|
65
|
+
title: string;
|
|
66
|
+
content: string;
|
|
67
|
+
authorId: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const docs = kv.getStore<Doc, undefined, "byTag">("docs/", {
|
|
71
|
+
byTag: {
|
|
72
|
+
key: (doc) => doc.tags, // each tag becomes an index entry
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await docs.set("doc-1", {
|
|
77
|
+
slug: "hello",
|
|
78
|
+
status: "published",
|
|
79
|
+
tags: ["typescript", "tutorial"],
|
|
80
|
+
title: "Hello",
|
|
81
|
+
content: "World",
|
|
82
|
+
authorId: "author-1",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Find all docs tagged "typescript"
|
|
86
|
+
for await (const key of docs.keys({ byTag: "typescript" })) {
|
|
87
|
+
console.log(key); // "doc-1"
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Non-Unique Indexes
|
|
92
|
+
|
|
93
|
+
Query non-unique indexes with `keys()` and `entries()`:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// Find all published docs
|
|
97
|
+
for await (const key of docs.keys({ byStatus: "published" })) {
|
|
98
|
+
console.log(key);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Iterate entries by index
|
|
102
|
+
for await (const [key, entry] of docs.entries({ byStatus: "draft" })) {
|
|
103
|
+
console.log(key, (await entry.value).title);
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Index Maintenance
|
|
108
|
+
|
|
109
|
+
Indexes are maintained automatically on `set()` and `delete()`. When you update a value, old index entries are removed and new ones are created.
|
|
110
|
+
|
|
111
|
+
## Reindexing
|
|
112
|
+
|
|
113
|
+
If indexes are added after data exists, or if index data becomes inconsistent, rebuild indexes with `reindex()`:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const result = await docs.reindex();
|
|
117
|
+
console.log(result.indexed); // number of entries reindexed
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Orphan Handling
|
|
121
|
+
|
|
122
|
+
Index entries pointing to deleted primary keys are self-healing: they are automatically cleaned up during reads.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
[Home](../README.md) | [Previous: Getting Started](getting-started.md) | [Next: Typed Stores](typed-stores.md)
|
|
2
|
+
|
|
3
|
+
# Iterating and Pagination
|
|
4
|
+
|
|
5
|
+
## Iterating Keys
|
|
6
|
+
|
|
7
|
+
Use `keys()` to iterate over all keys in a store:
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
for await (const key of users.keys()) {
|
|
11
|
+
console.log(key);
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Filter by prefix:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
for await (const key of kv.keys("users/active/")) {
|
|
19
|
+
console.log(key);
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Iterating Entries
|
|
24
|
+
|
|
25
|
+
`entries()` fetches values concurrently (default: 20 concurrent requests):
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
for await (const [key, entry] of users.entries()) {
|
|
29
|
+
console.log(key, await entry.value);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
With a prefix filter:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
for await (const [key, entry] of users.entries("active/")) {
|
|
37
|
+
console.log(key, entry.metadata);
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Fetching Multiple Keys
|
|
42
|
+
|
|
43
|
+
Use `getMany()` to fetch specific keys with bounded concurrency:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
const results = await users.getMany(["alice", "bob", "charlie"]);
|
|
47
|
+
for (const [key, entry] of results) {
|
|
48
|
+
console.log(key, await entry.value);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Cursor-Based Pagination
|
|
53
|
+
|
|
54
|
+
Both `keys()` and `entries()` support cursor-based pagination for HTTP APIs.
|
|
55
|
+
|
|
56
|
+
### Paginating Keys
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
const page = await kv.keys("users/").page(10);
|
|
60
|
+
console.log(page.keys); // string[]
|
|
61
|
+
console.log(page.cursor); // string | undefined (pass to next call)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Paginating Entries
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
const page = await kv.entries<User>("users/").page(10);
|
|
68
|
+
for (const [key, entry] of page.entries) {
|
|
69
|
+
console.log(key, await entry.value);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Next.js API Route Example
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
async function GET(req: Request) {
|
|
77
|
+
const url = new URL(req.url);
|
|
78
|
+
const cursor = url.searchParams.get("cursor") ?? undefined;
|
|
79
|
+
|
|
80
|
+
const { keys, cursor: nextCursor } = await users.keys().page(20, cursor);
|
|
81
|
+
const entries = await users.getMany(keys);
|
|
82
|
+
|
|
83
|
+
return Response.json({
|
|
84
|
+
users: await Promise.all(
|
|
85
|
+
keys.map(async (k) => {
|
|
86
|
+
const entry = entries.get(k);
|
|
87
|
+
return entry ? await entry.value : null;
|
|
88
|
+
}),
|
|
89
|
+
),
|
|
90
|
+
nextCursor,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
```
|
package/docs/metadata.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
[Home](../README.md) | [Previous: Optimistic Locking](optimistic-locking.md) | [Next: Schema and Trees](schema-and-trees.md)
|
|
2
|
+
|
|
3
|
+
# Metadata
|
|
4
|
+
|
|
5
|
+
## Typed Metadata
|
|
6
|
+
|
|
7
|
+
Parameterize `createKV<M>()` with a metadata type. Metadata is stored alongside each entry and is available immediately (no lazy loading):
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { createKV } from "@vercel/kv2";
|
|
11
|
+
|
|
12
|
+
interface Metadata {
|
|
13
|
+
updatedAt: number;
|
|
14
|
+
version: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface User {
|
|
18
|
+
name: string;
|
|
19
|
+
email: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const kv = createKV<Metadata>({ prefix: "app/" });
|
|
23
|
+
const users = kv.getStore<User>("users/");
|
|
24
|
+
|
|
25
|
+
// Metadata is required on set
|
|
26
|
+
await users.set("alice", { name: "Alice", email: "alice@example.com" }, { updatedAt: Date.now(), version: 1 });
|
|
27
|
+
|
|
28
|
+
const result = await users.get("alice");
|
|
29
|
+
if (result.exists) {
|
|
30
|
+
console.log(result.metadata.version); // 1 — available immediately
|
|
31
|
+
console.log((await result.value).name); // "Alice" — lazy loaded
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Metadata on Sub-Stores
|
|
36
|
+
|
|
37
|
+
Sub-stores inherit the metadata type from their parent:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { createKV } from "@vercel/kv2";
|
|
41
|
+
|
|
42
|
+
interface Metadata {
|
|
43
|
+
updatedAt: number;
|
|
44
|
+
version: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface Post {
|
|
48
|
+
title: string;
|
|
49
|
+
content: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const kv = createKV<Metadata>({ prefix: "app/" });
|
|
53
|
+
const posts = kv.getStore<Post>("posts/");
|
|
54
|
+
|
|
55
|
+
// Same metadata type required
|
|
56
|
+
await posts.set("hello", { title: "Hello", content: "World" }, { updatedAt: Date.now(), version: 1 });
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Metadata Without Values
|
|
60
|
+
|
|
61
|
+
Since metadata is available without loading the value, you can filter entries efficiently:
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { createKV } from "@vercel/kv2";
|
|
65
|
+
|
|
66
|
+
interface Metadata {
|
|
67
|
+
updatedAt: number;
|
|
68
|
+
version: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const kv = createKV<Metadata>({ prefix: "app/" });
|
|
72
|
+
|
|
73
|
+
// Iterate entries and inspect metadata before deciding to load the value
|
|
74
|
+
for await (const [key, entry] of kv.entries("posts/")) {
|
|
75
|
+
// metadata is immediately available
|
|
76
|
+
if (entry.metadata.updatedAt > Date.now() - 86400_000) {
|
|
77
|
+
// Only load the value for recent entries
|
|
78
|
+
const value = await entry.value;
|
|
79
|
+
console.log(key, value);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[Home](../README.md) | [Previous: Typed Stores](typed-stores.md) | [Next: Metadata](metadata.md)
|
|
2
|
+
|
|
3
|
+
# Optimistic Locking
|
|
4
|
+
|
|
5
|
+
## Why Optimistic Locking?
|
|
6
|
+
|
|
7
|
+
KV stores don't have multi-key transactions the way a SQL database does. If two requests read a value, modify it, and write it back at the same time, one update silently overwrites the other — a lost update.
|
|
8
|
+
|
|
9
|
+
Optimistic locking prevents this: every entry carries a `version` (an etag). When you write, you can say "only succeed if the version is still what I read." If someone else wrote in between, the store rejects your write with a `KVVersionConflictError` and you retry with fresh data. No locks are held, so throughput stays high — conflicts are detected, not prevented.
|
|
10
|
+
|
|
11
|
+
## Version-Based Updates
|
|
12
|
+
|
|
13
|
+
The simplest API is `entry.update()`. It captures the version at read time and passes it through automatically:
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
const entry = await users.get("alice");
|
|
17
|
+
if (entry.exists) {
|
|
18
|
+
const user = await entry.value;
|
|
19
|
+
user.name = "Alice Updated";
|
|
20
|
+
|
|
21
|
+
// Throws KVVersionConflictError if another process modified it
|
|
22
|
+
await entry.update(user);
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Transactions via Read-Modify-Write
|
|
27
|
+
|
|
28
|
+
Because there are no multi-key locks, the pattern for a "transaction" is a retry loop: read the current state, compute the new state, attempt the write, and retry if a conflict is detected. This is safe for any single-key mutation:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { KVVersionConflictError } from "@vercel/kv2";
|
|
32
|
+
|
|
33
|
+
async function incrementCounter(key: string): Promise<number> {
|
|
34
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
35
|
+
const entry = await kv.get<{ count: number }>(key);
|
|
36
|
+
if (!entry.exists) throw new Error("Key not found");
|
|
37
|
+
|
|
38
|
+
const current = await entry.value;
|
|
39
|
+
try {
|
|
40
|
+
await entry.update({ count: current.count + 1 });
|
|
41
|
+
return current.count + 1;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (error instanceof KVVersionConflictError) continue;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
throw new Error("Max retries exceeded");
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This is the same approach databases like DynamoDB and CouchDB use for conditional writes. It works well when conflicts are rare (most workloads) and degrades gracefully when they aren't — retries are cheap because reads are cached.
|
|
52
|
+
|
|
53
|
+
## Manual Version Control
|
|
54
|
+
|
|
55
|
+
For more control, pass `expectedVersion` in `SetOptions` directly:
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
const { version } = await kv.set("counter", { count: 0 }, metadata);
|
|
59
|
+
|
|
60
|
+
// Later, conditionally update only if version matches
|
|
61
|
+
await kv.set("counter", { count: 1 }, metadata, { expectedVersion: version });
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This is useful when the version comes from an external source (e.g. a form submission that includes the etag it was editing).
|
|
65
|
+
|
|
66
|
+
## Create-Only Writes
|
|
67
|
+
|
|
68
|
+
Use `override: false` to ensure a key is only written if it doesn't already exist — useful for generating unique IDs or claiming resources:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
await kv.set("user/new-id", userData, metadata, { override: false });
|
|
72
|
+
```
|