polystore 0.14.1 → 0.15.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 +4 -4
- package/readme.md +28 -5
- package/src/clients/api.js +71 -0
- package/src/clients/file.js +2 -1
- package/src/clients/folder.js +1 -1
- package/src/clients/index.js +2 -0
- package/src/server.js +75 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polystore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|
|
@@ -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
|
|
@@ -629,6 +630,25 @@ You don't need to `await` for the connect or similar, this will process it prope
|
|
|
629
630
|
</ul>
|
|
630
631
|
</details>
|
|
631
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
|
+
|
|
632
652
|
### File
|
|
633
653
|
|
|
634
654
|
Treat a JSON file in your filesystem as the source for the KV store:
|
|
@@ -643,7 +663,7 @@ console.log(await store.get("key1"));
|
|
|
643
663
|
// "Hello world"
|
|
644
664
|
```
|
|
645
665
|
|
|
646
|
-
> Note: an extension is needed, to
|
|
666
|
+
> Note: an extension is needed, to disambiguate with "folder"
|
|
647
667
|
|
|
648
668
|
You can also create multiple stores:
|
|
649
669
|
|
|
@@ -680,7 +700,7 @@ await fsp.writeFile(file, serialValue);
|
|
|
680
700
|
|
|
681
701
|
### Folder
|
|
682
702
|
|
|
683
|
-
Treat a single folder in your filesystem as the
|
|
703
|
+
Treat a single folder in your filesystem as the store, where each key is a file:
|
|
684
704
|
|
|
685
705
|
```js
|
|
686
706
|
import kv from "polystore";
|
|
@@ -688,21 +708,24 @@ import kv from "polystore";
|
|
|
688
708
|
const store = kv(new URL("file:///Users/me/project/data/"));
|
|
689
709
|
|
|
690
710
|
await store.set("key1", "Hello world", { expires: "1h" });
|
|
711
|
+
// Writes "./data/key1.json"
|
|
691
712
|
console.log(await store.get("key1"));
|
|
692
713
|
// "Hello world"
|
|
693
714
|
```
|
|
694
715
|
|
|
695
|
-
> Note: the ending slash `/` is needed, to
|
|
716
|
+
> Note: the ending slash `/` is needed, to disambiguate with "file"
|
|
696
717
|
|
|
697
718
|
You can also create multiple stores:
|
|
698
719
|
|
|
699
720
|
```js
|
|
700
|
-
// Paths need to be absolute, but you can use process.cwd() to make
|
|
701
|
-
// it relative to the current process
|
|
721
|
+
// Paths need to be absolute, but you can use `process.cwd()` to make
|
|
722
|
+
// it relative to the current process, or `import.meta.dirname`:
|
|
702
723
|
const store1 = kv(new URL(`file://${process.cwd()}/cache/`));
|
|
703
724
|
const store2 = kv(new URL(`file://${import.meta.dirname}/data/`));
|
|
704
725
|
```
|
|
705
726
|
|
|
727
|
+
The folder is created if it doesn't exist. When a key is deleted, the corresponding file is also deleted. The data is serialized as JSON, with a meta wrapper to store the expiration date.
|
|
728
|
+
|
|
706
729
|
<details>
|
|
707
730
|
<summary>Why use polystore with a folder?</summary>
|
|
708
731
|
<p>These benefits are for wrapping a folder with polystore:</p>
|
|
@@ -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
|
+
}
|
package/src/clients/file.js
CHANGED
|
@@ -29,7 +29,8 @@ export default class File {
|
|
|
29
29
|
// We want to make sure the file already exists, so attempt to
|
|
30
30
|
// create the folders and the file (but not OVERWRITE it, that's why the x flag)
|
|
31
31
|
// It fails if it already exists, hence the catch case
|
|
32
|
-
|
|
32
|
+
const folder = path.dirname(this.file);
|
|
33
|
+
await fsp.mkdir(folder, { recursive: true }).catch(() => {});
|
|
33
34
|
await fsp.writeFile(this.file, "{}", { flag: "wx" }).catch((err) => {
|
|
34
35
|
if (err.code !== "EEXIST") throw err;
|
|
35
36
|
});
|
package/src/clients/folder.js
CHANGED
|
@@ -32,7 +32,7 @@ export default class Folder {
|
|
|
32
32
|
|
|
33
33
|
// Make sure the folder already exists, so attempt to create it
|
|
34
34
|
// It fails if it already exists, hence the catch case
|
|
35
|
-
await fsp.mkdir(this.folder, { recursive: true }).catch((
|
|
35
|
+
await fsp.mkdir(this.folder, { recursive: true }).catch(() => {});
|
|
36
36
|
return fsp;
|
|
37
37
|
})();
|
|
38
38
|
}
|
package/src/clients/index.js
CHANGED
|
@@ -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 };
|