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 +3 -2
- package/readme.md +107 -28
- package/src/clients/file.js +11 -2
- package/src/clients/folder.js +88 -0
- package/src/clients/index.js +2 -0
- package/src/index.js +14 -19
- package/src/utils.js +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polystore",
|
|
3
|
-
"version": "0.
|
|
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 [](https://www.npmjs.com/package/polystore) [](https://github.com/franciscop/polystore/blob/master/.github/workflows/tests.yml) [](https://
|
|
1
|
+
# Polystore [](https://www.npmjs.com/package/polystore) [](https://github.com/franciscop/polystore/blob/master/.github/workflows/tests.yml) [](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
|
|
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) (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
>
|
|
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
|
|
316
|
+
Get all of the keys in the store as a simple array of strings:
|
|
311
317
|
|
|
312
318
|
```js
|
|
313
|
-
await store.keys(
|
|
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
|
|
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(
|
|
337
|
+
await store.values();
|
|
338
|
+
// ["valueA", "valueB", { hello: "world" }, ...]
|
|
324
339
|
```
|
|
325
340
|
|
|
326
|
-
|
|
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.
|
|
344
|
+
const sessions = await store.prefix("session:").values();
|
|
330
345
|
// A list of all the sessions
|
|
331
346
|
|
|
332
|
-
const companies = await store.
|
|
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
|
|
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(
|
|
367
|
+
const sessionEntries = await store.prefix('session:').entries();
|
|
368
|
+
// [["keyA", "valueA"], ["keyB", "valueB"]]
|
|
344
369
|
```
|
|
345
370
|
|
|
346
|
-
|
|
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
|
|
350
|
-
|
|
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
|
|
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
|
|
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
|
|
package/src/clients/file.js
CHANGED
|
@@ -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 (
|
|
6
|
-
|
|
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
|
+
}
|
package/src/clients/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
95
|
-
await this.set(
|
|
96
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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