polystore 0.9.3 → 0.11.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/assets/home.html +8 -4
- package/package.json +16 -4
- package/readme.md +77 -28
- package/src/clients/cloudflare.js +26 -2
- package/src/clients/cookie.js +21 -18
- package/src/clients/etcd.js +7 -0
- package/src/clients/file.js +5 -3
- package/src/clients/forage.js +9 -3
- package/src/clients/level.js +8 -0
- package/src/clients/memory.js +8 -0
- package/src/clients/redis.js +22 -1
- package/src/clients/storage.js +7 -0
- package/src/index.d.ts +167 -7
- package/src/index.js +106 -99
- package/src/index.test.js +39 -4
- package/src/test/customFull.js +7 -0
- package/src/test/customSimple.js +8 -4
- package/src/test/setup.js +19 -8
package/assets/home.html
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<a
|
|
15
15
|
class="pseudo button"
|
|
16
16
|
href="https://superpeer.com/francisco/-/javascript-and-react-help"
|
|
17
|
-
>
|
|
17
|
+
>Get JS help</a
|
|
18
18
|
>
|
|
19
19
|
</div>
|
|
20
20
|
</div>
|
|
@@ -77,7 +77,8 @@ console.log(await store.get(key));
|
|
|
77
77
|
<a href="/documentation#getting-started">Getting started</a>,
|
|
78
78
|
<a href="/documentation#api">API</a>,
|
|
79
79
|
<a href="/documentation#clients">Clients</a> and
|
|
80
|
-
<a href="/documentation#
|
|
80
|
+
<a href="/documentation#creating-a-store">custom stores</a> for your
|
|
81
|
+
convenience.
|
|
81
82
|
</p>
|
|
82
83
|
</div>
|
|
83
84
|
</div>
|
|
@@ -152,7 +153,7 @@ console.log(await store.get(key));
|
|
|
152
153
|
<p>
|
|
153
154
|
At
|
|
154
155
|
<a href="https://bundlephobia.com/package/polystore" target="_blank"
|
|
155
|
-
>just <strong>
|
|
156
|
+
>just <strong>3kb</strong></a
|
|
156
157
|
>
|
|
157
158
|
(min+gzip), the impact on your app loading time is minimal.
|
|
158
159
|
</p>
|
|
@@ -179,7 +180,10 @@ console.log(await store.get(key));
|
|
|
179
180
|
</svg>
|
|
180
181
|
<h3>Intuitive expirations</h3>
|
|
181
182
|
</header>
|
|
182
|
-
<p>
|
|
183
|
+
<p>
|
|
184
|
+
Write the expiration as <code>100s</code>, <code>1week</code>, etc. and
|
|
185
|
+
forget time-related bugs.
|
|
186
|
+
</p>
|
|
183
187
|
</div>
|
|
184
188
|
</div>
|
|
185
189
|
</section>
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polystore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "A small compatibility layer for many popular KV stores like localStorage, Redis, FileSystem, etc.",
|
|
5
|
-
"homepage": "https://
|
|
5
|
+
"homepage": "https://polystore.dev/",
|
|
6
6
|
"repository": "https://github.com/franciscop/polystore.git",
|
|
7
7
|
"bugs": "https://github.com/franciscop/polystore/issues",
|
|
8
8
|
"funding": "https://www.paypal.me/franciscopresencia/19",
|
|
@@ -24,11 +24,12 @@
|
|
|
24
24
|
"value"
|
|
25
25
|
],
|
|
26
26
|
"license": "MIT",
|
|
27
|
+
"dependencies": {},
|
|
27
28
|
"devDependencies": {
|
|
28
|
-
"
|
|
29
|
+
"@deno/kv": "^0.8.1",
|
|
30
|
+
"check-dts": "^0.8.0",
|
|
29
31
|
"dotenv": "^16.3.1",
|
|
30
32
|
"edge-mock": "^0.0.15",
|
|
31
|
-
"esbuild": "^0.19.4",
|
|
32
33
|
"etcd3": "^1.1.2",
|
|
33
34
|
"jest": "^29.7.0",
|
|
34
35
|
"jest-environment-jsdom": "^29.7.0",
|
|
@@ -36,6 +37,17 @@
|
|
|
36
37
|
"localforage": "^1.10.0",
|
|
37
38
|
"redis": "^4.6.10"
|
|
38
39
|
},
|
|
40
|
+
"documentation": {
|
|
41
|
+
"title": "🏬 Polystore - A universal library for standardizing any KV-store",
|
|
42
|
+
"home": "assets/home.html",
|
|
43
|
+
"homepage": "https://polystore.dev/",
|
|
44
|
+
"menu": {
|
|
45
|
+
"Documentation": "/documentation",
|
|
46
|
+
"Issues": "https://github.com/franciscop/polystore/issues",
|
|
47
|
+
"Get help": "https://superpeer.com/francisco/-/javascript-and-react-help",
|
|
48
|
+
"Github": "https://github.com/franciscop/polystore"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
39
51
|
"jest": {
|
|
40
52
|
"testEnvironment": "jsdom",
|
|
41
53
|
"setupFiles": [
|
package/readme.md
CHANGED
|
@@ -36,13 +36,13 @@ Available clients for the KV store:
|
|
|
36
36
|
- [**File** `new URL('file:///...')`](#file) (be): store the data in a single JSON file in your FS
|
|
37
37
|
- [**Redis Client** `redisClient`](#redis-client) (be): use the Redis instance that you connect to
|
|
38
38
|
- [**Cloudflare KV** `env.KV_NAMESPACE`](#cloudflare-kv) (be): use Cloudflare's KV store
|
|
39
|
-
- [**Level** `new Level('example', { valueEncoding: 'json' })`](#level): support the whole Level ecosystem
|
|
40
|
-
- [**Etcd** `new Etcd3()`](#etcd): the Microsoft's high performance KV store.
|
|
39
|
+
- [**Level** `new Level('example', { valueEncoding: 'json' })`](#level) (fe+be): support the whole Level ecosystem
|
|
40
|
+
- [**Etcd** `new Etcd3()`](#etcd) (be): the Microsoft's high performance KV store.
|
|
41
41
|
- [**_Custom_** `{}`](#creating-a-store) (?): create your own store with just 3 methods!
|
|
42
42
|
|
|
43
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
44
|
|
|
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 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:
|
|
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
46
|
|
|
47
47
|
```js
|
|
48
48
|
import MyApi from "my-api";
|
|
@@ -54,6 +54,38 @@ MyApi({ cache: env.KV_NAMESPACE }); // OR
|
|
|
54
54
|
// ...
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
## Getting started
|
|
58
|
+
|
|
59
|
+
First, install `polystore` and whatever [supported client](#clients) that you prefer. Let's see Redis as an example here:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
npm i polystore redis
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Then import both, initialize the Redis client and pass it to Polystore:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import kv from "polystore";
|
|
69
|
+
import { createClient } from "redis";
|
|
70
|
+
|
|
71
|
+
// Import the Redis configuration
|
|
72
|
+
const REDIS = process.env.REDIS_URL;
|
|
73
|
+
|
|
74
|
+
// Wrap the redis creation with Polystore (kv())
|
|
75
|
+
const store = kv(createClient(REDIS).connect());
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Now your store is ready to use! Add, set, get, del different keys. [See full API](#api).
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
const key = await store.add("Hello");
|
|
82
|
+
|
|
83
|
+
console.log(await store.get(key));
|
|
84
|
+
// Hello
|
|
85
|
+
|
|
86
|
+
await store.del(key);
|
|
87
|
+
```
|
|
88
|
+
|
|
57
89
|
## API
|
|
58
90
|
|
|
59
91
|
See how to initialize each store [in the Clients list documentation](#clients). But basically for every store, it's like this:
|
|
@@ -79,7 +111,7 @@ client.connect();
|
|
|
79
111
|
const store = kv(client);
|
|
80
112
|
```
|
|
81
113
|
|
|
82
|
-
While you can keep a reference to the store and access it directly, we strongly recommend if you are going to use a store, to only access it through `polystore`, since we
|
|
114
|
+
While you can keep a reference to the store and access it directly, we strongly recommend if you are going to use a store, to only access it through `polystore`, since we might add custom serialization and extra properties for e.g. expiration time:
|
|
83
115
|
|
|
84
116
|
```js
|
|
85
117
|
const map = new Map();
|
|
@@ -209,6 +241,26 @@ Remove a single key from the store and return the key itself:
|
|
|
209
241
|
await store.del(key: string);
|
|
210
242
|
```
|
|
211
243
|
|
|
244
|
+
It will ignore the operation if the key or value don't exist already (but won't thorw).
|
|
245
|
+
|
|
246
|
+
### _Iterator_
|
|
247
|
+
|
|
248
|
+
You can iterate over the whole store with an async iterator:
|
|
249
|
+
|
|
250
|
+
```js
|
|
251
|
+
for await (const [key, value] of store) {
|
|
252
|
+
// ...
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
This is very useful for performance resons. You can also iterate on a subset of the entries with .prefix:
|
|
257
|
+
|
|
258
|
+
```js
|
|
259
|
+
for await (const [key, value] of store.prefix("session:")) {
|
|
260
|
+
// ...
|
|
261
|
+
}
|
|
262
|
+
```
|
|
263
|
+
|
|
212
264
|
### .keys()
|
|
213
265
|
|
|
214
266
|
Get all of the keys in the store, optionally filtered by a prefix:
|
|
@@ -515,19 +567,19 @@ Please see the [creating a store](#creating-a-store) section for more details!
|
|
|
515
567
|
|
|
516
568
|
## Performance
|
|
517
569
|
|
|
518
|
-
> TL;DR: if you only use the item operations (add,set,get,has,del) and your
|
|
570
|
+
> 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!
|
|
519
571
|
|
|
520
572
|
While all of our stores support `expires`, `.prefix()` and group operations, the nature of those makes them to have different performance characteristics.
|
|
521
573
|
|
|
522
|
-
**Expires** we polyfill expiration when the underlying 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).
|
|
574
|
+
**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).
|
|
523
575
|
|
|
524
|
-
**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. 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.
|
|
576
|
+
**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.
|
|
525
577
|
|
|
526
578
|
**Substores** when dealing with a `.prefix()` substore, the same applies. Item operations should see no performance degradation from `.prefix()`, but group operations follow the above performance considerations. Some engines might have native prefix support, so performance in those is better for group operations in a substore than the whole store. But in general you should consider `.prefix()` as a convenient way of classifying your keys and not as a performance fix for group operations.
|
|
527
579
|
|
|
528
580
|
## Expires
|
|
529
581
|
|
|
530
|
-
> Warning: if a client doesn't support expiration natively, we will hide expired keys on the API calls for a nice DX, but _old data might not be evicted automatically_
|
|
582
|
+
> Warning: if a client doesn't support expiration natively, we will hide expired keys on the API calls for a nice DX, but _old data might not be evicted automatically_. See [the notes in Performance](#performance) for details on how to work around this.
|
|
531
583
|
|
|
532
584
|
We unify all of the clients diverse expiration methods into a single, easy one with `expires`:
|
|
533
585
|
|
|
@@ -572,7 +624,7 @@ For these and more situations, you can use `.prefix()` to simplify your life fur
|
|
|
572
624
|
|
|
573
625
|
## Creating a store
|
|
574
626
|
|
|
575
|
-
To create a store, you define a class with these methods:
|
|
627
|
+
To create a store, you define a class with these properties and methods:
|
|
576
628
|
|
|
577
629
|
```js
|
|
578
630
|
class MyClient {
|
|
@@ -580,10 +632,10 @@ class MyClient {
|
|
|
580
632
|
// the `.set()` and `.add()` receive a `expires` that is a `null` or `number`:
|
|
581
633
|
EXPIRES = false;
|
|
582
634
|
|
|
583
|
-
// Mandatory methods
|
|
635
|
+
// Mandatory methods
|
|
584
636
|
get (key): Promise<any>;
|
|
585
637
|
set (key, value, { expires: null|number }): Promise<null>;
|
|
586
|
-
|
|
638
|
+
iterate(prefix): AyncIterator<[string, any]>
|
|
587
639
|
|
|
588
640
|
// Optional item methods (for optimization or customization)
|
|
589
641
|
add (prefix, data, { expires: null|number }): Promise<string>;
|
|
@@ -591,6 +643,7 @@ class MyClient {
|
|
|
591
643
|
del (key): Promise<null>;
|
|
592
644
|
|
|
593
645
|
// Optional group methods
|
|
646
|
+
entries (prefix): Promise<[string, any][]>;
|
|
594
647
|
keys (prefix): Promise<string[]>;
|
|
595
648
|
values (prefix): Promise<any[]>;
|
|
596
649
|
clear (prefix): Promise<null>;
|
|
@@ -600,7 +653,7 @@ class MyClient {
|
|
|
600
653
|
}
|
|
601
654
|
```
|
|
602
655
|
|
|
603
|
-
Note that this is NOT the public API, it's the internal **client** API. It's simpler than the public API since we do some of the heavy lifting as an intermediate layer (e.g. the `expires` will always be a `null` or `number`, never `undefined` or a `string`), but also it differs from polystore's API, like `.add()` has a different signature, and the group methods all take a explicit prefix.
|
|
656
|
+
Note that this is NOT the public API, it's the internal **client** API. It's simpler than the public API since we do some of the heavy lifting as an intermediate layer (e.g. for the client, the `expires` will always be a `null` or `number`, never `undefined` or a `string`), but also it differs from polystore's public API, like `.add()` has a different signature, and the group methods all take a explicit prefix.
|
|
604
657
|
|
|
605
658
|
**Expires**: if you set the `EXPIRES = true`, then you are indicating that the client WILL manage the lifecycle of the data. This includes all methods, for example if an item is expired, then its key should not be returned in `.keys()`, it's value should not be returned in `.values()`, and the method `.has()` will return `false`. The good news is that you will always receive the option `expires`, which is either `null` (no expiration) or a `number` indicating the time when it will expire.
|
|
606
659
|
|
|
@@ -615,26 +668,20 @@ const value = await store.get("a");
|
|
|
615
668
|
// client.get("hello:world:a");
|
|
616
669
|
|
|
617
670
|
// User calls this, then the client is called with that:
|
|
618
|
-
const
|
|
619
|
-
|
|
671
|
+
for await (const entry of store.iterate()) {
|
|
672
|
+
}
|
|
673
|
+
// client.iterate("hello:world:");
|
|
620
674
|
```
|
|
621
675
|
|
|
622
|
-
> Note: all of the _group methods_ that return keys, should return them **with the prefix
|
|
676
|
+
> Note: all of the _group methods_ that return keys, should return them **with the prefix**:
|
|
623
677
|
|
|
624
678
|
```js
|
|
625
|
-
// Example if your client works around a simple object {}, we want to remove
|
|
626
|
-
// the `prefix` from the beginning of the keys returned:
|
|
627
679
|
client.keys = (prefix) => {
|
|
628
|
-
return
|
|
629
|
-
|
|
630
|
-
.map((key) => key.slice(prefix.length)); // <= Important!
|
|
680
|
+
// Filter the keys, and return them INCLUDING the prefix!
|
|
681
|
+
return Object.keys(subStore).filter((key) => key.startsWith(prefix));
|
|
631
682
|
};
|
|
632
683
|
```
|
|
633
684
|
|
|
634
|
-
You can and should just concatenate the `key + options.prefix`. We don't do it for two reasons: in some cases, like `.add()`, there's no key that we can use to concatenate, and also you might
|
|
635
|
-
|
|
636
|
-
For example, if the user of `polystore` does `kv(client).prefix('hello:').get('a')`, your store will be directly called with `client.get('a', { prefix: 'hello:' })`. You can safely concatenate `options.prefix + key` since this library always ensures that the prefix is defined and defaults to `''`. We don't concatenate it interally because in some cases (like in `.add()`) it makes more sense that this is handled by the client as an optimization.
|
|
637
|
-
|
|
638
685
|
While the signatures are different, you can check each entries on the output of Polystore API to see what is expected for the methods of the client to do, e.g. `.clear()` will remove all of the items that match the prefix (or everything if there's no prefix).
|
|
639
686
|
|
|
640
687
|
**Example: Plain Object client**
|
|
@@ -655,10 +702,12 @@ class MyClient {
|
|
|
655
702
|
}
|
|
656
703
|
|
|
657
704
|
// Filter them by the prefix, note that `prefix` will always be a string
|
|
658
|
-
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
705
|
+
*iterate(prefix) {
|
|
706
|
+
for (const [key, value] of Object.entries(dataSource)) {
|
|
707
|
+
if (key.startsWith(prefix)) {
|
|
708
|
+
yield [key, value];
|
|
709
|
+
}
|
|
710
|
+
}
|
|
662
711
|
}
|
|
663
712
|
}
|
|
664
713
|
```
|
|
@@ -31,9 +31,33 @@ export default class Cloudflare {
|
|
|
31
31
|
return this.client.delete(key);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
// Since we have pagination, we don't want to get all of the
|
|
35
|
+
// keys at once if we can avoid it
|
|
36
|
+
async *iterate(prefix = "") {
|
|
37
|
+
let cursor;
|
|
38
|
+
do {
|
|
39
|
+
const raw = await this.client.list({ prefix, cursor });
|
|
40
|
+
const keys = raw.keys.map((k) => k.name);
|
|
41
|
+
for (let key of keys) {
|
|
42
|
+
const value = await this.get(key);
|
|
43
|
+
// By the time this specific value is read, it could
|
|
44
|
+
// already be gone!
|
|
45
|
+
if (!value) continue;
|
|
46
|
+
yield [key, value];
|
|
47
|
+
}
|
|
48
|
+
cursor = raw.list_complete ? null : raw.cursor;
|
|
49
|
+
} while (cursor);
|
|
50
|
+
}
|
|
51
|
+
|
|
34
52
|
async keys(prefix = "") {
|
|
35
|
-
const
|
|
36
|
-
|
|
53
|
+
const keys = [];
|
|
54
|
+
let cursor;
|
|
55
|
+
do {
|
|
56
|
+
const raw = await this.client.list({ prefix, cursor });
|
|
57
|
+
keys.push(...raw.keys.map((k) => k.name));
|
|
58
|
+
cursor = raw.list_complete ? null : raw.cursor;
|
|
59
|
+
} while (cursor);
|
|
60
|
+
return keys;
|
|
37
61
|
}
|
|
38
62
|
|
|
39
63
|
async entries(prefix = "") {
|
package/src/clients/cookie.js
CHANGED
|
@@ -8,9 +8,25 @@ export default class Cookie {
|
|
|
8
8
|
return client === "cookie" || client === "cookies";
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// Group methods
|
|
12
|
+
#read() {
|
|
13
|
+
const all = {};
|
|
14
|
+
for (let entry of document.cookie.split(";")) {
|
|
15
|
+
try {
|
|
16
|
+
const [rawKey, rawValue] = entry.split("=");
|
|
17
|
+
const key = decodeURIComponent(rawKey.trim());
|
|
18
|
+
const value = JSON.parse(decodeURIComponent(rawValue.trim()));
|
|
19
|
+
all[key] = value;
|
|
20
|
+
} catch (error) {
|
|
21
|
+
// no-op (some 3rd party can set cookies independently)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return all;
|
|
25
|
+
}
|
|
26
|
+
|
|
11
27
|
// For cookies, an empty value is the same as null, even `""`
|
|
12
28
|
get(key) {
|
|
13
|
-
return this
|
|
29
|
+
return this.#read()[key] || null;
|
|
14
30
|
}
|
|
15
31
|
|
|
16
32
|
set(key, data = null, { expires } = {}) {
|
|
@@ -33,23 +49,10 @@ export default class Cookie {
|
|
|
33
49
|
return key;
|
|
34
50
|
}
|
|
35
51
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const [key, data] = entry.split("=");
|
|
41
|
-
const name = decodeURIComponent(key.trim());
|
|
42
|
-
if (!name.startsWith(prefix)) continue;
|
|
43
|
-
try {
|
|
44
|
-
all[name] = JSON.parse(decodeURIComponent(data.trim()));
|
|
45
|
-
} catch (error) {
|
|
46
|
-
// no-op (some 3rd party can set cookies independently)
|
|
47
|
-
}
|
|
52
|
+
async *iterate(prefix = "") {
|
|
53
|
+
for (let [key, value] of Object.entries(this.#read())) {
|
|
54
|
+
if (!key.startsWith(prefix)) continue;
|
|
55
|
+
yield [key, value];
|
|
48
56
|
}
|
|
49
|
-
return all;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
entries(prefix = "") {
|
|
53
|
-
return Object.entries(this.all(prefix));
|
|
54
57
|
}
|
|
55
58
|
}
|
package/src/clients/etcd.js
CHANGED
|
@@ -21,6 +21,13 @@ export default class Etcd {
|
|
|
21
21
|
await this.client.put(key).value(JSON.stringify(value));
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
async *iterate(prefix = "") {
|
|
25
|
+
const keys = await this.client.getAll().prefix(prefix).keys();
|
|
26
|
+
for (const key of keys) {
|
|
27
|
+
yield [key, await this.get(key)];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
async entries(prefix = "") {
|
|
25
32
|
const keys = await this.client.getAll().prefix(prefix).keys();
|
|
26
33
|
const values = await Promise.all(keys.map((k) => this.get(k)));
|
package/src/clients/file.js
CHANGED
|
@@ -53,10 +53,12 @@ export default class File {
|
|
|
53
53
|
return key;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
async entries(prefix = "") {
|
|
56
|
+
async *iterate(prefix = "") {
|
|
58
57
|
const data = await this.#read();
|
|
59
|
-
|
|
58
|
+
const entries = Object.entries(data).filter((p) => p[0].startsWith(prefix));
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
yield entry;
|
|
61
|
+
}
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
async clear(prefix = "") {
|
package/src/clients/forage.js
CHANGED
|
@@ -22,12 +22,18 @@ export default class Forage {
|
|
|
22
22
|
return key;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
async *iterate(prefix = "") {
|
|
26
|
+
const keys = await this.client.keys();
|
|
27
|
+
const list = keys.filter((k) => k.startsWith(prefix));
|
|
28
|
+
for (const key of list) {
|
|
29
|
+
yield [key, await this.get(key)];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
25
33
|
async entries(prefix = "") {
|
|
26
34
|
const all = await this.client.keys();
|
|
27
35
|
const keys = all.filter((k) => k.startsWith(prefix));
|
|
28
|
-
const values = await Promise.all(
|
|
29
|
-
keys.map((key) => this.client.getItem(key))
|
|
30
|
-
);
|
|
36
|
+
const values = await Promise.all(keys.map((key) => this.get(key)));
|
|
31
37
|
return keys.map((key, i) => [key, values[i]]);
|
|
32
38
|
}
|
|
33
39
|
|
package/src/clients/level.js
CHANGED
|
@@ -26,6 +26,14 @@ export default class Level {
|
|
|
26
26
|
return this.client.del(key);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
async *iterate(prefix = "") {
|
|
30
|
+
const keys = await this.client.keys().all();
|
|
31
|
+
const list = keys.filter((k) => k.startsWith(prefix));
|
|
32
|
+
for (const key of list) {
|
|
33
|
+
yield [key, await this.get(key)];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
async entries(prefix = "") {
|
|
30
38
|
const keys = await this.client.keys().all();
|
|
31
39
|
const list = keys.filter((k) => k.startsWith(prefix));
|
package/src/clients/memory.js
CHANGED
|
@@ -21,6 +21,14 @@ export default class Memory {
|
|
|
21
21
|
this.client.delete(key);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
*iterate(prefix = "") {
|
|
25
|
+
const entries = this.entries();
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
if (!entry[0].startsWith(prefix)) continue;
|
|
28
|
+
yield entry;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
// Group methods
|
|
25
33
|
entries(prefix = "") {
|
|
26
34
|
const entries = [...this.client.entries()];
|
package/src/clients/redis.js
CHANGED
|
@@ -37,12 +37,33 @@ export default class Redis {
|
|
|
37
37
|
return this.client.keys(prefix + "*");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// Go through each of the [key, value] in the set
|
|
41
|
+
async *iterate(prefix = "") {
|
|
42
|
+
const MATCH = prefix + "*";
|
|
43
|
+
for await (const key of this.client.scanIterator({ MATCH })) {
|
|
44
|
+
const value = await this.get(key);
|
|
45
|
+
yield [key, value];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Optimizing the retrieval of them all in bulk by loading the values
|
|
50
|
+
// in parallel
|
|
40
51
|
async entries(prefix = "") {
|
|
41
|
-
const keys = await this.
|
|
52
|
+
const keys = await this.keys(prefix);
|
|
42
53
|
const values = await Promise.all(keys.map((k) => this.get(k)));
|
|
43
54
|
return keys.map((k, i) => [k, values[i]]);
|
|
44
55
|
}
|
|
45
56
|
|
|
57
|
+
// Optimizing the retrieval of them by not getting their values
|
|
58
|
+
async keys(prefix = "") {
|
|
59
|
+
const MATCH = prefix + "*";
|
|
60
|
+
const keys = [];
|
|
61
|
+
for await (const key of this.client.scanIterator({ MATCH })) {
|
|
62
|
+
keys.push(key);
|
|
63
|
+
}
|
|
64
|
+
return keys;
|
|
65
|
+
}
|
|
66
|
+
|
|
46
67
|
async clear(prefix = "") {
|
|
47
68
|
if (!prefix) return this.client.flushAll();
|
|
48
69
|
|
package/src/clients/storage.js
CHANGED
|
@@ -25,6 +25,13 @@ export default class WebStorage {
|
|
|
25
25
|
return key;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
*iterate(prefix = "") {
|
|
29
|
+
const entries = this.entries(prefix);
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
yield entry;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
// Group methods
|
|
29
36
|
entries(prefix = "") {
|
|
30
37
|
const entries = Object.entries(this.client);
|
package/src/index.d.ts
CHANGED
|
@@ -1,21 +1,181 @@
|
|
|
1
1
|
type Options = { expires?: number | string | null };
|
|
2
2
|
type Value = null | string | { [key: string]: Value } | Value[];
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
interface Store {
|
|
5
|
+
/**
|
|
6
|
+
* Save the data on an autogenerated key, can add expiration as well:
|
|
7
|
+
*
|
|
8
|
+
* ```js
|
|
9
|
+
* const key1 = await store.add("value1");
|
|
10
|
+
* const key2 = await store.add({ hello: "world" });
|
|
11
|
+
* const key3 = await store.add("value3", { expires: "1h" });
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* **[→ Full .add() Docs](https://polystore.dev/documentation#add)**
|
|
15
|
+
*/
|
|
16
|
+
add: (value: Value, options?: Options) => Promise<string>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Save the data on the given key, can add expiration as well:
|
|
20
|
+
*
|
|
21
|
+
* ```js
|
|
22
|
+
* const key = await store.set("key1", "value1");
|
|
23
|
+
* await store.set("key2", { hello: "world" });
|
|
24
|
+
* await store.set("key3", "value3", { expires: "1h" });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* **[→ Full .set() Docs](https://polystore.dev/documentation#set)**
|
|
28
|
+
*/
|
|
29
|
+
set: (key: string, value: Value, options?: Options) => Promise<string>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Read a single value from the KV store:
|
|
33
|
+
*
|
|
34
|
+
* ```js
|
|
35
|
+
* const value1 = await store.get("key1");
|
|
36
|
+
* // null (doesn't exist or has expired)
|
|
37
|
+
* const value2 = await store.get("key2");
|
|
38
|
+
* // "value2"
|
|
39
|
+
* const value3 = await store.get("key3");
|
|
40
|
+
* // { hello: "world" }
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* **[→ Full .get() Docs](https://polystore.dev/documentation#get)**
|
|
44
|
+
*/
|
|
5
45
|
get: (key: string) => Promise<Value>;
|
|
6
|
-
|
|
7
|
-
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check whether a key exists or not:
|
|
49
|
+
*
|
|
50
|
+
* ```js
|
|
51
|
+
* if (await store.has("key1")) { ... }
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* If you are going to use the value, it's better to just read it:
|
|
55
|
+
*
|
|
56
|
+
* ```js
|
|
57
|
+
* const val = await store.get("key1");
|
|
58
|
+
* if (val) { ... }
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
*
|
|
62
|
+
* **[→ Full .has() Docs](https://polystore.dev/documentation#has)**
|
|
63
|
+
*/
|
|
8
64
|
has: (key: string) => Promise<boolean>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Remove a single key and its value from the store:
|
|
68
|
+
*
|
|
69
|
+
* ```js
|
|
70
|
+
* const key = await store.del("key1");
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* **[→ Full .del() Docs](https://polystore.dev/documentation#del)**
|
|
74
|
+
*/
|
|
9
75
|
del: (key: string) => Promise<null>;
|
|
10
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Return an array of the entries, in the [key, value] format:
|
|
79
|
+
*
|
|
80
|
+
* ```js
|
|
81
|
+
* const entries = await store.entries();
|
|
82
|
+
* // [["key1", "value1"], ["key2", { hello: "world" }], ...]
|
|
83
|
+
*
|
|
84
|
+
* // To limit it to a given prefix, use `.prefix()`:
|
|
85
|
+
* const sessions = await store.prefix("session:").entries();
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* **[→ Full .entries() Docs](https://polystore.dev/documentation#entries)**
|
|
89
|
+
*/
|
|
90
|
+
entries: () => Promise<[key: string, value: Value][]>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Return an array of the keys in the store:
|
|
94
|
+
*
|
|
95
|
+
* ```js
|
|
96
|
+
* const keys = await store.keys();
|
|
97
|
+
* // ["key1", "key2", ...]
|
|
98
|
+
*
|
|
99
|
+
* // To limit it to a given prefix, use `.prefix()`:
|
|
100
|
+
* const sessions = await store.prefix("session:").keys();
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* **[→ Full .keys() Docs](https://polystore.dev/documentation#keys)**
|
|
104
|
+
*/
|
|
11
105
|
keys: () => Promise<string[]>;
|
|
12
|
-
values: () => Promise<any[]>;
|
|
13
|
-
entries: () => Promise<[key: string, value: any][]>;
|
|
14
106
|
|
|
15
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Return an array of the values in the store:
|
|
109
|
+
*
|
|
110
|
+
* ```js
|
|
111
|
+
* const values = await store.values();
|
|
112
|
+
* // ["value1", { hello: "world" }, ...]
|
|
113
|
+
*
|
|
114
|
+
* // To limit it to a given prefix, use `.prefix()`:
|
|
115
|
+
* const sessions = await store.prefix("session:").values();
|
|
116
|
+
* ```
|
|
117
|
+
*
|
|
118
|
+
* **[→ Full .values() Docs](https://polystore.dev/documentation#values)**
|
|
119
|
+
*/
|
|
120
|
+
values: () => Promise<Value[]>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Return an object with the keys:values in the store:
|
|
124
|
+
*
|
|
125
|
+
* ```js
|
|
126
|
+
* const obj = await store.all();
|
|
127
|
+
* // { key1: "value1", key2: { hello: "world" }, ... }
|
|
128
|
+
*
|
|
129
|
+
* // To limit it to a given prefix, use `.prefix()`:
|
|
130
|
+
* const sessions = await store.prefix("session:").all();
|
|
131
|
+
* ```
|
|
132
|
+
*
|
|
133
|
+
* **[→ Full .all() Docs](https://polystore.dev/documentation#all)**
|
|
134
|
+
*/
|
|
135
|
+
all: () => Promise<{ [key: string]: Value }>;
|
|
16
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Delete all of the records of the store:
|
|
139
|
+
*
|
|
140
|
+
* ```js
|
|
141
|
+
* await store.clear();
|
|
142
|
+
* ```
|
|
143
|
+
*
|
|
144
|
+
* It's useful for cache invalidation, clearing the data, and testing.
|
|
145
|
+
*
|
|
146
|
+
* **[→ Full .clear() Docs](https://polystore.dev/documentation#clear)**
|
|
147
|
+
*/
|
|
17
148
|
clear: () => Promise<null>;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Create a substore where all the keys are stored with
|
|
152
|
+
* the given prefix:
|
|
153
|
+
*
|
|
154
|
+
* ```js
|
|
155
|
+
* const session = store.prefix("session:");
|
|
156
|
+
* await session.set("key1", "value1");
|
|
157
|
+
* console.log(await session.entries()); // session.
|
|
158
|
+
* // [["key1", "value1"]]
|
|
159
|
+
* console.log(await store.entries()); // store.
|
|
160
|
+
* // [["session:key1", "value1"]]
|
|
161
|
+
* ```
|
|
162
|
+
*
|
|
163
|
+
* **[→ Full .prefix() Docs](https://polystore.dev/documentation#prefix)**
|
|
164
|
+
*/
|
|
165
|
+
prefix: (prefix: string) => Store;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Stop the connection to the store, if any:
|
|
169
|
+
*
|
|
170
|
+
* ```js
|
|
171
|
+
* await session.set("key1", "value1");
|
|
172
|
+
* await store.close();
|
|
173
|
+
* await session.set("key2", "value2"); // error
|
|
174
|
+
* ```
|
|
175
|
+
*
|
|
176
|
+
* **[→ Full .close() Docs](https://polystore.dev/documentation#close)**
|
|
177
|
+
*/
|
|
18
178
|
close?: () => Promise<null>;
|
|
19
|
-
}
|
|
179
|
+
}
|
|
20
180
|
|
|
21
181
|
export default function (store?: any): Store;
|
package/src/index.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A number, or a string containing a number.
|
|
3
|
+
* @typedef {(number|string|object|array)} Value
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
import clients from "./clients/index.js";
|
|
2
7
|
import { createId, isClass, parse } from "./utils.js";
|
|
3
8
|
|
|
@@ -38,9 +43,9 @@ class Store {
|
|
|
38
43
|
|
|
39
44
|
// #region #validate()
|
|
40
45
|
#validate(client) {
|
|
41
|
-
if (!client.set || !client.get || !client.
|
|
46
|
+
if (!client.set || !client.get || !client.iterate) {
|
|
42
47
|
throw new Error(
|
|
43
|
-
"A client should have at least a .get(), .set() and .
|
|
48
|
+
"A client should have at least a .get(), .set() and .iterate()"
|
|
44
49
|
);
|
|
45
50
|
}
|
|
46
51
|
|
|
@@ -63,6 +68,31 @@ class Store {
|
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
70
|
|
|
71
|
+
#unix(expires) {
|
|
72
|
+
const now = new Date().getTime();
|
|
73
|
+
return expires === null ? null : now + expires * 1000;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check if the given data is fresh or not; if
|
|
77
|
+
#isFresh(data, key) {
|
|
78
|
+
// Should never happen, but COULD happen; schedule it for
|
|
79
|
+
// removal and mark it as stale
|
|
80
|
+
if (!data || !data.value || typeof data !== "object") {
|
|
81
|
+
if (key) this.del(key);
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// It never expires, so keep it
|
|
86
|
+
if (data.expires === null) return true;
|
|
87
|
+
|
|
88
|
+
// It's fresh, keep it
|
|
89
|
+
if (data.expires > Date.now()) return true;
|
|
90
|
+
|
|
91
|
+
// It's expired, remove it
|
|
92
|
+
if (key) this.del(key);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
|
|
66
96
|
// #region .add()
|
|
67
97
|
/**
|
|
68
98
|
* Save the data on an autogenerated key, can add expiration as well:
|
|
@@ -89,9 +119,10 @@ class Store {
|
|
|
89
119
|
}
|
|
90
120
|
|
|
91
121
|
// In the data we need the timestamp since we need it "absolute":
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
122
|
+
return this.client.add(this.PREFIX, {
|
|
123
|
+
expires: this.#unix(expires),
|
|
124
|
+
value,
|
|
125
|
+
});
|
|
95
126
|
}
|
|
96
127
|
|
|
97
128
|
const id = createId();
|
|
@@ -121,7 +152,7 @@ class Store {
|
|
|
121
152
|
const expires = parse(options.expire ?? options.expires);
|
|
122
153
|
|
|
123
154
|
// Quick delete
|
|
124
|
-
if (value === null) {
|
|
155
|
+
if (value === null || (typeof expires === "number" && expires <= 0)) {
|
|
125
156
|
await this.del(id);
|
|
126
157
|
return key;
|
|
127
158
|
}
|
|
@@ -132,16 +163,8 @@ class Store {
|
|
|
132
163
|
return key;
|
|
133
164
|
}
|
|
134
165
|
|
|
135
|
-
// Already expired, then delete it
|
|
136
|
-
if (expires === 0) {
|
|
137
|
-
await this.del(id);
|
|
138
|
-
return key;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
166
|
// In the data we need the timestamp since we need it "absolute":
|
|
142
|
-
|
|
143
|
-
const expDiff = expires === null ? null : now + expires * 1000;
|
|
144
|
-
await this.client.set(id, { expires: expDiff, value });
|
|
167
|
+
await this.client.set(id, { expires: this.#unix(expires), value });
|
|
145
168
|
return key;
|
|
146
169
|
}
|
|
147
170
|
|
|
@@ -151,7 +174,7 @@ class Store {
|
|
|
151
174
|
*
|
|
152
175
|
* ```js
|
|
153
176
|
* const value1 = await store.get("key1");
|
|
154
|
-
* // null (
|
|
177
|
+
* // null (doesn't exist or has expired)
|
|
155
178
|
* const value2 = await store.get("key2");
|
|
156
179
|
* // "value2"
|
|
157
180
|
* const value3 = await store.get("key3");
|
|
@@ -175,23 +198,8 @@ class Store {
|
|
|
175
198
|
// so we can assume it's the raw user data
|
|
176
199
|
if (this.client.EXPIRES) return data;
|
|
177
200
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
// We manage expiration manually, so we know it should have this structure
|
|
182
|
-
// TODO: ADD A CHECK HERE
|
|
183
|
-
const { expires, value } = data;
|
|
184
|
-
|
|
185
|
-
// It never expires
|
|
186
|
-
if (expires === null) return value ?? null;
|
|
187
|
-
|
|
188
|
-
// Already expired! Return nothing, and remove the whole key
|
|
189
|
-
if (expires <= new Date().getTime()) {
|
|
190
|
-
await this.del(key);
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return value;
|
|
201
|
+
if (!this.#isFresh(data, key)) return null;
|
|
202
|
+
return data.value;
|
|
195
203
|
}
|
|
196
204
|
|
|
197
205
|
// #region .has()
|
|
@@ -251,6 +259,19 @@ class Store {
|
|
|
251
259
|
return key;
|
|
252
260
|
}
|
|
253
261
|
|
|
262
|
+
async *[Symbol.asyncIterator]() {
|
|
263
|
+
await this.promise;
|
|
264
|
+
|
|
265
|
+
for await (const [name, data] of this.client.iterate(this.PREFIX)) {
|
|
266
|
+
const key = name.slice(this.PREFIX.length);
|
|
267
|
+
if (this.client.EXPIRES) {
|
|
268
|
+
yield [key, data];
|
|
269
|
+
} else if (this.#isFresh(data, key)) {
|
|
270
|
+
yield [key, data.value];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
254
275
|
// #region .entries()
|
|
255
276
|
/**
|
|
256
277
|
* Return an array of the entries, in the [key, value] format:
|
|
@@ -269,110 +290,84 @@ class Store {
|
|
|
269
290
|
async entries() {
|
|
270
291
|
await this.promise;
|
|
271
292
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
293
|
+
let list = [];
|
|
294
|
+
if (this.client.entries) {
|
|
295
|
+
list = (await this.client.entries(this.PREFIX)).map(([key, value]) => [
|
|
296
|
+
key.slice(this.PREFIX.length),
|
|
297
|
+
value,
|
|
298
|
+
]);
|
|
299
|
+
} else {
|
|
300
|
+
for await (const [key, value] of this.client.iterate(this.PREFIX)) {
|
|
301
|
+
list.push([key.slice(this.PREFIX.length), value]);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
277
304
|
|
|
278
305
|
// The client already manages the expiration, so we can assume
|
|
279
306
|
// that at this point, all entries are not-expired
|
|
280
307
|
if (this.client.EXPIRES) return list;
|
|
281
308
|
|
|
282
309
|
// We need to do manual expiration checking
|
|
283
|
-
const now = new Date().getTime();
|
|
284
310
|
return list
|
|
285
|
-
.filter(([key, data]) =>
|
|
286
|
-
// Should never happen
|
|
287
|
-
if (!data || data.value === null) return false;
|
|
288
|
-
|
|
289
|
-
// It never expires, so keep it
|
|
290
|
-
const { expires } = data;
|
|
291
|
-
if (expires === null) return true;
|
|
292
|
-
|
|
293
|
-
// It's expired, so remove it
|
|
294
|
-
if (expires <= now) {
|
|
295
|
-
this.del(key);
|
|
296
|
-
return false;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// It's not expired, keep it
|
|
300
|
-
return true;
|
|
301
|
-
})
|
|
311
|
+
.filter(([key, data]) => this.#isFresh(data, key))
|
|
302
312
|
.map(([key, data]) => [key, data.value]);
|
|
303
313
|
}
|
|
304
314
|
|
|
305
|
-
// #region .
|
|
315
|
+
// #region .keys()
|
|
306
316
|
/**
|
|
307
|
-
* Return an array of the
|
|
317
|
+
* Return an array of the keys in the store:
|
|
308
318
|
*
|
|
309
319
|
* ```js
|
|
310
|
-
* const
|
|
311
|
-
* // ["
|
|
320
|
+
* const keys = await store.keys();
|
|
321
|
+
* // ["key1", "key2", ...]
|
|
312
322
|
*
|
|
313
323
|
* // To limit it to a given prefix, use `.prefix()`:
|
|
314
|
-
* const sessions = await store.prefix("session:").
|
|
324
|
+
* const sessions = await store.prefix("session:").keys();
|
|
315
325
|
* ```
|
|
316
326
|
*
|
|
317
|
-
* **[→ Full .
|
|
318
|
-
* @returns {Promise<
|
|
327
|
+
* **[→ Full .keys() Docs](https://polystore.dev/documentation#keys)**
|
|
328
|
+
* @returns {Promise<string[]>}
|
|
319
329
|
*/
|
|
320
|
-
async
|
|
330
|
+
async keys() {
|
|
321
331
|
await this.promise;
|
|
322
332
|
|
|
323
|
-
if (this.client.
|
|
324
|
-
const list = this.client.
|
|
325
|
-
if (this.
|
|
326
|
-
|
|
327
|
-
return list
|
|
328
|
-
.filter((data) => {
|
|
329
|
-
// There's no data, so remove this
|
|
330
|
-
if (!data || data.value === null) return false;
|
|
331
|
-
|
|
332
|
-
// It never expires, so keep it
|
|
333
|
-
const { expires } = data;
|
|
334
|
-
if (expires === null) return true;
|
|
335
|
-
|
|
336
|
-
// It's expired, so remove it
|
|
337
|
-
// We cannot unfortunately evict it since we don't know the key!
|
|
338
|
-
if (expires <= now) return false;
|
|
339
|
-
|
|
340
|
-
// It's not expired, keep it
|
|
341
|
-
return true;
|
|
342
|
-
})
|
|
343
|
-
.map((data) => data.value);
|
|
333
|
+
if (this.client.keys) {
|
|
334
|
+
const list = await this.client.keys(this.PREFIX);
|
|
335
|
+
if (!this.PREFIX) return list;
|
|
336
|
+
return list.map((k) => k.slice(this.PREFIX.length));
|
|
344
337
|
}
|
|
345
338
|
|
|
346
339
|
const entries = await this.entries();
|
|
347
|
-
return entries.map((e) => e[
|
|
340
|
+
return entries.map((e) => e[0]);
|
|
348
341
|
}
|
|
349
342
|
|
|
350
|
-
// #region .
|
|
343
|
+
// #region .values()
|
|
351
344
|
/**
|
|
352
|
-
* Return an array of the
|
|
345
|
+
* Return an array of the values in the store:
|
|
353
346
|
*
|
|
354
347
|
* ```js
|
|
355
|
-
* const
|
|
356
|
-
* // ["
|
|
348
|
+
* const values = await store.values();
|
|
349
|
+
* // ["value1", { hello: "world" }, ...]
|
|
357
350
|
*
|
|
358
351
|
* // To limit it to a given prefix, use `.prefix()`:
|
|
359
|
-
* const sessions = await store.prefix("session:").
|
|
352
|
+
* const sessions = await store.prefix("session:").values();
|
|
360
353
|
* ```
|
|
361
354
|
*
|
|
362
|
-
* **[→ Full .
|
|
363
|
-
* @returns {Promise<
|
|
355
|
+
* **[→ Full .values() Docs](https://polystore.dev/documentation#values)**
|
|
356
|
+
* @returns {Promise<Value[]>}
|
|
364
357
|
*/
|
|
365
|
-
async
|
|
358
|
+
async values() {
|
|
366
359
|
await this.promise;
|
|
367
360
|
|
|
368
|
-
if (this.client.
|
|
369
|
-
const list =
|
|
370
|
-
if (
|
|
371
|
-
return list
|
|
361
|
+
if (this.client.values) {
|
|
362
|
+
const list = this.client.values(this.PREFIX);
|
|
363
|
+
if (this.client.EXPIRES) return list;
|
|
364
|
+
return list
|
|
365
|
+
.filter((data) => this.#isFresh(data))
|
|
366
|
+
.map((data) => data.value);
|
|
372
367
|
}
|
|
373
368
|
|
|
374
369
|
const entries = await this.entries();
|
|
375
|
-
return entries.map((e) => e[
|
|
370
|
+
return entries.map((e) => e[1]);
|
|
376
371
|
}
|
|
377
372
|
|
|
378
373
|
// #region .all()
|
|
@@ -458,6 +453,18 @@ class Store {
|
|
|
458
453
|
}
|
|
459
454
|
|
|
460
455
|
// #region .close()
|
|
456
|
+
/**
|
|
457
|
+
* Stop the connection to the store, if any:
|
|
458
|
+
*
|
|
459
|
+
* ```js
|
|
460
|
+
* await session.set("key1", "value1");
|
|
461
|
+
* await store.close();
|
|
462
|
+
* await session.set("key2", "value2"); // error
|
|
463
|
+
* ```
|
|
464
|
+
*
|
|
465
|
+
* **[→ Full .close() Docs](https://polystore.dev/documentation#close)**
|
|
466
|
+
* @returns {Store}
|
|
467
|
+
*/
|
|
461
468
|
async close() {
|
|
462
469
|
await this.promise;
|
|
463
470
|
|
package/src/index.test.js
CHANGED
|
@@ -11,8 +11,6 @@ import kv from "./index.js";
|
|
|
11
11
|
import customFull from "./test/customFull.js";
|
|
12
12
|
import customSimple from "./test/customSimple.js";
|
|
13
13
|
|
|
14
|
-
global.setImmediate = global.setImmediate || ((cb) => setTimeout(cb, 0));
|
|
15
|
-
|
|
16
14
|
const stores = [];
|
|
17
15
|
stores.push(["kv()", kv()]);
|
|
18
16
|
stores.push(["kv(new Map())", kv(new Map())]);
|
|
@@ -30,6 +28,7 @@ if (process.env.REDIS) {
|
|
|
30
28
|
stores.push(["kv(redis)", kv(createClient())]);
|
|
31
29
|
}
|
|
32
30
|
if (process.env.ETCD) {
|
|
31
|
+
// Note: need to add to .env "ETCD=true" and run `etcd` in the terminal
|
|
33
32
|
stores.push(["kv(new Etcd3())", kv(new Etcd3())]);
|
|
34
33
|
}
|
|
35
34
|
|
|
@@ -41,7 +40,7 @@ const delay = (t) => new Promise((done) => setTimeout(done, t));
|
|
|
41
40
|
class Base {
|
|
42
41
|
get() {}
|
|
43
42
|
set() {}
|
|
44
|
-
|
|
43
|
+
*iterate() {}
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
global.console = {
|
|
@@ -56,7 +55,7 @@ describe("potato", () => {
|
|
|
56
55
|
|
|
57
56
|
it("an empty object is not a valid store", async () => {
|
|
58
57
|
await expect(() => kv({}).get("any")).rejects.toThrow({
|
|
59
|
-
message: "A client should have at least a .get(), .set() and .
|
|
58
|
+
message: "A client should have at least a .get(), .set() and .iterate()",
|
|
60
59
|
});
|
|
61
60
|
});
|
|
62
61
|
|
|
@@ -343,6 +342,42 @@ for (let [name, store] of stores) {
|
|
|
343
342
|
expect(await store.get("a")).toBe("b");
|
|
344
343
|
});
|
|
345
344
|
|
|
345
|
+
describe("iteration", () => {
|
|
346
|
+
beforeEach(async () => {
|
|
347
|
+
await store.clear();
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("supports raw iteration", async () => {
|
|
351
|
+
await store.set("a", "b");
|
|
352
|
+
await store.set("c", "d");
|
|
353
|
+
|
|
354
|
+
const entries = [];
|
|
355
|
+
for await (const entry of store) {
|
|
356
|
+
entries.push(entry);
|
|
357
|
+
}
|
|
358
|
+
expect(entries).toEqual([
|
|
359
|
+
["a", "b"],
|
|
360
|
+
["c", "d"],
|
|
361
|
+
]);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("supports raw prefix iteration", async () => {
|
|
365
|
+
await store.set("a:a", "b");
|
|
366
|
+
await store.set("b:a", "d");
|
|
367
|
+
await store.set("a:c", "d");
|
|
368
|
+
await store.set("b:c", "d");
|
|
369
|
+
|
|
370
|
+
const entries = [];
|
|
371
|
+
for await (const entry of store.prefix("a:")) {
|
|
372
|
+
entries.push(entry);
|
|
373
|
+
}
|
|
374
|
+
expect(entries.sort()).toEqual([
|
|
375
|
+
["a", "b"],
|
|
376
|
+
["c", "d"],
|
|
377
|
+
]);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
346
381
|
describe("expires", () => {
|
|
347
382
|
// The mock implementation does NOT support expiration 😪
|
|
348
383
|
if (name === "kv(new KVNamespace())") return;
|
package/src/test/customFull.js
CHANGED
|
@@ -19,6 +19,13 @@ export default class MyClient {
|
|
|
19
19
|
delete dataSource[key];
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
*iterate(prefix) {
|
|
23
|
+
const entries = this.entries(prefix);
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
yield entry;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
22
29
|
// Filter them by the prefix, note that `prefix` will always be a string
|
|
23
30
|
entries(prefix) {
|
|
24
31
|
const entries = Object.entries(dataSource);
|
package/src/test/customSimple.js
CHANGED
|
@@ -15,9 +15,13 @@ export default class MyClient {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// Filter them by the prefix, note that `prefix` will always be a string
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
*iterate(prefix) {
|
|
19
|
+
const raw = Object.entries(dataSource);
|
|
20
|
+
const entries = prefix
|
|
21
|
+
? raw.filter(([key, value]) => key.startsWith(prefix))
|
|
22
|
+
: raw;
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
yield entry;
|
|
25
|
+
}
|
|
22
26
|
}
|
|
23
27
|
}
|
package/src/test/setup.js
CHANGED
|
@@ -2,11 +2,22 @@ import * as util from "util";
|
|
|
2
2
|
|
|
3
3
|
// ref: https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
|
|
4
4
|
// ref: https://github.com/jsdom/jsdom/issues/2524
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
if (typeof TextEncoder === "undefined") {
|
|
6
|
+
Object.defineProperty(window, "TextEncoder", {
|
|
7
|
+
writable: true,
|
|
8
|
+
value: util.TextEncoder,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
if (typeof TextDecoder === "undefined") {
|
|
12
|
+
Object.defineProperty(window, "TextDecoder", {
|
|
13
|
+
writable: true,
|
|
14
|
+
value: util.TextDecoder,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof setImmediate === "undefined") {
|
|
19
|
+
Object.defineProperty(window, "setImmediate", {
|
|
20
|
+
writable: true,
|
|
21
|
+
value: (cb) => setTimeout(cb, 0),
|
|
22
|
+
});
|
|
23
|
+
}
|