polystore 0.5.0 → 0.6.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polystore",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "A small compatibility layer for many popular KV stores like localStorage, Redis, FileSystem, etc.",
5
5
  "homepage": "https://github.com/franciscop/polystore",
6
6
  "repository": "https://github.com/franciscop/polystore.git",
package/readme.md CHANGED
@@ -1,4 +1,4 @@
1
- # Polystore [![npm install polystore](https://img.shields.io/badge/npm%20install-polystore-blue.svg)](https://www.npmjs.com/package/polystore) [![test badge](https://github.com/franciscop/polystore/workflows/tests/badge.svg "test badge")](https://github.com/franciscop/polystore/blob/master/.github/workflows/tests.yml) [![gzip size](https://img.badgesize.io/franciscop/polystore/master/index.min.js.svg?compression=gzip)](https://github.com/franciscop/polystore/blob/master/index.min.js)
1
+ # Polystore [![npm install polystore](https://img.shields.io/badge/npm%20install-polystore-blue.svg)](https://www.npmjs.com/package/polystore) [![test badge](https://github.com/franciscop/polystore/workflows/tests/badge.svg "test badge")](https://github.com/franciscop/polystore/blob/master/.github/workflows/tests.yml) [![gzip size](https://img.badgesize.io/franciscop/polystore/master/src/index.js.svg?compression=gzip)](https://github.com/franciscop/polystore/blob/master/src/index.js)
2
2
 
3
3
  A small compatibility layer for many KV stores like localStorage, Redis, FileSystem, etc:
4
4
 
@@ -14,9 +14,12 @@ This is the [API](#api) with all of the methods (they are all `async`):
14
14
 
15
15
  - `.get(key): any`: retrieve a single value, or `null` if it doesn't exist or is expired.
16
16
  - `.set(key, value, options?)`: save a single value, which can be anything that is serializable.
17
+ - `.add(value, options?)`: same as with `.set()`, but auto-generate the key.
17
18
  - `.has(key): boolean`: check whether the key is in the store or not.
18
19
  - `.del(key)`: delete a single value from the store.
19
20
  - `.keys(prefix?): string[]`: get a list of all the available strings in the store.
21
+ - `.values(prefix?): any[]`: get a list of all the values in the store.
22
+ - `.entries(prefix?): [string, any][]`: get a list of all the key-value pairs in the store.
20
23
  - `.clear()`: delete ALL of the data in the store, effectively resetting it.
21
24
  - `.close()`: (only _some_ stores) ends the connection to the store.
22
25
 
@@ -91,7 +94,7 @@ If the value is returned, it can be a simple type like `boolean`, `string` or `n
91
94
 
92
95
  ### .set()
93
96
 
94
- Create or update a value in the store. Will return a promise that resolves when the value has been saved. The value needs to be serializable:
97
+ Create or update a value in the store. Will return a promise that resolves with the key when the value has been saved. The value needs to be serializable:
95
98
 
96
99
  ```js
97
100
  await store.set(key: string, value: any, options?: { expires: number|string });
@@ -109,29 +112,53 @@ The value can be a simple type like `boolean`, `string` or `number`, or it can b
109
112
 
110
113
  #### Expires
111
114
 
112
- When the `expires` option is set, it can be a number (ms) or a string representing some time:
115
+ When the `expires` option is set, it can be a number (**seconds**) or a string representing some time:
113
116
 
114
117
  ```js
115
118
  // Valid "expire" values:
116
119
  0 - expire immediately (AKA delete it)
117
- 100 - expire after 100ms
118
- 60 * 60 * 1000 - expire after 1h
119
- 3_600_000 - expire after 1h
120
+ 0.1 - expire after 100ms*
121
+ 60 * 60 - expire after 1h
122
+ 3_600 - expire after 1h
120
123
  "10s" - expire after 10 seconds
121
124
  "2minutes" - expire after 2 minutes
122
125
  "5d" - expire after 5 days
123
126
  ```
124
127
 
128
+ \* not all stores support sub-second expirations, notably Redis and Cookies don't, so it's safer to always use an integer or an amount larger than 1s
129
+
125
130
  These are all the units available:
126
131
 
127
132
  > "ms", "millisecond", "s", "sec", "second", "m", "min", "minute", "h", "hr", "hour", "d", "day", "w", "wk", "week", "b" (month), "month", "y", "yr", "year"
128
133
 
134
+ ### .add()
135
+
136
+ Create a value in the store with a random key string. Will return a promise that resolves with the key when the value has been saved. The value needs to be serializable:
137
+
138
+ ```js
139
+ const key:string = await store.add(value: any, options?: { expires: number|string });
140
+
141
+ const key1 = await store.add("Hello World");
142
+ const key2 = await store.add(["my", "grocery", "list"], { expires: "1h" });
143
+ const key3 = await store.add({ name: "Francisco" }, { expires: 60 * 60 * 1000 });
144
+ ```
145
+
146
+ The generated key is 24 AlphaNumeric characters (including upper and lower case) generated with random cryptography to make sure it's unguessable, high entropy and safe to use in most contexts like URLs, queries, etc. We use [`nanoid`](https://github.com/ai/nanoid/) with a custom dictionary, so you can check the entropy [in this dictionary](https://zelark.github.io/nano-id-cc/) by removing the "\_" and "-", and setting it to 24 characters.
147
+
148
+ Here is the safety: "If you generate 1 million keys/second, it will take ~14 million years in order to have a 1% probability of at least one collision."
149
+
150
+ > Note: please make sure to read the [`.set()`](#set) section for all the details, since `.set()` and `.add()` behave the same way except for the first argument.
151
+
129
152
  ### .has()
130
153
 
131
154
  Check whether the key is available in the store and not expired:
132
155
 
133
156
  ```js
134
157
  await store.has(key: string);
158
+
159
+ if (await store.has('cookie-consent')) {
160
+ loadCookies();
161
+ }
135
162
  ```
136
163
 
137
164
  ### .del()
@@ -150,6 +177,45 @@ Get all of the keys in the store, optionally filtered by a prefix:
150
177
  await store.keys(filter?: string);
151
178
  ```
152
179
 
180
+ > We ensure that all of the keys returned by this method are _not_ expired, while discarding any potentially expired key. See [**expiration explained**](#expiration-explained) for more details.
181
+
182
+ ### .values()
183
+
184
+ Get all of the values in the store, optionally filtered by a **key** prefix:
185
+
186
+ ```js
187
+ await store.values(filter?: string);
188
+ ```
189
+
190
+ This is useful specially when you already have the id/key within the value as an object, then you can just get a list of all of them:
191
+
192
+ ```js
193
+ const sessions = await store.values("session:");
194
+ // A list of all the sessions
195
+
196
+ const companies = await store.values("company:");
197
+ // A list of all the companies
198
+ ```
199
+
200
+ > We ensure that all of the values returned by this method are _not_ expired, while discarding any potentially expired key. See [**expiration explained**](#expiration-explained) for more details.
201
+
202
+ ### .entries()
203
+
204
+ Get all of the entries (key:value tuples) in the store, optionally filtered by a **key** prefix:
205
+
206
+ ```js
207
+ await store.entries(filter?: string);
208
+ ```
209
+
210
+ It is in a format that you can easily build an object out of it:
211
+
212
+ ```js
213
+ const sessionEntries = await store.entries("session:");
214
+ const sessions = Object.fromEntries(sessionEntries);
215
+ ```
216
+
217
+ > We ensure that all of the entries returned by this method are _not_ expired, while discarding any potentially expired key. See [**expiration explained**](#expiration-explained) for more details.
218
+
153
219
  ### .clear()
154
220
 
155
221
  Remove all of the data from the store:
@@ -164,26 +230,26 @@ Create a sub-store where all the operations use the given prefix:
164
230
 
165
231
  ```js
166
232
  const store = kv(new Map());
167
- const sub = store.prefix("session:");
233
+ const session = store.prefix("session:");
168
234
  ```
169
235
 
170
236
  Then all of the operations will be converted internally to add the prefix when reading, writing, etc:
171
237
 
172
238
  ```js
173
- const val = await sub.get("key1"); // .get('session:key1');
174
- await sub.set("key2", "some data"); // .set('session:key2', ...);
175
- const val = await sub.has("key3"); // .has('session:key3');
176
- await sub.del("key4"); // .del('session:key4');
177
- await sub.keys(); // .keys('session:');
239
+ const val = await session.get("key1"); // .get('session:key1');
240
+ await session.set("key2", "some data"); // .set('session:key2', ...);
241
+ const val = await session.has("key3"); // .has('session:key3');
242
+ await session.del("key4"); // .del('session:key4');
243
+ await session.keys(); // .keys('session:');
178
244
  // ['key1', 'key2', ...] Note no prefix here
179
- await sub.clear(); // delete only keys with the prefix
245
+ await session.clear(); // delete only keys with the prefix
180
246
  ```
181
247
 
182
248
  This will probably never be stable given the nature of some engines, so as an alternative please consider using two stores instead of prefixes:
183
249
 
184
250
  ```js
185
251
  const store = kv(new Map());
186
- const sessionStore = kv(new Map());
252
+ const session = kv(new Map());
187
253
  ```
188
254
 
189
255
  The main reason this is not stable is because [_some_ store engines don't allow for atomic deletion of keys given a prefix](https://stackoverflow.com/q/4006324/938236). While we do still clear them internally in those cases, that is a non-atomic operation and it could have some trouble if some other thread is reading/writing the data _at the same time_.
@@ -251,7 +317,7 @@ console.log(await store.get("key1"));
251
317
 
252
318
  It is fairly limited for how powerful cookies are, but in exchange it has the same API as any other method or KV store. It works with browser-side Cookies (no http-only).
253
319
 
254
- > Note: the cookie expire resolution is in the seconds. While it still expects you to pass the number of ms as with the other methods (or [a string like `1h`](#expires)), times shorter than 1 second like `expires: 200` (ms) don't make sense for this storage method and won't properly save them.
320
+ > Note: the cookie expire resolution is in the seconds, so times shorter than 1 second like `expires: 0.02` (20 ms) don't make sense for this storage method and won't properly save them.
255
321
 
256
322
  ### Local Forage
257
323
 
@@ -280,7 +346,7 @@ await store.set("key1", "Hello world");
280
346
  console.log(await store.get("key1"));
281
347
  ```
282
348
 
283
- > Note: the Redis client expire resolution is in the seconds. While it still expects you to pass the number of ms as with the other methods (or [a string like `1h`](#expires)), times shorter than 1 second like `expires: 200` (ms) don't make sense for this storage method and won't properly save them.
349
+ > Note: the Redis client expire resolution is in the seconds, so times shorter than 1 second like `expires: 0.02` (20 ms) don't make sense for this storage method and won't properly save them.
284
350
 
285
351
  ### FS File
286
352
 
@@ -317,7 +383,7 @@ export default {
317
383
  };
318
384
  ```
319
385
 
320
- The Cloudflare native KV store only accepts strings and has you manually calculating timeouts, but as usual with `polystore` you can set/get any serializable value and set the timeout in a familiar format:
386
+ Why? The Cloudflare native KV store only accepts strings and has you manually calculating timeouts, but as usual with `polystore` you can set/get any serializable value and set the timeout in a familiar format:
321
387
 
322
388
  ```js
323
389
  // GOOD - with polystore
@@ -330,3 +396,55 @@ await env.YOUR_KV_NAMESPACE.put("user", serialValue, {
330
396
  expirationTtl: twoDaysInSeconds,
331
397
  });
332
398
  ```
399
+
400
+ ## Expiration explained
401
+
402
+ While different engines do expiration slightly differently internally, in creating polystore we want to ensure certain constrains, which _can_ affect performance. For example, if you do this operation:
403
+
404
+ ```js
405
+ // in-memory store
406
+ const store = polystore(new Map());
407
+ await store.set("a", "b", { expires: "1s" });
408
+
409
+ // These checks of course work:
410
+ console.log(await store.keys()); // ['a']
411
+ console.log(await store.has("a")); // true
412
+ console.log(await store.get("a")); // 'b'
413
+
414
+ // Make sure the key is expired
415
+ await delay(2000); // 2s
416
+
417
+ // Not only the .get() is null, but `.has()` returns false, and .keys() ignores it
418
+ console.log(await store.keys()); // []
419
+ console.log(await store.has("a")); // false
420
+ console.log(await store.get("a")); // null
421
+ ```
422
+
423
+ This is great because with polystore we do ensure that if a key has expired, it doesn't show up in `.keys()`, `.entries()`, `.values()`, `.has()` or `.get()`.
424
+
425
+ However, in some stores this does come with some potential performance disadvantages. For example, both the in-memory example above and localStorage _don't_ have a native expiration/eviction process, so we have to store that information as metadata, meaning that even to check if a key exists we need to read and decode its value. For one or few keys it's not a problem, but for large sets this can become an issue.
426
+
427
+ For other stores like Redis this is not a problem, because the low-level operations already do them natively, so we don't need to worry about this for performance at the user-level. Instead, Redis and cookies have the problem that they only have expiration resolution at the second level. Meaning that 800ms is not a valid Redis expiration time, it has to be 1s, 2s, etc.
428
+
429
+ ## Creating a store
430
+
431
+ A store needs at least 4 methods with these signatures:
432
+
433
+ ```js
434
+ const store = {};
435
+ store.get = (key: string) => Promise<any>;
436
+ store.set = (key: string, value: any, { expires: number }) => Promise<string>;
437
+ store.entries = (prefix: string = "") => Promise<[key:string, value:any][]>;
438
+ store.clear = () => Promise<null>;
439
+ ```
440
+
441
+ All of the other methods will be implemented on top of these if not available, but you can provide those as well for optimizations, incompatible APIs, etc. For example, `.set('a', null)` _should_ delete the key `a`, and for this you may provide a native implementation:
442
+
443
+ ```js
444
+ const native = myNativeStore();
445
+
446
+ const store = {};
447
+ store.get = (key) => native.getItem(key);
448
+ // ...
449
+ store.del = (key) => native.deleteItem(key);
450
+ ```
package/src/index.d.ts CHANGED
@@ -1,15 +1,19 @@
1
+ type Key = string;
2
+ type Options = { expires?: number | string | null };
3
+
1
4
  type Store = {
2
- get: (key: string) => Promise<any>;
3
- set: (
4
- key: string,
5
- value: any,
6
- opts?: { expires?: number | string | null }
7
- ) => Promise<null>;
8
- has: (key: string) => Promise<boolean>;
9
- del: (key: string) => Promise<null>;
5
+ get: (key: Key) => Promise<any>;
6
+ add: (value: any, options?: Options) => Promise<Key>;
7
+ set: (key: Key, value: any, options?: Options) => Promise<Key>;
8
+ has: (key: Key) => Promise<boolean>;
9
+ del: (key: Key) => Promise<null>;
10
10
 
11
11
  keys: (prefix?: string) => Promise<string[]>;
12
+ values: (prefix?: string) => Promise<any[]>;
13
+ entries: (prefix?: string) => Promise<[key: string, value: any][]>;
14
+
12
15
  clear: () => Promise<null>;
16
+ close?: () => Promise<null>;
13
17
  };
14
18
 
15
19
  export default function (store?: any): Store;
package/src/index.js CHANGED
@@ -2,8 +2,8 @@ const layers = {};
2
2
 
3
3
  const times = /(-?(?:\d+\.?\d*|\d*\.?\d+)(?:e[-+]?\d+)?)\s*([\p{L}]*)/iu;
4
4
 
5
- parse.millisecond = parse.ms = 1;
6
- parse.second = parse.sec = parse.s = parse[""] = parse.ms * 1000;
5
+ parse.millisecond = parse.ms = 0.001;
6
+ parse.second = parse.sec = parse.s = parse[""] = 1;
7
7
  parse.minute = parse.min = parse.m = parse.s * 60;
8
8
  parse.hour = parse.hr = parse.h = parse.m * 60;
9
9
  parse.day = parse.d = parse.h * 24;
@@ -22,17 +22,53 @@ function parse(str) {
22
22
  const unitValue = parse[units] || parse[units.replace(/s$/, "")];
23
23
  if (!unitValue) return null;
24
24
  const result = unitValue * parseFloat(value, 10);
25
- return Math.abs(Math.round(result));
25
+ return Math.abs(Math.round(result * 1000) / 1000);
26
26
  }
27
27
 
28
+ // "nanoid" imported manually
29
+ // Something about improved GZIP performance with this string
30
+ const urlAlphabet =
31
+ "useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict";
32
+
33
+ export let random = (bytes) => crypto.getRandomValues(new Uint8Array(bytes));
34
+
35
+ function generateId() {
36
+ let size = 24;
37
+ let id = "";
38
+ let bytes = crypto.getRandomValues(new Uint8Array(size));
39
+ while (size--) {
40
+ // Using the bitwise AND operator to "cap" the value of
41
+ // the random byte from 255 to 63, in that way we can make sure
42
+ // that the value will be a valid index for the "chars" string.
43
+ id += urlAlphabet[bytes[size] & 61];
44
+ }
45
+ return id;
46
+ }
47
+
48
+ layers.extra = (store) => {
49
+ const add = async (value, options) => store.set(generateId(), value, options);
50
+ const has = async (key) => (await store.get(key)) !== null;
51
+ const del = async (key) => store.set(key, null);
52
+ const keys = async (prefix = "") => {
53
+ const all = await store.entries(prefix);
54
+ return all.map((p) => p[0]);
55
+ };
56
+ const values = async (prefix = "") => {
57
+ const all = await store.entries(prefix);
58
+ return all.map((p) => p[1]);
59
+ };
60
+ return { add, has, del, keys, values, ...store };
61
+ };
62
+
28
63
  // Adds an expiration layer to those stores that don't have it;
29
64
  // it's not perfect since it's not deleted until it's read, but
30
65
  // hey it's better than nothing
31
66
  layers.expire = (store) => {
32
67
  // Item methods
33
68
  const get = async (key) => {
34
- if (!(await store.has(key))) return null;
35
- const { value, expire } = await store.get(key);
69
+ const data = await store.get(key);
70
+ if (!data) return null;
71
+ const { value, expire } = data;
36
72
  // It never expires
37
73
  if (expire === null) return value;
38
74
  const diff = expire - new Date().getTime();
@@ -42,122 +78,182 @@ layers.expire = (store) => {
42
78
  const set = async (key, value, { expire, expires } = {}) => {
43
79
  const time = parse(expire || expires);
44
80
  // Already expired, or do _not_ save it, then delete it
45
- if (value === null || time === 0) return del(key);
46
- const expDiff = time !== null ? new Date().getTime() + time : null;
81
+ if (value === null || time === 0) return store.set(key, null);
82
+ const expDiff = time !== null ? new Date().getTime() + time * 1000 : null;
47
83
  return store.set(key, { expire: expDiff, value });
48
84
  };
49
- const has = async (key) => (await store.get(key)) !== null;
50
- const del = store.del;
51
85
 
52
86
  // Group methods
53
- const keys = store.keys;
54
- const clear = store.clear;
87
+ const entries = async (prefix = "") => {
88
+ const all = await store.entries(prefix);
89
+ const now = new Date().getTime();
90
+ return all
91
+ .filter(([, data]) => {
92
+ // There's no data, so remove this
93
+ if (!data || data === null) return false;
94
+
95
+ // It never expires, so keep it
96
+ const { expire } = data;
97
+ if (expire === null) return true;
98
+
99
+ // It's expired, so remove it
100
+ if (expire - now <= 0) return false;
101
+
102
+ // It's not expired, keep it
103
+ return true;
104
+ })
105
+ .map(([key, data]) => [key, data.value]);
106
+ };
55
107
 
56
- return { get, set, has, del, keys, clear };
108
+ // We want to force overwrite here!
109
+ return { ...store, get, set, entries };
57
110
  };
58
111
 
59
112
  layers.memory = (store) => {
60
113
  // Item methods
61
114
  const get = async (key) => store.get(key) ?? null;
62
- const set = async (key, data) => store.set(key, data);
63
- const has = async (key) => store.has(key);
64
- const del = async (key) => store.delete(key);
115
+ const set = async (key, data) => {
116
+ if (data === null) {
117
+ await store.delete(key);
118
+ } else {
119
+ await store.set(key, data);
120
+ }
121
+ return key;
122
+ };
65
123
 
66
124
  // Group methods
67
- const keys = async (prefix = "") =>
68
- [...(await store.keys())].filter((k) => k.startsWith(prefix));
125
+ const entries = async (prefix = "") => {
126
+ const entries = [...store.entries()];
127
+ return entries.filter((p) => p[0].startsWith(prefix));
128
+ };
69
129
  const clear = () => store.clear();
70
130
 
71
- return { get, set, has, del, keys, clear };
131
+ return { get, set, entries, clear };
72
132
  };
73
133
 
74
134
  layers.storage = (store) => {
75
135
  // Item methods
76
136
  const get = async (key) => (store[key] ? JSON.parse(store[key]) : null);
77
- const set = async (key, data) => store.setItem(key, JSON.stringify(data));
78
- const has = async (key) => key in store;
79
- const del = async (key) => store.removeItem(key);
137
+ const set = async (key, data) => {
138
+ if (data === null) {
139
+ await store.removeItem(key);
140
+ } else {
141
+ await store.setItem(key, JSON.stringify(data));
142
+ }
143
+ return key;
144
+ };
80
145
 
81
146
  // Group methods
82
- const keys = async (prefix = "") =>
83
- Object.keys(store).filter((k) => k.startsWith(prefix));
147
+ const entries = async (prefix = "") => {
148
+ const entries = Object.entries(store);
149
+ return entries
150
+ .map((p) => [p[0], p[1] ? JSON.parse(p[1]) : null])
151
+ .filter((p) => p[0].startsWith(prefix));
152
+ };
84
153
  const clear = () => store.clear();
85
154
 
86
- return { get, set, has, del, keys, clear };
155
+ return { get, set, entries, clear };
87
156
  };
88
157
 
158
+ // Cookies auto-expire, so we cannot do expiration checks manually
89
159
  layers.cookie = () => {
90
- const get = async (key) => {
91
- const value =
92
- document.cookie
93
- .split("; ")
94
- .filter(Boolean)
95
- .find((row) => row.startsWith(key + "="))
96
- ?.split("=")[1] || null;
97
- return JSON.parse(decodeURIComponent(value));
160
+ const getAll = () => {
161
+ const all = {};
162
+ for (let entry of document.cookie
163
+ .split(";")
164
+ .map((k) => k.trim())
165
+ .filter(Boolean)) {
166
+ const [key, data] = entry.split("=");
167
+ try {
168
+ all[key.trim()] = JSON.parse(decodeURIComponent(data.trim()));
169
+ } catch (error) {
170
+ // no-op (some 3rd party can set cookies independently)
171
+ }
172
+ }
173
+ return all;
98
174
  };
99
175
 
176
+ const get = async (key) => getAll()[key] ?? null;
177
+
100
178
  const set = async (key, data, { expire, expires } = {}) => {
101
- const time = parse(expire || expires);
102
- const now = new Date().getTime();
103
- // NOTE: 0 is already considered here!
104
- const expireStr =
105
- time !== null ? `; expires=${new Date(now + time).toUTCString()}` : "";
106
- const value = encodeURIComponent(JSON.stringify(data));
107
- document.cookie = key + "=" + value + expireStr;
179
+ if (data === null) {
180
+ await set(key, "", { expire: -100 });
181
+ } else {
182
+ const time = parse(expire || expires);
183
+ const now = new Date().getTime();
184
+ // NOTE: 0 is already considered here!
185
+ const expireStr =
186
+ time !== null
187
+ ? `; expires=${new Date(now + time * 1000).toUTCString()}`
188
+ : "";
189
+ const value = encodeURIComponent(JSON.stringify(data));
190
+ document.cookie = key + "=" + value + expireStr;
191
+ }
192
+ return key;
108
193
  };
109
- const has = async (key) => (await keys()).includes(key);
110
- const del = async (key) => set(key, "", { expire: -100 });
111
194
 
112
195
  // Group methods
113
- const keys = async (prefix = "") =>
114
- document.cookie
115
- .split(";")
116
- .map((l) => l.split("=")[0].trim())
117
- .filter(Boolean)
118
- .filter((k) => k.startsWith(prefix));
196
+ const entries = async (prefix = "") => {
197
+ const all = Object.entries(getAll());
198
+ return all.filter((p) => p[0].startsWith(prefix));
199
+ };
200
+
119
201
  const clear = async () => {
120
- await Promise.all((await keys()).map(del));
202
+ const keys = Object.keys(getAll());
203
+ await Promise.all(keys.map((key) => set(key, null)));
121
204
  };
122
205
 
123
- return { get, set, has, del, keys, clear };
206
+ return { get, set, entries, clear };
124
207
  };
125
208
 
209
+ // Plain 'redis' and not ioredis or similar
126
210
  layers.redis = (store) => {
127
211
  const get = async (key) => {
128
- const client = await store;
129
- const value = await client.get(key);
212
+ const value = await store.get(key);
130
213
  if (!value) return null;
131
214
  return JSON.parse(value);
132
215
  };
133
216
  const set = async (key, value, { expire, expires } = {}) => {
134
217
  const time = parse(expire || expires);
135
218
  if (value === null || time === 0) return del(key);
136
- const client = await store;
137
- const EX = time ? Math.round(time / 1000) : undefined;
138
- return client.set(key, JSON.stringify(value), { EX });
219
+ const EX = time ? Math.round(time) : undefined;
220
+ await store.set(key, JSON.stringify(value), { EX });
221
+ return key;
139
222
  };
140
- const has = async (key) => Boolean(await (await store).exists(key));
141
- const del = async (key) => (await store).del(key);
142
-
143
- const keys = async (prefix = "") => (await store).keys(prefix + "*");
144
- const clear = async () => (await store).flushAll();
145
- const close = async () => (await store).quit();
223
+ const has = async (key) => Boolean(await store.exists(key));
224
+ const del = async (key) => store.del(key);
225
+
226
+ const keys = async (prefix = "") => store.keys(prefix + "*");
227
+ const entries = async (prefix = "") => {
228
+ const keys = await store.keys(prefix + "*");
229
+ const values = await Promise.all(keys.map((k) => get(k)));
230
+ return keys.map((k, i) => [k, values[i]]);
231
+ };
232
+ const clear = async () => store.flushAll();
233
+ const close = async () => store.quit();
146
234
 
147
- return { get, set, has, del, keys, clear, close };
235
+ return { get, set, has, del, keys, entries, clear, close };
148
236
  };
149
237
 
150
238
  layers.localForage = (store) => {
151
- const get = (key) => store.getItem(key);
152
- const set = (key, value) => store.setItem(key, value);
153
- const has = async (key) => (await get(key)) !== null;
154
- const del = (key) => store.removeItem(key);
155
-
156
- const keys = async (prefix = "") =>
157
- (await store.keys()).filter((k) => k.startsWith(prefix));
158
- const clear = () => store.clear();
239
+ const get = async (key) => store.getItem(key);
240
+ const set = async (key, value) => {
241
+ if (value === null) {
242
+ await store.removeItem(key);
243
+ } else {
244
+ await store.setItem(key, value);
245
+ }
246
+ return key;
247
+ };
248
+ const entries = async (prefix = "") => {
249
+ const all = await store.keys();
250
+ const keys = all.filter((k) => k.startsWith(prefix));
251
+ const values = await Promise.all(keys.map((key) => store.getItem(key)));
252
+ return keys.map((key, i) => [key, values[i]]);
253
+ };
254
+ const clear = async () => store.clear();
159
255
 
160
- return { get, set, has, del, keys, clear };
256
+ return { get, set, entries, clear };
161
257
  };
162
258
 
163
259
  layers.cloudflare = (store) => {
@@ -170,14 +266,25 @@ layers.cloudflare = (store) => {
170
266
  const time = parse(expire || expires);
171
267
  if (value === null || time === 0) return del(key);
172
268
  const client = await store;
173
- const expirationTtl = time ? Math.round(time / 1000) : undefined;
174
- return client.put(key, JSON.stringify(value), { expirationTtl });
269
+ const expirationTtl = time ? Math.round(time) : undefined;
270
+ client.put(key, JSON.stringify(value), { expirationTtl });
271
+ return key;
175
272
  };
176
273
  const has = async (key) => Boolean(await store.get(key));
177
274
  const del = (key) => store.delete(key);
178
- const keys = (prefix) => store.list({ prefix });
275
+
276
+ // Group methods
277
+ const keys = async (prefix = "") => {
278
+ const raw = await store.list({ prefix });
279
+ return raw.keys;
280
+ };
281
+ const entries = async (prefix = "") => {
282
+ const all = await keys(prefix);
283
+ const values = await Promise.all(all.map((k) => get(k)));
284
+ return all.map((key, i) => [key, values[i]]);
285
+ };
179
286
  const clear = () => {};
180
- return { get, set, has, del, keys, clear };
287
+ return { get, set, has, del, entries, keys, clear };
181
288
  };
182
289
 
183
290
  layers.file = (file) => {
@@ -209,57 +316,60 @@ layers.file = (file) => {
209
316
  };
210
317
  const set = async (key, value) => {
211
318
  const data = await getContent();
212
- data[key] = value;
319
+ if (value === null) {
320
+ delete data[key];
321
+ } else {
322
+ data[key] = value;
323
+ }
213
324
  await setContent(data);
325
+ return key;
214
326
  };
215
327
  const has = async (key) => (await get(key)) !== null;
216
- const del = async (key) => {
217
- const data = await getContent();
218
- delete data[key];
219
- await setContent(data);
220
- };
221
- const keys = async (prefix = "") => {
328
+ const del = async (key) => set(key, null);
329
+
330
+ // Group methods
331
+ const entries = async (prefix = "") => {
222
332
  const data = await getContent();
223
- return Object.keys(data).filter((k) => k.startsWith(prefix));
333
+ return Object.entries(data).filter((p) => p[0].startsWith(prefix));
224
334
  };
225
335
  const clear = async () => {
226
336
  await setContent({});
227
337
  };
228
- return { get, set, has, del, keys, clear };
338
+ return { get, set, has, del, entries, clear };
229
339
  };
230
340
 
231
341
  const getStore = async (store) => {
232
342
  // Convert it to the normalized kv, then add the expiry layer on top
233
343
  if (store instanceof Map) {
234
- return layers.expire(layers.memory(store));
344
+ return layers.extra(layers.expire(layers.memory(store)));
235
345
  }
236
346
 
237
347
  if (typeof localStorage !== "undefined" && store === localStorage) {
238
- return layers.expire(layers.storage(store));
348
+ return layers.extra(layers.expire(layers.storage(store)));
239
349
  }
240
350
 
241
351
  if (typeof sessionStorage !== "undefined" && store === sessionStorage) {
242
- return layers.expire(layers.storage(store));
352
+ return layers.extra(layers.expire(layers.storage(store)));
243
353
  }
244
354
 
245
355
  if (store === "cookie") {
246
- return layers.cookie();
356
+ return layers.extra(layers.cookie());
247
357
  }
248
358
 
249
359
  if (store.defineDriver && store.dropInstance && store.INDEXEDDB) {
250
- return layers.expire(layers.localForage(store));
360
+ return layers.extra(layers.expire(layers.localForage(store)));
251
361
  }
252
362
 
253
363
  if (store.protocol && store.protocol === "file:") {
254
- return layers.expire(layers.file(store));
364
+ return layers.extra(layers.expire(layers.file(store)));
255
365
  }
256
366
 
257
367
  if (store.pSubscribe && store.sSubscribe) {
258
- return layers.redis(store);
368
+ return layers.extra(layers.redis(store));
259
369
  }
260
370
 
261
371
  if (store?.constructor?.name === "KvNamespace") {
262
- return layers.cloudflare(store);
372
+ return layers.extra(layers.cloudflare(store));
263
373
  }
264
374
 
265
375
  // ¯\_(ツ)_/¯
@@ -270,17 +380,22 @@ export default function compat(storeClient = new Map()) {
270
380
  return new Proxy(
271
381
  {},
272
382
  {
273
- get: (_, key) => {
383
+ get: (instance, key) => {
274
384
  return async (...args) => {
275
- const store = await getStore(await storeClient);
385
+ // Only once, even if called twice in succession, since the
386
+ // second time will go straight to the await
387
+ if (!instance.store && !instance.promise) {
388
+ instance.promise = getStore(await storeClient);
389
+ }
390
+ instance.store = await instance.promise;
276
391
  // Throw at the first chance when the store failed to init:
277
- if (!store) {
392
+ if (!instance.store) {
278
393
  throw new Error("Store is not valid");
279
394
  }
280
395
  // The store.close() is the only one allowed to be called even
281
396
  // if it doesn't exist, since it's optional in some stores
282
- if (!store[key] && key === "close") return null;
283
- return store[key](...args);
397
+ if (!instance.store[key] && key === "close") return null;
398
+ return instance.store[key](...args);
284
399
  };
285
400
  },
286
401
  }
package/src/index.test.js CHANGED
@@ -46,10 +46,20 @@ for (let [name, store] of stores) {
46
46
  expect(await store.has("a")).toBe(false);
47
47
  });
48
48
 
49
+ it("can add() arbitrary values", async () => {
50
+ const key = await store.add("b");
51
+ expect(typeof key).toBe("string");
52
+ expect(await store.get(key)).toBe("b");
53
+ expect(await store.has(key)).toBe(true);
54
+ expect(key.length).toBe(24);
55
+ expect(key).toMatch(/^[a-zA-Z0-9]{24}$/);
56
+ });
57
+
49
58
  it("can store values", async () => {
50
- await store.set("a", "b");
59
+ const key = await store.set("a", "b");
51
60
  expect(await store.get("a")).toBe("b");
52
61
  expect(await store.has("a")).toBe(true);
62
+ expect(key).toBe("a");
53
63
  });
54
64
 
55
65
  it("can store basic types", async () => {
@@ -62,8 +72,8 @@ for (let [name, store] of stores) {
62
72
  });
63
73
 
64
74
  it("can store arrays of JSON values", async () => {
65
- await store.set("a", ["b"]);
66
- expect(await store.get("a")).toEqual(["b"]);
75
+ await store.set("a", ["b", "c"]);
76
+ expect(await store.get("a")).toEqual(["b", "c"]);
67
77
  expect(await store.has("a")).toBe(true);
68
78
  });
69
79
 
@@ -73,22 +83,83 @@ for (let [name, store] of stores) {
73
83
  expect(await store.has("a")).toBe(true);
74
84
  });
75
85
 
76
- it("can retrieve the prefixed keys with colon", async () => {
86
+ it("can get the keys", async () => {
87
+ await store.set("a", "b");
88
+ await store.set("c", "d");
89
+ expect(await store.keys()).toEqual(["a", "c"]);
90
+ });
91
+
92
+ it("can get the values", async () => {
93
+ await store.set("a", "b");
94
+ await store.set("c", "d");
95
+ expect(await store.values()).toEqual(["b", "d"]);
96
+ });
97
+
98
+ it("can get the entries", async () => {
99
+ await store.set("a", "b");
100
+ await store.set("c", "d");
101
+ expect(await store.entries()).toEqual([
102
+ ["a", "b"],
103
+ ["c", "d"],
104
+ ]);
105
+ });
106
+
107
+ it("can get the keys with a colon prefix", async () => {
77
108
  await store.set("a:0", "a0");
78
109
  await store.set("a:1", "a1");
79
110
  await store.set("b:0", "b0");
80
- await store.set("a:2", "b2");
111
+ await store.set("a:2", "a2");
81
112
  expect((await store.keys("a:")).sort()).toEqual(["a:0", "a:1", "a:2"]);
82
113
  });
83
114
 
84
- it("can retrieve the prefixed keys with dash", async () => {
115
+ it("can get the values with a colon prefix", async () => {
116
+ await store.set("a:0", "a0");
117
+ await store.set("a:1", "a1");
118
+ await store.set("b:0", "b0");
119
+ await store.set("a:2", "a2");
120
+ expect((await store.values("a:")).sort()).toEqual(["a0", "a1", "a2"]);
121
+ });
122
+
123
+ it("can get the entries with a colon prefix", async () => {
124
+ await store.set("a:0", "a0");
125
+ await store.set("a:1", "a1");
126
+ await store.set("b:0", "b0");
127
+ await store.set("a:2", "a2");
128
+ expect((await store.entries("a:")).sort()).toEqual([
129
+ ["a:0", "a0"],
130
+ ["a:1", "a1"],
131
+ ["a:2", "a2"],
132
+ ]);
133
+ });
134
+
135
+ it("can get the keys with a dash prefix", async () => {
85
136
  await store.set("a-0", "a0");
86
137
  await store.set("a-1", "a1");
87
138
  await store.set("b-0", "b0");
88
- await store.set("a-2", "b2");
139
+ await store.set("a-2", "a2");
89
140
  expect((await store.keys("a-")).sort()).toEqual(["a-0", "a-1", "a-2"]);
90
141
  });
91
142
 
143
+ it("can get the values with a dash prefix", async () => {
144
+ await store.set("a-0", "a0");
145
+ await store.set("a-1", "a1");
146
+ await store.set("b-0", "b0");
147
+ await store.set("a-2", "a2");
148
+ expect((await store.values("a-")).sort()).toEqual(["a0", "a1", "a2"]);
149
+ });
150
+
151
+ it("can get the entries with a dash prefix", async () => {
152
+ await store.set("a-0", "a0");
153
+ await store.set("a-1", "a1");
154
+ await store.set("b-0", "b0");
155
+ await store.set("a-2", "a2");
156
+ expect((await store.entries("a-")).sort()).toEqual([
157
+ ["a-0", "a0"],
158
+ ["a-1", "a1"],
159
+ ["a-2", "a2"],
160
+ ]);
161
+ });
162
+
92
163
  it("can delete the data", async () => {
93
164
  await store.set("a", "b");
94
165
  expect(await store.get("a")).toBe("b");
@@ -96,12 +167,6 @@ for (let [name, store] of stores) {
96
167
  expect(await store.get("a")).toBe(null);
97
168
  });
98
169
 
99
- it("can get the keys", async () => {
100
- await store.set("a", "b");
101
- await store.set("c", "d");
102
- expect(await store.keys()).toEqual(["a", "c"]);
103
- });
104
-
105
170
  it("can clear all the values", async () => {
106
171
  await store.set("a", "b");
107
172
  await store.set("c", "d");
@@ -116,6 +181,10 @@ for (let [name, store] of stores) {
116
181
  it("expires = 0 means immediately", async () => {
117
182
  await store.set("a", "b", { expires: 0 });
118
183
  expect(await store.get("a")).toBe(null);
184
+ expect(await store.has("a")).toBe(false);
185
+ expect(await store.keys()).toEqual([]);
186
+ expect(await store.values()).toEqual([]);
187
+ expect(await store.entries()).toEqual([]);
119
188
  });
120
189
 
121
190
  it("expires = potato means undefined = forever", async () => {
@@ -147,8 +216,9 @@ for (let [name, store] of stores) {
147
216
  });
148
217
 
149
218
  if (name !== "kv('cookie')" && name !== "kv(redis)") {
150
- it("can use 10 expire", async () => {
151
- await store.set("a", "b", { expires: 10 });
219
+ it("can use 0.01 expire", async () => {
220
+ // 10ms
221
+ await store.set("a", "b", { expires: 0.01 });
152
222
  expect(await store.get("a")).toBe("b");
153
223
  await delay(100);
154
224
  expect(await store.get("a")).toBe(null);
@@ -175,8 +245,8 @@ for (let [name, store] of stores) {
175
245
  expect(await store.get("a")).toBe(null);
176
246
  });
177
247
  } else {
178
- it("can use 1000 expire", async () => {
179
- await store.set("a", "b", { expires: 1000 });
248
+ it("can use 1 (second) expire", async () => {
249
+ await store.set("a", "b", { expires: 1 });
180
250
  expect(await store.get("a")).toBe("b");
181
251
  await delay(2000);
182
252
  expect(await store.get("a")).toBe(null);
@@ -8,6 +8,10 @@ const store = kv();
8
8
  await store.set("key", "value", {});
9
9
  await store.set("key", "value", { expires: 100 });
10
10
  await store.set("key", "value", { expires: "100s" });
11
+ const key1: string = await store.add("value");
12
+ const key2: string = await store.add("value", { expires: 100 });
13
+ const key3: string = await store.add("value", { expires: "100s" });
14
+ console.log(key1, key2, key3);
11
15
  if (await store.has("key")) {
12
16
  }
13
17
  })();