polystore 0.21.3 → 0.23.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 +11 -11
- package/index.js +189 -189
- package/package.json +70 -53
- package/readme.md +134 -59
- package/src/plugins/axios-cache-interceptor/index.d.ts +17 -0
- package/src/plugins/axios-cache-interceptor/index.js +49 -0
- package/src/plugins/better-auth/index.d.ts +18 -0
- package/src/plugins/better-auth/index.js +27 -0
- package/src/plugins/express/index.d.ts +20 -0
- package/src/{integrations/express.js → plugins/express/index.js} +3 -3
- package/src/plugins/hono-sessions/index.d.ts +15 -0
- package/src/{integrations/hono-sessions.js → plugins/hono-sessions/index.js} +3 -3
- package/src/integrations/express.d.ts +0 -307
- package/src/integrations/hono-sessions.d.ts +0 -299
package/package.json
CHANGED
|
@@ -1,41 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polystore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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
|
-
"repository": "
|
|
7
|
-
"bugs": "https://github.com/franciscop/polystore/issues",
|
|
8
|
-
"funding": "https://www.paypal.me/franciscopresencia/19",
|
|
6
|
+
"repository": "github:franciscop/polystore",
|
|
9
7
|
"author": "Francisco Presencia <public@francisco.io> (https://francisco.io/)",
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"./express": {
|
|
20
|
-
"types": "./src/integrations/express.d.ts",
|
|
21
|
-
"import": "./src/integrations/express.js"
|
|
22
|
-
},
|
|
23
|
-
"./hono-sessions": {
|
|
24
|
-
"types": "./src/integrations/hono-sessions.d.ts",
|
|
25
|
-
"import": "./src/integrations/hono-sessions.js"
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
"files": [
|
|
29
|
-
"index.js",
|
|
30
|
-
"index.d.ts",
|
|
31
|
-
"src/integrations/express.js",
|
|
32
|
-
"src/integrations/express.d.ts",
|
|
33
|
-
"src/integrations/hono-sessions.js",
|
|
34
|
-
"src/integrations/hono-sessions.d.ts"
|
|
8
|
+
"funding": "https://www.paypal.me/franciscopresencia/19",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"kv",
|
|
12
|
+
"store",
|
|
13
|
+
"polystore",
|
|
14
|
+
"key-value",
|
|
15
|
+
"key",
|
|
16
|
+
"value"
|
|
35
17
|
],
|
|
36
18
|
"scripts": {
|
|
37
|
-
"
|
|
38
|
-
"build": "bunx tsup src/index.ts --format esm --dts --out-dir . --target node24 && bunx tsup src/
|
|
19
|
+
"size": "echo $(gzip -c index.js | wc -c) bytes",
|
|
20
|
+
"build": "bunx tsup src/index.ts --format esm --dts --out-dir . --target node24 && bunx tsup src/plugins/express/index.ts --format esm --dts --out-dir src/plugins/express --target node24 --external polystore --external express-session && bunx tsup src/plugins/hono-sessions/index.ts --format esm --dts --out-dir src/plugins/hono-sessions --target node24 --external polystore --external hono-sessions && bunx tsup src/plugins/better-auth/index.ts --format esm --dts --out-dir src/plugins/better-auth --target node24 --external polystore && bunx tsup src/plugins/axios-cache-interceptor/index.ts --format esm --dts --out-dir src/plugins/axios-cache-interceptor --target node24 --external polystore --external axios-cache-interceptor",
|
|
39
21
|
"lint": "tsc",
|
|
40
22
|
"start": "bun test --watch",
|
|
41
23
|
"service:db": "etcd",
|
|
@@ -44,28 +26,73 @@
|
|
|
44
26
|
"service:server": "bun ./src/server.ts",
|
|
45
27
|
"services": "concurrently \"npm run service:db\" \"npm run service:redis\" \"npm run service:postgres\" \"npm run service:server\"",
|
|
46
28
|
"test": "bun run test:bun && bun run test:jest",
|
|
47
|
-
"test:bun": "bun test ./test/index.test.ts ./src/
|
|
29
|
+
"test:bun": "bun test ./test/index.test.ts ./src/plugins/",
|
|
48
30
|
"test:jest": "jest ./test/index.test.ts --detectOpenHandles --forceExit"
|
|
49
31
|
},
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"store",
|
|
53
|
-
"polystore",
|
|
54
|
-
"key-value",
|
|
55
|
-
"key",
|
|
56
|
-
"value"
|
|
32
|
+
"workspaces": [
|
|
33
|
+
"examples/*"
|
|
57
34
|
],
|
|
58
|
-
"
|
|
35
|
+
"main": "index.js",
|
|
36
|
+
"type": "module",
|
|
37
|
+
"types": "index.d.ts",
|
|
38
|
+
"files": [
|
|
39
|
+
"index.js",
|
|
40
|
+
"index.d.ts",
|
|
41
|
+
"src/plugins/express/index.js",
|
|
42
|
+
"src/plugins/express/index.d.ts",
|
|
43
|
+
"src/plugins/hono-sessions/index.js",
|
|
44
|
+
"src/plugins/hono-sessions/index.d.ts",
|
|
45
|
+
"src/plugins/better-auth/index.js",
|
|
46
|
+
"src/plugins/better-auth/index.d.ts",
|
|
47
|
+
"src/plugins/axios-cache-interceptor/index.js",
|
|
48
|
+
"src/plugins/axios-cache-interceptor/index.d.ts"
|
|
49
|
+
],
|
|
50
|
+
"exports": {
|
|
51
|
+
".": {
|
|
52
|
+
"types": "./index.d.ts",
|
|
53
|
+
"import": "./index.js"
|
|
54
|
+
},
|
|
55
|
+
"./express": {
|
|
56
|
+
"types": "./src/plugins/express/index.d.ts",
|
|
57
|
+
"import": "./src/plugins/express/index.js"
|
|
58
|
+
},
|
|
59
|
+
"./hono-sessions": {
|
|
60
|
+
"types": "./src/plugins/hono-sessions/index.d.ts",
|
|
61
|
+
"import": "./src/plugins/hono-sessions/index.js"
|
|
62
|
+
},
|
|
63
|
+
"./better-auth": {
|
|
64
|
+
"types": "./src/plugins/better-auth/index.d.ts",
|
|
65
|
+
"import": "./src/plugins/better-auth/index.js"
|
|
66
|
+
},
|
|
67
|
+
"./axios-cache-interceptor": {
|
|
68
|
+
"types": "./src/plugins/axios-cache-interceptor/index.d.ts",
|
|
69
|
+
"import": "./src/plugins/axios-cache-interceptor/index.js"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"documentation.page": {
|
|
73
|
+
"title": "🏬 Polystore - A universal library for standardizing any KV-store",
|
|
74
|
+
"home": "assets/home.html",
|
|
75
|
+
"homepage": "https://polystore.dev/",
|
|
76
|
+
"menu": {
|
|
77
|
+
"Documentation": "/documentation",
|
|
78
|
+
"Issues": "https://github.com/franciscop/polystore/issues",
|
|
79
|
+
"Get help": "https://superpeer.com/francisco/-/javascript-and-react-help",
|
|
80
|
+
"Github": "https://github.com/franciscop/polystore"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"dependencies": {},
|
|
59
84
|
"devDependencies": {
|
|
60
85
|
"@deno/kv": "^0.8.1",
|
|
61
86
|
"@types/better-sqlite3": "^7.6.13",
|
|
62
|
-
"@types/bun": "^1.3.
|
|
87
|
+
"@types/bun": "^1.3.0",
|
|
63
88
|
"@types/express": "^5.0.6",
|
|
64
89
|
"@types/express-session": "^1.18.2",
|
|
65
90
|
"@types/jest": "^30.0.0",
|
|
66
91
|
"@types/jsdom": "^27.0.0",
|
|
67
92
|
"@types/pg": "^8.11.10",
|
|
68
93
|
"@types/supertest": "^7.2.0",
|
|
94
|
+
"axios": "^1.9.0",
|
|
95
|
+
"axios-cache-interceptor": "^1.12.0",
|
|
69
96
|
"better-sqlite3": "^12.6.0",
|
|
70
97
|
"concurrently": "^9.2.1",
|
|
71
98
|
"dotenv": "^16.3.1",
|
|
@@ -86,17 +113,7 @@
|
|
|
86
113
|
"supertest": "^7.2.2",
|
|
87
114
|
"ts-jest": "^29.4.6",
|
|
88
115
|
"tsup": "^8.5.1",
|
|
89
|
-
"typescript": "^
|
|
116
|
+
"typescript": "^6.0.2"
|
|
90
117
|
},
|
|
91
|
-
"
|
|
92
|
-
"title": "🏬 Polystore - A universal library for standardizing any KV-store",
|
|
93
|
-
"home": "assets/home.html",
|
|
94
|
-
"homepage": "https://polystore.dev/",
|
|
95
|
-
"menu": {
|
|
96
|
-
"Documentation": "/documentation",
|
|
97
|
-
"Issues": "https://github.com/franciscop/polystore/issues",
|
|
98
|
-
"Get help": "https://superpeer.com/francisco/-/javascript-and-react-help",
|
|
99
|
-
"Github": "https://github.com/franciscop/polystore"
|
|
100
|
-
}
|
|
101
|
-
}
|
|
118
|
+
"sideEffects": false
|
|
102
119
|
}
|
package/readme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# Polystore [](https://www.npmjs.com/package/polystore) [](https://github.com/franciscop/polystore/actions) [](https://github.com/franciscop/polystore/blob/master/index.js) [](https://github.com/franciscop/polystore/blob/master/package.json)
|
|
2
2
|
|
|
3
|
-
A key-value library to unify the API of [many
|
|
3
|
+
A key-value library to unify the API of [many adapters](#adapters): localStorage, Redis, FileSystem, SQLite, etc.
|
|
4
4
|
|
|
5
5
|
```js
|
|
6
6
|
import kv from "polystore";
|
|
@@ -8,7 +8,7 @@ const store1 = kv(new Map()); // in-memory
|
|
|
8
8
|
const store2 = kv(localStorage); // Persist in the browser
|
|
9
9
|
const store3 = kv(redisClient); // Use a Redis client for backend persistence
|
|
10
10
|
const store4 = kv(yourOwnStore); // Create a store based on your code
|
|
11
|
-
// Many more
|
|
11
|
+
// Many more adapters available
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
These are all the methods of the [API](#api) (they are all `async`):
|
|
@@ -29,7 +29,7 @@ These are all the methods of the [API](#api) (they are all `async`):
|
|
|
29
29
|
- [`.prune()`](#prune): delete only the **expired** data from the store.
|
|
30
30
|
- [`.close()`](#close): (only _some_ stores) ends the connection to the store.
|
|
31
31
|
|
|
32
|
-
Available
|
|
32
|
+
Available adapters for the KV store:
|
|
33
33
|
|
|
34
34
|
- [**Memory** `new Map()`](#memory) (fe+be): an in-memory API to keep your KV store.
|
|
35
35
|
- [**Local Storage** `localStorage`](#local-storage) (fe): persist the data in the browser's localStorage.
|
|
@@ -42,7 +42,7 @@ Available clients for the KV store:
|
|
|
42
42
|
- [**File** `"file:///[...].json"`](#file) (be): store the data in a single JSON file in your FS.
|
|
43
43
|
- [**Folder** `"file:///[...]/"`](#folder) (be): store each key in a folder as json files.
|
|
44
44
|
- [**Cloudflare KV** `env.KV_NAMESPACE`](#cloudflare-kv) (be): use Cloudflare's KV store.
|
|
45
|
-
- [**Postgres** `
|
|
45
|
+
- [**Postgres** `pg`](#postgres) (be): use PostgreSQL with the pg library.
|
|
46
46
|
- [**Level** `new Level('example', { valueEncoding: 'json' })`](#level) (fe+be): support the whole Level ecosystem.
|
|
47
47
|
- [**Etcd** `new Etcd3()`](#etcd) (be): the Microsoft's high performance KV store.
|
|
48
48
|
- [**_Custom_** `{}`](#creating-a-store) (fe+be): create your own store with just 3 methods!
|
|
@@ -61,7 +61,7 @@ MyApi({ cache: env.KV_NAMESPACE }); // OR
|
|
|
61
61
|
|
|
62
62
|
## Getting started
|
|
63
63
|
|
|
64
|
-
First, install `polystore` and whatever [supported
|
|
64
|
+
First, install `polystore` and whatever [supported adapter](#adapters) that you prefer. Let's see Redis as an example here:
|
|
65
65
|
|
|
66
66
|
```sh
|
|
67
67
|
npm i polystore redis
|
|
@@ -93,7 +93,7 @@ await store.del(key);
|
|
|
93
93
|
|
|
94
94
|
## API
|
|
95
95
|
|
|
96
|
-
The base `kv()` initialization is shared across
|
|
96
|
+
The base `kv()` initialization is shared across adapters ([see full adapters list](#adapters)); an argument that receives the adapter or a string representing the adapter and then the options:
|
|
97
97
|
|
|
98
98
|
```js
|
|
99
99
|
import kv from "polystore";
|
|
@@ -104,9 +104,6 @@ const store = kv(MyClientInstance, { expires: null, prefix: "" });
|
|
|
104
104
|
// use the store
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
> [!IMPORTANT]
|
|
108
|
-
> The library delivers excellent performance for item-level operations (GET, SET, ADD, HAS, DEL). For other methods or detailed guidance, check the performance considerations and consult your specific client’s documentation.
|
|
109
|
-
|
|
110
107
|
You can enforce **types** for the values either at store creation or at the method level:
|
|
111
108
|
|
|
112
109
|
```ts
|
|
@@ -129,14 +126,14 @@ store.set<number>("abc", "hello"); // FAILS
|
|
|
129
126
|
|
|
130
127
|
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.
|
|
131
128
|
|
|
132
|
-
These are the exported types, `
|
|
129
|
+
These are the exported types, `Adapter`, `Serializable`, `Store` and `Options`:
|
|
133
130
|
|
|
134
131
|
```ts
|
|
135
132
|
import kv from "polystore";
|
|
136
|
-
import type {
|
|
133
|
+
import type { Adapter, Serializable, Store, Options } from "polystore";
|
|
137
134
|
|
|
138
|
-
const
|
|
139
|
-
const store: Store = kv(
|
|
135
|
+
const adapter: Adapter = ...; // See #creating-a-store
|
|
136
|
+
const store: Store = kv(adapter, opts as Options);
|
|
140
137
|
const key = await store.set('hello', 'b', opts as Options)
|
|
141
138
|
const value: Serializable = await store.get('hello');
|
|
142
139
|
```
|
|
@@ -308,9 +305,9 @@ const key3 = await store.add({ name: "Francisco" }, { expires: 60 * 60 });
|
|
|
308
305
|
|
|
309
306
|
The value and options are similar to [`.set()`](#set), except for the lack of the first argument, since `.add()` automatically generates the key.
|
|
310
307
|
|
|
311
|
-
The default key is 24 AlphaNumeric characters (upper+lower case), however this can change if you are using a `.prefix()` or some
|
|
308
|
+
The default key is 24 AlphaNumeric characters (upper+lower case), however this can change if you are using a `.prefix()` or some adapters might generate it differently (only custom adapters can do that right now).
|
|
312
309
|
|
|
313
|
-
Some
|
|
310
|
+
Some adapters 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).
|
|
314
311
|
|
|
315
312
|
<details>
|
|
316
313
|
<summary>Key Generation details</summary>
|
|
@@ -553,7 +550,7 @@ This operation is O(n) and should typically be run in a scheduled job or mainten
|
|
|
553
550
|
|
|
554
551
|
### .close()
|
|
555
552
|
|
|
556
|
-
Close the connection (if any) from the
|
|
553
|
+
Close the connection (if any) from the adapter:
|
|
557
554
|
|
|
558
555
|
```js
|
|
559
556
|
await store.close();
|
|
@@ -561,7 +558,7 @@ await store.close();
|
|
|
561
558
|
|
|
562
559
|
### .prefix()
|
|
563
560
|
|
|
564
|
-
Creates **a new instance** of the Store, _with the same
|
|
561
|
+
Creates **a new instance** of the Store, _with the same adapter_ as you provided, but now any key you read, write, etc. will be passed with the given prefix to the adapter. 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:
|
|
565
562
|
|
|
566
563
|
```js
|
|
567
564
|
const store = kv(new Map());
|
|
@@ -586,7 +583,7 @@ for await (const [key, value] of session) {
|
|
|
586
583
|
}
|
|
587
584
|
```
|
|
588
585
|
|
|
589
|
-
Different
|
|
586
|
+
Different adapters have better/worse support for substores, and in some cases some operations might be slower. This should be documented on each adapter's documentation (see below). As an alternative, you can always create two different stores instead of a substore:
|
|
590
587
|
|
|
591
588
|
```js
|
|
592
589
|
// Two in-memory stores
|
|
@@ -625,15 +622,15 @@ You can combine it with .prefix():
|
|
|
625
622
|
const sessions = store.prefix("session:").expires("1day");
|
|
626
623
|
```
|
|
627
624
|
|
|
628
|
-
##
|
|
625
|
+
## Adapters
|
|
629
626
|
|
|
630
|
-
|
|
627
|
+
An adapter is the library that manages the low-level store operations. For example, the Redis adapter, 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"`.
|
|
631
628
|
|
|
632
629
|
Polystore provides a unified API you can use `Promises`, `expires` and `.prefix()` even with those stores that do not support these operations natively.
|
|
633
630
|
|
|
634
631
|
Quick overview:
|
|
635
632
|
|
|
636
|
-
|
|
|
633
|
+
| Adapter | Runtime | Persistence | Native expiration | Notes |
|
|
637
634
|
|---|---|---|---|---|
|
|
638
635
|
| [Memory](#memory) | Node.js + Browser | ❌ | ❌ | Great for tests and ephemeral caches |
|
|
639
636
|
| [Local Storage](#local-storage) | Browser | ✅ | ❌ | Persistent browser storage |
|
|
@@ -642,7 +639,7 @@ Quick overview:
|
|
|
642
639
|
| [Local Forage](#local-forage) | Browser | ✅ | ❓ | Better capacity than localStorage |
|
|
643
640
|
| [Redis](#redis) | Node.js | ✅ | ✅ | Good distributed cache backend |
|
|
644
641
|
| [SQLite](#sqlite) | Node.js | ✅ | ❌ | Simple local persistence |
|
|
645
|
-
| [Fetch API](#fetch-api) |
|
|
642
|
+
| [Fetch API](#fetch-api) | Node.js + Browser | ❓ | ❓ | Bring your own API |
|
|
646
643
|
| [File](#file) | Node.js | ✅ | ❌ | Single JSON file store |
|
|
647
644
|
| [Folder](#folder) | Node.js | ✅ | ❌ | One-file-per-key store |
|
|
648
645
|
| [Cloudflare KV](#cloudflare-kv) | Cloudflare | ✅ | ✅ | Edge-native KV |
|
|
@@ -650,7 +647,7 @@ Quick overview:
|
|
|
650
647
|
| [Etcd](#etcd) | Node.js | ✅ | ✅ | Distributed KV |
|
|
651
648
|
| [Postgres](#postgres) | Node.js | ✅ | ❌ | Table-backed KV |
|
|
652
649
|
|
|
653
|
-
While you can keep a reference to the
|
|
650
|
+
While you can keep a reference to the underlying adapter 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:
|
|
654
651
|
|
|
655
652
|
```js
|
|
656
653
|
const map = new Map();
|
|
@@ -1225,12 +1222,14 @@ This keeps a single table while preserving namespace-style grouping through pref
|
|
|
1225
1222
|
|
|
1226
1223
|
Please see the [creating a store](#creating-a-store) section for all the details!
|
|
1227
1224
|
|
|
1228
|
-
##
|
|
1225
|
+
## Plugins
|
|
1229
1226
|
|
|
1230
|
-
Polystore has
|
|
1227
|
+
Polystore has easy plugins to drop into popular frameworks and auth libraries. Each one has a runnable example in the [`examples/`](https://github.com/franciscop/polystore/tree/master/examples) folder.
|
|
1231
1228
|
|
|
1232
1229
|
### Express
|
|
1233
1230
|
|
|
1231
|
+
> [Full example →](https://github.com/franciscop/polystore/tree/master/examples/express)
|
|
1232
|
+
|
|
1234
1233
|
Use any Polystore-compatible store as an [express-session](https://github.com/expressjs/session) store:
|
|
1235
1234
|
|
|
1236
1235
|
```js
|
|
@@ -1243,7 +1242,7 @@ app.use(session({
|
|
|
1243
1242
|
}));
|
|
1244
1243
|
```
|
|
1245
1244
|
|
|
1246
|
-
By default it uses an in-memory `Map`, which is fine for development. For production, pass any Polystore
|
|
1245
|
+
By default it uses an in-memory `Map`, which is fine for development. For production, pass any Polystore adapter:
|
|
1247
1246
|
|
|
1248
1247
|
```js
|
|
1249
1248
|
import { createClient } from "redis";
|
|
@@ -1255,7 +1254,7 @@ const store = expressStore(createClient().connect());
|
|
|
1255
1254
|
app.use(session({ secret: "my-secret", store }));
|
|
1256
1255
|
```
|
|
1257
1256
|
|
|
1258
|
-
Any
|
|
1257
|
+
Any adapter works — Redis, Postgres, SQLite, file-based, etc. Session TTL is read automatically from `cookie.originalMaxAge` so you don't need to configure it separately.
|
|
1259
1258
|
|
|
1260
1259
|
Use `.prefix()` to namespace sessions, for example in a multi-tenant app:
|
|
1261
1260
|
|
|
@@ -1270,30 +1269,35 @@ app.use((req, res, next) => {
|
|
|
1270
1269
|
|
|
1271
1270
|
### Hono Sessions
|
|
1272
1271
|
|
|
1272
|
+
> [Full example →](https://github.com/franciscop/polystore/tree/master/examples/hono-sessions)
|
|
1273
|
+
|
|
1273
1274
|
Use any Polystore-compatible store as a [hono-sessions](https://github.com/jcs224/hono_sessions) store:
|
|
1274
1275
|
|
|
1275
1276
|
```js
|
|
1276
1277
|
import { Hono } from "hono";
|
|
1277
1278
|
import { sessionMiddleware } from "hono-sessions";
|
|
1278
|
-
import
|
|
1279
|
+
import kv from "polystore/hono-sessions";
|
|
1279
1280
|
|
|
1280
|
-
const
|
|
1281
|
+
const store = kv();
|
|
1281
1282
|
|
|
1283
|
+
const app = new Hono();
|
|
1282
1284
|
app.use("*", sessionMiddleware({
|
|
1283
|
-
store
|
|
1285
|
+
store,
|
|
1284
1286
|
encryptionKey: process.env.SESSION_KEY,
|
|
1285
1287
|
expireAfterSeconds: 3600,
|
|
1286
1288
|
}));
|
|
1287
1289
|
```
|
|
1288
1290
|
|
|
1289
|
-
By default it uses an in-memory `Map`. For production, pass any Polystore
|
|
1291
|
+
By default it uses an in-memory `Map`. For production, pass any Polystore adapter:
|
|
1290
1292
|
|
|
1291
1293
|
```js
|
|
1292
1294
|
import { createClient } from "redis";
|
|
1293
|
-
import
|
|
1295
|
+
import kv from "polystore/hono-sessions";
|
|
1296
|
+
|
|
1297
|
+
const store = kv(createClient().connect());
|
|
1294
1298
|
|
|
1295
1299
|
app.use("*", sessionMiddleware({
|
|
1296
|
-
store
|
|
1300
|
+
store,
|
|
1297
1301
|
encryptionKey: process.env.SESSION_KEY,
|
|
1298
1302
|
expireAfterSeconds: 3600,
|
|
1299
1303
|
}));
|
|
@@ -1316,6 +1320,77 @@ app.use("*", (c, next) => {
|
|
|
1316
1320
|
```
|
|
1317
1321
|
|
|
1318
1322
|
|
|
1323
|
+
### Better Auth
|
|
1324
|
+
|
|
1325
|
+
> [Full example →](https://github.com/franciscop/polystore/tree/master/examples/better-auth)
|
|
1326
|
+
|
|
1327
|
+
Use any Polystore-compatible store as the [`secondaryStorage`](https://www.better-auth.com/docs/concepts/database#secondary-storage) for [Better Auth](https://better-auth.com). No database required — Polystore handles session caching and token storage:
|
|
1328
|
+
|
|
1329
|
+
```js
|
|
1330
|
+
import { betterAuth } from "better-auth";
|
|
1331
|
+
import betterAuthStorage from "polystore/better-auth";
|
|
1332
|
+
|
|
1333
|
+
export const auth = betterAuth({
|
|
1334
|
+
secondaryStorage: betterAuthStorage(), // in-memory by default
|
|
1335
|
+
emailAndPassword: { enabled: true },
|
|
1336
|
+
});
|
|
1337
|
+
```
|
|
1338
|
+
|
|
1339
|
+
For production, swap in any Polystore adapter:
|
|
1340
|
+
|
|
1341
|
+
```js
|
|
1342
|
+
import { createClient } from "redis";
|
|
1343
|
+
|
|
1344
|
+
secondaryStorage: betterAuthStorage(createClient().connect())
|
|
1345
|
+
```
|
|
1346
|
+
|
|
1347
|
+
Use `.prefix()` to namespace keys in a shared store:
|
|
1348
|
+
|
|
1349
|
+
```js
|
|
1350
|
+
secondaryStorage: betterAuthStorage(createClient().connect()).prefix("auth:")
|
|
1351
|
+
```
|
|
1352
|
+
|
|
1353
|
+
### Axios Cache Interceptor
|
|
1354
|
+
|
|
1355
|
+
> [Full example →](https://github.com/franciscop/polystore/tree/master/examples/axios-cache-interceptor)
|
|
1356
|
+
|
|
1357
|
+
Use any Polystore-compatible store as the cache storage for [axios-cache-interceptor](https://axios-cache-interceptor.js.org/):
|
|
1358
|
+
|
|
1359
|
+
```js
|
|
1360
|
+
import axios from "axios";
|
|
1361
|
+
import { setupCache } from "axios-cache-interceptor";
|
|
1362
|
+
import axiosCacheStorage from "polystore/axios-cache-interceptor";
|
|
1363
|
+
|
|
1364
|
+
const http = setupCache(axios, {
|
|
1365
|
+
storage: axiosCacheStorage(), // in-memory by default
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
// First request hits the network, second is served from cache
|
|
1369
|
+
const { data } = await http.get("https://api.example.com/users");
|
|
1370
|
+
await http.get("https://api.example.com/users"); // cached
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
For production, swap in any Polystore adapter:
|
|
1374
|
+
|
|
1375
|
+
```js
|
|
1376
|
+
import { createClient } from "redis";
|
|
1377
|
+
import axiosCacheStorage from "polystore/axios-cache-interceptor";
|
|
1378
|
+
|
|
1379
|
+
const http = setupCache(axios, {
|
|
1380
|
+
storage: axiosCacheStorage(createClient().connect()),
|
|
1381
|
+
});
|
|
1382
|
+
```
|
|
1383
|
+
|
|
1384
|
+
Use `.prefix()` to namespace cache keys in a shared store:
|
|
1385
|
+
|
|
1386
|
+
```js
|
|
1387
|
+
const http = setupCache(axios, {
|
|
1388
|
+
storage: axiosCacheStorage(createClient().connect()).prefix("api-cache:"),
|
|
1389
|
+
});
|
|
1390
|
+
```
|
|
1391
|
+
|
|
1392
|
+
Cache TTL is derived automatically from each response's cache headers (via `Cache-Control: max-age`, `Expires`, etc.) so you don't need to configure it separately. The cache entry is stored with a matching expiration so backends like Redis will evict it automatically.
|
|
1393
|
+
|
|
1319
1394
|
### fch
|
|
1320
1395
|
|
|
1321
1396
|
[Fch](https://www.npmjs.com/package/fch) is a lightweight fetch wrapper that uses Polystore natively for caching. Pass any Polystore store as the `cache` option and GET responses are cached automatically:
|
|
@@ -1378,7 +1453,7 @@ export default server({ session }).get("/", (ctx) => {
|
|
|
1378
1453
|
|
|
1379
1454
|
### Performance
|
|
1380
1455
|
|
|
1381
|
-
> TL;DR: if you only use the item operations (add, set, get, has, del) and your
|
|
1456
|
+
> TL;DR: if you only use the item operations (add, set, get, has, del) and your adapter supports expiration natively, you have nothing to worry about! Otherwise, please read on.
|
|
1382
1457
|
|
|
1383
1458
|
While all of our stores support `expires`, `.prefix()` and group operations, the nature of those makes them to have different performance characteristics.
|
|
1384
1459
|
|
|
@@ -1390,9 +1465,9 @@ While all of our stores support `expires`, `.prefix()` and group operations, the
|
|
|
1390
1465
|
|
|
1391
1466
|
### Expirations
|
|
1392
1467
|
|
|
1393
|
-
> Warning: if
|
|
1468
|
+
> Warning: if an adapter 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.
|
|
1394
1469
|
|
|
1395
|
-
We unify all of the
|
|
1470
|
+
We unify all of the adapters' diverse expiration methods into a single, easy one with `expires` (**seconds** | string):
|
|
1396
1471
|
|
|
1397
1472
|
```js
|
|
1398
1473
|
// in-memory store
|
|
@@ -1438,13 +1513,13 @@ However, in some stores this does come with some potential performance disadvant
|
|
|
1438
1513
|
|
|
1439
1514
|
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.
|
|
1440
1515
|
|
|
1441
|
-
These details are explained in the respective
|
|
1516
|
+
These details are explained in the respective adapter information.
|
|
1442
1517
|
|
|
1443
1518
|
### Substores
|
|
1444
1519
|
|
|
1445
1520
|
> There's some [basic `.prefix()` API info](#prefix) for everyday usage, this section is the in-depth explanation.
|
|
1446
1521
|
|
|
1447
|
-
What `.prefix()` does is it creates **a new instance** of the Store, _with the same
|
|
1522
|
+
What `.prefix()` does is it creates **a new instance** of the Store, _with the same adapter_ as you provided, but now any key you read, write, etc. will be passed with the given prefix to the adapter. The issue is that support from the underlying adapters is inconsistent.
|
|
1448
1523
|
|
|
1449
1524
|
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:
|
|
1450
1525
|
|
|
@@ -1465,15 +1540,15 @@ Polystore methods return promises and surface errors from the underlying client.
|
|
|
1465
1540
|
Invalid JSON payloads, invalid value encoding, or data that was written outside Polystore and cannot be decoded with its metadata expectations.
|
|
1466
1541
|
|
|
1467
1542
|
3. **Usage/configuration errors**
|
|
1468
|
-
Invalid
|
|
1543
|
+
Invalid adapter setup, invalid URLs/paths, or unsupported operations in a specific runtime.
|
|
1469
1544
|
|
|
1470
1545
|
Recommended patterns:
|
|
1471
1546
|
|
|
1472
1547
|
- Use `try/catch` around all write/read operations in production paths.
|
|
1473
1548
|
- Prefer returning safe fallbacks for cache-like usage (`null`, stale response, or refetch).
|
|
1474
|
-
- Log enough context (`
|
|
1475
|
-
- For remote
|
|
1476
|
-
- Call `.close()` during shutdown when the
|
|
1549
|
+
- Log enough context (`adapter type`, `key`, operation name) without logging sensitive values.
|
|
1550
|
+
- For remote adapters, consider retry/backoff only for transient failures.
|
|
1551
|
+
- Call `.close()` during shutdown when the adapter supports it.
|
|
1477
1552
|
|
|
1478
1553
|
Example:
|
|
1479
1554
|
|
|
@@ -1524,43 +1599,43 @@ class MyClient {
|
|
|
1524
1599
|
}
|
|
1525
1600
|
```
|
|
1526
1601
|
|
|
1527
|
-
Note that this is NOT the public API, it's the internal **
|
|
1602
|
+
Note that this is NOT the public API, it's the internal **adapter** API. It's simpler than the public API since we do some of the heavy lifting as an intermediate layer (e.g. for the adapter, 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.
|
|
1528
1603
|
|
|
1529
|
-
**Expires**: if you set the `HAS_EXPIRATION = true`, then you are indicating that the
|
|
1604
|
+
**Expires**: if you set the `HAS_EXPIRATION = true`, then you are indicating that the adapter 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.
|
|
1530
1605
|
|
|
1531
1606
|
**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:
|
|
1532
1607
|
|
|
1533
1608
|
```js
|
|
1534
1609
|
// What the user of polystore does:
|
|
1535
|
-
const store = await kv(
|
|
1610
|
+
const store = await kv(adapter).prefix("hello:").prefix("world:");
|
|
1536
1611
|
|
|
1537
|
-
// User calls this, then the
|
|
1612
|
+
// User calls this, then the adapter is called with that:
|
|
1538
1613
|
const value = await store.get("a");
|
|
1539
|
-
//
|
|
1614
|
+
// adapter.get("hello:world:a");
|
|
1540
1615
|
|
|
1541
|
-
// User calls this, then the
|
|
1616
|
+
// User calls this, then the adapter is called with that:
|
|
1542
1617
|
for await (const [key, value] of store) {}
|
|
1543
|
-
//
|
|
1618
|
+
// adapter.iterate("hello:world:");
|
|
1544
1619
|
```
|
|
1545
1620
|
|
|
1546
1621
|
> Note: all of the _group methods_ that return keys, should return them **with the prefix**:
|
|
1547
1622
|
|
|
1548
1623
|
```js
|
|
1549
|
-
|
|
1624
|
+
adapter.keys = (prefix) => {
|
|
1550
1625
|
// Filter the keys, and return them INCLUDING the prefix!
|
|
1551
1626
|
return Object.keys(subStore).filter((key) => key.startsWith(prefix));
|
|
1552
1627
|
};
|
|
1553
1628
|
```
|
|
1554
1629
|
|
|
1555
|
-
While the signatures are different, you can check each entries on the output of Polystore API to see what is expected for the methods of the
|
|
1630
|
+
While the signatures are different, you can check each entries on the output of Polystore API to see what is expected for the methods of the adapter to do, e.g. `.clear()` will remove all of the items that match the prefix (or everything if there's no prefix).
|
|
1556
1631
|
|
|
1557
1632
|
|
|
1558
1633
|
|
|
1559
1634
|
## Examples
|
|
1560
1635
|
|
|
1561
|
-
### Plain Object
|
|
1636
|
+
### Plain Object adapter
|
|
1562
1637
|
|
|
1563
|
-
This is a good example of how simple a store can be, however do not use it literally since it behaves the same as the already-supported `new Map()`, only use it as the base for your own
|
|
1638
|
+
This is a good example of how simple a store can be, however do not use it literally since it behaves the same as the already-supported `new Map()`, only use it as the base for your own adapters:
|
|
1564
1639
|
|
|
1565
1640
|
```js
|
|
1566
1641
|
const dataSource = {};
|
|
@@ -1590,10 +1665,10 @@ We don't set `HAS_EXPIRATION` to true since plain objects do NOT support expirat
|
|
|
1590
1665
|
|
|
1591
1666
|
### Custom ID generation
|
|
1592
1667
|
|
|
1593
|
-
You might want to provide your custom key generation algorithm, which I'm going to call `customId()` for example purposes. The only place where `polystore` generates IDs is in `add`, so you can provide your
|
|
1668
|
+
You might want to provide your custom key generation algorithm, which I'm going to call `customId()` for example purposes. The only place where `polystore` generates IDs is in `add`, so you can provide your adapter with a custom generator:
|
|
1594
1669
|
|
|
1595
1670
|
```js
|
|
1596
|
-
class
|
|
1671
|
+
class MyAdapter {
|
|
1597
1672
|
|
|
1598
1673
|
// Add the opt method .add() to have more control over the ID generation
|
|
1599
1674
|
async add (prefix, data, expires) {
|
|
@@ -1623,10 +1698,10 @@ const id2 = await store.prefix("hello:").add({ hello: "world" });
|
|
|
1623
1698
|
|
|
1624
1699
|
### Serializing the data
|
|
1625
1700
|
|
|
1626
|
-
If you need to serialize the data before storing it, you can do it within your custom
|
|
1701
|
+
If you need to serialize the data before storing it, you can do it within your custom adapter. Here's an example of how you can handle data serialization when setting values:
|
|
1627
1702
|
|
|
1628
1703
|
```js
|
|
1629
|
-
class
|
|
1704
|
+
class MyAdapter {
|
|
1630
1705
|
get(key) {
|
|
1631
1706
|
const data = dataSource[key];
|
|
1632
1707
|
return data ? JSON.parse(data) : null;
|
|
@@ -1741,7 +1816,7 @@ You can store either the raw data, or the processed data. Depending on whether t
|
|
|
1741
1816
|
|
|
1742
1817
|
### Dev vs Prod
|
|
1743
1818
|
|
|
1744
|
-
With Polystore it's easy to configure your KV solution to use a different
|
|
1819
|
+
With Polystore it's easy to configure your KV solution to use a different adapter in dev vs production. We've found particularly useful to use an easy-to-debug adapter in dev like [Folder](#folder) and a high-performance adapter in production like [Redis](#redis):
|
|
1745
1820
|
|
|
1746
1821
|
```ts
|
|
1747
1822
|
// store.ts
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Store } from 'polystore';
|
|
2
|
+
import { AxiosStorage, CacheRequestConfig, StorageValue, NotEmptyStorageValue } from 'axios-cache-interceptor';
|
|
3
|
+
|
|
4
|
+
declare class PolystoreAxiosCacheStorage implements AxiosStorage {
|
|
5
|
+
"is-storage": number;
|
|
6
|
+
private store;
|
|
7
|
+
private _storage;
|
|
8
|
+
constructor(store: Store);
|
|
9
|
+
prefix(prefix?: string): PolystoreAxiosCacheStorage;
|
|
10
|
+
get(key: string, currentRequest?: CacheRequestConfig): Promise<StorageValue>;
|
|
11
|
+
set(key: string, value: NotEmptyStorageValue, currentRequest?: CacheRequestConfig): Promise<void>;
|
|
12
|
+
remove(key: string, currentRequest?: CacheRequestConfig): Promise<void>;
|
|
13
|
+
clear(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
declare function axiosCacheStorage(store?: any): PolystoreAxiosCacheStorage;
|
|
16
|
+
|
|
17
|
+
export { PolystoreAxiosCacheStorage, axiosCacheStorage as default };
|