polystore 0.8.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 +291 -82
- 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 -391
- package/src/index.test.js +273 -37
- 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,40 +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
|
-
- **
|
|
37
|
-
-
|
|
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.
|
|
38
44
|
|
|
39
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:
|
|
40
46
|
|
|
@@ -50,7 +56,7 @@ MyApi({ cache: env.KV_NAMESPACE }); // OR
|
|
|
50
56
|
|
|
51
57
|
## API
|
|
52
58
|
|
|
53
|
-
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:
|
|
54
60
|
|
|
55
61
|
```js
|
|
56
62
|
import kv from "polystore";
|
|
@@ -164,7 +170,7 @@ if (await store.has('cookie-consent')) {
|
|
|
164
170
|
|
|
165
171
|
### .del()
|
|
166
172
|
|
|
167
|
-
Remove a single key from the store:
|
|
173
|
+
Remove a single key from the store and return the key itself:
|
|
168
174
|
|
|
169
175
|
```js
|
|
170
176
|
await store.del(key: string);
|
|
@@ -225,9 +231,11 @@ Remove all of the data from the store:
|
|
|
225
231
|
await store.clear();
|
|
226
232
|
```
|
|
227
233
|
|
|
228
|
-
### .prefix()
|
|
234
|
+
### .prefix()
|
|
229
235
|
|
|
230
|
-
|
|
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:
|
|
231
239
|
|
|
232
240
|
```js
|
|
233
241
|
const store = kv(new Map());
|
|
@@ -237,16 +245,17 @@ const session = store.prefix("session:");
|
|
|
237
245
|
Then all of the operations will be converted internally to add the prefix when reading, writing, etc:
|
|
238
246
|
|
|
239
247
|
```js
|
|
240
|
-
const
|
|
241
|
-
await session.
|
|
242
|
-
|
|
243
|
-
await session.
|
|
244
|
-
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
|
|
245
254
|
// ['key1', 'key2', ...] Note no prefix here
|
|
246
255
|
await session.clear(); // delete only keys with the prefix
|
|
247
256
|
```
|
|
248
257
|
|
|
249
|
-
|
|
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:
|
|
250
259
|
|
|
251
260
|
```js
|
|
252
261
|
// Two in-memory stores
|
|
@@ -260,9 +269,11 @@ const books = kv(new URL(`file://${import.meta.dirname}/books.json`));
|
|
|
260
269
|
|
|
261
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_.
|
|
262
271
|
|
|
263
|
-
##
|
|
272
|
+
## Clients
|
|
264
273
|
|
|
265
|
-
|
|
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"`.
|
|
275
|
+
|
|
276
|
+
Polystore provides a unified API you can use `Promises`, `expires` and `.prefix()` even with those stores that do not support these operations natively.
|
|
266
277
|
|
|
267
278
|
### Memory
|
|
268
279
|
|
|
@@ -271,18 +282,31 @@ An in-memory KV store, with promises and expiration time:
|
|
|
271
282
|
```js
|
|
272
283
|
import kv from "polystore";
|
|
273
284
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
await store.set("key1", "Hello world");
|
|
285
|
+
const store = kv(new Map());
|
|
286
|
+
|
|
287
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
278
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";
|
|
279
296
|
|
|
280
|
-
|
|
297
|
+
const store = kv();
|
|
281
298
|
const store = kv(new Map());
|
|
282
|
-
await store.set("key1", "Hello world");
|
|
283
|
-
console.log(await store.get("key1"));
|
|
284
299
|
```
|
|
285
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
|
+
|
|
286
310
|
### Local Storage
|
|
287
311
|
|
|
288
312
|
The traditional localStorage that we all know and love, this time with a unified API, and promises:
|
|
@@ -291,8 +315,10 @@ The traditional localStorage that we all know and love, this time with a unified
|
|
|
291
315
|
import kv from "polystore";
|
|
292
316
|
|
|
293
317
|
const store = kv(localStorage);
|
|
294
|
-
|
|
318
|
+
|
|
319
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
295
320
|
console.log(await store.get("key1"));
|
|
321
|
+
// "Hello world"
|
|
296
322
|
```
|
|
297
323
|
|
|
298
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)!
|
|
@@ -305,8 +331,10 @@ Same as localStorage, but now for the session only:
|
|
|
305
331
|
import kv from "polystore";
|
|
306
332
|
|
|
307
333
|
const store = kv(sessionStorage);
|
|
308
|
-
|
|
334
|
+
|
|
335
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
309
336
|
console.log(await store.get("key1"));
|
|
337
|
+
// "Hello world"
|
|
310
338
|
```
|
|
311
339
|
|
|
312
340
|
### Cookies
|
|
@@ -316,9 +344,11 @@ Supports native browser cookies, including setting the expire time:
|
|
|
316
344
|
```js
|
|
317
345
|
import kv from "polystore";
|
|
318
346
|
|
|
319
|
-
const store = kv("cookie"); //
|
|
320
|
-
|
|
347
|
+
const store = kv("cookie"); // just a plain string
|
|
348
|
+
|
|
349
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
321
350
|
console.log(await store.get("key1"));
|
|
351
|
+
// "Hello world"
|
|
322
352
|
```
|
|
323
353
|
|
|
324
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).
|
|
@@ -334,8 +364,10 @@ import kv from "polystore";
|
|
|
334
364
|
import localForage from "localforage";
|
|
335
365
|
|
|
336
366
|
const store = kv(localForage);
|
|
367
|
+
|
|
337
368
|
await store.set("key1", "Hello world", { expires: "1h" });
|
|
338
369
|
console.log(await store.get("key1"));
|
|
370
|
+
// "Hello world"
|
|
339
371
|
```
|
|
340
372
|
|
|
341
373
|
### Redis Client
|
|
@@ -346,25 +378,37 @@ Supports the official Node Redis Client. You can pass either the client or the p
|
|
|
346
378
|
import kv from "polystore";
|
|
347
379
|
import { createClient } from "redis";
|
|
348
380
|
|
|
349
|
-
// Note: no need for await or similar
|
|
350
381
|
const store = kv(createClient().connect());
|
|
351
|
-
|
|
382
|
+
|
|
383
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
352
384
|
console.log(await store.get("key1"));
|
|
385
|
+
// "Hello world"
|
|
353
386
|
```
|
|
354
387
|
|
|
388
|
+
You don't need to `await` for the connect or similar, this will process it properly.
|
|
389
|
+
|
|
355
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.
|
|
356
391
|
|
|
357
|
-
###
|
|
392
|
+
### File
|
|
393
|
+
|
|
394
|
+
Treat a JSON file in your filesystem as the source for the KV store:
|
|
358
395
|
|
|
359
396
|
```js
|
|
360
397
|
import kv from "polystore";
|
|
361
398
|
|
|
362
|
-
// Create a url with the file protocol:
|
|
363
399
|
const store = kv(new URL("file:///Users/me/project/cache.json"));
|
|
364
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
|
|
365
409
|
// Paths need to be absolute, but you can use process.cwd() to make
|
|
366
410
|
// it relative to the current process:
|
|
367
|
-
const
|
|
411
|
+
const store1 = kv(new URL(`file://${process.cwd()}/cache.json`));
|
|
368
412
|
const store2 = kv(new URL(`file://${import.meta.dirname}/data.json`));
|
|
369
413
|
```
|
|
370
414
|
|
|
@@ -379,18 +423,16 @@ export default {
|
|
|
379
423
|
async fetch(request, env, ctx) {
|
|
380
424
|
const store = kv(env.YOUR_KV_NAMESPACE);
|
|
381
425
|
|
|
382
|
-
await store.set("
|
|
383
|
-
|
|
426
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
427
|
+
console.log(await store.get("key1"));
|
|
428
|
+
// "Hello world"
|
|
384
429
|
|
|
385
|
-
|
|
386
|
-
return new Response("Value not found", { status: 404 });
|
|
387
|
-
}
|
|
388
|
-
return new Response(value);
|
|
430
|
+
return new Response("My response");
|
|
389
431
|
},
|
|
390
432
|
};
|
|
391
433
|
```
|
|
392
434
|
|
|
393
|
-
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:
|
|
394
436
|
|
|
395
437
|
```js
|
|
396
438
|
// GOOD - with polystore
|
|
@@ -404,9 +446,57 @@ await env.YOUR_KV_NAMESPACE.put("user", serialValue, {
|
|
|
404
446
|
});
|
|
405
447
|
```
|
|
406
448
|
|
|
407
|
-
|
|
449
|
+
### Level
|
|
408
450
|
|
|
409
|
-
|
|
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)).
|
|
498
|
+
|
|
499
|
+
We unify all of the clients diverse expiration methods into a single, easy one with `expires`:
|
|
410
500
|
|
|
411
501
|
```js
|
|
412
502
|
// in-memory store
|
|
@@ -421,7 +511,7 @@ console.log(await store.get("a")); // 'b'
|
|
|
421
511
|
// Make sure the key is expired
|
|
422
512
|
await delay(2000); // 2s
|
|
423
513
|
|
|
424
|
-
//
|
|
514
|
+
// The group methods also ignore expired keys
|
|
425
515
|
console.log(await store.keys()); // []
|
|
426
516
|
console.log(await store.has("a")); // false
|
|
427
517
|
console.log(await store.get("a")); // null
|
|
@@ -433,25 +523,144 @@ However, in some stores this does come with some potential performance disadvant
|
|
|
433
523
|
|
|
434
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.
|
|
435
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
|
+
|
|
436
540
|
## Creating a store
|
|
437
541
|
|
|
438
|
-
|
|
542
|
+
To create a store, you define a class with these methods:
|
|
439
543
|
|
|
440
544
|
```js
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
+
}
|
|
446
568
|
```
|
|
447
569
|
|
|
448
|
-
|
|
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:
|
|
449
575
|
|
|
450
576
|
```js
|
|
451
|
-
|
|
577
|
+
// What the user of polystore does:
|
|
578
|
+
const store = await kv(client).prefix("hello:").prefix("world:");
|
|
452
579
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
//
|
|
456
|
-
|
|
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}`
|
|
457
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
|
+
}
|