polystore 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polystore",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
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",
@@ -14,11 +14,11 @@
14
14
  "src/"
15
15
  ],
16
16
  "scripts": {
17
- "size": "echo $(gzip -c src/index.js | wc -c) bytes",
18
17
  "lint": "check-dts test/index.types.ts",
19
18
  "start": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --coverage --detectOpenHandles",
20
19
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --ci --watchAll=false --detectOpenHandles",
21
- "db": "etcd"
20
+ "db": "etcd",
21
+ "server": "bun ./src/server.js"
22
22
  },
23
23
  "keywords": [
24
24
  "kv",
@@ -29,10 +29,10 @@
29
29
  "value"
30
30
  ],
31
31
  "license": "MIT",
32
- "dependencies": {},
33
32
  "devDependencies": {
34
33
  "@deno/kv": "^0.8.1",
35
34
  "check-dts": "^0.8.0",
35
+ "cross-fetch": "^4.0.0",
36
36
  "dotenv": "^16.3.1",
37
37
  "edge-mock": "^0.0.15",
38
38
  "etcd3": "^1.1.2",
package/readme.md CHANGED
@@ -35,6 +35,7 @@ Available clients for the KV store:
35
35
  - [**Session Storage** `sessionStorage`](#session-storage) (fe): persist the data in the browser's sessionStorage.
36
36
  - [**Cookies** `"cookie"`](#cookies) (fe): persist the data using cookies
37
37
  - [**LocalForage** `localForage`](#local-forage) (fe): persist the data on IndexedDB
38
+ - [**Fetch API** `"https://..."`](#fetch-api) (fe+be): call an API to save/retrieve the data
38
39
  - [**File** `new URL('file:///...')`](#file) (be): store the data in a single JSON file in your FS
39
40
  - [**Folder** `new URL('file:///...')`](#folder) (be): store each key in a folder as json files
40
41
  - [**Redis Client** `redisClient`](#redis-client) (be): use the Redis instance that you connect to
@@ -462,13 +463,23 @@ console.log(await store.get("key1"));
462
463
 
463
464
  <details>
464
465
  <summary>Why use polystore with <code>new Map()</code>?</summary>
465
- <p>Besides the other benefits already documented elsewhere, these are the ones specifically for wrapping Map() with polystore:</p>
466
+ <p>These benefits are for wrapping Map() with polystore:</p>
466
467
  <ul>
467
468
  <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>
468
469
  <li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
469
470
  </ul>
470
471
  </details>
471
472
 
473
+ ```js
474
+ // GOOD - with polystore
475
+ await store.set("key1", { name: "Francisco" }, { expires: "2days" });
476
+
477
+ // COMPLEX - With sessionStorage
478
+ const data = new Map();
479
+ data.set("key1", { name: "Francisco" });
480
+ // Expiration not supported
481
+ ```
482
+
472
483
  ### Local Storage
473
484
 
474
485
  The traditional localStorage that we all know and love, this time with a unified API, and promises:
@@ -485,6 +496,26 @@ console.log(await store.get("key1"));
485
496
 
486
497
  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)!
487
498
 
499
+ <details>
500
+ <summary>Why use polystore with <code>localStorage</code>?</summary>
501
+ <p>These benefits are for wrapping localStorage with polystore:</p>
502
+ <ul>
503
+ <li><strong>Data structures</strong>: with Polystore you can pass more complex data structures and we'll handle the serialization/deserialization.</li>
504
+ <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>
505
+ <li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
506
+ </ul>
507
+ </details>
508
+
509
+ ```js
510
+ // GOOD - with polystore
511
+ await store.set("key1", { name: "Francisco" }, { expires: "2days" });
512
+
513
+ // COMPLEX - With localStorage
514
+ const serialValue = JSON.stringify({ name: "Francisco" });
515
+ localStorage.set("key1", serialValue);
516
+ // Expiration not supported
517
+ ```
518
+
488
519
  ### Session Storage
489
520
 
490
521
  Same as localStorage, but now for the session only:
@@ -499,6 +530,26 @@ console.log(await store.get("key1"));
499
530
  // "Hello world"
500
531
  ```
501
532
 
533
+ <details>
534
+ <summary>Why use polystore with <code>sessionStorage</code>?</summary>
535
+ <p>These benefits are for wrapping sessionStorage with polystore:</p>
536
+ <ul>
537
+ <li><strong>Data structures</strong>: with Polystore you can pass more complex data structures and we'll handle the serialization/deserialization.</li>
538
+ <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>
539
+ <li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
540
+ </ul>
541
+ </details>
542
+
543
+ ```js
544
+ // GOOD - with polystore
545
+ await store.set("key1", { name: "Francisco" }, { expires: "2days" });
546
+
547
+ // COMPLEX - With sessionStorage
548
+ const serialValue = JSON.stringify({ name: "Francisco" });
549
+ sessionStorage.set("key1", serialValue);
550
+ // Expiration not supported
551
+ ```
552
+
502
553
  ### Cookies
503
554
 
504
555
  Supports native browser cookies, including setting the expire time:
@@ -517,6 +568,16 @@ It is fairly limited for how powerful cookies are, but in exchange it has the sa
517
568
 
518
569
  > 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.
519
570
 
571
+ <details>
572
+ <summary>Why use polystore with <code>cookies</code>?</summary>
573
+ <p>These benefits are for wrapping cookies with polystore:</p>
574
+ <ul>
575
+ <li><strong>Data structures</strong>: with Polystore you can pass more complex data structures and we'll handle the serialization/deserialization.</li>
576
+ <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>
577
+ <li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
578
+ </ul>
579
+ </details>
580
+
520
581
  ### Local Forage
521
582
 
522
583
  Supports localForage (with any driver it uses) so that you have a unified API. It also _adds_ the `expires` option to the setters!
@@ -532,6 +593,15 @@ console.log(await store.get("key1"));
532
593
  // "Hello world"
533
594
  ```
534
595
 
596
+ <details>
597
+ <summary>Why use polystore with <code>localStorage</code>?</summary>
598
+ <p>These benefits are for wrapping localStorage with polystore:</p>
599
+ <ul>
600
+ <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>
601
+ <li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
602
+ </ul>
603
+ </details>
604
+
535
605
  ### Redis Client
536
606
 
537
607
  Supports the official Node Redis Client. You can pass either the client or the promise:
@@ -551,6 +621,34 @@ You don't need to `await` for the connect or similar, this will process it prope
551
621
 
552
622
  > 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.
553
623
 
624
+ <details>
625
+ <summary>Why use polystore with <code>Redis</code>?</summary>
626
+ <p>These benefits are for wrapping Redis with polystore:</p>
627
+ <ul>
628
+ <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>
629
+ <li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
630
+ </ul>
631
+ </details>
632
+
633
+ ### Fetch API
634
+
635
+ Calls an API to get/put the data:
636
+
637
+ ```js
638
+ import kv from "polystore";
639
+
640
+ const store = kv("https://kv.example.com/");
641
+
642
+ await store.set("key1", "Hello world", { expires: "1h" });
643
+ console.log(await store.get("key1"));
644
+ // "Hello world"
645
+ ```
646
+
647
+ > Note: the API 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.
648
+
649
+ > Note: see the reference implementation in src/server.js
650
+
651
+
554
652
  ### File
555
653
 
556
654
  Treat a JSON file in your filesystem as the source for the KV store:
@@ -565,7 +663,7 @@ console.log(await store.get("key1"));
565
663
  // "Hello world"
566
664
  ```
567
665
 
568
- > Note: an extension is needed, to desambiguate with "folder"
666
+ > Note: an extension is needed, to disambiguate with "folder"
569
667
 
570
668
  You can also create multiple stores:
571
669
 
@@ -576,6 +674,30 @@ const store1 = kv(new URL(`file://${process.cwd()}/cache.json`));
576
674
  const store2 = kv(new URL(`file://${import.meta.dirname}/data.json`));
577
675
  ```
578
676
 
677
+ <details>
678
+ <summary>Why use polystore with a file?</summary>
679
+ <p>These benefits are for wrapping a file with polystore:</p>
680
+ <ul>
681
+ <li><strong>Data structures</strong>: with Polystore you can pass more complex data structures and we'll handle the serialization/deserialization.</li>
682
+ <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>
683
+ <li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
684
+ </ul>
685
+ </details>
686
+
687
+ ```js
688
+ // GOOD - with polystore
689
+ await store.set("key1", { name: "Francisco" }, { expires: "2days" });
690
+
691
+ // COMPLEX - With native file managing
692
+ const file = './data/users.json';
693
+ const str = await fsp.readFile(file, "utf-8");
694
+ const data = JSON.parse(str);
695
+ data["key1"] = { name: "Francisco" };
696
+ const serialValue = JSON.stringify(data);
697
+ await fsp.writeFile(file, serialValue);
698
+ // Expiration not supported (and error handling not shown)
699
+ ```
700
+
579
701
  ### Folder
580
702
 
581
703
  Treat a single folder in your filesystem as the source for the KV store, with each key being within a file:
@@ -590,7 +712,7 @@ console.log(await store.get("key1"));
590
712
  // "Hello world"
591
713
  ```
592
714
 
593
- > Note: the ending slash `/` is needed, to desambiguate with "file"
715
+ > Note: the ending slash `/` is needed, to disambiguate with "file"
594
716
 
595
717
  You can also create multiple stores:
596
718
 
@@ -601,6 +723,27 @@ const store1 = kv(new URL(`file://${process.cwd()}/cache/`));
601
723
  const store2 = kv(new URL(`file://${import.meta.dirname}/data/`));
602
724
  ```
603
725
 
726
+ <details>
727
+ <summary>Why use polystore with a folder?</summary>
728
+ <p>These benefits are for wrapping a folder with polystore:</p>
729
+ <ul>
730
+ <li><strong>Data structures</strong>: with Polystore you can pass more complex data structures and we'll handle the serialization/deserialization.</li>
731
+ <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>
732
+ <li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
733
+ </ul>
734
+ </details>
735
+
736
+ ```js
737
+ // GOOD - with polystore
738
+ await store.set("key1", { name: "Francisco" }, { expires: "2days" });
739
+
740
+ // COMPLEX - With native folder
741
+ const file = './data/user/key1.json';
742
+ const serialValue = JSON.stringify({ name: "Francisco" });
743
+ await fsp.writeFile(file, serialValue);
744
+ // Expiration not supported (and error handling not shown)
745
+ ```
746
+
604
747
  ### Cloudflare KV
605
748
 
606
749
  Supports the official Cloudflare's KV stores. Follow [the official guide](https://developers.cloudflare.com/kv/get-started/), then load it like this:
@@ -621,7 +764,15 @@ export default {
621
764
  };
622
765
  ```
623
766
 
624
- 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:
767
+ <details>
768
+ <summary>Why use polystore with Cloudflare's KV?</summary>
769
+ <p>These benefits are for wrapping Cloudflare's KV with polystore:</p>
770
+ <ul>
771
+ <li><strong>Data structures</strong>: with Polystore you can pass more complex data structures and we'll handle the serialization/deserialization.</li>
772
+ <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>
773
+ <li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
774
+ </ul>
775
+ </details>
625
776
 
626
777
  ```js
627
778
  // GOOD - with polystore
@@ -650,7 +801,13 @@ console.log(await store.get("key1"));
650
801
  // "Hello world"
651
802
  ```
652
803
 
653
- Why use polystore? The main reason is that we add expiration on top of Level, which is not supported by Level:
804
+ <details>
805
+ <summary>Why use polystore with Level?</summary>
806
+ <p>These benefits are for wrapping Level with polystore:</p>
807
+ <ul>
808
+ <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>
809
+ </ul>
810
+ </details>
654
811
 
655
812
  ```js
656
813
  // GOOD - with polystore
@@ -675,6 +832,15 @@ console.log(await store.get("key1"));
675
832
  // "Hello world"
676
833
  ```
677
834
 
835
+ <details>
836
+ <summary>Why use polystore with Etcd?</summary>
837
+ <p>These benefits are for wrapping Etcd with polystore:</p>
838
+ <ul>
839
+ <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>
840
+ <li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
841
+ </ul>
842
+ </details>
843
+
678
844
  ### Custom store
679
845
 
680
846
  Please see the [creating a store](#creating-a-store) section for all the details!
@@ -782,8 +948,7 @@ const value = await store.get("a");
782
948
  // client.get("hello:world:a");
783
949
 
784
950
  // User calls this, then the client is called with that:
785
- for await (const entry of store.iterate()) {
786
- }
951
+ for await (const [key, value] of store) {}
787
952
  // client.iterate("hello:world:");
788
953
  ```
789
954
 
@@ -0,0 +1,71 @@
1
+ // Use fetch()
2
+ export default class Api {
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
+ typeof client === "string" &&
10
+ (client.startsWith("https://") || client.startsWith("http://"))
11
+ );
12
+ }
13
+
14
+ constructor(client) {
15
+ client = client.replace(/\/$/, "") + "/";
16
+ this.client = async (path, opts = {}) => {
17
+ const query = Object.entries(opts.query || {})
18
+ .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
19
+ .join("&");
20
+ let url = client + path.replace(/^\//, "") + "?" + query;
21
+ opts.headers = opts.headers || {};
22
+ opts.headers.accept = "application/json";
23
+ if (opts.body) opts.headers["content-type"] = "application/json";
24
+ const res = await fetch(url, opts);
25
+ if (!res.ok) return null;
26
+ if (!res.headers["content-type"] !== "application/json") {
27
+ console.warn("Not a JSON API");
28
+ }
29
+ return res.json();
30
+ };
31
+ }
32
+
33
+ async get(key) {
34
+ return await this.client(`/${key}`);
35
+ }
36
+
37
+ async set(key, value, { expires } = {}) {
38
+ return await this.client(`/${encodeURIComponent(key)}`, {
39
+ query: { expires },
40
+ method: "put",
41
+ body: JSON.stringify(value),
42
+ });
43
+ }
44
+
45
+ async del(key) {
46
+ return await this.client(`/${encodeURIComponent(key)}`, {
47
+ method: "delete",
48
+ });
49
+ }
50
+
51
+ // Since we have pagination, we don't want to get all of the
52
+ // keys at once if we can avoid it
53
+ async *iterate(prefix = "") {
54
+ const data = await this.client("/", { query: { prefix } });
55
+ if (!data) return [];
56
+ for (let [key, value] of Object.entries(data)) {
57
+ yield [prefix + key, value];
58
+ }
59
+ }
60
+
61
+ async keys(prefix = "") {
62
+ const data = await this.client(`/`, { query: { prefix } });
63
+ if (!data) return [];
64
+ return Object.keys(data).map((k) => prefix + k);
65
+ }
66
+
67
+ async clear(prefix = "") {
68
+ const list = await this.keys(prefix);
69
+ return Promise.all(list.map((k) => this.del(k)));
70
+ }
71
+ }
@@ -23,7 +23,7 @@ export default class Folder {
23
23
  constructor(folder) {
24
24
  this.folder =
25
25
  typeof folder === "string"
26
- ? folder.slice("folder://".length).replace(/\/$/, "") + "/"
26
+ ? folder.slice("file://".length).replace(/\/$/, "") + "/"
27
27
  : folder.pathname.replace(/\/$/, "") + "/";
28
28
 
29
29
  // Run this once on launch; import the FS module and reset the file
@@ -48,7 +48,7 @@ export default class Folder {
48
48
  async set(key, value) {
49
49
  const fsp = await this.promise;
50
50
  const file = this.folder + key + ".json";
51
- await fsp.writeFile(file, JSON.stringify(value), "utf8");
51
+ await fsp.writeFile(file, JSON.stringify(value, null, 2), "utf8");
52
52
  return file;
53
53
  }
54
54
 
@@ -61,15 +61,10 @@ export default class Folder {
61
61
 
62
62
  async *iterate(prefix = "") {
63
63
  const fsp = await this.promise;
64
- const all = await fsp.readdir(this.folder, { withFileTypes: true });
65
- const files = all.filter((f) => !f.isDirectory());
66
- const keys = files
67
- .map((file) =>
68
- (file.path.replace(/\/$/, "") + "/" + file.name)
69
- .replace(this.folder, "")
70
- .replace(".json", ""),
71
- )
72
- .filter((k) => k.startsWith(prefix));
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));
73
68
  for (const key of keys) {
74
69
  const data = await this.get(key);
75
70
  yield [key, data];
@@ -1,3 +1,4 @@
1
+ import api from "./api.js";
1
2
  import cloudflare from "./cloudflare.js";
2
3
  import cookie from "./cookie.js";
3
4
  import etcd from "./etcd.js";
@@ -10,6 +11,7 @@ import redis from "./redis.js";
10
11
  import storage from "./storage.js";
11
12
 
12
13
  export default {
14
+ api,
13
15
  cloudflare,
14
16
  cookie,
15
17
  etcd,
package/src/server.js ADDED
@@ -0,0 +1,75 @@
1
+ // This is an example server implementation of the HTTP library!
2
+ import http from "node:http";
3
+ import kv from "./index.js";
4
+ import { parse } from "./utils.js";
5
+
6
+ // Modify this to use any sub-store as desired. It's nice
7
+ // to use polystore itself for the polystore server library!'
8
+ const store = kv(new Map());
9
+
10
+ // Some reply helpers
11
+ const notFound = () => new Response(null, { status: 404 });
12
+ const sendJson = (data, status = 200) => {
13
+ const body = JSON.stringify(data);
14
+ const headers = { "content-type": "application/json" };
15
+ return new Response(body, { status, headers });
16
+ };
17
+
18
+ async function fetch({ method, url, body }) {
19
+ method = method.toLowerCase();
20
+ url = new URL(url);
21
+ let [, id] = url.pathname.split("/");
22
+ id = decodeURIComponent(id);
23
+ const expires = Number(url.searchParams.get("expires")) || null;
24
+ const prefix = url.searchParams.get("prefix") || null;
25
+
26
+ let local = store;
27
+ if (prefix) local = store.prefix(prefix);
28
+
29
+ if (method === "get") {
30
+ if (id === "ping") return new Response(null, { status: 200 });
31
+ if (!id) return sendJson(await local.all());
32
+ const data = await local.get(id);
33
+ if (data === null) return notFound();
34
+ return sendJson(data);
35
+ }
36
+
37
+ if (method === "put") {
38
+ if (!id) return notFound();
39
+ const data = await new Response(body).json();
40
+ if (!data) return notFound();
41
+ await local.set(id, data, { expires });
42
+ return sendJson(id);
43
+ }
44
+
45
+ if (method === "delete" && id) {
46
+ await local.del(id);
47
+ return sendJson(id);
48
+ }
49
+
50
+ return notFound();
51
+ }
52
+
53
+ // http or express server-like handler:
54
+ async function server(req, res) {
55
+ const url = new URL(req.url, "http://localhost:3000/").href;
56
+ const reply = await fetch({ ...req, url });
57
+ res.writeHead(reply.status, null, reply.headers || {});
58
+ if (reply.body) res.write(reply.body);
59
+ res.end();
60
+ }
61
+
62
+ function start(port = 3000) {
63
+ return new Promise((resolve, reject) => {
64
+ const server = http.createServer(server);
65
+ server.on("clientError", (error, socket) => {
66
+ reject(error);
67
+ socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
68
+ });
69
+ server.listen(port, resolve);
70
+ return () => server.close();
71
+ });
72
+ }
73
+
74
+ export { fetch, server, start };
75
+ export default { fetch, server, start };