polystore 0.13.0 → 0.14.1
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 +1 -1
- package/readme.md +254 -30
- package/src/clients/file.js +11 -2
- package/src/clients/folder.js +83 -0
- package/src/clients/index.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polystore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "A small compatibility layer for many popular KV stores like localStorage, Redis, FileSystem, etc.",
|
|
5
5
|
"homepage": "https://polystore.dev/",
|
|
6
6
|
"repository": "https://github.com/franciscop/polystore.git",
|
package/readme.md
CHANGED
|
@@ -24,7 +24,9 @@ These are all the methods of the [API](#api) (they are all `async`):
|
|
|
24
24
|
- [`.all()`](#all): get an object with the key:values mapped.
|
|
25
25
|
- [`.clear()`](#clear): delete ALL of the data in the store, effectively resetting it.
|
|
26
26
|
- [`.close()`](#close): (only _some_ stores) ends the connection to the store.
|
|
27
|
-
- [`.prefix(prefix)`](#prefix): create a sub-store that
|
|
27
|
+
- [`.prefix(prefix)`](#prefix): create a sub-store that manages the keys with that prefix.
|
|
28
|
+
|
|
29
|
+
> This library has very high performance with the item methods (GET/SET/ADD/HAS/DEL). For other methods or to learn more, see [the performance considerations](#performance) and read the docs on your specific client.
|
|
28
30
|
|
|
29
31
|
Available clients for the KV store:
|
|
30
32
|
|
|
@@ -34,15 +36,14 @@ Available clients for the KV store:
|
|
|
34
36
|
- [**Cookies** `"cookie"`](#cookies) (fe): persist the data using cookies
|
|
35
37
|
- [**LocalForage** `localForage`](#local-forage) (fe): persist the data on IndexedDB
|
|
36
38
|
- [**File** `new URL('file:///...')`](#file) (be): store the data in a single JSON file in your FS
|
|
39
|
+
- [**Folder** `new URL('file:///...')`](#folder) (be): store each key in a folder as json files
|
|
37
40
|
- [**Redis Client** `redisClient`](#redis-client) (be): use the Redis instance that you connect to
|
|
38
41
|
- [**Cloudflare KV** `env.KV_NAMESPACE`](#cloudflare-kv) (be): use Cloudflare's KV store
|
|
39
42
|
- [**Level** `new Level('example', { valueEncoding: 'json' })`](#level) (fe+be): support the whole Level ecosystem
|
|
40
43
|
- [**Etcd** `new Etcd3()`](#etcd) (be): the Microsoft's high performance KV store.
|
|
41
|
-
- [**_Custom_** `{}`](#creating-a-store) (
|
|
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.
|
|
44
|
+
- [**_Custom_** `{}`](#creating-a-store) (fe+be): create your own store with just 3 methods!
|
|
44
45
|
|
|
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
|
|
46
|
+
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 universal (Node.js, Bun and the Browser) and tiny (~3KB). For example, let's say you create an API library, then you can accept the stores from your client:
|
|
46
47
|
|
|
47
48
|
```js
|
|
48
49
|
import MyApi from "my-api";
|
|
@@ -127,9 +128,9 @@ console.log(await store.get("key2")); // ["my", "grocery", "list"]
|
|
|
127
128
|
console.log(await store.get("key3")); // { name: "Francisco" }
|
|
128
129
|
```
|
|
129
130
|
|
|
130
|
-
If the value is returned, it can be a simple type like `boolean`, `string` or `number`, or it can be a plain Object or Array
|
|
131
|
+
If the value is returned, it can be a simple type like `boolean`, `string` or `number`, or it can be a plain `Object` or `Array`, or any combination of those.
|
|
131
132
|
|
|
132
|
-
|
|
133
|
+
When there's no value (either never set, or expired), `null` will be returned from the operation.
|
|
133
134
|
|
|
134
135
|
### .set()
|
|
135
136
|
|
|
@@ -143,7 +144,7 @@ await store.set("key2", ["my", "grocery", "list"], { expires: "1h" });
|
|
|
143
144
|
await store.set("key3", { name: "Francisco" }, { expires: 60 * 60 });
|
|
144
145
|
```
|
|
145
146
|
|
|
146
|
-
The value can be a simple type like `boolean`, `string` or `number`, or it can be a plain Object or Array
|
|
147
|
+
The value can be a simple type like `boolean`, `string` or `number`, or it can be a plain `Object` or `Array`, or a combination of those. It **cannot** be a more complex or non-serializable values like a `Date()`, `Infinity`, `undefined` (casted to `null`), a `Symbol`, etc.
|
|
147
148
|
|
|
148
149
|
- By default the keys _don't expire_.
|
|
149
150
|
- Setting the `value` to `null`, or the `expires` to `0` is the equivalent of deleting the key+value.
|
|
@@ -182,11 +183,16 @@ const key2 = await store.add(["my", "grocery", "list"], { expires: "1h" });
|
|
|
182
183
|
const key3 = await store.add({ name: "Francisco" }, { expires: 60 * 60 });
|
|
183
184
|
```
|
|
184
185
|
|
|
185
|
-
The
|
|
186
|
+
The options and details are similar to [`.set()`](#set), except for the lack of the first argument, since `.add()` will generate the key automatically.
|
|
186
187
|
|
|
187
|
-
|
|
188
|
+
The default key will be 24 AlphaNumeric characters (upper+lower case), however this can change if you are using a `.prefix()` or some clients might generate it differently (only custom clients can do that right now).
|
|
188
189
|
|
|
189
|
-
>
|
|
190
|
+
<details>
|
|
191
|
+
<summary>Key Generation details</summary>
|
|
192
|
+
The default key will be 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.
|
|
193
|
+
|
|
194
|
+
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."
|
|
195
|
+
</details>
|
|
190
196
|
|
|
191
197
|
The main reason why `.add()` exists is to allow it to work with the prefix seamlessly:
|
|
192
198
|
|
|
@@ -307,29 +313,38 @@ There are also methods to retrieve all of the keys, values, or entries at once b
|
|
|
307
313
|
|
|
308
314
|
### .keys()
|
|
309
315
|
|
|
310
|
-
Get all of the keys in the store
|
|
316
|
+
Get all of the keys in the store as a simple array of strings:
|
|
317
|
+
|
|
318
|
+
```js
|
|
319
|
+
await store.keys();
|
|
320
|
+
// ["keyA", "keyB", "keyC", ...]
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
If you want to filter for a particular prefix, use `.prefix()`, which will return the values with the keys with that prefix (the keys have the prefix stripped!):
|
|
311
324
|
|
|
312
325
|
```js
|
|
313
|
-
await store.keys(
|
|
326
|
+
const sessions = await store.prefix("session:").keys();
|
|
327
|
+
// ["keyA", "keyB"]
|
|
314
328
|
```
|
|
315
329
|
|
|
316
330
|
> 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.
|
|
317
331
|
|
|
318
332
|
### .values()
|
|
319
333
|
|
|
320
|
-
Get all of the values in the store
|
|
334
|
+
Get all of the values in the store as a simple array with all the values:
|
|
321
335
|
|
|
322
336
|
```js
|
|
323
|
-
await store.values(
|
|
337
|
+
await store.values();
|
|
338
|
+
// ["valueA", "valueB", { hello: "world" }, ...]
|
|
324
339
|
```
|
|
325
340
|
|
|
326
|
-
|
|
341
|
+
If you want to filter for a particular prefix, use `.prefix()`, which will return the values with the keys with that prefix:
|
|
327
342
|
|
|
328
343
|
```js
|
|
329
|
-
const sessions = await store.
|
|
344
|
+
const sessions = await store.prefix("session:").values();
|
|
330
345
|
// A list of all the sessions
|
|
331
346
|
|
|
332
|
-
const companies = await store.
|
|
347
|
+
const companies = await store.prefix("company:").values();
|
|
333
348
|
// A list of all the companies
|
|
334
349
|
```
|
|
335
350
|
|
|
@@ -337,21 +352,45 @@ const companies = await store.values("company:");
|
|
|
337
352
|
|
|
338
353
|
### .entries()
|
|
339
354
|
|
|
340
|
-
Get all of the entries (key:value tuples) in the store
|
|
355
|
+
Get all of the entries (key:value tuples) in the store:
|
|
356
|
+
|
|
357
|
+
```js
|
|
358
|
+
const entries = await store.entries();
|
|
359
|
+
// [["keyA", "valueA"], ["keyB", "valueB"], ["keyC", { hello: "world" }], ...]
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
It's in the same format as `Object.entries(obj)`, so it's an array of [key, value] tuples.
|
|
363
|
+
|
|
364
|
+
If you want to filter for a particular prefix, use `.prefix()`, which will return the entries that have that given prefix (the keys have the prefix stripped!):
|
|
365
|
+
|
|
366
|
+
```js
|
|
367
|
+
const sessionEntries = await store.prefix('session:').entries();
|
|
368
|
+
// [["keyA", "valueA"], ["keyB", "valueB"]]
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
> 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.
|
|
372
|
+
|
|
373
|
+
### .all()
|
|
374
|
+
|
|
375
|
+
Get all of the entries (key:value) in the store as an object:
|
|
341
376
|
|
|
342
377
|
```js
|
|
343
|
-
await store.
|
|
378
|
+
const obj = await store.all(filter?: string);
|
|
379
|
+
// { keyA: "valueA", keyB: "valueB", keyC: { hello: "world" }, ... }
|
|
344
380
|
```
|
|
345
381
|
|
|
346
|
-
It
|
|
382
|
+
It's in the format of a normal key:value object, where the object key is the store's key and the object value is the store's value.
|
|
383
|
+
|
|
384
|
+
If you want to filter for a particular prefix, use `.prefix()`, which will return the object with only the keys that have that given prefix (stripping the keys of the prefix!):
|
|
347
385
|
|
|
348
386
|
```js
|
|
349
|
-
const
|
|
350
|
-
|
|
387
|
+
const sessionObj = await store.prefix('session:').entries();
|
|
388
|
+
// { keyA: "valueA", keyB: "valueB" }
|
|
351
389
|
```
|
|
352
390
|
|
|
353
391
|
> 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.
|
|
354
392
|
|
|
393
|
+
|
|
355
394
|
### .clear()
|
|
356
395
|
|
|
357
396
|
Remove all of the data from the store and resets it to the original state:
|
|
@@ -382,6 +421,9 @@ await session.del("key4"); // store.del('session:key4');
|
|
|
382
421
|
await session.keys(); // store.keys(); + filter
|
|
383
422
|
// ['key1', 'key2', ...] Note no prefix here
|
|
384
423
|
await session.clear(); // delete only keys with the prefix
|
|
424
|
+
for await (const [key, value] of session) {
|
|
425
|
+
console.log(key, value);
|
|
426
|
+
}
|
|
385
427
|
```
|
|
386
428
|
|
|
387
429
|
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:
|
|
@@ -420,13 +462,23 @@ console.log(await store.get("key1"));
|
|
|
420
462
|
|
|
421
463
|
<details>
|
|
422
464
|
<summary>Why use polystore with <code>new Map()</code>?</summary>
|
|
423
|
-
<p>
|
|
465
|
+
<p>These benefits are for wrapping Map() with polystore:</p>
|
|
424
466
|
<ul>
|
|
425
467
|
<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>
|
|
426
468
|
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
427
469
|
</ul>
|
|
428
470
|
</details>
|
|
429
471
|
|
|
472
|
+
```js
|
|
473
|
+
// GOOD - with polystore
|
|
474
|
+
await store.set("key1", { name: "Francisco" }, { expires: "2days" });
|
|
475
|
+
|
|
476
|
+
// COMPLEX - With sessionStorage
|
|
477
|
+
const data = new Map();
|
|
478
|
+
data.set("key1", { name: "Francisco" });
|
|
479
|
+
// Expiration not supported
|
|
480
|
+
```
|
|
481
|
+
|
|
430
482
|
### Local Storage
|
|
431
483
|
|
|
432
484
|
The traditional localStorage that we all know and love, this time with a unified API, and promises:
|
|
@@ -443,6 +495,26 @@ console.log(await store.get("key1"));
|
|
|
443
495
|
|
|
444
496
|
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)!
|
|
445
497
|
|
|
498
|
+
<details>
|
|
499
|
+
<summary>Why use polystore with <code>localStorage</code>?</summary>
|
|
500
|
+
<p>These benefits are for wrapping localStorage with polystore:</p>
|
|
501
|
+
<ul>
|
|
502
|
+
<li><strong>Data structures</strong>: with Polystore you can pass more complex data structures and we'll handle the serialization/deserialization.</li>
|
|
503
|
+
<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>
|
|
504
|
+
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
505
|
+
</ul>
|
|
506
|
+
</details>
|
|
507
|
+
|
|
508
|
+
```js
|
|
509
|
+
// GOOD - with polystore
|
|
510
|
+
await store.set("key1", { name: "Francisco" }, { expires: "2days" });
|
|
511
|
+
|
|
512
|
+
// COMPLEX - With localStorage
|
|
513
|
+
const serialValue = JSON.stringify({ name: "Francisco" });
|
|
514
|
+
localStorage.set("key1", serialValue);
|
|
515
|
+
// Expiration not supported
|
|
516
|
+
```
|
|
517
|
+
|
|
446
518
|
### Session Storage
|
|
447
519
|
|
|
448
520
|
Same as localStorage, but now for the session only:
|
|
@@ -457,6 +529,26 @@ console.log(await store.get("key1"));
|
|
|
457
529
|
// "Hello world"
|
|
458
530
|
```
|
|
459
531
|
|
|
532
|
+
<details>
|
|
533
|
+
<summary>Why use polystore with <code>sessionStorage</code>?</summary>
|
|
534
|
+
<p>These benefits are for wrapping sessionStorage with polystore:</p>
|
|
535
|
+
<ul>
|
|
536
|
+
<li><strong>Data structures</strong>: with Polystore you can pass more complex data structures and we'll handle the serialization/deserialization.</li>
|
|
537
|
+
<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>
|
|
538
|
+
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
539
|
+
</ul>
|
|
540
|
+
</details>
|
|
541
|
+
|
|
542
|
+
```js
|
|
543
|
+
// GOOD - with polystore
|
|
544
|
+
await store.set("key1", { name: "Francisco" }, { expires: "2days" });
|
|
545
|
+
|
|
546
|
+
// COMPLEX - With sessionStorage
|
|
547
|
+
const serialValue = JSON.stringify({ name: "Francisco" });
|
|
548
|
+
sessionStorage.set("key1", serialValue);
|
|
549
|
+
// Expiration not supported
|
|
550
|
+
```
|
|
551
|
+
|
|
460
552
|
### Cookies
|
|
461
553
|
|
|
462
554
|
Supports native browser cookies, including setting the expire time:
|
|
@@ -475,6 +567,16 @@ It is fairly limited for how powerful cookies are, but in exchange it has the sa
|
|
|
475
567
|
|
|
476
568
|
> 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.
|
|
477
569
|
|
|
570
|
+
<details>
|
|
571
|
+
<summary>Why use polystore with <code>cookies</code>?</summary>
|
|
572
|
+
<p>These benefits are for wrapping cookies with polystore:</p>
|
|
573
|
+
<ul>
|
|
574
|
+
<li><strong>Data structures</strong>: with Polystore you can pass more complex data structures and we'll handle the serialization/deserialization.</li>
|
|
575
|
+
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#expiration-explained">Expiration explained</a>.</li>
|
|
576
|
+
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
577
|
+
</ul>
|
|
578
|
+
</details>
|
|
579
|
+
|
|
478
580
|
### Local Forage
|
|
479
581
|
|
|
480
582
|
Supports localForage (with any driver it uses) so that you have a unified API. It also _adds_ the `expires` option to the setters!
|
|
@@ -490,6 +592,15 @@ console.log(await store.get("key1"));
|
|
|
490
592
|
// "Hello world"
|
|
491
593
|
```
|
|
492
594
|
|
|
595
|
+
<details>
|
|
596
|
+
<summary>Why use polystore with <code>localStorage</code>?</summary>
|
|
597
|
+
<p>These benefits are for wrapping localStorage with polystore:</p>
|
|
598
|
+
<ul>
|
|
599
|
+
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#expiration-explained">Expiration explained</a>.</li>
|
|
600
|
+
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
601
|
+
</ul>
|
|
602
|
+
</details>
|
|
603
|
+
|
|
493
604
|
### Redis Client
|
|
494
605
|
|
|
495
606
|
Supports the official Node Redis Client. You can pass either the client or the promise:
|
|
@@ -509,6 +620,15 @@ You don't need to `await` for the connect or similar, this will process it prope
|
|
|
509
620
|
|
|
510
621
|
> 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.
|
|
511
622
|
|
|
623
|
+
<details>
|
|
624
|
+
<summary>Why use polystore with <code>Redis</code>?</summary>
|
|
625
|
+
<p>These benefits are for wrapping Redis with polystore:</p>
|
|
626
|
+
<ul>
|
|
627
|
+
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#expiration-explained">Expiration explained</a>.</li>
|
|
628
|
+
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
629
|
+
</ul>
|
|
630
|
+
</details>
|
|
631
|
+
|
|
512
632
|
### File
|
|
513
633
|
|
|
514
634
|
Treat a JSON file in your filesystem as the source for the KV store:
|
|
@@ -523,6 +643,8 @@ console.log(await store.get("key1"));
|
|
|
523
643
|
// "Hello world"
|
|
524
644
|
```
|
|
525
645
|
|
|
646
|
+
> Note: an extension is needed, to desambiguate with "folder"
|
|
647
|
+
|
|
526
648
|
You can also create multiple stores:
|
|
527
649
|
|
|
528
650
|
```js
|
|
@@ -532,6 +654,76 @@ const store1 = kv(new URL(`file://${process.cwd()}/cache.json`));
|
|
|
532
654
|
const store2 = kv(new URL(`file://${import.meta.dirname}/data.json`));
|
|
533
655
|
```
|
|
534
656
|
|
|
657
|
+
<details>
|
|
658
|
+
<summary>Why use polystore with a file?</summary>
|
|
659
|
+
<p>These benefits are for wrapping a file with polystore:</p>
|
|
660
|
+
<ul>
|
|
661
|
+
<li><strong>Data structures</strong>: with Polystore you can pass more complex data structures and we'll handle the serialization/deserialization.</li>
|
|
662
|
+
<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>
|
|
663
|
+
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
664
|
+
</ul>
|
|
665
|
+
</details>
|
|
666
|
+
|
|
667
|
+
```js
|
|
668
|
+
// GOOD - with polystore
|
|
669
|
+
await store.set("key1", { name: "Francisco" }, { expires: "2days" });
|
|
670
|
+
|
|
671
|
+
// COMPLEX - With native file managing
|
|
672
|
+
const file = './data/users.json';
|
|
673
|
+
const str = await fsp.readFile(file, "utf-8");
|
|
674
|
+
const data = JSON.parse(str);
|
|
675
|
+
data["key1"] = { name: "Francisco" };
|
|
676
|
+
const serialValue = JSON.stringify(data);
|
|
677
|
+
await fsp.writeFile(file, serialValue);
|
|
678
|
+
// Expiration not supported (and error handling not shown)
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### Folder
|
|
682
|
+
|
|
683
|
+
Treat a single folder in your filesystem as the source for the KV store, with each key being within a file:
|
|
684
|
+
|
|
685
|
+
```js
|
|
686
|
+
import kv from "polystore";
|
|
687
|
+
|
|
688
|
+
const store = kv(new URL("file:///Users/me/project/data/"));
|
|
689
|
+
|
|
690
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
691
|
+
console.log(await store.get("key1"));
|
|
692
|
+
// "Hello world"
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
> Note: the ending slash `/` is needed, to desambiguate with "file"
|
|
696
|
+
|
|
697
|
+
You can also create multiple stores:
|
|
698
|
+
|
|
699
|
+
```js
|
|
700
|
+
// Paths need to be absolute, but you can use process.cwd() to make
|
|
701
|
+
// it relative to the current process:
|
|
702
|
+
const store1 = kv(new URL(`file://${process.cwd()}/cache/`));
|
|
703
|
+
const store2 = kv(new URL(`file://${import.meta.dirname}/data/`));
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
<details>
|
|
707
|
+
<summary>Why use polystore with a folder?</summary>
|
|
708
|
+
<p>These benefits are for wrapping a folder with polystore:</p>
|
|
709
|
+
<ul>
|
|
710
|
+
<li><strong>Data structures</strong>: with Polystore you can pass more complex data structures and we'll handle the serialization/deserialization.</li>
|
|
711
|
+
<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>
|
|
712
|
+
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
713
|
+
</ul>
|
|
714
|
+
</details>
|
|
715
|
+
|
|
716
|
+
```js
|
|
717
|
+
// GOOD - with polystore
|
|
718
|
+
await store.set("key1", { name: "Francisco" }, { expires: "2days" });
|
|
719
|
+
|
|
720
|
+
// COMPLEX - With native folder
|
|
721
|
+
const file = './data/user/key1.json';
|
|
722
|
+
const serialValue = JSON.stringify({ name: "Francisco" });
|
|
723
|
+
await fsp.writeFile(file, serialValue);
|
|
724
|
+
// Expiration not supported (and error handling not shown)
|
|
725
|
+
```
|
|
726
|
+
|
|
535
727
|
### Cloudflare KV
|
|
536
728
|
|
|
537
729
|
Supports the official Cloudflare's KV stores. Follow [the official guide](https://developers.cloudflare.com/kv/get-started/), then load it like this:
|
|
@@ -552,7 +744,15 @@ export default {
|
|
|
552
744
|
};
|
|
553
745
|
```
|
|
554
746
|
|
|
555
|
-
|
|
747
|
+
<details>
|
|
748
|
+
<summary>Why use polystore with Cloudflare's KV?</summary>
|
|
749
|
+
<p>These benefits are for wrapping Cloudflare's KV with polystore:</p>
|
|
750
|
+
<ul>
|
|
751
|
+
<li><strong>Data structures</strong>: with Polystore you can pass more complex data structures and we'll handle the serialization/deserialization.</li>
|
|
752
|
+
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#expiration-explained">Expiration explained</a>.</li>
|
|
753
|
+
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
754
|
+
</ul>
|
|
755
|
+
</details>
|
|
556
756
|
|
|
557
757
|
```js
|
|
558
758
|
// GOOD - with polystore
|
|
@@ -581,6 +781,22 @@ console.log(await store.get("key1"));
|
|
|
581
781
|
// "Hello world"
|
|
582
782
|
```
|
|
583
783
|
|
|
784
|
+
<details>
|
|
785
|
+
<summary>Why use polystore with Level?</summary>
|
|
786
|
+
<p>These benefits are for wrapping Level with polystore:</p>
|
|
787
|
+
<ul>
|
|
788
|
+
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#expiration-explained">Expiration explained</a>.</li>
|
|
789
|
+
</ul>
|
|
790
|
+
</details>
|
|
791
|
+
|
|
792
|
+
```js
|
|
793
|
+
// GOOD - with polystore
|
|
794
|
+
await store.set("user", { hello: 'world' }, { expires: "2days" });
|
|
795
|
+
|
|
796
|
+
// With Level:
|
|
797
|
+
?? // Just not possible
|
|
798
|
+
```
|
|
799
|
+
|
|
584
800
|
### Etcd
|
|
585
801
|
|
|
586
802
|
Connect to Microsoft's Etcd Key-Value store:
|
|
@@ -596,17 +812,26 @@ console.log(await store.get("key1"));
|
|
|
596
812
|
// "Hello world"
|
|
597
813
|
```
|
|
598
814
|
|
|
815
|
+
<details>
|
|
816
|
+
<summary>Why use polystore with Etcd?</summary>
|
|
817
|
+
<p>These benefits are for wrapping Etcd with polystore:</p>
|
|
818
|
+
<ul>
|
|
819
|
+
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#expiration-explained">Expiration explained</a>.</li>
|
|
820
|
+
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
821
|
+
</ul>
|
|
822
|
+
</details>
|
|
823
|
+
|
|
599
824
|
### Custom store
|
|
600
825
|
|
|
601
|
-
Please see the [creating a store](#creating-a-store) section for
|
|
826
|
+
Please see the [creating a store](#creating-a-store) section for all the details!
|
|
602
827
|
|
|
603
828
|
## Performance
|
|
604
829
|
|
|
605
|
-
> TL;DR: if you only use the item operations (add,set,get,has,del) and your client supports expiration natively, you have nothing to worry about!
|
|
830
|
+
> TL;DR: if you only use the item operations (add,set,get,has,del) and your client supports expiration natively, you have nothing to worry about! Otherwise, please read on.
|
|
606
831
|
|
|
607
832
|
While all of our stores support `expires`, `.prefix()` and group operations, the nature of those makes them to have different performance characteristics.
|
|
608
833
|
|
|
609
|
-
**Expires** we polyfill expiration when the underlying client 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
|
|
834
|
+
**Expires** we polyfill expiration when the underlying client 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).
|
|
610
835
|
|
|
611
836
|
**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 in the general case. 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.
|
|
612
837
|
|
|
@@ -703,8 +928,7 @@ const value = await store.get("a");
|
|
|
703
928
|
// client.get("hello:world:a");
|
|
704
929
|
|
|
705
930
|
// User calls this, then the client is called with that:
|
|
706
|
-
for await (const
|
|
707
|
-
}
|
|
931
|
+
for await (const [key, value] of store) {}
|
|
708
932
|
// client.iterate("hello:world:");
|
|
709
933
|
```
|
|
710
934
|
|
package/src/clients/file.js
CHANGED
|
@@ -2,8 +2,17 @@
|
|
|
2
2
|
export default class File {
|
|
3
3
|
// Check if this is the right class for the given client
|
|
4
4
|
static test(client) {
|
|
5
|
-
if (
|
|
6
|
-
|
|
5
|
+
if (
|
|
6
|
+
typeof client === "string" &&
|
|
7
|
+
client.startsWith("file:") &&
|
|
8
|
+
client.includes(".")
|
|
9
|
+
)
|
|
10
|
+
return true;
|
|
11
|
+
return (
|
|
12
|
+
client instanceof URL &&
|
|
13
|
+
client.protocol === "file:" &&
|
|
14
|
+
client.pathname.includes(".")
|
|
15
|
+
);
|
|
7
16
|
}
|
|
8
17
|
|
|
9
18
|
constructor(file) {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const noFileOk = (error) => {
|
|
2
|
+
if (error.code === "ENOENT") return null;
|
|
3
|
+
throw error;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
// A client that uses a single file (JSON) as a store
|
|
7
|
+
export default class Folder {
|
|
8
|
+
// Check if this is the right class for the given client
|
|
9
|
+
static test(client) {
|
|
10
|
+
if (
|
|
11
|
+
typeof client === "string" &&
|
|
12
|
+
client.startsWith("file:") &&
|
|
13
|
+
client.endsWith("/")
|
|
14
|
+
)
|
|
15
|
+
return true;
|
|
16
|
+
return (
|
|
17
|
+
client instanceof URL &&
|
|
18
|
+
client.protocol === "file:" &&
|
|
19
|
+
client.pathname.endsWith("/")
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
constructor(folder) {
|
|
24
|
+
this.folder =
|
|
25
|
+
typeof folder === "string"
|
|
26
|
+
? folder.slice("file://".length).replace(/\/$/, "") + "/"
|
|
27
|
+
: folder.pathname.replace(/\/$/, "") + "/";
|
|
28
|
+
|
|
29
|
+
// Run this once on launch; import the FS module and reset the file
|
|
30
|
+
this.promise = (async () => {
|
|
31
|
+
const fsp = await import("node:fs/promises");
|
|
32
|
+
|
|
33
|
+
// Make sure the folder already exists, so attempt to create it
|
|
34
|
+
// It fails if it already exists, hence the catch case
|
|
35
|
+
await fsp.mkdir(this.folder, { recursive: true }).catch((err) => {});
|
|
36
|
+
return fsp;
|
|
37
|
+
})();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async get(key) {
|
|
41
|
+
const fsp = await this.promise;
|
|
42
|
+
const file = this.folder + key + ".json";
|
|
43
|
+
const text = await fsp.readFile(file, "utf8").catch(noFileOk);
|
|
44
|
+
if (!text) return null;
|
|
45
|
+
return JSON.parse(text);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async set(key, value) {
|
|
49
|
+
const fsp = await this.promise;
|
|
50
|
+
const file = this.folder + key + ".json";
|
|
51
|
+
await fsp.writeFile(file, JSON.stringify(value, null, 2), "utf8");
|
|
52
|
+
return file;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async del(key) {
|
|
56
|
+
const file = this.folder + key + ".json";
|
|
57
|
+
const fsp = await this.promise;
|
|
58
|
+
await fsp.unlink(file).catch(noFileOk);
|
|
59
|
+
return file;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async *iterate(prefix = "") {
|
|
63
|
+
const fsp = await this.promise;
|
|
64
|
+
const all = await fsp.readdir(this.folder);
|
|
65
|
+
const keys = all
|
|
66
|
+
.filter((f) => f.startsWith(prefix) && f.endsWith(".json"))
|
|
67
|
+
.map((name) => name.slice(0, -".json".length));
|
|
68
|
+
for (const key of keys) {
|
|
69
|
+
const data = await this.get(key);
|
|
70
|
+
yield [key, data];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// async clear(prefix = "") {
|
|
75
|
+
// const data = await this.#read();
|
|
76
|
+
// for (let key in data) {
|
|
77
|
+
// if (key.startsWith(prefix)) {
|
|
78
|
+
// delete data[key];
|
|
79
|
+
// }
|
|
80
|
+
// }
|
|
81
|
+
// await this.#write(data);
|
|
82
|
+
// }
|
|
83
|
+
}
|
package/src/clients/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import cloudflare from "./cloudflare.js";
|
|
|
2
2
|
import cookie from "./cookie.js";
|
|
3
3
|
import etcd from "./etcd.js";
|
|
4
4
|
import file from "./file.js";
|
|
5
|
+
import folder from "./folder.js";
|
|
5
6
|
import forage from "./forage.js";
|
|
6
7
|
import level from "./level.js";
|
|
7
8
|
import memory from "./memory.js";
|
|
@@ -13,6 +14,7 @@ export default {
|
|
|
13
14
|
cookie,
|
|
14
15
|
etcd,
|
|
15
16
|
file,
|
|
17
|
+
folder,
|
|
16
18
|
forage,
|
|
17
19
|
level,
|
|
18
20
|
memory,
|