polystore 0.15.7 → 0.15.8

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.15.7",
3
+ "version": "0.15.8",
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",
@@ -15,6 +15,7 @@
15
15
  "src/"
16
16
  ],
17
17
  "scripts": {
18
+ "analyze": "esbuild ./ --bundle --packages=external --format=esm --minify --outfile=index.min.js && gzip-size index.min.js && rm index.min.js",
18
19
  "lint": "check-dts test/index.types.ts",
19
20
  "start": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --coverage --detectOpenHandles",
20
21
  "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --ci --watchAll=false --detectOpenHandles",
@@ -0,0 +1,7 @@
1
+ export default class Client {
2
+ constructor(client) {
3
+ this.client = client;
4
+ }
5
+ encode = (val) => JSON.stringify(val, null, 2);
6
+ decode = (val) => (val ? JSON.parse(val) : null);
7
+ }
@@ -1,35 +1,28 @@
1
- const enc = encodeURIComponent; // Optimization of size
1
+ import Client from "./Client";
2
2
 
3
3
  // Handle an API endpoint with fetch()
4
- export default class Api {
4
+ export default class Api extends Client {
5
5
  // Indicate that the file handler DOES handle expirations
6
6
  EXPIRES = true;
7
7
 
8
8
  static test = (client) =>
9
9
  typeof client === "string" && /^https?:\/\//.test(client);
10
10
 
11
- constructor(client) {
12
- client = client.replace(/\/$/, "");
13
- this.client = async (path, method = "GET", body) => {
14
- const url = `${client}/${path.replace(/^\//, "")}`;
15
- const headers = { accept: "application/json" };
16
- if (body) headers["content-type"] = "application/json";
17
- const res = await fetch(url, { method, headers, body });
18
- return res.ok ? res.json() : null;
19
- };
20
- }
11
+ #api = async (key, opts = "", method = "GET", body) => {
12
+ const url = `${this.client.replace(/\/$/, "")}/${encodeURIComponent(key)}${opts}`;
13
+ const headers = { accept: "application/json" };
14
+ if (body) headers["content-type"] = "application/json";
15
+ const res = await fetch(url, { method, headers, body });
16
+ return res.ok ? res.json() : null;
17
+ };
21
18
 
22
- get = (key) => this.client(`/${enc(key)}`);
19
+ get = (key) => this.#api(key);
23
20
  set = (key, value, { expires } = {}) =>
24
- this.client(
25
- `/${enc(key)}?expires=${enc(expires || "")}`,
26
- "PUT",
27
- JSON.stringify(value),
28
- );
29
- del = (key) => this.client(`/${enc(key)}`, "DELETE");
21
+ this.#api(key, `?expires=${expires || ""}`, "PUT", this.encode(value));
22
+ del = (key) => this.#api(key, "", "DELETE");
30
23
 
31
24
  async *iterate(prefix = "") {
32
- const data = await this.client(`/?prefix=${enc(prefix)}`);
25
+ const data = await this.#api("", `?prefix=${encodeURIComponent(prefix)}`);
33
26
  for (let [key, value] of Object.entries(data || {})) {
34
27
  yield [prefix + key, value];
35
28
  }
@@ -1,5 +1,7 @@
1
+ import Client from "./Client";
2
+
1
3
  // Use Cloudflare's KV store
2
- export default class Cloudflare {
4
+ export default class Cloudflare extends Client {
3
5
  // Indicate that the file handler does NOT handle expirations
4
6
  EXPIRES = true;
5
7
 
@@ -8,21 +10,13 @@ export default class Cloudflare {
8
10
  client?.constructor?.name === "KvNamespace" ||
9
11
  client?.constructor?.name === "EdgeKVNamespace";
10
12
 
11
- constructor(client) {
12
- this.client = client;
13
- }
14
-
15
- get = async (key) => {
16
- const text = await this.client.get(key);
17
- return text ? JSON.parse(text) : null;
18
- };
19
-
13
+ get = async (key) => this.decode(await this.client.get(key));
20
14
  set = (key, value, { expires } = {}) => {
21
15
  const expirationTtl = expires ? Math.round(expires) : undefined;
22
16
  if (expirationTtl && expirationTtl < 60) {
23
17
  throw new Error("Cloudflare's min expiration is '60s'");
24
18
  }
25
- return this.client.put(key, JSON.stringify(value), { expirationTtl });
19
+ return this.client.put(key, this.encode(value), { expirationTtl });
26
20
  };
27
21
 
28
22
  del = (key) => this.client.delete(key);
@@ -1,5 +1,7 @@
1
+ import Client from "./Client";
2
+
1
3
  // A client that uses a single file (JSON) as a store
2
- export default class Cookie {
4
+ export default class Cookie extends Client {
3
5
  // Indicate if this client handles expirations (true = it does)
4
6
  EXPIRES = true;
5
7
 
@@ -34,7 +36,7 @@ export default class Cookie {
34
36
  expireStr = `; expires=${time}`;
35
37
  }
36
38
 
37
- const value = encodeURIComponent(JSON.stringify(data || ""));
39
+ const value = encodeURIComponent(this.encode(data || ""));
38
40
  document.cookie = encodeURIComponent(key) + "=" + value + expireStr;
39
41
  };
40
42
 
@@ -1,14 +1,12 @@
1
+ import Client from "./Client";
2
+
1
3
  // Use a redis client to back up the store
2
- export default class Etcd {
4
+ export default class Etcd extends Client {
3
5
  // Check if this is the right class for the given client
4
6
  static test = (client) => client?.constructor?.name === "Etcd3";
5
7
 
6
- constructor(client) {
7
- this.client = client;
8
- }
9
-
10
8
  get = (key) => this.client.get(key).json();
11
- set = (key, value) => this.client.put(key).value(JSON.stringify(value));
9
+ set = (key, value) => this.client.put(key).value(this.encode(value));
12
10
  del = (key) => this.client.delete().key(key).exec();
13
11
 
14
12
  async *iterate(prefix = "") {
@@ -1,5 +1,7 @@
1
+ import Client from "./Client";
2
+
1
3
  // A client that uses a single file (JSON) as a store
2
- export default class File {
4
+ export default class File extends Client {
3
5
  // Check if this is the right class for the given client
4
6
  static test = (client) => {
5
7
  if (client instanceof URL) client = client.href;
@@ -10,35 +12,25 @@ export default class File {
10
12
  );
11
13
  };
12
14
 
13
- constructor(file) {
14
- if (file instanceof URL) file = file.href;
15
- this.file = file.replace(/^file:\/\//, "");
16
-
17
- // Run this once on launch; import the FS module and reset the file
18
- this.promise = (async () => {
19
- // We want to make sure the file already exists, so attempt to
20
- // create the folders and the file (but not OVERWRITE it, that's why the x flag)
21
- // It fails if it already exists, hence the catch case
22
- const fsp = await import("node:fs/promises");
23
- const folder = this.file.split("/").slice(0, -1).join("/");
24
- await fsp.mkdir(folder, { recursive: true }).catch(() => {});
25
- await fsp.writeFile(this.file, "{}", { flag: "wx" }).catch((err) => {
26
- if (err.code !== "EEXIST") throw err;
27
- });
28
- return fsp;
29
- })();
30
- }
15
+ // We want to make sure the file already exists, so attempt to
16
+ // create the folders and the file (but not OVERWRITE it, that's why the x flag)
17
+ // It fails if it already exists, hence the catch case
18
+ #promise = (async () => {
19
+ this.fsp = await import("node:fs/promises");
20
+ this.file = (this.client?.href || this.client).replace(/^file:\/\//, "");
21
+ const folder = this.file.split("/").slice(0, -1).join("/");
22
+ await this.fsp.mkdir(folder, { recursive: true }).catch(() => {});
23
+ await this.fsp.writeFile(this.file, "{}", { flag: "wx" }).catch(() => {});
24
+ })();
31
25
 
32
26
  // Internal
33
27
  #read = async () => {
34
- const fsp = await this.promise;
35
- const text = await fsp.readFile(this.file, "utf8");
28
+ const text = await this.fsp.readFile(this.file, "utf8");
36
29
  return text ? JSON.parse(text) : {};
37
30
  };
38
31
 
39
32
  #write = async (data) => {
40
- const fsp = await this.promise;
41
- return fsp.writeFile(this.file, JSON.stringify(data, null, 2));
33
+ return this.fsp.writeFile(this.file, this.encode(data));
42
34
  };
43
35
 
44
36
  get = async (key) => {
@@ -1,7 +1,12 @@
1
- const json = (data) => JSON.stringify(data, null, 2);
1
+ import Client from "./Client";
2
+
3
+ const noFileOk = (error) => {
4
+ if (error.code === "ENOENT") return null;
5
+ throw error;
6
+ };
2
7
 
3
8
  // A client that uses a single file (JSON) as a store
4
- export default class Folder {
9
+ export default class Folder extends Client {
5
10
  // Check if this is the right class for the given client
6
11
  static test = (client) => {
7
12
  if (client instanceof URL) client = client.href;
@@ -12,47 +17,31 @@ export default class Folder {
12
17
  );
13
18
  };
14
19
 
15
- constructor(folder) {
16
- if (folder instanceof URL) folder = folder.href;
17
- folder = folder.replace(/^file:\/\//, "");
18
-
19
- // Run this once on launch; import the FS module and reset the file
20
- const prom = import("node:fs/promises").then((fsp) => {
21
- // Make sure the folder already exists, so attempt to create it
22
- // It fails if it already exists, hence the catch case
23
- return fsp.mkdir(folder, { recursive: true }).then(
24
- () => fsp,
25
- () => {},
26
- );
27
- });
20
+ // Make sure the folder already exists, so attempt to create it
21
+ // It fails if it already exists, hence the catch case
22
+ #promise = (async () => {
23
+ this.fsp = await import("node:fs/promises");
24
+ this.folder = (this.client?.href || this.client).replace(/^file:\/\//, "");
25
+ await this.fsp.mkdir(this.folder, { recursive: true }).catch(() => {});
26
+ })();
28
27
 
29
- const getter = (_, name) => {
30
- return async (key, ...props) => {
31
- const file = folder + (key ? key + ".json" : "");
32
- const method = (await prom)[name];
33
- return method(file, ...props).catch((error) => {
34
- if (error.code === "ENOENT") return null;
35
- throw error;
36
- });
37
- };
38
- };
28
+ file = (key) => this.folder + key + ".json";
39
29
 
40
- this.fs = new Proxy({}, { get: getter });
41
- }
42
-
43
- get = async (key) => {
44
- const text = await this.fs.readFile(key, "utf8");
45
- return text ? JSON.parse(text) : null;
30
+ get = (key) => {
31
+ return this.fsp
32
+ .readFile(this.file(key), "utf8")
33
+ .then(this.decode, noFileOk);
34
+ };
35
+ set = (key, value) => {
36
+ return this.fsp.writeFile(this.file(key), this.encode(value), "utf8");
46
37
  };
47
- set = (key, value) => this.fs.writeFile(key, json(value), "utf8");
48
- del = (key) => this.fs.unlink(key);
38
+ del = (key) => this.fsp.unlink(this.file(key)).catch(noFileOk);
49
39
 
50
40
  async *iterate(prefix = "") {
51
- const all = await this.fs.readdir();
52
- const keys = all
53
- .filter((f) => f.startsWith(prefix) && f.endsWith(".json"))
54
- .map((name) => name.slice(0, -".json".length));
55
- for (const key of keys) {
41
+ const all = await this.fsp.readdir(this.folder);
42
+ const keys = all.filter((f) => f.startsWith(prefix) && f.endsWith(".json"));
43
+ for (const name of keys) {
44
+ const key = name.slice(0, -".json".length);
56
45
  const data = await this.get(key);
57
46
  yield [key, data];
58
47
  }
@@ -1,13 +1,11 @@
1
+ import Client from "./Client";
2
+
1
3
  // Use localForage for managing the KV
2
- export default class Forage {
4
+ export default class Forage extends Client {
3
5
  // Check if this is the right class for the given client
4
6
  static test = (client) =>
5
7
  client?.defineDriver && client?.dropInstance && client?.INDEXEDDB;
6
8
 
7
- constructor(client) {
8
- this.client = client;
9
- }
10
-
11
9
  get = (key) => this.client.getItem(key);
12
10
  set = (key, value) => this.client.setItem(key, value);
13
11
  del = (key) => this.client.removeItem(key);
@@ -1,3 +1,5 @@
1
+ import Client from "./Client";
2
+
1
3
  const valueEncoding = "json";
2
4
  const notFound = (error) => {
3
5
  if (error?.code === "LEVEL_NOT_FOUND") return null;
@@ -5,14 +7,10 @@ const notFound = (error) => {
5
7
  };
6
8
 
7
9
  // Level KV DB - https://github.com/Level/level
8
- export default class Level {
10
+ export default class Level extends Client {
9
11
  // Check if this is the right class for the given client
10
12
  static test = (client) => client?.constructor?.name === "ClassicLevel";
11
13
 
12
- constructor(client) {
13
- this.client = client;
14
- }
15
-
16
14
  get = (key) => this.client.get(key, { valueEncoding }).catch(notFound);
17
15
  set = (key, value) => this.client.put(key, value, { valueEncoding });
18
16
  del = (key) => this.client.del(key);
@@ -1,12 +1,10 @@
1
+ import Client from "./Client";
2
+
1
3
  // Use a Map() as an in-memory client
2
- export default class Memory {
4
+ export default class Memory extends Client {
3
5
  // Check if this is the right class for the given client
4
6
  static test = (client) => client instanceof Map;
5
7
 
6
- constructor(client) {
7
- this.client = client;
8
- }
9
-
10
8
  get = (key) => this.client.get(key) ?? null;
11
9
  set = (key, data) => this.client.set(key, data);
12
10
  del = (key) => this.client.delete(key);
@@ -1,25 +1,18 @@
1
+ import Client from "./Client";
2
+
1
3
  // Use a redis client to back up the store
2
- export default class Redis {
4
+ export default class Redis extends Client {
3
5
  // Indicate if this client handles expirations (true = it does)
4
6
  EXPIRES = true;
5
7
 
6
8
  // Check if this is the right class for the given client
7
9
  static test = (client) => client && client.pSubscribe && client.sSubscribe;
8
10
 
9
- constructor(client) {
10
- this.client = client;
11
- }
12
-
13
- get = async (key) => {
14
- const text = await this.client.get(key);
15
- return text ? JSON.parse(text) : null;
16
- };
17
-
11
+ get = async (key) => this.decode(await this.client.get(key));
18
12
  set = async (key, value, { expires } = {}) => {
19
13
  const EX = expires ? Math.round(expires) : undefined;
20
- return this.client.set(key, JSON.stringify(value), { EX });
14
+ return this.client.set(key, this.encode(value), { EX });
21
15
  };
22
-
23
16
  del = (key) => this.client.del(key);
24
17
 
25
18
  has = async (key) => Boolean(await this.client.exists(key));
@@ -1,29 +1,23 @@
1
+ import Client from "./Client";
2
+
1
3
  // A client that uses a single file (JSON) as a store
2
- export default class WebStorage {
4
+ export default class WebStorage extends Client {
3
5
  // Check if this is the right class for the given client
4
6
  static test(client) {
5
7
  if (typeof Storage === "undefined") return false;
6
8
  return client instanceof Storage;
7
9
  }
8
10
 
9
- constructor(client) {
10
- this.client = client;
11
- }
12
-
13
11
  // Item methods
14
- get = (key) => {
15
- const text = this.client[key];
16
- return text ? JSON.parse(text) : null;
17
- };
18
- set = (key, data) => this.client.setItem(key, JSON.stringify(data));
12
+ get = (key) => this.decode(this.client[key]);
13
+ set = (key, data) => this.client.setItem(key, this.encode(data));
19
14
  del = (key) => this.client.removeItem(key);
20
15
 
21
16
  *iterate(prefix = "") {
22
17
  for (const key of Object.keys(this.client)) {
23
18
  if (!key.startsWith(prefix)) continue;
24
19
  const value = this.get(key);
25
- if (!value) continue;
26
- yield [key, value];
20
+ if (value) yield [key, value];
27
21
  }
28
22
  }
29
23
 
package/src/index.js CHANGED
@@ -9,6 +9,7 @@ class Store {
9
9
  this.client = this.#find(client);
10
10
  this.#validate(this.client);
11
11
  this.promise = null;
12
+ await this.client.promise;
12
13
  return client;
13
14
  });
14
15
  }
@@ -47,7 +48,7 @@ class Store {
47
48
  for (let method of ["has", "keys", "values"]) {
48
49
  if (client[method]) {
49
50
  throw new Error(
50
- `You can only define client.${method}() when the client manages the expiration; otherwise please do NOT define .${method}() and let us manage it`,
51
+ `You can only define client.${method}() when the client manages the expiration.`,
51
52
  );
52
53
  }
53
54
  }
@@ -91,8 +92,7 @@ class Store {
91
92
  }
92
93
 
93
94
  const key = createId();
94
- await this.set(key, value, { expires });
95
- return key; // The plain one without the prefix
95
+ return this.set(key, value, { expires });
96
96
  }
97
97
 
98
98
  async set(key, value, options = {}) {
@@ -102,8 +102,7 @@ class Store {
102
102
 
103
103
  // Quick delete
104
104
  if (value === null || (typeof expires === "number" && expires <= 0)) {
105
- await this.del(id);
106
- return key;
105
+ return this.del(id);
107
106
  }
108
107
 
109
108
  // The client manages the expiration, so let it manage it
@@ -143,8 +142,7 @@ class Store {
143
142
  return this.client.has(id);
144
143
  }
145
144
 
146
- const value = await this.get(key);
147
- return value !== null;
145
+ return (await this.get(key)) !== null;
148
146
  }
149
147
 
150
148
  async del(key) {
@@ -262,7 +260,7 @@ class Store {
262
260
 
263
261
  prefix(prefix = "") {
264
262
  const store = new Store(
265
- Promise.resolve(this.promise).then((client) => client || this.client),
263
+ Promise.resolve(this.promise).then(() => this.client),
266
264
  );
267
265
  store.PREFIX = this.PREFIX + prefix;
268
266
  return store;