@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
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
[Home](../README.md) | [Previous: Metadata](metadata.md) | [Next: Indexes](indexes.md)
|
|
2
|
+
|
|
3
|
+
# Schema and Trees
|
|
4
|
+
|
|
5
|
+
For hierarchical data, define a schema and fetch trees with batched concurrent loading.
|
|
6
|
+
|
|
7
|
+
## Defining a Schema
|
|
8
|
+
|
|
9
|
+
Use `defineSchema()` to declare your entity hierarchy with patterns and value types:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { defineSchema, createSchemaKV, createKV } from "@vercel/kv2";
|
|
13
|
+
|
|
14
|
+
interface Board { name: string }
|
|
15
|
+
interface Column { name: string; order: number }
|
|
16
|
+
interface Task { title: string; done: boolean }
|
|
17
|
+
|
|
18
|
+
const schema = defineSchema("kanban/", {
|
|
19
|
+
boards: {
|
|
20
|
+
pattern: "*",
|
|
21
|
+
value: {} as Board,
|
|
22
|
+
children: {
|
|
23
|
+
columns: {
|
|
24
|
+
pattern: "columns/*",
|
|
25
|
+
value: {} as Column,
|
|
26
|
+
children: {
|
|
27
|
+
tasks: { pattern: "tasks/*", value: {} as Task }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Each node in the schema defines:
|
|
36
|
+
- `pattern` — a glob pattern for matching keys at that level
|
|
37
|
+
- `value` — a type witness (use `{} as T`)
|
|
38
|
+
- `children` — optional nested entity definitions
|
|
39
|
+
|
|
40
|
+
## Creating a Schema KV
|
|
41
|
+
|
|
42
|
+
Combine a schema with a KV store:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { defineSchema, createSchemaKV, createKV } from "@vercel/kv2";
|
|
46
|
+
|
|
47
|
+
interface Board { name: string }
|
|
48
|
+
interface Column { name: string; order: number }
|
|
49
|
+
interface Task { title: string; done: boolean }
|
|
50
|
+
|
|
51
|
+
const schema = defineSchema("kanban/", {
|
|
52
|
+
boards: {
|
|
53
|
+
pattern: "*",
|
|
54
|
+
value: {} as Board,
|
|
55
|
+
children: {
|
|
56
|
+
columns: {
|
|
57
|
+
pattern: "columns/*",
|
|
58
|
+
value: {} as Column,
|
|
59
|
+
children: {
|
|
60
|
+
tasks: { pattern: "tasks/*", value: {} as Task }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const kv = createKV({ prefix: "kanban/" });
|
|
68
|
+
const kanban = createSchemaKV(schema, kv);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Type-Safe Key Builders
|
|
72
|
+
|
|
73
|
+
The schema KV provides key builders that produce correctly-formatted keys:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import { defineSchema, createSchemaKV, createKV } from "@vercel/kv2";
|
|
77
|
+
|
|
78
|
+
interface Board { name: string }
|
|
79
|
+
interface Column { name: string; order: number }
|
|
80
|
+
interface Task { title: string; done: boolean }
|
|
81
|
+
|
|
82
|
+
const schema = defineSchema("kanban/", {
|
|
83
|
+
boards: {
|
|
84
|
+
pattern: "*",
|
|
85
|
+
value: {} as Board,
|
|
86
|
+
children: {
|
|
87
|
+
columns: {
|
|
88
|
+
pattern: "columns/*",
|
|
89
|
+
value: {} as Column,
|
|
90
|
+
children: {
|
|
91
|
+
tasks: { pattern: "tasks/*", value: {} as Task }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const kv = createKV({ prefix: "kanban/" });
|
|
99
|
+
const kanban = createSchemaKV(schema, kv);
|
|
100
|
+
|
|
101
|
+
kanban.key.boards("board-1"); // "board-1"
|
|
102
|
+
kanban.key.columns("board-1", "col-1"); // "board-1/columns/col-1"
|
|
103
|
+
kanban.key.tasks("board-1", "col-1", "task-1"); // "board-1/columns/col-1/tasks/task-1"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Fetching Trees
|
|
107
|
+
|
|
108
|
+
Use `tree()` to fetch an entity and all its descendants. Internally, it makes a single `keys()` call to discover descendants, then `getMany()` to fetch values concurrently:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { defineSchema, createSchemaKV, createKV } from "@vercel/kv2";
|
|
112
|
+
|
|
113
|
+
interface Board { name: string }
|
|
114
|
+
interface Column { name: string; order: number }
|
|
115
|
+
interface Task { title: string; done: boolean }
|
|
116
|
+
|
|
117
|
+
const schema = defineSchema("kanban/", {
|
|
118
|
+
boards: {
|
|
119
|
+
pattern: "*",
|
|
120
|
+
value: {} as Board,
|
|
121
|
+
children: {
|
|
122
|
+
columns: {
|
|
123
|
+
pattern: "columns/*",
|
|
124
|
+
value: {} as Column,
|
|
125
|
+
children: {
|
|
126
|
+
tasks: { pattern: "tasks/*", value: {} as Task }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const kv = createKV({ prefix: "kanban/" });
|
|
134
|
+
const kanban = createSchemaKV(schema, kv);
|
|
135
|
+
|
|
136
|
+
const board = await kanban.tree("boards", "board-1");
|
|
137
|
+
console.log(board.value.name);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Lazy Iteration
|
|
141
|
+
|
|
142
|
+
Children on a tree node are async iterables. They support `page()`, `collect()`, `count()`, and `keys()`:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
import { defineSchema, createSchemaKV, createKV } from "@vercel/kv2";
|
|
146
|
+
|
|
147
|
+
interface Board { name: string }
|
|
148
|
+
interface Column { name: string; order: number }
|
|
149
|
+
interface Task { title: string; done: boolean }
|
|
150
|
+
|
|
151
|
+
const schema = defineSchema("kanban/", {
|
|
152
|
+
boards: {
|
|
153
|
+
pattern: "*",
|
|
154
|
+
value: {} as Board,
|
|
155
|
+
children: {
|
|
156
|
+
columns: {
|
|
157
|
+
pattern: "columns/*",
|
|
158
|
+
value: {} as Column,
|
|
159
|
+
children: {
|
|
160
|
+
tasks: { pattern: "tasks/*", value: {} as Task }
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const kv = createKV({ prefix: "kanban/" });
|
|
168
|
+
const kanban = createSchemaKV(schema, kv);
|
|
169
|
+
|
|
170
|
+
const board = await kanban.tree("boards", "board-1");
|
|
171
|
+
|
|
172
|
+
// Iterate children lazily
|
|
173
|
+
for await (const column of board.columns) {
|
|
174
|
+
console.log(column.value.name);
|
|
175
|
+
for await (const task of column.tasks) {
|
|
176
|
+
console.log(task.value.title);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## CRUD via Schema
|
|
182
|
+
|
|
183
|
+
The schema KV provides typed `get`, `set`, and `delete` methods:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { defineSchema, createSchemaKV, createKV } from "@vercel/kv2";
|
|
187
|
+
|
|
188
|
+
interface Board { name: string }
|
|
189
|
+
interface Column { name: string; order: number }
|
|
190
|
+
interface Task { title: string; done: boolean }
|
|
191
|
+
|
|
192
|
+
const schema = defineSchema("kanban/", {
|
|
193
|
+
boards: {
|
|
194
|
+
pattern: "*",
|
|
195
|
+
value: {} as Board,
|
|
196
|
+
children: {
|
|
197
|
+
columns: {
|
|
198
|
+
pattern: "columns/*",
|
|
199
|
+
value: {} as Column,
|
|
200
|
+
children: {
|
|
201
|
+
tasks: { pattern: "tasks/*", value: {} as Task }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const kv = createKV({ prefix: "kanban/" });
|
|
209
|
+
const kanban = createSchemaKV(schema, kv);
|
|
210
|
+
|
|
211
|
+
// Set a board
|
|
212
|
+
await kanban.set("boards", { name: "Sprint 1" }, undefined, "board-1");
|
|
213
|
+
|
|
214
|
+
// Get a specific task
|
|
215
|
+
const task = await kanban.get("tasks", "board-1", "col-1", "task-1");
|
|
216
|
+
if (task.exists) {
|
|
217
|
+
console.log((await task.value).title);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Delete a column
|
|
221
|
+
await kanban.delete("columns", "board-1", "col-1");
|
|
222
|
+
```
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[Home](../README.md) | [Previous: Caching](caching.md) | [Next: Copy-on-Write Branches](copy-on-write-branches.md)
|
|
2
|
+
|
|
3
|
+
# Streaming
|
|
4
|
+
|
|
5
|
+
## Why Streaming?
|
|
6
|
+
|
|
7
|
+
By default, `result.value` buffers the entire value into memory before returning it. For a 2 KB JSON object that's fine, but for a 50 MB image or a large dataset it means your serverless function allocates 50 MB of RAM just to pass the bytes through to a response.
|
|
8
|
+
|
|
9
|
+
Streaming avoids this. With `result.stream` you get a `ReadableStream<Uint8Array>` that flows bytes directly from blob storage to the consumer — memory stays flat regardless of value size.
|
|
10
|
+
|
|
11
|
+
Use streaming when you're:
|
|
12
|
+
- Serving files or media to clients without processing them
|
|
13
|
+
- Piping data between storage and another service
|
|
14
|
+
- Working near your function's memory limit
|
|
15
|
+
|
|
16
|
+
## Streaming Reads
|
|
17
|
+
|
|
18
|
+
Use `result.stream` instead of `result.value` to read without buffering:
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
const result = await kv.get("large-file");
|
|
22
|
+
if (result.exists) {
|
|
23
|
+
const stream = await result.stream;
|
|
24
|
+
// Pipe directly to the HTTP response — no buffering
|
|
25
|
+
const response = new Response(stream);
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Streaming Writes
|
|
30
|
+
|
|
31
|
+
Pass a `ReadableStream<Uint8Array>` to `set()` to write without buffering the full payload:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
35
|
+
start(controller) {
|
|
36
|
+
controller.enqueue(new TextEncoder().encode("hello world"));
|
|
37
|
+
controller.close();
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await kv.set("streamed-value", stream, metadata);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This is useful for proxying uploads or piping data from another source directly into the store.
|
|
45
|
+
|
|
46
|
+
## Large Value Threshold
|
|
47
|
+
|
|
48
|
+
Values below the threshold (default: 1 MB) are inlined in a single JSON blob — small and fast. Values above it are stored in a binary format that separates the header (metadata) from the payload, enabling streaming without parsing the entire blob.
|
|
49
|
+
|
|
50
|
+
You can tune the threshold:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { KV2 } from "@vercel/kv2";
|
|
54
|
+
|
|
55
|
+
const kv = new KV2({
|
|
56
|
+
prefix: "files/",
|
|
57
|
+
largeValueThreshold: 512 * 1024, // 512KB
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Lower it if you serve many medium-sized values and want to stream them. Raise it (or leave the default) if your values are mostly small JSON and you want the simplicity of a single fetch.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
[Home](../README.md) | [Previous: Copy-on-Write Branches](copy-on-write-branches.md) | [Next: CLI](cli.md)
|
|
2
|
+
|
|
3
|
+
# Testing and Tracing
|
|
4
|
+
|
|
5
|
+
## Testing
|
|
6
|
+
|
|
7
|
+
### FakeBlobStore
|
|
8
|
+
|
|
9
|
+
An in-memory blob store for unit tests. No network calls, no tokens needed:
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { FakeBlobStore } from "@vercel/kv2/testing";
|
|
13
|
+
import { KV2 } from "@vercel/kv2";
|
|
14
|
+
|
|
15
|
+
const blobStore = new FakeBlobStore();
|
|
16
|
+
const kv = new KV2({ prefix: "test/", blobStore });
|
|
17
|
+
|
|
18
|
+
await kv.set("key", { hello: "world" });
|
|
19
|
+
const result = await kv.get("key");
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### FakeCache
|
|
23
|
+
|
|
24
|
+
An in-memory cache with call tracking and programmable error injection:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { FakeCache } from "@vercel/kv2/testing";
|
|
28
|
+
import { KV2 } from "@vercel/kv2";
|
|
29
|
+
|
|
30
|
+
const cache = new FakeCache();
|
|
31
|
+
const blobStore = new FakeBlobStore();
|
|
32
|
+
const kv = new KV2({ prefix: "test/", blobStore, cache });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### createTestKV
|
|
36
|
+
|
|
37
|
+
A convenience factory that creates a KV2 backed by `FakeBlobStore` (or real Vercel Blob when `INTEGRATION_TEST=1`):
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { createTestKV } from "@vercel/kv2/testing";
|
|
41
|
+
|
|
42
|
+
const { kv, blobStore, cleanup } = createTestKV();
|
|
43
|
+
|
|
44
|
+
await kv.set("key", { hello: "world" });
|
|
45
|
+
|
|
46
|
+
// Clean up after tests
|
|
47
|
+
await cleanup();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Integration Tests
|
|
51
|
+
|
|
52
|
+
Set environment variables and run:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
INTEGRATION_TEST=1 pnpm test
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Required env vars for integration tests:
|
|
59
|
+
- `BLOB_READ_WRITE_TOKEN` — Vercel Blob access token
|
|
60
|
+
- `PROTECTION_BYPASS` — Protection bypass token
|
|
61
|
+
|
|
62
|
+
## Tracing
|
|
63
|
+
|
|
64
|
+
### Built-in Tracers
|
|
65
|
+
|
|
66
|
+
**No-op tracer** — zero overhead, the default:
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import { noopTracer, KV2 } from "@vercel/kv2";
|
|
70
|
+
|
|
71
|
+
const kv = new KV2({ prefix: "app/", tracer: noopTracer });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Console tracer** — logs span timing and attributes to console:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { consoleTracer, KV2 } from "@vercel/kv2";
|
|
78
|
+
|
|
79
|
+
const kv = new KV2({ prefix: "app/", tracer: consoleTracer });
|
|
80
|
+
// [KV] kv.get 2.3ms key=users/alice source=blob
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### OpenTelemetry
|
|
84
|
+
|
|
85
|
+
Adapt an OpenTelemetry tracer with `createOtelTracer()`:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { createOtelTracer, KV2 } from "@vercel/kv2";
|
|
89
|
+
|
|
90
|
+
// Provide any OTEL-compatible tracer
|
|
91
|
+
const otelTracer: import("@vercel/kv2").OtelTracerLike = {
|
|
92
|
+
startSpan(name, options) {
|
|
93
|
+
return {
|
|
94
|
+
setAttribute() {},
|
|
95
|
+
setStatus() {},
|
|
96
|
+
recordException() {},
|
|
97
|
+
end() {},
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const tracer = createOtelTracer(otelTracer);
|
|
103
|
+
const kv = new KV2({ prefix: "app/", tracer });
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Statistics Tracer
|
|
107
|
+
|
|
108
|
+
Collect timing data for performance analysis:
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { createStatsTracer, KV2 } from "@vercel/kv2";
|
|
112
|
+
|
|
113
|
+
const { tracer, printStats, getStats, clear } = createStatsTracer();
|
|
114
|
+
const kv = new KV2({ prefix: "app/", tracer });
|
|
115
|
+
|
|
116
|
+
// ... run operations ...
|
|
117
|
+
|
|
118
|
+
// Print a summary table
|
|
119
|
+
printStats();
|
|
120
|
+
|
|
121
|
+
// Get stats for a specific operation
|
|
122
|
+
const stats = getStats("kv.get");
|
|
123
|
+
if (stats) {
|
|
124
|
+
console.log(`avg: ${stats.avg}ms, p95: ${stats.p95}ms`);
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Traced Operations
|
|
129
|
+
|
|
130
|
+
KV2 emits spans for these operations:
|
|
131
|
+
|
|
132
|
+
| Span Name | Description |
|
|
133
|
+
|-----------|-------------|
|
|
134
|
+
| `kv.get` | Single key read |
|
|
135
|
+
| `kv.set` | Single key write |
|
|
136
|
+
| `kv.delete` | Single key delete |
|
|
137
|
+
| `kv.keys` | Key listing |
|
|
138
|
+
| `kv.entries` | Entry listing |
|
|
139
|
+
| `kv.getMany` | Batch read |
|
|
140
|
+
|
|
141
|
+
Each span includes attributes like `key`, `source` (cache/blob), and `hit` (cache hit/miss).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[Home](../README.md) | [Previous: Iterating and Pagination](iterating-and-pagination.md) | [Next: Optimistic Locking](optimistic-locking.md)
|
|
2
|
+
|
|
3
|
+
# Typed Stores
|
|
4
|
+
|
|
5
|
+
## Why Typed Stores?
|
|
6
|
+
|
|
7
|
+
A bare KV store is untyped — every `get()` returns `unknown` and every `set()` accepts anything. As your app grows you end up with ad-hoc key conventions ("users/" + id, "posts/" + slug) scattered across your codebase and `as User` casts at every call site.
|
|
8
|
+
|
|
9
|
+
Typed stores solve this by giving you a narrow, strongly-typed view over a slice of your keyspace:
|
|
10
|
+
|
|
11
|
+
- **Type safety** — `get()` returns `Post`, `set()` only accepts `Post`. No casts, no runtime surprises.
|
|
12
|
+
- **Key namespacing** — the store owns its prefix, so you work with short relative keys (`"hello-world"`) instead of full paths (`"posts/hello-world"`).
|
|
13
|
+
- **Composability** — stores nest, so you can model `posts/drafts/` or `users/active/` as their own typed sub-stores without any string concatenation.
|
|
14
|
+
|
|
15
|
+
## Creating a Typed Store
|
|
16
|
+
|
|
17
|
+
Use `getStore<T>()` to create a type-safe sub-store with automatic key prefixing:
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
interface Post {
|
|
21
|
+
title: string;
|
|
22
|
+
content: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const posts = kv.getStore<Post>("posts/");
|
|
26
|
+
|
|
27
|
+
await posts.set("hello-world", { title: "Hello", content: "World" });
|
|
28
|
+
|
|
29
|
+
const result = await posts.get("hello-world");
|
|
30
|
+
if (result.exists) {
|
|
31
|
+
const post = await result.value; // Post
|
|
32
|
+
console.log(post.title);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Key Prefixing
|
|
37
|
+
|
|
38
|
+
Keys are relative to the store. The store prepends its prefix automatically, so you never build key strings by hand:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
const posts = kv.getStore<Post>("posts/");
|
|
42
|
+
|
|
43
|
+
// You use relative keys
|
|
44
|
+
await posts.set("hello-world", { title: "Hello", content: "World" });
|
|
45
|
+
|
|
46
|
+
for await (const key of posts.keys()) {
|
|
47
|
+
console.log(key); // "hello-world" (not "posts/hello-world")
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Nested Stores
|
|
52
|
+
|
|
53
|
+
Stores can be nested. Prefixes accumulate, giving you a natural hierarchy without manual string building:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
interface Post {
|
|
57
|
+
title: string;
|
|
58
|
+
content: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const posts = kv.getStore<Post>("posts/");
|
|
62
|
+
const drafts = posts.getStore<Post>("drafts/");
|
|
63
|
+
|
|
64
|
+
await drafts.set("my-draft", { title: "Draft", content: "..." });
|
|
65
|
+
// Actual blob path includes: "posts/drafts/my-draft"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
This lets different parts of your application own their own keyspace — a `drafts` module only sees draft keys and can't accidentally read or overwrite published posts.
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vercel/kv2",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "KV2 - A type-safe KV store backed by Vercel private blobs with regional caching",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"!dist/**/*.test.*",
|
|
9
|
+
"!dist/chaos",
|
|
10
|
+
"README.md",
|
|
11
|
+
"SKILL.md",
|
|
12
|
+
"docs"
|
|
13
|
+
],
|
|
14
|
+
"bin": {
|
|
15
|
+
"kv2": "./dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./testing": {
|
|
25
|
+
"types": "./dist/testing/index.d.ts",
|
|
26
|
+
"import": "./dist/testing/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./testing/test-index": {
|
|
29
|
+
"import": "./dist/testing/test-index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"kv",
|
|
34
|
+
"cache",
|
|
35
|
+
"vercel",
|
|
36
|
+
"blob"
|
|
37
|
+
],
|
|
38
|
+
"author": "",
|
|
39
|
+
"license": "ISC",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@vercel/blob": "2.2.0-f6bb7b4-20260204165325",
|
|
42
|
+
"@vercel/functions": "^3.3.6"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@biomejs/biome": "^1.9.0",
|
|
46
|
+
"@types/node": "^22.0.0",
|
|
47
|
+
"dotenv": "^17.2.3",
|
|
48
|
+
"knip": "^5.0.0",
|
|
49
|
+
"typescript": "^5.7.0"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"cli": "node dist/cli.js",
|
|
53
|
+
"build": "tsc",
|
|
54
|
+
"typecheck": "tsc --noEmit --project tsconfig.test.json",
|
|
55
|
+
"check": "biome check --write .",
|
|
56
|
+
"lint": "biome lint .",
|
|
57
|
+
"lint:fix": "biome lint --write .",
|
|
58
|
+
"test": "tsc && node dist/testing/run-tests.js",
|
|
59
|
+
"test:integration": "INTEGRATION_TEST=1 sh -c 'tsc && node dist/testing/run-tests.js'",
|
|
60
|
+
"knip": "knip",
|
|
61
|
+
"validate": "pnpm run knip && pnpm run typecheck && pnpm run test"
|
|
62
|
+
}
|
|
63
|
+
}
|