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.
Files changed (3) hide show
  1. package/index.js +55 -21
  2. package/package.json +14 -6
  3. 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 = async (key) => {
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("", `?prefix=${encodeURIComponent(prefix)}`);
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 && value !== void 0) {
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) => this.decode(await this.client.get(key));
52
- set = (key, data, opts) => {
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
- return this.client.put(key, this.encode(data), { expirationTtl });
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) => this.client.get(key).json();
144
- set = (key, value) => this.client.put(key).value(this.encode(value));
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 = "") => this.client.getAll().prefix(prefix).keys();
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
- return this.fsp.readFile(this.file(key), "utf8").then(this.decode, noFileOk);
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
- return this.fsp.writeFile(this.file(key), this.encode(value), "utf8");
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
- // expirations much easier, so it's really "somewhere in between"
444
+ // expirations much easier, so it's really "somewhere in between"
432
445
  EXPIRES = true;
433
- static test = (client) => typeof client?.prepare === "function";
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.run(`DELETE FROM kv`);
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.16.1",
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.5.0",
44
+ "better-sqlite3": "^12.6.0",
41
45
  "check-dts": "^0.8.0",
42
- "cross-fetch": "^4.0.0",
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
- "tsup": "^8.5.1"
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 here
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): same as `.set()`, but auto-generates the key.
19
- - [`.has(key)`](#has): check whether a key exists or not.
20
- - [`.del(key)`](#del): delete a single value from the store.
21
- - [`.keys()`](#keys): get a list of all the available strings in the store.
22
- - [`.values()`](#values): get a list of all the values in the store.
23
- - [`.entries()`](#entries): get a list of all the key-value pairs.
24
- - [`.all()`](#all): get an object with the key:values mapped.
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
- - [**Fetch API** `"https://..."`](#fetch-api) (fe+be): call an API to save/retrieve the data
39
- - [**File** `"file:///[...].json"`](#file) (be): store the data in a single JSON file in your FS
40
- - [**Folder** `"file:///[...]/"`](#folder) (be): store each key in a folder as json files
41
- - [**Redis Client** `redisClient`](#redis-client) (be): use the Redis instance that you connect to
42
- - [**Cloudflare KV** `env.KV_NAMESPACE`](#cloudflare-kv) (be): use Cloudflare's KV store
43
- - [**Level** `new Level('example', { valueEncoding: 'json' })`](#level) (fe+be): support the whole Level ecosystem
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 (~3KB). For example, let's say you create an API library, then you can accept the stores from your client:
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
- See how to initialize each store [in the Clients list documentation](#clients). But basically for every store, it's like this:
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
- 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
+ > [!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 the **types** for the store values directly at the store creation, or at the method level:
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
- > 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.
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
- 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
+ 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
- Retrieve a single value from the store. Will return `null` if the value is not set in the store, or if it was set but has already expired:
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
- ```js
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
- If the value is returned, it can be a simple type like `boolean`, `string` or `number`, or it can be a plain `Object` or `Array`, or any combination of those.
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. The value needs to be serializable:
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
- The value can be a simple type like `boolean`, `string` or `number`, or it can be a plain `Object` or `Array`, or a combination of those. It **cannot** be a more complex or non-serializable values like a `Date()`, `Infinity`, `undefined` (casted to `null`), a `Symbol`, etc.
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 Client
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!