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