1 |
|
2 |
|
3 | import type { StorageArea, AllowedKey, Key } from 'kv-storage-interface';
|
4 |
|
5 | import { encodeKey, decodeKey, throwForDisallowedKey } from 'idb-key-to-string';
|
6 |
|
7 | import { KVPacker, StructuredPacker } from './packer.js';
|
8 |
|
9 | const OLD_DEFAULT_KV_NAMESPACE_KEY = 'CF_STORAGE_AREA__DEFAULT_KV_NAMESPACE';
|
10 | const DEFAULT_KV_NAMESPACE_KEY = 'DEFAULT_KV_NAMESPACE';
|
11 | const DEFAULT_STORAGE_AREA_NAME = 'default';
|
12 | const DIV = '/';
|
13 |
|
14 | const getProcessEnv = (k: string) => Reflect.get(Reflect.get(Reflect.get(self, 'process') || {}, 'env') || {}, k);
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 | export class CloudflareStorageArea implements StorageArea {
|
29 |
|
30 | #kv: KVNamespace;
|
31 | #packer: KVPacker;
|
32 | #encodeKey: typeof encodeKey;
|
33 | #decodeKey: typeof decodeKey;
|
34 | #paginationHelper: typeof paginationHelper;
|
35 |
|
36 |
|
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 |
|
128 | export interface KVOptions {
|
129 |
|
130 | namespace?: KVNamespace;
|
131 |
|
132 | packer?: KVPacker;
|
133 | [k: string]: any;
|
134 | }
|
135 |
|
136 | export interface KVPutOptions {
|
137 | expiration?: string | number;
|
138 | expirationTtl?: string | number;
|
139 | [k: string]: any;
|
140 | }
|
141 |
|
142 | export interface KVListOptions {
|
143 | prefix?: string
|
144 | [k: string]: any;
|
145 | }
|
146 |
|
147 |
|
148 |
|
149 | async 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 |
|
160 | export class KVStorageArea extends CloudflareStorageArea { }
|
161 |
|
162 | export type { AllowedKey, Key };
|
163 | export { CloudflareStorageArea as CFStorageArea };
|
164 | export { CloudflareStorageArea as StorageArea };
|