polystore 0.18.0 → 0.19.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 +59 -27
- package/index.js +108 -59
- package/package.json +10 -2
- package/readme.md +145 -150
- package/src/express.js +47 -0
package/index.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
type Prefix = string;
|
|
1
2
|
type Expires = number | null | string;
|
|
3
|
+
type Options = {
|
|
4
|
+
prefix?: Prefix;
|
|
5
|
+
expires?: Expires;
|
|
6
|
+
};
|
|
2
7
|
type StoreData<T extends Serializable = Serializable> = {
|
|
3
8
|
value: T;
|
|
4
9
|
expires: number | null;
|
|
@@ -8,7 +13,7 @@ type Serializable = string | number | boolean | null | (Serializable | null)[] |
|
|
|
8
13
|
};
|
|
9
14
|
interface ClientExpires {
|
|
10
15
|
TYPE: string;
|
|
11
|
-
|
|
16
|
+
HAS_EXPIRATION: true;
|
|
12
17
|
promise?: Promise<any>;
|
|
13
18
|
test?: (client: any) => boolean;
|
|
14
19
|
get<T extends Serializable>(key: string): Promise<T | null> | T | null;
|
|
@@ -27,7 +32,7 @@ interface ClientExpires {
|
|
|
27
32
|
}
|
|
28
33
|
interface ClientNonExpires {
|
|
29
34
|
TYPE: string;
|
|
30
|
-
|
|
35
|
+
HAS_EXPIRATION: false;
|
|
31
36
|
promise?: Promise<any>;
|
|
32
37
|
test?: (client: any) => boolean;
|
|
33
38
|
get<T extends Serializable>(key: string): Promise<StoreData<T> | null> | StoreData<T> | null;
|
|
@@ -46,13 +51,14 @@ interface ClientNonExpires {
|
|
|
46
51
|
}
|
|
47
52
|
type Client = ClientExpires | ClientNonExpires;
|
|
48
53
|
|
|
49
|
-
declare class Store<
|
|
54
|
+
declare class Store<TD extends Serializable = Serializable> {
|
|
50
55
|
#private;
|
|
51
|
-
PREFIX:
|
|
56
|
+
PREFIX: Prefix;
|
|
57
|
+
EXPIRES: Expires;
|
|
52
58
|
promise: Promise<Client> | null;
|
|
53
59
|
client: Client;
|
|
54
60
|
type: string;
|
|
55
|
-
constructor(clientPromise?: any);
|
|
61
|
+
constructor(clientPromise?: any, options?: Options);
|
|
56
62
|
/**
|
|
57
63
|
* Save the data on an autogenerated key, can add expiration as well:
|
|
58
64
|
*
|
|
@@ -64,8 +70,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
|
|
|
64
70
|
*
|
|
65
71
|
* **[→ Full .add() Docs](https://polystore.dev/documentation#add)**
|
|
66
72
|
*/
|
|
67
|
-
add(value:
|
|
68
|
-
add<T extends
|
|
73
|
+
add(value: TD, options?: Options): Promise<string>;
|
|
74
|
+
add<T extends TD>(value: T, options?: Options): Promise<string>;
|
|
69
75
|
/**
|
|
70
76
|
* Save the data on the given key, can add expiration as well:
|
|
71
77
|
*
|
|
@@ -77,8 +83,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
|
|
|
77
83
|
*
|
|
78
84
|
* **[→ Full .set() Docs](https://polystore.dev/documentation#set)**
|
|
79
85
|
*/
|
|
80
|
-
set(key: string, value:
|
|
81
|
-
set<T extends
|
|
86
|
+
set(key: string, value: TD, options?: Options): Promise<string>;
|
|
87
|
+
set<T extends TD>(key: string, value: T, options?: Options): Promise<string>;
|
|
82
88
|
/**
|
|
83
89
|
* Read a single value from the KV store:
|
|
84
90
|
*
|
|
@@ -93,8 +99,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
|
|
|
93
99
|
*
|
|
94
100
|
* **[→ Full .get() Docs](https://polystore.dev/documentation#get)**
|
|
95
101
|
*/
|
|
96
|
-
get(key: string): Promise<
|
|
97
|
-
get<T extends
|
|
102
|
+
get(key: string): Promise<TD | null>;
|
|
103
|
+
get<T extends TD>(key: string): Promise<T | null>;
|
|
98
104
|
/**
|
|
99
105
|
* Check whether a key exists or not:
|
|
100
106
|
*
|
|
@@ -144,8 +150,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
|
|
|
144
150
|
*
|
|
145
151
|
* **[→ Full Iterator Docs](https://polystore.dev/documentation#iterator)**
|
|
146
152
|
*/
|
|
147
|
-
[Symbol.asyncIterator](): AsyncGenerator<[string,
|
|
148
|
-
[Symbol.asyncIterator]<T extends
|
|
153
|
+
[Symbol.asyncIterator](): AsyncGenerator<[string, TD], void, unknown>;
|
|
154
|
+
[Symbol.asyncIterator]<T extends TD>(): AsyncGenerator<[
|
|
149
155
|
string,
|
|
150
156
|
T
|
|
151
157
|
], void, unknown>;
|
|
@@ -162,8 +168,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
|
|
|
162
168
|
*
|
|
163
169
|
* **[→ Full .entries() Docs](https://polystore.dev/documentation#entries)**
|
|
164
170
|
*/
|
|
165
|
-
entries(): Promise<[string,
|
|
166
|
-
entries<T extends
|
|
171
|
+
entries(): Promise<[string, TD][]>;
|
|
172
|
+
entries<T extends TD>(): Promise<[string, T][]>;
|
|
167
173
|
/**
|
|
168
174
|
* Return an array of the keys in the store:
|
|
169
175
|
*
|
|
@@ -191,8 +197,8 @@ declare class Store<TDefault extends Serializable = Serializable> {
|
|
|
191
197
|
*
|
|
192
198
|
* **[→ Full .values() Docs](https://polystore.dev/documentation#values)**
|
|
193
199
|
*/
|
|
194
|
-
values(): Promise<
|
|
195
|
-
values<T extends
|
|
200
|
+
values(): Promise<TD[]>;
|
|
201
|
+
values<T extends TD>(): Promise<T[]>;
|
|
196
202
|
/**
|
|
197
203
|
* Return an object with the keys:values in the store:
|
|
198
204
|
*
|
|
@@ -206,20 +212,24 @@ declare class Store<TDefault extends Serializable = Serializable> {
|
|
|
206
212
|
*
|
|
207
213
|
* **[→ Full .all() Docs](https://polystore.dev/documentation#all)**
|
|
208
214
|
*/
|
|
209
|
-
all(): Promise<Record<string,
|
|
210
|
-
all<T extends
|
|
215
|
+
all(): Promise<Record<string, TD>>;
|
|
216
|
+
all<T extends TD>(): Promise<Record<string, T>>;
|
|
211
217
|
/**
|
|
212
|
-
*
|
|
218
|
+
* Create a substore where all the keys are stored with
|
|
219
|
+
* the given prefix:
|
|
213
220
|
*
|
|
214
221
|
* ```js
|
|
215
|
-
*
|
|
222
|
+
* const session = store.prefix("session:");
|
|
223
|
+
* await session.set("key1", "value1");
|
|
224
|
+
* console.log(await session.entries()); // session.
|
|
225
|
+
* // [["key1", "value1"]]
|
|
226
|
+
* console.log(await store.entries()); // store.
|
|
227
|
+
* // [["session:key1", "value1"]]
|
|
216
228
|
* ```
|
|
217
229
|
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
* **[→ Full .clear() Docs](https://polystore.dev/documentation#clear)**
|
|
230
|
+
* **[→ Full .prefix() Docs](https://polystore.dev/documentation#prefix)**
|
|
221
231
|
*/
|
|
222
|
-
|
|
232
|
+
prefix(prefix?: Prefix): Store<TD>;
|
|
223
233
|
/**
|
|
224
234
|
* Create a substore where all the keys are stored with
|
|
225
235
|
* the given prefix:
|
|
@@ -235,7 +245,29 @@ declare class Store<TDefault extends Serializable = Serializable> {
|
|
|
235
245
|
*
|
|
236
246
|
* **[→ Full .prefix() Docs](https://polystore.dev/documentation#prefix)**
|
|
237
247
|
*/
|
|
238
|
-
|
|
248
|
+
expires(expires?: Expires): Store<TD>;
|
|
249
|
+
/**
|
|
250
|
+
* Delete all of the records of the store:
|
|
251
|
+
*
|
|
252
|
+
* ```js
|
|
253
|
+
* await store.clear();
|
|
254
|
+
* ```
|
|
255
|
+
*
|
|
256
|
+
* It's useful for cache invalidation, clearing the data, and testing.
|
|
257
|
+
*
|
|
258
|
+
* **[→ Full .clear() Docs](https://polystore.dev/documentation#clear)**
|
|
259
|
+
*/
|
|
260
|
+
clear(): Promise<void>;
|
|
261
|
+
/**
|
|
262
|
+
* Remove all expired records from the store.
|
|
263
|
+
*
|
|
264
|
+
* ```js
|
|
265
|
+
* await store.prune();
|
|
266
|
+
* ```
|
|
267
|
+
*
|
|
268
|
+
* Only affects stores where expiration is managed by this wrapper.
|
|
269
|
+
*/
|
|
270
|
+
prune(): Promise<void>;
|
|
239
271
|
/**
|
|
240
272
|
* Stop the connection to the store, if any:
|
|
241
273
|
*
|
|
@@ -250,6 +282,6 @@ declare class Store<TDefault extends Serializable = Serializable> {
|
|
|
250
282
|
close(): Promise<void>;
|
|
251
283
|
}
|
|
252
284
|
declare function createStore(): Store<Serializable>;
|
|
253
|
-
declare function createStore<T extends Serializable = Serializable>(client?: any): Store<T>;
|
|
285
|
+
declare function createStore<T extends Serializable = Serializable>(client?: any, options?: Options): Store<T>;
|
|
254
286
|
|
|
255
287
|
export { type Client, type Serializable, Store, createStore as default };
|
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/clients/Client.ts
|
|
2
2
|
var Client = class {
|
|
3
3
|
TYPE;
|
|
4
|
-
|
|
4
|
+
HAS_EXPIRATION = false;
|
|
5
5
|
client;
|
|
6
6
|
encode = (val) => JSON.stringify(val, null, 2);
|
|
7
7
|
decode = (val) => val ? JSON.parse(val) : null;
|
|
@@ -14,7 +14,7 @@ var Client = class {
|
|
|
14
14
|
var Api = class extends Client {
|
|
15
15
|
TYPE = "API";
|
|
16
16
|
// Indicate that the file handler DOES handle expirations
|
|
17
|
-
|
|
17
|
+
HAS_EXPIRATION = true;
|
|
18
18
|
static test = (client) => typeof client === "string" && /^https?:\/\//.test(client);
|
|
19
19
|
#api = async (key, opts = "", method = "GET", body) => {
|
|
20
20
|
const url = `${this.client.replace(/\/$/, "")}/${encodeURIComponent(key)}${opts}`;
|
|
@@ -49,7 +49,7 @@ var Api = class extends Client {
|
|
|
49
49
|
var Cloudflare = class extends Client {
|
|
50
50
|
TYPE = "CLOUDFLARE";
|
|
51
51
|
// It handles expirations natively
|
|
52
|
-
|
|
52
|
+
HAS_EXPIRATION = true;
|
|
53
53
|
static testKeys = ["getWithMetadata", "get", "list", "delete"];
|
|
54
54
|
get = async (key) => {
|
|
55
55
|
const value = await this.client.get(key);
|
|
@@ -98,7 +98,7 @@ var Cloudflare = class extends Client {
|
|
|
98
98
|
var Cookie = class extends Client {
|
|
99
99
|
TYPE = "COOKIE";
|
|
100
100
|
// It handles expirations natively
|
|
101
|
-
|
|
101
|
+
HAS_EXPIRATION = true;
|
|
102
102
|
// Check if this is the right class for the given client
|
|
103
103
|
static test = (client) => {
|
|
104
104
|
return client === "cookie" || client === "cookies";
|
|
@@ -145,7 +145,7 @@ var Cookie = class extends Client {
|
|
|
145
145
|
var Etcd = class extends Client {
|
|
146
146
|
TYPE = "ETCD3";
|
|
147
147
|
// It desn't handle expirations natively
|
|
148
|
-
|
|
148
|
+
HAS_EXPIRATION = false;
|
|
149
149
|
// Check if this is the right class for the given client
|
|
150
150
|
static testKeys = ["leaseClient", "watchClient", "watchManager"];
|
|
151
151
|
get = async (key) => {
|
|
@@ -172,7 +172,7 @@ var Etcd = class extends Client {
|
|
|
172
172
|
var File = class extends Client {
|
|
173
173
|
TYPE = "FILE";
|
|
174
174
|
// It desn't handle expirations natively
|
|
175
|
-
|
|
175
|
+
HAS_EXPIRATION = false;
|
|
176
176
|
fsp;
|
|
177
177
|
file = "";
|
|
178
178
|
#lock = Promise.resolve();
|
|
@@ -266,7 +266,7 @@ var noFileOk = (error) => {
|
|
|
266
266
|
var Folder = class extends Client {
|
|
267
267
|
TYPE = "FOLDER";
|
|
268
268
|
// It desn't handle expirations natively
|
|
269
|
-
|
|
269
|
+
HAS_EXPIRATION = false;
|
|
270
270
|
fsp;
|
|
271
271
|
folder;
|
|
272
272
|
// Check if this is the right class for the given client
|
|
@@ -312,7 +312,7 @@ var Folder = class extends Client {
|
|
|
312
312
|
var Forage = class extends Client {
|
|
313
313
|
TYPE = "FORAGE";
|
|
314
314
|
// It desn't handle expirations natively
|
|
315
|
-
|
|
315
|
+
HAS_EXPIRATION = false;
|
|
316
316
|
// Check if this is the right class for the given client
|
|
317
317
|
static test = (client) => client?.defineDriver && client?.dropInstance && client?.INDEXEDDB;
|
|
318
318
|
get = (key) => this.client.getItem(key);
|
|
@@ -346,7 +346,7 @@ var notFound = (error) => {
|
|
|
346
346
|
var Level = class extends Client {
|
|
347
347
|
TYPE = "LEVEL";
|
|
348
348
|
// It desn't handle expirations natively
|
|
349
|
-
|
|
349
|
+
HAS_EXPIRATION = false;
|
|
350
350
|
// Check if this is the right class for the given client
|
|
351
351
|
static testKeys = ["attachResource", "detachResource", "prependOnceListener"];
|
|
352
352
|
get = (key) => this.client.get(key, { valueEncoding }).catch(notFound);
|
|
@@ -381,7 +381,7 @@ var Level = class extends Client {
|
|
|
381
381
|
var Memory = class extends Client {
|
|
382
382
|
TYPE = "MEMORY";
|
|
383
383
|
// It desn't handle expirations natively
|
|
384
|
-
|
|
384
|
+
HAS_EXPIRATION = false;
|
|
385
385
|
// Check if this is the right class for the given client
|
|
386
386
|
static test = (client) => client instanceof Map;
|
|
387
387
|
get = (key) => this.client.get(key) ?? null;
|
|
@@ -399,7 +399,7 @@ var Memory = class extends Client {
|
|
|
399
399
|
var Redis = class extends Client {
|
|
400
400
|
TYPE = "REDIS";
|
|
401
401
|
// Indicate if this client handles expirations (true = it does)
|
|
402
|
-
|
|
402
|
+
HAS_EXPIRATION = true;
|
|
403
403
|
// Check if this is the right class for the given client
|
|
404
404
|
static test = (client) => client && client.pSubscribe && client.sSubscribe;
|
|
405
405
|
get = async (key) => this.decode(await this.client.get(key));
|
|
@@ -446,7 +446,7 @@ var SQLite = class extends Client {
|
|
|
446
446
|
// sqlite does not natively support expirations. This is because it does
|
|
447
447
|
// support creating a `expires_at:Date` column that makes managing
|
|
448
448
|
// expirations much easier, so it's really "somewhere in between"
|
|
449
|
-
|
|
449
|
+
HAS_EXPIRATION = true;
|
|
450
450
|
// The table name to use
|
|
451
451
|
table = "kv";
|
|
452
452
|
// Make sure the folder already exists, so attempt to create it
|
|
@@ -530,7 +530,7 @@ ${prefix ? "AND id LIKE ?" : ""}
|
|
|
530
530
|
var WebStorage = class extends Client {
|
|
531
531
|
TYPE = "STORAGE";
|
|
532
532
|
// It desn't handle expirations natively
|
|
533
|
-
|
|
533
|
+
HAS_EXPIRATION = false;
|
|
534
534
|
// Check if this is the right class for the given client
|
|
535
535
|
static test(client) {
|
|
536
536
|
if (typeof Storage === "undefined") return false;
|
|
@@ -610,10 +610,16 @@ function unix(expires) {
|
|
|
610
610
|
// src/index.ts
|
|
611
611
|
var Store = class _Store {
|
|
612
612
|
PREFIX = "";
|
|
613
|
+
EXPIRES = null;
|
|
613
614
|
promise;
|
|
614
615
|
client;
|
|
615
616
|
type = "UNKNOWN";
|
|
616
|
-
constructor(clientPromise = /* @__PURE__ */ new Map()
|
|
617
|
+
constructor(clientPromise = /* @__PURE__ */ new Map(), options = {
|
|
618
|
+
prefix: "",
|
|
619
|
+
expires: null
|
|
620
|
+
}) {
|
|
621
|
+
this.PREFIX = options.prefix || "";
|
|
622
|
+
this.EXPIRES = parse(options.expires || null);
|
|
617
623
|
this.promise = Promise.resolve(clientPromise).then(async (client) => {
|
|
618
624
|
this.client = this.#find(client);
|
|
619
625
|
this.#validate(this.client);
|
|
@@ -645,7 +651,7 @@ var Store = class _Store {
|
|
|
645
651
|
if (!client.set || !client.get || !client.iterate) {
|
|
646
652
|
throw new Error("Client should have .get(), .set() and .iterate()");
|
|
647
653
|
}
|
|
648
|
-
if (client.
|
|
654
|
+
if (client.HAS_EXPIRATION) return;
|
|
649
655
|
for (let method of ["has", "keys", "values"]) {
|
|
650
656
|
if (client[method]) {
|
|
651
657
|
const msg = `You can only define client.${method}() when the client manages the expiration.`;
|
|
@@ -653,50 +659,48 @@ var Store = class _Store {
|
|
|
653
659
|
}
|
|
654
660
|
}
|
|
655
661
|
}
|
|
656
|
-
// Check if the given data is fresh or not
|
|
662
|
+
// Check if the given data is fresh or not
|
|
657
663
|
#isFresh(data, key) {
|
|
658
664
|
if (!data || typeof data !== "object" || !("value" in data)) {
|
|
659
|
-
if (key) this.del(key);
|
|
660
665
|
return false;
|
|
661
666
|
}
|
|
662
|
-
|
|
663
|
-
if (data.expires > Date.now()) return true;
|
|
664
|
-
if (key) this.del(key);
|
|
665
|
-
return false;
|
|
667
|
+
return data.expires === null || data.expires > Date.now();
|
|
666
668
|
}
|
|
667
|
-
|
|
669
|
+
// Normalize returns the instance's `prefix` and `expires`
|
|
670
|
+
#expiration(expires) {
|
|
671
|
+
return parse(expires !== void 0 ? expires : this.EXPIRES);
|
|
672
|
+
}
|
|
673
|
+
async add(value, options) {
|
|
668
674
|
await this.promise;
|
|
669
|
-
|
|
675
|
+
const expires = this.#expiration(options?.expires);
|
|
676
|
+
const prefix = options?.prefix || this.PREFIX;
|
|
670
677
|
if (this.client.add) {
|
|
671
|
-
if (this.client.
|
|
672
|
-
return
|
|
678
|
+
if (this.client.HAS_EXPIRATION) {
|
|
679
|
+
return this.client.add(prefix, value, expires);
|
|
673
680
|
}
|
|
674
|
-
expires
|
|
675
|
-
const key2 = await this.client.add(this.PREFIX, { expires, value });
|
|
676
|
-
return key2;
|
|
681
|
+
return this.client.add(prefix, { expires: unix(expires), value });
|
|
677
682
|
}
|
|
678
|
-
|
|
679
|
-
return this.set(key, value, expires);
|
|
683
|
+
return this.set(createId(), value, { prefix, expires });
|
|
680
684
|
}
|
|
681
|
-
async set(key, value,
|
|
685
|
+
async set(key, value, options) {
|
|
682
686
|
await this.promise;
|
|
683
|
-
const
|
|
684
|
-
|
|
687
|
+
const expires = this.#expiration(options?.expires);
|
|
688
|
+
const prefix = options?.prefix || this.PREFIX;
|
|
689
|
+
const id = prefix + key;
|
|
685
690
|
if (value === null || typeof expires === "number" && expires <= 0) {
|
|
686
691
|
return this.del(key);
|
|
687
692
|
}
|
|
688
|
-
if (this.client.
|
|
693
|
+
if (this.client.HAS_EXPIRATION) {
|
|
689
694
|
await this.client.set(id, value, expires);
|
|
690
695
|
return key;
|
|
691
696
|
}
|
|
692
|
-
expires
|
|
693
|
-
await this.client.set(id, { expires, value });
|
|
697
|
+
await this.client.set(id, { expires: unix(expires), value });
|
|
694
698
|
return key;
|
|
695
699
|
}
|
|
696
700
|
async get(key) {
|
|
697
701
|
await this.promise;
|
|
698
702
|
const id = this.PREFIX + key;
|
|
699
|
-
if (this.client.
|
|
703
|
+
if (this.client.HAS_EXPIRATION) {
|
|
700
704
|
const data = await this.client.get(id) ?? null;
|
|
701
705
|
if (data === null) return null;
|
|
702
706
|
return data;
|
|
@@ -747,7 +751,7 @@ var Store = class _Store {
|
|
|
747
751
|
await this.client.del(id);
|
|
748
752
|
return key;
|
|
749
753
|
}
|
|
750
|
-
if (this.client.
|
|
754
|
+
if (this.client.HAS_EXPIRATION) {
|
|
751
755
|
await this.client.set(id, null, 0);
|
|
752
756
|
} else {
|
|
753
757
|
await this.client.set(id, null);
|
|
@@ -769,7 +773,7 @@ var Store = class _Store {
|
|
|
769
773
|
}
|
|
770
774
|
async *[Symbol.asyncIterator]() {
|
|
771
775
|
await this.promise;
|
|
772
|
-
if (this.client.
|
|
776
|
+
if (this.client.HAS_EXPIRATION) {
|
|
773
777
|
for await (const [name, data] of this.client.iterate(this.PREFIX)) {
|
|
774
778
|
const key = name.slice(this.PREFIX.length);
|
|
775
779
|
yield [key, data];
|
|
@@ -787,7 +791,7 @@ var Store = class _Store {
|
|
|
787
791
|
await this.promise;
|
|
788
792
|
const trim = (key) => key.slice(this.PREFIX.length);
|
|
789
793
|
if (this.client.entries) {
|
|
790
|
-
if (this.client.
|
|
794
|
+
if (this.client.HAS_EXPIRATION) {
|
|
791
795
|
const entries = await this.client.entries(this.PREFIX);
|
|
792
796
|
return entries.map(([k, v]) => [trim(k), v]);
|
|
793
797
|
} else {
|
|
@@ -795,7 +799,7 @@ var Store = class _Store {
|
|
|
795
799
|
return entries.map(([k, v]) => [trim(k), v]).filter(([key, data]) => this.#isFresh(data, key)).map(([key, data]) => [key, data.value]);
|
|
796
800
|
}
|
|
797
801
|
}
|
|
798
|
-
if (this.client.
|
|
802
|
+
if (this.client.HAS_EXPIRATION) {
|
|
799
803
|
const list = [];
|
|
800
804
|
for await (const [k, v] of this.client.iterate(this.PREFIX)) {
|
|
801
805
|
list.push([trim(k), v]);
|
|
@@ -837,7 +841,7 @@ var Store = class _Store {
|
|
|
837
841
|
async values() {
|
|
838
842
|
await this.promise;
|
|
839
843
|
if (this.client.values) {
|
|
840
|
-
if (this.client.
|
|
844
|
+
if (this.client.HAS_EXPIRATION) return this.client.values(this.PREFIX);
|
|
841
845
|
const list = await this.client.values(this.PREFIX);
|
|
842
846
|
return list.filter((data) => this.#isFresh(data)).map((data) => data.value);
|
|
843
847
|
}
|
|
@@ -848,6 +852,52 @@ var Store = class _Store {
|
|
|
848
852
|
const entries = await this.entries();
|
|
849
853
|
return Object.fromEntries(entries);
|
|
850
854
|
}
|
|
855
|
+
/**
|
|
856
|
+
* Create a substore where all the keys are stored with
|
|
857
|
+
* the given prefix:
|
|
858
|
+
*
|
|
859
|
+
* ```js
|
|
860
|
+
* const session = store.prefix("session:");
|
|
861
|
+
* await session.set("key1", "value1");
|
|
862
|
+
* console.log(await session.entries()); // session.
|
|
863
|
+
* // [["key1", "value1"]]
|
|
864
|
+
* console.log(await store.entries()); // store.
|
|
865
|
+
* // [["session:key1", "value1"]]
|
|
866
|
+
* ```
|
|
867
|
+
*
|
|
868
|
+
* **[→ Full .prefix() Docs](https://polystore.dev/documentation#prefix)**
|
|
869
|
+
*/
|
|
870
|
+
prefix(prefix = "") {
|
|
871
|
+
const store = new _Store(
|
|
872
|
+
Promise.resolve(this.promise).then(() => this.client)
|
|
873
|
+
);
|
|
874
|
+
store.PREFIX = this.PREFIX + prefix;
|
|
875
|
+
store.EXPIRES = this.EXPIRES;
|
|
876
|
+
return store;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Create a substore where all the keys are stored with
|
|
880
|
+
* the given prefix:
|
|
881
|
+
*
|
|
882
|
+
* ```js
|
|
883
|
+
* const session = store.prefix("session:");
|
|
884
|
+
* await session.set("key1", "value1");
|
|
885
|
+
* console.log(await session.entries()); // session.
|
|
886
|
+
* // [["key1", "value1"]]
|
|
887
|
+
* console.log(await store.entries()); // store.
|
|
888
|
+
* // [["session:key1", "value1"]]
|
|
889
|
+
* ```
|
|
890
|
+
*
|
|
891
|
+
* **[→ Full .prefix() Docs](https://polystore.dev/documentation#prefix)**
|
|
892
|
+
*/
|
|
893
|
+
expires(expires = null) {
|
|
894
|
+
const store = new _Store(
|
|
895
|
+
Promise.resolve(this.promise).then(() => this.client)
|
|
896
|
+
);
|
|
897
|
+
store.EXPIRES = parse(expires);
|
|
898
|
+
store.PREFIX = this.PREFIX;
|
|
899
|
+
return store;
|
|
900
|
+
}
|
|
851
901
|
/**
|
|
852
902
|
* Delete all of the records of the store:
|
|
853
903
|
*
|
|
@@ -871,26 +921,25 @@ var Store = class _Store {
|
|
|
871
921
|
await Promise.all(keys.map((key) => this.del(key)));
|
|
872
922
|
}
|
|
873
923
|
/**
|
|
874
|
-
*
|
|
875
|
-
* the given prefix:
|
|
924
|
+
* Remove all expired records from the store.
|
|
876
925
|
*
|
|
877
926
|
* ```js
|
|
878
|
-
*
|
|
879
|
-
* await session.set("key1", "value1");
|
|
880
|
-
* console.log(await session.entries()); // session.
|
|
881
|
-
* // [["key1", "value1"]]
|
|
882
|
-
* console.log(await store.entries()); // store.
|
|
883
|
-
* // [["session:key1", "value1"]]
|
|
927
|
+
* await store.prune();
|
|
884
928
|
* ```
|
|
885
929
|
*
|
|
886
|
-
*
|
|
930
|
+
* Only affects stores where expiration is managed by this wrapper.
|
|
887
931
|
*/
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
932
|
+
async prune() {
|
|
933
|
+
await this.promise;
|
|
934
|
+
if (this.client.HAS_EXPIRATION) return;
|
|
935
|
+
for await (const [name, data] of this.client.iterate(
|
|
936
|
+
this.PREFIX
|
|
937
|
+
)) {
|
|
938
|
+
const key = name.slice(this.PREFIX.length);
|
|
939
|
+
if (!this.#isFresh(data, key)) {
|
|
940
|
+
await this.del(key);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
894
943
|
}
|
|
895
944
|
/**
|
|
896
945
|
* Stop the connection to the store, if any:
|
|
@@ -910,8 +959,8 @@ var Store = class _Store {
|
|
|
910
959
|
}
|
|
911
960
|
}
|
|
912
961
|
};
|
|
913
|
-
function createStore(client) {
|
|
914
|
-
return new Store(client);
|
|
962
|
+
function createStore(client, options) {
|
|
963
|
+
return new Store(client, options);
|
|
915
964
|
}
|
|
916
965
|
export {
|
|
917
966
|
createStore as default
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polystore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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",
|
|
@@ -11,9 +11,17 @@
|
|
|
11
11
|
"sideEffects": false,
|
|
12
12
|
"main": "index.js",
|
|
13
13
|
"types": "index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./index.d.ts",
|
|
17
|
+
"import": "./index.js"
|
|
18
|
+
},
|
|
19
|
+
"./express": "./src/express.js"
|
|
20
|
+
},
|
|
14
21
|
"files": [
|
|
15
22
|
"index.js",
|
|
16
|
-
"index.d.ts"
|
|
23
|
+
"index.d.ts",
|
|
24
|
+
"src/express.js"
|
|
17
25
|
],
|
|
18
26
|
"scripts": {
|
|
19
27
|
"analyze": "npm run build && esbuild src/index.ts --bundle --packages=external --format=esm --minify --outfile=index.min.js && echo 'Final size:' && gzip-size index.min.js && rm index.min.js",
|
package/readme.md
CHANGED
|
@@ -14,18 +14,20 @@ 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, options?)`](#set): save a single value that is serializable.
|
|
18
|
+
- [`.add(value, options?)`](#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
|
+
- [`.prefix(prefix)`](#prefix): create a sub-store that manages the keys with that prefix.
|
|
22
|
+
- [`.expires(expires)`](#expires): create a sub-store with a different default expiration.
|
|
21
23
|
- [Iterator](#iterator): go through all of the key/values one by one.
|
|
22
24
|
- [`.keys()`](#keys): get a list of all the available keys in the store.
|
|
23
25
|
- [`.values()`](#values): get a list of all the available values in the store.
|
|
24
26
|
- [`.entries()`](#entries): get a list of all the available key-value pairs.
|
|
25
27
|
- [`.all()`](#all): get an object of all the key:values mapped.
|
|
26
|
-
- [`.clear()`](#clear): delete
|
|
28
|
+
- [`.clear()`](#clear): delete **all** of the data in the store, effectively resetting it.
|
|
29
|
+
- [`.prune()`](#prune): delete only the **expired** data from the store.
|
|
27
30
|
- [`.close()`](#close): (only _some_ stores) ends the connection to the store.
|
|
28
|
-
- [`.prefix(prefix)`](#prefix): create a sub-store that manages the keys with that prefix.
|
|
29
31
|
|
|
30
32
|
Available clients for the KV store:
|
|
31
33
|
|
|
@@ -97,7 +99,7 @@ The base `kv()` initialization is shared across clients ([see full clients list]
|
|
|
97
99
|
import kv from "polystore";
|
|
98
100
|
|
|
99
101
|
// Initialize it; NO "new"; NO "await", just a plain function wrap:
|
|
100
|
-
const store = kv(
|
|
102
|
+
const store = kv(MyClientInstance, { expires: null, prefix: "" });
|
|
101
103
|
|
|
102
104
|
// use the store
|
|
103
105
|
```
|
|
@@ -120,23 +122,24 @@ store.get<number>("abc"); // number | null
|
|
|
120
122
|
store.set<number>("abc", 10);
|
|
121
123
|
|
|
122
124
|
store.set<number>("abc", "hello"); // FAILS
|
|
123
|
-
|
|
125
|
+
```
|
|
124
126
|
|
|
125
127
|
> [!WARNING]
|
|
126
|
-
> If you enforce types at _both_ the store level and method level, the method type must be a subset of the store type. For example, `kv<string | number>().get<string>("a")` works, but `kv<string>().get<number>("a")`
|
|
128
|
+
> If you enforce types at _both_ the store level and method level, the method type must be a subset of the store type. For example, `kv<string | number>().get<string>("a")` works, but `kv<string>().get<number>("a")` will show an error as expected.
|
|
127
129
|
|
|
128
130
|
Store values must be JSON-like data. The Serializable type represents values composed of `string`, `number`, `boolean`, `null`, and `arrays` and plain `objects` whose values are serializable. Class instances or non-plain objects will lose their prototypes and methods when stored.
|
|
129
131
|
|
|
130
|
-
These are the exported types, `Client`, `Serializable` and `
|
|
132
|
+
These are the exported types, `Client`, `Serializable`, `Store` and `Options`:
|
|
131
133
|
|
|
132
134
|
```ts
|
|
133
135
|
import kv from "polystore";
|
|
134
|
-
import type { Client, Serializable, Store } from "polystore";
|
|
136
|
+
import type { Client, Serializable, Store, Options } from "polystore";
|
|
135
137
|
|
|
136
138
|
const client: Client = ...; // See #creating-a-store
|
|
137
|
-
const store: Store = kv(client);
|
|
138
|
-
const
|
|
139
|
-
|
|
139
|
+
const store: Store = kv(client, opts as Options);
|
|
140
|
+
const key = await store.set('hello', 'b', opts as Options)
|
|
141
|
+
const value: Serializable = await store.get('hello');
|
|
142
|
+
```
|
|
140
143
|
|
|
141
144
|
### .get()
|
|
142
145
|
|
|
@@ -150,7 +153,7 @@ console.log(await store.get("key2")); // ["my", "grocery", "list"]
|
|
|
150
153
|
console.log(await store.get("key3")); // { name: "Francisco" }
|
|
151
154
|
```
|
|
152
155
|
|
|
153
|
-
You can specify the type
|
|
156
|
+
You can specify the type of the return:
|
|
154
157
|
|
|
155
158
|
```ts
|
|
156
159
|
console.log(await store.get<string>("key1")); // "Hello World"
|
|
@@ -197,15 +200,12 @@ await store.del('key2');
|
|
|
197
200
|
console.log(await store.get("key2")); // null
|
|
198
201
|
|
|
199
202
|
// Expired
|
|
200
|
-
await store.set("key3", "Hello", "1s");
|
|
203
|
+
await store.set("key3", "Hello", { expires: "1s" });
|
|
201
204
|
console.log(await store.get("key3")); // "Hello"
|
|
202
205
|
await new Promise((done) => setTimeout(done, 2000)); // Wait 2 seconds
|
|
203
206
|
console.log(await store.get("key3")); // null (already expired)
|
|
204
207
|
```
|
|
205
208
|
|
|
206
|
-
> [!WARNING]
|
|
207
|
-
> Attempting to read an expired key that wasn't automatically evicted will trigger a delete internally. This should not affect you directly, but it's good to know since you might expect a `read` operation not to modify the underlying data. See the [Eviction](#eviction) section and your specific client for details.
|
|
208
|
-
|
|
209
209
|
If you are using a substore with `.prefix()`, `.get()` will respect it:
|
|
210
210
|
|
|
211
211
|
```ts
|
|
@@ -220,12 +220,16 @@ console.log(await session.get('key1'));
|
|
|
220
220
|
|
|
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
|
-
```
|
|
224
|
-
await store.set(
|
|
223
|
+
```ts
|
|
224
|
+
const key = await store.set(
|
|
225
|
+
key: string,
|
|
226
|
+
value: any,
|
|
227
|
+
options?: { expires?: number|string; prefix?: string }
|
|
228
|
+
);
|
|
225
229
|
|
|
226
230
|
await store.set("key1", "Hello World");
|
|
227
|
-
await store.set("key2", ["my", "grocery", "list"], "1h");
|
|
228
|
-
await store.set("key3", { name: "Francisco" }, 60 * 60);
|
|
231
|
+
await store.set("key2", ["my", "grocery", "list"], { expires: "1h" });
|
|
232
|
+
await store.set("key3", { name: "Francisco" }, { expires: 60 * 60 });
|
|
229
233
|
```
|
|
230
234
|
|
|
231
235
|
You can specify the type either at [the store level](#api) or at the method level:
|
|
@@ -268,7 +272,7 @@ In short, only JSON-serializable data is safe to store.
|
|
|
268
272
|
|
|
269
273
|
#### Expires
|
|
270
274
|
|
|
271
|
-
When the `
|
|
275
|
+
When the `expires` option is set, it can be a number (**seconds**) or a string representing some time:
|
|
272
276
|
|
|
273
277
|
```js
|
|
274
278
|
// Valid "expire" values:
|
|
@@ -292,14 +296,17 @@ These are all the units available:
|
|
|
292
296
|
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
297
|
|
|
294
298
|
```js
|
|
295
|
-
const key:string = await store.add(
|
|
299
|
+
const key:string = await store.add(
|
|
300
|
+
value: any,
|
|
301
|
+
options?: { expires?: number|string; prefix?: string }
|
|
302
|
+
);
|
|
296
303
|
|
|
297
304
|
const key1 = await store.add("Hello World");
|
|
298
|
-
const key2 = await store.add(["my", "grocery", "list"], "1h");
|
|
299
|
-
const key3 = await store.add({ name: "Francisco" }, 60 * 60);
|
|
305
|
+
const key2 = await store.add(["my", "grocery", "list"], { expires: "1h" });
|
|
306
|
+
const key3 = await store.add({ name: "Francisco" }, { expires: 60 * 60 });
|
|
300
307
|
```
|
|
301
308
|
|
|
302
|
-
The value and
|
|
309
|
+
The value and options are similar to [`.set()`](#set), except for the lack of the first argument, since `.add()` automatically generates the key.
|
|
303
310
|
|
|
304
311
|
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
312
|
|
|
@@ -346,7 +353,7 @@ In many cases, internally the check for `.has()` is the same as `.get()`, so if
|
|
|
346
353
|
|
|
347
354
|
```js
|
|
348
355
|
const val = await store.get("key1");
|
|
349
|
-
if (val) { ... }
|
|
356
|
+
if (val !== null) { ... }
|
|
350
357
|
```
|
|
351
358
|
|
|
352
359
|
An example of an exception of the above is when you use it as a cache, then you can write code like this:
|
|
@@ -357,7 +364,7 @@ An example of an exception of the above is when you use it as a cache, then you
|
|
|
357
364
|
async function fetchUser(id) {
|
|
358
365
|
if (!(await store.has(id))) {
|
|
359
366
|
const { data } = await axios.get(`/users/${id}`);
|
|
360
|
-
await store.set(id, data, "1h");
|
|
367
|
+
await store.set(id, data, { expires: "1h" });
|
|
361
368
|
}
|
|
362
369
|
return store.get(id);
|
|
363
370
|
}
|
|
@@ -379,7 +386,7 @@ const has3 = await store.has("session:key1");
|
|
|
379
386
|
Remove a single key from the store and return the key itself:
|
|
380
387
|
|
|
381
388
|
```js
|
|
382
|
-
await store.del(key: string);
|
|
389
|
+
const key = await store.del(key: string);
|
|
383
390
|
```
|
|
384
391
|
|
|
385
392
|
It will ignore the operation if the key or value don't exist already (but won't throw). The API makes it easy to delete multiple keys at once:
|
|
@@ -387,7 +394,7 @@ It will ignore the operation if the key or value don't exist already (but won't
|
|
|
387
394
|
```js
|
|
388
395
|
const keys = ["key1", "key2"];
|
|
389
396
|
await Promise.all(keys.map(store.del));
|
|
390
|
-
console.log(done);
|
|
397
|
+
console.log("done");
|
|
391
398
|
```
|
|
392
399
|
|
|
393
400
|
An example with a prefix:
|
|
@@ -411,7 +418,7 @@ for await (const [key, value] of store) {
|
|
|
411
418
|
}
|
|
412
419
|
```
|
|
413
420
|
|
|
414
|
-
This is very useful for performance
|
|
421
|
+
This is very useful for performance reasons since it will retrieve the data sequentially, avoiding blocking the client while retrieving it all at once. The main disadvantage is if you keep writing data asynchronously while the async iterator is running.
|
|
415
422
|
|
|
416
423
|
You can also iterate on a subset of the entries with `.prefix()` (the prefix is stripped from the key here, see [.`prefix()`](#prefix)):
|
|
417
424
|
|
|
@@ -428,11 +435,11 @@ for await (const [key, value] of store.prefix("session:")) {
|
|
|
428
435
|
}
|
|
429
436
|
```
|
|
430
437
|
|
|
431
|
-
There are also methods to retrieve all
|
|
438
|
+
There are also methods to retrieve all the keys, values, or entries at once below, but those [have worse performance](#performance).
|
|
432
439
|
|
|
433
440
|
### .keys()
|
|
434
441
|
|
|
435
|
-
Get all
|
|
442
|
+
Get all the keys in the store as a simple array of strings:
|
|
436
443
|
|
|
437
444
|
```js
|
|
438
445
|
await store.keys();
|
|
@@ -446,11 +453,11 @@ const sessions = await store.prefix("session:").keys();
|
|
|
446
453
|
// ["keyA", "keyB"]
|
|
447
454
|
```
|
|
448
455
|
|
|
449
|
-
> We ensure that all
|
|
456
|
+
> We ensure that all the keys returned by this method are _not_ expired, while discarding any potentially expired key. See [**expirations**](#expirations) for more details.
|
|
450
457
|
|
|
451
458
|
### .values()
|
|
452
459
|
|
|
453
|
-
Get all
|
|
460
|
+
Get all the values in the store as a simple array with all the values:
|
|
454
461
|
|
|
455
462
|
```js
|
|
456
463
|
await store.values();
|
|
@@ -467,11 +474,11 @@ const companies = await store.prefix("company:").values();
|
|
|
467
474
|
// A list of all the companies
|
|
468
475
|
```
|
|
469
476
|
|
|
470
|
-
> We ensure that all
|
|
477
|
+
> We ensure that all the values returned by this method are _not_ expired, while discarding any potentially expired key. See [**expirations**](#expirations) for more details.
|
|
471
478
|
|
|
472
479
|
### .entries()
|
|
473
480
|
|
|
474
|
-
Get all
|
|
481
|
+
Get all the entries (key:value tuples) in the store:
|
|
475
482
|
|
|
476
483
|
```js
|
|
477
484
|
const entries = await store.entries();
|
|
@@ -512,31 +519,57 @@ const sessionObj = await store.prefix('session:').all();
|
|
|
512
519
|
|
|
513
520
|
### .clear()
|
|
514
521
|
|
|
515
|
-
Remove all
|
|
522
|
+
Remove all data from the current store and resets it to the original state:
|
|
516
523
|
|
|
517
524
|
```js
|
|
518
525
|
await store.clear();
|
|
519
526
|
```
|
|
520
527
|
|
|
528
|
+
If called on a `.prefix()` substore, only keys with that prefix are removed:
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
const sessions = store.prefix("session:");
|
|
532
|
+
|
|
533
|
+
await sessions.clear();
|
|
534
|
+
// removes only session:* keys
|
|
535
|
+
````
|
|
536
|
+
|
|
537
|
+
### .prune()
|
|
538
|
+
|
|
539
|
+
> [!IMPORTANT]
|
|
540
|
+
> For stores with native expiration (Redis, Cloudflare KV, etc.), `.prune()` usually does nothing because the underlying client already removes expired keys automatically.
|
|
541
|
+
|
|
542
|
+
Remove only expired records from the store.
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
await store.prune();
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
This method is only useful for stores that do not support native expiration (such as Map, localStorage, files, etc.). In those cases expired records are hidden by the API but may still exist internally until they are removed.
|
|
549
|
+
|
|
550
|
+
Calling `.prune()` scans the store and deletes all expired entries.
|
|
551
|
+
|
|
552
|
+
This operation is O(n) and should typically be run in a scheduled job or maintenance task rather than on every request.
|
|
553
|
+
|
|
521
554
|
### .close()
|
|
522
555
|
|
|
523
|
-
Close the
|
|
556
|
+
Close the connection (if any) from the client:
|
|
524
557
|
|
|
525
558
|
```js
|
|
526
559
|
await store.close();
|
|
527
|
-
|
|
560
|
+
```
|
|
528
561
|
|
|
529
562
|
### .prefix()
|
|
530
563
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
Creates **a new instance** of the Store, _with the same client_ as you provided, but now any key you read, write, etc. will be passed with the given prefix to the client. You only write `.prefix()` once and then don't need to worry about any prefix for any method anymore, it's all automatic. It's **the only method** that you don't need to await:
|
|
564
|
+
Creates **a new instance** of the Store, _with the same client_ as you provided, but now any key you read, write, etc. will be passed with the given prefix to the client. You only write `.prefix()` once and then don't need to worry about any prefix for any method anymore, it's all automatic. You don't need to await for it:
|
|
534
565
|
|
|
535
566
|
```js
|
|
536
567
|
const store = kv(new Map());
|
|
537
568
|
const session = store.prefix("session:");
|
|
538
569
|
```
|
|
539
570
|
|
|
571
|
+
> There's [an in-depth explanation about Substores](#substores) that is very informative for production usage.
|
|
572
|
+
|
|
540
573
|
Then all of the operations will be converted internally to add the prefix when reading, writing, etc:
|
|
541
574
|
|
|
542
575
|
```js
|
|
@@ -567,6 +600,31 @@ const books = kv(`file://${import.meta.dirname}/books.json`);
|
|
|
567
600
|
|
|
568
601
|
The main reason this is not stable is because [_some_ store engines don't allow for atomic deletion of keys given a prefix](https://stackoverflow.com/q/4006324/938236). While we do still clear them internally in those cases, that is a non-atomic operation and it could have some trouble if some other thread is reading/writing the data _at the same time_.
|
|
569
602
|
|
|
603
|
+
### .expires()
|
|
604
|
+
|
|
605
|
+
Create a substore with a different default expiration time.
|
|
606
|
+
|
|
607
|
+
```ts
|
|
608
|
+
const store = kv(new Map()); // No expiration
|
|
609
|
+
const cache = store.expires("10min"); // 10 min expiration
|
|
610
|
+
await cache.set("a", "b"); // 10 mins
|
|
611
|
+
await cache.set("c", "e", { expires: '5s' }); // 5 seconds, overwrites it
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
> There's [an in-depth explanation about Expirations](#expirations) that is very informative for production usage.
|
|
615
|
+
|
|
616
|
+
But applied automatically to all writes in the substore. Explicit expiration always overrides the default:
|
|
617
|
+
|
|
618
|
+
```ts
|
|
619
|
+
await cache.set("a", "b", { expires: "1h" });
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
You can combine it with .prefix():
|
|
623
|
+
|
|
624
|
+
```ts
|
|
625
|
+
const sessions = store.prefix("session:").expires("1day");
|
|
626
|
+
```
|
|
627
|
+
|
|
570
628
|
## Clients
|
|
571
629
|
|
|
572
630
|
A client is the library that manages the low-level store operations. For example, the Redis Client, or the browser's `localStorage` API. In some exceptions it's just a string and we do a bit more work on Polystore, like with `"cookie"` or `"file:///users/me/data.json"`.
|
|
@@ -599,7 +657,7 @@ import kv from "polystore";
|
|
|
599
657
|
|
|
600
658
|
const store = kv(new Map());
|
|
601
659
|
|
|
602
|
-
await store.set("key1", "Hello world", "1h");
|
|
660
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
603
661
|
console.log(await store.get("key1"));
|
|
604
662
|
// "Hello world"
|
|
605
663
|
```
|
|
@@ -615,9 +673,9 @@ console.log(await store.get("key1"));
|
|
|
615
673
|
|
|
616
674
|
```js
|
|
617
675
|
// GOOD - with polystore
|
|
618
|
-
await store.set("key1", { name: "Francisco" }, "2days");
|
|
676
|
+
await store.set("key1", { name: "Francisco" }, { expires: "2days" });
|
|
619
677
|
|
|
620
|
-
// COMPLEX - With
|
|
678
|
+
// COMPLEX - With plain Map
|
|
621
679
|
const data = new Map();
|
|
622
680
|
data.set("key1", { name: "Francisco" });
|
|
623
681
|
// Expiration not supported
|
|
@@ -632,7 +690,7 @@ import kv from "polystore";
|
|
|
632
690
|
|
|
633
691
|
const store = kv(localStorage);
|
|
634
692
|
|
|
635
|
-
await store.set("key1", "Hello world", "1h");
|
|
693
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
636
694
|
console.log(await store.get("key1"));
|
|
637
695
|
// "Hello world"
|
|
638
696
|
```
|
|
@@ -651,7 +709,7 @@ Same limitations as always apply to localStorage, if you think you are going to
|
|
|
651
709
|
|
|
652
710
|
```js
|
|
653
711
|
// GOOD - with polystore
|
|
654
|
-
await store.set("key1", { name: "Francisco" }, "2days");
|
|
712
|
+
await store.set("key1", { name: "Francisco" }, { expires: "2days" });
|
|
655
713
|
|
|
656
714
|
// COMPLEX - With localStorage
|
|
657
715
|
const serialValue = JSON.stringify({ name: "Francisco" });
|
|
@@ -668,7 +726,7 @@ import kv from "polystore";
|
|
|
668
726
|
|
|
669
727
|
const store = kv(sessionStorage);
|
|
670
728
|
|
|
671
|
-
await store.set("key1", "Hello world", "1h");
|
|
729
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
672
730
|
console.log(await store.get("key1"));
|
|
673
731
|
// "Hello world"
|
|
674
732
|
```
|
|
@@ -685,7 +743,7 @@ console.log(await store.get("key1"));
|
|
|
685
743
|
|
|
686
744
|
```js
|
|
687
745
|
// GOOD - with polystore
|
|
688
|
-
await store.set("key1", { name: "Francisco" }, "2days");
|
|
746
|
+
await store.set("key1", { name: "Francisco" }, { expires: "2days" });
|
|
689
747
|
|
|
690
748
|
// COMPLEX - With sessionStorage
|
|
691
749
|
const serialValue = JSON.stringify({ name: "Francisco" });
|
|
@@ -702,7 +760,7 @@ import kv from "polystore";
|
|
|
702
760
|
|
|
703
761
|
const store = kv("cookie"); // just a plain string
|
|
704
762
|
|
|
705
|
-
await store.set("key1", "Hello world", "1h");
|
|
763
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
706
764
|
console.log(await store.get("key1"));
|
|
707
765
|
// "Hello world"
|
|
708
766
|
```
|
|
@@ -731,7 +789,7 @@ import localForage from "localforage";
|
|
|
731
789
|
|
|
732
790
|
const store = kv(localForage);
|
|
733
791
|
|
|
734
|
-
await store.set("key1", "Hello world", "1h");
|
|
792
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
735
793
|
console.log(await store.get("key1"));
|
|
736
794
|
// "Hello world"
|
|
737
795
|
```
|
|
@@ -755,7 +813,7 @@ import { createClient } from "redis";
|
|
|
755
813
|
|
|
756
814
|
const store = kv(createClient().connect());
|
|
757
815
|
|
|
758
|
-
await store.set("key1", "Hello world", "1h");
|
|
816
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
759
817
|
console.log(await store.get("key1"));
|
|
760
818
|
// "Hello world"
|
|
761
819
|
```
|
|
@@ -790,7 +848,7 @@ import Database from "better-sqlite3";
|
|
|
790
848
|
const db = new Database("data.db")
|
|
791
849
|
const store = kv(db);
|
|
792
850
|
|
|
793
|
-
await store.set("key1", "Hello world", "1h");
|
|
851
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
794
852
|
console.log(await store.get("key1"));
|
|
795
853
|
// "Hello world"
|
|
796
854
|
```
|
|
@@ -830,13 +888,13 @@ db.exec(`
|
|
|
830
888
|
db.exec(
|
|
831
889
|
`CREATE INDEX IF NOT EXISTS idx_kv_expires_at ON kv (expires_at)`,
|
|
832
890
|
);
|
|
833
|
-
|
|
891
|
+
```
|
|
834
892
|
|
|
835
893
|
#### SQLite expirations
|
|
836
894
|
|
|
837
895
|
If `expires` is provided, Polystore will convert it to a timestamp and persist it in `expires_at`. We handle reading/writing rows and expiration checks.
|
|
838
896
|
|
|
839
|
-
|
|
897
|
+
Expired rows remain in the table unless you delete them manually. To avoid having stale data that is not used anymore, it's recommended you set a periodic check and clear expired records manually.
|
|
840
898
|
|
|
841
899
|
There's many ways of doing this, but a simple/basic one is this:
|
|
842
900
|
|
|
@@ -845,7 +903,7 @@ There's many ways of doing this, but a simple/basic one is this:
|
|
|
845
903
|
setInterval(() => {
|
|
846
904
|
db.prepare(`DELETE FROM kv WHERE expires_at < ?`).run(Date.now());
|
|
847
905
|
}, 10 * 60 * 1000);
|
|
848
|
-
|
|
906
|
+
```
|
|
849
907
|
|
|
850
908
|
Note that Polystore is self-reliant and won't have any problem even if you don't set that script, it will never render a stale record. It's just for both your convenience and privacy reasons.
|
|
851
909
|
|
|
@@ -862,19 +920,19 @@ import kv from "polystore";
|
|
|
862
920
|
|
|
863
921
|
const store = kv("https://kv.example.com/");
|
|
864
922
|
|
|
865
|
-
await store.set("key1", "Hello world", "1h");
|
|
923
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
866
924
|
console.log(await store.get("key1"));
|
|
867
925
|
// "Hello world"
|
|
868
926
|
```
|
|
869
927
|
|
|
870
|
-
> Note: the API client expire resolution is in
|
|
928
|
+
> Note: the API client expire resolution is in seconds, so times shorter than 1 second like `expires: 0.02` (20 ms) don't make sense for this storage method and won't properly save them.
|
|
871
929
|
|
|
872
930
|
> Note: see the [reference implementation in src/server.js](https://github.com/franciscop/polystore/blob/master/src/server.js)
|
|
873
931
|
|
|
874
932
|
|
|
875
933
|
### File
|
|
876
934
|
|
|
877
|
-
Treat a JSON file in your filesystem as the source for the KV store. Pass it an absolute `file://` url or a `new URL(
|
|
935
|
+
Treat a JSON file in your filesystem as the source for the KV store. Pass it an absolute `file://` url or a `new URL("file://...")` instance:
|
|
878
936
|
|
|
879
937
|
```js
|
|
880
938
|
import kv from "polystore";
|
|
@@ -882,7 +940,7 @@ import kv from "polystore";
|
|
|
882
940
|
// Path is "/Users/me/project/cache.json"
|
|
883
941
|
const store = kv("file:///Users/me/project/cache.json");
|
|
884
942
|
|
|
885
|
-
await store.set("key1", "Hello world", "1h");
|
|
943
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
886
944
|
console.log(await store.get("key1"));
|
|
887
945
|
// "Hello world"
|
|
888
946
|
```
|
|
@@ -916,7 +974,7 @@ const store1 = kv(new URL(`file://${process.cwd()}/cache.json`));
|
|
|
916
974
|
|
|
917
975
|
```js
|
|
918
976
|
// GOOD - with polystore
|
|
919
|
-
await store.set("key1", { name: "Francisco" }, "2days");
|
|
977
|
+
await store.set("key1", { name: "Francisco" }, { expires: "2days" });
|
|
920
978
|
|
|
921
979
|
// COMPLEX - With native file managing
|
|
922
980
|
const file = './data/users.json';
|
|
@@ -937,7 +995,7 @@ import kv from "polystore";
|
|
|
937
995
|
|
|
938
996
|
const store = kv("file:///Users/me/project/data/");
|
|
939
997
|
|
|
940
|
-
await store.set("key1", "Hello world", "1h");
|
|
998
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
941
999
|
// Writes "./data/key1.json"
|
|
942
1000
|
console.log(await store.get("key1"));
|
|
943
1001
|
// "Hello world"
|
|
@@ -975,7 +1033,7 @@ const store1 = kv(new URL(`file://${process.cwd()}/cache/`));
|
|
|
975
1033
|
|
|
976
1034
|
```js
|
|
977
1035
|
// GOOD - with polystore
|
|
978
|
-
await store.set("key1", { name: "Francisco" }, "2days");
|
|
1036
|
+
await store.set("key1", { name: "Francisco" }, { expires: "2days" });
|
|
979
1037
|
|
|
980
1038
|
// COMPLEX - With native folder
|
|
981
1039
|
const file = './data/user/key1.json';
|
|
@@ -995,7 +1053,7 @@ export default {
|
|
|
995
1053
|
async fetch(request, env, ctx) {
|
|
996
1054
|
const store = kv(env.YOUR_KV_NAMESPACE);
|
|
997
1055
|
|
|
998
|
-
await store.set("key1", "Hello world", "1h");
|
|
1056
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
999
1057
|
console.log(await store.get("key1"));
|
|
1000
1058
|
// "Hello world"
|
|
1001
1059
|
|
|
@@ -1018,7 +1076,7 @@ It expects that you pass the namespace from Cloudflare straight as a `kv()` argu
|
|
|
1018
1076
|
|
|
1019
1077
|
```js
|
|
1020
1078
|
// GOOD - with polystore
|
|
1021
|
-
await store.set("user", { name: "Francisco" }, "2days");
|
|
1079
|
+
await store.set("user", { name: "Francisco" }, { expires: "2days" });
|
|
1022
1080
|
|
|
1023
1081
|
// COMPLEX - With native Cloudflare KV
|
|
1024
1082
|
const serialValue = JSON.stringify({ name: "Francisco" });
|
|
@@ -1038,7 +1096,7 @@ import { Level } from "level";
|
|
|
1038
1096
|
|
|
1039
1097
|
const store = kv(new Level("example", { valueEncoding: "json" }));
|
|
1040
1098
|
|
|
1041
|
-
await store.set("key1", "Hello world", "1h");
|
|
1099
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
1042
1100
|
console.log(await store.get("key1"));
|
|
1043
1101
|
// "Hello world"
|
|
1044
1102
|
```
|
|
@@ -1055,7 +1113,7 @@ You will need to set the `valueEncoding` to `"json"` for the store to work as ex
|
|
|
1055
1113
|
|
|
1056
1114
|
```js
|
|
1057
1115
|
// GOOD - with polystore
|
|
1058
|
-
await store.set("user", { hello: 'world' }, "2days");
|
|
1116
|
+
await store.set("user", { hello: 'world' }, { expires: "2days" });
|
|
1059
1117
|
|
|
1060
1118
|
// With Level:
|
|
1061
1119
|
?? // Just not possible
|
|
@@ -1071,7 +1129,7 @@ import { Etcd3 } from "etcd3";
|
|
|
1071
1129
|
|
|
1072
1130
|
const store = kv(new Etcd3());
|
|
1073
1131
|
|
|
1074
|
-
await store.set("key1", "Hello world", "1h");
|
|
1132
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
1075
1133
|
console.log(await store.get("key1"));
|
|
1076
1134
|
// "Hello world"
|
|
1077
1135
|
```
|
|
@@ -1101,7 +1159,7 @@ await client.connect();
|
|
|
1101
1159
|
|
|
1102
1160
|
const store = kv(client);
|
|
1103
1161
|
|
|
1104
|
-
await store.set("key1", "Hello world", "1h");
|
|
1162
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
1105
1163
|
console.log(await store.get("key1"));
|
|
1106
1164
|
// "Hello world"
|
|
1107
1165
|
```
|
|
@@ -1150,9 +1208,9 @@ Please see the [creating a store](#creating-a-store) section for all the details
|
|
|
1150
1208
|
|
|
1151
1209
|
While all of our stores support `expires`, `.prefix()` and group operations, the nature of those makes them to have different performance characteristics.
|
|
1152
1210
|
|
|
1153
|
-
**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.
|
|
1211
|
+
**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. Expired keys 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.prune();` 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).
|
|
1154
1212
|
|
|
1155
|
-
**Group operations**
|
|
1213
|
+
**Group operations** are mostly intended for small datasets, 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.
|
|
1156
1214
|
|
|
1157
1215
|
**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.
|
|
1158
1216
|
|
|
@@ -1164,8 +1222,8 @@ We unify all of the clients diverse expiration methods into a single, easy one w
|
|
|
1164
1222
|
|
|
1165
1223
|
```js
|
|
1166
1224
|
// in-memory store
|
|
1167
|
-
const store =
|
|
1168
|
-
await store.set("a", "b", "1s");
|
|
1225
|
+
const store = kv(new Map());
|
|
1226
|
+
await store.set("a", "b", { expires: "1s" });
|
|
1169
1227
|
|
|
1170
1228
|
// These checks of course work:
|
|
1171
1229
|
console.log(await store.keys()); // ['a']
|
|
@@ -1214,7 +1272,7 @@ These details are explained in the respective client information.
|
|
|
1214
1272
|
|
|
1215
1273
|
What `.prefix()` does is it creates **a new instance** of the Store, _with the same client_ as you provided, but now any key you read, write, etc. will be passed with the given prefix to the client. The issue is that support from the underlying clients is inconsistent.
|
|
1216
1274
|
|
|
1217
|
-
When dealing with large or complex amounts of data in a KV store,
|
|
1275
|
+
When dealing with large or complex amounts of data in a KV store, sometimes it's useful to divide them by categories. Some examples might be:
|
|
1218
1276
|
|
|
1219
1277
|
- You use KV as a cache, and have different categories of data.
|
|
1220
1278
|
- You use KV as a session store, and want to differentiate different kinds of sessions.
|
|
@@ -1230,12 +1288,12 @@ To create a store, you define a class with these properties and methods:
|
|
|
1230
1288
|
class MyClient {
|
|
1231
1289
|
// If this is set to `true`, the CLIENT (you) handle the expiration, so
|
|
1232
1290
|
// the `.set()` and `.add()` receive a `expires` that is a `null` or `number`:
|
|
1233
|
-
|
|
1291
|
+
HAS_EXPIRATION = false;
|
|
1234
1292
|
|
|
1235
1293
|
// Mandatory methods
|
|
1236
1294
|
get (key): Promise<any>;
|
|
1237
1295
|
set (key, value, null|number): Promise<null>;
|
|
1238
|
-
iterate(prefix):
|
|
1296
|
+
iterate(prefix): AsyncIterator<[string, any]>
|
|
1239
1297
|
|
|
1240
1298
|
// Optional item methods (for optimization or customization)
|
|
1241
1299
|
add (prefix, data, null|number): Promise<string>;
|
|
@@ -1255,7 +1313,7 @@ class MyClient {
|
|
|
1255
1313
|
|
|
1256
1314
|
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.
|
|
1257
1315
|
|
|
1258
|
-
**Expires**: if you set the `
|
|
1316
|
+
**Expires**: if you set the `HAS_EXPIRATION = 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 **seconds** for the key/value to will expire.
|
|
1259
1317
|
|
|
1260
1318
|
**Prefix**: we manage the `prefix` as an invisible layer on top, you only need to be aware of it in the `.add()` method, as well as in the group methods:
|
|
1261
1319
|
|
|
@@ -1311,7 +1369,7 @@ class MyClient {
|
|
|
1311
1369
|
}
|
|
1312
1370
|
```
|
|
1313
1371
|
|
|
1314
|
-
We don't set `
|
|
1372
|
+
We don't set `HAS_EXPIRATION` to true since plain objects do NOT support expiration natively. So by not adding the `HAS_EXPIRATION` property, it's the same as setting it to `false`, and polystore will manage all the expirations as a layer on top of the data. We could be more explicit and set it to `HAS_EXPIRATION = false`, but it's not needed in this case.
|
|
1315
1373
|
|
|
1316
1374
|
### Example: custom ID generation
|
|
1317
1375
|
|
|
@@ -1321,10 +1379,10 @@ You might want to provide your custom key generation algorithm, which I'm going
|
|
|
1321
1379
|
class MyClient {
|
|
1322
1380
|
|
|
1323
1381
|
// Add the opt method .add() to have more control over the ID generation
|
|
1324
|
-
async add (prefix, data,
|
|
1382
|
+
async add (prefix, data, expires) {
|
|
1325
1383
|
const id = customId();
|
|
1326
1384
|
const key = prefix + id;
|
|
1327
|
-
return this.set(key, data,
|
|
1385
|
+
return this.set(key, data, expires);
|
|
1328
1386
|
}
|
|
1329
1387
|
|
|
1330
1388
|
//
|
|
@@ -1375,7 +1433,7 @@ class MyClient {
|
|
|
1375
1433
|
|
|
1376
1434
|
In this example on one of my projects, I needed to use Cloudflare's REST API since I didn't have access to any KV store I was happy with on Netlify's Edge Functions. So I created it like this:
|
|
1377
1435
|
|
|
1378
|
-
> Warning: this code snippet is an experimental example and hasn't gone through
|
|
1436
|
+
> Warning: this code snippet is an experimental example and hasn't gone through rigorous testing as the rest of the library, so please treat with caution.
|
|
1379
1437
|
|
|
1380
1438
|
```js
|
|
1381
1439
|
const {
|
|
@@ -1392,7 +1450,7 @@ const headers = {
|
|
|
1392
1450
|
};
|
|
1393
1451
|
|
|
1394
1452
|
class CloudflareCustom {
|
|
1395
|
-
|
|
1453
|
+
HAS_EXPIRATION = true;
|
|
1396
1454
|
|
|
1397
1455
|
async get(key) {
|
|
1398
1456
|
const res = await fetch(`${baseUrl}/values/${key}`, { headers });
|
|
@@ -1439,9 +1497,9 @@ class CloudflareCustom {
|
|
|
1439
1497
|
}
|
|
1440
1498
|
|
|
1441
1499
|
const store = kv(CloudflareCustom);
|
|
1442
|
-
|
|
1500
|
+
```
|
|
1443
1501
|
|
|
1444
|
-
It's lacking few things, so make sure to adapt to your needs, but it worked for my very simple cache needs.
|
|
1502
|
+
It's lacking a few things, so make sure to adapt to your needs, but it worked for my very simple cache needs.
|
|
1445
1503
|
|
|
1446
1504
|
|
|
1447
1505
|
## Examples
|
|
@@ -1461,7 +1519,7 @@ async function getProductInfo(id: string) {
|
|
|
1461
1519
|
// Some processing here
|
|
1462
1520
|
const clean = raw??;
|
|
1463
1521
|
|
|
1464
|
-
await store.set(id, clean,
|
|
1522
|
+
await store.set(id, clean, { expires: "10days" });
|
|
1465
1523
|
return clean;
|
|
1466
1524
|
}
|
|
1467
1525
|
```
|
|
@@ -1485,75 +1543,12 @@ if (process.env.REDIS_URL) {
|
|
|
1485
1543
|
store = kv(createClient(process.env.REDIS_URL).connect());
|
|
1486
1544
|
} else {
|
|
1487
1545
|
console.log('kv:folder using a folder for cache data');
|
|
1488
|
-
store = kv(
|
|
1546
|
+
store = kv(`file://${process.cwd()}/data/`);
|
|
1489
1547
|
}
|
|
1490
1548
|
|
|
1491
1549
|
export default store;
|
|
1492
1550
|
```
|
|
1493
1551
|
|
|
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
1552
|
### @server/next
|
|
1558
1553
|
|
|
1559
1554
|
> [!info]
|
package/src/express.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import session from "express-session";
|
|
2
|
+
import kv from "../index.js";
|
|
3
|
+
|
|
4
|
+
const ttlFromSession = (data) => {
|
|
5
|
+
const maxAge = data?.cookie?.originalMaxAge;
|
|
6
|
+
return typeof maxAge === "number" ? Math.ceil(maxAge / 1000) : null;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export class PolystoreSessionStore extends session.Store {
|
|
10
|
+
constructor(store) {
|
|
11
|
+
super();
|
|
12
|
+
this.store = store;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
prefix(prefix = "") {
|
|
16
|
+
return new PolystoreSessionStore(this.store.prefix(prefix));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get(sid, cb) {
|
|
20
|
+
this.store.get(sid).then((data) => cb(null, data)).catch(cb);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
set(sid, data, cb) {
|
|
24
|
+
this.store
|
|
25
|
+
.set(sid, data, ttlFromSession(data))
|
|
26
|
+
.then(() => cb && cb())
|
|
27
|
+
.catch((error) => cb && cb(error));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
destroy(sid, cb) {
|
|
31
|
+
this.store
|
|
32
|
+
.del(sid)
|
|
33
|
+
.then(() => cb && cb())
|
|
34
|
+
.catch((error) => cb && cb(error));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
touch(sid, data, cb) {
|
|
38
|
+
this.store
|
|
39
|
+
.set(sid, data, ttlFromSession(data))
|
|
40
|
+
.then(() => cb && cb())
|
|
41
|
+
.catch((error) => cb && cb(error));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default function expressStore(client = new Map()) {
|
|
46
|
+
return new PolystoreSessionStore(kv(client));
|
|
47
|
+
}
|