polystore 0.17.0 → 0.18.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/index.d.ts +23 -11
- package/index.js +54 -29
- package/package.json +2 -2
- package/readme.md +169 -38
package/index.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
type
|
|
2
|
-
expires?: number | null | string;
|
|
3
|
-
};
|
|
1
|
+
type Expires = number | null | string;
|
|
4
2
|
type StoreData<T extends Serializable = Serializable> = {
|
|
5
3
|
value: T;
|
|
6
4
|
expires: number | null;
|
|
@@ -9,13 +7,14 @@ type Serializable = string | number | boolean | null | (Serializable | null)[] |
|
|
|
9
7
|
[key: string]: Serializable | null;
|
|
10
8
|
};
|
|
11
9
|
interface ClientExpires {
|
|
10
|
+
TYPE: string;
|
|
12
11
|
EXPIRES: true;
|
|
13
12
|
promise?: Promise<any>;
|
|
14
13
|
test?: (client: any) => boolean;
|
|
15
14
|
get<T extends Serializable>(key: string): Promise<T | null> | T | null;
|
|
16
|
-
set<T extends Serializable>(key: string, value: T,
|
|
15
|
+
set<T extends Serializable>(key: string, value: T, expires?: Expires): Promise<any> | any;
|
|
17
16
|
iterate<T extends Serializable>(prefix: string): AsyncGenerator<[string, T], void, unknown> | Generator<[string, T], void, unknown>;
|
|
18
|
-
add?<T extends Serializable>(prefix: string, value: T,
|
|
17
|
+
add?<T extends Serializable>(prefix: string, value: T, expires?: Expires): Promise<string>;
|
|
19
18
|
has?(key: string): Promise<boolean> | boolean;
|
|
20
19
|
del?(key: string): Promise<any> | any;
|
|
21
20
|
keys?(prefix: string): Promise<string[]> | string[];
|
|
@@ -27,13 +26,14 @@ interface ClientExpires {
|
|
|
27
26
|
close?(): Promise<any> | any;
|
|
28
27
|
}
|
|
29
28
|
interface ClientNonExpires {
|
|
29
|
+
TYPE: string;
|
|
30
30
|
EXPIRES: false;
|
|
31
31
|
promise?: Promise<any>;
|
|
32
32
|
test?: (client: any) => boolean;
|
|
33
33
|
get<T extends Serializable>(key: string): Promise<StoreData<T> | null> | StoreData<T> | null;
|
|
34
|
-
set<T extends Serializable>(key: string, value: StoreData<T> | null,
|
|
34
|
+
set<T extends Serializable>(key: string, value: StoreData<T> | null, ttl?: Expires): Promise<any> | any;
|
|
35
35
|
iterate<T extends Serializable>(prefix: string): AsyncGenerator<[string, StoreData<T>], void, unknown> | Generator<[string, StoreData<T>], void, unknown>;
|
|
36
|
-
add?<T extends Serializable>(prefix: string, value: StoreData<T>,
|
|
36
|
+
add?<T extends Serializable>(prefix: string, value: StoreData<T>, ttl?: Expires): Promise<string>;
|
|
37
37
|
has?(key: string): Promise<boolean> | boolean;
|
|
38
38
|
del?(key: string): Promise<any> | any;
|
|
39
39
|
keys?(prefix: string): Promise<string[]> | string[];
|
|
@@ -51,6 +51,7 @@ declare class Store<TDefault extends Serializable = Serializable> {
|
|
|
51
51
|
PREFIX: string;
|
|
52
52
|
promise: Promise<Client> | null;
|
|
53
53
|
client: Client;
|
|
54
|
+
type: string;
|
|
54
55
|
constructor(clientPromise?: any);
|
|
55
56
|
/**
|
|
56
57
|
* Save the data on an autogenerated key, can add expiration as well:
|
|
@@ -63,8 +64,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
|
|
|
63
64
|
*
|
|
64
65
|
* **[→ Full .add() Docs](https://polystore.dev/documentation#add)**
|
|
65
66
|
*/
|
|
66
|
-
add(value: TDefault,
|
|
67
|
-
add<T extends TDefault>(value: T,
|
|
67
|
+
add(value: TDefault, ttl?: Expires): Promise<string>;
|
|
68
|
+
add<T extends TDefault>(value: T, ttl?: Expires): Promise<string>;
|
|
68
69
|
/**
|
|
69
70
|
* Save the data on the given key, can add expiration as well:
|
|
70
71
|
*
|
|
@@ -76,8 +77,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
|
|
|
76
77
|
*
|
|
77
78
|
* **[→ Full .set() Docs](https://polystore.dev/documentation#set)**
|
|
78
79
|
*/
|
|
79
|
-
set(key: string, value: TDefault,
|
|
80
|
-
set<T extends TDefault>(key: string, value: T,
|
|
80
|
+
set(key: string, value: TDefault, ttl?: Expires): Promise<string>;
|
|
81
|
+
set<T extends TDefault>(key: string, value: T, ttl?: Expires): Promise<string>;
|
|
81
82
|
/**
|
|
82
83
|
* Read a single value from the KV store:
|
|
83
84
|
*
|
|
@@ -121,6 +122,17 @@ declare class Store<TDefault extends Serializable = Serializable> {
|
|
|
121
122
|
* **[→ Full .del() Docs](https://polystore.dev/documentation#del)**
|
|
122
123
|
*/
|
|
123
124
|
del(key: string): Promise<string>;
|
|
125
|
+
/**
|
|
126
|
+
* @alias of .del(key: string)
|
|
127
|
+
* Remove a single key and its value from the store:
|
|
128
|
+
*
|
|
129
|
+
* ```js
|
|
130
|
+
* const key = await store.delete("key1");
|
|
131
|
+
* ```
|
|
132
|
+
*
|
|
133
|
+
* **[→ Full .del() Docs](https://polystore.dev/documentation#del)**
|
|
134
|
+
*/
|
|
135
|
+
delete(key: string): Promise<string>;
|
|
124
136
|
/**
|
|
125
137
|
* An iterator that goes through all of the key:value pairs in the client
|
|
126
138
|
*
|
package/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/clients/Client.ts
|
|
2
2
|
var Client = class {
|
|
3
|
+
TYPE;
|
|
3
4
|
EXPIRES = false;
|
|
4
5
|
client;
|
|
5
6
|
encode = (val) => JSON.stringify(val, null, 2);
|
|
@@ -11,6 +12,7 @@ var Client = class {
|
|
|
11
12
|
|
|
12
13
|
// src/clients/api.ts
|
|
13
14
|
var Api = class extends Client {
|
|
15
|
+
TYPE = "API";
|
|
14
16
|
// Indicate that the file handler DOES handle expirations
|
|
15
17
|
EXPIRES = true;
|
|
16
18
|
static test = (client) => typeof client === "string" && /^https?:\/\//.test(client);
|
|
@@ -25,7 +27,7 @@ var Api = class extends Client {
|
|
|
25
27
|
return this.decode(await res.text());
|
|
26
28
|
};
|
|
27
29
|
get = (key) => this.#api(key);
|
|
28
|
-
set = async (key, value,
|
|
30
|
+
set = async (key, value, expires) => {
|
|
29
31
|
const exp = typeof expires === "number" ? `?expires=${expires}` : "";
|
|
30
32
|
await this.#api(key, exp, "PUT", this.encode(value));
|
|
31
33
|
};
|
|
@@ -45,15 +47,16 @@ var Api = class extends Client {
|
|
|
45
47
|
|
|
46
48
|
// src/clients/cloudflare.ts
|
|
47
49
|
var Cloudflare = class extends Client {
|
|
50
|
+
TYPE = "CLOUDFLARE";
|
|
48
51
|
// It handles expirations natively
|
|
49
52
|
EXPIRES = true;
|
|
50
|
-
|
|
51
|
-
static test = (client) => client?.constructor?.name === "KvNamespace" || client?.constructor?.name === "EdgeKVNamespace";
|
|
53
|
+
static testKeys = ["getWithMetadata", "get", "list", "delete"];
|
|
52
54
|
get = async (key) => {
|
|
53
|
-
|
|
55
|
+
const value = await this.client.get(key);
|
|
56
|
+
return this.decode(value);
|
|
54
57
|
};
|
|
55
|
-
set = async (key, data,
|
|
56
|
-
const expirationTtl =
|
|
58
|
+
set = async (key, data, expires) => {
|
|
59
|
+
const expirationTtl = expires ? Math.round(expires) : void 0;
|
|
57
60
|
if (expirationTtl && expirationTtl < 60) {
|
|
58
61
|
throw new Error("Cloudflare's min expiration is '60s'");
|
|
59
62
|
}
|
|
@@ -93,6 +96,7 @@ var Cloudflare = class extends Client {
|
|
|
93
96
|
|
|
94
97
|
// src/clients/cookie.ts
|
|
95
98
|
var Cookie = class extends Client {
|
|
99
|
+
TYPE = "COOKIE";
|
|
96
100
|
// It handles expirations natively
|
|
97
101
|
EXPIRES = true;
|
|
98
102
|
// Check if this is the right class for the given client
|
|
@@ -118,7 +122,7 @@ var Cookie = class extends Client {
|
|
|
118
122
|
const all = this.#read();
|
|
119
123
|
return key in all ? all[key] : null;
|
|
120
124
|
};
|
|
121
|
-
set = (key, data,
|
|
125
|
+
set = (key, data, expires) => {
|
|
122
126
|
const k = encodeURIComponent(key);
|
|
123
127
|
const value = encodeURIComponent(this.encode(data ?? ""));
|
|
124
128
|
let exp = "";
|
|
@@ -128,7 +132,7 @@ var Cookie = class extends Client {
|
|
|
128
132
|
}
|
|
129
133
|
document.cookie = `${k}=${value}${exp}`;
|
|
130
134
|
};
|
|
131
|
-
del = (key) => this.set(key, "",
|
|
135
|
+
del = (key) => this.set(key, "", -100);
|
|
132
136
|
async *iterate(prefix = "") {
|
|
133
137
|
for (let [key, value] of Object.entries(this.#read())) {
|
|
134
138
|
if (!key.startsWith(prefix)) continue;
|
|
@@ -139,10 +143,11 @@ var Cookie = class extends Client {
|
|
|
139
143
|
|
|
140
144
|
// src/clients/etcd.ts
|
|
141
145
|
var Etcd = class extends Client {
|
|
146
|
+
TYPE = "ETCD3";
|
|
142
147
|
// It desn't handle expirations natively
|
|
143
148
|
EXPIRES = false;
|
|
144
149
|
// Check if this is the right class for the given client
|
|
145
|
-
static
|
|
150
|
+
static testKeys = ["leaseClient", "watchClient", "watchManager"];
|
|
146
151
|
get = async (key) => {
|
|
147
152
|
const data = await this.client.get(key).json();
|
|
148
153
|
return data;
|
|
@@ -157,14 +162,6 @@ var Etcd = class extends Client {
|
|
|
157
162
|
yield [key, await this.get(key)];
|
|
158
163
|
}
|
|
159
164
|
}
|
|
160
|
-
keys = (prefix = "") => {
|
|
161
|
-
return this.client.getAll().prefix(prefix).keys();
|
|
162
|
-
};
|
|
163
|
-
entries = async (prefix = "") => {
|
|
164
|
-
const keys = await this.keys(prefix);
|
|
165
|
-
const values = await Promise.all(keys.map((k) => this.get(k)));
|
|
166
|
-
return keys.map((k, i) => [k, values[i]]);
|
|
167
|
-
};
|
|
168
165
|
clear = async (prefix = "") => {
|
|
169
166
|
if (!prefix) return this.client.delete().all();
|
|
170
167
|
return this.client.delete().prefix(prefix);
|
|
@@ -173,6 +170,7 @@ var Etcd = class extends Client {
|
|
|
173
170
|
|
|
174
171
|
// src/clients/file.ts
|
|
175
172
|
var File = class extends Client {
|
|
173
|
+
TYPE = "FILE";
|
|
176
174
|
// It desn't handle expirations natively
|
|
177
175
|
EXPIRES = false;
|
|
178
176
|
fsp;
|
|
@@ -266,6 +264,7 @@ var noFileOk = (error) => {
|
|
|
266
264
|
throw error;
|
|
267
265
|
};
|
|
268
266
|
var Folder = class extends Client {
|
|
267
|
+
TYPE = "FOLDER";
|
|
269
268
|
// It desn't handle expirations natively
|
|
270
269
|
EXPIRES = false;
|
|
271
270
|
fsp;
|
|
@@ -311,6 +310,7 @@ var Folder = class extends Client {
|
|
|
311
310
|
|
|
312
311
|
// src/clients/forage.ts
|
|
313
312
|
var Forage = class extends Client {
|
|
313
|
+
TYPE = "FORAGE";
|
|
314
314
|
// It desn't handle expirations natively
|
|
315
315
|
EXPIRES = false;
|
|
316
316
|
// Check if this is the right class for the given client
|
|
@@ -344,10 +344,11 @@ var notFound = (error) => {
|
|
|
344
344
|
throw error;
|
|
345
345
|
};
|
|
346
346
|
var Level = class extends Client {
|
|
347
|
+
TYPE = "LEVEL";
|
|
347
348
|
// It desn't handle expirations natively
|
|
348
349
|
EXPIRES = false;
|
|
349
350
|
// Check if this is the right class for the given client
|
|
350
|
-
static
|
|
351
|
+
static testKeys = ["attachResource", "detachResource", "prependOnceListener"];
|
|
351
352
|
get = (key) => this.client.get(key, { valueEncoding }).catch(notFound);
|
|
352
353
|
set = (key, value) => this.client.put(key, value, { valueEncoding });
|
|
353
354
|
del = (key) => this.client.del(key);
|
|
@@ -378,6 +379,7 @@ var Level = class extends Client {
|
|
|
378
379
|
|
|
379
380
|
// src/clients/memory.ts
|
|
380
381
|
var Memory = class extends Client {
|
|
382
|
+
TYPE = "MEMORY";
|
|
381
383
|
// It desn't handle expirations natively
|
|
382
384
|
EXPIRES = false;
|
|
383
385
|
// Check if this is the right class for the given client
|
|
@@ -395,12 +397,13 @@ var Memory = class extends Client {
|
|
|
395
397
|
|
|
396
398
|
// src/clients/redis.ts
|
|
397
399
|
var Redis = class extends Client {
|
|
400
|
+
TYPE = "REDIS";
|
|
398
401
|
// Indicate if this client handles expirations (true = it does)
|
|
399
402
|
EXPIRES = true;
|
|
400
403
|
// Check if this is the right class for the given client
|
|
401
404
|
static test = (client) => client && client.pSubscribe && client.sSubscribe;
|
|
402
405
|
get = async (key) => this.decode(await this.client.get(key));
|
|
403
|
-
set = async (key, value,
|
|
406
|
+
set = async (key, value, expires) => {
|
|
404
407
|
const EX = expires ? Math.round(expires) : void 0;
|
|
405
408
|
return this.client.set(key, this.encode(value), { EX });
|
|
406
409
|
};
|
|
@@ -438,6 +441,7 @@ var Redis = class extends Client {
|
|
|
438
441
|
|
|
439
442
|
// src/clients/sqlite.ts
|
|
440
443
|
var SQLite = class extends Client {
|
|
444
|
+
TYPE = "SQLITE";
|
|
441
445
|
// This one is doing manual time management internally even though
|
|
442
446
|
// sqlite does not natively support expirations. This is because it does
|
|
443
447
|
// support creating a `expires_at:Date` column that makes managing
|
|
@@ -474,7 +478,7 @@ var SQLite = class extends Client {
|
|
|
474
478
|
}
|
|
475
479
|
return this.decode(row.value);
|
|
476
480
|
};
|
|
477
|
-
set = (id, data,
|
|
481
|
+
set = (id, data, expires) => {
|
|
478
482
|
const value = this.encode(data);
|
|
479
483
|
const expires_at = expires ? Date.now() + expires * 1e3 : null;
|
|
480
484
|
this.client.prepare(
|
|
@@ -524,6 +528,7 @@ ${prefix ? "AND id LIKE ?" : ""}
|
|
|
524
528
|
|
|
525
529
|
// src/clients/storage.ts
|
|
526
530
|
var WebStorage = class extends Client {
|
|
531
|
+
TYPE = "STORAGE";
|
|
527
532
|
// It desn't handle expirations natively
|
|
528
533
|
EXPIRES = false;
|
|
529
534
|
// Check if this is the right class for the given client
|
|
@@ -607,21 +612,28 @@ var Store = class _Store {
|
|
|
607
612
|
PREFIX = "";
|
|
608
613
|
promise;
|
|
609
614
|
client;
|
|
615
|
+
type = "UNKNOWN";
|
|
610
616
|
constructor(clientPromise = /* @__PURE__ */ new Map()) {
|
|
611
617
|
this.promise = Promise.resolve(clientPromise).then(async (client) => {
|
|
612
618
|
this.client = this.#find(client);
|
|
613
619
|
this.#validate(this.client);
|
|
614
620
|
this.promise = null;
|
|
615
621
|
await this.client.promise;
|
|
622
|
+
this.type = this.client?.TYPE || this.type;
|
|
616
623
|
return client;
|
|
617
624
|
});
|
|
618
625
|
}
|
|
619
626
|
#find(store) {
|
|
620
627
|
if (store instanceof _Store) return store.client;
|
|
621
628
|
for (let client of Object.values(clients_default)) {
|
|
622
|
-
if (
|
|
629
|
+
if ("test" in client && client.test(store)) {
|
|
623
630
|
return new client(store);
|
|
624
631
|
}
|
|
632
|
+
if ("testKeys" in client && typeof store === "object") {
|
|
633
|
+
if (client.testKeys.every((key) => store[key])) {
|
|
634
|
+
return new client(store);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
625
637
|
}
|
|
626
638
|
if (typeof store === "function" && /^class\s/.test(Function.prototype.toString.call(store))) {
|
|
627
639
|
return new store();
|
|
@@ -652,29 +664,29 @@ var Store = class _Store {
|
|
|
652
664
|
if (key) this.del(key);
|
|
653
665
|
return false;
|
|
654
666
|
}
|
|
655
|
-
async add(value,
|
|
667
|
+
async add(value, ttl) {
|
|
656
668
|
await this.promise;
|
|
657
|
-
let expires = parse(
|
|
669
|
+
let expires = parse(ttl);
|
|
658
670
|
if (this.client.add) {
|
|
659
671
|
if (this.client.EXPIRES) {
|
|
660
|
-
return await this.client.add(this.PREFIX, value,
|
|
672
|
+
return await this.client.add(this.PREFIX, value, expires);
|
|
661
673
|
}
|
|
662
674
|
expires = unix(expires);
|
|
663
675
|
const key2 = await this.client.add(this.PREFIX, { expires, value });
|
|
664
676
|
return key2;
|
|
665
677
|
}
|
|
666
678
|
const key = createId();
|
|
667
|
-
return this.set(key, value,
|
|
679
|
+
return this.set(key, value, expires);
|
|
668
680
|
}
|
|
669
|
-
async set(key, value,
|
|
681
|
+
async set(key, value, ttl) {
|
|
670
682
|
await this.promise;
|
|
671
683
|
const id = this.PREFIX + key;
|
|
672
|
-
let expires = parse(
|
|
684
|
+
let expires = parse(ttl);
|
|
673
685
|
if (value === null || typeof expires === "number" && expires <= 0) {
|
|
674
686
|
return this.del(key);
|
|
675
687
|
}
|
|
676
688
|
if (this.client.EXPIRES) {
|
|
677
|
-
await this.client.set(id, value,
|
|
689
|
+
await this.client.set(id, value, expires);
|
|
678
690
|
return key;
|
|
679
691
|
}
|
|
680
692
|
expires = unix(expires);
|
|
@@ -736,12 +748,25 @@ var Store = class _Store {
|
|
|
736
748
|
return key;
|
|
737
749
|
}
|
|
738
750
|
if (this.client.EXPIRES) {
|
|
739
|
-
await this.client.set(id, null,
|
|
751
|
+
await this.client.set(id, null, 0);
|
|
740
752
|
} else {
|
|
741
753
|
await this.client.set(id, null);
|
|
742
754
|
}
|
|
743
755
|
return key;
|
|
744
756
|
}
|
|
757
|
+
/**
|
|
758
|
+
* @alias of .del(key: string)
|
|
759
|
+
* Remove a single key and its value from the store:
|
|
760
|
+
*
|
|
761
|
+
* ```js
|
|
762
|
+
* const key = await store.delete("key1");
|
|
763
|
+
* ```
|
|
764
|
+
*
|
|
765
|
+
* **[→ Full .del() Docs](https://polystore.dev/documentation#del)**
|
|
766
|
+
*/
|
|
767
|
+
async delete(key) {
|
|
768
|
+
return this.del(key);
|
|
769
|
+
}
|
|
745
770
|
async *[Symbol.asyncIterator]() {
|
|
746
771
|
await this.promise;
|
|
747
772
|
if (this.client.EXPIRES) {
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polystore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "A small compatibility layer for many popular KV stores like localStorage, Redis, FileSystem, etc.",
|
|
5
|
-
"homepage": "https://polystore.dev
|
|
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",
|
package/readme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
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
|
-
A key-value library to unify the API of [many clients](#clients)
|
|
3
|
+
A key-value library to unify the API of [many clients](#clients): localStorage, Redis, FileSystem, SQLite, etc.
|
|
4
4
|
|
|
5
5
|
```js
|
|
6
6
|
import kv from "polystore";
|
|
@@ -14,8 +14,8 @@ const store4 = kv(yourOwnStore); // Create a store based on your code
|
|
|
14
14
|
These are all the methods of the [API](#api) (they are all `async`):
|
|
15
15
|
|
|
16
16
|
- [`.get(key)`](#get): read a single value, or `null` if it doesn't exist or is expired.
|
|
17
|
-
- [`.set(key, value,
|
|
18
|
-
- [`.add(value,
|
|
17
|
+
- [`.set(key, value, ttl?)`](#set): save a single value that is serializable.
|
|
18
|
+
- [`.add(value, ttl?)`](#add): save a single value with an auto-generated key.
|
|
19
19
|
- [`.has(key)`](#has): check whether a key exists and is not expired.
|
|
20
20
|
- [`.del(key)`](#del): delete a single key/value from the store.
|
|
21
21
|
- [Iterator](#iterator): go through all of the key/values one by one.
|
|
@@ -197,7 +197,7 @@ await store.del('key2');
|
|
|
197
197
|
console.log(await store.get("key2")); // null
|
|
198
198
|
|
|
199
199
|
// Expired
|
|
200
|
-
await store.set("key3", "Hello",
|
|
200
|
+
await store.set("key3", "Hello", "1s");
|
|
201
201
|
console.log(await store.get("key3")); // "Hello"
|
|
202
202
|
await new Promise((done) => setTimeout(done, 2000)); // Wait 2 seconds
|
|
203
203
|
console.log(await store.get("key3")); // null (already expired)
|
|
@@ -221,11 +221,11 @@ console.log(await session.get('key1'));
|
|
|
221
221
|
Create or update a value in the store. Will return a promise that resolves with the key when the value has been saved:
|
|
222
222
|
|
|
223
223
|
```js
|
|
224
|
-
await store.set(key: string, value: any,
|
|
224
|
+
await store.set(key: string, value: any, expires?: number|string);
|
|
225
225
|
|
|
226
226
|
await store.set("key1", "Hello World");
|
|
227
|
-
await store.set("key2", ["my", "grocery", "list"],
|
|
228
|
-
await store.set("key3", { name: "Francisco" },
|
|
227
|
+
await store.set("key2", ["my", "grocery", "list"], "1h");
|
|
228
|
+
await store.set("key3", { name: "Francisco" }, 60 * 60);
|
|
229
229
|
```
|
|
230
230
|
|
|
231
231
|
You can specify the type either at [the store level](#api) or at the method level:
|
|
@@ -268,7 +268,7 @@ In short, only JSON-serializable data is safe to store.
|
|
|
268
268
|
|
|
269
269
|
#### Expires
|
|
270
270
|
|
|
271
|
-
When the `
|
|
271
|
+
When the `ttl` option is set, it can be a number (**seconds**) or a string representing some time:
|
|
272
272
|
|
|
273
273
|
```js
|
|
274
274
|
// Valid "expire" values:
|
|
@@ -292,14 +292,14 @@ These are all the units available:
|
|
|
292
292
|
Create a value in the store with an auto-generated key. Will return a promise that resolves with the key when the value has been saved. The value needs to be serializable:
|
|
293
293
|
|
|
294
294
|
```js
|
|
295
|
-
const key:string = await store.add(value: any,
|
|
295
|
+
const key:string = await store.add(value: any, expires?: number|string);
|
|
296
296
|
|
|
297
297
|
const key1 = await store.add("Hello World");
|
|
298
|
-
const key2 = await store.add(["my", "grocery", "list"],
|
|
299
|
-
const key3 = await store.add({ name: "Francisco" },
|
|
298
|
+
const key2 = await store.add(["my", "grocery", "list"], "1h");
|
|
299
|
+
const key3 = await store.add({ name: "Francisco" }, 60 * 60);
|
|
300
300
|
```
|
|
301
301
|
|
|
302
|
-
The
|
|
302
|
+
The value and expires are similar to [`.set()`](#set), except for the lack of the first argument, since `.add()` will generate the key automatically.
|
|
303
303
|
|
|
304
304
|
The default key is 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).
|
|
305
305
|
|
|
@@ -357,7 +357,7 @@ An example of an exception of the above is when you use it as a cache, then you
|
|
|
357
357
|
async function fetchUser(id) {
|
|
358
358
|
if (!(await store.has(id))) {
|
|
359
359
|
const { data } = await axios.get(`/users/${id}`);
|
|
360
|
-
await store.set(id, data,
|
|
360
|
+
await store.set(id, data, "1h");
|
|
361
361
|
}
|
|
362
362
|
return store.get(id);
|
|
363
363
|
}
|
|
@@ -599,7 +599,7 @@ import kv from "polystore";
|
|
|
599
599
|
|
|
600
600
|
const store = kv(new Map());
|
|
601
601
|
|
|
602
|
-
await store.set("key1", "Hello world",
|
|
602
|
+
await store.set("key1", "Hello world", "1h");
|
|
603
603
|
console.log(await store.get("key1"));
|
|
604
604
|
// "Hello world"
|
|
605
605
|
```
|
|
@@ -615,7 +615,7 @@ console.log(await store.get("key1"));
|
|
|
615
615
|
|
|
616
616
|
```js
|
|
617
617
|
// GOOD - with polystore
|
|
618
|
-
await store.set("key1", { name: "Francisco" },
|
|
618
|
+
await store.set("key1", { name: "Francisco" }, "2days");
|
|
619
619
|
|
|
620
620
|
// COMPLEX - With sessionStorage
|
|
621
621
|
const data = new Map();
|
|
@@ -632,7 +632,7 @@ import kv from "polystore";
|
|
|
632
632
|
|
|
633
633
|
const store = kv(localStorage);
|
|
634
634
|
|
|
635
|
-
await store.set("key1", "Hello world",
|
|
635
|
+
await store.set("key1", "Hello world", "1h");
|
|
636
636
|
console.log(await store.get("key1"));
|
|
637
637
|
// "Hello world"
|
|
638
638
|
```
|
|
@@ -651,7 +651,7 @@ Same limitations as always apply to localStorage, if you think you are going to
|
|
|
651
651
|
|
|
652
652
|
```js
|
|
653
653
|
// GOOD - with polystore
|
|
654
|
-
await store.set("key1", { name: "Francisco" },
|
|
654
|
+
await store.set("key1", { name: "Francisco" }, "2days");
|
|
655
655
|
|
|
656
656
|
// COMPLEX - With localStorage
|
|
657
657
|
const serialValue = JSON.stringify({ name: "Francisco" });
|
|
@@ -668,7 +668,7 @@ import kv from "polystore";
|
|
|
668
668
|
|
|
669
669
|
const store = kv(sessionStorage);
|
|
670
670
|
|
|
671
|
-
await store.set("key1", "Hello world",
|
|
671
|
+
await store.set("key1", "Hello world", "1h");
|
|
672
672
|
console.log(await store.get("key1"));
|
|
673
673
|
// "Hello world"
|
|
674
674
|
```
|
|
@@ -685,7 +685,7 @@ console.log(await store.get("key1"));
|
|
|
685
685
|
|
|
686
686
|
```js
|
|
687
687
|
// GOOD - with polystore
|
|
688
|
-
await store.set("key1", { name: "Francisco" },
|
|
688
|
+
await store.set("key1", { name: "Francisco" }, "2days");
|
|
689
689
|
|
|
690
690
|
// COMPLEX - With sessionStorage
|
|
691
691
|
const serialValue = JSON.stringify({ name: "Francisco" });
|
|
@@ -702,7 +702,7 @@ import kv from "polystore";
|
|
|
702
702
|
|
|
703
703
|
const store = kv("cookie"); // just a plain string
|
|
704
704
|
|
|
705
|
-
await store.set("key1", "Hello world",
|
|
705
|
+
await store.set("key1", "Hello world", "1h");
|
|
706
706
|
console.log(await store.get("key1"));
|
|
707
707
|
// "Hello world"
|
|
708
708
|
```
|
|
@@ -731,7 +731,7 @@ import localForage from "localforage";
|
|
|
731
731
|
|
|
732
732
|
const store = kv(localForage);
|
|
733
733
|
|
|
734
|
-
await store.set("key1", "Hello world",
|
|
734
|
+
await store.set("key1", "Hello world", "1h");
|
|
735
735
|
console.log(await store.get("key1"));
|
|
736
736
|
// "Hello world"
|
|
737
737
|
```
|
|
@@ -755,7 +755,7 @@ import { createClient } from "redis";
|
|
|
755
755
|
|
|
756
756
|
const store = kv(createClient().connect());
|
|
757
757
|
|
|
758
|
-
await store.set("key1", "Hello world",
|
|
758
|
+
await store.set("key1", "Hello world", "1h");
|
|
759
759
|
console.log(await store.get("key1"));
|
|
760
760
|
// "Hello world"
|
|
761
761
|
```
|
|
@@ -790,7 +790,7 @@ import Database from "better-sqlite3";
|
|
|
790
790
|
const db = new Database("data.db")
|
|
791
791
|
const store = kv(db);
|
|
792
792
|
|
|
793
|
-
await store.set("key1", "Hello world",
|
|
793
|
+
await store.set("key1", "Hello world", "1h");
|
|
794
794
|
console.log(await store.get("key1"));
|
|
795
795
|
// "Hello world"
|
|
796
796
|
```
|
|
@@ -862,7 +862,7 @@ import kv from "polystore";
|
|
|
862
862
|
|
|
863
863
|
const store = kv("https://kv.example.com/");
|
|
864
864
|
|
|
865
|
-
await store.set("key1", "Hello world",
|
|
865
|
+
await store.set("key1", "Hello world", "1h");
|
|
866
866
|
console.log(await store.get("key1"));
|
|
867
867
|
// "Hello world"
|
|
868
868
|
```
|
|
@@ -882,7 +882,7 @@ import kv from "polystore";
|
|
|
882
882
|
// Path is "/Users/me/project/cache.json"
|
|
883
883
|
const store = kv("file:///Users/me/project/cache.json");
|
|
884
884
|
|
|
885
|
-
await store.set("key1", "Hello world",
|
|
885
|
+
await store.set("key1", "Hello world", "1h");
|
|
886
886
|
console.log(await store.get("key1"));
|
|
887
887
|
// "Hello world"
|
|
888
888
|
```
|
|
@@ -916,7 +916,7 @@ const store1 = kv(new URL(`file://${process.cwd()}/cache.json`));
|
|
|
916
916
|
|
|
917
917
|
```js
|
|
918
918
|
// GOOD - with polystore
|
|
919
|
-
await store.set("key1", { name: "Francisco" },
|
|
919
|
+
await store.set("key1", { name: "Francisco" }, "2days");
|
|
920
920
|
|
|
921
921
|
// COMPLEX - With native file managing
|
|
922
922
|
const file = './data/users.json';
|
|
@@ -937,7 +937,7 @@ import kv from "polystore";
|
|
|
937
937
|
|
|
938
938
|
const store = kv("file:///Users/me/project/data/");
|
|
939
939
|
|
|
940
|
-
await store.set("key1", "Hello world",
|
|
940
|
+
await store.set("key1", "Hello world", "1h");
|
|
941
941
|
// Writes "./data/key1.json"
|
|
942
942
|
console.log(await store.get("key1"));
|
|
943
943
|
// "Hello world"
|
|
@@ -975,7 +975,7 @@ const store1 = kv(new URL(`file://${process.cwd()}/cache/`));
|
|
|
975
975
|
|
|
976
976
|
```js
|
|
977
977
|
// GOOD - with polystore
|
|
978
|
-
await store.set("key1", { name: "Francisco" },
|
|
978
|
+
await store.set("key1", { name: "Francisco" }, "2days");
|
|
979
979
|
|
|
980
980
|
// COMPLEX - With native folder
|
|
981
981
|
const file = './data/user/key1.json';
|
|
@@ -995,7 +995,7 @@ export default {
|
|
|
995
995
|
async fetch(request, env, ctx) {
|
|
996
996
|
const store = kv(env.YOUR_KV_NAMESPACE);
|
|
997
997
|
|
|
998
|
-
await store.set("key1", "Hello world",
|
|
998
|
+
await store.set("key1", "Hello world", "1h");
|
|
999
999
|
console.log(await store.get("key1"));
|
|
1000
1000
|
// "Hello world"
|
|
1001
1001
|
|
|
@@ -1018,7 +1018,7 @@ It expects that you pass the namespace from Cloudflare straight as a `kv()` argu
|
|
|
1018
1018
|
|
|
1019
1019
|
```js
|
|
1020
1020
|
// GOOD - with polystore
|
|
1021
|
-
await store.set("user", { name: "Francisco" },
|
|
1021
|
+
await store.set("user", { name: "Francisco" }, "2days");
|
|
1022
1022
|
|
|
1023
1023
|
// COMPLEX - With native Cloudflare KV
|
|
1024
1024
|
const serialValue = JSON.stringify({ name: "Francisco" });
|
|
@@ -1038,7 +1038,7 @@ import { Level } from "level";
|
|
|
1038
1038
|
|
|
1039
1039
|
const store = kv(new Level("example", { valueEncoding: "json" }));
|
|
1040
1040
|
|
|
1041
|
-
await store.set("key1", "Hello world",
|
|
1041
|
+
await store.set("key1", "Hello world", "1h");
|
|
1042
1042
|
console.log(await store.get("key1"));
|
|
1043
1043
|
// "Hello world"
|
|
1044
1044
|
```
|
|
@@ -1055,7 +1055,7 @@ You will need to set the `valueEncoding` to `"json"` for the store to work as ex
|
|
|
1055
1055
|
|
|
1056
1056
|
```js
|
|
1057
1057
|
// GOOD - with polystore
|
|
1058
|
-
await store.set("user", { hello: 'world' },
|
|
1058
|
+
await store.set("user", { hello: 'world' }, "2days");
|
|
1059
1059
|
|
|
1060
1060
|
// With Level:
|
|
1061
1061
|
?? // Just not possible
|
|
@@ -1071,7 +1071,7 @@ import { Etcd3 } from "etcd3";
|
|
|
1071
1071
|
|
|
1072
1072
|
const store = kv(new Etcd3());
|
|
1073
1073
|
|
|
1074
|
-
await store.set("key1", "Hello world",
|
|
1074
|
+
await store.set("key1", "Hello world", "1h");
|
|
1075
1075
|
console.log(await store.get("key1"));
|
|
1076
1076
|
// "Hello world"
|
|
1077
1077
|
```
|
|
@@ -1101,7 +1101,7 @@ await client.connect();
|
|
|
1101
1101
|
|
|
1102
1102
|
const store = kv(client);
|
|
1103
1103
|
|
|
1104
|
-
await store.set("key1", "Hello world",
|
|
1104
|
+
await store.set("key1", "Hello world", "1h");
|
|
1105
1105
|
console.log(await store.get("key1"));
|
|
1106
1106
|
// "Hello world"
|
|
1107
1107
|
```
|
|
@@ -1165,7 +1165,7 @@ We unify all of the clients diverse expiration methods into a single, easy one w
|
|
|
1165
1165
|
```js
|
|
1166
1166
|
// in-memory store
|
|
1167
1167
|
const store = polystore(new Map());
|
|
1168
|
-
await store.set("a", "b",
|
|
1168
|
+
await store.set("a", "b", "1s");
|
|
1169
1169
|
|
|
1170
1170
|
// These checks of course work:
|
|
1171
1171
|
console.log(await store.keys()); // ['a']
|
|
@@ -1234,11 +1234,11 @@ class MyClient {
|
|
|
1234
1234
|
|
|
1235
1235
|
// Mandatory methods
|
|
1236
1236
|
get (key): Promise<any>;
|
|
1237
|
-
set (key, value,
|
|
1237
|
+
set (key, value, null|number): Promise<null>;
|
|
1238
1238
|
iterate(prefix): AyncIterator<[string, any]>
|
|
1239
1239
|
|
|
1240
1240
|
// Optional item methods (for optimization or customization)
|
|
1241
|
-
add (prefix, data,
|
|
1241
|
+
add (prefix, data, null|number): Promise<string>;
|
|
1242
1242
|
has (key): Promise<boolean>;
|
|
1243
1243
|
del (key): Promise<null>;
|
|
1244
1244
|
|
|
@@ -1283,7 +1283,7 @@ client.keys = (prefix) => {
|
|
|
1283
1283
|
|
|
1284
1284
|
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).
|
|
1285
1285
|
|
|
1286
|
-
###
|
|
1286
|
+
### Plain Object client
|
|
1287
1287
|
|
|
1288
1288
|
This is a good example of how simple a store can be, however do not use it literally since it behaves the same as the already-supported `new Map()`, only use it as the base for your own clients:
|
|
1289
1289
|
|
|
@@ -1442,3 +1442,134 @@ const store = kv(CloudflareCustom);
|
|
|
1442
1442
|
````
|
|
1443
1443
|
|
|
1444
1444
|
It's lacking few things, so make sure to adapt to your needs, but it worked for my very simple cache needs.
|
|
1445
|
+
|
|
1446
|
+
|
|
1447
|
+
## Examples
|
|
1448
|
+
|
|
1449
|
+
### Simple cache
|
|
1450
|
+
|
|
1451
|
+
I've used Polystore in many projects as a simple cache. With `fetch()`, it's fairly easy:
|
|
1452
|
+
|
|
1453
|
+
```ts
|
|
1454
|
+
async function getProductInfo(id: string) {
|
|
1455
|
+
const data = await store.get(id);
|
|
1456
|
+
if (data) return data;
|
|
1457
|
+
|
|
1458
|
+
const res = await fetch(`https://some-url.com/products/${id}`);
|
|
1459
|
+
const raw = await res.json();
|
|
1460
|
+
|
|
1461
|
+
// Some processing here
|
|
1462
|
+
const clean = raw??;
|
|
1463
|
+
|
|
1464
|
+
await store.set(id, clean, '10days');
|
|
1465
|
+
return clean;
|
|
1466
|
+
}
|
|
1467
|
+
```
|
|
1468
|
+
|
|
1469
|
+
You can store either the raw data, or the processed data. Depending on whether the processing is sync or async, and the data size of the raw vs processing data, we pick one or the other.
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
### Dev vs Prod
|
|
1474
|
+
|
|
1475
|
+
With Polystore it's easy to configure your KV solution to use a different client in dev vs production. We've found particularly useful to use an easy-to-debug client in dev like [Folder](#folder) and a high-performance client in production like [Redis](#redis):
|
|
1476
|
+
|
|
1477
|
+
```ts
|
|
1478
|
+
// store.ts
|
|
1479
|
+
import kv from "polystore";
|
|
1480
|
+
import { createClient } from "redis";
|
|
1481
|
+
|
|
1482
|
+
let store;
|
|
1483
|
+
if (process.env.REDIS_URL) {
|
|
1484
|
+
console.log('kv:redis using Redis for cache data');
|
|
1485
|
+
store = kv(createClient(process.env.REDIS_URL).connect());
|
|
1486
|
+
} else {
|
|
1487
|
+
console.log('kv:folder using a folder for cache data');
|
|
1488
|
+
store = kv(`${process.cwd()}/data/`);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
export default store;
|
|
1492
|
+
```
|
|
1493
|
+
|
|
1494
|
+
|
|
1495
|
+
### Better Auth
|
|
1496
|
+
|
|
1497
|
+
Polystore is directly compatible with Better Auth, so no need for any wrapper. For example, for Redis:
|
|
1498
|
+
|
|
1499
|
+
```ts
|
|
1500
|
+
import kv from "polystore";
|
|
1501
|
+
import { createClient } from "redis";
|
|
1502
|
+
import { betterAuth } from "better-auth";
|
|
1503
|
+
|
|
1504
|
+
const secondaryStorage = kv(createClient().connect());
|
|
1505
|
+
|
|
1506
|
+
export const auth = betterAuth({
|
|
1507
|
+
// ... other options
|
|
1508
|
+
secondaryStorage
|
|
1509
|
+
});
|
|
1510
|
+
````
|
|
1511
|
+
|
|
1512
|
+
Compare that with their official documentation for using Redis:
|
|
1513
|
+
|
|
1514
|
+
```ts
|
|
1515
|
+
import { createClient } from "redis";
|
|
1516
|
+
import { betterAuth } from "better-auth";
|
|
1517
|
+
|
|
1518
|
+
const redis = createClient();
|
|
1519
|
+
await redis.connect();
|
|
1520
|
+
|
|
1521
|
+
export const auth = betterAuth({
|
|
1522
|
+
// ... other options
|
|
1523
|
+
secondaryStorage: {
|
|
1524
|
+
get: async (key) => {
|
|
1525
|
+
return await redis.get(key);
|
|
1526
|
+
},
|
|
1527
|
+
set: async (key, value, ttl) => {
|
|
1528
|
+
if (ttl) await redis.set(key, value, { EX: ttl });
|
|
1529
|
+
// or for ioredis:
|
|
1530
|
+
// if (ttl) await redis.set(key, value, 'EX', ttl)
|
|
1531
|
+
else await redis.set(key, value);
|
|
1532
|
+
},
|
|
1533
|
+
delete: async (key) => {
|
|
1534
|
+
await redis.del(key);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
```
|
|
1539
|
+
|
|
1540
|
+
### Express.js
|
|
1541
|
+
|
|
1542
|
+
```ts
|
|
1543
|
+
import kv from 'polystore/express';
|
|
1544
|
+
|
|
1545
|
+
// This is a special, Express-only store
|
|
1546
|
+
const store = kv();
|
|
1547
|
+
|
|
1548
|
+
app.use(session({ store, ... }));
|
|
1549
|
+
````
|
|
1550
|
+
|
|
1551
|
+
### Hono
|
|
1552
|
+
|
|
1553
|
+
### Elysia?
|
|
1554
|
+
|
|
1555
|
+
### Axios Cache Interceptor
|
|
1556
|
+
|
|
1557
|
+
### @server/next
|
|
1558
|
+
|
|
1559
|
+
> [!info]
|
|
1560
|
+
> @server/next is still experimental, but it's the main reason I created Polystore and so I wanted to document it as well
|
|
1561
|
+
|
|
1562
|
+
Server.js supports Polystore directly:
|
|
1563
|
+
|
|
1564
|
+
```ts
|
|
1565
|
+
import kv from "polystore";
|
|
1566
|
+
import server from "../../";
|
|
1567
|
+
|
|
1568
|
+
const session = kv(new Map());
|
|
1569
|
+
|
|
1570
|
+
export default server({ session }).get("/", (ctx) => {
|
|
1571
|
+
if (!ctx.session.counter) ctx.session.counter = 0;
|
|
1572
|
+
ctx.session.counter++;
|
|
1573
|
+
return `User visited ${ctx.session.counter} times`;
|
|
1574
|
+
});
|
|
1575
|
+
```
|