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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polystore",
3
- "version": "0.13.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 only manages the keys with the given prefix.
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) (?): 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.
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 isomorphic (Node.js, Bun 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:
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, or a combination of those.
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
- > The value cannot be more complex or non-serializable values like a `Date()`, `Infinity`, `undefined`, a `Symbol`, etc.
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, 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
+ 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 generated key is 24 AlphaNumeric characters (including upper and lower case) generated with random cryptography to make sure it's unguessable, high entropy and safe to use in most contexts like URLs, queries, etc. We use [`nanoid`](https://github.com/ai/nanoid/) with a custom dictionary, so you can check the entropy [in this dictionary](https://zelark.github.io/nano-id-cc/) by removing the "\_" and "-", and setting it to 24 characters.
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
- 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."
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
- > Note: please make sure to read the [`.set()`](#set) section for all the details, since `.set()` and `.add()` behave the same way except for the first argument.
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, optionally filtered by a prefix:
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(filter?: string);
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, optionally filtered by a **key** prefix:
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(filter?: string);
337
+ await store.values();
338
+ // ["valueA", "valueB", { hello: "world" }, ...]
324
339
  ```
325
340
 
326
- This is useful specially when you already have the id/key within the value as an object, then you can just get a list of all of them:
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.values("session:");
344
+ const sessions = await store.prefix("session:").values();
330
345
  // A list of all the sessions
331
346
 
332
- const companies = await store.values("company:");
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, optionally filtered by a **key** prefix:
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.entries(filter?: string);
378
+ const obj = await store.all(filter?: string);
379
+ // { keyA: "valueA", keyB: "valueB", keyC: { hello: "world" }, ... }
344
380
  ```
345
381
 
346
- It is in a format that you can easily build an object out of 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 sessionEntries = await store.entries("session:");
350
- const sessions = Object.fromEntries(sessionEntries);
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>Besides the other benefits already documented elsewhere, these are the ones specifically for wrapping Map() with polystore:</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
- 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:
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 more details!
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, 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).
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 entry of store.iterate()) {
707
- }
931
+ for await (const [key, value] of store) {}
708
932
  // client.iterate("hello:world:");
709
933
  ```
710
934
 
@@ -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 (typeof client === "string" && client.startsWith("file:")) return true;
6
- return client instanceof URL && client.protocol === "file:";
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
+ }
@@ -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,