polystore 0.16.1 → 0.17.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.js +55 -21
- package/package.json +14 -6
- package/readme.md +211 -146
package/index.js
CHANGED
|
@@ -29,13 +29,14 @@ var Api = class extends Client {
|
|
|
29
29
|
const exp = typeof expires === "number" ? `?expires=${expires}` : "";
|
|
30
30
|
await this.#api(key, exp, "PUT", this.encode(value));
|
|
31
31
|
};
|
|
32
|
-
del =
|
|
33
|
-
await this.#api(key, "", "DELETE");
|
|
34
|
-
};
|
|
32
|
+
del = (key) => this.#api(key, "", "DELETE");
|
|
35
33
|
async *iterate(prefix = "") {
|
|
36
|
-
const data = await this.#api(
|
|
34
|
+
const data = await this.#api(
|
|
35
|
+
"",
|
|
36
|
+
`?prefix=${encodeURIComponent(prefix)}`
|
|
37
|
+
);
|
|
37
38
|
for (let [key, value] of Object.entries(data || {})) {
|
|
38
|
-
if (value !== null
|
|
39
|
+
if (value !== null) {
|
|
39
40
|
yield [prefix + key, value];
|
|
40
41
|
}
|
|
41
42
|
}
|
|
@@ -48,13 +49,15 @@ var Cloudflare = class extends Client {
|
|
|
48
49
|
EXPIRES = true;
|
|
49
50
|
// Check whether the given store is a FILE-type
|
|
50
51
|
static test = (client) => client?.constructor?.name === "KvNamespace" || client?.constructor?.name === "EdgeKVNamespace";
|
|
51
|
-
get = async (key) =>
|
|
52
|
-
|
|
52
|
+
get = async (key) => {
|
|
53
|
+
return this.decode(await this.client.get(key));
|
|
54
|
+
};
|
|
55
|
+
set = async (key, data, opts) => {
|
|
53
56
|
const expirationTtl = opts.expires ? Math.round(opts.expires) : void 0;
|
|
54
57
|
if (expirationTtl && expirationTtl < 60) {
|
|
55
58
|
throw new Error("Cloudflare's min expiration is '60s'");
|
|
56
59
|
}
|
|
57
|
-
|
|
60
|
+
await this.client.put(key, this.encode(data), { expirationTtl });
|
|
58
61
|
};
|
|
59
62
|
del = (key) => this.client.delete(key);
|
|
60
63
|
// Since we have pagination, we don't want to get all of the
|
|
@@ -84,7 +87,7 @@ var Cloudflare = class extends Client {
|
|
|
84
87
|
entries = async (prefix = "") => {
|
|
85
88
|
const keys = await this.keys(prefix);
|
|
86
89
|
const values = await Promise.all(keys.map((k) => this.get(k)));
|
|
87
|
-
return keys.map((k, i) => [k, values[i]]);
|
|
90
|
+
return keys.map((k, i) => [k, values[i]]).filter((p) => p[1] !== null);
|
|
88
91
|
};
|
|
89
92
|
};
|
|
90
93
|
|
|
@@ -140,8 +143,13 @@ var Etcd = class extends Client {
|
|
|
140
143
|
EXPIRES = false;
|
|
141
144
|
// Check if this is the right class for the given client
|
|
142
145
|
static test = (client) => client?.constructor?.name === "Etcd3";
|
|
143
|
-
get = (key) =>
|
|
144
|
-
|
|
146
|
+
get = async (key) => {
|
|
147
|
+
const data = await this.client.get(key).json();
|
|
148
|
+
return data;
|
|
149
|
+
};
|
|
150
|
+
set = async (key, value) => {
|
|
151
|
+
await this.client.put(key).value(this.encode(value));
|
|
152
|
+
};
|
|
145
153
|
del = (key) => this.client.delete().key(key).exec();
|
|
146
154
|
async *iterate(prefix = "") {
|
|
147
155
|
const keys = await this.client.getAll().prefix(prefix).keys();
|
|
@@ -149,7 +157,9 @@ var Etcd = class extends Client {
|
|
|
149
157
|
yield [key, await this.get(key)];
|
|
150
158
|
}
|
|
151
159
|
}
|
|
152
|
-
keys = (prefix = "") =>
|
|
160
|
+
keys = (prefix = "") => {
|
|
161
|
+
return this.client.getAll().prefix(prefix).keys();
|
|
162
|
+
};
|
|
153
163
|
entries = async (prefix = "") => {
|
|
154
164
|
const keys = await this.keys(prefix);
|
|
155
165
|
const values = await Promise.all(keys.map((k) => this.get(k)));
|
|
@@ -274,13 +284,16 @@ var Folder = class extends Client {
|
|
|
274
284
|
});
|
|
275
285
|
})();
|
|
276
286
|
file = (key) => this.folder + key + ".json";
|
|
277
|
-
get = (key) => {
|
|
278
|
-
|
|
287
|
+
get = async (key) => {
|
|
288
|
+
const file = await this.fsp.readFile(this.file(key), "utf8").catch(noFileOk);
|
|
289
|
+
return this.decode(file);
|
|
279
290
|
};
|
|
280
|
-
set = (key, value) => {
|
|
281
|
-
|
|
291
|
+
set = async (key, value) => {
|
|
292
|
+
await this.fsp.writeFile(this.file(key), this.encode(value), "utf8");
|
|
293
|
+
};
|
|
294
|
+
del = async (key) => {
|
|
295
|
+
await this.fsp.unlink(this.file(key)).catch(noFileOk);
|
|
282
296
|
};
|
|
283
|
-
del = (key) => this.fsp.unlink(this.file(key)).catch(noFileOk);
|
|
284
297
|
async *iterate(prefix = "") {
|
|
285
298
|
const all = await this.fsp.readdir(this.folder);
|
|
286
299
|
const keys = all.filter((f) => f.startsWith(prefix) && f.endsWith(".json"));
|
|
@@ -288,7 +301,7 @@ var Folder = class extends Client {
|
|
|
288
301
|
const key = name.slice(0, -".json".length);
|
|
289
302
|
try {
|
|
290
303
|
const data = await this.get(key);
|
|
291
|
-
yield [key, data];
|
|
304
|
+
if (data !== null && data !== void 0) yield [key, data];
|
|
292
305
|
} catch {
|
|
293
306
|
continue;
|
|
294
307
|
}
|
|
@@ -428,9 +441,30 @@ var SQLite = class extends Client {
|
|
|
428
441
|
// This one is doing manual time management internally even though
|
|
429
442
|
// sqlite does not natively support expirations. This is because it does
|
|
430
443
|
// support creating a `expires_at:Date` column that makes managing
|
|
431
|
-
//
|
|
444
|
+
// expirations much easier, so it's really "somewhere in between"
|
|
432
445
|
EXPIRES = true;
|
|
433
|
-
|
|
446
|
+
// The table name to use
|
|
447
|
+
table = "kv";
|
|
448
|
+
// Make sure the folder already exists, so attempt to create it
|
|
449
|
+
// It fails if it already exists, hence the catch case
|
|
450
|
+
promise = (async () => {
|
|
451
|
+
if (!/^[a-zA-Z_]+$/.test(this.table)) {
|
|
452
|
+
throw new Error(`Invalid table name ${this.table}`);
|
|
453
|
+
}
|
|
454
|
+
this.client.exec(`
|
|
455
|
+
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
456
|
+
id TEXT PRIMARY KEY,
|
|
457
|
+
value TEXT NOT NULL,
|
|
458
|
+
expires_at INTEGER
|
|
459
|
+
)
|
|
460
|
+
`);
|
|
461
|
+
this.client.exec(
|
|
462
|
+
`CREATE INDEX IF NOT EXISTS idx_${this.table}_expires_at ON ${this.table} (expires_at)`
|
|
463
|
+
);
|
|
464
|
+
})();
|
|
465
|
+
static test = (client) => {
|
|
466
|
+
return typeof client?.prepare === "function" && typeof client?.exec === "function";
|
|
467
|
+
};
|
|
434
468
|
get = (id) => {
|
|
435
469
|
const row = this.client.prepare(`SELECT value, expires_at FROM kv WHERE id = ?`).get(id);
|
|
436
470
|
if (!row) return null;
|
|
@@ -481,7 +515,7 @@ ${prefix ? "AND id LIKE ?" : ""}
|
|
|
481
515
|
this.client.prepare(`DELETE FROM kv WHERE expires_at < ?`).run(Date.now());
|
|
482
516
|
};
|
|
483
517
|
clearAll = () => {
|
|
484
|
-
this.client.
|
|
518
|
+
this.client.exec(`DELETE FROM kv`);
|
|
485
519
|
};
|
|
486
520
|
close = () => {
|
|
487
521
|
this.client.close?.();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polystore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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",
|
|
@@ -16,11 +16,13 @@
|
|
|
16
16
|
"index.d.ts"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
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",
|
|
19
|
+
"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",
|
|
20
20
|
"build": "bunx tsup src/index.ts --format esm --dts --out-dir . --target node24",
|
|
21
21
|
"lint": "npx tsc --noEmit",
|
|
22
22
|
"start": "bun test --watch",
|
|
23
|
-
"test": "bun test",
|
|
23
|
+
"test": "npm run test:bun && npm run test:jest",
|
|
24
|
+
"test:bun": "bun test ./test/index.test.ts",
|
|
25
|
+
"test:jest": "jest ./test/index.test.ts --detectOpenHandles --forceExit",
|
|
24
26
|
"run:db": "etcd",
|
|
25
27
|
"run:server": "bun ./src/server.ts"
|
|
26
28
|
},
|
|
@@ -35,21 +37,27 @@
|
|
|
35
37
|
"license": "MIT",
|
|
36
38
|
"devDependencies": {
|
|
37
39
|
"@deno/kv": "^0.8.1",
|
|
40
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
38
41
|
"@types/bun": "^1.3.3",
|
|
42
|
+
"@types/jest": "^30.0.0",
|
|
39
43
|
"@types/jsdom": "^27.0.0",
|
|
40
|
-
"better-sqlite3": "^12.
|
|
44
|
+
"better-sqlite3": "^12.6.0",
|
|
41
45
|
"check-dts": "^0.8.0",
|
|
42
|
-
"cross-fetch": "^4.
|
|
46
|
+
"cross-fetch": "^4.1.0",
|
|
43
47
|
"dotenv": "^16.3.1",
|
|
44
48
|
"edge-mock": "^0.0.15",
|
|
45
49
|
"esbuild": "^0.27.0",
|
|
46
50
|
"etcd3": "^1.1.2",
|
|
47
51
|
"gzip-size-cli": "^5.1.0",
|
|
52
|
+
"jest": "^30.2.0",
|
|
48
53
|
"jsdom": "^27.2.0",
|
|
49
54
|
"level": "^8.0.1",
|
|
50
55
|
"localforage": "^1.10.0",
|
|
51
56
|
"redis": "^4.6.10",
|
|
52
|
-
"
|
|
57
|
+
"ts-jest": "^29.4.6",
|
|
58
|
+
"ts-node": "^10.9.2",
|
|
59
|
+
"tsup": "^8.5.1",
|
|
60
|
+
"typescript": "^5.9.3"
|
|
53
61
|
},
|
|
54
62
|
"documentation": {
|
|
55
63
|
"title": "🏬 Polystore - A universal library for standardizing any KV-store",
|
package/readme.md
CHANGED
|
@@ -8,45 +8,44 @@ 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 clients available
|
|
12
12
|
```
|
|
13
13
|
|
|
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
17
|
- [`.set(key, value, options?)`](#set): save a single value that is serializable.
|
|
18
|
-
- [`.add(value, options?)`](#add):
|
|
19
|
-
- [`.has(key)`](#has): check whether a key exists
|
|
20
|
-
- [`.del(key)`](#del): delete a single value from the store.
|
|
21
|
-
- [
|
|
22
|
-
- [`.
|
|
23
|
-
- [`.
|
|
24
|
-
- [`.
|
|
18
|
+
- [`.add(value, options?)`](#add): save a single value with an auto-generated key.
|
|
19
|
+
- [`.has(key)`](#has): check whether a key exists and is not expired.
|
|
20
|
+
- [`.del(key)`](#del): delete a single key/value from the store.
|
|
21
|
+
- [Iterator](#iterator): go through all of the key/values one by one.
|
|
22
|
+
- [`.keys()`](#keys): get a list of all the available keys in the store.
|
|
23
|
+
- [`.values()`](#values): get a list of all the available values in the store.
|
|
24
|
+
- [`.entries()`](#entries): get a list of all the available key-value pairs.
|
|
25
|
+
- [`.all()`](#all): get an object of all the key:values mapped.
|
|
25
26
|
- [`.clear()`](#clear): delete ALL of the data in the store, effectively resetting it.
|
|
26
27
|
- [`.close()`](#close): (only _some_ stores) ends the connection to the store.
|
|
27
28
|
- [`.prefix(prefix)`](#prefix): create a sub-store that manages the keys with that prefix.
|
|
28
29
|
|
|
29
|
-
> This library has very high performance with the item methods (GET/SET/ADD/HAS/DEL). For other methods or to learn more, see [the performance considerations](#performance) and read the docs on your specific client.
|
|
30
|
-
|
|
31
30
|
Available clients for the KV store:
|
|
32
31
|
|
|
33
32
|
- [**Memory** `new Map()`](#memory) (fe+be): an in-memory API to keep your KV store.
|
|
34
33
|
- [**Local Storage** `localStorage`](#local-storage) (fe): persist the data in the browser's localStorage.
|
|
35
34
|
- [**Session Storage** `sessionStorage`](#session-storage) (fe): persist the data in the browser's sessionStorage.
|
|
36
|
-
- [**Cookies** `"cookie"`](#cookies) (fe): persist the data using cookies
|
|
37
|
-
- [**LocalForage** `localForage`](#local-forage) (fe): persist the data on IndexedDB
|
|
38
|
-
- [**
|
|
39
|
-
- [**
|
|
40
|
-
- [**
|
|
41
|
-
- [**
|
|
42
|
-
- [**
|
|
43
|
-
- [**
|
|
35
|
+
- [**Cookies** `"cookie"`](#cookies) (fe): persist the data using cookies.
|
|
36
|
+
- [**LocalForage** `localForage`](#local-forage) (fe): persist the data on IndexedDB.
|
|
37
|
+
- [**Redis** `redisClient`](#redis) (be): use the Redis instance that you connect to.
|
|
38
|
+
- [**SQLite** `sqlite`](#sqlite) (be): use a SQLite instance with a table called `kv`.
|
|
39
|
+
- [**Fetch API** `"https://..."`](#fetch-api) (fe+be): call an API to save/retrieve the data.
|
|
40
|
+
- [**File** `"file:///[...].json"`](#file) (be): store the data in a single JSON file in your FS.
|
|
41
|
+
- [**Folder** `"file:///[...]/"`](#folder) (be): store each key in a folder as json files.
|
|
42
|
+
- [**Cloudflare KV** `env.KV_NAMESPACE`](#cloudflare-kv) (be): use Cloudflare's KV store.
|
|
43
|
+
- [**Postgres** `pool`](#postgres) (be): use PostgreSQL with the pg library.
|
|
44
|
+
- [**Level** `new Level('example', { valueEncoding: 'json' })`](#level) (fe+be): support the whole Level ecosystem.
|
|
44
45
|
- [**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
|
|
47
46
|
- [**_Custom_** `{}`](#creating-a-store) (fe+be): create your own store with just 3 methods!
|
|
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 (~
|
|
48
|
+
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), idempotent (`kv(kv(A))` ~= `kv(A)`) and tiny (~4KB). For example, let's say you create an API library, then you can accept the stores from your client:
|
|
50
49
|
|
|
51
50
|
```js
|
|
52
51
|
import MyApi from "my-api";
|
|
@@ -79,7 +78,7 @@ const REDIS = process.env.REDIS_URL;
|
|
|
79
78
|
const store = kv(createClient({ url: REDIS }).connect());
|
|
80
79
|
```
|
|
81
80
|
|
|
82
|
-
Now your store is ready to use! Add, set, get, del different keys. [See full API](#api).
|
|
81
|
+
This follows the recommended naming, the default exported `kv()` for the base function, then `store` for the store instance. Now your store is ready to use! Add, set, get, del different keys. [See full API](#api).
|
|
83
82
|
|
|
84
83
|
```js
|
|
85
84
|
const key = await store.add("Hello");
|
|
@@ -92,7 +91,7 @@ await store.del(key);
|
|
|
92
91
|
|
|
93
92
|
## API
|
|
94
93
|
|
|
95
|
-
|
|
94
|
+
The base `kv()` initialization is shared across clients ([see full clients list](#clients)); single argument that receives the client or a string representing the client:
|
|
96
95
|
|
|
97
96
|
```js
|
|
98
97
|
import kv from "polystore";
|
|
@@ -103,9 +102,10 @@ const store = kv(MyClientOrStoreInstance);
|
|
|
103
102
|
// use the store
|
|
104
103
|
```
|
|
105
104
|
|
|
106
|
-
|
|
105
|
+
> [!IMPORTANT]
|
|
106
|
+
> 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.
|
|
107
107
|
|
|
108
|
-
You can enforce
|
|
108
|
+
You can enforce **types** for store values either at store creation or at the method level:
|
|
109
109
|
|
|
110
110
|
```ts
|
|
111
111
|
const store = kv<number>(new Map());
|
|
@@ -122,9 +122,12 @@ store.set<number>("abc", 10);
|
|
|
122
122
|
store.set<number>("abc", "hello"); // FAILS
|
|
123
123
|
````
|
|
124
124
|
|
|
125
|
-
>
|
|
125
|
+
> [!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")` _won't work_.
|
|
126
127
|
|
|
127
|
-
|
|
128
|
+
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
|
+
|
|
130
|
+
These are the exported types, `Client`, `Serializable` and `Store`:
|
|
128
131
|
|
|
129
132
|
```ts
|
|
130
133
|
import kv from "polystore";
|
|
@@ -137,9 +140,9 @@ const value: Serializable = store.get('hello');
|
|
|
137
140
|
|
|
138
141
|
### .get()
|
|
139
142
|
|
|
140
|
-
|
|
143
|
+
Retrieves a single value from the store. If the key has never been set, was deleted, or has expired, it returns `null`:
|
|
141
144
|
|
|
142
|
-
```
|
|
145
|
+
```ts
|
|
143
146
|
const value = await store.get(key: string);
|
|
144
147
|
|
|
145
148
|
console.log(await store.get("key1")); // "Hello World"
|
|
@@ -147,13 +150,75 @@ console.log(await store.get("key2")); // ["my", "grocery", "list"]
|
|
|
147
150
|
console.log(await store.get("key3")); // { name: "Francisco" }
|
|
148
151
|
```
|
|
149
152
|
|
|
150
|
-
|
|
153
|
+
You can specify the type either at [the store level](#api) or at the method level:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
console.log(await store.get<string>("key1")); // "Hello World"
|
|
157
|
+
console.log(await store.get<string[]>("key2")); // ["my", "grocery", "list"]
|
|
158
|
+
console.log(await store.get<User>("key3")); // { name: "Francisco" }
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
<details>
|
|
162
|
+
<summary>The value and its types must be <b>Serializable</b></summary>
|
|
163
|
+
|
|
164
|
+
The value returned by `.get()` must be **serializable**. Valid types include:
|
|
165
|
+
|
|
166
|
+
* `string`, `number`, `boolean`
|
|
167
|
+
* Arrays or plain objects whose values are themselves serializable
|
|
168
|
+
* Nested `null` values are allowed
|
|
169
|
+
|
|
170
|
+
Class instances or non-plain objects will lose their prototypes and methods when stored. Only JSON-serializable data is safe. Examples:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
// Valid
|
|
174
|
+
type ValueType = string;
|
|
175
|
+
type ValueType = string | number;
|
|
176
|
+
type ValueType = { id: number; name: string; age: number | null };
|
|
177
|
+
|
|
178
|
+
// Invalid
|
|
179
|
+
type ValueType = Date; // Loses prototype
|
|
180
|
+
type ValueType = null; // Only allowed as nested value
|
|
181
|
+
type ValueType = Infinity; // Not serializable
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
In short, only JSON-serializable data is safe to store.
|
|
185
|
+
</details>
|
|
186
|
+
|
|
187
|
+
When there's no value (either never set, deleted, or expired), `.get()` will return `null`:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
// Never set
|
|
191
|
+
console.log(await store.get("key1")); // null
|
|
192
|
+
|
|
193
|
+
// Deleted
|
|
194
|
+
await store.set("key2", "Hello");
|
|
195
|
+
console.log(await store.get("key2")); // "Hello"
|
|
196
|
+
await store.del('key2');
|
|
197
|
+
console.log(await store.get("key2")); // null
|
|
198
|
+
|
|
199
|
+
// Expired
|
|
200
|
+
await store.set("key3", "Hello", { expires: '1s' });
|
|
201
|
+
console.log(await store.get("key3")); // "Hello"
|
|
202
|
+
await new Promise((done) => setTimeout(done, 2000)); // Wait 2 seconds
|
|
203
|
+
console.log(await store.get("key3")); // null (already expired)
|
|
204
|
+
```
|
|
205
|
+
|
|
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
|
+
If you are using a substore with `.prefix()`, `.get()` will respect it:
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const session = store.prefix('session:');
|
|
213
|
+
|
|
214
|
+
console.log(await session.get('key1'));
|
|
215
|
+
// Same as store.get("session:key1")
|
|
216
|
+
```
|
|
151
217
|
|
|
152
|
-
When there's no value (either never set, or expired), `null` will be returned from the operation.
|
|
153
218
|
|
|
154
219
|
### .set()
|
|
155
220
|
|
|
156
|
-
Create or update a value in the store. Will return a promise that resolves with the key when the value has been saved
|
|
221
|
+
Create or update a value in the store. Will return a promise that resolves with the key when the value has been saved:
|
|
157
222
|
|
|
158
223
|
```js
|
|
159
224
|
await store.set(key: string, value: any, options?: { expires: number|string });
|
|
@@ -163,7 +228,39 @@ await store.set("key2", ["my", "grocery", "list"], { expires: "1h" });
|
|
|
163
228
|
await store.set("key3", { name: "Francisco" }, { expires: 60 * 60 });
|
|
164
229
|
```
|
|
165
230
|
|
|
166
|
-
|
|
231
|
+
You can specify the type either at [the store level](#api) or at the method level:
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
await store.set<string>("key1", "Hello World");
|
|
235
|
+
await store.set<string[]>("key2", ["my", "grocery", "list"]);
|
|
236
|
+
await store.set<User>("key3", { name: "Francisco" });
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
<details>
|
|
240
|
+
<summary>The value and its types must be <b>Serializable</b></summary>
|
|
241
|
+
|
|
242
|
+
The value returned by `.get()` must be **serializable**. Valid types include:
|
|
243
|
+
|
|
244
|
+
* `string`, `number`, `boolean`
|
|
245
|
+
* Arrays or plain objects whose values are themselves serializable
|
|
246
|
+
* Nested `null` values are allowed
|
|
247
|
+
|
|
248
|
+
Class instances or non-plain objects will lose their prototypes and methods when stored. Only JSON-serializable data is safe. Examples:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
// Valid
|
|
252
|
+
type ValueType = string;
|
|
253
|
+
type ValueType = string | number;
|
|
254
|
+
type ValueType = { id: number; name: string; age: number | null };
|
|
255
|
+
|
|
256
|
+
// Invalid
|
|
257
|
+
type ValueType = Date; // Loses prototype
|
|
258
|
+
type ValueType = null; // Only allowed as nested value
|
|
259
|
+
type ValueType = Infinity; // Not serializable
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
In short, only JSON-serializable data is safe to store.
|
|
263
|
+
</details>
|
|
167
264
|
|
|
168
265
|
- By default the keys _don't expire_.
|
|
169
266
|
- Setting the `value` to `null`, or the `expires` to `0` is the equivalent of deleting the key+value.
|
|
@@ -648,7 +745,7 @@ console.log(await store.get("key1"));
|
|
|
648
745
|
</ul>
|
|
649
746
|
</details>
|
|
650
747
|
|
|
651
|
-
### Redis
|
|
748
|
+
### Redis
|
|
652
749
|
|
|
653
750
|
Supports the official Node Redis Client. You can pass either the client or the promise:
|
|
654
751
|
|
|
@@ -676,6 +773,86 @@ You don't need to `await` for the connect or similar, this will process it prope
|
|
|
676
773
|
</ul>
|
|
677
774
|
</details>
|
|
678
775
|
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
### SQLite
|
|
779
|
+
|
|
780
|
+
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:
|
|
781
|
+
|
|
782
|
+
> [!IMPORTANT]
|
|
783
|
+
> The table `kv` will be created if it doesn't already exist
|
|
784
|
+
|
|
785
|
+
```js
|
|
786
|
+
import kv from "polystore";
|
|
787
|
+
import Database from "better-sqlite3";
|
|
788
|
+
// Or: import Database from "bun:sqlite";
|
|
789
|
+
|
|
790
|
+
const db = new Database("data.db")
|
|
791
|
+
const store = kv(db);
|
|
792
|
+
|
|
793
|
+
await store.set("key1", "Hello world", { expires: "1h" });
|
|
794
|
+
console.log(await store.get("key1"));
|
|
795
|
+
// "Hello world"
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
<details>
|
|
799
|
+
<summary>Why use polystore with <code>SQLite</code>?</summary>
|
|
800
|
+
<p>These benefits apply when wrapping a SQLite DB with polystore:</p>
|
|
801
|
+
<ul>
|
|
802
|
+
<li><strong>Intuitive expirations</strong>: specify expiration times like <code>10min</code> or <code>2h</code> without manual date handling.</li>
|
|
803
|
+
<li><strong>Substores</strong>: use prefixes to isolate sets of keys cleanly.</li>
|
|
804
|
+
<li><strong>Simple persistence</strong>: full on-disk durability with a minimal driver.</li>
|
|
805
|
+
</ul>
|
|
806
|
+
</details>
|
|
807
|
+
|
|
808
|
+
#### SQLite schema
|
|
809
|
+
|
|
810
|
+
This is the required schema:
|
|
811
|
+
|
|
812
|
+
```sql
|
|
813
|
+
CREATE TABLE kv (
|
|
814
|
+
id TEXT PRIMARY KEY,
|
|
815
|
+
value TEXT,
|
|
816
|
+
expires_at INTEGER
|
|
817
|
+
);
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
You can create these with failsafes to make it much easier to initialize:
|
|
821
|
+
|
|
822
|
+
```js
|
|
823
|
+
db.exec(`
|
|
824
|
+
CREATE TABLE IF NOT EXISTS kv (
|
|
825
|
+
id TEXT PRIMARY KEY,
|
|
826
|
+
value TEXT NOT NULL,
|
|
827
|
+
expires_at INTEGER
|
|
828
|
+
)
|
|
829
|
+
`);
|
|
830
|
+
db.exec(
|
|
831
|
+
`CREATE INDEX IF NOT EXISTS idx_kv_expires_at ON kv (expires_at)`,
|
|
832
|
+
);
|
|
833
|
+
````
|
|
834
|
+
|
|
835
|
+
#### SQLite expirations
|
|
836
|
+
|
|
837
|
+
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
|
+
|
|
839
|
+
However, these are not auto-evicted since SQLite doesn't have native expiration eviction. To avoid having stale data that is not used anymore, it's recommended you set a periodic check and clear expired records manually.
|
|
840
|
+
|
|
841
|
+
There's many ways of doing this, but a simple/basic one is this:
|
|
842
|
+
|
|
843
|
+
```js
|
|
844
|
+
// Clear expired keys once every 10 minutes
|
|
845
|
+
setInterval(() => {
|
|
846
|
+
db.prepare(`DELETE FROM kv WHERE expires_at < ?`).run(Date.now());
|
|
847
|
+
}, 10 * 60 * 1000);
|
|
848
|
+
````
|
|
849
|
+
|
|
850
|
+
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
|
+
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
|
|
679
856
|
### Fetch API
|
|
680
857
|
|
|
681
858
|
Calls an API to get/put the data:
|
|
@@ -911,79 +1088,6 @@ You'll need to be running the etcd store for this to work as expected.
|
|
|
911
1088
|
</details>
|
|
912
1089
|
|
|
913
1090
|
|
|
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
1091
|
### Postgres
|
|
988
1092
|
|
|
989
1093
|
Use PostgreSQL with the `pg` library as a key-value store:
|
|
@@ -1036,45 +1140,6 @@ This maps prefixes to table names for better performance on group operations.
|
|
|
1036
1140
|
</ul>
|
|
1037
1141
|
</details>
|
|
1038
1142
|
|
|
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>
|
|
1074
|
-
<li><strong>Substores</strong>: you can also create substores and manage partial data with ease. <a href="#prefix">Details about substores</a>.</li>
|
|
1075
|
-
</ul>
|
|
1076
|
-
</details>
|
|
1077
|
-
|
|
1078
1143
|
### Custom store
|
|
1079
1144
|
|
|
1080
1145
|
Please see the [creating a store](#creating-a-store) section for all the details!
|