UNPKG

5.38 kBPlain TextView Raw
1// deno-lint-ignore-file no-explicit-any
2// import 'https://cdn.skypack.dev/@cloudflare/workers-types@3.11.0?dts'
3import type { StorageArea, AllowedKey, Key } from 'kv-storage-interface';
4
5import { encodeKey, decodeKey, throwForDisallowedKey } from 'idb-key-to-string';
6
7import { KVPacker, StructuredPacker } from './packer.js';
8
9const OLD_DEFAULT_KV_NAMESPACE_KEY = 'CF_STORAGE_AREA__DEFAULT_KV_NAMESPACE';
10const DEFAULT_KV_NAMESPACE_KEY = 'DEFAULT_KV_NAMESPACE';
11const DEFAULT_STORAGE_AREA_NAME = 'default';
12const DIV = '/';
13
14const getProcessEnv = (k: string) => Reflect.get(Reflect.get(Reflect.get(self, 'process') || {}, 'env') || {}, k);
15
16/**
17 * An implementation of the `StorageArea` interface wrapping Cloudflare Worker's KV store.
18 *
19 * The goal of this class is ease of use and compatibility with other Storage Area implementations,
20 * such as <https://github.com/GoogleChromeLabs/kv-storage-polyfill>.
21 *
22 * While work on [the specification](https://wicg.github.io/kv-storage/) itself has stopped,
23 * it's still a good interface for asynchronous data access that feels native to JavaScript.
24 *
25 * Note that efficiency is not a goal. Specifically, if you have sizable `ArrayBuffer`s,
26 * it's much better to use Cloudflare's KV directly.
27 */
28export class CloudflareStorageArea implements StorageArea {
29 // @ts-ignore: deno only
30 #kv: KVNamespace;
31 #packer: KVPacker;
32 #encodeKey: typeof encodeKey;
33 #decodeKey: typeof decodeKey;
34 #paginationHelper: typeof paginationHelper;
35
36 // @ts-ignore: deno only
37 static defaultKVNamespace?: KVNamespace;
38
39 constructor(name?: string, opts?: KVOptions);
40 // @ts-ignore: deno only
41 constructor(name?: KVNamespace, opts?: Omit<KVOptions, 'namespace'>);
42 // @ts-ignore: deno only
43 constructor(name: string | KVNamespace = DEFAULT_STORAGE_AREA_NAME, options: KVOptions = {}) {
44 let { namespace, packer = new StructuredPacker() } = options;
45
46 namespace = namespace
47 || CloudflareStorageArea.defaultKVNamespace
48 || Reflect.get(self, Reflect.get(self, DEFAULT_KV_NAMESPACE_KEY))
49 || Reflect.get(self, Reflect.get(self, OLD_DEFAULT_KV_NAMESPACE_KEY))
50 || Reflect.get(self, getProcessEnv(DEFAULT_KV_NAMESPACE_KEY));
51
52 this.#kv = namespace
53 ? namespace
54 : typeof name === 'string'
55 ? Reflect.get(self, name)
56 : name;
57
58 if (!this.#kv) {
59 throw Error('KV binding missing. Consult Workers documentation for details');
60 }
61
62 this.#encodeKey = !namespace
63 ? encodeKey
64 : k => `${name}${DIV}${encodeKey(k)}`;
65
66 this.#decodeKey = !namespace
67 ? decodeKey
68 : k => decodeKey(k.substring((name as string).length + 1));
69
70 this.#paginationHelper = !namespace
71 ? paginationHelper
72 : (kv, { prefix, ...opts } = {}) => paginationHelper(kv, {
73 prefix: `${name}${DIV}${prefix ?? ''}`,
74 ...opts,
75 });
76
77 this.#packer = packer;
78 }
79
80 get<T>(key: AllowedKey, opts?: unknown): Promise<T> {
81 throwForDisallowedKey(key);
82 return this.#packer.get(this.#kv, this.#encodeKey(key), opts);
83 }
84
85 async set<T>(key: AllowedKey, value: T | undefined, opts?: KVPutOptions): Promise<void> {
86 throwForDisallowedKey(key);
87 if (value === undefined)
88 await this.#kv.delete(this.#encodeKey(key));
89 else {
90 await this.#packer.set(this.#kv, this.#encodeKey(key), value, opts);
91 }
92 }
93
94 delete(key: AllowedKey) {
95 throwForDisallowedKey(key);
96 return this.#kv.delete(this.#encodeKey(key));
97 }
98
99 async clear(opts?: KVListOptions) {
100 for await (const key of this.#paginationHelper(this.#kv, opts)) {
101 await this.#kv.delete(key)
102 }
103 }
104
105 async *keys(opts?: KVListOptions): AsyncGenerator<Key> {
106 for await (const key of this.#paginationHelper(this.#kv, opts)) {
107 yield this.#decodeKey(key);
108 }
109 }
110
111 async *values<T>(opts?: KVListOptions): AsyncGenerator<T> {
112 for await (const key of this.#paginationHelper(this.#kv, opts)) {
113 yield this.#packer.get(this.#kv, key, opts);
114 }
115 }
116
117 async *entries<T>(opts?: KVListOptions): AsyncGenerator<[Key, T]> {
118 for await (const key of this.#paginationHelper(this.#kv, opts)) {
119 yield [this.#decodeKey(key), await this.#packer.get(this.#kv, key, opts)];
120 }
121 }
122
123 backingStore() {
124 return this.#kv;
125 }
126}
127
128export interface KVOptions {
129 // @ts-ignore: deno only
130 namespace?: KVNamespace;
131 /** @deprecated This feature is not stable yet. */
132 packer?: KVPacker;
133 [k: string]: any;
134}
135
136export interface KVPutOptions {
137 expiration?: string | number;
138 expirationTtl?: string | number;
139 [k: string]: any;
140}
141
142export interface KVListOptions {
143 prefix?: string
144 [k: string]: any;
145}
146
147/** Abstracts Cloudflare KV's cursor-based pagination with async iteration. */
148// @ts-ignore: deno only
149async function* paginationHelper(kv: KVNamespace, opts: KVListOptions = {}) {
150 let keys: { name: string; expiration?: number; metadata?: unknown }[];
151 let done: boolean;
152 let cursor: string | undefined;
153 do {
154 ({ keys, list_complete: done, cursor } = await kv.list({ ...cursor ? { ...opts, cursor } : opts }));
155 for (const { name } of keys) yield name;
156 } while (!done);
157}
158
159/** @deprecated for backwards compat with v0.2.0 */
160export class KVStorageArea extends CloudflareStorageArea { }
161
162export type { AllowedKey, Key };
163export { CloudflareStorageArea as CFStorageArea };
164export { CloudflareStorageArea as StorageArea };