@uploadista/kv-store-cloudflare-kv 0.0.3

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.
@@ -0,0 +1,5 @@
1
+
2
+ 
3
+ > @uploadista/kv-store-cloudflare-kv@0.0.2 build /Users/denislaboureyras/Documents/uploadista/dev/uploadista-workspace/uploadista-sdk/packages/kv-stores/cloudflare-kv
4
+ > tsc -b
5
+
@@ -0,0 +1,5 @@
1
+
2
+ > @uploadista/kv-store-cloudflare-kv@ check /Users/denislaboureyras/Documents/uploadista/dev/uploadista/packages/uploadista/kv-stores/cloudflare-kv
3
+ > biome check --write ./src
4
+
5
+ Checked 2 files in 20ms. No fixes applied.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 uploadista
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,582 @@
1
+ # @uploadista/kv-store-cloudflare-kv
2
+
3
+ Cloudflare KV-backed key-value store for Uploadista. Deploy globally distributed storage on Cloudflare's edge network with zero-downtime updates.
4
+
5
+ ## Overview
6
+
7
+ The Cloudflare KV store uses the `@cloudflare/workers-types` KVNamespace API for storing state on Cloudflare's edge network. Perfect for:
8
+
9
+ - **Edge Deployment**: Store data globally at 300+ edge locations
10
+ - **Serverless Uploads**: Run on Cloudflare Workers with no origin server
11
+ - **Zero Cold Starts**: Pre-loaded KV namespaces at each edge
12
+ - **Low Latency**: ~10ms read/write latency from any location
13
+ - **Distributed by Default**: No replication config needed
14
+
15
+ Ideal for Hono-based upload servers deployed to Cloudflare Workers.
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @uploadista/kv-store-cloudflare-kv
21
+ # or
22
+ pnpm add @uploadista/kv-store-cloudflare-kv
23
+ ```
24
+
25
+ ### Prerequisites
26
+
27
+ - Cloudflare Workers project with `wrangler`
28
+ - Cloudflare account with Workers enabled
29
+ - KV namespace created via `wrangler kv:namespace`
30
+
31
+ ## Quick Start
32
+
33
+ ```typescript
34
+ import { cloudflareKvStore } from "@uploadista/kv-store-cloudflare-kv";
35
+ import { Effect } from "effect";
36
+
37
+ // In Cloudflare Worker environment, KV is provided as a binding
38
+ export default {
39
+ fetch(request: Request, env: { KV_STORE: KVNamespace }) {
40
+ const program = Effect.gen(function* () {
41
+ // The cloudflareKvStore is automatically available
42
+ });
43
+
44
+ return Effect.runPromise(
45
+ program.pipe(
46
+ Effect.provide(cloudflareKvStore({ kv: env.KV_STORE }))
47
+ )
48
+ );
49
+ },
50
+ };
51
+ ```
52
+
53
+ ## Features
54
+
55
+ - ✅ **Global Distribution**: Data replicated to 300+ edge locations
56
+ - ✅ **Low Latency**: Sub-second read/write operations
57
+ - ✅ **Serverless**: No origin server infrastructure needed
58
+ - ✅ **Auto-Scaling**: Handles traffic spikes automatically
59
+ - ✅ **Strong Consistency**: Atomic reads and writes
60
+ - ✅ **No Cold Starts**: Workers start in ~1ms
61
+ - ✅ **Type Safe**: Full TypeScript support
62
+
63
+ ## API Reference
64
+
65
+ ### Main Exports
66
+
67
+ #### `cloudflareKvStore(config: CloudflareKvStoreConfig): Layer<BaseKvStoreService>`
68
+
69
+ Creates an Effect layer providing the `BaseKvStoreService` backed by Cloudflare KV.
70
+
71
+ ```typescript
72
+ import { cloudflareKvStore } from "@uploadista/kv-store-cloudflare-kv";
73
+
74
+ export default {
75
+ fetch(request: Request, env: { KV_STORE: KVNamespace }) {
76
+ const layer = cloudflareKvStore({ kv: env.KV_STORE });
77
+ },
78
+ };
79
+ ```
80
+
81
+ **Configuration**:
82
+
83
+ ```typescript
84
+ type CloudflareKvStoreConfig = {
85
+ kv: KVNamespace<string>; // Cloudflare KV namespace binding
86
+ };
87
+ ```
88
+
89
+ #### `makeCloudflareBaseKvStore(config: CloudflareKvStoreConfig): BaseKvStore`
90
+
91
+ Factory function for creating a KV store with an existing KVNamespace.
92
+
93
+ ```typescript
94
+ import { makeCloudflareBaseKvStore } from "@uploadista/kv-store-cloudflare-kv";
95
+
96
+ const store = makeCloudflareBaseKvStore({ kv: env.KV_STORE });
97
+ ```
98
+
99
+ ### Available Operations
100
+
101
+ The Cloudflare KV store implements the `BaseKvStore` interface:
102
+
103
+ #### `get(key: string): Effect<string | null>`
104
+
105
+ Retrieve a value by key.
106
+
107
+ ```typescript
108
+ const program = Effect.gen(function* () {
109
+ const value = yield* store.get("upload:abc123");
110
+ // Returns immediately from nearest edge location
111
+ });
112
+ ```
113
+
114
+ #### `set(key: string, value: string): Effect<void>`
115
+
116
+ Store a string value globally.
117
+
118
+ ```typescript
119
+ const program = Effect.gen(function* () {
120
+ yield* store.set("upload:abc123", JSON.stringify(metadata));
121
+ // Replicated to all edge locations within seconds
122
+ });
123
+ ```
124
+
125
+ #### `delete(key: string): Effect<void>`
126
+
127
+ Remove a key globally.
128
+
129
+ ```typescript
130
+ const program = Effect.gen(function* () {
131
+ yield* store.delete("upload:abc123");
132
+ });
133
+ ```
134
+
135
+ #### `list(keyPrefix: string): Effect<string[]>`
136
+
137
+ List keys matching a prefix.
138
+
139
+ ```typescript
140
+ const program = Effect.gen(function* () {
141
+ const keys = yield* store.list("upload:");
142
+ // Returns matching keys
143
+ });
144
+ ```
145
+
146
+ ## Configuration
147
+
148
+ ### Basic Setup in wrangler.toml
149
+
150
+ ```toml
151
+ name = "uploadista-worker"
152
+ type = "javascript"
153
+
154
+ [env.production]
155
+ kv_namespaces = [
156
+ { binding = "KV_STORE", id = "abc123def456", preview_id = "preview789" }
157
+ ]
158
+
159
+ [env.staging]
160
+ kv_namespaces = [
161
+ { binding = "KV_STORE", id = "staging-id", preview_id = "staging-preview" }
162
+ ]
163
+ ```
164
+
165
+ ### Worker Environment Setup
166
+
167
+ ```typescript
168
+ export interface Env {
169
+ KV_STORE: KVNamespace<string>;
170
+ ENVIRONMENT: "production" | "staging" | "development";
171
+ }
172
+
173
+ export default {
174
+ async fetch(request: Request, env: Env) {
175
+ const program = Effect.gen(function* () {
176
+ // Use env.KV_STORE
177
+ });
178
+
179
+ return Effect.runPromise(
180
+ program.pipe(
181
+ Effect.provide(cloudflareKvStore({ kv: env.KV_STORE }))
182
+ )
183
+ );
184
+ },
185
+ };
186
+ ```
187
+
188
+ ### Environment-Specific Configuration
189
+
190
+ ```typescript
191
+ export default {
192
+ async fetch(request: Request, env: Env) {
193
+ const isProduction = env.ENVIRONMENT === "production";
194
+
195
+ const program = Effect.gen(function* () {
196
+ // Use KV with environment awareness
197
+ const cacheKey = isProduction ? "cache:prod" : "cache:dev";
198
+ });
199
+
200
+ return Effect.runPromise(
201
+ program.pipe(
202
+ Effect.provide(cloudflareKvStore({ kv: env.KV_STORE }))
203
+ )
204
+ );
205
+ },
206
+ };
207
+ ```
208
+
209
+ ## Examples
210
+
211
+ ### Example 1: Cloudflare Workers Upload Server
212
+
213
+ Complete serverless upload handler:
214
+
215
+ ```typescript
216
+ import { cloudflareKvStore } from "@uploadista/kv-store-cloudflare-kv";
217
+ import { uploadServer } from "@uploadista/server";
218
+ import { Effect } from "effect";
219
+
220
+ export interface Env {
221
+ KV_STORE: KVNamespace;
222
+ BUCKET: R2Bucket; // Cloudflare R2 for file storage
223
+ }
224
+
225
+ export default {
226
+ async fetch(request: Request, env: Env) {
227
+ const url = new URL(request.url);
228
+
229
+ if (url.pathname === "/api/upload" && request.method === "POST") {
230
+ const program = Effect.gen(function* () {
231
+ const server = yield* uploadServer;
232
+
233
+ // Create upload session
234
+ const upload = yield* server.createUpload(
235
+ {
236
+ filename: "user-file.pdf",
237
+ size: 5242880,
238
+ mimeType: "application/pdf",
239
+ },
240
+ "client:123"
241
+ );
242
+
243
+ return new Response(
244
+ JSON.stringify({
245
+ uploadId: upload.id,
246
+ chunkSize: 1048576,
247
+ })
248
+ );
249
+ });
250
+
251
+ return Effect.runPromise(
252
+ program.pipe(
253
+ Effect.provide(cloudflareKvStore({ kv: env.KV_STORE }))
254
+ )
255
+ );
256
+ }
257
+
258
+ return new Response("Not found", { status: 404 });
259
+ },
260
+ };
261
+ ```
262
+
263
+ ### Example 2: Request Deduplication
264
+
265
+ Use KV to prevent duplicate uploads:
266
+
267
+ ```typescript
268
+ import { cloudflareKvStore } from "@uploadista/kv-store-cloudflare-kv";
269
+ import { Effect } from "effect";
270
+
271
+ const dedupKey = (clientId: string, checksum: string) =>
272
+ `dedup:${clientId}:${checksum}`;
273
+
274
+ const checkDedup = (kv: KVNamespace, clientId: string, checksum: string) =>
275
+ Effect.gen(function* () {
276
+ const key = dedupKey(clientId, checksum);
277
+ const existing = yield* Effect.tryPromise({
278
+ try: () => kv.get(key),
279
+ catch: () => null,
280
+ });
281
+
282
+ if (existing) {
283
+ return JSON.parse(existing); // Return existing upload
284
+ }
285
+
286
+ return null; // New upload
287
+ });
288
+
289
+ const recordDedup = (
290
+ kv: KVNamespace,
291
+ clientId: string,
292
+ checksum: string,
293
+ uploadId: string
294
+ ) =>
295
+ Effect.gen(function* () {
296
+ const key = dedupKey(clientId, checksum);
297
+ yield* Effect.tryPromise({
298
+ try: () =>
299
+ kv.put(key, JSON.stringify({ uploadId }), {
300
+ expirationTtl: 86400, // 24 hours
301
+ }),
302
+ catch: () => null,
303
+ });
304
+ });
305
+
306
+ // Usage in worker
307
+ export default {
308
+ async fetch(request: Request, env: Env) {
309
+ const existing = yield* checkDedup(env.KV_STORE, clientId, checksum);
310
+
311
+ if (existing) {
312
+ return new Response(JSON.stringify(existing));
313
+ }
314
+
315
+ // Create new upload...
316
+ yield* recordDedup(env.KV_STORE, clientId, checksum, uploadId);
317
+ },
318
+ };
319
+ ```
320
+
321
+ ### Example 3: Cache-Aside Pattern
322
+
323
+ Use KV for upload metadata caching:
324
+
325
+ ```typescript
326
+ import { cloudflareKvStore } from "@uploadista/kv-store-cloudflare-kv";
327
+ import { Effect } from "effect";
328
+
329
+ const getUploadMetadata = (
330
+ kv: KVNamespace,
331
+ uploadId: string
332
+ ) =>
333
+ Effect.gen(function* () {
334
+ // Check cache first
335
+ const cached = yield* Effect.tryPromise({
336
+ try: () => kv.get(`meta:${uploadId}`),
337
+ catch: () => null,
338
+ });
339
+
340
+ if (cached) {
341
+ return JSON.parse(cached);
342
+ }
343
+
344
+ // Fetch from source (e.g., database)
345
+ const metadata = yield* fetchFromDatabase(uploadId);
346
+
347
+ // Cache result
348
+ yield* Effect.tryPromise({
349
+ try: () =>
350
+ kv.put(`meta:${uploadId}`, JSON.stringify(metadata), {
351
+ expirationTtl: 3600, // 1 hour
352
+ }),
353
+ catch: () => null,
354
+ });
355
+
356
+ return metadata;
357
+ });
358
+ ```
359
+
360
+ ## Performance Characteristics
361
+
362
+ | Operation | Latency | Global Sync |
363
+ |-----------|---------|-------------|
364
+ | get() | ~10ms | Immediate |
365
+ | set() | ~50ms | ~30 seconds |
366
+ | delete() | ~50ms | ~30 seconds |
367
+ | list() | ~100ms | N/A (edge) |
368
+
369
+ All operations are served from the nearest edge location to the client.
370
+
371
+ ## Limits & Quotas
372
+
373
+ | Limit | Value |
374
+ |-------|-------|
375
+ | Key Size | 512 bytes max |
376
+ | Value Size | 25 MB max |
377
+ | Namespace Storage | Unlimited (Capped at billing) |
378
+ | Read/Write Rate | 10,000 ops/sec per namespace |
379
+ | API Requests | Included in Workers plan |
380
+
381
+ For most upload use cases, these limits are more than sufficient.
382
+
383
+ ## Best Practices
384
+
385
+ ### 1. Use Meaningful Key Prefixes
386
+
387
+ ```typescript
388
+ // Good: Hierarchical, searchable
389
+ "upload:abc123"
390
+ "upload:abc123:chunk:0"
391
+ "session:user:xyz"
392
+
393
+ // Avoid: Unclear prefixes
394
+ "data1", "tmp", "x"
395
+ ```
396
+
397
+ ### 2. Set Expiration on Temporary Data
398
+
399
+ ```typescript
400
+ // Temporary upload sessions expire after 24 hours
401
+ kv.put("session:user123", sessionData, {
402
+ expirationTtl: 86400, // seconds
403
+ });
404
+ ```
405
+
406
+ ### 3. Handle Eventual Consistency
407
+
408
+ ```typescript
409
+ // Wait for global replication
410
+ const isReplicated = yield* Effect.gen(function* () {
411
+ const retries = 3;
412
+ for (let i = 0; i < retries; i++) {
413
+ const value = yield* store.get(key);
414
+ if (value === expectedValue) {
415
+ return true;
416
+ }
417
+ yield* Effect.sleep("100 millis");
418
+ }
419
+ return false;
420
+ });
421
+ ```
422
+
423
+ ### 4. Batch Operations Efficiently
424
+
425
+ ```typescript
426
+ // Instead of individual writes
427
+ for (const item of items) {
428
+ yield* store.set(`item:${item.id}`, JSON.stringify(item));
429
+ }
430
+
431
+ // Better: Batch at application level
432
+ // Or use metadata object
433
+ const batch = items.reduce(
434
+ (acc, item) => ({ ...acc, [`item:${item.id}`]: item }),
435
+ {}
436
+ );
437
+ ```
438
+
439
+ ## Deployment
440
+
441
+ ### Local Development
442
+
443
+ ```bash
444
+ # Create local KV namespace
445
+ wrangler kv:namespace create "KV_STORE" --preview
446
+
447
+ # Run locally
448
+ wrangler dev
449
+
450
+ # Access KV from http://localhost:8787
451
+ ```
452
+
453
+ ### Production Deployment
454
+
455
+ ```bash
456
+ # Create production KV namespace
457
+ wrangler kv:namespace create "KV_STORE"
458
+
459
+ # Deploy to Cloudflare
460
+ wrangler publish
461
+
462
+ # Deploy to specific environment
463
+ wrangler publish --env production
464
+ ```
465
+
466
+ ### GitHub Actions
467
+
468
+ ```yaml
469
+ name: Deploy
470
+
471
+ on:
472
+ push:
473
+ branches: [main]
474
+
475
+ jobs:
476
+ deploy:
477
+ runs-on: ubuntu-latest
478
+ steps:
479
+ - uses: actions/checkout@v3
480
+ - uses: actions/setup-node@v3
481
+ with:
482
+ node-version: "18"
483
+ - run: npm ci
484
+ - run: npm run build
485
+ - uses: cloudflare/wrangler-action@v3
486
+ with:
487
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
488
+ ```
489
+
490
+ ## Integration with Other Services
491
+
492
+ ### Cloudflare R2 (File Storage)
493
+
494
+ ```typescript
495
+ export interface Env {
496
+ KV_STORE: KVNamespace;
497
+ R2_BUCKET: R2Bucket;
498
+ }
499
+
500
+ export default {
501
+ async fetch(request: Request, env: Env) {
502
+ // Use KV for metadata, R2 for files
503
+ yield* store.set("file:abc", JSON.stringify({ bucket: "uploads", key: "abc.pdf" }));
504
+ await env.R2_BUCKET.put("uploads/abc.pdf", file);
505
+ },
506
+ };
507
+ ```
508
+
509
+ ### Cloudflare D1 (Database)
510
+
511
+ ```typescript
512
+ export interface Env {
513
+ KV_STORE: KVNamespace;
514
+ DB: D1Database;
515
+ }
516
+
517
+ // Use KV for cache, D1 for persistent queries
518
+ const metadata = yield* getCachedMetadata(env.KV_STORE, id);
519
+ if (!metadata) {
520
+ const result = await env.DB.prepare("SELECT * FROM uploads WHERE id = ?").bind(id).first();
521
+ yield* store.set(`meta:${id}`, JSON.stringify(result));
522
+ }
523
+ ```
524
+
525
+ ## Related Packages
526
+
527
+ - [@uploadista/core](../../core) - Core types
528
+ - [@uploadista/adapters-hono](../../servers/adapters-hono) - Hono adapter for Cloudflare
529
+ - [@uploadista/data-store-s3](../../data-stores/s3) - For file storage on R2
530
+ - [@uploadista/server](../../servers/server) - Upload server
531
+ - [@uploadista/kv-store-cloudflare-do](../cloudflare-do) - Durable Objects for real-time state
532
+
533
+ ## Troubleshooting
534
+
535
+ ### "KV_STORE not found" Error
536
+
537
+ Ensure namespace is defined in `wrangler.toml`:
538
+
539
+ ```toml
540
+ kv_namespaces = [
541
+ { binding = "KV_STORE", id = "your-namespace-id" }
542
+ ]
543
+ ```
544
+
545
+ Then run `wrangler dev` to load bindings.
546
+
547
+ ### Data Not Visible Across Regions
548
+
549
+ KV replication takes 30-60 seconds globally. For immediate consistency:
550
+
551
+ ```typescript
552
+ // Read from same origin temporarily
553
+ const data = await kv.get(key, { cacheTtl: 0 });
554
+ ```
555
+
556
+ ### Memory/Size Limits
557
+
558
+ If hitting 25MB limit for single key:
559
+
560
+ 1. Split data: `"upload:123:part:0"`, `"upload:123:part:1"`
561
+ 2. Use R2 for actual files, KV only for metadata
562
+ 3. Implement cleanup: set `expirationTtl` on temporary data
563
+
564
+ ### Rate Limiting (10k ops/sec)
565
+
566
+ If hitting rate limit:
567
+
568
+ 1. Implement batching on client
569
+ 2. Use longer cache TTL
570
+ 3. Request quota increase from Cloudflare
571
+
572
+ ## License
573
+
574
+ See [LICENSE](../../../LICENSE) in the main repository.
575
+
576
+ ## See Also
577
+
578
+ - [KV Stores Comparison Guide](../KV_STORES_COMPARISON.md) - Compare with other stores
579
+ - [Cloudflare Workers Documentation](https://developers.cloudflare.com/workers/) - Official docs
580
+ - [Cloudflare KV API Reference](https://developers.cloudflare.com/workers/runtime-apis/kv/) - KV API
581
+ - [Server Setup with Cloudflare](../../../SERVER_SETUP.md#cloudflare-workers) - Server deployment
582
+ - [@uploadista/adapters-hono](../../servers/adapters-hono) - Hono integration example
@@ -0,0 +1,9 @@
1
+ import type { KVNamespace } from "@cloudflare/workers-types";
2
+ import { type BaseKvStore, BaseKvStoreService } from "@uploadista/core/types";
3
+ import { Layer } from "effect";
4
+ export type CloudflareKvStoreConfig = {
5
+ kv: KVNamespace<string>;
6
+ };
7
+ export declare function makeCloudflareBaseKvStore({ kv, }: CloudflareKvStoreConfig): BaseKvStore;
8
+ export declare const cloudflareKvStore: (config: CloudflareKvStoreConfig) => Layer.Layer<BaseKvStoreService, never, never>;
9
+ //# sourceMappingURL=cloudflare-kv-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloudflare-kv-store.d.ts","sourceRoot":"","sources":["../src/cloudflare-kv-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EAEZ,MAAM,2BAA2B,CAAC;AAGnC,OAAO,EAAE,KAAK,WAAW,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC9E,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AAEvC,MAAM,MAAM,uBAAuB,GAAG;IACpC,EAAE,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;CACzB,CAAC;AAGF,wBAAgB,yBAAyB,CAAC,EACxC,EAAE,GACH,EAAE,uBAAuB,GAAG,WAAW,CAiDvC;AAGD,eAAO,MAAM,iBAAiB,GAAI,QAAQ,uBAAuB,kDACK,CAAC"}
@@ -0,0 +1,42 @@
1
+ import { UploadistaError } from "@uploadista/core/errors";
2
+ import { BaseKvStoreService } from "@uploadista/core/types";
3
+ import { Effect, Layer } from "effect";
4
+ // Base CloudFlare KV store that stores raw strings
5
+ export function makeCloudflareBaseKvStore({ kv, }) {
6
+ return {
7
+ get: (key) => Effect.tryPromise({
8
+ try: () => kv.get(key),
9
+ catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
10
+ }),
11
+ set: (key, value) => Effect.tryPromise({
12
+ try: () => kv.put(key, value),
13
+ catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
14
+ }).pipe(Effect.asVoid),
15
+ delete: (key) => Effect.tryPromise({
16
+ try: () => kv.delete(key),
17
+ catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
18
+ }).pipe(Effect.asVoid),
19
+ list: (keyPrefix) => Effect.gen(function* (_) {
20
+ const keys = new Set();
21
+ let cursor = null;
22
+ do {
23
+ const result = yield* _(Effect.tryPromise({
24
+ try: () => kv.list({
25
+ prefix: keyPrefix,
26
+ limit: 20,
27
+ cursor,
28
+ }),
29
+ catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
30
+ }));
31
+ cursor = result.list_complete ? null : result.cursor;
32
+ for (const key of result.keys) {
33
+ const unprefixedKey = key.name.replace(keyPrefix, "");
34
+ keys.add(unprefixedKey);
35
+ }
36
+ } while (cursor);
37
+ return Array.from(keys);
38
+ }),
39
+ };
40
+ }
41
+ // Base store layer
42
+ export const cloudflareKvStore = (config) => Layer.succeed(BaseKvStoreService, makeCloudflareBaseKvStore(config));
@@ -0,0 +1,2 @@
1
+ export * from "./cloudflare-kv-store";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from "./cloudflare-kv-store";
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@uploadista/kv-store-cloudflare-kv",
3
+ "type": "module",
4
+ "version": "0.0.3",
5
+ "description": "Cloudflare KV store for Uploadista",
6
+ "license": "MIT",
7
+ "author": "Uploadista",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js",
12
+ "default": "./dist/index.js"
13
+ }
14
+ },
15
+ "dependencies": {
16
+ "@cloudflare/workers-types": "4.20251011.0",
17
+ "effect": "3.18.4",
18
+ "@uploadista/core": "0.0.3"
19
+ },
20
+ "devDependencies": {
21
+ "@uploadista/typescript-config": "0.0.3"
22
+ },
23
+ "scripts": {
24
+ "build": "tsc -b",
25
+ "format": "biome format --write ./src",
26
+ "lint": "biome lint --write ./src",
27
+ "check": "biome check --write ./src"
28
+ }
29
+ }
@@ -0,0 +1,70 @@
1
+ import type {
2
+ KVNamespace,
3
+ KVNamespaceListResult,
4
+ } from "@cloudflare/workers-types";
5
+ import { UploadistaError } from "@uploadista/core/errors";
6
+
7
+ import { type BaseKvStore, BaseKvStoreService } from "@uploadista/core/types";
8
+ import { Effect, Layer } from "effect";
9
+
10
+ export type CloudflareKvStoreConfig = {
11
+ kv: KVNamespace<string>;
12
+ };
13
+
14
+ // Base CloudFlare KV store that stores raw strings
15
+ export function makeCloudflareBaseKvStore({
16
+ kv,
17
+ }: CloudflareKvStoreConfig): BaseKvStore {
18
+ return {
19
+ get: (key: string) =>
20
+ Effect.tryPromise({
21
+ try: () => kv.get(key),
22
+ catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
23
+ }),
24
+
25
+ set: (key: string, value: string) =>
26
+ Effect.tryPromise({
27
+ try: () => kv.put(key, value),
28
+ catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
29
+ }).pipe(Effect.asVoid),
30
+
31
+ delete: (key: string) =>
32
+ Effect.tryPromise({
33
+ try: () => kv.delete(key),
34
+ catch: (cause) => UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
35
+ }).pipe(Effect.asVoid),
36
+
37
+ list: (keyPrefix: string) =>
38
+ Effect.gen(function* (_) {
39
+ const keys = new Set<string>();
40
+ let cursor: string | null = null;
41
+
42
+ do {
43
+ const result: KVNamespaceListResult<unknown, string> = yield* _(
44
+ Effect.tryPromise({
45
+ try: () =>
46
+ kv.list({
47
+ prefix: keyPrefix,
48
+ limit: 20,
49
+ cursor,
50
+ }),
51
+ catch: (cause) =>
52
+ UploadistaError.fromCode("UNKNOWN_ERROR", { cause }),
53
+ }),
54
+ );
55
+
56
+ cursor = result.list_complete ? null : result.cursor;
57
+ for (const key of result.keys) {
58
+ const unprefixedKey = key.name.replace(keyPrefix, "");
59
+ keys.add(unprefixedKey);
60
+ }
61
+ } while (cursor);
62
+
63
+ return Array.from(keys);
64
+ }),
65
+ };
66
+ }
67
+
68
+ // Base store layer
69
+ export const cloudflareKvStore = (config: CloudflareKvStoreConfig) =>
70
+ Layer.succeed(BaseKvStoreService, makeCloudflareBaseKvStore(config));
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./cloudflare-kv-store";
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "@uploadista/typescript-config/server.json",
3
+ "compilerOptions": {
4
+ "baseUrl": "./",
5
+ "paths": {
6
+ "@/*": ["./src/*"]
7
+ },
8
+ "outDir": "./dist",
9
+ "rootDir": "./src",
10
+ "typeRoots": ["../../../../node_modules/@types"]
11
+ },
12
+ "include": ["src"]
13
+ }
@@ -0,0 +1 @@
1
+ {"root":["./src/cloudflare-kv-store.ts","./src/index.ts"],"version":"5.9.3"}