polystore 0.7.0 → 0.9.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/assets/autocomplete.png +0 -0
- package/assets/autocomplete.webp +0 -0
- package/assets/favicon.png +0 -0
- package/assets/home.html +379 -0
- package/assets/splash.png +0 -0
- package/documentation.page.json +12 -0
- package/package.json +8 -3
- package/readme.md +297 -81
- package/src/clients/cloudflare.js +49 -0
- package/src/clients/cookie.js +55 -0
- package/src/clients/etcd.js +39 -0
- package/src/clients/file.js +75 -0
- package/src/clients/forage.js +42 -0
- package/src/clients/index.js +21 -0
- package/src/clients/level.js +45 -0
- package/src/clients/memory.js +38 -0
- package/src/clients/redis.js +56 -0
- package/src/clients/storage.js +43 -0
- package/src/index.js +269 -358
- package/src/index.test.js +314 -22
- package/src/index.types.ts +1 -0
- package/src/{index.d.ts → indexa.d.ts} +5 -3
- package/src/test/customFull.js +38 -0
- package/src/test/customSimple.js +23 -0
- package/src/test/setup.js +12 -0
- package/src/utils.js +44 -0
package/readme.md
CHANGED
|
@@ -1,39 +1,46 @@
|
|
|
1
1
|
# Polystore [](https://www.npmjs.com/package/polystore) [](https://github.com/franciscop/polystore/blob/master/.github/workflows/tests.yml) [](https://github.com/franciscop/polystore/blob/master/src/index.js)
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A key-value library to unify the API of [many clients](#clients), like localStorage, Redis, FileSystem, etc:
|
|
4
4
|
|
|
5
5
|
```js
|
|
6
6
|
import kv from "polystore";
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- `.
|
|
17
|
-
- `.
|
|
18
|
-
- `.
|
|
19
|
-
- `.
|
|
20
|
-
- `.
|
|
21
|
-
- `.
|
|
22
|
-
- `.
|
|
23
|
-
- `.
|
|
24
|
-
- `.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- **
|
|
32
|
-
- **
|
|
33
|
-
- **
|
|
34
|
-
- **
|
|
35
|
-
- **
|
|
36
|
-
-
|
|
7
|
+
const store1 = kv(new Map()); // in-memory
|
|
8
|
+
const store2 = kv(localStorage); // Persist in the browser
|
|
9
|
+
const store3 = kv(redisClient); // Use a Redis client for backend persistence
|
|
10
|
+
const store4 = kv(yourOwnStore); // Create a store based on your code
|
|
11
|
+
// Many more here
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
These are all the methods of the [API](#api) (they are all `async`):
|
|
15
|
+
|
|
16
|
+
- [`.get(key)`](#get): read a single value, or `null` if it doesn't exist or is expired.
|
|
17
|
+
- [`.set(key, value, options?)`](#set): save a single value that is serializable.
|
|
18
|
+
- [`.add(value, options?)`](#add): same as `.set()`, but auto-generates the key.
|
|
19
|
+
- [`.has(key)`](#has): check whether a key exists or not.
|
|
20
|
+
- [`.del(key)`](#del): delete a single value from the store.
|
|
21
|
+
- [`.keys()`](#keys): get a list of all the available strings in the store.
|
|
22
|
+
- [`.values()`](#values): get a list of all the values in the store.
|
|
23
|
+
- [`.entries()`](#entries): get a list of all the key-value pairs.
|
|
24
|
+
- [`.all()`](#all): get an object with the key:values mapped.
|
|
25
|
+
- [`.clear()`](#clear): delete ALL of the data in the store, effectively resetting it.
|
|
26
|
+
- [`.close()`](#close): (only _some_ stores) ends the connection to the store.
|
|
27
|
+
- [`.prefix(prefix)`](#prefix): create a sub-store that only manages the keys with the given prefix.
|
|
28
|
+
|
|
29
|
+
Available clients for the KV store:
|
|
30
|
+
|
|
31
|
+
- [**Memory** `new Map()`](#memory) (fe+be): an in-memory API to keep your KV store.
|
|
32
|
+
- [**Local Storage** `localStorage`](#local-storage) (fe): persist the data in the browser's localStorage.
|
|
33
|
+
- [**Session Storage** `sessionStorage`](#session-storage) (fe): persist the data in the browser's sessionStorage.
|
|
34
|
+
- [**Cookies** `"cookie"`](#cookies) (fe): persist the data using cookies
|
|
35
|
+
- [**LocalForage** `localForage`](#local-forage) (fe): persist the data on IndexedDB
|
|
36
|
+
- [**File** `new URL('file:///...')`](#file) (be): store the data in a single JSON file in your FS
|
|
37
|
+
- [**Redis Client** `redisClient`](#redis-client) (be): use the Redis instance that you connect to
|
|
38
|
+
- [**Cloudflare KV** `env.KV_NAMESPACE`](#cloudflare-kv) (be): use Cloudflare's KV store
|
|
39
|
+
- [**Level** `new Level('example', { valueEncoding: 'json' })`](#level): support the whole Level ecosystem
|
|
40
|
+
- [**Etcd** `new Etcd3()`](#etcd): the Microsoft's high performance KV store.
|
|
41
|
+
- [**_Custom_** `{}`](#creating-a-store) (?): create your own store with just 3 methods!
|
|
42
|
+
|
|
43
|
+
> This library should be as performant as the client you use with the item methods (GET/SET/ADD/HAS/DEL). For other and advanced cases, see [the performance considerations](#performance) and read the docs on your client.
|
|
37
44
|
|
|
38
45
|
I made this library to be used as a "building block" of other libraries, so that _your library_ can accept many cache stores effortlessly! It's isomorphic (Node.js and the Browser) and tiny (~2KB). For example, let's say you create an API library, then you can accept the stores from your client:
|
|
39
46
|
|
|
@@ -49,7 +56,7 @@ MyApi({ cache: env.KV_NAMESPACE }); // OR
|
|
|
49
56
|
|
|
50
57
|
## API
|
|
51
58
|
|
|
52
|
-
See how to initialize each store [in the
|
|
59
|
+
See how to initialize each store [in the Clients list documentation](#clients). But basically for every store, it's like this:
|
|
53
60
|
|
|
54
61
|
```js
|
|
55
62
|
import kv from "polystore";
|
|
@@ -163,7 +170,7 @@ if (await store.has('cookie-consent')) {
|
|
|
163
170
|
|
|
164
171
|
### .del()
|
|
165
172
|
|
|
166
|
-
Remove a single key from the store:
|
|
173
|
+
Remove a single key from the store and return the key itself:
|
|
167
174
|
|
|
168
175
|
```js
|
|
169
176
|
await store.del(key: string);
|
|
@@ -224,9 +231,11 @@ Remove all of the data from the store:
|
|
|
224
231
|
await store.clear();
|
|
225
232
|
```
|
|
226
233
|
|
|
227
|
-
### .prefix()
|
|
234
|
+
### .prefix()
|
|
228
235
|
|
|
229
|
-
|
|
236
|
+
> There's [an in-depth explanation about Substores](#substores) that is very informative for production usage.
|
|
237
|
+
|
|
238
|
+
Creates **a new instance** of the Store, _with the same client_ as you provided, but now any key you read, write, etc. will be passed with the given prefix to the client. You only write `.prefix()` once and then don't need to worry about any prefix for any method anymore, it's all automatic. It's **the only method** that you don't need to await:
|
|
230
239
|
|
|
231
240
|
```js
|
|
232
241
|
const store = kv(new Map());
|
|
@@ -236,27 +245,35 @@ const session = store.prefix("session:");
|
|
|
236
245
|
Then all of the operations will be converted internally to add the prefix when reading, writing, etc:
|
|
237
246
|
|
|
238
247
|
```js
|
|
239
|
-
const
|
|
240
|
-
await session.
|
|
241
|
-
|
|
242
|
-
await session.
|
|
243
|
-
await session.
|
|
248
|
+
const session = store.prefix("session:");
|
|
249
|
+
const val = await session.get("key1"); // store.get('session:key1');
|
|
250
|
+
await session.set("key2", "some data"); // store.set('session:key2', ...);
|
|
251
|
+
const val = await session.has("key3"); // store.has('session:key3');
|
|
252
|
+
await session.del("key4"); // store.del('session:key4');
|
|
253
|
+
await session.keys(); // store.keys(); + filter
|
|
244
254
|
// ['key1', 'key2', ...] Note no prefix here
|
|
245
255
|
await session.clear(); // delete only keys with the prefix
|
|
246
256
|
```
|
|
247
257
|
|
|
248
|
-
|
|
258
|
+
Different clients have better/worse support for substores, and in some cases some operations might be slower. This should be documented on each client's documentation (see below). As an alternative, you can always create two different stores instead of a substore:
|
|
249
259
|
|
|
250
260
|
```js
|
|
261
|
+
// Two in-memory stores
|
|
251
262
|
const store = kv(new Map());
|
|
252
263
|
const session = kv(new Map());
|
|
264
|
+
|
|
265
|
+
// Two file-stores
|
|
266
|
+
const users = kv(new URL(`file://${import.meta.dirname}/users.json`));
|
|
267
|
+
const books = kv(new URL(`file://${import.meta.dirname}/books.json`));
|
|
253
268
|
```
|
|
254
269
|
|
|
255
270
|
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_.
|
|
256
271
|
|
|
257
|
-
##
|
|
272
|
+
## Clients
|
|
273
|
+
|
|
274
|
+
A client is the library that manages the low-level store operations. For example, the Redis Client, or the browser's `localStorage` API. In some exceptions it's just a string and we do a bit more work on Polystore, like with `"cookie"` or `"file:///users/me/data.json"`.
|
|
258
275
|
|
|
259
|
-
|
|
276
|
+
Polystore provides a unified API you can use `Promises`, `expires` and `.prefix()` even with those stores that do not support these operations natively.
|
|
260
277
|
|
|
261
278
|
### Memory
|
|
262
279
|
|
|
@@ -265,18 +282,31 @@ An in-memory KV store, with promises and expiration time:
|
|
|
265
282
|
```js
|
|
266
283
|
import kv from "polystore";
|
|
267
284
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
await store.set("key1", "Hello world");
|
|
285
|
+
const store = kv(new Map());
|
|
286
|
+
|
|
287
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
272
288
|
console.log(await store.get("key1"));
|
|
289
|
+
// "Hello world"
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
It can also be initialized empty, then it'll use the in-memory store:
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
import kv from "polystore";
|
|
273
296
|
|
|
274
|
-
|
|
297
|
+
const store = kv();
|
|
275
298
|
const store = kv(new Map());
|
|
276
|
-
await store.set("key1", "Hello world");
|
|
277
|
-
console.log(await store.get("key1"));
|
|
278
299
|
```
|
|
279
300
|
|
|
301
|
+
<details>
|
|
302
|
+
<summary>Why use polystore with <code>new Map()</code>?</summary>
|
|
303
|
+
<p>Besides the other benefits already documented elsewhere, these are the ones specifically for wrapping Map() with polystore:</p>
|
|
304
|
+
<ul>
|
|
305
|
+
<li><strong>Expiration</strong>: you can now set lifetime to your values so that they are automatically evicted when the time passes. <a href="#expiration-explained">Expiration explained</a>.</li>
|
|
306
|
+
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
307
|
+
</ul>
|
|
308
|
+
</details>
|
|
309
|
+
|
|
280
310
|
### Local Storage
|
|
281
311
|
|
|
282
312
|
The traditional localStorage that we all know and love, this time with a unified API, and promises:
|
|
@@ -285,8 +315,10 @@ The traditional localStorage that we all know and love, this time with a unified
|
|
|
285
315
|
import kv from "polystore";
|
|
286
316
|
|
|
287
317
|
const store = kv(localStorage);
|
|
288
|
-
|
|
318
|
+
|
|
319
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
289
320
|
console.log(await store.get("key1"));
|
|
321
|
+
// "Hello world"
|
|
290
322
|
```
|
|
291
323
|
|
|
292
324
|
Same limitations as always apply to localStorage, if you think you are going to use too much storage try instead our integration with [Local Forage](#local-forage)!
|
|
@@ -299,8 +331,10 @@ Same as localStorage, but now for the session only:
|
|
|
299
331
|
import kv from "polystore";
|
|
300
332
|
|
|
301
333
|
const store = kv(sessionStorage);
|
|
302
|
-
|
|
334
|
+
|
|
335
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
303
336
|
console.log(await store.get("key1"));
|
|
337
|
+
// "Hello world"
|
|
304
338
|
```
|
|
305
339
|
|
|
306
340
|
### Cookies
|
|
@@ -310,9 +344,11 @@ Supports native browser cookies, including setting the expire time:
|
|
|
310
344
|
```js
|
|
311
345
|
import kv from "polystore";
|
|
312
346
|
|
|
313
|
-
const store = kv("cookie"); //
|
|
314
|
-
|
|
347
|
+
const store = kv("cookie"); // just a plain string
|
|
348
|
+
|
|
349
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
315
350
|
console.log(await store.get("key1"));
|
|
351
|
+
// "Hello world"
|
|
316
352
|
```
|
|
317
353
|
|
|
318
354
|
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).
|
|
@@ -328,8 +364,10 @@ import kv from "polystore";
|
|
|
328
364
|
import localForage from "localforage";
|
|
329
365
|
|
|
330
366
|
const store = kv(localForage);
|
|
367
|
+
|
|
331
368
|
await store.set("key1", "Hello world", { expires: "1h" });
|
|
332
369
|
console.log(await store.get("key1"));
|
|
370
|
+
// "Hello world"
|
|
333
371
|
```
|
|
334
372
|
|
|
335
373
|
### Redis Client
|
|
@@ -340,25 +378,38 @@ Supports the official Node Redis Client. You can pass either the client or the p
|
|
|
340
378
|
import kv from "polystore";
|
|
341
379
|
import { createClient } from "redis";
|
|
342
380
|
|
|
343
|
-
// Note: no need for await or similar
|
|
344
381
|
const store = kv(createClient().connect());
|
|
345
|
-
|
|
382
|
+
|
|
383
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
346
384
|
console.log(await store.get("key1"));
|
|
385
|
+
// "Hello world"
|
|
347
386
|
```
|
|
348
387
|
|
|
388
|
+
You don't need to `await` for the connect or similar, this will process it properly.
|
|
389
|
+
|
|
349
390
|
> 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.
|
|
350
391
|
|
|
351
|
-
###
|
|
392
|
+
### File
|
|
393
|
+
|
|
394
|
+
Treat a JSON file in your filesystem as the source for the KV store:
|
|
352
395
|
|
|
353
396
|
```js
|
|
354
397
|
import kv from "polystore";
|
|
355
398
|
|
|
356
|
-
// Create a url with the file protocol:
|
|
357
399
|
const store = kv(new URL("file:///Users/me/project/cache.json"));
|
|
358
400
|
|
|
401
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
402
|
+
console.log(await store.get("key1"));
|
|
403
|
+
// "Hello world"
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
You can also create multiple stores:
|
|
407
|
+
|
|
408
|
+
```js
|
|
359
409
|
// Paths need to be absolute, but you can use process.cwd() to make
|
|
360
410
|
// it relative to the current process:
|
|
361
|
-
const
|
|
411
|
+
const store1 = kv(new URL(`file://${process.cwd()}/cache.json`));
|
|
412
|
+
const store2 = kv(new URL(`file://${import.meta.dirname}/data.json`));
|
|
362
413
|
```
|
|
363
414
|
|
|
364
415
|
### Cloudflare KV
|
|
@@ -372,18 +423,16 @@ export default {
|
|
|
372
423
|
async fetch(request, env, ctx) {
|
|
373
424
|
const store = kv(env.YOUR_KV_NAMESPACE);
|
|
374
425
|
|
|
375
|
-
await store.set("
|
|
376
|
-
|
|
426
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
427
|
+
console.log(await store.get("key1"));
|
|
428
|
+
// "Hello world"
|
|
377
429
|
|
|
378
|
-
|
|
379
|
-
return new Response("Value not found", { status: 404 });
|
|
380
|
-
}
|
|
381
|
-
return new Response(value);
|
|
430
|
+
return new Response("My response");
|
|
382
431
|
},
|
|
383
432
|
};
|
|
384
433
|
```
|
|
385
434
|
|
|
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:
|
|
435
|
+
Why use polystore? 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:
|
|
387
436
|
|
|
388
437
|
```js
|
|
389
438
|
// GOOD - with polystore
|
|
@@ -397,9 +446,57 @@ await env.YOUR_KV_NAMESPACE.put("user", serialValue, {
|
|
|
397
446
|
});
|
|
398
447
|
```
|
|
399
448
|
|
|
400
|
-
|
|
449
|
+
### Level
|
|
450
|
+
|
|
451
|
+
Support [the Level ecosystem](https://github.com/Level/level), which is itself composed of modular methods:
|
|
452
|
+
|
|
453
|
+
```js
|
|
454
|
+
import kv from "polystore";
|
|
455
|
+
import { Level } from "level";
|
|
456
|
+
|
|
457
|
+
const store = kv(new Level("example", { valueEncoding: "json" }));
|
|
458
|
+
|
|
459
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
460
|
+
console.log(await store.get("key1"));
|
|
461
|
+
// "Hello world"
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Etcd
|
|
465
|
+
|
|
466
|
+
Connect to Microsoft's Etcd Key-Value store:
|
|
467
|
+
|
|
468
|
+
```js
|
|
469
|
+
import kv from "polystore";
|
|
470
|
+
import { Etcd3 } from "etcd3";
|
|
471
|
+
|
|
472
|
+
const store = kv(new Etcd3());
|
|
473
|
+
|
|
474
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
475
|
+
console.log(await store.get("key1"));
|
|
476
|
+
// "Hello world"
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Custom store
|
|
480
|
+
|
|
481
|
+
Please see the [creating a store](#creating-a-store) section for more details!
|
|
482
|
+
|
|
483
|
+
## Performance
|
|
484
|
+
|
|
485
|
+
> TL;DR: if you only use the item operations (add,set,get,has,del) and your store supports expiration natively, you have nothing to worry about!
|
|
486
|
+
|
|
487
|
+
While all of our stores support `expires`, `.prefix()` and group operations, the nature of those makes them to have different performance characteristics.
|
|
488
|
+
|
|
489
|
+
**Expires** we polyfill expiration when the underlying library does not support it. The impact on read/write operations and on data size of each key should be minimal. However, it can have a big impact in storage size, since the expired keys are not evicted automatically. Note that when attempting to read an expired key, polystore **will delete that key**. However, if an expired key is never read, it would remain in the datastore and could create some old-data issues. This is **especially important where sensitive data is involved**! To fix this, the easiest way is calling `await store.entries();` on a cron job and that should evict all of the old keys (this operation is O(n) though, so not suitable for calling it on EVERY API call, see the next point).
|
|
490
|
+
|
|
491
|
+
**Group operations** these are there mostly for small datasets only, for one-off scripts or for dev purposes, since by their own nature they can _never_ be high performance. But this is normal if you think about traditional DBs, reading a single record by its ID is O(1), while reading all of the IDs in the DB into an array is going to be O(n). Same applies with polystore.
|
|
492
|
+
|
|
493
|
+
**Substores** when dealing with a `.prefix()` substore, the same applies. Item operations should see no performance degradation from `.prefix()`, but group operations follow the above performance considerations. Some engines might have native prefix support, so performance in those is better for group operations in a substore than the whole store. But in general you should consider `.prefix()` as a convenient way of classifying your keys and not as a performance fix for group operations.
|
|
494
|
+
|
|
495
|
+
## Expires
|
|
496
|
+
|
|
497
|
+
> Warning: if a client doesn't support expiration natively, we will hide expired keys on the API calls for a nice DX, but _old data might not be evicted automatically_, which is relevant especially for sensitive information. You'd want to set up a cron job to evict it manually, since for large datasets it might be more expensive (O(n)).
|
|
401
498
|
|
|
402
|
-
|
|
499
|
+
We unify all of the clients diverse expiration methods into a single, easy one with `expires`:
|
|
403
500
|
|
|
404
501
|
```js
|
|
405
502
|
// in-memory store
|
|
@@ -414,7 +511,7 @@ console.log(await store.get("a")); // 'b'
|
|
|
414
511
|
// Make sure the key is expired
|
|
415
512
|
await delay(2000); // 2s
|
|
416
513
|
|
|
417
|
-
//
|
|
514
|
+
// The group methods also ignore expired keys
|
|
418
515
|
console.log(await store.keys()); // []
|
|
419
516
|
console.log(await store.has("a")); // false
|
|
420
517
|
console.log(await store.get("a")); // null
|
|
@@ -426,25 +523,144 @@ However, in some stores this does come with some potential performance disadvant
|
|
|
426
523
|
|
|
427
524
|
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
525
|
|
|
526
|
+
## Substores
|
|
527
|
+
|
|
528
|
+
> There's some [basic `.prefix()` API info](#prefix) for everyday usage, this section is the in-depth explanation.
|
|
529
|
+
|
|
530
|
+
What `.prefix()` does is it creates **a new instance** of the Store, _with the same client_ as you provided, but now any key you read, write, etc. will be passed with the given prefix to the client. The issue is that support from the underlying clients is inconsistent.
|
|
531
|
+
|
|
532
|
+
When dealing with large or complex amounts of data in a KV store, some times it's useful to divide them by categories. Some examples might be:
|
|
533
|
+
|
|
534
|
+
- You use KV as a cache, and have different categories of data.
|
|
535
|
+
- You use KV as a session store, and want to differentiate different kinds of sessions.
|
|
536
|
+
- You use KV as a primary data store, and have different types of datasets.
|
|
537
|
+
|
|
538
|
+
For these and more situations, you can use `.prefix()` to simplify your life further.
|
|
539
|
+
|
|
429
540
|
## Creating a store
|
|
430
541
|
|
|
431
|
-
|
|
542
|
+
To create a store, you define a class with these methods:
|
|
432
543
|
|
|
433
544
|
```js
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
545
|
+
class MyClient {
|
|
546
|
+
// If this is set to `true`, the CLIENT (you) handle the expiration, so
|
|
547
|
+
// the `.set()` and `.add()` receive a `expires` that is a `null` or `number`:
|
|
548
|
+
EXPIRES = false;
|
|
549
|
+
|
|
550
|
+
// Mandatory methods (2 item-methods, 2 group-methods)
|
|
551
|
+
get (key): Promise<any>;
|
|
552
|
+
set (key, value, { expires: null|number }): Promise<null>;
|
|
553
|
+
entries (prefix): Promise<[string, any][]>;
|
|
554
|
+
|
|
555
|
+
// Optional item methods (for optimization or customization)
|
|
556
|
+
add (prefix, data, { expires: null|number }): Promise<string>;
|
|
557
|
+
has (key): Promise<boolean>;
|
|
558
|
+
del (key): Promise<null>;
|
|
559
|
+
|
|
560
|
+
// Optional group methods
|
|
561
|
+
keys (prefix): Promise<string[]>;
|
|
562
|
+
values (prefix): Promise<any[]>;
|
|
563
|
+
clear (prefix): Promise<null>;
|
|
564
|
+
|
|
565
|
+
// Optional misc method
|
|
566
|
+
close (): Promise<null>;
|
|
567
|
+
}
|
|
439
568
|
```
|
|
440
569
|
|
|
441
|
-
|
|
570
|
+
Note that this is NOT the public API, it's the internal **client** API. It's simpler than the public API since we do some of the heavy lifting as an intermediate layer (e.g. the `expires` will always be a `null` or `number`, never `undefined` or a `string`), but also it differs from polystore's API, like `.add()` has a different signature, and the group methods all take a explicit prefix.
|
|
571
|
+
|
|
572
|
+
**Expires**: if you set the `EXPIRES = true`, then you are indicating that the client WILL manage the lifecycle of the data. This includes all methods, for example if an item is expired, then its key should not be returned in `.keys()`, it's value should not be returned in `.values()`, and the method `.has()` will return `false`. The good news is that you will always receive the option `expires`, which is either `null` (no expiration) or a `number` indicating the time when it will expire.
|
|
573
|
+
|
|
574
|
+
**Prefix**: we manage the `prefix` as an invisible layer on top, you only need to be aware of it in the `.add()` method, as well as in the group methods:
|
|
442
575
|
|
|
443
576
|
```js
|
|
444
|
-
|
|
577
|
+
// What the user of polystore does:
|
|
578
|
+
const store = await kv(client).prefix("hello:").prefix("world:");
|
|
445
579
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
//
|
|
449
|
-
|
|
580
|
+
// User calls this, then the client is called with that:
|
|
581
|
+
const value = await store.get("a");
|
|
582
|
+
// client.get("hello:world:a");
|
|
583
|
+
|
|
584
|
+
// User calls this, then the client is called with that:
|
|
585
|
+
const value = await store.entries();
|
|
586
|
+
// client.entries("hello:world:");
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
> Note: all of the _group methods_ that return keys, should return them **with the prefix stripped**:
|
|
590
|
+
|
|
591
|
+
```js
|
|
592
|
+
// Example if your client works around a simple object {}, we want to remove
|
|
593
|
+
// the `prefix` from the beginning of the keys returned:
|
|
594
|
+
client.keys = (prefix) => {
|
|
595
|
+
return Object.keys(subStore)
|
|
596
|
+
.filter((key) => key.startsWith(prefix))
|
|
597
|
+
.map((key) => key.slice(prefix.length)); // <= Important!
|
|
598
|
+
};
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
You can and should just concatenate the `key + options.prefix`. We don't do it for two reasons: in some cases, like `.add()`, there's no key that we can use to concatenate, and also you might
|
|
602
|
+
|
|
603
|
+
For example, if the user of `polystore` does `kv(client).prefix('hello:').get('a')`, your store will be directly called with `client.get('a', { prefix: 'hello:' })`. You can safely concatenate `options.prefix + key` since this library always ensures that the prefix is defined and defaults to `''`. We don't concatenate it interally because in some cases (like in `.add()`) it makes more sense that this is handled by the client as an optimization.
|
|
604
|
+
|
|
605
|
+
While the signatures are different, you can check each entries on the output of Polystore API to see what is expected for the methods of the client to do, e.g. `.clear()` will remove all of the items that match the prefix (or everything if there's no prefix).
|
|
606
|
+
|
|
607
|
+
**Example: Plain Object client**
|
|
608
|
+
|
|
609
|
+
This is a good example of how simple a store can be, however do not use it literally since it behaves the same as the already-supported `new Map()`, only use it as the base for your own clients:
|
|
610
|
+
|
|
611
|
+
```js
|
|
612
|
+
const dataSource = {};
|
|
613
|
+
|
|
614
|
+
class MyClient {
|
|
615
|
+
get(key) {
|
|
616
|
+
return dataSource[key];
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// No need to stringify it or anything for a plain object storage
|
|
620
|
+
set(key, value) {
|
|
621
|
+
dataSource[key] = value;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Filter them by the prefix, note that `prefix` will always be a string
|
|
625
|
+
entries(prefix) {
|
|
626
|
+
const entries = Object.entries(dataSource);
|
|
627
|
+
if (!prefix) return entries;
|
|
628
|
+
return entries.filter(([key, value]) => key.startsWith(prefix));
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
We don't set `EXPIRES` to true since plain objects do NOT support expiration natively. So by not adding the `EXPIRES` property, it's the same as setting it to `false`, and polystore will manage all the expirations as a layer on top of the data. We could be more explicit and set it to `EXPIRES = false`, but it's not needed in this case.
|
|
634
|
+
|
|
635
|
+
**Example: custom ID generation**
|
|
636
|
+
|
|
637
|
+
You might want to provide your custom key generation algorithm, which I'm going to call `customId()` for example purposes. The only place where `polystore` generates IDs is in `add`, so you can provide your client with a custom generator:
|
|
638
|
+
|
|
639
|
+
```js
|
|
640
|
+
class MyClient {
|
|
641
|
+
|
|
642
|
+
// Add the opt method .add() to have more control over the ID generation
|
|
643
|
+
async add (prefix, data, { expires }) {
|
|
644
|
+
const id = customId();
|
|
645
|
+
const key = prefix + id;
|
|
646
|
+
return this.set(key, data, { expires });
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
//
|
|
650
|
+
async set (...) {
|
|
651
|
+
// ...
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
That way, when using the store, you can simply use `.add()` to generate it:
|
|
657
|
+
|
|
658
|
+
```js
|
|
659
|
+
import kv from "polystore";
|
|
660
|
+
|
|
661
|
+
const store = kv(MyClient);
|
|
662
|
+
const id = await store.add({ hello: "world" });
|
|
663
|
+
// this is your own custom id
|
|
664
|
+
const id2 = await store.prefix("hello:").add({ hello: "world" });
|
|
665
|
+
// this is `hello:{your own custom id}`
|
|
450
666
|
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Use Cloudflare's KV store
|
|
2
|
+
export default class Cloudflare {
|
|
3
|
+
// Indicate that the file handler does NOT handle expirations
|
|
4
|
+
EXPIRES = true;
|
|
5
|
+
|
|
6
|
+
// Check whether the given store is a FILE-type
|
|
7
|
+
static test(client) {
|
|
8
|
+
return (
|
|
9
|
+
client?.constructor?.name === "KvNamespace" ||
|
|
10
|
+
client?.constructor?.name === "EdgeKVNamespace"
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
constructor(client) {
|
|
15
|
+
this.client = client;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async get(key) {
|
|
19
|
+
const data = await this.client.get(key);
|
|
20
|
+
if (!data) return null;
|
|
21
|
+
return JSON.parse(data);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async set(key, value, { expires } = {}) {
|
|
25
|
+
const expirationTtl = expires ? Math.round(expires) : undefined;
|
|
26
|
+
this.client.put(key, JSON.stringify(value), { expirationTtl });
|
|
27
|
+
return key;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async del(key) {
|
|
31
|
+
return this.client.delete(key);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async keys(prefix = "") {
|
|
35
|
+
const raw = await this.client.list({ prefix });
|
|
36
|
+
return raw.keys.map((k) => k.name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async entries(prefix = "") {
|
|
40
|
+
const keys = await this.keys(prefix);
|
|
41
|
+
const values = await Promise.all(keys.map((k) => this.get(k)));
|
|
42
|
+
return keys.map((key, i) => [key, values[i]]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async clear(prefix = "") {
|
|
46
|
+
const list = await this.keys(prefix);
|
|
47
|
+
return Promise.all(list.map((k) => this.del(k)));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// A client that uses a single file (JSON) as a store
|
|
2
|
+
export default class Cookie {
|
|
3
|
+
// Indicate if this client handles expirations (true = it does)
|
|
4
|
+
EXPIRES = true;
|
|
5
|
+
|
|
6
|
+
// Check if this is the right class for the given client
|
|
7
|
+
static test(client) {
|
|
8
|
+
return client === "cookie" || client === "cookies";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// For cookies, an empty value is the same as null, even `""`
|
|
12
|
+
get(key) {
|
|
13
|
+
return this.all()[key] || null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
set(key, data = null, { expires } = {}) {
|
|
17
|
+
// Setting it to null deletes it
|
|
18
|
+
if (data === null) {
|
|
19
|
+
data = "";
|
|
20
|
+
expires = -100;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let expireStr = "";
|
|
24
|
+
// NOTE: 0 is already considered here!
|
|
25
|
+
if (expires !== null) {
|
|
26
|
+
const now = new Date().getTime();
|
|
27
|
+
const time = new Date(now + expires * 1000).toUTCString();
|
|
28
|
+
expireStr = `; expires=${time}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const value = encodeURIComponent(JSON.stringify(data));
|
|
32
|
+
document.cookie = encodeURIComponent(key) + "=" + value + expireStr;
|
|
33
|
+
return key;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Group methods
|
|
37
|
+
all(prefix = "") {
|
|
38
|
+
const all = {};
|
|
39
|
+
for (let entry of document.cookie.split(";")) {
|
|
40
|
+
const [key, data] = entry.split("=");
|
|
41
|
+
const name = decodeURIComponent(key.trim());
|
|
42
|
+
if (!name.startsWith(prefix)) continue;
|
|
43
|
+
try {
|
|
44
|
+
all[name] = JSON.parse(decodeURIComponent(data.trim()));
|
|
45
|
+
} catch (error) {
|
|
46
|
+
// no-op (some 3rd party can set cookies independently)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return all;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
entries(prefix = "") {
|
|
53
|
+
return Object.entries(this.all(prefix));
|
|
54
|
+
}
|
|
55
|
+
}
|