polystore 0.15.13 → 0.16.1
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 +243 -0
- package/index.js +859 -0
- package/package.json +20 -24
- package/readme.md +271 -39
- package/src/clients/Client.js +0 -7
- package/src/clients/api.js +0 -34
- package/src/clients/cloudflare.js +0 -56
- package/src/clients/cookie.js +0 -51
- package/src/clients/etcd.js +0 -29
- package/src/clients/file.js +0 -70
- package/src/clients/folder.js +0 -49
- package/src/clients/forage.js +0 -29
- package/src/clients/index.js +0 -25
- package/src/clients/level.js +0 -40
- package/src/clients/memory.js +0 -19
- package/src/clients/redis.js +0 -51
- package/src/clients/storage.js +0 -25
- package/src/index.d.ts +0 -209
- package/src/index.js +0 -278
- package/src/server.js +0 -81
- package/src/utils.js +0 -47
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polystore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
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",
|
|
@@ -9,18 +9,20 @@
|
|
|
9
9
|
"author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
|
|
10
10
|
"type": "module",
|
|
11
11
|
"sideEffects": false,
|
|
12
|
-
"main": "
|
|
13
|
-
"types": "
|
|
12
|
+
"main": "index.js",
|
|
13
|
+
"types": "index.d.ts",
|
|
14
14
|
"files": [
|
|
15
|
-
"
|
|
15
|
+
"index.js",
|
|
16
|
+
"index.d.ts"
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|
|
18
|
-
"analyze": "esbuild
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
19
|
+
"analyze": "npm run build && esbuild src/index.ts --bundle --packages=external --format=esm --minify --outfile=index.min.js && gzip-size index.min.js && rm index.min.js",
|
|
20
|
+
"build": "bunx tsup src/index.ts --format esm --dts --out-dir . --target node24",
|
|
21
|
+
"lint": "npx tsc --noEmit",
|
|
22
|
+
"start": "bun test --watch",
|
|
23
|
+
"test": "bun test",
|
|
24
|
+
"run:db": "etcd",
|
|
25
|
+
"run:server": "bun ./src/server.ts"
|
|
24
26
|
},
|
|
25
27
|
"keywords": [
|
|
26
28
|
"kv",
|
|
@@ -33,16 +35,21 @@
|
|
|
33
35
|
"license": "MIT",
|
|
34
36
|
"devDependencies": {
|
|
35
37
|
"@deno/kv": "^0.8.1",
|
|
38
|
+
"@types/bun": "^1.3.3",
|
|
39
|
+
"@types/jsdom": "^27.0.0",
|
|
40
|
+
"better-sqlite3": "^12.5.0",
|
|
36
41
|
"check-dts": "^0.8.0",
|
|
37
42
|
"cross-fetch": "^4.0.0",
|
|
38
43
|
"dotenv": "^16.3.1",
|
|
39
44
|
"edge-mock": "^0.0.15",
|
|
45
|
+
"esbuild": "^0.27.0",
|
|
40
46
|
"etcd3": "^1.1.2",
|
|
41
|
-
"
|
|
42
|
-
"
|
|
47
|
+
"gzip-size-cli": "^5.1.0",
|
|
48
|
+
"jsdom": "^27.2.0",
|
|
43
49
|
"level": "^8.0.1",
|
|
44
50
|
"localforage": "^1.10.0",
|
|
45
|
-
"redis": "^4.6.10"
|
|
51
|
+
"redis": "^4.6.10",
|
|
52
|
+
"tsup": "^8.5.1"
|
|
46
53
|
},
|
|
47
54
|
"documentation": {
|
|
48
55
|
"title": "🏬 Polystore - A universal library for standardizing any KV-store",
|
|
@@ -54,16 +61,5 @@
|
|
|
54
61
|
"Get help": "https://superpeer.com/francisco/-/javascript-and-react-help",
|
|
55
62
|
"Github": "https://github.com/franciscop/polystore"
|
|
56
63
|
}
|
|
57
|
-
},
|
|
58
|
-
"jest": {
|
|
59
|
-
"testTimeout": 15000,
|
|
60
|
-
"testEnvironment": "jsdom",
|
|
61
|
-
"setupFiles": [
|
|
62
|
-
"./test/setup.js"
|
|
63
|
-
],
|
|
64
|
-
"transform": {},
|
|
65
|
-
"modulePathIgnorePatterns": [
|
|
66
|
-
"test/cloudflare"
|
|
67
|
-
]
|
|
68
64
|
}
|
|
69
65
|
}
|
package/readme.md
CHANGED
|
@@ -42,6 +42,8 @@ Available clients for the KV store:
|
|
|
42
42
|
- [**Cloudflare KV** `env.KV_NAMESPACE`](#cloudflare-kv) (be): use Cloudflare's KV store
|
|
43
43
|
- [**Level** `new Level('example', { valueEncoding: 'json' })`](#level) (fe+be): support the whole Level ecosystem
|
|
44
44
|
- [**Etcd** `new Etcd3()`](#etcd) (be): the Microsoft's high performance KV store.
|
|
45
|
+
- [**Postgres** `pool`](#postgres) (be): use PostgreSQL with the pg library
|
|
46
|
+
- [**Prisma** `prisma.store`](#prisma) (be): use Prisma ORM as a key-value store
|
|
45
47
|
- [**_Custom_** `{}`](#creating-a-store) (fe+be): create your own store with just 3 methods!
|
|
46
48
|
|
|
47
49
|
I made this library to be used as a "building block" of other libraries, so that _your library_ can accept many cache stores effortlessly! It's universal (Node.js, Bun and the Browser) and tiny (~3KB). For example, let's say you create an API library, then you can accept the stores from your client:
|
|
@@ -74,7 +76,7 @@ import { createClient } from "redis";
|
|
|
74
76
|
const REDIS = process.env.REDIS_URL;
|
|
75
77
|
|
|
76
78
|
// Wrap the redis creation with Polystore (kv())
|
|
77
|
-
const store = kv(createClient(REDIS).connect());
|
|
79
|
+
const store = kv(createClient({ url: REDIS }).connect());
|
|
78
80
|
```
|
|
79
81
|
|
|
80
82
|
Now your store is ready to use! Add, set, get, del different keys. [See full API](#api).
|
|
@@ -101,21 +103,37 @@ const store = kv(MyClientOrStoreInstance);
|
|
|
101
103
|
// use the store
|
|
102
104
|
```
|
|
103
105
|
|
|
104
|
-
|
|
106
|
+
The above represents the recommended naming; the default export, `kv` in this case, is a wrapper that will generate a "store" that then you use all around your codebase.
|
|
105
107
|
|
|
106
|
-
|
|
107
|
-
const map = new Map();
|
|
108
|
-
const store = kv(map);
|
|
108
|
+
You can enforce the **types** for the store values directly at the store creation, or at the method level:
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
```ts
|
|
111
|
+
const store = kv<number>(new Map());
|
|
112
|
+
store.get("abc"); // number | null
|
|
113
|
+
store.set("abc", 10);
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
store.set("abc", "hello"); // FAILS
|
|
116
|
+
|
|
117
|
+
// At the method level
|
|
118
|
+
const store = kv(new Map());
|
|
119
|
+
store.get<number>("abc"); // number | null
|
|
120
|
+
store.set<number>("abc", 10);
|
|
121
|
+
|
|
122
|
+
store.set<number>("abc", "hello"); // FAILS
|
|
123
|
+
````
|
|
124
|
+
|
|
125
|
+
> If you try to enforce data structure at _both_ the store level AND method level, then the method data type _should_ be a subclass of the store data structure, e.g. `kv<string | number>().get<string>("a")` will work, but `kv<string>().get<number>("a")` will _not_ work.
|
|
126
|
+
|
|
127
|
+
The type should always be `Serializable`, which is `number | string | boolean | Object | Array` (values can be `null` inside Object+Array). These types, along with the Store and Client, are exported as well:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import kv from "polystore";
|
|
131
|
+
import type { Client, Serializable, Store } from "polystore";
|
|
132
|
+
|
|
133
|
+
const client: Client = ...; // See #creating-a-store
|
|
134
|
+
const store: Store = kv(client);
|
|
135
|
+
const value: Serializable = store.get('hello');
|
|
136
|
+
````
|
|
119
137
|
|
|
120
138
|
### .get()
|
|
121
139
|
|
|
@@ -174,7 +192,7 @@ These are all the units available:
|
|
|
174
192
|
|
|
175
193
|
### .add()
|
|
176
194
|
|
|
177
|
-
Create a value in the store with
|
|
195
|
+
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:
|
|
178
196
|
|
|
179
197
|
```js
|
|
180
198
|
const key:string = await store.add(value: any, options?: { expires: number|string });
|
|
@@ -186,7 +204,9 @@ const key3 = await store.add({ name: "Francisco" }, { expires: 60 * 60 });
|
|
|
186
204
|
|
|
187
205
|
The options and details are similar to [`.set()`](#set), except for the lack of the first argument, since `.add()` will generate the key automatically.
|
|
188
206
|
|
|
189
|
-
The default key
|
|
207
|
+
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).
|
|
208
|
+
|
|
209
|
+
Some clients will generate their own key, e.g. you can connect to a SQL client that does auto-incremental integers (always casted to `string` since a `key` is always a string in Polystore).
|
|
190
210
|
|
|
191
211
|
<details>
|
|
192
212
|
<summary>Key Generation details</summary>
|
|
@@ -206,7 +226,8 @@ const key1 = await session.add("value1");
|
|
|
206
226
|
|
|
207
227
|
console.log(await session.keys()); // on the "session" store
|
|
208
228
|
// ["c4ONlvweshXPUEy76q3eFHPL"]
|
|
209
|
-
|
|
229
|
+
//
|
|
230
|
+
console.log(await store.keys()); // on the ROOT store
|
|
210
231
|
// ["session:c4ONlvweshXPUEy76q3eFHPL"]
|
|
211
232
|
```
|
|
212
233
|
|
|
@@ -264,7 +285,7 @@ Remove a single key from the store and return the key itself:
|
|
|
264
285
|
await store.del(key: string);
|
|
265
286
|
```
|
|
266
287
|
|
|
267
|
-
It will ignore the operation if the key or value don't exist already (but won't
|
|
288
|
+
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:
|
|
268
289
|
|
|
269
290
|
```js
|
|
270
291
|
const keys = ["key1", "key2"];
|
|
@@ -293,7 +314,7 @@ for await (const [key, value] of store) {
|
|
|
293
314
|
}
|
|
294
315
|
```
|
|
295
316
|
|
|
296
|
-
This is very useful for performance resons 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 while the async iterator is running.
|
|
317
|
+
This is very useful for performance resons 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.
|
|
297
318
|
|
|
298
319
|
You can also iterate on a subset of the entries with `.prefix()` (the prefix is stripped from the key here, see [.`prefix()`](#prefix)):
|
|
299
320
|
|
|
@@ -328,7 +349,7 @@ const sessions = await store.prefix("session:").keys();
|
|
|
328
349
|
// ["keyA", "keyB"]
|
|
329
350
|
```
|
|
330
351
|
|
|
331
|
-
> We ensure that all of the keys returned by this method are _not_ expired, while discarding any potentially expired key. See [**
|
|
352
|
+
> We ensure that all of the keys returned by this method are _not_ expired, while discarding any potentially expired key. See [**expirations**](#expirations) for more details.
|
|
332
353
|
|
|
333
354
|
### .values()
|
|
334
355
|
|
|
@@ -349,7 +370,7 @@ const companies = await store.prefix("company:").values();
|
|
|
349
370
|
// A list of all the companies
|
|
350
371
|
```
|
|
351
372
|
|
|
352
|
-
> We ensure that all of the values returned by this method are _not_ expired, while discarding any potentially expired key. See [**
|
|
373
|
+
> We ensure that all of the values returned by this method are _not_ expired, while discarding any potentially expired key. See [**expirations**](#expirations) for more details.
|
|
353
374
|
|
|
354
375
|
### .entries()
|
|
355
376
|
|
|
@@ -369,7 +390,7 @@ const sessionEntries = await store.prefix('session:').entries();
|
|
|
369
390
|
// [["keyA", "valueA"], ["keyB", "valueB"]]
|
|
370
391
|
```
|
|
371
392
|
|
|
372
|
-
> We ensure that all of the entries returned by this method are _not_ expired, while discarding any potentially expired key. See [**
|
|
393
|
+
> We ensure that all of the entries returned by this method are _not_ expired, while discarding any potentially expired key. See [**expirations**](#expirations) for more details.
|
|
373
394
|
|
|
374
395
|
### .all()
|
|
375
396
|
|
|
@@ -385,21 +406,29 @@ It's in the format of a normal key:value object, where the object key is the sto
|
|
|
385
406
|
If you want to filter for a particular prefix, use `.prefix()`, which will return the object with only the keys that have that given prefix (stripping the keys of the prefix!):
|
|
386
407
|
|
|
387
408
|
```js
|
|
388
|
-
const sessionObj = await store.prefix('session:').
|
|
409
|
+
const sessionObj = await store.prefix('session:').all();
|
|
389
410
|
// { keyA: "valueA", keyB: "valueB" }
|
|
390
411
|
```
|
|
391
412
|
|
|
392
|
-
> We ensure that all of the entries returned by this method are _not_ expired, while discarding any potentially expired key. See [**
|
|
413
|
+
> We ensure that all of the entries returned by this method are _not_ expired, while discarding any potentially expired key. See [**expirations**](#expirations) for more details.
|
|
393
414
|
|
|
394
415
|
|
|
395
416
|
### .clear()
|
|
396
417
|
|
|
397
|
-
Remove all of the data from the
|
|
418
|
+
Remove all of the data from the client and resets it to the original state:
|
|
398
419
|
|
|
399
420
|
```js
|
|
400
421
|
await store.clear();
|
|
401
422
|
```
|
|
402
423
|
|
|
424
|
+
### .close()
|
|
425
|
+
|
|
426
|
+
Close the connetion (if any) from the client:
|
|
427
|
+
|
|
428
|
+
```js
|
|
429
|
+
await store.close();
|
|
430
|
+
````
|
|
431
|
+
|
|
403
432
|
### .prefix()
|
|
404
433
|
|
|
405
434
|
> There's [an in-depth explanation about Substores](#substores) that is very informative for production usage.
|
|
@@ -447,6 +476,23 @@ A client is the library that manages the low-level store operations. For example
|
|
|
447
476
|
|
|
448
477
|
Polystore provides a unified API you can use `Promises`, `expires` and `.prefix()` even with those stores that do not support these operations natively.
|
|
449
478
|
|
|
479
|
+
While you can keep a reference to the client and access it directly, we strongly recommend to only access it through `polystore`, since we might add custom serialization and extra properties for e.g. expiration time:
|
|
480
|
+
|
|
481
|
+
```js
|
|
482
|
+
const map = new Map();
|
|
483
|
+
const store = kv(map);
|
|
484
|
+
|
|
485
|
+
// Works as expected
|
|
486
|
+
await store.set("a", "b");
|
|
487
|
+
console.log(await store.get("a"));
|
|
488
|
+
|
|
489
|
+
// DON'T DO THIS; this will break the app since we apply more
|
|
490
|
+
// advanced serialization to the values stored in memory
|
|
491
|
+
map.set("a", "b");
|
|
492
|
+
console.log(await store.get("a")); // THROWS ERROR
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
|
|
450
496
|
### Memory
|
|
451
497
|
|
|
452
498
|
An in-memory KV store, with promises and expiration time:
|
|
@@ -465,7 +511,7 @@ console.log(await store.get("key1"));
|
|
|
465
511
|
<summary>Why use polystore with <code>new Map()</code>?</summary>
|
|
466
512
|
<p>These benefits are for wrapping Map() with polystore:</p>
|
|
467
513
|
<ul>
|
|
468
|
-
<li><strong>Expiration</strong>: you can now set lifetime to your values so that they are automatically evicted when the time passes. <a href="#
|
|
514
|
+
<li><strong>Expiration</strong>: you can now set lifetime to your values so that they are automatically evicted when the time passes. <a href="#expirations">Expirations</a>.</li>
|
|
469
515
|
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
470
516
|
</ul>
|
|
471
517
|
</details>
|
|
@@ -501,7 +547,7 @@ Same limitations as always apply to localStorage, if you think you are going to
|
|
|
501
547
|
<p>These benefits are for wrapping localStorage with polystore:</p>
|
|
502
548
|
<ul>
|
|
503
549
|
<li><strong>Data structures</strong>: with Polystore you can pass more complex data structures and we'll handle the serialization/deserialization.</li>
|
|
504
|
-
<li><strong>Expiration</strong>: you can now set lifetime to your values so that they are automatically evicted when the time passes. <a href="#
|
|
550
|
+
<li><strong>Expiration</strong>: you can now set lifetime to your values so that they are automatically evicted when the time passes. <a href="#expirations">Expirations</a>.</li>
|
|
505
551
|
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
506
552
|
</ul>
|
|
507
553
|
</details>
|
|
@@ -535,7 +581,7 @@ console.log(await store.get("key1"));
|
|
|
535
581
|
<p>These benefits are for wrapping sessionStorage with polystore:</p>
|
|
536
582
|
<ul>
|
|
537
583
|
<li><strong>Data structures</strong>: with Polystore you can pass more complex data structures and we'll handle the serialization/deserialization.</li>
|
|
538
|
-
<li><strong>Expiration</strong>: you can now set lifetime to your values so that they are automatically evicted when the time passes. <a href="#
|
|
584
|
+
<li><strong>Expiration</strong>: you can now set lifetime to your values so that they are automatically evicted when the time passes. <a href="#expirations">Expirations</a>.</li>
|
|
539
585
|
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
540
586
|
</ul>
|
|
541
587
|
</details>
|
|
@@ -573,7 +619,7 @@ It is fairly limited for how powerful cookies are, but in exchange it has the sa
|
|
|
573
619
|
<p>These benefits are for wrapping cookies with polystore:</p>
|
|
574
620
|
<ul>
|
|
575
621
|
<li><strong>Data structures</strong>: with Polystore you can pass more complex data structures and we'll handle the serialization/deserialization.</li>
|
|
576
|
-
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#
|
|
622
|
+
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#expirations">Expirations</a>.</li>
|
|
577
623
|
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
578
624
|
</ul>
|
|
579
625
|
</details>
|
|
@@ -594,10 +640,10 @@ console.log(await store.get("key1"));
|
|
|
594
640
|
```
|
|
595
641
|
|
|
596
642
|
<details>
|
|
597
|
-
<summary>Why use polystore with <code>
|
|
643
|
+
<summary>Why use polystore with <code>localForage</code>?</summary>
|
|
598
644
|
<p>These benefits are for wrapping localStorage with polystore:</p>
|
|
599
645
|
<ul>
|
|
600
|
-
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#
|
|
646
|
+
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#expirations">Expirations</a>.</li>
|
|
601
647
|
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
602
648
|
</ul>
|
|
603
649
|
</details>
|
|
@@ -625,7 +671,7 @@ You don't need to `await` for the connect or similar, this will process it prope
|
|
|
625
671
|
<summary>Why use polystore with <code>Redis</code>?</summary>
|
|
626
672
|
<p>These benefits are for wrapping Redis with polystore:</p>
|
|
627
673
|
<ul>
|
|
628
|
-
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#
|
|
674
|
+
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#expirations">Expirations</a>.</li>
|
|
629
675
|
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
630
676
|
</ul>
|
|
631
677
|
</details>
|
|
@@ -686,7 +732,7 @@ const store1 = kv(new URL(`file://${process.cwd()}/cache.json`));
|
|
|
686
732
|
<p>These benefits are for wrapping a file with polystore:</p>
|
|
687
733
|
<ul>
|
|
688
734
|
<li><strong>Data structures</strong>: with Polystore you can pass more complex data structures and we'll handle the serialization/deserialization.</li>
|
|
689
|
-
<li><strong>Expiration</strong>: you can now set lifetime to your values so that they are automatically evicted when the time passes. <a href="#
|
|
735
|
+
<li><strong>Expiration</strong>: you can now set lifetime to your values so that they are automatically evicted when the time passes. <a href="#expirations">Expirations</a>.</li>
|
|
690
736
|
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
691
737
|
</ul>
|
|
692
738
|
</details>
|
|
@@ -745,7 +791,7 @@ const store1 = kv(new URL(`file://${process.cwd()}/cache/`));
|
|
|
745
791
|
<p>These benefits are for wrapping a folder with polystore:</p>
|
|
746
792
|
<ul>
|
|
747
793
|
<li><strong>Data structures</strong>: with Polystore you can pass more complex data structures and we'll handle the serialization/deserialization.</li>
|
|
748
|
-
<li><strong>Expiration</strong>: you can now set lifetime to your values so that they are automatically evicted when the time passes. <a href="#
|
|
794
|
+
<li><strong>Expiration</strong>: you can now set lifetime to your values so that they are automatically evicted when the time passes. <a href="#expirations">Expirations</a>.</li>
|
|
749
795
|
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
750
796
|
</ul>
|
|
751
797
|
</details>
|
|
@@ -788,7 +834,7 @@ It expects that you pass the namespace from Cloudflare straight as a `kv()` argu
|
|
|
788
834
|
<p>These benefits are for wrapping Cloudflare's KV with polystore:</p>
|
|
789
835
|
<ul>
|
|
790
836
|
<li><strong>Data structures</strong>: with Polystore you can pass more complex data structures and we'll handle the serialization/deserialization.</li>
|
|
791
|
-
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#
|
|
837
|
+
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#expirations">Expirations</a>.</li>
|
|
792
838
|
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
793
839
|
</ul>
|
|
794
840
|
</details>
|
|
@@ -826,7 +872,7 @@ You will need to set the `valueEncoding` to `"json"` for the store to work as ex
|
|
|
826
872
|
<summary>Why use polystore with Level?</summary>
|
|
827
873
|
<p>These benefits are for wrapping Level with polystore:</p>
|
|
828
874
|
<ul>
|
|
829
|
-
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#
|
|
875
|
+
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#expirations">Expirations</a>.</li>
|
|
830
876
|
</ul>
|
|
831
877
|
</details>
|
|
832
878
|
|
|
@@ -859,7 +905,172 @@ You'll need to be running the etcd store for this to work as expected.
|
|
|
859
905
|
<summary>Why use polystore with Etcd?</summary>
|
|
860
906
|
<p>These benefits are for wrapping Etcd with polystore:</p>
|
|
861
907
|
<ul>
|
|
862
|
-
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#
|
|
908
|
+
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#expirations">Expirations</a>.</li>
|
|
909
|
+
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
910
|
+
</ul>
|
|
911
|
+
</details>
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
## SQLite
|
|
915
|
+
|
|
916
|
+
Supports both **`bun:sqlite`** and **`better-sqlite3`** directly. Pass an already-opened database instance to `kv()` and Polystore will use the `kv` table to store keys, values, and expirations:
|
|
917
|
+
|
|
918
|
+
> [!IMPORTANT]
|
|
919
|
+
> The table `kv` must already exist in the Database
|
|
920
|
+
|
|
921
|
+
```js
|
|
922
|
+
import kv from "polystore";
|
|
923
|
+
import Database from "better-sqlite3";
|
|
924
|
+
// Or: import Database from "bun:sqlite";
|
|
925
|
+
|
|
926
|
+
const db = new Database("data.db")
|
|
927
|
+
const store = kv(db);
|
|
928
|
+
|
|
929
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
930
|
+
console.log(await store.get("key1"));
|
|
931
|
+
// "Hello world"
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
### SQLite schema
|
|
935
|
+
|
|
936
|
+
This is the required schema:
|
|
937
|
+
|
|
938
|
+
```sql
|
|
939
|
+
CREATE TABLE kv (
|
|
940
|
+
id TEXT PRIMARY KEY,
|
|
941
|
+
value TEXT,
|
|
942
|
+
expires_at INTEGER
|
|
943
|
+
);
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
You can create these with failsafes to make it much easier to initialize:
|
|
947
|
+
|
|
948
|
+
```js
|
|
949
|
+
db.run(`
|
|
950
|
+
CREATE TABLE IF NOT EXISTS kv (
|
|
951
|
+
id TEXT PRIMARY KEY,
|
|
952
|
+
value TEXT NOT NULL,
|
|
953
|
+
expires_at INTEGER
|
|
954
|
+
)
|
|
955
|
+
`);
|
|
956
|
+
db.run(
|
|
957
|
+
`CREATE INDEX IF NOT EXISTS idx_kv_expires_at ON kv (expires_at)`,
|
|
958
|
+
);
|
|
959
|
+
````
|
|
960
|
+
|
|
961
|
+
### SQLite expirations
|
|
962
|
+
|
|
963
|
+
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.
|
|
964
|
+
|
|
965
|
+
However, these are not auto-evicted since SQLite doesn't have a native expiration. To avoid having stale data that is not used anymore, it's recommended you set a periodic check and clear expired records manually:
|
|
966
|
+
|
|
967
|
+
```js
|
|
968
|
+
// Clear expired keys once every 10 minutes
|
|
969
|
+
setInterval(() => {
|
|
970
|
+
db.prepare(`DELETE FROM kv WHERE expires_at < ?`).run(Date.now());
|
|
971
|
+
}, 10 * 60 * 1000);
|
|
972
|
+
````
|
|
973
|
+
|
|
974
|
+
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.
|
|
975
|
+
|
|
976
|
+
<details>
|
|
977
|
+
<summary>Why use polystore with <code>SQLite</code>?</summary>
|
|
978
|
+
<p>These benefits apply when wrapping a SQLite DB with polystore:</p>
|
|
979
|
+
<ul>
|
|
980
|
+
<li><strong>Intuitive expirations</strong>: specify expiration times like <code>10min</code> or <code>2h</code> without manual date handling.</li>
|
|
981
|
+
<li><strong>Substores</strong>: use prefixes to isolate sets of keys cleanly.</li>
|
|
982
|
+
<li><strong>Simple persistence</strong>: full on-disk durability with a minimal driver.</li>
|
|
983
|
+
</ul>
|
|
984
|
+
</details>
|
|
985
|
+
|
|
986
|
+
|
|
987
|
+
### Postgres
|
|
988
|
+
|
|
989
|
+
Use PostgreSQL with the `pg` library as a key-value store:
|
|
990
|
+
|
|
991
|
+
```js
|
|
992
|
+
import kv from "polystore";
|
|
993
|
+
import { Client } from "pg";
|
|
994
|
+
|
|
995
|
+
const client = new Client({ connectionString: process.env.DATABASE_URL });
|
|
996
|
+
await client.connect();
|
|
997
|
+
|
|
998
|
+
const store = kv(client);
|
|
999
|
+
|
|
1000
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
1001
|
+
console.log(await store.get("key1"));
|
|
1002
|
+
// "Hello world"
|
|
1003
|
+
```
|
|
1004
|
+
|
|
1005
|
+
You can also use `pg.Pool` instead of `pg.Client` for connection pooling.
|
|
1006
|
+
|
|
1007
|
+
Your database needs a table with three columns: `id` (text), `value` (text), and `expiresAt` (timestamp, nullable):
|
|
1008
|
+
|
|
1009
|
+
```sql
|
|
1010
|
+
CREATE TABLE kv (
|
|
1011
|
+
id TEXT PRIMARY KEY,
|
|
1012
|
+
value TEXT NOT NULL,
|
|
1013
|
+
"expiresAt" TIMESTAMP
|
|
1014
|
+
);
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
The default table name is `kv`, but you can use different tables via `.prefix()`:
|
|
1018
|
+
|
|
1019
|
+
```js
|
|
1020
|
+
const sessions = store.prefix("session:"); // Uses 'session' table
|
|
1021
|
+
const cache = store.prefix("cache:"); // Uses 'cache' table
|
|
1022
|
+
|
|
1023
|
+
await sessions.set("user123", { name: "Alice" });
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
This maps prefixes to table names for better performance on group operations.
|
|
1027
|
+
|
|
1028
|
+
<details>
|
|
1029
|
+
<summary>Why use polystore with Postgres?</summary>
|
|
1030
|
+
<p>These benefits are for wrapping Postgres with polystore:</p>
|
|
1031
|
+
<ul>
|
|
1032
|
+
<li><strong>Unified API</strong>: use the same API across all your storage backends.</li>
|
|
1033
|
+
<li><strong>Database-backed persistence</strong>: leverage your existing database for key-value storage.</li>
|
|
1034
|
+
<li><strong>Table-based substores</strong>: <code>.prefix()</code> maps to different tables for optimal query performance.</li>
|
|
1035
|
+
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#expirations">Expirations</a>.</li>
|
|
1036
|
+
</ul>
|
|
1037
|
+
</details>
|
|
1038
|
+
|
|
1039
|
+
### Prisma
|
|
1040
|
+
|
|
1041
|
+
Use Prisma as a key-value store by passing a table model directly:
|
|
1042
|
+
|
|
1043
|
+
```js
|
|
1044
|
+
import kv from "polystore";
|
|
1045
|
+
import { PrismaClient } from "@prisma/client";
|
|
1046
|
+
|
|
1047
|
+
const prisma = new PrismaClient();
|
|
1048
|
+
const store = kv(prisma.session);
|
|
1049
|
+
|
|
1050
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
1051
|
+
console.log(await store.get("key1"));
|
|
1052
|
+
// "Hello world"
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
Your Prisma schema needs a model with three columns: `id` (String), `value` (String/Text), and `expiresAt` (DateTime, nullable):
|
|
1056
|
+
|
|
1057
|
+
```prisma
|
|
1058
|
+
model session {
|
|
1059
|
+
id String @id
|
|
1060
|
+
value String @db.Text
|
|
1061
|
+
expiresAt DateTime?
|
|
1062
|
+
}
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
All three columns are required. The `expiresAt` column should be nullable (`DateTime?`) to support records without expiration.
|
|
1066
|
+
|
|
1067
|
+
<details>
|
|
1068
|
+
<summary>Why use polystore with Prisma?</summary>
|
|
1069
|
+
<p>These benefits are for wrapping Prisma with polystore:</p>
|
|
1070
|
+
<ul>
|
|
1071
|
+
<li><strong>Unified API</strong>: use the same API across all your storage backends.</li>
|
|
1072
|
+
<li><strong>Database-backed persistence</strong>: leverage your existing database for key-value storage.</li>
|
|
1073
|
+
<li><strong>Intuitive expirations</strong>: use plain English to specify the expiration time like <code>10min</code>. <a href="#expiration">Expirations</a>.</li>
|
|
863
1074
|
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
864
1075
|
</ul>
|
|
865
1076
|
</details>
|
|
@@ -870,7 +1081,7 @@ Please see the [creating a store](#creating-a-store) section for all the details
|
|
|
870
1081
|
|
|
871
1082
|
## Performance
|
|
872
1083
|
|
|
873
|
-
> TL;DR: if you only use the item operations (add,set,get,has,del) and your client supports expiration natively, you have nothing to worry about! Otherwise, please read on.
|
|
1084
|
+
> TL;DR: if you only use the item operations (add, set, get, has, del) and your client supports expiration natively, you have nothing to worry about! Otherwise, please read on.
|
|
874
1085
|
|
|
875
1086
|
While all of our stores support `expires`, `.prefix()` and group operations, the nature of those makes them to have different performance characteristics.
|
|
876
1087
|
|
|
@@ -880,11 +1091,11 @@ While all of our stores support `expires`, `.prefix()` and group operations, the
|
|
|
880
1091
|
|
|
881
1092
|
**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.
|
|
882
1093
|
|
|
883
|
-
##
|
|
1094
|
+
## Expirations
|
|
884
1095
|
|
|
885
1096
|
> Warning: if a client doesn't support expiration natively, we will hide expired keys on the API calls for a nice DX, but _old data might not be evicted automatically_. See [the notes in Performance](#performance) for details on how to work around this.
|
|
886
1097
|
|
|
887
|
-
We unify all of the clients diverse expiration methods into a single, easy one with `expires
|
|
1098
|
+
We unify all of the clients diverse expiration methods into a single, easy one with `expires` (**seconds** | string):
|
|
888
1099
|
|
|
889
1100
|
```js
|
|
890
1101
|
// in-memory store
|
|
@@ -905,12 +1116,33 @@ console.log(await store.has("a")); // false
|
|
|
905
1116
|
console.log(await store.get("a")); // null
|
|
906
1117
|
```
|
|
907
1118
|
|
|
1119
|
+
These can be set with natural language, or a single number for the seconds:
|
|
1120
|
+
|
|
1121
|
+
```js
|
|
1122
|
+
// Valid "expire" values:
|
|
1123
|
+
0 - expire immediately (AKA delete it)
|
|
1124
|
+
0.1 - expire after 100ms*
|
|
1125
|
+
60 * 60 - expire after 1h
|
|
1126
|
+
3_600 - expire after 1h
|
|
1127
|
+
"10s" - expire after 10 seconds
|
|
1128
|
+
"2minutes" - expire after 2 minutes
|
|
1129
|
+
"5d" - expire after 5 days
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1132
|
+
These are all the units available:
|
|
1133
|
+
|
|
1134
|
+
> "ms", "millisecond", "s", "sec", "second", "m", "min", "minute", "h", "hr", "hour", "d", "day", "w", "wk", "week", "b" (month), "month", "y", "yr", "year"
|
|
1135
|
+
|
|
908
1136
|
This is great because with polystore we do ensure that if a key has expired, it doesn't show up in `.keys()`, `.entries()`, `.values()`, `.has()` or `.get()`.
|
|
909
1137
|
|
|
1138
|
+
### Eviction
|
|
1139
|
+
|
|
910
1140
|
However, in some stores this does come with some potential performance disadvantages. For example, both the in-memory example above and localStorage _don't_ have a native expiration/eviction process, so we have to store that information as metadata, meaning that even to check if a key exists we need to read and decode its value. For one or few keys it's not a problem, but for large sets this can become an issue.
|
|
911
1141
|
|
|
912
1142
|
For other stores like Redis this is not a problem, because the low-level operations already do them natively, so we don't need to worry about this for performance at the user-level. Instead, Redis and cookies have the problem that they only have expiration resolution at the second level. Meaning that 800ms is not a valid Redis expiration time, it has to be 1s, 2s, etc.
|
|
913
1143
|
|
|
1144
|
+
These details are explained in the respective client information.
|
|
1145
|
+
|
|
914
1146
|
## Substores
|
|
915
1147
|
|
|
916
1148
|
> There's some [basic `.prefix()` API info](#prefix) for everyday usage, this section is the in-depth explanation.
|
package/src/clients/Client.js
DELETED
package/src/clients/api.js
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import Client from "./Client.js";
|
|
2
|
-
|
|
3
|
-
// Handle an API endpoint with fetch()
|
|
4
|
-
export default class Api extends Client {
|
|
5
|
-
// Indicate that the file handler DOES handle expirations
|
|
6
|
-
EXPIRES = true;
|
|
7
|
-
|
|
8
|
-
static test = (client) =>
|
|
9
|
-
typeof client === "string" && /^https?:\/\//.test(client);
|
|
10
|
-
|
|
11
|
-
#api = async (key, opts = "", method = "GET", body) => {
|
|
12
|
-
const url = `${this.client.replace(/\/$/, "")}/${encodeURIComponent(key)}${opts}`;
|
|
13
|
-
const headers = { accept: "application/json" };
|
|
14
|
-
if (body) headers["content-type"] = "application/json";
|
|
15
|
-
const res = await fetch(url, { method, headers, body });
|
|
16
|
-
if (!res.ok) return null;
|
|
17
|
-
if (res.headers.get("content-type")?.includes("application/json")) {
|
|
18
|
-
return res.json();
|
|
19
|
-
}
|
|
20
|
-
return res.text();
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
get = (key) => this.#api(key);
|
|
24
|
-
set = (key, value, { expires } = {}) =>
|
|
25
|
-
this.#api(key, `?expires=${expires || ""}`, "PUT", this.encode(value));
|
|
26
|
-
del = (key) => this.#api(key, "", "DELETE");
|
|
27
|
-
|
|
28
|
-
async *iterate(prefix = "") {
|
|
29
|
-
const data = await this.#api("", `?prefix=${encodeURIComponent(prefix)}`);
|
|
30
|
-
for (let [key, value] of Object.entries(data || {})) {
|
|
31
|
-
yield [prefix + key, value];
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|