polystore 0.12.1 → 0.14.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.12.1",
3
+ "version": "0.14.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",
@@ -17,7 +17,8 @@
17
17
  "size": "echo $(gzip -c src/index.js | wc -c) bytes",
18
18
  "lint": "check-dts test/index.types.ts",
19
19
  "start": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch --coverage --detectOpenHandles",
20
- "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --ci --watchAll=false --detectOpenHandles"
20
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --ci --watchAll=false --detectOpenHandles",
21
+ "db": "etcd"
21
22
  },
22
23
  "keywords": [
23
24
  "kv",
package/readme.md CHANGED
@@ -1,4 +1,4 @@
1
- # Polystore [![npm install polystore](https://img.shields.io/badge/npm%20install-polystore-blue.svg)](https://www.npmjs.com/package/polystore) [![test badge](https://github.com/franciscop/polystore/workflows/tests/badge.svg "test badge")](https://github.com/franciscop/polystore/blob/master/.github/workflows/tests.yml) [![gzip size](https://badgen.net/bundlephobia/minzip/polystore?label=gzip&color=green)](https://github.com/franciscop/polystore/blob/master/src/index.js)
1
+ # Polystore [![npm install polystore](https://img.shields.io/badge/npm%20install-polystore-blue.svg)](https://www.npmjs.com/package/polystore) [![test badge](https://github.com/franciscop/polystore/workflows/tests/badge.svg "test badge")](https://github.com/franciscop/polystore/blob/master/.github/workflows/tests.yml) [![gzip size](https://badgen.net/bundlephobia/minzip/polystore?label=gzip&color=green)](https://bundlephobia.com/package/polystore)
2
2
 
3
3
  A key-value library to unify the API of [many clients](#clients), like localStorage, Redis, FileSystem, etc:
4
4
 
@@ -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:
311
317
 
312
318
  ```js
313
- await store.keys(filter?: string);
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!):
324
+
325
+ ```js
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,24 +352,48 @@ 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!):
341
365
 
342
366
  ```js
343
- await store.entries(filter?: string);
367
+ const sessionEntries = await store.prefix('session:').entries();
368
+ // [["keyA", "valueA"], ["keyB", "valueB"]]
344
369
  ```
345
370
 
346
- It is in a format that you can easily build an object out of it:
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:
347
376
 
348
377
  ```js
349
- const sessionEntries = await store.entries("session:");
350
- const sessions = Object.fromEntries(sessionEntries);
378
+ const obj = await store.all(filter?: string);
379
+ // { keyA: "valueA", keyB: "valueB", keyC: { hello: "world" }, ... }
380
+ ```
381
+
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!):
385
+
386
+ ```js
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
- Remove all of the data from the store:
396
+ Remove all of the data from the store and resets it to the original state:
358
397
 
359
398
  ```js
360
399
  await store.clear();
@@ -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:
@@ -523,6 +565,8 @@ console.log(await store.get("key1"));
523
565
  // "Hello world"
524
566
  ```
525
567
 
568
+ > Note: an extension is needed, to desambiguate with "folder"
569
+
526
570
  You can also create multiple stores:
527
571
 
528
572
  ```js
@@ -532,6 +576,31 @@ const store1 = kv(new URL(`file://${process.cwd()}/cache.json`));
532
576
  const store2 = kv(new URL(`file://${import.meta.dirname}/data.json`));
533
577
  ```
534
578
 
579
+ ### Folder
580
+
581
+ Treat a single folder in your filesystem as the source for the KV store, with each key being within a file:
582
+
583
+ ```js
584
+ import kv from "polystore";
585
+
586
+ const store = kv(new URL("file:///Users/me/project/data/"));
587
+
588
+ await store.set("key1", "Hello world", { expires: "1h" });
589
+ console.log(await store.get("key1"));
590
+ // "Hello world"
591
+ ```
592
+
593
+ > Note: the ending slash `/` is needed, to desambiguate with "file"
594
+
595
+ You can also create multiple stores:
596
+
597
+ ```js
598
+ // Paths need to be absolute, but you can use process.cwd() to make
599
+ // it relative to the current process:
600
+ const store1 = kv(new URL(`file://${process.cwd()}/cache/`));
601
+ const store2 = kv(new URL(`file://${import.meta.dirname}/data/`));
602
+ ```
603
+
535
604
  ### Cloudflare KV
536
605
 
537
606
  Supports the official Cloudflare's KV stores. Follow [the official guide](https://developers.cloudflare.com/kv/get-started/), then load it like this:
@@ -581,6 +650,16 @@ console.log(await store.get("key1"));
581
650
  // "Hello world"
582
651
  ```
583
652
 
653
+ Why use polystore? The main reason is that we add expiration on top of Level, which is not supported by Level:
654
+
655
+ ```js
656
+ // GOOD - with polystore
657
+ await store.set("user", { hello: 'world' }, { expires: "2days" });
658
+
659
+ // With Level:
660
+ ?? // Just not possible
661
+ ```
662
+
584
663
  ### Etcd
585
664
 
586
665
  Connect to Microsoft's Etcd Key-Value store:
@@ -598,15 +677,15 @@ console.log(await store.get("key1"));
598
677
 
599
678
  ### Custom store
600
679
 
601
- Please see the [creating a store](#creating-a-store) section for more details!
680
+ Please see the [creating a store](#creating-a-store) section for all the details!
602
681
 
603
682
  ## Performance
604
683
 
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!
684
+ > 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
685
 
607
686
  While all of our stores support `expires`, `.prefix()` and group operations, the nature of those makes them to have different performance characteristics.
608
687
 
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).
688
+ **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
689
 
611
690
  **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
691
 
@@ -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,88 @@
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("folder://".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), "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, { 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));
73
+ for (const key of keys) {
74
+ const data = await this.get(key);
75
+ yield [key, data];
76
+ }
77
+ }
78
+
79
+ // async clear(prefix = "") {
80
+ // const data = await this.#read();
81
+ // for (let key in data) {
82
+ // if (key.startsWith(prefix)) {
83
+ // delete data[key];
84
+ // }
85
+ // }
86
+ // await this.#write(data);
87
+ // }
88
+ }
@@ -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,
package/src/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import clients from "./clients/index.js";
2
- import { createId, isClass, parse } from "./utils.js";
2
+ import { createId, isClass, parse, unix } from "./utils.js";
3
3
 
4
4
  class Store {
5
5
  PREFIX = "";
@@ -42,18 +42,13 @@ class Store {
42
42
  for (let method of ["has", "keys", "values"]) {
43
43
  if (client[method]) {
44
44
  throw new Error(
45
- `You can only define client.${method}() when the client manages the expiration; otherwise please do NOT define .${method}() and let us manage it`
45
+ `You can only define client.${method}() when the client manages the expiration; otherwise please do NOT define .${method}() and let us manage it`,
46
46
  );
47
47
  }
48
48
  }
49
49
  }
50
50
  }
51
51
 
52
- #unix(expires) {
53
- const now = new Date().getTime();
54
- return expires === null ? null : now + expires * 1000;
55
- }
56
-
57
52
  // Check if the given data is fresh or not; if
58
53
  #isFresh(data, key) {
59
54
  // Should never happen, but COULD happen; schedule it for
@@ -76,30 +71,29 @@ class Store {
76
71
 
77
72
  async add(value, options = {}) {
78
73
  await this.promise;
79
- const expires = parse(options.expire ?? options.expires);
74
+ let expires = parse(options.expire ?? options.expires);
80
75
 
81
76
  // Use the underlying one from the client if found
82
77
  if (this.client.add) {
83
78
  if (this.client.EXPIRES) {
84
- return this.client.add(this.PREFIX, value, { expires });
79
+ return await this.client.add(this.PREFIX, value, { expires });
85
80
  }
86
81
 
87
82
  // In the data we need the timestamp since we need it "absolute":
88
- return this.client.add(this.PREFIX, {
89
- expires: this.#unix(expires),
90
- value,
91
- });
83
+ expires = unix(expires);
84
+ const key = await this.client.add(this.PREFIX, { expires, value });
85
+ return key;
92
86
  }
93
87
 
94
- const id = createId();
95
- await this.set(id, value, { expires });
96
- return id; // The plain one without the prefix
88
+ const key = createId();
89
+ await this.set(key, value, { expires });
90
+ return key; // The plain one without the prefix
97
91
  }
98
92
 
99
93
  async set(key, value, options = {}) {
100
94
  await this.promise;
101
95
  const id = this.PREFIX + key;
102
- const expires = parse(options.expire ?? options.expires);
96
+ let expires = parse(options.expire ?? options.expires);
103
97
 
104
98
  // Quick delete
105
99
  if (value === null || (typeof expires === "number" && expires <= 0)) {
@@ -114,7 +108,8 @@ class Store {
114
108
  }
115
109
 
116
110
  // In the data we need the timestamp since we need it "absolute":
117
- await this.client.set(id, { expires: this.#unix(expires), value });
111
+ expires = unix(expires);
112
+ await this.client.set(id, { expires, value });
118
113
  return key;
119
114
  }
120
115
 
@@ -258,7 +253,7 @@ class Store {
258
253
 
259
254
  prefix(prefix = "") {
260
255
  const store = new Store(
261
- Promise.resolve(this.promise).then((client) => client || this.client)
256
+ Promise.resolve(this.promise).then((client) => client || this.client),
262
257
  );
263
258
  store.PREFIX = this.PREFIX + prefix;
264
259
  return store;
package/src/utils.js CHANGED
@@ -47,3 +47,8 @@ export function isClass(func) {
47
47
  /^class\s/.test(Function.prototype.toString.call(func))
48
48
  );
49
49
  }
50
+
51
+ export function unix(expires) {
52
+ const now = new Date().getTime();
53
+ return expires === null ? null : now + expires * 1000;
54
+ }